using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using OpenSim.Framework;
using OSHttpServer.Exceptions;
namespace OSHttpServer
{
///
/// Contains server side HTTP request information.
///
public class HttpRequest : IHttpRequest
{
private const int MAXCONTENTLENGTH = 250 * 1024 * 1024;
///
/// Chars used to split an URL path into multiple parts.
///
public static readonly char[] UriSplitters = new[] { '/' };
public static uint baseID = 0;
private readonly NameValueCollection m_headers = new();
//private readonly HttpParam m_param = new(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 IHttpClientContext m_context;
IPEndPoint m_remoteIPEndPoint = null;
public HttpRequest(IHttpClientContext pContext)
{
ID = ++baseID;
m_context = pContext;
}
public uint ID { get; private set; }
///
/// Gets or sets a value indicating whether this is secure.
///
public bool Secure { get { return m_context.IsSecured; } }
public IHttpClientContext Context { get { return m_context; } }
///
/// Path and query (will be merged with the host header) and put in Uri
///
///
public string UriPath
{
get { return m_uriPath; }
set { m_uriPath = value; }
}
///
/// Assign a form.
///
///
/*
internal void AssignForm(HttpForm form)
{
_form = form;
}
*/
#region IHttpRequest Members
///
/// Gets kind of types accepted by the client.
///
public string[] AcceptTypes { get; private set; }
///
/// Gets or sets body stream.
///
public Stream Body
{
get { return m_body; }
set { m_body = value; }
}
///
/// Gets or sets kind of connection used for the session.
///
public ConnectionType Connection
{
get { return m_connection; }
set { m_connection = value; }
}
///
/// Gets or sets number of bytes in the body.
///
public int ContentLength
{
get { return m_contentLength; }
set
{
m_contentLength = value;
m_bodyBytesLeft = value;
}
}
///
/// Gets headers sent by the client.
///
public NameValueCollection Headers
{
get { return m_headers; }
}
///
/// Gets or sets version of HTTP protocol that's used.
///
///
/// Probably or .
///
///
public string HttpVersion
{
get { return m_httpVersion; }
set { m_httpVersion = value; }
}
///
/// Gets or sets requested method.
///
///
///
/// Will always be in upper case.
///
///
public string Method
{
get { return m_method; }
set { m_method = value; }
}
///
/// Gets variables sent in the query string
///
public NameValueCollection QueryString
{
get
{
if(m_queryString is null)
{
if(m_uri is 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("http://localhost/");
///
/// Gets or sets requested URI.
///
public Uri Uri
{
get { return m_uri; }
set { m_uri = value ?? EmptyUri; } // not safe
}
/*
///
/// Gets parameter from or .
///
public HttpParam Param
{
get { return m_param; }
}
*/
///
/// Gets form parameters.
///
/*
public HttpForm Form
{
get { return _form; }
}
*/
///
/// Gets whether the request was made by Ajax (Asynchronous JavaScript)
///
public bool IsAjax { get; private set; }
///
/// Gets cookies that was sent with the request.
///
public RequestCookies Cookies { get; private set; }
public double ArrivalTS { get; set;}
///
///Creates a new object that is a copy of the current instance.
///
///
///
///A new object that is a copy of this instance.
///
///2
public object Clone()
{
// this method was mainly created for testing.
// dont use it that much...
var request = new HttpRequest(Context)
{
Method = m_method,
m_httpVersion = m_httpVersion,
m_queryString = m_queryString,
Uri = m_uri
};
if (AcceptTypes != null)
{
request.AcceptTypes = new string[AcceptTypes.Length];
AcceptTypes.CopyTo(request.AcceptTypes, 0);
}
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;
}
///
/// Decode body into a form.
///
/// A list with form decoders.
/// If body contents is not valid for the chosen decoder.
/// If body is still being transferred.
/*
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);
}
*/
///
/// Cookies
///
///the cookies
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;
}
}
}
m_remoteIPEndPoint ??= m_context.LocalIPEndPoint;
return m_remoteIPEndPoint;
}
}
/*
///
/// Create a response object.
///
/// A new .
public IHttpResponse CreateResponse(IHttpClientContext context)
{
return new HttpResponse(context, this);
}
*/
///
/// Called during parsing of a .
///
/// Name of the header, should not be URL encoded
/// Value of the header, should not be URL encoded
/// If a header is incorrect.
public void AddHeader(string name, string value)
{
if (string.IsNullOrEmpty(name))
throw new BadRequestException("Invalid header name: " + name ?? "");
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.");
if (t > MAXCONTENTLENGTH)
throw new OSHttpServer.Exceptions.HttpException(HttpStatusCode.RequestEntityTooLarge,"Request Entity Too Large");
ContentLength = t;
break;
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(Util.SplitSemicolonArray);
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[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(Util.SplitCommaArray);
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;
case "user-agent":
break;
*/
default:
m_headers.Add(name, value);
break;
}
}
///
/// Add bytes to the body
///
/// buffer to read bytes from
/// where to start read
/// number of bytes to read
/// Number of bytes actually read (same as length unless we got all body bytes).
/// If body is not writable
/// bytes is null.
/// offset is out of range.
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;
}
///
/// Clear everything in the request
///
public void Clear()
{
if (m_body != null)
{
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;
m_context = null;
//_form.Clear();
}
#endregion
}
}