Merge pull request #1066 from esdrubal/bug19313
[mono.git] / mcs / class / System / System.Net / DigestClient.cs
1 //
2 // System.Net.DigestClient.cs
3 //
4 // Authors:
5 //      Greg Reinacker (gregr@rassoc.com)
6 //      Sebastien Pouliot (spouliot@motus.com)
7 //      Gonzalo Paniagua Javier (gonzalo@ximian.com
8 //
9 // Copyright 2002-2003 Greg Reinacker, Reinacker & Associates, Inc. All rights reserved.
10 // Portions (C) 2003 Motus Technologies Inc. (http://www.motus.com)
11 // (c) 2003 Novell, Inc. (http://www.novell.com)
12 //
13 // Original (server-side) source code available at
14 // http://www.rassoc.com/gregr/weblog/stories/2002/07/09/webServicesSecurityHttpDigestAuthenticationWithoutActiveDirectory.html
15 //
16
17 //
18 // Permission is hereby granted, free of charge, to any person obtaining
19 // a copy of this software and associated documentation files (the
20 // "Software"), to deal in the Software without restriction, including
21 // without limitation the rights to use, copy, modify, merge, publish,
22 // distribute, sublicense, and/or sell copies of the Software, and to
23 // permit persons to whom the Software is furnished to do so, subject to
24 // the following conditions:
25 // 
26 // The above copyright notice and this permission notice shall be
27 // included in all copies or substantial portions of the Software.
28 // 
29 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
31 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
32 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
33 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
34 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
35 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
36 //
37
38 using System;
39 using System.Collections;
40 using System.Collections.Specialized;
41 using System.IO;
42 using System.Net;
43 using System.Security.Cryptography;
44 using System.Text;
45
46 namespace System.Net
47 {
48         //
49         // This works with apache mod_digest
50         //TODO:
51         //      MD5-sess
52         //      qop (auth-int)
53         //
54         //      See RFC 2617 for details.
55         //
56
57
58         class DigestHeaderParser
59         {
60                 string header;
61                 int length;
62                 int pos;
63                 static string [] keywords = { "realm", "opaque", "nonce", "algorithm", "qop" };
64                 string [] values = new string [keywords.Length];
65
66                 public DigestHeaderParser (string header)
67                 {
68                         this.header = header.Trim ();
69                 }
70
71                 public string Realm {
72                         get { return values [0]; }
73                 }
74
75                 public string Opaque {
76                         get { return values [1]; }
77                 }
78
79                 public string Nonce {
80                         get { return values [2]; }
81                 }
82                 
83                 public string Algorithm {
84                         get { return values [3]; }
85                 }
86                 
87                 public string QOP {
88                         get { return values [4]; }
89                 }
90
91                 public bool Parse ()
92                 {
93                         if (!header.ToLower ().StartsWith ("digest "))
94                                 return false;
95
96                         pos = 6;
97                         length = this.header.Length;
98                         while (pos < length) {
99                                 string key, value;
100                                 if (!GetKeywordAndValue (out key, out value))
101                                         return false;
102
103                                 SkipWhitespace ();
104                                 if (pos < length && header [pos] == ',')
105                                         pos++;
106
107                                 int idx = Array.IndexOf (keywords, (key));
108                                 if (idx == -1)
109                                         continue;
110
111                                 if (values [idx] != null)
112                                         return false;
113
114                                 values [idx] = value;
115                         }
116
117                         if (Realm == null || Nonce == null)
118                                 return false;
119
120                         return true;
121                 }
122
123                 void SkipWhitespace ()
124                 {
125                         char c = ' ';
126                         while (pos < length && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
127                                 c = header [pos++];
128                         }
129                         pos--;
130                 }
131                 
132                 string GetKey ()
133                 {
134                         SkipWhitespace ();
135                         int begin = pos;
136                         while (pos < length && header [pos] != '=') {
137                                 pos++;
138                         }
139                         
140                         string key = header.Substring (begin, pos - begin).Trim ().ToLower ();
141                         return key;
142                 }
143
144                 bool GetKeywordAndValue (out string key, out string value)
145                 {
146                         key = null;
147                         value = null;
148                         key = GetKey ();
149                         if (pos >= length)
150                                 return false;
151
152                         SkipWhitespace ();
153                         if (pos + 1 >= length || header [pos++] != '=')
154                                 return false;
155
156                         SkipWhitespace ();
157                         // note: Apache doesn't use " in all case (like algorithm)
158                         if (pos + 1 >= length)
159                                 return false;
160
161                         bool useQuote = false;
162                         if (header [pos] == '"') {
163                                 pos++;
164                                 useQuote = true;
165                         }
166
167                         int beginQ = pos;
168                         if (useQuote) {
169                                 pos = header.IndexOf ('"', pos);
170                                 if (pos == -1)
171                                         return false;
172                         } else {
173                                 do {
174                                         char c = header [pos];
175                                         if (c == ',' || c == ' ' || c == '\t' || c == '\r' || c == '\n')
176                                                 break;
177                                 } while (++pos < length);
178
179                                 if (pos >= length && beginQ == pos)
180                                         return false;
181                         }
182
183                         value = header.Substring (beginQ, pos - beginQ);
184                         pos += useQuote ? 2 : 1;
185                         return true;
186                 }
187         }
188
189         class DigestSession
190         {
191                 static RandomNumberGenerator rng;
192                 DateTime lastUse;
193                 
194                 static DigestSession () 
195                 {
196                         rng = RandomNumberGenerator.Create ();
197                 }
198
199                 private int _nc;
200                 private HashAlgorithm hash;
201                 private DigestHeaderParser parser;
202                 private string _cnonce;
203
204                 public DigestSession () 
205                 {
206                         _nc = 1;
207                         lastUse = DateTime.Now;
208                 }
209
210                 public string Algorithm {
211                         get { return parser.Algorithm; }
212                 }
213
214                 public string Realm {
215                         get { return parser.Realm; }
216                 }
217
218                 public string Nonce {
219                         get { return parser.Nonce; }
220                 }
221
222                 public string Opaque {
223                         get { return parser.Opaque; }
224                 }
225
226                 public string QOP {
227                         get { return parser.QOP; }
228                 }
229
230                 public string CNonce {
231                         get { 
232                                 if (_cnonce == null) {
233                                         // 15 is a multiple of 3 which is better for base64 because it
234                                         // wont end with '=' and risk messing up the server parsing
235                                         byte[] bincnonce = new byte [15];
236                                         rng.GetBytes (bincnonce);
237                                         _cnonce = Convert.ToBase64String (bincnonce);
238                                         Array.Clear (bincnonce, 0, bincnonce.Length);
239                                 }
240                                 return _cnonce;
241                         }
242                 }
243
244                 public bool Parse (string challenge) 
245                 {
246                         parser = new DigestHeaderParser (challenge);
247                         if (!parser.Parse ()) {
248                                 return false;
249                         }
250
251                         // build the hash object (only MD5 is defined in RFC2617)
252                         if ((parser.Algorithm == null) || (parser.Algorithm.ToUpper ().StartsWith ("MD5")))
253                                 hash = MD5.Create ();
254
255                         return true;
256                 }
257
258                 private string HashToHexString (string toBeHashed) 
259                 {
260                         if (hash == null)
261                                 return null;
262
263                         hash.Initialize ();
264                         byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
265
266                         StringBuilder sb = new StringBuilder ();
267                         foreach (byte b in result)
268                                 sb.Append (b.ToString ("x2"));
269                         return sb.ToString ();
270                 }
271
272                 private string HA1 (string username, string password) 
273                 {
274                         string ha1 = String.Format ("{0}:{1}:{2}", username, Realm, password);
275                         if (Algorithm != null && Algorithm.ToLower () == "md5-sess")
276                                 ha1 = String.Format ("{0}:{1}:{2}", HashToHexString (ha1), Nonce, CNonce);
277                         return HashToHexString (ha1);
278                 }
279
280                 private string HA2 (HttpWebRequest webRequest) 
281                 {
282                         string ha2 = String.Format ("{0}:{1}", webRequest.Method, webRequest.RequestUri.PathAndQuery);
283                         if (QOP == "auth-int") {
284                                 // TODO
285                                 // ha2 += String.Format (":{0}", hentity);
286                         }               
287                         return HashToHexString (ha2);
288                 }
289
290                 private string Response (string username, string password, HttpWebRequest webRequest) 
291                 {
292                         string response = String.Format ("{0}:{1}:", HA1 (username, password), Nonce);
293                         if (QOP != null)
294                                 response += String.Format ("{0}:{1}:{2}:", _nc.ToString ("X8"), CNonce, QOP);
295                         response += HA2 (webRequest);
296                         return HashToHexString (response);
297                 }
298
299                 public Authorization Authenticate (WebRequest webRequest, ICredentials credentials) 
300                 {
301                         if (parser == null)
302                                 throw new InvalidOperationException ();
303
304                         HttpWebRequest request = webRequest as HttpWebRequest;
305                         if (request == null)
306                                 return null;
307         
308                         lastUse = DateTime.Now;
309                         NetworkCredential cred = credentials.GetCredential (request.RequestUri, "digest");
310                         if (cred == null)
311                                 return null;
312
313                         string userName = cred.UserName;
314                         if (userName == null || userName == "")
315                                 return null;
316
317                         string password = cred.Password;
318         
319                         StringBuilder auth = new StringBuilder ();
320                         auth.AppendFormat ("Digest username=\"{0}\", ", userName);
321                         auth.AppendFormat ("realm=\"{0}\", ", Realm);
322                         auth.AppendFormat ("nonce=\"{0}\", ", Nonce);
323                         auth.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
324
325                         if (Algorithm != null) { // hash algorithm (only MD5 in RFC2617)
326                                 auth.AppendFormat ("algorithm=\"{0}\", ", Algorithm);
327                         }
328
329                         auth.AppendFormat ("response=\"{0}\", ", Response (userName, password, request));
330
331                         if (QOP != null) { // quality of protection (server decision)
332                                 auth.AppendFormat ("qop=\"{0}\", ", QOP);
333                         }
334
335                         lock (this) {
336                                 // _nc MUST NOT change from here...
337                                 // number of request using this nonce
338                                 if (QOP != null) {
339                                         auth.AppendFormat ("nc={0:X8}, ", _nc);
340                                         _nc++;
341                                 }
342                                 // until here, now _nc can change
343                         }
344
345                         if (CNonce != null) // opaque value from the client
346                                 auth.AppendFormat ("cnonce=\"{0}\", ", CNonce);
347
348                         if (Opaque != null) // exact same opaque value as received from server
349                                 auth.AppendFormat ("opaque=\"{0}\", ", Opaque);
350
351                         auth.Length -= 2; // remove ", "
352                         return new Authorization (auth.ToString ());
353                 }
354
355                 public DateTime LastUse {
356                         get { return lastUse; }
357                 }
358         }
359
360         class DigestClient : IAuthenticationModule
361         {
362
363                 static readonly Hashtable cache = Hashtable.Synchronized (new Hashtable ());
364                 
365                 static Hashtable Cache {
366                         get {
367                                 lock (cache.SyncRoot) {
368                                         CheckExpired (cache.Count);
369                                 }
370                                 
371                                 return cache;
372                         }
373                 }
374
375                 static void CheckExpired (int count)
376                 {
377                         if (count < 10)
378                                 return;
379
380                         DateTime t = DateTime.MaxValue;
381                         DateTime now = DateTime.Now;
382                         ArrayList list = null;
383                         foreach (int key in cache.Keys) {
384                                 DigestSession elem = (DigestSession) cache [key];
385                                 if (elem.LastUse < t &&
386                                     (elem.LastUse - now).Ticks > TimeSpan.TicksPerMinute * 10) {
387                                         t = elem.LastUse;
388                                         if (list == null)
389                                                 list = new ArrayList ();
390
391                                         list.Add (key);
392                                 }
393                         }
394
395                         if (list != null) {
396                                 foreach (int k in list)
397                                         cache.Remove (k);
398                         }
399                 }
400                 
401                 // IAuthenticationModule
402         
403                 public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials) 
404                 {
405                         if (credentials == null || challenge == null)
406                                 return null;
407         
408                         string header = challenge.Trim ();
409                         if (header.ToLower ().IndexOf ("digest") == -1)
410                                 return null;
411
412                         HttpWebRequest request = webRequest as HttpWebRequest;
413                         if (request == null)
414                                 return null;
415
416                         DigestSession currDS = new DigestSession();
417                         if (!currDS.Parse (challenge))
418                                 return null;
419
420                         int hashcode = request.Address.GetHashCode () ^ credentials.GetHashCode () ^ currDS.Nonce.GetHashCode ();
421                         DigestSession ds = (DigestSession) Cache [hashcode];
422                         bool addDS = (ds == null);
423                         if (addDS)
424                                 ds = currDS;
425                         else if (!ds.Parse (challenge))
426                                 return null;
427
428                         if (addDS)
429                                 Cache.Add (hashcode, ds);
430
431                         return ds.Authenticate (webRequest, credentials);
432                 }
433
434                 public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials) 
435                 {
436                         HttpWebRequest request = webRequest as HttpWebRequest;
437                         if (request == null)
438                                 return null;
439
440                         if (credentials == null)
441                                 return null;
442
443                         int hashcode = request.Address.GetHashCode () ^ credentials.GetHashCode ();
444                         DigestSession ds = (DigestSession) Cache [hashcode];
445                         if (ds == null)
446                                 return null;
447
448                         return ds.Authenticate (webRequest, credentials);
449                 }
450         
451                 public string AuthenticationType { 
452                         get { return "Digest"; }
453                 }
454         
455                 public bool CanPreAuthenticate { 
456                         get { return true; }
457                 }
458         }
459 }
460