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