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