mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-19 20:09:03 -07:00
572 lines
18 KiB
C#
572 lines
18 KiB
C#
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);
|
|
}
|
|
}
|
|
} |