/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSim Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading; using System.Xml; using System.Drawing; using OpenSim.Framework; using OpenSim.Framework.Servers; using OpenSim.Framework.Communications; using OpenSim.Framework.Communications.Cache; using OpenMetaverse; using OpenMetaverse.Imaging; using Nini.Config; 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 { // digest scheme seems borked: disable it for the time // being rdata.scheme = Rest.AS_BASIC; 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,"appearance data not found"); } 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; AvatarAppearance old = Rest.AvatarServices.GetUserAppearance(rdata.userProfile.ID); rdata.userAppearance = new AvatarAppearance(); rdata.userAppearance.Owner = old.Owner; rdata.userAppearance.Serial = old.Serial; if (GetUserAppearance(rdata)) { modified = rdata.userAppearance != null; created = !modified; Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance); } else { created = true; Rest.AvatarServices.UpdateUserAppearance(rdata.userProfile.ID, rdata.userAppearance); } 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); } 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 = 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 = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.BodyAsset = xml.Value; indata = true; } break; case "Skin" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.SkinItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.SkinAsset = xml.Value; indata = true; } break; case "Hair" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.HairItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.HairAsset = xml.Value; indata = true; } break; case "Eyes" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.EyesItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.EyesAsset = xml.Value; indata = true; } break; case "Shirt" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.ShirtItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.ShirtAsset = xml.Value; indata = true; } break; case "Pants" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.PantsItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.PantsAsset = xml.Value; indata = true; } break; case "Shoes" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.ShoesItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.ShoesAsset = xml.Value; indata = true; } break; case "Socks" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.SocksItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.SocksAsset = xml.Value; indata = true; } break; case "Jacket" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.JacketItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.JacketAsset = xml.Value; indata = true; } break; case "Gloves" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.GlovesItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.GlovesAsset = xml.Value; indata = true; } break; case "UnderShirt" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.UnderShirtItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.UnderShirtAsset = xml.Value; indata = true; } break; case "UnderPants" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.UnderPantsItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.UnderPantsAsset = xml.Value; indata = true; } break; case "Skirt" : if (xml.MoveToAttribute("Item")) { rdata.userAppearance.SkirtItem = xml.Value; indata = true; } if (xml.MoveToAttribute("Asset")) { rdata.userAppearance.SkirtAsset = 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 FormatUserAppearance(AppearanceRequestData rdata) { rdata.writer.WriteStartElement("Appearance"); rdata.writer.WriteAttributeString("Height", rdata.userAppearance.AvatarHeight.ToString()); rdata.writer.WriteAttributeString("Owner", rdata.userAppearance.Owner.ToString()); rdata.writer.WriteAttributeString("Serial", rdata.userAppearance.Serial.ToString()); rdata.writer.WriteStartElement("Body"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.BodyItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.BodyAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Skin"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.SkinItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.SkinAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Hair"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.HairItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.HairAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Eyes"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.EyesItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.EyesAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Shirt"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.ShirtItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.ShirtAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Pants"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.PantsItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.PantsAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Shoes"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.ShoesItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.ShoesAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Socks"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.SocksItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.SocksAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Jacket"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.JacketItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.JacketAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Gloves"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.GlovesItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.GlovesAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("UnderShirt"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.UnderShirtItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.UnderShirtAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("UnderPants"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.UnderPantsItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.UnderPantsAsset.ToString()); rdata.writer.WriteEndElement(); rdata.writer.WriteStartElement("Skirt"); rdata.writer.WriteAttributeString("Item",rdata.userAppearance.SkirtItem.ToString()); rdata.writer.WriteAttributeString("Asset",rdata.userAppearance.SkirtAsset.ToString()); rdata.writer.WriteEndElement(); Hashtable attachments = rdata.userAppearance.GetAttachments(); rdata.writer.WriteStartElement("Attachments"); 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 } }