2003-05-21 Ben Maurer <bmaurer@users.sourceforge.net>
[mono.git] / mcs / class / System / System.Net / HttpWebResponse.cs
1 //
2 // System.Net.HttpWebResponse
3 //
4 // Authors:
5 //      Lawrence Pit (loz@cable.a2000.nl)
6 //      Gonzalo Paniagua Javier (gonzalo@ximian.com)
7 //
8 // (c) 2002 Lawrence Pit
9 // (c) 2003 Ximian, Inc. (http://www.ximian.com)
10 //
11
12 using System;
13 using System.IO;
14 using System.Net.Sockets;
15 using System.Runtime.Serialization;
16 using System.Text;
17
18 namespace System.Net 
19 {
20         [Serializable]
21         public class HttpWebResponse : WebResponse, ISerializable, IDisposable
22         {
23                 private Uri uri;
24                 private WebHeaderCollection webHeaders;
25                 private CookieCollection cookieCollection = null;
26                 private string method = null;
27                 private Version version = null;
28                 private HttpStatusCode statusCode;
29                 private string statusDescription = null;
30                 bool chunked;
31
32                 private HttpWebResponseStream responseStream;           
33                 private bool disposed = false;
34                 
35                 // Constructors
36                 
37                 internal HttpWebResponse (Uri uri, string method, Socket socket, int timeout, EventHandler onClose)
38                 { 
39                         Text.StringBuilder value = null;
40                         string last = null;
41                         string line = null;
42                         string[] protocol, header;
43
44                         this.uri = uri;
45                         this.method = method;
46                         this.webHeaders = new WebHeaderCollection();
47
48                         responseStream = new HttpWebResponseStream (socket, onClose);
49                         if (!socket.Poll (timeout, SelectMode.SelectRead))
50                                 throw new WebException("The request timed out", WebExceptionStatus.Timeout);
51
52                         this.statusCode = 0;
53                         do {
54                                 if (statusCode == HttpStatusCode.Continue)
55                                         while ((line = ReadHttpLine (responseStream)) != "");
56
57                                 line = ReadHttpLine (responseStream);
58
59                                 protocol = line.Split (' ');
60                         
61                                 switch (protocol[0]) {
62                                         case "HTTP/1.0":
63                                                 this.version = HttpVersion.Version10;
64                                                 break;
65                                         case "HTTP/1.1":
66                                                 this.version = HttpVersion.Version11;
67                                                 break;
68                                         default:
69                                                 throw new WebException ("Unrecognized HTTP Version: " + line);
70                                 }
71                         
72                                 this.statusCode = (HttpStatusCode) Int32.Parse (protocol[1]);
73                         } while (this.statusCode == HttpStatusCode.Continue);
74
75                         while ((line = ReadHttpLine (responseStream)).Length != 0) {
76                                 if (!Char.IsWhiteSpace (line[0])) { // new header
77                                         header = line.Split (new char[] {':'}, 2);
78                                         if (header.Length != 2)
79                                                 throw new WebException ("Bad HTTP Header");
80                                         if (last != null) { // not the first header
81                                                 if (last.Equals ("Set-Cookie"))
82                                                         SetCookie (value.ToString());
83                                                 else if (last.Equals ("Set-Cookie2"))
84                                                         SetCookie2 (value.ToString());
85                                                 else //don't save Set-Cookie headers
86                                                         this.webHeaders[last] = value.ToString();
87                                         }
88                                         last = header[0];
89                                         value = new Text.StringBuilder (header[1].Trim());
90                                         if (last == "Transfer-Encoding" && header [1].IndexOf ("chunked") != -1)
91                                                 chunked = true;
92                                 }
93                                 else
94                                         value.Append (line.Trim());
95                         }
96                         
97                         responseStream.Chunked = chunked;
98                         this.webHeaders[last] = value.ToString(); // otherwise we miss the last header
99
100                         responseStream.BytesLeft = ContentLength;
101                 }
102                 
103                 protected HttpWebResponse (SerializationInfo serializationInfo, StreamingContext streamingContext)
104                 {
105                         uri = (Uri) serializationInfo.GetValue ("uri", typeof (Uri));
106                         webHeaders = (WebHeaderCollection) serializationInfo.GetValue ("webHeaders",
107                                                                                         typeof (WebHeaderCollection));
108                         cookieCollection = (CookieCollection) serializationInfo.GetValue ("cookieCollection",
109                                                                                            typeof (CookieCollection));
110                         method = serializationInfo.GetString ("method");
111                         version = (Version) serializationInfo.GetValue ("version", typeof (Version));
112                         statusCode = (HttpStatusCode) serializationInfo.GetValue ("statusCode", typeof (HttpStatusCode));
113                         statusDescription = serializationInfo.GetString ("statusDescription");
114                         chunked = serializationInfo.GetBoolean ("chunked");
115                 }
116                 
117                 // Properties
118                 
119                 public string CharacterSet {
120                         // Content-Type   = "Content-Type" ":" media-type
121                         // media-type     = type "/" subtype *( ";" parameter )
122                         // parameter      = attribute "=" value
123                         // 3.7.1. default is ISO-8859-1
124                         get { 
125                                 CheckDisposed ();
126                                 string contentType = ContentType;
127                                 if (contentType == null)
128                                         return "ISO-8859-1";
129                                 string val = contentType.ToLower ();                                    
130                                 int pos = val.IndexOf ("charset=");
131                                 if (pos == -1)
132                                         return "ISO-8859-1";
133                                 pos += 8;
134                                 int pos2 = val.IndexOf (';', pos);
135                                 return (pos2 == -1)
136                                      ? contentType.Substring (pos) 
137                                      : contentType.Substring (pos, pos2 - pos);
138                         }
139                 }
140                 
141                 public string ContentEncoding {
142                         get { 
143                                 CheckDisposed ();
144                                 return webHeaders ["Content-Encoding"];
145                         }
146                 }
147                 
148                 public override long ContentLength {            
149                         get { 
150                                 CheckDisposed ();
151                                 try {
152                                         return Int64.Parse (webHeaders ["Content-Length"]); 
153                                 } catch (Exception) {
154                                         return -1;
155                                 }
156                         }
157                 }
158                 
159                 public override string ContentType {            
160                         get {
161                                 CheckDisposed ();
162                                 return webHeaders ["Content-Type"];
163                         }
164                 }
165                 
166                 public CookieCollection Cookies {
167                         get { 
168                                 CheckDisposed ();
169                                 
170                                 if (cookieCollection == null)
171                                         cookieCollection = new CookieCollection ();
172                                 return cookieCollection;
173                         }
174                         set {
175                                 CheckDisposed ();
176                                 // ?? don't understand how you can set cookies on a response.
177                                 throw new NotSupportedException ();
178                         }
179                 }
180                 
181                 public override WebHeaderCollection Headers {           
182                         get { 
183                                 CheckDisposed ();
184                                 return webHeaders; 
185                         }
186                 }
187                 
188                 public DateTime LastModified {
189                         get {
190                                 CheckDisposed ();
191                                 try {
192                                         string dtStr = webHeaders ["Last-Modified"];
193                                         return MonoHttpDate.Parse (dtStr);
194                                 } catch (Exception) {
195                                         return DateTime.Now;    
196                                 }
197                         }
198                 }
199                 
200                 public string Method {
201                         get { 
202                                 CheckDisposed ();
203                                 return method; 
204                         }
205                 }
206                 
207                 public Version ProtocolVersion {
208                         get { 
209                                 CheckDisposed ();
210                                 return version; 
211                         }
212                 }
213                 
214                 public override Uri ResponseUri {               
215                         get { 
216                                 CheckDisposed ();
217                                 return uri; 
218                         }
219                 }               
220                 
221                 public string Server {
222                         get { 
223                                 CheckDisposed ();
224                                 return webHeaders ["Server"]; 
225                         }
226                 }
227                 
228                 public HttpStatusCode StatusCode {
229                         get { 
230                                 CheckDisposed ();
231                                 return statusCode; 
232                         }
233                 }
234                 
235                 public string StatusDescription {
236                         get { 
237                                 CheckDisposed ();
238                                 return statusDescription; 
239                         }
240                 }
241
242                 // Methods
243                 
244                 public override int GetHashCode ()
245                 {
246                         CheckDisposed ();
247                         return base.GetHashCode ();
248                 }
249                 
250                 public string GetResponseHeader (string headerName)
251                 {
252                         CheckDisposed ();
253                         return webHeaders [headerName];
254                 }
255                 
256                 public override Stream GetResponseStream ()
257                 {
258                         CheckDisposed ();
259                         if (method.Equals ("HEAD")) // see par 4.3 & 9.4
260                                 return Stream.Null;  
261                         return responseStream;
262                 }
263                 
264                 void ISerializable.GetObjectData (SerializationInfo serializationInfo,
265                                                   StreamingContext streamingContext)
266                 {
267                         CheckDisposed ();
268                         serializationInfo.AddValue ("uri", uri);
269                         serializationInfo.AddValue ("webHeaders", webHeaders);
270                         serializationInfo.AddValue ("cookieCollection", cookieCollection);
271                         serializationInfo.AddValue ("method", method);
272                         serializationInfo.AddValue ("version", version);
273                         serializationInfo.AddValue ("statusCode", statusCode);
274                         serializationInfo.AddValue ("statusDescription", statusDescription);
275                         serializationInfo.AddValue ("chunked", chunked);
276                 }               
277
278
279                 // Cleaning up stuff
280
281                 ~HttpWebResponse ()
282                 {
283                         Dispose (false);
284                 }               
285                 
286                 public override void Close ()
287                 {
288                         ((IDisposable) this).Dispose ();
289                 }
290                 
291                 void IDisposable.Dispose ()
292                 {
293                         Dispose (true);
294                         GC.SuppressFinalize (this);  
295                 }
296                 
297                 protected virtual void Dispose (bool disposing) 
298                 {
299                         if (this.disposed)
300                                 return;
301                         this.disposed = true;
302                         
303                         if (disposing) {
304                                 // release managed resources
305                                 uri = null;
306                                 webHeaders = null;
307                                 cookieCollection = null;
308                                 method = null;
309                                 version = null;
310                                 statusDescription = null;
311                         }
312                         
313                         // release unmanaged resources
314                         Stream stream = responseStream;
315                         responseStream = null;
316                         if (stream != null)
317                                 stream.Close ();
318                 }
319                 
320                 private void CheckDisposed () 
321                 {
322                         if (disposed)
323                                 throw new ObjectDisposedException (GetType ().FullName);
324                 }
325
326                 private static string ReadHttpLine (Stream stream)
327                 {
328                         StringBuilder line = new StringBuilder();
329                         byte last = (byte)'\n';
330                         bool read_last = false;
331                         int c;
332                         
333                         while ((c = stream.ReadByte ()) != -1) {
334                                 if (c == '\r') {
335                                         if ((last = (byte) stream.ReadByte ()) == '\n') // headers; not at EOS
336                                                 break;
337                                         read_last = true;
338                                 }
339
340                                 line.Append ((char) c);
341                                 if (read_last) {
342                                         line.Append (Convert.ToChar (last));
343                                         read_last = false;
344                                 }
345                         }
346                         
347                         return line.ToString();
348                 }
349
350                 private void SetCookie (string cookie_str)
351                 {
352                         string[] parts = null;
353                         Collections.Queue options = null;
354                         Cookie cookie = null;
355
356                         options = new Collections.Queue (cookie_str.Split (';'));
357                         parts = ((string)options.Dequeue()).Split ('='); // NAME=VALUE must be first
358
359                         cookie = new Cookie (parts[0], parts[1]);
360
361                         while (options.Count > 0) {
362                                 parts = ((string)options.Dequeue()).Split ('=');
363                                 switch (parts[0].ToUpper()) { // cookie options are case-insensitive
364                                         case "COMMENT":
365                                                 if (cookie.Comment == null)
366                                                         cookie.Comment = parts[1];
367                                         break;
368                                         case "COMMENTURL":
369                                                 if (cookie.CommentUri == null)
370                                                         cookie.CommentUri = new Uri(parts[1]);
371                                         break;
372                                         case "DISCARD":
373                                                 cookie.Discard = true;
374                                         break;
375                                         case "DOMAIN":
376                                                 if (cookie.Domain == null)
377                                                         cookie.Domain = parts[1];
378                                         break;
379                                         case "MAX-AGE": // RFC Style Set-Cookie2
380                                                 if (cookie.Expires == DateTime.MinValue)
381                                                         cookie.Expires = cookie.TimeStamp.AddSeconds (Int32.Parse (parts[1]));
382                                         break;
383                                         case "EXPIRES": // Netscape Style Set-Cookie
384                                                 if (cookie.Expires == DateTime.MinValue)
385                                                         cookie.Expires = DateTime.Parse (parts[1]);
386                                         break;
387                                         case "PATH":
388                                                 if (cookie.Path == null)
389                                                         cookie.Path = parts[1];
390                                         break;
391                                         case "PORT":
392                                                 if (cookie.Port == null)
393                                                         cookie.Port = parts[1];
394                                         break;
395                                         case "SECURE":
396                                                 cookie.Secure = true;
397                                         break;
398                                         case "VERSION":
399                                                 cookie.Version = Int32.Parse (parts[1]);
400                                         break;
401                                 } // switch
402                         } // while
403
404                         if (cookieCollection == null)
405                                 cookieCollection = new CookieCollection();
406
407                         if (cookie.Domain == null)
408                                 cookie.Domain = uri.Host;
409
410                         cookieCollection.Add (cookie);
411                 }
412
413                 private void SetCookie2 (string cookies_str)
414                 {
415                         string[] cookies = cookies_str.Split (',');
416         
417                         foreach (string cookie_str in cookies)
418                                 SetCookie (cookie_str);
419
420                 }
421
422                 class HttpWebResponseStream : NetworkStream
423                 {
424                         bool disposed;
425                         bool chunked;
426                         int chunkSize;
427                         int chunkLeft;
428                         bool readingChunkSize;
429                         EventHandler onClose;
430                         //
431                         // If ContentLength provided, the number of bytes left to read
432                         //
433                         internal long BytesLeft = -1;
434
435                         public HttpWebResponseStream (Socket socket, EventHandler onClose)
436                                 : base (socket, FileAccess.Read, false)
437                         {
438                                 this.onClose = onClose;
439                                 chunkSize = -1;
440                                 chunkLeft = 0;
441                         }
442
443                         public bool Chunked {
444                                 get { return chunked; }
445                                 set { chunked = value; }
446                         }
447                         
448                         protected override void Dispose (bool disposing)
449                         {
450                                 if (disposed)
451                                         return;
452
453                                 disposed = true;
454                                 if (disposing) {
455                                         /* This does not work !??
456                                         if (Socket.Connected)
457                                                 Socket.Shutdown (SocketShutdown.Receive);
458                                         */
459
460                                         if (onClose != null)
461                                                 onClose (this, EventArgs.Empty);
462                                 }
463
464                                 onClose = null;
465                                 base.Dispose (disposing);
466                         }
467
468                         void ReadChunkSize ()
469                         {
470                                 bool cr = false;
471                                 bool lf = false;
472                                 int size = 0;
473                                 // 8 hex digits should be enough
474                                 for (int i = 0; i < 10; i++) {
475                                         char c = Char.ToUpper ((char) ReadByte ());
476                                         if (c == '\r') {
477                                                 if (!cr) {
478                                                         cr = true;
479                                                         continue;
480                                                 }
481                                                 throw new IOException ("Bad stream: 2 CR");
482                                         } 
483                                         
484                                         if (c == '\n' && cr == true) {
485                                                 if (!lf) {
486                                                         lf = true;
487                                                         break;
488                                                 }
489
490                                                 throw new IOException ("Bad stream: got LF but no CR");
491                                         }
492                                         
493                                         if (i < 8 && ((c >= '0' && c <= '9') || c >= 'A' && c <= 'F')) {
494                                                 size = size << 4;
495                                                 if (c >= 'A' && c <= 'F')
496                                                         size += c - 'A' + 10;
497                                                 else
498                                                         size += c - '0';
499                                                 continue;
500                                         }
501
502                                         throw new IOException ("Bad stream: got " + c);
503                                 }
504
505                                 if (!cr || !lf)
506                                         throw new IOException ("Bad stream: no CR or LF after chunk size");
507
508                                 chunkSize = size;
509                                 chunkLeft = size;
510                         }
511
512                         int GetMaxSizeFromChunkLeft (int requestedSize)
513                         {
514                                 if (!chunked)
515                                         return requestedSize;
516
517                                 if (chunkSize == -1 || chunkLeft == 0) {
518                                         lock (this) {
519                                                 if (chunkSize == -1 || chunkLeft == 0) {
520                                                         readingChunkSize = true;
521                                                         try {
522                                                                 ReadChunkSize ();
523                                                         } finally {
524                                                                 readingChunkSize = false;
525                                                         }
526                                                 }
527                                         }
528                                 }
529
530                                 return (chunkLeft < requestedSize) ? chunkLeft : requestedSize;
531                         }
532                         
533                         public override IAsyncResult BeginRead (byte [] buffer, int offset, int size,
534                                                                 AsyncCallback callback, object state)
535                         {
536                                 CheckDisposed ();                               
537                                 IAsyncResult retval;
538
539                                 if (buffer == null)
540                                         throw new ArgumentNullException ("buffer is null");
541
542                                 int len = buffer.Length;
543                                 if (offset < 0 || offset >= len)
544                                         throw new ArgumentOutOfRangeException ("offset exceeds the size of buffer");
545
546                                 if (offset + size < 0 || offset+size > len)
547                                         throw new ArgumentOutOfRangeException ("offset+size exceeds the size of buffer");
548
549                                 if (!readingChunkSize)
550                                         size = GetMaxSizeFromChunkLeft (size);
551
552                                 try {
553                                         retval = base.BeginRead (buffer, offset, size, callback, state);
554                                 } catch {
555                                         throw new IOException ("BeginReceive failure");
556                                 }
557
558                                 return retval;
559                         }
560
561                         public override int EndRead (IAsyncResult ar)
562                         {
563                                 CheckDisposed ();
564                                 int res;
565
566                                 if (BytesLeft == 0)
567                                         return 0;
568                                 
569                                 if (ar == null)
570                                         throw new ArgumentNullException ("async result is null");
571
572                                 try {
573                                         res = base.EndRead (ar);
574                                         if (BytesLeft != -1)
575                                                 BytesLeft -= res;
576                                 } catch (Exception e) {
577                                         throw new IOException ("EndRead failure", e);
578                                 }
579
580                                 AdjustChunkLeft (res);
581                                 return res;
582                         }
583
584                         public override int Read (byte [] buffer, int offset, int size)
585                         {
586                                 CheckDisposed ();
587                                 int res;
588                                 if (buffer == null)
589                                         throw new ArgumentNullException ("buffer is null");
590
591                                 if (offset < 0 || offset >= buffer.Length)
592                                         throw new ArgumentOutOfRangeException ("offset exceeds the size of buffer");
593
594                                 if (offset + size < 0 || offset + size > buffer.Length)
595                                         throw new ArgumentOutOfRangeException ("offset+size exceeds the size of buffer");
596
597                                 if (!readingChunkSize)
598                                         size = GetMaxSizeFromChunkLeft (size);
599
600                                 try {
601                                         if (BytesLeft == 0)
602                                                 return 0;
603                                 
604                                         res = base.Read (buffer, offset, size);
605                                         if (BytesLeft != -1)
606                                                 BytesLeft -= res;
607                                 } catch (Exception e) {
608                                         throw new IOException ("Read failure", e);
609                                 }
610
611                                 AdjustChunkLeft (res);
612
613                                 return res;
614                         }
615
616                         void CheckDisposed ()
617                         {
618                                 if (disposed)
619                                         throw new ObjectDisposedException (GetType ().FullName);
620                         }
621
622                         void AdjustChunkLeft (int read)
623                         {
624                                 if (!chunked)
625                                         return;
626
627                                 chunkLeft -= read;
628                                 if (chunkLeft < 0)
629                                         chunkLeft = 0;
630                         }
631                 }
632         }       
633 }
634