| // | 
| // HttpListenerResponse.cs | 
| //    Copied from System.Net.HttpListenerResponse.cs | 
| // | 
| // Author: | 
| //    Gonzalo Paniagua Javier (gonzalo@novell.com) | 
| // | 
| // Copyright (c) 2005 Novell, Inc. (http://www.novell.com) | 
| // Copyright (c) 2012 sta.blockhead (sta.blockhead@gmail.com) | 
| // | 
| // Permission is hereby granted, free of charge, to any person obtaining | 
| // a copy of this software and associated documentation files (the | 
| // "Software"), to deal in the Software without restriction, including | 
| // without limitation the rights to use, copy, modify, merge, publish, | 
| // distribute, sublicense, and/or sell copies of the Software, and to | 
| // permit persons to whom the Software is furnished to do so, subject to | 
| // the following conditions: | 
| //  | 
| // The above copyright notice and this permission notice shall be | 
| // included in all copies or substantial portions of the Software. | 
| //  | 
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | 
| // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | 
| // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | 
| // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | 
| // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | 
| // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | 
| // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | 
| // | 
|   | 
| using System; | 
| using System.Globalization; | 
| using System.IO; | 
| using System.Net; | 
| using System.Text; | 
|   | 
| namespace WebSocketSharp.Net | 
| { | 
|     public sealed class HttpListenerResponse : IDisposable | 
|     { | 
|         #region Private Fields | 
|   | 
|         bool chunked; | 
|         bool cl_set; | 
|         Encoding content_encoding; | 
|         long content_length; | 
|         string content_type; | 
|         HttpListenerContext context; | 
|         CookieCollection cookies; | 
|         bool disposed; | 
|         bool force_close_chunked; | 
|         WebHeaderCollection headers; | 
|         bool keep_alive; | 
|         string location; | 
|         ResponseStream output_stream; | 
|         int status_code; | 
|         string status_description; | 
|         Version version; | 
|   | 
|         #endregion | 
|   | 
|         #region Internal Fields | 
|   | 
|         internal bool HeadersSent; | 
|   | 
|         #endregion | 
|   | 
|         #region Constructor | 
|   | 
|         internal HttpListenerResponse(HttpListenerContext context) | 
|         { | 
|             this.context = context; | 
|             Init(); | 
|         } | 
|   | 
|         #endregion | 
|   | 
|         #region Internal Property | 
|   | 
|         internal bool ForceCloseChunked | 
|         { | 
|             get { return force_close_chunked; } | 
|         } | 
|   | 
|         #endregion | 
|   | 
|         #region Public Properties | 
|   | 
|         public Encoding ContentEncoding | 
|         { | 
|             get | 
|             { | 
|                 if (content_encoding == null) | 
|                     content_encoding = Encoding.Default; | 
|                 return content_encoding; | 
|             } | 
|             set | 
|             { | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 // TODO: is null ok? | 
|                 if (HeadersSent) | 
|                     throw new InvalidOperationException("Cannot be changed after headers are sent."); | 
|   | 
|                 content_encoding = value; | 
|             } | 
|         } | 
|   | 
|         public long ContentLength64 | 
|         { | 
|             get { return content_length; } | 
|             set | 
|             { | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 if (HeadersSent) | 
|                     throw new InvalidOperationException("Cannot be changed after headers are sent."); | 
|   | 
|                 if (value < 0) | 
|                     throw new ArgumentOutOfRangeException("Must be >= 0", "value"); | 
|   | 
|                 cl_set = true; | 
|                 content_length = value; | 
|             } | 
|         } | 
|   | 
|         public string ContentType | 
|         { | 
|             get { return content_type; } | 
|             set | 
|             { | 
|                 // TODO: is null ok? | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 if (HeadersSent) | 
|                     throw new InvalidOperationException("Cannot be changed after headers are sent."); | 
|   | 
|                 content_type = value; | 
|             } | 
|         } | 
|   | 
|         // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html | 
|         public CookieCollection Cookies | 
|         { | 
|             get | 
|             { | 
|                 if (cookies == null) | 
|                     cookies = new CookieCollection(); | 
|                 return cookies; | 
|             } | 
|             set { cookies = value; } // null allowed? | 
|         } | 
|   | 
|         public WebHeaderCollection Headers | 
|         { | 
|             get { return headers; } | 
|             set | 
|             { | 
|                 /** | 
|                  *    "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or | 
|                  *    WWW-Authenticate header using the Headers property, an exception will be | 
|                  *    thrown. Use the KeepAlive or ContentLength64 properties to set these headers. | 
|                  *    You cannot set the Transfer-Encoding or WWW-Authenticate headers manually." | 
|                 */ | 
|                 // TODO: check if this is marked readonly after headers are sent. | 
|                 headers = value; | 
|             } | 
|         } | 
|   | 
|         public bool KeepAlive | 
|         { | 
|             get { return keep_alive; } | 
|             set | 
|             { | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 if (HeadersSent) | 
|                     throw new InvalidOperationException("Cannot be changed after headers are sent."); | 
|   | 
|                 keep_alive = value; | 
|             } | 
|         } | 
|   | 
|         public Stream OutputStream | 
|         { | 
|             get | 
|             { | 
|                 if (output_stream == null) | 
|                     output_stream = context.Connection.GetResponseStream(); | 
|                 return output_stream; | 
|             } | 
|         } | 
|   | 
|         public Version ProtocolVersion | 
|         { | 
|             get { return version; } | 
|             set | 
|             { | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 if (HeadersSent) | 
|                     throw new InvalidOperationException("Cannot be changed after headers are sent."); | 
|   | 
|                 if (value == null) | 
|                     throw new ArgumentNullException("value"); | 
|   | 
|                 if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) | 
|                     throw new ArgumentException("Must be 1.0 or 1.1", "value"); | 
|   | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 version = value; | 
|             } | 
|         } | 
|   | 
|         public string RedirectLocation | 
|         { | 
|             get { return location; } | 
|             set | 
|             { | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 if (HeadersSent) | 
|                     throw new InvalidOperationException("Cannot be changed after headers are sent."); | 
|   | 
|                 location = value; | 
|             } | 
|         } | 
|   | 
|         public bool SendChunked | 
|         { | 
|             get { return chunked; } | 
|             set | 
|             { | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 if (HeadersSent) | 
|                     throw new InvalidOperationException("Cannot be changed after headers are sent."); | 
|   | 
|                 chunked = value; | 
|             } | 
|         } | 
|   | 
|         public int StatusCode | 
|         { | 
|             get { return status_code; } | 
|             set | 
|             { | 
|                 if (disposed) | 
|                     throw new ObjectDisposedException(GetType().ToString()); | 
|   | 
|                 if (HeadersSent) | 
|                     throw new InvalidOperationException("Cannot be changed after headers are sent."); | 
|   | 
|                 if (value < 100 || value > 999) | 
|                     throw new ProtocolViolationException("StatusCode must be between 100 and 999."); | 
|   | 
|                 status_code = value; | 
|                 status_description = Ext.GetStatusDescription(value); | 
|             } | 
|         } | 
|   | 
|         public string StatusDescription | 
|         { | 
|             get { return status_description; } | 
|             set | 
|             { | 
|                 status_description = value; | 
|             } | 
|         } | 
|   | 
|         #endregion | 
|   | 
|         #region Private Methods | 
|   | 
|         void Close(bool force) | 
|         { | 
|             disposed = true; | 
|             context.Connection.Close(force); | 
|         } | 
|   | 
|         void IDisposable.Dispose() | 
|         { | 
|             Close(true); // TODO: Abort or Close? | 
|         } | 
|   | 
|         bool FindCookie(Cookie cookie) | 
|         { | 
|             string name = cookie.Name; | 
|             string domain = cookie.Domain; | 
|             string path = cookie.Path; | 
|             foreach (Cookie c in cookies) | 
|             { | 
|                 if (name != c.Name) | 
|                     continue; | 
|                 if (domain != c.Domain) | 
|                     continue; | 
|                 if (path == c.Path) | 
|                     return true; | 
|             } | 
|   | 
|             return false; | 
|         } | 
|   | 
|         void Init() | 
|         { | 
|             headers = new WebHeaderCollection(); | 
|             keep_alive = true; | 
|             status_code = 200; | 
|             status_description = "OK"; | 
|             version = HttpVersion.Version11; | 
|         } | 
|   | 
|         #endregion | 
|   | 
|         #region Internal Method | 
|   | 
|         internal void SendHeaders(bool closing, MemoryStream ms) | 
|         { | 
|             Encoding encoding = content_encoding; | 
|             if (encoding == null) | 
|                 encoding = Encoding.Default; | 
|   | 
|             if (content_type != null) | 
|             { | 
|                 if (content_encoding != null && content_type.IndexOf("charset=", StringComparison.Ordinal) == -1) | 
|                 { | 
|                     string enc_name = content_encoding.WebName; | 
|                     headers.SetInternal("Content-Type", content_type + "; charset=" + enc_name); | 
|                 } | 
|                 else | 
|                 { | 
|                     headers.SetInternal("Content-Type", content_type); | 
|                 } | 
|             } | 
|   | 
|             if (headers["Server"] == null) | 
|                 headers.SetInternal("Server", "Mono-HTTPAPI/1.0"); | 
|   | 
|             CultureInfo inv = CultureInfo.InvariantCulture; | 
|             if (headers["Date"] == null) | 
|                 headers.SetInternal("Date", DateTime.UtcNow.ToString("r", inv)); | 
|   | 
|             if (!chunked) | 
|             { | 
|                 if (!cl_set && closing) | 
|                 { | 
|                     cl_set = true; | 
|                     content_length = 0; | 
|                 } | 
|   | 
|                 if (cl_set) | 
|                     headers.SetInternal("Content-Length", content_length.ToString(inv)); | 
|             } | 
|   | 
|             Version v = context.Request.ProtocolVersion; | 
|             if (!cl_set && !chunked && v >= HttpVersion.Version11) | 
|                 chunked = true; | 
|   | 
|             /* Apache forces closing the connection for these status codes: | 
|              *    HttpStatusCode.BadRequest                 400 | 
|              *    HttpStatusCode.RequestTimeout             408 | 
|              *    HttpStatusCode.LengthRequired             411 | 
|              *    HttpStatusCode.RequestEntityTooLarge     413 | 
|              *    HttpStatusCode.RequestUriTooLong         414 | 
|              *    HttpStatusCode.InternalServerError         500 | 
|              *    HttpStatusCode.ServiceUnavailable         503 | 
|              */ | 
|             bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || | 
|                     status_code == 413 || status_code == 414 || status_code == 500 || | 
|                     status_code == 503); | 
|   | 
|             if (conn_close == false) | 
|                 conn_close = !context.Request.KeepAlive; | 
|   | 
|             // They sent both KeepAlive: true and Connection: close!? | 
|             if (!keep_alive || conn_close) | 
|             { | 
|                 headers.SetInternal("Connection", "close"); | 
|                 conn_close = true; | 
|             } | 
|   | 
|             if (chunked) | 
|                 headers.SetInternal("Transfer-Encoding", "chunked"); | 
|   | 
|             int reuses = context.Connection.Reuses; | 
|             if (reuses >= 100) | 
|             { | 
|                 force_close_chunked = true; | 
|                 if (!conn_close) | 
|                 { | 
|                     headers.SetInternal("Connection", "close"); | 
|                     conn_close = true; | 
|                 } | 
|             } | 
|   | 
|             if (!conn_close) | 
|             { | 
|                 headers.SetInternal("Keep-Alive", String.Format("timeout=15,max={0}", 100 - reuses)); | 
|                 if (context.Request.ProtocolVersion <= HttpVersion.Version10) | 
|                     headers.SetInternal("Connection", "keep-alive"); | 
|             } | 
|   | 
|             if (location != null) | 
|                 headers.SetInternal("Location", location); | 
|   | 
|             if (cookies != null) | 
|             { | 
|                 foreach (Cookie cookie in cookies) | 
|                     headers.SetInternal("Set-Cookie", cookie.ToClientString()); | 
|             } | 
|   | 
|             StreamWriter writer = new StreamWriter(ms, encoding, 256); | 
|             writer.Write("HTTP/{0} {1} {2}\r\n", version, status_code, status_description); | 
|             string headers_str = headers.ToStringMultiValue(); | 
|             writer.Write(headers_str); | 
|             writer.Flush(); | 
|             int preamble = (encoding.CodePage == 65001) ? 3 : encoding.GetPreamble().Length; | 
|             if (output_stream == null) | 
|                 output_stream = context.Connection.GetResponseStream(); | 
|   | 
|             /* Assumes that the ms was at position 0 */ | 
|             ms.Position = preamble; | 
|             HeadersSent = true; | 
|         } | 
|   | 
|         #endregion | 
|   | 
|         #region Public Methods | 
|   | 
|         public void Abort() | 
|         { | 
|             if (disposed) | 
|                 return; | 
|   | 
|             Close(true); | 
|         } | 
|   | 
|         public void AddHeader(string name, string value) | 
|         { | 
|             if (name == null) | 
|                 throw new ArgumentNullException("name"); | 
|   | 
|             if (name == "") | 
|                 throw new ArgumentException("'name' cannot be empty", "name"); | 
|   | 
|             // TODO: check for forbidden headers and invalid characters | 
|             if (value.Length > 65535) | 
|                 throw new ArgumentOutOfRangeException("value"); | 
|   | 
|             headers.Set(name, value); | 
|         } | 
|   | 
|         public void AppendCookie(Cookie cookie) | 
|         { | 
|             if (cookie == null) | 
|                 throw new ArgumentNullException("cookie"); | 
|   | 
|             Cookies.Add(cookie); | 
|         } | 
|   | 
|         public void AppendHeader(string name, string value) | 
|         { | 
|             if (name == null) | 
|                 throw new ArgumentNullException("name"); | 
|   | 
|             if (name == "") | 
|                 throw new ArgumentException("'name' cannot be empty", "name"); | 
|   | 
|             if (value.Length > 65535) | 
|                 throw new ArgumentOutOfRangeException("value"); | 
|   | 
|             headers.Add(name, value); | 
|         } | 
|   | 
|         public void Close() | 
|         { | 
|             if (disposed) | 
|                 return; | 
|   | 
|             Close(false); | 
|         } | 
|   | 
|         public void Close(byte[] responseEntity, bool willBlock) | 
|         { | 
|             if (disposed) | 
|                 return; | 
|   | 
|             if (responseEntity == null) | 
|                 throw new ArgumentNullException("responseEntity"); | 
|   | 
|             // TODO: if willBlock -> BeginWrite + Close ? | 
|             ContentLength64 = responseEntity.Length; | 
|             OutputStream.Write(responseEntity, 0, (int)content_length); | 
|             Close(false); | 
|         } | 
|   | 
|         public void CopyFrom(HttpListenerResponse templateResponse) | 
|         { | 
|             headers.Clear(); | 
|             headers.Add(templateResponse.headers); | 
|             content_length = templateResponse.content_length; | 
|             status_code = templateResponse.status_code; | 
|             status_description = templateResponse.status_description; | 
|             keep_alive = templateResponse.keep_alive; | 
|             version = templateResponse.version; | 
|         } | 
|   | 
|         public void Redirect(string url) | 
|         { | 
|             StatusCode = 302; // Found | 
|             location = url; | 
|         } | 
|   | 
|         public void SetCookie(Cookie cookie) | 
|         { | 
|             if (cookie == null) | 
|                 throw new ArgumentNullException("cookie"); | 
|   | 
|             if (cookies != null) | 
|             { | 
|                 if (FindCookie(cookie)) | 
|                     throw new ArgumentException("The cookie already exists."); | 
|             } | 
|             else | 
|             { | 
|                 cookies = new CookieCollection(); | 
|             } | 
|   | 
|             cookies.Add(cookie); | 
|         } | 
|   | 
|         #endregion | 
|     } | 
| } |