#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-2021 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 { /// /// Represents an HTTP response to an HTTP request received by /// a instance. /// /// /// This 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 ResponseStream _outputStream; private Uri _redirectLocation; 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 WebHeaderCollection FullHeaders { get { var headers = new WebHeaderCollection (HttpHeaderType.Response, true); if (_headers != null) headers.Add (_headers); if (_contentType != null) { headers.InternalSet ( "Content-Type", createContentTypeHeaderText (_contentType, _contentEncoding), true ); } if (headers["Server"] == null) headers.InternalSet ("Server", "websocket-sharp/1.0", true); if (headers["Date"] == null) { headers.InternalSet ( "Date", DateTime.UtcNow.ToString ("r", CultureInfo.InvariantCulture), true ); } if (_sendChunked) { headers.InternalSet ("Transfer-Encoding", "chunked", true); } else { headers.InternalSet ( "Content-Length", _contentLength.ToString (CultureInfo.InvariantCulture), 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 (_redirectLocation != null) headers.InternalSet ("Location", _redirectLocation.AbsoluteUri, true); if (_cookies != null) { foreach (var cookie in _cookies) { headers.InternalSet ( "Set-Cookie", cookie.ToResponseString (), true ); } } return headers; } } internal bool HeadersSent { get { return _headersSent; } set { _headersSent = value; } } internal string StatusLine { get { return String.Format ( "HTTP/{0} {1} {2}\r\n", _version, _statusCode, _statusDescription ); } } #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. /// /// /// The default value is . /// /// /// /// The response is already being sent. /// /// /// This instance is closed. /// public Encoding ContentEncoding { get { return _contentEncoding; } set { if (_disposed) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } _contentEncoding = value; } } /// /// Gets or sets the number of bytes in the entity body data included in /// the response. /// /// /// /// A that represents the number of bytes in /// the entity body data. /// /// /// It is used for the value of the Content-Length header. /// /// /// The default value is zero. /// /// /// /// 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) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } 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 header. /// /// /// if no media type is specified. /// /// /// The default value is . /// /// /// /// /// The value specified for a set operation is an empty string. /// /// /// -or- /// /// /// The value specified for a set operation contains /// an invalid character. /// /// /// /// The response is already being sent. /// /// /// This instance is closed. /// public string ContentType { get { return _contentType; } set { if (_disposed) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } if (value == null) { _contentType = null; return; } if (value.Length == 0) { var msg = "An empty string."; throw new ArgumentException (msg, "value"); } if (!isValidForContentType (value)) { var msg = "It contains an invalid character."; throw new ArgumentException (msg, "value"); } _contentType = value; } } /// /// Gets or sets the collection of cookies sent with the response. /// /// /// A that contains the cookies sent with /// the response. /// public CookieCollection Cookies { get { if (_cookies == null) _cookies = new CookieCollection (); return _cookies; } set { _cookies = value; } } /// /// Gets or sets the collection of the HTTP headers sent to the client. /// /// /// A that contains the headers sent to /// the client. /// /// /// The value specified for a set operation is not valid for a response. /// public WebHeaderCollection Headers { get { if (_headers == null) _headers = new WebHeaderCollection (HttpHeaderType.Response, false); return _headers; } set { if (value == null) { _headers = null; return; } if (value.State != HttpHeaderType.Response) { var msg = "The value is not valid for a response."; throw new InvalidOperationException (msg); } _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) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } 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) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } if (_outputStream == null) _outputStream = _context.Connection.GetResponseStream (); return _outputStream; } } /// /// Gets the HTTP version used for the response. /// /// /// /// A that represents the HTTP version used for /// the response. /// /// /// Always returns same as 1.1. /// /// public Version ProtocolVersion { get { return _version; } } /// /// Gets or sets the URL to which the client is redirected to locate /// a requested resource. /// /// /// /// A that represents the absolute URL for /// the redirect location. /// /// /// It is used for the value of the Location header. /// /// /// if no redirect location is specified. /// /// /// The default value is . /// /// /// /// /// The value specified for a set operation is an empty string. /// /// /// -or- /// /// /// The value specified for a set operation is not an absolute URL. /// /// /// /// The response is already being sent. /// /// /// This instance is closed. /// public string RedirectLocation { get { return _redirectLocation != null ? _redirectLocation.OriginalString : null; } set { if (_disposed) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } if (value == null) { _redirectLocation = null; return; } if (value.Length == 0) { var msg = "An empty string."; throw new ArgumentException (msg, "value"); } Uri uri; if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) { var msg = "Not an absolute URL."; throw new ArgumentException (msg, "value"); } _redirectLocation = uri; } } /// /// 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) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } 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 HTTP status code for /// the response to the request. /// /// /// The default value is 200. It indicates that the request has /// succeeded. /// /// /// /// The response is already being sent. /// /// /// This instance 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 { if (_disposed) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } if (value < 100 || value > 999) { var msg = "A value is not between 100 and 999 inclusive."; throw new System.Net.ProtocolViolationException (msg); } _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 HTTP status code for the response to the request. /// /// /// The default value is /// the /// RFC 2616 description for the /// property value. /// /// /// An empty string if an RFC 2616 description does not exist. /// /// /// /// The value specified for a set operation is . /// /// /// The value specified for a set operation contains an invalid character. /// /// /// The response is already being sent. /// /// /// This instance is closed. /// public string StatusDescription { get { return _statusDescription; } set { if (_disposed) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } if (value == null) throw new ArgumentNullException ("value"); if (value.Length == 0) { _statusDescription = _statusCode.GetStatusDescription (); return; } if (!isValidForStatusDescription (value)) { var msg = "It contains an invalid character."; throw new ArgumentException (msg, "value"); } _statusDescription = value; } } #endregion #region Private Methods private bool canSetCookie (Cookie cookie) { 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 close (bool force) { _disposed = true; _context.Connection.Close (force); } private void close (byte[] responseEntity, int bufferLength, bool willBlock) { var stream = OutputStream; if (willBlock) { stream.WriteBytes (responseEntity, bufferLength); close (false); return; } stream.WriteBytesAsync ( responseEntity, bufferLength, () => close (false), null ); } private static string createContentTypeHeaderText ( string value, Encoding encoding ) { if (value.IndexOf ("charset=", StringComparison.Ordinal) > -1) return value; if (encoding == null) return value; return String.Format ("{0}; charset={1}", value, encoding.WebName); } private IEnumerable findCookie (Cookie cookie) { if (_cookies == null || _cookies.Count == 0) yield break; foreach (var c in _cookies) { if (c.EqualsWithoutValueAndVersion (cookie)) yield return c; } } private static bool isValidForContentType (string value) { foreach (var c in value) { if (c < 0x20) return false; if (c > 0x7e) return false; if ("()<>@:\\[]?{}".IndexOf (c) > -1) return false; } return true; } private static bool isValidForStatusDescription (string value) { foreach (var c in value) { if (c < 0x20) return false; if (c > 0x7e) return false; } return true; } #endregion #region Public Methods /// /// Closes the connection to the client without sending a response. /// public void Abort () { if (_disposed) return; close (true); } /// /// Appends the specified cookie to the cookies sent with the response. /// /// /// A to append. /// /// /// is . /// public void AppendCookie (Cookie cookie) { Cookies.Add (cookie); } /// /// Appends an HTTP header with the specified name and value to /// the headers for the response. /// /// /// A that specifies the name of the header to /// append. /// /// /// A that specifies the value of the header to /// append. /// /// /// is . /// /// /// /// is an empty string. /// /// /// -or- /// /// /// is a string of spaces. /// /// /// -or- /// /// /// contains an invalid character. /// /// /// -or- /// /// /// contains an invalid character. /// /// /// -or- /// /// /// is a restricted header name. /// /// /// /// The length of is greater than 65,535 /// characters. /// /// /// The current headers do not allow the header. /// public void AppendHeader (string name, string value) { Headers.Add (name, value); } /// /// Sends the response to the client and releases the resources used by /// this instance. /// public void Close () { if (_disposed) return; close (false); } /// /// Sends 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. /// /// /// A : 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) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } if (responseEntity == null) throw new ArgumentNullException ("responseEntity"); var len = responseEntity.LongLength; if (len > Int32.MaxValue) { close (responseEntity, 1024, willBlock); return; } var stream = OutputStream; if (willBlock) { stream.Write (responseEntity, 0, (int) len); close (false); return; } stream.BeginWrite ( responseEntity, 0, (int) len, ar => { stream.EndWrite (ar); close (false); }, null ); } /// /// Copies some properties from the specified response instance to /// this instance. /// /// /// A to copy. /// /// /// is . /// public void CopyFrom (HttpListenerResponse templateResponse) { if (templateResponse == null) throw new ArgumentNullException ("templateResponse"); var headers = templateResponse._headers; if (headers != null) { if (_headers != null) _headers.Clear (); Headers.Add (headers); } else { _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 URL. /// /// /// This method sets the property to /// , the property to /// 302, and the property to "Found". /// /// /// A that specifies the absolute URL to which /// the client is redirected to locate a requested resource. /// /// /// is . /// /// /// /// is an empty string. /// /// /// -or- /// /// /// is not an absolute URL. /// /// /// /// The response is already being sent. /// /// /// This instance is closed. /// public void Redirect (string url) { if (_disposed) { var name = GetType ().ToString (); throw new ObjectDisposedException (name); } if (_headersSent) { var msg = "The response is already being sent."; throw new InvalidOperationException (msg); } if (url == null) throw new ArgumentNullException ("url"); if (url.Length == 0) { var msg = "An empty string."; throw new ArgumentException (msg, "url"); } Uri uri; if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) { var msg = "Not an absolute URL."; throw new ArgumentException (msg, "url"); } _redirectLocation = uri; _statusCode = 302; _statusDescription = "Found"; } /// /// Adds or updates a cookie in the cookies sent with the response. /// /// /// A to set. /// /// /// is . /// /// /// already exists in the cookies but /// it cannot be updated. /// public void SetCookie (Cookie cookie) { if (cookie == null) throw new ArgumentNullException ("cookie"); if (!canSetCookie (cookie)) { var msg = "It cannot be updated."; throw new ArgumentException (msg, "cookie"); } Cookies.Add (cookie); } /// /// Adds or updates an HTTP header with the specified name and value in /// the headers for the response. /// /// /// A that specifies the name of the header to set. /// /// /// A that specifies the value of the header to set. /// /// /// is . /// /// /// /// is an empty string. /// /// /// -or- /// /// /// is a string of spaces. /// /// /// -or- /// /// /// contains an invalid character. /// /// /// -or- /// /// /// contains an invalid character. /// /// /// -or- /// /// /// is a restricted header name. /// /// /// /// The length of is greater than 65,535 /// characters. /// /// /// The current headers do not allow the header. /// public void SetHeader (string name, string value) { Headers.Set (name, value); } #endregion #region Explicit Interface Implementations /// /// Releases all resources used by this instance. /// void IDisposable.Dispose () { if (_disposed) return; close (true); } #endregion } }