/*
* 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
{
#region local static state
///
/// 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.
///
private static bool handlersLoaded = false;
private static List classes = new List();
private static List handlers = new List();
private static Type[] parms = new Type[1];
private static Object[] args = new Object[1];
static RestHandler()
{
Module[] mods = Assembly.GetExecutingAssembly().GetModules();
foreach (Module m in mods)
{
Type[] types = m.GetTypes();
foreach (Type t in types)
{
if (t.GetInterface("IRest") != null)
{
classes.Add(t);
}
}
}
}
#endregion local static state
#region local instance state
///
/// The handler delegate is 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
internal Dictionary pathHandlers = new Dictionary();
internal Dictionary pathAllocators = new Dictionary();
internal Dictionary streamHandlers = new Dictionary();
///
/// This routine loads all of the handlers discovered during
/// instance initialization. Each handler is responsible for
/// registering itself with this handler.
/// I was not able to make this code work in a constructor.
///
private void LoadHandlers()
{
lock (handlers)
{
if (!handlersLoaded)
{
parms[0] = this.GetType();
args[0] = this;
ConstructorInfo ci;
Object ht;
foreach (Type t in classes)
{
ci = t.GetConstructor(parms);
ht = ci.Invoke(args);
handlers.Add((IRest)ht);
}
handlersLoaded = true;
}
}
}
#endregion local instance state
#region overriding properties
// Used to differentiate the message header.
public override string Name
{
get { return "HANDLER"; }
}
// Used to partition the 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);
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 (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. We don't currently
/// have any, but the design allows for it.
///
foreach (IRest handler in handlers)
{
handler.Initialize();
}
///
/// Now that everything is setup we can proceed and
/// 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 server to match the client, it could
/// just return true if we only want one such handler. For now we
/// match any explicitly specified client.
///
public bool Match(OSHttpRequest request, OSHttpResponse response)
{
string path = request.RawUrl;
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;
}
}
return false;
}
///
/// 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;
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);
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;
}
// Preserves the original handler's semantics
public new 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);
}
}
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;
}
internal void AddPathHandler(RestMethodHandler mh, string path, RestMethodAllocator ra)
{
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);
}
}
}