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 #if NET_2_0 && SECURITY_DEP
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;
52 internal bool HeadersSent;
53 bool force_close_chunked;
55 internal HttpListenerResponse (HttpListenerContext context)
57 this.context = context;
60 internal bool ForceCloseChunked {
61 get { return force_close_chunked; }
64 public Encoding ContentEncoding {
66 if (content_encoding == null)
67 content_encoding = Encoding.Default;
68 return content_encoding;
72 throw new ObjectDisposedException (GetType ().ToString ());
76 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
78 content_encoding = value;
82 public long ContentLength64 {
83 get { return content_length; }
86 throw new ObjectDisposedException (GetType ().ToString ());
89 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
92 throw new ArgumentOutOfRangeException ("Must be >= 0", "value");
95 content_length = value;
99 public string ContentType {
100 get { return content_type; }
104 throw new ObjectDisposedException (GetType ().ToString ());
107 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
109 content_type = value;
113 // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html
114 public CookieCollection Cookies {
117 cookies = new CookieCollection ();
120 set { cookies = value; } // null allowed?
123 public WebHeaderCollection Headers {
124 get { return headers; }
127 * "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or
128 * WWW-Authenticate header using the Headers property, an exception will be
129 * thrown. Use the KeepAlive or ContentLength64 properties to set these headers.
130 * You cannot set the Transfer-Encoding or WWW-Authenticate headers manually."
132 // TODO: check if this is marked readonly after headers are sent.
137 public bool KeepAlive {
138 get { return keep_alive; }
141 throw new ObjectDisposedException (GetType ().ToString ());
144 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
150 public Stream OutputStream {
152 if (output_stream == null)
153 output_stream = context.Connection.GetResponseStream ();
154 return output_stream;
158 public Version ProtocolVersion {
159 get { return version; }
162 throw new ObjectDisposedException (GetType ().ToString ());
165 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
168 throw new ArgumentNullException ("value");
170 if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
171 throw new ArgumentException ("Must be 1.0 or 1.1", "value");
174 throw new ObjectDisposedException (GetType ().ToString ());
180 public string RedirectLocation {
181 get { return location; }
184 throw new ObjectDisposedException (GetType ().ToString ());
187 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
193 public bool SendChunked {
194 get { return chunked; }
197 throw new ObjectDisposedException (GetType ().ToString ());
200 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
206 public int StatusCode {
207 get { return status_code; }
210 throw new ObjectDisposedException (GetType ().ToString ());
213 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
215 if (value < 100 || value > 999)
216 throw new ProtocolViolationException ("StatusCode must be between 100 and 999.");
218 status_description = GetStatusDescription (value);
222 internal static string GetStatusDescription (int code)
225 case 100: return "Continue";
226 case 101: return "Switching Protocols";
227 case 102: return "Processing";
228 case 200: return "OK";
229 case 201: return "Created";
230 case 202: return "Accepted";
231 case 203: return "Non-Authoritative Information";
232 case 204: return "No Content";
233 case 205: return "Reset Content";
234 case 206: return "Partial Content";
235 case 207: return "Multi-Status";
236 case 300: return "Multiple Choices";
237 case 301: return "Moved Permanently";
238 case 302: return "Found";
239 case 303: return "See Other";
240 case 304: return "Not Modified";
241 case 305: return "Use Proxy";
242 case 307: return "Temporary Redirect";
243 case 400: return "Bad Request";
244 case 401: return "Unauthorized";
245 case 402: return "Payment Required";
246 case 403: return "Forbidden";
247 case 404: return "Not Found";
248 case 405: return "Method Not Allowed";
249 case 406: return "Not Acceptable";
250 case 407: return "Proxy Authentication Required";
251 case 408: return "Request Timeout";
252 case 409: return "Conflict";
253 case 410: return "Gone";
254 case 411: return "Length Required";
255 case 412: return "Precondition Failed";
256 case 413: return "Request Entity Too Large";
257 case 414: return "Request-Uri Too Long";
258 case 415: return "Unsupported Media Type";
259 case 416: return "Requested Range Not Satisfiable";
260 case 417: return "Expectation Failed";
261 case 422: return "Unprocessable Entity";
262 case 423: return "Locked";
263 case 424: return "Failed Dependency";
264 case 500: return "Internal Server Error";
265 case 501: return "Not Implemented";
266 case 502: return "Bad Gateway";
267 case 503: return "Service Unavailable";
268 case 504: return "Gateway Timeout";
269 case 505: return "Http Version Not Supported";
270 case 507: return "Insufficient Storage";
275 public string StatusDescription {
276 get { return status_description; }
278 status_description = value;
282 void IDisposable.Dispose ()
284 Close (true); //TODO: Abort or Close?
295 public void AddHeader (string name, string value)
298 throw new ArgumentNullException ("name");
301 throw new ArgumentException ("'name' cannot be empty", "name");
303 //TODO: check for forbidden headers and invalid characters
304 if (value.Length > 65535)
305 throw new ArgumentOutOfRangeException ("value");
307 headers.Set (name, value);
310 public void AppendCookie (Cookie cookie)
313 throw new ArgumentNullException ("cookie");
315 Cookies.Add (cookie);
318 public void AppendHeader (string name, string value)
321 throw new ArgumentNullException ("name");
324 throw new ArgumentException ("'name' cannot be empty", "name");
326 if (value.Length > 65535)
327 throw new ArgumentOutOfRangeException ("value");
329 headers.Add (name, value);
332 void Close (bool force)
334 // TODO: use the 'force' argument
335 context.Connection.Close ();
347 public void Close (byte [] responseEntity, bool willBlock)
352 if (responseEntity == null)
353 throw new ArgumentNullException ("responseEntity");
355 //TODO: if willBlock -> BeginWrite + Close ?
356 ContentLength64 = responseEntity.Length;
357 OutputStream.Write (responseEntity, 0, (int) content_length);
361 public void CopyFrom (HttpListenerResponse templateResponse)
364 headers.Add (templateResponse.headers);
365 content_length = templateResponse.content_length;
366 status_code = templateResponse.status_code;
367 status_description = templateResponse.status_description;
368 keep_alive = templateResponse.keep_alive;
369 version = templateResponse.version;
372 public void Redirect (string url)
374 StatusCode = 302; // Found
378 bool FindCookie (Cookie cookie)
380 string name = cookie.Name;
381 string domain = cookie.Domain;
382 string path = cookie.Path;
383 foreach (Cookie c in cookies) {
386 if (domain != c.Domain)
395 internal void SendHeaders (bool closing, MemoryStream ms)
397 //TODO: When do we send KeepAlive?
398 Encoding encoding = content_encoding;
399 if (encoding == null)
400 encoding = Encoding.Default;
402 if (content_type != null) {
403 if (content_encoding != null && content_type.IndexOf ("charset=") == -1) {
404 string enc_name = content_encoding.WebName;
405 headers.SetInternal ("Content-Type", content_type + "; charset=" + enc_name);
407 headers.SetInternal ("Content-Type", content_type);
411 if (headers ["Server"] == null)
412 headers.SetInternal ("Server", "Mono-HTTPAPI/1.0");
414 CultureInfo inv = CultureInfo.InvariantCulture;
415 if (headers ["Date"] == null)
416 headers.SetInternal ("Date", DateTime.UtcNow.ToString ("r", inv));
419 if (!cl_set && closing) {
425 headers.SetInternal ("Content-Length", content_length.ToString (inv));
428 Version v = context.Request.ProtocolVersion;
429 if (!cl_set && !chunked && v >= HttpVersion.Version11)
432 /* Apache forces closing the connection for these status codes:
433 * HttpStatusCode.BadRequest 400
434 * HttpStatusCode.RequestTimeout 408
435 * HttpStatusCode.LengthRequired 411
436 * HttpStatusCode.RequestEntityTooLarge 413
437 * HttpStatusCode.RequestUriTooLong 414
438 * HttpStatusCode.InternalServerError 500
439 * HttpStatusCode.ServiceUnavailable 503
441 bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
442 status_code == 413 || status_code == 414 || status_code == 500 ||
445 if (conn_close == false) {
446 conn_close = (context.Request.Headers ["connection"] == "close");
447 conn_close |= (v <= HttpVersion.Version10);
450 // They sent both KeepAlive: true and Connection: close!?
451 if (!keep_alive || conn_close)
452 headers.SetInternal ("Connection", "close");
455 headers.SetInternal ("Transfer-Encoding", "chunked");
457 int chunked_uses = context.Connection.ChunkedUses;
458 if (chunked_uses >= 100) {
459 force_close_chunked = true;
461 headers.SetInternal ("Connection", "close");
464 if (location != null)
465 headers.SetInternal ("Location", location);
467 if (cookies != null) {
468 bool firstDone = false;
469 StringBuilder cookieSB = new StringBuilder ();
470 foreach (Cookie cookie in cookies) {
472 cookieSB.Append (",");
474 cookieSB.Append (cookie.ToClientString ());
476 headers.SetInternal("Set-Cookie2", cookieSB.ToString ());
479 StreamWriter writer = new StreamWriter (ms, encoding);
480 writer.Write ("HTTP/{0} {1} {2}\r\n", version, status_code, status_description);
481 string headers_str = headers.ToString ();
482 writer.Write (headers_str);
484 int preamble = encoding.GetPreamble ().Length;
485 if (output_stream == null)
486 output_stream = context.Connection.GetResponseStream ();
488 /* Assumes that the ms was at position 0 */
489 ms.Position = preamble;
493 public void SetCookie (Cookie cookie)
496 throw new ArgumentNullException ("cookie");
498 if (cookies != null) {
499 if (FindCookie (cookie))
500 throw new ArgumentException ("The cookie already exists.");
502 cookies = new CookieCollection ();
505 cookies.Add (cookie);