#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 #region Contributors /* * Contributors: * - Nicholas Devenish */ #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 _closeConnection; private Encoding _contentEncoding; private long _contentLength; private string _contentType; private HttpListenerContext _context; private CookieCollection _cookies; private bool _disposed; private WebHeaderCollection _headers; private bool _headersSent; private bool _keepAlive; private string _location; private ResponseStream _outputStream; private bool _sendChunked; private int _statusCode; private string _statusDescription; private Version _version; #endregion #region Internal Constructors internal HttpListenerResponse (HttpListenerContext context) { _context = context; _keepAlive = true; _statusCode = 200; _statusDescription = "OK"; _version = HttpVersion.Version11; } #endregion #region Internal Properties internal bool CloseConnection { get { return _closeConnection; } set { _closeConnection = value; } } internal bool HeadersSent { get { return _headersSent; } set { _headersSent = value; } } #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. /// /// /// if no encoding is specified. /// /// /// /// This instance is closed. /// public Encoding ContentEncoding { get { return _contentEncoding; } set { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); _contentEncoding = value; } } /// /// Gets or sets the number of bytes in the entity body data included in /// the response. /// /// /// A that represents the value of the Content-Length /// header. /// /// /// The value specified for a set operation is less than zero. /// /// /// The response is already being sent. /// /// /// This instance is closed. /// public long ContentLength64 { get { return _contentLength; } set { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } if (value < 0) { var msg = "Less than zero."; throw new ArgumentOutOfRangeException (msg, "value"); } _contentLength = value; } } /// /// Gets or sets the media type of the entity body included in the response. /// /// /// /// A that represents the media type of the entity /// body. /// /// /// It is used for the value of the Content-Type entity-header. /// /// /// if no media type is specified. /// /// /// /// The value specified for a set operation is empty. /// /// /// This instance is closed. /// public string ContentType { get { return _contentType; } set { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (value != null && 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. /// public CookieCollection Cookies { get { return _cookies ?? (_cookies = new CookieCollection ()); } set { _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 isn't valid for a response. /// public WebHeaderCollection Headers { get { return _headers ?? (_headers = new WebHeaderCollection (HttpHeaderType.Response, false)); } set { if (value != null && value.State != HttpHeaderType.Response) throw new InvalidOperationException ( "The specified headers aren't valid for a response."); _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 is already being sent. /// /// /// This instance is closed. /// public bool KeepAlive { get { return _keepAlive; } set { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } _keepAlive = value; } } /// /// Gets a stream instance to which the entity body data can be written. /// /// /// A instance to which the entity body data can be /// written. /// /// /// This instance is closed. /// public Stream OutputStream { get { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (_outputStream == null) _outputStream = _context.Connection.GetResponseStream (); return _outputStream; } } /// /// Gets or sets the HTTP version used for the response. /// /// /// A that represents the HTTP version used for /// the response. /// /// /// The value specified for a set operation is . /// /// /// /// The value specified for a set operation does not have its Major /// property set to 1. /// /// /// -or- /// /// /// The value specified for a set operation does not have its Minor /// property set to either 0 or 1. /// /// /// /// The response is already being sent. /// /// /// This instance is closed. /// public Version ProtocolVersion { get { return _version; } set { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } if (value == null) throw new ArgumentNullException ("value"); if (value.Major != 1) { var msg = "Its Major property is not 1."; throw new ArgumentException (msg, "value"); } if (value.Minor < 0 || value.Minor > 1) { var msg = "Its Minor property is not 0 or 1."; throw new ArgumentException (msg, "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. /// /// /// if no redirect location is specified. /// /// /// /// The value specified for a set operation is not an absolute URL. /// /// /// This instance is closed. /// public string RedirectLocation { get { return _location; } set { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (value == null) { _location = null; return; } if (!value.MaybeUri ()) throw new ArgumentException ("Not an absolute URL.", "value"); Uri uri; if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) throw new ArgumentException ("Not an absolute URL.", "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 default value is false. /// /// /// /// The response is already being sent. /// /// /// This instance is closed. /// public bool SendChunked { get { return _sendChunked; } set { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } _sendChunked = 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 same as . /// /// /// The response has already been sent. /// /// /// This object is closed. /// /// /// The value specified for a set operation is invalid. Valid values are /// between 100 and 999 inclusive. /// 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 inclusive."); _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 default /// value is the RFC 2616 /// description for the property value, /// or if an RFC 2616 description doesn't exist. /// /// /// The value specified for a set operation contains invalid characters. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public string StatusDescription { get { return _statusDescription; } set { checkDisposedOrHeadersSent (); if (value == null || value.Length == 0) { _statusDescription = _statusCode.GetStatusDescription (); return; } if (!value.IsText () || value.IndexOfAny (new[] { '\r', '\n' }) > -1) throw new ArgumentException ("Contains invalid characters.", "value"); _statusDescription = value; } } #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 checkDisposedOrHeadersSent () { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (_headersSent) 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 WebHeaderCollection WriteHeadersTo (MemoryStream destination) { var headers = new WebHeaderCollection (HttpHeaderType.Response, true); if (_headers != null) headers.Add (_headers); 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 (!_sendChunked) headers.InternalSet ("Content-Length", _contentLength.ToString (prov), true); else headers.InternalSet ("Transfer-Encoding", "chunked", true); /* * Apache forces closing the connection for these status codes: * - 400 Bad Request * - 408 Request Timeout * - 411 Length Required * - 413 Request Entity Too Large * - 414 Request-Uri Too Long * - 500 Internal Server Error * - 503 Service Unavailable */ var closeConn = !_context.Request.KeepAlive || !_keepAlive || _statusCode == 400 || _statusCode == 408 || _statusCode == 411 || _statusCode == 413 || _statusCode == 414 || _statusCode == 500 || _statusCode == 503; var reuses = _context.Connection.Reuses; if (closeConn || reuses >= 100) { headers.InternalSet ("Connection", "close", true); } else { headers.InternalSet ( "Keep-Alive", String.Format ("timeout=15,max={0}", 100 - reuses), true); if (_context.Request.ProtocolVersion < 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 (destination, enc, 256); writer.Write ("HTTP/{0} {1} {2}\r\n", _version, _statusCode, _statusDescription); writer.Write (headers.ToStringMultiValue (true)); writer.Flush (); // Assumes that the destination was at position 0. destination.Position = enc.GetPreamble ().Length; return headers; } #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. /// /// /// is or empty. /// /// /// /// or contains invalid characters. /// /// /// -or- /// /// /// is a restricted header name. /// /// /// /// The length of is greater than 65,535 characters. /// /// /// The header cannot be allowed to add to the current headers. /// public void AddHeader (string name, string value) { Headers.Set (name, value); } /// /// Appends the specified to the cookies sent with the response. /// /// /// A to append. /// /// /// is . /// public void AppendCookie (Cookie cookie) { 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. /// /// /// is or empty. /// /// /// /// or contains invalid characters. /// /// /// -or- /// /// /// is a restricted header name. /// /// /// /// The length of is greater than 65,535 characters. /// /// /// The current headers cannot allow the header to append a value. /// public void AppendHeader (string name, string value) { 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 entity body data to the client /// and releases the resources used by this instance. /// /// /// An array of that contains the entity body data. /// /// /// true if this method blocks execution while flushing the stream to /// the client; otherwise, false. /// /// /// is . /// /// /// This instance is closed. /// public void Close (byte[] responseEntity, bool willBlock) { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); if (responseEntity == null) throw new ArgumentNullException ("responseEntity"); var len = responseEntity.Length; 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 some properties from the specified to /// this response. /// /// /// A to copy. /// /// /// is . /// public void CopyFrom (HttpListenerResponse templateResponse) { if (templateResponse == null) throw new ArgumentNullException ("templateResponse"); if (templateResponse._headers != null) { if (_headers != null) _headers.Clear (); Headers.Add (templateResponse._headers); } else if (_headers != null) { _headers = null; } _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 . /// /// /// This method sets the property to /// , the property to /// 302, and the property to /// "Found". /// /// /// A that represents the URL to redirect the client's request to. /// /// /// is . /// /// /// isn't an absolute URL. /// /// /// The response has already been sent. /// /// /// This object is closed. /// public void Redirect (string url) { checkDisposedOrHeadersSent (); if (url == null) throw new ArgumentNullException ("url"); Uri uri = null; if (!url.MaybeUri () || !Uri.TryCreate (url, UriKind.Absolute, out uri)) throw new ArgumentException ("Not an absolute URL.", "url"); _location = url; _statusCode = 302; _statusDescription = "Found"; } /// /// Adds or updates a in the cookies sent with the response. /// /// /// A to set. /// /// /// is . /// /// /// already exists in the cookies and couldn't be replaced. /// public void SetCookie (Cookie cookie) { 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 } }