2 // System.Net.HttpListenerResponse
5 // Gonzalo Paniagua Javier (gonzalo@novell.com)
7 // Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
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:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
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.
31 using System.Globalization;
34 namespace System.Net {
35 public sealed class HttpListenerResponse : IDisposable
38 Encoding content_encoding;
42 CookieCollection cookies;
43 WebHeaderCollection headers = new WebHeaderCollection ();
44 bool keep_alive = true;
45 ResponseStream output_stream;
46 Version version = HttpVersion.Version11;
48 int status_code = 200;
49 string status_description = "OK";
51 HttpListenerContext context;
53 internal bool HeadersSent;
54 internal object headers_lock = new object ();
56 bool force_close_chunked;
58 internal HttpListenerResponse (HttpListenerContext context)
60 this.context = context;
63 internal bool ForceCloseChunked {
64 get { return force_close_chunked; }
67 public Encoding ContentEncoding {
69 if (content_encoding == null)
70 content_encoding = Encoding.Default;
71 return content_encoding;
75 throw new ObjectDisposedException (GetType ().ToString ());
79 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
81 content_encoding = value;
85 public long ContentLength64 {
86 get { return content_length; }
89 throw new ObjectDisposedException (GetType ().ToString ());
92 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
95 throw new ArgumentOutOfRangeException ("Must be >= 0", "value");
98 content_length = value;
102 public string ContentType {
103 get { return content_type; }
107 throw new ObjectDisposedException (GetType ().ToString ());
110 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
112 content_type = value;
116 // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html
117 public CookieCollection Cookies {
120 cookies = new CookieCollection ();
123 set { cookies = value; } // null allowed?
126 public WebHeaderCollection Headers {
127 get { return headers; }
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."
135 // TODO: check if this is marked readonly after headers are sent.
140 public bool KeepAlive {
141 get { return keep_alive; }
144 throw new ObjectDisposedException (GetType ().ToString ());
147 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
153 public Stream OutputStream {
155 if (output_stream == null)
156 output_stream = context.Connection.GetResponseStream ();
157 return output_stream;
161 public Version ProtocolVersion {
162 get { return version; }
165 throw new ObjectDisposedException (GetType ().ToString ());
168 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
171 throw new ArgumentNullException ("value");
173 if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
174 throw new ArgumentException ("Must be 1.0 or 1.1", "value");
177 throw new ObjectDisposedException (GetType ().ToString ());
183 public string RedirectLocation {
184 get { return location; }
187 throw new ObjectDisposedException (GetType ().ToString ());
190 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
196 public bool SendChunked {
197 get { return chunked; }
200 throw new ObjectDisposedException (GetType ().ToString ());
203 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
209 public int StatusCode {
210 get { return status_code; }
213 throw new ObjectDisposedException (GetType ().ToString ());
216 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
218 if (value < 100 || value > 999)
219 throw new ProtocolViolationException ("StatusCode must be between 100 and 999.");
221 status_description = HttpListenerResponseHelper.GetStatusDescription (value);
225 public string StatusDescription {
226 get { return status_description; }
228 status_description = value;
232 void IDisposable.Dispose ()
234 Close (true); //TODO: Abort or Close?
245 public void AddHeader (string name, string value)
248 throw new ArgumentNullException ("name");
251 throw new ArgumentException ("'name' cannot be empty", "name");
253 //TODO: check for forbidden headers and invalid characters
254 if (value.Length > 65535)
255 throw new ArgumentOutOfRangeException ("value");
257 headers.Set (name, value);
260 public void AppendCookie (Cookie cookie)
263 throw new ArgumentNullException ("cookie");
265 Cookies.Add (cookie);
268 public void AppendHeader (string name, string value)
271 throw new ArgumentNullException ("name");
274 throw new ArgumentException ("'name' cannot be empty", "name");
276 if (value.Length > 65535)
277 throw new ArgumentOutOfRangeException ("value");
279 headers.Add (name, value);
282 void Close (bool force)
285 context.Connection.Close (force);
296 public void Close (byte [] responseEntity, bool willBlock)
301 if (responseEntity == null)
302 throw new ArgumentNullException ("responseEntity");
304 //TODO: if willBlock -> BeginWrite + Close ?
305 ContentLength64 = responseEntity.Length;
306 OutputStream.Write (responseEntity, 0, (int) content_length);
310 public void CopyFrom (HttpListenerResponse templateResponse)
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;
321 public void Redirect (string url)
323 StatusCode = 302; // Found
327 bool FindCookie (Cookie cookie)
329 string name = cookie.Name;
330 string domain = cookie.Domain;
331 string path = cookie.Path;
332 foreach (Cookie c in cookies) {
335 if (domain != c.Domain)
344 internal void SendHeaders (bool closing, MemoryStream ms)
346 Encoding encoding = content_encoding;
347 if (encoding == null)
348 encoding = Encoding.Default;
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);
355 headers.SetInternal ("Content-Type", content_type);
359 if (headers ["Server"] == null)
360 headers.SetInternal ("Server", "Mono-HTTPAPI/1.0");
362 CultureInfo inv = CultureInfo.InvariantCulture;
363 if (headers ["Date"] == null)
364 headers.SetInternal ("Date", DateTime.UtcNow.ToString ("r", inv));
367 if (!cl_set && closing) {
373 headers.SetInternal ("Content-Length", content_length.ToString (inv));
376 Version v = context.Request.ProtocolVersion;
377 if (!cl_set && !chunked && v >= HttpVersion.Version11)
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
389 bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
390 status_code == 413 || status_code == 414 || status_code == 500 ||
393 if (conn_close == false)
394 conn_close = !context.Request.KeepAlive;
396 // They sent both KeepAlive: true and Connection: close!?
397 if (!keep_alive || conn_close) {
398 headers.SetInternal ("Connection", "close");
403 headers.SetInternal ("Transfer-Encoding", "chunked");
405 int reuses = context.Connection.Reuses;
407 force_close_chunked = true;
409 headers.SetInternal ("Connection", "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");
420 if (location != null)
421 headers.SetInternal ("Location", location);
423 if (cookies != null) {
424 foreach (Cookie cookie in cookies)
425 headers.SetInternal ("Set-Cookie", CookieToClientString (cookie));
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 = FormatHeaders (headers);
431 writer.Write (headers_str);
433 int preamble = encoding.GetPreamble ().Length;
434 if (output_stream == null)
435 output_stream = context.Connection.GetResponseStream ();
437 /* Assumes that the ms was at position 0 */
438 ms.Position = preamble;
442 static string FormatHeaders (WebHeaderCollection headers)
444 var sb = new StringBuilder();
446 for (int i = 0; i < headers.Count ; i++) {
447 string key = headers.GetKey (i);
448 if (WebHeaderCollection.AllowMultiValues (key)) {
449 foreach (string v in headers.GetValues (i)) {
450 sb.Append (key).Append (": ").Append (v).Append ("\r\n");
453 sb.Append (key).Append (": ").Append (headers.Get (i)).Append ("\r\n");
457 return sb.Append("\r\n").ToString();
460 static string CookieToClientString (Cookie cookie)
462 if (cookie.Name.Length == 0)
465 StringBuilder result = new StringBuilder (64);
467 if (cookie.Version > 0)
468 result.Append ("Version=").Append (cookie.Version).Append (";");
470 result.Append (cookie.Name).Append ("=").Append (cookie.Value);
472 if (cookie.Path != null && cookie.Path.Length != 0)
473 result.Append (";Path=").Append (QuotedString (cookie, cookie.Path));
475 if (cookie.Domain != null && cookie.Domain.Length != 0)
476 result.Append (";Domain=").Append (QuotedString (cookie, cookie.Domain));
478 if (cookie.Port != null && cookie.Port.Length != 0)
479 result.Append (";Port=").Append (cookie.Port);
481 return result.ToString ();
484 static string QuotedString (Cookie cookie, string value)
486 if (cookie.Version == 0 || IsToken (value))
489 return "\"" + value.Replace("\"", "\\\"") + "\"";
492 static string tspecials = "()<>@,;:\\\"/[]?={} \t"; // from RFC 2965, 2068
494 static bool IsToken (string value)
496 int len = value.Length;
497 for (int i = 0; i < len; i++) {
499 if (c < 0x20 || c >= 0x7f || tspecials.IndexOf (c) != -1)
505 public void SetCookie (Cookie cookie)
508 throw new ArgumentNullException ("cookie");
510 if (cookies != null) {
511 if (FindCookie (cookie))
512 throw new ArgumentException ("The cookie already exists.");
514 cookies = new CookieCollection ();
517 cookies.Add (cookie);
521 // do not inline into HttpListenerResponse as this recursively brings everything that's
522 // reachable by IDisposable.Dispose (and that's quite a lot in this case).
523 static class HttpListenerResponseHelper {
525 internal static string GetStatusDescription (int code)
528 case 100: return "Continue";
529 case 101: return "Switching Protocols";
530 case 102: return "Processing";
531 case 200: return "OK";
532 case 201: return "Created";
533 case 202: return "Accepted";
534 case 203: return "Non-Authoritative Information";
535 case 204: return "No Content";
536 case 205: return "Reset Content";
537 case 206: return "Partial Content";
538 case 207: return "Multi-Status";
539 case 300: return "Multiple Choices";
540 case 301: return "Moved Permanently";
541 case 302: return "Found";
542 case 303: return "See Other";
543 case 304: return "Not Modified";
544 case 305: return "Use Proxy";
545 case 307: return "Temporary Redirect";
546 case 400: return "Bad Request";
547 case 401: return "Unauthorized";
548 case 402: return "Payment Required";
549 case 403: return "Forbidden";
550 case 404: return "Not Found";
551 case 405: return "Method Not Allowed";
552 case 406: return "Not Acceptable";
553 case 407: return "Proxy Authentication Required";
554 case 408: return "Request Timeout";
555 case 409: return "Conflict";
556 case 410: return "Gone";
557 case 411: return "Length Required";
558 case 412: return "Precondition Failed";
559 case 413: return "Request Entity Too Large";
560 case 414: return "Request-Uri Too Long";
561 case 415: return "Unsupported Media Type";
562 case 416: return "Requested Range Not Satisfiable";
563 case 417: return "Expectation Failed";
564 case 422: return "Unprocessable Entity";
565 case 423: return "Locked";
566 case 424: return "Failed Dependency";
567 case 500: return "Internal Server Error";
568 case 501: return "Not Implemented";
569 case 502: return "Bad Gateway";
570 case 503: return "Service Unavailable";
571 case 504: return "Gateway Timeout";
572 case 505: return "Http Version Not Supported";
573 case 507: return "Insufficient Storage";