/* * 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.Reflection; using OpenSim.Framework; using OpenSim.Framework.Servers; using OpenSim.ApplicationPlugins.Rest; namespace OpenSim.ApplicationPlugins.Rest.Inventory { public class RestHandler : RestPlugin, IHttpAgentHandler { /// /// The handler delegates are not noteworthy. The allocator allows /// a given handler to optionally subclass the base RequestData /// structure to carry any locally required per-request state /// needed. /// internal delegate void RestMethodHandler(RequestData rdata); internal delegate RequestData RestMethodAllocator(OSHttpRequest request, OSHttpResponse response); // Handler tables: both stream and REST are supported. The path handlers and their // respective allocators are stored in separate tables. internal Dictionary pathHandlers = new Dictionary(); internal Dictionary pathAllocators = new Dictionary(); internal Dictionary streamHandlers = new Dictionary(); #region local static state private static bool handlersLoaded = false; private static List classes = new List(); private static List handlers = new List(); private static Type[] parms = new Type[0]; private static Object[] args = new Object[0]; /// /// This static initializer scans the ASSEMBLY for classes that /// export the IRest interface and builds a list of them. These /// are later activated by the handler. To add a new handler it /// is only necessary to create a new services class that implements /// the IRest interface, and recompile the handler. This gives /// all of the build-time flexibility of a modular approach /// while not introducing yet-another module loader. Note that /// multiple assembles can still be built, each with its own set /// of handlers. Examples of services classes are RestInventoryServices /// and RestSkeleton. /// static RestHandler() { Module[] mods = Assembly.GetExecutingAssembly().GetModules(); foreach (Module m in mods) { Type[] types = m.GetTypes(); foreach (Type t in types) { try { if (t.GetInterface("IRest") != null) { classes.Add(t); } } catch (Exception) { Rest.Log.WarnFormat("[STATIC-HANDLER]: #0 Error scanning {1}", t); Rest.Log.InfoFormat("[STATIC-HANDLER]: #0 {1} is not included", t); } } } } #endregion local static state #region local instance state /// /// This routine loads all of the handlers discovered during /// instance initialization. /// A table of all loaded and successfully constructed handlers /// is built, and this table is then used by the constructor to /// initialize each of the handlers in turn. /// NOTE: The loading process does not automatically imply that /// the handler has registered any kind of an interface, that /// may be (optionally) done by the handler either during /// construction, or during initialization. /// /// I was not able to make this code work within a constructor /// so it is islated within this method. /// private void LoadHandlers() { lock (handlers) { if (!handlersLoaded) { ConstructorInfo ci; Object ht; foreach (Type t in classes) { try { ci = t.GetConstructor(parms); ht = ci.Invoke(args); handlers.Add((IRest)ht); } catch (Exception e) { Rest.Log.WarnFormat("{0} Unable to load {1} : {2}", MsgId, t, e.Message); } } handlersLoaded = true; } } } #endregion local instance state #region overriding properties // These properties override definitions // in the base class. // Name is used to differentiate the message header. public override string Name { get { return "HANDLER"; } } // Used to partition the .ini configuration space. public override string ConfigName { get { return "RestHandler"; } } // We have to rename these because we want // to be able to share the values with other // classes in our assembly and the base // names are protected. internal string MsgId { get { return base.MsgID; } } internal string RequestId { get { return base.RequestID; } } #endregion overriding properties #region overriding methods /// /// This method is called by OpenSimMain immediately after loading the /// plugin and after basic server setup, but before running any server commands. /// /// /// Note that entries MUST be added to the active configuration files before /// the plugin can be enabled. /// public override void Initialise(OpenSimBase openSim) { try { // This plugin will only be enabled if the broader // REST plugin mechanism is enabled. Rest.Log.InfoFormat("{0} Plugin is initializing", MsgId); base.Initialise(openSim); // IsEnabled is implemented by the base class and // reflects an overall RestPlugin status if (!IsEnabled) { Rest.Log.WarnFormat("{0} Plugins are disabled", MsgId); return; } Rest.Log.InfoFormat("{0} Plugin will be enabled", MsgId); // These are stored in static variables to make // them easy to reach from anywhere in the assembly. Rest.main = openSim; Rest.Plugin = this; Rest.Comms = App.CommunicationsManager; Rest.UserServices = Rest.Comms.UserService; Rest.InventoryServices = Rest.Comms.InventoryService; Rest.AssetServices = Rest.Comms.AssetCache; Rest.Config = Config; Rest.Prefix = Prefix; Rest.GodKey = GodKey; Rest.Authenticate = Rest.Config.GetBoolean("authenticate",true); Rest.Secure = Rest.Config.GetBoolean("secured",true); Rest.ExtendedEscape = Rest.Config.GetBoolean("extended-escape",true); Rest.Realm = Rest.Config.GetString("realm","OpenSim REST"); Rest.DumpAsset = Rest.Config.GetBoolean("dump-asset",false); Rest.DumpLineSize = Rest.Config.GetInt("dump-line-size",32); Rest.Log.InfoFormat("{0} Authentication is {1}required", MsgId, (Rest.Authenticate ? "" : "not ")); Rest.Log.InfoFormat("{0} Security is {1}enabled", MsgId, (Rest.Authenticate ? "" : "not ")); Rest.Log.InfoFormat("{0} Extended URI escape processing is {1}enabled", MsgId, (Rest.ExtendedEscape ? "" : "not ")); Rest.Log.InfoFormat("{0} Dumping of asset data is {1}enabled", MsgId, (Rest.DumpAsset ? "" : "not ")); // If data dumping is requested, report on the chosen line // length. if (Rest.DumpAsset) { Rest.Log.InfoFormat("{0} Dump {1} bytes per line", MsgId, Rest.DumpLineSize); } // Load all of the handlers present in the // assembly // In principle, as we're an application plug-in, // most of what needs to be done could be done using // static resources, however the Open Sim plug-in // model makes this an instance, so that's what we // need to be. // There is only one Communications manager per // server, and by inference, only one each of the // user, asset, and inventory servers. So we can cache // those using a static initializer. // We move all of this processing off to another // services class to minimize overlap between function // and infrastructure. LoadHandlers(); // The intention of a post construction initializer // is to allow for setup that is dependent upon other // activities outside of the agency. foreach (IRest handler in handlers) { try { handler.Initialize(); } catch (Exception e) { Rest.Log.ErrorFormat("{0} initialization error: {1}", MsgId, e.Message); } } // Now that everything is setup we can proceed to // add THIS agent to the HTTP server's handler list if (!AddAgentHandler(Rest.Name,this)) { Rest.Log.ErrorFormat("{0} Unable to activate handler interface", MsgId); foreach (IRest handler in handlers) { handler.Close(); } } } catch (Exception e) { Rest.Log.ErrorFormat("{0} Plugin initialization has failed: {1}", MsgId, e.Message); } } /// /// In the interests of efficiency, and because we cannot determine whether /// or not this instance will actually be harvested, we clobber the only /// anchoring reference to the working state for this plug-in. What the /// call to close does is irrelevant to this class beyond knowing that it /// can nullify the reference when it returns. /// To make sure everything is copacetic we make sure the primary interface /// is disabled by deleting the handler from the HTTP server tables. /// public override void Close() { Rest.Log.InfoFormat("{0} Plugin is terminating", MsgId); try { RemoveAgentHandler(Rest.Name, this); } catch (KeyNotFoundException){} foreach (IRest handler in handlers) { handler.Close(); } } #endregion overriding methods #region interface methods /// /// This method is called by the HTTP server to match an incoming /// request. It scans all of the strings registered by the /// underlying handlers and looks for the best match. It returns /// true if a match is found. /// The matching process could be made arbitrarily complex. /// public bool Match(OSHttpRequest request, OSHttpResponse response) { string path = request.RawUrl; try { foreach (string key in pathHandlers.Keys) { if (path.StartsWith(key)) { return ( path.Length == key.Length || path.Substring(key.Length,1) == Rest.UrlPathSeparator); } } path = String.Format("{0}{1}{2}", request.HttpMethod, Rest.UrlMethodSeparator, path); foreach (string key in streamHandlers.Keys) { if (path.StartsWith(key)) { return true; } } } catch (Exception e) { Rest.Log.ErrorFormat("{0} matching exception for path <{1}> : {2}", MsgId, path, e.Message); } return false; } /// /// This is called by the HTTP server once the handler has indicated /// that t is able to handle the request. /// Preconditions: /// [1] request != null and is a valid request object /// [2] response != null and is a valid response object /// Behavior is undefined if preconditions are not satisfied. /// public bool Handle(OSHttpRequest request, OSHttpResponse response) { bool handled; base.MsgID = base.RequestID; // Debug only if (Rest.DEBUG) { Rest.Log.DebugFormat("{0} ENTRY", MsgId); Rest.Log.DebugFormat("{0} Agent: {1}", MsgId, request.UserAgent); Rest.Log.DebugFormat("{0} Method: {1}", MsgId, request.HttpMethod); for (int i = 0; i < request.Headers.Count; i++) { Rest.Log.DebugFormat("{0} Header [{1}] : <{2}> = <{3}>", MsgId, i, request.Headers.GetKey(i), request.Headers.Get(i)); } Rest.Log.DebugFormat("{0} URI: {1}", MsgId, request.RawUrl); } // If a path handler worked we're done, otherwise try any // available stream handlers too. try { handled = FindPathHandler(request, response) || FindStreamHandler(request, response); } catch (Exception e) { // A raw exception indicates that something we weren't expecting has // happened. This should always reflect a shortcoming in the plugin, // or a failure to satisfy the preconditions. Rest.Log.ErrorFormat("{0} Plugin error: {1}", MsgId, e.Message); handled = true; } Rest.Log.DebugFormat("{0} EXIT", MsgId); return handled; } #endregion interface methods /// /// If there is a stream handler registered that can handle the /// request, then fine. If the request is not matched, do /// nothing. /// private bool FindStreamHandler(OSHttpRequest request, OSHttpResponse response) { RequestData rdata = new RequestData(request, response, String.Empty); string bestMatch = null; string path = String.Format("{0}:{1}", rdata.method, rdata.path); Rest.Log.DebugFormat("{0} Checking for stream handler for <{1}>", MsgId, path); if (!IsEnabled) { return false; } foreach (string pattern in streamHandlers.Keys) { if (path.StartsWith(pattern)) { if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length) { bestMatch = pattern; } } } // Handle using the best match available if (!String.IsNullOrEmpty(bestMatch)) { Rest.Log.DebugFormat("{0} Stream-based handler matched with <{1}>", MsgId, bestMatch); RestStreamHandler handler = streamHandlers[bestMatch]; rdata.buffer = handler.Handle(rdata.path, rdata.request.InputStream, rdata.request, rdata.response); rdata.AddHeader(rdata.response.ContentType,handler.ContentType); rdata.Respond("FindStreamHandler Completion"); } return rdata.handled; } /// /// Add a stream handler for the designated HTTP method and path prefix. /// If the handler is not enabled, the request is ignored. If the path /// does not start with the REST prefix, it is added. If method-qualified /// path has not already been registered, the method is added to the active /// handler table. /// public void AddStreamHandler(string httpMethod, string path, RestMethod method) { if (!IsEnabled) { return; } if (!path.StartsWith(Rest.Prefix)) { path = String.Format("{0}{1}", Rest.Prefix, path); } path = String.Format("{0}{1}{2}", httpMethod, Rest.UrlMethodSeparator, path); // Conditionally add to the list if (!streamHandlers.ContainsKey(path)) { streamHandlers.Add(path, new RestStreamHandler(httpMethod, path, method)); Rest.Log.DebugFormat("{0} Added handler for {1}", MsgId, path); } else { Rest.Log.WarnFormat("{0} Ignoring duplicate handler for {1}", MsgId, path); } } /// /// Given the supplied request/response, if the handler is enabled, the inbound /// information is used to match an entry in the active path handler tables, using /// the method-qualified path information. If a match is found, then the handler is /// invoked. The result is the boolean result of the handler, or false if no /// handler was located. The boolean indicates whether or not the request has been /// handled, not whether or not the request was successful - that information is in /// the response. /// internal bool FindPathHandler(OSHttpRequest request, OSHttpResponse response) { RequestData rdata = null; string bestMatch = null; if (!IsEnabled) { return false; } // Conditionally add to the list Rest.Log.DebugFormat("{0} Checking for path handler for <{1}>", MsgId, request.RawUrl); foreach (string pattern in pathHandlers.Keys) { if (request.RawUrl.StartsWith(pattern)) { if (String.IsNullOrEmpty(bestMatch) || pattern.Length > bestMatch.Length) { bestMatch = pattern; } } } if (!String.IsNullOrEmpty(bestMatch)) { rdata = pathAllocators[bestMatch](request, response); Rest.Log.DebugFormat("{0} Path based REST handler matched with <{1}>", MsgId, bestMatch); try { pathHandlers[bestMatch](rdata); } // A plugin generated error indicates a request-related error // that has been handled by the plugin. catch (RestException r) { Rest.Log.WarnFormat("{0} Request failed: {1}", MsgId, r.Message); } } return (rdata == null) ? false : rdata.handled; } /// /// A method handler and a request allocator are stored using the designated /// path as a key. If an entry already exists, it is replaced by the new one. /// internal void AddPathHandler(RestMethodHandler mh, string path, RestMethodAllocator ra) { if (!IsEnabled) { return; } if (pathHandlers.ContainsKey(path)) { Rest.Log.DebugFormat("{0} Replacing handler for <${1}>", MsgId, path); pathHandlers.Remove(path); } if (pathAllocators.ContainsKey(path)) { Rest.Log.DebugFormat("{0} Replacing allocator for <${1}>", MsgId, path); pathAllocators.Remove(path); } Rest.Log.DebugFormat("{0} Adding path handler for {1}", MsgId, path); pathHandlers.Add(path, mh); pathAllocators.Add(path, ra); } } }