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;
33 namespace System.Net {
34 public sealed class HttpListenerResponse : IDisposable
37 Encoding content_encoding;
41 CookieCollection cookies;
42 WebHeaderCollection headers = new WebHeaderCollection ();
43 bool keep_alive = true;
44 ResponseStream output_stream;
45 Version version = HttpVersion.Version11;
47 int status_code = 200;
48 string status_description = "OK";
50 HttpListenerContext context;
52 internal bool HeadersSent;
53 internal object headers_lock = new object ();
55 bool force_close_chunked;
57 internal HttpListenerResponse (HttpListenerContext context)
59 this.context = context;
62 internal bool ForceCloseChunked {
63 get { return force_close_chunked; }
66 public Encoding ContentEncoding {
68 if (content_encoding == null)
69 content_encoding = Encoding.Default;
70 return content_encoding;
74 throw new ObjectDisposedException (GetType ().ToString ());
78 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
80 content_encoding = value;
84 public long ContentLength64 {
85 get { return content_length; }
88 throw new ObjectDisposedException (GetType ().ToString ());
91 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
94 throw new ArgumentOutOfRangeException ("Must be >= 0", "value");
97 content_length = value;
101 public string ContentType {
102 get { return content_type; }
106 throw new ObjectDisposedException (GetType ().ToString ());
109 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
111 content_type = value;
115 // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html
116 public CookieCollection Cookies {
119 cookies = new CookieCollection ();
122 set { cookies = value; } // null allowed?
125 public WebHeaderCollection Headers {
126 get { return headers; }
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."
134 // TODO: check if this is marked readonly after headers are sent.
139 public bool KeepAlive {
140 get { return keep_alive; }
143 throw new ObjectDisposedException (GetType ().ToString ());
146 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
152 public Stream OutputStream {
154 if (output_stream == null)
155 output_stream = context.Connection.GetResponseStream ();
156 return output_stream;
160 public Version ProtocolVersion {
161 get { return version; }
164 throw new ObjectDisposedException (GetType ().ToString ());
167 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
170 throw new ArgumentNullException ("value");
172 if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
173 throw new ArgumentException ("Must be 1.0 or 1.1", "value");
176 throw new ObjectDisposedException (GetType ().ToString ());
182 public string RedirectLocation {
183 get { return location; }
186 throw new ObjectDisposedException (GetType ().ToString ());
189 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
195 public bool SendChunked {
196 get { return chunked; }
199 throw new ObjectDisposedException (GetType ().ToString ());
202 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
208 public int StatusCode {
209 get { return status_code; }
212 throw new ObjectDisposedException (GetType ().ToString ());
215 throw new InvalidOperationException ("Cannot be changed after headers are sent.");
217 if (value < 100 || value > 999)
218 throw new ProtocolViolationException ("StatusCode must be between 100 and 999.");
220 status_description = HttpListenerResponseHelper.GetStatusDescription (value);
224 public string StatusDescription {
225 get { return status_description; }
227 status_description = value;
231 void IDisposable.Dispose ()
233 Close (true); //TODO: Abort or Close?
244 public void AddHeader (string name, string value)
247 throw new ArgumentNullException ("name");
250 throw new ArgumentException ("'name' cannot be empty", "name");
252 //TODO: check for forbidden headers and invalid characters
253 if (value.Length > 65535)
254 throw new ArgumentOutOfRangeException ("value");
256 headers.Set (name, value);
259 public void AppendCookie (Cookie cookie)
262 throw new ArgumentNullException ("cookie");
264 Cookies.Add (cookie);
267 public void AppendHeader (string name, string value)
270 throw new ArgumentNullException ("name");
273 throw new ArgumentException ("'name' cannot be empty", "name");
275 if (value.Length > 65535)
276 throw new ArgumentOutOfRangeException ("value");
278 headers.Add (name, value);
281 void Close (bool force)
284 context.Connection.Close (force);
295 public void Close (byte [] responseEntity, bool willBlock)
300 if (responseEntity == null)
301 throw new ArgumentNullException ("responseEntity");
303 //TODO: if willBlock -> BeginWrite + Close ?
304 ContentLength64 = responseEntity.Length;
305 OutputStream.Write (responseEntity, 0, (int) content_length);
309 public void CopyFrom (HttpListenerResponse templateResponse)
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;
320 public void Redirect (string url)
322 StatusCode = 302; // Found
326 bool FindCookie (Cookie cookie)
328 string name = cookie.Name;
329 string domain = cookie.Domain;
330 string path = cookie.Path;
331 foreach (Cookie c in cookies) {
334 if (domain != c.Domain)
343 internal void SendHeaders (bool closing, MemoryStream ms)
345 Encoding encoding = content_encoding;
346 if (encoding == null)
347 encoding = Encoding.Default;
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);
354 headers.SetInternal ("Content-Type", content_type);
358 if (headers ["Server"] == null)
359 headers.SetInternal ("Server", "Mono-HTTPAPI/1.0");
361 CultureInfo inv = CultureInfo.InvariantCulture;
362 if (headers ["Date"] == null)
363 headers.SetInternal ("Date", DateTime.UtcNow.ToString ("r", inv));
366 if (!cl_set && closing) {
372 headers.SetInternal ("Content-Length", content_length.ToString (inv));
375 Version v = context.Request.ProtocolVersion;
376 if (!cl_set && !chunked && v >= HttpVersion.Version11)
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
388 bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
389 status_code == 413 || status_code == 414 || status_code == 500 ||
392 if (conn_close == false)
393 conn_close = !context.Request.KeepAlive;
395 // They sent both KeepAlive: true and Connection: close!?
396 if (!keep_alive || conn_close) {
397 headers.SetInternal ("Connection", "close");
402 headers.SetInternal ("Transfer-Encoding", "chunked");
404 int reuses = context.Connection.Reuses;
406 force_close_chunked = true;
408 headers.SetInternal ("Connection", "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");
419 if (location != null)
420 headers.SetInternal ("Location", location);
422 if (cookies != null) {
423 foreach (Cookie cookie in cookies)
424 headers.SetInternal ("Set-Cookie", CookieToClientString (cookie));
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);
432 int preamble = encoding.GetPreamble ().Length;
433 if (output_stream == null)
434 output_stream = context.Connection.GetResponseStream ();
436 /* Assumes that the ms was at position 0 */
437 ms.Position = preamble;
441 static string FormatHeaders (WebHeaderCollection headers)
443 var sb = new StringBuilder();
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");
452 sb.Append (key).Append (": ").Append (headers.Get (i)).Append ("\r\n");
456 return sb.Append("\r\n").ToString();
459 static string CookieToClientString (Cookie cookie)
461 if (cookie.Name.Length == 0)
464 StringBuilder result = new StringBuilder (64);
466 if (cookie.Version > 0)
467 result.Append ("Version=").Append (cookie.Version).Append (";");
469 result.Append (cookie.Name).Append ("=").Append (cookie.Value);
471 if (cookie.Path != null && cookie.Path.Length != 0)
472 result.Append (";Path=").Append (QuotedString (cookie, cookie.Path));
474 if (cookie.Domain != null && cookie.Domain.Length != 0)
475 result.Append (";Domain=").Append (QuotedString (cookie, cookie.Domain));
477 if (cookie.Port != null && cookie.Port.Length != 0)
478 result.Append (";Port=").Append (cookie.Port);
480 return result.ToString ();
483 static string QuotedString (Cookie cookie, string value)
485 if (cookie.Version == 0 || IsToken (value))
488 return "\"" + value.Replace("\"", "\\\"") + "\"";
491 static string tspecials = "()<>@,;:\\\"/[]?={} \t"; // from RFC 2965, 2068
493 static bool IsToken (string value)
495 int len = value.Length;
496 for (int i = 0; i < len; i++) {
498 if (c < 0x20 || c >= 0x7f || tspecials.IndexOf (c) != -1)
504 public void SetCookie (Cookie cookie)
507 throw new ArgumentNullException ("cookie");
509 if (cookies != null) {
510 if (FindCookie (cookie))
511 throw new ArgumentException ("The cookie already exists.");
513 cookies = new CookieCollection ();
516 cookies.Add (cookie);