Merge pull request #2992 from lambdageek/dev/monoerror-other-invokes
[mono.git] / mcs / class / System / System.Net / HttpListenerResponse.cs
1 //
2 // System.Net.HttpListenerResponse
3 //
4 // Author:
5 //      Gonzalo Paniagua Javier (gonzalo@novell.com)
6 //
7 // Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 // 
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 // 
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28
29 #if SECURITY_DEP
30
31 using System.Globalization;
32 using System.IO;
33 using System.Text;
34 namespace System.Net {
35         public sealed class HttpListenerResponse : IDisposable
36         {
37                 bool disposed;
38                 Encoding content_encoding;
39                 long content_length;
40                 bool cl_set;
41                 string content_type;
42                 CookieCollection cookies;
43                 WebHeaderCollection headers = new WebHeaderCollection ();
44                 bool keep_alive = true;
45                 ResponseStream output_stream;
46                 Version version = HttpVersion.Version11;
47                 string location;
48                 int status_code = 200;
49                 string status_description = "OK";
50                 bool chunked;
51                 HttpListenerContext context;
52                 
53                 internal bool HeadersSent;
54                 internal object headers_lock = new object ();
55                 
56                 bool force_close_chunked;
57
58                 internal HttpListenerResponse (HttpListenerContext context)
59                 {
60                         this.context = context;
61                 }
62
63                 internal bool ForceCloseChunked {
64                         get { return force_close_chunked; }
65                 }
66
67                 public Encoding ContentEncoding {
68                         get {
69                                 if (content_encoding == null)
70                                         content_encoding = Encoding.Default;
71                                 return content_encoding;
72                         }
73                         set {
74                                 if (disposed)
75                                         throw new ObjectDisposedException (GetType ().ToString ());
76
77                                 //TODO: is null ok?
78                                 if (HeadersSent)
79                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
80                                         
81                                 content_encoding = value;
82                         }
83                 }
84
85                 public long ContentLength64 {
86                         get { return content_length; }
87                         set {
88                                 if (disposed)
89                                         throw new ObjectDisposedException (GetType ().ToString ());
90
91                                 if (HeadersSent)
92                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
93
94                                 if (value < 0)
95                                         throw new ArgumentOutOfRangeException ("Must be >= 0", "value");
96
97                                 cl_set = true;
98                                 content_length = value;
99                         }
100                 }
101                 
102                 public string ContentType {
103                         get { return content_type; }
104                         set {
105                                 // TODO: is null ok?
106                                 if (disposed)
107                                         throw new ObjectDisposedException (GetType ().ToString ());
108
109                                 if (HeadersSent)
110                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
111
112                                 content_type = value;
113                         }
114                 }
115
116                 // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html
117                 public CookieCollection Cookies {
118                         get {
119                                 if (cookies == null)
120                                         cookies = new CookieCollection ();
121                                 return cookies;
122                         }
123                         set { cookies = value; } // null allowed?
124                 }
125
126                 public WebHeaderCollection Headers {
127                         get { return headers; }
128                         set {
129                 /**
130                  *      "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or
131                  *      WWW-Authenticate header using the Headers property, an exception will be
132                  *      thrown. Use the KeepAlive or ContentLength64 properties to set these headers.
133                  *      You cannot set the Transfer-Encoding or WWW-Authenticate headers manually."
134                 */
135                 // TODO: check if this is marked readonly after headers are sent.
136                                 headers = value;
137                         }
138                 }
139
140                 public bool KeepAlive {
141                         get { return keep_alive; }
142                         set {
143                                 if (disposed)
144                                         throw new ObjectDisposedException (GetType ().ToString ());
145
146                                 if (HeadersSent)
147                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
148                                         
149                                 keep_alive = value;
150                         }
151                 }
152
153                 public Stream OutputStream {
154                         get {
155                                 if (output_stream == null)
156                                         output_stream = context.Connection.GetResponseStream ();
157                                 return output_stream;
158                         }
159                 }
160                 
161                 public Version ProtocolVersion {
162                         get { return version; }
163                         set {
164                                 if (disposed)
165                                         throw new ObjectDisposedException (GetType ().ToString ());
166
167                                 if (HeadersSent)
168                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
169                                         
170                                 if (value == null)
171                                         throw new ArgumentNullException ("value");
172
173                                 if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
174                                         throw new ArgumentException ("Must be 1.0 or 1.1", "value");
175
176                                 if (disposed)
177                                         throw new ObjectDisposedException (GetType ().ToString ());
178
179                                 version = value;
180                         }
181                 }
182
183                 public string RedirectLocation {
184                         get { return location; }
185                         set {
186                                 if (disposed)
187                                         throw new ObjectDisposedException (GetType ().ToString ());
188
189                                 if (HeadersSent)
190                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
191                                         
192                                 location = value;
193                         }
194                 }
195
196                 public bool SendChunked {
197                         get { return chunked; }
198                         set {
199                                 if (disposed)
200                                         throw new ObjectDisposedException (GetType ().ToString ());
201
202                                 if (HeadersSent)
203                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
204                                         
205                                 chunked = value;
206                         }
207                 }
208
209                 public int StatusCode {
210                         get { return status_code; }
211                         set {
212                                 if (disposed)
213                                         throw new ObjectDisposedException (GetType ().ToString ());
214
215                                 if (HeadersSent)
216                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
217                                         
218                                 if (value < 100 || value > 999)
219                                         throw new ProtocolViolationException ("StatusCode must be between 100 and 999.");
220                                 status_code = value;
221                                 status_description = HttpListenerResponseHelper.GetStatusDescription (value);
222                         }
223                 }
224
225                 public string StatusDescription {
226                         get { return status_description; }
227                         set {
228                                 status_description = value;
229                         }
230                 }
231
232                 void IDisposable.Dispose ()
233                 {
234                         Close (true); //TODO: Abort or Close?
235                 }
236
237                 public void Abort ()
238                 {
239                         if (disposed)
240                                 return;
241
242                         Close (true);
243                 }
244
245                 public void AddHeader (string name, string value)
246                 {
247                         if (name == null)
248                                 throw new ArgumentNullException ("name");
249
250                         if (name == "")
251                                 throw new ArgumentException ("'name' cannot be empty", "name");
252                         
253                         //TODO: check for forbidden headers and invalid characters
254                         if (value.Length > 65535)
255                                 throw new ArgumentOutOfRangeException ("value");
256
257                         headers.Set (name, value);
258                 }
259
260                 public void AppendCookie (Cookie cookie)
261                 {
262                         if (cookie == null)
263                                 throw new ArgumentNullException ("cookie");
264                         
265                         Cookies.Add (cookie);
266                 }
267
268                 public void AppendHeader (string name, string value)
269                 {
270                         if (name == null)
271                                 throw new ArgumentNullException ("name");
272
273                         if (name == "")
274                                 throw new ArgumentException ("'name' cannot be empty", "name");
275                         
276                         if (value.Length > 65535)
277                                 throw new ArgumentOutOfRangeException ("value");
278
279                         headers.Add (name, value);
280                 }
281
282                 void Close (bool force)
283                 {
284                         disposed = true;
285                         context.Connection.Close (force);
286                 }
287
288                 public void Close ()
289                 {
290                         if (disposed)
291                                 return;
292
293                         Close (false);
294                 }
295
296                 public void Close (byte [] responseEntity, bool willBlock)
297                 {
298                         if (disposed)
299                                 return;
300
301                         if (responseEntity == null)
302                                 throw new ArgumentNullException ("responseEntity");
303
304                         //TODO: if willBlock -> BeginWrite + Close ?
305                         ContentLength64 = responseEntity.Length;
306                         OutputStream.Write (responseEntity, 0, (int) content_length);
307                         Close (false);
308                 }
309
310                 public void CopyFrom (HttpListenerResponse templateResponse)
311                 {
312                         headers.Clear ();
313                         headers.Add (templateResponse.headers);
314                         content_length = templateResponse.content_length;
315                         status_code = templateResponse.status_code;
316                         status_description = templateResponse.status_description;
317                         keep_alive = templateResponse.keep_alive;
318                         version = templateResponse.version;
319                 }
320
321                 public void Redirect (string url)
322                 {
323                         StatusCode = 302; // Found
324                         location = url;
325                 }
326
327                 bool FindCookie (Cookie cookie)
328                 {
329                         string name = cookie.Name;
330                         string domain = cookie.Domain;
331                         string path = cookie.Path;
332                         foreach (Cookie c in cookies) {
333                                 if (name != c.Name)
334                                         continue;
335                                 if (domain != c.Domain)
336                                         continue;
337                                 if (path == c.Path)
338                                         return true;
339                         }
340
341                         return false;
342                 }
343
344                 internal void SendHeaders (bool closing, MemoryStream ms)
345                 {
346                         Encoding encoding = content_encoding;
347                         if (encoding == null)
348                                 encoding = Encoding.Default;
349
350                         if (content_type != null) {
351                                 if (content_encoding != null && content_type.IndexOf ("charset=", StringComparison.Ordinal) == -1) {
352                                         string enc_name = content_encoding.WebName;
353                                         headers.SetInternal ("Content-Type", content_type + "; charset=" + enc_name);
354                                 } else {
355                                         headers.SetInternal ("Content-Type", content_type);
356                                 }
357                         }
358
359                         if (headers ["Server"] == null)
360                                 headers.SetInternal ("Server", "Mono-HTTPAPI/1.0");
361
362                         CultureInfo inv = CultureInfo.InvariantCulture;
363                         if (headers ["Date"] == null)
364                                 headers.SetInternal ("Date", DateTime.UtcNow.ToString ("r", inv));
365
366                         if (!chunked) {
367                                 if (!cl_set && closing) {
368                                         cl_set = true;
369                                         content_length = 0;
370                                 }
371
372                                 if (cl_set)
373                                         headers.SetInternal ("Content-Length", content_length.ToString (inv));
374                         }
375
376                         Version v = context.Request.ProtocolVersion;
377                         if (!cl_set && !chunked && v >= HttpVersion.Version11)
378                                 chunked = true;
379                                 
380                         /* Apache forces closing the connection for these status codes:
381                          *      HttpStatusCode.BadRequest               400
382                          *      HttpStatusCode.RequestTimeout           408
383                          *      HttpStatusCode.LengthRequired           411
384                          *      HttpStatusCode.RequestEntityTooLarge    413
385                          *      HttpStatusCode.RequestUriTooLong        414
386                          *      HttpStatusCode.InternalServerError      500
387                          *      HttpStatusCode.ServiceUnavailable       503
388                          */
389                         bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
390                                         status_code == 413 || status_code == 414 || status_code == 500 ||
391                                         status_code == 503);
392
393                         if (conn_close == false)
394                                 conn_close = !context.Request.KeepAlive;
395
396                         // They sent both KeepAlive: true and Connection: close!?
397                         if (!keep_alive || conn_close) {
398                                 headers.SetInternal ("Connection", "close");
399                                 conn_close = true;
400                         }
401
402                         if (chunked)
403                                 headers.SetInternal ("Transfer-Encoding", "chunked");
404
405                         int reuses = context.Connection.Reuses;
406                         if (reuses >= 100) {
407                                 force_close_chunked = true;
408                                 if (!conn_close) {
409                                         headers.SetInternal ("Connection", "close");
410                                         conn_close = true;
411                                 }
412                         }
413
414                         if (!conn_close) {
415                                 headers.SetInternal ("Keep-Alive", String.Format ("timeout=15,max={0}", 100 - reuses));
416                                 if (context.Request.ProtocolVersion <= HttpVersion.Version10)
417                                         headers.SetInternal ("Connection", "keep-alive");
418                         }
419
420                         if (location != null)
421                                 headers.SetInternal ("Location", location);
422
423                         if (cookies != null) {
424                                 foreach (Cookie cookie in cookies)
425                                         headers.SetInternal ("Set-Cookie", CookieToClientString (cookie));
426                         }
427
428                         StreamWriter writer = new StreamWriter (ms, encoding, 256);
429                         writer.Write ("HTTP/{0} {1} {2}\r\n", version, status_code, status_description);
430                         string headers_str = headers.ToString ();
431                         writer.Write (headers_str);
432                         writer.Flush ();
433                         int preamble = (encoding.CodePage == 65001) ? 3 : encoding.GetPreamble ().Length;
434                         if (output_stream == null)
435                                 output_stream = context.Connection.GetResponseStream ();
436
437                         /* Assumes that the ms was at position 0 */
438                         ms.Position = preamble;
439                         HeadersSent = true;
440                 }
441
442                 static string CookieToClientString (Cookie cookie)
443                 {
444                         if (cookie.Name.Length == 0)
445                                 return String.Empty;
446
447                         StringBuilder result = new StringBuilder (64);
448
449                         if (cookie.Version > 0)
450                                 result.Append ("Version=").Append (cookie.Version).Append (";");
451
452                         result.Append (cookie.Name).Append ("=").Append (cookie.Value);
453
454                         if (cookie.Path != null && cookie.Path.Length != 0)
455                                 result.Append (";Path=").Append (QuotedString (cookie, cookie.Path));
456
457                         if (cookie.Domain != null && cookie.Domain.Length != 0)
458                                 result.Append (";Domain=").Append (QuotedString (cookie, cookie.Domain));                       
459
460                         if (cookie.Port != null && cookie.Port.Length != 0)
461                                 result.Append (";Port=").Append (cookie.Port);  
462
463                         return result.ToString ();
464                 }
465
466                 static string QuotedString (Cookie cookie, string value)
467                 {
468                         if (cookie.Version == 0 || IsToken (value))
469                                 return value;
470                         else 
471                                 return "\"" + value.Replace("\"", "\\\"") + "\"";
472                 }       
473
474                 static string tspecials = "()<>@,;:\\\"/[]?={} \t";   // from RFC 2965, 2068
475
476             static bool IsToken (string value) 
477                 {
478                         int len = value.Length;
479                         for (int i = 0; i < len; i++) {
480                             char c = value [i];
481                                 if (c < 0x20 || c >= 0x7f || tspecials.IndexOf (c) != -1)
482                                         return false;
483                         }
484                         return true;
485                 }
486
487                 public void SetCookie (Cookie cookie)
488                 {
489                         if (cookie == null)
490                                 throw new ArgumentNullException ("cookie");
491
492                         if (cookies != null) {
493                                 if (FindCookie (cookie))
494                                         throw new ArgumentException ("The cookie already exists.");
495                         } else {
496                                 cookies = new CookieCollection ();
497                         }
498
499                         cookies.Add (cookie);
500                 }
501         }
502
503         // do not inline into HttpListenerResponse as this recursively brings everything that's 
504         // reachable by IDisposable.Dispose (and that's quite a lot in this case). 
505         static class HttpListenerResponseHelper {
506
507                 internal static string GetStatusDescription (int code)
508                 {
509                         switch (code){
510                         case 100: return "Continue";
511                         case 101: return "Switching Protocols";
512                         case 102: return "Processing";
513                         case 200: return "OK";
514                         case 201: return "Created";
515                         case 202: return "Accepted";
516                         case 203: return "Non-Authoritative Information";
517                         case 204: return "No Content";
518                         case 205: return "Reset Content";
519                         case 206: return "Partial Content";
520                         case 207: return "Multi-Status";
521                         case 300: return "Multiple Choices";
522                         case 301: return "Moved Permanently";
523                         case 302: return "Found";
524                         case 303: return "See Other";
525                         case 304: return "Not Modified";
526                         case 305: return "Use Proxy";
527                         case 307: return "Temporary Redirect";
528                         case 400: return "Bad Request";
529                         case 401: return "Unauthorized";
530                         case 402: return "Payment Required";
531                         case 403: return "Forbidden";
532                         case 404: return "Not Found";
533                         case 405: return "Method Not Allowed";
534                         case 406: return "Not Acceptable";
535                         case 407: return "Proxy Authentication Required";
536                         case 408: return "Request Timeout";
537                         case 409: return "Conflict";
538                         case 410: return "Gone";
539                         case 411: return "Length Required";
540                         case 412: return "Precondition Failed";
541                         case 413: return "Request Entity Too Large";
542                         case 414: return "Request-Uri Too Long";
543                         case 415: return "Unsupported Media Type";
544                         case 416: return "Requested Range Not Satisfiable";
545                         case 417: return "Expectation Failed";
546                         case 422: return "Unprocessable Entity";
547                         case 423: return "Locked";
548                         case 424: return "Failed Dependency";
549                         case 500: return "Internal Server Error";
550                         case 501: return "Not Implemented";
551                         case 502: return "Bad Gateway";
552                         case 503: return "Service Unavailable";
553                         case 504: return "Gateway Timeout";
554                         case 505: return "Http Version Not Supported";
555                         case 507: return "Insufficient Storage";
556                         }
557                         return "";
558                 }
559         }
560 }
561 #endif
562