jellyfin-server/SocketHttpListener/Net/HttpListenerRequest.cs
2017-09-02 22:42:13 -04:00

664 lines
21 KiB
C#

using System;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.Text;
using SocketHttpListener.Primitives;
namespace SocketHttpListener.Net
{
public sealed class HttpListenerRequest
{
string[] accept_types;
Encoding content_encoding;
long content_length;
bool cl_set;
CookieCollection cookies;
WebHeaderCollection headers;
string method;
Stream input_stream;
Version version;
QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness
string raw_url;
Uri url;
Uri referrer;
string[] user_languages;
HttpListenerContext context;
bool is_chunked;
bool ka_set;
bool? _keepAlive;
private readonly ITextEncoding _textEncoding;
internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding)
{
this.context = context;
_textEncoding = textEncoding;
headers = new WebHeaderCollection();
version = HttpVersion.Version10;
}
static char[] separators = new char[] { ' ' };
internal void SetRequestLine(string req)
{
string[] parts = req.Split(separators, 3);
if (parts.Length != 3)
{
context.ErrorMessage = "Invalid request line (parts).";
return;
}
method = parts[0];
foreach (char c in method)
{
int ic = (int)c;
if ((ic >= 'A' && ic <= 'Z') ||
(ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' &&
c != '<' && c != '>' && c != '@' && c != ',' && c != ';' &&
c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' &&
c != ']' && c != '?' && c != '=' && c != '{' && c != '}'))
continue;
context.ErrorMessage = "(Invalid verb)";
return;
}
raw_url = parts[1];
if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/"))
{
context.ErrorMessage = "Invalid request line (version).";
return;
}
try
{
version = new Version(parts[2].Substring(5));
if (version.Major < 1)
throw new Exception();
}
catch
{
context.ErrorMessage = "Invalid request line (version).";
return;
}
}
void CreateQueryString(string query)
{
if (query == null || query.Length == 0)
{
query_string = new QueryParamCollection();
return;
}
query_string = new QueryParamCollection();
if (query[0] == '?')
query = query.Substring(1);
string[] components = query.Split('&');
foreach (string kv in components)
{
int pos = kv.IndexOf('=');
if (pos == -1)
{
query_string.Add(null, WebUtility.UrlDecode(kv));
}
else
{
string key = WebUtility.UrlDecode(kv.Substring(0, pos));
string val = WebUtility.UrlDecode(kv.Substring(pos + 1));
query_string.Add(key, val);
}
}
}
internal void FinishInitialization()
{
string host = UserHostName;
if (version > HttpVersion.Version10 && (host == null || host.Length == 0))
{
context.ErrorMessage = "Invalid host name";
return;
}
string path;
Uri raw_uri = null;
if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri))
path = raw_uri.PathAndQuery;
else
path = raw_url;
if ((host == null || host.Length == 0))
host = UserHostAddress;
if (raw_uri != null)
host = raw_uri.Host;
int colon = host.LastIndexOf(':');
if (colon >= 0)
host = host.Substring(0, colon);
string base_uri = String.Format("{0}://{1}:{2}",
(IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"),
host, LocalEndPoint.Port);
if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url))
{
context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path);
return; return;
}
CreateQueryString(url.Query);
if (version >= HttpVersion.Version11)
{
string t_encoding = Headers["Transfer-Encoding"];
is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0);
// 'identity' is not valid!
if (t_encoding != null && !is_chunked)
{
context.Connection.SendError(null, 501);
return;
}
}
if (!is_chunked && !cl_set)
{
if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 ||
String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0)
{
context.Connection.SendError(null, 411);
return;
}
}
if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0)
{
var output = (HttpResponseStream)context.Connection.GetResponseStream(true);
var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n");
output.InternalWrite(_100continue, 0, _100continue.Length);
}
}
static bool MaybeUri(string s)
{
int p = s.IndexOf(':');
if (p == -1)
return false;
if (p >= 10)
return false;
return IsPredefinedScheme(s.Substring(0, p));
}
//
// Using a simple block of if's is twice as slow as the compiler generated
// switch statement. But using this tuned code is faster than the
// compiler generated code, with a million loops on x86-64:
//
// With "http": .10 vs .51 (first check)
// with "https": .16 vs .51 (second check)
// with "foo": .22 vs .31 (never found)
// with "mailto": .12 vs .51 (last check)
//
//
static bool IsPredefinedScheme(string scheme)
{
if (scheme == null || scheme.Length < 3)
return false;
char c = scheme[0];
if (c == 'h')
return (scheme == "http" || scheme == "https");
if (c == 'f')
return (scheme == "file" || scheme == "ftp");
if (c == 'n')
{
c = scheme[1];
if (c == 'e')
return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp");
if (scheme == "nntp")
return true;
return false;
}
if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto"))
return true;
return false;
}
internal static string Unquote(String str)
{
int start = str.IndexOf('\"');
int end = str.LastIndexOf('\"');
if (start >= 0 && end >= 0)
str = str.Substring(start + 1, end - 1);
return str.Trim();
}
internal void AddHeader(string header)
{
int colon = header.IndexOf(':');
if (colon == -1 || colon == 0)
{
context.ErrorMessage = "Bad Request";
context.ErrorStatus = 400;
return;
}
string name = header.Substring(0, colon).Trim();
string val = header.Substring(colon + 1).Trim();
string lower = name.ToLowerInvariant();
headers.SetInternal(name, val);
switch (lower)
{
case "accept-language":
user_languages = val.Split(','); // yes, only split with a ','
break;
case "accept":
accept_types = val.Split(','); // yes, only split with a ','
break;
case "content-length":
try
{
//TODO: max. content_length?
content_length = Int64.Parse(val.Trim());
if (content_length < 0)
context.ErrorMessage = "Invalid Content-Length.";
cl_set = true;
}
catch
{
context.ErrorMessage = "Invalid Content-Length.";
}
break;
case "content-type":
{
var contents = val.Split(';');
foreach (var content in contents)
{
var tmp = content.Trim();
if (tmp.StartsWith("charset"))
{
var charset = tmp.GetValue("=");
if (charset != null && charset.Length > 0)
{
try
{
// Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n
charset = charset.Trim('"');
var index = charset.IndexOf('"');
if (index != -1) charset = charset.Substring(0, index);
content_encoding = Encoding.GetEncoding(charset);
}
catch
{
context.ErrorMessage = "Invalid Content-Type header: " + charset;
}
}
break;
}
}
}
break;
case "referer":
try
{
referrer = new Uri(val);
}
catch
{
referrer = new Uri("http://someone.is.screwing.with.the.headers.com/");
}
break;
case "cookie":
if (cookies == null)
cookies = new CookieCollection();
string[] cookieStrings = val.Split(new char[] { ',', ';' });
Cookie current = null;
int version = 0;
foreach (string cookieString in cookieStrings)
{
string str = cookieString.Trim();
if (str.Length == 0)
continue;
if (str.StartsWith("$Version"))
{
version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1)));
}
else if (str.StartsWith("$Path"))
{
if (current != null)
current.Path = str.Substring(str.IndexOf('=') + 1).Trim();
}
else if (str.StartsWith("$Domain"))
{
if (current != null)
current.Domain = str.Substring(str.IndexOf('=') + 1).Trim();
}
else if (str.StartsWith("$Port"))
{
if (current != null)
current.Port = str.Substring(str.IndexOf('=') + 1).Trim();
}
else
{
if (current != null)
{
cookies.Add(current);
}
current = new Cookie();
int idx = str.IndexOf('=');
if (idx > 0)
{
current.Name = str.Substring(0, idx).Trim();
current.Value = str.Substring(idx + 1).Trim();
}
else
{
current.Name = str.Trim();
current.Value = String.Empty;
}
current.Version = version;
}
}
if (current != null)
{
cookies.Add(current);
}
break;
}
}
// returns true is the stream could be reused.
internal bool FlushInput()
{
if (!HasEntityBody)
return true;
int length = 2048;
if (content_length > 0)
length = (int)Math.Min(content_length, (long)length);
byte[] bytes = new byte[length];
while (true)
{
// TODO: test if MS has a timeout when doing this
try
{
var task = InputStream.ReadAsync(bytes, 0, length);
var result = Task.WaitAll(new [] { task }, 1000);
if (!result)
{
return false;
}
if (task.Result <= 0)
{
return true;
}
}
catch (ObjectDisposedException e)
{
input_stream = null;
return true;
}
catch
{
return false;
}
}
}
public string[] AcceptTypes
{
get { return accept_types; }
}
public int ClientCertificateError
{
get
{
HttpConnection cnc = context.Connection;
//if (cnc.ClientCertificate == null)
// throw new InvalidOperationException("No client certificate");
//int[] errors = cnc.ClientCertificateErrors;
//if (errors != null && errors.Length > 0)
// return errors[0];
return 0;
}
}
public Encoding ContentEncoding
{
get
{
if (content_encoding == null)
content_encoding = _textEncoding.GetDefaultEncoding();
return content_encoding;
}
}
public long ContentLength64
{
get { return is_chunked ? -1 : content_length; }
}
public string ContentType
{
get { return headers["content-type"]; }
}
public CookieCollection Cookies
{
get
{
// TODO: check if the collection is read-only
if (cookies == null)
cookies = new CookieCollection();
return cookies;
}
}
public bool HasEntityBody
{
get { return (content_length > 0 || is_chunked); }
}
public QueryParamCollection Headers
{
get { return headers; }
}
public string HttpMethod
{
get { return method; }
}
public Stream InputStream
{
get
{
if (input_stream == null)
{
if (is_chunked || content_length > 0)
input_stream = context.Connection.GetRequestStream(is_chunked, content_length);
else
input_stream = Stream.Null;
}
return input_stream;
}
}
public bool IsAuthenticated
{
get { return false; }
}
public bool IsLocal
{
get
{
var remoteEndPoint = RemoteEndPoint;
return remoteEndPoint.Address.Equals(IPAddress.Loopback) ||
remoteEndPoint.Address.Equals(IPAddress.IPv6Loopback) ||
LocalEndPoint.Address.Equals(remoteEndPoint.Address);
}
}
public bool IsSecureConnection
{
get { return context.Connection.IsSecure; }
}
public bool KeepAlive
{
get
{
if (!_keepAlive.HasValue)
{
string header = Headers["Proxy-Connection"];
if (string.IsNullOrEmpty(header))
{
header = Headers["Connection"];
}
if (string.IsNullOrEmpty(header))
{
if (ProtocolVersion >= HttpVersion.Version11)
{
_keepAlive = true;
}
else
{
header = Headers["Keep-Alive"];
_keepAlive = !string.IsNullOrEmpty(header);
}
}
else
{
header = header.ToLower(CultureInfo.InvariantCulture);
_keepAlive =
header.IndexOf("close", StringComparison.OrdinalIgnoreCase) < 0 ||
header.IndexOf("keep-alive", StringComparison.OrdinalIgnoreCase) >= 0;
}
}
return _keepAlive.Value;
}
}
public IPEndPoint LocalEndPoint
{
get { return context.Connection.LocalEndPoint; }
}
public Version ProtocolVersion
{
get { return version; }
}
public QueryParamCollection QueryString
{
get { return query_string; }
}
public string RawUrl
{
get { return raw_url; }
}
public IPEndPoint RemoteEndPoint
{
get { return context.Connection.RemoteEndPoint; }
}
public Guid RequestTraceIdentifier
{
get { return Guid.Empty; }
}
public Uri Url
{
get { return url; }
}
public Uri UrlReferrer
{
get { return referrer; }
}
public string UserAgent
{
get { return headers["user-agent"]; }
}
public string UserHostAddress
{
get { return LocalEndPoint.ToString(); }
}
public string UserHostName
{
get { return headers["host"]; }
}
public string[] UserLanguages
{
get { return user_languages; }
}
public string ServiceName
{
get
{
return null;
}
}
private bool _websocketRequestWasSet;
private bool _websocketRequest;
/// <summary>
/// Gets a value indicating whether the request is a WebSocket connection request.
/// </summary>
/// <value>
/// <c>true</c> if the request is a WebSocket connection request; otherwise, <c>false</c>.
/// </value>
public bool IsWebSocketRequest
{
get
{
if (!_websocketRequestWasSet)
{
_websocketRequest = method == "GET" &&
version > HttpVersion.Version10 &&
headers.Contains("Upgrade", "websocket") &&
headers.Contains("Connection", "Upgrade");
_websocketRequestWasSet = true;
}
return _websocketRequest;
}
}
}
}