#region License /* * HttpListenerResponse.cs * * This code is derived from HttpListenerResponse.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) * Copyright (c) 2012-2015 sta.blockhead * * 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. */ #endregion #region Authors /* * Authors: * - Gonzalo Paniagua Javier */ #endregion using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; namespace WebSocketSharp.Net { /// /// Provides the access to a response to a request received by the . /// /// /// The HttpListenerResponse class cannot be inherited. /// public sealed class HttpListenerResponse : IDisposable { #region Private Fields private bool _chunked; private Encoding _contentEncoding; private long _contentLength; private bool _contentLengthWasSet; private string _contentType; private HttpListenerContext _context; private CookieCollection _cookies; private bool _disposed; private bool _forceCloseChunked; private WebHeaderCollection _headers; private bool _headersWereSent; private bool _keepAlive; private string _location; private ResponseStream _outputStream; private int _statusCode; private string _statusDescription; private Version _version; #endregion #region Internal Constructors internal HttpListenerResponse (HttpListenerContext context) { _context = context; _headers = new WebHeaderCollection (); _keepAlive = true; _statusCode = 200; _statusDescription = "OK"; _version = HttpVersion.Version11; } #endregion #region Internal Properties internal bool CloseConnection { get { return _headers["Connection"] == "close"; } } internal bool ForceCloseChunked { get { return _forceCloseChunked; } } internal bool HeadersSent { get { return _headersWereSent; } } #endregion #region Public Properties /// /// Gets or sets the encoding for the entity body data included in the response. /// /// /// A that represents the encoding for the entity body data, /// or if no encoding is specified. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public Encoding ContentEncoding { get { return _contentEncoding; } set { checkDisposedOrHeadersSent (); _contentEncoding = value; } } /// /// Gets or sets the size of the entity body data included in the response. /// /// /// A that represents the value of the Content-Length entity-header. /// The value is a number of bytes in the entity body data. /// /// /// The value specified for a set operation is less than zero. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public long ContentLength64 { get { return _contentLength; } set { checkDisposedOrHeadersSent (); if (value < 0) throw new ArgumentOutOfRangeException ("Less than zero.", "value"); _contentLengthWasSet = true; _contentLength = value; } } /// /// Gets or sets the media type of the entity body included in the response. /// /// /// A that represents the value of the Content-Type entity-header. /// /// /// The value specified for a set operation is empty. /// /// /// The value specified for a set operation is . /// /// /// The response has already been sent. /// /// /// This object is closed. /// public string ContentType { get { return _contentType; } set { checkDisposedOrHeadersSent (); if (value == null) throw new ArgumentNullException ("value"); if (value.Length == 0) throw new ArgumentException ("An empty string.", "value"); _contentType = value; } } /// /// Gets or sets the cookies sent with the response. /// /// /// A that contains the cookies sent with the response. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public CookieCollection Cookies { get { return _cookies ?? (_cookies = new CookieCollection ()); } set { checkDisposedOrHeadersSent (); _cookies = value; } } /// /// Gets or sets the HTTP headers sent to the client. /// /// /// A that contains the headers sent to the client. /// /// /// The value specified for a set operation is . /// /// /// The response has already been sent. /// /// /// This object is closed. /// 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 ContentLength64 or KeepAlive properties to set * these headers. You cannot set the Transfer-Encoding or WWW-Authenticate * headers manually." */ // TODO: Check if this is marked readonly after the headers are sent. checkDisposedOrHeadersSent (); if (value == null) throw new ArgumentNullException ("value"); _headers = value; } } /// /// Gets or sets a value indicating whether the server requests a persistent connection. /// /// /// true if the server requests a persistent connection; otherwise, false. /// The default value is true. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public bool KeepAlive { get { return _keepAlive; } set { checkDisposedOrHeadersSent (); _keepAlive = value; } } /// /// Gets a to use to write the entity body data. /// /// /// A to use to write the entity body data. /// /// /// This object is closed. /// public Stream OutputStream { get { checkDisposed (); return _outputStream ?? (_outputStream = _context.Connection.GetResponseStream ()); } } /// /// Gets or sets the HTTP version used in the response. /// /// /// A that represents the version used in the response. /// /// /// The value specified for a set operation doesn't have its Major property set to 1 or /// doesn't have its Minor property set to either 0 or 1. /// /// /// The value specified for a set operation is . /// /// /// The response has already been sent. /// /// /// This object is closed. /// public Version ProtocolVersion { get { return _version; } set { checkDisposedOrHeadersSent (); if (value == null) throw new ArgumentNullException ("value"); if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) throw new ArgumentException ("Not 1.0 or 1.1.", "value"); _version = value; } } /// /// Gets or sets the URL to which the client is redirected to locate a requested resource. /// /// /// A that represents the value of the Location response-header. /// /// /// The value specified for a set operation is empty. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public string RedirectLocation { get { return _location; } set { checkDisposedOrHeadersSent (); if (value.Length == 0) throw new ArgumentException ("An empty string.", "value"); _location = value; } } /// /// Gets or sets a value indicating whether the response uses the chunked transfer encoding. /// /// /// true if the response uses the chunked transfer encoding; otherwise, false. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public bool SendChunked { get { return _chunked; } set { checkDisposedOrHeadersSent (); _chunked = value; } } /// /// Gets or sets the HTTP status code returned to the client. /// /// /// An that represents the status code for the response to the request. /// The default value is . /// /// /// The response has already been sent. /// /// /// The value specified for a set operation is invalid. Valid values are between 100 and 999. /// /// /// This object is closed. /// public int StatusCode { get { return _statusCode; } set { checkDisposedOrHeadersSent (); if (value < 100 || value > 999) throw new System.Net.ProtocolViolationException ("A value isn't between 100 and 999."); _statusCode = value; _statusDescription = value.GetStatusDescription (); } } /// /// Gets or sets the description of the HTTP status code returned to the client. /// /// /// A that represents the description of the status code. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public string StatusDescription { get { return _statusDescription; } set { checkDisposedOrHeadersSent (); _statusDescription = value != null && value.Length > 0 ? value : _statusCode.GetStatusDescription (); } } #endregion #region Private Methods private bool canAddOrUpdate (Cookie cookie) { if (_cookies == null || _cookies.Count == 0) return true; var found = findCookie (cookie).ToList (); if (found.Count == 0) return true; var ver = cookie.Version; foreach (var c in found) if (c.Version == ver) return true; return false; } private void checkDisposed () { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); } private void checkDisposedOrHeadersSent () { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (_headersWereSent) throw new InvalidOperationException ("Cannot be changed after the headers are sent."); } private void close (bool force) { _disposed = true; _context.Connection.Close (force); } private IEnumerable findCookie (Cookie cookie) { var name = cookie.Name; var domain = cookie.Domain; var path = cookie.Path; if (_cookies != null) foreach (Cookie c in _cookies) if (c.Name.Equals (name, StringComparison.OrdinalIgnoreCase) && c.Domain.Equals (domain, StringComparison.OrdinalIgnoreCase) && c.Path.Equals (path, StringComparison.Ordinal)) yield return c; } #endregion #region Internal Methods internal void SendHeaders (MemoryStream stream, bool closing) { if (_contentType != null) { var type = _contentType.IndexOf ("charset=", StringComparison.Ordinal) == -1 && _contentEncoding != null ? String.Format ("{0}; charset={1}", _contentType, _contentEncoding.WebName) : _contentType; _headers.InternalSet ("Content-Type", type, true); } if (_headers["Server"] == null) _headers.InternalSet ("Server", "websocket-sharp/1.0", true); var prov = CultureInfo.InvariantCulture; if (_headers["Date"] == null) _headers.InternalSet ("Date", DateTime.UtcNow.ToString ("r", prov), true); if (!_chunked) { if (!_contentLengthWasSet && closing) { _contentLengthWasSet = true; _contentLength = 0; } if (_contentLengthWasSet) _headers.InternalSet ("Content-Length", _contentLength.ToString (prov), true); } var ver = _context.Request.ProtocolVersion; if (!_contentLengthWasSet && !_chunked && ver > HttpVersion.Version10) _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 */ var closeConn = _statusCode == 400 || _statusCode == 408 || _statusCode == 411 || _statusCode == 413 || _statusCode == 414 || _statusCode == 500 || _statusCode == 503; if (!closeConn) closeConn = !_context.Request.KeepAlive; // They sent both KeepAlive: true and Connection: close!? if (!_keepAlive || closeConn) { _headers.InternalSet ("Connection", "close", true); closeConn = true; } if (_chunked) _headers.InternalSet ("Transfer-Encoding", "chunked", true); var reuses = _context.Connection.Reuses; if (reuses >= 100) { _forceCloseChunked = true; if (!closeConn) { _headers.InternalSet ("Connection", "close", true); closeConn = true; } } if (!closeConn) { _headers.InternalSet ( "Keep-Alive", String.Format ("timeout=15,max={0}", 100 - reuses), true); if (ver < HttpVersion.Version11) _headers.InternalSet ("Connection", "keep-alive", true); } if (_location != null) _headers.InternalSet ("Location", _location, true); if (_cookies != null) foreach (Cookie cookie in _cookies) _headers.InternalSet ("Set-Cookie", cookie.ToResponseString (), true); var enc = _contentEncoding ?? Encoding.Default; var writer = new StreamWriter (stream, enc, 256); writer.Write ("HTTP/{0} {1} {2}\r\n", _version, _statusCode, _statusDescription); writer.Write (_headers.ToStringMultiValue (true)); writer.Flush (); // Assumes that the stream was at position 0. stream.Position = enc.CodePage == 65001 ? 3 : enc.GetPreamble ().Length; if (_outputStream == null) _outputStream = _context.Connection.GetResponseStream (); _headersWereSent = true; } #endregion #region Public Methods /// /// Closes the connection to the client without returning a response. /// public void Abort () { if (_disposed) return; close (true); } /// /// Adds an HTTP header with the specified and /// to the headers for the response. /// /// /// A that represents the name of the header to add. /// /// /// A that represents the value of the header to add. /// /// /// /// or contains invalid characters. /// /// /// -or- /// /// /// is a restricted header name. /// /// /// /// is or empty. /// /// /// The length of is greater than 65,535 characters. /// /// /// /// The response has already been sent. /// /// /// -or- /// /// /// The header cannot be allowed to add to the current headers. /// /// /// /// This object is closed. /// public void AddHeader (string name, string value) { checkDisposedOrHeadersSent (); _headers.Set (name, value); } /// /// Appends the specified to the cookies sent with the response. /// /// /// A to append. /// /// /// is . /// /// /// The response has already been sent. /// /// /// This object is closed. /// public void AppendCookie (Cookie cookie) { checkDisposedOrHeadersSent (); Cookies.Add (cookie); } /// /// Appends a to the specified HTTP header sent with the response. /// /// /// A that represents the name of the header to append /// to. /// /// /// A that represents the value to append to the header. /// /// /// /// or contains invalid characters. /// /// /// -or- /// /// /// is a restricted header name. /// /// /// /// is or empty. /// /// /// The length of is greater than 65,535 characters. /// /// /// /// The response has already been sent. /// /// /// -or- /// /// /// The current headers cannot allow the header to append a value. /// /// /// /// This object is closed. /// public void AppendHeader (string name, string value) { checkDisposedOrHeadersSent (); _headers.Add (name, value); } /// /// Returns the response to the client and releases the resources used by /// this instance. /// public void Close () { if (_disposed) return; close (false); } /// /// Returns the response with the specified array of to the client and /// releases the resources used by this instance. /// /// /// An array of that contains the response entity body data. /// /// /// true if this method blocks execution while flushing the stream to the client; /// otherwise, false. /// /// /// is . /// /// /// The response has already been sent. /// /// /// This object is closed. /// public void Close (byte[] responseEntity, bool willBlock) { if (responseEntity == null) throw new ArgumentNullException ("responseEntity"); var len = responseEntity.Length; ContentLength64 = len; var output = OutputStream; if (willBlock) { output.Write (responseEntity, 0, len); close (false); return; } output.BeginWrite ( responseEntity, 0, len, ar => { output.EndWrite (ar); close (false); }, null); } /// /// Copies properties from the specified to this response. /// /// /// A to copy. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public void CopyFrom (HttpListenerResponse templateResponse) { checkDisposedOrHeadersSent (); _headers.Clear (); _headers.Add (templateResponse._headers); _contentLength = templateResponse._contentLength; _statusCode = templateResponse._statusCode; _statusDescription = templateResponse._statusDescription; _keepAlive = templateResponse._keepAlive; _version = templateResponse._version; } /// /// Configures the response to redirect the client's request to the specified /// . /// /// /// A that represents the URL to redirect the client's request to. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public void Redirect (string url) { StatusCode = (int) HttpStatusCode.Redirect; _location = url; } /// /// Adds or updates a in the cookies sent with the response. /// /// /// A to set. /// /// /// already exists in the cookies and couldn't be replaced. /// /// /// is . /// /// /// The response has already been sent. /// /// /// This object is closed. /// public void SetCookie (Cookie cookie) { checkDisposedOrHeadersSent (); if (cookie == null) throw new ArgumentNullException ("cookie"); if (!canAddOrUpdate (cookie)) throw new ArgumentException ("Cannot be replaced.", "cookie"); Cookies.Add (cookie); } #endregion #region Explicit Interface Implementations /// /// Releases all resources used by the . /// void IDisposable.Dispose () { if (_disposed) return; close (true); // Same as the Abort method. } #endregion } }