From 7692f3e18f49fc57b63efbe54b7ac71de2fb7e16 Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Mon, 14 Jul 2008 12:18:32 +0000 Subject: [PATCH] further work in progress on the HttpServer side: XmlRpc handler path almost complete and soon to be ready for testing; OSHttpResponse code out. --- OpenSim/Framework/Servers/BaseHttpServer.cs | 2 +- OpenSim/Framework/Servers/OSHttpHandler.cs | 25 +- OpenSim/Framework/Servers/OSHttpRequest.cs | 39 ++- .../Framework/Servers/OSHttpRequestPump.cs | 82 +++-- OpenSim/Framework/Servers/OSHttpResponse.cs | 311 +++++++++++++++--- .../Framework/Servers/OSHttpXmlRpcHandler.cs | 222 +++++++++++++ 6 files changed, 598 insertions(+), 83 deletions(-) create mode 100644 OpenSim/Framework/Servers/OSHttpXmlRpcHandler.cs diff --git a/OpenSim/Framework/Servers/BaseHttpServer.cs b/OpenSim/Framework/Servers/BaseHttpServer.cs index 22698d0e3c..9d2a804aec 100644 --- a/OpenSim/Framework/Servers/BaseHttpServer.cs +++ b/OpenSim/Framework/Servers/BaseHttpServer.cs @@ -416,7 +416,7 @@ namespace OpenSim.Framework.Servers { try { - response.OutputStream.Close(); + response.Send(); } catch (SocketException e) { diff --git a/OpenSim/Framework/Servers/OSHttpHandler.cs b/OpenSim/Framework/Servers/OSHttpHandler.cs index da96cca577..a9f42f3e92 100644 --- a/OpenSim/Framework/Servers/OSHttpHandler.cs +++ b/OpenSim/Framework/Servers/OSHttpHandler.cs @@ -59,10 +59,18 @@ namespace OpenSim.Framework.Servers { Unprocessed, Pass, - Handled, - Detached, + 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 interface OSHttpHandler { /// @@ -98,6 +106,19 @@ namespace OpenSim.Framework.Servers get; } + + /// + /// 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 + OSHttpContentTypeChecker ContentTypeChecker + { + get; + } + OSHttpHandlerResult Process(OSHttpRequest request); } } \ No newline at end of file diff --git a/OpenSim/Framework/Servers/OSHttpRequest.cs b/OpenSim/Framework/Servers/OSHttpRequest.cs index c5231431ab..7549f08142 100644 --- a/OpenSim/Framework/Servers/OSHttpRequest.cs +++ b/OpenSim/Framework/Servers/OSHttpRequest.cs @@ -59,7 +59,7 @@ namespace OpenSim.Framework.Servers private IPEndPoint _ipEndPoint; private HttpRequest _request; - // private HttpClientContext _context; + private HttpClientContext _context; public string[] AcceptTypes { @@ -152,11 +152,28 @@ namespace OpenSim.Framework.Servers get { return _ipEndPoint; } } - public HttpRequest HttpRequest + + internal HttpRequest HttpRequest { get { return _request; } } + internal HttpClientContext HttpClientContext + { + get { return _context; } + } + + /// + /// Internal whiteboard for handlers to store temporary stuff + /// into. + /// + internal Dictionary Whiteboard + { + get { return _whiteboard; } + } + private Dictionary _whiteboard = new Dictionary(); + + public OSHttpRequest() { } @@ -185,7 +202,7 @@ namespace OpenSim.Framework.Servers public OSHttpRequest(HttpClientContext context, HttpRequest req) { - // _context = context; + _context = context; _request = req; _acceptTypes = req.AcceptTypes; @@ -215,5 +232,21 @@ namespace OpenSim.Framework.Servers // _isSecureConnection = req.IsSecureConnection; // _isAuthenticated = req.IsAuthenticated; } + + public override string ToString() + { + StringBuilder me = new StringBuilder(); + me.Append(String.Format("OSHttpRequest: {0} {1}\n", HttpMethod, RawUrl)); + foreach (string k in Headers.AllKeys) + { + me.Append(String.Format(" {0}: {1}\n", k, Headers[k])); + } + if (null != RemoteIPEndPoint) + { + me.Append(String.Format(" IP: {0}\n", RemoteIPEndPoint.ToString())); + } + + return me.ToString(); + } } } diff --git a/OpenSim/Framework/Servers/OSHttpRequestPump.cs b/OpenSim/Framework/Servers/OSHttpRequestPump.cs index 4218be582c..56714fad36 100644 --- a/OpenSim/Framework/Servers/OSHttpRequestPump.cs +++ b/OpenSim/Framework/Servers/OSHttpRequestPump.cs @@ -28,6 +28,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.IO; using System.Net; using System.Reflection; using System.Text.RegularExpressions; @@ -114,7 +115,7 @@ namespace OpenSim.Framework.Servers // process req: we try each handler in turn until // we are either out of handlers or get back a - // Handled or Detached + // Pass or Done OSHttpHandlerResult rc = OSHttpHandlerResult.Unprocessed; foreach (OSHttpHandler h in handlers) { @@ -123,22 +124,35 @@ namespace OpenSim.Framework.Servers // Pass: handler did not process the request, // try next handler if (OSHttpHandlerResult.Pass == rc) continue; - // Detached: handler is taking over processing - // of request, we are done - if (OSHttpHandlerResult.Detached == rc) break; - - if (OSHttpHandlerResult.Handled != rc) - { - // something went wrong - throw new Exception(String.Format("[{0}] got unexpected OSHttpHandlerResult {1}", EngineID, rc)); - } - - // Handled: clean up now - req.HttpRequest.AddHeader("keep-alive", "false"); - break; + // 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.HttpResponse.Send(); } - } catch (Exception e) { @@ -199,17 +213,37 @@ namespace OpenSim.Framework.Servers NameValueCollection headers = req.HttpRequest.Headers; foreach (string tag in headerRegexs.Keys) { - if (null != headers[tag]) + // do we have a header "tag"? + if (null == headers[tag]) { - Match hm = headerRegexs[tag].Match(headers[tag]); - if (hm.Success) { - headersMatch++; - continue; - } + // no: remove the handler if it was added + // earlier and on to the next one + scoredHandlers.Remove(h); + break; } - - scoredHandlers.Remove(h); - break; + + // does the content of header "tag" match + // the supplied regex? + Match hm = headerRegexs[tag].Match(headers[tag]); + if (!hm.Success) { + // no: remove the handler if it was added + // earlier and on to the next one + scoredHandlers.Remove(h); + break; + } + + // if we are looking at the "content-type" tag, + // check wether h has a ContentTypeChecker and + // invoke it if it has + if ((null != h.ContentTypeChecker) && !h.ContentTypeChecker(req)) + { + scoredHandlers.Remove(h); + break; + } + + // ok: header matches + headersMatch++; + continue; } // check whether h got kicked out if (!scoredHandlers.ContainsKey(h)) continue; diff --git a/OpenSim/Framework/Servers/OSHttpResponse.cs b/OpenSim/Framework/Servers/OSHttpResponse.cs index e1ab005c97..352c6f6e09 100644 --- a/OpenSim/Framework/Servers/OSHttpResponse.cs +++ b/OpenSim/Framework/Servers/OSHttpResponse.cs @@ -25,141 +25,346 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +using System; using System.Collections; using System.IO; using System.Net; using System.Text; +using HttpServer; namespace OpenSim.Framework.Servers { + /// + /// OSHttpResponse is the OpenSim representation of an HTTP + /// response. + /// + /// + /// OSHttpResponse is currently dual "homed" in that it support + /// both the .NET HttpListenerResponse and the HttpServer + /// HttpResponse (similar to OSHttpRequest); this duality is only + /// temporary and the .NET usage will disappear once the switch to + /// HttpServer is completed. + /// public class OSHttpResponse { - private string _contentType; - private bool _contentTypeSet; + + // property code below is a bit messy, will all resolve to + // harmony once we've completed the switch + + /// + /// Content type property. + /// + /// + /// Setting this property will also set IsContentTypeSet to + /// true. + /// public string ContentType { - get { return _contentType; } + get + { + return HttpServer ? _httpResponse.ContentType : _contentType; + } set { - _contentType = value; - _contentTypeSet = true; + if (HttpServer) + { + _httpResponse.ContentType = value; + } + else + { + _contentType = value; + _contentTypeSet = true; + } } } + private string _contentType; + + /// + /// Boolean property indicating whether the content type + /// property actively has been set. + /// + /// + /// IsContentTypeSet will go away together with .NET base. + /// public bool IsContentTypeSet { get { return _contentTypeSet; } } + private bool _contentTypeSet; - private long _contentLength64; + + /// + /// Length of the body content; 0 if there is no body. + /// + public long ContentLength + { + get + { + return HttpServer ? _httpResponse.ContentLength : _contentLength; + } + set + { + if (HttpServer) + _httpResponse.ContentLength = value; + else + _contentLength = value; + } + } + private long _contentLength; + + /// + /// Aliases for ContentLength. + /// public long ContentLength64 { - get { return _contentLength64; } - set - { - _contentLength64 = value; - if (null != _resp) _resp.ContentLength64 = value; - } + get { return ContentLength; } + set { ContentLength = value; } } - private Encoding _contentEncoding; + /// + /// Encoding of the body content. + /// public Encoding ContentEncoding { - get { return _contentEncoding; } + get { + return HttpServer ? _httpResponse.Encoding : _contentEncoding; + } set { - _contentEncoding = value; - if (null != _resp) _resp.ContentEncoding = value; + if (HttpServer) + _httpResponse.Encoding = value; + else + _contentEncoding = value; } } + private Encoding _contentEncoding; - public WebHeaderCollection Headers; - // public CookieCollection Cookies; + /// + /// Headers of the response. + /// + public WebHeaderCollection Headers + { + get + { + return HttpServer ? null : _headers; + } + } + private WebHeaderCollection _headers; - private bool _keepAlive; + /// + /// Get or set the keep alive property. + /// public bool KeepAlive { - get { return _keepAlive; } + get + { + if (HttpServer) + return _httpResponse.Connection == ConnectionType.KeepAlive; + else + return _keepAlive; + } set { - _keepAlive = value; - if (null != _resp) _resp.KeepAlive = value; + if (HttpServer) + _httpResponse.Connection = ConnectionType.KeepAlive; + else + _keepAlive = value; + } + } + private bool _keepAlive; + + /// + /// Return the output stream feeding the body. + /// + /// + /// On its way out... + /// + public Stream OutputStream + { + get + { + return HttpServer ? _httpResponse.Body : _outputStream; + } + } + private Stream _outputStream; + + + /// + /// Return the output stream feeding the body. + /// + public Stream Body + { + get + { + if (HttpServer) + return _httpResponse.Body; + throw new Exception("[OSHttpResponse] mixed .NET and HttpServer access"); } } - public Stream OutputStream; - - private string _redirectLocation; + /// + /// Set a redirct location. + /// public string RedirectLocation { - get { return _redirectLocation; } + // get { return _redirectLocation; } set { - _redirectLocation = value; - if (null != _resp) _resp.RedirectLocation = value; + if (HttpServer) + _httpResponse.Redirect(value); + // else + // _redirectLocation = value; } } + // private string _redirectLocation; - private bool _sendChunked; + + + /// + /// Chunk transfers. + /// public bool SendChunked { - get { return _sendChunked; } + get + { + return HttpServer ? _httpResponse.Chunked :_sendChunked; + } + set { - _sendChunked = value; - if (null != _resp) _resp.SendChunked = value; + if (HttpServer) + _httpResponse.Chunked = value; + else + _sendChunked = value; } } + private bool _sendChunked; - private int _statusCode; + + /// + /// HTTP status code. + /// public int StatusCode { - get { return _statusCode; } + get + { + return HttpServer ? (int)_httpResponse.Status : _statusCode; + } + set { - _statusCode = value; - if (null != _resp) _resp.StatusCode = value; + if (HttpServer) + _httpResponse.Status = (HttpStatusCode)value; + else + _statusCode = value; } } + private int _statusCode; - private string _statusDescription; + + /// + /// HTTP status description. + /// public string StatusDescription { - get { return _statusDescription; } + get + { + return HttpServer ? _httpResponse.Reason : _statusDescription; + } + set { - _statusDescription = value; - if (null != _resp) _resp.StatusDescription = value; + if (HttpServer) + _httpResponse.Reason = value; + else + _statusDescription = value; } } + private string _statusDescription; - private HttpListenerResponse _resp; + private HttpResponse _httpResponse; + + internal bool HttpServer + { + get { return null != _httpResponse; } + } + + internal HttpResponse HttpResponse + { + get { return _httpResponse; } + } public OSHttpResponse() { } + /// + /// Instantiate an OSHttpResponse object based on an + /// underlying .NET HttpListenerResponse. + /// + /// + /// Almost deprecated; will go west to make once HttpServer + /// base takes over. + /// public OSHttpResponse(HttpListenerResponse resp) { - ContentEncoding = resp.ContentEncoding; - ContentLength64 = resp.ContentLength64; + _contentEncoding = resp.ContentEncoding; + _contentLength = resp.ContentLength64; _contentType = resp.ContentType; - Headers = resp.Headers; - // Cookies = resp.Cookies; - KeepAlive = resp.KeepAlive; - OutputStream = resp.OutputStream; - RedirectLocation = resp.RedirectLocation; - SendChunked = resp.SendChunked; - StatusCode = resp.StatusCode; - StatusDescription = resp.StatusDescription; + _headers = resp.Headers; + // _cookies = resp.Cookies; + _keepAlive = resp.KeepAlive; + _outputStream = resp.OutputStream; + // _redirectLocation = resp.RedirectLocation; + _sendChunked = resp.SendChunked; + _statusCode = resp.StatusCode; + _statusDescription = resp.StatusDescription; _contentTypeSet = false; - _resp = resp; + // _resp = resp; } + /// + /// Instantiate an OSHttpResponse object from an OSHttpRequest + /// object. + /// Incoming OSHttpRequest to which we are + /// replying + public OSHttpResponse(OSHttpRequest req) + { + _httpResponse = new HttpResponse(req.HttpClientContext, req.HttpRequest); + } + + /// + /// Add a header field and content to the response. + /// + /// string containing the header field + /// name + /// string containing the header field + /// value public void AddHeader(string key, string value) { - Headers.Add(key, value); + if (HttpServer) + _headers.Add(key, value); + else + _httpResponse.AddHeader(key, value); + } + + /// + /// Send the response back to the remote client + /// + public void Send() + { + if (HttpServer) + { + _httpResponse.Body.Flush(); + _httpResponse.Send(); + } + else + { + OutputStream.Close(); + } } } } diff --git a/OpenSim/Framework/Servers/OSHttpXmlRpcHandler.cs b/OpenSim/Framework/Servers/OSHttpXmlRpcHandler.cs new file mode 100644 index 0000000000..420554774e --- /dev/null +++ b/OpenSim/Framework/Servers/OSHttpXmlRpcHandler.cs @@ -0,0 +1,222 @@ +/* + * 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 OpenSim 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 +{ + public delegate XmlRpcResponse OSHttpXmlRpcProcessor(XmlRpcRequest request); + + public class OSHttpXmlRpcHandler: OSHttpHandler + { + private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + /// + /// Regular expression used to match against path of incoming + /// HTTP request. If you want to match any string either use + /// '.*' or null. To match for the emtpy string use '^$' + /// + public Regex Path + { + get { return _pathsRegex; } + } + private Regex _pathsRegex; + + /// + /// Dictionary of (header name, regular expression) tuples, + /// allowing us to match on HTTP header fields. + /// + public Dictionary Headers + { + get { return _headers; } + } + private Dictionary _headers; + + /// + /// Regex to whitelist IP end points. A null value disables + /// checking of IP end points. + /// + /// + /// This feature is currently not implemented as it requires + /// (trivial) changes to HttpServer.HttpListener that have not + /// been implemented. + /// + public Regex IPEndPointWhitelist + { + get { return _ipEndPointRegex; } + } + private Regex _ipEndPointRegex; + + /// + /// 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 OSHttpContentTypeChecker ContentTypeChecker + { + get + { + return delegate(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 OSHttpXmlRpcProcessor _handler; + + // contains XmlRpc method name + private string _methodName; + + + /// + /// Instantiate an XmlRpc handler. + /// + /// OSHttpXmlRpcProcessor + /// 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(OSHttpXmlRpcProcessor handler, string methodName, Regex path, + Dictionary headers, Regex whitelist) + { + _handler = handler; + _pathsRegex = path; + _methodName = methodName; + + if (null == _headers) _headers = new Dictionary(); + _headers.Add("content-type", new Regex(@"^(text|application)/xml", RegexOptions.IgnoreCase | + RegexOptions.Compiled)); + + _ipEndPointRegex = whitelist; + } + + + /// + /// Instantiate an XmlRpc handler. + /// + /// OSHttpXmlRpcProcessor + /// delegate + /// XmlRpc method name + public OSHttpXmlRpcHandler(OSHttpXmlRpcProcessor handler, string methodName) + : this(handler, methodName, null, null, null) + { + } + + + /// + /// Invoked by OSHttpRequestPump. + /// + public OSHttpHandlerResult Process(OSHttpRequest request) + { + XmlRpcResponse xmlRpcResponse; + string responseString; + + 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; + } + } +} \ No newline at end of file