From d40bea4a8e09be1f8e87cf41405aaa60fa8826cb Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Wed, 2 Jul 2008 09:02:30 +0000 Subject: [PATCH] From: Alan M Webb This adds REST services for inventory access. It also allows inventory uploads. --- .../Rest/Inventory/IRest.cs | 47 + .../Rest/Inventory/RequestData.cs | 1201 ++++++++++ .../ApplicationPlugins/Rest/Inventory/Rest.cs | 479 ++++ .../Rest/Inventory/RestAssetServices.cs | 257 +++ .../Rest/Inventory/RestHandler.cs | 547 +++++ .../Rest/Inventory/RestInventoryServices.cs | 1993 +++++++++++++++++ prebuild.xml | 38 + 7 files changed, 4562 insertions(+) create mode 100644 OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs create mode 100644 OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs create mode 100644 OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs create mode 100644 OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs create mode 100644 OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs create mode 100644 OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs new file mode 100644 index 0000000000..6f52582b75 --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/IRest.cs @@ -0,0 +1,47 @@ +/* + * 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; + +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. + /// + + internal interface IRest + { + void Initialize(); + void Close(); + } + +} diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs new file mode 100644 index 0000000000..3de9f3658f --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs @@ -0,0 +1,1201 @@ +/* + * 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.IO; +using System.Reflection; +using System.Text; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Collections.Generic; +using System.Collections.Specialized; +using OpenSim.Framework.Servers; +using libsecondlife; +using System.Xml; + +namespace OpenSim.ApplicationPlugins.Rest.Inventory +{ + + /// + /// This class represents the current REST request. It + /// encapsulates the request/response state and takes care + /// of response generation without exposing the REST handler + /// to the actual mechanisms involved. + /// + /// 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 + /// 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 + /// of this. + /// + /// If possible, the underlying request/response state is not + /// changed until the handler explicitly issues a Respond call. + /// This ensures that the request/response pair can be safely + /// processed by subsequent, unrelated, handlers even id the + /// agent handler had completed much of its processing. Think + /// of it as a transactional req/resp capability. + /// + + internal class RequestData + { + + // HTTP Server interface data + + internal OSHttpRequest request = null; + internal OSHttpResponse response = 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; + internal string entity = null; + internal string path = null; + internal string method = null; + internal string statusDescription = null; + internal string redirectLocation = null; + internal string[] pathNodes = null; + internal string[] parameters = null; + internal int statusCode = 0; + internal bool handled = false; + internal LLUUID uuid = LLUUID.Zero; + internal Encoding encoding = Rest.Encoding; + internal Uri uri = null; + internal string query = null; + internal bool fail = false; + internal string hostname = "localhost"; + internal int port = 80; + internal string prefix = Rest.UrlPathSeparator; + + // Authentication related state + + internal bool authenticated = false; + internal string scheme = Rest.AS_DIGEST; + internal string realm = Rest.Realm; + internal string domain = null; + internal string nonce = null; + internal string cnonce = null; + internal string qop = Rest.Qop_Auth; + internal string opaque = null; + internal string stale = null; + internal string algorithm = Rest.Digest_MD5; + internal string authParms = null; + internal string authPrefix = null; + internal string userName = String.Empty; + internal string userPass = String.Empty; + internal LLUUID client = LLUUID.Zero; + + // XML related state + + internal XmlWriter writer = null; + internal XmlReader reader = null; + + // Internal working state + + private StringBuilder sbuilder = new StringBuilder(1024); + private MemoryStream xmldata = null; + + private static readonly string[] EmptyPath = { String.Empty }; + + // Session related tables. These are only needed if QOP is set to "auth-sess" + // and for now at least, it is not. Session related authentication is of + // questionable merit in the context of REST anyway, but it is, arguably, more + // secure. + + private static Dictionary cntable = new Dictionary(); + private static Dictionary sktable = new Dictionary(); + + // This dictionary is used to keep track fo all of the parameters discovered + // when the authorisation header is anaylsed. + + private Dictionary authparms = new Dictionary(); + + // These regular expressions are used to decipher the various header entries. + + private static Regex schema = new Regex("^\\s*(?\\w+)\\s*.*", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + 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+)\"", + 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*)", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + // For efficiency, we create static instances of these objects + + private static MD5 md5hash = MD5.Create(); + + private static StringComparer sc = StringComparer.OrdinalIgnoreCase; + + // Constructor + + internal RequestData(OSHttpRequest p_request, OSHttpResponse p_response, string qprefix) + { + + request = p_request; + response = p_response; + + sbuilder.Length = 0; + + encoding = request.ContentEncoding; + if (encoding == null) + { + encoding = Rest.Encoding; + } + + method = request.HttpMethod.ToLower(); + initUrl(); + + initParameters(qprefix.Length); + + } + + // Just for convenience... + + internal string MsgId + { + get { return Rest.MsgId; } + } + + // Defer authentication check until requested + + internal bool IsAuthenticated + { + get + { + if (Rest.Authenticate) + { + if (!authenticated) + { + authenticate(); + } + + return authenticated; + } + else return true; + } + } + + /// + /// The REST handler has requested authentication. Authentication + /// is considered to be with respect to the current values for + /// Realm, domain, etc. + /// + /// This method checks to see if the current request is already + /// authenticated for this domain. If it is, then it returns + /// true. If it is not, then it issues a challenge to the client + /// and responds negatively to the request. + /// + + private void authenticate() + { + + string authdata = request.Headers.Get("Authorization"); + string reqscheme = String.Empty; + + // If we don't have an authorization header, then this + // user is certainly not authorized. This is the typical + // pivot for the 1st request by a client. + + if (authdata == null) + { + Rest.Log.DebugFormat("{0} Challenge reason: No authorization data", MsgId); + DoChallenge(); + } + + // So, we have authentication data, now we have to check to + // see what we got and whether or not it is valid for the + // current domain. To do this we need to interpret the data + // provided in the Authorization header. First we need to + // identify the scheme being used and route accordingly. + + MatchCollection matches = schema.Matches(authdata); + + foreach (Match m in matches) + { + Rest.Log.DebugFormat("{0} Scheme matched : {1}", MsgId, m.Groups["scheme"].Value); + reqscheme = m.Groups["scheme"].Value.ToLower(); + } + + // If we want a specific authentication mechanism, make sure + // we get it. + + if (scheme != null && scheme.ToLower() != reqscheme) + { + Rest.Log.DebugFormat("{0} Challenge reason: Required scheme not accepted", MsgId); + DoChallenge(); + } + + // In the future, these could be made into plug-ins... + // But for now at least we have no reason to use anything other + // then MD5. TLS/SSL are taken care of elsewhere. + + 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; + } + + // If the current header is invalid, then a challenge is still needed. + + if (!authenticated) + { + Rest.Log.DebugFormat("{0} Challenge reason: Authentication failed", MsgId); + DoChallenge(); + } + + } + + /// + /// Construct the necessary WWW-Authenticate headers and fail the request + /// with a NOT AUTHORIZED response. The parameters are the union of values + /// required by the supported schemes. + /// + + private void DoChallenge() + { + Flush(); + nonce = Rest.NonceGenerator(); // should be unique per 401 (and it is) + Challenge(scheme, realm, domain, nonce, opaque, stale, algorithm, qop, authParms); + Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); + } + + /// + /// Interpret a BASIC authorization claim + /// This is here for completeness, it is not used. + /// + + private void DoBasic(string authdata) + { + + string response = null; + + MatchCollection matches = basicParms.Matches(authdata); + + // In the case of basic authentication there is + // only expected to be a single argument. + + foreach (Match m in matches) + { + authparms.Add("response",m.Groups["pval"].Value); + Rest.Log.DebugFormat("{0} Parameter matched : {1} = {2}", + MsgId, "response", m.Groups["pval"].Value); + } + + // Did we get a valid response? + + if (authparms.TryGetValue("response", out response)) + { + // Decode + response = Rest.Base64ToString(response); + Rest.Log.DebugFormat("{0} Auth response is: <{1}>", MsgId, response); + + // Extract user & password + Match m = reuserPass.Match(response); + userName = m.Groups["user"].Value; + userPass = m.Groups["pass"].Value; + + // Validate against user database + authenticated = Validate(userName,userPass); + } + + } + + /// + /// This is an RFC2617 compliant HTTP MD5 Digest authentication + /// implementation. It has been tested with Firefox, Java HTTP client, + /// and Miscrosoft's Internet Explorer V7. + /// + + private void DoDigest(string authdata) + { + + string response = null; + + MatchCollection matches = digestParm1.Matches(authdata); + + // Collect all of the supplied parameters and store them + // in a dictionary (for ease of access) + + foreach (Match m in matches) + { + authparms.Add(m.Groups["parm"].Value,m.Groups["pval"].Value); + Rest.Log.DebugFormat("{0} String Parameter matched : {1} = {2}", + MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); + } + + // And pick up any tokens too + + matches = digestParm2.Matches(authdata); + + foreach (Match m in matches) + { + authparms.Add(m.Groups["parm"].Value,m.Groups["pval"].Value); + Rest.Log.DebugFormat("{0} Tokenized Parameter matched : {1} = {2}", + MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); + } + + // A response string MUST be returned, otherwise we are + // NOT authenticated. + + Rest.Log.DebugFormat("{0} Validating authorization parameters", MsgId); + + if (authparms.TryGetValue("response", out response)) + { + + string temp = null; + + do + { + + string nck = null; + string ncl = null; + + // The userid is sent in clear text. Needed for the + // verification. + + authparms.TryGetValue("username", out userName); + + // All URI's of which this is a prefix are + // optimistically considered to be authenticated by the + // client. This is also needed to verify the response. + + 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 + // to us, as it should. + + if (!authparms.TryGetValue("nonce", out nonce)) + { + Rest.Log.WarnFormat("{0} Authentication failed: nonce missing", MsgId); + break; + } + + // If there is an opaque string present, it had better + // match what we sent. + + if (authparms.TryGetValue("opaque", out temp)) + { + if (temp != opaque) + { + Rest.Log.WarnFormat("{0} Authentication failed: bad opaque value", MsgId); + break; + } + } + + // If an algorithm string is present, it had better + // match what we sent. + + if (authparms.TryGetValue("algorithm", out temp)) + { + if (temp != algorithm) + { + Rest.Log.WarnFormat("{0} Authentication failed: bad algorithm value", MsgId); + break; + } + } + + // Quality of protection considerations... + + if (authparms.TryGetValue("qop", out temp)) + { + + qop = temp.ToLower(); // replace with actual value used + + // if QOP was specified then + // these MUST be present. + + if (!authparms.ContainsKey("cnonce")) + { + Rest.Log.WarnFormat("{0} Authentication failed: cnonce missing", MsgId); + break; + } + + cnonce = authparms["cnonce"]; + + if (!authparms.ContainsKey("nc")) + { + Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId); + break; + } + + nck = authparms["nc"]; + + if (cntable.TryGetValue(cnonce, out ncl)) + { + if (Rest.Hex2Int(ncl) <= Rest.Hex2Int(nck)) + { + Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId); + break; + } + cntable[cnonce] = nck; + } + else + { + lock(cntable) cntable.Add(cnonce, nck); + } + + } + else + { + + qop = String.Empty; + + // if QOP was not specified then + // these MUST NOT be present. + if (authparms.ContainsKey("cnonce")) + { + Rest.Log.WarnFormat("{0} Authentication failed: invalid cnonce", MsgId); + break; + } + if (authparms.ContainsKey("nc")) + { + Rest.Log.WarnFormat("{0} Authentication failed: invalid cnonce counter[2]", MsgId); + break; + } + } + + // Validate the supplied userid/password info + + authenticated = ValidateDigest(userName, nonce, cnonce, nck, authPrefix, response); + + } + while (false); + + } + + } + + // Indicate that authentication is required + + internal void Challenge(string scheme, string realm, string domain, string nonce, + string opaque, string stale, string alg, + string qop, string auth) + { + + sbuilder.Length = 0; + + if (scheme == null || scheme == Rest.AS_DIGEST) + { + + sbuilder.Append(Rest.AS_DIGEST); + sbuilder.Append(" "); + + if (realm != null) + { + sbuilder.Append("realm="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(realm); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + if (nonce != null) + { + sbuilder.Append("nonce="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(nonce); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + if (opaque != null) + { + sbuilder.Append("opaque="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(opaque); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + if (stale != null) + { + sbuilder.Append("stale="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(stale); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + if (alg != null) + { + sbuilder.Append("algorithm="); + sbuilder.Append(alg); + sbuilder.Append(Rest.CS_COMMA); + } + + if (qop != String.Empty) + { + sbuilder.Append("qop="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(qop); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + if (auth != null) + { + sbuilder.Append(auth); + 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); + } + + if (sbuilder[sbuilder.Length-1] == Rest.C_COMMA) + { + sbuilder.Length = sbuilder.Length-1; + } + + AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); + + } + + 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()); + } + + } + + private bool Validate(string user, string pass) + { + Rest.Log.DebugFormat("{0} Validating {1}:{2}", MsgId, user, pass); + return user == "awebb" && pass == getPassword(user); + } + + private string getPassword(string user) + { + return Rest.GodKey; + } + + // Validate the request-digest + private bool ValidateDigest(string user, string nonce, string cnonce, string nck, string uri, string response) + { + + string patt = null; + string payl = String.Empty; + string KDS = null; + string HA1 = null; + string HA2 = null; + string pass = getPassword(user); + + // Generate H(A1) + + if (algorithm == Rest.Digest_MD5Sess) + { + if (!sktable.ContainsKey(cnonce)) + { + patt = String.Format("{0}:{1}:{2}:{3}:{4}", user, realm, pass, nonce, cnonce); + HA1 = HashToString(patt); + sktable.Add(cnonce, HA1); + } + else + { + HA1 = sktable[cnonce]; + } + } + else + { + patt = String.Format("{0}:{1}:{2}", user, realm, pass); + HA1 = HashToString(patt); + } + + // Generate H(A2) + + if (qop == "auth-int") + { + patt = String.Format("{0}:{1}:{2}", request.HttpMethod, uri, HashToString(payl)); + } + else + { + patt = String.Format("{0}:{1}", request.HttpMethod, uri); + } + + HA2 = HashToString(patt); + + // Generate Digest + + if (qop != String.Empty) + { + patt = String.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, nonce, nck, cnonce, qop, HA2); + } + else + { + patt = String.Format("{0}:{1}:{2}", HA1, nonce, HA2); + } + + KDS = HashToString(patt); + + // Compare the generated sequence with the original + + return (0 == sc.Compare(KDS, response)); + + } + + private string HashToString(string pattern) + { + + Rest.Log.DebugFormat("{0} Generate <{1}>", MsgId, pattern); + + byte[] hash = md5hash.ComputeHash(encoding.GetBytes(pattern)); + + sbuilder.Length = 0; + + for (int i = 0; i < hash.Length; i++) + { + sbuilder.Append(hash[i].ToString("x2")); + } + + Rest.Log.DebugFormat("{0} Hash = <{1}>", MsgId, sbuilder.ToString()); + + return sbuilder.ToString(); + + } + + internal void Complete() + { + statusCode = Rest.HttpStatusCodeOK; + statusDescription = Rest.HttpStatusDescOK; + } + + internal void Redirect(string Url, bool temp) + { + + redirectLocation = Url; + + if (temp) + { + statusCode = Rest.HttpStatusCodeTemporaryRedirect; + statusDescription = Rest.HttpStatusDescTemporaryRedirect; + } + else + { + statusCode = Rest.HttpStatusCodePermanentRedirect; + statusDescription = Rest.HttpStatusDescPermanentRedirect; + } + + Fail(statusCode, statusDescription, true); + + } + + // Fail for an arbitrary reason. Just a failure with + // headers. + + internal void Fail(int code, string message) + { + Fail(code, message, true); + } + + // More adventurous. This failure also includes a + // specified entity. + + internal void Fail(int code, string message, string data) + { + buffer = null; + body = data; + Fail(code, message, false); + } + + internal void Fail(int code, string message, bool reset) + { + + statusCode = code; + statusDescription = message; + + if (reset) + { + buffer = null; + body = null; + } + + if (Rest.DEBUG) + { + Rest.Log.DebugFormat("{0} Scheme = {1}", MsgId, scheme); + Rest.Log.DebugFormat("{0} Realm = {1}", MsgId, realm); + Rest.Log.DebugFormat("{0} Domain = {1}", MsgId, domain); + Rest.Log.DebugFormat("{0} Nonce = {1}", MsgId, nonce); + Rest.Log.DebugFormat("{0} CNonce = {1}", MsgId, cnonce); + Rest.Log.DebugFormat("{0} Opaque = {1}", MsgId, opaque); + Rest.Log.DebugFormat("{0} Stale = {1}", MsgId, stale); + Rest.Log.DebugFormat("{0} Algorithm = {1}", MsgId, algorithm); + Rest.Log.DebugFormat("{0} QOP = {1}", MsgId, qop); + Rest.Log.DebugFormat("{0} AuthPrefix = {1}", MsgId, authPrefix); + Rest.Log.DebugFormat("{0} UserName = {1}", MsgId, userName); + Rest.Log.DebugFormat("{0} UserPass = {1}", MsgId, userPass); + } + + fail = true; + + Respond("Failure response"); + + RestException re = new RestException(message+" <"+code+">"); + + re.statusCode = code; + re.statusDesc = message; + re.httpmethod = method; + re.httppath = path; + + throw re; + + } + + // Reject this request + + internal void Reject() + { + Fail(Rest.HttpStatusCodeNotImplemented, Rest.HttpStatusDescNotImplemented); + } + + // This MUST be called by an agent handler before it returns + // control to Handle, otherwise the request will be ignored. + // This is called implciitly for the REST stream handlers and + // is harmless if it is called twice. + + internal virtual bool Respond(string reason) + { + + Rest.Log.DebugFormat("{0} Respond ENTRY, handled = {1}, reason = {2}", MsgId, handled, reason); + + if (!handled) + { + + Rest.Log.DebugFormat("{0} Generating Response", MsgId); + + // Process any arbitrary headers collected + + BuildHeaders(); + + // A Head request can NOT have a body! + if (method != Rest.HEAD) + { + + Rest.Log.DebugFormat("{0} Response is not abbreviated", MsgId); + + if (writer != null) + { + Rest.Log.DebugFormat("{0} XML Response handler extension ENTRY", MsgId); + Rest.Log.DebugFormat("{0} XML Response exists", MsgId); + writer.Flush(); + writer.Close(); + if (!fail) + { + buffer = xmldata.ToArray(); + AddHeader("Content-Type","application/xml"); + } + xmldata.Close(); + Rest.Log.DebugFormat("{0} XML Response encoded", MsgId); + Rest.Log.DebugFormat("{0} XML Response handler extension EXIT", MsgId); + } + + // If buffer != null, then we assume that + // this has already been done some other + // way. For example, transfer encoding might + // have been done. + + if (buffer == null) + { + if (body != null && body.Length > 0) + { + Rest.Log.DebugFormat("{0} String-based entity", MsgId); + buffer = encoding.GetBytes(body); + } + } + + 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; + } + + } + + // Set the status code & description. If nothing + // has been stored, we consider that a success + + if (statusCode == 0) + { + Complete(); + } + + response.StatusCode = statusCode; + + if (response.StatusCode == (int)OSHttpStatusCode.RedirectMovedTemporarily || + response.StatusCode == (int)OSHttpStatusCode.RedirectMovedPermanently) + { + response.RedirectLocation = redirectLocation; + } + + if (statusDescription != null) + { + response.StatusDescription = statusDescription; + } + + // Finally we send back our response, consuming + // any exceptions that doing so might produce. + + // We've left the setting of handled' until the + // last minute because the header settings included + // above are pretty harmless. But everything from + // here on down probably leaves the response + // element unusable by anyone else. + + handled = true; + + if (buffer != null && buffer.Length != 0) + { + Rest.Log.DebugFormat("{0} Entity buffer, length = {1} : <{2}>", + MsgId, buffer.Length, encoding.GetString(buffer)); + response.OutputStream.Write(buffer, 0, buffer.Length); + } + + response.OutputStream.Close(); + + if (request.InputStream != null) + { + request.InputStream.Close(); + } + + } + + Rest.Log.DebugFormat("{0} Respond EXIT, handled = {1}, reason = {2}", MsgId, handled, reason); + + return handled; + + } + + // Add a header to the table. If the header + // already exists, it is replaced. + + internal void AddHeader(string hdr, string data) + { + + if (headers == null) + { + headers = new NameValueCollection(); + } + + headers[hdr] = data; + + } + + // Keep explicit track of any headers which + // are to be removed. + + internal void RemoveHeader(string hdr) + { + + if (removed_headers == null) + { + removed_headers = new List(); + } + + removed_headers.Add(hdr); + + if (headers != null) + { + 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. + + private void BuildHeaders() + { + 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) + { + for (int i = 0; i < headers.Count; i++) + { + Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>", + MsgId, headers.GetKey(i), headers.Get(i)); + response.Headers.Add(headers.GetKey(i), headers.Get(i)); + } + } + } + + /// + /// Helper methods for deconstructing and reconstructing + /// URI path data. + /// + + private void initUrl() + { + + uri = request.Url; + + if (query == null) + { + query = uri.Query; + } + + // If the path has not been previously initialized, + // do so now. + + if (path == null) + { + 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 + // additional pre-processing required. + + if (path != null) + { + if (Rest.ExtendedEscape) + { + // Handle "+". Not a standard substitution, but + // common enough... + path = path.Replace(Rest.C_PLUS,Rest.C_SPACE); + } + pathNodes = path.Split(Rest.CA_PATHSEP); + } + else + { + pathNodes = EmptyPath; + } + + // Request server context info + + hostname = uri.Host; + port = uri.Port; + + } + + internal int initParameters(int prfxlen) + { + + if (prfxlen < path.Length-1) + { + parameters = path.Substring(prfxlen+1).Split(Rest.CA_PATHSEP); + } + else + { + parameters = new string[0]; + } + + // Generate a debug list of the decoded parameters + + if (Rest.DEBUG && prfxlen < path.Length-1) + { + Rest.Log.DebugFormat("{0} URI: Parameters: {1}", MsgId, path.Substring(prfxlen)); + for (int i = 0; i < parameters.Length; i++) + { + Rest.Log.DebugFormat("{0} Parameter[{1}]: {2}", MsgId, i, parameters[i]); + } + } + + return parameters.Length; + + } + + internal string[] PathNodes + { + get + { + if (pathNodes == null) + { + initUrl(); + } + return pathNodes; + } + } + + internal string BuildUrl(int first, int last) + { + + if (pathNodes == null) + { + initUrl(); + } + + if (first < 0) + { + first = first + pathNodes.Length; + } + + if (last < 0) + { + last = last + pathNodes.Length; + if (last < 0) + { + return Rest.UrlPathSeparator; + } + } + + sbuilder.Length = 0; + sbuilder.Append(Rest.UrlPathSeparator); + + if (first <= last) + { + for (int i = first; i <= last; i++) + { + sbuilder.Append(pathNodes[i]); + sbuilder.Append(Rest.UrlPathSeparator); + } + } + else + { + for (int i = last; i >= first; i--) + { + sbuilder.Append(pathNodes[i]); + sbuilder.Append(Rest.UrlPathSeparator); + } + } + + return sbuilder.ToString(); + + } + + // Setup the XML writer for output + + internal void initXmlWriter() + { + XmlWriterSettings settings = new XmlWriterSettings(); + xmldata = new MemoryStream(); + settings.Indent = true; + settings.IndentChars = " "; + settings.Encoding = encoding; + settings.CloseOutput = false; + settings.OmitXmlDeclaration = true; + settings.ConformanceLevel = ConformanceLevel.Fragment; + writer = XmlWriter.Create(xmldata, settings); + } + + 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() + { + byte[] dbuffer = new byte[8192]; + while (request.InputStream.Read(dbuffer,0,dbuffer.Length) != 0); + return; + } + + // This allows us to make errors a bit more apparent in REST + + internal void SendHtml(string text) + { + SendHtml("OpenSim REST Interface 1.0", text); + } + + internal void SendHtml(string title, string text) + { + + AddHeader(Rest.HttpHeaderContentType, "text/html"); + sbuilder.Length = 0; + + sbuilder.Append(""); + sbuilder.Append(""); + sbuilder.Append(""); + sbuilder.Append(title); + sbuilder.Append(""); + sbuilder.Append(""); + + sbuilder.Append(""); + sbuilder.Append("
"); + sbuilder.Append("

"); + sbuilder.Append(text); + sbuilder.Append("

"); + sbuilder.Append(""); + sbuilder.Append(""); + + html = sbuilder.ToString(); + + } + } +} diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs new file mode 100644 index 0000000000..e88c54d3d7 --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs @@ -0,0 +1,479 @@ +/* + * 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 System.Text; +using OpenSim.Framework; +using OpenSim.Framework.Servers; +using OpenSim.Framework.Communications; +using OpenSim.Framework.Communications.Cache; +using Nini.Config; + +namespace OpenSim.ApplicationPlugins.Rest.Inventory +{ + + public class Rest + { + + internal static readonly log4net.ILog Log = + log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + internal static bool DEBUG = Log.IsDebugEnabled; + + /// + /// These values have a single value for the whole + /// domain and lifetime of the plugin handler. We + /// make them static for ease of reference within + /// the assembly. These are initialized by the + /// RestHandler class during start-up. + /// + + internal static RestHandler Plugin = null; + internal static OpenSimBase main = null; + internal static CommunicationsManager Comms = null; + internal static IInventoryServices InventoryServices = null; + internal static IUserService UserServices = null; + internal static AssetCache AssetServices = null; + internal static string Prefix = null; + internal static IConfig Config = null; + internal static string GodKey = null; + internal static bool Authenticate = true; + internal static bool Secure = true; + 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 + + internal static string MsgId + { + get { return Plugin.MsgId; } + } + + internal static string RequestId + { + get { return Plugin.RequestId; } + } + + internal static Encoding Encoding = Encoding.UTF8; + + /// + /// Version control for REST implementation. This + /// refers to the overall infrastructure represented + /// by the following classes + /// RequestData + /// RequestInventoryPlugin + /// Rest + /// It does no describe implementation classes such as + /// RestInventoryServices, which may morph much more + /// often. Such classes ARE dependent upon this however + /// and should check it in their Initialize method. + /// + + public static readonly float Version = 1.0F; + public const string Name = "REST 1.0"; + + /// + /// Currently defined HTTP methods. + /// Only GET and HEAD are required to be + /// supported by all servers. See Respond + /// to see how these are handled. + /// + + // REST AGENT 1.0 interpretations + public const string GET = "get"; // information retrieval - server state unchanged + public const string HEAD = "head"; // same as get except only the headers are returned. + public const string POST = "post"; // Replace the URI designated resource with the entity. + public const string PUT = "put"; // Add the entity to the context represented by the URI + public const string DELETE = "delete"; // Remove the URI designated resource from the server. + + public const string OPTIONS = "options"; // + public const string TRACE = "trace"; // + public const string CONNECT = "connect"; // + + // Define this in one place... + + public const string UrlPathSeparator = "/"; + public const string UrlMethodSeparator = ":"; + + // Redirection qualifications + + public const bool PERMANENT = false; + public const bool TEMPORARY = true; + + // Constant arrays used by String.Split + + public static readonly char C_SPACE = ' '; + public static readonly char C_SLASH = '/'; + public static readonly char C_PATHSEP = '/'; + public static readonly char C_COLON = ':'; + public static readonly char C_PLUS = '+'; + public static readonly char C_PERIOD = '.'; + public static readonly char C_COMMA = ','; + public static readonly char C_DQUOTE = '"'; + + public static readonly string CS_SPACE = " "; + public static readonly string CS_SLASH = "/"; + public static readonly string CS_PATHSEP = "/"; + public static readonly string CS_COLON = ":"; + public static readonly string CS_PLUS = "+"; + public static readonly string CS_PERIOD = "."; + public static readonly string CS_COMMA = ","; + public static readonly string CS_DQUOTE = "\""; + + public static readonly char[] CA_SPACE = { C_SPACE }; + public static readonly char[] CA_SLASH = { C_SLASH }; + public static readonly char[] CA_PATHSEP = { C_PATHSEP }; + public static readonly char[] CA_COLON = { C_COLON }; + public static readonly char[] CA_PERIOD = { C_PERIOD }; + public static readonly char[] CA_PLUS = { C_PLUS }; + public static readonly char[] CA_COMMA = { C_COMMA }; + public static readonly char[] CA_DQUOTE = { C_DQUOTE }; + + // HTTP Code Values (in value order) + + public const int HttpStatusCodeContinue = 100; + public const int HttpStatusCodeSwitchingProtocols = 101; + + public const int HttpStatusCodeOK = 200; + public const int HttpStatusCodeCreated = 201; + public const int HttpStatusCodeAccepted = 202; + public const int HttpStatusCodeNonAuthoritative = 203; + public const int HttpStatusCodeNoContent = 204; + public const int HttpStatusCodeResetContent = 205; + public const int HttpStatusCodePartialContent = 206; + + public const int HttpStatusCodeMultipleChoices = 300; + public const int HttpStatusCodePermanentRedirect = 301; + public const int HttpStatusCodeFound = 302; + public const int HttpStatusCodeSeeOther = 303; + public const int HttpStatusCodeNotModified = 304; + public const int HttpStatusCodeUseProxy = 305; + public const int HttpStatusCodeReserved306 = 306; + public const int HttpStatusCodeTemporaryRedirect = 307; + + public const int HttpStatusCodeBadRequest = 400; + public const int HttpStatusCodeNotAuthorized = 401; + public const int HttpStatusCodePaymentRequired = 402; + public const int HttpStatusCodeForbidden = 403; + public const int HttpStatusCodeNotFound = 404; + public const int HttpStatusCodeMethodNotAllowed = 405; + public const int HttpStatusCodeNotAcceptable = 406; + public const int HttpStatusCodeProxyAuthenticate = 407; + public const int HttpStatusCodeTimeOut = 408; + public const int HttpStatusCodeConflict = 409; + public const int HttpStatusCodeGone = 410; + public const int HttpStatusCodeLengthRequired = 411; + public const int HttpStatusCodePreconditionFailed = 412; + public const int HttpStatusCodeEntityTooLarge = 413; + public const int HttpStatusCodeUriTooLarge = 414; + public const int HttpStatusCodeUnsupportedMedia = 415; + public const int HttpStatusCodeRangeNotSatsified = 416; + public const int HttpStatusCodeExpectationFailed = 417; + + public const int HttpStatusCodeServerError = 500; + public const int HttpStatusCodeNotImplemented = 501; + public const int HttpStatusCodeBadGateway = 502; + public const int HttpStatusCodeServiceUnavailable = 503; + public const int HttpStatusCodeGatewayTimeout = 504; + public const int HttpStatusCodeHttpVersionError = 505; + + // HTTP Status Descriptions (in status code order) + + public const string HttpStatusDescContinue = "Continue Request"; // 100 + public const string HttpStatusDescSwitchingProtocols = "Switching Protocols"; // 101 + + public const string HttpStatusDescOK = "OK"; + public const string HttpStatusDescCreated = "CREATED"; + public const string HttpStatusDescAccepted = "ACCEPTED"; + public const string HttpStatusDescNonAuthoritative = "NON-AUTHORITATIVE INFORMATION"; + public const string HttpStatusDescNoContent = "NO CONTENT"; + public const string HttpStatusDescResetContent = "RESET CONTENT"; + public const string HttpStatusDescPartialContent = "PARTIAL CONTENT"; + + public const string HttpStatusDescMultipleChoices = "MULTIPLE CHOICES"; + public const string HttpStatusDescPermanentRedirect = "PERMANENT REDIRECT"; + public const string HttpStatusDescFound = "FOUND"; + public const string HttpStatusDescSeeOther = "SEE OTHER"; + public const string HttpStatusDescNotModified = "NOT MODIFIED"; + public const string HttpStatusDescUseProxy = "USE PROXY"; + public const string HttpStatusDescReserved306 = "RESERVED CODE 306"; + public const string HttpStatusDescTemporaryRedirect = "TEMPORARY REDIRECT"; + + public const string HttpStatusDescBadRequest = "BAD REQUEST"; + public const string HttpStatusDescNotAuthorized = "NOT AUTHORIZED"; + public const string HttpStatusDescPaymentRequired = "PAYMENT REQUIRED"; + public const string HttpStatusDescForbidden = "FORBIDDEN"; + public const string HttpStatusDescNotFound = "NOT FOUND"; + public const string HttpStatusDescMethodNotAllowed = "METHOD NOT ALLOWED"; + public const string HttpStatusDescNotAcceptable = "NOT ACCEPTABLE"; + public const string HttpStatusDescProxyAuthenticate = "PROXY AUTHENTICATION REQUIRED"; + public const string HttpStatusDescTimeOut = "TIMEOUT"; + public const string HttpStatusDescConflict = "CONFLICT"; + public const string HttpStatusDescGone = "GONE"; + public const string HttpStatusDescLengthRequired = "LENGTH REQUIRED"; + public const string HttpStatusDescPreconditionFailed = "PRECONDITION FAILED"; + public const string HttpStatusDescEntityTooLarge = "ENTITY TOO LARGE"; + public const string HttpStatusDescUriTooLarge = "URI TOO LARGE"; + public const string HttpStatusDescUnsupportedMedia = "UNSUPPORTED MEDIA"; + public const string HttpStatusDescRangeNotSatisfied = "RANGE NOT SATISFIED"; + public const string HttpStatusDescExpectationFailed = "EXPECTATION FAILED"; + + public const string HttpStatusDescServerError = "SERVER ERROR"; + public const string HttpStatusDescNotImplemented = "NOT IMPLEMENTED"; + public const string HttpStatusDescBadGateway = "BAD GATEWAY"; + public const string HttpStatusDescServiceUnavailable = "SERVICE UNAVAILABLE"; + public const string HttpStatusDescGatewayTimeout = "GATEWAY TIMEOUT"; + public const string HttpStatusDescHttpVersionError = "HTTP VERSION NOT SUPPORTED"; + + // HTTP Headers + + public const string HttpHeaderAccept = "Accept"; + public const string HttpHeaderAcceptCharset = "Accept-Charset"; + public const string HttpHeaderAcceptEncoding = "Accept-Encoding"; + public const string HttpHeaderAcceptLanguage = "Accept-Language"; + public const string HttpHeaderAcceptRanges = "Accept-Ranges"; + public const string HttpHeaderAge = "Age"; + public const string HttpHeaderAllow = "Allow"; + public const string HttpHeaderAuthorization = "Authorization"; + public const string HttpHeaderCacheControl = "Cache-Control"; + public const string HttpHeaderConnection = "Connection"; + public const string HttpHeaderContentEncoding = "Content-Encoding"; + public const string HttpHeaderContentLanguage = "Content-Language"; + public const string HttpHeaderContentLength = "Content-Length"; + public const string HttpHeaderContentLocation = "Content-Location"; + public const string HttpHeaderContentMD5 = "Content-MD5"; + public const string HttpHeaderContentRange = "Content-Range"; + public const string HttpHeaderContentType = "Content-Type"; + public const string HttpHeaderDate = "Date"; + public const string HttpHeaderETag = "ETag"; + public const string HttpHeaderExpect = "Expect"; + public const string HttpHeaderExpires = "Expires"; + public const string HttpHeaderFrom = "From"; + public const string HttpHeaderHost = "Host"; + public const string HttpHeaderIfMatch = "If-Match"; + public const string HttpHeaderIfModifiedSince = "If-Modified-Since"; + public const string HttpHeaderIfNoneMatch = "If-None-Match"; + public const string HttpHeaderIfRange = "If-Range"; + public const string HttpHeaderIfUnmodifiedSince = "If-Unmodified-Since"; + public const string HttpHeaderLastModified = "Last-Modified"; + public const string HttpHeaderLocation = "Location"; + public const string HttpHeaderMaxForwards = "Max-Forwards"; + public const string HttpHeaderPragma = "Pragma"; + public const string HttpHeaderProxyAuthenticate = "Proxy-Authenticate"; + public const string HttpHeaderProxyAuthorization = "Proxy-Authorization"; + public const string HttpHeaderRange = "Range"; + public const string HttpHeaderReferer = "Referer"; + public const string HttpHeaderRetryAfter = "Retry-After"; + public const string HttpHeaderServer = "Server"; + public const string HttpHeaderTE = "TE"; + public const string HttpHeaderTrailer = "Trailer"; + public const string HttpHeaderTransferEncoding = "Transfer-Encoding"; + public const string HttpHeaderUpgrade = "Upgrade"; + public const string HttpHeaderUserAgent = "User-Agent"; + public const string HttpHeaderVary = "Vary"; + public const string HttpHeaderVia = "Via"; + public const string HttpHeaderWarning = "Warning"; + public const string HttpHeaderWWWAuthenticate = "WWW-Authenticate"; + + /// + /// Supported authentication schemes + /// + + public const string AS_BASIC = "Basic"; + public const string AS_DIGEST = "Digest"; + + /// Supported Digest algorithms + + public const string Digest_MD5 = "MD5"; // assumedd efault if omitted + public const string Digest_MD5Sess = "MD5-sess"; + + public const string Qop_Auth = "auth"; + public const string Qop_Int = "auth-int"; + + /// Utility routines + + public static string StringToBase64(string str) + { + try + { + byte[] encData_byte = new byte[str.Length]; + encData_byte = Encoding.UTF8.GetBytes(str); + return Convert.ToBase64String(encData_byte); + } + catch + { + return String.Empty; + } + } + + public static string Base64ToString(string str) + { + UTF8Encoding encoder = new UTF8Encoding(); + Decoder utf8Decode = encoder.GetDecoder(); + try + { + byte[] todecode_byte = Convert.FromBase64String(str); + int charCount = utf8Decode.GetCharCount(todecode_byte, 0, todecode_byte.Length); + char[] decoded_char = new char[charCount]; + utf8Decode.GetChars(todecode_byte, 0, todecode_byte.Length, decoded_char, 0); + return new String(decoded_char); + } + catch + { + return String.Empty; + } + } + + private const string hvals = "0123456789abcdef"; + + public static int Hex2Int(string hex) + { + int val = 0; + int sum = 0; + string tmp = null; + + if (hex != null) + { + tmp = hex.ToLower(); + for (int i = 0; i < tmp.Length; i++) + { + val = hvals.IndexOf(tmp[i]); + if (val == -1) + break; + sum *= 16; + sum += val; + } + } + + return sum; + + } + + public static string Int2Hex8(int val) + { + string res = String.Empty; + for (int i = 0; i < 8; i++) + { + res = (val % 16) + res; + val = val / 16; + } + return res; + } + + public static string ToHex32(int val) + { + return String.Empty; + } + + public static string ToHex32(string val) + { + return String.Empty; + } + + // Nonce management + + public static string NonceGenerator() + { + return StringToBase64(Guid.NewGuid().ToString()); + } + + // Dump he specified data stream; + + public static void Dump(byte[] data) + { + + char[] buffer = new char[Rest.DumpLineSize]; + int cc = 0; + + for (int i = 0; i < data.Length; i++) + { + + if (i % Rest.DumpLineSize == 0) Console.Write("\n{0}: ",i.ToString("d8")); + + if (i % 4 == 0) Console.Write(" "); +// if (i%16 == 0) Console.Write(" "); + + Console.Write("{0}",data[i].ToString("x2")); + + if (data[i] < 127 && data[i] > 31) + buffer[i % Rest.DumpLineSize] = (char) data[i]; + else + buffer[i % Rest.DumpLineSize] = '.'; + + cc++; + + if (i != 0 && (i + 1) % Rest.DumpLineSize == 0) + { + Console.Write(" |"+(new String(buffer))+"|"); + cc = 0; + } + + } + + // Finish off any incomplete line + + if (cc != 0) + { + for (int i = cc ; i < Rest.DumpLineSize; i++) + { + if (i % 4 == 0) Console.Write(" "); + // if (i%16 == 0) Console.Write(" "); + Console.Write(" "); + buffer[i % Rest.DumpLineSize] = ' '; + } + Console.WriteLine(" |"+(new String(buffer))+"|"); + } + else + { + Console.Write("\n"); + } + + } + + } + + // Local exception type + + public class RestException : Exception + { + + internal int statusCode; + internal string statusDesc; + internal string httpmethod; + internal string httppath; + + public RestException(string msg) : base(msg) + { + } + } + +} diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs new file mode 100644 index 0000000000..839e0f21ac --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs @@ -0,0 +1,257 @@ +/* + * 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 libsecondlife; +using Nini.Config; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Xml; +using OpenSim.Framework; +using OpenSim.Framework.Servers; +using OpenSim.Framework.Communications; +using OpenSim.Framework.Communications.Cache; + +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) + { + + Rest.Log.InfoFormat("{0} Asset services initializing", MsgId); + Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version); + + // Integrate domain + + 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 + + Rest.Plugin.AddPathHandler(DoAsset, qPrefix, Allocate); + + // Activate + + enabled = true; + + Rest.Log.InfoFormat("{0} Asset services initialization complete", MsgId); + + } + + // 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 + + public void Close() + { + enabled = false; + Rest.Log.InfoFormat("{0} Asset services closing down", MsgId); + } + + // Properties + + internal string MsgId + { + get { return Rest.MsgId; } + } + + #region Interface + + private RequestData Allocate(OSHttpRequest request, OSHttpResponse response) + { + return (RequestData) new AssetRequestData(request, response, qPrefix); + } + + // Asset Handler + + private void DoAsset(RequestData rparm) + { + + if (!enabled) return; + + AssetRequestData rdata = (AssetRequestData) rparm; + + Rest.Log.DebugFormat("{0} REST Asset handler ENTRY", MsgId); + + // Now that we know this is a serious attempt to + // access inventory data, we should find out who + // is asking, and make sure they are authorized + // to do so. We need to validate the caller's + // identity before revealing anything about the + // status quo. Authenticate throws an exception + // via Fail if no identity information is present. + // + // With the present HTTP server we can't use the + // builtin authentication mechanisms because they + // would be enforced for all in-bound requests. + // Instead we look at the headers ourselves and + // handle authentication directly. + + try + { + if (!rdata.IsAuthenticated) + { + rdata.Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); + } + } + catch (RestException e) + { + if (e.statusCode == Rest.HttpStatusCodeNotAuthorized) + { + Rest.Log.WarnFormat("{0} User not authenticated", MsgId); + Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, + rdata.request.Headers.Get("Authorization")); + } + else + { + Rest.Log.ErrorFormat("{0} User authentication failed", MsgId); + Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, + rdata.request.Headers.Get("Authorization")); + } + throw (e); + } + + // Remove the prefix and what's left are the parameters. If we don't have + // the parameters we need, fail the request. Parameters do NOT include + // any supplied query values. + + if (rdata.parameters.Length > 0) + { + switch (rdata.method) + { + case "get" : + DoGet(rdata); + break; + case "put" : + case "post" : + case "delete" : + default : + Rest.Log.WarnFormat("{0} Asset: Method not supported: {1}", + MsgId, rdata.method); + rdata.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + break; + } + } + else + { + Rest.Log.WarnFormat("{0} Asset: No agent information provided", MsgId); + rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); + } + + Rest.Log.DebugFormat("{0} REST Asset handler EXIT", MsgId); + + } + + #endregion Interface + + private void DoGet(AssetRequestData rdata) + { + + bool istexture = false; + + Rest.Log.DebugFormat("{0} REST Asset handler, Method = <{1}> ENTRY", MsgId, rdata.method); + + // The only parameter we accept is an LLUUID for + // the asset + + if (rdata.parameters.Length == 1) + { + + LLUUID uuid = new LLUUID(rdata.parameters[0]); + AssetBase asset = Rest.AssetServices.GetAsset(uuid, istexture); + + if (asset != null) + { + + Rest.Log.DebugFormat("{0} Asset located <{1}>", MsgId, rdata.parameters[0]); + + rdata.initXmlWriter(); + + rdata.writer.WriteStartElement(String.Empty,"Asset",String.Empty); + + rdata.writer.WriteAttributeString("id", asset.ID.ToString()); + rdata.writer.WriteAttributeString("name", asset.Name); + rdata.writer.WriteAttributeString("desc", asset.Description); + rdata.writer.WriteAttributeString("type", asset.Type.ToString()); + rdata.writer.WriteAttributeString("invtype", asset.InvType.ToString()); + rdata.writer.WriteAttributeString("local", asset.Local.ToString()); + rdata.writer.WriteAttributeString("temporary", asset.Temporary.ToString()); + + rdata.writer.WriteBase64(asset.Data,0,asset.Data.Length); + + rdata.writer.WriteFullEndElement(); + + } + else + { + Rest.Log.DebugFormat("{0} Invalid parameters: <{1}>", MsgId, rdata.path); + rdata.Fail(Rest.HttpStatusCodeNotFound, + Rest.HttpStatusDescNotFound); + } + } + + rdata.Complete(); + rdata.Respond("Asset " + rdata.method + ": Normal completion"); + + } + + internal class AssetRequestData : RequestData + { + internal AssetRequestData(OSHttpRequest request, OSHttpResponse response, string prefix) + : base(request, response, prefix) + { + } + } + + } +} diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs new file mode 100644 index 0000000000..0a0bf3f44e --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs @@ -0,0 +1,547 @@ +/* + * 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; +using Mono.Addins; + +[assembly : Addin] +[assembly : AddinDependency("OpenSim", "0.5")] + +namespace OpenSim.ApplicationPlugins.Rest.Inventory +{ + + [Extension("/OpenSim/Startup")] + + 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); + + } + } +} diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs new file mode 100644 index 0000000000..0fc10f9a5b --- /dev/null +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs @@ -0,0 +1,1993 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSim Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Xml; +using OpenSim.Framework; +using OpenSim.Framework.Servers; +using OpenSim.Framework.Communications; +using OpenSim.Framework.Communications.Cache; +using libsecondlife; +using Nini.Config; + +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. + + public RestInventoryServices(RestHandler p_rest) + { + + 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 (!qPrefix.StartsWith(Rest.UrlPathSeparator)) + { + qPrefix = Rest.Prefix + Rest.UrlPathSeparator + qPrefix; + } + + // Authentication domain + + Rest.Domains.Add(key, Rest.Config.GetString("inventory-domain",qPrefix)); + + // Register interface + + Rest.Plugin.AddPathHandler(DoInventory,qPrefix,Allocate); + + // Activate + + enabled = true; + + Rest.Log.InfoFormat("{0} Inventory services initialization complete", MsgId); + + } + + // 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 + + public void Close() + { + enabled = false; + Rest.Log.InfoFormat("{0} Inventory services closing down", MsgId); + } + + // Convenient properties + + internal string MsgId + { + get { return Rest.MsgId; } + } + + #region Interface + + 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. + /// It handles all aspects of inventory REST processing. + /// + + private void DoInventory(RequestData hdata) + { + + InventoryRequestData rdata = (InventoryRequestData) hdata; + + Rest.Log.DebugFormat("{0} DoInventory ENTRY", MsgId); + + // We're disabled + if (!enabled) + { + return; + } + + // Now that we know this is a serious attempt to + // access inventory data, we should find out who + // is asking, and make sure they are authorized + // to do so. We need to validate the caller's + // identity before revealing anything about the + // status quo. Authenticate throws an exception + // via Fail if no identity information is present. + // + // With the present HTTP server we can't use the + // builtin authentication mechanisms because they + // would be enforced for all in-bound requests. + // Instead we look at the headers ourselves and + // handle authentication directly. + + try + { + if (!rdata.IsAuthenticated) + { + rdata.Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); + } + } + catch (RestException e) + { + if (e.statusCode == Rest.HttpStatusCodeNotAuthorized) + { + Rest.Log.WarnFormat("{0} User not authenticated", MsgId); + Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization")); + } + else + { + Rest.Log.ErrorFormat("{0} User authentication failed", MsgId); + Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization")); + } + throw (e); + } + + 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. + + // 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); + } + + // The next parameter MUST be the agent identification, either an LLUUID + // or a space-separated First-name Last-Name specification. + + try + { + rdata.uuid = new LLUUID(rdata.parameters[0]); + Rest.Log.DebugFormat("{0} LLUUID supplied", MsgId); + rdata.userProfile = Rest.UserServices.GetUserProfile(rdata.uuid); + } + catch + { + string[] names = rdata.parameters[0].Split(Rest.CA_SPACE); + if (names.Length == 2) + { + Rest.Log.DebugFormat("{0} Agent Name supplied [2]", MsgId); + rdata.userProfile = Rest.UserServices.GetUserProfile(names[0],names[1]); + } + else + { + Rest.Log.DebugFormat("{0} A Valid UUID or both first and last names must be specified", MsgId); + rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); + } + } + + if (rdata.userProfile != null) + { + Rest.Log.DebugFormat("{0} Profile obtained for agent {1} {2}", + MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); + } + else + { + Rest.Log.DebugFormat("{0} No profile for {1}", MsgId, rdata.path); + rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound); + } + + // If we get to here, then we have successfully obtained an inventory + // for the specified user. + + rdata.uuid = rdata.userProfile.ID; + + if (Rest.InventoryServices.HasInventoryForUser(rdata.uuid)) + { + + rdata.root = Rest.InventoryServices.RequestRootFolder(rdata.uuid); + + Rest.Log.DebugFormat("{0} Inventory Root retrieved for {1} {2}", + MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); + + Rest.InventoryServices.RequestInventoryForUser(rdata.uuid, rdata.GetUserInventory); + + Rest.Log.DebugFormat("{0} Inventory catalog requested for {1} {2}", + MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); + + lock(rdata) + { + if (!rdata.HaveInventory) + { + Monitor.Wait(rdata); + } + } + + if (rdata.root == null) + { + 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); + } + + } + else + { + 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); + } + + // If we get here, then we have successfully retrieved the user's information + // and inventory information is now available locally. + + switch (rdata.method) + { + + case Rest.HEAD : // Do the processing, set the status code, suppress entity + DoGet(rdata); + rdata.buffer = null; + break; + + case Rest.GET : // Do the processing, set the status code, return entity + DoGet(rdata); + break; + + case Rest.PUT : // Add new information + DoPut(rdata); + break; + + case Rest.POST : // Update (replace) + DoPost(rdata); + break; + + case Rest.DELETE : // Delete information + DoDelete(rdata); + break; + + default : + Rest.Log.DebugFormat("{0} Method {1} not supported for {2}", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed, + Rest.HttpStatusDescMethodNotAllowed); + break; + } + + } + + #endregion Interface + + #region method-specific processing + + /// + /// This method implements GET processing for inventory. + /// Any remaining parameters are used to locate the + /// corresponding subtree based upon node name. + /// + + private void DoGet(InventoryRequestData rdata) + { + + rdata.initXmlWriter(); + + rdata.writer.WriteStartElement(String.Empty,"Inventory",String.Empty); + + if (rdata.parameters.Length == 1) + { + formatInventory(rdata, rdata.root, String.Empty); + } + else + { + traverseInventory(rdata, rdata.root, 1); + } + + rdata.writer.WriteFullEndElement(); + + rdata.Complete(); + rdata.Respond("Inventory " + rdata.method + ": Normal completion"); + + } + + /// + /// In the case of the inventory, and probably much else + /// 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. + /// + /// 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. + /// + /// 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. + /// + + /// + /// PUT adds new information to the inventory at the + /// context identified by the URI. + /// + + private void DoPut(InventoryRequestData rdata) + { + + // Resolve the context node specified in the URI. Entity + // data will be ADDED beneath this node. + + Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); + + // Processing depends upon the type of inventory node + // identified in the URI. This is the CONTEXT for the + // change. We either got a context or we threw an + // exception. + + // It follows that we can only add information if the URI + // has identified a folder. So only folder is supported + // in this case. + + if (typeof(InventoryFolderBase) == InventoryNode.GetType() || + typeof(InventoryFolderImpl) == InventoryNode.GetType()) + { + + // Cast the context node appropriately. + + InventoryFolderBase context = (InventoryFolderBase) InventoryNode; + + 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. + + XmlInventoryCollection entity = ReconstituteEntity(rdata); + + // Inlined assest included in entity. If anything fails, + // return failure to requestor. + + if (entity.Assets.Count > 0) + { + + Rest.Log.DebugFormat("{0} Adding {1} assets to server", + MsgId, entity.Assets.Count); + + foreach (AssetBase asset in entity.Assets) + { + 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); + } + } + + } + + // Modify the context using the collection of folders and items + // returned in the XmlInventoryCollection. + + foreach (InventoryFolderBase folder in entity.Folders) + { + + InventoryFolderBase found = null; + + // 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 + // case we don't have to do it here. + + if (folder.ParentID == LLUUID.Zero) + { + folder.ParentID = context.ID; + } + + // Search the existing inventory for an existing entry. If + // we have once, 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 + // in which case an update is most certainly necessary. + + found = null; + + foreach (InventoryFolderBase xf in rdata.folders) + { + // Compare identifying attribute + if (xf.ID == folder.ID) + { + found = xf; + } + } + + if (found != null && FolderHasChanged(folder,found)) + { + Rest.Log.DebugFormat("{0} Updating existing folder", MsgId); + Rest.InventoryServices.MoveFolder(folder); + } + else + { + Rest.Log.DebugFormat("{0} Adding new folder", MsgId); + Rest.InventoryServices.AddFolder(folder); + } + + } + + // Now we repeat a similar process for the items included + // in the entity. + + foreach (InventoryItemBase item in entity.Items) + { + + InventoryItemBase found = null; + + // If the parentID is zero, then this is going + // directly into the root identified by the URI. + + if (item.Folder == LLUUID.Zero) + { + item.Folder = context.ID; + } + + // Determine whether this is a new item or a + // replacement definition. + + foreach (InventoryItemBase xi in rdata.items) + { + // Compare identifying attribute + if (xi.ID == item.ID) + { + found = xi; + } + } + + if (found != null && ItemHasChanged(item, found)) + { + Rest.Log.DebugFormat("{0} Updating item {1} {2} {3} {4} {5}", + MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name); + Rest.InventoryServices.UpdateItem(item); + } + else + { + Rest.Log.DebugFormat("{0} Adding item {1} {2} {3} {4} {5}", + MsgId, item.ID, item.AssetID, item.InvType, item.AssetType, item.Name); + Rest.InventoryServices.AddItem(item); + } + + } + + } + else + { + 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); + } + + rdata.Complete(); + rdata.Respond("Inventory " + rdata.method + ": Normal completion"); + + } + + /// + /// POST updates the URI-identified element in the inventory. This + /// is actually far more flexible than it might at first sound. For + /// POST the URI serves two purposes: + /// [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 + /// 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 + /// to an entity containing a URI relative specification, i.e. one or + /// more elements do not specify parent folder information. These + /// 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 + /// 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. + /// + + private void DoPost(InventoryRequestData rdata) + { + + int count = 0; + + // Resolve the inventory node that is to be modified. + + 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 + // subtree before doing anything else. Note that we + // etiher got a context or we threw an exception. + + XmlInventoryCollection entity = ReconstituteEntity(rdata); + + // Incorporate any inlined assets first + + if (entity.Assets.Count != 0) + { + foreach (AssetBase asset in entity.Assets) + { + // Asset was validated during the collection + // process + Rest.AssetServices.AddAsset(asset); + } + } + + /// + /// URI specifies a folder 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. + /// + + if (typeof(InventoryFolderBase) == InventoryNode.GetType() || + typeof(InventoryFolderImpl) == InventoryNode.GetType()) + { + + InventoryFolderBase uri = (InventoryFolderBase) InventoryNode; + InventoryFolderBase xml = null; + + // Scan the set of folders in the entity collection for an + // entry that macthes 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 + // ambiguity in this case because this is POST and we are + // supposed to be modifying a specific node. + // We assign any element IDs required as an economy; we don't + // want to iterate over the fodler set again if it can be + // helped. + + foreach (InventoryFolderBase folder in entity.Folders) + { + if (folder.ParentID == uri.ParentID || + folder.ParentID == LLUUID.Zero) + { + folder.ParentID = uri.ParentID; + xml = folder; + count++; + } + if (xml.ID == LLUUID.Zero) + { + xml.ID = LLUUID.Random(); + } + } + + // More than one entry is ambiguous + + if (count > 1) + { + Rest.Log.DebugFormat("{0} {1}: Request for <{2}> is ambiguous", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + } + + // Exactly one entry means we ARE replacing the node + // identified by the URI. So we delete the old folder + // by moving it to the trash and then purging it. + // We then add all of the folders and items we + // included in the entity. The subtree has been + // modified. + + if (count == 1) + { + + InventoryFolderBase TrashCan = GetTrashCan(rdata); + + uri.ParentID = TrashCan.ID; + Rest.InventoryServices.MoveFolder(uri); + Rest.InventoryServices.PurgeFolder(TrashCan); + + } + + // Now, regardelss of what they represent, we + // integrate all of the elements in the entity. + + foreach (InventoryFolderBase f in entity.Folders) + { + Rest.InventoryServices.MoveFolder(f); + } + + foreach (InventoryItemBase it in entity.Items) + { + Rest.InventoryServices.AddItem(it); + } + + } + + /// + /// URI specifies an item to be updated + /// + /// + /// The entity must contain a single item node to be + /// updated. ID and Folder ID must be correct. + /// + + else + { + + InventoryItemBase uri = (InventoryItemBase) InventoryNode; + InventoryItemBase xml = null; + + if (entity.Folders.Count != 0) + { + Rest.Log.DebugFormat("{0} {1}: Request should not contain any folders <{2}>", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + } + + if (entity.Items.Count > 1) + { + Rest.Log.DebugFormat("{0} {1}: Entity contains too many items <{2}>", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + } + + xml = entity.Items[0]; + + if (xml.ID == LLUUID.Zero) + { + xml.ID = LLUUID.Random(); + } + + // If the folder reference has changed, then this item is + // being moved. Otherwise we'll just delete the old, and + // add in the new. + + // Delete the old item + + Rest.InventoryServices.DeleteItem(uri); + + // Add the new item to the inventory + + Rest.InventoryServices.AddItem(xml); + + } + + rdata.Complete(); + rdata.Respond("Inventory " + rdata.method + ": Normal completion"); + + } + + /// + /// Arguably the most damaging REST interface. It deletes the inventory + /// item or folder identified by the URI. + /// + /// We only process if the URI identified node appears to exist + /// We do not test for success because we either get a context, + /// or an exception is thrown. + /// + /// Folders are deleted by moving them to another folder and then + /// purging that folder. We'll do that by creating a temporary + /// sub-folder in the TrashCan and purging that folder's + /// contents. If we can't can it, we don't delete it... + /// So, if no trashcan is available, the request does nothing. + /// Items are summarily deleted. + /// + /// In the interests of safety, a delete request should normally + /// be performed using UUID, as a name might identify several + /// elements. + /// + + private void DoDelete(InventoryRequestData rdata) + { + + Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); + + if (typeof(InventoryFolderBase) == InventoryNode.GetType() || + typeof(InventoryFolderImpl) == InventoryNode.GetType()) + { + + InventoryFolderBase TrashCan = GetTrashCan(rdata); + + InventoryFolderBase folder = (InventoryFolderBase) InventoryNode; + Rest.Log.DebugFormat("{0} {1}: Folder {2} will be deleted", + MsgId, rdata.method, rdata.path); + folder.ParentID = TrashCan.ID; + Rest.InventoryServices.MoveFolder(folder); + Rest.InventoryServices.PurgeFolder(TrashCan); + + } + + // Deleting items is much more straight forward. + + else + { + InventoryItemBase item = (InventoryItemBase) InventoryNode; + Rest.Log.DebugFormat("{0} {1}: Item {2} will be deleted", + MsgId, rdata.method, rdata.path); + Rest.InventoryServices.DeleteItem(item); + } + + rdata.Complete(); + rdata.Respond("Inventory " + rdata.method + ": Normal completion"); + + } + +#endregion method-specific processing + + /// + /// This method is called to obtain the OpenSim inventory object identified + /// by the supplied URI. This may be either an Item or a Folder, so a suitably + /// ambiguous return type is employed (Object). This method recurses as + /// necessary to process the designated hierarchy. + /// + /// If we reach the end of the URI then we return the contextural folder to + /// our caller. + /// + /// If we are not yet at the end of the URI we attempt to find a child folder + /// and if we succeed we recurse. + /// + /// If this is the last node, then we look to see if this is an item. If it is, + /// we return that item. + /// + /// Otherwise we fail the request on the ground of an invalid URI. + /// + /// + /// This mechanism cannot detect the case where duplicate subtrees satisfy a + /// request. In such a case the 1st element gets processed. If this is a + /// problem, then UUID should be used to identify the end-node. This is basic + /// premise of normal inventory processing. The name is an informational, and + /// not a defining, attribute. + /// + /// + /// + + private Object getInventoryNode(InventoryRequestData rdata, InventoryFolderBase folder, int pi) + { + + Rest.Log.DebugFormat("{0} Searching folder {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); + + // We have just run off the end of the parameter sequence + + if (pi >= rdata.parameters.Length) + { + return folder; + } + + // More names in the sequence, look for a folder that might + // get us there. + + if (rdata.folders != null) + foreach (InventoryFolderBase f in rdata.folders) + { + // Look for the present node in the directory list + if (f.ParentID == folder.ID && + (f.Name == rdata.parameters[pi] || + f.ID.ToString() == rdata.parameters[pi])) + { + return getInventoryNode(rdata, f, pi+1); + } + } + + // No folders that match. Perhaps this parameter identifies an item? If + // it does, then it MUST also be the last name in the sequence. + + if (pi == rdata.parameters.Length-1) + { + if (rdata.items != null) + { + int k = 0; + InventoryItemBase li = null; + foreach (InventoryItemBase i in rdata.items) + { + if (i.Folder == folder.ID && + (i.Name == rdata.parameters[pi] || + i.ID.ToString() == rdata.parameters[pi])) + { + li = i; + k++; + } + } + if (k == 1) + { + return li; + } + else + { + Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound); + } + } + } + + // No, so abandon the request + + Rest.Log.DebugFormat("{0} {1}: Resource {2} not found", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound); + + return null; /* Never reached */ + + } + + /// + /// This routine traverse the inventory's structure until the end-point identified + /// in the URI is reached, the remainder of the inventory (if any) is then formatted + /// and returned to the requestor. + /// + /// Note that this method is only interested in those folder that match elements of + /// the URI supplied by the requestor, so once a match is fund, the processing does + /// not need to consider any further elements. + /// + /// Only the last element in the URI should identify an item. + /// + + private void traverseInventory(InventoryRequestData rdata, InventoryFolderBase folder, int pi) + { + + Rest.Log.DebugFormat("{0} Folder : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); + + if (rdata.folders != null) + { + foreach (InventoryFolderBase f in rdata.folders) + { + if (f.ParentID == folder.ID && + (f.Name == rdata.parameters[pi] || + f.ID.ToString() == rdata.parameters[pi])) + { + if (pi < rdata.parameters.Length-1) + { + traverseInventory(rdata, f, pi+1); + } + else + { + formatInventory(rdata, f, String.Empty); + } + return; + } + } + } + + if (pi == rdata.parameters.Length-1) + { + if (rdata.items != null) + { + foreach (InventoryItemBase i in rdata.items) + { + if (i.Folder == folder.ID && + (i.Name == rdata.parameters[pi] || + i.ID.ToString() == rdata.parameters[pi])) + { + // Fetching an Item has a special significance. In this + // case we also want to fetch the associated asset. + // To make it interesting, we'll d this via redirection. + string asseturl = "http://" + rdata.hostname + ":" + rdata.port + + "/admin/assets" + Rest.UrlPathSeparator + i.AssetID.ToString(); + rdata.Redirect(asseturl,Rest.PERMANENT); + Rest.Log.DebugFormat("{0} Never Reached"); + } + } + } + } + + Rest.Log.DebugFormat("{0} Inventory does not contain item/folder: <{1}>", + MsgId, rdata.path); + rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound); + + } + + /// + /// This method generates XML that describes an instance of InventoryFolderBase. + /// It recurses as necessary to reflect a folder hierarchy, and calls formatItem + /// to generate XML for any items encountered along the way. + /// The indentation parameter is solely for the benefit of trace record + /// formatting. + /// + + private void formatInventory(InventoryRequestData rdata, InventoryFolderBase folder, string indent) + { + + if (Rest.DEBUG) + { + Rest.Log.DebugFormat("{0} Folder : {1} {2} {3}", MsgId, folder.ID, indent, folder.Name); + indent += "\t"; + } + + // Start folder item + + rdata.writer.WriteStartElement(String.Empty,"Folder",String.Empty); + rdata.writer.WriteAttributeString("name",String.Empty,folder.Name); + rdata.writer.WriteAttributeString("uuid",String.Empty,folder.ID.ToString()); + rdata.writer.WriteAttributeString("owner",String.Empty,folder.Owner.ToString()); + rdata.writer.WriteAttributeString("type",String.Empty,folder.Type.ToString()); + rdata.writer.WriteAttributeString("version",String.Empty,folder.Version.ToString()); + + if (rdata.folders != null) + { + foreach (InventoryFolderBase f in rdata.folders) + { + if (f.ParentID == folder.ID) + { + formatInventory(rdata, f, indent); + } + } + } + + if (rdata.items != null) + { + foreach (InventoryItemBase i in rdata.items) + { + if (i.Folder == folder.ID) + { + formatItem(rdata, i, indent); + } + } + } + + // End folder item + + rdata.writer.WriteEndElement(); + + } + + /// + /// This method generates XML that describes an instance of InventoryItemBase. + /// + + private void formatItem(InventoryRequestData rdata, InventoryItemBase i, string indent) + { + + Rest.Log.DebugFormat("{0} Item : {1} {2} {3}", MsgId, i.ID, indent, i.Name); + + rdata.writer.WriteStartElement(String.Empty,"Item",String.Empty); + + rdata.writer.WriteAttributeString("name",String.Empty,i.Name); + rdata.writer.WriteAttributeString("desc",String.Empty,i.Description); + rdata.writer.WriteAttributeString("uuid",String.Empty,i.ID.ToString()); + rdata.writer.WriteAttributeString("owner",String.Empty,i.Owner.ToString()); + rdata.writer.WriteAttributeString("creator",String.Empty,i.Creator.ToString()); + rdata.writer.WriteAttributeString("creationdate",String.Empty,i.CreationDate.ToString()); + rdata.writer.WriteAttributeString("type",String.Empty,i.InvType.ToString()); + rdata.writer.WriteAttributeString("assettype",String.Empty,i.AssetType.ToString()); + rdata.writer.WriteAttributeString("groupowned",String.Empty,i.GroupOwned.ToString()); + rdata.writer.WriteAttributeString("groupid",String.Empty,i.GroupID.ToString()); + rdata.writer.WriteAttributeString("saletype",String.Empty,i.SaleType.ToString()); + rdata.writer.WriteAttributeString("saleprice",String.Empty,i.SalePrice.ToString()); + rdata.writer.WriteAttributeString("flags",String.Empty,i.Flags.ToString("X")); + + rdata.writer.WriteStartElement(String.Empty,"Permissions",String.Empty); + rdata.writer.WriteAttributeString("current",String.Empty,i.CurrentPermissions.ToString("X")); + rdata.writer.WriteAttributeString("next",String.Empty,i.NextPermissions.ToString("X")); + rdata.writer.WriteAttributeString("everyone",String.Empty,i.EveryOnePermissions.ToString("X")); + rdata.writer.WriteAttributeString("base",String.Empty,i.BasePermissions.ToString("X")); + rdata.writer.WriteEndElement(); + + rdata.writer.WriteElementString("Asset",i.AssetID.ToString()); + + rdata.writer.WriteEndElement(); + + } + + /// + /// This method creates a "trashcan" folder to support folder and item + /// deletions by this interface. The xisting trash folder is found and + /// this folder is created within it. It is called "tmp" to indicate to + /// the client that it is OK to delete this folder. The REST interface + /// will recreate the folder on an as-required basis. + /// If the trash can cannot be created, then by implication the request + /// that required it cannot be completed, and it fails accordingly. + /// + + private InventoryFolderBase GetTrashCan(InventoryRequestData rdata) + { + + InventoryFolderBase TrashCan = null; + + foreach (InventoryFolderBase f in rdata.folders) + { + if (f.Name == "Trash") + { + foreach (InventoryFolderBase t in rdata.folders) + { + if (t.Name == "tmp") + { + TrashCan = t; + } + } + if (TrashCan == null) + { + TrashCan = new InventoryFolderBase(); + TrashCan.Name = "tmp"; + TrashCan.ID = LLUUID.Random(); + TrashCan.Version = 1; + TrashCan.Type = (short) AssetType.TrashFolder; + TrashCan.ParentID = f.ID; + Rest.InventoryServices.AddFolder(TrashCan); + } + } + } + + if (TrashCan == null) + { + Rest.Log.DebugFormat("{0} No Trash Can available", MsgId); + rdata.Fail(Rest.HttpStatusCodeServerError, + Rest.HttpStatusDescServerError); + } + + return TrashCan; + + } + + /// + /// Make sure that an unchanged folder is not unnecessarily + /// processed. + /// + + private bool FolderHasChanged(InventoryFolderBase newf, InventoryFolderBase oldf) + { + return ( newf.Name != oldf.Name + || newf.ParentID != oldf.ParentID + || newf.Owner != oldf.Owner + || newf.Type != oldf.Type + || newf.Version != oldf.Version + ); + } + + /// + /// Make sure that an unchanged item is not unnecessarily + /// processed. + /// + + private bool ItemHasChanged(InventoryItemBase newf, InventoryItemBase oldf) + { + return ( newf.Name != oldf.Name + || newf.Folder != oldf.Description + || newf.Description != oldf.Description + || newf.Owner != oldf.Owner + || newf.Creator != oldf.Creator + || newf.AssetID != oldf.AssetID + || newf.GroupID != oldf.GroupID + || newf.GroupOwned != oldf.GroupOwned + || newf.InvType != oldf.InvType + || newf.AssetType != oldf.AssetType + ); + } + + /// + /// This method is called by PUT and POST to create an XmlInventoryCollection + /// instance that reflects the content of the entity supplied on the request. + /// Any elements in the completed collection whose UUID is zero, are + /// considered to be located relative to the end-point identified int he + /// URI. In this way, an entire sub-tree can be conveyed in a single REST + /// PUT or POST request. + /// + /// A new instance of XmlInventoryCollection is created and, if the request + /// has an entity, it is more completely initialized. thus, if no entity was + /// provided the collection is valid, but empty. + /// + /// The entity is then scanned and each tag is processed to produce the + /// appropriate inventory elements. At the end f the scan, teh XmlInventoryCollection + /// will reflect the subtree described by the entity. + /// + /// This is a very flexible mechanism, the entity may contain arbitrary, + /// discontiguous tree fragments, or may contain single element. The caller is + /// responsible for integrating this collection (and ensuring that any + /// missing parent IDs are resolved). + /// + + internal XmlInventoryCollection ReconstituteEntity(InventoryRequestData rdata) + { + + Rest.Log.DebugFormat("{0} Reconstituting entity", MsgId); + + XmlInventoryCollection ic = new XmlInventoryCollection(); + + if (rdata.request.HasEntityBody) + { + + Rest.Log.DebugFormat("{0} Entity present", MsgId); + + ic.init(rdata); + + try + { + while (ic.xml.Read()) + { + switch (ic.xml.NodeType) + { + case XmlNodeType.Element : + Rest.Log.DebugFormat("{0} StartElement: <{1}>", + MsgId, ic.xml.Name); + switch (ic.xml.Name) + { + case "Folder" : + Rest.Log.DebugFormat("{0} Processing {1} element", + MsgId, ic.xml.Name); + CollectFolder(ic); + break; + case "Item" : + Rest.Log.DebugFormat("{0} Processing {1} element", + MsgId, ic.xml.Name); + CollectItem(ic); + break; + case "Asset" : + Rest.Log.DebugFormat("{0} Processing {1} element", + MsgId, ic.xml.Name); + CollectAsset(ic); + break; + case "Permissions" : + Rest.Log.DebugFormat("{0} Processing {1} element", + MsgId, ic.xml.Name); + CollectPermissions(ic); + break; + default : + Rest.Log.DebugFormat("{0} Ignoring {1} element", + MsgId, ic.xml.Name); + break; + } + // This stinks, but the ReadElement call above not only reads + // the imbedded data, but also consumes the end tag for Asset + // and moves the element pointer on to the containing Item's + // element-end, however, if there was a permissions element + // following, it would get us to the start of that.. + if (ic.xml.NodeType == XmlNodeType.EndElement && + ic.xml.Name == "Item") + { + Validate(ic); + } + break; + case XmlNodeType.EndElement : + switch (ic.xml.Name) + { + case "Folder" : + Rest.Log.DebugFormat("{0} Completing {1} element", + MsgId, ic.xml.Name); + ic.Pop(); + break; + case "Item" : + Rest.Log.DebugFormat("{0} Completing {1} element", + MsgId, ic.xml.Name); + Validate(ic); + break; + case "Asset" : + Rest.Log.DebugFormat("{0} Completing {1} element", + MsgId, ic.xml.Name); + break; + case "Permissions" : + Rest.Log.DebugFormat("{0} Completing {1} element", + MsgId, ic.xml.Name); + break; + default : + Rest.Log.DebugFormat("{0} Ignoring {1} element", + MsgId, ic.xml.Name); + break; + } + break; + default : + Rest.Log.DebugFormat("{0} [0] Ignoring: <{1}>:<2>", + MsgId, ic.xml.NodeType, ic.xml.Value); + break; + } + } + } + catch (XmlException e) + { + Rest.Log.WarnFormat("{0} XML parsing error: {1}", MsgId, e.Message); + throw e; + } + catch (Exception e) + { + Rest.Log.WarnFormat("{0} Unexpected XML parsing error: {1}", MsgId, e.Message); + throw e; + } + + } + else + { + Rest.Log.DebugFormat("{0} Entity absent", MsgId); + } + + if (Rest.DEBUG) + { + Rest.Log.DebugFormat("{0} Reconstituted entity", MsgId); + Rest.Log.DebugFormat("{0} {1} assets", MsgId, ic.Assets.Count); + Rest.Log.DebugFormat("{0} {1} folder", MsgId, ic.Folders.Count); + Rest.Log.DebugFormat("{0} {1} items", MsgId, ic.Items.Count); + } + + return ic; + + } + + /// + /// This method creates an inventory Folder from the + /// information supplied in the request's entity. + /// A folder instance is created and initialized to reflect + /// default values. These values are then overridden + /// by information supplied in the entity. + /// If context was not explicitly provided, then the + /// appropriate ID values are determined. + /// + + private void CollectFolder(XmlInventoryCollection ic) + { + + Rest.Log.DebugFormat("{0} Interpret folder element", MsgId); + + InventoryFolderBase result = new InventoryFolderBase(); + + // Default values + + result.Name = String.Empty; + result.ID = LLUUID.Zero; + result.Owner = ic.UserID; + result.ParentID = LLUUID.Zero; // Context + result.Type = (short) AssetType.Folder; + result.Version = 1; + + if (ic.xml.HasAttributes) + { + for (int i = 0; i < ic.xml.AttributeCount; i++) + { + ic.xml.MoveToAttribute(i); + switch (ic.xml.Name) + { + case "name" : + result.Name = ic.xml.Value; + break; + case "uuid" : + result.ID = new LLUUID(ic.xml.Value); + break; + case "parent" : + result.ParentID = new LLUUID(ic.xml.Value); + break; + case "owner" : + result.Owner = new LLUUID(ic.xml.Value); + break; + case "type" : + result.Type = Int16.Parse(ic.xml.Value); + break; + case "version" : + result.Version = UInt16.Parse(ic.xml.Value); + break; + default : + Rest.Log.DebugFormat("{0} Folder: unrecognized attribute: {1}:{2}", + MsgId, ic.xml.Name, ic.xml.Value); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + break; + } + } + } + + ic.xml.MoveToElement(); + + // The client is relying upon the reconstitution process + // to determine the parent's UUID based upon context. This + // is necessary where a new folder may have been + // introduced. + + if (result.ParentID == LLUUID.Zero) + { + result.ParentID = ic.Parent(); + } + else + { + + bool found = false; + + foreach (InventoryFolderBase parent in ic.rdata.folders) + { + if ( parent.ID == result.ParentID ) + { + found = true; + break; + } + } + + if (!found) + { + Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in folder {2}", + MsgId, ic.Item.Folder, result.ID); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + } + + } + + // This is a new folder, so no existing UUID is available + // or appropriate + + if (result.ID == LLUUID.Zero) + { + result.ID = LLUUID.Random(); + } + + // Treat this as a new context. Any other information is + // obsolete as a consequence. + + ic.Push(result); + + } + + /// + /// This method is called to handle the construction of an Item + /// instance from the supplied request entity. It is called + /// whenever an Item start tag is detected. + /// An instance of an Item is created and initialized to default + /// values. These values are then overridden from values supplied + /// as attributes to the Item element. + /// This item is then stored in the XmlInventoryCollection and + /// will be verified by Validate. + /// All context is reset whenever the effective folder changes + /// or an item is successfully validated. + /// + + private void CollectItem(XmlInventoryCollection ic) + { + + Rest.Log.DebugFormat("{0} Interpret item element", MsgId); + + InventoryItemBase result = new InventoryItemBase(); + + result.Name = String.Empty; + result.Description = String.Empty; + result.ID = LLUUID.Zero; + result.Folder = LLUUID.Zero; + result.Owner = ic.UserID; + result.Creator = ic.UserID; + result.AssetID = LLUUID.Zero; + result.GroupID = LLUUID.Zero; + result.GroupOwned = false; + result.InvType = (int) InventoryType.Unknown; + result.AssetType = (int) AssetType.Unknown; + + if (ic.xml.HasAttributes) + { + for (int i = 0; i < ic.xml.AttributeCount; i++) + { + + ic.xml.MoveToAttribute(i); + + switch (ic.xml.Name) + { + case "name" : + result.Name = ic.xml.Value; + break; + case "desc" : + result.Description = ic.xml.Value; + break; + case "uuid" : + result.ID = new LLUUID(ic.xml.Value); + break; + case "folder" : + result.Folder = new LLUUID(ic.xml.Value); + break; + case "owner" : + result.Owner = new LLUUID(ic.xml.Value); + break; + case "invtype" : + result.InvType = Int32.Parse(ic.xml.Value); + break; + case "creator" : + result.Creator = new LLUUID(ic.xml.Value); + break; + case "assettype" : + result.AssetType = Int32.Parse(ic.xml.Value); + break; + case "groupowned" : + result.GroupOwned = Boolean.Parse(ic.xml.Value); + break; + case "groupid" : + result.GroupID = new LLUUID(ic.xml.Value); + break; + case "flags" : + result.Flags = UInt32.Parse(ic.xml.Value); + break; + case "creationdate" : + result.CreationDate = Int32.Parse(ic.xml.Value); + break; + case "saletype" : + result.SaleType = Byte.Parse(ic.xml.Value); + break; + case "saleprice" : + result.SalePrice = Int32.Parse(ic.xml.Value); + break; + + default : + Rest.Log.DebugFormat("{0} Item: Unrecognized attribute: {1}:{2}", + MsgId, ic.xml.Name, ic.xml.Value); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + break; + } + } + } + + ic.xml.MoveToElement(); + + ic.Push(result); + + } + + /// + /// This method assembles an asset instance from the + /// information supplied in the request's entity. It is + /// called as a result of detecting a start tag for a + /// type of Asset. + /// The information is collected locally, and an asset + /// instance is created only if the basic XML parsing + /// completes successfully. + /// Default values for all parts of the asset are + /// established before overriding them from the supplied + /// XML. + /// If an asset has inline=true as an attribute, then + /// the element contains the data representing the + /// asset. This is saved as the data component. + /// inline=false means that the element's payload is + /// simply the UUID of the asset referenced by the + /// item being constructed. + /// An asset, if created is stored in the + /// XmlInventoryCollection + /// + + private void CollectAsset(XmlInventoryCollection ic) + { + + Rest.Log.DebugFormat("{0} Interpret asset element", MsgId); + + AssetBase asset = null; + + string name = String.Empty; + string desc = String.Empty; + sbyte type = (sbyte) AssetType.Unknown; + sbyte itype = (sbyte) AssetType.Unknown; + bool temp = false; + bool local = false; + + // This is not a persistent attribute + bool inline = true; + + LLUUID uuid = LLUUID.Zero; + + // Attribute is optional + if (ic.xml.HasAttributes) + { + for (int i = 0; i < ic.xml.AttributeCount; i++) + { + ic.xml.MoveToAttribute(i); + switch (ic.xml.Name) + { + + case "name" : + name = ic.xml.Value; + break; + + case "type" : + type = SByte.Parse(ic.xml.Value); + break; + + case "description" : + desc = ic.xml.Value; + break; + + case "temporary" : + temp = Boolean.Parse(ic.xml.Value); + break; + + case "invtype" : + itype = SByte.Parse(ic.xml.Value); + break; + + case "uuid" : + uuid = new LLUUID(ic.xml.Value); + break; + + case "inline" : + inline = Boolean.Parse(ic.xml.Value); + break; + + case "local" : + local = Boolean.Parse(ic.xml.Value); + break; + + default : + Rest.Log.DebugFormat("{0} Asset: Unrecognized attribute: {1}:{2}", + MsgId, ic.xml.Name, ic.xml.Value); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + break; + } + } + } + + ic.xml.MoveToElement(); + + // If this is a reference to an existing asset, just store the + // asset ID into the item. + + if (!inline) + { + if (ic.Item != null) + { + ic.Item.AssetID = new LLUUID(ic.xml.ReadElementContentAsString()); + Rest.Log.DebugFormat("{0} Asset ID supplied: {1}", MsgId, ic.Item.AssetID); + } + else + { + Rest.Log.DebugFormat("{0} LLUID unimbedded asset must be inline", MsgId); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + } + } + + // Otherwise, generate an asset ID, store that into the item, and + // create an entry in the asset list for the inlined asset. But + // only if the size is non-zero. + + else + { + + string b64string = null; + + // Generate a UUID of none were given, and generally none should + // be. Ever. + + if (uuid == LLUUID.Zero) + { + uuid = LLUUID.Random(); + } + + // Create AssetBase entity to hold the inlined asset + + asset = new AssetBase(uuid, name); + + asset.Description = desc; + asset.Type = type; // type == 0 == texture + asset.InvType = itype; + asset.Local = local; + asset.Temporary = temp; + + b64string = ic.xml.ReadElementContentAsString(); + + Rest.Log.DebugFormat("{0} Data length is {1}", MsgId, b64string.Length); + Rest.Log.DebugFormat("{0} Data content starts with: \n\t<{1}>", MsgId, + b64string.Substring(0, b64string.Length > 132 ? 132 : b64string.Length)); + + asset.Data = Convert.FromBase64String(b64string); + + // Ensure the asset always has some kind of data component + + if (asset.Data == null) + { + asset.Data = new byte[1]; + } + + // If this is in the context of an item, establish + // a link with the item in context. + + if (ic.Item != null && ic.Item.AssetID == LLUUID.Zero) + { + ic.Item.AssetID = uuid; + } + + } + + ic.Push(asset); + + } + + /// + /// Store any permissions information provided by the request. + /// This overrides the default permissions set when the + /// XmlInventoryCollection object was created. + /// + + private void CollectPermissions(XmlInventoryCollection ic) + { + + if (ic.xml.HasAttributes) + { + for (int i = 0; i < ic.xml.AttributeCount; i++) + { + ic.xml.MoveToAttribute(i); + switch (ic.xml.Name) + { + case "current" : + ic.CurrentPermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber); + break; + case "next" : + ic.NextPermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber); + break; + case "everyone" : + ic.EveryOnePermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber); + break; + case "base" : + ic.BasePermissions = UInt32.Parse(ic.xml.Value, System.Globalization.NumberStyles.HexNumber); + break; + default : + Rest.Log.DebugFormat("{0} Permissions: invalid attribute {1}:{2}", + MsgId,ic.xml.Name, ic.xml.Value); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + break; + } + } + } + + ic.xml.MoveToElement(); + + } + + /// + /// This method is called whenever an Item has been successfully + /// reconstituted from the request's entity. + /// It uses the information curren tin the XmlInventoryCollection + /// to complete the item's specification, including any implied + /// context and asset associations. + /// It fails the request if any necessary item or asset information + /// is missing. + /// + + private void Validate(XmlInventoryCollection ic) + { + + // There really should be an item present if we've + // called validate. So fail if there is not. + + if (ic.Item == null) + { + Rest.Log.ErrorFormat("{0} Unable to parse request", MsgId); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + } + + // Every item is required to have a name (via REST anyway) + + if (ic.Item.Name == String.Empty) + { + Rest.Log.ErrorFormat("{0} An item name MUST be specified", MsgId); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + } + + // An item MUST have an asset ID. AssetID should never be zero + // here. It should always get set from the information stored + // when the Asset element was processed. + + if (ic.Item.AssetID == LLUUID.Zero) + { + + Rest.Log.ErrorFormat("{0} Unable to complete request", MsgId); + Rest.Log.InfoFormat("{0} Asset information is missing", MsgId); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + + } + + // If the item is new, then assign it an ID + + if (ic.Item.ID == LLUUID.Zero) + { + ic.Item.ID = LLUUID.Random(); + } + + // If the context is being implied, obtain the current + // folder item's ID. If it was specified explicitly, make + // sure that theparent folder exists. + + if (ic.Item.Folder == LLUUID.Zero) + { + ic.Item.Folder = ic.Parent(); + } + else + { + + bool found = false; + + foreach (InventoryFolderBase parent in ic.rdata.folders) + { + if ( parent.ID == ic.Item.Folder ) + { + found = true; + break; + } + } + + if (!found) + { + Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in item {2}", + MsgId, ic.Item.Folder, ic.Item.ID); + ic.Fail(Rest.HttpStatusCodeBadRequest, + Rest.HttpStatusDescBadRequest); + } + + } + + // If this is an inline asset being constructed in the context + // of a new Item, then use the itm's name here too. + + if (ic.Asset != null) + { + if (ic.Asset.Name == String.Empty) + ic.Asset.Name = ic.Item.Name; + if (ic.Asset.Description == String.Empty) + ic.Asset.Description = ic.Item.Description; + } + + // Assign permissions + + ic.Item.CurrentPermissions = ic.CurrentPermissions; + ic.Item.EveryOnePermissions = ic.EveryOnePermissions; + ic.Item.BasePermissions = ic.BasePermissions; + ic.Item.NextPermissions = ic.NextPermissions; + + // If no type was specified for this item, we can attempt to + // infer something from the file type maybe. This is NOT as + // good as having type be specified in the XML. + + if (ic.Item.AssetType == (int) AssetType.Unknown || + ic.Item.InvType == (int) AssetType.Unknown) + { + + Rest.Log.DebugFormat("{0} Attempting to infer item type", MsgId); + + string[] parts = ic.Item.Name.Split(Rest.CA_PERIOD); + + if (Rest.DEBUG) + { + for (int i = 0; i < parts.Length; i++) + { + Rest.Log.DebugFormat("{0} Name part {1} : {2}", + MsgId, i, parts[i]); + } + } + + // If the associated item name is multi-part, then maybe + // the last part will indicate the item type - if we're + // lucky. + + if (parts.Length > 1) + { + Rest.Log.DebugFormat("{0} File type is {1}", + MsgId, parts[parts.Length - 1]); + switch (parts[parts.Length - 1]) + { + case "jpeg2000" : + case "jpeg-2000" : + case "jpg2000" : + case "jpg-2000" : + Rest.Log.DebugFormat("{0} Type {1} inferred", + MsgId, parts[parts.Length-1]); + if (ic.Item.AssetType == (int) AssetType.Unknown) + ic.Item.AssetType = (int) AssetType.ImageJPEG; + if (ic.Item.InvType == (int) AssetType.Unknown) + ic.Item.InvType = (int) AssetType.ImageJPEG; + break; + case "jpg" : + case "jpeg" : + Rest.Log.DebugFormat("{0} Type {1} inferred", + MsgId, parts[parts.Length - 1]); + if (ic.Item.AssetType == (int) AssetType.Unknown) + ic.Item.AssetType = (int) AssetType.ImageJPEG; + if (ic.Item.InvType == (int) AssetType.Unknown) + ic.Item.InvType = (int) AssetType.ImageJPEG; + break; + default : + Rest.Log.DebugFormat("{0} Type was not inferred", MsgId); + break; + } + } + } + + ic.reset(); + + } + + #region Inventory RequestData extension + + internal class InventoryRequestData : RequestData + { + + /// + /// These are the inventory specific request/response state + /// extensions. + /// + + internal bool HaveInventory = false; + internal ICollection folders = null; + internal ICollection items = null; + internal UserProfileData userProfile = null; + internal InventoryFolderBase root = null; + + internal InventoryRequestData(OSHttpRequest request, OSHttpResponse response, string prefix) + : base(request, response, prefix) + { + } + + /// + /// This is the callback method required by inventory services. The + /// requestor issues an inventory request and then blocks until this + /// method signals the monitor. + /// + + internal void GetUserInventory(ICollection folders, ICollection items) + { + Rest.Log.DebugFormat("{0} Asynchronously updating inventory data", MsgId); + this.folders = folders; + this.items = items; + this.HaveInventory = true; + lock(this) + { + Monitor.Pulse(this); + } + } + + } + + #endregion Inventory RequestData extension + + /// + /// This class is used to record and manage the hierarchy + /// constructed from the entity supplied in the request for + /// PUT and POST. + /// + + internal class XmlInventoryCollection : InventoryCollection + { + + internal InventoryRequestData rdata; + private Stack stk; + + internal List Assets; + + internal InventoryItemBase Item; + internal AssetBase Asset; + internal XmlReader xml; + + internal /*static*/ const uint DefaultCurrent = 0x7FFFFFFF; + internal /*static*/ const uint DefaultNext = 0x82000; + internal /*static*/ const uint DefaultBase = 0x7FFFFFFF; + internal /*static*/ const uint DefaultEveryOne = 0x0; + + internal uint CurrentPermissions = 0x00; + internal uint NextPermissions = 0x00; + internal uint BasePermissions = 0x00; + internal uint EveryOnePermissions = 0x00; + + internal XmlInventoryCollection() + { + Folders = new List(); + Items = new List(); + Assets = new List(); + } + + internal void init(InventoryRequestData p_rdata) + { + rdata = p_rdata; + UserID = rdata.uuid; + stk = new Stack(); + rdata.initXmlReader(); + xml = rdata.reader; + initPermissions(); + } + + internal void initPermissions() + { + CurrentPermissions = DefaultCurrent; + NextPermissions = DefaultNext; + BasePermissions = DefaultBase; + EveryOnePermissions = DefaultEveryOne; + } + + internal LLUUID Parent() + { + if (stk.Count != 0) + { + return stk.Peek().ID; + } + else + { + return LLUUID.Zero; + } + } + + internal void Push(InventoryFolderBase folder) + { + stk.Push(folder); + Folders.Add(folder); + reset(); + } + + internal void Push(InventoryItemBase item) + { + Item = item; + Items.Add(item); + } + + internal void Push(AssetBase asset) + { + Asset = asset; + Assets.Add(asset); + } + + internal void Pop() + { + stk.Pop(); + reset(); + } + + internal void reset() + { + Item = null; + Asset = null; + initPermissions(); + } + + internal void Fail(int code, string desc) + { + rdata.Fail(code, desc); + } + + } + } +} diff --git a/prebuild.xml b/prebuild.xml index fdc9eeec3e..29c8a23a26 100644 --- a/prebuild.xml +++ b/prebuild.xml @@ -1198,6 +1198,44 @@ + + + + + ../../../../bin/ + + + + + ../../../../bin/ + + + + ../../../../bin/ + + + + + + + + + + + + + + + + + + + + + + +