diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2017-06-15 13:22:05 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2017-06-15 13:22:05 -0400 |
| commit | ccb5b14d772c2d1028fa9003ee64fde3cd4196b3 (patch) | |
| tree | 294a56f3d34ed3e921257ca124f7d2fa28f79d2e /SocketHttpListener/Net/HttpListenerResponse.cs | |
| parent | b615a2aeb16a527fe56a88a352574d76e77783e8 (diff) | |
update series resolver
Diffstat (limited to 'SocketHttpListener/Net/HttpListenerResponse.cs')
| -rw-r--r-- | SocketHttpListener/Net/HttpListenerResponse.cs | 652 |
1 files changed, 191 insertions, 461 deletions
diff --git a/SocketHttpListener/Net/HttpListenerResponse.cs b/SocketHttpListener/Net/HttpListenerResponse.cs index da7aff081..240c67930 100644 --- a/SocketHttpListener/Net/HttpListenerResponse.cs +++ b/SocketHttpListener/Net/HttpListenerResponse.cs @@ -1,572 +1,302 @@ -using System; -using System.Globalization; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Text; -using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Text; -using SocketHttpListener.Primitives; +using System.Globalization; +using System.Runtime.InteropServices; +using System.ComponentModel; +using System.Diagnostics; +using Microsoft.Win32.SafeHandles; namespace SocketHttpListener.Net { - public sealed class HttpListenerResponse : IDisposable + public sealed unsafe partial class HttpListenerResponse : IDisposable { - bool disposed; - Encoding content_encoding; - long content_length; - bool cl_set; - string content_type; - CookieCollection cookies; - WebHeaderCollection headers = new WebHeaderCollection(); - bool keep_alive = true; - Stream output_stream; - Version version = HttpVersion.Version11; - string location; - int status_code = 200; - string status_description = "OK"; - bool chunked; - HttpListenerContext context; - - internal bool HeadersSent; - internal object headers_lock = new object(); - - private readonly ILogger _logger; - private readonly ITextEncoding _textEncoding; - private readonly IFileSystem _fileSystem; - - internal HttpListenerResponse(HttpListenerContext context, ILogger logger, ITextEncoding textEncoding, IFileSystem fileSystem) - { - this.context = context; - _logger = logger; - _textEncoding = textEncoding; - _fileSystem = fileSystem; - } - - internal bool CloseConnection - { - get - { - return headers["Connection"] == "close"; - } - } - - public bool ForceCloseChunked - { - get { return false; } - } + private BoundaryType _boundaryType = BoundaryType.None; + private CookieCollection _cookies; + private HttpListenerContext _httpContext; + private bool _keepAlive = true; + private HttpResponseStream _responseStream; + private string _statusDescription; + private WebHeaderCollection _webHeaders = new WebHeaderCollection(); - public Encoding ContentEncoding + public WebHeaderCollection Headers { - get - { - if (content_encoding == null) - content_encoding = _textEncoding.GetDefaultEncoding(); - return content_encoding; - } - set - { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - content_encoding = value; - } + get => _webHeaders; } - 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 Encoding ContentEncoding { get; set; } public string ContentType { - get { return content_type; } + get => Headers["Content-Type"]; set { - // TODO: is null ok? - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - content_type = value; + CheckDisposed(); + if (string.IsNullOrEmpty(value)) + { + Headers.Remove("Content-Type"); + } + else + { + Headers.Set("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? - } + private HttpListenerContext HttpListenerContext => _httpContext; - 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; - } - } + private HttpListenerRequest HttpListenerRequest => HttpListenerContext.Request; - public bool KeepAlive + internal EntitySendFormat EntitySendFormat { - get { return keep_alive; } + get => (EntitySendFormat)_boundaryType; set { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - keep_alive = value; + CheckDisposed(); + CheckSentHeaders(); + if (value == EntitySendFormat.Chunked && HttpListenerRequest.ProtocolVersion.Minor == 0) + { + throw new ProtocolViolationException("net_nochunkuploadonhttp10"); + } + _boundaryType = (BoundaryType)value; + if (value != EntitySendFormat.ContentLength) + { + _contentLength = -1; + } } } - public Stream OutputStream + public bool SendChunked { - get - { - if (output_stream == null) - output_stream = context.Connection.GetResponseStream(); - return output_stream; - } + get => EntitySendFormat == EntitySendFormat.Chunked; + set => EntitySendFormat = value ? EntitySendFormat.Chunked : EntitySendFormat.ContentLength; } - public Version ProtocolVersion + // We MUST NOT send message-body when we send responses with these Status codes + private static readonly int[] s_noResponseBody = { 100, 101, 204, 205, 304 }; + + private static bool CanSendResponseBody(int responseCode) { - get { return version; } - set + for (int i = 0; i < s_noResponseBody.Length; i++) { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - 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; + if (responseCode == s_noResponseBody[i]) + { + return false; + } } + return true; } - public string RedirectLocation + public long ContentLength64 { - get { return location; } + get => _contentLength; set { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - location = value; + CheckDisposed(); + CheckSentHeaders(); + if (value >= 0) + { + _contentLength = value; + _boundaryType = BoundaryType.ContentLength; + } + else + { + throw new ArgumentOutOfRangeException("net_clsmall"); + } } } - public bool SendChunked + public CookieCollection Cookies { - get { return chunked; } - set - { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - chunked = value; - } + get => _cookies ?? (_cookies = new CookieCollection()); + set => _cookies = value; } - public int StatusCode + public bool KeepAlive { - get { return status_code; } + get => _keepAlive; set { - if (disposed) - throw new ObjectDisposedException(GetType().ToString()); - - if (value < 100 || value > 999) - throw new ProtocolViolationException("StatusCode must be between 100 and 999."); - status_code = value; - status_description = GetStatusDescription(value); + CheckDisposed(); + _keepAlive = value; } } - internal static string GetStatusDescription(int code) + public Stream OutputStream { - switch (code) + get { - case 100: return "Continue"; - case 101: return "Switching Protocols"; - case 102: return "Processing"; - case 200: return "OK"; - case 201: return "Created"; - case 202: return "Accepted"; - case 203: return "Non-Authoritative Information"; - case 204: return "No Content"; - case 205: return "Reset Content"; - case 206: return "Partial Content"; - case 207: return "Multi-Status"; - case 300: return "Multiple Choices"; - case 301: return "Moved Permanently"; - case 302: return "Found"; - case 303: return "See Other"; - case 304: return "Not Modified"; - case 305: return "Use Proxy"; - case 307: return "Temporary Redirect"; - case 400: return "Bad Request"; - case 401: return "Unauthorized"; - case 402: return "Payment Required"; - case 403: return "Forbidden"; - case 404: return "Not Found"; - case 405: return "Method Not Allowed"; - case 406: return "Not Acceptable"; - case 407: return "Proxy Authentication Required"; - case 408: return "Request Timeout"; - case 409: return "Conflict"; - case 410: return "Gone"; - case 411: return "Length Required"; - case 412: return "Precondition Failed"; - case 413: return "Request Entity Too Large"; - case 414: return "Request-Uri Too Long"; - case 415: return "Unsupported Media Type"; - case 416: return "Requested Range Not Satisfiable"; - case 417: return "Expectation Failed"; - case 422: return "Unprocessable Entity"; - case 423: return "Locked"; - case 424: return "Failed Dependency"; - case 500: return "Internal Server Error"; - case 501: return "Not Implemented"; - case 502: return "Bad Gateway"; - case 503: return "Service Unavailable"; - case 504: return "Gateway Timeout"; - case 505: return "Http Version Not Supported"; - case 507: return "Insufficient Storage"; + CheckDisposed(); + EnsureResponseStream(); + return _responseStream; } - return ""; } - public string StatusDescription + public string RedirectLocation { - get { return status_description; } + get => Headers["Location"]; set { - status_description = value; - } - } - - void IDisposable.Dispose() - { - Close(true); //TODO: Abort or Close? - } - - 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); - } - - private void Close(bool force) - { - if (force) - { - _logger.Debug("HttpListenerResponse force closing HttpConnection"); + // note that this doesn't set the status code to a redirect one + CheckDisposed(); + if (string.IsNullOrEmpty(value)) + { + Headers.Remove("Location"); + } + else + { + Headers.Set("Location", value); + } } - disposed = true; - context.Connection.Close(force); } - public void Close(byte[] responseEntity, bool willBlock) + public string StatusDescription { - //CheckDisposed(); - - if (responseEntity == null) - { - throw new ArgumentNullException(nameof(responseEntity)); - } - - //if (_boundaryType != BoundaryType.Chunked) - { - ContentLength64 = responseEntity.Length; - } - - if (willBlock) + get { - try + if (_statusDescription == null) { - OutputStream.Write(responseEntity, 0, responseEntity.Length); + // if the user hasn't set this, generated on the fly, if possible. + // We know this one is safe, no need to verify it as in the setter. + _statusDescription = HttpStatusDescription.Get(StatusCode); } - finally + if (_statusDescription == null) { - Close(false); + _statusDescription = string.Empty; } + return _statusDescription; } - else + set { - OutputStream.BeginWrite(responseEntity, 0, responseEntity.Length, iar => + CheckDisposed(); + if (value == null) { - var thisRef = (HttpListenerResponse)iar.AsyncState; - try - { - thisRef.OutputStream.EndWrite(iar); - } - finally + throw new ArgumentNullException(nameof(value)); + } + + // Need to verify the status description doesn't contain any control characters except HT. We mask off the high + // byte since that's how it's encoded. + for (int i = 0; i < value.Length; i++) + { + char c = (char)(0x000000ff & (uint)value[i]); + if ((c <= 31 && c != (byte)'\t') || c == 127) { - thisRef.Close(false); + throw new ArgumentException("net_WebHeaderInvalidControlChars"); } - }, this); + } + + _statusDescription = value; } } - public void Close() + public void AddHeader(string name, string value) { - if (disposed) - return; - - Close(false); + Headers.Set(name, value); } - public void Redirect(string url) + public void AppendHeader(string name, string value) { - StatusCode = 302; // Found - location = url; + Headers.Add(name, value); } - bool FindCookie(Cookie cookie) + public void AppendCookie(Cookie cookie) { - string name = cookie.Name; - string domain = cookie.Domain; - string path = cookie.Path; - foreach (Cookie c in cookies) + if (cookie == null) { - if (name != c.Name) - continue; - if (domain != c.Domain) - continue; - if (path == c.Path) - return true; + throw new ArgumentNullException(nameof(cookie)); } - - return false; + Cookies.Add(cookie); } - public void DetermineIfChunked() + private void ComputeCookies() { - if (chunked) + if (_cookies != null) { - return; - } - - Version v = context.Request.ProtocolVersion; - if (!cl_set && !chunked && v >= HttpVersion.Version11) - chunked = true; - if (!chunked && string.Equals(headers["Transfer-Encoding"], "chunked")) - { - chunked = true; + // now go through the collection, and concatenate all the cookies in per-variant strings + //string setCookie2 = null, setCookie = null; + //for (int index = 0; index < _cookies.Count; index++) + //{ + // Cookie cookie = _cookies[index]; + // string cookieString = cookie.ToServerString(); + // if (cookieString == null || cookieString.Length == 0) + // { + // continue; + // } + + // if (cookie.IsRfc2965Variant()) + // { + // setCookie2 = setCookie2 == null ? cookieString : setCookie2 + ", " + cookieString; + // } + // else + // { + // setCookie = setCookie == null ? cookieString : setCookie + ", " + cookieString; + // } + //} + + //if (!string.IsNullOrEmpty(setCookie)) + //{ + // Headers.Set(HttpKnownHeaderNames.SetCookie, setCookie); + // if (string.IsNullOrEmpty(setCookie2)) + // { + // Headers.Remove(HttpKnownHeaderNames.SetCookie2); + // } + //} + + //if (!string.IsNullOrEmpty(setCookie2)) + //{ + // Headers.Set(HttpKnownHeaderNames.SetCookie2, setCookie2); + // if (string.IsNullOrEmpty(setCookie)) + // { + // Headers.Remove(HttpKnownHeaderNames.SetCookie); + // } + //} } } - internal void SendHeaders(bool closing, MemoryStream ms) + public void Redirect(string url) { - Encoding encoding = content_encoding; - if (encoding == null) - encoding = _textEncoding.GetDefaultEncoding(); - - if (content_type != null) - { - if (content_encoding != null && content_type.IndexOf("charset=", StringComparison.OrdinalIgnoreCase) == -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)); - } + Headers["Location"] = url; + StatusCode = (int)HttpStatusCode.Redirect; + StatusDescription = "Found"; + } - 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) + public void SetCookie(Cookie cookie) + { + if (cookie == null) { - headers.SetInternal("Connection", "close"); - conn_close = true; + throw new ArgumentNullException(nameof(cookie)); } - if (chunked) - headers.SetInternal("Transfer-Encoding", "chunked"); + //Cookie newCookie = cookie.Clone(); + //int added = Cookies.InternalAdd(newCookie, true); - //int reuses = context.Connection.Reuses; - //if (reuses >= 100) + //if (added != 1) //{ - // _logger.Debug("HttpListenerResponse - keep alive has exceeded 100 uses and will be closed."); - - // force_close_chunked = true; - // if (!conn_close) - // { - // headers.SetInternal("Connection", "close"); - // conn_close = true; - // } + // // The Cookie already existed and couldn't be replaced. + // throw new ArgumentException("Cookie exists"); //} + } - if (!conn_close) - { - 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.ToString()); - } - - headers.SetInternal("Status", status_code.ToString(CultureInfo.InvariantCulture)); + void IDisposable.Dispose() => Dispose(); - using (StreamWriter writer = new StreamWriter(ms, encoding, 256, true)) + private void CheckDisposed() + { + if (Disposed) { - writer.Write("HTTP/{0} {1} {2}\r\n", version, status_code, status_description); - string headers_str = headers.ToStringMultiValue(); - writer.Write(headers_str); - writer.Flush(); + throw new ObjectDisposedException(GetType().FullName); } - - int preamble = 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; } - public void SetCookie(Cookie cookie) + private void CheckSentHeaders() { - if (cookie == null) - throw new ArgumentNullException("cookie"); - - if (cookies != null) + if (SentHeaders) { - if (FindCookie(cookie)) - throw new ArgumentException("The cookie already exists."); + throw new InvalidOperationException(); } - else - { - cookies = new CookieCollection(); - } - - cookies.Add(cookie); - } - - public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) - { - return ((HttpResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken); } } -}
\ No newline at end of file +} |
