From 7025a8040e06250d73c295aa641969d7189474d8 Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Fri, 25 Jul 2008 09:56:35 +0000 Subject: From: awebb Further improvements to the REST handlers. --- OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs | 5 +- .../Rest/Inventory/RequestData.cs | 371 ++++++++++++++------- OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs | 1 - .../Rest/Inventory/RestAssetServices.cs | 14 +- .../Rest/Inventory/RestHandler.cs | 242 +++++++++----- .../Rest/Inventory/RestInventoryServices.cs | 295 ++++++++++------ OpenSim/Framework/Servers/BaseHttpServer.cs | 27 +- 7 files changed, 636 insertions(+), 319 deletions(-) diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs index 6f52582..5fd0219 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs @@ -34,8 +34,9 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// /// This interface represents the boundary between the general purpose /// REST plugin handling, and the functionally specific handlers. The - /// handler knows only to initialzie and terminate all such handlers - /// that it finds. + /// handler knows only to initialize and terminate all such handlers + /// that it finds. Implementing this interface identifies the class as + /// a REST handler implementation. /// internal interface IRest diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs index a885b25..6e42b6c 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs @@ -34,6 +34,7 @@ using System.Security.Cryptography; using System.Text.RegularExpressions; using System.Collections.Generic; using System.Collections.Specialized; +using OpenSim.Framework; using OpenSim.Framework.Servers; using libsecondlife; using System.Xml; @@ -50,7 +51,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// This structure is created on entry to the Handler /// method and is disposed of upon return. It is part of /// the plug-in infrastructure, rather than the functionally - /// specifici REST handler, and fundamental changes to + /// specific REST handler, and fundamental changes to /// this should be reflected in the Rest HandlerVersion. The /// object is instantiated, and may be extended by, any /// given handler. See the inventory handler for an example @@ -71,11 +72,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory internal OSHttpRequest request = null; internal OSHttpResponse response = null; + internal string qprefix = null; // Request lifetime values - internal NameValueCollection headers = null; - internal List removed_headers = null; internal byte[] buffer = null; internal string body = null; internal string html = null; @@ -96,11 +96,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory internal string hostname = "localhost"; internal int port = 80; internal string prefix = Rest.UrlPathSeparator; + internal bool keepAlive = false; + internal bool chunked = false; // Authentication related state internal bool authenticated = false; - internal string scheme = Rest.AS_DIGEST; + // internal string scheme = Rest.AS_DIGEST; + // internal string scheme = Rest.AS_BASIC; + internal string scheme = null; internal string realm = Rest.Realm; internal string domain = null; internal string nonce = null; @@ -148,13 +152,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory private static Regex basicParms = new Regex("^\\s*(?:\\w+)\\s+(?\\S+)\\s*", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static Regex digestParm1 = new Regex("\\s*(?\\w+)\\s*=\\s*\"(?\\S+)\"", + private static Regex digestParm1 = new Regex("\\s*(?\\w+)\\s*=\\s*\"(?[^\"]+)\"", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static Regex digestParm2 = new Regex("\\s*(?\\w+)\\s*=\\s*(?[^\\p{P}\\s]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static Regex reuserPass = new Regex("\\s*(?\\w+)\\s*:\\s*(?\\S*)", + private static Regex reuserPass = new Regex("\\s*(?[^:]+)\\s*:\\s*(?\\S*)", RegexOptions.Compiled | RegexOptions.IgnoreCase); // For efficiency, we create static instances of these objects @@ -165,11 +169,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // Constructor - internal RequestData(OSHttpRequest p_request, OSHttpResponse p_response, string qprefix) + internal RequestData(OSHttpRequest p_request, OSHttpResponse p_response, string p_qprefix) { request = p_request; response = p_response; + qprefix = p_qprefix; sbuilder.Length = 0; @@ -182,7 +187,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory method = request.HttpMethod.ToLower(); initUrl(); - initParameters(qprefix.Length); + initParameters(p_qprefix.Length); } @@ -254,11 +259,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } // If we want a specific authentication mechanism, make sure - // we get it. + // we get it. null indicates we don't care. non-null indicates + // a specific scheme requirement. if (scheme != null && scheme.ToLower() != reqscheme) { - Rest.Log.DebugFormat("{0} Challenge reason: Required scheme not accepted", MsgId); + Rest.Log.DebugFormat("{0} Challenge reason: Requested scheme not acceptable", MsgId); DoChallenge(); } @@ -268,15 +274,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory switch (reqscheme) { - case "digest" : - Rest.Log.DebugFormat("{0} Digest authentication offered", MsgId); - DoDigest(authdata); - break; - - case "basic" : - Rest.Log.DebugFormat("{0} Basic authentication offered", MsgId); - DoBasic(authdata); - break; + case "digest" : + Rest.Log.DebugFormat("{0} Digest authentication offered", MsgId); + DoDigest(authdata); + break; + + case "basic" : + Rest.Log.DebugFormat("{0} Basic authentication offered", MsgId); + DoBasic(authdata); + break; } // If the current header is invalid, then a challenge is still needed. @@ -406,10 +412,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory authparms.TryGetValue("uri", out authPrefix); // There MUST be a nonce string present. We're not preserving any server - // side state and we can;t validate the MD5 unless the lcient returns it + // side state and we can't validate the MD5 unless the client returns it // to us, as it should. - if (!authparms.TryGetValue("nonce", out nonce)) + if (!authparms.TryGetValue("nonce", out nonce) || nonce == null) { Rest.Log.WarnFormat("{0} Authentication failed: nonce missing", MsgId); break; @@ -457,26 +463,28 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory cnonce = authparms["cnonce"]; - if (!authparms.ContainsKey("nc")) + if (!authparms.TryGetValue("nc", out nck) || nck == null) { Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId); break; } - nck = authparms["nc"]; + Rest.Log.DebugFormat("{0} Comparing nonce indices", MsgId); - if (cntable.TryGetValue(cnonce, out ncl)) + if (cntable.TryGetValue(nonce, out ncl)) { - if (Rest.Hex2Int(ncl) <= Rest.Hex2Int(nck)) + Rest.Log.DebugFormat("{0} nonce values: Verify that request({1}) > Reference({2})", MsgId, nck, ncl); + + if (Rest.Hex2Int(ncl) >= Rest.Hex2Int(nck)) { Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId); break; } - cntable[cnonce] = nck; + cntable[nonce] = nck; } else { - lock (cntable) cntable.Add(cnonce, nck); + lock (cntable) cntable.Add(nonce, nck); } } @@ -519,6 +527,22 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory sbuilder.Length = 0; + if (scheme == null || scheme == Rest.AS_BASIC) + { + + sbuilder.Append(Rest.AS_BASIC); + + if (realm != null) + { + sbuilder.Append(" realm=\""); + sbuilder.Append(realm); + sbuilder.Append("\""); + } + AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); + } + + sbuilder.Length = 0; + if (scheme == null || scheme == Rest.AS_DIGEST) { @@ -583,57 +607,135 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory sbuilder.Append(Rest.CS_COMMA); } - if (Rest.Domains.Count != 0) - { - sbuilder.Append("domain="); - sbuilder.Append(Rest.CS_DQUOTE); - foreach (string dom in Rest.Domains.Values) - { - sbuilder.Append(dom); - sbuilder.Append(Rest.CS_SPACE); - } - if (sbuilder[sbuilder.Length-1] == Rest.C_SPACE) - { - sbuilder.Length = sbuilder.Length-1; - } - sbuilder.Append(Rest.CS_DQUOTE); - sbuilder.Append(Rest.CS_COMMA); - } + // We don;t know the userid that will be used + // so we cannot make any authentication domain + // assumptions. So the prefix will determine + // this. - if (sbuilder[sbuilder.Length-1] == Rest.C_COMMA) - { - sbuilder.Length = sbuilder.Length-1; - } + sbuilder.Append("domain="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(qprefix); + sbuilder.Append(Rest.CS_DQUOTE); AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); } - if (scheme == null || scheme == Rest.AS_BASIC) - { + } - sbuilder.Append(Rest.AS_BASIC); + /// + /// This method provides validation in support of the BASIC + /// authentication method. This is not normaly expected to be + /// used, but is included for completeness (and because I tried + /// it first). + /// - if (realm != null) - { - sbuilder.Append(" realm=\""); - sbuilder.Append(realm); - sbuilder.Append("\""); - } - AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); - } + private bool Validate(string user, string pass) + { + + Rest.Log.DebugFormat("{0} Simple User Validation", MsgId); + + // Both values are required + + if (user == null || pass == null) + return false; + + // Eliminate any leading or trailing spaces + user = user.Trim(); + + return vetPassword(user, pass); } - private bool Validate(string user, string pass) + /// + /// This mechanism is used by the digest authetnication mechanism + /// to return the user's password. In fact, because the OpenSim + /// user's passwords are already hashed, and the HTTP mechanism + /// does not supply an open password, the hashed passwords cannot + /// be used unless the cliemt has used the same salting mechanism + /// to has the password before using it in the authentication + /// algorithn. This is not inconceivable... + /// + + private string getPassword(string user) { - Rest.Log.DebugFormat("{0} Validating {1}:{2}", MsgId, user, pass); - return user == "awebb" && pass == getPassword(user); + + int x; + string first; + string last; + + // Distinguish the parts, if necessary + + if ((x=user.IndexOf(Rest.C_SPACE)) != -1) + { + first = user.Substring(0,x); + last = user.Substring(x+1); + } + else + { + first = user; + last = String.Empty; + } + + UserProfileData udata = Rest.UserServices.GetUserProfile(first, last); + + // If we don;t recognize the user id, perhaps it is god? + + if (udata == null) + { + Rest.Log.DebugFormat("{0} Administrator", MsgId); + return Rest.GodKey; + } + else + { + Rest.Log.DebugFormat("{0} Normal User {1}", MsgId, user); + return udata.PasswordHash; + } + } - private string getPassword(string user) + /// + /// This is used by the BASIC authentication scheme to calculate + /// the double hash used by OpenSim to encode user's passwords. + /// It returns true, if the supplied password is actually correct. + /// If the specified user-id is not recognized, but the password + /// matches the God password, then this is accepted as an admin + /// session. + /// + + private bool vetPassword(string user, string pass) { - return Rest.GodKey; + + int x; + string HA1; + string first; + string last; + + // Distinguish the parts, if necessary + + if ((x=user.IndexOf(Rest.C_SPACE)) != -1) + { + first = user.Substring(0,x); + last = user.Substring(x+1); + } + else + { + first = user; + last = String.Empty; + } + + UserProfileData udata = Rest.UserServices.GetUserProfile(first, last); + + // If we don;t recognize the user id, perhaps it is god? + + if (udata == null) + return pass == Rest.GodKey; + + HA1 = HashToString(pass); + HA1 = HashToString(String.Format("{0}:{1}",HA1,udata.PasswordSalt)); + + return (0 == sc.Compare(HA1, udata.PasswordHash)); + } // Validate the request-digest @@ -773,11 +875,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (reset) { buffer = null; - body = null; + SendHtml(message); + body = html; } if (Rest.DEBUG) { + Rest.Log.DebugFormat("{0} Request Failure State Dump", MsgId); Rest.Log.DebugFormat("{0} Scheme = {1}", MsgId, scheme); Rest.Log.DebugFormat("{0} Realm = {1}", MsgId, realm); Rest.Log.DebugFormat("{0} Domain = {1}", MsgId, domain); @@ -828,10 +932,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.DebugFormat("{0} Generating Response", MsgId); - - // Process any arbitrary headers collected - - BuildHeaders(); + Rest.Log.DebugFormat("{0} Method is {1}", MsgId, method); // A Head request can NOT have a body! if (method != Rest.HEAD) @@ -839,6 +940,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Response is not abbreviated", MsgId); + // If the writer is non-null then we know that an XML + // data component exists. Flush and close the writer and + // then convert the result to the expected buffer format + // unless the request has already been failed for some + // reason. + if (writer != null) { Rest.Log.DebugFormat("{0} XML Response handler extension ENTRY", MsgId); @@ -869,41 +976,57 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } } + // OK, if the buffer contains something, regardless of how + // it got there, set various response headers accordingly. + if (buffer != null) { Rest.Log.DebugFormat("{0} Buffer-based entity", MsgId); - if (response.Headers.Get("Content-Encoding") == null) - response.ContentEncoding = encoding; response.ContentLength64 = buffer.Length; - response.SendChunked = false; - response.KeepAlive = false; } + else + { + response.ContentLength64 = 0; + } + + if (response.Headers.Get("Content-Encoding") == null) + response.ContentEncoding = encoding; + + response.SendChunked = chunked; + response.KeepAlive = keepAlive; } - // Set the status code & description. If nothing - // has been stored, we consider that a success + // Set the status code & description. If nothing has been stored, + // we consider that a success. if (statusCode == 0) { Complete(); } + // Set the response code in the actual carrier + response.StatusCode = statusCode; - if (response.StatusCode == (int)OSHttpStatusCode.RedirectMovedTemporarily || - response.StatusCode == (int)OSHttpStatusCode.RedirectMovedPermanently) + // For a redirect we need to set the relocation header accordingly + + if (response.StatusCode == (int) Rest.HttpStatusCodeTemporaryRedirect || + response.StatusCode == (int) Rest.HttpStatusCodePermanentRedirect) { + Rest.Log.DebugFormat("{0} Re-direct location is {1}", MsgId, redirectLocation); response.RedirectLocation = redirectLocation; } + // And include the status description if provided. + if (statusDescription != null) { + Rest.Log.DebugFormat("{0} Status description is {1}", MsgId, statusDescription); response.StatusDescription = statusDescription; } - // Finally we send back our response, consuming - // any exceptions that doing so might produce. + // Finally we send back our response. // We've left the setting of handled' until the // last minute because the header settings included @@ -913,6 +1036,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory handled = true; + DumpHeaders(); + + // if (request.InputStream != null) + // { + // Rest.Log.DebugFormat("{0} Closing input stream", MsgId); + // request.InputStream.Close(); + // } + if (buffer != null && buffer.Length != 0) { Rest.Log.DebugFormat("{0} Entity buffer, length = {1} : <{2}>", @@ -920,12 +1051,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory response.OutputStream.Write(buffer, 0, buffer.Length); } - response.OutputStream.Close(); + // Closing the outputstream should complete the transmission process - if (request.InputStream != null) - { - request.InputStream.Close(); - } + Rest.Log.DebugFormat("{0} Closing output stream", MsgId); + response.OutputStream.Close(); } @@ -935,19 +1064,23 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } - // Add a header to the table. If the header - // already exists, it is replaced. + // Add a header to the table. We need to allow + // multiple instances of many of the headers. + // If the internal void AddHeader(string hdr, string data) { - - if (headers == null) + if (Rest.DEBUG) { - headers = new NameValueCollection(); + Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>", + MsgId, hdr, data); + if (response.Headers.Get(hdr) != null) + { + Rest.Log.DebugFormat("{0} Multipe {1} headers will be generated>", + MsgId, hdr); + } } - - headers[hdr] = data; - + response.Headers.Add(hdr, data); } // Keep explicit track of any headers which @@ -955,43 +1088,30 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory internal void RemoveHeader(string hdr) { - - if (removed_headers == null) - { - removed_headers = new List(); - } - - removed_headers.Add(hdr); - - if (headers != null) + if (Rest.DEBUG) { - headers.Remove(hdr); + Rest.Log.DebugFormat("{0} Removing header: <{1}>", MsgId, hdr); + if (response.Headers.Get(hdr) == null) + { + Rest.Log.DebugFormat("{0} No such header existed", + MsgId, hdr); + } } - + response.Headers.Remove(hdr); } - // Should it prove necessary, we could always - // restore the header collection from a cloned - // copy, but for now we'll assume that that is - // not necessary. + /// + /// Dump headers that will be generated in the response + /// - private void BuildHeaders() + internal void DumpHeaders() { - if (removed_headers != null) - { - foreach (string h in removed_headers) - { - Rest.Log.DebugFormat("{0} Removing header: <{1}>", MsgId, h); - response.Headers.Remove(h); - } - } - if (headers!= null) + if (Rest.DEBUG) { - for (int i = 0; i < headers.Count; i++) + for (int i=0;i", - MsgId, headers.GetKey(i), headers.Get(i)); - response.Headers.Add(headers.GetKey(i), headers.Get(i)); + Rest.Log.DebugFormat("{0} Header[{1}] : {2}", MsgId, i, + response.Headers.Get(i)); } } } @@ -1019,7 +1139,6 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory path = uri.AbsolutePath; if (path.EndsWith(Rest.UrlPathSeparator)) path = path.Substring(0,path.Length-1); - path = Uri.UnescapeDataString(path); } // If we succeeded in getting a path, perform any @@ -1040,6 +1159,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory pathNodes = EmptyPath; } + // Elimiate any %-escaped values. This is left until here + // so that escaped "+' are not mistakenly replaced. + + path = Uri.UnescapeDataString(path); + // Request server context info hostname = uri.Host; @@ -1149,14 +1273,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory internal void initXmlReader() { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.ConformanceLevel = ConformanceLevel.Fragment; settings.IgnoreComments = true; settings.IgnoreWhitespace = true; settings.IgnoreProcessingInstructions = true; settings.ValidationType = ValidationType.None; - // reader = XmlReader.Create(new StringReader(entity),settings); + reader = XmlReader.Create(request.InputStream,settings); + } private void Flush() diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs index e88c54d..439bbb4 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs @@ -69,7 +69,6 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory internal static bool ExtendedEscape = true; internal static bool DumpAsset = false; internal static string Realm = "REST"; - internal static Dictionary Domains = new Dictionary(); internal static int CreationDate = (int) (DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; internal static int DumpLineSize = 32; // Should be a multiple of 16 or (possibly) 4 diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs index 0fc937f..70957f5 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs @@ -44,35 +44,31 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory public class RestAssetServices : IRest { - private string key = "assets"; private bool enabled = false; private string qPrefix = "assets"; // A simple constructor is used to handle any once-only // initialization of working classes. - public RestAssetServices(RestHandler p_rest) + public RestAssetServices() { Rest.Log.InfoFormat("{0} Asset services initializing", MsgId); Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version); - // Integrate domain + // If the handler specifies a relative path for its domain + // then we must add the standard absolute prefix, e.g. /admin if (!qPrefix.StartsWith(Rest.UrlPathSeparator)) { qPrefix = Rest.Prefix + Rest.UrlPathSeparator + qPrefix; } - // Authentication domain - - Rest.Domains.Add(key,Rest.Config.GetString("asset-domain",qPrefix)); - - // Register interface + // Register interface using the fully-qualified prefix Rest.Plugin.AddPathHandler(DoAsset, qPrefix, Allocate); - // Activate + // Activate if all went OK enabled = true; diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs index 50412c9..7bd83c1 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs @@ -38,10 +38,33 @@ 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 + /// 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 @@ -49,73 +72,78 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// 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. + /// of handlers. Examples of services classes are RestInventoryServices + /// and RestSkeleton. /// - 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) + try { - classes.Add(t); + 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 - /// - /// 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. + /// 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) { - 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); + 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; } @@ -126,14 +154,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory #region overriding properties - // Used to differentiate the message header. + // 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 configuration space. + // Used to partition the .ini configuration space. public override string ConfigName { @@ -167,32 +198,32 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// 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. - /// + // This plugin will only be enabled if the broader + // REST plugin mechanism is enabled. - Rest.Log.InfoFormat("{0} Plugin is initializing", MsgID); + 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); + Rest.Log.WarnFormat("{0} Plugins are disabled", MsgId); return; } - Rest.Log.InfoFormat("{0} Plugin will be enabled", MsgID); + 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. - /// + // These are stored in static variables to make + // them easy to reach from anywhere in the assembly. Rest.main = openSim; Rest.Plugin = this; @@ -223,6 +254,9 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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, @@ -247,22 +281,24 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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. - /// + // 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) { - handler.Initialize(); + try + { + handler.Initialize(); + } + catch (Exception e) + { + Rest.Log.ErrorFormat("{0} initialization error: {1}", MsgId, e.Message); + } } - /// - /// Now that everything is setup we can proceed and - /// add this agent to the HTTP server's handler list - /// + // Now that everything is setup we can proceed to + // add THIS agent to the HTTP server's handler list if (!AddAgentHandler(Rest.Name,this)) { @@ -276,7 +312,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } catch (Exception e) { - Rest.Log.ErrorFormat("{0} Plugin initialization has failed: {1}", MsgID, e.Message); + Rest.Log.ErrorFormat("{0} Plugin initialization has failed: {1}", MsgId, e.Message); } } @@ -290,10 +326,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// 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); + Rest.Log.InfoFormat("{0} Plugin is terminating", MsgId); try { @@ -313,45 +350,62 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory #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. + /// 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; - foreach (string key in pathHandlers.Keys) + + try { - if (path.StartsWith(key)) + foreach (string key in pathHandlers.Keys) { - return ( path.Length == key.Length || - path.Substring(key.Length,1) == Rest.UrlPathSeparator); + 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)) + path = String.Format("{0}{1}{2}", request.HttpMethod, Rest.UrlMethodSeparator, path); + foreach (string key in streamHandlers.Keys) { - return true; + 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); @@ -371,8 +425,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory try { - handled = FindPathHandler(request, response) || - FindStreamHandler(request, response); + handled = ( FindPathHandler(request, response) || + FindStreamHandler(request, response) ); } catch (Exception e) { @@ -406,6 +460,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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)) @@ -432,7 +491,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } - // Preserves the original handler's semantics + /// + /// 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) { @@ -454,17 +519,26 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!streamHandlers.ContainsKey(path)) { streamHandlers.Add(path, new RestStreamHandler(httpMethod, path, method)); - Rest.Log.DebugFormat("{0} Added handler for {1}", MsgID, path); + Rest.Log.DebugFormat("{0} Added handler for {1}", MsgId, path); } else { - Rest.Log.WarnFormat("{0} Ignoring duplicate handler for {1}", MsgID, path); + 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) + internal bool FindPathHandler(OSHttpRequest request, OSHttpResponse response) { RequestData rdata = null; @@ -516,8 +590,19 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } + /// + /// 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); @@ -537,4 +622,5 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } } + } diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs index 65603c5..5de2cb4 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs @@ -31,6 +31,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Xml; +using OpenJPEGNet; using OpenSim.Framework; using OpenSim.Framework.Servers; using OpenSim.Framework.Communications; @@ -44,35 +45,33 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory public class RestInventoryServices : IRest { - private string key = "inventory"; private bool enabled = false; private string qPrefix = "inventory"; - // A simple constructor is used to handle any once-only - // initialization of working classes. + /// + /// A simple constructor is used to handle any once-only + /// initialization of working classes. + /// - public RestInventoryServices(RestHandler p_rest) + public RestInventoryServices() { Rest.Log.InfoFormat("{0} Inventory services initializing", MsgId); Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version); - // Update to reflect the full prefix if not absolute + // If a relative path was specified for the handler's domain, + // add the standard prefix to make it absolute, e.g. /admin if (!qPrefix.StartsWith(Rest.UrlPathSeparator)) { qPrefix = Rest.Prefix + Rest.UrlPathSeparator + qPrefix; } - // Authentication domain - - Rest.Domains.Add(key, Rest.Config.GetString("inventory-domain",qPrefix)); - - // Register interface + // Register interface using the absolute URI. Rest.Plugin.AddPathHandler(DoInventory,qPrefix,Allocate); - // Activate + // Activate if everything went OK enabled = true; @@ -80,16 +79,20 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } - // Post-construction, pre-enabled initialization opportunity - // Not currently exploited. + /// + /// Post-construction, pre-enabled initialization opportunity + /// Not currently exploited. + /// public void Initialize() { } - // Called by the plug-in to halt REST processing. Local processing is - // disabled, and control blocks until all current processing has - // completed. No new processing will be started + /// + /// Called by the plug-in to halt REST processing. Local processing is + /// disabled, and control blocks until all current processing has + /// completed. No new processing will be started + /// public void Close() { @@ -97,7 +100,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.InfoFormat("{0} Inventory services closing down", MsgId); } - // Convenient properties + /// + /// This property is declared locally because it is used a lot and + /// brevity is nice. + /// internal string MsgId { @@ -106,15 +112,22 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory #region Interface + /// + /// The plugin (RestHandler) calls this method to allocate the request + /// state carrier for a new request. It is destroyed when the request + /// completes. All request-instance specific state is kept here. This + /// is registered when this service provider is registered. + /// + private RequestData Allocate(OSHttpRequest request, OSHttpResponse response) { return (RequestData) new InventoryRequestData(request, response, qPrefix); } /// - /// This method is registered with the handler when this class is - /// initialized. It is called whenever the URI includes this handler's - /// prefix string. + /// This method is registered with the handler when this service provider + /// is initialized. It is called whenever the plug-in identifies this service + /// provider as the best match. /// It handles all aspects of inventory REST processing. /// @@ -125,7 +138,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} DoInventory ENTRY", MsgId); - // We're disabled + // If we're disabled, do nothing. + if (!enabled) { return; @@ -169,23 +183,32 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Authenticated {1}", MsgId, rdata.userName); - // We can only get here if we're authorized - // - // The requestor may have specified an LLUUID or - // a conjoined FirstNameLastName string. We'll - // try both. If we fail with the first, UUID, - // attempt, then we need two nodes to construct - // a valid avatar name. + /// + /// We can only get here if we are authorized + /// + /// The requestor may have specified an LLUUID or + /// a conjoined FirstName LastName string. We'll + /// try both. If we fail with the first, UUID, + /// attempt, we try the other. As an example, the + /// URI for a valid inventory request might be: + /// + /// http://:/admin/inventory/Arthur Dent + /// + /// Indicating that this is an inventory request for + /// an avatar named Arthur Dent. This is ALl that is + /// required to designate a GET for an entire + /// inventory. + /// // Do we have at least a user agent name? if (rdata.parameters.Length < 1) { Rest.Log.WarnFormat("{0} Inventory: No user agent identifier specified", MsgId); - rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); + rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest+": No user identity specified"); } - // The next parameter MUST be the agent identification, either an LLUUID + // The first parameter MUST be the agent identification, either an LLUUID // or a space-separated First-name Last-Name specification. try @@ -205,10 +228,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory else { Rest.Log.DebugFormat("{0} A Valid UUID or both first and last names must be specified", MsgId); - rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); + rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest+": invalid user identity"); } } + // If the user rpofile is null then either the server is broken, or the + // user is not known. We always assume the latter case. + if (rdata.userProfile != null) { Rest.Log.DebugFormat("{0} Profile obtained for agent {1} {2}", @@ -217,11 +243,20 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory else { Rest.Log.DebugFormat("{0} No profile for {1}", MsgId, rdata.path); - rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound); + rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound+": unrecognized user identity"); } - // If we get to here, then we have successfully obtained an inventory - // for the specified user. + // If we get to here, then we have effectively validated the user's + // identity. Now we need to get the inventory. If the server does not + // have the inventory, we reject the request with an appropriate explanation. + // + // Note that inventory retrieval is an asynchronous event, we use the rdata + // class instance as the basis for our synchronization. + // + // TODO + // If something went wrong in inventory processing the thread could stall here + // indefinitely. There should be a watchdog timer to fail the request if the + // response is not recieved in a timely fashion. rdata.uuid = rdata.userProfile.ID; @@ -250,7 +285,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.DebugFormat("{0} Inventory is not available [1] for agent {1} {2}", MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); - rdata.Fail(Rest.HttpStatusCodeServerError,Rest.HttpStatusDescServerError); + rdata.Fail(Rest.HttpStatusCodeServerError,Rest.HttpStatusDescServerError+": inventory retrieval failed"); } } @@ -258,7 +293,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.DebugFormat("{0} Inventory is not available for agent [3] {1} {2}", MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); - rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound); + rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound+": no inventory for user"); } // If we get here, then we have successfully retrieved the user's information @@ -292,7 +327,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Method {1} not supported for {2}", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed, - Rest.HttpStatusDescMethodNotAllowed); + Rest.HttpStatusDescMethodNotAllowed+": "+rdata.method+" not supported"); break; } @@ -315,10 +350,19 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory rdata.writer.WriteStartElement(String.Empty,"Inventory",String.Empty); + // If there was only one parameter, then the entire + // inventory is being requested. + if (rdata.parameters.Length == 1) { formatInventory(rdata, rdata.root, String.Empty); } + + // If there are additional parameters, then these represent + // a path relative to the root of the inventory. This path + // must be traversed before we format the sub-tree thus + // identified. + else { traverseInventory(rdata, rdata.root, 1); @@ -332,33 +376,35 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } /// - /// In the case of the inventory, and probably much else + /// In the case of the inventory, and probably in general, /// the distinction between PUT and POST is not always /// easy to discern. Adding a directory can be viewed as /// an addition, or as a modification to the inventory as - /// a whole. + /// a whole. This is exacerbated by a lack of consistency + /// across different implementations. + /// + /// For OpenSim POST is an update and PUT is an addition. /// - /// The best distinction may be the relationship between - /// the entity and the URI. If we view POST as an update, - /// then the enity represents a replacement for the - /// element named by the URI. If the operation is PUT, - /// then the URI describes the context into which the - /// entity will be added. + /// The best way to exaplain the distinction is to + /// consider the relationship between the URI and the + /// entity in question. For POST, the URI identifies the + /// entity to be modified or replaced. + /// If the operation is PUT,then the URI describes the + /// context into which the new entity will be added. /// /// As an example, suppose the URI contains: /// /admin/inventory/Clothing - /// Suppose the entity represents a Folder, called - /// "Clothes". - /// - /// A POST request will result in the replacement of - /// "Clothing" by "Clothes". Whereas a PUT request - /// would add Clothes as a sub-directory of Clothing. /// - /// This is the model followed by this implementation. + /// A POST request will result in some modification of + /// the folder or item named "Clothing". Whereas a PUT + /// request will add some new information into the + /// content identified by Clothing. It follows from this + /// that for PUT, the element identified by the URI must + /// be a folder. /// /// - /// PUT adds new information to the inventory at the + /// PUT adds new information to the inventory in the /// context identified by the URI. /// @@ -376,7 +422,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // exception. // It follows that we can only add information if the URI - // has identified a folder. So only folder is supported + // has identified a folder. So only a type of folder is supported // in this case. if (typeof(InventoryFolderBase) == InventoryNode.GetType() || @@ -390,14 +436,19 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} {1}: Resource(s) will be added to folder {2}", MsgId, rdata.method, rdata.path); - // Reconstitute inventory sub-tree from the XML supplied in the entity. - // This is a stand-alone inventory subtree, not yet integrated into the - // existing tree. + // Reconstitute the inventory sub-tree from the XML supplied in the entity. + // The result is a stand-alone inventory subtree, not yet integrated into the + // existing tree. An inventory collection consists of three components: + // [1] A (possibly empty) set of folders. + // [2] A (possibly empty) set of items. + // [3] A (possibly empty) set of assets. + // If all of these are empty, then the PUT is a harmless no-operation. XmlInventoryCollection entity = ReconstituteEntity(rdata); - // Inlined assest included in entity. If anything fails, - // return failure to requestor. + // Inlined assets can be included in entity. These must be incorporated into + // the asset database before we attempt to update the inventory. If anything + // fails, return a failure to requestor. if (entity.Assets.Count > 0) { @@ -410,10 +461,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Rest asset: {1} {2} {3}", MsgId, asset.ID, asset.Type, asset.Name); Rest.AssetServices.AddAsset(asset); + if (Rest.DumpAsset) { Rest.Dump(asset.Data); } + } } @@ -424,11 +477,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory foreach (InventoryFolderBase folder in entity.Folders) { - InventoryFolderBase found = null; + InventoryFolderBase found; - // If the parentID is zero, then this is going - // into the root identified by the URI. The requestor - // may have already set the parent ID correctly, in which + // If the parentID is zero, then this folder is going + // into the root folder identified by the URI. The requestor + // may have already set the parent ID explicitly, in which // case we don't have to do it here. if (folder.ParentID == LLUUID.Zero) @@ -437,7 +490,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } // Search the existing inventory for an existing entry. If - // we have once, we need to decide if it has really changed. + // we have one, we need to decide if it has really changed. // It could just be present as (unnecessary) context, and we // don't want to waste time updating the database in that // case, OR, it could be being moved from another location @@ -451,6 +504,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (xf.ID == folder.ID) { found = xf; + break; } } @@ -492,6 +546,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (xi.ID == item.ID) { found = xi; + break; } } @@ -516,7 +571,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} {1}: Resource {2} is not a valid context: {3}", MsgId, rdata.method, rdata.path, InventoryNode.GetType()); rdata.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": invalid resource context"); } rdata.Complete(); @@ -531,7 +586,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// [1] It identifies the user whose inventory is to be /// processed. /// [2] It optionally specifies a subtree of the inventory - /// that is to be used to resolve an relative subtree + /// that is to be used to resolve any relative subtree /// specifications in the entity. If nothing is specified /// then the whole inventory is implied. /// Please note that the subtree specified by the URI is only relevant @@ -540,7 +595,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// elements will be implicitly referenced within the context identified /// by the URI. /// If an element in the entity specifies an explicit parent folder, then - /// that parent is effective, regardless of nay value specified in the + /// that parent is effective, regardless of any value specified in the /// URI. If the parent does not exist, then the element, and any dependent /// elements, are ignored. This case is actually detected and handled /// during the reconstitution process. @@ -555,33 +610,54 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); - // As long as we have a context, then we have something - // meaningful to do, unlike PUT. So reconstitute the + // As long as we have a node, then we have something + // meaningful to do, unlike PUT. So we reconstitute the // subtree before doing anything else. Note that we - // etiher got a context or we threw an exception. + // etiher got a valid node or we threw an exception. XmlInventoryCollection entity = ReconstituteEntity(rdata); - // Incorporate any inlined assets first + // Incorporate any inlined assets first. Any failures + // will terminate the request. - if (entity.Assets.Count != 0) + if (entity.Assets.Count > 0) { + Rest.Log.DebugFormat("{0} Adding {1} assets to server", + MsgId, entity.Assets.Count); + foreach (AssetBase asset in entity.Assets) { - // Asset was validated during the collection - // process + Rest.Log.DebugFormat("{0} Rest asset: {1} {2} {3}", + MsgId, asset.ID, asset.Type, asset.Name); + + // The asset was validated during the collection process + Rest.AssetServices.AddAsset(asset); + + if (Rest.DumpAsset) + { + Rest.Dump(asset.Data); + } + } } /// - /// URI specifies a folder to be updated. + /// The URI specifies either a folder or an item to be updated. /// /// - /// The root node in the entity must have the same - /// UUID as the node identified by the URI. The - /// parentID if different indicates that the updated - /// folder is actually being moved too. + /// The root node in the entity will replace the node identified + /// by the URI. This means the parent will remain the same, but + /// any or all attributes associated with the named element + /// will change. + /// + /// If the inventory collection contains an element with a zero + /// parent ID, then this is taken to be the replacement for the + /// named node. The collection MAY also specify an explicit + /// parent ID, in this case it MAY identify the same parent as + /// the current node, or it MAY specify a different parent, + /// indicating that the folder is being moved in addition to any + /// other modifications being made. /// if (typeof(InventoryFolderBase) == InventoryNode.GetType() || @@ -592,7 +668,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory InventoryFolderBase xml = null; // Scan the set of folders in the entity collection for an - // entry that macthes the context folder. It is assumed that + // entry that matches the context folder. It is assumed that // the only reliable indicator of this is a zero UUID ( using // implicit context), or the parent's UUID matches that of the // URI designated node (explicit context). We don't allow @@ -617,14 +693,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } } - // More than one entry is ambiguous + // More than one entry is ambiguous. Other folders should be + // added using the PUT verb. if (count > 1) { Rest.Log.DebugFormat("{0} {1}: Request for <{2}> is ambiguous", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": context is ambiguous"); } // Exactly one entry means we ARE replacing the node @@ -679,7 +756,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} {1}: Request should not contain any folders <{2}>", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": folder is not allowed"); } if (entity.Items.Count > 1) @@ -687,7 +764,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} {1}: Entity contains too many items <{2}>", MsgId, rdata.method, rdata.path); rdata.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": too may items"); } xml = entity.Items[0]; @@ -854,7 +931,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous", MsgId, rdata.method, rdata.path); - rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound); + rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound+": request is ambiguous"); } } } @@ -863,7 +940,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} {1}: Resource {2} not found", MsgId, rdata.method, rdata.path); - rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound); + rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound+": resource "+rdata.path+" not found"); return null; /* Never reached */ @@ -931,7 +1008,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Inventory does not contain item/folder: <{1}>", MsgId, rdata.path); - rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound); + rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound+": no such item/folder"); } @@ -1061,6 +1138,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory TrashCan.Version = 1; TrashCan.Type = (short) AssetType.TrashFolder; TrashCan.ParentID = f.ID; + TrashCan.Owner = f.Owner; Rest.InventoryServices.AddFolder(TrashCan); } } @@ -1070,7 +1148,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.DebugFormat("{0} No Trash Can available", MsgId); rdata.Fail(Rest.HttpStatusCodeServerError, - Rest.HttpStatusDescServerError); + Rest.HttpStatusDescServerError+": unable to create trash can"); } return TrashCan; @@ -1313,7 +1391,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Folder: unrecognized attribute: {1}:{2}", MsgId, ic.xml.Name, ic.xml.Value); ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": unrecognized attribute"); break; } } @@ -1349,7 +1427,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in folder {2}", MsgId, ic.Item.Folder, result.ID); ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": invalid parent"); } } @@ -1457,7 +1535,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Item: Unrecognized attribute: {1}:{2}", MsgId, ic.xml.Name, ic.xml.Value); ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": unrecognized attribute"); break; } } @@ -1570,7 +1648,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.DebugFormat("{0} LLUID unimbedded asset must be inline", MsgId); ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": no context for asset"); } } @@ -1691,7 +1769,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.ErrorFormat("{0} Unable to parse request", MsgId); ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": request parse error"); } // Every item is required to have a name (via REST anyway) @@ -1700,7 +1778,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.ErrorFormat("{0} An item name MUST be specified", MsgId); ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": item name required"); } // An item MUST have an asset ID. AssetID should never be zero @@ -1713,7 +1791,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.ErrorFormat("{0} Unable to complete request", MsgId); Rest.Log.InfoFormat("{0} Asset information is missing", MsgId); ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": asset information required"); } @@ -1751,7 +1829,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in item {2}", MsgId, ic.Item.Folder, ic.Item.ID); ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + Rest.HttpStatusDescBadRequest+": parent information required"); } } @@ -1825,6 +1903,22 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (ic.Item.InvType == (int) AssetType.Unknown) ic.Item.InvType = (int) AssetType.ImageJPEG; break; + case "tga" : + if (parts[parts.Length - 2].IndexOf("_texture") != -1) + { + if (ic.Item.AssetType == (int) AssetType.Unknown) + ic.Item.AssetType = (int) AssetType.TextureTGA; + if (ic.Item.InvType == (int) AssetType.Unknown) + ic.Item.InvType = (int) AssetType.TextureTGA; + } + else + { + if (ic.Item.AssetType == (int) AssetType.Unknown) + ic.Item.AssetType = (int) AssetType.ImageTGA; + if (ic.Item.InvType == (int) AssetType.Unknown) + ic.Item.InvType = (int) AssetType.ImageTGA; + } + break; default : Rest.Log.DebugFormat("{0} Type was not inferred", MsgId); break; @@ -1832,6 +1926,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } } + /// If this is a TGA remember the fact + + if (ic.Item.AssetType == (int) AssetType.TextureTGA || + ic.Item.AssetType == (int) AssetType.ImageTGA) + { + // TODO: DO we need to convert it? Or is it enough to flag + // it appropriately? + } + ic.reset(); } diff --git a/OpenSim/Framework/Servers/BaseHttpServer.cs b/OpenSim/Framework/Servers/BaseHttpServer.cs index 170a653..4dc5994 100644 --- a/OpenSim/Framework/Servers/BaseHttpServer.cs +++ b/OpenSim/Framework/Servers/BaseHttpServer.cs @@ -171,18 +171,20 @@ namespace OpenSim.Framework.Servers OSHttpRequest request = new OSHttpRequest(context.Request); OSHttpResponse response = new OSHttpResponse(context.Response); - // user agent based requests? not sure where this actually gets used from - if (request.UserAgent != null) - { - IHttpAgentHandler agentHandler; + // This is the REST agent interface. We require an agent to properly identify + // itself. If the REST handler recognizes the prefix it will attempt to + // satisfy the request. If it is not recognizable, and no damage has occurred + // the request can be passed through to the other handlers. This is a low + // probability event; if a request is matched it is normally expected to be + // handled + + IHttpAgentHandler agentHandler; - if (TryGetAgentHandler(request, response, out agentHandler)) + if (TryGetAgentHandler(request, response, out agentHandler)) + { + if (HandleAgentRequest(agentHandler, request, response)) { - if (HandleAgentRequest(agentHandler, request, response)) - { - m_log.DebugFormat("[HTTP-AGENT] Handler located for {0}", request.UserAgent); - return; - } + return; } } @@ -508,7 +510,7 @@ namespace OpenSim.Framework.Servers catch (Exception e) { // If the handler did in fact close the stream, then this will blow - // chunks, so that that doesn;t disturb anybody we throw away any + // chunks. So that that doesn't disturb anybody we throw away any // and all exceptions raised. We've done our best to release the // client. try @@ -524,7 +526,10 @@ namespace OpenSim.Framework.Servers } } + // Indicate that the request has been "handled" + return true; + } public void HandleHTTPRequest(OSHttpRequest request, OSHttpResponse response) -- cgit v1.1