using System; using System.Globalization; using System.IO; 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; namespace SocketHttpListener.Net { public sealed 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; } } public Encoding ContentEncoding { get { if (content_encoding == null) content_encoding = _textEncoding.GetDefaultEncoding(); return content_encoding; } set { if (disposed) throw new ObjectDisposedException(GetType().ToString()); 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()); 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()); 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 (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()); location = value; } } public bool SendChunked { get { return chunked; } set { if (disposed) throw new ObjectDisposedException(GetType().ToString()); chunked = value; } } public int StatusCode { get { return status_code; } 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); } } internal static string GetStatusDescription(int code) { switch (code) { 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"; } return ""; } public string StatusDescription { get { return status_description; } 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"); } disposed = true; context.Connection.Close(force); } public void Close(byte[] responseEntity, bool willBlock) { //CheckDisposed(); if (responseEntity == null) { throw new ArgumentNullException(nameof(responseEntity)); } //if (_boundaryType != BoundaryType.Chunked) { ContentLength64 = responseEntity.Length; } if (willBlock) { try { OutputStream.Write(responseEntity, 0, responseEntity.Length); } finally { Close(false); } } else { OutputStream.BeginWrite(responseEntity, 0, responseEntity.Length, iar => { var thisRef = (HttpListenerResponse)iar.AsyncState; try { thisRef.OutputStream.EndWrite(iar); } finally { thisRef.Close(false); } }, this); } } public void Close() { if (disposed) return; Close(false); } public void Redirect(string url) { StatusCode = 302; // Found location = url; } 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; } public void DetermineIfChunked() { if (chunked) { 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; } } internal void SendHeaders(bool closing, MemoryStream ms) { 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)); } 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) //{ // _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; // } //} 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)); using (StreamWriter writer = new StreamWriter(ms, encoding, 256, true)) { 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.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) { 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); } public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) { return ((HttpResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken); } } }