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
parent
650b051cdf
commit
67cd5efab3
|
@ -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.SoundWAV] = ASSET_EXTENSION_SEPARATOR + "sound.wav";
|
||||||
ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.Texture] = ASSET_EXTENSION_SEPARATOR + "texture.jp2";
|
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)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 + "animation.bvh"] = (sbyte)AssetType.Animation;
|
||||||
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "bodypart.txt"] = (sbyte)AssetType.Bodypart;
|
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.jp2"] = (sbyte)AssetType.Texture;
|
||||||
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "texture.tga"] = (sbyte)AssetType.TextureTGA;
|
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 + "material.xml"] = (sbyte)OpenSimAssetType.Material;
|
||||||
|
EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "settings.bin"] = (sbyte)AssetType.Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string CreateOarLandDataPath(LandData ld)
|
public static string CreateOarLandDataPath(LandData ld)
|
||||||
|
|
|
@ -28,27 +28,24 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
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.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.Text;
|
||||||
using System.Threading;
|
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using HttpServer;
|
using OSHttpServer;
|
||||||
|
using tinyHTTPListener = OSHttpServer.OSHttpListener;
|
||||||
using log4net;
|
using log4net;
|
||||||
using Nwc.XmlRpc;
|
using Nwc.XmlRpc;
|
||||||
using OpenMetaverse.StructuredData;
|
|
||||||
using CoolHTTPListener = HttpServer.HttpListener;
|
|
||||||
using LogPrio = HttpServer.LogPrio;
|
|
||||||
using OpenSim.Framework.Monitoring;
|
using OpenSim.Framework.Monitoring;
|
||||||
using System.IO.Compression;
|
using OpenMetaverse.StructuredData;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using OpenSim.Framework.Servers;
|
|
||||||
|
|
||||||
namespace OpenSim.Framework.Servers.HttpServer
|
namespace OpenSim.Framework.Servers.HttpServer
|
||||||
{
|
{
|
||||||
|
@ -94,8 +91,7 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
private volatile int NotSocketErrors = 0;
|
private volatile int NotSocketErrors = 0;
|
||||||
public volatile bool HTTPDRunning = false;
|
public volatile bool HTTPDRunning = false;
|
||||||
|
|
||||||
// protected HttpListener m_httpListener;
|
protected tinyHTTPListener m_httpListener;
|
||||||
protected CoolHTTPListener m_httpListener2;
|
|
||||||
protected Dictionary<string, XmlRpcMethod> m_rpcHandlers = new Dictionary<string, XmlRpcMethod>();
|
protected Dictionary<string, XmlRpcMethod> m_rpcHandlers = new Dictionary<string, XmlRpcMethod>();
|
||||||
protected Dictionary<string, JsonRPCMethod> jsonRpcHandlers = new Dictionary<string, JsonRPCMethod>();
|
protected Dictionary<string, JsonRPCMethod> jsonRpcHandlers = new Dictionary<string, JsonRPCMethod>();
|
||||||
protected Dictionary<string, bool> m_rpcHandlersKeepAlive = new Dictionary<string, bool>();
|
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;
|
protected IPAddress m_listenIPAddress = IPAddress.Any;
|
||||||
|
|
||||||
|
|
||||||
public string Protocol
|
public string Protocol
|
||||||
{
|
{
|
||||||
get { return m_ssl ? "https://" : "http://"; }
|
get { return m_ssl ? "https://" : "http://"; }
|
||||||
|
@ -1955,7 +1950,7 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
|
|
||||||
if (responsecode == (int)OSHttpStatusCode.RedirectMovedPermanently)
|
if (responsecode == (int)OSHttpStatusCode.RedirectMovedPermanently)
|
||||||
{
|
{
|
||||||
response.RedirectLocation = (string)responsedata["str_redirect_location"];
|
response.AddHeader("Location:", (string)responsedata["str_redirect_location"]);
|
||||||
response.StatusCode = responsecode;
|
response.StatusCode = responsecode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2051,32 +2046,32 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
NotSocketErrors = 0;
|
NotSocketErrors = 0;
|
||||||
if (!m_ssl)
|
if (!m_ssl)
|
||||||
{
|
{
|
||||||
//m_httpListener.Prefixes.Add("http://+:" + m_port + "/");
|
m_httpListener = tinyHTTPListener.Create(m_listenIPAddress, (int)m_port);
|
||||||
//m_httpListener.Prefixes.Add("http://10.1.1.5:" + m_port + "/");
|
m_httpListener.ExceptionThrown += httpServerException;
|
||||||
m_httpListener2 = CoolHTTPListener.Create(m_listenIPAddress, (int)m_port);
|
if (DebugLevel > 0)
|
||||||
m_httpListener2.ExceptionThrown += httpServerException;
|
{
|
||||||
m_httpListener2.LogWriter = httpserverlog;
|
m_httpListener.LogWriter = httpserverlog;
|
||||||
|
httpserverlog.DebugLevel = 1;
|
||||||
|
}
|
||||||
// Uncomment this line in addition to those in HttpServerLogWriter
|
// Uncomment this line in addition to those in HttpServerLogWriter
|
||||||
// if you want more detailed trace information from the HttpServer
|
// if you want more detailed trace information from the HttpServer
|
||||||
//m_httpListener2.UseTraceLogs = true;
|
|
||||||
|
|
||||||
//m_httpListener2.DisconnectHandler = httpServerDisconnectMonitor;
|
//m_httpListener2.DisconnectHandler = httpServerDisconnectMonitor;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//m_httpListener.Prefixes.Add("https://+:" + (m_sslport) + "/");
|
m_httpListener = tinyHTTPListener.Create(IPAddress.Any, (int)m_port, m_cert);
|
||||||
//m_httpListener.Prefixes.Add("http://+:" + m_port + "/");
|
|
||||||
m_httpListener2 = CoolHTTPListener.Create(IPAddress.Any, (int)m_port, m_cert);
|
|
||||||
if(m_certificateValidationCallback != null)
|
if(m_certificateValidationCallback != null)
|
||||||
m_httpListener2.CertificateValidationCallback = m_certificateValidationCallback;
|
m_httpListener.CertificateValidationCallback = m_certificateValidationCallback;
|
||||||
m_httpListener2.ExceptionThrown += httpServerException;
|
m_httpListener.ExceptionThrown += httpServerException;
|
||||||
m_httpListener2.LogWriter = httpserverlog;
|
if (DebugLevel > 0)
|
||||||
|
{
|
||||||
|
m_httpListener.LogWriter = httpserverlog;
|
||||||
|
httpserverlog.DebugLevel = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_httpListener2.RequestReceived += OnRequest;
|
m_httpListener.RequestReceived += OnRequest;
|
||||||
//m_httpListener.Start();
|
m_httpListener.Start(64);
|
||||||
m_httpListener2.Start(64);
|
|
||||||
|
|
||||||
lock(m_generalLock)
|
lock(m_generalLock)
|
||||||
{
|
{
|
||||||
|
@ -2089,13 +2084,6 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPDRunning = true;
|
HTTPDRunning = true;
|
||||||
|
|
||||||
//HttpListenerContext context;
|
|
||||||
//while (true)
|
|
||||||
//{
|
|
||||||
// context = m_httpListener.GetContext();
|
|
||||||
// ThreadPool.UnsafeQueueUserWorkItem(new WaitCallback(HandleRequest), context);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -2155,12 +2143,12 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
m_pollServiceManager.Stop();
|
m_pollServiceManager.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_httpListener2.ExceptionThrown -= httpServerException;
|
m_httpListener.ExceptionThrown -= httpServerException;
|
||||||
//m_httpListener2.DisconnectHandler = null;
|
//m_httpListener2.DisconnectHandler = null;
|
||||||
|
|
||||||
m_httpListener2.LogWriter = null;
|
m_httpListener.LogWriter = null;
|
||||||
m_httpListener2.RequestReceived -= OnRequest;
|
m_httpListener.RequestReceived -= OnRequest;
|
||||||
m_httpListener2.Stop();
|
m_httpListener.Stop();
|
||||||
}
|
}
|
||||||
catch (NullReferenceException)
|
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
|
/// You may also be able to get additional trace information from HttpServer if you uncomment the UseTraceLogs
|
||||||
/// property in StartHttp() for the HttpListener
|
/// property in StartHttp() for the HttpListener
|
||||||
|
///
|
||||||
public class HttpServerLogWriter : ILogWriter
|
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)
|
public void Write(object source, LogPrio priority, string message)
|
||||||
{
|
{
|
||||||
/*
|
if((int)priority < DebugLevel)
|
||||||
|
return;
|
||||||
|
|
||||||
switch (priority)
|
switch (priority)
|
||||||
{
|
{
|
||||||
case LogPrio.Trace:
|
case LogPrio.Trace:
|
||||||
|
@ -2335,8 +2327,6 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,17 +92,16 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
Stream OutputStream { get; }
|
Stream OutputStream { get; }
|
||||||
|
|
||||||
string ProtocolVersion { get; set; }
|
string ProtocolVersion { get; set; }
|
||||||
|
int Priority { get; set; }
|
||||||
|
byte[] RawBuffer { get; set; }
|
||||||
|
int RawBufferStart { get; set; }
|
||||||
|
int RawBufferLen { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return the output stream feeding the body.
|
/// Return the output stream feeding the body.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Stream Body { get; }
|
Stream Body { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set a redirct location.
|
|
||||||
/// </summary>
|
|
||||||
string RedirectLocation { set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Chunk transfers.
|
/// Chunk transfers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -34,7 +34,7 @@ using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using HttpServer;
|
using OSHttpServer;
|
||||||
using log4net;
|
using log4net;
|
||||||
|
|
||||||
namespace OpenSim.Framework.Servers.HttpServer
|
namespace OpenSim.Framework.Servers.HttpServer
|
||||||
|
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,7 +28,7 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using HttpServer;
|
using OSHttpServer;
|
||||||
|
|
||||||
namespace OpenSim.Framework.Servers.HttpServer
|
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>
|
/// <summary>
|
||||||
/// Chunk transfers.
|
/// Chunk transfers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,7 +30,7 @@ using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using HttpServer;
|
using OSHttpServer;
|
||||||
using log4net;
|
using log4net;
|
||||||
using OpenMetaverse;
|
using OpenMetaverse;
|
||||||
|
|
||||||
|
@ -130,8 +130,16 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
return;
|
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"))
|
if (responsedata.ContainsKey("http_protocol_version"))
|
||||||
response.ProtocolVersion = (string)responsedata["http_protocol_version"];
|
response.ProtocolVersion = (string)responsedata["http_protocol_version"];
|
||||||
|
@ -142,17 +150,13 @@ namespace OpenSim.Framework.Servers.HttpServer
|
||||||
if (responsedata.ContainsKey("prio"))
|
if (responsedata.ContainsKey("prio"))
|
||||||
response.Priority = (int)responsedata["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
|
// Cross-Origin Resource Sharing with simple requests
|
||||||
if (responsedata.ContainsKey("access_control_allow_origin"))
|
if (responsedata.ContainsKey("access_control_allow_origin"))
|
||||||
response.AddHeader("Access-Control-Allow-Origin", (string)responsedata["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))
|
if (string.IsNullOrEmpty(contentType))
|
||||||
response.AddHeader("Content-Type", "text/html");
|
response.AddHeader("Content-Type", "text/html");
|
||||||
else
|
else
|
||||||
|
|
|
@ -32,7 +32,7 @@ using System.Net;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using HttpServer;
|
using OSHttpServer;
|
||||||
|
|
||||||
namespace OpenSim.Framework.Servers.HttpServer
|
namespace OpenSim.Framework.Servers.HttpServer
|
||||||
{
|
{
|
||||||
|
|
|
@ -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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
|
@ -603,7 +603,8 @@ namespace OpenSim.Region.Framework.Scenes
|
||||||
|| (sbyte)AssetType.Notecard == assetType
|
|| (sbyte)AssetType.Notecard == assetType
|
||||||
|| (sbyte)AssetType.LSLText == assetType
|
|| (sbyte)AssetType.LSLText == assetType
|
||||||
|| (sbyte)OpenSimAssetType.Material == assetType
|
|| (sbyte)OpenSimAssetType.Material == assetType
|
||||||
|| (sbyte)AssetType.Object == assetType)
|
|| (sbyte)AssetType.Object == assetType
|
||||||
|
|| (sbyte)AssetType.Settings == assetType)
|
||||||
{
|
{
|
||||||
AddForInspection(assetUuid);
|
AddForInspection(assetUuid);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using HttpServer;
|
using OSHttpServer;
|
||||||
using OpenSim.Framework;
|
using OpenSim.Framework;
|
||||||
|
|
||||||
namespace OpenSim.Tests.Common
|
namespace OpenSim.Tests.Common
|
||||||
|
|
|
@ -28,8 +28,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using HttpServer;
|
using OSHttpServer;
|
||||||
using HttpServer.FormDecoders;
|
|
||||||
|
|
||||||
namespace OpenSim.Tests.Common
|
namespace OpenSim.Tests.Common
|
||||||
{
|
{
|
||||||
|
|
|
@ -29,7 +29,7 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using HttpServer;
|
using OSHttpServer;
|
||||||
|
|
||||||
namespace OpenSim.Tests.Common
|
namespace OpenSim.Tests.Common
|
||||||
{
|
{
|
||||||
|
|
|
@ -68,6 +68,11 @@ namespace OpenSim.Tests.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long ContentLength64 { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Encoding of the body content.
|
/// Encoding of the body content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -97,11 +102,6 @@ namespace OpenSim.Tests.Common
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Stream Body { get; private set; }
|
public Stream Body { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set a redirct location.
|
|
||||||
/// </summary>
|
|
||||||
public string RedirectLocation { private get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Chunk transfers.
|
/// Chunk transfers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
Binary file not shown.
14
prebuild.xml
14
prebuild.xml
|
@ -209,20 +209,11 @@
|
||||||
<Reference name="OpenMetaverseTypes" path="../../../../bin/"/>
|
<Reference name="OpenMetaverseTypes" path="../../../../bin/"/>
|
||||||
<Reference name="XMLRPC" path="../../../../bin/"/>
|
<Reference name="XMLRPC" path="../../../../bin/"/>
|
||||||
<Reference name="log4net" path="../../../../bin/"/>
|
<Reference name="log4net" path="../../../../bin/"/>
|
||||||
<Reference name="HttpServer_OpenSim" path="../../../../bin/"/>
|
|
||||||
<Reference name="SmartThreadPool"/>
|
<Reference name="SmartThreadPool"/>
|
||||||
|
|
||||||
<Files>
|
<Files>
|
||||||
<Match pattern="*.cs" recurse="true">
|
<Match pattern="*.cs" recurse="true">
|
||||||
<Exclude name="obj" pattern="obj"/>
|
<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>
|
</Match>
|
||||||
</Files>
|
</Files>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -369,11 +360,11 @@
|
||||||
<Reference name="OpenMetaverseTypes" path="../../../bin/"/>
|
<Reference name="OpenMetaverseTypes" path="../../../bin/"/>
|
||||||
<Reference name="XMLRPC" path="../../../bin/"/>
|
<Reference name="XMLRPC" path="../../../bin/"/>
|
||||||
<Reference name="log4net" path="../../../bin/"/>
|
<Reference name="log4net" path="../../../bin/"/>
|
||||||
<Reference name="HttpServer_OpenSim" path="../../../bin/"/>
|
|
||||||
<Reference name="Nini" path="../../../bin/"/>
|
<Reference name="Nini" path="../../../bin/"/>
|
||||||
|
|
||||||
<Files>
|
<Files>
|
||||||
<Match pattern="*.cs" recurse="false"/>
|
<Match pattern="*.cs" recurse="false"/>
|
||||||
|
<Exclude name="obj" pattern="obj"/>
|
||||||
</Files>
|
</Files>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
||||||
|
@ -2700,7 +2691,6 @@
|
||||||
<Reference name="System.Drawing"/>
|
<Reference name="System.Drawing"/>
|
||||||
<Reference name="System.Xml"/>
|
<Reference name="System.Xml"/>
|
||||||
<Reference name="System.Web"/>
|
<Reference name="System.Web"/>
|
||||||
<Reference name="HttpServer_OpenSim" path="../../../bin/"/>
|
|
||||||
<Reference name="log4net" path="../../../bin/"/>
|
<Reference name="log4net" path="../../../bin/"/>
|
||||||
<Reference name="Mono.Addins" path="../../../bin/"/>
|
<Reference name="Mono.Addins" path="../../../bin/"/>
|
||||||
<Reference name="Nini" path="../../../bin/"/>
|
<Reference name="Nini" path="../../../bin/"/>
|
||||||
|
@ -3018,7 +3008,6 @@
|
||||||
<Reference name="OpenSim.Framework.Servers"/>
|
<Reference name="OpenSim.Framework.Servers"/>
|
||||||
<Reference name="OpenSim.Framework.Servers.HttpServer"/>
|
<Reference name="OpenSim.Framework.Servers.HttpServer"/>
|
||||||
<Reference name="log4net" path="../../../../bin/"/>
|
<Reference name="log4net" path="../../../../bin/"/>
|
||||||
<Reference name="HttpServer_OpenSim" path="../../../../bin/"/>
|
|
||||||
<Reference name="nunit.framework" path="../../../../bin/"/>
|
<Reference name="nunit.framework" path="../../../../bin/"/>
|
||||||
|
|
||||||
<Files>
|
<Files>
|
||||||
|
@ -3222,7 +3211,6 @@
|
||||||
<ReferencePath>../../../../../bin/</ReferencePath>
|
<ReferencePath>../../../../../bin/</ReferencePath>
|
||||||
<Reference name="System"/>
|
<Reference name="System"/>
|
||||||
<Reference name="System.Xml"/>
|
<Reference name="System.Xml"/>
|
||||||
<Reference name="HttpServer_OpenSim" path="../../../../../bin/"/>
|
|
||||||
<Reference name="log4net" path="../../../../../bin/"/>
|
<Reference name="log4net" path="../../../../../bin/"/>
|
||||||
<Reference name="Nini" path="../../../../../bin/"/>
|
<Reference name="Nini" path="../../../../../bin/"/>
|
||||||
<Reference name="nunit.framework" path="../../../../../bin/"/>
|
<Reference name="nunit.framework" path="../../../../../bin/"/>
|
||||||
|
|
Loading…
Reference in New Issue