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.
29 using System.Globalization;
32 namespace System.Net {
33 public sealed class HttpListenerResponse : IDisposable
36 Encoding content_encoding;
40 CookieCollection cookies;
41 WebHeaderCollection headers = new WebHeaderCollection ();
42 bool keep_alive = true;
43 ResponseStream output_stream;
44 Version version = HttpVersion.Version11;
46 int status_code = 200;
47 string status_description = "OK";
49 HttpListenerContext context;
50 internal bool HeadersSent;
51 bool force_close_chunked;
53 internal HttpListenerResponse (HttpListenerContext context)
55 this.context = context;
58 internal bool ForceCloseChunked {
59 get { return force_close_chunked; }
62 public Encoding ContentEncoding {
64 if (content_encoding == null)
65 content_encoding = Encoding.Default;
66 return content_encoding;
70 throw new ObjectDisposedException (GetType ().ToString ());
74 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
76 content_encoding = value;
80 public long ContentLength64 {
81 get { return content_length; }
84 throw new ObjectDisposedException (GetType ().ToString ());
87 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
90 throw new ArgumentOutOfRangeException ("Must be >= 0", "value");
93 content_length = value;
97 public string ContentType {
98 get { return content_type; }
102 throw new ObjectDisposedException (GetType ().ToString ());
105 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
107 content_type = value;
111 // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html
112 public CookieCollection Cookies {
115 cookies = new CookieCollection ();
118 set { cookies = value; } // null allowed?
121 public WebHeaderCollection Headers {
122 get { return headers; }
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."
130 // TODO: check if this is marked readonly after headers are sent.
135 public bool KeepAlive {
136 get { return keep_alive; }
139 throw new ObjectDisposedException (GetType ().ToString ());
142 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
148 public Stream OutputStream {
151 throw new ObjectDisposedException (GetType ().ToString ());
153 if (output_stream == null)
154 output_stream = context.Connection.GetResponseStream ();
155 return output_stream;
159 public Version ProtocolVersion {
160 get { return version; }
163 throw new ObjectDisposedException (GetType ().ToString ());
166 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
169 throw new ArgumentNullException ("value");
171 if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
172 throw new ArgumentException ("Must be 1.0 or 1.1", "value");
175 throw new ObjectDisposedException (GetType ().ToString ());
181 public string RedirectLocation {
182 get { return location; }
185 throw new ObjectDisposedException (GetType ().ToString ());
188 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
194 public bool SendChunked {
195 get { return chunked; }
198 throw new ObjectDisposedException (GetType ().ToString ());
201 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
207 public int StatusCode {
208 get { return status_code; }
211 throw new ObjectDisposedException (GetType ().ToString ());
214 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
216 if (value < 100 || value > 999)
217 throw new ProtocolViolationException ("StatusCode must be between 100 and 999.");
219 status_description = GetStatusDescription (value);
223 internal static string GetStatusDescription (int 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";
276 public string StatusDescription {
277 get { return status_description; }
279 status_description = value;
283 void IDisposable.Dispose ()
285 Close (true); //TODO: Abort or Close?
296 public void AddHeader (string name, string value)
299 throw new ArgumentNullException ("name");
302 throw new ArgumentException ("'name' cannot be empty", "name");
304 //TODO: check for forbidden headers and invalid characters
305 if (value.Length > 65535)
306 throw new ArgumentOutOfRangeException ("value");
308 headers.Set (name, value);
311 public void AppendCookie (Cookie cookie)
314 throw new ArgumentNullException ("cookie");
316 cookies.Add (cookie);
319 public void AppendHeader (string name, string value)
322 throw new ArgumentNullException ("name");
325 throw new ArgumentException ("'name' cannot be empty", "name");
327 if (value.Length > 65535)
328 throw new ArgumentOutOfRangeException ("value");
330 headers.Add (name, value);
333 void Close (bool force)
335 // TODO: use the 'force' argument
336 context.Connection.Close ();
348 public void Close (byte [] responseEntity, bool willBlock)
353 if (responseEntity == null)
354 throw new ArgumentNullException ("responseEntity");
356 //TODO: if willBlock -> BeginWrite + Close ?
357 ContentLength64 = responseEntity.Length;
358 OutputStream.Write (responseEntity, 0, (int) content_length);
362 public void CopyFrom (HttpListenerResponse templateResponse)
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;
373 public void Redirect (string url)
375 StatusCode = 302; // Found
379 bool FindCookie (Cookie cookie)
381 string name = cookie.Name;
382 string domain = cookie.Domain;
383 string path = cookie.Path;
384 foreach (Cookie c in cookies) {
387 if (domain != c.Domain)
396 internal void SendHeaders (bool closing)
398 //TODO: When do we send KeepAlive?
400 MemoryStream ms = new MemoryStream ();
401 Encoding encoding = content_encoding;
402 if (encoding == null)
403 encoding = Encoding.Default;
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);
410 headers.SetInternal ("Content-Type", content_type);
414 if (headers ["Server"] == null)
415 headers.SetInternal ("Server", "Mono-HTTPAPI/1.0");
417 CultureInfo inv = CultureInfo.InvariantCulture;
418 if (headers ["Date"] == null)
419 headers.SetInternal ("Date", DateTime.UtcNow.ToString ("r", inv));
422 if (!cl_set && closing) {
428 headers.SetInternal ("Content-Length", content_length.ToString (inv));
431 Version v = context.Request.ProtocolVersion;
432 if (!cl_set && !chunked && v >= HttpVersion.Version11)
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
444 bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
445 status_code == 413 || status_code == 414 || status_code == 500 ||
448 if (conn_close == false)
449 conn_close = (context.Request.Headers ["connection"] == "close");
451 // They sent both KeepAlive: true and Connection: close!?
452 if (!chunked || conn_close)
453 headers.SetInternal ("Connection", "close");
456 headers.SetInternal ("Transfer-Encoding", "chunked");
458 int chunked_uses = context.Connection.ChunkedUses;
459 if (chunked_uses >= 100) {
460 force_close_chunked = true;
462 headers.SetInternal ("Connection", "close");
465 if (location != null)
466 headers.SetInternal ("Location", location);
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);
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 ();
478 output_stream.InternalWrite (ms.GetBuffer (), 0 + preamble, (int) ms.Length - preamble);
482 public void SetCookie (Cookie cookie)
485 throw new ArgumentNullException ("cookie");
487 if (cookies != null) {
488 if (FindCookie (cookie))
489 throw new ArgumentException ("The cookie already exists.");
491 cookies = new CookieCollection ();
494 cookies.Add (cookie);