From 5e83a758157520d48b15c725f5be2b196d2414e3 Mon Sep 17 00:00:00 2001 From: Dr Scofield Date: Wed, 20 Aug 2008 10:11:11 +0000 Subject: From: Alan Webb cleanups of the REST inventory code. --- .../Rest/Inventory/RequestData.cs | 1106 ++++++++++++-------- OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs | 233 +++-- .../Rest/Inventory/RestAssetServices.cs | 59 +- .../Rest/Inventory/RestHandler.cs | 141 ++- .../Rest/Inventory/RestInventoryServices.cs | 916 ++++++++++------ .../Rest/Inventory/RestTestServices.cs | 43 +- 6 files changed, 1588 insertions(+), 910 deletions(-) (limited to 'OpenSim') diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs index d08f830..6742402 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RequestData.cs @@ -23,6 +23,7 @@ * 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; @@ -40,16 +41,17 @@ using System.Xml; namespace OpenSim.ApplicationPlugins.Rest.Inventory { + /// /// This class represents the current REST request. It - /// encapsulates the request/response state and takes care + /// 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 - /// specific REST handler, and fundamental changes to + /// specific REST handler, and fundamental changes to /// this should be reflected in the Rest HandlerVersion. The /// object is instantiated, and may be extended by, any /// given handler. See the inventory handler for an example @@ -63,44 +65,109 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// of it as a transactional req/resp capability. /// - internal class RequestData + public class RequestData { - // HTTP Server interface data + + // HTTP Server interface data (Received values) internal OSHttpRequest request = null; internal OSHttpResponse response = null; internal string qprefix = null; // Request lifetime values + // buffer is global because it is referenced by the handler + // in supported of streamed requests. + // If a service provider wants to construct the message + // body explicitly it can use body to do this. The value + // in body is used if the buffer is still null when a response + // is generated. + // Storing information in body will suppress the return of + // statusBody which is only intended to report status on + // requests which do not themselves ordinarily generate + // an informational response. All of this is handled in + // Respond(). internal byte[] buffer = null; - internal string body = null; - internal string html = null; - internal string entity = null; + internal string body = null; + internal string bodyType = "text/html"; + + // The encoding in effect is set to a server default. It may + // subsequently be overridden by a Content header. This + // value is established during construction and is used + // wherever encoding services are needed. + + internal Encoding encoding = Rest.Encoding; + + // These values are derived from the supplied URL. They + // are initialized during construction. + 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; + + // The path part of the URI is decomposed. pathNodes + // is an array of every element in the URI. Parameters + // is an array that contains only those nodes that + // are not a part of the authority prefix + + private string[] pathNodes = null; + private string[] parameters = null; + private static readonly string[] EmptyPath = { String.Empty }; + + // The status code gets set during the course of processing + // and is the HTTP completion code. The status body is + // initialized during construction, is appended to during the + // course of execution, and is finalized during Respond + // processing. + // + // Fail processing marks the request as failed and this is + // then used to inhibit processing during Response processing. + + internal int statusCode = 0; + internal string statusBody = String.Empty; + internal bool fail = false; + + // This carries the URL to which the client should be redirected. + // It is set by the service provider using the Redirect call. + + internal string redirectLocation = null; + + // These values influence response processing. They can be set by + // service providers according to need. The defaults are generally + // good. + internal bool keepAlive = false; internal bool chunked = false; - // Authentication related state + // XML related state + internal XmlWriter writer = null; + internal XmlReader reader = null; + + // Internal working state + + private StringBuilder sbuilder = new StringBuilder(1024); + private MemoryStream xmldata = null; + + // This is used to make the response mechanism idempotent. + + internal bool handled = false; + + // Authentication related state + // + // Two supported authentication mechanisms are: + // scheme = Rest.AS_BASIC; + // scheme = Rest.AS_DIGEST; + // Presented in that order (as required by spec) + // A service provider can set the scheme variable to + // force selection of a particular authentication model + // (choosing from amongst those supported of course) + // + internal bool authenticated = false; - // internal string scheme = Rest.AS_DIGEST; - // internal string scheme = Rest.AS_BASIC; internal string scheme = null; internal string realm = Rest.Realm; internal string domain = null; @@ -114,22 +181,9 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 + // 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. @@ -145,46 +199,26 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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*\"(?[^\"]+)\"", 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\\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 p_qprefix) - { - request = p_request; - response = p_response; - qprefix = p_qprefix; - - sbuilder.Length = 0; - - encoding = request.ContentEncoding; - if (encoding == null) - { - encoding = Rest.Encoding; - } - - method = request.HttpMethod.ToLower(); - initUrl(); - - initParameters(p_qprefix.Length); - } +#region properties // Just for convenience... @@ -193,12 +227,16 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory get { return Rest.MsgId; } } - // Defer authentication check until requested + /// + /// Return a boolean indication of whether or no an authenticated user is + /// associated with this request. This could be wholly integrated, but + /// that would make authentication mandatory. + /// internal bool IsAuthenticated { get - { + { if (Rest.Authenticate) { if (!authenticated) @@ -208,24 +246,87 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory return authenticated; } - else - return true; + else return true; + } + } + + /// + /// Access to all 'nodes' in the supplied URI as an + /// array of strings. + /// + + internal string[] PathNodes + { + get + { + return pathNodes; + } + } + + /// + /// Access to all non-prefix 'nodes' in the supplied URI as an + /// array of strings. These identify a specific resource that + /// is managed by the authority (the prefix). + /// + + internal string[] Parameters + { + get + { + return parameters; + } + } + +#endregion properties + +#region constructors + + // Constructor + + internal RequestData(OSHttpRequest p_request, OSHttpResponse p_response, string p_qprefix) + { + + request = p_request; + response = p_response; + qprefix = p_qprefix; + + sbuilder.Length = 0; + + encoding = request.ContentEncoding; + if (encoding == null) + { + encoding = Rest.Encoding; } + + method = request.HttpMethod.ToLower(); + initUrl(); + + initParameters(p_qprefix.Length); + } +#endregion constructors + +#region authentication_common + /// /// 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 + /// 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. + /// + /// As soon as authentication failure is detected the method calls + /// DoChallenge() which terminates the request with REST exception + /// for unauthroized access. /// private void authenticate() { + string authdata = request.Headers.Get("Authorization"); string reqscheme = String.Empty; @@ -238,7 +339,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 @@ -287,6 +388,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Challenge reason: Authentication failed", MsgId); DoChallenge(); } + } /// @@ -300,16 +402,193 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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); + Fail(Rest.HttpStatusCodeNotAuthorized); + } + + /// + /// The Flush() call is here to support a problem encountered with the + /// client where an authentication rejection was lost because the rejection + /// may flow before the clienthas finished sending us the inbound data stream, + /// in which case the client responds to the socket error on out put, and + /// never sees the authentication challenge. The client should be fixed, + /// because this solution leaves the server prone to DOS attacks. A message + /// will be issued whenever flushing occurs. It can be enabled/disabled from + /// the configuration file. + /// + + private void Flush() + { + if (Rest.FlushEnabled) + { + byte[] dbuffer = new byte[8192]; + Rest.Log.WarnFormat("{0} REST server is flushing the inbound data stream", MsgId); + while (request.InputStream.Read(dbuffer,0,dbuffer.Length) != 0); + } + return; + } + + // Indicate that authentication is required + + private void Challenge(string scheme, string realm, string domain, string nonce, + string opaque, string stale, string alg, + string qop, string auth) + { + + sbuilder.Length = 0; + + // The service provider can force a particular scheme by + // assigning a value to scheme. + + // Basic authentication is pretty simple. + // Just specify the realm in question. + + if (scheme == null || scheme == Rest.AS_BASIC) + { + + sbuilder.Append(Rest.AS_BASIC); + + if (realm != null) + { + sbuilder.Append(" realm=\""); + sbuilder.Append(realm); + sbuilder.Append("\""); + } + AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); + } + + sbuilder.Length = 0; + + // Digest authentication takes somewhat more + // to express. + + if (scheme == null || scheme == Rest.AS_DIGEST) + { + + sbuilder.Append(Rest.AS_DIGEST); + sbuilder.Append(" "); + + // Specify the effective realm. This should + // never be null if we are uthenticating, as it is required for all + // authentication schemes. It defines, in conjunction with the + // absolute URI information, the domain to which the authentication + // applies. It is an arbitrary string. I *believe* this allows an + // authentication to apply to disjoint resources within the same + // server. + + if (realm != null) + { + sbuilder.Append("realm="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(realm); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + // Share our nonce. This is *uniquely* generated each time a 401 is + // returned. We do not generate a very sophisticated nonce at the + // moment (it's simply a base64 encoded UUID). + + if (nonce != null) + { + sbuilder.Append("nonce="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(nonce); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + // The opaque string should be returned by the client unchanged in all + // subsequent requests. + + if (opaque != null) + { + sbuilder.Append("opaque="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(opaque); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + // This flag indicates that the authentication was rejected because the + // included nonce was stale. The server might use timestamp information + // in the nonce to determine this. We do not. + + if (stale != null) + { + sbuilder.Append("stale="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(stale); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(Rest.CS_COMMA); + } + + // Identifies the algorithm used to produce the digest and checksum. + // The default is MD5. + + if (alg != null) + { + sbuilder.Append("algorithm="); + sbuilder.Append(alg); + sbuilder.Append(Rest.CS_COMMA); + } + + // Theoretically QOP is optional, but it is required by a compliant + // with current versions of the scheme. In fact IE requires that QOP + // be specified and will refuse to authenticate otherwise. + + 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); + } + + // This parameter allows for arbitrary extensions to the protocol. + // Unrecognized values should be simply ignored. + + if (auth != null) + { + sbuilder.Append(auth); + sbuilder.Append(Rest.CS_COMMA); + } + + // We don't know the userid that will be used + // so we cannot make any authentication domain + // assumptions. So the prefix will determine + // this. + + sbuilder.Append("domain="); + sbuilder.Append(Rest.CS_DQUOTE); + sbuilder.Append(qprefix); + sbuilder.Append(Rest.CS_DQUOTE); + + // Generate the authenticate header and we're basically + // done. + + AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); + + } + } +#endregion authentication_common + +#region authentication_basic + /// - /// Interpret a BASIC authorization claim - /// This is here for completeness, it is not used. + /// Interpret a BASIC authorization claim. Some clients can only + /// understand this and also expect it to be the first one + /// offered. So we do. + /// OpenSim also needs this, as it is the only scheme that allows + /// authentication using the hashed passwords stored in the + /// user database. /// private void DoBasic(string authdata) { + string response = null; MatchCollection matches = basicParms.Matches(authdata); @@ -320,7 +599,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory foreach (Match m in matches) { authparms.Add("response",m.Groups["pval"].Value); - Rest.Log.DebugFormat("{0} Parameter matched : {1} = {2}", + Rest.Log.DebugFormat("{0} Parameter matched : {1} = {2}", MsgId, "response", m.Groups["pval"].Value); } @@ -340,38 +619,111 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // Validate against user database authenticated = Validate(userName,userPass); } + + } + + /// + /// This method provides validation in support of the BASIC + /// authentication method. This is not normaly expected to be + /// used, but is included for completeness (and because I tried + /// it first). + /// + + private bool Validate(string user, string pass) + { + + Rest.Log.DebugFormat("{0} Simple User Validation", MsgId); + + // Both values are required + + if (user == null || pass == null) + return false; + + // Eliminate any leading or trailing spaces + user = user.Trim(); + + return vetPassword(user, pass); + + } + + /// + /// This is used by the BASIC authentication scheme to calculate + /// the double hash used by OpenSim to encode user's passwords. + /// It returns true, if the supplied password is actually correct. + /// If the specified user-id is not recognized, but the password + /// matches the God password, then this is accepted as an admin + /// session. + /// + + private bool vetPassword(string user, string pass) + { + + int x; + string HA1; + string first; + string last; + + // Distinguish the parts, if necessary + + if ((x=user.IndexOf(Rest.C_SPACE)) != -1) + { + first = user.Substring(0,x); + last = user.Substring(x+1); + } + else + { + first = user; + last = String.Empty; + } + + UserProfileData udata = Rest.UserServices.GetUserProfile(first, last); + + // If we don;t recognize the user id, perhaps it is god? + + if (udata == null) + return pass == Rest.GodKey; + + HA1 = HashToString(pass); + HA1 = HashToString(String.Format("{0}:{1}",HA1,udata.PasswordSalt)); + + return (0 == sc.Compare(HA1, udata.PasswordHash)); + } +#endregion authentication_basic + +#region authentication_digest + /// /// 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. + /// and Microsoft's Internet Explorer V7. /// private void DoDigest(string authdata) { + string response = null; - MatchCollection matches = digestParm1.Matches(authdata); + // Find all of the values of the for x = "y" - // Collect all of the supplied parameters and store them - // in a dictionary (for ease of access) + MatchCollection matches = digestParm1.Matches(authdata); foreach (Match m in matches) { authparms.Add(m.Groups["parm"].Value,m.Groups["pval"].Value); - Rest.Log.DebugFormat("{0} String Parameter matched : {1} = {2}", + Rest.Log.DebugFormat("{0} String Parameter matched : {1} = {2}", MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); } - // And pick up any tokens too + // Find all of the values of the for x = y 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}", + Rest.Log.DebugFormat("{0} Tokenized Parameter matched : {1} = {2}", MsgId, m.Groups["parm"].Value,m.Groups["pval"].Value); } @@ -382,10 +734,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (authparms.TryGetValue("response", out response)) { + string temp = null; do { + string nck = null; string ncl = null; @@ -406,7 +760,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!authparms.TryGetValue("nonce", out nonce) || nonce == null) { - Rest.Log.WarnFormat("{0} Authentication failed: nonce missing", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: nonce missing", MsgId); break; } @@ -417,7 +771,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { if (temp != opaque) { - Rest.Log.WarnFormat("{0} Authentication failed: bad opaque value", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: bad opaque value", MsgId); break; } } @@ -429,7 +783,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { if (temp != algorithm) { - Rest.Log.WarnFormat("{0} Authentication failed: bad algorithm value", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: bad algorithm value", MsgId); break; } } @@ -438,6 +792,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (authparms.TryGetValue("qop", out temp)) { + qop = temp.ToLower(); // replace with actual value used // if QOP was specified then @@ -445,7 +800,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!authparms.ContainsKey("cnonce")) { - Rest.Log.WarnFormat("{0} Authentication failed: cnonce missing", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: cnonce missing", MsgId); break; } @@ -453,7 +808,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!authparms.TryGetValue("nc", out nck) || nck == null) { - Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: cnonce counter missing", MsgId); break; } @@ -465,7 +820,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (Rest.Hex2Int(ncl) >= Rest.Hex2Int(nck)) { - Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId); + Rest.Log.WarnFormat("{0} Authentication failed: bad cnonce counter", MsgId); break; } cntable[nonce] = nck; @@ -474,179 +829,62 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { lock (cntable) cntable.Add(nonce, 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; - } } + else + { - // 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_BASIC) - { - - sbuilder.Append(Rest.AS_BASIC); - - if (realm != null) - { - sbuilder.Append(" realm=\""); - sbuilder.Append(realm); - sbuilder.Append("\""); - } - AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); - } - - sbuilder.Length = 0; - - if (scheme == null || scheme == Rest.AS_DIGEST) - { - 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); - } - - // We don;t know the userid that will be used - // so we cannot make any authentication domain - // assumptions. So the prefix will determine - // this. - - sbuilder.Append("domain="); - sbuilder.Append(Rest.CS_DQUOTE); - sbuilder.Append(qprefix); - sbuilder.Append(Rest.CS_DQUOTE); - - AddHeader(Rest.HttpHeaderWWWAuthenticate,sbuilder.ToString()); - } - } + qop = String.Empty; - /// - /// This method provides validation in support of the BASIC - /// authentication method. This is not normaly expected to be - /// used, but is included for completeness (and because I tried - /// it first). - /// + // if 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; + } + } - private bool Validate(string user, string pass) - { - Rest.Log.DebugFormat("{0} Simple User Validation", MsgId); + // Validate the supplied userid/password info - // Both values are required + authenticated = ValidateDigest(userName, nonce, cnonce, nck, authPrefix, response); - if (user == null || pass == null) - return false; + } + while (false); - // Eliminate any leading or trailing spaces - user = user.Trim(); + } - return vetPassword(user, pass); } /// - /// This mechanism is used by the digest authetnication mechanism + /// This mechanism is used by the digest authentication mechanism /// to return the user's password. In fact, because the OpenSim - /// user's passwords are already hashed, and the HTTP mechanism - /// does not supply an open password, the hashed passwords cannot - /// be used unless the cliemt has used the same salting mechanism - /// to has the password before using it in the authentication - /// algorithm. This is not inconceivable... + /// user's passwords are already hashed, and the HTTP mechanism + /// does not supply an open password, the hashed passwords cannot + /// be used unless the client has used the same salting mechanism + /// to has the password before using it in the authentication + /// algorithn. This is not inconceivable... /// private string getPassword(string user) { + int x; string first; string last; // Distinguish the parts, if necessary - + if ((x=user.IndexOf(Rest.C_SPACE)) != -1) { first = user.Substring(0,x); last = user.Substring(x+1); - } + } else { first = user; @@ -667,53 +905,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Normal User {1}", MsgId, user); return udata.PasswordHash; } - } - - /// - /// This is used by the BASIC authentication scheme to calculate - /// the double hash used by OpenSim to encode user's passwords. - /// It returns true, if the supplied password is actually correct. - /// If the specified user-id is not recognized, but the password - /// matches the God password, then this is accepted as an admin - /// session. - /// - - private bool vetPassword(string user, string pass) - { - int x; - string HA1; - string first; - string last; - - // Distinguish the parts, if necessary - - if ((x=user.IndexOf(Rest.C_SPACE)) != -1) - { - first = user.Substring(0,x); - last = user.Substring(x+1); - } - else - { - first = user; - last = String.Empty; - } - - UserProfileData udata = Rest.UserServices.GetUserProfile(first, last); - - // If we don;t recognize the user id, perhaps it is god? - if (udata == null) - return pass == Rest.GodKey; - - HA1 = HashToString(pass); - HA1 = HashToString(String.Format("{0}:{1}",HA1,udata.PasswordSalt)); - - return (0 == sc.Compare(HA1, udata.PasswordHash)); } // Validate the request-digest + 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; @@ -756,7 +955,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory HA2 = HashToString(patt); // Generate Digest - + if (qop != String.Empty) { patt = String.Format("{0}:{1}:{2}:{3}:{4}:{5}", HA1, nonce, nck, cnonce, qop, HA2); @@ -771,10 +970,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // 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)); @@ -789,62 +990,98 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Hash = <{1}>", MsgId, sbuilder.ToString()); return sbuilder.ToString(); + } +#endregion authentication_digest + +#region service_interface + + /// + /// Conditionally set a normal completion code. This allows a normal + /// execution path to default. + /// + internal void Complete() { - statusCode = Rest.HttpStatusCodeOK; - statusDescription = Rest.HttpStatusDescOK; + if (statusCode == 0) + { + statusCode = Rest.HttpStatusCodeOK; + } + } + + /// + /// Indicate a functionally-dependent conclusion to the + /// request. See Rest.cs for a list of possible values. + /// + + internal void Complete(int code) + { + statusCode = code; } + /// + /// Indicate that a request should be redirected, using + /// the HTTP completion codes. Permanent and temporary + /// redirections may be indicated. The supplied URL is + /// the new location of the resource. + /// + 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(statusCode, String.Empty, true); + } - // Fail for an arbitrary reason. Just a failure with - // headers. + /// + /// Fail for an arbitrary reason. Just a failure with + /// headers. The supplied message will be returned in the + /// message body. + /// - internal void Fail(int code, string message) + internal void Fail(int code) { - Fail(code, message, true); + Fail(code, String.Empty, false); } - // More adventurous. This failure also includes a - // specified entity. + /// + /// For the more adventurous. This failure also includes a + /// specified entity to be appended to the code-related + /// status string. + /// - internal void Fail(int code, string message, string data) + internal void Fail(int code, string addendum) { - buffer = null; - body = data; - Fail(code, message, false); + Fail(code, addendum, false); } - internal void Fail(int code, string message, bool reset) + internal void Fail(int code, string addendum, bool reset) { + statusCode = code; - statusDescription = message; + appendStatus(String.Format("({0}) : {1}", code, Rest.HttpStatusDesc[code])); - if (reset) + // Add any final addendum to the status information + + if (addendum != String.Empty) { - buffer = null; - SendHtml(message); - body = html; + appendStatus(String.Format(addendum)); } + // Help us understand why the request is being rejected + if (Rest.DEBUG) { Rest.Log.DebugFormat("{0} Request Failure State Dump", MsgId); @@ -864,42 +1101,61 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory fail = true; - Respond("Failure response"); + // Respond to the client's request, tag the response (for the + // benefit of trace) to indicate the reason. - RestException re = new RestException(message+" <"+code+">"); + Respond(String.Format("Failure response: ({0}) : {1}", + code, Rest.HttpStatusDesc[code])); + + // Finally initialize and the throw a RestException. All of the + // handler's infrastructure knows that this is a "normal" + // completion from a code point-of-view. + + RestException re = new RestException(Rest.HttpStatusDesc[code]+" <"+code+">"); re.statusCode = code; - re.statusDesc = message; + re.statusDesc = Rest.HttpStatusDesc[code]; re.httpmethod = method; re.httppath = path; throw re; + } // Reject this request internal void Reject() { - Fail(Rest.HttpStatusCodeNotImplemented, Rest.HttpStatusDescNotImplemented); + Fail(Rest.HttpStatusCodeNotImplemented, "request rejected (not implemented)"); } - // This MUST be called by an agent handler before it returns + // 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); + // We do this to try and make multiple Respond requests harmless, + // as it is sometimes convenient to isse a response without + // certain knowledge that it has not previously been done. + if (!handled) { + Rest.Log.DebugFormat("{0} Generating Response", MsgId); Rest.Log.DebugFormat("{0} Method is {1}", MsgId, method); - // A Head request can NOT have a body! + // A Head request can NOT have a body! So don't waste time on + // formatting if we're going to reject it anyway! + if (method != Rest.HEAD) { + Rest.Log.DebugFormat("{0} Response is not abbreviated", MsgId); // If the writer is non-null then we know that an XML @@ -924,18 +1180,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 (buffer == null && body != null) { - if (body != null && body.Length > 0) - { - Rest.Log.DebugFormat("{0} String-based entity", MsgId); - buffer = encoding.GetBytes(body); - } + buffer = encoding.GetBytes(body); + AddHeader("Content-Type",bodyType); } // OK, if the buffer contains something, regardless of how @@ -944,21 +1192,37 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (buffer != null) { Rest.Log.DebugFormat("{0} Buffer-based entity", MsgId); - response.ContentLength64 = buffer.Length; } else { - response.ContentLength64 = 0; + if (statusBody != String.Empty) + { + statusBody += Rest.statusTail; + buffer = encoding.GetBytes(statusBody); + AddHeader("Content-Type","text/html"); + } + else + { + statusBody = Rest.statusHead; + appendStatus(String.Format(": ({0}) {1}", + statusCode, Rest.HttpStatusDesc[statusCode])); + statusBody += Rest.statusTail; + buffer = encoding.GetBytes(statusBody); + AddHeader("Content-Type","text/html"); + } } + response.ContentLength64 = buffer.Length; + if (response.Headers.Get("Content-Encoding") == null) response.ContentEncoding = encoding; response.SendChunked = chunked; response.KeepAlive = keepAlive; + } - // Set the status code & description. If nothing has been stored, + // Set the status code & description. If nothing has been stored, // we consider that a success. if (statusCode == 0) @@ -972,7 +1236,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // For a redirect we need to set the relocation header accordingly - if (response.StatusCode == (int) Rest.HttpStatusCodeTemporaryRedirect || + if (response.StatusCode == (int) Rest.HttpStatusCodeTemporaryRedirect || response.StatusCode == (int) Rest.HttpStatusCodePermanentRedirect) { Rest.Log.DebugFormat("{0} Re-direct location is {1}", MsgId, redirectLocation); @@ -981,18 +1245,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // And include the status description if provided. - if (statusDescription != null) - { - Rest.Log.DebugFormat("{0} Status description is {1}", MsgId, statusDescription); - response.StatusDescription = statusDescription; - } + response.StatusDescription = Rest.HttpStatusDesc[response.StatusCode]; // Finally we send back our response. // 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 + // here on down probably leaves the response // element unusable by anyone else. handled = true; @@ -1007,7 +1267,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (buffer != null && buffer.Length != 0) { - Rest.Log.DebugFormat("{0} Entity buffer, length = {1} : <{2}>", + Rest.Log.DebugFormat("{0} Entity buffer, length = {1} : <{2}>", MsgId, buffer.Length, encoding.GetString(buffer)); response.OutputStream.Write(buffer, 0, buffer.Length); } @@ -1016,35 +1276,36 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Closing output stream", MsgId); response.OutputStream.Close(); + } Rest.Log.DebugFormat("{0} Respond EXIT, handled = {1}, reason = {2}", MsgId, handled, reason); return handled; + } - // Add a header to the table. We need to allow - // multiple instances of many of the headers. - // If the + /// + /// These methods allow a service provider to manipulate the + /// request/response headers. The DumpHeaders method is intended + /// for problem diagnosis. + /// internal void AddHeader(string hdr, string data) { if (Rest.DEBUG) { - Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>", + Rest.Log.DebugFormat("{0} Adding header: <{1}: {2}>", MsgId, hdr, data); if (response.Headers.Get(hdr) != null) { - Rest.Log.DebugFormat("{0} Multipe {1} headers will be generated>", + Rest.Log.DebugFormat("{0} Multipe {1} headers will be generated>", MsgId, hdr); } } response.Headers.Add(hdr, data); } - // Keep explicit track of any headers which - // are to be removed. - internal void RemoveHeader(string hdr) { if (Rest.DEBUG) @@ -1052,29 +1313,69 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Removing header: <{1}>", MsgId, hdr); if (response.Headers.Get(hdr) == null) { - Rest.Log.DebugFormat("{0} No such header existed", + Rest.Log.DebugFormat("{0} No such header existed", MsgId, hdr); } } response.Headers.Remove(hdr); } - /// - /// Dump headers that will be generated in the response - /// - internal void DumpHeaders() { if (Rest.DEBUG) { for (int i=0;i /// Helper methods for deconstructing and reconstructing /// URI path data. @@ -1082,6 +1383,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory private void initUrl() { + uri = request.Url; if (query == null) @@ -1096,19 +1398,19 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { path = uri.AbsolutePath; if (path.EndsWith(Rest.UrlPathSeparator)) - path = path.Substring(0, path.Length-1); + path = path.Substring(0,path.Length-1); } // If we succeeded in getting a path, perform any // additional pre-processing required. - if (path != null) + if (path != null) { if (Rest.ExtendedEscape) { // Handle "+". Not a standard substitution, but // common enough... - path = path.Replace(Rest.C_PLUS, Rest.C_SPACE); + path = path.Replace(Rest.C_PLUS,Rest.C_SPACE); } pathNodes = path.Split(Rest.CA_PATHSEP); } @@ -1126,10 +1428,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory hostname = uri.Host; port = uri.Port; + } - internal int initParameters(int prfxlen) + private int initParameters(int prfxlen) { + if (prfxlen < path.Length-1) { parameters = path.Substring(prfxlen+1).Split(Rest.CA_PATHSEP); @@ -1138,139 +1442,23 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { 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(request.InputStream,settings); - } - - private void Flush() - { - byte[] dbuffer = new byte[8192]; - while (request.InputStream.Read(dbuffer,0,dbuffer.Length) != 0); - } - - // 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; +#endregion internal_methods - 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 index e8c0ee8..fa22481 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/Rest.cs @@ -23,7 +23,7 @@ * 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; @@ -38,9 +38,11 @@ using Nini.Config; namespace OpenSim.ApplicationPlugins.Rest.Inventory { + public class Rest { - internal static readonly log4net.ILog Log = + + internal static readonly log4net.ILog Log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); internal static bool DEBUG = Log.IsDebugEnabled; @@ -53,7 +55,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// RestHandler class during start-up. ///
- internal static RestHandler Plugin = null; + internal static IRestHandler Plugin = null; internal static OpenSimBase main = null; internal static CommunicationsManager Comms = null; internal static IInventoryServices InventoryServices = null; @@ -66,10 +68,47 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory internal static bool Secure = true; internal static bool ExtendedEscape = true; internal static bool DumpAsset = false; + internal static bool Fill = true; + internal static bool FlushEnabled = true; internal static string Realm = "REST"; - 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 + /// + /// HTTP requires that status information be generated for PUT + /// and POST opertaions. This is in support of that. The + /// operation verb gets substituted into the first string, + /// and the completion code is inserted into the tail. The + /// strings are put here to encourage consistency. + /// + + internal static string statusHead = "{0} status"; + internal static string statusTail = ""; + + internal static Dictionary HttpStatusDesc; + + static Rest() + { + HttpStatusDesc = new Dictionary(); + if (HttpStatusCodeArray.Length != HttpStatusDescArray.Length) + { + Log.ErrorFormat("{0} HTTP Status Code and Description arrays do not match"); + throw new Exception("HTTP Status array discrepancy"); + } + + // Repackage the data into something more tractable. The sparse + // nature of HTTP return codes makes an array a bad choice. + + for (int i=0; i - + // 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. @@ -136,7 +175,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 = "/"; @@ -145,7 +184,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 }; @@ -203,53 +242,97 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory public const int HttpStatusCodeGatewayTimeout = 504; public const int HttpStatusCodeHttpVersionError = 505; - // HTTP Status Descriptions (in status code order) + public static readonly int[] HttpStatusCodeArray = { + HttpStatusCodeContinue, + HttpStatusCodeSwitchingProtocols, + HttpStatusCodeOK, + HttpStatusCodeCreated, + HttpStatusCodeAccepted, + HttpStatusCodeNonAuthoritative, + HttpStatusCodeNoContent, + HttpStatusCodeResetContent, + HttpStatusCodePartialContent, + HttpStatusCodeMultipleChoices, + HttpStatusCodePermanentRedirect, + HttpStatusCodeFound, + HttpStatusCodeSeeOther, + HttpStatusCodeNotModified, + HttpStatusCodeUseProxy, + HttpStatusCodeReserved306, + HttpStatusCodeTemporaryRedirect, + HttpStatusCodeBadRequest, + HttpStatusCodeNotAuthorized, + HttpStatusCodePaymentRequired, + HttpStatusCodeForbidden, + HttpStatusCodeNotFound, + HttpStatusCodeMethodNotAllowed, + HttpStatusCodeNotAcceptable, + HttpStatusCodeProxyAuthenticate, + HttpStatusCodeTimeOut, + HttpStatusCodeConflict, + HttpStatusCodeGone, + HttpStatusCodeLengthRequired, + HttpStatusCodePreconditionFailed, + HttpStatusCodeEntityTooLarge, + HttpStatusCodeUriTooLarge, + HttpStatusCodeUnsupportedMedia, + HttpStatusCodeRangeNotSatsified, + HttpStatusCodeExpectationFailed, + HttpStatusCodeServerError, + HttpStatusCodeNotImplemented, + HttpStatusCodeBadGateway, + HttpStatusCodeServiceUnavailable, + HttpStatusCodeGatewayTimeout, + HttpStatusCodeHttpVersionError + }; - 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 Status Descriptions (in status code order) + // This array must be kept strictly consistent with respect + // to the status code array above. + + public static readonly string[] HttpStatusDescArray = { + "Continue Request", + "Switching Protocols", + "OK", + "CREATED", + "ACCEPTED", + "NON-AUTHORITATIVE INFORMATION", + "NO CONTENT", + "RESET CONTENT", + "PARTIAL CONTENT", + "MULTIPLE CHOICES", + "PERMANENT REDIRECT", + "FOUND", + "SEE OTHER", + "NOT MODIFIED", + "USE PROXY", + "RESERVED CODE 306", + "TEMPORARY REDIRECT", + "BAD REQUEST", + "NOT AUTHORIZED", + "PAYMENT REQUIRED", + "FORBIDDEN", + "NOT FOUND", + "METHOD NOT ALLOWED", + "NOT ACCEPTABLE", + "PROXY AUTHENTICATION REQUIRED", + "TIMEOUT", + "CONFLICT", + "GONE", + "LENGTH REQUIRED", + "PRECONDITION FAILED", + "ENTITY TOO LARGE", + "URI TOO LARGE", + "UNSUPPORTED MEDIA", + "RANGE NOT SATISFIED", + "EXPECTATION FAILED", + "SERVER ERROR", + "NOT IMPLEMENTED", + "BAD GATEWAY", + "SERVICE UNAVAILABLE", + "GATEWAY TIMEOUT", + "HTTP VERSION NOT SUPPORTED" + }; // HTTP Headers @@ -309,7 +392,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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"; @@ -357,7 +440,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory int val = 0; int sum = 0; string tmp = null; - + if (hex != null) { tmp = hex.ToLower(); @@ -372,40 +455,21 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } 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()); + return StringToBase64(CreationDate + Guid.NewGuid().ToString()); } // Dump he specified data stream; public static void Dump(byte[] data) { + char[] buffer = new char[Rest.DumpLineSize]; int cc = 0; @@ -415,7 +479,6 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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")); @@ -431,6 +494,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Console.Write(" |"+(new String(buffer))+"|"); cc = 0; } + } // Finish off any incomplete line @@ -440,30 +504,33 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory for (int i = cc ; i < Rest.DumpLineSize; i++) { if (i % 4 == 0) Console.Write(" "); - // if (i%16 == 0) Console.Write(" "); - Console.Write(" "); + Console.Write(" "); buffer[i % Rest.DumpLineSize] = ' '; } Console.WriteLine(" |"+(new String(buffer))+"|"); } else { - Console.Write("\n"); + 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) - { + public RestException(string msg) : base(msg) + { } } + } diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs index 85748fa..a40d197 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestAssetServices.cs @@ -23,6 +23,7 @@ * 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; @@ -39,8 +40,10 @@ using OpenSim.Framework.Communications.Cache; namespace OpenSim.ApplicationPlugins.Rest.Inventory { + public class RestAssetServices : IRest { + private bool enabled = false; private string qPrefix = "assets"; @@ -49,6 +52,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory public RestAssetServices() { + Rest.Log.InfoFormat("{0} Asset services initializing", MsgId); Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version); @@ -69,6 +73,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory enabled = true; Rest.Log.InfoFormat("{0} Asset services initialization complete", MsgId); + } // Post-construction, pre-enabled initialization opportunity @@ -79,7 +84,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } // Called by the plug-in to halt REST processing. Local processing is - // disabled, and control blocks until all current processing has + // disabled, and control blocks until all current processing has // completed. No new processing will be started public void Close() @@ -106,14 +111,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory private void DoAsset(RequestData rparm) { - if (!enabled) - return; + + 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 + // 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 @@ -124,14 +129,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // 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 + // Instead we look at the headers ourselves and // handle authentication directly. - + try { if (!rdata.IsAuthenticated) { - rdata.Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); + rdata.Fail(Rest.HttpStatusCodeNotAuthorized, String.Format("user \"{0}\" could not be authenticated")); } } catch (RestException e) @@ -139,13 +144,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (e.statusCode == Rest.HttpStatusCodeNotAuthorized) { Rest.Log.WarnFormat("{0} User not authenticated", MsgId); - Rest.Log.DebugFormat("{0} Authorization header: {1}", 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, + Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization")); } throw (e); @@ -155,7 +160,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // the parameters we need, fail the request. Parameters do NOT include // any supplied query values. - if (rdata.parameters.Length > 0) + if (rdata.Parameters.Length > 0) { switch (rdata.method) { @@ -168,26 +173,27 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory case "post" : case "delete" : default : - Rest.Log.WarnFormat("{0} Asset: Method not supported: {1}", + Rest.Log.WarnFormat("{0} Asset: Method not supported: {1}", MsgId, rdata.method); - rdata.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + rdata.Fail(Rest.HttpStatusCodeBadRequest,String.Format("method <{0}> not supported", rdata.method)); break; } } else { Rest.Log.WarnFormat("{0} Asset: No agent information provided", MsgId); - rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "no agent information provided"); } 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); @@ -195,14 +201,16 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // The only parameter we accept is an LLUUID for // the asset - if (rdata.parameters.Length == 1) + if (rdata.Parameters.Length == 1) { - LLUUID uuid = new LLUUID(rdata.parameters[0]); + + 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]); + + Rest.Log.DebugFormat("{0} Asset located <{1}>", MsgId, rdata.Parameters[0]); rdata.initXmlWriter(); @@ -218,17 +226,18 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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.Fail(Rest.HttpStatusCodeNotFound, "invalid parameters"); } } rdata.Complete(); rdata.Respond("Asset " + rdata.method + ": Normal completion"); + } private void DoPut(AssetRequestData rdata) @@ -238,7 +247,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // The only parameter we accept is an LLUUID for // the asset - if (rdata.parameters.Length == 1) + if (rdata.Parameters.Length == 1) { rdata.initXmlReader(); XmlReader xml = rdata.reader; @@ -246,12 +255,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!xml.ReadToFollowing("Asset")) { Rest.Log.DebugFormat("{0} Invalid request data: <{1}>", MsgId, rdata.path); - rdata.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + rdata.Fail(Rest.HttpStatusCodeBadRequest,"invalid request data"); } AssetBase asset = new AssetBase(); - asset.ID = rdata.parameters[0]; + asset.ID = rdata.Parameters[0]; asset.Name = xml.GetAttribute("name"); asset.Description = xml.GetAttribute("desc"); asset.Type = SByte.Parse(xml.GetAttribute("type")); @@ -264,12 +272,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory else { Rest.Log.DebugFormat("{0} Invalid parameters: <{1}>", MsgId, rdata.path); - rdata.Fail(Rest.HttpStatusCodeNotFound, - Rest.HttpStatusDescNotFound); + rdata.Fail(Rest.HttpStatusCodeNotFound, "invalid parameters"); } rdata.Complete(); rdata.Respond("Asset " + rdata.method + ": Normal completion"); + } internal class AssetRequestData : RequestData @@ -279,5 +287,6 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { } } + } } diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs index 9853f16..cb80846 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestHandler.cs @@ -23,6 +23,7 @@ * 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; @@ -34,8 +35,27 @@ using OpenSim.ApplicationPlugins.Rest; namespace OpenSim.ApplicationPlugins.Rest.Inventory { - public class RestHandler : RestPlugin, IHttpAgentHandler + + /// + /// The class signature reveals the roles that RestHandler plays. + /// + /// [1] It is a sub-class of RestPlugin. It inherits and extends + /// the functionality of this class, constraining it to the + /// specific needs of this REST implementation. This relates + /// to the plug-in mechanism supported by OpenSim, the specifics + /// of which are mostly hidden by RestPlugin. + /// [2] IRestHandler describes the interface that this class + /// exports to service implementations. This is the services + /// management interface. + /// [3] IHttpAgentHandler describes the interface that is exported + /// to the BaseHttpServer in support of this particular HTTP + /// processing model. This is the request interface of the + /// handler. + /// + + public class RestHandler : RestPlugin, IRestHandler, IHttpAgentHandler { + /// /// The handler delegates are not noteworthy. The allocator allows /// a given handler to optionally subclass the base RequestData @@ -43,8 +63,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// needed. /// - internal delegate void RestMethodHandler(RequestData rdata); - internal delegate RequestData RestMethodAllocator(OSHttpRequest request, OSHttpResponse response); + // internal delegate void RestMethodHandler(RequestData rdata); + // internal delegate RequestData RestMethodAllocator(OSHttpRequest request, OSHttpResponse response); // Handler tables: both stream and REST are supported. The path handlers and their // respective allocators are stored in separate tables. @@ -64,10 +84,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// /// 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 + /// 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 + /// all of the build-time flexibility of a modular approach /// while not introducing yet-another module loader. Note that /// multiple assembles can still be built, each with its own set /// of handlers. Examples of services classes are RestInventoryServices @@ -76,12 +96,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory static RestHandler() { + Module[] mods = Assembly.GetExecutingAssembly().GetModules(); foreach (Module m in mods) { Type[] types = m.GetTypes(); - foreach (Type t in types) + foreach (Type t in types) { try { @@ -97,6 +118,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } } } + } #endregion local static state @@ -105,13 +127,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// /// This routine loads all of the handlers discovered during - /// instance initialization. + /// instance initialization. /// A table of all loaded and successfully constructed handlers /// is built, and this table is then used by the constructor to /// initialize each of the handlers in turn. /// NOTE: The loading process does not automatically imply that - /// the handler has registered any kind of an interface, that - /// may be (optionally) done by the handler either during + /// the handler has registered any kind of an interface, that + /// may be (optionally) done by the handler either during /// construction, or during initialization. /// /// I was not able to make this code work within a constructor @@ -124,6 +146,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { if (!handlersLoaded) { + ConstructorInfo ci; Object ht; @@ -154,8 +177,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // Name is used to differentiate the message header. - public override string Name - { + public override string Name + { get { return "HANDLER"; } } @@ -168,15 +191,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // We have to rename these because we want // to be able to share the values with other - // classes in our assembly and the base + // classes in our assembly and the base // names are protected. - internal string MsgId + public string MsgId { get { return base.MsgID; } } - internal string RequestId + public string RequestId { get { return base.RequestID; } } @@ -198,6 +221,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { try { + // This plugin will only be enabled if the broader // REST plugin mechanism is enabled. @@ -208,7 +232,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // IsEnabled is implemented by the base class and // reflects an overall RestPlugin status - if (!IsEnabled) + if (!IsEnabled) { Rest.Log.WarnFormat("{0} Plugins are disabled", MsgId); return; @@ -221,7 +245,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.main = openSim; Rest.Plugin = this; - Rest.Comms = App.CommunicationsManager; + Rest.Comms = Rest.main.CommunicationsManager; Rest.UserServices = Rest.Comms.UserService; Rest.InventoryServices = Rest.Comms.InventoryService; Rest.AssetServices = Rest.Comms.AssetCache; @@ -234,7 +258,9 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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.Fill = Rest.Config.GetBoolean("path-fill",true); Rest.DumpLineSize = Rest.Config.GetInt("dump-line-size",32); + Rest.FlushEnabled = Rest.Config.GetBoolean("flush-on-error",true); Rest.Log.InfoFormat("{0} Authentication is {1}required", MsgId, (Rest.Authenticate ? "" : "not ")); @@ -248,6 +274,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.InfoFormat("{0} Dumping of asset data is {1}enabled", MsgId, (Rest.DumpAsset ? "" : "not ")); + // The supplied prefix MUST be absolute + + if (Rest.Prefix.Substring(0,1) != Rest.UrlPathSeparator) + Rest.Prefix = Rest.UrlPathSeparator+Rest.Prefix; + // If data dumping is requested, report on the chosen line // length. @@ -257,15 +288,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.DumpLineSize); } - // Load all of the handlers present in the + // 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 + // 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 + // 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. @@ -308,12 +339,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { 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 + /// 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 @@ -322,6 +354,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory public override void Close() { + Rest.Log.InfoFormat("{0} Plugin is terminating", MsgId); try @@ -329,11 +362,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory RemoveAgentHandler(Rest.Name, this); } catch (KeyNotFoundException){} - + foreach (IRest handler in handlers) { handler.Close(); } + } #endregion overriding methods @@ -352,25 +386,57 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { string path = request.RawUrl; + Rest.Log.DebugFormat("{0} Match ENTRY", MsgId); + try { foreach (string key in pathHandlers.Keys) { + Rest.Log.DebugFormat("{0} Match testing {1} against agent prefix <{2}>", MsgId, path, key); + + // Note that Match will not necessarily find the handler that will + // actually be used - it does no test for the "closest" fit. It + // simply reflects that at least one possible handler exists. + if (path.StartsWith(key)) { - return (path.Length == key.Length || - path.Substring(key.Length, 1) == Rest.UrlPathSeparator); + Rest.Log.DebugFormat("{0} Matched prefix <{1}>", MsgId, key); + + // This apparently odd evaluation is needed to prevent a match + // on anything other than a URI token boundary. Otherwise we + // may match on URL's that were not intended for this handler. + + 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) { + + Rest.Log.DebugFormat("{0} Match testing {1} against stream prefix <{2}>", MsgId, path, key); + + // Note that Match will not necessarily find the handler that will + // actually be used - it does no test for the "closest" fit. It + // simply reflects that at least one possible handler exists. + if (path.StartsWith(key)) { - return true; + Rest.Log.DebugFormat("{0} Matched prefix <{1}>", MsgId, key); + + // This apparently odd evaluation is needed to prevent a match + // on anything other than a URI token boundary. Otherwise we + // may match on URL's that were not intended for this handler. + + return ( path.Length == key.Length || + path.Substring(key.Length,1) == Rest.UrlPathSeparator); + } } + } catch (Exception e) { @@ -404,7 +470,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory for (int i = 0; i < request.Headers.Count; i++) { - Rest.Log.DebugFormat("{0} Header [{1}] : <{2}> = <{3}>", + 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); @@ -415,8 +481,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory try { - handled = FindPathHandler(request, response) || - FindStreamHandler(request, response); + handled = ( FindPathHandler(request, response) || + FindStreamHandler(request, response) ); } catch (Exception e) { @@ -430,6 +496,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} EXIT", MsgId); return handled; + } #endregion interface methods @@ -477,6 +544,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } return rdata.handled; + } /// @@ -489,12 +557,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory public void AddStreamHandler(string httpMethod, string path, RestMethod method) { + if (!IsEnabled) { return; } - if (!path.StartsWith(Rest.Prefix)) + if (!path.StartsWith(Rest.Prefix)) { path = String.Format("{0}{1}", Rest.Prefix, path); } @@ -512,6 +581,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.WarnFormat("{0} Ignoring duplicate handler for {1}", MsgId, path); } + } /// @@ -526,9 +596,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory internal bool FindPathHandler(OSHttpRequest request, OSHttpResponse response) { + RequestData rdata = null; string bestMatch = null; - + if (!IsEnabled) { return false; @@ -551,6 +622,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!String.IsNullOrEmpty(bestMatch)) { + rdata = pathAllocators[bestMatch](request, response); Rest.Log.DebugFormat("{0} Path based REST handler matched with <{1}>", MsgId, bestMatch); @@ -559,7 +631,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { pathHandlers[bestMatch](rdata); } - + // A plugin generated error indicates a request-related error // that has been handled by the plugin. @@ -567,9 +639,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.WarnFormat("{0} Request failed: {1}", MsgId, r.Message); } + } return (rdata == null) ? false : rdata.handled; + } /// @@ -577,8 +651,9 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// path as a key. If an entry already exists, it is replaced by the new one. /// - internal void AddPathHandler(RestMethodHandler mh, string path, RestMethodAllocator ra) + public void AddPathHandler(RestMethodHandler mh, string path, RestMethodAllocator ra) { + if (!IsEnabled) { return; @@ -600,6 +675,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory pathHandlers.Add(path, mh); pathAllocators.Add(path, ra); + } } + } diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs index 8a0eba5..6a0fdf2 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestInventoryServices.cs @@ -23,6 +23,7 @@ * 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; @@ -41,22 +42,30 @@ using Nini.Config; namespace OpenSim.ApplicationPlugins.Rest.Inventory { + public class RestInventoryServices : IRest { + + private static readonly int PARM_USERID = 0; + private static readonly int PARM_PATH = 1; + private bool enabled = false; private string qPrefix = "inventory"; + private static readonly string PRIVATE_ROOT_NAME = "My Inventory"; + /// - /// A simple constructor is used to handle any once-only - /// initialization of working classes. + /// The constructor makes sure that the service prefix is absolute + /// and the registers the service handler and the allocator. /// public RestInventoryServices() { + Rest.Log.InfoFormat("{0} Inventory services initializing", MsgId); Rest.Log.InfoFormat("{0} Using REST Implementation Version {1}", MsgId, Rest.Version); - // If a relative path was specified for the handler's domain, + // If a relative path was specified for the handler's domain, // add the standard prefix to make it absolute, e.g. /admin if (!qPrefix.StartsWith(Rest.UrlPathSeparator)) @@ -73,6 +82,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory enabled = true; Rest.Log.InfoFormat("{0} Inventory services initialization complete", MsgId); + } /// @@ -85,9 +95,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } /// - /// Called by the plug-in to halt REST processing. Local processing is - /// disabled, and control blocks until all current processing has - /// completed. No new processing will be started + /// Called by the plug-in to halt service processing. Local processing is + /// disabled. /// public void Close() @@ -114,6 +123,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// completes. All request-instance specific state is kept here. This /// is registered when this service provider is registered. /// + /// Inbound HTTP request information + /// Outbound HTTP request information + /// REST service domain prefix + /// A RequestData instance suitable for this service private RequestData Allocate(OSHttpRequest request, OSHttpResponse response) { @@ -123,12 +136,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// /// This method is registered with the handler when this service provider /// is initialized. It is called whenever the plug-in identifies this service - /// provider as the best match. - /// It handles all aspects of inventory REST processing. + /// provider as the best match for a given request. + /// It handles all aspects of inventory REST processing, i.e. /admin/inventory /// + /// A consolidated HTTP request work area private void DoInventory(RequestData hdata) { + InventoryRequestData rdata = (InventoryRequestData) hdata; Rest.Log.DebugFormat("{0} DoInventory ENTRY", MsgId); @@ -140,7 +155,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory return; } - // Now that we know this is a serious attempt to + // 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 @@ -151,14 +166,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // 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 + // Instead we look at the headers ourselves and // handle authentication directly. - + try { if (!rdata.IsAuthenticated) { - rdata.Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); + rdata.Fail(Rest.HttpStatusCodeNotAuthorized,String.Format("user \"{0}\" could not be authenticated", rdata.userName)); } } catch (RestException e) @@ -178,43 +193,43 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} Authenticated {1}", MsgId, rdata.userName); - /// - /// We can only get here if we are authorized - /// - /// The requestor may have specified an LLUUID or - /// a conjoined FirstName LastName string. We'll - /// try both. If we fail with the first, UUID, - /// attempt, we try the other. As an example, the - /// URI for a valid inventory request might be: - /// - /// http://:/admin/inventory/Arthur Dent - /// - /// Indicating that this is an inventory request for - /// an avatar named Arthur Dent. This is ALl that is - /// required to designate a GET for an entire - /// inventory. - /// - + // We can only get here if we are authorized + // + // The requestor may have specified an LLUUID or + // a conjoined FirstName LastName string. We'll + // try both. If we fail with the first, UUID, + // attempt, we try the other. As an example, the + // URI for a valid inventory request might be: + // + // http://:/admin/inventory/Arthur Dent + // + // Indicating that this is an inventory request for + // an avatar named Arthur Dent. This is ALL that is + // required to designate a GET for an entire + // inventory. + // // Do we have at least a user agent name? - if (rdata.parameters.Length < 1) + if (rdata.Parameters.Length < 1) { Rest.Log.WarnFormat("{0} Inventory: No user agent identifier specified", MsgId); - rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest+": No user identity specified"); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "no user identity specified"); } // The first parameter MUST be the agent identification, either an LLUUID - // or a space-separated First-name Last-Name specification. + // or a space-separated First-name Last-Name specification. We check for + // an LLUUID first, if anyone names their character using a valid LLUUID + // that identifies another existing avatar will cause this a problem... try { - rdata.uuid = new LLUUID(rdata.parameters[0]); + rdata.uuid = new LLUUID(rdata.Parameters[PARM_USERID]); Rest.Log.DebugFormat("{0} LLUUID supplied", MsgId); rdata.userProfile = Rest.UserServices.GetUserProfile(rdata.uuid); } catch { - string[] names = rdata.parameters[0].Split(Rest.CA_SPACE); + string[] names = rdata.Parameters[PARM_USERID].Split(Rest.CA_SPACE); if (names.Length == 2) { Rest.Log.DebugFormat("{0} Agent Name supplied [2]", MsgId); @@ -222,23 +237,23 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } else { - Rest.Log.DebugFormat("{0} A Valid UUID or both first and last names must be specified", MsgId); - rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest+": invalid user identity"); + Rest.Log.WarnFormat("{0} A Valid UUID or both first and last names must be specified", MsgId); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "invalid user identity"); } } - // If the user rpofile is null then either the server is broken, or the + // If the user profile is null then either the server is broken, or the // user is not known. We always assume the latter case. if (rdata.userProfile != null) { - Rest.Log.DebugFormat("{0} Profile obtained for agent {1} {2}", + 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+": unrecognized user identity"); + Rest.Log.WarnFormat("{0} No profile for {1}", MsgId, rdata.path); + rdata.Fail(Rest.HttpStatusCodeNotFound, "unrecognized user identity"); } // If we get to here, then we have effectively validated the user's @@ -254,17 +269,18 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // response is not recieved in a timely fashion. 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}", + 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}", + Rest.Log.DebugFormat("{0} Inventory catalog requested for {1} {2}", MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); lock (rdata) @@ -277,16 +293,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (rdata.root == null) { - Rest.Log.DebugFormat("{0} Inventory is not available [1] for agent {1} {2}", + Rest.Log.WarnFormat("{0} Inventory is not available [1] for agent {1} {2}", MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); - rdata.Fail(Rest.HttpStatusCodeServerError,Rest.HttpStatusDescServerError+": inventory retrieval failed"); + rdata.Fail(Rest.HttpStatusCodeServerError, "inventory retrieval failed"); } + } else { - Rest.Log.DebugFormat("{0} Inventory is not available for agent [3] {1} {2}", + Rest.Log.WarnFormat("{0} Inventory is not locally available for agent {1} {2}", MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); - rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound+": no inventory for user"); + rdata.Fail(Rest.HttpStatusCodeNotFound, "no local inventory for user"); } // If we get here, then we have successfully retrieved the user's information @@ -294,34 +311,35 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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.HEAD : // Do the processing, set the status code, suppress entity + DoGet(rdata); + rdata.buffer = null; + break; - case Rest.PUT : // Add new information - DoPut(rdata); - break; + case Rest.GET : // Do the processing, set the status code, return entity + DoGet(rdata); + break; - case Rest.POST : // Update (replace) - DoPost(rdata); - break; + case Rest.PUT : // Update named element + DoUpdate(rdata); + break; - case Rest.DELETE : // Delete information - DoDelete(rdata); - break; + case Rest.POST : // Add new information to identified context. + DoExtend(rdata); + break; - default : - Rest.Log.DebugFormat("{0} Method {1} not supported for {2}", - MsgId, rdata.method, rdata.path); - rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed, - Rest.HttpStatusDescMethodNotAllowed+": "+rdata.method+" not supported"); - break; + case Rest.DELETE : // Delete information + DoDelete(rdata); + break; + + default : + Rest.Log.WarnFormat("{0} Method {1} not supported for {2}", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeMethodNotAllowed, rdata.method+" not supported"); + break; } + } #endregion Interface @@ -333,84 +351,97 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// Any remaining parameters are used to locate the /// corresponding subtree based upon node name. /// + /// HTTP service request work area private void DoGet(InventoryRequestData rdata) { + rdata.initXmlWriter(); rdata.writer.WriteStartElement(String.Empty,"Inventory",String.Empty); - // If there was only one parameter, then the entire - // inventory is being requested. - - if (rdata.parameters.Length == 1) - { - formatInventory(rdata, rdata.root, String.Empty); - } - // If there are additional parameters, then these represent // a path relative to the root of the inventory. This path - // must be traversed before we format the sub-tree thus + // must be traversed before we format the sub-tree thus // identified. - else - { - traverseInventory(rdata, rdata.root, 1); - } + traverse(rdata, rdata.root, PARM_PATH); + + // Close all open elements rdata.writer.WriteFullEndElement(); + // Indicate a successful request + rdata.Complete(); - rdata.Respond("Inventory " + rdata.method + ": Normal completion"); - } + // Send the response to the user. The body will be implicitly + // constructed from the result of the XML writer. + + rdata.Respond(String.Format("Inventory {0} Normal completion", rdata.method)); + + } + /// /// In the case of the inventory, and probably in general, /// the distinction between PUT and POST is not always - /// easy to discern. Adding a directory can be viewed as + /// easy to discern. The standard is badly worded in places, + /// and adding a node to a hierarchy can be viewed as /// an addition, or as a modification to the inventory as - /// a whole. This is exacerbated by a lack of consistency - /// across different implementations. + /// a whole. This is exacerbated by an unjustified lack of + /// consistency across different implementations. /// - /// For OpenSim POST is an update and PUT is an addition. + /// For OpenSim PUT is an update and POST is an addition. This + /// is the behavior required by the HTTP specification and + /// therefore as required by REST. /// - /// The best way to exaplain the distinction is to + /// The best way to explain the distinction is to /// consider the relationship between the URI and the - /// entity in question. For POST, the URI identifies the - /// entity to be modified or replaced. - /// If the operation is PUT,then the URI describes the + /// enclosed entity. For PUT, the URI identifies the + /// actual entity to be modified or replaced, i.e. the + /// enclosed entity. + /// + /// If the operation is POST,then the URI describes the /// context into which the new entity will be added. /// /// As an example, suppose the URI contains: /// /admin/inventory/Clothing /// - /// A POST request will result in some modification of - /// the folder or item named "Clothing". Whereas a PUT - /// request will add some new information into the + /// A PUT request will normally result in some modification of + /// the folder or item named "Clothing". Whereas a POST + /// request will normally add some new information into the /// content identified by Clothing. It follows from this - /// that for PUT, the element identified by the URI must + /// that for POST, the element identified by the URI MUST /// be a folder. /// /// - /// PUT adds new information to the inventory in the + /// POST adds new information to the inventory in the /// context identified by the URI. /// + /// HTTP service request work area - private void DoPut(InventoryRequestData rdata) + private void DoExtend(InventoryRequestData rdata) { + + bool created = false; + bool modified = false; + string newnode = String.Empty; + // Resolve the context node specified in the URI. Entity - // data will be ADDED beneath this node. + // data will be ADDED beneath this node. rdata already contains + // information about the current content of the user's + // inventory. - Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); + Object InventoryNode = getInventoryNode(rdata, rdata.root, PARM_PATH, Rest.Fill); // 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 + // 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 a type of folder is supported + // has identified a folder. So only a type of folder is supported // in this case. if (typeof(InventoryFolderBase) == InventoryNode.GetType() || @@ -430,16 +461,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // [1] A (possibly empty) set of folders. // [2] A (possibly empty) set of items. // [3] A (possibly empty) set of assets. - // If all of these are empty, then the PUT is a harmless no-operation. + // If all of these are empty, then the POST is a harmless no-operation. XmlInventoryCollection entity = ReconstituteEntity(rdata); // Inlined assets can be included in entity. These must be incorporated into - // the asset database before we attempt to update the inventory. If anything + // the asset database before we attempt to update the inventory. If anything // fails, return a failure to requestor. if (entity.Assets.Count > 0) { + Rest.Log.DebugFormat("{0} Adding {1} assets to server", MsgId, entity.Assets.Count); @@ -449,11 +481,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory MsgId, asset.ID, asset.Type, asset.Name); Rest.AssetServices.AddAsset(asset); - if (Rest.DumpAsset) + created = true; + rdata.appendStatus(String.Format("

Created asset {0}, UUID {1}

", + asset.Name, asset.ID)); + + if (Rest.DEBUG && Rest.DumpAsset) { Rest.Dump(asset.Data); } + } + } // Modify the context using the collection of folders and items @@ -461,6 +499,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory foreach (InventoryFolderBase folder in entity.Folders) { + InventoryFolderBase found; // If the parentID is zero, then this folder is going @@ -468,9 +507,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // may have already set the parent ID explicitly, in which // case we don't have to do it here. - if (folder.ParentID == LLUUID.Zero) + if (folder.ParentID == LLUUID.Zero || folder.ParentID == context.ID) { + if (newnode != String.Empty) + { + Rest.Log.DebugFormat("{0} Too many resources", MsgId); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "only one root entity is allowed"); + } folder.ParentID = context.ID; + newnode = folder.Name; } // Search the existing inventory for an existing entry. If @@ -496,12 +541,22 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.DebugFormat("{0} Updating existing folder", MsgId); Rest.InventoryServices.MoveFolder(folder); + + modified = true; + rdata.appendStatus(String.Format("

Created folder {0}, UUID {1}

", + folder.Name, folder.ID)); } else { Rest.Log.DebugFormat("{0} Adding new folder", MsgId); Rest.InventoryServices.AddFolder(folder); + + created = true; + rdata.appendStatus(String.Format("

Modified folder {0}, UUID {1}

", + folder.Name, folder.ID)); + } + } // Now we repeat a similar process for the items included @@ -509,6 +564,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory foreach (InventoryItemBase item in entity.Items) { + InventoryItemBase found = null; // If the parentID is zero, then this is going @@ -519,7 +575,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory item.Folder = context.ID; } - // Determine whether this is a new item or a + // Determine whether this is a new item or a // replacement definition. foreach (InventoryItemBase xi in rdata.items) @@ -537,60 +593,90 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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); + modified = true; + rdata.appendStatus(String.Format("

Modified item {0}, UUID {1}

", item.Name, item.ID)); } 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); + created = true; + rdata.appendStatus(String.Format("

Created item {2}, UUID {3}

", item.Name, item.ID)); } + } + + if (created) + { + // Must include a location header with a URI that identifies the new resource. + rdata.AddHeader(Rest.HttpHeaderLocation,String.Format("http://{0}{1}/{2}", + rdata.hostname+":"+rdata.port,rdata.path,newnode)); + rdata.Complete(Rest.HttpStatusCodeCreated); + } + else + { + if (modified) + { + rdata.Complete(Rest.HttpStatusCodeOK); + } + else + { + rdata.Complete(Rest.HttpStatusCodeNoContent); + } + } + + rdata.Respond("Inventory " + rdata.method + ": Normal completion"); + } 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+": invalid resource context"); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "invalid resource context"); } - rdata.Complete(); - rdata.Respond("Inventory " + rdata.method + ": Normal completion"); } ///

- /// POST updates the URI-identified element in the inventory. This + /// PUT 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 + /// PUT 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 any relative subtree + /// that is to be used to resolve any relative subtree /// specifications in the entity. If nothing is specified - /// then the whole inventory is implied. + /// then the whole of the private 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 + /// 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 any value specified in the /// URI. If the parent does not exist, then the element, and any dependent - /// elements, are ignored. This case is actually detected and handled + /// elements, are ignored. This case is actually detected and handled /// during the reconstitution process. /// + /// HTTP service request work area - private void DoPost(InventoryRequestData rdata) + private void DoUpdate(InventoryRequestData rdata) { - int count = 0; + + int count = 0; + bool created = false; + bool modified = false; // Resolve the inventory node that is to be modified. + // rdata already contains information about the current + // content of the user's inventory. - Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); + Object InventoryNode = getInventoryNode(rdata, rdata.root, PARM_PATH, Rest.Fill); // As long as we have a node, then we have something - // meaningful to do, unlike PUT. So we reconstitute the - // subtree before doing anything else. Note that we + // meaningful to do, unlike POST. So we reconstitute the + // subtree before doing anything else. Note that we // etiher got a valid node or we threw an exception. XmlInventoryCollection entity = ReconstituteEntity(rdata); @@ -612,46 +698,77 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.AssetServices.AddAsset(asset); - if (Rest.DumpAsset) + created = true; + rdata.appendStatus(String.Format("

Created asset {0}, UUID {1}

", asset.Name, asset.ID)); + + if (Rest.DEBUG && Rest.DumpAsset) { Rest.Dump(asset.Data); } + } } - ///

- /// The URI specifies either a folder or an item to be updated. - /// - /// - /// The root node in the entity will replace the node identified - /// by the URI. This means the parent will remain the same, but - /// any or all attributes associated with the named element - /// will change. - /// - /// If the inventory collection contains an element with a zero - /// parent ID, then this is taken to be the replacement for the - /// named node. The collection MAY also specify an explicit - /// parent ID, in this case it MAY identify the same parent as - /// the current node, or it MAY specify a different parent, - /// indicating that the folder is being moved in addition to any - /// other modifications being made. - /// + // The URI specifies either a folder or an item to be updated. + // + // The root node in the entity will replace the node identified + // by the URI. This means the parent will remain the same, but + // any or all attributes associated with the named element + // will change. + // + // If the inventory collection contains an element with a zero + // parent ID, then this is taken to be the replacement for the + // named node. The collection MAY also specify an explicit + // parent ID, in this case it MAY identify the same parent as + // the current node, or it MAY specify a different parent, + // indicating that the folder is being moved in addition to any + // other modifications being made. if (typeof(InventoryFolderBase) == InventoryNode.GetType() || typeof(InventoryFolderImpl) == InventoryNode.GetType()) { + + bool rfound = false; InventoryFolderBase uri = (InventoryFolderBase) InventoryNode; InventoryFolderBase xml = null; + + // If the entity to be replaced resolved to be the root + // directory itself (My Inventory), then make sure that + // the supplied data include as appropriately typed and + // named folder. Note that we can;t rule out the possibility + // of a sub-directory being called "My Inventory", so that + // is anticipated. + + if (uri == rdata.root) + { + + foreach (InventoryFolderBase folder in entity.Folders) + { + if ((rfound = (folder.Name == PRIVATE_ROOT_NAME))) + { + if ((rfound = (folder.ParentID == LLUUID.Zero))) + break; + } + } + + if (!rfound) + { + Rest.Log.DebugFormat("{0} {1}: Path <{2}> will result in loss of inventory", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "invalid inventory structure"); + } + + } // Scan the set of folders in the entity collection for an // entry that matches the context folder. It is assumed that - // the only reliable indicator of this is a zero UUID (using + // 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 + // want to iterate over the fodler set again if it can be // helped. foreach (InventoryFolderBase folder in entity.Folders) @@ -663,51 +780,60 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory xml = folder; count++; } - if (xml.ID == LLUUID.Zero) - { - xml.ID = LLUUID.Random(); - } } // More than one entry is ambiguous. Other folders should be - // added using the PUT verb. + // added using the POST verb. if (count > 1) { Rest.Log.DebugFormat("{0} {1}: Request for <{2}> is ambiguous", MsgId, rdata.method, rdata.path); - rdata.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest+": context is ambiguous"); + rdata.Fail(Rest.HttpStatusCodeConflict, "context is ambiguous"); } // 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 + // 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); + // All went well, so we generate a UUID is one is + // needed. + + if (xml.ID == LLUUID.Zero) + { + xml.ID = LLUUID.Random(); + } + uri.ParentID = TrashCan.ID; Rest.InventoryServices.MoveFolder(uri); Rest.InventoryServices.PurgeFolder(TrashCan); + modified = true; + } - // Now, regardelss of what they represent, we + // Now, regardelss of what they represent, we // integrate all of the elements in the entity. foreach (InventoryFolderBase f in entity.Folders) { + rdata.appendStatus(String.Format("

Moving folder {0} UUID {1}

", f.Name, f.ID)); Rest.InventoryServices.MoveFolder(f); } foreach (InventoryItemBase it in entity.Items) { + rdata.appendStatus(String.Format("

Storing item {0} UUID {1}

", it.Name, it.ID)); Rest.InventoryServices.AddItem(it); } + } ///

@@ -720,6 +846,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory else { + InventoryItemBase uri = (InventoryItemBase) InventoryNode; InventoryItemBase xml = null; @@ -727,20 +854,18 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { Rest.Log.DebugFormat("{0} {1}: Request should not contain any folders <{2}>", MsgId, rdata.method, rdata.path); - rdata.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest+": folder is not allowed"); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "folder is not allowed"); } 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+": too may items"); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "too may items"); } xml = entity.Items[0]; - + if (xml.ID == LLUUID.Zero) { xml.ID = LLUUID.Random(); @@ -757,10 +882,29 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // Add the new item to the inventory Rest.InventoryServices.AddItem(xml); + + rdata.appendStatus(String.Format("

Storing item {0} UUID {1}

", xml.Name, xml.ID)); + + } + + if (created) + { + rdata.Complete(Rest.HttpStatusCodeCreated); + } + else + { + if (modified) + { + rdata.Complete(Rest.HttpStatusCodeOK); + } + else + { + rdata.Complete(Rest.HttpStatusCodeNoContent); + } } - rdata.Complete(); rdata.Respond("Inventory " + rdata.method + ": Normal completion"); + } ///

@@ -773,7 +917,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// /// 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 + /// 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. @@ -782,14 +926,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// be performed using UUID, as a name might identify several /// elements. /// + /// HTTP service request work area private void DoDelete(InventoryRequestData rdata) { - Object InventoryNode = getInventoryNode(rdata, rdata.root, 1); + + Object InventoryNode = getInventoryNode(rdata, rdata.root, PARM_PATH, false); if (typeof(InventoryFolderBase) == InventoryNode.GetType() || typeof(InventoryFolderImpl) == InventoryNode.GetType()) { + InventoryFolderBase TrashCan = GetTrashCan(rdata); InventoryFolderBase folder = (InventoryFolderBase) InventoryNode; @@ -798,6 +945,9 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory folder.ParentID = TrashCan.ID; Rest.InventoryServices.MoveFolder(folder); Rest.InventoryServices.PurgeFolder(TrashCan); + + rdata.appendStatus(String.Format("

Deleted folder {0} UUID {1}

", folder.Name, folder.ID)); + } // Deleting items is much more straight forward. @@ -808,21 +958,23 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.DebugFormat("{0} {1}: Item {2} will be deleted", MsgId, rdata.method, rdata.path); Rest.InventoryServices.DeleteItem(item); + rdata.appendStatus(String.Format("

Deleted item {0} UUID {1}

", item.Name, item.ID)); } rdata.Complete(); rdata.Respond("Inventory " + rdata.method + ": Normal completion"); + } - #endregion method-specific processing +#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 + /// 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 + /// + /// If we reach the end of the URI then we return the contextual folder to /// our caller. /// /// If we are not yet at the end of the URI we attempt to find a child folder @@ -831,50 +983,79 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// If this is the last node, then we look to see if this is an item. If it is, /// we return that item. /// + /// If we reach the end of an inventory path and the URI si not yet exhausted, + /// then if 'fill' is specified, we create the intermediate nodes. + /// /// 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. - /// + /// An ambiguous request causes the request to fail. /// /// - - private Object getInventoryNode(InventoryRequestData rdata, InventoryFolderBase folder, int pi) + /// HTTP service request work area + /// The folder to be searched (parent) + /// URI parameter index + /// Should missing path members be created? + + private Object getInventoryNode(InventoryRequestData rdata, + InventoryFolderBase folder, + int pi, bool fill) { + + InventoryFolderBase foundf = null; + int fk = 0; + 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) + if (pi >= rdata.Parameters.Length) { return folder; } - // More names in the sequence, look for a folder that might - // get us there. + // There are more names in the parameter sequence, + // look for the folder named by param[pi] as a + // child of the folder supplied as an argument. + // Note that a UUID may have been supplied as the + // identifier (it is the ONLY guaranteed unambiguous + // option. 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])) + if (f.ParentID == folder.ID && + (f.Name == rdata.Parameters[pi] || + f.ID.ToString() == rdata.Parameters[pi])) { - return getInventoryNode(rdata, f, pi+1); + foundf = f; + fk++; } } + + // If more than one node matched, then the path, as specified + // is ambiguous. + + if (fk > 1) + { + Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous", + MsgId, rdata.method, rdata.path); + rdata.Fail(Rest.HttpStatusCodeConflict, "request is ambiguous"); + } + + // If we find a match, then the method + // increment the parameter index, and calls itself + // passing the found folder as the new context. + + if (foundf != null) + { + return getInventoryNode(rdata, foundf, pi+1, fill); } // 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 (pi == rdata.Parameters.Length-1) { if (rdata.items != null) { @@ -882,9 +1063,9 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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])) + if (i.Folder == folder.ID && + (i.Name == rdata.Parameters[pi] || + i.ID.ToString() == rdata.Parameters[pi])) { li = i; k++; @@ -894,26 +1075,35 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { return li; } - else + else if (k > 1) { Rest.Log.DebugFormat("{0} {1}: Request for {2} is ambiguous", MsgId, rdata.method, rdata.path); - rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound+": request is ambiguous"); + rdata.Fail(Rest.HttpStatusCodeConflict, "request is ambiguous"); } } } - // No, so abandon the request + // If fill is enabled, then we must create the missing intermediate nodes. + // And of course, even this is not straightforward. All intermediate nodes + // are obviously folders, but the last node may be a folder or an item. + + if (fill) + { + } + + // No fill, so abandon the request Rest.Log.DebugFormat("{0} {1}: Resource {2} not found", MsgId, rdata.method, rdata.path); - rdata.Fail(Rest.HttpStatusCodeNotFound, Rest.HttpStatusDescNotFound+": resource "+rdata.path+" not found"); + rdata.Fail(Rest.HttpStatusCodeNotFound, "resource "+rdata.path+" not found"); return null; /* Never reached */ + } /// - /// This routine traverse the inventory's structure until the end-point identified + /// 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. /// @@ -923,57 +1113,142 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// /// Only the last element in the URI should identify an item. /// + /// HTTP service request work area + /// The folder to be searched (parent) + /// URI parameter index - private void traverseInventory(InventoryRequestData rdata, InventoryFolderBase folder, int pi) + private void traverse(InventoryRequestData rdata, InventoryFolderBase folder, int pi) { - Rest.Log.DebugFormat("{0} Folder : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); + + Rest.Log.DebugFormat("{0} Traverse[initial] : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); if (rdata.folders != null) { - foreach (InventoryFolderBase f in rdata.folders) + + // If there was only one parameter (avatar name), then the entire + // inventory is being requested. + + if (rdata.Parameters.Length == 1) { - if (f.ParentID == folder.ID && - (f.Name == rdata.parameters[pi] || - f.ID.ToString() == rdata.parameters[pi])) + formatInventory(rdata, rdata.root, String.Empty); + } + + // Has the client specified the root directory name explicitly? + // if yes, then we just absorb the reference, because the folder + // we start looking in for a match *is* the root directory. If there + // are more parameters remaining we tarverse, otehrwise it's time + // to format. Otherwise,we consider the "My Inventory" to be implied + // and we just traverse normally. + + else if (folder.ID.ToString() == rdata.Parameters[pi] || + folder.Name == rdata.Parameters[pi]) + { + // Length is -1 because the avatar name is a parameter + if (pi<(rdata.Parameters.Length-1)) { - if (pi < rdata.parameters.Length-1) - { - traverseInventory(rdata, f, pi+1); - } - else - { - formatInventory(rdata, f, String.Empty); - } - return; + traverseInventory(rdata, folder, pi+1); } + else + { + formatInventory(rdata, folder, String.Empty); + } + } + else + { + traverseInventory(rdata, folder, pi); } + + return; + } + } + + /// + /// This is the recursive method. I've separated them in this way so that + /// we do not have to waste cycles on any first-case-only processing. + /// + + private void traverseInventory(InventoryRequestData rdata, InventoryFolderBase folder, int pi) + { + + int fk = 0; + InventoryFolderBase ffound = null; + InventoryItemBase ifound = null; - if (pi == rdata.parameters.Length-1) + Rest.Log.DebugFormat("{0} Traverse Folder : {1} {2} [{3}]", MsgId, folder.ID, folder.Name, pi); + + foreach (InventoryFolderBase f in rdata.folders) { + if (f.ParentID == folder.ID && + (f.Name == rdata.Parameters[pi] || + f.ID.ToString() == rdata.Parameters[pi])) + { + fk++; + ffound = f; + } + } + + // If this is the last element in the parameter sequence, then + // it is reasonable to check for an item. All intermediate nodes + // MUST be folders. + + if (pi == rdata.Parameters.Length-1) + { + + // Only if there are any items, and there pretty much always are. + 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])) + 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"); + fk++; + ifound = i; } } } } - Rest.Log.DebugFormat("{0} Inventory does not contain item/folder: <{1}>", + if (fk == 1) + { + if (ffound != null) + { + if (pi < rdata.Parameters.Length-1) + { + traverseInventory(rdata, ffound, pi+1); + } + else + { + formatInventory(rdata, ffound, String.Empty); + } + return; + } + else + { + // 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 + ifound.AssetID.ToString(); + rdata.Redirect(asseturl,Rest.PERMANENT); + Rest.Log.DebugFormat("{0} Never Reached", MsgId); + } + } + else if (fk > 1) + { + rdata.Fail(Rest.HttpStatusCodeConflict, + String.Format("ambiguous element ({0}) in path specified: <{1}>", + pi, rdata.path)); + } + + Rest.Log.DebugFormat("{0} Inventory does not contain item/folder: <{1}>", MsgId, rdata.path); - rdata.Fail(Rest.HttpStatusCodeNotFound,Rest.HttpStatusDescNotFound+": no such item/folder"); + rdata.Fail(Rest.HttpStatusCodeNotFound,String.Format("no such item/folder : {0}", + rdata.Parameters[pi])); + } /// @@ -983,12 +1258,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// The indentation parameter is solely for the benefit of trace record /// formatting. /// + /// HTTP service request work area + /// The folder to be searched (parent) + /// pretty print indentation 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); + Rest.Log.DebugFormat("{0} Folder : {1} {2} {3} type = {4}", + MsgId, folder.ID, indent, folder.Name, folder.Type); indent += "\t"; } @@ -997,6 +1277,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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("parent",String.Empty,folder.ParentID.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()); @@ -1026,21 +1307,28 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // End folder item rdata.writer.WriteEndElement(); + } /// /// This method generates XML that describes an instance of InventoryItemBase. /// + /// HTTP service request work area + /// The item to be formatted + /// Pretty print indentation private void formatItem(InventoryRequestData rdata, InventoryItemBase i, string indent) { - Rest.Log.DebugFormat("{0} Item : {1} {2} {3}", MsgId, i.ID, indent, i.Name); + + Rest.Log.DebugFormat("{0} Item : {1} {2} {3} Type = {4}, AssetType = {5}", + MsgId, i.ID, indent, i.Name, i.InvType, i.AssetType); 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("folder",String.Empty,i.Folder.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()); @@ -1062,10 +1350,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory rdata.writer.WriteElementString("Asset",i.AssetID.ToString()); rdata.writer.WriteEndElement(); + } /// - /// This method creates a "trashcan" folder to support folder and item + /// 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 @@ -1073,9 +1362,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// If the trash can cannot be created, then by implication the request /// that required it cannot be completed, and it fails accordingly. /// + /// HTTP service request work area private InventoryFolderBase GetTrashCan(InventoryRequestData rdata) { + InventoryFolderBase TrashCan = null; foreach (InventoryFolderBase f in rdata.folders) @@ -1102,29 +1393,31 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } } } - + if (TrashCan == null) { Rest.Log.DebugFormat("{0} No Trash Can available", MsgId); - rdata.Fail(Rest.HttpStatusCodeServerError, - Rest.HttpStatusDescServerError+": unable to create trash can"); + rdata.Fail(Rest.HttpStatusCodeServerError, "unable to create trash can"); } return TrashCan; + } /// /// Make sure that an unchanged folder is not unnecessarily /// processed. /// + /// Folder obtained from enclosed entity + /// Folder obtained from the user's inventory 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 + return ( newf.Name != oldf.Name + || newf.ParentID != oldf.ParentID + || newf.Owner != oldf.Owner + || newf.Type != oldf.Type + || newf.Version != oldf.Version ); } @@ -1132,27 +1425,29 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// Make sure that an unchanged item is not unnecessarily /// processed. ///
+ /// Item obtained from enclosed entity + /// Item obtained from the user's inventory 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 + 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 + /// 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. /// @@ -1160,24 +1455,27 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// 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 + /// 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 + /// responsible for integrating this collection (and ensuring that any /// missing parent IDs are resolved). /// + /// HTTP service request work area 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); @@ -1273,6 +1571,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Rest.Log.WarnFormat("{0} Unexpected XML parsing error: {1}", MsgId, e.Message); throw e; } + } else { @@ -1288,13 +1587,14 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } 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 + /// 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. @@ -1302,6 +1602,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory private void CollectFolder(XmlInventoryCollection ic) { + Rest.Log.DebugFormat("{0} Interpret folder element", MsgId); InventoryFolderBase result = new InventoryFolderBase(); @@ -1341,10 +1642,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory result.Version = UInt16.Parse(ic.xml.Value); break; default : - Rest.Log.DebugFormat("{0} Folder: unrecognized attribute: {1}:{2}", + Rest.Log.DebugFormat("{0} Folder: unrecognized attribute: {1}:{2}", MsgId, ic.xml.Name, ic.xml.Value); - ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest+": unrecognized attribute"); + ic.Fail(Rest.HttpStatusCodeBadRequest, String.Format("unrecognized attribute <{0}>", + ic.xml.Name)); break; } } @@ -1363,11 +1664,12 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } else { + bool found = false; foreach (InventoryFolderBase parent in ic.rdata.folders) { - if (parent.ID == result.ParentID) + if ( parent.ID == result.ParentID ) { found = true; break; @@ -1376,11 +1678,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!found) { - Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in folder {2}", + Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in folder {2}", MsgId, ic.Item.Folder, result.ID); - ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest+": invalid parent"); + ic.Fail(Rest.HttpStatusCodeBadRequest, "invalid parent"); } + } // This is a new folder, so no existing UUID is available @@ -1395,14 +1697,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // 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 + /// 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 + /// 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. @@ -1412,6 +1715,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory private void CollectItem(XmlInventoryCollection ic) { + Rest.Log.DebugFormat("{0} Interpret item element", MsgId); InventoryItemBase result = new InventoryItemBase(); @@ -1432,6 +1736,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { for (int i = 0; i < ic.xml.AttributeCount; i++) { + ic.xml.MoveToAttribute(i); switch (ic.xml.Name) @@ -1480,36 +1785,37 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory break; default : - Rest.Log.DebugFormat("{0} Item: Unrecognized attribute: {1}:{2}", + Rest.Log.DebugFormat("{0} Item: Unrecognized attribute: {1}:{2}", MsgId, ic.xml.Name, ic.xml.Value); - ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest+": unrecognized attribute"); + ic.Fail(Rest.HttpStatusCodeBadRequest, String.Format("unrecognized attribute", + ic.xml.Name)); 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 + /// 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 + /// 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 + /// simply the UUID of the asset referenced by the /// item being constructed. /// An asset, if created is stored in the /// XmlInventoryCollection @@ -1570,10 +1876,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory break; default : - Rest.Log.DebugFormat("{0} Asset: Unrecognized attribute: {1}:{2}", + Rest.Log.DebugFormat("{0} Asset: Unrecognized attribute: {1}:{2}", MsgId, ic.xml.Name, ic.xml.Value); - ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + ic.Fail(Rest.HttpStatusCodeBadRequest, + String.Format("unrecognized attribute <{0}>", ic.xml.Name)); break; } } @@ -1583,7 +1889,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // If this is a reference to an existing asset, just store the // asset ID into the item. - + if (!inline) { if (ic.Item != null) @@ -1594,17 +1900,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory else { Rest.Log.DebugFormat("{0} LLUID unimbedded asset must be inline", MsgId); - ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest+": no context for asset"); + ic.Fail(Rest.HttpStatusCodeBadRequest, "no context for asset"); } } - // Otherwise, generate an asset ID, store that into the item, and + // 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 @@ -1617,17 +1923,17 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // Create AssetBase entity to hold the inlined asset - asset = new AssetBase(uuid, name); + asset = new AssetBase(uuid, name); asset.Description = desc; asset.Type = type; // type == 0 == texture 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, + 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); @@ -1646,19 +1952,22 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { ic.Item.AssetID = uuid; } + } ic.Push(asset); + } /// /// Store any permissions information provided by the request. - /// This overrides the default permissions set when the + /// 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++) @@ -1681,14 +1990,15 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory default : Rest.Log.DebugFormat("{0} Permissions: invalid attribute {1}:{2}", MsgId,ic.xml.Name, ic.xml.Value); - ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest); + ic.Fail(Rest.HttpStatusCodeBadRequest, + String.Format("invalid attribute <{0}>", ic.xml.Name)); break; } } } ic.xml.MoveToElement(); + } /// @@ -1703,35 +2013,35 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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+": request parse error"); + ic.Fail(Rest.HttpStatusCodeBadRequest, "request parse error"); } - + // 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+": item name required"); + ic.Fail(Rest.HttpStatusCodeBadRequest, "item name required"); } - - // An item MUST have an asset ID. AssetID should never be zero + + // 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+": asset information required"); + ic.Fail(Rest.HttpStatusCodeBadRequest, "asset information required"); + } // If the item is new, then assign it an ID @@ -1744,18 +2054,19 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // 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) + if ( parent.ID == ic.Item.Folder ) { found = true; break; @@ -1764,11 +2075,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (!found) { - Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in item {2}", + Rest.Log.ErrorFormat("{0} Invalid parent ID ({1}) in item {2}", MsgId, ic.Item.Folder, ic.Item.ID); - ic.Fail(Rest.HttpStatusCodeBadRequest, - Rest.HttpStatusDescBadRequest+": parent information required"); + ic.Fail(Rest.HttpStatusCodeBadRequest, "parent information required"); } + } // If this is an inline asset being constructed in the context @@ -1790,12 +2101,13 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory 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 + // 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); @@ -1815,7 +2127,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory if (parts.Length > 1) { - Rest.Log.DebugFormat("{0} File type is {1}", + Rest.Log.DebugFormat("{0} File type is {1}", MsgId, parts[parts.Length - 1]); switch (parts[parts.Length - 1]) { @@ -1823,7 +2135,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory case "jpeg-2000" : case "jpg2000" : case "jpg-2000" : - Rest.Log.DebugFormat("{0} Type {1} inferred", + 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; @@ -1832,7 +2144,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory break; case "jpg" : case "jpeg" : - Rest.Log.DebugFormat("{0} Type {1} inferred", + 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; @@ -1873,19 +2185,22 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory temp = OpenJPEGNet.LoadTGAClass.LoadTGA(tgadata); ic.Asset.Data = OpenJPEGNet.OpenJPEG.EncodeFromImage(temp, true); } - + ic.reset(); + } #region Inventory RequestData extension internal class InventoryRequestData : RequestData { + /// /// These are the inventory specific request/response state /// extensions. /// + internal LLUUID uuid = LLUUID.Zero; internal bool HaveInventory = false; internal ICollection folders = null; internal ICollection items = null; @@ -1898,7 +2213,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory } /// - /// This is the callback method required by inventory services. The + /// This is the callback method required by inventory services. The /// requestor issues an inventory request and then blocks until this /// method signals the monitor. /// @@ -1914,6 +2229,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Monitor.Pulse(this); } } + } #endregion Inventory RequestData extension @@ -1926,6 +2242,7 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory internal class XmlInventoryCollection : InventoryCollection { + internal InventoryRequestData rdata; private Stack stk; @@ -2014,10 +2331,11 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory initPermissions(); } - internal void Fail(int code, string desc) + internal void Fail(int code, string addendum) { - rdata.Fail(code, desc); + rdata.Fail(code, addendum); } + } } } diff --git a/OpenSim/ApplicationPlugins/Rest/Inventory/RestTestServices.cs b/OpenSim/ApplicationPlugins/Rest/Inventory/RestTestServices.cs index a5165d9..984f6d3 100644 --- a/OpenSim/ApplicationPlugins/Rest/Inventory/RestTestServices.cs +++ b/OpenSim/ApplicationPlugins/Rest/Inventory/RestTestServices.cs @@ -140,7 +140,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory { if (!rdata.IsAuthenticated) { - rdata.Fail(Rest.HttpStatusCodeNotAuthorized, Rest.HttpStatusDescNotAuthorized); + rdata.Fail(Rest.HttpStatusCodeNotAuthorized, + String.Format("user \"{0}\" could not be authenticated", rdata.userName)); } } catch (RestException e) @@ -160,10 +161,10 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory // Check that a test was specified - if (rdata.parameters.Length < 1) + if (rdata.Parameters.Length < 1) { Rest.Log.DebugFormat("{0} Insufficient parameters", MsgId); - rdata.Fail(Rest.HttpStatusCodeBadRequest, Rest.HttpStatusDescBadRequest); + rdata.Fail(Rest.HttpStatusCodeBadRequest, "not enough parameters"); } // Select the test @@ -180,8 +181,8 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory private static bool testsLoaded = false; private static List classes = new List(); private static List tests = new List(); - private static Type[] parms = new Type[1]; - private static Object[] args = new Object[1]; + private static Type[] parms = new Type[0]; + private static Object[] args = new Object[0]; static RestTestServices() { @@ -191,9 +192,16 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory Type[] types = m.GetTypes(); foreach (Type t in types) { - if (t.GetInterface("ITest") != null) + try { - classes.Add(t); + if (t.GetInterface("ITest") != null) + { + classes.Add(t); + } + } + catch (Exception e) + { + Rest.Log.WarnFormat("[STATIC-TEST] Unable to include test {0} : {1}", t, e.Message); } } } @@ -205,27 +213,38 @@ namespace OpenSim.ApplicationPlugins.Rest.Inventory /// registering itself with this handler. /// I was not able to make this code work in a constructor. /// + private void loadTests() { lock (tests) { if (!testsLoaded) { - parms[0] = this.GetType(); - args[0] = this; ConstructorInfo ci; Object ht; foreach (Type t in classes) { - ci = t.GetConstructor(parms); - ht = ci.Invoke(args); - tests.Add((ITest)ht); + try + { + if (t.GetInterface("ITest") != null) + { + ci = t.GetConstructor(parms); + ht = ci.Invoke(args); + tests.Add((ITest)ht); + Rest.Log.WarnFormat("{0} Test {1} added", MsgId, t); + } + } + catch (Exception e) + { + Rest.Log.WarnFormat("{0} Unable to load test {1} : {2}", MsgId, t, e.Message); + } } testsLoaded = true; } } } + } } -- cgit v1.1