/* * 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.Reflection; using System.Text.RegularExpressions; using System.Threading; using System.Web; using OpenMetaverse; using OpenMetaverse.StructuredData; using log4net; using Nwc.XmlRpc; using OpenSim.Framework.Communications.Cache; using OpenSim.Framework.Statistics; namespace OpenSim.Framework.Communications { public abstract class LoginService { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); protected string m_welcomeMessage = "Welcome to OpenSim"; protected UserManagerBase m_userManager = null; protected Mutex m_loginMutex = new Mutex(false); /// /// Used during login to send the skeleton of the OpenSim Library to the client. /// protected LibraryRootFolder m_libraryRootFolder; /// /// Constructor /// /// /// /// public LoginService(UserManagerBase userManager, LibraryRootFolder libraryRootFolder, string welcomeMess) { m_userManager = userManager; m_libraryRootFolder = libraryRootFolder; if (welcomeMess != String.Empty) { m_welcomeMessage = welcomeMess; } } /// /// Customises the login response and fills in missing values. /// /// The existing response /// The user profile public abstract void CustomiseResponse(LoginResponse response, UserProfileData theUser, string startLocationRequest); /// /// If the user is already logged in, try to notify the region that the user they've got is dead. /// /// public virtual void LogOffUser(UserProfileData theUser, string message) { } /// /// Get the initial login inventory skeleton (in other words, the folder structure) for the given user. /// /// /// /// This will be thrown if there is a problem with the inventory service protected abstract InventoryData GetInventorySkeleton(UUID userID); /// /// Called when we receive the client's initial XMLRPC login_to_simulator request message /// /// The XMLRPC request /// The response to send public XmlRpcResponse XmlRpcLoginMethod(XmlRpcRequest request) { // Temporary fix m_loginMutex.WaitOne(); try { //CFK: CustomizeResponse contains sufficient strings to alleviate the need for this. //CKF: m_log.Info("[LOGIN]: Attempting login now..."); XmlRpcResponse response = new XmlRpcResponse(); Hashtable requestData = (Hashtable) request.Params[0]; bool GoodXML = (requestData.Contains("first") && requestData.Contains("last") && (requestData.Contains("passwd") || requestData.Contains("web_login_key"))); bool GoodLogin = false; string startLocationRequest = "last"; UserProfileData userProfile; LoginResponse logResponse = new LoginResponse(); string firstname = String.Empty; string lastname = String.Empty; if (GoodXML) { firstname = (string) requestData["first"]; lastname = (string) requestData["last"]; m_log.InfoFormat( "[LOGIN BEGIN]: XMLRPC Received login request message from user '{0}' '{1}'", firstname, lastname); string clientVersion = "Unknown"; if (requestData.Contains("version")) { clientVersion = (string)requestData["version"]; } if (requestData.Contains("start")) { startLocationRequest = (string)requestData["start"]; } m_log.DebugFormat( "[LOGIN]: XMLRPC Client is {0}, start location is {1}", clientVersion, startLocationRequest); userProfile = GetTheUser(firstname, lastname); if (userProfile == null) { m_log.Info("[LOGIN END]: XMLRPC Could not find a profile for " + firstname + " " + lastname); return logResponse.CreateLoginFailedResponse(); } if (requestData.Contains("passwd")) { string passwd = (string)requestData["passwd"]; GoodLogin = AuthenticateUser(userProfile, passwd); } else if (requestData.Contains("web_login_key")) { UUID webloginkey = null; try { webloginkey = new UUID((string)requestData["web_login_key"]); } catch (Exception e) { m_log.InfoFormat( "[LOGIN END]: XMLRPC Bad web_login_key: {0} for user {1} {2}, exception {3}", requestData["web_login_key"], firstname, lastname, e); return logResponse.CreateFailedResponse(); } GoodLogin = AuthenticateUser(userProfile, webloginkey); } } else { m_log.Info( "[LOGIN END]: XMLRPC login_to_simulator login message did not contain all the required data"); return logResponse.CreateGridErrorResponse(); } if (!GoodLogin) { m_log.InfoFormat("[LOGIN END]: XMLRPC User {0} {1} failed authentication", firstname, lastname); return logResponse.CreateLoginFailedResponse(); } else { // If we already have a session... if (userProfile.CurrentAgent != null && userProfile.CurrentAgent.AgentOnline) { //TODO: The following statements can cause trouble: // If agentOnline could not turn from true back to false normally // because of some problem, for instance, the crashment of server or client, // the user cannot log in any longer. userProfile.CurrentAgent.AgentOnline = false; m_userManager.CommitAgent(ref userProfile); // try to tell the region that their user is dead. LogOffUser(userProfile, " XMLRPC You were logged off because you logged in from another location"); // Reject the login m_log.InfoFormat( "[LOGIN END]: XMLRPC Notifying user {0} {1} that they are already logged in", firstname, lastname); return logResponse.CreateAlreadyLoggedInResponse(); } // Otherwise... // Create a new agent session m_userManager.ResetAttachments(userProfile.ID); CreateAgent(userProfile, request); try { UUID agentID = userProfile.ID; InventoryData inventData = null; try { inventData = GetInventorySkeleton(agentID); } catch (Exception e) { m_log.ErrorFormat( "[LOGIN END]: XMLRPC Error retrieving inventory skeleton of agent {0}, {1} - {2}", agentID, e.GetType(), e.Message); return logResponse.CreateLoginInventoryFailedResponse(); } ArrayList AgentInventoryArray = inventData.InventoryArray; Hashtable InventoryRootHash = new Hashtable(); InventoryRootHash["folder_id"] = inventData.RootFolderID.ToString(); ArrayList InventoryRoot = new ArrayList(); InventoryRoot.Add(InventoryRootHash); userProfile.RootInventoryFolderID = inventData.RootFolderID; // Inventory Library Section Hashtable InventoryLibRootHash = new Hashtable(); InventoryLibRootHash["folder_id"] = "00000112-000f-0000-0000-000100bba000"; ArrayList InventoryLibRoot = new ArrayList(); InventoryLibRoot.Add(InventoryLibRootHash); logResponse.InventoryLibRoot = InventoryLibRoot; logResponse.InventoryLibraryOwner = GetLibraryOwner(); logResponse.InventoryRoot = InventoryRoot; logResponse.InventorySkeleton = AgentInventoryArray; logResponse.InventoryLibrary = GetInventoryLibrary(); // Circuit Code uint circode = (uint) (Util.RandomClass.Next()); logResponse.Lastname = userProfile.SurName; logResponse.Firstname = userProfile.FirstName; logResponse.AgentID = agentID.ToString(); logResponse.SessionID = userProfile.CurrentAgent.SessionID.ToString(); logResponse.SecureSessionID = userProfile.CurrentAgent.SecureSessionID.ToString(); logResponse.CircuitCode = (Int32) circode; //logResponse.RegionX = 0; //overwritten //logResponse.RegionY = 0; //overwritten logResponse.Home = "!!null temporary value {home}!!"; // Overwritten //logResponse.LookAt = "\n[r" + TheUser.homeLookAt.X.ToString() + ",r" + TheUser.homeLookAt.Y.ToString() + ",r" + TheUser.homeLookAt.Z.ToString() + "]\n"; //logResponse.SimAddress = "127.0.0.1"; //overwritten //logResponse.SimPort = 0; //overwritten logResponse.Message = GetMessage(); logResponse.BuddList = ConvertFriendListItem(m_userManager.GetUserFriendList(agentID)); logResponse.StartLocation = startLocationRequest; try { CustomiseResponse(logResponse, userProfile, startLocationRequest); } catch (Exception e) { m_log.Info("[LOGIN END]: XMLRPC " + e.ToString()); return logResponse.CreateDeadRegionResponse(); //return logResponse.ToXmlRpcResponse(); } userProfile.LastLogin = userProfile.CurrentAgent.LoginTime; CommitAgent(ref userProfile); // If we reach this point, then the login has successfully logged onto the grid if (StatsManager.UserStats != null) StatsManager.UserStats.AddSuccessfulLogin(); m_log.DebugFormat( "[LOGIN END]: XMLRPC Authentication of user {0} {1} successful. Sending response to client.", firstname, lastname); return logResponse.ToXmlRpcResponse(); } catch (Exception e) { m_log.Info("[LOGIN END]: XMLRPC Login failed, " + e.ToString()); } } m_log.Info("[LOGIN END]: XMLRPC Login failed. Sending back blank XMLRPC response"); return response; } finally { m_loginMutex.ReleaseMutex(); } } public LLSD LLSDLoginMethod(LLSD request) { // Temporary fix m_loginMutex.WaitOne(); try { bool GoodLogin = false; string startLocationRequest = "last"; UserProfileData userProfile = null; LoginResponse logResponse = new LoginResponse(); if (request.Type == LLSDType.Map) { LLSDMap map = (LLSDMap)request; if (map.ContainsKey("first") && map.ContainsKey("last") && map.ContainsKey("passwd")) { string firstname = map["first"].AsString(); string lastname = map["last"].AsString(); string passwd = map["passwd"].AsString(); if (map.ContainsKey("start")) { m_log.Info("[LOGIN]: LLSD StartLocation Requested: " + map["start"].AsString()); startLocationRequest = map["start"].AsString(); } m_log.Info("[LOGIN]: LLSD Login Requested for: '" + firstname+"' '"+lastname+"' / "+passwd); userProfile = GetTheUser(firstname, lastname); if (userProfile == null) { m_log.Info("[LOGIN]: LLSD Could not find a profile for " + firstname + " " + lastname); return logResponse.CreateLoginFailedResponseLLSD(); } GoodLogin = AuthenticateUser(userProfile, passwd); } } if (!GoodLogin) { return logResponse.CreateLoginFailedResponseLLSD(); } else { // If we already have a session... if (userProfile.CurrentAgent != null && userProfile.CurrentAgent.AgentOnline) { userProfile.CurrentAgent.AgentOnline = false; m_userManager.CommitAgent(ref userProfile); // try to tell the region that their user is dead. LogOffUser(userProfile, " LLSD You were logged off because you logged in from another location"); // Reject the login m_log.InfoFormat( "[LOGIN END]: LLSD Notifying user {0} {1} that they are already logged in", userProfile.FirstName, userProfile.SurName); userProfile.CurrentAgent = null; return logResponse.CreateAlreadyLoggedInResponseLLSD(); } // Otherwise... // Create a new agent session m_userManager.ResetAttachments(userProfile.ID); CreateAgent(userProfile, request); try { UUID agentID = userProfile.ID; //InventoryData inventData = GetInventorySkeleton(agentID); InventoryData inventData = null; try { inventData = GetInventorySkeleton(agentID); } catch (Exception e) { m_log.ErrorFormat( "[LOGIN END]: LLSD Error retrieving inventory skeleton of agent {0}, {1} - {2}", agentID, e.GetType(), e.Message); return logResponse.CreateLoginFailedResponseLLSD();// .CreateLoginInventoryFailedResponseLLSD (); } ArrayList AgentInventoryArray = inventData.InventoryArray; Hashtable InventoryRootHash = new Hashtable(); InventoryRootHash["folder_id"] = inventData.RootFolderID.ToString(); ArrayList InventoryRoot = new ArrayList(); InventoryRoot.Add(InventoryRootHash); userProfile.RootInventoryFolderID = inventData.RootFolderID; // Inventory Library Section Hashtable InventoryLibRootHash = new Hashtable(); InventoryLibRootHash["folder_id"] = "00000112-000f-0000-0000-000100bba000"; ArrayList InventoryLibRoot = new ArrayList(); InventoryLibRoot.Add(InventoryLibRootHash); logResponse.InventoryLibRoot = InventoryLibRoot; logResponse.InventoryLibraryOwner = GetLibraryOwner(); logResponse.InventoryRoot = InventoryRoot; logResponse.InventorySkeleton = AgentInventoryArray; logResponse.InventoryLibrary = GetInventoryLibrary(); // Circuit Code uint circode = (uint)(Util.RandomClass.Next()); logResponse.Lastname = userProfile.SurName; logResponse.Firstname = userProfile.FirstName; logResponse.AgentID = agentID.ToString(); logResponse.SessionID = userProfile.CurrentAgent.SessionID.ToString(); logResponse.SecureSessionID = userProfile.CurrentAgent.SecureSessionID.ToString(); logResponse.CircuitCode = (Int32)circode; //logResponse.RegionX = 0; //overwritten //logResponse.RegionY = 0; //overwritten logResponse.Home = "!!null temporary value {home}!!"; // Overwritten //logResponse.LookAt = "\n[r" + TheUser.homeLookAt.X.ToString() + ",r" + TheUser.homeLookAt.Y.ToString() + ",r" + TheUser.homeLookAt.Z.ToString() + "]\n"; //logResponse.SimAddress = "127.0.0.1"; //overwritten //logResponse.SimPort = 0; //overwritten logResponse.Message = GetMessage(); logResponse.BuddList = ConvertFriendListItem(m_userManager.GetUserFriendList(agentID)); logResponse.StartLocation = startLocationRequest; try { CustomiseResponse(logResponse, userProfile, startLocationRequest); } catch (Exception ex) { m_log.Info("[LOGIN]: LLSD " + ex.ToString()); return logResponse.CreateDeadRegionResponseLLSD(); } userProfile.LastLogin = userProfile.CurrentAgent.LoginTime; CommitAgent(ref userProfile); // If we reach this point, then the login has successfully logged onto the grid if (StatsManager.UserStats != null) StatsManager.UserStats.AddSuccessfulLogin(); m_log.DebugFormat( "[LOGIN END]: LLSD Authentication of user {0} {1} successful. Sending response to client.", userProfile.FirstName, userProfile.SurName); return logResponse.ToLLSDResponse(); } catch (Exception ex) { m_log.Info("[LOGIN]: LLSD " + ex.ToString()); return logResponse.CreateFailedResponseLLSD(); } } } finally { m_loginMutex.ReleaseMutex(); } } public Hashtable ProcessHTMLLogin(Hashtable keysvals) { // Matches all unspecified characters // Currently specified,; lowercase letters, upper case letters, numbers, underline // period, space, parens, and dash. Regex wfcut = new Regex("[^a-zA-Z0-9_\\.\\$ \\(\\)\\-]"); Hashtable returnactions = new Hashtable(); int statuscode = 200; string firstname = String.Empty; string lastname = String.Empty; string location = String.Empty; string region =String.Empty; string grid = String.Empty; string channel = String.Empty; string version = String.Empty; string lang = String.Empty; string password = String.Empty; string errormessages = String.Empty; // the client requires the HTML form field be named 'username' // however, the data it sends when it loads the first time is 'firstname' // another one of those little nuances. if (keysvals.Contains("firstname")) firstname = wfcut.Replace((string)keysvals["firstname"], String.Empty, 99999); if (keysvals.Contains("username")) firstname = wfcut.Replace((string)keysvals["username"], String.Empty, 99999); if (keysvals.Contains("lastname")) lastname = wfcut.Replace((string)keysvals["lastname"], String.Empty, 99999); if (keysvals.Contains("location")) location = wfcut.Replace((string)keysvals["location"], String.Empty, 99999); if (keysvals.Contains("region")) region = wfcut.Replace((string)keysvals["region"], String.Empty, 99999); if (keysvals.Contains("grid")) grid = wfcut.Replace((string)keysvals["grid"], String.Empty, 99999); if (keysvals.Contains("channel")) channel = wfcut.Replace((string)keysvals["channel"], String.Empty, 99999); if (keysvals.Contains("version")) version = wfcut.Replace((string)keysvals["version"], String.Empty, 99999); if (keysvals.Contains("lang")) lang = wfcut.Replace((string)keysvals["lang"], String.Empty, 99999); if (keysvals.Contains("password")) password = wfcut.Replace((string)keysvals["password"], String.Empty, 99999); // load our login form. string loginform = GetLoginForm(firstname, lastname, location, region, grid, channel, version, lang, password, errormessages); if (keysvals.ContainsKey("show_login_form")) { UserProfileData user = GetTheUser(firstname, lastname); bool goodweblogin = false; if (user != null) goodweblogin = AuthenticateUser(user, password); if (goodweblogin) { UUID webloginkey = UUID.Random(); m_userManager.StoreWebLoginKey(user.ID, webloginkey); statuscode = 301; string redirectURL = "about:blank?redirect-http-hack=" + HttpUtility.UrlEncode("secondlife:///app/login?first_name=" + firstname + "&last_name=" + lastname + "&location=" + location + "&grid=Other&web_login_key=" + webloginkey.ToString()); //m_log.Info("[WEB]: R:" + redirectURL); returnactions["int_response_code"] = statuscode; returnactions["str_redirect_location"] = redirectURL; returnactions["str_response_string"] = "GoodLogin"; } else { errormessages = "The Username and password supplied did not match our records. Check your caps lock and try again"; loginform = GetLoginForm(firstname, lastname, location, region, grid, channel, version, lang, password, errormessages); returnactions["int_response_code"] = statuscode; returnactions["str_response_string"] = loginform; } } else { returnactions["int_response_code"] = statuscode; returnactions["str_response_string"] = loginform; } return returnactions; } public string GetLoginForm(string firstname, string lastname, string location, string region, string grid, string channel, string version, string lang, string password, string errormessages) { // inject our values in the form at the markers string loginform=String.Empty; string file = Path.Combine(Util.configDir(), "http_loginform.html"); if (!File.Exists(file)) { loginform = GetDefaultLoginForm(); } else { StreamReader sr = File.OpenText(file); loginform = sr.ReadToEnd(); sr.Close(); } loginform = loginform.Replace("[$firstname]", firstname); loginform = loginform.Replace("[$lastname]", lastname); loginform = loginform.Replace("[$location]", location); loginform = loginform.Replace("[$region]", region); loginform = loginform.Replace("[$grid]", grid); loginform = loginform.Replace("[$channel]", channel); loginform = loginform.Replace("[$version]", version); loginform = loginform.Replace("[$lang]", lang); loginform = loginform.Replace("[$password]", password); loginform = loginform.Replace("[$errors]", errormessages); return loginform; } public string GetDefaultLoginForm() { string responseString = ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += "OpenSim Login"; responseString += "
"; responseString += "
"; responseString += "
"; responseString += "
[$errors]
"; responseString += "
"; responseString += "First Name:"; responseString += ""; responseString += "
"; responseString += "
"; responseString += "Last Name:"; responseString += ""; responseString += "
"; responseString += "
"; responseString += "Password:"; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += "
"; responseString += "
"; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += ""; responseString += "
"; responseString += ""; responseString += "
"; responseString += "
Connecting...
"; responseString += "
"; responseString += " | "; responseString += ""; responseString += "
"; responseString += "
[$channel] | [$version]=[$lang]
"; responseString += "
"; responseString += ""; responseString += "
"; responseString += ""; responseString += ""; responseString += ""; return responseString; } /// /// Saves a target agent to the database /// /// The users profile /// Successful? public bool CommitAgent(ref UserProfileData profile) { return m_userManager.CommitAgent(ref profile); } /// /// Checks a user against it's password hash /// /// The users profile /// The supplied password /// Authenticated? public virtual bool AuthenticateUser(UserProfileData profile, string password) { bool passwordSuccess = false; //m_log.InfoFormat("[LOGIN]: Authenticating {0} {1} ({2})", profile.FirstName, profile.SurName, profile.ID); // Web Login method seems to also occasionally send the hashed password itself // we do this to get our hash in a form that the server password code can consume // when the web-login-form submits the password in the clear (supposed to be over SSL!) if (!password.StartsWith("$1$")) password = "$1$" + Util.Md5Hash(password); password = password.Remove(0, 3); //remove $1$ string s = Util.Md5Hash(password + ":" + profile.PasswordSalt); // Testing... //m_log.Info("[LOGIN]: SubHash:" + s + " userprofile:" + profile.passwordHash); //m_log.Info("[LOGIN]: userprofile:" + profile.passwordHash + " SubCT:" + password); passwordSuccess = (profile.PasswordHash.Equals(s.ToString(), StringComparison.InvariantCultureIgnoreCase) || profile.PasswordHash.Equals(password, StringComparison.InvariantCultureIgnoreCase)); return passwordSuccess; } public virtual bool AuthenticateUser(UserProfileData profile, UUID webloginkey) { bool passwordSuccess = false; m_log.InfoFormat("[LOGIN]: Authenticating {0} {1} ({2})", profile.FirstName, profile.SurName, profile.ID); // Match web login key unless it's the default weblogin key UUID.Zero passwordSuccess = ((profile.WebLoginKey==webloginkey) && profile.WebLoginKey != UUID.Zero); return passwordSuccess; } /// /// /// /// /// public void CreateAgent(UserProfileData profile, XmlRpcRequest request) { m_userManager.CreateAgent(profile, request); } public void CreateAgent(UserProfileData profile, LLSD request) { m_userManager.CreateAgent(profile, request); } /// /// /// /// /// /// public virtual UserProfileData GetTheUser(string firstname, string lastname) { return m_userManager.GetUserProfile(firstname, lastname); } /// /// /// /// public virtual string GetMessage() { return m_welcomeMessage; } private static LoginResponse.BuddyList ConvertFriendListItem(List LFL) { LoginResponse.BuddyList buddylistreturn = new LoginResponse.BuddyList(); foreach (FriendListItem fl in LFL) { LoginResponse.BuddyList.BuddyInfo buddyitem = new LoginResponse.BuddyList.BuddyInfo(fl.Friend); buddyitem.BuddyID = fl.Friend; buddyitem.BuddyRightsHave = (int)fl.FriendListOwnerPerms; buddyitem.BuddyRightsGiven = (int) fl.FriendPerms; buddylistreturn.AddNewBuddy(buddyitem); } return buddylistreturn; } /// /// Converts the inventory library skeleton into the form required by the rpc request. /// /// protected virtual ArrayList GetInventoryLibrary() { Dictionary rootFolders = m_libraryRootFolder.RequestSelfAndDescendentFolders(); ArrayList folderHashes = new ArrayList(); foreach (InventoryFolderBase folder in rootFolders.Values) { Hashtable TempHash = new Hashtable(); TempHash["name"] = folder.Name; TempHash["parent_id"] = folder.ParentID.ToString(); TempHash["version"] = (Int32)folder.Version; TempHash["type_default"] = (Int32)folder.Type; TempHash["folder_id"] = folder.ID.ToString(); folderHashes.Add(TempHash); } return folderHashes; } /// /// /// /// protected virtual ArrayList GetLibraryOwner() { //for now create random inventory library owner Hashtable TempHash = new Hashtable(); TempHash["agent_id"] = "11111111-1111-0000-0000-000100bba000"; ArrayList inventoryLibOwner = new ArrayList(); inventoryLibOwner.Add(TempHash); return inventoryLibOwner; } public class InventoryData { public ArrayList InventoryArray = null; public UUID RootFolderID = UUID.Zero; public InventoryData(ArrayList invList, UUID rootID) { InventoryArray = invList; RootFolderID = rootID; } } } }