[System]: Remove 'SECURITY_DEP' conditional from HttpListener and related classes...
[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 using System.Globalization;
30 using System.IO;
31 using System.Text;
32
33 namespace System.Net {
34         public sealed class HttpListenerResponse : IDisposable
35         {
36                 bool disposed;
37                 Encoding content_encoding;
38                 long content_length;
39                 bool cl_set;
40                 string content_type;
41                 CookieCollection cookies;
42                 WebHeaderCollection headers = new WebHeaderCollection ();
43                 bool keep_alive = true;
44                 ResponseStream output_stream;
45                 Version version = HttpVersion.Version11;
46                 string location;
47                 int status_code = 200;
48                 string status_description = "OK";
49                 bool chunked;
50                 HttpListenerContext context;
51                 
52                 internal bool HeadersSent;
53                 internal object headers_lock = new object ();
54                 
55                 bool force_close_chunked;
56
57                 internal HttpListenerResponse (HttpListenerContext context)
58                 {
59                         this.context = context;
60                 }
61
62                 internal bool ForceCloseChunked {
63                         get { return force_close_chunked; }
64                 }
65
66                 public Encoding ContentEncoding {
67                         get {
68                                 if (content_encoding == null)
69                                         content_encoding = Encoding.Default;
70                                 return content_encoding;
71                         }
72                         set {
73                                 if (disposed)
74                                         throw new ObjectDisposedException (GetType ().ToString ());
75
76                                 //TODO: is null ok?
77                                 if (HeadersSent)
78                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
79                                         
80                                 content_encoding = value;
81                         }
82                 }
83
84                 public long ContentLength64 {
85                         get { return content_length; }
86                         set {
87                                 if (disposed)
88                                         throw new ObjectDisposedException (GetType ().ToString ());
89
90                                 if (HeadersSent)
91                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
92
93                                 if (value < 0)
94                                         throw new ArgumentOutOfRangeException ("Must be >= 0", "value");
95
96                                 cl_set = true;
97                                 content_length = value;
98                         }
99                 }
100                 
101                 public string ContentType {
102                         get { return content_type; }
103                         set {
104                                 // TODO: is null ok?
105                                 if (disposed)
106                                         throw new ObjectDisposedException (GetType ().ToString ());
107
108                                 if (HeadersSent)
109                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
110
111                                 content_type = value;
112                         }
113                 }
114
115                 // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html
116                 public CookieCollection Cookies {
117                         get {
118                                 if (cookies == null)
119                                         cookies = new CookieCollection ();
120                                 return cookies;
121                         }
122                         set { cookies = value; } // null allowed?
123                 }
124
125                 public WebHeaderCollection Headers {
126                         get { return headers; }
127                         set {
128                 /**
129                  *      "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or
130                  *      WWW-Authenticate header using the Headers property, an exception will be
131                  *      thrown. Use the KeepAlive or ContentLength64 properties to set these headers.
132                  *      You cannot set the Transfer-Encoding or WWW-Authenticate headers manually."
133                 */
134                 // TODO: check if this is marked readonly after headers are sent.
135                                 headers = value;
136                         }
137                 }
138
139                 public bool KeepAlive {
140                         get { return keep_alive; }
141                         set {
142                                 if (disposed)
143                                         throw new ObjectDisposedException (GetType ().ToString ());
144
145                                 if (HeadersSent)
146                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
147                                         
148                                 keep_alive = value;
149                         }
150                 }
151
152                 public Stream OutputStream {
153                         get {
154                                 if (output_stream == null)
155                                         output_stream = context.Connection.GetResponseStream ();
156                                 return output_stream;
157                         }
158                 }
159                 
160                 public Version ProtocolVersion {
161                         get { return version; }
162                         set {
163                                 if (disposed)
164                                         throw new ObjectDisposedException (GetType ().ToString ());
165
166                                 if (HeadersSent)
167                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
168                                         
169                                 if (value == null)
170                                         throw new ArgumentNullException ("value");
171
172                                 if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
173                                         throw new ArgumentException ("Must be 1.0 or 1.1", "value");
174
175                                 if (disposed)
176                                         throw new ObjectDisposedException (GetType ().ToString ());
177
178                                 version = value;
179                         }
180                 }
181
182                 public string RedirectLocation {
183                         get { return location; }
184                         set {
185                                 if (disposed)
186                                         throw new ObjectDisposedException (GetType ().ToString ());
187
188                                 if (HeadersSent)
189                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
190                                         
191                                 location = value;
192                         }
193                 }
194
195                 public bool SendChunked {
196                         get { return chunked; }
197                         set {
198                                 if (disposed)
199                                         throw new ObjectDisposedException (GetType ().ToString ());
200
201                                 if (HeadersSent)
202                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
203                                         
204                                 chunked = value;
205                         }
206                 }
207
208                 public int StatusCode {
209                         get { return status_code; }
210                         set {
211                                 if (disposed)
212                                         throw new ObjectDisposedException (GetType ().ToString ());
213
214                                 if (HeadersSent)
215                                         throw new InvalidOperationException ("Cannot be changed after headers are sent.");
216                                         
217                                 if (value < 100 || value > 999)
218                                         throw new ProtocolViolationException ("StatusCode must be between 100 and 999.");
219                                 status_code = value;
220                                 status_description = HttpListenerResponseHelper.GetStatusDescription (value);
221                         }
222                 }
223
224                 public string StatusDescription {
225                         get { return status_description; }
226                         set {
227                                 status_description = value;
228                         }
229                 }
230
231                 void IDisposable.Dispose ()
232                 {
233                         Close (true); //TODO: Abort or Close?
234                 }
235
236                 public void Abort ()
237                 {
238                         if (disposed)
239                                 return;
240
241                         Close (true);
242                 }
243
244                 public void AddHeader (string name, string value)
245                 {
246                         if (name == null)
247                                 throw new ArgumentNullException ("name");
248
249                         if (name == "")
250                                 throw new ArgumentException ("'name' cannot be empty", "name");
251                         
252                         //TODO: check for forbidden headers and invalid characters
253                         if (value.Length > 65535)
254                                 throw new ArgumentOutOfRangeException ("value");
255
256                         headers.Set (name, value);
257                 }
258
259                 public void AppendCookie (Cookie cookie)
260                 {
261                         if (cookie == null)
262                                 throw new ArgumentNullException ("cookie");
263                         
264                         Cookies.Add (cookie);
265                 }
266
267                 public void AppendHeader (string name, string value)
268                 {
269                         if (name == null)
270                                 throw new ArgumentNullException ("name");
271
272                         if (name == "")
273                                 throw new ArgumentException ("'name' cannot be empty", "name");
274                         
275                         if (value.Length > 65535)
276                                 throw new ArgumentOutOfRangeException ("value");
277
278                         headers.Add (name, value);
279                 }
280
281                 void Close (bool force)
282                 {
283                         disposed = true;
284                         context.Connection.Close (force);
285                 }
286
287                 public void Close ()
288                 {
289                         if (disposed)
290                                 return;
291
292                         Close (false);
293                 }
294
295                 public void Close (byte [] responseEntity, bool willBlock)
296                 {
297                         if (disposed)
298                                 return;
299
300                         if (responseEntity == null)
301                                 throw new ArgumentNullException ("responseEntity");
302
303                         //TODO: if willBlock -> BeginWrite + Close ?
304                         ContentLength64 = responseEntity.Length;
305                         OutputStream.Write (responseEntity, 0, (int) content_length);
306                         Close (false);
307                 }
308
309                 public void CopyFrom (HttpListenerResponse templateResponse)
310                 {
311                         headers.Clear ();
312                         headers.Add (templateResponse.headers);
313                         content_length = templateResponse.content_length;
314                         status_code = templateResponse.status_code;
315                         status_description = templateResponse.status_description;
316                         keep_alive = templateResponse.keep_alive;
317                         version = templateResponse.version;
318                 }
319
320                 public void Redirect (string url)
321                 {
322                         StatusCode = 302; // Found
323                         location = url;
324                 }
325
326                 bool FindCookie (Cookie cookie)
327                 {
328                         string name = cookie.Name;
329                         string domain = cookie.Domain;
330                         string path = cookie.Path;
331                         foreach (Cookie c in cookies) {
332                                 if (name != c.Name)
333                                         continue;
334                                 if (domain != c.Domain)
335                                         continue;
336                                 if (path == c.Path)
337                                         return true;
338                         }
339
340                         return false;
341                 }
342
343                 internal void SendHeaders (bool closing, MemoryStream ms)
344                 {
345                         Encoding encoding = content_encoding;
346                         if (encoding == null)
347                                 encoding = Encoding.Default;
348
349                         if (content_type != null) {
350                                 if (content_encoding != null && content_type.IndexOf ("charset=", StringComparison.Ordinal) == -1) {
351                                         string enc_name = content_encoding.WebName;
352                                         headers.SetInternal ("Content-Type", content_type + "; charset=" + enc_name);
353                                 } else {
354                                         headers.SetInternal ("Content-Type", content_type);
355                                 }
356                         }
357
358                         if (headers ["Server"] == null)
359                                 headers.SetInternal ("Server", "Mono-HTTPAPI/1.0");
360
361                         CultureInfo inv = CultureInfo.InvariantCulture;
362                         if (headers ["Date"] == null)
363                                 headers.SetInternal ("Date", DateTime.UtcNow.ToString ("r", inv));
364
365                         if (!chunked) {
366                                 if (!cl_set && closing) {
367                                         cl_set = true;
368                                         content_length = 0;
369                                 }
370
371                                 if (cl_set)
372                                         headers.SetInternal ("Content-Length", content_length.ToString (inv));
373                         }
374
375                         Version v = context.Request.ProtocolVersion;
376                         if (!cl_set && !chunked && v >= HttpVersion.Version11)
377                                 chunked = true;
378                                 
379                         /* Apache forces closing the connection for these status codes:
380                          *      HttpStatusCode.BadRequest               400
381                          *      HttpStatusCode.RequestTimeout           408
382                          *      HttpStatusCode.LengthRequired           411
383                          *      HttpStatusCode.RequestEntityTooLarge    413
384                          *      HttpStatusCode.RequestUriTooLong        414
385                          *      HttpStatusCode.InternalServerError      500
386                          *      HttpStatusCode.ServiceUnavailable       503
387                          */
388                         bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
389                                         status_code == 413 || status_code == 414 || status_code == 500 ||
390                                         status_code == 503);
391
392                         if (conn_close == false)
393                                 conn_close = !context.Request.KeepAlive;
394
395                         // They sent both KeepAlive: true and Connection: close!?
396                         if (!keep_alive || conn_close) {
397                                 headers.SetInternal ("Connection", "close");
398                                 conn_close = true;
399                         }
400
401                         if (chunked)
402                                 headers.SetInternal ("Transfer-Encoding", "chunked");
403
404                         int reuses = context.Connection.Reuses;
405                         if (reuses >= 100) {
406                                 force_close_chunked = true;
407                                 if (!conn_close) {
408                                         headers.SetInternal ("Connection", "close");
409                                         conn_close = true;
410                                 }
411                         }
412
413                         if (!conn_close) {
414                                 headers.SetInternal ("Keep-Alive", String.Format ("timeout=15,max={0}", 100 - reuses));
415                                 if (context.Request.ProtocolVersion <= HttpVersion.Version10)
416                                         headers.SetInternal ("Connection", "keep-alive");
417                         }
418
419                         if (location != null)
420                                 headers.SetInternal ("Location", location);
421
422                         if (cookies != null) {
423                                 foreach (Cookie cookie in cookies)
424                                         headers.SetInternal ("Set-Cookie", CookieToClientString (cookie));
425                         }
426
427                         StreamWriter writer = new StreamWriter (ms, encoding, 256);
428                         writer.Write ("HTTP/{0} {1} {2}\r\n", version, status_code, status_description);
429                         string headers_str = FormatHeaders (headers);
430                         writer.Write (headers_str);
431                         writer.Flush ();
432                         int preamble = encoding.GetPreamble ().Length;
433                         if (output_stream == null)
434                                 output_stream = context.Connection.GetResponseStream ();
435
436                         /* Assumes that the ms was at position 0 */
437                         ms.Position = preamble;
438                         HeadersSent = true;
439                 }
440
441                 static string FormatHeaders (WebHeaderCollection headers)
442                 {
443                         var sb = new StringBuilder();
444
445                         for (int i = 0; i < headers.Count ; i++) {
446                                 string key = headers.GetKey (i);
447                                 if (WebHeaderCollection.AllowMultiValues (key)) {
448                                         foreach (string v in headers.GetValues (i)) {
449                                                 sb.Append (key).Append (": ").Append (v).Append ("\r\n");
450                                         }
451                                 } else {
452                                         sb.Append (key).Append (": ").Append (headers.Get (i)).Append ("\r\n");
453                                 }
454                         }
455
456                         return sb.Append("\r\n").ToString();
457                 }
458
459                 static string CookieToClientString (Cookie cookie)
460                 {
461                         if (cookie.Name.Length == 0)
462                                 return String.Empty;
463
464                         StringBuilder result = new StringBuilder (64);
465
466                         if (cookie.Version > 0)
467                                 result.Append ("Version=").Append (cookie.Version).Append (";");
468
469                         result.Append (cookie.Name).Append ("=").Append (cookie.Value);
470
471                         if (cookie.Path != null && cookie.Path.Length != 0)
472                                 result.Append (";Path=").Append (QuotedString (cookie, cookie.Path));
473
474                         if (cookie.Domain != null && cookie.Domain.Length != 0)
475                                 result.Append (";Domain=").Append (QuotedString (cookie, cookie.Domain));                       
476
477                         if (cookie.Port != null && cookie.Port.Length != 0)
478                                 result.Append (";Port=").Append (cookie.Port);  
479
480                         return result.ToString ();
481                 }
482
483                 static string QuotedString (Cookie cookie, string value)
484                 {
485                         if (cookie.Version == 0 || IsToken (value))
486                                 return value;
487                         else 
488                                 return "\"" + value.Replace("\"", "\\\"") + "\"";
489                 }       
490
491                 static string tspecials = "()<>@,;:\\\"/[]?={} \t";   // from RFC 2965, 2068
492
493             static bool IsToken (string value) 
494                 {
495                         int len = value.Length;
496                         for (int i = 0; i < len; i++) {
497                             char c = value [i];
498                                 if (c < 0x20 || c >= 0x7f || tspecials.IndexOf (c) != -1)
499                                         return false;
500                         }
501                         return true;
502                 }
503
504                 public void SetCookie (Cookie cookie)
505                 {
506                         if (cookie == null)
507                                 throw new ArgumentNullException ("cookie");
508
509                         if (cookies != null) {
510                                 if (FindCookie (cookie))
511                                         throw new ArgumentException ("The cookie already exists.");
512                         } else {
513                                 cookies = new CookieCollection ();
514                         }
515
516                         cookies.Add (cookie);
517                 }
518         }
519 }
520