aboutsummaryrefslogtreecommitdiff
path: root/SocketHttpListener/Net/HttpListenerResponse.cs
diff options
context:
space:
mode:
Diffstat (limited to 'SocketHttpListener/Net/HttpListenerResponse.cs')
-rw-r--r--SocketHttpListener/Net/HttpListenerResponse.cs525
1 files changed, 525 insertions, 0 deletions
diff --git a/SocketHttpListener/Net/HttpListenerResponse.cs b/SocketHttpListener/Net/HttpListenerResponse.cs
new file mode 100644
index 000000000..3cb6a0d75
--- /dev/null
+++ b/SocketHttpListener/Net/HttpListenerResponse.cs
@@ -0,0 +1,525 @@
+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 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()
+ {
+ 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 ((ResponseStream)OutputStream).TransmitFile(path, offset, count, fileShareMode, cancellationToken);
+ }
+ }
+} \ No newline at end of file