Copied remotely
[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                 static char [] endSeparator = new char[] { '"', ',' };
65                 string [] values = new string [keywords.Length];
66
67                 public DigestHeaderParser (string header)
68                 {
69                         this.header = header.Trim ();
70                 }
71
72                 public string Realm {
73                         get { return values [0]; }
74                 }
75
76                 public string Opaque {
77                         get { return values [1]; }
78                 }
79
80                 public string Nonce {
81                         get { return values [2]; }
82                 }
83                 
84                 public string Algorithm {
85                         get { return values [3]; }
86                 }
87                 
88                 public string QOP {
89                         get { return values [4]; }
90                 }
91
92                 public bool Parse ()
93                 {
94                         if (!header.ToLower ().StartsWith ("digest "))
95                                 return false;
96
97                         pos = 6;
98                         length = this.header.Length;
99                         while (pos < length) {
100                                 string key, value;
101                                 if (!GetKeywordAndValue (out key, out value))
102                                         return false;
103
104                                 SkipWhitespace ();
105                                 if (pos < length && header [pos] == ',')
106                                         pos++;
107
108                                 int idx = Array.IndexOf (keywords, (key));
109                                 if (idx == -1)
110                                         continue;
111
112                                 if (values [idx] != null)
113                                         return false;
114
115                                 values [idx] = value;
116                         }
117
118                         if (Realm == null || Nonce == null)
119                                 return false;
120
121                         return true;
122                 }
123
124                 void SkipWhitespace ()
125                 {
126                         char c = ' ';
127                         while (pos < length && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) {
128                                 c = header [pos++];
129                         }
130                         pos--;
131                 }
132                 
133                 void SkipNonWhitespace ()
134                 {
135                         char c = 'a';
136                         while (pos < length && c != ' ' && c != '\t' && c != '\r' && c != '\n') {
137                                 c = header [pos++];
138                         }
139                         pos--;
140                 }
141                 
142                 string GetKey ()
143                 {
144                         SkipWhitespace ();
145                         int begin = pos;
146                         while (pos < length && header [pos] != '=') {
147                                 pos++;
148                         }
149                         
150                         string key = header.Substring (begin, pos - begin).Trim ().ToLower ();
151                         return key;
152                 }
153
154                 bool GetKeywordAndValue (out string key, out string value)
155                 {
156                         key = null;
157                         value = null;
158                         key = GetKey ();
159                         if (pos >= length)
160                                 return false;
161
162                         SkipWhitespace ();
163                         if (pos + 1 >= length || header [pos++] != '=')
164                                 return false;
165
166                         SkipWhitespace ();
167                         // note: Apache doesn't use " in all case (like algorithm)
168                         if (pos + 1 >= length)
169                                 return false;
170
171                         bool useQuote = false;
172                         if (header [pos] == '"') {
173                                 pos++;
174                                 useQuote = true;
175                         }
176
177                         int beginQ = pos;
178                         if (useQuote) {
179                                 pos = header.IndexOf ('"', pos);
180                                 if (pos == -1)
181                                         return false;
182                         } else {
183                                 do {
184                                         char c = header [pos];
185                                         if (c == ',' || c == ' ' || c == '\t' || c == '\r' || c == '\n')
186                                                 break;
187                                 } while (++pos < length);
188
189                                 if (pos >= length && beginQ == pos)
190                                         return false;
191                         }
192
193                         value = header.Substring (beginQ, pos - beginQ);
194                         pos += 2;
195                         return true;
196                 }
197         }
198
199         class DigestSession
200         {
201                 static RandomNumberGenerator rng;
202                 DateTime lastUse;
203                 
204                 static DigestSession () 
205                 {
206                         rng = RandomNumberGenerator.Create ();
207                 }
208
209                 private int _nc;
210                 private HashAlgorithm hash;
211                 private DigestHeaderParser parser;
212                 private string _cnonce;
213
214                 public DigestSession () 
215                 {
216                         _nc = 1;
217                         lastUse = DateTime.Now;
218                 }
219
220                 public string Algorithm {
221                         get { return parser.Algorithm; }
222                 }
223
224                 public string Realm {
225                         get { return parser.Realm; }
226                 }
227
228                 public string Nonce {
229                         get { return parser.Nonce; }
230                 }
231
232                 public string Opaque {
233                         get { return parser.Opaque; }
234                 }
235
236                 public string QOP {
237                         get { return parser.QOP; }
238                 }
239
240                 public string CNonce {
241                         get { 
242                                 if (_cnonce == null) {
243                                         // 15 is a multiple of 3 which is better for base64 because it
244                                         // wont end with '=' and risk messing up the server parsing
245                                         byte[] bincnonce = new byte [15];
246                                         rng.GetBytes (bincnonce);
247                                         _cnonce = Convert.ToBase64String (bincnonce);
248                                         Array.Clear (bincnonce, 0, bincnonce.Length);
249                                 }
250                                 return _cnonce;
251                         }
252                 }
253
254                 public bool Parse (string challenge) 
255                 {
256                         parser = new DigestHeaderParser (challenge);
257                         if (!parser.Parse ()) {
258                                 return false;
259                         }
260
261                         // build the hash object (only MD5 is defined in RFC2617)
262                         if ((parser.Algorithm == null) || (parser.Algorithm.ToUpper ().StartsWith ("MD5")))
263                                 hash = HashAlgorithm.Create ("MD5");
264
265                         return true;
266                 }
267
268                 private string HashToHexString (string toBeHashed) 
269                 {
270                         if (hash == null)
271                                 return null;
272
273                         hash.Initialize ();
274                         byte[] result = hash.ComputeHash (Encoding.ASCII.GetBytes (toBeHashed));
275
276                         StringBuilder sb = new StringBuilder ();
277                         foreach (byte b in result)
278                                 sb.Append (b.ToString ("x2"));
279                         return sb.ToString ();
280                 }
281
282                 private string HA1 (string username, string password) 
283                 {
284                         string ha1 = String.Format ("{0}:{1}:{2}", username, Realm, password);
285                         if (Algorithm != null && Algorithm.ToLower () == "md5-sess")
286                                 ha1 = String.Format ("{0}:{1}:{2}", HashToHexString (ha1), Nonce, CNonce);
287                         return HashToHexString (ha1);
288                 }
289
290                 private string HA2 (HttpWebRequest webRequest) 
291                 {
292                         string ha2 = String.Format ("{0}:{1}", webRequest.Method, webRequest.RequestUri.AbsolutePath);
293                         if (QOP == "auth-int") {
294                                 // TODO
295                                 // ha2 += String.Format (":{0}", hentity);
296                         }               
297                         return HashToHexString (ha2);
298                 }
299
300                 private string Response (string username, string password, HttpWebRequest webRequest) 
301                 {
302                         string response = String.Format ("{0}:{1}:", HA1 (username, password), Nonce);
303                         if (QOP != null)
304                                 response += String.Format ("{0}:{1}:{2}:", _nc.ToString ("x8"), CNonce, QOP);
305                         response += HA2 (webRequest);
306                         return HashToHexString (response);
307                 }
308
309                 public Authorization Authenticate (WebRequest webRequest, ICredentials credentials) 
310                 {
311                         if (parser == null)
312                                 throw new InvalidOperationException ();
313
314                         HttpWebRequest request = webRequest as HttpWebRequest;
315                         if (request == null)
316                                 return null;
317         
318                         lastUse = DateTime.Now;
319                         NetworkCredential cred = credentials.GetCredential (request.RequestUri, "digest");
320                         string userName = cred.UserName;
321                         if (userName == null || userName == "")
322                                 return null;
323
324                         string password = cred.Password;
325         
326                         StringBuilder auth = new StringBuilder ();
327                         auth.AppendFormat ("Digest username=\"{0}\", ", userName);
328                         auth.AppendFormat ("realm=\"{0}\", ", Realm);
329                         auth.AppendFormat ("nonce=\"{0}\", ", Nonce);
330                         auth.AppendFormat ("uri=\"{0}\", ", request.Address.PathAndQuery);
331
332                         if (Algorithm != null) { // hash algorithm (only MD5 in RFC2617)
333                                 auth.AppendFormat ("algorithm=\"{0}\", ", Algorithm);
334                         }
335
336                         auth.AppendFormat ("response=\"{0}\", ", Response (userName, password, request));
337
338                         if (QOP != null) { // quality of protection (server decision)
339                                 auth.AppendFormat ("qop={0}, ", QOP);
340                         }
341
342                         lock (this) {
343                                 // _nc MUST NOT change from here...
344                                 // number of request using this nonce
345                                 if (QOP != null) {
346                                         auth.AppendFormat ("nc={0:X8}, ", _nc);
347                                         _nc++;
348                                 }
349                                 // until here, now _nc can change
350                         }
351
352                         if (CNonce != null) // opaque value from the client
353                                 auth.AppendFormat ("cnonce=\"{0}\", ", CNonce);
354
355                         if (Opaque != null) // exact same opaque value as received from server
356                                 auth.AppendFormat ("opaque=\"{0}\", ", Opaque);
357
358                         auth.Length -= 2; // remove ", "
359                         return new Authorization (auth.ToString ());
360                 }
361
362                 public DateTime LastUse {
363                         get { return lastUse; }
364                 }
365         }
366
367         class DigestClient : IAuthenticationModule
368         {
369
370                 static Hashtable cache;
371
372                 public DigestClient () {}
373
374                 static Hashtable Cache {
375                         get {
376                                 lock (typeof (DigestClient)) {
377                                         if (cache == null) {
378                                                 cache = Hashtable.Synchronized (new Hashtable ());
379                                         } else {
380                                                 CheckExpired (cache.Count);
381                                         }
382
383                                         return cache;
384                                 }
385                         }
386                 }
387
388                 static void CheckExpired (int count)
389                 {
390                         if (count < 10)
391                                 return;
392
393                         DateTime t = DateTime.MaxValue;
394                         DateTime now = DateTime.Now;
395                         ArrayList list = null;
396                         foreach (int key in cache.Keys) {
397                                 DigestSession elem = (DigestSession) cache [key];
398                                 if (elem.LastUse < t &&
399                                     (elem.LastUse - now).Ticks > TimeSpan.TicksPerMinute * 10) {
400                                         t = elem.LastUse;
401                                         if (list == null)
402                                                 list = new ArrayList ();
403
404                                         list.Add (key);
405                                 }
406                         }
407
408                         if (list != null) {
409                                 foreach (int k in list)
410                                         cache.Remove (k);
411                         }
412                 }
413                 
414                 // IAuthenticationModule
415         
416                 public Authorization Authenticate (string challenge, WebRequest webRequest, ICredentials credentials) 
417                 {
418                         if (credentials == null || challenge == null)
419                                 return null;
420         
421                         string header = challenge.Trim ();
422                         if (header.ToLower ().IndexOf ("digest") == -1)
423                                 return null;
424
425                         HttpWebRequest request = webRequest as HttpWebRequest;
426                         if (request == null)
427                                 return null;
428
429                         int hashcode = request.Address.GetHashCode () ^ credentials.GetHashCode ();
430                         DigestSession ds = (DigestSession) Cache [hashcode];
431                         bool addDS = (ds == null);
432                         if (addDS)
433                                 ds = new DigestSession ();
434
435                         if (!ds.Parse (challenge))
436                                 return null;
437
438                         if (addDS)
439                                 Cache.Add (hashcode, ds);
440
441                         return ds.Authenticate (webRequest, credentials);
442                 }
443
444                 public Authorization PreAuthenticate (WebRequest webRequest, ICredentials credentials) 
445                 {
446                         HttpWebRequest request = webRequest as HttpWebRequest;
447                         if (request == null)
448                                 return null;
449
450                         if (credentials == null)
451                                 return null;
452
453                         int hashcode = request.Address.GetHashCode () ^ credentials.GetHashCode ();
454                         DigestSession ds = (DigestSession) Cache [hashcode];
455                         if (ds == null)
456                                 return null;
457
458                         return ds.Authenticate (webRequest, credentials);
459                 }
460         
461                 public string AuthenticationType { 
462                         get { return "Digest"; }
463                 }
464         
465                 public bool CanPreAuthenticate { 
466                         get { return true; }
467                 }
468         }
469 }
470