From 67cd5efab34b3f36840dcb9a956791ca94e6e43d Mon Sep 17 00:00:00 2001 From: UbitUmarov Date: Thu, 2 Apr 2020 21:44:34 +0100 Subject: [PATCH] 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 --- .../Serialization/ArchiveConstants.cs | 4 +- .../Servers/HttpServer/BaseHttpServer.cs | 94 +-- .../HttpServer/Interfaces/IOSHttpResponse.cs | 9 +- .../Servers/HttpServer/OSHttpHandler.cs | 183 ---- .../Servers/HttpServer/OSHttpHttpHandler.cs | 145 ---- .../Servers/HttpServer/OSHttpRequest.cs | 2 +- .../Servers/HttpServer/OSHttpRequestPump.cs | 298 ------- .../Servers/HttpServer/OSHttpRequestQueue.cs | 68 -- .../Servers/HttpServer/OSHttpResponse.cs | 15 +- .../Servers/HttpServer/OSHttpServer.cs | 210 ----- .../OSHttpServer/BadRequestException.cs | 33 + .../HttpServer/OSHttpServer/BodyEventArgs.cs | 55 ++ .../OSHttpServer/ClientAcceptedEventArgs.cs | 50 ++ .../OSHttpServer/ContextTimeoutManager.cs | 413 +++++++++ .../OSHttpServer/ExceptionEventArgs.cs | 29 + .../OSHttpServer/ExceptionHandler.cs | 16 + .../OSHttpServer/ForbiddenException.cs | 25 + .../OSHttpServer/HeaderEventArgs.cs | 38 + .../OSHttpServer/HttpClientContext.cs | 786 ++++++++++++++++++ .../OSHttpServer/HttpContextFactory.cs | 195 +++++ .../HttpServer/OSHttpServer/HttpException.cs | 43 + .../HttpServer/OSHttpServer/HttpHelper.cs | 118 +++ .../HttpServer/OSHttpServer/HttpInput.cs | 263 ++++++ .../HttpServer/OSHttpServer/HttpInputItem.cs | 309 +++++++ .../HttpServer/OSHttpServer/HttpListener.cs | 248 ++++++ .../HttpServer/OSHttpServer/HttpParam.cs | 114 +++ .../HttpServer/OSHttpServer/HttpRequest.cs | 435 ++++++++++ .../OSHttpServer/HttpRequestParser.cs | 417 ++++++++++ .../HttpServer/OSHttpServer/HttpResponse.cs | 670 +++++++++++++++ .../OSHttpServer/IHttpClientContext.cs | 146 ++++ .../HttpServer/OSHttpServer/IHttpRequest.cs | 165 ++++ .../OSHttpServer/IHttpRequestParser.cs | 94 +++ .../HttpServer/OSHttpServer/IHttpResponse.cs | 180 ++++ .../HttpServer/OSHttpServer/ILogWriter.cs | 81 ++ .../OSHttpServer/InternalServerException.cs | 38 + .../OSHttpServer/NotFoundException.cs | 29 + .../HttpServer/OSHttpServer/RequestCookie.cs | 70 ++ .../HttpServer/OSHttpServer/RequestCookies.cs | 164 ++++ .../OSHttpServer/RequestLineEventArgs.cs | 48 ++ .../OSHttpServer/RequestParserFactory.cs | 33 + .../HttpServer/OSHttpServer/ResponseCookie.cs | 123 +++ .../OSHttpServer/ResponseCookies.cs | 108 +++ .../OSHttpServer/UnauthorizedException.cs | 56 ++ .../Servers/HttpServer/OSHttpXmlRpcHandler.cs | 180 ---- .../HttpServer/PollServiceHttpRequest.cs | 24 +- .../HttpServer/WebsocketServerHandler.cs | 2 +- .../Framework/Servers/Tests/OSHttpTests.cs | 439 ---------- .../Region/Framework/Scenes/UuidGatherer.cs | 3 +- .../Common/Mock/TestHttpClientContext.cs | 2 +- OpenSim/Tests/Common/Mock/TestHttpRequest.cs | 3 +- OpenSim/Tests/Common/Mock/TestHttpResponse.cs | 2 +- .../Tests/Common/Mock/TestOSHttpResponse.cs | 10 +- bin/HttpServer_OpenSim.dll | Bin 128512 -> 0 bytes prebuild.xml | 14 +- 54 files changed, 5669 insertions(+), 1630 deletions(-) delete mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpHandler.cs delete mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpHttpHandler.cs delete mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpRequestPump.cs delete mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpRequestQueue.cs delete mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/BadRequestException.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/BodyEventArgs.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/ClientAcceptedEventArgs.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/ContextTimeoutManager.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/ExceptionEventArgs.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/ExceptionHandler.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/ForbiddenException.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HeaderEventArgs.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpClientContext.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpContextFactory.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpException.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpHelper.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpInput.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpInputItem.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpListener.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpParam.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpRequest.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpRequestParser.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpResponse.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpClientContext.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpRequest.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpRequestParser.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpResponse.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/ILogWriter.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/InternalServerException.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/NotFoundException.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestCookie.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestCookies.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestLineEventArgs.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestParserFactory.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/ResponseCookie.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/ResponseCookies.cs create mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpServer/UnauthorizedException.cs delete mode 100644 OpenSim/Framework/Servers/HttpServer/OSHttpXmlRpcHandler.cs delete mode 100644 OpenSim/Framework/Servers/Tests/OSHttpTests.cs delete mode 100755 bin/HttpServer_OpenSim.dll diff --git a/OpenSim/Framework/Serialization/ArchiveConstants.cs b/OpenSim/Framework/Serialization/ArchiveConstants.cs index 9081411129..d3557a714e 100644 --- a/OpenSim/Framework/Serialization/ArchiveConstants.cs +++ b/OpenSim/Framework/Serialization/ArchiveConstants.cs @@ -125,7 +125,8 @@ namespace OpenSim.Framework.Serialization ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.SoundWAV] = ASSET_EXTENSION_SEPARATOR + "sound.wav"; ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.Texture] = ASSET_EXTENSION_SEPARATOR + "texture.jp2"; ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.TextureTGA] = ASSET_EXTENSION_SEPARATOR + "texture.tga"; - ASSET_TYPE_TO_EXTENSION[(sbyte)OpenSimAssetType.Material] = ASSET_EXTENSION_SEPARATOR + "material.xml"; // Not sure if we'll ever see this + ASSET_TYPE_TO_EXTENSION[(sbyte)OpenSimAssetType.Material] = ASSET_EXTENSION_SEPARATOR + "material.xml"; + ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.Settings] = ASSET_EXTENSION_SEPARATOR + "settings.bin"; EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "animation.bvh"] = (sbyte)AssetType.Animation; EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "bodypart.txt"] = (sbyte)AssetType.Bodypart; @@ -147,6 +148,7 @@ namespace OpenSim.Framework.Serialization EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "texture.jp2"] = (sbyte)AssetType.Texture; EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "texture.tga"] = (sbyte)AssetType.TextureTGA; EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "material.xml"] = (sbyte)OpenSimAssetType.Material; + EXTENSION_TO_ASSET_TYPE[ASSET_EXTENSION_SEPARATOR + "settings.bin"] = (sbyte)AssetType.Settings; } public static string CreateOarLandDataPath(LandData ld) diff --git a/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs b/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs index e108eb58f7..188042df14 100644 --- a/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs +++ b/OpenSim/Framework/Servers/HttpServer/BaseHttpServer.cs @@ -28,27 +28,24 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Reflection; using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Reflection; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Threading; using System.Xml; -using HttpServer; +using OSHttpServer; +using tinyHTTPListener = OSHttpServer.OSHttpListener; using log4net; using Nwc.XmlRpc; -using OpenMetaverse.StructuredData; -using CoolHTTPListener = HttpServer.HttpListener; -using LogPrio = HttpServer.LogPrio; using OpenSim.Framework.Monitoring; -using System.IO.Compression; -using System.Security.Cryptography; -using OpenSim.Framework.Servers; +using OpenMetaverse.StructuredData; + namespace OpenSim.Framework.Servers.HttpServer { @@ -94,8 +91,7 @@ namespace OpenSim.Framework.Servers.HttpServer private volatile int NotSocketErrors = 0; public volatile bool HTTPDRunning = false; - // protected HttpListener m_httpListener; - protected CoolHTTPListener m_httpListener2; + protected tinyHTTPListener m_httpListener; protected Dictionary m_rpcHandlers = new Dictionary(); protected Dictionary jsonRpcHandlers = new Dictionary(); protected Dictionary m_rpcHandlersKeepAlive = new Dictionary(); @@ -121,7 +117,6 @@ namespace OpenSim.Framework.Servers.HttpServer protected IPAddress m_listenIPAddress = IPAddress.Any; - public string Protocol { get { return m_ssl ? "https://" : "http://"; } @@ -1243,7 +1238,7 @@ namespace OpenSim.Framework.Servers.HttpServer rcn = "SSLCN:" + rcn; xmlRprcRequest.Params.Add(rcn); // Param[4] or Param[5] } - + try { xmlRpcResponse = method(xmlRprcRequest, request.RemoteIPEndPoint); @@ -1955,7 +1950,7 @@ namespace OpenSim.Framework.Servers.HttpServer if (responsecode == (int)OSHttpStatusCode.RedirectMovedPermanently) { - response.RedirectLocation = (string)responsedata["str_redirect_location"]; + response.AddHeader("Location:", (string)responsedata["str_redirect_location"]); response.StatusCode = responsecode; } @@ -2051,32 +2046,32 @@ namespace OpenSim.Framework.Servers.HttpServer NotSocketErrors = 0; if (!m_ssl) { - //m_httpListener.Prefixes.Add("http://+:" + m_port + "/"); - //m_httpListener.Prefixes.Add("http://10.1.1.5:" + m_port + "/"); - m_httpListener2 = CoolHTTPListener.Create(m_listenIPAddress, (int)m_port); - m_httpListener2.ExceptionThrown += httpServerException; - m_httpListener2.LogWriter = httpserverlog; - + m_httpListener = tinyHTTPListener.Create(m_listenIPAddress, (int)m_port); + m_httpListener.ExceptionThrown += httpServerException; + if (DebugLevel > 0) + { + m_httpListener.LogWriter = httpserverlog; + httpserverlog.DebugLevel = 1; + } // Uncomment this line in addition to those in HttpServerLogWriter // if you want more detailed trace information from the HttpServer - //m_httpListener2.UseTraceLogs = true; - //m_httpListener2.DisconnectHandler = httpServerDisconnectMonitor; } else { - //m_httpListener.Prefixes.Add("https://+:" + (m_sslport) + "/"); - //m_httpListener.Prefixes.Add("http://+:" + m_port + "/"); - m_httpListener2 = CoolHTTPListener.Create(IPAddress.Any, (int)m_port, m_cert); + m_httpListener = tinyHTTPListener.Create(IPAddress.Any, (int)m_port, m_cert); if(m_certificateValidationCallback != null) - m_httpListener2.CertificateValidationCallback = m_certificateValidationCallback; - m_httpListener2.ExceptionThrown += httpServerException; - m_httpListener2.LogWriter = httpserverlog; + m_httpListener.CertificateValidationCallback = m_certificateValidationCallback; + m_httpListener.ExceptionThrown += httpServerException; + if (DebugLevel > 0) + { + m_httpListener.LogWriter = httpserverlog; + httpserverlog.DebugLevel = 1; + } } - m_httpListener2.RequestReceived += OnRequest; - //m_httpListener.Start(); - m_httpListener2.Start(64); + m_httpListener.RequestReceived += OnRequest; + m_httpListener.Start(64); lock(m_generalLock) { @@ -2089,13 +2084,6 @@ namespace OpenSim.Framework.Servers.HttpServer } HTTPDRunning = true; - - //HttpListenerContext context; - //while (true) - //{ - // context = m_httpListener.GetContext(); - // ThreadPool.UnsafeQueueUserWorkItem(new WaitCallback(HandleRequest), context); - // } } catch (Exception e) { @@ -2155,12 +2143,12 @@ namespace OpenSim.Framework.Servers.HttpServer m_pollServiceManager.Stop(); } - m_httpListener2.ExceptionThrown -= httpServerException; + m_httpListener.ExceptionThrown -= httpServerException; //m_httpListener2.DisconnectHandler = null; - m_httpListener2.LogWriter = null; - m_httpListener2.RequestReceived -= OnRequest; - m_httpListener2.Stop(); + m_httpListener.LogWriter = null; + m_httpListener.RequestReceived -= OnRequest; + m_httpListener.Stop(); } catch (NullReferenceException) { @@ -2305,13 +2293,17 @@ namespace OpenSim.Framework.Servers.HttpServer /// /// You may also be able to get additional trace information from HttpServer if you uncomment the UseTraceLogs /// property in StartHttp() for the HttpListener + /// public class HttpServerLogWriter : ILogWriter { -// private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + public int DebugLevel {get; set;} = (int)LogPrio.Error; public void Write(object source, LogPrio priority, string message) { - /* + if((int)priority < DebugLevel) + return; + switch (priority) { case LogPrio.Trace: @@ -2335,8 +2327,6 @@ namespace OpenSim.Framework.Servers.HttpServer default: break; } - */ - return; } } diff --git a/OpenSim/Framework/Servers/HttpServer/Interfaces/IOSHttpResponse.cs b/OpenSim/Framework/Servers/HttpServer/Interfaces/IOSHttpResponse.cs index aba15b019c..ae1ce1e3d4 100644 --- a/OpenSim/Framework/Servers/HttpServer/Interfaces/IOSHttpResponse.cs +++ b/OpenSim/Framework/Servers/HttpServer/Interfaces/IOSHttpResponse.cs @@ -92,17 +92,16 @@ namespace OpenSim.Framework.Servers.HttpServer Stream OutputStream { get; } string ProtocolVersion { get; set; } + int Priority { get; set; } + byte[] RawBuffer { get; set; } + int RawBufferStart { get; set; } + int RawBufferLen { get; set; } /// /// Return the output stream feeding the body. /// Stream Body { get; } - /// - /// Set a redirct location. - /// - string RedirectLocation { set; } - /// /// Chunk transfers. /// diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpHandler.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpHandler.cs deleted file mode 100644 index 2c2b47ded2..0000000000 --- a/OpenSim/Framework/Servers/HttpServer/OSHttpHandler.cs +++ /dev/null @@ -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 -{ - /// - /// Any OSHttpHandler must return one of the following results: - /// - /// - /// result code - /// meaning - /// - /// - /// Pass - /// handler did not process the request - /// - /// - /// Done - /// handler did process the request, OSHttpServer - /// can clean up and close the request - /// - /// - /// - public enum OSHttpHandlerResult - { - Unprocessed, - Pass, - Done, - } - - /// - /// An OSHttpHandler that matches on the "content-type" header can - /// supply an OSHttpContentTypeChecker delegate which will be - /// invoked by the request matcher in OSHttpRequestPump. - /// - /// true if the handler is interested in the content; - /// false otherwise - public delegate bool OSHttpContentTypeChecker(OSHttpRequest req); - - public abstract class OSHttpHandler - { - /// - /// 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 - /// '^$'. - /// - public virtual Regex Method - { - get { return _method; } - } - protected Regex _method; - - /// - /// 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 - /// '^$'. - /// - public virtual Regex Path - { - get { return _path; } - } - protected Regex _path; - - /// - /// Dictionary of (query name, regular expression) tuples, - /// allowing us to match on URI query fields. - /// - public virtual Dictionary Query - { - get { return _query; } - } - protected Dictionary _query; - - /// - /// Dictionary of (header name, regular expression) tuples, - /// allowing us to match on HTTP header fields. - /// - public virtual Dictionary Headers - { - get { return _headers; } - } - protected Dictionary _headers; - - /// - /// Dictionary of (header name, regular expression) tuples, - /// allowing us to match on HTTP header fields. - /// - /// - /// This feature is currently not implemented as it requires - /// (trivial) changes to HttpServer.HttpListener that have not - /// been implemented. - /// - public virtual Regex IPEndPointWhitelist - { - get { return _ipEndPointRegex; } - } - protected Regex _ipEndPointRegex; - - - /// - /// Base class constructor. - /// - /// null or path regex - /// null or dictionary of header - /// regexs - /// null or content type - /// regex - /// null or IP address regex - public OSHttpHandler(Regex method, Regex path, Dictionary query, - Dictionary headers, Regex contentType, Regex whitelist) - { - _method = method; - _path = path; - _query = query; - _ipEndPointRegex = whitelist; - - if (null == _headers && null != contentType) - { - _headers = new Dictionary(); - _headers.Add("content-type", contentType); - } - } - - - /// - /// Process an incoming OSHttpRequest that matched our - /// requirements. - /// - /// - /// OSHttpHandlerResult.Pass if we are after all not - /// interested in the request; OSHttpHandlerResult.Done if we - /// did process the request. - /// - 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(); - } - } -} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpHttpHandler.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpHttpHandler.cs deleted file mode 100644 index 95dafcdd36..0000000000 --- a/OpenSim/Framework/Servers/HttpServer/OSHttpHttpHandler.cs +++ /dev/null @@ -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; - - /// - /// Instantiate an HTTP handler. - /// - /// a GenericHTTPMethod - /// null or HTTP method regex - /// null or path regex - /// null or dictionary with query regexs - /// null or dictionary with header - /// regexs - /// null or IP address whitelist - public OSHttpHttpHandler(GenericHTTPMethod handler, Regex method, Regex path, - Dictionary query, - Dictionary headers, Regex whitelist) - : base(method, path, query, headers, new Regex(@"^text/html", RegexOptions.IgnoreCase | RegexOptions.Compiled), - whitelist) - { - _handler = handler; - } - - /// - /// Instantiate an HTTP handler. - /// - /// a GenericHTTPMethod - public OSHttpHttpHandler(GenericHTTPMethod handler) - : this(handler, new Regex(@"^GET$", RegexOptions.IgnoreCase | RegexOptions.Compiled), null, null, null, null) - { - } - - /// - /// Invoked by OSHttpRequestPump. - /// - 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; - } - } -} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpRequest.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpRequest.cs index 1a6b8cf5f9..2ffb7a96af 100644 --- a/OpenSim/Framework/Servers/HttpServer/OSHttpRequest.cs +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpRequest.cs @@ -34,7 +34,7 @@ using System.Net; using System.Reflection; using System.Text; using System.Web; -using HttpServer; +using OSHttpServer; using log4net; namespace OpenSim.Framework.Servers.HttpServer diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpRequestPump.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpRequestPump.cs deleted file mode 100644 index bdea278810..0000000000 --- a/OpenSim/Framework/Servers/HttpServer/OSHttpRequestPump.cs +++ /dev/null @@ -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 -{ - /// - /// 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. - /// - 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 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(""); - body.WriteLine("
Ooops...
"); - body.WriteLine(String.Format("

{0}

", resp.StatusDescription)); - body.WriteLine(""); - 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 MatchHandlers(OSHttpRequest req, List handlers) - { - Dictionary scoredHandlers = new Dictionary(); - - _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 matchingHandlers = new List(scoredHandlers.Keys); - matchingHandlers.Sort(delegate(OSHttpHandler x, OSHttpHandler y) - { - return scoredHandlers[x] - scoredHandlers[y]; - }); - LogDumpHandlerList(matchingHandlers); - return matchingHandlers; - } - - protected int MatchOnNameValueCollection(NameValueCollection collection, Dictionary 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 l) - { - _log.DebugFormat("[{0}] OSHttpHandlerList dump:", EngineID); - foreach (OSHttpHandler h in l) - _log.DebugFormat(" ", h.ToString()); - } - } -} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpRequestQueue.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpRequestQueue.cs deleted file mode 100644 index 881c5d17e6..0000000000 --- a/OpenSim/Framework/Servers/HttpServer/OSHttpRequestQueue.cs +++ /dev/null @@ -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 -{ - /// - /// OSHttpRequestQueues are used to hand over incoming HTTP - /// requests to OSHttpRequestPump objects. - /// - public class OSHttpRequestQueue : Queue - { - 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; - } - } -} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpResponse.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpResponse.cs index b505d4eb13..179257d545 100644 --- a/OpenSim/Framework/Servers/HttpServer/OSHttpResponse.cs +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpResponse.cs @@ -28,7 +28,7 @@ using System.IO; using System.Net; using System.Text; -using HttpServer; +using OSHttpServer; namespace OpenSim.Framework.Servers.HttpServer { @@ -243,19 +243,6 @@ namespace OpenSim.Framework.Servers.HttpServer } } - /// - /// Set a redirct location. - /// - public string RedirectLocation - { - // get { return _redirectLocation; } - set - { - _httpResponse.Redirect(value); - } - } - - /// /// Chunk transfers. /// diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer.cs deleted file mode 100644 index cd6284259a..0000000000 --- a/OpenSim/Framework/Servers/HttpServer/OSHttpServer.cs +++ /dev/null @@ -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 -{ - /// - /// 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. - /// - 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; } - } - - /// - /// True if this is an HTTPS connection; false otherwise. - /// - protected bool _isSecure; - public bool IsSecure - { - get { return _isSecure; } - } - - public int QueueSize - { - get { return _pumps.Length; } - } - - /// - /// List of registered OSHttpHandlers for this OSHttpServer instance. - /// - protected List _httpHandlers = new List(); - public List OSHttpHandlers - { - get - { - lock (_httpHandlers) - { - return new List(_httpHandlers); - } - } - } - - - /// - /// Instantiate an HTTP server. - /// - 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); - } - - /// - /// Instantiate an HTTPS server. - /// - 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); - } - - /// - /// Turn an HttpRequest into an OSHttpRequestItem and place it - /// in the queue. The OSHttpRequestQueue object will pulse the - /// next available idle pump. - /// - 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); - } - - /// - /// Start the HTTP server engine. - /// - 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); - } - - /// - /// Engine keeps the HTTP server running. - /// - 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); - } - - - /// - /// Add an HTTP request handler. - /// - /// OSHttpHandler delegate - /// regex object for path matching - /// dictionary containing header names - /// and regular expressions to match against header values - 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); - } - } - } -} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/BadRequestException.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/BadRequestException.cs new file mode 100644 index 0000000000..223e4e5487 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/BadRequestException.cs @@ -0,0 +1,33 @@ +using System; +using System.Net; + +namespace OSHttpServer.Exceptions +{ + /// + /// 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 + /// + public class BadRequestException : HttpException + { + /// + /// Create a new bad request exception. + /// + /// reason to why the request was bad. + public BadRequestException(string errMsg) + : base(HttpStatusCode.BadRequest, errMsg) + { + } + + /// + /// Create a new bad request exception. + /// + /// reason to why the request was bad. + /// inner exception + public BadRequestException(string errMsg, Exception inner) + : base(HttpStatusCode.BadRequest, errMsg, inner) + { + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/BodyEventArgs.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/BodyEventArgs.cs new file mode 100644 index 0000000000..91dec7945a --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/BodyEventArgs.cs @@ -0,0 +1,55 @@ +using System; + +namespace OSHttpServer.Parser +{ + /// + /// Arguments used when more body bytes have come. + /// + public class BodyEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// buffer that contains the received bytes. + /// offset in buffer where to start processing. + /// number of bytes from that should be parsed. + public BodyEventArgs(byte[] buffer, int offset, int count) + { + Buffer = buffer; + Offset = offset; + Count = count; + } + + /// + /// Initializes a new instance of the class. + /// + public BodyEventArgs() + { + } + + /// + /// Gets or sets buffer that contains the received bytes. + /// + public byte[] Buffer { get; set; } + /* + /// + /// Gets or sets number of bytes used by the request. + /// + public int BytesUsed { get; set; } + */ + /// + /// Gets or sets number of bytes from that should be parsed. + /// + public int Count { get; set; } + /* + /// + /// Gets or sets whether the body is complete. + /// + public bool IsBodyComplete { get; set; } + */ + /// + /// Gets or sets offset in buffer where to start processing. + /// + public int Offset { get; set; } + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ClientAcceptedEventArgs.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ClientAcceptedEventArgs.cs new file mode 100644 index 0000000000..4139f9f6e3 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ClientAcceptedEventArgs.cs @@ -0,0 +1,50 @@ +using System; +using System.Net.Sockets; + +namespace OSHttpServer +{ + /// + /// Invoked when a client have been accepted by the + /// + /// + /// Can be used to revoke incoming connections + /// + public class ClientAcceptedEventArgs : EventArgs + { + private readonly Socket _socket; + private bool _revoke; + + /// + /// Initializes a new instance of the class. + /// + /// The socket. + public ClientAcceptedEventArgs(Socket socket) + { + _socket = socket; + } + + /// + /// Accepted socket. + /// + public Socket Socket + { + get { return _socket; } + } + + /// + /// Client should be revoked. + /// + public bool Revoked + { + get { return _revoke; } + } + + /// + /// Client may not be handled. + /// + public void Revoke() + { + _revoke = true; + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ContextTimeoutManager.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ContextTimeoutManager.cs new file mode 100644 index 0000000000..aa20786442 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ContextTimeoutManager.cs @@ -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 +{ + /// + /// Timeout Manager. Checks for dead clients. Clients with open connections that are not doing anything. Closes sessions opened with keepalive. + /// + public static class ContextTimeoutManager + { + /// + /// Use a Thread or a Timer to monitor the ugly + /// + private static Thread m_internalThread = null; + private static object m_threadLock = new object(); + private static ConcurrentQueue m_contexts = new ConcurrentQueue(); + private static ConcurrentQueue m_highPrio = new ConcurrentQueue(); + private static ConcurrentQueue m_midPrio = new ConcurrentQueue(); + private static ConcurrentQueue m_lowPrio = new ConcurrentQueue(); + 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); + } + + /// + /// Causes the watcher to immediately check the connections. + /// + 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); + } + + /// + /// 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. + /// + /// + public static int EnvironmentTickCount() + { + return Environment.TickCount & EnvironmentTickCountMask; + } + const int EnvironmentTickCountMask = 0x3fffffff; + + /// + /// 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. + /// + /// + /// + /// subtraction of passed prevValue from current Environment.TickCount + public static int EnvironmentTickCountSubtract(Int32 newValue, Int32 prevValue) + { + int diff = newValue - prevValue; + return (diff >= 0) ? diff : (diff + EnvironmentTickCountMask + 1); + } + + /// + /// 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. + /// + /// + /// + /// subtraction of passed prevValue from current Environment.TickCount + 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; + } + + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ExceptionEventArgs.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ExceptionEventArgs.cs new file mode 100644 index 0000000000..b93ee850e1 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ExceptionEventArgs.cs @@ -0,0 +1,29 @@ +using System; + +namespace OSHttpServer +{ + /// + /// An unhandled exception have been caught by the system. + /// + public class ExceptionEventArgs : EventArgs + { + private readonly Exception _exception; + + /// + /// Initializes a new instance of the class. + /// + /// Caught exception. + public ExceptionEventArgs(Exception exception) + { + _exception = exception; + } + + /// + /// caught exception + /// + public Exception Exception + { + get { return _exception; } + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ExceptionHandler.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ExceptionHandler.cs new file mode 100644 index 0000000000..c8f41a8138 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ExceptionHandler.cs @@ -0,0 +1,16 @@ +using System; + +namespace OSHttpServer +{ + /// + /// 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. + /// + /// Class that the exception was thrown in. + /// Exception + /// + /// Server will throw a InternalServerException in release version if you dont + /// handle this delegate. + /// + public delegate void ExceptionHandler(object source, Exception exception); +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ForbiddenException.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ForbiddenException.cs new file mode 100644 index 0000000000..2eccdd4116 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ForbiddenException.cs @@ -0,0 +1,25 @@ +using System.Net; + +namespace OSHttpServer.Exceptions +{ + /// + /// 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 + /// + public class ForbiddenException : HttpException + { + /// + /// Initializes a new instance of the class. + /// + /// error message + public ForbiddenException(string errorMsg) + : base(HttpStatusCode.Forbidden, errorMsg) + { + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HeaderEventArgs.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HeaderEventArgs.cs new file mode 100644 index 0000000000..89f0f8fe44 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HeaderEventArgs.cs @@ -0,0 +1,38 @@ +using System; + +namespace OSHttpServer.Parser +{ + /// + /// Event arguments used when a new header have been parsed. + /// + public class HeaderEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// Name of header. + /// Header value. + public HeaderEventArgs(string name, string value) + { + Name = name; + Value = value; + } + + /// + /// Initializes a new instance of the class. + /// + public HeaderEventArgs() + { + } + + /// + /// Gets or sets header name. + /// + public string Name { get; set; } + + /// + /// Gets or sets header value. + /// + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpClientContext.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpClientContext.cs new file mode 100644 index 0000000000..053764d550 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpClientContext.cs @@ -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 +{ + /// + /// Contains a connection to a browser/client. + /// + /// + /// Remember to after you have hooked the event. + /// + 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 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; + + /// + /// Context have been started (a new client have connected) + /// + public event EventHandler Started; + + /// + /// Initializes a new instance of the class. + /// + /// true if the connection is secured (SSL/TLS) + /// client that connected. + /// Stream used for communication + /// Used to create a . + /// Size of buffer to use when reading data. Must be at least 4096 bytes. + /// If fails + /// Stream must be writable and readable. + 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(); + + 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; + } + + /// + /// Process incoming body bytes. + /// + /// + /// Bytes + protected virtual void OnBodyBytesReceived(object sender, BodyEventArgs e) + { + m_currentRequest.AddToBody(e.Buffer, e.Offset, e.Count); + } + + /// + /// + /// + /// + /// + 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; + } + + /// + /// Start reading content. + /// + /// + /// Make sure to call base.Start() if you override this method. + /// + public virtual void Start() + { + ReceiveLoop(); + Started?.Invoke(this, EventArgs.Empty); + } + + /// + /// Clean up context. + /// + /// + /// + 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(); + } + + /// + /// Using SSL or other encryption method. + /// + [Obsolete("Use IsSecured instead.")] + public bool Secured + { + get { return IsSecured; } + } + + /// + /// Using SSL or other encryption method. + /// + public bool IsSecured { get; internal set; } + + + // returns the SSL commonName of remote Certificate + public string SSLCommonName { get; internal set; } + + /// + /// Specify which logger to use. + /// + public ILogWriter LogWriter + { + get { return _log; } + set + { + _log = value ?? NullLogWriter.Instance; + m_parser.LogWriter = _log; + } + } + + private Stream _stream; + + /// + /// Gets or sets the network stream. + /// + internal Stream Stream + { + get { return _stream; } + set { _stream = value; } + } + + /// + /// Gets or sets IP address that the client connected from. + /// + internal string RemoteAddress { get; set; } + + /// + /// Gets or sets port that the client connected from. + /// + internal string RemotePort { get; set; } + + /// + /// Disconnect from client + /// + /// error to report in the event. + 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; + } + } + } + + /// + /// Send a response. + /// + /// Either or + /// HTTP status code + /// reason for the status code. + /// HTML body contents, can be null or empty. + /// A content type to return the body as, i.e. 'text/html' or 'text/plain', defaults to 'text/html' if null or empty + /// If is invalid. + 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; + } + + /// + /// Send a response. + /// + /// Either or + /// HTTP status code + /// reason for the status code. + public void Respond(string httpVersion, HttpStatusCode statusCode, string reason) + { + Respond(httpVersion, statusCode, reason, null, null); + } + + /// + /// send a whole buffer + /// + /// buffer to send + /// + public bool Send(byte[] buffer) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + return Send(buffer, 0, buffer.Length); + } + + /// + /// Send data using the stream + /// + /// Contains data to send + /// Start position in buffer + /// number of bytes to send + /// + /// + + 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 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; + } + + /// + /// The context have been disconnected. + /// + /// + /// Event can be used to clean up a context, or to reuse it. + /// + public event EventHandler Disconnected = delegate { }; + /// + /// A request have been received in the context. + /// + public event EventHandler 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(); + } + } + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpContextFactory.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpContextFactory.cs new file mode 100644 index 0000000000..7b0bbdcfce --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpContextFactory.cs @@ -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 +{ + /// + /// Used to create and reuse contexts. + /// + public class HttpContextFactory : IHttpContextFactory + { + private readonly ConcurrentDictionary m_activeContexts = new ConcurrentDictionary(); + private readonly IRequestParserFactory m_parserfactory; + private readonly ILogWriter m_logWriter; + + /// + /// A request have been received from one of the contexts. + /// + public event EventHandler RequestReceived; + + /// + /// Initializes a new instance of the class. + /// + /// The writer. + /// Amount of bytes to read from the incoming socket stream. + /// Used to create a request parser. + public HttpContextFactory(ILogWriter writer, IRequestParserFactory factory) + { + m_logWriter = writer; + m_parserfactory = factory; + ContextTimeoutManager.Start(); + } + + /// + /// Create a new context. + /// + /// true if socket is running HTTPS. + /// Client that connected + /// Network/SSL stream. + /// A context. + 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; + } + + /// + /// Create a new context. + /// + /// true if HTTPS is used. + /// Remote client + /// Network stream, + /// A new context (always). + 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 + + /// + /// Create a secure . + /// + /// Client socket (accepted by the ). + /// HTTPS certificate to use. + /// Kind of HTTPS protocol. Usually TLS or SSL. + /// + /// A created . + /// + 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); + } + + /// + /// Creates a that handles a connected client. + /// + /// Client socket (accepted by the ). + /// + /// A creates . + /// + 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 + + /// + /// Server is shutting down so shut down the factory + /// + public void Shutdown() + { + ContextTimeoutManager.Stop(); + } + } + + /// + /// Used to create es. + /// + public interface IHttpContextFactory + { + /// + /// Creates a that handles a connected client. + /// + /// Client socket (accepted by the ). + /// A creates . + IHttpClientContext CreateContext(Socket socket); + + /// + /// Create a secure . + /// + /// Client socket (accepted by the ). + /// HTTPS certificate to use. + /// Kind of HTTPS protocol. Usually TLS or SSL. + /// A created . + IHttpClientContext CreateSecureContext(Socket socket, X509Certificate certificate, + SslProtocols protocol, RemoteCertificateValidationCallback _clientCallback = null); + + /// + /// A request have been received from one of the contexts. + /// + event EventHandler RequestReceived; + + /// + /// Server is shutting down so shut down the factory + /// + void Shutdown(); + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpException.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpException.cs new file mode 100644 index 0000000000..db62590db2 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpException.cs @@ -0,0 +1,43 @@ +using System; +using System.Net; + +namespace OSHttpServer.Exceptions +{ + /// + /// All HTTP based exceptions will derive this class. + /// + public class HttpException : Exception + { + private readonly HttpStatusCode _code; + + /// + /// Create a new HttpException + /// + /// http status code (sent in the response) + /// error description + public HttpException(HttpStatusCode code, string message) : base(code + ": " + message) + { + _code = code; + } + + /// + /// Create a new HttpException + /// + /// http status code (sent in the response) + /// error description + /// inner exception + public HttpException(HttpStatusCode code, string message, Exception inner) + : base(code + ": " + message, inner) + { + _code = code; + } + + /// + /// status code to use in the response. + /// + public HttpStatusCode HttpStatusCode + { + get { return _code; } + } + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpHelper.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpHelper.cs new file mode 100644 index 0000000000..9cdce7caff --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpHelper.cs @@ -0,0 +1,118 @@ +using System; +using System.Web; + +namespace OSHttpServer +{ + /// + /// Generic helper functions for HTTP + /// + public static class HttpHelper + { + /// + /// An empty URI + /// + public static readonly Uri EmptyUri = new Uri("http://localhost/"); + + /// + /// Parses a query string. + /// + /// Query string (URI encoded) + /// A object if successful; otherwise + /// queryString is null. + /// If string cannot be parsed. + 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)); + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpInput.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpInput.cs new file mode 100644 index 0000000000..c6585b197c --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpInput.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace OSHttpServer +{ + /// + /// Contains some kind of input from the browser/client. + /// can be QueryString, form data or any other request body content. + /// + public class HttpInput : IHttpInput + { + /// Representation of a non-initialized class instance + public static readonly HttpInput Empty = new HttpInput("Empty", true); + private readonly IDictionary _items = new Dictionary(); + private string _name; + + /// Variable telling the class that it is non-initialized + protected readonly bool _ignoreChanges; + + /// + /// Initializes a new instance of the class. + /// + /// form name. + public HttpInput(string name) + { + Name = name; + } + + /// + /// Initializes a new instance of the class. + /// + /// form name. + /// if set to true all changes will be ignored. + /// this constructor should only be used by Empty + protected HttpInput(string name, bool ignoreChanges) + { + _name = name; + _ignoreChanges = ignoreChanges; + } + + /// Creates a deep copy of the HttpInput class + /// The object to copy + /// The function makes a deep copy of quite a lot which can be slow + protected HttpInput(HttpInput input) + { + foreach (HttpInputItem item in input) + _items.Add(item.Name, new HttpInputItem(item)); + + _name = input._name; + _ignoreChanges = input._ignoreChanges; + } + + /// + /// Form name as lower case + /// + public string Name + { + get { return _name; } + set { _name = value; } + } + + /// + /// Add a new element. Form array elements are parsed + /// and added in a correct hierarchy. + /// + /// Name is converted to lower case. + /// + /// name is null. + /// Cannot add stuff to . + 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)); + } + } + + /// + /// Get a form item. + /// + /// + /// Returns if item was not found. + public HttpInputItem this[string name] + { + get + { + return _items.ContainsKey(name) ? _items[name] : HttpInputItem.Empty; + } + } + + /// + /// Returns true if the class contains a with the corresponding name. + /// + /// The field/query string name + /// True if the value exists + public bool Contains(string name) + { + return _items.ContainsKey(name) && _items[name].Value != null; + } + + /// + /// Parses an item and returns it. + /// This function is primarily used to parse array items as in user[name]. + /// + /// + /// + /// + 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; + } + + /// Outputs the instance representing all its values joined together + /// + public override string ToString() + { + string temp = string.Empty; + foreach (KeyValuePair item in _items) + temp += item.Value.ToString(Name); + return temp; + } + + /// Returns all items as an unescaped query string. + /// + public string ToString(bool asQueryString) + { + if (!asQueryString) + return ToString(); + + string temp = string.Empty; + foreach (KeyValuePair item in _items) + temp += item.Value.ToString(null, true) + '&'; + + return temp.Length > 0 ? temp.Substring(0, temp.Length - 1) : string.Empty; + } + + /// + /// Extracts one parameter from an array + /// + /// Containing the string array + /// All but the first value + /// + /// string test1 = ExtractOne("system[user][extension][id]"); + /// string test2 = ExtractOne(test1); + /// string test3 = ExtractOne(test2); + /// // test1 = user[extension][id] + /// // test2 = extension[id] + /// // test3 = id + /// + 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; + } + + /// Resets all data contained by class + virtual public void Clear() + { + _name = string.Empty; + _items.Clear(); + } + + /// + ///Returns an enumerator that iterates through the collection. + /// + /// + /// + ///A that can be used to iterate through the collection. + /// + ///1 + IEnumerator IEnumerable.GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + + /// + ///Returns an enumerator that iterates through a collection. + /// + /// + /// + ///An object that can be used to iterate through the collection. + /// + ///2 + public IEnumerator GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + } + + /// + /// Base class for request data containers + /// + public interface IHttpInput : IEnumerable + { + /// + /// Adds a parameter mapped to the presented name + /// + /// The name to map the parameter to + /// The parameter value + void Add(string name, string value); + + /// + /// Returns a request parameter + /// + /// The name associated with the parameter + /// + HttpInputItem this[string name] + { get; } + + /// + /// Returns true if the container contains the requested parameter + /// + /// Parameter id + /// True if parameter exists + bool Contains(string name); + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpInputItem.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpInputItem.cs new file mode 100644 index 0000000000..0ab7690618 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpInputItem.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace OSHttpServer +{ + /// + /// 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 + /// + /// + /// // becomes: + /// Console.WriteLine("Value: {0}", form["user"]["FirstName"].Value); + /// + /// + /// All names in a form SHOULD be in lowercase. + /// + public class HttpInputItem : IHttpInput + { + /// Representation of a non-initialized . + public static readonly HttpInputItem Empty = new HttpInputItem(string.Empty, true); + private readonly IDictionary _items = new Dictionary(); + private readonly List _values = new List(); + private string _name; + private readonly bool _ignoreChanges; + + /// + /// Initializes an input item setting its name/identifier and value + /// + /// Parameter name/id + /// Parameter value + public HttpInputItem(string name, string value) + { + Name = name; + Add(value); + } + + private HttpInputItem(string name, bool ignore) + { + Name = name; + _ignoreChanges = ignore; + } + + /// Creates a deep copy of the item specified + /// The item to copy + /// The function makes a deep copy of quite a lot which can be slow + public HttpInputItem(HttpInputItem item) + { + foreach (KeyValuePair 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; + } + + /// + /// Number of values + /// + public int Count + { + get { return _values.Count; } + } + + /// + /// Get a sub item + /// + /// name in lower case. + /// if no item was found. + public HttpInputItem this[string name] + { + get { + return _items.ContainsKey(name) ? _items[name] : Empty; + } + } + + /// + /// Name of item (in lower case). + /// + public string Name + { + get { return _name; } + set { _name = value; } + } + + /// + /// Returns the first value, or null if no value exist. + /// + public string Value + { + get { + return _values.Count == 0 ? null : _values[0]; + } + set + { + if (_values.Count == 0) + _values.Add(value); + else + _values[0] = value; + } + } + + /// + /// Returns the last value, or null if no value exist. + /// + public string LastValue + { + get + { + return _values.Count == 0 ? null : _values[_values.Count - 1]; + } + } + + /// + /// Returns the list with values. + /// + public IList Values + { + get { return _values.AsReadOnly(); } + } + + + /// + /// Add another value to this item + /// + /// Value to add. + /// Cannot add stuff to . + public void Add(string value) + { + if (value == null) + return; + if (_ignoreChanges) + throw new InvalidOperationException("Cannot add stuff to HttpInput.Empty."); + + _values.Add(value); + } + + /// + /// checks if a sub-item exists (and has a value). + /// + /// name in lower case + /// true if the sub-item exists and has a value; otherwise false. + public bool Contains(string name) + { + return _items.ContainsKey(name) && _items[name].Value != null; + } + + /// Returns a formatted representation of the instance with the values of all contained parameters + public override string ToString() + { + return ToString(string.Empty); + } + + /// + /// Outputs the string in a formatted manner + /// + /// A prefix to append, used internally + /// produce a query string + 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 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 item in _items) + temp += item.Value.ToString(name, false); + return temp; + } + } + + #region IHttpInput Members + + /// + /// + /// + /// name in lower case + /// + HttpInputItem IHttpInput.this[string name] + { + get + { + return _items.ContainsKey(name) ? _items[name] : Empty; + } + } + + /// + /// Add a sub item. + /// + /// Can contain array formatting, the item is then parsed and added in multiple levels + /// Value to add. + /// Argument is null. + /// Cannot add stuff to . + 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 + + /// + ///Returns an enumerator that iterates through the collection. + /// + /// + /// + ///A that can be used to iterate through the collection. + /// + ///1 + IEnumerator IEnumerable.GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + + #region IEnumerable Members + + /// + ///Returns an enumerator that iterates through a collection. + /// + /// + /// + ///An object that can be used to iterate through the collection. + /// + ///2 + public IEnumerator GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + #endregion + + /// + /// Outputs the string in a formatted manner + /// + /// A prefix to append, used internally + /// + public string ToString(string prefix) + { + return ToString(prefix, false); + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpListener.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpListener.cs new file mode 100644 index 0000000000..2c7bd8a0dc --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpListener.cs @@ -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 Accepted; + public event ExceptionHandler ExceptionThrown; + public event EventHandler RequestReceived; + + /// + /// Listen for regular HTTP connections + /// + /// IP Address to accept connections on + /// TCP Port to listen on, default HTTP port is 80. + /// Factory used to create es. + /// address is null. + /// Port must be a positive number. + protected OSHttpListener(IPAddress address, int port, IHttpContextFactory factory) + { + m_address = address; + m_port = port; + m_contextFactory = factory; + m_contextFactory.RequestReceived += OnRequestReceived; + } + + /// + /// Initializes a new instance of the class. + /// + /// IP Address to accept connections on + /// TCP Port to listen on, default HTTPS port is 443 + /// Factory used to create es. + /// Certificate to use + /// which HTTPS protocol to use, default is TLS. + protected OSHttpListener(IPAddress address, int port, IHttpContextFactory factory, X509Certificate certificate, + SslProtocols protocol) + : this(address, port, factory, certificate) + { + m_sslProtocol = protocol; + } + + /// + /// Initializes a new instance of the class. + /// + /// IP Address to accept connections on + /// TCP Port to listen on, default HTTPS port is 443 + /// Factory used to create es. + /// Certificate to use + 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; } + } + + /// + /// Gives you a change to receive log entries for all internals of the HTTP library. + /// + /// + /// You may not switch log writer after starting the listener. + /// + 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); + } + } + + /// + /// True if we should turn on trace logs. + /// + public bool UseTraceLogs { get; set; } + + + /// 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(); + } + } + + /// + /// Will try to accept connections one more time. + /// + /// If any exceptions is thrown. + 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); + } + } + + /// + /// Can be used to create filtering of new connections. + /// + /// Accepted socket + /// true if connection can be accepted; otherwise false. + protected bool OnAcceptingSocket(Socket socket) + { + ClientAcceptedEventArgs args = new ClientAcceptedEventArgs(socket); + Accepted?.Invoke(this, args); + return !args.Revoked; + } + + /// + /// Start listen for new connections + /// + /// Number of connections that can stand in a queue to be accepted. + /// Listener have already been started. + 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); + } + + /// + /// Stop the listener + /// + /// + 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(); + } + } + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpParam.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpParam.cs new file mode 100644 index 0000000000..c1a14b6a57 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpParam.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace OSHttpServer +{ + /// + /// Returns item either from a form or a query string (checks them in that order) + /// + public class HttpParam : IHttpInput + { + /// Representation of a non-initialized HttpParam + public static readonly HttpParam Empty = new HttpParam(HttpInput.Empty, HttpInput.Empty); + + private IHttpInput m_form; + private IHttpInput m_query; + + private List _items = new List(); + + /// Initialises the class to hold a value either from a post request or a querystring request + public HttpParam(IHttpInput form, IHttpInput query) + { + m_form = form; + m_query = query; + } + + #region IHttpInput Members + + /// + /// The add method is not availible for HttpParam + /// since HttpParam checks both Request.Form and Request.QueryString + /// + /// name identifying the value + /// value to add + /// + [Obsolete("Not implemented for HttpParam")] + public void Add(string name, string value) + { + throw new NotImplementedException(); + } + + /// + /// Checks whether the form or querystring has the specified value + /// + /// Name, case sensitive + /// true if found; otherwise false. + public bool Contains(string name) + { + return m_form.Contains(name) || m_query.Contains(name); + } + + /// + /// Fetch an item from the form or querystring (in that order). + /// + /// + /// Item if found; otherwise HttpInputItem.EmptyLanguageNode + 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; + } + + /// + ///Returns an enumerator that iterates through the collection. + /// + /// + /// + ///A that can be used to iterate through the collection. + /// + ///1 + IEnumerator IEnumerable.GetEnumerator() + { + List items = new List(m_query); + items.AddRange(m_form); + return items.GetEnumerator(); + } + + #region IEnumerable Members + + /// + ///Returns an enumerator that iterates through a collection. + /// + /// + /// + ///An object that can be used to iterate through the collection. + /// + ///2 + public IEnumerator GetEnumerator() + { + List items = new List(m_query); + items.AddRange(m_form); + return items.GetEnumerator(); + } + + #endregion + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpRequest.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpRequest.cs new file mode 100644 index 0000000000..10a3745eb0 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpRequest.cs @@ -0,0 +1,435 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Text; +using OSHttpServer.Exceptions; + + +namespace OSHttpServer +{ + /// + /// Contains server side HTTP request information. + /// + public class HttpRequest : IHttpRequest + { + /// + /// Chars used to split an URL path into multiple parts. + /// + public static readonly char[] UriSplitters = new[] { '/' }; + public static uint baseID = 0; + + private readonly NameValueCollection m_headers = new 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; } + + /// + /// Gets or sets a value indicating whether this is secure. + /// + public bool Secure { get; internal set; } + + public IHttpClientContext Context { get { return m_context; } } + /// + /// Path and query (will be merged with the host header) and put in Uri + /// + /// + public string UriPath + { + get { return m_uriPath; } + set + { + m_uriPath = value; + 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); + } + } + } + + /// + /// Assign a form. + /// + /// + /* + internal void AssignForm(HttpForm form) + { + _form = form; + } + */ + + #region IHttpRequest Members + + /// + /// Gets whether the body is complete. + /// + public bool BodyIsComplete + { + get { return m_bodyBytesLeft == 0; } + } + + /// + /// Gets kind of types accepted by the client. + /// + public string[] AcceptTypes { get; private set; } + + /// + /// Gets or sets body stream. + /// + public Stream Body + { + get { return m_body; } + set { m_body = value; } + } + + /// + /// Gets or sets kind of connection used for the session. + /// + public ConnectionType Connection + { + get { return m_connection; } + set { m_connection = value; } + } + + /// + /// Gets or sets number of bytes in the body. + /// + public int ContentLength + { + get { return m_contentLength; } + set + { + m_contentLength = value; + m_bodyBytesLeft = value; + } + } + + /// + /// Gets headers sent by the client. + /// + public NameValueCollection Headers + { + get { return m_headers; } + } + + /// + /// Gets or sets version of HTTP protocol that's used. + /// + /// + /// Probably or . + /// + /// + public string HttpVersion + { + get { return m_httpVersion; } + set { m_httpVersion = value; } + } + + /// + /// Gets or sets requested method. + /// + /// + /// + /// Will always be in upper case. + /// + /// + public string Method + { + get { return m_method; } + set { m_method = value; } + } + + /// + /// Gets variables sent in the query string + /// + public HttpInput QueryString + { + get { return m_queryString; } + } + + + /// + /// Gets or sets requested URI. + /// + public Uri Uri + { + get { return m_uri; } + set + { + m_uri = value ?? HttpHelper.EmptyUri; + UriParts = m_uri.AbsolutePath.Split(UriSplitters, StringSplitOptions.RemoveEmptyEntries); + } + } + + /// + /// Uri absolute path splitted into parts. + /// + /// + /// // uri is: http://gauffin.com/code/tiny/ + /// Console.WriteLine(request.UriParts[0]); // result: code + /// Console.WriteLine(request.UriParts[1]); // result: tiny + /// + /// + /// 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. + /// + /// + public string[] UriParts { get; private set; } + + /// + /// Gets parameter from or . + /// + public HttpParam Param + { + get { return m_param; } + } + + /// + /// Gets form parameters. + /// + /* + public HttpForm Form + { + get { return _form; } + } + */ + /// + /// Gets whether the request was made by Ajax (Asynchronous JavaScript) + /// + public bool IsAjax { get; private set; } + + /// + /// Gets cookies that was sent with the request. + /// + public RequestCookies Cookies { get; private set; } + + /// + ///Creates a new object that is a copy of the current instance. + /// + /// + /// + ///A new object that is a copy of this instance. + /// + ///2 + public object Clone() + { + // this method was mainly created for testing. + // dont use it that much... + var request = new HttpRequest(Context); + 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; + } + + /// + /// Decode body into a form. + /// + /// A list with form decoders. + /// If body contents is not valid for the chosen decoder. + /// If body is still being transferred. + /* + public void DecodeBody(FormDecoderProvider providers) + { + if (_bodyBytesLeft > 0) + throw new InvalidOperationException("Body have not yet been completed."); + + _form = providers.Decode(_headers["content-type"], _body, Encoding.UTF8); + if (_form != HttpInput.Empty) + _param.SetForm(_form); + } + */ + /// + /// Cookies + /// + ///the cookies + public void SetCookies(RequestCookies cookies) + { + Cookies = cookies; + } + /* + /// + /// Create a response object. + /// + /// A new . + public IHttpResponse CreateResponse(IHttpClientContext context) + { + return new HttpResponse(context, this); + } + */ + /// + /// Called during parsing of a . + /// + /// Name of the header, should not be URL encoded + /// Value of the header, should not be URL encoded + /// If a header is incorrect. + public void AddHeader(string name, string value) + { + if (string.IsNullOrEmpty(name)) + throw new BadRequestException("Invalid header name: " + name ?? ""); + if (string.IsNullOrEmpty(value)) + throw new BadRequestException("Header '" + name + "' do not contain a value."); + + name = name.ToLowerInvariant(); + + switch (name) + { + case "http_x_requested_with": + case "x-requested-with": + if (string.Compare(value, "XMLHttpRequest", true) == 0) + IsAjax = true; + break; + case "accept": + AcceptTypes = value.Split(','); + for (int i = 0; i < AcceptTypes.Length; ++i) + AcceptTypes[i] = AcceptTypes[i].Trim(); + break; + case "content-length": + 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; + } + } + + /// + /// Add bytes to the body + /// + /// buffer to read bytes from + /// where to start read + /// number of bytes to read + /// Number of bytes actually read (same as length unless we got all body bytes). + /// If body is not writable + /// bytes is null. + /// offset is out of range. + public int AddToBody(byte[] bytes, int offset, int length) + { + if (bytes == null) + throw new ArgumentNullException("bytes"); + if (offset + length > bytes.Length) + throw new ArgumentOutOfRangeException("offset"); + if (length == 0) + return 0; + if (!m_body.CanWrite) + throw new InvalidOperationException("Body is not writable."); + + if (length > m_bodyBytesLeft) + { + length = m_bodyBytesLeft; + } + + m_body.Write(bytes, offset, length); + m_bodyBytesLeft -= length; + + return length; + } + + /// + /// Clear everything in the request + /// + public void Clear() + { + if (m_body != null && m_body.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 + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpRequestParser.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpRequestParser.cs new file mode 100644 index 0000000000..2f5d5bdd21 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpRequestParser.cs @@ -0,0 +1,417 @@ +using System; +using System.Text; +using OSHttpServer.Exceptions; + +namespace OSHttpServer.Parser +{ + /// + /// Parses a HTTP request directly from a stream + /// + 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; + + /// + /// More body bytes have been received. + /// + public event EventHandler BodyBytesReceived; + + /// + /// Request line have been received. + /// + public event EventHandler RequestLineReceived; + + /// + /// A header have been received. + /// + public event EventHandler HeaderReceived; + + + /// + /// Create a new request parser + /// + /// delegate receiving log entries. + public HttpRequestParser(ILogWriter logWriter) + { + m_log = logWriter ?? NullLogWriter.Instance; + } + + /// + /// Add a number of bytes to the body + /// + /// buffer containing more body bytes. + /// starting offset in buffer + /// number of bytes, from offset, to read. + /// offset to continue from. + 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; + } + + /// + /// Remove all state information for the request. + /// + public void Clear() + { + m_bodyBytesLeft = 0; + m_curHeaderName = string.Empty; + m_curHeaderValue = string.Empty; + CurrentState = RequestParserState.FirstLine; + } + + /// + /// Gets or sets the log writer. + /// + public ILogWriter LogWriter + { + get { return m_log; } + set { m_log = value ?? NullLogWriter.Instance; } + } + + /// + /// Parse request line + /// + /// + /// If line is incorrect + /// Expects the following format: "Method SP Request-URI SP HTTP-Version CRLF" + 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); + } + + /// + /// We've parsed a new header. + /// + /// Name in lower case + /// Value, unmodified. + /// If content length cannot be parsed. + 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 + + /// + /// Current state in parser. + /// + public RequestParserState CurrentState { get; private set; } + + /// + /// Parse a message + /// + /// bytes to parse. + /// where in buffer that parsing should start + /// number of bytes to parse, starting on . + /// offset (where to start parsing next). + /// BadRequestException. + 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; + + // + // 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). + // + 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; + } + + /// + /// A request have been successfully parsed. + /// + public event EventHandler RequestCompleted; + + #endregion + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpResponse.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpResponse.cs new file mode 100644 index 0000000000..25553a6f32 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/HttpResponse.cs @@ -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; + + /// + /// Initializes a new instance of the class. + /// + /// Client that send the . + /// Contains information of what the client want to receive. + /// cannot be empty. + 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; + } + + /// + /// Initializes a new instance of the class. + /// + /// Client that send the . + /// Version of HTTP protocol that the client uses. + /// Type of HTTP connection used. + 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 + + /// + /// The body stream is used to cache the body contents + /// before sending everything to the client. It's the simplest + /// way to serve documents. + /// + public Stream Body + { + get + { + if(m_body == null) + m_body = new MemoryStream(); + return m_body; + } + set { m_body = value; } + } + + /// + /// 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. + /// + public bool Chunked { get; set; } + + + /// + /// Defines the version of the HTTP Response for applications where it's required + /// for this to be forced. + /// + public string ProtocolVersion + { + get { return m_httpVersion; } + set { m_httpVersion = value; } + } + + /// + /// Encoding to use when sending stuff to the client. + /// + /// Default is UTF8 + public Encoding Encoding + { + get { return m_encoding; } + set { m_encoding = value; } + } + + + /// + /// Number of seconds to keep connection alive + /// + /// Only used if Connection property is set to . + 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; + } + } + + /// + /// Status code that is sent to the client. + /// + /// Default is + public HttpStatusCode Status { get; set; } + + /// + /// Information about why a specific status code was used. + /// + public string Reason { get; set; } + + /// + /// Size of the body. MUST be specified before sending the header, + /// unless property Chunked is set to true. + /// + public long ContentLength + { + get { return m_contentLength; } + set { m_contentLength = value; } + } + + /// + /// Kind of content in the body + /// + /// Default type is "text/html" + public string ContentType + { + get { return m_contentType; } + set { m_contentType = value; } + } + + /// + /// Headers have been sent to the client- + /// + /// You can not send any additional headers if they have already been sent. + public bool HeadersSent { get; private set; } + + /// + /// The whole response have been sent. + /// + public bool Sent { get; private set; } + + /// + /// Cookies that should be created/changed. + /// + public ResponseCookies Cookies + { + get { return m_cookies; } + } + + /// + /// Add another header to the document. + /// + /// Name of the header, case sensitive, use lower cases. + /// Header values can span over multiple lines as long as each line starts with a white space. New line chars should be \r\n + /// If headers already been sent. + /// If value conditions have not been met. + /// Adding any header will override the default ones and those specified by properties. + 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; + } + + /// + /// Send headers and body to the browser. + /// + /// If content have already been sent. + 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); + } + + + /// + /// Make sure that you have specified and sent the headers first. + /// + /// + /// If headers have not been sent. + /// + /// offset of first byte to send + /// number of bytes to send. + /// + /// + /// 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. + 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; + } + + /// + /// Make sure that you have specified and sent the headers first. + /// + /// + /// If headers have not been sent. + /// + /// + /// + /// 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. + 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; + } + + /// + /// Send headers to the client. + /// + /// If headers already been sent. + /// + /// + /// + 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); + } + + /// + /// Redirect client to somewhere else using the 302 status code. + /// + /// Destination of the redirect + /// If headers already been sent. + /// You can not do anything more with the request when a redirect have been done. This should be your last + /// action. + public void Redirect(Uri uri) + { + Status = HttpStatusCode.Redirect; + m_headers["location"] = uri.ToString(); + } + + /// + /// redirect to somewhere + /// + /// where the redirect should go + /// + /// No body are allowed when doing redirects. + /// + public void Redirect(string url) + { + Status = HttpStatusCode.Redirect; + m_headers["location"] = url; + } + + public void Clear() + { + if(Body != null && Body.CanRead) + Body.Dispose(); + } + #endregion + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpClientContext.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpClientContext.cs new file mode 100644 index 0000000000..8d4fc79c06 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpClientContext.cs @@ -0,0 +1,146 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace OSHttpServer +{ + /// + /// Contains a connection to a browser/client. + /// + public interface IHttpClientContext + { + + /// + /// Get SSL commonName of remote peer + /// + string SSLCommonName { get; } + + /// + /// Using SSL or other encryption method. + /// + bool IsSecured { get; } + + int contextID {get;} + int TimeoutKeepAlive {get; set; } + int MAXRequests{get; set; } + + bool CanSend(); + bool IsSending(); + + /// + /// Disconnect from client + /// + /// error to report in the event. + void Disconnect(SocketError error); + + /// + /// Send a response. + /// + /// Either or + /// HTTP status code + /// reason for the status code. + /// HTML body contents, can be null or empty. + /// A content type to return the body as, i.e. 'text/html' or 'text/plain', defaults to 'text/html' if null or empty + /// If is invalid. + void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType); + + /// + /// Send a response. + /// + /// Either or + /// HTTP status code + /// reason for the status code. + void Respond(string httpVersion, HttpStatusCode statusCode, string reason); + + /// + /// send a whole buffer + /// + /// buffer to send + /// + bool Send(byte[] buffer); + + /// + /// Send data using the stream + /// + /// Contains data to send + /// Start position in buffer + /// number of bytes to send + /// + /// + bool Send(byte[] buffer, int offset, int size); + Task SendAsync(byte[] buffer, int offset, int size); + + /// + /// Closes the streams and disposes of the unmanaged resources + /// + void Close(); + + /// + /// The context have been disconnected. + /// + /// + /// Event can be used to clean up a context, or to reuse it. + /// + event EventHandler Disconnected; + + /// + /// A request have been received in the context. + /// + event EventHandler 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; + } + + /// + /// A have been disconnected. + /// + public class DisconnectedEventArgs : EventArgs + { + /// + /// Gets reason to why client disconnected. + /// + public SocketError Error { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// Reason to disconnection. + public DisconnectedEventArgs(SocketError error) + { + Error = error; + } + } + + /// + /// + /// + public class RequestEventArgs : EventArgs + { + /// + /// Gets received request. + /// + public IHttpRequest Request { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The request. + public RequestEventArgs(IHttpRequest request) + { + Request = request; + } + } + +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpRequest.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpRequest.cs new file mode 100644 index 0000000000..ea3dc48499 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpRequest.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using OSHttpServer.Exceptions; + +namespace OSHttpServer +{ + /// + /// Contains server side HTTP request information. + /// + public interface IHttpRequest : ICloneable + { + /// + /// Gets kind of types accepted by the client. + /// + string[] AcceptTypes { get; } + + uint ID {get; } + /// + /// Gets or sets body stream. + /// + Stream Body { get; set; } + + /// + /// Gets whether the body is complete. + /// + bool BodyIsComplete { get; } + + /// + /// Gets or sets kind of connection used for the session. + /// + ConnectionType Connection { get; set; } + + IHttpClientContext Context { get; } + + /// + /// Gets or sets number of bytes in the body. + /// + int ContentLength { get; set; } + + /// + /// Gets cookies that was sent with the request. + /// + RequestCookies Cookies { get; } + + /// + /// Gets form parameters. + /// + //HttpForm Form { get; } + + /// + /// Gets headers sent by the client. + /// + NameValueCollection Headers { get; } + + /// + /// Gets or sets version of HTTP protocol that's used. + /// + /// + /// Probably or . + /// + /// + string HttpVersion { get; set; } + + /// + /// Gets whether the request was made by Ajax (Asynchronous JavaScript) + /// + bool IsAjax { get; } + + /// + /// Gets or sets requested method. + /// + /// + /// Will always be in upper case. + /// + /// + string Method { get; set; } + + /// + /// Gets parameter from or . + /// + HttpParam Param { get; } + + /// + /// Gets variables sent in the query string + /// + HttpInput QueryString { get; } + + /// + /// Gets or sets requested URI. + /// + Uri Uri { get; set; } + + /// + /// Gets URI absolute path divided into parts. + /// + /// + /// // URI is: http://gauffin.com/code/tiny/ + /// Console.WriteLine(request.UriParts[0]); // result: code + /// Console.WriteLine(request.UriParts[1]); // result: tiny + /// + /// + /// 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. + /// + /// + string[] UriParts { get; } + + /// + /// Gets or sets path and query. + /// + /// + /// + /// Are only used during request parsing. Cannot be set after "Host" header have been + /// added. + /// + string UriPath { get; set; } + + /// + /// Called during parsing of a . + /// + /// Name of the header, should not be URL encoded + /// Value of the header, should not be URL encoded + /// If a header is incorrect. + void AddHeader(string name, string value); + + /// + /// Add bytes to the body + /// + /// buffer to read bytes from + /// where to start read + /// number of bytes to read + /// Number of bytes actually read (same as length unless we got all body bytes). + /// If body is not writable + /// bytes is null. + /// offset is out of range. + int AddToBody(byte[] bytes, int offset, int length); + + /// + /// Clear everything in the request + /// + void Clear(); + + /// + /// Decode body into a form. + /// + /// A list with form decoders. + /// If body contents is not valid for the chosen decoder. + /// If body is still being transferred. + //void DecodeBody(FormDecoderProvider providers); + + /// + /// Sets the cookies. + /// + /// The cookies. + void SetCookies(RequestCookies cookies); + + /// + /// Create a response object. + /// + /// Context for the connected client. + /// A new . + //IHttpResponse CreateResponse(IHttpClientContext context); + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpRequestParser.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpRequestParser.cs new file mode 100644 index 0000000000..953fab343c --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpRequestParser.cs @@ -0,0 +1,94 @@ +using System; +using OSHttpServer.Exceptions; +using OSHttpServer.Parser; + +namespace OSHttpServer +{ + /// + /// Event driven parser used to parse incoming HTTP requests. + /// + /// + /// The parser supports partial messages and keeps the states between + /// each parsed buffer. It's therefore important that the parser gets + /// ed if a client disconnects. + /// + public interface IHttpRequestParser + { + /// + /// Current state in parser. + /// + RequestParserState CurrentState { get; } + + /// + /// Parse partial or complete message. + /// + /// buffer containing incoming bytes + /// where in buffer that parsing should start + /// number of bytes to parse + /// Unparsed bytes left in buffer. + /// BadRequestException. + int Parse(byte[] buffer, int offset, int count); + + /// + /// A request have been successfully parsed. + /// + event EventHandler RequestCompleted; + + /// + /// More body bytes have been received. + /// + event EventHandler BodyBytesReceived; + + /// + /// Request line have been received. + /// + event EventHandler RequestLineReceived; + + /// + /// A header have been received. + /// + event EventHandler HeaderReceived; + + /// + /// Clear parser state. + /// + void Clear(); + + /// + /// Gets or sets the log writer. + /// + ILogWriter LogWriter { get; set; } + } + + /// + /// Current state in the parsing. + /// + public enum RequestParserState + { + /// + /// Should parse the request line + /// + FirstLine, + /// + /// Searching for a complete header name + /// + HeaderName, + /// + /// Searching for colon after header name (ignoring white spaces) + /// + AfterName, + /// + /// Searching for start of header value (ignoring white spaces) + /// + Between, + /// + /// Searching for a complete header value (can span over multiple lines, as long as they are prefixed with one/more whitespaces) + /// + HeaderValue, + + /// + /// Adding bytes to body + /// + Body + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpResponse.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpResponse.cs new file mode 100644 index 0000000000..6cfb772b39 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/IHttpResponse.cs @@ -0,0 +1,180 @@ +using System; +using System.IO; +using System.Net; +using System.Text; + +namespace OSHttpServer +{ + /// + /// 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. + /// + /// + /// public void MyHandler(IHttpRequest request, IHttpResponse response) + /// { + /// + /// } + /// + public interface IHttpResponse + { + /// + /// The body stream is used to cache the body contents + /// before sending everything to the client. It's the simplest + /// way to serve documents. + /// + Stream Body { get; set; } + byte[] RawBuffer { get; set; } + int RawBufferStart { get; set; } + int RawBufferLen { get; set; } + uint requestID { get; } + + /// + /// Defines the version of the HTTP Response for applications where it's required + /// for this to be forced. + /// + string ProtocolVersion { get; set; } + int Priority { get; set; } + + /// + /// 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. + /// + bool Chunked { get; set; } + + /// + /// Kind of connection + /// + ConnectionType Connection { get; set; } + + /// + /// Encoding to use when sending stuff to the client. + /// + /// Default is UTF8 + Encoding Encoding { get; set; } + + /// + /// Number of seconds to keep connection alive + /// + /// Only used if Connection property is set to ConnectionType.KeepAlive + int KeepAlive { get; set; } + + /// + /// Status code that is sent to the client. + /// + /// Default is HttpStatusCode.Ok + HttpStatusCode Status { get; set; } + + /// + /// Information about why a specific status code was used. + /// + string Reason { get; set; } + + /// + /// Size of the body. MUST be specified before sending the header, + /// unless property Chunked is set to true. + /// + long ContentLength { get; set; } + + /// + /// Kind of content in the body + /// + /// Default is text/html + string ContentType { get; set; } + + /// + /// Headers have been sent to the client- + /// + /// You can not send any additional headers if they have already been sent. + bool HeadersSent { get; } + + /// + /// The whole response have been sent. + /// + bool Sent { get; } + + /// + /// Cookies that should be created/changed. + /// + ResponseCookies Cookies { get; } + + /// + /// Add another header to the document. + /// + /// Name of the header, case sensitive, use lower cases. + /// Header values can span over multiple lines as long as each line starts with a white space. New line chars should be \r\n + /// If headers already been sent. + /// If value conditions have not been met. + /// Adding any header will override the default ones and those specified by properties. + void AddHeader(string name, string value); + + /// + /// Send headers and body to the browser. + /// + /// If content have already been sent. + void Send(); + + /// + /// Make sure that you have specified ContentLength and sent the headers first. + /// + /// + /// If headers have not been sent. + /// + /// offest of first byte to send + /// number of bytes to send. + /// + /// + /// 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. + bool SendBody(byte[] buffer, int offset, int count); + + /// + /// Make sure that you have specified ContentLength and sent the headers first. + /// + /// + /// If headers have not been sent. + /// + /// + /// + /// 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. + bool SendBody(byte[] buffer); + + /// + /// Send headers to the client. + /// + /// If headers already been sent. + /// + /// + /// + bool SendHeaders(); + } + + /// + /// Type of HTTP connection + /// + public enum ConnectionType + { + /// + /// Connection is closed after each request-response + /// + Close, + + /// + /// Connection is kept alive for X seconds (unless another request have been made) + /// + KeepAlive + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ILogWriter.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ILogWriter.cs new file mode 100644 index 0000000000..790c9a500d --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ILogWriter.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using System.Text; + +namespace OSHttpServer +{ + /// + /// Priority for log entries + /// + /// + + public enum LogPrio + { + None, + /// + /// Very detailed logs to be able to follow the flow of the program. + /// + Trace, + + /// + /// Logs to help debug errors in the application + /// + Debug, + + /// + /// Information to be able to keep track of state changes etc. + /// + Info, + + /// + /// Something did not go as we expected, but it's no problem. + /// + Warning, + + /// + /// Something that should not fail failed, but we can still keep + /// on going. + /// + Error, + + /// + /// Something failed, and we cannot handle it properly. + /// + Fatal + } + + /// + /// Interface used to write to log files. + /// + public interface ILogWriter + { + /// + /// Write an entry to the log file. + /// + /// object that is writing to the log + /// importance of the log message + /// the message + void Write(object source, LogPrio priority, string message); + } + + /// + /// Default log writer, writes everything to null (nowhere). + /// + /// + + public sealed class NullLogWriter : ILogWriter + { + /// + /// The logging instance. + /// + public static readonly NullLogWriter Instance = new NullLogWriter(); + + /// + /// Writes everything to null + /// + /// object that wrote the log entry. + /// Importance of the log message + /// The message. + public void Write(object source, LogPrio prio, string message) {} + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/InternalServerException.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/InternalServerException.cs new file mode 100644 index 0000000000..6b8467d0f8 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/InternalServerException.cs @@ -0,0 +1,38 @@ +using System; +using System.Net; + +namespace OSHttpServer.Exceptions +{ + /// + /// The server encountered an unexpected condition which prevented it from fulfilling the request. + /// + public class InternalServerException : HttpException + { + /// + /// Initializes a new instance of the class. + /// + public InternalServerException() + : base(HttpStatusCode.InternalServerError, "The server encountered an unexpected condition which prevented it from fulfilling the request.") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// error message. + public InternalServerException(string message) + : base(HttpStatusCode.InternalServerError, message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// error message. + /// inner exception. + public InternalServerException(string message, Exception inner) + : base(HttpStatusCode.InternalServerError, message, inner) + { + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/NotFoundException.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/NotFoundException.cs new file mode 100644 index 0000000000..1f3d8d76d7 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/NotFoundException.cs @@ -0,0 +1,29 @@ +using System; +using System.Net; + +namespace OSHttpServer.Exceptions +{ + /// + /// The requested resource was not found in the web server. + /// + public class NotFoundException : HttpException + { + /// + /// Create a new exception + /// + /// message describing the error + /// inner exception + public NotFoundException(string message, Exception inner) : base(HttpStatusCode.NotFound, message, inner) + { + } + + /// + /// Create a new exception + /// + /// message describing the error + public NotFoundException(string message) + : base(HttpStatusCode.NotFound, message) + { + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestCookie.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestCookie.cs new file mode 100644 index 0000000000..bededb9931 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestCookie.cs @@ -0,0 +1,70 @@ +using System; +using System.Web; + +namespace OSHttpServer +{ + /// + /// cookie sent by the client/browser + /// + /// + public class RequestCookie + { + private readonly string _name = null; + private string _value = null; + + /// + /// Constructor. + /// + /// cookie identifier + /// cookie content + /// id or content is null + /// id is empty + 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 + + /// + /// Gets the cookie HTML representation. + /// + /// cookie string + public override string ToString() + { + return string.Format("{0}={1}; ", HttpUtility.UrlEncode(_name), HttpUtility.UrlEncode(_value)); + } + + #endregion + + #region public properties + + /// + /// Gets the cookie identifier. + /// + public string Name + { + get { return _name; } + } + + + /// + /// Cookie value. Set to null to remove cookie. + /// + public string Value + { + get { return _value; } + set + { + _value = value; + } + } + + #endregion + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestCookies.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestCookies.cs new file mode 100644 index 0000000000..2c11a937cd --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestCookies.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace OSHttpServer +{ + /// + /// 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 + /// + public sealed class RequestCookies : IEnumerable + { + private readonly IDictionary _items = new Dictionary(); + + /// + /// Let's copy all the cookies. + /// + /// value from cookie header. + 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))); + } + + /// + /// Adds a cookie in the collection. + /// + /// cookie to add + /// cookie is null + 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); + } + + /// + /// Gets the count of cookies in the collection. + /// + public int Count + { + get { return _items.Count; } + } + + + /// + /// Gets the cookie of a given identifier (null if not existing). + /// + public RequestCookie this[string id] + { + get + { + return _items.ContainsKey(id) ? _items[id] : null; + } + } + /// + /// Gets a collection enumerator on the cookie list. + /// + /// collection enumerator + public IEnumerator GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + + /// + /// Remove all cookies. + /// + public void Clear() + { + _items.Clear(); + } + + #region IEnumerable Members + + /// + ///Returns an enumerator that iterates through the collection. + /// + /// + /// + ///A that can be used to iterate through the collection. + /// + ///1 + IEnumerator IEnumerable.GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + #endregion + + /// + /// Remove a cookie from the collection. + /// + /// Name of cookie. + public void Remove(string cookieName) + { + lock (_items) + { + if (!_items.ContainsKey(cookieName)) + return; + + _items.Remove(cookieName); + } + } + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestLineEventArgs.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestLineEventArgs.cs new file mode 100644 index 0000000000..1b9b8dc69b --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestLineEventArgs.cs @@ -0,0 +1,48 @@ +using System; + +namespace OSHttpServer.Parser +{ + /// + /// Used when the request line have been successfully parsed. + /// + public class RequestLineEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The HTTP method. + /// The URI path. + /// The HTTP version. + public RequestLineEventArgs(string httpMethod, string uriPath, string httpVersion) + { + HttpMethod = httpMethod; + UriPath = uriPath; + HttpVersion = httpVersion; + } + + /// + /// Initializes a new instance of the class. + /// + public RequestLineEventArgs() + { + } + + /// + /// Gets or sets http method. + /// + /// + /// Should be one of the methods declared in . + /// + public string HttpMethod { get; set; } + + /// + /// Gets or sets the version of the HTTP protocol that the client want to use. + /// + public string HttpVersion { get; set; } + + /// + /// Gets or sets requested URI path. + /// + public string UriPath { get; set; } + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestParserFactory.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestParserFactory.cs new file mode 100644 index 0000000000..03762efafb --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/RequestParserFactory.cs @@ -0,0 +1,33 @@ +using OSHttpServer.Parser; + +namespace OSHttpServer +{ + /// + /// Creates request parsers when needed. + /// + public class RequestParserFactory : IRequestParserFactory + { + /// + /// Create a new request parser. + /// + /// Used when logging should be enabled. + /// A new request parser. + public IHttpRequestParser CreateParser(ILogWriter logWriter) + { + return new HttpRequestParser(logWriter); + } + } + + /// + /// Creates request parsers when needed. + /// + public interface IRequestParserFactory + { + /// + /// Create a new request parser. + /// + /// Used when logging should be enabled. + /// A new request parser. + IHttpRequestParser CreateParser(ILogWriter logWriter); + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ResponseCookie.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ResponseCookie.cs new file mode 100644 index 0000000000..f6931f50d6 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ResponseCookie.cs @@ -0,0 +1,123 @@ +using System; +using System.Web; + +namespace OSHttpServer +{ + /// + /// cookie being sent back to the browser. + /// + /// + public class ResponseCookie : RequestCookie + { + private const string _nullPath = "/"; + private bool _persistant = false; + private DateTime _expires; + private string _path = "/"; + private readonly string _domain; + + #region constructors + + + /// + /// Constructor. + /// + /// cookie identifier + /// cookie content + /// cookie expiration date. Use DateTime.MinValue for session cookie. + /// id or content is null + /// id is empty + public ResponseCookie(string id, string content, DateTime expiresAt) + : base(id, content) + { + if (expiresAt != DateTime.MinValue) + { + _expires = expiresAt; + _persistant = true; + } + } + + /// + /// Create a new cookie + /// + /// name identifying the cookie + /// cookie value + /// when the cookie expires. Setting DateTime.MinValue will delete the cookie when the session is closed. + /// Path to where the cookie is valid + /// Domain that the cookie is valid for. + public ResponseCookie(string name, string value, DateTime expires, string path, string domain) + : this(name, value, expires) + { + _domain = domain; + _path = path; + } + + /// + /// Create a new cookie + /// + /// Name and value will be used + /// when the cookie expires. + public ResponseCookie(RequestCookie cookie, DateTime expires) + : this(cookie.Name, cookie.Value, expires) + {} + + #endregion + + #region inherited methods + + /// + /// Gets the cookie HTML representation. + /// + /// cookie string + 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 + + /// + /// When the cookie expires. + /// DateTime.MinValue means that the cookie expires when the session do so. + /// + public DateTime Expires + { + get { return _expires; } + set + { + _expires = value; + _persistant = value != DateTime.MinValue; + } + } + + /// + /// Cookie is only valid under this path. + /// + public string Path + { + get { return _path; } + set + { + if (!string.IsNullOrEmpty(value)) + _path = value; + else + _path = _nullPath; + } + } + + #endregion + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ResponseCookies.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ResponseCookies.cs new file mode 100644 index 0000000000..e8caf3b174 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/ResponseCookies.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace OSHttpServer +{ + /// + /// Cookies that should be set. + /// + public sealed class ResponseCookies : IEnumerable + { + private readonly IDictionary _items = new Dictionary(); + + /// + /// Adds a cookie in the collection. + /// + /// cookie to add + /// cookie is null + 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); + } + + /// + /// Copy a request cookie + /// + /// + /// When the cookie should expire + public void Add(RequestCookie cookie, DateTime expires) + { + Add(new ResponseCookie(cookie, expires)); + } + + /// + /// Gets the count of cookies in the collection. + /// + public int Count + { + get { return _items.Count; } + } + + + /// + /// Gets the cookie of a given identifier (null if not existing). + /// + 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); + } + } + /// + /// Gets a collection enumerator on the cookie list. + /// + /// collection enumerator + public IEnumerator GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + + /// + /// Remove all cookies + /// + public void Clear() + { + _items.Clear(); + } + + #region IEnumerable Members + + /// + ///Returns an enumerator that iterates through the collection. + /// + /// + /// + ///A that can be used to iterate through the collection. + /// + ///1 + IEnumerator IEnumerable.GetEnumerator() + { + return _items.Values.GetEnumerator(); + } + + #endregion + } +} diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpServer/UnauthorizedException.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/UnauthorizedException.cs new file mode 100644 index 0000000000..84c552cb71 --- /dev/null +++ b/OpenSim/Framework/Servers/HttpServer/OSHttpServer/UnauthorizedException.cs @@ -0,0 +1,56 @@ +using System; +using System.Net; + +namespace OSHttpServer.Exceptions +{ + /// + /// 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) + /// + public class UnauthorizedException : HttpException + { + /// + /// Create a new unauhtorized exception. + /// + /// + public UnauthorizedException() + : base(HttpStatusCode.Unauthorized, "The request requires user authentication.") + { + + } + + /// + /// Create a new unauhtorized exception. + /// + /// reason to why the request was unauthorized. + /// inner exception + public UnauthorizedException(string message, Exception inner) + : base(HttpStatusCode.Unauthorized, message, inner) + { + } + + /// + /// Create a new unauhtorized exception. + /// + /// reason to why the request was unauthorized. + public UnauthorizedException(string message) + : base(HttpStatusCode.Unauthorized, message) + { + } + } +} \ No newline at end of file diff --git a/OpenSim/Framework/Servers/HttpServer/OSHttpXmlRpcHandler.cs b/OpenSim/Framework/Servers/HttpServer/OSHttpXmlRpcHandler.cs deleted file mode 100644 index ef573a4186..0000000000 --- a/OpenSim/Framework/Servers/HttpServer/OSHttpXmlRpcHandler.cs +++ /dev/null @@ -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); - - /// - /// 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. - /// - /// true if the handler is interested in the content; - /// false otherwise - 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; - - - /// - /// Instantiate an XmlRpc handler. - /// - /// XmlRpcMethod - /// delegate - /// XmlRpc method name - /// XmlRpc path prefix (regular expression) - /// Dictionary with header names and - /// regular expressions to match content of headers - /// IP whitelist of remote end points - /// to accept (regular expression) - /// - /// 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. - /// - public OSHttpXmlRpcHandler(XmlRpcMethod handler, string methodName, Regex path, - Dictionary 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; - } - - - /// - /// Instantiate an XmlRpc handler. - /// - /// XmlRpcMethod - /// delegate - /// XmlRpc method name - public OSHttpXmlRpcHandler(XmlRpcMethod handler, string methodName) - : this(handler, methodName, null, null, null) - { - } - - - /// - /// Invoked by OSHttpRequestPump. - /// - 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; - } - } -} diff --git a/OpenSim/Framework/Servers/HttpServer/PollServiceHttpRequest.cs b/OpenSim/Framework/Servers/HttpServer/PollServiceHttpRequest.cs index 2f89034259..20eeb03b2f 100644 --- a/OpenSim/Framework/Servers/HttpServer/PollServiceHttpRequest.cs +++ b/OpenSim/Framework/Servers/HttpServer/PollServiceHttpRequest.cs @@ -30,7 +30,7 @@ using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Text; -using HttpServer; +using OSHttpServer; using log4net; using OpenMetaverse; @@ -130,8 +130,16 @@ namespace OpenSim.Framework.Servers.HttpServer return; } - if (responsedata.ContainsKey("error_status_text")) - response.StatusDescription = (string)responsedata["error_status_text"]; + + response.StatusCode = responsecode; + if (responsecode == (int)OSHttpStatusCode.RedirectMovedPermanently) + { + response.AddHeader("Location:", (string)responsedata["str_redirect_location"]); + response.KeepAlive = false; + PollServiceArgs.RequestsHandled++; + response.Send(); + return; + } if (responsedata.ContainsKey("http_protocol_version")) response.ProtocolVersion = (string)responsedata["http_protocol_version"]; @@ -142,17 +150,13 @@ namespace OpenSim.Framework.Servers.HttpServer if (responsedata.ContainsKey("prio")) response.Priority = (int)responsedata["prio"]; + if (responsedata.ContainsKey("error_status_text")) + response.StatusDescription = (string)responsedata["error_status_text"]; + // Cross-Origin Resource Sharing with simple requests if (responsedata.ContainsKey("access_control_allow_origin")) response.AddHeader("Access-Control-Allow-Origin", (string)responsedata["access_control_allow_origin"]); - response.StatusCode = responsecode; - - if (responsecode == (int)OSHttpStatusCode.RedirectMovedPermanently) - { - response.RedirectLocation = (string)responsedata["str_redirect_location"]; - } - if (string.IsNullOrEmpty(contentType)) response.AddHeader("Content-Type", "text/html"); else diff --git a/OpenSim/Framework/Servers/HttpServer/WebsocketServerHandler.cs b/OpenSim/Framework/Servers/HttpServer/WebsocketServerHandler.cs index c8af90f75c..6d9395710e 100644 --- a/OpenSim/Framework/Servers/HttpServer/WebsocketServerHandler.cs +++ b/OpenSim/Framework/Servers/HttpServer/WebsocketServerHandler.cs @@ -32,7 +32,7 @@ using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading; -using HttpServer; +using OSHttpServer; namespace OpenSim.Framework.Servers.HttpServer { diff --git a/OpenSim/Framework/Servers/Tests/OSHttpTests.cs b/OpenSim/Framework/Servers/Tests/OSHttpTests.cs deleted file mode 100644 index e5f704381b..0000000000 --- a/OpenSim/Framework/Servers/Tests/OSHttpTests.cs +++ /dev/null @@ -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 Disconnected = delegate { }; - /// - /// A request have been received in the context. - /// - public event EventHandler 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); - } - /// - /// Path and query (will be merged with the host header) and put in 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")); - } - } -*/ -} diff --git a/OpenSim/Region/Framework/Scenes/UuidGatherer.cs b/OpenSim/Region/Framework/Scenes/UuidGatherer.cs index 9f8b209345..c23004e174 100644 --- a/OpenSim/Region/Framework/Scenes/UuidGatherer.cs +++ b/OpenSim/Region/Framework/Scenes/UuidGatherer.cs @@ -603,7 +603,8 @@ namespace OpenSim.Region.Framework.Scenes || (sbyte)AssetType.Notecard == assetType || (sbyte)AssetType.LSLText == assetType || (sbyte)OpenSimAssetType.Material == assetType - || (sbyte)AssetType.Object == assetType) + || (sbyte)AssetType.Object == assetType + || (sbyte)AssetType.Settings == assetType) { AddForInspection(assetUuid); } diff --git a/OpenSim/Tests/Common/Mock/TestHttpClientContext.cs b/OpenSim/Tests/Common/Mock/TestHttpClientContext.cs index 7b20b8c8d5..7293830492 100644 --- a/OpenSim/Tests/Common/Mock/TestHttpClientContext.cs +++ b/OpenSim/Tests/Common/Mock/TestHttpClientContext.cs @@ -31,7 +31,7 @@ using System.IO; using System.Net; using System.Net.Sockets; using System.Text; -using HttpServer; +using OSHttpServer; using OpenSim.Framework; namespace OpenSim.Tests.Common diff --git a/OpenSim/Tests/Common/Mock/TestHttpRequest.cs b/OpenSim/Tests/Common/Mock/TestHttpRequest.cs index 4c5ea4a148..b69c70db5c 100644 --- a/OpenSim/Tests/Common/Mock/TestHttpRequest.cs +++ b/OpenSim/Tests/Common/Mock/TestHttpRequest.cs @@ -28,8 +28,7 @@ using System; using System.Collections.Specialized; using System.IO; -using HttpServer; -using HttpServer.FormDecoders; +using OSHttpServer; namespace OpenSim.Tests.Common { diff --git a/OpenSim/Tests/Common/Mock/TestHttpResponse.cs b/OpenSim/Tests/Common/Mock/TestHttpResponse.cs index 190f6d5d8a..f6f789819c 100644 --- a/OpenSim/Tests/Common/Mock/TestHttpResponse.cs +++ b/OpenSim/Tests/Common/Mock/TestHttpResponse.cs @@ -29,7 +29,7 @@ using System; using System.IO; using System.Net; using System.Text; -using HttpServer; +using OSHttpServer; namespace OpenSim.Tests.Common { diff --git a/OpenSim/Tests/Common/Mock/TestOSHttpResponse.cs b/OpenSim/Tests/Common/Mock/TestOSHttpResponse.cs index 4577ca352f..56cec5957d 100644 --- a/OpenSim/Tests/Common/Mock/TestOSHttpResponse.cs +++ b/OpenSim/Tests/Common/Mock/TestOSHttpResponse.cs @@ -68,6 +68,11 @@ namespace OpenSim.Tests.Common /// 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; } + /// /// Encoding of the body content. /// @@ -97,11 +102,6 @@ namespace OpenSim.Tests.Common /// public Stream Body { get; private set; } - /// - /// Set a redirct location. - /// - public string RedirectLocation { private get; set; } - /// /// Chunk transfers. /// diff --git a/bin/HttpServer_OpenSim.dll b/bin/HttpServer_OpenSim.dll deleted file mode 100755 index 6136987045b00decf4cecbe24fb5df7e87672778..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128512 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%0AoSMyYo!|z+y#_n8{)Av^j`$0n=*}|r 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 X-!ujiW*w&ZEWUH`cmMwX!@&OoGhmmN diff --git a/prebuild.xml b/prebuild.xml index 0f2b6c9f6d..f2b7705c59 100644 --- a/prebuild.xml +++ b/prebuild.xml @@ -209,20 +209,11 @@ - - - - - - - - - @@ -369,11 +360,11 @@ - + @@ -2700,7 +2691,6 @@ - @@ -3018,7 +3008,6 @@ - @@ -3222,7 +3211,6 @@ ../../../../../bin/ -