replace external httpserver by embedded one (based on same code) - This may still be very bad; clean solution and runprebuild, or clone to clan folder

master
UbitUmarov 2020-04-02 21:44:34 +01:00
parent 650b051cdf
commit 67cd5efab3
54 changed files with 5669 additions and 1630 deletions

View File

@ -125,7 +125,8 @@ namespace OpenSim.Framework.Serialization
ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.SoundWAV] = ASSET_EXTENSION_SEPARATOR + "sound.wav";
ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.Texture] = ASSET_EXTENSION_SEPARATOR + "texture.jp2";
ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.TextureTGA] = ASSET_EXTENSION_SEPARATOR + "texture.tga";
ASSET_TYPE_TO_EXTENSION[(sbyte)OpenSimAssetType.Material] = ASSET_EXTENSION_SEPARATOR + "material.xml"; // Not sure if we'll ever see this
ASSET_TYPE_TO_EXTENSION[(sbyte)OpenSimAssetType.Material] = ASSET_EXTENSION_SEPARATOR + "material.xml";
ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.Settings] = ASSET_EXTENSION_SEPARATOR + "settings.bin";
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "animation.bvh"] = (sbyte)AssetType.Animation;
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "bodypart.txt"] = (sbyte)AssetType.Bodypart;
@ -147,6 +148,7 @@ namespace OpenSim.Framework.Serialization
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "texture.jp2"] = (sbyte)AssetType.Texture;
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "texture.tga"] = (sbyte)AssetType.TextureTGA;
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "material.xml"] = (sbyte)OpenSimAssetType.Material;
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "settings.bin"] = (sbyte)AssetType.Settings;
}
public static string CreateOarLandDataPath(LandData ld)

View File

@ -28,27 +28,24 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Reflection;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Xml;
using HttpServer;
using OSHttpServer;
using tinyHTTPListener = OSHttpServer.OSHttpListener;
using log4net;
using Nwc.XmlRpc;
using OpenMetaverse.StructuredData;
using CoolHTTPListener = HttpServer.HttpListener;
using LogPrio = HttpServer.LogPrio;
using OpenSim.Framework.Monitoring;
using System.IO.Compression;
using System.Security.Cryptography;
using OpenSim.Framework.Servers;
using OpenMetaverse.StructuredData;
namespace OpenSim.Framework.Servers.HttpServer
{
@ -94,8 +91,7 @@ namespace OpenSim.Framework.Servers.HttpServer
private volatile int NotSocketErrors = 0;
public volatile bool HTTPDRunning = false;
// protected HttpListener m_httpListener;
protected CoolHTTPListener m_httpListener2;
protected tinyHTTPListener m_httpListener;
protected Dictionary<string, XmlRpcMethod> m_rpcHandlers = new Dictionary<string, XmlRpcMethod>();
protected Dictionary<string, JsonRPCMethod> jsonRpcHandlers = new Dictionary<string, JsonRPCMethod>();
protected Dictionary<string, bool> m_rpcHandlersKeepAlive = new Dictionary<string, bool>();
@ -121,7 +117,6 @@ namespace OpenSim.Framework.Servers.HttpServer
protected IPAddress m_listenIPAddress = IPAddress.Any;
public string Protocol
{
get { return m_ssl ? "https://" : "http://"; }
@ -1243,7 +1238,7 @@ namespace OpenSim.Framework.Servers.HttpServer
rcn = "SSLCN:" + rcn;
xmlRprcRequest.Params.Add(rcn); // Param[4] or Param[5]
}
try
{
xmlRpcResponse = method(xmlRprcRequest, request.RemoteIPEndPoint);
@ -1955,7 +1950,7 @@ namespace OpenSim.Framework.Servers.HttpServer
if (responsecode == (int)OSHttpStatusCode.RedirectMovedPermanently)
{
response.RedirectLocation = (string)responsedata["str_redirect_location"];
response.AddHeader("Location:", (string)responsedata["str_redirect_location"]);
response.StatusCode = responsecode;
}
@ -2051,32 +2046,32 @@ namespace OpenSim.Framework.Servers.HttpServer
NotSocketErrors = 0;
if (!m_ssl)
{
//m_httpListener.Prefixes.Add("http://+:" + m_port + "/");
//m_httpListener.Prefixes.Add("http://10.1.1.5:" + m_port + "/");
m_httpListener2 = CoolHTTPListener.Create(m_listenIPAddress, (int)m_port);
m_httpListener2.ExceptionThrown += httpServerException;
m_httpListener2.LogWriter = httpserverlog;
m_httpListener = tinyHTTPListener.Create(m_listenIPAddress, (int)m_port);
m_httpListener.ExceptionThrown += httpServerException;
if (DebugLevel > 0)
{
m_httpListener.LogWriter = httpserverlog;
httpserverlog.DebugLevel = 1;
}
// Uncomment this line in addition to those in HttpServerLogWriter
// if you want more detailed trace information from the HttpServer
//m_httpListener2.UseTraceLogs = true;
//m_httpListener2.DisconnectHandler = httpServerDisconnectMonitor;
}
else
{
//m_httpListener.Prefixes.Add("https://+:" + (m_sslport) + "/");
//m_httpListener.Prefixes.Add("http://+:" + m_port + "/");
m_httpListener2 = CoolHTTPListener.Create(IPAddress.Any, (int)m_port, m_cert);
m_httpListener = tinyHTTPListener.Create(IPAddress.Any, (int)m_port, m_cert);
if(m_certificateValidationCallback != null)
m_httpListener2.CertificateValidationCallback = m_certificateValidationCallback;
m_httpListener2.ExceptionThrown += httpServerException;
m_httpListener2.LogWriter = httpserverlog;
m_httpListener.CertificateValidationCallback = m_certificateValidationCallback;
m_httpListener.ExceptionThrown += httpServerException;
if (DebugLevel > 0)
{
m_httpListener.LogWriter = httpserverlog;
httpserverlog.DebugLevel = 1;
}
}
m_httpListener2.RequestReceived += OnRequest;
//m_httpListener.Start();
m_httpListener2.Start(64);
m_httpListener.RequestReceived += OnRequest;
m_httpListener.Start(64);
lock(m_generalLock)
{
@ -2089,13 +2084,6 @@ namespace OpenSim.Framework.Servers.HttpServer
}
HTTPDRunning = true;
//HttpListenerContext context;
//while (true)
//{
// context = m_httpListener.GetContext();
// ThreadPool.UnsafeQueueUserWorkItem(new WaitCallback(HandleRequest), context);
// }
}
catch (Exception e)
{
@ -2155,12 +2143,12 @@ namespace OpenSim.Framework.Servers.HttpServer
m_pollServiceManager.Stop();
}
m_httpListener2.ExceptionThrown -= httpServerException;
m_httpListener.ExceptionThrown -= httpServerException;
//m_httpListener2.DisconnectHandler = null;
m_httpListener2.LogWriter = null;
m_httpListener2.RequestReceived -= OnRequest;
m_httpListener2.Stop();
m_httpListener.LogWriter = null;
m_httpListener.RequestReceived -= OnRequest;
m_httpListener.Stop();
}
catch (NullReferenceException)
{
@ -2305,13 +2293,17 @@ namespace OpenSim.Framework.Servers.HttpServer
///
/// You may also be able to get additional trace information from HttpServer if you uncomment the UseTraceLogs
/// property in StartHttp() for the HttpListener
///
public class HttpServerLogWriter : ILogWriter
{
// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public int DebugLevel {get; set;} = (int)LogPrio.Error;
public void Write(object source, LogPrio priority, string message)
{
/*
if((int)priority < DebugLevel)
return;
switch (priority)
{
case LogPrio.Trace:
@ -2335,8 +2327,6 @@ namespace OpenSim.Framework.Servers.HttpServer
default:
break;
}
*/
return;
}
}

View File

@ -92,17 +92,16 @@ namespace OpenSim.Framework.Servers.HttpServer
Stream OutputStream { get; }
string ProtocolVersion { get; set; }
int Priority { get; set; }
byte[] RawBuffer { get; set; }
int RawBufferStart { get; set; }
int RawBufferLen { get; set; }
/// <summary>
/// Return the output stream feeding the body.
/// </summary>
Stream Body { get; }
/// <summary>
/// Set a redirct location.
/// </summary>
string RedirectLocation { set; }
/// <summary>
/// Chunk transfers.
/// </summary>

View File

@ -1,183 +0,0 @@
/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
namespace OpenSim.Framework.Servers.HttpServer
{
/// <sumary>
/// Any OSHttpHandler must return one of the following results:
/// <list type = "table">
/// <listheader>
/// <term>result code</term>
/// <description>meaning</description>
/// </listheader>
/// <item>
/// <term>Pass</term>
/// <description>handler did not process the request</request>
/// </item>
/// <item>
/// <term>Done</term>
/// <description>handler did process the request, OSHttpServer
/// can clean up and close the request</request>
/// </item>
/// </list>
/// </summary>
public enum OSHttpHandlerResult
{
Unprocessed,
Pass,
Done,
}
/// <summary>
/// An OSHttpHandler that matches on the "content-type" header can
/// supply an OSHttpContentTypeChecker delegate which will be
/// invoked by the request matcher in OSHttpRequestPump.
/// </summary>
/// <returns>true if the handler is interested in the content;
/// false otherwise</returns>
public delegate bool OSHttpContentTypeChecker(OSHttpRequest req);
public abstract class OSHttpHandler
{
/// <summary>
/// Regular expression used to match against method of
/// the incoming HTTP request. If you want to match any string
/// either use '.*' or null. To match on the empty string use
/// '^$'.
/// </summary>
public virtual Regex Method
{
get { return _method; }
}
protected Regex _method;
/// <summary>
/// Regular expression used to match against path of the
/// incoming HTTP request. If you want to match any string
/// either use '.*' or null. To match on the empty string use
/// '^$'.
/// </summary>
public virtual Regex Path
{
get { return _path; }
}
protected Regex _path;
/// <summary>
/// Dictionary of (query name, regular expression) tuples,
/// allowing us to match on URI query fields.
/// </summary>
public virtual Dictionary<string, Regex> Query
{
get { return _query; }
}
protected Dictionary<string, Regex> _query;
/// <summary>
/// Dictionary of (header name, regular expression) tuples,
/// allowing us to match on HTTP header fields.
/// </summary>
public virtual Dictionary<string, Regex> Headers
{
get { return _headers; }
}
protected Dictionary<string, Regex> _headers;
/// <summary>
/// Dictionary of (header name, regular expression) tuples,
/// allowing us to match on HTTP header fields.
/// </summary>
/// <remarks>
/// This feature is currently not implemented as it requires
/// (trivial) changes to HttpServer.HttpListener that have not
/// been implemented.
/// </remarks>
public virtual Regex IPEndPointWhitelist
{
get { return _ipEndPointRegex; }
}
protected Regex _ipEndPointRegex;
/// <summary>
/// Base class constructor.
/// </summary>
/// <param name="path">null or path regex</param>
/// <param name="headers">null or dictionary of header
/// regexs</param>
/// <param name="contentType">null or content type
/// regex</param>
/// <param name="whitelist">null or IP address regex</param>
public OSHttpHandler(Regex method, Regex path, Dictionary<string, Regex> query,
Dictionary<string, Regex> headers, Regex contentType, Regex whitelist)
{
_method = method;
_path = path;
_query = query;
_ipEndPointRegex = whitelist;
if (null == _headers && null != contentType)
{
_headers = new Dictionary<string, Regex>();
_headers.Add("content-type", contentType);
}
}
/// <summary>
/// Process an incoming OSHttpRequest that matched our
/// requirements.
/// </summary>
/// <returns>
/// OSHttpHandlerResult.Pass if we are after all not
/// interested in the request; OSHttpHandlerResult.Done if we
/// did process the request.
/// </returns>
public abstract OSHttpHandlerResult Process(OSHttpRequest request);
public override string ToString()
{
StringWriter sw = new StringWriter();
sw.WriteLine("{0}", base.ToString());
sw.WriteLine(" method regex {0}", null == Method ? "null" : Method.ToString());
sw.WriteLine(" path regex {0}", null == Path ? "null": Path.ToString());
foreach (string tag in Headers.Keys)
{
sw.WriteLine(" header {0} : {1}", tag, Headers[tag].ToString());
}
sw.WriteLine(" IP whitelist {0}", null == IPEndPointWhitelist ? "null" : IPEndPointWhitelist.ToString());
sw.WriteLine();
sw.Close();
return sw.ToString();
}
}
}

View File

@ -1,145 +0,0 @@
/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using log4net;
using Nwc.XmlRpc;
namespace OpenSim.Framework.Servers.HttpServer
{
public delegate XmlRpcResponse OSHttpHttpProcessor(XmlRpcRequest request);
public class OSHttpHttpHandler: OSHttpHandler
{
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
// contains handler for processing HTTP Request
private GenericHTTPMethod _handler;
/// <summary>
/// Instantiate an HTTP handler.
/// </summary>
/// <param name="handler">a GenericHTTPMethod</param>
/// <param name="method">null or HTTP method regex</param>
/// <param name="path">null or path regex</param>
/// <param name="query">null or dictionary with query regexs</param>
/// <param name="headers">null or dictionary with header
/// regexs</param>
/// <param name="whitelist">null or IP address whitelist</param>
public OSHttpHttpHandler(GenericHTTPMethod handler, Regex method, Regex path,
Dictionary<string, Regex> query,
Dictionary<string, Regex> headers, Regex whitelist)
: base(method, path, query, headers, new Regex(@"^text/html", RegexOptions.IgnoreCase | RegexOptions.Compiled),
whitelist)
{
_handler = handler;
}
/// <summary>
/// Instantiate an HTTP handler.
/// </summary>
/// <param name="handler">a GenericHTTPMethod</param>
public OSHttpHttpHandler(GenericHTTPMethod handler)
: this(handler, new Regex(@"^GET$", RegexOptions.IgnoreCase | RegexOptions.Compiled), null, null, null, null)
{
}
/// <summary>
/// Invoked by OSHttpRequestPump.
/// </summary>
public override OSHttpHandlerResult Process(OSHttpRequest request)
{
// call handler method
Hashtable responseData = _handler(request.Query);
int responseCode = (int)responseData["int_response_code"];
string responseString = (string)responseData["str_response_string"];
string contentType = (string)responseData["content_type"];
//Even though only one other part of the entire code uses HTTPHandlers, we shouldn't expect this
//and should check for NullReferenceExceptions
if (string.IsNullOrEmpty(contentType))
{
contentType = "text/html";
}
OSHttpResponse response = new OSHttpResponse(request);
// We're forgoing the usual error status codes here because the client
// ignores anything but 200 and 301
response.StatusCode = (int)OSHttpStatusCode.SuccessOk;
if (responseCode == (int)OSHttpStatusCode.RedirectMovedPermanently)
{
response.RedirectLocation = (string)responseData["str_redirect_location"];
response.StatusCode = responseCode;
}
response.AddHeader("Content-type", contentType);
byte[] buffer;
if (!contentType.Contains("image"))
{
buffer = Encoding.UTF8.GetBytes(responseString);
}
else
{
buffer = Convert.FromBase64String(responseString);
}
response.SendChunked = false;
response.ContentLength64 = buffer.Length;
response.ContentEncoding = Encoding.UTF8;
try
{
response.Body.Write(buffer, 0, buffer.Length);
}
catch (Exception ex)
{
_log.ErrorFormat("[OSHttpHttpHandler]: Error: {0}", ex.Message);
}
finally
{
response.Send();
}
return OSHttpHandlerResult.Done;
}
}
}

View File

@ -34,7 +34,7 @@ using System.Net;
using System.Reflection;
using System.Text;
using System.Web;
using HttpServer;
using OSHttpServer;
using log4net;
namespace OpenSim.Framework.Servers.HttpServer

View File

@ -1,298 +0,0 @@
/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// #define DEBUGGING
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using log4net;
using HttpServer;
namespace OpenSim.Framework.Servers.HttpServer
{
/// <summary>
/// An OSHttpRequestPump fetches incoming OSHttpRequest objects
/// from the OSHttpRequestQueue and feeds them to all subscribed
/// parties. Each OSHttpRequestPump encapsulates one thread to do
/// the work and there is a fixed number of pumps for each
/// OSHttpServer object.
/// </summary>
public class OSHttpRequestPump
{
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
protected OSHttpServer _server;
protected OSHttpRequestQueue _queue;
protected Thread _engine;
private int _id;
public string EngineID
{
get { return String.Format("{0} pump {1}", _server.EngineID, _id); }
}
public OSHttpRequestPump(OSHttpServer server, OSHttpRequestQueue queue, int id)
{
_server = server;
_queue = queue;
_id = id;
_engine = new Thread(new ThreadStart(Engine));
_engine.IsBackground = true;
_engine.Start();
_engine.Name = string.Format ("Engine:{0}",EngineID);
ThreadTracker.Add(_engine);
}
public static OSHttpRequestPump[] Pumps(OSHttpServer server, OSHttpRequestQueue queue, int poolSize)
{
OSHttpRequestPump[] pumps = new OSHttpRequestPump[poolSize];
for (int i = 0; i < pumps.Length; i++)
{
pumps[i] = new OSHttpRequestPump(server, queue, i);
}
return pumps;
}
public void Start()
{
_engine = new Thread(new ThreadStart(Engine));
_engine.IsBackground = true;
_engine.Start();
_engine.Name = string.Format ("Engine:{0}",EngineID);
ThreadTracker.Add(_engine);
}
public void Engine()
{
OSHttpRequest req = null;
while (true)
{
try
{
// dequeue an OSHttpRequest from OSHttpServer's
// request queue
req = _queue.Dequeue();
// get a copy of the list of registered handlers
List<OSHttpHandler> handlers = _server.OSHttpHandlers;
// prune list and have it sorted from most
// specific to least specific
handlers = MatchHandlers(req, handlers);
// process req: we try each handler in turn until
// we are either out of handlers or get back a
// Pass or Done
OSHttpHandlerResult rc = OSHttpHandlerResult.Unprocessed;
foreach (OSHttpHandler h in handlers)
{
rc = h.Process(req);
// Pass: handler did not process the request,
// try next handler
if (OSHttpHandlerResult.Pass == rc) continue;
// Handled: handler has processed the request
if (OSHttpHandlerResult.Done == rc) break;
// hmm, something went wrong
throw new Exception(String.Format("[{0}] got unexpected OSHttpHandlerResult {1}", EngineID, rc));
}
if (OSHttpHandlerResult.Unprocessed == rc)
{
_log.InfoFormat("[{0}] OSHttpHandler: no handler registered for {1}", EngineID, req);
// set up response header
OSHttpResponse resp = new OSHttpResponse(req);
resp.StatusCode = (int)OSHttpStatusCode.ClientErrorNotFound;
resp.StatusDescription = String.Format("no handler on call for {0}", req);
resp.ContentType = "text/html";
// add explanatory message
StreamWriter body = new StreamWriter(resp.Body);
body.WriteLine("<html>");
body.WriteLine("<header><title>Ooops...</title><header>");
body.WriteLine(String.Format("<body><p>{0}</p></body>", resp.StatusDescription));
body.WriteLine("</html>");
body.Flush();
// and ship it back
resp.Send();
}
}
catch (Exception e)
{
_log.DebugFormat("[{0}] OSHttpHandler problem: {1}", EngineID, e.ToString());
_log.ErrorFormat("[{0}] OSHttpHandler problem: {1}", EngineID, e.Message);
}
}
}
protected List<OSHttpHandler> MatchHandlers(OSHttpRequest req, List<OSHttpHandler> handlers)
{
Dictionary<OSHttpHandler, int> scoredHandlers = new Dictionary<OSHttpHandler, int>();
_log.DebugFormat("[{0}] MatchHandlers for {1}", EngineID, req);
foreach (OSHttpHandler h in handlers)
{
// initial anchor
scoredHandlers[h] = 0;
// first, check whether IPEndPointWhitelist applies
// and, if it does, whether client is on that white
// list.
if (null != h.IPEndPointWhitelist)
{
// TODO: following code requires code changes to
// HttpServer.HttpRequest to become functional
IPEndPoint remote = req.RemoteIPEndPoint;
if (null != remote)
{
Match epm = h.IPEndPointWhitelist.Match(remote.ToString());
if (!epm.Success)
{
scoredHandlers.Remove(h);
continue;
}
}
}
if (null != h.Method)
{
Match m = h.Method.Match(req.HttpMethod);
if (!m.Success)
{
scoredHandlers.Remove(h);
continue;
}
scoredHandlers[h]++;
}
// whitelist ok, now check path
if (null != h.Path)
{
Match m = h.Path.Match(req.RawUrl);
if (!m.Success)
{
scoredHandlers.Remove(h);
continue;
}
scoredHandlers[h] += m.ToString().Length;
}
// whitelist & path ok, now check query string
if (null != h.Query)
{
int queriesMatch = MatchOnNameValueCollection(req.QueryString, h.Query);
if (0 == queriesMatch)
{
_log.DebugFormat("[{0}] request {1}", EngineID, req);
_log.DebugFormat("[{0}] dropping handler {1}", EngineID, h);
scoredHandlers.Remove(h);
continue;
}
scoredHandlers[h] += queriesMatch;
}
// whitelist, path, query string ok, now check headers
if (null != h.Headers)
{
int headersMatch = MatchOnNameValueCollection(req.Headers, h.Headers);
if (0 == headersMatch)
{
_log.DebugFormat("[{0}] request {1}", EngineID, req);
_log.DebugFormat("[{0}] dropping handler {1}", EngineID, h);
scoredHandlers.Remove(h);
continue;
}
scoredHandlers[h] += headersMatch;
}
}
List<OSHttpHandler> matchingHandlers = new List<OSHttpHandler>(scoredHandlers.Keys);
matchingHandlers.Sort(delegate(OSHttpHandler x, OSHttpHandler y)
{
return scoredHandlers[x] - scoredHandlers[y];
});
LogDumpHandlerList(matchingHandlers);
return matchingHandlers;
}
protected int MatchOnNameValueCollection(NameValueCollection collection, Dictionary<string, Regex> regexs)
{
int matched = 0;
foreach (string tag in regexs.Keys)
{
// do we have a header "tag"?
if (null == collection[tag])
{
return 0;
}
// does the content of collection[tag] match
// the supplied regex?
Match cm = regexs[tag].Match(collection[tag]);
if (!cm.Success)
{
return 0;
}
// ok: matches
matched++;
continue;
}
return matched;
}
[ConditionalAttribute("DEBUGGING")]
private void LogDumpHandlerList(List<OSHttpHandler> l)
{
_log.DebugFormat("[{0}] OSHttpHandlerList dump:", EngineID);
foreach (OSHttpHandler h in l)
_log.DebugFormat(" ", h.ToString());
}
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Threading;
using HttpServer;
namespace OpenSim.Framework.Servers.HttpServer
{
/// <summary>
/// OSHttpRequestQueues are used to hand over incoming HTTP
/// requests to OSHttpRequestPump objects.
/// </summary>
public class OSHttpRequestQueue : Queue<OSHttpRequest>
{
private object _syncObject = new object();
new public void Enqueue(OSHttpRequest req)
{
lock (_syncObject)
{
base.Enqueue(req);
Monitor.Pulse(_syncObject);
}
}
new public OSHttpRequest Dequeue()
{
OSHttpRequest req = null;
lock (_syncObject)
{
while (null == req)
{
Monitor.Wait(_syncObject);
if (0 != this.Count) req = base.Dequeue();
}
}
return req;
}
}
}

View File

@ -28,7 +28,7 @@
using System.IO;
using System.Net;
using System.Text;
using HttpServer;
using OSHttpServer;
namespace OpenSim.Framework.Servers.HttpServer
{
@ -243,19 +243,6 @@ namespace OpenSim.Framework.Servers.HttpServer
}
}
/// <summary>
/// Set a redirct location.
/// </summary>
public string RedirectLocation
{
// get { return _redirectLocation; }
set
{
_httpResponse.Redirect(value);
}
}
/// <summary>
/// Chunk transfers.
/// </summary>

View File

@ -1,210 +0,0 @@
/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Security.Cryptography.X509Certificates;
using log4net;
using HttpServer;
using HttpListener = HttpServer.HttpListener;
namespace OpenSim.Framework.Servers.HttpServer
{
/// <summary>
/// OSHttpServer provides an HTTP server bound to a specific
/// port. When instantiated with just address and port it uses
/// normal HTTP, when instantiated with address, port, and X509
/// certificate, it uses HTTPS.
/// </summary>
public class OSHttpServer
{
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
private object _syncObject = new object();
// underlying HttpServer.HttpListener
protected HttpListener _listener;
// underlying core/engine thread
protected Thread _engine;
// Queue containing (OS)HttpRequests
protected OSHttpRequestQueue _queue;
// OSHttpRequestPumps "pumping" incoming OSHttpRequests
// upwards
protected OSHttpRequestPump[] _pumps;
// thread identifier
protected string _engineId;
public string EngineID
{
get { return _engineId; }
}
/// <summary>
/// True if this is an HTTPS connection; false otherwise.
/// </summary>
protected bool _isSecure;
public bool IsSecure
{
get { return _isSecure; }
}
public int QueueSize
{
get { return _pumps.Length; }
}
/// <summary>
/// List of registered OSHttpHandlers for this OSHttpServer instance.
/// </summary>
protected List<OSHttpHandler> _httpHandlers = new List<OSHttpHandler>();
public List<OSHttpHandler> OSHttpHandlers
{
get
{
lock (_httpHandlers)
{
return new List<OSHttpHandler>(_httpHandlers);
}
}
}
/// <summary>
/// Instantiate an HTTP server.
/// </summary>
public OSHttpServer(IPAddress address, int port, int poolSize)
{
_engineId = String.Format("OSHttpServer (HTTP:{0})", port);
_isSecure = false;
_log.DebugFormat("[{0}] HTTP server instantiated", EngineID);
_listener = new HttpListener(address, port);
_queue = new OSHttpRequestQueue();
_pumps = OSHttpRequestPump.Pumps(this, _queue, poolSize);
}
/// <summary>
/// Instantiate an HTTPS server.
/// </summary>
public OSHttpServer(IPAddress address, int port, X509Certificate certificate, int poolSize)
{
_engineId = String.Format("OSHttpServer [HTTPS:{0}/ps:{1}]", port, poolSize);
_isSecure = true;
_log.DebugFormat("[{0}] HTTPS server instantiated", EngineID);
_listener = new HttpListener(address, port, certificate);
_queue = new OSHttpRequestQueue();
_pumps = OSHttpRequestPump.Pumps(this, _queue, poolSize);
}
/// <summary>
/// Turn an HttpRequest into an OSHttpRequestItem and place it
/// in the queue. The OSHttpRequestQueue object will pulse the
/// next available idle pump.
/// </summary>
protected void OnHttpRequest(HttpClientContext client, HttpRequest request)
{
// turn request into OSHttpRequest
OSHttpRequest req = new OSHttpRequest(client, request);
// place OSHttpRequest into _httpRequestQueue, will
// trigger Pulse to idle waiting pumps
_queue.Enqueue(req);
}
/// <summary>
/// Start the HTTP server engine.
/// </summary>
public void Start()
{
_engine = new Thread(new ThreadStart(Engine));
_engine.IsBackground = true;
_engine.Start();
_engine.Name = string.Format ("Engine:{0}",_engineId);
ThreadTracker.Add(_engine);
// start the pumps...
for (int i = 0; i < _pumps.Length; i++)
_pumps[i].Start();
}
public void Stop()
{
lock (_syncObject) Monitor.Pulse(_syncObject);
}
/// <summary>
/// Engine keeps the HTTP server running.
/// </summary>
private void Engine()
{
try {
_listener.RequestHandler += OnHttpRequest;
_listener.Start(QueueSize);
_log.InfoFormat("[{0}] HTTP server started", EngineID);
lock (_syncObject) Monitor.Wait(_syncObject);
}
catch (Exception ex)
{
_log.DebugFormat("[{0}] HTTP server startup failed: {1}", EngineID, ex.ToString());
}
_log.InfoFormat("[{0}] HTTP server terminated", EngineID);
}
/// <summary>
/// Add an HTTP request handler.
/// </summary>
/// <param name="handler">OSHttpHandler delegate</param>
/// <param name="path">regex object for path matching</parm>
/// <param name="headers">dictionary containing header names
/// and regular expressions to match against header values</param>
public void AddHandler(OSHttpHandler handler)
{
lock (_httpHandlers)
{
if (_httpHandlers.Contains(handler))
{
_log.DebugFormat("[OSHttpServer] attempt to add already existing handler ignored");
return;
}
_httpHandlers.Add(handler);
}
}
}
}

View File

@ -0,0 +1,33 @@
using System;
using System.Net;
namespace OSHttpServer.Exceptions
{
/// <summary>
/// The request could not be understood by the server due to malformed syntax.
/// The client SHOULD NOT repeat the request without modifications.
///
/// Text taken from: http://www.submissionchamber.com/help-guides/error-codes.php
/// </summary>
public class BadRequestException : HttpException
{
/// <summary>
/// Create a new bad request exception.
/// </summary>
/// <param name="errMsg">reason to why the request was bad.</param>
public BadRequestException(string errMsg)
: base(HttpStatusCode.BadRequest, errMsg)
{
}
/// <summary>
/// Create a new bad request exception.
/// </summary>
/// <param name="errMsg">reason to why the request was bad.</param>
/// <param name="inner">inner exception</param>
public BadRequestException(string errMsg, Exception inner)
: base(HttpStatusCode.BadRequest, errMsg, inner)
{
}
}
}

View File

@ -0,0 +1,55 @@
using System;
namespace OSHttpServer.Parser
{
/// <summary>
/// Arguments used when more body bytes have come.
/// </summary>
public class BodyEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="BodyEventArgs"/> class.
/// </summary>
/// <param name="buffer">buffer that contains the received bytes.</param>
/// <param name="offset">offset in buffer where to start processing.</param>
/// <param name="count">number of bytes from <paramref name="offset"/> that should be parsed.</param>
public BodyEventArgs(byte[] buffer, int offset, int count)
{
Buffer = buffer;
Offset = offset;
Count = count;
}
/// <summary>
/// Initializes a new instance of the <see cref="BodyEventArgs"/> class.
/// </summary>
public BodyEventArgs()
{
}
/// <summary>
/// Gets or sets buffer that contains the received bytes.
/// </summary>
public byte[] Buffer { get; set; }
/*
/// <summary>
/// Gets or sets number of bytes used by the request.
/// </summary>
public int BytesUsed { get; set; }
*/
/// <summary>
/// Gets or sets number of bytes from <see cref="Offset"/> that should be parsed.
/// </summary>
public int Count { get; set; }
/*
/// <summary>
/// Gets or sets whether the body is complete.
/// </summary>
public bool IsBodyComplete { get; set; }
*/
/// <summary>
/// Gets or sets offset in buffer where to start processing.
/// </summary>
public int Offset { get; set; }
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Net.Sockets;
namespace OSHttpServer
{
/// <summary>
/// Invoked when a client have been accepted by the <see cref="OSHttpListener"/>
/// </summary>
/// <remarks>
/// Can be used to revoke incoming connections
/// </remarks>
public class ClientAcceptedEventArgs : EventArgs
{
private readonly Socket _socket;
private bool _revoke;
/// <summary>
/// Initializes a new instance of the <see cref="ClientAcceptedEventArgs"/> class.
/// </summary>
/// <param name="socket">The socket.</param>
public ClientAcceptedEventArgs(Socket socket)
{
_socket = socket;
}
/// <summary>
/// Accepted socket.
/// </summary>
public Socket Socket
{
get { return _socket; }
}
/// <summary>
/// Client should be revoked.
/// </summary>
public bool Revoked
{
get { return _revoke; }
}
/// <summary>
/// Client may not be handled.
/// </summary>
public void Revoke()
{
_revoke = true;
}
}
}

View File

@ -0,0 +1,413 @@
/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace OSHttpServer
{
/// <summary>
/// Timeout Manager. Checks for dead clients. Clients with open connections that are not doing anything. Closes sessions opened with keepalive.
/// </summary>
public static class ContextTimeoutManager
{
/// <summary>
/// Use a Thread or a Timer to monitor the ugly
/// </summary>
private static Thread m_internalThread = null;
private static object m_threadLock = new object();
private static ConcurrentQueue<HttpClientContext> m_contexts = new ConcurrentQueue<HttpClientContext>();
private static ConcurrentQueue<HttpClientContext> m_highPrio = new ConcurrentQueue<HttpClientContext>();
private static ConcurrentQueue<HttpClientContext> m_midPrio = new ConcurrentQueue<HttpClientContext>();
private static ConcurrentQueue<HttpClientContext> m_lowPrio = new ConcurrentQueue<HttpClientContext>();
private static AutoResetEvent m_processWaitEven = new AutoResetEvent(false);
private static bool m_shuttingDown;
private static int m_ActiveSendingCount;
private static double m_lastTimeOutCheckTime = 0;
private static double m_lastSendCheckTime = 0;
const int m_maxBandWidth = 10485760; //80Mbps
const int m_maxConcurrenSend = 32;
static ContextTimeoutManager()
{
TimeStampClockPeriod = 1.0 / (double)Stopwatch.Frequency;
TimeStampClockPeriodMS = 1e3 / (double)Stopwatch.Frequency;
}
public static void Start()
{
lock (m_threadLock)
{
if (m_internalThread != null)
return;
m_lastTimeOutCheckTime = GetTimeStampMS();
m_internalThread = new Thread(ThreadRunProcess);
m_internalThread.Priority = ThreadPriority.Normal;
m_internalThread.IsBackground = true;
m_internalThread.CurrentCulture = new CultureInfo("en-US", false);
m_internalThread.Name = "HttpServerMain";
m_internalThread.Start();
}
}
public static void Stop()
{
m_shuttingDown = true;
m_internalThread.Join();
ProcessShutDown();
}
private static void ThreadRunProcess()
{
while (!m_shuttingDown)
{
m_processWaitEven.WaitOne(100);
if(m_shuttingDown)
return;
double now = GetTimeStampMS();
if(m_contexts.Count > 0)
{
ProcessSendQueues(now);
if (now - m_lastTimeOutCheckTime > 1000)
{
ProcessContextTimeouts();
m_lastTimeOutCheckTime = now;
}
}
else
m_lastTimeOutCheckTime = now;
}
}
public static void ProcessShutDown()
{
try
{
SocketError disconnectError = SocketError.HostDown;
for (int i = 0; i < m_contexts.Count; i++)
{
HttpClientContext context = null;
if (m_contexts.TryDequeue(out context))
{
try
{
context.Disconnect(disconnectError);
}
catch { }
}
}
m_processWaitEven.Dispose();
m_processWaitEven = null;
}
catch
{
// We can't let this crash.
}
}
public static void ProcessSendQueues(double now)
{
int inqueues = m_highPrio.Count + m_midPrio.Count + m_lowPrio.Count;
if(inqueues == 0)
return;
double dt = now - m_lastSendCheckTime;
m_lastSendCheckTime = now;
int totalSending = m_ActiveSendingCount;
int curConcurrentLimit = m_maxConcurrenSend - totalSending;
if(curConcurrentLimit <= 0)
return;
if(curConcurrentLimit > inqueues)
curConcurrentLimit = inqueues;
if (dt > 0.1)
dt = 0.1;
dt /= curConcurrentLimit;
int curbytesLimit = (int)(m_maxBandWidth * dt);
if(curbytesLimit < 8192)
curbytesLimit = 8192;
HttpClientContext ctx;
int sent;
while (curConcurrentLimit > 0)
{
sent = 0;
while (m_highPrio.TryDequeue(out ctx))
{
if(TrySend(ctx, curbytesLimit))
m_highPrio.Enqueue(ctx);
if (m_shuttingDown)
return;
--curConcurrentLimit;
if (++sent == 4)
break;
}
sent = 0;
while(m_midPrio.TryDequeue(out ctx))
{
if(TrySend(ctx, curbytesLimit))
m_midPrio.Enqueue(ctx);
if (m_shuttingDown)
return;
--curConcurrentLimit;
if (++sent >= 2)
break;
}
if (m_lowPrio.TryDequeue(out ctx))
{
--curConcurrentLimit;
if(TrySend(ctx, curbytesLimit))
m_lowPrio.Enqueue(ctx);
}
if (m_shuttingDown)
return;
}
}
private static bool TrySend(HttpClientContext ctx, int bytesLimit)
{
if(!ctx.CanSend())
return false;
return ctx.TrySendResponse(bytesLimit);
}
/// <summary>
/// Causes the watcher to immediately check the connections.
/// </summary>
public static void ProcessContextTimeouts()
{
try
{
for (int i = 0; i < m_contexts.Count; i++)
{
if (m_shuttingDown)
return;
if (m_contexts.TryDequeue(out HttpClientContext context))
{
if (!ContextTimedOut(context, out SocketError disconnectError))
m_contexts.Enqueue(context);
else if(disconnectError != SocketError.InProgress)
context.Disconnect(disconnectError);
}
}
}
catch
{
// We can't let this crash.
}
}
private static bool ContextTimedOut(HttpClientContext context, out SocketError disconnectError)
{
disconnectError = SocketError.InProgress;
// First our error conditions
if (context.contextID < 0 || context.StopMonitoring || context.StreamPassedOff)
return true;
// Now we start checking for actual timeouts
// First we check that we got at least one line within context.TimeoutFirstLine milliseconds
if (!context.FirstRequestLineReceived)
{
if (EnvironmentTickCountAdd(context.TimeoutFirstLine, context.MonitorStartMS) <= EnvironmentTickCount())
{
disconnectError = SocketError.TimedOut;
context.MonitorStartMS = 0;
return true;
}
}
if (!context.FullRequestReceived)
{
if (EnvironmentTickCountAdd(context.TimeoutRequestReceived, context.MonitorStartMS) <= EnvironmentTickCount())
{
disconnectError = SocketError.TimedOut;
context.MonitorStartMS = 0;
return true;
}
}
//
if (!context.FullRequestProcessed)
{
if (EnvironmentTickCountAdd(context.TimeoutFullRequestProcessed, context.MonitorStartMS) <= EnvironmentTickCount())
{
disconnectError = SocketError.TimedOut;
context.MonitorStartMS = 0;
return true;
}
}
if (context.TriggerKeepalive)
{
context.TriggerKeepalive = false;
context.MonitorKeepaliveMS = EnvironmentTickCount();
}
if (context.FullRequestProcessed && context.MonitorKeepaliveMS == 0)
return true;
if (context.MonitorKeepaliveMS != 0 &&
EnvironmentTickCountAdd(context.TimeoutKeepAlive, context.MonitorKeepaliveMS) <= EnvironmentTickCount())
{
disconnectError = SocketError.TimedOut;
context.MonitorStartMS = 0;
context.MonitorKeepaliveMS = 0;
return true;
}
return false;
}
public static void StartMonitoringContext(HttpClientContext context)
{
context.MonitorStartMS = EnvironmentTickCount();
m_contexts.Enqueue(context);
}
public static void EnqueueSend(HttpClientContext context, int priority)
{
switch(priority)
{
case 0:
m_highPrio.Enqueue(context);
break;
case 1:
m_midPrio.Enqueue(context);
break;
case 2:
m_lowPrio.Enqueue(context);
break;
default:
return;
}
m_processWaitEven.Set();
}
public static void ContextEnterActiveSend()
{
Interlocked.Increment(ref m_ActiveSendingCount);
}
public static void ContextLeaveActiveSend()
{
Interlocked.Decrement(ref m_ActiveSendingCount);
}
/// <summary>
/// Environment.TickCount is an int but it counts all 32 bits so it goes positive
/// and negative every 24.9 days. This trims down TickCount so it doesn't wrap
/// for the callers.
/// This trims it to a 12 day interval so don't let your frame time get too long.
/// </summary>
/// <returns></returns>
public static int EnvironmentTickCount()
{
return Environment.TickCount & EnvironmentTickCountMask;
}
const int EnvironmentTickCountMask = 0x3fffffff;
/// <summary>
/// Environment.TickCount is an int but it counts all 32 bits so it goes positive
/// and negative every 24.9 days. Subtracts the passed value (previously fetched by
/// 'EnvironmentTickCount()') and accounts for any wrapping.
/// </summary>
/// <param name="newValue"></param>
/// <param name="prevValue"></param>
/// <returns>subtraction of passed prevValue from current Environment.TickCount</returns>
public static int EnvironmentTickCountSubtract(Int32 newValue, Int32 prevValue)
{
int diff = newValue - prevValue;
return (diff >= 0) ? diff : (diff + EnvironmentTickCountMask + 1);
}
/// <summary>
/// Environment.TickCount is an int but it counts all 32 bits so it goes positive
/// and negative every 24.9 days. Subtracts the passed value (previously fetched by
/// 'EnvironmentTickCount()') and accounts for any wrapping.
/// </summary>
/// <param name="newValue"></param>
/// <param name="prevValue"></param>
/// <returns>subtraction of passed prevValue from current Environment.TickCount</returns>
public static int EnvironmentTickCountAdd(Int32 newValue, Int32 prevValue)
{
int ret = newValue + prevValue;
return (ret >= 0) ? ret : (ret + EnvironmentTickCountMask + 1);
}
public static double TimeStampClockPeriodMS;
public static double TimeStampClockPeriod;
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static double GetTimeStamp()
{
return Stopwatch.GetTimestamp() * TimeStampClockPeriod;
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static double GetTimeStampMS()
{
return Stopwatch.GetTimestamp() * TimeStampClockPeriodMS;
}
// doing math in ticks is usefull to avoid loss of resolution
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static long GetTimeStampTicks()
{
return Stopwatch.GetTimestamp();
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static double TimeStampTicksToMS(long ticks)
{
return ticks * TimeStampClockPeriodMS;
}
}
}

View File

@ -0,0 +1,29 @@
using System;
namespace OSHttpServer
{
/// <summary>
/// An unhandled exception have been caught by the system.
/// </summary>
public class ExceptionEventArgs : EventArgs
{
private readonly Exception _exception;
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionEventArgs"/> class.
/// </summary>
/// <param name="exception">Caught exception.</param>
public ExceptionEventArgs(Exception exception)
{
_exception = exception;
}
/// <summary>
/// caught exception
/// </summary>
public Exception Exception
{
get { return _exception; }
}
}
}

View File

@ -0,0 +1,16 @@
using System;
namespace OSHttpServer
{
/// <summary>
/// We dont want to let the server to die due to exceptions thrown in worker threads.
/// therefore we use this delegate to give you a change to handle uncaught exceptions.
/// </summary>
/// <param name="source">Class that the exception was thrown in.</param>
/// <param name="exception">Exception</param>
/// <remarks>
/// Server will throw a InternalServerException in release version if you dont
/// handle this delegate.
/// </remarks>
public delegate void ExceptionHandler(object source, Exception exception);
}

View File

@ -0,0 +1,25 @@
using System.Net;
namespace OSHttpServer.Exceptions
{
/// <summary>
/// The server understood the request, but is refusing to fulfill it.
/// Authorization will not help and the request SHOULD NOT be repeated.
/// If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled,
/// it SHOULD describe the reason for the refusal in the entity. If the server does not wish to make this information
/// available to the client, the status code 404 (Not Found) can be used instead.
///
/// Text taken from: http://www.submissionchamber.com/help-guides/error-codes.php
/// </summary>
public class ForbiddenException : HttpException
{
/// <summary>
/// Initializes a new instance of the <see cref="ForbiddenException"/> class.
/// </summary>
/// <param name="errorMsg">error message</param>
public ForbiddenException(string errorMsg)
: base(HttpStatusCode.Forbidden, errorMsg)
{
}
}
}

View File

@ -0,0 +1,38 @@
using System;
namespace OSHttpServer.Parser
{
/// <summary>
/// Event arguments used when a new header have been parsed.
/// </summary>
public class HeaderEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="HeaderEventArgs"/> class.
/// </summary>
/// <param name="name">Name of header.</param>
/// <param name="value">Header value.</param>
public HeaderEventArgs(string name, string value)
{
Name = name;
Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="HeaderEventArgs"/> class.
/// </summary>
public HeaderEventArgs()
{
}
/// <summary>
/// Gets or sets header name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets header value.
/// </summary>
public string Value { get; set; }
}
}

View File

@ -0,0 +1,786 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using OSHttpServer.Exceptions;
using OSHttpServer.Parser;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace OSHttpServer
{
/// <summary>
/// Contains a connection to a browser/client.
/// </summary>
/// <remarks>
/// Remember to <see cref="Start"/> after you have hooked the <see cref="RequestReceived"/> event.
/// </remarks>
public class HttpClientContext : IHttpClientContext, IDisposable
{
const int MAXREQUESTS = 20;
const int MAXKEEPALIVE = 60000;
static private int basecontextID;
private readonly byte[] m_ReceiveBuffer;
private int m_ReceiveBytesLeft;
private ILogWriter _log;
private readonly IHttpRequestParser m_parser;
private readonly int m_bufferSize;
private HashSet<uint> requestsInServiceIDs;
private Socket m_sock;
public bool Available = true;
public bool StreamPassedOff = false;
public int MonitorStartMS = 0;
public int MonitorKeepaliveMS = 0;
public bool TriggerKeepalive = false;
public int TimeoutFirstLine = 70000; // 70 seconds
public int TimeoutRequestReceived = 180000; // 180 seconds
// The difference between this and request received is on POST more time is needed before we get the full request.
public int TimeoutFullRequestProcessed = 600000; // 10 minutes
public int m_TimeoutKeepAlive = MAXKEEPALIVE; // 400 seconds before keepalive timeout
// public int TimeoutKeepAlive = 120000; // 400 seconds before keepalive timeout
public int m_maxRequests = MAXREQUESTS;
public bool FirstRequestLineReceived;
public bool FullRequestReceived;
public bool FullRequestProcessed;
private bool isSendingResponse = false;
private HttpRequest m_currentRequest;
private HttpResponse m_currentResponse;
public int contextID { get; private set; }
public int TimeoutKeepAlive
{
get { return m_TimeoutKeepAlive; }
set
{
m_TimeoutKeepAlive = (value > MAXKEEPALIVE) ? MAXKEEPALIVE : value;
}
}
public int MAXRequests
{
get { return m_maxRequests; }
set
{
m_maxRequests = value > MAXREQUESTS ? MAXREQUESTS : value;
}
}
public bool IsSending()
{
return isSendingResponse;
}
public bool StopMonitoring;
/// <summary>
/// Context have been started (a new client have connected)
/// </summary>
public event EventHandler Started;
/// <summary>
/// Initializes a new instance of the <see cref="HttpClientContext"/> class.
/// </summary>
/// <param name="secured">true if the connection is secured (SSL/TLS)</param>
/// <param name="remoteEndPoint">client that connected.</param>
/// <param name="stream">Stream used for communication</param>
/// <param name="parserFactory">Used to create a <see cref="IHttpRequestParser"/>.</param>
/// <param name="bufferSize">Size of buffer to use when reading data. Must be at least 4096 bytes.</param>
/// <exception cref="SocketException">If <see cref="Socket.BeginReceive(byte[],int,int,SocketFlags,AsyncCallback,object)"/> fails</exception>
/// <exception cref="ArgumentException">Stream must be writable and readable.</exception>
public HttpClientContext(bool secured, IPEndPoint remoteEndPoint,
Stream stream, IRequestParserFactory parserFactory, Socket sock)
{
if (!stream.CanWrite || !stream.CanRead)
throw new ArgumentException("Stream must be writable and readable.");
RemoteAddress = remoteEndPoint.Address.ToString();
RemotePort = remoteEndPoint.Port.ToString();
_log = NullLogWriter.Instance;
m_parser = parserFactory.CreateParser(_log);
m_parser.RequestCompleted += OnRequestCompleted;
m_parser.RequestLineReceived += OnRequestLine;
m_parser.HeaderReceived += OnHeaderReceived;
m_parser.BodyBytesReceived += OnBodyBytesReceived;
m_currentRequest = new HttpRequest(this);
IsSecured = secured;
_stream = stream;
m_sock = sock;
m_bufferSize = 8196;
m_ReceiveBuffer = new byte[m_bufferSize];
requestsInServiceIDs = new HashSet<uint>();
SSLCommonName = "";
if (secured)
{
SslStream _ssl = (SslStream)_stream;
X509Certificate _cert1 = _ssl.RemoteCertificate;
if (_cert1 != null)
{
X509Certificate2 _cert2 = new X509Certificate2(_cert1);
if (_cert2 != null)
SSLCommonName = _cert2.GetNameInfo(X509NameType.SimpleName, false);
}
}
++basecontextID;
if (basecontextID <= 0)
basecontextID = 1;
contextID = basecontextID;
}
public bool CanSend()
{
if (contextID < 0)
return false;
if (Stream == null || m_sock == null || !m_sock.Connected)
return false;
return true;
}
/// <summary>
/// Process incoming body bytes.
/// </summary>
/// <param name="sender"><see cref="IHttpRequestParser"/></param>
/// <param name="e">Bytes</param>
protected virtual void OnBodyBytesReceived(object sender, BodyEventArgs e)
{
m_currentRequest.AddToBody(e.Buffer, e.Offset, e.Count);
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected virtual void OnHeaderReceived(object sender, HeaderEventArgs e)
{
if (string.Compare(e.Name, "expect", true) == 0 && e.Value.Contains("100-continue"))
{
lock (requestsInServiceIDs)
{
if (requestsInServiceIDs.Count == 0)
Respond("HTTP/1.1", HttpStatusCode.Continue, "Please continue.");
}
}
m_currentRequest.AddHeader(e.Name, e.Value);
}
private void OnRequestLine(object sender, RequestLineEventArgs e)
{
m_currentRequest.Method = e.HttpMethod;
m_currentRequest.HttpVersion = e.HttpVersion;
m_currentRequest.UriPath = e.UriPath;
m_currentRequest.AddHeader("remote_addr", RemoteAddress);
m_currentRequest.AddHeader("remote_port", RemotePort);
FirstRequestLineReceived = true;
TriggerKeepalive = false;
MonitorKeepaliveMS = 0;
}
/// <summary>
/// Start reading content.
/// </summary>
/// <remarks>
/// Make sure to call base.Start() if you override this method.
/// </remarks>
public virtual void Start()
{
ReceiveLoop();
Started?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Clean up context.
/// </summary>
/// <remarks>
/// </remarks>
public virtual void Cleanup()
{
if (StreamPassedOff)
return;
if (Stream != null)
{
Stream.Close();
Stream = null;
m_sock = null;
}
m_currentRequest?.Clear();
m_currentRequest = null;
m_currentResponse?.Clear();
m_currentResponse = null;
requestsInServiceIDs.Clear();
FirstRequestLineReceived = false;
FullRequestReceived = false;
FullRequestProcessed = false;
MonitorStartMS = 0;
StopMonitoring = true;
MonitorKeepaliveMS = 0;
TriggerKeepalive = false;
isSendingResponse = false;
m_ReceiveBytesLeft = 0;
contextID = -100;
m_parser.Clear();
}
public void Close()
{
Dispose();
}
/// <summary>
/// Using SSL or other encryption method.
/// </summary>
[Obsolete("Use IsSecured instead.")]
public bool Secured
{
get { return IsSecured; }
}
/// <summary>
/// Using SSL or other encryption method.
/// </summary>
public bool IsSecured { get; internal set; }
// returns the SSL commonName of remote Certificate
public string SSLCommonName { get; internal set; }
/// <summary>
/// Specify which logger to use.
/// </summary>
public ILogWriter LogWriter
{
get { return _log; }
set
{
_log = value ?? NullLogWriter.Instance;
m_parser.LogWriter = _log;
}
}
private Stream _stream;
/// <summary>
/// Gets or sets the network stream.
/// </summary>
internal Stream Stream
{
get { return _stream; }
set { _stream = value; }
}
/// <summary>
/// Gets or sets IP address that the client connected from.
/// </summary>
internal string RemoteAddress { get; set; }
/// <summary>
/// Gets or sets port that the client connected from.
/// </summary>
internal string RemotePort { get; set; }
/// <summary>
/// Disconnect from client
/// </summary>
/// <param name="error">error to report in the <see cref="Disconnected"/> event.</param>
public void Disconnect(SocketError error)
{
// disconnect may not throw any exceptions
try
{
try
{
if (Stream != null)
{
if (error == SocketError.Success)
Stream.Flush();
Stream.Close();
Stream = null;
}
m_sock = null;
}
catch { }
Disconnected?.Invoke(this, new DisconnectedEventArgs(error));
}
catch (Exception err)
{
LogWriter.Write(this, LogPrio.Error, "Disconnect threw an exception: " + err);
}
}
private void OnReceive(IAsyncResult ar)
{
try
{
int bytesRead = 0;
if (Stream == null)
return;
try
{
bytesRead = Stream.EndRead(ar);
}
catch (NullReferenceException)
{
Disconnect(SocketError.ConnectionReset);
return;
}
if (bytesRead == 0)
{
Disconnect(SocketError.ConnectionReset);
return;
}
m_ReceiveBytesLeft += bytesRead;
if (m_ReceiveBytesLeft > m_ReceiveBuffer.Length)
{
throw new BadRequestException("HTTP header Too large: " + m_ReceiveBytesLeft);
}
int offset = m_parser.Parse(m_ReceiveBuffer, 0, m_ReceiveBytesLeft);
if (Stream == null)
return; // "Connection: Close" in effect.
// try again to see if we can parse another message (check parser to see if it is looking for a new message)
int nextOffset;
int nextBytesleft = m_ReceiveBytesLeft - offset;
while (offset != 0 && nextBytesleft > 0)
{
nextOffset = m_parser.Parse(m_ReceiveBuffer, offset, nextBytesleft);
if (Stream == null)
return; // "Connection: Close" in effect.
if (nextOffset == 0)
break;
offset = nextOffset;
nextBytesleft = m_ReceiveBytesLeft - offset;
}
// copy unused bytes to the beginning of the array
if (offset > 0 && m_ReceiveBytesLeft > offset)
Buffer.BlockCopy(m_ReceiveBuffer, offset, m_ReceiveBuffer, 0, m_ReceiveBytesLeft - offset);
m_ReceiveBytesLeft -= offset;
if (Stream != null && Stream.CanRead)
{
if (!StreamPassedOff)
Stream.BeginRead(m_ReceiveBuffer, m_ReceiveBytesLeft, m_ReceiveBuffer.Length - m_ReceiveBytesLeft, OnReceive, null);
else
{
_log.Write(this, LogPrio.Warning, "Could not read any more from the socket.");
Disconnect(SocketError.Success);
}
}
}
catch (BadRequestException err)
{
LogWriter.Write(this, LogPrio.Warning, "Bad request, responding with it. Error: " + err);
try
{
Respond("HTTP/1.0", HttpStatusCode.BadRequest, err.Message);
}
catch (Exception err2)
{
LogWriter.Write(this, LogPrio.Fatal, "Failed to reply to a bad request. " + err2);
}
Disconnect(SocketError.NoRecovery);
}
catch (IOException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message);
if (err.InnerException is SocketException)
Disconnect((SocketError)((SocketException)err.InnerException).ErrorCode);
else
Disconnect(SocketError.ConnectionReset);
}
catch (ObjectDisposedException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : " + err.Message);
Disconnect(SocketError.NotSocket);
}
catch (NullReferenceException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : NullRef: " + err.Message);
Disconnect(SocketError.NoRecovery);
}
catch (Exception err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message);
Disconnect(SocketError.NoRecovery);
}
}
private async void ReceiveLoop()
{
m_ReceiveBytesLeft = 0;
try
{
while(true)
{
if (_stream == null || !_stream.CanRead)
return;
int bytesRead = await _stream.ReadAsync(m_ReceiveBuffer, m_ReceiveBytesLeft, m_ReceiveBuffer.Length - m_ReceiveBytesLeft).ConfigureAwait(false);
if (bytesRead == 0)
{
Disconnect(SocketError.ConnectionReset);
return;
}
m_ReceiveBytesLeft += bytesRead;
if (m_ReceiveBytesLeft > m_ReceiveBuffer.Length)
throw new BadRequestException("HTTP header Too large: " + m_ReceiveBytesLeft);
int offset = m_parser.Parse(m_ReceiveBuffer, 0, m_ReceiveBytesLeft);
if (Stream == null)
return; // "Connection: Close" in effect.
// try again to see if we can parse another message (check parser to see if it is looking for a new message)
int nextOffset;
int nextBytesleft = m_ReceiveBytesLeft - offset;
while (offset != 0 && nextBytesleft > 0)
{
nextOffset = m_parser.Parse(m_ReceiveBuffer, offset, nextBytesleft);
if (Stream == null)
return; // "Connection: Close" in effect.
if (nextOffset == 0)
break;
offset = nextOffset;
nextBytesleft = m_ReceiveBytesLeft - offset;
}
// copy unused bytes to the beginning of the array
if (offset > 0 && m_ReceiveBytesLeft > offset)
Buffer.BlockCopy(m_ReceiveBuffer, offset, m_ReceiveBuffer, 0, m_ReceiveBytesLeft - offset);
m_ReceiveBytesLeft -= offset;
if (StreamPassedOff)
return; //?
}
}
catch (BadRequestException err)
{
LogWriter.Write(this, LogPrio.Warning, "Bad request, responding with it. Error: " + err);
try
{
Respond("HTTP/1.0", HttpStatusCode.BadRequest, err.Message);
}
catch (Exception err2)
{
LogWriter.Write(this, LogPrio.Fatal, "Failed to reply to a bad request. " + err2);
}
Disconnect(SocketError.NoRecovery);
}
catch (IOException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message);
if (err.InnerException is SocketException)
Disconnect((SocketError)((SocketException)err.InnerException).ErrorCode);
else
Disconnect(SocketError.ConnectionReset);
}
catch (ObjectDisposedException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : " + err.Message);
Disconnect(SocketError.NotSocket);
}
catch (NullReferenceException err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : NullRef: " + err.Message);
Disconnect(SocketError.NoRecovery);
}
catch (Exception err)
{
LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message);
Disconnect(SocketError.NoRecovery);
}
}
private void OnRequestCompleted(object source, EventArgs args)
{
TriggerKeepalive = false;
MonitorKeepaliveMS = 0;
// load cookies if they exist
RequestCookies cookies = m_currentRequest.Headers["cookie"] != null
? new RequestCookies(m_currentRequest.Headers["cookie"]) : new RequestCookies(String.Empty);
m_currentRequest.SetCookies(cookies);
m_currentRequest.Body.Seek(0, SeekOrigin.Begin);
FullRequestReceived = true;
int nreqs;
lock (requestsInServiceIDs)
{
nreqs = requestsInServiceIDs.Count;
requestsInServiceIDs.Add(m_currentRequest.ID);
if (m_maxRequests > 0)
m_maxRequests--;
}
// for now pipeline requests need to be serialized by opensim
RequestReceived(this, new RequestEventArgs(m_currentRequest));
m_currentRequest = new HttpRequest(this);
int nreqsnow;
lock (requestsInServiceIDs)
{
nreqsnow = requestsInServiceIDs.Count;
}
if (nreqs != nreqsnow)
{
// request was not done by us
}
}
public void ReqResponseAboutToSend(uint requestID)
{
isSendingResponse = true;
}
public void StartSendResponse(HttpResponse response)
{
isSendingResponse = true;
m_currentResponse = response;
ContextTimeoutManager.EnqueueSend(this, response.Priority);
}
public bool TrySendResponse(int bytesLimit)
{
if(m_currentResponse == null)
return false;
if (m_currentResponse.Sent)
return false;
if(!CanSend())
return false;
m_currentResponse?.SendNextAsync(bytesLimit);
return false;
}
public void ContinueSendResponse()
{
if(m_currentResponse == null)
return;
ContextTimeoutManager.EnqueueSend(this, m_currentResponse.Priority);
}
public void ReqResponseSent(uint requestID, ConnectionType ctype)
{
isSendingResponse = false;
m_currentResponse?.Clear();
m_currentResponse = null;
bool doclose = ctype == ConnectionType.Close;
lock (requestsInServiceIDs)
{
requestsInServiceIDs.Remove(requestID);
// doclose = doclose && requestsInServiceIDs.Count == 0;
if (requestsInServiceIDs.Count > 1)
{
}
}
if (doclose)
Disconnect(SocketError.Success);
else
{
lock (requestsInServiceIDs)
{
if (requestsInServiceIDs.Count == 0)
TriggerKeepalive = true;
}
}
}
/// <summary>
/// Send a response.
/// </summary>
/// <param name="httpVersion">Either <see cref="HttpHelper.HTTP10"/> or <see cref="HttpHelper.HTTP11"/></param>
/// <param name="statusCode">HTTP status code</param>
/// <param name="reason">reason for the status code.</param>
/// <param name="body">HTML body contents, can be null or empty.</param>
/// <param name="contentType">A content type to return the body as, i.e. 'text/html' or 'text/plain', defaults to 'text/html' if null or empty</param>
/// <exception cref="ArgumentException">If <paramref name="httpVersion"/> is invalid.</exception>
public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType)
{
if (string.IsNullOrEmpty(contentType))
contentType = "text/html";
if (string.IsNullOrEmpty(reason))
reason = statusCode.ToString();
string response = string.IsNullOrEmpty(body)
? httpVersion + " " + (int)statusCode + " " + reason + "\r\n\r\n"
: string.Format("{0} {1} {2}\r\nContent-Type: {5}\r\nContent-Length: {3}\r\n\r\n{4}",
httpVersion, (int)statusCode, reason ?? statusCode.ToString(),
body.Length, body, contentType);
byte[] buffer = Encoding.ASCII.GetBytes(response);
Send(buffer);
if (m_currentRequest.Connection == ConnectionType.Close)
FullRequestProcessed = true;
}
/// <summary>
/// Send a response.
/// </summary>
/// <param name="httpVersion">Either <see cref="HttpHelper.HTTP10"/> or <see cref="HttpHelper.HTTP11"/></param>
/// <param name="statusCode">HTTP status code</param>
/// <param name="reason">reason for the status code.</param>
public void Respond(string httpVersion, HttpStatusCode statusCode, string reason)
{
Respond(httpVersion, statusCode, reason, null, null);
}
/// <summary>
/// send a whole buffer
/// </summary>
/// <param name="buffer">buffer to send</param>
/// <exception cref="ArgumentNullException"></exception>
public bool Send(byte[] buffer)
{
if (buffer == null)
throw new ArgumentNullException("buffer");
return Send(buffer, 0, buffer.Length);
}
/// <summary>
/// Send data using the stream
/// </summary>
/// <param name="buffer">Contains data to send</param>
/// <param name="offset">Start position in buffer</param>
/// <param name="size">number of bytes to send</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
private object sendLock = new object();
public bool Send(byte[] buffer, int offset, int size)
{
if (Stream == null || m_sock == null || !m_sock.Connected)
return false;
if (offset + size > buffer.Length)
throw new ArgumentOutOfRangeException("offset", offset, "offset + size is beyond end of buffer.");
bool ok = true;
lock (sendLock) // can't have overlaps here
{
try
{
Stream.Write(buffer, offset, size);
}
catch
{
ok = false;
}
if (!ok && Stream != null)
Disconnect(SocketError.NoRecovery);
return ok;
}
}
public async Task<bool> SendAsync(byte[] buffer, int offset, int size)
{
if (Stream == null || m_sock == null || !m_sock.Connected)
return false;
if (offset + size > buffer.Length)
throw new ArgumentOutOfRangeException("offset", offset, "offset + size is beyond end of buffer.");
bool ok = true;
ContextTimeoutManager.ContextEnterActiveSend();
try
{
await Stream.WriteAsync(buffer, offset, size).ConfigureAwait(false);
}
catch
{
ok = false;
}
ContextTimeoutManager.ContextLeaveActiveSend();
if (!ok && Stream != null)
Disconnect(SocketError.NoRecovery);
return ok;
}
/// <summary>
/// The context have been disconnected.
/// </summary>
/// <remarks>
/// Event can be used to clean up a context, or to reuse it.
/// </remarks>
public event EventHandler<DisconnectedEventArgs> Disconnected = delegate { };
/// <summary>
/// A request have been received in the context.
/// </summary>
public event EventHandler<RequestEventArgs> RequestReceived = delegate { };
public HTTPNetworkContext GiveMeTheNetworkStreamIKnowWhatImDoing()
{
StreamPassedOff = true;
m_parser.RequestCompleted -= OnRequestCompleted;
m_parser.RequestLineReceived -= OnRequestLine;
m_parser.HeaderReceived -= OnHeaderReceived;
m_parser.BodyBytesReceived -= OnBodyBytesReceived;
m_parser.Clear();
return new HTTPNetworkContext() { Socket = m_sock, Stream = _stream as NetworkStream };
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose(bool disposing)
{
if (contextID >= 0)
{
StreamPassedOff = false;
Cleanup();
}
}
}
}

View File

@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
namespace OSHttpServer
{
/// <summary>
/// Used to create and reuse contexts.
/// </summary>
public class HttpContextFactory : IHttpContextFactory
{
private readonly ConcurrentDictionary<int, HttpClientContext> m_activeContexts = new ConcurrentDictionary<int, HttpClientContext>();
private readonly IRequestParserFactory m_parserfactory;
private readonly ILogWriter m_logWriter;
/// <summary>
/// A request have been received from one of the contexts.
/// </summary>
public event EventHandler<RequestEventArgs> RequestReceived;
/// <summary>
/// Initializes a new instance of the <see cref="HttpContextFactory"/> class.
/// </summary>
/// <param name="writer">The writer.</param>
/// <param name="bufferSize">Amount of bytes to read from the incoming socket stream.</param>
/// <param name="factory">Used to create a request parser.</param>
public HttpContextFactory(ILogWriter writer, IRequestParserFactory factory)
{
m_logWriter = writer;
m_parserfactory = factory;
ContextTimeoutManager.Start();
}
/// <summary>
/// Create a new context.
/// </summary>
/// <param name="isSecured">true if socket is running HTTPS.</param>
/// <param name="endPoint">Client that connected</param>
/// <param name="stream">Network/SSL stream.</param>
/// <returns>A context.</returns>
protected HttpClientContext CreateContext(bool isSecured, IPEndPoint endPoint, Stream stream, Socket sock)
{
HttpClientContext context;
context = CreateNewContext(isSecured, endPoint, stream, sock);
context.Disconnected += OnFreeContext;
context.RequestReceived += OnRequestReceived;
context.Stream = stream;
context.IsSecured = isSecured;
context.RemotePort = endPoint.Port.ToString();
context.RemoteAddress = endPoint.Address.ToString();
ContextTimeoutManager.StartMonitoringContext(context);
m_activeContexts[context.contextID] = context;
context.Start();
return context;
}
/// <summary>
/// Create a new context.
/// </summary>
/// <param name="isSecured">true if HTTPS is used.</param>
/// <param name="endPoint">Remote client</param>
/// <param name="stream">Network stream, <see cref="HttpClientContext"/></param>
/// <returns>A new context (always).</returns>
protected virtual HttpClientContext CreateNewContext(bool isSecured, IPEndPoint endPoint, Stream stream, Socket sock)
{
return new HttpClientContext(isSecured, endPoint, stream, m_parserfactory, sock);
}
private void OnRequestReceived(object sender, RequestEventArgs e)
{
RequestReceived?.Invoke(sender, e);
}
private void OnFreeContext(object sender, DisconnectedEventArgs e)
{
var imp = sender as HttpClientContext;
if (imp == null || imp.contextID < 0)
return;
m_activeContexts.TryRemove(imp.contextID, out HttpClientContext dummy);
imp.Close();
}
#region IHttpContextFactory Members
/// <summary>
/// Create a secure <see cref="IHttpClientContext"/>.
/// </summary>
/// <param name="socket">Client socket (accepted by the <see cref="OSHttpListener"/>).</param>
/// <param name="certificate">HTTPS certificate to use.</param>
/// <param name="protocol">Kind of HTTPS protocol. Usually TLS or SSL.</param>
/// <returns>
/// A created <see cref="IHttpClientContext"/>.
/// </returns>
public IHttpClientContext CreateSecureContext(Socket socket, X509Certificate certificate,
SslProtocols protocol, RemoteCertificateValidationCallback _clientCallback = null)
{
socket.NoDelay = true;
var networkStream = new NetworkStream(socket, true);
var remoteEndPoint = (IPEndPoint)socket.RemoteEndPoint;
SslStream sslStream = null;
try
{
if (_clientCallback == null)
{
sslStream = new SslStream(networkStream, false);
sslStream.AuthenticateAsServer(certificate, false, protocol, false);
}
else
{
sslStream = new SslStream(networkStream, false,
new RemoteCertificateValidationCallback(_clientCallback));
sslStream.AuthenticateAsServer(certificate, true, protocol, false);
}
}
catch (Exception e)
{
m_logWriter.Write(this, LogPrio.Error, e.Message);
sslStream.Close();
return null;
}
return CreateContext(true, remoteEndPoint, sslStream, socket);
}
/// <summary>
/// Creates a <see cref="IHttpClientContext"/> that handles a connected client.
/// </summary>
/// <param name="socket">Client socket (accepted by the <see cref="OSHttpListener"/>).</param>
/// <returns>
/// A creates <see cref="IHttpClientContext"/>.
/// </returns>
public IHttpClientContext CreateContext(Socket socket)
{
socket.NoDelay = true;
var networkStream = new NetworkStream(socket, true);
var remoteEndPoint = (IPEndPoint)socket.RemoteEndPoint;
return CreateContext(false, remoteEndPoint, networkStream, socket);
}
#endregion
/// <summary>
/// Server is shutting down so shut down the factory
/// </summary>
public void Shutdown()
{
ContextTimeoutManager.Stop();
}
}
/// <summary>
/// Used to create <see cref="IHttpClientContext"/>es.
/// </summary>
public interface IHttpContextFactory
{
/// <summary>
/// Creates a <see cref="IHttpClientContext"/> that handles a connected client.
/// </summary>
/// <param name="socket">Client socket (accepted by the <see cref="OSHttpListener"/>).</param>
/// <returns>A creates <see cref="IHttpClientContext"/>.</returns>
IHttpClientContext CreateContext(Socket socket);
/// <summary>
/// Create a secure <see cref="IHttpClientContext"/>.
/// </summary>
/// <param name="socket">Client socket (accepted by the <see cref="OSHttpListener"/>).</param>
/// <param name="certificate">HTTPS certificate to use.</param>
/// <param name="protocol">Kind of HTTPS protocol. Usually TLS or SSL.</param>
/// <returns>A created <see cref="IHttpClientContext"/>.</returns>
IHttpClientContext CreateSecureContext(Socket socket, X509Certificate certificate,
SslProtocols protocol, RemoteCertificateValidationCallback _clientCallback = null);
/// <summary>
/// A request have been received from one of the contexts.
/// </summary>
event EventHandler<RequestEventArgs> RequestReceived;
/// <summary>
/// Server is shutting down so shut down the factory
/// </summary>
void Shutdown();
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Net;
namespace OSHttpServer.Exceptions
{
/// <summary>
/// All HTTP based exceptions will derive this class.
/// </summary>
public class HttpException : Exception
{
private readonly HttpStatusCode _code;
/// <summary>
/// Create a new HttpException
/// </summary>
/// <param name="code">http status code (sent in the response)</param>
/// <param name="message">error description</param>
public HttpException(HttpStatusCode code, string message) : base(code + ": " + message)
{
_code = code;
}
/// <summary>
/// Create a new HttpException
/// </summary>
/// <param name="code">http status code (sent in the response)</param>
/// <param name="message">error description</param>
/// <param name="inner">inner exception</param>
public HttpException(HttpStatusCode code, string message, Exception inner)
: base(code + ": " + message, inner)
{
_code = code;
}
/// <summary>
/// status code to use in the response.
/// </summary>
public HttpStatusCode HttpStatusCode
{
get { return _code; }
}
}
}

View File

@ -0,0 +1,118 @@
using System;
using System.Web;
namespace OSHttpServer
{
/// <summary>
/// Generic helper functions for HTTP
/// </summary>
public static class HttpHelper
{
/// <summary>
/// An empty URI
/// </summary>
public static readonly Uri EmptyUri = new Uri("http://localhost/");
/// <summary>
/// Parses a query string.
/// </summary>
/// <param name="queryString">Query string (URI encoded)</param>
/// <returns>A <see cref="HttpInput"/> object if successful; otherwise <see cref="HttpInput.Empty"/></returns>
/// <exception cref="ArgumentNullException"><c>queryString</c> is null.</exception>
/// <exception cref="FormatException">If string cannot be parsed.</exception>
public static HttpInput ParseQueryString(string queryString)
{
if (queryString == null)
throw new ArgumentNullException("queryString");
if (queryString == string.Empty)
return HttpInput.Empty;
HttpInput input = new HttpInput("QueryString");
queryString = queryString.TrimStart('?', '&');
// a simple value.
if (queryString.IndexOf("&") == -1 && !queryString.Contains("%3d") && !queryString.Contains("%3D") && !queryString.Contains("="))
{
input.Add(string.Empty, queryString);
return input;
}
int state = 0;
int startpos = 0;
string name = null;
for (int i = 0; i < queryString.Length; ++i)
{
int newIndexPos;
if (state == 0 && IsEqual(queryString, ref i, out newIndexPos))
{
name = queryString.Substring(startpos, i - startpos);
i = newIndexPos;
startpos = i + 1;
++state;
}
else if (state == 1 && IsAmp(queryString, ref i, out newIndexPos))
{
Add(input, name, queryString.Substring(startpos, i - startpos));
i = newIndexPos;
startpos = i + 1;
state = 0;
name = null;
}
}
if (state == 0 && !input.GetEnumerator().MoveNext())
throw new FormatException("Not a valid query string: " + queryString);
if (startpos <= queryString.Length)
{
if (name != null)
Add(input, name, queryString.Substring(startpos, queryString.Length - startpos));
else
Add(input, string.Empty, queryString.Substring(startpos, queryString.Length - startpos));
}
return input;
}
private static bool IsEqual(string queryStr, ref int index, out int outIndex)
{
outIndex = index;
if (queryStr[index] == '=')
return true;
if (queryStr[index] == '%' && queryStr.Length > index + 2 && queryStr[index + 1] == '3'
&& (queryStr[index + 2] == 'd' || queryStr[index + 2] == 'D'))
{
outIndex += 2;
return true;
}
return false;
}
private static bool IsAmp(string queryStr, ref int index, out int outIndex)
{
outIndex = index;
if (queryStr[index] == '%' && queryStr.Length > index + 2 && queryStr[index + 1] == '2' &&
queryStr[index + 2] == '6')
outIndex += 2;
else if (queryStr[index] == '&')
{
if (queryStr.Length > index + 4
&& (queryStr[index + 1] == 'a' || queryStr[index + 1] == 'A')
&& (queryStr[index + 2] == 'm' || queryStr[index + 2] == 'M')
&& (queryStr[index + 3] == 'p' || queryStr[index + 3] == 'P')
&& queryStr[index + 4] == ';')
outIndex += 4;
}
else
return false;
return true;
}
private static void Add(IHttpInput input, string name, string value)
{
input.Add(HttpUtility.UrlDecode(name), HttpUtility.UrlDecode(value));
}
}
}

View File

@ -0,0 +1,263 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace OSHttpServer
{
/// <summary>
/// Contains some kind of input from the browser/client.
/// can be QueryString, form data or any other request body content.
/// </summary>
public class HttpInput : IHttpInput
{
/// <summary> Representation of a non-initialized class instance </summary>
public static readonly HttpInput Empty = new HttpInput("Empty", true);
private readonly IDictionary<string, HttpInputItem> _items = new Dictionary<string, HttpInputItem>();
private string _name;
/// <summary> Variable telling the class that it is non-initialized <see cref="Empty"/> </summary>
protected readonly bool _ignoreChanges;
/// <summary>
/// Initializes a new instance of the <see cref="HttpInput"/> class.
/// </summary>
/// <param name="name">form name.</param>
public HttpInput(string name)
{
Name = name;
}
/// <summary>
/// Initializes a new instance of the <see cref="HttpInput"/> class.
/// </summary>
/// <param name="name">form name.</param>
/// <param name="ignoreChanges">if set to <c>true</c> all changes will be ignored. </param>
/// <remarks>this constructor should only be used by Empty</remarks>
protected HttpInput(string name, bool ignoreChanges)
{
_name = name;
_ignoreChanges = ignoreChanges;
}
/// <summary>Creates a deep copy of the HttpInput class</summary>
/// <param name="input">The object to copy</param>
/// <remarks>The function makes a deep copy of quite a lot which can be slow</remarks>
protected HttpInput(HttpInput input)
{
foreach (HttpInputItem item in input)
_items.Add(item.Name, new HttpInputItem(item));
_name = input._name;
_ignoreChanges = input._ignoreChanges;
}
/// <summary>
/// Form name as lower case
/// </summary>
public string Name
{
get { return _name; }
set { _name = value; }
}
/// <summary>
/// Add a new element. Form array elements are parsed
/// and added in a correct hierarchy.
/// </summary>
/// <param name="name">Name is converted to lower case.</param>
/// <param name="value"></param>
/// <exception cref="ArgumentNullException"><c>name</c> is null.</exception>
/// <exception cref="InvalidOperationException">Cannot add stuff to <see cref="HttpInput.Empty"/>.</exception>
public void Add(string name, string value)
{
if (name == null)
throw new ArgumentNullException("name");
if (_ignoreChanges)
throw new InvalidOperationException("Cannot add stuff to HttpInput.Empty.");
// Check if it's a sub item.
// we can have multiple levels of sub items as in user[extension[id]] => user -> extension -> id
int pos = name.IndexOf('[');
if (pos != -1)
{
string name1 = name.Substring(0, pos);
string name2 = ExtractOne(name);
if (!_items.ContainsKey(name1))
_items.Add(name1, new HttpInputItem(name1, null));
_items[name1].Add(name2, value);
}
else
{
if (_items.ContainsKey(name))
_items[name].Add(value);
else
_items.Add(name, new HttpInputItem(name, value));
}
}
/// <summary>
/// Get a form item.
/// </summary>
/// <param name="name"></param>
/// <returns>Returns <see cref="HttpInputItem.Empty"/> if item was not found.</returns>
public HttpInputItem this[string name]
{
get
{
return _items.ContainsKey(name) ? _items[name] : HttpInputItem.Empty;
}
}
/// <summary>
/// Returns true if the class contains a <see cref="HttpInput"/> with the corresponding name.
/// </summary>
/// <param name="name">The field/query string name</param>
/// <returns>True if the value exists</returns>
public bool Contains(string name)
{
return _items.ContainsKey(name) && _items[name].Value != null;
}
/// <summary>
/// Parses an item and returns it.
/// This function is primarily used to parse array items as in user[name].
/// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
public static HttpInputItem ParseItem(string name, string value)
{
HttpInputItem item;
// Check if it's a sub item.
// we can have multiple levels of sub items as in user[extension[id]]] => user -> extension -> id
int pos = name.IndexOf('[');
if (pos != -1)
{
string name1 = name.Substring(0, pos);
string name2 = ExtractOne(name);
item = new HttpInputItem(name1, null);
item.Add(name2, value);
}
else
item = new HttpInputItem(name, value);
return item;
}
/// <summary> Outputs the instance representing all its values joined together </summary>
/// <returns></returns>
public override string ToString()
{
string temp = string.Empty;
foreach (KeyValuePair<string, HttpInputItem> item in _items)
temp += item.Value.ToString(Name);
return temp;
}
/// <summary>Returns all items as an unescaped query string.</summary>
/// <returns></returns>
public string ToString(bool asQueryString)
{
if (!asQueryString)
return ToString();
string temp = string.Empty;
foreach (KeyValuePair<string, HttpInputItem> item in _items)
temp += item.Value.ToString(null, true) + '&';
return temp.Length > 0 ? temp.Substring(0, temp.Length - 1) : string.Empty;
}
/// <summary>
/// Extracts one parameter from an array
/// </summary>
/// <param name="value">Containing the string array</param>
/// <returns>All but the first value</returns>
/// <example>
/// string test1 = ExtractOne("system[user][extension][id]");
/// string test2 = ExtractOne(test1);
/// string test3 = ExtractOne(test2);
/// // test1 = user[extension][id]
/// // test2 = extension[id]
/// // test3 = id
/// </example>
public static string ExtractOne(string value)
{
int pos = value.IndexOf('[');
if (pos != -1)
{
++pos;
int gotMore = value.IndexOf('[', pos + 1);
if (gotMore != -1)
value = value.Substring(pos, gotMore - pos - 1) + value.Substring(gotMore);
else
value = value.Substring(pos, value.Length - pos - 1);
}
return value;
}
/// <summary>Resets all data contained by class</summary>
virtual public void Clear()
{
_name = string.Empty;
_items.Clear();
}
///<summary>
///Returns an enumerator that iterates through the collection.
///</summary>
///
///<returns>
///A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection.
///</returns>
///<filterpriority>1</filterpriority>
IEnumerator<HttpInputItem> IEnumerable<HttpInputItem>.GetEnumerator()
{
return _items.Values.GetEnumerator();
}
///<summary>
///Returns an enumerator that iterates through a collection.
///</summary>
///
///<returns>
///An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
///</returns>
///<filterpriority>2</filterpriority>
public IEnumerator GetEnumerator()
{
return _items.Values.GetEnumerator();
}
}
/// <summary>
/// Base class for request data containers
/// </summary>
public interface IHttpInput : IEnumerable<HttpInputItem>
{
/// <summary>
/// Adds a parameter mapped to the presented name
/// </summary>
/// <param name="name">The name to map the parameter to</param>
/// <param name="value">The parameter value</param>
void Add(string name, string value);
/// <summary>
/// Returns a request parameter
/// </summary>
/// <param name="name">The name associated with the parameter</param>
/// <returns></returns>
HttpInputItem this[string name]
{ get; }
/// <summary>
/// Returns true if the container contains the requested parameter
/// </summary>
/// <param name="name">Parameter id</param>
/// <returns>True if parameter exists</returns>
bool Contains(string name);
}
}

View File

@ -0,0 +1,309 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace OSHttpServer
{
/// <summary>
/// represents a HTTP input item. Each item can have multiple sub items, a sub item
/// is made in a HTML form by using square brackets
/// </summary>
/// <example>
/// // <input type="text" name="user[FirstName]" value="jonas" /> becomes:
/// Console.WriteLine("Value: {0}", form["user"]["FirstName"].Value);
/// </example>
/// <remarks>
/// All names in a form SHOULD be in lowercase.
/// </remarks>
public class HttpInputItem : IHttpInput
{
/// <summary> Representation of a non-initialized <see cref="HttpInputItem"/>.</summary>
public static readonly HttpInputItem Empty = new HttpInputItem(string.Empty, true);
private readonly IDictionary<string, HttpInputItem> _items = new Dictionary<string, HttpInputItem>();
private readonly List<string> _values = new List<string>();
private string _name;
private readonly bool _ignoreChanges;
/// <summary>
/// Initializes an input item setting its name/identifier and value
/// </summary>
/// <param name="name">Parameter name/id</param>
/// <param name="value">Parameter value</param>
public HttpInputItem(string name, string value)
{
Name = name;
Add(value);
}
private HttpInputItem(string name, bool ignore)
{
Name = name;
_ignoreChanges = ignore;
}
/// <summary>Creates a deep copy of the item specified</summary>
/// <param name="item">The item to copy</param>
/// <remarks>The function makes a deep copy of quite a lot which can be slow</remarks>
public HttpInputItem(HttpInputItem item)
{
foreach (KeyValuePair<string, HttpInputItem> pair in item._items)
_items.Add(pair.Key, pair.Value);
foreach (string value in item._values)
_values.Add(value);
_ignoreChanges = item._ignoreChanges;
_name = item.Name;
}
/// <summary>
/// Number of values
/// </summary>
public int Count
{
get { return _values.Count; }
}
/// <summary>
/// Get a sub item
/// </summary>
/// <param name="name">name in lower case.</param>
/// <returns><see cref="HttpInputItem.Empty"/> if no item was found.</returns>
public HttpInputItem this[string name]
{
get {
return _items.ContainsKey(name) ? _items[name] : Empty;
}
}
/// <summary>
/// Name of item (in lower case).
/// </summary>
public string Name
{
get { return _name; }
set { _name = value; }
}
/// <summary>
/// Returns the first value, or null if no value exist.
/// </summary>
public string Value
{
get {
return _values.Count == 0 ? null : _values[0];
}
set
{
if (_values.Count == 0)
_values.Add(value);
else
_values[0] = value;
}
}
/// <summary>
/// Returns the last value, or null if no value exist.
/// </summary>
public string LastValue
{
get
{
return _values.Count == 0 ? null : _values[_values.Count - 1];
}
}
/// <summary>
/// Returns the list with values.
/// </summary>
public IList<string> Values
{
get { return _values.AsReadOnly(); }
}
/// <summary>
/// Add another value to this item
/// </summary>
/// <param name="value">Value to add.</param>
/// <exception cref="InvalidOperationException">Cannot add stuff to <see cref="HttpInput.Empty"/>.</exception>
public void Add(string value)
{
if (value == null)
return;
if (_ignoreChanges)
throw new InvalidOperationException("Cannot add stuff to HttpInput.Empty.");
_values.Add(value);
}
/// <summary>
/// checks if a sub-item exists (and has a value).
/// </summary>
/// <param name="name">name in lower case</param>
/// <returns>true if the sub-item exists and has a value; otherwise false.</returns>
public bool Contains(string name)
{
return _items.ContainsKey(name) && _items[name].Value != null;
}
/// <summary> Returns a formatted representation of the instance with the values of all contained parameters </summary>
public override string ToString()
{
return ToString(string.Empty);
}
/// <summary>
/// Outputs the string in a formatted manner
/// </summary>
/// <param name="prefix">A prefix to append, used internally</param>
/// <param name="asQuerySting">produce a query string</param>
public string ToString(string prefix, bool asQuerySting)
{
string name;
if (string.IsNullOrEmpty(prefix))
name = Name;
else
name = prefix + "[" + Name + "]";
if (asQuerySting)
{
string temp;
if (_values.Count == 0 && _items.Count > 0)
temp = string.Empty;
else
temp = name;
if (_values.Count > 0)
{
temp += '=';
foreach (string value in _values)
temp += value + ',';
temp = temp.Remove(temp.Length - 1, 1);
}
foreach (KeyValuePair<string, HttpInputItem> item in _items)
temp += item.Value.ToString(name, true) + '&';
return _items.Count > 0 ? temp.Substring(0, temp.Length - 1) : temp;
}
else
{
string temp = name;
if (_values.Count > 0)
{
temp += " = ";
foreach (string value in _values)
temp += value + ", ";
temp = temp.Remove(temp.Length - 2, 2);
}
temp += Environment.NewLine;
foreach (KeyValuePair<string, HttpInputItem> item in _items)
temp += item.Value.ToString(name, false);
return temp;
}
}
#region IHttpInput Members
/// <summary>
///
/// </summary>
/// <param name="name">name in lower case</param>
/// <returns></returns>
HttpInputItem IHttpInput.this[string name]
{
get
{
return _items.ContainsKey(name) ? _items[name] : Empty;
}
}
/// <summary>
/// Add a sub item.
/// </summary>
/// <param name="name">Can contain array formatting, the item is then parsed and added in multiple levels</param>
/// <param name="value">Value to add.</param>
/// <exception cref="ArgumentNullException">Argument is null.</exception>
/// <exception cref="InvalidOperationException">Cannot add stuff to <see cref="HttpInput.Empty"/>.</exception>
public void Add(string name, string value)
{
if (name == null && value != null)
throw new ArgumentNullException("name");
if (name == null)
return;
if (_ignoreChanges)
throw new InvalidOperationException("Cannot add stuff to HttpInput.Empty.");
int pos = name.IndexOf('[');
if (pos != -1)
{
string name1 = name.Substring(0, pos);
string name2 = HttpInput.ExtractOne(name);
if (!_items.ContainsKey(name1))
_items.Add(name1, new HttpInputItem(name1, null));
_items[name1].Add(name2, value);
/*
HttpInputItem item = HttpInput.ParseItem(name, value);
// Add the value to an existing sub item
if (_items.ContainsKey(item.Name))
_items[item.Name].Add(item.Value);
else
_items.Add(item.Name, item);
*/
}
else
{
if (_items.ContainsKey(name))
_items[name].Add(value);
else
_items.Add(name, new HttpInputItem(name, value));
}
}
#endregion
///<summary>
///Returns an enumerator that iterates through the collection.
///</summary>
///
///<returns>
///A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection.
///</returns>
///<filterpriority>1</filterpriority>
IEnumerator<HttpInputItem> IEnumerable<HttpInputItem>.GetEnumerator()
{
return _items.Values.GetEnumerator();
}
#region IEnumerable Members
///<summary>
///Returns an enumerator that iterates through a collection.
///</summary>
///
///<returns>
///An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
///</returns>
///<filterpriority>2</filterpriority>
public IEnumerator GetEnumerator()
{
return _items.Values.GetEnumerator();
}
#endregion
/// <summary>
/// Outputs the string in a formatted manner
/// </summary>
/// <param name="prefix">A prefix to append, used internally</param>
/// <returns></returns>
public string ToString(string prefix)
{
return ToString(prefix, false);
}
}
}

View File

@ -0,0 +1,248 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
namespace OSHttpServer
{
public class OSHttpListener: IDisposable
{
private readonly IPAddress m_address;
private readonly X509Certificate m_certificate;
private readonly IHttpContextFactory m_contextFactory;
private readonly int m_port;
private readonly ManualResetEvent m_shutdownEvent = new ManualResetEvent(false);
private readonly SslProtocols m_sslProtocol = SslProtocols.Tls | SslProtocols.Ssl3 | SslProtocols.Ssl2;
private TcpListener m_listener;
private ILogWriter m_logWriter = NullLogWriter.Instance;
private int m_pendingAccepts;
private bool m_shutdown;
protected RemoteCertificateValidationCallback m_clientCertValCallback = null;
public event EventHandler<ClientAcceptedEventArgs> Accepted;
public event ExceptionHandler ExceptionThrown;
public event EventHandler<RequestEventArgs> RequestReceived;
/// <summary>
/// Listen for regular HTTP connections
/// </summary>
/// <param name="address">IP Address to accept connections on</param>
/// <param name="port">TCP Port to listen on, default HTTP port is 80.</param>
/// <param name="factory">Factory used to create <see cref="IHttpClientContext"/>es.</param>
/// <exception cref="ArgumentNullException"><c>address</c> is null.</exception>
/// <exception cref="ArgumentException">Port must be a positive number.</exception>
protected OSHttpListener(IPAddress address, int port, IHttpContextFactory factory)
{
m_address = address;
m_port = port;
m_contextFactory = factory;
m_contextFactory.RequestReceived += OnRequestReceived;
}
/// <summary>
/// Initializes a new instance of the <see cref="OSHttpListener"/> class.
/// </summary>
/// <param name="address">IP Address to accept connections on</param>
/// <param name="port">TCP Port to listen on, default HTTPS port is 443</param>
/// <param name="factory">Factory used to create <see cref="IHttpClientContext"/>es.</param>
/// <param name="certificate">Certificate to use</param>
/// <param name="protocol">which HTTPS protocol to use, default is TLS.</param>
protected OSHttpListener(IPAddress address, int port, IHttpContextFactory factory, X509Certificate certificate,
SslProtocols protocol)
: this(address, port, factory, certificate)
{
m_sslProtocol = protocol;
}
/// <summary>
/// Initializes a new instance of the <see cref="OSHttpListener"/> class.
/// </summary>
/// <param name="address">IP Address to accept connections on</param>
/// <param name="port">TCP Port to listen on, default HTTPS port is 443</param>
/// <param name="factory">Factory used to create <see cref="IHttpClientContext"/>es.</param>
/// <param name="certificate">Certificate to use</param>
protected OSHttpListener(IPAddress address, int port, IHttpContextFactory factory, X509Certificate certificate)
: this(address, port, factory)
{
m_certificate = certificate;
}
public static OSHttpListener Create(IPAddress address, int port)
{
RequestParserFactory requestFactory = new RequestParserFactory();
HttpContextFactory factory = new HttpContextFactory(NullLogWriter.Instance, requestFactory);
return new OSHttpListener(address, port, factory);
}
public static OSHttpListener Create(IPAddress address, int port, X509Certificate certificate)
{
RequestParserFactory requestFactory = new RequestParserFactory();
HttpContextFactory factory = new HttpContextFactory(NullLogWriter.Instance, requestFactory);
return new OSHttpListener(address, port, factory, certificate);
}
public static OSHttpListener Create(IPAddress address, int port, X509Certificate certificate, SslProtocols protocol)
{
RequestParserFactory requestFactory = new RequestParserFactory();
HttpContextFactory factory = new HttpContextFactory(NullLogWriter.Instance, requestFactory);
return new OSHttpListener(address, port, factory, certificate, protocol);
}
private void OnRequestReceived(object sender, RequestEventArgs e)
{
RequestReceived?.Invoke(sender, e);
}
public RemoteCertificateValidationCallback CertificateValidationCallback
{
set { m_clientCertValCallback = value; }
}
/// <summary>
/// Gives you a change to receive log entries for all internals of the HTTP library.
/// </summary>
/// <remarks>
/// You may not switch log writer after starting the listener.
/// </remarks>
public ILogWriter LogWriter
{
get { return m_logWriter; }
set
{
m_logWriter = value ?? NullLogWriter.Instance;
if (m_certificate != null)
m_logWriter.Write(this, LogPrio.Info,
"HTTPS(" + m_sslProtocol + ") listening on " + m_address + ":" + m_port);
else
m_logWriter.Write(this, LogPrio.Info, "HTTP listening on " + m_address + ":" + m_port);
}
}
/// <summary>
/// True if we should turn on trace logs.
/// </summary>
public bool UseTraceLogs { get; set; }
/// <exception cref="Exception"><c>Exception</c>.</exception>
private void OnAccept(IAsyncResult ar)
{
bool beginAcceptCalled = false;
try
{
int count = Interlocked.Decrement(ref m_pendingAccepts);
if (m_shutdown)
{
if (count == 0)
m_shutdownEvent.Set();
return;
}
Interlocked.Increment(ref m_pendingAccepts);
m_listener.BeginAcceptSocket(OnAccept, null);
beginAcceptCalled = true;
Socket socket = m_listener.EndAcceptSocket(ar);
if (!OnAcceptingSocket(socket))
{
socket.Disconnect(true);
return;
}
m_logWriter.Write(this, LogPrio.Debug, "Accepted connection from: " + socket.RemoteEndPoint);
if (m_certificate != null)
m_contextFactory.CreateSecureContext(socket, m_certificate, m_sslProtocol, m_clientCertValCallback);
else
m_contextFactory.CreateContext(socket);
}
catch (Exception err)
{
m_logWriter.Write(this, LogPrio.Debug, err.Message);
ExceptionThrown?.Invoke(this, err);
if (!beginAcceptCalled)
RetryBeginAccept();
}
}
/// <summary>
/// Will try to accept connections one more time.
/// </summary>
/// <exception cref="Exception">If any exceptions is thrown.</exception>
private void RetryBeginAccept()
{
try
{
m_logWriter.Write(this, LogPrio.Error, "Trying to accept connections again.");
m_listener.BeginAcceptSocket(OnAccept, null);
}
catch (Exception err)
{
m_logWriter.Write(this, LogPrio.Fatal, err.Message);
ExceptionThrown?.Invoke(this, err);
}
}
/// <summary>
/// Can be used to create filtering of new connections.
/// </summary>
/// <param name="socket">Accepted socket</param>
/// <returns>true if connection can be accepted; otherwise false.</returns>
protected bool OnAcceptingSocket(Socket socket)
{
ClientAcceptedEventArgs args = new ClientAcceptedEventArgs(socket);
Accepted?.Invoke(this, args);
return !args.Revoked;
}
/// <summary>
/// Start listen for new connections
/// </summary>
/// <param name="backlog">Number of connections that can stand in a queue to be accepted.</param>
/// <exception cref="InvalidOperationException">Listener have already been started.</exception>
public void Start(int backlog)
{
if (m_listener != null)
throw new InvalidOperationException("Listener have already been started.");
m_listener = new TcpListener(m_address, m_port);
m_listener.Start(backlog);
Interlocked.Increment(ref m_pendingAccepts);
m_listener.BeginAcceptSocket(OnAccept, null);
}
/// <summary>
/// Stop the listener
/// </summary>
/// <exception cref="SocketException"></exception>
public void Stop()
{
m_shutdown = true;
m_contextFactory.Shutdown();
m_listener.Stop();
if (!m_shutdownEvent.WaitOne())
m_logWriter.Write(this, LogPrio.Error, "Failed to shutdown listener properly.");
m_listener = null;
Dispose();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose(bool disposing)
{
if (m_shutdownEvent != null)
{
m_shutdownEvent.Dispose();
}
}
}
}

View File

@ -0,0 +1,114 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace OSHttpServer
{
/// <summary>
/// Returns item either from a form or a query string (checks them in that order)
/// </summary>
public class HttpParam : IHttpInput
{
/// <summary> Representation of a non-initialized HttpParam </summary>
public static readonly HttpParam Empty = new HttpParam(HttpInput.Empty, HttpInput.Empty);
private IHttpInput m_form;
private IHttpInput m_query;
private List<HttpInputItem> _items = new List<HttpInputItem>();
/// <summary>Initialises the class to hold a value either from a post request or a querystring request</summary>
public HttpParam(IHttpInput form, IHttpInput query)
{
m_form = form;
m_query = query;
}
#region IHttpInput Members
/// <summary>
/// The add method is not availible for HttpParam
/// since HttpParam checks both Request.Form and Request.QueryString
/// </summary>
/// <param name="name">name identifying the value</param>
/// <param name="value">value to add</param>
/// <exception cref="NotImplementedException"></exception>
[Obsolete("Not implemented for HttpParam")]
public void Add(string name, string value)
{
throw new NotImplementedException();
}
/// <summary>
/// Checks whether the form or querystring has the specified value
/// </summary>
/// <param name="name">Name, case sensitive</param>
/// <returns>true if found; otherwise false.</returns>
public bool Contains(string name)
{
return m_form.Contains(name) || m_query.Contains(name);
}
/// <summary>
/// Fetch an item from the form or querystring (in that order).
/// </summary>
/// <param name="name"></param>
/// <returns>Item if found; otherwise HttpInputItem.EmptyLanguageNode</returns>
public HttpInputItem this[string name]
{
get
{
if (m_form[name] != HttpInputItem.Empty)
return m_form[name];
else
return m_query[name];
}
}
#endregion
internal void SetQueryString(HttpInput query)
{
m_query = query;
}
internal void SetForm(HttpInput form)
{
m_form = form;
}
///<summary>
///Returns an enumerator that iterates through the collection.
///</summary>
///
///<returns>
///A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection.
///</returns>
///<filterpriority>1</filterpriority>
IEnumerator<HttpInputItem> IEnumerable<HttpInputItem>.GetEnumerator()
{
List<HttpInputItem> items = new List<HttpInputItem>(m_query);
items.AddRange(m_form);
return items.GetEnumerator();
}
#region IEnumerable Members
///<summary>
///Returns an enumerator that iterates through a collection.
///</summary>
///
///<returns>
///An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
///</returns>
///<filterpriority>2</filterpriority>
public IEnumerator GetEnumerator()
{
List<HttpInputItem> items = new List<HttpInputItem>(m_query);
items.AddRange(m_form);
return items.GetEnumerator();
}
#endregion
}
}

View File

@ -0,0 +1,435 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Text;
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 HttpInput m_queryString = HttpInput.Empty;
private Uri m_uri = HttpHelper.EmptyUri;
private string m_uriPath;
public readonly IHttpClientContext m_context;
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; internal set; }
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;
int pos = m_uriPath.IndexOf('?');
if (pos != -1)
{
m_queryString = HttpHelper.ParseQueryString(m_uriPath.Substring(pos + 1));
m_param.SetQueryString(m_queryString);
string path = m_uriPath.Substring(0, pos);
m_uriPath = System.Web.HttpUtility.UrlDecode(path) + "?" + m_uriPath.Substring(pos + 1);
UriParts = value.Substring(0, pos).Split(UriSplitters, StringSplitOptions.RemoveEmptyEntries);
}
else
{
m_uriPath = System.Web.HttpUtility.UrlDecode(m_uriPath);
UriParts = value.Split(UriSplitters, StringSplitOptions.RemoveEmptyEntries);
}
}
}
/// <summary>
/// Assign a form.
/// </summary>
/// <param name="form"></param>
/*
internal void AssignForm(HttpForm form)
{
_form = form;
}
*/
#region IHttpRequest Members
/// <summary>
/// Gets whether the body is complete.
/// </summary>
public bool BodyIsComplete
{
get { return m_bodyBytesLeft == 0; }
}
/// <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 HttpInput QueryString
{
get { return m_queryString; }
}
/// <summary>
/// Gets or sets requested URI.
/// </summary>
public Uri Uri
{
get { return m_uri; }
set
{
m_uri = value ?? HttpHelper.EmptyUri;
UriParts = m_uri.AbsolutePath.Split(UriSplitters, StringSplitOptions.RemoveEmptyEntries);
}
}
/// <summary>
/// Uri absolute path splitted into parts.
/// </summary>
/// <example>
/// // uri is: http://gauffin.com/code/tiny/
/// Console.WriteLine(request.UriParts[0]); // result: code
/// Console.WriteLine(request.UriParts[1]); // result: tiny
/// </example>
/// <remarks>
/// If you're using controllers than the first part is controller name,
/// the second part is method name and the third part is Id property.
/// </remarks>
/// <seealso cref="Uri"/>
public string[] UriParts { get; private set; }
/// <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;
}
/*
/// <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":
int t;
if (!int.TryParse(value, out 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);
UriParts = m_uri.AbsolutePath.Split(UriSplitters, StringSplitOptions.RemoveEmptyEntries);
}
catch (UriFormatException err)
{
throw new BadRequestException("Failed to parse uri: " + value + m_uriPath, err);
}
break;
case "remote_addr":
// to prevent hacking (since it's added by IHttpClientContext before parsing).
if (m_headers[name] == null)
m_headers.Add(name, value);
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 = HttpHelper.EmptyUri;
m_queryString = HttpInput.Empty;
m_bodyBytesLeft = 0;
m_headers.Clear();
m_connection = ConnectionType.KeepAlive;
IsAjax = false;
//_form.Clear();
}
#endregion
}
}

View File

@ -0,0 +1,417 @@
using System;
using System.Text;
using OSHttpServer.Exceptions;
namespace OSHttpServer.Parser
{
/// <summary>
/// Parses a HTTP request directly from a stream
/// </summary>
public class HttpRequestParser : IHttpRequestParser
{
private ILogWriter m_log;
private readonly BodyEventArgs m_bodyArgs = new BodyEventArgs();
private readonly HeaderEventArgs m_headerArgs = new HeaderEventArgs();
private readonly RequestLineEventArgs m_requestLineArgs = new RequestLineEventArgs();
private string m_curHeaderName = string.Empty;
private string m_curHeaderValue = string.Empty;
private int m_bodyBytesLeft;
/// <summary>
/// More body bytes have been received.
/// </summary>
public event EventHandler<BodyEventArgs> BodyBytesReceived;
/// <summary>
/// Request line have been received.
/// </summary>
public event EventHandler<RequestLineEventArgs> RequestLineReceived;
/// <summary>
/// A header have been received.
/// </summary>
public event EventHandler<HeaderEventArgs> HeaderReceived;
/// <summary>
/// Create a new request parser
/// </summary>
/// <param name="logWriter">delegate receiving log entries.</param>
public HttpRequestParser(ILogWriter logWriter)
{
m_log = logWriter ?? NullLogWriter.Instance;
}
/// <summary>
/// Add a number of bytes to the body
/// </summary>
/// <param name="buffer">buffer containing more body bytes.</param>
/// <param name="offset">starting offset in buffer</param>
/// <param name="count">number of bytes, from offset, to read.</param>
/// <returns>offset to continue from.</returns>
private int AddToBody(byte[] buffer, int offset, int count)
{
// got all bytes we need, or just a few of them?
int bytesUsed = count > m_bodyBytesLeft ? m_bodyBytesLeft : count;
m_bodyArgs.Buffer = buffer;
m_bodyArgs.Offset = offset;
m_bodyArgs.Count = bytesUsed;
BodyBytesReceived?.Invoke(this, m_bodyArgs);
m_bodyBytesLeft -= bytesUsed;
if (m_bodyBytesLeft == 0)
{
// got a complete request.
m_log.Write(this, LogPrio.Trace, "Request parsed successfully.");
OnRequestCompleted();
Clear();
}
return offset + bytesUsed;
}
/// <summary>
/// Remove all state information for the request.
/// </summary>
public void Clear()
{
m_bodyBytesLeft = 0;
m_curHeaderName = string.Empty;
m_curHeaderValue = string.Empty;
CurrentState = RequestParserState.FirstLine;
}
/// <summary>
/// Gets or sets the log writer.
/// </summary>
public ILogWriter LogWriter
{
get { return m_log; }
set { m_log = value ?? NullLogWriter.Instance; }
}
/// <summary>
/// Parse request line
/// </summary>
/// <param name="value"></param>
/// <exception cref="BadRequestException">If line is incorrect</exception>
/// <remarks>Expects the following format: "Method SP Request-URI SP HTTP-Version CRLF"</remarks>
protected void OnFirstLine(string value)
{
//
//todo: In the interest of robustness, servers SHOULD ignore any empty line(s) received where a Request-Line is expected.
// In other words, if the server is reading the protocol stream at the beginning of a message and receives a CRLF first, it should ignore the CRLF.
//
m_log.Write(this, LogPrio.Debug, "Got request: " + value);
//Request-Line = Method SP Request-URI SP HTTP-Version CRLF
int pos = value.IndexOf(' ');
if (pos == -1 || pos + 1 >= value.Length)
{
m_log.Write(this, LogPrio.Warning, "Invalid request line, missing Method. Line: " + value);
throw new BadRequestException("Invalid request line, missing Method. Line: " + value);
}
string method = value.Substring(0, pos).ToUpper();
int oldPos = pos + 1;
pos = value.IndexOf(' ', oldPos);
if (pos == -1)
{
m_log.Write(this, LogPrio.Warning, "Invalid request line, missing URI. Line: " + value);
throw new BadRequestException("Invalid request line, missing URI. Line: " + value);
}
string path = value.Substring(oldPos, pos - oldPos);
if (path.Length > 4196)
throw new BadRequestException("Too long URI.");
if (pos + 1 >= value.Length)
{
m_log.Write(this, LogPrio.Warning, "Invalid request line, missing HTTP-Version. Line: " + value);
throw new BadRequestException("Invalid request line, missing HTTP-Version. Line: " + value);
}
string version = value.Substring(pos + 1);
if (version.Length < 4 || string.Compare(version.Substring(0, 4), "HTTP", true) != 0)
{
m_log.Write(this, LogPrio.Warning, "Invalid HTTP version in request line. Line: " + value);
throw new BadRequestException("Invalid HTTP version in Request line. Line: " + value);
}
m_requestLineArgs.HttpMethod = method;
m_requestLineArgs.HttpVersion = version;
m_requestLineArgs.UriPath = path;
RequestLineReceived(this, m_requestLineArgs);
}
/// <summary>
/// We've parsed a new header.
/// </summary>
/// <param name="name">Name in lower case</param>
/// <param name="value">Value, unmodified.</param>
/// <exception cref="BadRequestException">If content length cannot be parsed.</exception>
protected void OnHeader(string name, string value)
{
m_headerArgs.Name = name;
m_headerArgs.Value = value;
if (string.Compare(name, "content-length", true) == 0)
{
if (!int.TryParse(value, out m_bodyBytesLeft))
throw new BadRequestException("Content length is not a number.");
}
HeaderReceived?.Invoke(this, m_headerArgs);
}
private void OnRequestCompleted()
{
RequestCompleted?.Invoke(this, EventArgs.Empty);
}
#region IHttpRequestParser Members
/// <summary>
/// Current state in parser.
/// </summary>
public RequestParserState CurrentState { get; private set; }
/// <summary>
/// Parse a message
/// </summary>
/// <param name="buffer">bytes to parse.</param>
/// <param name="offset">where in buffer that parsing should start</param>
/// <param name="count">number of bytes to parse, starting on <paramref name="offset"/>.</param>
/// <returns>offset (where to start parsing next).</returns>
/// <exception cref="BadRequestException"><c>BadRequestException</c>.</exception>
public int Parse(byte[] buffer, int offset, int count)
{
// add body bytes
if (CurrentState == RequestParserState.Body)
{
return AddToBody(buffer, offset, count);
}
#if DEBUG
string temp = Encoding.ASCII.GetString(buffer, offset, count);
_log.Write(this, LogPrio.Trace, "\r\n\r\n HTTP MESSAGE: " + temp + "\r\n");
#endif
int currentLine = 1;
int startPos = -1;
// set start pos since this is from an partial request
if (CurrentState == RequestParserState.HeaderValue)
startPos = 0;
int endOfBufferPos = offset + count;
//<summary>
// Handled bytes are used to keep track of the number of bytes processed.
// We do this since we can handle partial requests (to be able to check headers and abort
// invalid requests directly without having to process the whole header / body).
// </summary>
int handledBytes = 0;
for (int currentPos = offset; currentPos < endOfBufferPos; ++currentPos)
{
var ch = (char)buffer[currentPos];
char nextCh = endOfBufferPos > currentPos + 1 ? (char)buffer[currentPos + 1] : char.MinValue;
if (ch == '\r')
++currentLine;
switch (CurrentState)
{
case RequestParserState.FirstLine:
if (currentPos == 8191)
{
m_log.Write(this, LogPrio.Warning, "HTTP Request is too large.");
throw new BadRequestException("Too large request line.");
}
if (char.IsLetterOrDigit(ch) && startPos == -1)
startPos = currentPos;
if (startPos == -1 && (ch != '\r' || nextCh != '\n'))
{
m_log.Write(this, LogPrio.Warning, "Request line is not found.");
throw new BadRequestException("Invalid request line.");
}
if (startPos != -1 && (ch == '\r' || ch == '\n'))
{
int size = GetLineBreakSize(buffer, currentPos);
OnFirstLine(Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos));
CurrentState = CurrentState + 1;
currentPos += size - 1;
handledBytes = currentPos + size - 1;
startPos = -1;
}
break;
case RequestParserState.HeaderName:
if (ch == '\r' || ch == '\n')
{
currentPos += GetLineBreakSize(buffer, currentPos);
if (m_bodyBytesLeft == 0)
{
CurrentState = RequestParserState.FirstLine;
m_log.Write(this, LogPrio.Trace, "Request parsed successfully (no content).");
OnRequestCompleted();
Clear();
return currentPos;
}
CurrentState = RequestParserState.Body;
if (currentPos + 1 < endOfBufferPos)
{
m_log.Write(this, LogPrio.Trace, "Adding bytes to the body");
return AddToBody(buffer, currentPos, endOfBufferPos - currentPos);
}
return currentPos;
}
if (char.IsWhiteSpace(ch) || ch == ':')
{
if (startPos == -1)
{
m_log.Write(this, LogPrio.Warning,
"Expected header name, got colon on line " + currentLine);
throw new BadRequestException("Expected header name, got colon on line " + currentLine);
}
m_curHeaderName = Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos);
handledBytes = currentPos + 1;
startPos = -1;
CurrentState = CurrentState + 1;
if (ch == ':')
CurrentState = CurrentState + 1;
}
else if (startPos == -1)
startPos = currentPos;
else if (!char.IsLetterOrDigit(ch) && ch != '-')
{
m_log.Write(this, LogPrio.Warning, "Invalid character in header name on line " + currentLine);
throw new BadRequestException("Invalid character in header name on line " + currentLine);
}
if (startPos != -1 && currentPos - startPos > 200)
{
m_log.Write(this, LogPrio.Warning, "Invalid header name on line " + currentLine);
throw new BadRequestException("Invalid header name on line " + currentLine);
}
break;
case RequestParserState.AfterName:
if (ch == ':')
{
handledBytes = currentPos + 1;
CurrentState = CurrentState + 1;
}
break;
case RequestParserState.Between:
{
if (ch == ' ' || ch == '\t')
continue;
int newLineSize = GetLineBreakSize(buffer, currentPos);
if (newLineSize > 0 && currentPos + newLineSize < endOfBufferPos &&
char.IsWhiteSpace((char)buffer[currentPos + newLineSize]))
{
++currentPos;
continue;
}
startPos = currentPos;
CurrentState = CurrentState + 1;
handledBytes = currentPos;
continue;
}
case RequestParserState.HeaderValue:
{
if (ch != '\r' && ch != '\n')
continue;
int newLineSize = GetLineBreakSize(buffer, currentPos);
if (startPos == -1)
continue; // allow new lines before start of value
if (m_curHeaderName == string.Empty)
throw new BadRequestException("Missing header on line " + currentLine);
if (startPos == -1)
{
m_log.Write(this, LogPrio.Warning, "Missing header value for '" + m_curHeaderName);
throw new BadRequestException("Missing header value for '" + m_curHeaderName);
}
if (currentPos - startPos > 8190)
{
m_log.Write(this, LogPrio.Warning, "Too large header value on line " + currentLine);
throw new BadRequestException("Too large header value on line " + currentLine);
}
// Header fields can be extended over multiple lines by preceding each extra line with at
// least one SP or HT.
if (endOfBufferPos > currentPos + newLineSize
&& (buffer[currentPos + newLineSize] == ' ' || buffer[currentPos + newLineSize] == '\t'))
{
if (startPos != -1)
m_curHeaderValue = Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos);
m_log.Write(this, LogPrio.Trace, "Header value is on multiple lines.");
CurrentState = RequestParserState.Between;
startPos = -1;
currentPos += newLineSize - 1;
handledBytes = currentPos + newLineSize - 1;
continue;
}
m_curHeaderValue += Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos);
m_log.Write(this, LogPrio.Trace, "Header [" + m_curHeaderName + ": " + m_curHeaderValue + "]");
OnHeader(m_curHeaderName, m_curHeaderValue);
startPos = -1;
CurrentState = RequestParserState.HeaderName;
m_curHeaderValue = string.Empty;
m_curHeaderName = string.Empty;
++currentPos;
handledBytes = currentPos + 1;
// Check if we got a colon so we can cut header name, or crlf for end of header.
bool canContinue = false;
for (int j = currentPos; j < endOfBufferPos; ++j)
{
if (buffer[j] != ':' && buffer[j] != '\r' && buffer[j] != '\n') continue;
canContinue = true;
break;
}
if (!canContinue)
{
m_log.Write(this, LogPrio.Trace, "Cant continue, no colon.");
return currentPos + 1;
}
}
break;
}
}
return handledBytes;
}
int GetLineBreakSize(byte[] buffer, int offset)
{
if (buffer[offset] == '\r')
{
if (buffer.Length > offset + 1 && buffer[offset + 1] == '\n')
return 2;
else
throw new BadRequestException("Got invalid linefeed.");
}
else if (buffer[offset] == '\n')
{
if (buffer.Length == offset + 1)
return 1; // linux line feed
if (buffer[offset + 1] != '\r')
return 1; // linux line feed
else
return 2; // win line feed
}
else
return 0;
}
/// <summary>
/// A request have been successfully parsed.
/// </summary>
public event EventHandler RequestCompleted;
#endregion
}
}

View File

@ -0,0 +1,670 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace OSHttpServer
{
public class HttpResponse : IHttpResponse
{
private const string DefaultContentType = "text/html;charset=UTF-8";
private readonly IHttpClientContext m_context;
private readonly ResponseCookies m_cookies = new ResponseCookies();
private readonly NameValueCollection m_headers = new NameValueCollection();
private string m_httpVersion;
private Stream m_body;
private long m_contentLength;
private string m_contentType;
private Encoding m_encoding = Encoding.UTF8;
private int m_keepAlive = 60;
public uint requestID { get; private set; }
public byte[] RawBuffer { get; set; }
public int RawBufferStart { get; set; }
public int RawBufferLen { get; set; }
internal byte[] m_headerBytes = null;
/// <summary>
/// Initializes a new instance of the <see cref="IHttpResponse"/> class.
/// </summary>
/// <param name="context">Client that send the <see cref="IHttpRequest"/>.</param>
/// <param name="request">Contains information of what the client want to receive.</param>
/// <exception cref="ArgumentException"><see cref="IHttpRequest.HttpVersion"/> cannot be empty.</exception>
public HttpResponse(IHttpClientContext context, IHttpRequest request)
{
m_httpVersion = request.HttpVersion;
if (string.IsNullOrEmpty(m_httpVersion))
m_httpVersion = "HTTP/1.0";
Status = HttpStatusCode.OK;
m_context = context;
m_Connetion = request.Connection;
requestID = request.ID;
RawBufferStart = -1;
RawBufferLen = -1;
}
/// <summary>
/// Initializes a new instance of the <see cref="IHttpResponse"/> class.
/// </summary>
/// <param name="context">Client that send the <see cref="IHttpRequest"/>.</param>
/// <param name="httpVersion">Version of HTTP protocol that the client uses.</param>
/// <param name="connectionType">Type of HTTP connection used.</param>
internal HttpResponse(IHttpClientContext context, string httpVersion, ConnectionType connectionType)
{
Status = HttpStatusCode.OK;
m_context = context;
m_httpVersion = httpVersion;
m_Connetion = connectionType;
}
private ConnectionType m_Connetion;
public ConnectionType Connection
{
get { return m_Connetion; }
set { return; }
}
private int m_priority = 0;
public int Priority
{
get { return m_priority;}
set { m_priority = (value > 0 && m_priority < 3)? value : 0;}
}
#region IHttpResponse Members
/// <summary>
/// The body stream is used to cache the body contents
/// before sending everything to the client. It's the simplest
/// way to serve documents.
/// </summary>
public Stream Body
{
get
{
if(m_body == null)
m_body = new MemoryStream();
return m_body;
}
set { m_body = value; }
}
/// <summary>
/// The chunked encoding modifies the body of a message in order to
/// transfer it as a series of chunks, each with its own size indicator,
/// followed by an OPTIONAL trailer containing entity-header fields. This
/// allows dynamically produced content to be transferred along with the
/// information necessary for the recipient to verify that it has
/// received the full message.
/// </summary>
public bool Chunked { get; set; }
/// <summary>
/// Defines the version of the HTTP Response for applications where it's required
/// for this to be forced.
/// </summary>
public string ProtocolVersion
{
get { return m_httpVersion; }
set { m_httpVersion = value; }
}
/// <summary>
/// Encoding to use when sending stuff to the client.
/// </summary>
/// <remarks>Default is UTF8</remarks>
public Encoding Encoding
{
get { return m_encoding; }
set { m_encoding = value; }
}
/// <summary>
/// Number of seconds to keep connection alive
/// </summary>
/// <remarks>Only used if Connection property is set to <see cref="ConnectionType.KeepAlive"/>.</remarks>
public int KeepAlive
{
get { return m_keepAlive; }
set
{
if (value > 400)
m_keepAlive = 400;
else if (value <= 0)
m_keepAlive = 0;
else
m_keepAlive = value;
}
}
/// <summary>
/// Status code that is sent to the client.
/// </summary>
/// <remarks>Default is <see cref="HttpStatusCode.OK"/></remarks>
public HttpStatusCode Status { get; set; }
/// <summary>
/// Information about why a specific status code was used.
/// </summary>
public string Reason { get; set; }
/// <summary>
/// Size of the body. MUST be specified before sending the header,
/// unless property Chunked is set to true.
/// </summary>
public long ContentLength
{
get { return m_contentLength; }
set { m_contentLength = value; }
}
/// <summary>
/// Kind of content in the body
/// </summary>
/// <remarks>Default type is "text/html"</remarks>
public string ContentType
{
get { return m_contentType; }
set { m_contentType = value; }
}
/// <summary>
/// Headers have been sent to the client-
/// </summary>
/// <remarks>You can not send any additional headers if they have already been sent.</remarks>
public bool HeadersSent { get; private set; }
/// <summary>
/// The whole response have been sent.
/// </summary>
public bool Sent { get; private set; }
/// <summary>
/// Cookies that should be created/changed.
/// </summary>
public ResponseCookies Cookies
{
get { return m_cookies; }
}
/// <summary>
/// Add another header to the document.
/// </summary>
/// <param name="name">Name of the header, case sensitive, use lower cases.</param>
/// <param name="value">Header values can span over multiple lines as long as each line starts with a white space. New line chars should be \r\n</param>
/// <exception cref="InvalidOperationException">If headers already been sent.</exception>
/// <exception cref="ArgumentException">If value conditions have not been met.</exception>
/// <remarks>Adding any header will override the default ones and those specified by properties.</remarks>
public void AddHeader(string name, string value)
{
if (HeadersSent)
throw new InvalidOperationException("Headers have already been sent.");
for (int i = 1; i < value.Length; ++i)
{
if (value[i] == '\r' && !char.IsWhiteSpace(value[i - 1]))
throw new ArgumentException("New line in value do not start with a white space.");
if (value[i] == '\n' && value[i - 1] != '\r')
throw new ArgumentException("Invalid new line sequence, should be \\r\\n (crlf).");
}
m_headers[name] = value;
}
/// <summary>
/// Send headers and body to the browser.
/// </summary>
/// <exception cref="InvalidOperationException">If content have already been sent.</exception>
public void SendOri()
{
if (Sent)
throw new InvalidOperationException("Everything have already been sent.");
m_context.ReqResponseAboutToSend(requestID);
if (m_context.MAXRequests == 0 || m_keepAlive == 0)
{
Connection = ConnectionType.Close;
m_context.TimeoutKeepAlive = 0;
}
else
{
if (m_keepAlive > 0)
m_context.TimeoutKeepAlive = m_keepAlive * 1000;
}
if (!HeadersSent)
{
if (!SendHeaders())
{
m_body.Dispose();
Sent = true;
return;
}
}
if(RawBuffer != null)
{
if(RawBufferStart >= 0 && RawBufferLen > 0)
{
if (RawBufferStart > RawBuffer.Length)
RawBufferStart = 0;
if (RawBufferLen + RawBufferStart > RawBuffer.Length)
RawBufferLen = RawBuffer.Length - RawBufferStart;
/*
int curlen;
while(RawBufferLen > 0)
{
curlen = RawBufferLen;
if(curlen > 8192)
curlen = 8192;
if (!_context.Send(RawBuffer, RawBufferStart, curlen))
{
RawBuffer = null;
RawBufferStart = -1;
RawBufferLen = -1;
Body.Dispose();
return;
}
RawBufferLen -= curlen;
RawBufferStart += curlen;
}
*/
if(RawBufferLen > 0)
{
if (!m_context.Send(RawBuffer, RawBufferStart, RawBufferLen))
{
RawBuffer = null;
RawBufferStart = -1;
RawBufferLen = -1;
if(m_body != null)
m_body.Dispose();
Sent = true;
return;
}
}
}
RawBuffer = null;
RawBufferStart = -1;
RawBufferLen = -1;
}
if(m_body != null && m_body.Length > 0)
{
m_body.Flush();
m_body.Seek(0, SeekOrigin.Begin);
var buffer = new byte[8192];
int bytesRead = m_body.Read(buffer, 0, 8192);
while (bytesRead > 0)
{
if (!m_context.Send(buffer, 0, bytesRead))
break;
bytesRead = m_body.Read(buffer, 0, 8192);
}
m_body.Dispose();
}
Sent = true;
m_context.ReqResponseSent(requestID, Connection);
}
/// <summary>
/// Make sure that you have specified <see cref="ContentLength"/> and sent the headers first.
/// </summary>
/// <param name="buffer"></param>
/// <exception cref="InvalidOperationException">If headers have not been sent.</exception>
/// <see cref="SendHeaders"/>
/// <param name="offset">offset of first byte to send</param>
/// <param name="count">number of bytes to send.</param>
/// <seealso cref="Send"/>
/// <seealso cref="SendHeaders"/>
/// <remarks>This method can be used if you want to send body contents without caching them first. This
/// is recommended for larger files to keep the memory usage low.</remarks>
public bool SendBody(byte[] buffer, int offset, int count)
{
if (!HeadersSent)
throw new InvalidOperationException("Send headers, and remember to specify ContentLength first.");
bool sent = m_context.Send(buffer, offset, count);
Sent = true;
if (sent)
m_context.ReqResponseSent(requestID, Connection);
return sent;
}
/// <summary>
/// Make sure that you have specified <see cref="ContentLength"/> and sent the headers first.
/// </summary>
/// <param name="buffer"></param>
/// <exception cref="InvalidOperationException">If headers have not been sent.</exception>
/// <see cref="SendHeaders"/>
/// <seealso cref="Send"/>
/// <seealso cref="SendHeaders"/>
/// <remarks>This method can be used if you want to send body contents without caching them first. This
/// is recommended for larger files to keep the memory usage low.</remarks>
public bool SendBody(byte[] buffer)
{
if (!HeadersSent)
throw new InvalidOperationException("Send headers, and remember to specify ContentLength first.");
bool sent = m_context.Send(buffer);
if (sent)
m_context.ReqResponseSent(requestID, Connection);
Sent = true;
return sent;
}
/// <summary>
/// Send headers to the client.
/// </summary>
/// <exception cref="InvalidOperationException">If headers already been sent.</exception>
/// <seealso cref="AddHeader"/>
/// <seealso cref="Send"/>
/// <seealso cref="SendBody(byte[])"/>
public bool SendHeaders()
{
if (HeadersSent)
throw new InvalidOperationException("Header have already been sent.");
HeadersSent = true;
if (m_headers["Date"] == null)
m_headers["Date"] = DateTime.Now.ToString("r");
if (m_headers["Content-Length"] == null)
{
int len = (int)m_contentLength;
if(len == 0)
{
if(m_body != null)
len = (int)m_body.Length;
if(RawBuffer != null)
len += RawBufferLen;
}
m_headers["Content-Length"] = len.ToString();
}
if (m_headers["Content-Type"] == null)
m_headers["Content-Type"] = m_contentType ?? DefaultContentType;
if (m_headers["Server"] == null)
m_headers["Server"] = "Tiny WebServer";
int keepaliveS = m_context.TimeoutKeepAlive / 1000;
if (Connection == ConnectionType.KeepAlive && keepaliveS > 0 && m_context.MAXRequests > 0)
{
m_headers["Keep-Alive"] = "timeout=" + keepaliveS + ", max=" + m_context.MAXRequests;
m_headers["Connection"] = "Keep-Alive";
}
else
m_headers["Connection"] = "close";
var sb = new StringBuilder();
sb.AppendFormat("{0} {1} {2}\r\n", m_httpVersion, (int)Status,
string.IsNullOrEmpty(Reason) ? Status.ToString() : Reason);
for (int i = 0; i < m_headers.Count; ++i)
{
string headerName = m_headers.AllKeys[i];
string[] values = m_headers.GetValues(i);
if (values == null) continue;
foreach (string value in values)
sb.AppendFormat("{0}: {1}\r\n", headerName, value);
}
foreach (ResponseCookie cookie in Cookies)
sb.AppendFormat("Set-Cookie: {0}\r\n", cookie);
sb.Append("\r\n");
m_headers.Clear();
return m_context.Send(Encoding.GetBytes(sb.ToString()));
}
public byte[] GetHeaders()
{
HeadersSent = true;
var sb = new StringBuilder();
if(string.IsNullOrWhiteSpace(m_httpVersion))
sb.AppendFormat("HTTP1/0 {0} {1}\r\n", (int)Status,
string.IsNullOrEmpty(Reason) ? Status.ToString() : Reason);
else
sb.AppendFormat("{0} {1} {2}\r\n", m_httpVersion, (int)Status,
string.IsNullOrEmpty(Reason) ? Status.ToString() : Reason);
if (m_headers["Date"] == null)
sb.AppendFormat("Date: {0}\r\n", DateTime.Now.ToString("r"));
if (m_headers["Content-Length"] == null)
{
long len = m_contentLength;
if (len == 0)
{
len = Body.Length;
if (RawBuffer != null && RawBufferLen > 0)
len += RawBufferLen;
}
sb.AppendFormat("Content-Length: {0}\r\n", len);
}
if (m_headers["Content-Type"] == null)
sb.AppendFormat("Content-Type: {0}\r\n", m_contentType ?? DefaultContentType);
if (m_headers["Server"] == null)
sb.Append("Server: OSWebServer\r\n");
int keepaliveS = m_context.TimeoutKeepAlive / 1000;
if (Connection == ConnectionType.KeepAlive && keepaliveS > 0 && m_context.MAXRequests > 0)
{
sb.AppendFormat("Keep-Alive:timeout={0}, max={1}\r\n", keepaliveS, m_context.MAXRequests);
sb.Append("Connection: Keep-Alive\r\n");
}
else
sb.Append("Connection: close\r\n");
if (m_headers["Connection"] != null)
m_headers["Connection"] = null;
if (m_headers["Keep-Alive"] != null)
m_headers["Keep-Alive"] = null;
for (int i = 0; i < m_headers.Count; ++i)
{
string headerName = m_headers.AllKeys[i];
string[] values = m_headers.GetValues(i);
if (values == null) continue;
foreach (string value in values)
sb.AppendFormat("{0}: {1}\r\n", headerName, value);
}
foreach (ResponseCookie cookie in Cookies)
sb.AppendFormat("Set-Cookie: {0}\r\n", cookie);
sb.Append("\r\n");
m_headers.Clear();
return Encoding.GetBytes(sb.ToString());
}
public void Send()
{
if (Sent)
throw new InvalidOperationException("Everything have already been sent.");
if (m_context.MAXRequests == 0 || m_keepAlive == 0)
{
Connection = ConnectionType.Close;
m_context.TimeoutKeepAlive = 0;
}
else
{
if (m_keepAlive > 0)
m_context.TimeoutKeepAlive = m_keepAlive * 1000;
}
m_headerBytes = GetHeaders();
if (RawBuffer != null)
{
if (RawBufferStart < 0 || RawBufferStart > RawBuffer.Length)
return;
if (RawBufferLen < 0)
RawBufferLen = RawBuffer.Length;
if (RawBufferLen + RawBufferStart > RawBuffer.Length)
RawBufferLen = RawBuffer.Length - RawBufferStart;
int tlen = m_headerBytes.Length + RawBufferLen;
if(RawBufferLen > 0 && tlen < 16384)
{
byte[] tmp = new byte[tlen];
Array.Copy(m_headerBytes, tmp, m_headerBytes.Length);
Array.Copy(RawBuffer, RawBufferStart, tmp, m_headerBytes.Length, RawBufferLen);
m_headerBytes = null;
RawBuffer = tmp;
RawBufferStart = 0;
RawBufferLen = tlen;
}
}
m_context.StartSendResponse(this);
}
public async Task SendNextAsync(int bytesLimit)
{
if (m_headerBytes != null)
{
if(!await m_context.SendAsync(m_headerBytes, 0, m_headerBytes.Length).ConfigureAwait(false))
{
if(m_body != null)
m_body.Dispose();
RawBuffer = null;
Sent = true;
return;
}
bytesLimit -= m_headerBytes.Length;
m_headerBytes = null;
if(bytesLimit <= 0)
{
m_context.ContinueSendResponse();
return;
}
}
if (RawBuffer != null)
{
if (RawBufferLen > 0)
{
bool sendRes;
if(RawBufferLen > bytesLimit)
{
sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, bytesLimit).ConfigureAwait(false);
RawBufferLen -= bytesLimit;
RawBufferStart += bytesLimit;
}
else
{
sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, RawBufferLen).ConfigureAwait(false);
RawBufferLen = 0;
}
if (!sendRes)
{
RawBuffer = null;
if(m_body != null)
Body.Dispose();
Sent = true;
return;
}
}
if (RawBufferLen <= 0)
RawBuffer = null;
else
{
m_context.ContinueSendResponse();
return;
}
}
if (m_body != null && m_body.Length != 0)
{
m_body.Flush();
m_body.Seek(0, SeekOrigin.Begin);
RawBuffer = new byte[m_body.Length];
RawBufferLen = m_body.Read(RawBuffer, 0, (int)m_body.Length);
m_body.Dispose();
if(RawBufferLen > 0)
{
bool sendRes;
if (RawBufferLen > bytesLimit)
{
sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, bytesLimit).ConfigureAwait(false);
RawBufferLen -= bytesLimit;
RawBufferStart += bytesLimit;
}
else
{
sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, RawBufferLen).ConfigureAwait(false);
RawBufferLen = 0;
}
if (!sendRes)
{
RawBuffer = null;
Sent = true;
return;
}
}
if (RawBufferLen > 0)
{
m_context.ContinueSendResponse();
return;
}
}
if (m_body != null)
m_body.Dispose();
Sent = true;
m_context.ReqResponseSent(requestID, Connection);
}
/// <summary>
/// Redirect client to somewhere else using the 302 status code.
/// </summary>
/// <param name="uri">Destination of the redirect</param>
/// <exception cref="InvalidOperationException">If headers already been sent.</exception>
/// <remarks>You can not do anything more with the request when a redirect have been done. This should be your last
/// action.</remarks>
public void Redirect(Uri uri)
{
Status = HttpStatusCode.Redirect;
m_headers["location"] = uri.ToString();
}
/// <summary>
/// redirect to somewhere
/// </summary>
/// <param name="url">where the redirect should go</param>
/// <remarks>
/// No body are allowed when doing redirects.
/// </remarks>
public void Redirect(string url)
{
Status = HttpStatusCode.Redirect;
m_headers["location"] = url;
}
public void Clear()
{
if(Body != null && Body.CanRead)
Body.Dispose();
}
#endregion
}
}

View File

@ -0,0 +1,146 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace OSHttpServer
{
/// <summary>
/// Contains a connection to a browser/client.
/// </summary>
public interface IHttpClientContext
{
/// <summary>
/// Get SSL commonName of remote peer
/// </summary>
string SSLCommonName { get; }
/// <summary>
/// Using SSL or other encryption method.
/// </summary>
bool IsSecured { get; }
int contextID {get;}
int TimeoutKeepAlive {get; set; }
int MAXRequests{get; set; }
bool CanSend();
bool IsSending();
/// <summary>
/// Disconnect from client
/// </summary>
/// <param name="error">error to report in the <see cref="Disconnected"/> event.</param>
void Disconnect(SocketError error);
/// <summary>
/// Send a response.
/// </summary>
/// <param name="httpVersion">Either <see cref="HttpHelper.HTTP10"/> or <see cref="HttpHelper.HTTP11"/></param>
/// <param name="statusCode">HTTP status code</param>
/// <param name="reason">reason for the status code.</param>
/// <param name="body">HTML body contents, can be null or empty.</param>
/// <param name="contentType">A content type to return the body as, i.e. 'text/html' or 'text/plain', defaults to 'text/html' if null or empty</param>
/// <exception cref="ArgumentException">If <paramref name="httpVersion"/> is invalid.</exception>
void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType);
/// <summary>
/// Send a response.
/// </summary>
/// <param name="httpVersion">Either <see cref="HttpHelper.HTTP10"/> or <see cref="HttpHelper.HTTP11"/></param>
/// <param name="statusCode">HTTP status code</param>
/// <param name="reason">reason for the status code.</param>
void Respond(string httpVersion, HttpStatusCode statusCode, string reason);
/// <summary>
/// send a whole buffer
/// </summary>
/// <param name="buffer">buffer to send</param>
/// <exception cref="ArgumentNullException"></exception>
bool Send(byte[] buffer);
/// <summary>
/// Send data using the stream
/// </summary>
/// <param name="buffer">Contains data to send</param>
/// <param name="offset">Start position in buffer</param>
/// <param name="size">number of bytes to send</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
bool Send(byte[] buffer, int offset, int size);
Task<bool> SendAsync(byte[] buffer, int offset, int size);
/// <summary>
/// Closes the streams and disposes of the unmanaged resources
/// </summary>
void Close();
/// <summary>
/// The context have been disconnected.
/// </summary>
/// <remarks>
/// Event can be used to clean up a context, or to reuse it.
/// </remarks>
event EventHandler<DisconnectedEventArgs> Disconnected;
/// <summary>
/// A request have been received in the context.
/// </summary>
event EventHandler<RequestEventArgs> RequestReceived;
HTTPNetworkContext GiveMeTheNetworkStreamIKnowWhatImDoing();
void StartSendResponse(HttpResponse response);
void ContinueSendResponse();
void ReqResponseAboutToSend(uint requestID);
void ReqResponseSent(uint requestID, ConnectionType connection);
bool TrySendResponse(int limit);
}
public class HTTPNetworkContext
{
public NetworkStream Stream;
public Socket Socket;
}
/// <summary>
/// A <see cref="IHttpClientContext"/> have been disconnected.
/// </summary>
public class DisconnectedEventArgs : EventArgs
{
/// <summary>
/// Gets reason to why client disconnected.
/// </summary>
public SocketError Error { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="DisconnectedEventArgs"/> class.
/// </summary>
/// <param name="error">Reason to disconnection.</param>
public DisconnectedEventArgs(SocketError error)
{
Error = error;
}
}
/// <summary>
///
/// </summary>
public class RequestEventArgs : EventArgs
{
/// <summary>
/// Gets received request.
/// </summary>
public IHttpRequest Request { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="RequestEventArgs"/> class.
/// </summary>
/// <param name="request">The request.</param>
public RequestEventArgs(IHttpRequest request)
{
Request = request;
}
}
}

View File

@ -0,0 +1,165 @@
using System;
using System.Collections.Specialized;
using System.IO;
using OSHttpServer.Exceptions;
namespace OSHttpServer
{
/// <summary>
/// Contains server side HTTP request information.
/// </summary>
public interface IHttpRequest : ICloneable
{
/// <summary>
/// Gets kind of types accepted by the client.
/// </summary>
string[] AcceptTypes { get; }
uint ID {get; }
/// <summary>
/// Gets or sets body stream.
/// </summary>
Stream Body { get; set; }
/// <summary>
/// Gets whether the body is complete.
/// </summary>
bool BodyIsComplete { get; }
/// <summary>
/// Gets or sets kind of connection used for the session.
/// </summary>
ConnectionType Connection { get; set; }
IHttpClientContext Context { get; }
/// <summary>
/// Gets or sets number of bytes in the body.
/// </summary>
int ContentLength { get; set; }
/// <summary>
/// Gets cookies that was sent with the request.
/// </summary>
RequestCookies Cookies { get; }
/// <summary>
/// Gets form parameters.
/// </summary>
//HttpForm Form { get; }
/// <summary>
/// Gets headers sent by the client.
/// </summary>
NameValueCollection Headers { get; }
/// <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"/>
string HttpVersion { get; set; }
/// <summary>
/// Gets whether the request was made by Ajax (Asynchronous JavaScript)
/// </summary>
bool IsAjax { get; }
/// <summary>
/// Gets or sets requested method.
/// </summary>
/// <remarks>
/// Will always be in upper case.
/// </remarks>
/// <see cref="Method"/>
string Method { get; set; }
/// <summary>
/// Gets parameter from <see cref="QueryString"/> or <see cref="Form"/>.
/// </summary>
HttpParam Param { get; }
/// <summary>
/// Gets variables sent in the query string
/// </summary>
HttpInput QueryString { get; }
/// <summary>
/// Gets or sets requested URI.
/// </summary>
Uri Uri { get; set; }
/// <summary>
/// Gets URI absolute path divided into parts.
/// </summary>
/// <example>
/// // URI is: http://gauffin.com/code/tiny/
/// Console.WriteLine(request.UriParts[0]); // result: code
/// Console.WriteLine(request.UriParts[1]); // result: tiny
/// </example>
/// <remarks>
/// If you're using controllers than the first part is controller name,
/// the second part is method name and the third part is Id property.
/// </remarks>
/// <seealso cref="Uri"/>
string[] UriParts { get; }
/// <summary>
/// Gets or sets path and query.
/// </summary>
/// <see cref="Uri"/>
/// <remarks>
/// Are only used during request parsing. Cannot be set after "Host" header have been
/// added.
/// </remarks>
string UriPath { get; set; }
/// <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>
void AddHeader(string name, string value);
/// <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>
int AddToBody(byte[] bytes, int offset, int length);
/// <summary>
/// Clear everything in the request
/// </summary>
void Clear();
/// <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>
//void DecodeBody(FormDecoderProvider providers);
/// <summary>
/// Sets the cookies.
/// </summary>
/// <param name="cookies">The cookies.</param>
void SetCookies(RequestCookies cookies);
/// <summary>
/// Create a response object.
/// </summary>
/// <param name="context">Context for the connected client.</param>
/// <returns>A new <see cref="IHttpResponse"/>.</returns>
//IHttpResponse CreateResponse(IHttpClientContext context);
}
}

View File

@ -0,0 +1,94 @@
using System;
using OSHttpServer.Exceptions;
using OSHttpServer.Parser;
namespace OSHttpServer
{
/// <summary>
/// Event driven parser used to parse incoming HTTP requests.
/// </summary>
/// <remarks>
/// The parser supports partial messages and keeps the states between
/// each parsed buffer. It's therefore important that the parser gets
/// <see cref="Clear"/>ed if a client disconnects.
/// </remarks>
public interface IHttpRequestParser
{
/// <summary>
/// Current state in parser.
/// </summary>
RequestParserState CurrentState { get; }
/// <summary>
/// Parse partial or complete message.
/// </summary>
/// <param name="buffer">buffer containing incoming bytes</param>
/// <param name="offset">where in buffer that parsing should start</param>
/// <param name="count">number of bytes to parse</param>
/// <returns>Unparsed bytes left in buffer.</returns>
/// <exception cref="BadRequestException"><c>BadRequestException</c>.</exception>
int Parse(byte[] buffer, int offset, int count);
/// <summary>
/// A request have been successfully parsed.
/// </summary>
event EventHandler RequestCompleted;
/// <summary>
/// More body bytes have been received.
/// </summary>
event EventHandler<BodyEventArgs> BodyBytesReceived;
/// <summary>
/// Request line have been received.
/// </summary>
event EventHandler<RequestLineEventArgs> RequestLineReceived;
/// <summary>
/// A header have been received.
/// </summary>
event EventHandler<HeaderEventArgs> HeaderReceived;
/// <summary>
/// Clear parser state.
/// </summary>
void Clear();
/// <summary>
/// Gets or sets the log writer.
/// </summary>
ILogWriter LogWriter { get; set; }
}
/// <summary>
/// Current state in the parsing.
/// </summary>
public enum RequestParserState
{
/// <summary>
/// Should parse the request line
/// </summary>
FirstLine,
/// <summary>
/// Searching for a complete header name
/// </summary>
HeaderName,
/// <summary>
/// Searching for colon after header name (ignoring white spaces)
/// </summary>
AfterName,
/// <summary>
/// Searching for start of header value (ignoring white spaces)
/// </summary>
Between,
/// <summary>
/// Searching for a complete header value (can span over multiple lines, as long as they are prefixed with one/more whitespaces)
/// </summary>
HeaderValue,
/// <summary>
/// Adding bytes to body
/// </summary>
Body
}
}

View File

@ -0,0 +1,180 @@
using System;
using System.IO;
using System.Net;
using System.Text;
namespace OSHttpServer
{
/// <summary>
/// Response that is sent back to the web browser / client.
///
/// A response can be sent if different ways. The easiest one is
/// to just fill the Body stream with content, everything else
/// will then be taken care of by the framework. The default content-type
/// is text/html, you should change it if you send anything else.
///
/// The second and slighty more complex way is to send the response
/// as parts. Start with sending the header using the SendHeaders method and
/// then you can send the body using SendBody method, but do not forget
/// to set ContentType and ContentLength before doing so.
/// </summary>
/// <example>
/// public void MyHandler(IHttpRequest request, IHttpResponse response)
/// {
///
/// }
/// </example>
public interface IHttpResponse
{
/// <summary>
/// The body stream is used to cache the body contents
/// before sending everything to the client. It's the simplest
/// way to serve documents.
/// </summary>
Stream Body { get; set; }
byte[] RawBuffer { get; set; }
int RawBufferStart { get; set; }
int RawBufferLen { get; set; }
uint requestID { get; }
/// <summary>
/// Defines the version of the HTTP Response for applications where it's required
/// for this to be forced.
/// </summary>
string ProtocolVersion { get; set; }
int Priority { get; set; }
/// <summary>
/// The chunked encoding modifies the body of a message in order to
/// transfer it as a series of chunks, each with its own size indicator,
/// followed by an OPTIONAL trailer containing entity-header fields. This
/// allows dynamically produced content to be transferred along with the
/// information necessary for the recipient to verify that it has
/// received the full message.
/// </summary>
bool Chunked { get; set; }
/// <summary>
/// Kind of connection
/// </summary>
ConnectionType Connection { get; set; }
/// <summary>
/// Encoding to use when sending stuff to the client.
/// </summary>
/// <remarks>Default is UTF8</remarks>
Encoding Encoding { get; set; }
/// <summary>
/// Number of seconds to keep connection alive
/// </summary>
/// <remarks>Only used if Connection property is set to ConnectionType.KeepAlive</remarks>
int KeepAlive { get; set; }
/// <summary>
/// Status code that is sent to the client.
/// </summary>
/// <remarks>Default is HttpStatusCode.Ok</remarks>
HttpStatusCode Status { get; set; }
/// <summary>
/// Information about why a specific status code was used.
/// </summary>
string Reason { get; set; }
/// <summary>
/// Size of the body. MUST be specified before sending the header,
/// unless property Chunked is set to true.
/// </summary>
long ContentLength { get; set; }
/// <summary>
/// Kind of content in the body
/// </summary>
/// <remarks>Default is text/html</remarks>
string ContentType { get; set; }
/// <summary>
/// Headers have been sent to the client-
/// </summary>
/// <remarks>You can not send any additional headers if they have already been sent.</remarks>
bool HeadersSent { get; }
/// <summary>
/// The whole response have been sent.
/// </summary>
bool Sent { get; }
/// <summary>
/// Cookies that should be created/changed.
/// </summary>
ResponseCookies Cookies { get; }
/// <summary>
/// Add another header to the document.
/// </summary>
/// <param name="name">Name of the header, case sensitive, use lower cases.</param>
/// <param name="value">Header values can span over multiple lines as long as each line starts with a white space. New line chars should be \r\n</param>
/// <exception cref="InvalidOperationException">If headers already been sent.</exception>
/// <exception cref="ArgumentException">If value conditions have not been met.</exception>
/// <remarks>Adding any header will override the default ones and those specified by properties.</remarks>
void AddHeader(string name, string value);
/// <summary>
/// Send headers and body to the browser.
/// </summary>
/// <exception cref="InvalidOperationException">If content have already been sent.</exception>
void Send();
/// <summary>
/// Make sure that you have specified ContentLength and sent the headers first.
/// </summary>
/// <param name="buffer"></param>
/// <exception cref="InvalidOperationException">If headers have not been sent.</exception>
/// <see cref="IHttpResponse.SendHeaders"/>
/// <param name="offset">offest of first byte to send</param>
/// <param name="count">number of bytes to send.</param>
/// <seealso cref="IHttpResponse.Send"/>
/// <seealso cref="IHttpResponse.SendHeaders"/>
/// <remarks>This method can be used if you want to send body contents without caching them first. This
/// is recommended for larger files to keep the memory usage low.</remarks>
bool SendBody(byte[] buffer, int offset, int count);
/// <summary>
/// Make sure that you have specified ContentLength and sent the headers first.
/// </summary>
/// <param name="buffer"></param>
/// <exception cref="InvalidOperationException">If headers have not been sent.</exception>
/// <see cref="IHttpResponse.SendHeaders"/>
/// <seealso cref="IHttpResponse.Send"/>
/// <seealso cref="IHttpResponse.SendHeaders"/>
/// <remarks>This method can be used if you want to send body contents without caching them first. This
/// is recommended for larger files to keep the memory usage low.</remarks>
bool SendBody(byte[] buffer);
/// <summary>
/// Send headers to the client.
/// </summary>
/// <exception cref="InvalidOperationException">If headers already been sent.</exception>
/// <seealso cref="IHttpResponse.AddHeader"/>
/// <seealso cref="IHttpResponse.Send"/>
/// <seealso cref="IHttpResponse.SendBody(byte[])"/>
bool SendHeaders();
}
/// <summary>
/// Type of HTTP connection
/// </summary>
public enum ConnectionType
{
/// <summary>
/// Connection is closed after each request-response
/// </summary>
Close,
/// <summary>
/// Connection is kept alive for X seconds (unless another request have been made)
/// </summary>
KeepAlive
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Diagnostics;
using System.Text;
namespace OSHttpServer
{
/// <summary>
/// Priority for log entries
/// </summary>
/// <seealso cref="ILogWriter"/>
public enum LogPrio
{
None,
/// <summary>
/// Very detailed logs to be able to follow the flow of the program.
/// </summary>
Trace,
/// <summary>
/// Logs to help debug errors in the application
/// </summary>
Debug,
/// <summary>
/// Information to be able to keep track of state changes etc.
/// </summary>
Info,
/// <summary>
/// Something did not go as we expected, but it's no problem.
/// </summary>
Warning,
/// <summary>
/// Something that should not fail failed, but we can still keep
/// on going.
/// </summary>
Error,
/// <summary>
/// Something failed, and we cannot handle it properly.
/// </summary>
Fatal
}
/// <summary>
/// Interface used to write to log files.
/// </summary>
public interface ILogWriter
{
/// <summary>
/// Write an entry to the log file.
/// </summary>
/// <param name="source">object that is writing to the log</param>
/// <param name="priority">importance of the log message</param>
/// <param name="message">the message</param>
void Write(object source, LogPrio priority, string message);
}
/// <summary>
/// Default log writer, writes everything to null (nowhere).
/// </summary>
/// <seealso cref="ILogWriter"/>
public sealed class NullLogWriter : ILogWriter
{
/// <summary>
/// The logging instance.
/// </summary>
public static readonly NullLogWriter Instance = new NullLogWriter();
/// <summary>
/// Writes everything to null
/// </summary>
/// <param name="source">object that wrote the log entry.</param>
/// <param name="prio">Importance of the log message</param>
/// <param name="message">The message.</param>
public void Write(object source, LogPrio prio, string message) {}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Net;
namespace OSHttpServer.Exceptions
{
/// <summary>
/// The server encountered an unexpected condition which prevented it from fulfilling the request.
/// </summary>
public class InternalServerException : HttpException
{
/// <summary>
/// Initializes a new instance of the <see cref="InternalServerException"/> class.
/// </summary>
public InternalServerException()
: base(HttpStatusCode.InternalServerError, "The server encountered an unexpected condition which prevented it from fulfilling the request.")
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InternalServerException"/> class.
/// </summary>
/// <param name="message">error message.</param>
public InternalServerException(string message)
: base(HttpStatusCode.InternalServerError, message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InternalServerException"/> class.
/// </summary>
/// <param name="message">error message.</param>
/// <param name="inner">inner exception.</param>
public InternalServerException(string message, Exception inner)
: base(HttpStatusCode.InternalServerError, message, inner)
{
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Net;
namespace OSHttpServer.Exceptions
{
/// <summary>
/// The requested resource was not found in the web server.
/// </summary>
public class NotFoundException : HttpException
{
/// <summary>
/// Create a new exception
/// </summary>
/// <param name="message">message describing the error</param>
/// <param name="inner">inner exception</param>
public NotFoundException(string message, Exception inner) : base(HttpStatusCode.NotFound, message, inner)
{
}
/// <summary>
/// Create a new exception
/// </summary>
/// <param name="message">message describing the error</param>
public NotFoundException(string message)
: base(HttpStatusCode.NotFound, message)
{
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Web;
namespace OSHttpServer
{
/// <summary>
/// cookie sent by the client/browser
/// </summary>
/// <seealso cref="ResponseCookie"/>
public class RequestCookie
{
private readonly string _name = null;
private string _value = null;
/// <summary>
/// Constructor.
/// </summary>
/// <param name="id">cookie identifier</param>
/// <param name="content">cookie content</param>
/// <exception cref="ArgumentNullException">id or content is null</exception>
/// <exception cref="ArgumentException">id is empty</exception>
public RequestCookie(string id, string content)
{
if (string.IsNullOrEmpty(id)) throw new ArgumentNullException("id");
if (content == null) throw new ArgumentNullException("content");
_name = id;
_value = content;
}
#region inherited methods
/// <summary>
/// Gets the cookie HTML representation.
/// </summary>
/// <returns>cookie string</returns>
public override string ToString()
{
return string.Format("{0}={1}; ", HttpUtility.UrlEncode(_name), HttpUtility.UrlEncode(_value));
}
#endregion
#region public properties
/// <summary>
/// Gets the cookie identifier.
/// </summary>
public string Name
{
get { return _name; }
}
/// <summary>
/// Cookie value. Set to null to remove cookie.
/// </summary>
public string Value
{
get { return _value; }
set
{
_value = value;
}
}
#endregion
}
}

View File

@ -0,0 +1,164 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace OSHttpServer
{
/// <summary>
/// This class is created as a wrapper, since there are two different cookie types in .Net (Cookie and HttpCookie).
/// The framework might switch class in the future and we dont want to have to replace all instances
/// </summary>
public sealed class RequestCookies : IEnumerable<RequestCookie>
{
private readonly IDictionary<string, RequestCookie> _items = new Dictionary<string, RequestCookie>();
/// <summary>
/// Let's copy all the cookies.
/// </summary>
/// <param name="cookies">value from cookie header.</param>
public RequestCookies(string cookies)
{
if (string.IsNullOrEmpty(cookies))
return;
string name = string.Empty;
int state = 0;
int start = -1;
for (int i = 0; i < cookies.Length; ++i)
{
char ch = cookies[i];
// searching for start of cookie name
switch (state)
{
case 0:
if (char.IsWhiteSpace(ch))
continue;
start = i;
++state;
break;
case 1:
if (char.IsWhiteSpace(ch) || ch == '=')
{
if (start == -1)
return; // todo: decide if an exception should be thrown.
name = cookies.Substring(start, i - start);
start = -1;
++state;
}
break;
case 2:
if (!char.IsWhiteSpace(ch) && ch != '=')
{
start = i;
++state;
}
break;
case 3:
if (ch == ';')
{
if (start >= -1)
Add(new RequestCookie(name, cookies.Substring(start, i - start)));
start = -1;
state = 0;
name = string.Empty;
}
break;
}
}
// last cookie
if (name != string.Empty)
Add(new RequestCookie(name, cookies.Substring(start, cookies.Length - start)));
}
/// <summary>
/// Adds a cookie in the collection.
/// </summary>
/// <param name="cookie">cookie to add</param>
/// <exception cref="ArgumentNullException">cookie is null</exception>
internal void Add(RequestCookie cookie)
{
// Verifies the parameter
if (cookie == null)
throw new ArgumentNullException("cookie");
if (cookie.Name == null || cookie.Name.Trim() == string.Empty)
throw new ArgumentException("Name must be specified.");
if (cookie.Value == null || cookie.Value.Trim() == string.Empty)
throw new ArgumentException("Content must be specified.");
if (_items.ContainsKey(cookie.Name))
_items[cookie.Name] = cookie;
else _items.Add(cookie.Name, cookie);
}
/// <summary>
/// Gets the count of cookies in the collection.
/// </summary>
public int Count
{
get { return _items.Count; }
}
/// <summary>
/// Gets the cookie of a given identifier (null if not existing).
/// </summary>
public RequestCookie this[string id]
{
get
{
return _items.ContainsKey(id) ? _items[id] : null;
}
}
/// <summary>
/// Gets a collection enumerator on the cookie list.
/// </summary>
/// <returns>collection enumerator</returns>
public IEnumerator GetEnumerator()
{
return _items.Values.GetEnumerator();
}
/// <summary>
/// Remove all cookies.
/// </summary>
public void Clear()
{
_items.Clear();
}
#region IEnumerable<RequestCookie> Members
///<summary>
///Returns an enumerator that iterates through the collection.
///</summary>
///
///<returns>
///A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection.
///</returns>
///<filterpriority>1</filterpriority>
IEnumerator<RequestCookie> IEnumerable<RequestCookie>.GetEnumerator()
{
return _items.Values.GetEnumerator();
}
#endregion
/// <summary>
/// Remove a cookie from the collection.
/// </summary>
/// <param name="cookieName">Name of cookie.</param>
public void Remove(string cookieName)
{
lock (_items)
{
if (!_items.ContainsKey(cookieName))
return;
_items.Remove(cookieName);
}
}
}
}

View File

@ -0,0 +1,48 @@
using System;
namespace OSHttpServer.Parser
{
/// <summary>
/// Used when the request line have been successfully parsed.
/// </summary>
public class RequestLineEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="RequestLineEventArgs"/> class.
/// </summary>
/// <param name="httpMethod">The HTTP method.</param>
/// <param name="uriPath">The URI path.</param>
/// <param name="httpVersion">The HTTP version.</param>
public RequestLineEventArgs(string httpMethod, string uriPath, string httpVersion)
{
HttpMethod = httpMethod;
UriPath = uriPath;
HttpVersion = httpVersion;
}
/// <summary>
/// Initializes a new instance of the <see cref="RequestLineEventArgs"/> class.
/// </summary>
public RequestLineEventArgs()
{
}
/// <summary>
/// Gets or sets http method.
/// </summary>
/// <remarks>
/// Should be one of the methods declared in <see cref="Method"/>.
/// </remarks>
public string HttpMethod { get; set; }
/// <summary>
/// Gets or sets the version of the HTTP protocol that the client want to use.
/// </summary>
public string HttpVersion { get; set; }
/// <summary>
/// Gets or sets requested URI path.
/// </summary>
public string UriPath { get; set; }
}
}

View File

@ -0,0 +1,33 @@
using OSHttpServer.Parser;
namespace OSHttpServer
{
/// <summary>
/// Creates request parsers when needed.
/// </summary>
public class RequestParserFactory : IRequestParserFactory
{
/// <summary>
/// Create a new request parser.
/// </summary>
/// <param name="logWriter">Used when logging should be enabled.</param>
/// <returns>A new request parser.</returns>
public IHttpRequestParser CreateParser(ILogWriter logWriter)
{
return new HttpRequestParser(logWriter);
}
}
/// <summary>
/// Creates request parsers when needed.
/// </summary>
public interface IRequestParserFactory
{
/// <summary>
/// Create a new request parser.
/// </summary>
/// <param name="logWriter">Used when logging should be enabled.</param>
/// <returns>A new request parser.</returns>
IHttpRequestParser CreateParser(ILogWriter logWriter);
}
}

View File

@ -0,0 +1,123 @@
using System;
using System.Web;
namespace OSHttpServer
{
/// <summary>
/// cookie being sent back to the browser.
/// </summary>
/// <seealso cref="ResponseCookie"/>
public class ResponseCookie : RequestCookie
{
private const string _nullPath = "/";
private bool _persistant = false;
private DateTime _expires;
private string _path = "/";
private readonly string _domain;
#region constructors
/// <summary>
/// Constructor.
/// </summary>
/// <param name="id">cookie identifier</param>
/// <param name="content">cookie content</param>
/// <param name="expiresAt">cookie expiration date. Use DateTime.MinValue for session cookie.</param>
/// <exception cref="ArgumentNullException">id or content is null</exception>
/// <exception cref="ArgumentException">id is empty</exception>
public ResponseCookie(string id, string content, DateTime expiresAt)
: base(id, content)
{
if (expiresAt != DateTime.MinValue)
{
_expires = expiresAt;
_persistant = true;
}
}
/// <summary>
/// Create a new cookie
/// </summary>
/// <param name="name">name identifying the cookie</param>
/// <param name="value">cookie value</param>
/// <param name="expires">when the cookie expires. Setting DateTime.MinValue will delete the cookie when the session is closed.</param>
/// <param name="path">Path to where the cookie is valid</param>
/// <param name="domain">Domain that the cookie is valid for.</param>
public ResponseCookie(string name, string value, DateTime expires, string path, string domain)
: this(name, value, expires)
{
_domain = domain;
_path = path;
}
/// <summary>
/// Create a new cookie
/// </summary>
/// <param name="cookie">Name and value will be used</param>
/// <param name="expires">when the cookie expires.</param>
public ResponseCookie(RequestCookie cookie, DateTime expires)
: this(cookie.Name, cookie.Value, expires)
{}
#endregion
#region inherited methods
/// <summary>
/// Gets the cookie HTML representation.
/// </summary>
/// <returns>cookie string</returns>
public override string ToString()
{
string temp = string.Format("{0}={1}; ", HttpUtility.UrlEncode(Name), HttpUtility.UrlEncode(Value));
if (_persistant)
{
TimeSpan span = DateTime.Now - DateTime.UtcNow;
DateTime utc = _expires.Subtract(span);
temp += string.Format("expires={0};", utc.ToString("r"));
}
if (!string.IsNullOrEmpty(_path))
temp += string.Format("path={0}; ", _path);
if (!string.IsNullOrEmpty(_domain))
temp += string.Format("domain={0}; ", _domain);
return temp;
}
#endregion
#region public properties
/// <summary>
/// When the cookie expires.
/// DateTime.MinValue means that the cookie expires when the session do so.
/// </summary>
public DateTime Expires
{
get { return _expires; }
set
{
_expires = value;
_persistant = value != DateTime.MinValue;
}
}
/// <summary>
/// Cookie is only valid under this path.
/// </summary>
public string Path
{
get { return _path; }
set
{
if (!string.IsNullOrEmpty(value))
_path = value;
else
_path = _nullPath;
}
}
#endregion
}
}

View File

@ -0,0 +1,108 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace OSHttpServer
{
/// <summary>
/// Cookies that should be set.
/// </summary>
public sealed class ResponseCookies : IEnumerable<ResponseCookie>
{
private readonly IDictionary<string, ResponseCookie> _items = new Dictionary<string, ResponseCookie>();
/// <summary>
/// Adds a cookie in the collection.
/// </summary>
/// <param name="cookie">cookie to add</param>
/// <exception cref="ArgumentNullException">cookie is null</exception>
public void Add(ResponseCookie cookie)
{
// Verifies the parameter
if (cookie == null)
throw new ArgumentNullException("cookie");
if (cookie.Name == null || cookie.Name.Trim() == string.Empty)
throw new ArgumentException("Name must be specified.");
if (cookie.Value == null || cookie.Value.Trim() == string.Empty)
throw new ArgumentException("Content must be specified.");
if (_items.ContainsKey(cookie.Name))
_items[cookie.Name] = cookie;
else _items.Add(cookie.Name, cookie);
}
/// <summary>
/// Copy a request cookie
/// </summary>
/// <param name="cookie"></param>
/// <param name="expires">When the cookie should expire</param>
public void Add(RequestCookie cookie, DateTime expires)
{
Add(new ResponseCookie(cookie, expires));
}
/// <summary>
/// Gets the count of cookies in the collection.
/// </summary>
public int Count
{
get { return _items.Count; }
}
/// <summary>
/// Gets the cookie of a given identifier (null if not existing).
/// </summary>
public ResponseCookie this[string id]
{
get
{
if (_items.ContainsKey(id))
return _items[id];
else
return null;
}
set
{
if (_items.ContainsKey(id))
_items[id] = value;
else
Add(value);
}
}
/// <summary>
/// Gets a collection enumerator on the cookie list.
/// </summary>
/// <returns>collection enumerator</returns>
public IEnumerator GetEnumerator()
{
return _items.Values.GetEnumerator();
}
/// <summary>
/// Remove all cookies
/// </summary>
public void Clear()
{
_items.Clear();
}
#region IEnumerable<ResponseCookie> Members
///<summary>
///Returns an enumerator that iterates through the collection.
///</summary>
///
///<returns>
///A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection.
///</returns>
///<filterpriority>1</filterpriority>
IEnumerator<ResponseCookie> IEnumerable<ResponseCookie>.GetEnumerator()
{
return _items.Values.GetEnumerator();
}
#endregion
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Net;
namespace OSHttpServer.Exceptions
{
/// <summary>
/// The request requires user authentication. The response MUST include a
/// WWW-Authenticate header field (section 14.47) containing a challenge
/// applicable to the requested resource.
///
/// The client MAY repeat the request with a suitable Authorization header
/// field (section 14.8). If the request already included Authorization
/// credentials, then the 401 response indicates that authorization has been
/// refused for those credentials. If the 401 response contains the same challenge
/// as the prior response, and the user agent has already attempted authentication
/// at least once, then the user SHOULD be presented the entity that was given in the response,
/// since that entity might include relevant diagnostic information.
///
/// HTTP access authentication is explained in rfc2617:
/// http://www.ietf.org/rfc/rfc2617.txt
///
/// (description is taken from
/// http://www.submissionchamber.com/help-guides/error-codes.php#sec10.4.2)
/// </summary>
public class UnauthorizedException : HttpException
{
/// <summary>
/// Create a new unauhtorized exception.
/// </summary>
/// <seealso cref="UnauthorizedException"/>
public UnauthorizedException()
: base(HttpStatusCode.Unauthorized, "The request requires user authentication.")
{
}
/// <summary>
/// Create a new unauhtorized exception.
/// </summary>
/// <param name="message">reason to why the request was unauthorized.</param>
/// <param name="inner">inner exception</param>
public UnauthorizedException(string message, Exception inner)
: base(HttpStatusCode.Unauthorized, message, inner)
{
}
/// <summary>
/// Create a new unauhtorized exception.
/// </summary>
/// <param name="message">reason to why the request was unauthorized.</param>
public UnauthorizedException(string message)
: base(HttpStatusCode.Unauthorized, message)
{
}
}
}

View File

@ -1,180 +0,0 @@
/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using log4net;
using Nwc.XmlRpc;
namespace OpenSim.Framework.Servers.HttpServer
{
public delegate XmlRpcResponse OSHttpXmlRpcProcessor(XmlRpcRequest request);
public class OSHttpXmlRpcHandler: OSHttpHandler
{
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// XmlRpcMethodMatch tries to reify (deserialize) an incoming
/// XmlRpc request (and posts it to the "whiteboard") and
/// checks whether the method name is one we are interested
/// in.
/// </summary>
/// <returns>true if the handler is interested in the content;
/// false otherwise</returns>
protected bool XmlRpcMethodMatch(OSHttpRequest req)
{
XmlRpcRequest xmlRpcRequest = null;
// check whether req is already reified
// if not: reify (and post to whiteboard)
try
{
if (req.Whiteboard.ContainsKey("xmlrequest"))
{
xmlRpcRequest = req.Whiteboard["xmlrequest"] as XmlRpcRequest;
}
else
{
StreamReader body = new StreamReader(req.InputStream);
string requestBody = body.ReadToEnd();
xmlRpcRequest = (XmlRpcRequest)(new XmlRpcRequestDeserializer()).Deserialize(requestBody);
req.Whiteboard["xmlrequest"] = xmlRpcRequest;
}
}
catch (XmlException)
{
_log.ErrorFormat("[OSHttpXmlRpcHandler] failed to deserialize XmlRpcRequest from {0}", req.ToString());
return false;
}
// check against methodName
if ((null != xmlRpcRequest)
&& !String.IsNullOrEmpty(xmlRpcRequest.MethodName)
&& xmlRpcRequest.MethodName == _methodName)
{
_log.DebugFormat("[OSHttpXmlRpcHandler] located handler {0} for {1}", _methodName, req.ToString());
return true;
}
return false;
}
// contains handler for processing XmlRpc Request
private XmlRpcMethod _handler;
// contains XmlRpc method name
private string _methodName;
/// <summary>
/// Instantiate an XmlRpc handler.
/// </summary>
/// <param name="handler">XmlRpcMethod
/// delegate</param>
/// <param name="methodName">XmlRpc method name</param>
/// <param name="path">XmlRpc path prefix (regular expression)</param>
/// <param name="headers">Dictionary with header names and
/// regular expressions to match content of headers</param>
/// <param name="whitelist">IP whitelist of remote end points
/// to accept (regular expression)</param>
/// <remarks>
/// Except for handler and methodName, all other parameters
/// can be null, in which case they are not taken into account
/// when the handler is being looked up.
/// </remarks>
public OSHttpXmlRpcHandler(XmlRpcMethod handler, string methodName, Regex path,
Dictionary<string, Regex> headers, Regex whitelist)
: base(new Regex(@"^POST$", RegexOptions.IgnoreCase | RegexOptions.Compiled), path, null, headers,
new Regex(@"^(text|application)/xml", RegexOptions.IgnoreCase | RegexOptions.Compiled),
whitelist)
{
_handler = handler;
_methodName = methodName;
}
/// <summary>
/// Instantiate an XmlRpc handler.
/// </summary>
/// <param name="handler">XmlRpcMethod
/// delegate</param>
/// <param name="methodName">XmlRpc method name</param>
public OSHttpXmlRpcHandler(XmlRpcMethod handler, string methodName)
: this(handler, methodName, null, null, null)
{
}
/// <summary>
/// Invoked by OSHttpRequestPump.
/// </summary>
public override OSHttpHandlerResult Process(OSHttpRequest request)
{
XmlRpcResponse xmlRpcResponse;
string responseString;
// check whether we are interested in this request
if (!XmlRpcMethodMatch(request)) return OSHttpHandlerResult.Pass;
OSHttpResponse resp = new OSHttpResponse(request);
try
{
// reified XmlRpcRequest must still be on the whiteboard
XmlRpcRequest xmlRpcRequest = request.Whiteboard["xmlrequest"] as XmlRpcRequest;
xmlRpcResponse = _handler(xmlRpcRequest);
responseString = XmlRpcResponseSerializer.Singleton.Serialize(xmlRpcResponse);
resp.ContentType = "text/xml";
byte[] buffer = Encoding.UTF8.GetBytes(responseString);
resp.SendChunked = false;
resp.ContentLength = buffer.Length;
resp.ContentEncoding = Encoding.UTF8;
resp.Body.Write(buffer, 0, buffer.Length);
resp.Body.Flush();
resp.Send();
}
catch (Exception ex)
{
_log.WarnFormat("[OSHttpXmlRpcHandler]: Error: {0}", ex.Message);
return OSHttpHandlerResult.Pass;
}
return OSHttpHandlerResult.Done;
}
}
}

View File

@ -30,7 +30,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using HttpServer;
using OSHttpServer;
using log4net;
using OpenMetaverse;
@ -130,8 +130,16 @@ namespace OpenSim.Framework.Servers.HttpServer
return;
}
if (responsedata.ContainsKey("error_status_text"))
response.StatusDescription = (string)responsedata["error_status_text"];
response.StatusCode = responsecode;
if (responsecode == (int)OSHttpStatusCode.RedirectMovedPermanently)
{
response.AddHeader("Location:", (string)responsedata["str_redirect_location"]);
response.KeepAlive = false;
PollServiceArgs.RequestsHandled++;
response.Send();
return;
}
if (responsedata.ContainsKey("http_protocol_version"))
response.ProtocolVersion = (string)responsedata["http_protocol_version"];
@ -142,17 +150,13 @@ namespace OpenSim.Framework.Servers.HttpServer
if (responsedata.ContainsKey("prio"))
response.Priority = (int)responsedata["prio"];
if (responsedata.ContainsKey("error_status_text"))
response.StatusDescription = (string)responsedata["error_status_text"];
// Cross-Origin Resource Sharing with simple requests
if (responsedata.ContainsKey("access_control_allow_origin"))
response.AddHeader("Access-Control-Allow-Origin", (string)responsedata["access_control_allow_origin"]);
response.StatusCode = responsecode;
if (responsecode == (int)OSHttpStatusCode.RedirectMovedPermanently)
{
response.RedirectLocation = (string)responsedata["str_redirect_location"];
}
if (string.IsNullOrEmpty(contentType))
response.AddHeader("Content-Type", "text/html");
else

View File

@ -32,7 +32,7 @@ using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using HttpServer;
using OSHttpServer;
namespace OpenSim.Framework.Servers.HttpServer
{

View File

@ -1,439 +0,0 @@
/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using HttpServer;
using HttpServer.FormDecoders;
using NUnit.Framework;
using OpenSim.Framework.Servers.HttpServer;
using OpenSim.Tests.Common;
namespace OpenSim.Framework.Servers.Tests
{
/*
[TestFixture]
public class OSHttpTests : OpenSimTestCase
{
// we need an IHttpClientContext for our tests
public class TestHttpClientContext: IHttpClientContext
{
private bool _secured;
public bool IsSecured
{
get { return _secured; }
}
public bool Secured
{
get { return _secured; }
}
public TestHttpClientContext(bool secured)
{
_secured = secured;
}
public void Disconnect(SocketError error) {}
public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body) {}
public void Respond(string httpVersion, HttpStatusCode statusCode, string reason) {}
public void Respond(string body) {}
public void Send(byte[] buffer) {}
public void Send(byte[] buffer, int offset, int size) {}
public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType) {}
public void Close() { }
public bool EndWhenDone { get { return false;} set { return;}}
public HTTPNetworkContext GiveMeTheNetworkStreamIKnowWhatImDoing()
{
return new HTTPNetworkContext();
}
public event EventHandler<DisconnectedEventArgs> Disconnected = delegate { };
/// <summary>
/// A request have been received in the context.
/// </summary>
public event EventHandler<RequestEventArgs> RequestReceived = delegate { };
public bool CanSend { get { return true; } }
public string RemoteEndPoint { get { return ""; } }
public string RemoteEndPointAddress { get { return ""; } }
public string RemoteEndPointPort { get { return ""; } }
}
public class TestHttpRequest: IHttpRequest
{
private string _uriPath;
public bool BodyIsComplete
{
get { return true; }
}
public string[] AcceptTypes
{
get {return _acceptTypes; }
}
private string[] _acceptTypes;
public Stream Body
{
get { return _body; }
set { _body = value;}
}
private Stream _body;
public ConnectionType Connection
{
get { return _connection; }
set { _connection = value; }
}
private ConnectionType _connection;
public int ContentLength
{
get { return _contentLength; }
set { _contentLength = value; }
}
private int _contentLength;
public NameValueCollection Headers
{
get { return _headers; }
}
private NameValueCollection _headers = new NameValueCollection();
public string HttpVersion
{
get { return _httpVersion; }
set { _httpVersion = value; }
}
private string _httpVersion = null;
public string Method
{
get { return _method; }
set { _method = value; }
}
private string _method = null;
public HttpInput QueryString
{
get { return _queryString; }
}
private HttpInput _queryString = null;
public Uri Uri
{
get { return _uri; }
set { _uri = value; }
}
private Uri _uri = null;
public string[] UriParts
{
get { return _uri.Segments; }
}
public HttpParam Param
{
get { return null; }
}
public HttpForm Form
{
get { return null; }
}
public bool IsAjax
{
get { return false; }
}
public RequestCookies Cookies
{
get { return null; }
}
public TestHttpRequest() {}
public TestHttpRequest(string contentEncoding, string contentType, string userAgent,
string remoteAddr, string remotePort, string[] acceptTypes,
ConnectionType connectionType, int contentLength, Uri uri)
{
_headers["content-encoding"] = contentEncoding;
_headers["content-type"] = contentType;
_headers["user-agent"] = userAgent;
_headers["remote_addr"] = remoteAddr;
_headers["remote_port"] = remotePort;
_acceptTypes = acceptTypes;
_connection = connectionType;
_contentLength = contentLength;
_uri = uri;
}
public void DecodeBody(FormDecoderProvider providers) {}
public void SetCookies(RequestCookies cookies) {}
public void AddHeader(string name, string value)
{
_headers.Add(name, value);
}
public int AddToBody(byte[] bytes, int offset, int length)
{
return 0;
}
public void Clear() {}
public object Clone()
{
TestHttpRequest clone = new TestHttpRequest();
clone._acceptTypes = _acceptTypes;
clone._connection = _connection;
clone._contentLength = _contentLength;
clone._uri = _uri;
clone._headers = new NameValueCollection(_headers);
return clone;
}
public IHttpResponse CreateResponse(IHttpClientContext context)
{
return new HttpResponse(context, this);
}
/// <summary>
/// Path and query (will be merged with the host header) and put in Uri
/// </summary>
/// <see cref="Uri"/>
public string UriPath
{
get { return _uriPath; }
set
{
_uriPath = value;
}
}
}
public class TestHttpResponse: IHttpResponse
{
public Stream Body
{
get { return _body; }
set { _body = value; }
}
private Stream _body;
public string ProtocolVersion
{
get { return _protocolVersion; }
set { _protocolVersion = value; }
}
private string _protocolVersion;
public bool Chunked
{
get { return _chunked; }
set { _chunked = value; }
}
private bool _chunked;
public ConnectionType Connection
{
get { return _connection; }
set { _connection = value; }
}
private ConnectionType _connection;
public Encoding Encoding
{
get { return _encoding; }
set { _encoding = value; }
}
private Encoding _encoding;
public int KeepAlive
{
get { return _keepAlive; }
set { _keepAlive = value; }
}
private int _keepAlive;
public HttpStatusCode Status
{
get { return _status; }
set { _status = value; }
}
private HttpStatusCode _status;
public string Reason
{
get { return _reason; }
set { _reason = value; }
}
private string _reason;
public long ContentLength
{
get { return _contentLength; }
set { _contentLength = value; }
}
private long _contentLength;
public string ContentType
{
get { return _contentType; }
set { _contentType = value; }
}
private string _contentType;
public bool HeadersSent
{
get { return _headersSent; }
}
private bool _headersSent;
public bool Sent
{
get { return _sent; }
}
private bool _sent;
public ResponseCookies Cookies
{
get { return _cookies; }
}
private ResponseCookies _cookies = null;
public TestHttpResponse()
{
_headersSent = false;
_sent = false;
}
public void AddHeader(string name, string value) {}
public void Send()
{
if (!_headersSent) SendHeaders();
if (_sent) throw new InvalidOperationException("stuff already sent");
_sent = true;
}
public void SendBody(byte[] buffer, int offset, int count)
{
if (!_headersSent) SendHeaders();
_sent = true;
}
public void SendBody(byte[] buffer)
{
if (!_headersSent) SendHeaders();
_sent = true;
}
public void SendHeaders()
{
if (_headersSent) throw new InvalidOperationException("headers already sent");
_headersSent = true;
}
public void Redirect(Uri uri) {}
public void Redirect(string url) {}
}
public OSHttpRequest req0;
public OSHttpRequest req1;
public OSHttpResponse rsp0;
public IPEndPoint ipEP0;
[TestFixtureSetUp]
public void Init()
{
TestHttpRequest threq0 = new TestHttpRequest("utf-8", "text/xml", "OpenSim Test Agent", "192.168.0.1", "4711",
new string[] {"text/xml"},
ConnectionType.KeepAlive, 4711,
new Uri("http://127.0.0.1/admin/inventory/Dr+Who/Tardis"));
threq0.Method = "GET";
threq0.HttpVersion = HttpHelper.HTTP10;
TestHttpRequest threq1 = new TestHttpRequest("utf-8", "text/xml", "OpenSim Test Agent", "192.168.0.1", "4711",
new string[] {"text/xml"},
ConnectionType.KeepAlive, 4711,
new Uri("http://127.0.0.1/admin/inventory/Dr+Who/Tardis?a=0&b=1&c=2"));
threq1.Method = "POST";
threq1.HttpVersion = HttpHelper.HTTP11;
threq1.Headers["x-wuff"] = "wuffwuff";
threq1.Headers["www-authenticate"] = "go away";
req0 = new OSHttpRequest(new TestHttpClientContext(false), threq0);
req1 = new OSHttpRequest(new TestHttpClientContext(false), threq1);
rsp0 = new OSHttpResponse(new TestHttpResponse());
ipEP0 = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 4711);
}
[Test]
public void T000_OSHttpRequest()
{
Assert.That(req0.HttpMethod, Is.EqualTo("GET"));
Assert.That(req0.ContentType, Is.EqualTo("text/xml"));
Assert.That(req0.ContentLength, Is.EqualTo(4711));
Assert.That(req1.HttpMethod, Is.EqualTo("POST"));
}
[Test]
public void T001_OSHttpRequestHeaderAccess()
{
Assert.That(req1.Headers["x-wuff"], Is.EqualTo("wuffwuff"));
Assert.That(req1.Headers.Get("x-wuff"), Is.EqualTo("wuffwuff"));
Assert.That(req1.Headers["www-authenticate"], Is.EqualTo("go away"));
Assert.That(req1.Headers.Get("www-authenticate"), Is.EqualTo("go away"));
Assert.That(req0.RemoteIPEndPoint, Is.EqualTo(ipEP0));
}
[Test]
public void T002_OSHttpRequestUriParsing()
{
Assert.That(req0.RawUrl, Is.EqualTo("/admin/inventory/Dr+Who/Tardis"));
Assert.That(req1.Url.ToString(), Is.EqualTo("http://127.0.0.1/admin/inventory/Dr+Who/Tardis?a=0&b=1&c=2"));
}
[Test]
public void T100_OSHttpResponse()
{
rsp0.ContentType = "text/xml";
Assert.That(rsp0.ContentType, Is.EqualTo("text/xml"));
}
}
*/
}

View File

@ -603,7 +603,8 @@ namespace OpenSim.Region.Framework.Scenes
|| (sbyte)AssetType.Notecard == assetType
|| (sbyte)AssetType.LSLText == assetType
|| (sbyte)OpenSimAssetType.Material == assetType
|| (sbyte)AssetType.Object == assetType)
|| (sbyte)AssetType.Object == assetType
|| (sbyte)AssetType.Settings == assetType)
{
AddForInspection(assetUuid);
}

View File

@ -31,7 +31,7 @@ using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using HttpServer;
using OSHttpServer;
using OpenSim.Framework;
namespace OpenSim.Tests.Common

View File

@ -28,8 +28,7 @@
using System;
using System.Collections.Specialized;
using System.IO;
using HttpServer;
using HttpServer.FormDecoders;
using OSHttpServer;
namespace OpenSim.Tests.Common
{

View File

@ -29,7 +29,7 @@ using System;
using System.IO;
using System.Net;
using System.Text;
using HttpServer;
using OSHttpServer;
namespace OpenSim.Tests.Common
{

View File

@ -68,6 +68,11 @@ namespace OpenSim.Tests.Common
/// </summary>
public long ContentLength64 { get; set; }
public int Priority { get; set; }
public byte[] RawBuffer { get; set; }
public int RawBufferStart { get; set; }
public int RawBufferLen { get; set; }
/// <summary>
/// Encoding of the body content.
/// </summary>
@ -97,11 +102,6 @@ namespace OpenSim.Tests.Common
/// </summary>
public Stream Body { get; private set; }
/// <summary>
/// Set a redirct location.
/// </summary>
public string RedirectLocation { private get; set; }
/// <summary>
/// Chunk transfers.
/// </summary>

Binary file not shown.

View File

@ -209,20 +209,11 @@
<Reference name="OpenMetaverseTypes" path="../../../../bin/"/>
<Reference name="XMLRPC" path="../../../../bin/"/>
<Reference name="log4net" path="../../../../bin/"/>
<Reference name="HttpServer_OpenSim" path="../../../../bin/"/>
<Reference name="SmartThreadPool"/>
<Files>
<Match pattern="*.cs" recurse="true">
<Exclude name="obj" pattern="obj"/>
<Exclude pattern="Tests"/>
<!-- on temporary suspension -->
<Exclude pattern="OSHttpHandler\.cs"/>
<Exclude pattern="OSHttpHttpHandler\.cs"/>
<Exclude pattern="OSHttpRequestPump\.cs"/>
<Exclude pattern="OSHttpRequestQueue\.cs"/>
<Exclude pattern="OSHttpServer.*\.cs"/>
<Exclude pattern="OSHttpXmlRpcHandler.*\.cs"/>
</Match>
</Files>
</Project>
@ -369,11 +360,11 @@
<Reference name="OpenMetaverseTypes" path="../../../bin/"/>
<Reference name="XMLRPC" path="../../../bin/"/>
<Reference name="log4net" path="../../../bin/"/>
<Reference name="HttpServer_OpenSim" path="../../../bin/"/>
<Reference name="Nini" path="../../../bin/"/>
<Files>
<Match pattern="*.cs" recurse="false"/>
<Exclude name="obj" pattern="obj"/>
</Files>
</Project>
@ -2700,7 +2691,6 @@
<Reference name="System.Drawing"/>
<Reference name="System.Xml"/>
<Reference name="System.Web"/>
<Reference name="HttpServer_OpenSim" path="../../../bin/"/>
<Reference name="log4net" path="../../../bin/"/>
<Reference name="Mono.Addins" path="../../../bin/"/>
<Reference name="Nini" path="../../../bin/"/>
@ -3018,7 +3008,6 @@
<Reference name="OpenSim.Framework.Servers"/>
<Reference name="OpenSim.Framework.Servers.HttpServer"/>
<Reference name="log4net" path="../../../../bin/"/>
<Reference name="HttpServer_OpenSim" path="../../../../bin/"/>
<Reference name="nunit.framework" path="../../../../bin/"/>
<Files>
@ -3222,7 +3211,6 @@
<ReferencePath>../../../../../bin/</ReferencePath>
<Reference name="System"/>
<Reference name="System.Xml"/>
<Reference name="HttpServer_OpenSim" path="../../../../../bin/"/>
<Reference name="log4net" path="../../../../../bin/"/>
<Reference name="Nini" path="../../../../../bin/"/>
<Reference name="nunit.framework" path="../../../../../bin/"/>