/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSimulator Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections; using System.Xml; using OpenMetaverse; using OpenSim.Framework; using OpenSim.Framework.Servers; using OpenSim.Framework.Servers.HttpServer; namespace OpenSim.ApplicationPlugins.Rest.Inventory { public class RestAppearanceServices : IRest { private static readonly int PARM_USERID = 0; // private static readonly int PARM_PATH = 1; private bool enabled = false; private string qPrefix = "appearance"; /// /// The constructor makes sure that the service prefix is absolute /// and the registers the service handler and the allocator. /// public RestAppearanceServices() { Rest.Log.InfoFormat("{0} User appearance 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, // add the standard prefix to make it absolute, e.g. /admin if (!qPrefix.StartsWith(Rest.UrlPathSeparator)) { Rest.Log.InfoFormat("{0} Domain is relative, adding absolute prefix", MsgId); qPrefix = String.Format("{0}{1}{2}", Rest.Prefix, Rest.UrlPathSeparator, qPrefix); Rest.Log.InfoFormat("{0} Domain is now <{1}>", MsgId, qPrefix); } // Register interface using the absolute URI. Rest.Plugin.AddPathHandler(DoAppearance,qPrefix,Allocate); // Activate if everything went OK enabled = true; Rest.Log.InfoFormat("{0} User appearance services initialization complete", MsgId); } /// /// Post-construction, pre-enabled initialization opportunity /// Not currently exploited. /// public void Initialize() { } /// /// Called by the plug-in to halt service processing. Local processing is /// disabled. /// public void Close() { enabled = false; Rest.Log.InfoFormat("{0} User appearance services closing down", MsgId); } /// /// This property is declared locally because it is used a lot and /// brevity is nice. /// internal string MsgId { get { return Rest.MsgId; } } #region Interface /// /// The plugin (RestHandler) calls this method to allocate the request /// state carrier for a new request. It is destroyed when the request /// completes. All request-instance specific state is kept here. This /// is registered when this service provider is registered. /// /// 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, string prefix) { return (RequestData) new AppearanceRequestData(request, response, prefix); } /// /// 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 for a given request. /// It handles all aspects of inventory REST processing, i.e. /admin/inventory /// /// A consolidated HTTP request work area private void DoAppearance(RequestData hdata) { AppearanceRequestData rdata = (AppearanceRequestData) hdata; Rest.Log.DebugFormat("{0} DoAppearance ENTRY", MsgId); // If we're disabled, do nothing. if (!enabled) { return; } // Now that we know this is a serious attempt to // access inventory data, we should find out who // is asking, and make sure they are authorized // to do so. We need to validate the caller's // identity before revealing anything about the // status quo. Authenticate throws an exception // via Fail if no identity information is present. // // With the present HTTP server we can't use the // builtin authentication mechanisms because they // would be enforced for all in-bound requests. // Instead we look at the headers ourselves and // handle authentication directly. try { if (!rdata.IsAuthenticated) { rdata.Fail(Rest.HttpStatusCodeNotAuthorized,String.Format("user \"{0}\" could not be authenticated", rdata.userName)); } } catch (RestException e) { if (e.statusCode == Rest.HttpStatusCodeNotAuthorized) { Rest.Log.WarnFormat("{0} User not authenticated", MsgId); Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization")); } else { Rest.Log.ErrorFormat("{0} User authentication failed", MsgId); Rest.Log.DebugFormat("{0} Authorization header: {1}", MsgId, rdata.request.Headers.Get("Authorization")); } throw (e); } Rest.Log.DebugFormat("{0} Authenticated {1}", MsgId, rdata.userName); // We can only get here if we are authorized // // The requestor may have specified an UUID or // a conjoined FirstName LastName string. We'll // try both. If we fail with the first, UUID, // attempt, we try the other. As an example, the // URI for a valid inventory request might be: // // http://:/admin/inventory/Arthur Dent // // Indicating that this is an inventory request for // an avatar named Arthur Dent. This is ALL that is // required to designate a GET for an entire // inventory. // // Do we have at least a user agent name? if (rdata.Parameters.Length < 1) { Rest.Log.WarnFormat("{0} Appearance: No user agent identifier specified", MsgId); rdata.Fail(Rest.HttpStatusCodeBadRequest, "no user identity specified"); } // The first parameter MUST be the agent identification, either an UUID // or a space-separated First-name Last-Name specification. We check for // an UUID first, if anyone names their character using a valid UUID // that identifies another existing avatar will cause this a problem... try { rdata.uuid = new UUID(rdata.Parameters[PARM_USERID]); Rest.Log.DebugFormat("{0} UUID supplied", MsgId); rdata.userProfile = Rest.UserServices.GetUserProfile(rdata.uuid); } catch { string[] names = rdata.Parameters[PARM_USERID].Split(Rest.CA_SPACE); if (names.Length == 2) { Rest.Log.DebugFormat("{0} Agent Name supplied [2]", MsgId); rdata.userProfile = Rest.UserServices.GetUserProfile(names[0],names[1]); } else { Rest.Log.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 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} User profile obtained for agent {1} {2}", MsgId, rdata.userProfile.FirstName, rdata.userProfile.SurName); } else { Rest.Log.WarnFormat("{0} No user 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 switch (rdata.method) { case Rest.HEAD : // Do the processing, set the status code, suppress entity DoGet(rdata); rdata.buffer = null; break; case Rest.GET : // Do the processing, set the status code, return entity DoGet(rdata); break; case Rest.PUT : // Update named element DoUpdate(rdata); break; case Rest.POST : // Add new information to identified context. DoExtend(rdata); 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, String.Format("{0} not supported", rdata.method)); break; } } #endregion Interface #region method-specific processing /// /// This method implements GET processing for user's appearance. /// /// HTTP service request work area private void DoGet(AppearanceRequestData rdata) { rdata.userAppearance = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID); if (rdata.userAppearance == null) { rdata.Fail(Rest.HttpStatusCodeNoContent, String.Format("appearance data not found for user {0} {1}", rdata.userProfile.FirstName, rdata.userProfile.SurName)); } rdata.initXmlWriter(); FormatUserAppearance(rdata); // Indicate a successful request rdata.Complete(); // Send the response to the user. The body will be implicitly // constructed from the result of the XML writer. rdata.Respond(String.Format("Appearance {0} Normal completion", rdata.method)); } /// /// POST adds NEW information to the user profile database. /// This effectively resets the appearance before applying those /// characteristics supplied in the request. /// private void DoExtend(AppearanceRequestData rdata) { bool created = false; bool modified = false; string newnode = String.Empty; Rest.Log.DebugFormat("{0} POST ENTRY", MsgId); //AvatarAppearance old = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID); rdata.userAppearance = new AvatarAppearance(); // Although the following behavior is admitted by HTTP I am becoming // increasingly doubtful that it is appropriate for REST. If I attempt to // add a new record, and it already exists, then it seems to me that the // attempt should fail, rather than update the existing record. if (GetUserAppearance(rdata)) { modified = rdata.userAppearance != null; created = !modified; Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance); // Rest.UserServices.UpdateUserProfile(rdata.userProfile); } else { created = true; Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance); // Rest.UserServices.UpdateUserProfile(rdata.userProfile); } if (created) { newnode = String.Format("{0} {1}", rdata.userProfile.FirstName, rdata.userProfile.SurName); // Must include a location header with a URI that identifies the new resource. rdata.AddHeader(Rest.HttpHeaderLocation,String.Format("http://{0}{1}:{2}{3}{4}", rdata.hostname,rdata.port,rdata.path,Rest.UrlPathSeparator, newnode)); rdata.Complete(Rest.HttpStatusCodeCreated); } else { if (modified) { rdata.Complete(Rest.HttpStatusCodeOK); } else { rdata.Complete(Rest.HttpStatusCodeNoContent); } } rdata.Respond(String.Format("Appearance {0} : Normal completion", rdata.method)); } /// /// This updates the user's appearance. not all aspects need to be provided, /// only those supplied will be changed. /// private void DoUpdate(AppearanceRequestData rdata) { bool created = false; bool modified = false; rdata.userAppearance = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID); // If the user exists then this is considered a modification regardless // of what may, or may not be, specified in the payload. if (rdata.userAppearance != null) { modified = true; Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance); Rest.UserServices.UpdateUserProfile(rdata.userProfile); } if (created) { rdata.Complete(Rest.HttpStatusCodeCreated); } else { if (modified) { rdata.Complete(Rest.HttpStatusCodeOK); } else { rdata.Complete(Rest.HttpStatusCodeNoContent); } } rdata.Respond(String.Format("Appearance {0} : Normal completion", rdata.method)); } /// /// Delete the specified user's appearance. This actually performs a reset /// to the default avatar appearance, if the info is already there. /// Existing ownership is preserved. All prior updates are lost and can not /// be recovered. /// private void DoDelete(AppearanceRequestData rdata) { AvatarAppearance old = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID); if (old != null) { rdata.userAppearance = new AvatarAppearance(); rdata.userAppearance.Owner = old.Owner; Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance); rdata.Complete(); } else { rdata.Complete(Rest.HttpStatusCodeNoContent); } rdata.Respond(String.Format("Appearance {0} : Normal completion", rdata.method)); } #endregion method-specific processing private bool GetUserAppearance(AppearanceRequestData rdata) { XmlReader xml; bool indata = false; rdata.initXmlReader(); xml = rdata.reader; while (xml.Read()) { switch (xml.NodeType) { case XmlNodeType.Element : switch (xml.Name) { case "Appearance" : if (xml.MoveToAttribute("Height")) { rdata.userAppearance.AvatarHeight = (float) Convert.ToDouble(xml.Value); indata = true; } if (xml.MoveToAttribute("Owner")) { rdata.userAppearance.Owner = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Serial")) { rdata.userAppearance.Serial = Convert.ToInt32(xml.Value); indata = true; } break; case "Body" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.BodyItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.BodyAsset = (UUID)xml.Value; indata = true; } break; case "Skin" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.SkinItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.SkinAsset = (UUID)xml.Value; indata = true; } break; case "Hair" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.HairItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.HairAsset = (UUID)xml.Value; indata = true; } break; case "Eyes" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.EyesItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.EyesAsset = (UUID)xml.Value; indata = true; } break; case "Shirt" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.ShirtItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.ShirtAsset = (UUID)xml.Value; indata = true; } break; case "Pants" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.PantsItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.PantsAsset = (UUID)xml.Value; indata = true; } break; case "Shoes" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.ShoesItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.ShoesAsset = (UUID)xml.Value; indata = true; } break; case "Socks" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.SocksItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.SocksAsset = (UUID)xml.Value; indata = true; } break; case "Jacket" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.JacketItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.JacketAsset = (UUID)xml.Value; indata = true; } break; case "Gloves" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.GlovesItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.GlovesAsset = (UUID)xml.Value; indata = true; } break; case "UnderShirt" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.UnderShirtItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.UnderShirtAsset = (UUID)xml.Value; indata = true; } break; case "UnderPants" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.UnderPantsItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.UnderPantsAsset = (UUID)xml.Value; indata = true; } break; case "Skirt" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.SkirtItem = (UUID)xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.SkirtAsset = (UUID)xml.Value; indata = true; } break; case "Attachment" : { int ap; UUID asset; UUID item; if (xml.MoveToAttribute("AtPoint")) { ap = Convert.ToInt32(xml.Value); if (xml.MoveToAttribute("Asset")) { asset = new UUID(xml.Value); if (xml.MoveToAttribute("Asset")) { item = new UUID(xml.Value); rdata.userAppearance.SetAttachment(ap, item, asset); indata = true; } } } } break; case "Texture" : if (xml.MoveToAttribute("Default")) { rdata.userAppearance.Texture = new Primitive.TextureEntry(new UUID(xml.Value)); indata = true; } break; case "Face" : { uint index; if (xml.MoveToAttribute("Index")) { index = Convert.ToUInt32(xml.Value); if (xml.MoveToAttribute("Id")) { rdata.userAppearance.Texture.CreateFace(index).TextureID = new UUID(xml.Value); indata = true; } } } break; case "VisualParameters" : { xml.ReadContentAsBase64(rdata.userAppearance.VisualParams, 0, rdata.userAppearance.VisualParams.Length); indata = true; } break; } break; } } return indata; } private void FormatPart(AppearanceRequestData rdata, string part, UUID item, UUID asset) { if (item != UUID.Zero || asset != UUID.Zero) { rdata.writer.WriteStartElement(part); if (item != UUID.Zero) { rdata.writer.WriteAttributeString("Item",item.ToString()); } if (asset != UUID.Zero) { rdata.writer.WriteAttributeString("Asset",asset.ToString()); } rdata.writer.WriteEndElement(); } } private void FormatUserAppearance(AppearanceRequestData rdata) { Rest.Log.DebugFormat("{0} FormatUserAppearance", MsgId); if (rdata.userAppearance != null) { Rest.Log.DebugFormat("{0} FormatUserAppearance: appearance object exists", MsgId); rdata.writer.WriteStartElement("Appearance"); rdata.writer.WriteAttributeString("Height", rdata.userAppearance.AvatarHeight.ToString()); if (rdata.userAppearance.Owner != UUID.Zero) rdata.writer.WriteAttributeString("Owner", rdata.userAppearance.Owner.ToString()); rdata.writer.WriteAttributeString("Serial", rdata.userAppearance.Serial.ToString()); FormatPart(rdata, "Body", rdata.userAppearance.BodyItem, rdata.userAppearance.BodyAsset); FormatPart(rdata, "Skin", rdata.userAppearance.SkinItem, rdata.userAppearance.SkinAsset); FormatPart(rdata, "Hair", rdata.userAppearance.HairItem, rdata.userAppearance.HairAsset); FormatPart(rdata, "Eyes", rdata.userAppearance.EyesItem, rdata.userAppearance.EyesAsset); FormatPart(rdata, "Shirt", rdata.userAppearance.ShirtItem, rdata.userAppearance.ShirtAsset); FormatPart(rdata, "Pants", rdata.userAppearance.PantsItem, rdata.userAppearance.PantsAsset); FormatPart(rdata, "Skirt", rdata.userAppearance.SkirtItem, rdata.userAppearance.SkirtAsset); FormatPart(rdata, "Shoes", rdata.userAppearance.ShoesItem, rdata.userAppearance.ShoesAsset); FormatPart(rdata, "Socks", rdata.userAppearance.SocksItem, rdata.userAppearance.SocksAsset); FormatPart(rdata, "Jacket", rdata.userAppearance.JacketItem, rdata.userAppearance.JacketAsset); FormatPart(rdata, "Gloves", rdata.userAppearance.GlovesItem, rdata.userAppearance.GlovesAsset); FormatPart(rdata, "UnderShirt", rdata.userAppearance.UnderShirtItem, rdata.userAppearance.UnderShirtAsset); FormatPart(rdata, "UnderPants", rdata.userAppearance.UnderPantsItem, rdata.userAppearance.UnderPantsAsset); Hashtable attachments = rdata.userAppearance.GetAttachments(); if (attachments != null) { Rest.Log.DebugFormat("{0} FormatUserAppearance: Formatting attachments", MsgId); rdata.writer.WriteStartElement("Attachments"); for (int i = 0; i < attachments.Count; i++) { Hashtable attachment = attachments[i] as Hashtable; rdata.writer.WriteStartElement("Attachment"); rdata.writer.WriteAttributeString("AtPoint", i.ToString()); rdata.writer.WriteAttributeString("Item", (string) attachment["item"]); rdata.writer.WriteAttributeString("Asset", (string) attachment["asset"]); rdata.writer.WriteEndElement(); } rdata.writer.WriteEndElement(); } Primitive.TextureEntry texture = rdata.userAppearance.Texture; if (texture != null && (texture.DefaultTexture != null || texture.FaceTextures != null)) { Rest.Log.DebugFormat("{0} FormatUserAppearance: Formatting textures", MsgId); rdata.writer.WriteStartElement("Texture"); if (texture.DefaultTexture != null) { Rest.Log.DebugFormat("{0} FormatUserAppearance: Formatting default texture", MsgId); rdata.writer.WriteAttributeString("Default", texture.DefaultTexture.TextureID.ToString()); } if (texture.FaceTextures != null) { Rest.Log.DebugFormat("{0} FormatUserAppearance: Formatting face textures", MsgId); for (int i=0; i /// These are the inventory specific request/response state /// extensions. /// internal UUID uuid = UUID.Zero; internal UserProfileData userProfile = null; internal AvatarAppearance userAppearance = null; internal AppearanceRequestData(OSHttpRequest request, OSHttpResponse response, string prefix) : base(request, response, prefix) { } } #endregion Appearance RequestData extension } }