606 lines
23 KiB
C#
606 lines
23 KiB
C#
/*
|
|
* 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
|
|
{
|
|
/// <remarks>
|
|
/// 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.
|
|
/// </remarks>
|
|
|
|
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<string,RestMethodHandler> pathHandlers = new Dictionary<string,RestMethodHandler>();
|
|
internal Dictionary<string,RestMethodAllocator> pathAllocators = new Dictionary<string,RestMethodAllocator>();
|
|
internal Dictionary<string,RestStreamHandler> streamHandlers = new Dictionary<string,RestStreamHandler>();
|
|
|
|
#region local static state
|
|
|
|
private static bool handlersLoaded = false;
|
|
private static List<Type> classes = new List<Type>();
|
|
private static List<IRest> handlers = new List<IRest>();
|
|
private static Type[] parms = new Type[0];
|
|
private static Object[] args = new Object[0];
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// This method is called by OpenSimMain immediately after loading the
|
|
/// plugin and after basic server setup, but before running any server commands.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Note that entries MUST be added to the active configuration files before
|
|
/// the plugin can be enabled.
|
|
/// </remarks>
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// If there is a stream handler registered that can handle the
|
|
/// request, then fine. If the request is not matched, do
|
|
/// nothing.
|
|
/// </summary>
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|