472 lines
16 KiB
C#
472 lines
16 KiB
C#
using System;
|
|
using System.Collections.Specialized;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Text;
|
|
using System.Web;
|
|
using OSHttpServer.Exceptions;
|
|
|
|
|
|
namespace OSHttpServer
|
|
{
|
|
/// <summary>
|
|
/// Contains server side HTTP request information.
|
|
/// </summary>
|
|
public class HttpRequest : IHttpRequest
|
|
{
|
|
/// <summary>
|
|
/// Chars used to split an URL path into multiple parts.
|
|
/// </summary>
|
|
public static readonly char[] UriSplitters = new[] { '/' };
|
|
public static uint baseID = 0;
|
|
|
|
private readonly NameValueCollection m_headers = new NameValueCollection();
|
|
private readonly HttpParam m_param = new HttpParam(HttpInput.Empty, HttpInput.Empty);
|
|
private Stream m_body = new MemoryStream();
|
|
private int m_bodyBytesLeft;
|
|
private ConnectionType m_connection = ConnectionType.KeepAlive;
|
|
private int m_contentLength;
|
|
private string m_httpVersion = string.Empty;
|
|
private string m_method = string.Empty;
|
|
private NameValueCollection m_queryString = null;
|
|
private Uri m_uri = null;
|
|
private string m_uriPath;
|
|
public readonly IHttpClientContext m_context;
|
|
IPEndPoint m_remoteIPEndPoint = null;
|
|
|
|
public HttpRequest(IHttpClientContext pContext)
|
|
{
|
|
ID = ++baseID;
|
|
m_context = pContext;
|
|
}
|
|
|
|
public uint ID { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this <see cref="HttpRequest"/> is secure.
|
|
/// </summary>
|
|
public bool Secure { get { return m_context.IsSecured; } }
|
|
|
|
public IHttpClientContext Context { get { return m_context; } }
|
|
/// <summary>
|
|
/// Path and query (will be merged with the host header) and put in Uri
|
|
/// </summary>
|
|
/// <see cref="Uri"/>
|
|
public string UriPath
|
|
{
|
|
get { return m_uriPath; }
|
|
set { m_uriPath = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assign a form.
|
|
/// </summary>
|
|
/// <param name="form"></param>
|
|
/*
|
|
internal void AssignForm(HttpForm form)
|
|
{
|
|
_form = form;
|
|
}
|
|
*/
|
|
|
|
#region IHttpRequest Members
|
|
|
|
/// <summary>
|
|
/// Gets kind of types accepted by the client.
|
|
/// </summary>
|
|
public string[] AcceptTypes { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets body stream.
|
|
/// </summary>
|
|
public Stream Body
|
|
{
|
|
get { return m_body; }
|
|
set { m_body = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets kind of connection used for the session.
|
|
/// </summary>
|
|
public ConnectionType Connection
|
|
{
|
|
get { return m_connection; }
|
|
set { m_connection = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets number of bytes in the body.
|
|
/// </summary>
|
|
public int ContentLength
|
|
{
|
|
get { return m_contentLength; }
|
|
set
|
|
{
|
|
m_contentLength = value;
|
|
m_bodyBytesLeft = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets headers sent by the client.
|
|
/// </summary>
|
|
public NameValueCollection Headers
|
|
{
|
|
get { return m_headers; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets version of HTTP protocol that's used.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Probably <see cref="HttpHelper.HTTP10"/> or <see cref="HttpHelper.HTTP11"/>.
|
|
/// </remarks>
|
|
/// <seealso cref="HttpHelper"/>
|
|
public string HttpVersion
|
|
{
|
|
get { return m_httpVersion; }
|
|
set { m_httpVersion = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets requested method.
|
|
/// </summary>
|
|
/// <value></value>
|
|
/// <remarks>
|
|
/// Will always be in upper case.
|
|
/// </remarks>
|
|
/// <see cref="OSHttpServer.Method"/>
|
|
public string Method
|
|
{
|
|
get { return m_method; }
|
|
set { m_method = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets variables sent in the query string
|
|
/// </summary>
|
|
public NameValueCollection QueryString
|
|
{
|
|
get
|
|
{
|
|
if(m_queryString == null)
|
|
{
|
|
if(m_uri == null || m_uri.Query.Length == 0)
|
|
m_queryString = new NameValueCollection();
|
|
else
|
|
{
|
|
try
|
|
{
|
|
m_queryString = HttpUtility.ParseQueryString(m_uri.Query);
|
|
}
|
|
catch { m_queryString = new NameValueCollection(); }
|
|
}
|
|
}
|
|
|
|
return m_queryString;
|
|
}
|
|
}
|
|
|
|
public static readonly Uri EmptyUri = new Uri("http://localhost/");
|
|
/// <summary>
|
|
/// Gets or sets requested URI.
|
|
/// </summary>
|
|
public Uri Uri
|
|
{
|
|
get { return m_uri; }
|
|
set { m_uri = value ?? EmptyUri; } // not safe
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets parameter from <see cref="QueryString"/> or <see cref="Form"/>.
|
|
/// </summary>
|
|
public HttpParam Param
|
|
{
|
|
get { return m_param; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets form parameters.
|
|
/// </summary>
|
|
/*
|
|
public HttpForm Form
|
|
{
|
|
get { return _form; }
|
|
}
|
|
*/
|
|
/// <summary>
|
|
/// Gets whether the request was made by Ajax (Asynchronous JavaScript)
|
|
/// </summary>
|
|
public bool IsAjax { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets cookies that was sent with the request.
|
|
/// </summary>
|
|
public RequestCookies Cookies { get; private set; }
|
|
|
|
///<summary>
|
|
///Creates a new object that is a copy of the current instance.
|
|
///</summary>
|
|
///
|
|
///<returns>
|
|
///A new object that is a copy of this instance.
|
|
///</returns>
|
|
///<filterpriority>2</filterpriority>
|
|
public object Clone()
|
|
{
|
|
// this method was mainly created for testing.
|
|
// dont use it that much...
|
|
var request = new HttpRequest(Context);
|
|
request.Method = m_method;
|
|
if (AcceptTypes != null)
|
|
{
|
|
request.AcceptTypes = new string[AcceptTypes.Length];
|
|
AcceptTypes.CopyTo(request.AcceptTypes, 0);
|
|
}
|
|
request.m_httpVersion = m_httpVersion;
|
|
request.m_queryString = m_queryString;
|
|
request.Uri = m_uri;
|
|
|
|
var buffer = new byte[m_body.Length];
|
|
m_body.Read(buffer, 0, (int)m_body.Length);
|
|
request.Body = new MemoryStream();
|
|
request.Body.Write(buffer, 0, buffer.Length);
|
|
request.Body.Seek(0, SeekOrigin.Begin);
|
|
request.Body.Flush();
|
|
|
|
request.m_headers.Clear();
|
|
foreach (string key in m_headers)
|
|
{
|
|
string[] values = m_headers.GetValues(key);
|
|
if (values != null)
|
|
foreach (string value in values)
|
|
request.AddHeader(key, value);
|
|
}
|
|
return request;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode body into a form.
|
|
/// </summary>
|
|
/// <param name="providers">A list with form decoders.</param>
|
|
/// <exception cref="InvalidDataException">If body contents is not valid for the chosen decoder.</exception>
|
|
/// <exception cref="InvalidOperationException">If body is still being transferred.</exception>
|
|
/*
|
|
public void DecodeBody(FormDecoderProvider providers)
|
|
{
|
|
if (_bodyBytesLeft > 0)
|
|
throw new InvalidOperationException("Body have not yet been completed.");
|
|
|
|
_form = providers.Decode(_headers["content-type"], _body, Encoding.UTF8);
|
|
if (_form != HttpInput.Empty)
|
|
_param.SetForm(_form);
|
|
}
|
|
*/
|
|
///<summary>
|
|
/// Cookies
|
|
///</summary>
|
|
///<param name="cookies">the cookies</param>
|
|
public void SetCookies(RequestCookies cookies)
|
|
{
|
|
Cookies = cookies;
|
|
}
|
|
|
|
public IPEndPoint LocalIPEndPoint { get {return m_context.LocalIPEndPoint; }}
|
|
|
|
public IPEndPoint RemoteIPEndPoint
|
|
{
|
|
get
|
|
{
|
|
if(m_remoteIPEndPoint == null)
|
|
{
|
|
string addr = m_headers["x-forwarded-for"];
|
|
if(!string.IsNullOrEmpty(addr))
|
|
{
|
|
int port = m_context.LocalIPEndPoint.Port;
|
|
try
|
|
{
|
|
m_remoteIPEndPoint = new IPEndPoint(IPAddress.Parse(addr), port);
|
|
}
|
|
catch
|
|
{
|
|
m_remoteIPEndPoint = null;
|
|
}
|
|
}
|
|
}
|
|
if (m_remoteIPEndPoint == null)
|
|
m_remoteIPEndPoint = m_context.LocalIPEndPoint;
|
|
|
|
return m_remoteIPEndPoint;
|
|
}
|
|
}
|
|
/*
|
|
/// <summary>
|
|
/// Create a response object.
|
|
/// </summary>
|
|
/// <returns>A new <see cref="IHttpResponse"/>.</returns>
|
|
public IHttpResponse CreateResponse(IHttpClientContext context)
|
|
{
|
|
return new HttpResponse(context, this);
|
|
}
|
|
*/
|
|
/// <summary>
|
|
/// Called during parsing of a <see cref="IHttpRequest"/>.
|
|
/// </summary>
|
|
/// <param name="name">Name of the header, should not be URL encoded</param>
|
|
/// <param name="value">Value of the header, should not be URL encoded</param>
|
|
/// <exception cref="BadRequestException">If a header is incorrect.</exception>
|
|
public void AddHeader(string name, string value)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
throw new BadRequestException("Invalid header name: " + name ?? "<null>");
|
|
if (string.IsNullOrEmpty(value))
|
|
throw new BadRequestException("Header '" + name + "' do not contain a value.");
|
|
|
|
name = name.ToLowerInvariant();
|
|
|
|
switch (name)
|
|
{
|
|
case "http_x_requested_with":
|
|
case "x-requested-with":
|
|
if (string.Compare(value, "XMLHttpRequest", true) == 0)
|
|
IsAjax = true;
|
|
break;
|
|
case "accept":
|
|
AcceptTypes = value.Split(',');
|
|
for (int i = 0; i < AcceptTypes.Length; ++i)
|
|
AcceptTypes[i] = AcceptTypes[i].Trim();
|
|
break;
|
|
case "content-length":
|
|
if (!int.TryParse(value, out int t))
|
|
throw new BadRequestException("Invalid content length.");
|
|
ContentLength = t;
|
|
break; //todo: maybe throw an exception
|
|
case "host":
|
|
try
|
|
{
|
|
m_uri = new Uri((Secure ? "https://" : "http://") + value + m_uriPath);
|
|
m_uriPath = m_uri.AbsolutePath;
|
|
}
|
|
catch (UriFormatException err)
|
|
{
|
|
throw new BadRequestException("Failed to parse uri: " + value + m_uriPath, err);
|
|
}
|
|
break;
|
|
case "remote_addr":
|
|
if (m_headers[name] == null)
|
|
m_headers.Add(name, value);
|
|
break;
|
|
|
|
case "forwarded":
|
|
string[] parts = value.Split(new char[]{';'});
|
|
string addr = string.Empty;
|
|
for(int i = 0; i < parts.Length; ++i)
|
|
{
|
|
string s = parts[i].TrimStart();
|
|
if(s.Length < 10)
|
|
continue;
|
|
if(s.StartsWith("for", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
int indx = s.IndexOf("=", 3);
|
|
if(indx < 0 || indx >= s.Length - 1)
|
|
continue;
|
|
s = s.Substring(indx);
|
|
addr = s.Trim();
|
|
}
|
|
}
|
|
if(addr.Length > 7)
|
|
{
|
|
m_headers.Add("x-forwarded-for", addr);
|
|
}
|
|
break;
|
|
case "x-forwarded-for":
|
|
if (value.Length > 7)
|
|
{
|
|
string[] xparts = value.Split(new char[]{','});
|
|
if(xparts.Length > 0)
|
|
{
|
|
string xs = xparts[0].Trim();
|
|
if(xs.Length > 7)
|
|
m_headers.Add("x-forwarded-for", xs);
|
|
}
|
|
}
|
|
break;
|
|
case "connection":
|
|
if (string.Compare(value, "close", true) == 0)
|
|
Connection = ConnectionType.Close;
|
|
else if (value.StartsWith("keep-alive", StringComparison.CurrentCultureIgnoreCase))
|
|
Connection = ConnectionType.KeepAlive;
|
|
else if (value.StartsWith("Upgrade", StringComparison.CurrentCultureIgnoreCase))
|
|
Connection = ConnectionType.KeepAlive;
|
|
else
|
|
throw new BadRequestException("Unknown 'Connection' header type.");
|
|
break;
|
|
|
|
case "expect":
|
|
if (value.Contains("100-continue"))
|
|
{
|
|
|
|
}
|
|
m_headers.Add(name, value);
|
|
break;
|
|
|
|
default:
|
|
m_headers.Add(name, value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add bytes to the body
|
|
/// </summary>
|
|
/// <param name="bytes">buffer to read bytes from</param>
|
|
/// <param name="offset">where to start read</param>
|
|
/// <param name="length">number of bytes to read</param>
|
|
/// <returns>Number of bytes actually read (same as length unless we got all body bytes).</returns>
|
|
/// <exception cref="InvalidOperationException">If body is not writable</exception>
|
|
/// <exception cref="ArgumentNullException"><c>bytes</c> is null.</exception>
|
|
/// <exception cref="ArgumentOutOfRangeException"><c>offset</c> is out of range.</exception>
|
|
public int AddToBody(byte[] bytes, int offset, int length)
|
|
{
|
|
if (bytes == null)
|
|
throw new ArgumentNullException("bytes");
|
|
if (offset + length > bytes.Length)
|
|
throw new ArgumentOutOfRangeException("offset");
|
|
if (length == 0)
|
|
return 0;
|
|
if (!m_body.CanWrite)
|
|
throw new InvalidOperationException("Body is not writable.");
|
|
|
|
if (length > m_bodyBytesLeft)
|
|
{
|
|
length = m_bodyBytesLeft;
|
|
}
|
|
|
|
m_body.Write(bytes, offset, length);
|
|
m_bodyBytesLeft -= length;
|
|
|
|
return length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear everything in the request
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
if (m_body != null && m_body.CanRead)
|
|
m_body.Dispose();
|
|
m_body = null;
|
|
m_contentLength = 0;
|
|
m_method = string.Empty;
|
|
m_uri = null;
|
|
m_queryString = null;
|
|
m_bodyBytesLeft = 0;
|
|
m_headers.Clear();
|
|
m_connection = ConnectionType.KeepAlive;
|
|
IsAjax = false;
|
|
//_form.Clear();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |