/* * Copyright (c) 2006-2014, openmetaverse.org * All rights reserved. * * - 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. * - Neither the name of the openmetaverse.org 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT OWNER OR 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.Threading; using System.IO; using System.Net; using System.Xml; using System.Security.Cryptography.X509Certificates; using Nwc.XmlRpc; using OpenMetaverse.StructuredData; using OpenMetaverse.Http; using OpenMetaverse.Packets; namespace OpenMetaverse { #region Enums /// /// /// public enum LoginStatus { /// Failed = -1, /// None = 0, /// ConnectingToLogin, /// ReadingResponse, /// ConnectingToSim, /// Redirecting, /// Success } /// /// Status of the last application run. /// Used for error reporting to the grid login service for statistical purposes. /// public enum LastExecStatus { /// Application exited normally Normal = 0, /// Application froze Froze, /// Application detected error and exited abnormally ForcedCrash, /// Other crash OtherCrash, /// Application froze during logout LogoutFroze, /// Application crashed during logout LogoutCrash } #endregion Enums #region Structs /// /// Login Request Parameters /// public class LoginParams { /// The URL of the Login Server public string URI; /// The number of milliseconds to wait before a login is considered /// failed due to timeout public int Timeout; /// The request method /// login_to_simulator is currently the only supported method public string MethodName; /// The Agents First name public string FirstName; /// The Agents Last name public string LastName; /// A md5 hashed password /// plaintext password will be automatically hashed public string Password; /// The agents starting location once logged in /// Either "last", "home", or a string encoded URI /// containing the simulator name and x/y/z coordinates e.g: uri:hooper&128&152&17 public string Start; /// A string containing the client software channel information /// Second Life Release public string Channel; /// The client software version information /// The official viewer uses: Second Life Release n.n.n.n /// where n is replaced with the current version of the viewer public string Version; /// A string containing the platform information the agent is running on public string Platform; /// A string hash of the network cards Mac Address public string MAC; /// Unknown or deprecated public string ViewerDigest; /// A string hash of the first disk drives ID used to identify this clients uniqueness public string ID0; /// A string containing the viewers Software, this is not directly sent to the login server but /// instead is used to generate the Version string public string UserAgent; /// A string representing the software creator. This is not directly sent to the login server but /// is used by the library to generate the Version information public string Author; /// If true, this agent agrees to the Terms of Service of the grid its connecting to public bool AgreeToTos; /// Unknown public bool ReadCritical; /// Status of the last application run sent to the grid login server for statistical purposes public LastExecStatus LastExecEvent = LastExecStatus.Normal; /// An array of string sent to the login server to enable various options public string[] Options; /// A randomly generated ID to distinguish between login attempts. This value is only used /// internally in the library and is never sent over the wire internal UUID LoginID; /// /// Default constuctor, initializes sane default values /// public LoginParams() { List options = new List(16); options.Add("inventory-root"); options.Add("inventory-skeleton"); options.Add("inventory-lib-root"); options.Add("inventory-lib-owner"); options.Add("inventory-skel-lib"); options.Add("initial-outfit"); options.Add("gestures"); options.Add("event_categories"); options.Add("event_notifications"); options.Add("classified_categories"); options.Add("buddy-list"); options.Add("ui-config"); options.Add("tutorial_settings"); options.Add("login-flags"); options.Add("global-textures"); options.Add("adult_compliant"); this.Options = options.ToArray(); this.MethodName = "login_to_simulator"; this.Start = "last"; this.Platform = NetworkManager.GetPlatform(); this.MAC = NetworkManager.GetMAC(); this.ViewerDigest = String.Empty; this.ID0 = NetworkManager.GetMAC(); this.AgreeToTos = true; this.ReadCritical = true; this.LastExecEvent = LastExecStatus.Normal; } /// /// Instantiates new LoginParams object and fills in the values /// /// Instance of GridClient to read settings from /// Login first name /// Login last name /// Password /// Login channnel (application name) /// Client version, should be application name + version number public LoginParams(GridClient client, string firstName, string lastName, string password, string channel, string version) : this() { this.URI = client.Settings.LOGIN_SERVER; this.Timeout = client.Settings.LOGIN_TIMEOUT; this.FirstName = firstName; this.LastName = lastName; this.Password = password; this.Channel = channel; this.Version = version; } /// /// Instantiates new LoginParams object and fills in the values /// /// Instance of GridClient to read settings from /// Login first name /// Login last name /// Password /// Login channnel (application name) /// Client version, should be application name + version number /// URI of the login server public LoginParams(GridClient client, string firstName, string lastName, string password, string channel, string version, string loginURI) : this(client, firstName, lastName, password, channel, version) { this.URI = loginURI; } } public struct BuddyListEntry { public int buddy_rights_given; public string buddy_id; public int buddy_rights_has; } /// /// The decoded data returned from the login server after a successful login /// public struct LoginResponseData { /// true, false, indeterminate //[XmlRpcMember("login")] public string Login; public bool Success; public string Reason; /// Login message of the day public string Message; public UUID AgentID; public UUID SessionID; public UUID SecureSessionID; public string FirstName; public string LastName; public string StartLocation; /// M or PG, also agent_region_access and agent_access_max public string AgentAccess; public Vector3 LookAt; public ulong HomeRegion; public Vector3 HomePosition; public Vector3 HomeLookAt; public int CircuitCode; public int RegionX; public int RegionY; public int SimPort; public IPAddress SimIP; public string SeedCapability; public BuddyListEntry[] BuddyList; public int SecondsSinceEpoch; public string UDPBlacklist; #region Inventory public UUID InventoryRoot; public UUID LibraryRoot; public InventoryFolder[] InventorySkeleton; public InventoryFolder[] LibrarySkeleton; public UUID LibraryOwner; #endregion #region Redirection public string NextMethod; public string NextUrl; public string[] NextOptions; public int NextDuration; #endregion // These aren't currently being utilized by the library public string AgentAccessMax; public string AgentRegionAccess; public int AOTransition; public string InventoryHost; public int MaxAgentGroups; public string OpenIDUrl; public string AgentAppearanceServiceURL; public uint COFVersion; public string InitialOutfit; public bool FirstLogin; /// /// Parse LLSD Login Reply Data /// /// An /// contaning the login response data /// XML-RPC logins do not require this as XML-RPC.NET /// automatically populates the struct properly using attributes public void Parse(OSDMap reply) { try { AgentID = ParseUUID("agent_id", reply); SessionID = ParseUUID("session_id", reply); SecureSessionID = ParseUUID("secure_session_id", reply); FirstName = ParseString("first_name", reply).Trim('"'); LastName = ParseString("last_name", reply).Trim('"'); StartLocation = ParseString("start_location", reply); AgentAccess = ParseString("agent_access", reply); LookAt = ParseVector3("look_at", reply); Reason = ParseString("reason", reply); Message = ParseString("message", reply); Login = reply["login"].AsString(); Success = reply["login"].AsBoolean(); } catch (OSDException e) { Logger.Log("Login server returned (some) invalid data: " + e.Message, Helpers.LogLevel.Warning); } // Home OSDMap home = null; OSD osdHome = OSDParser.DeserializeLLSDNotation(reply["home"].AsString()); if (osdHome.Type == OSDType.Map) { home = (OSDMap)osdHome; OSD homeRegion; if (home.TryGetValue("region_handle", out homeRegion) && homeRegion.Type == OSDType.Array) { OSDArray homeArray = (OSDArray)homeRegion; if (homeArray.Count == 2) HomeRegion = Utils.UIntsToLong((uint)homeArray[0].AsInteger(), (uint)homeArray[1].AsInteger()); else HomeRegion = 0; } HomePosition = ParseVector3("position", home); HomeLookAt = ParseVector3("look_at", home); } else { HomeRegion = 0; HomePosition = Vector3.Zero; HomeLookAt = Vector3.Zero; } CircuitCode = (int)ParseUInt("circuit_code", reply); RegionX = (int)ParseUInt("region_x", reply); RegionY = (int)ParseUInt("region_y", reply); SimPort = (short)ParseUInt("sim_port", reply); string simIP = ParseString("sim_ip", reply); IPAddress.TryParse(simIP, out SimIP); SeedCapability = ParseString("seed_capability", reply); // Buddy list OSD buddyLLSD; if (reply.TryGetValue("buddy-list", out buddyLLSD) && buddyLLSD.Type == OSDType.Array) { List buddys = new List(); OSDArray buddyArray = (OSDArray)buddyLLSD; for (int i = 0; i < buddyArray.Count; i++) { if (buddyArray[i].Type == OSDType.Map) { BuddyListEntry bud = new BuddyListEntry(); OSDMap buddy = (OSDMap)buddyArray[i]; bud.buddy_id = buddy["buddy_id"].AsString(); bud.buddy_rights_given = (int)ParseUInt("buddy_rights_given", buddy); bud.buddy_rights_has = (int)ParseUInt("buddy_rights_has", buddy); buddys.Add(bud); } BuddyList = buddys.ToArray(); } } SecondsSinceEpoch = (int)ParseUInt("seconds_since_epoch", reply); InventoryRoot = ParseMappedUUID("inventory-root", "folder_id", reply); InventorySkeleton = ParseInventorySkeleton("inventory-skeleton", reply); LibraryOwner = ParseMappedUUID("inventory-lib-owner", "agent_id", reply); LibraryRoot = ParseMappedUUID("inventory-lib-root", "folder_id", reply); LibrarySkeleton = ParseInventorySkeleton("inventory-skel-lib", reply); } public void Parse(Hashtable reply) { try { AgentID = ParseUUID("agent_id", reply); SessionID = ParseUUID("session_id", reply); SecureSessionID = ParseUUID("secure_session_id", reply); FirstName = ParseString("first_name", reply).Trim('"'); LastName = ParseString("last_name", reply).Trim('"'); // "first_login" for brand new accounts StartLocation = ParseString("start_location", reply); AgentAccess = ParseString("agent_access", reply); LookAt = ParseVector3("look_at", reply); Reason = ParseString("reason", reply); Message = ParseString("message", reply); if (reply.ContainsKey("login")) { Login = (string)reply["login"]; Success = Login == "true"; // Parse redirect options if (Login == "indeterminate") { NextUrl = ParseString("next_url", reply); NextDuration = (int)ParseUInt("next_duration", reply); NextMethod = ParseString("next_method", reply); NextOptions = (string[])((ArrayList)reply["next_options"]).ToArray(typeof(string)); } } } catch (Exception e) { Logger.Log("Login server returned (some) invalid data: " + e.Message, Helpers.LogLevel.Warning); } if (!Success) return; // Home OSDMap home = null; if (reply.ContainsKey("home")) { OSD osdHome = OSDParser.DeserializeLLSDNotation(reply["home"].ToString()); if (osdHome.Type == OSDType.Map) { home = (OSDMap)osdHome; OSD homeRegion; if (home.TryGetValue("region_handle", out homeRegion) && homeRegion.Type == OSDType.Array) { OSDArray homeArray = (OSDArray)homeRegion; if (homeArray.Count == 2) HomeRegion = Utils.UIntsToLong((uint)homeArray[0].AsInteger(), (uint)homeArray[1].AsInteger()); else HomeRegion = 0; } HomePosition = ParseVector3("position", home); HomeLookAt = ParseVector3("look_at", home); } } else { HomeRegion = 0; HomePosition = Vector3.Zero; HomeLookAt = Vector3.Zero; } CircuitCode = (int)ParseUInt("circuit_code", reply); RegionX = (int)ParseUInt("region_x", reply); RegionY = (int)ParseUInt("region_y", reply); SimPort = (short)ParseUInt("sim_port", reply); string simIP = ParseString("sim_ip", reply); IPAddress.TryParse(simIP, out SimIP); SeedCapability = ParseString("seed_capability", reply); // Buddy list if (reply.ContainsKey("buddy-list") && reply["buddy-list"] is ArrayList) { List buddys = new List(); ArrayList buddyArray = (ArrayList)reply["buddy-list"]; for (int i = 0; i < buddyArray.Count; i++) { if (buddyArray[i] is Hashtable) { BuddyListEntry bud = new BuddyListEntry(); Hashtable buddy = (Hashtable)buddyArray[i]; bud.buddy_id = ParseString("buddy_id", buddy); bud.buddy_rights_given = (int)ParseUInt("buddy_rights_given", buddy); bud.buddy_rights_has = (int)ParseUInt("buddy_rights_has", buddy); buddys.Add(bud); } } BuddyList = buddys.ToArray(); } SecondsSinceEpoch = (int)ParseUInt("seconds_since_epoch", reply); InventoryRoot = ParseMappedUUID("inventory-root", "folder_id", reply); InventorySkeleton = ParseInventorySkeleton("inventory-skeleton", reply); LibraryOwner = ParseMappedUUID("inventory-lib-owner", "agent_id", reply); LibraryRoot = ParseMappedUUID("inventory-lib-root", "folder_id", reply); LibrarySkeleton = ParseInventorySkeleton("inventory-skel-lib", reply); // UDP Blacklist if (reply.ContainsKey("udp_blacklist")) { UDPBlacklist = ParseString("udp_blacklist", reply); } if (reply.ContainsKey("max-agent-groups")) { MaxAgentGroups = (int)ParseUInt("max-agent-groups", reply); } else { MaxAgentGroups = -1; } if (reply.ContainsKey("openid_url")) { OpenIDUrl = ParseString("openid_url", reply); } if (reply.ContainsKey("agent_appearance_service")) { AgentAppearanceServiceURL = ParseString("agent_appearance_service", reply); } COFVersion = 0; if (reply.ContainsKey("cof_version")) { COFVersion = ParseUInt("cof_version", reply); } InitialOutfit = string.Empty; if (reply.ContainsKey("initial-outfit") && reply["initial-outfit"] is ArrayList) { ArrayList array = (ArrayList)reply["initial-outfit"]; for (int i = 0; i < array.Count; i++) { if (array[i] is Hashtable) { Hashtable map = (Hashtable)array[i]; InitialOutfit = ParseString("folder_name", map); } } } FirstLogin = false; if (reply.ContainsKey("login-flags") && reply["login-flags"] is ArrayList) { ArrayList array = (ArrayList)reply["login-flags"]; for (int i = 0; i < array.Count; i++) { if (array[i] is Hashtable) { Hashtable map = (Hashtable)array[i]; FirstLogin = ParseString("ever_logged_in", map) == "N"; } } } } #region Parsing Helpers public static uint ParseUInt(string key, OSDMap reply) { OSD osd; if (reply.TryGetValue(key, out osd)) return osd.AsUInteger(); else return 0; } public static uint ParseUInt(string key, Hashtable reply) { if (reply.ContainsKey(key)) { object value = reply[key]; if (value is int) return (uint)(int)value; } return 0; } public static UUID ParseUUID(string key, OSDMap reply) { OSD osd; if (reply.TryGetValue(key, out osd)) return osd.AsUUID(); else return UUID.Zero; } public static UUID ParseUUID(string key, Hashtable reply) { if (reply.ContainsKey(key)) { UUID value; if (UUID.TryParse((string)reply[key], out value)) return value; } return UUID.Zero; } public static string ParseString(string key, OSDMap reply) { OSD osd; if (reply.TryGetValue(key, out osd)) return osd.AsString(); else return String.Empty; } public static string ParseString(string key, Hashtable reply) { if (reply.ContainsKey(key)) return String.Format("{0}", reply[key]); return String.Empty; } public static Vector3 ParseVector3(string key, OSDMap reply) { OSD osd; if (reply.TryGetValue(key, out osd)) { if (osd.Type == OSDType.Array) { return ((OSDArray)osd).AsVector3(); } else if (osd.Type == OSDType.String) { OSDArray array = (OSDArray)OSDParser.DeserializeLLSDNotation(osd.AsString()); return array.AsVector3(); } } return Vector3.Zero; } public static Vector3 ParseVector3(string key, Hashtable reply) { if (reply.ContainsKey(key)) { object value = reply[key]; if (value is IList) { IList list = (IList)value; if (list.Count == 3) { float x, y, z; Single.TryParse((string)list[0], out x); Single.TryParse((string)list[1], out y); Single.TryParse((string)list[2], out z); return new Vector3(x, y, z); } } else if (value is string) { OSDArray array = (OSDArray)OSDParser.DeserializeLLSDNotation((string)value); return array.AsVector3(); } } return Vector3.Zero; } public static UUID ParseMappedUUID(string key, string key2, OSDMap reply) { OSD folderOSD; if (reply.TryGetValue(key, out folderOSD) && folderOSD.Type == OSDType.Array) { OSDArray array = (OSDArray)folderOSD; if (array.Count == 1 && array[0].Type == OSDType.Map) { OSDMap map = (OSDMap)array[0]; OSD folder; if (map.TryGetValue(key2, out folder)) return folder.AsUUID(); } } return UUID.Zero; } public static UUID ParseMappedUUID(string key, string key2, Hashtable reply) { if (reply.ContainsKey(key) && reply[key] is ArrayList) { ArrayList array = (ArrayList)reply[key]; if (array.Count == 1 && array[0] is Hashtable) { Hashtable map = (Hashtable)array[0]; return ParseUUID(key2, map); } } return UUID.Zero; } public static InventoryFolder[] ParseInventoryFolders(string key, UUID owner, OSDMap reply) { List folders = new List(); OSD skeleton; if (reply.TryGetValue(key, out skeleton) && skeleton.Type == OSDType.Array) { OSDArray array = (OSDArray)skeleton; for (int i = 0; i < array.Count; i++) { if (array[i].Type == OSDType.Map) { OSDMap map = (OSDMap)array[i]; InventoryFolder folder = new InventoryFolder(map["folder_id"].AsUUID()); folder.PreferredType = (AssetType)map["type_default"].AsInteger(); folder.Version = map["version"].AsInteger(); folder.OwnerID = owner; folder.ParentUUID = map["parent_id"].AsUUID(); folder.Name = map["name"].AsString(); folders.Add(folder); } } } return folders.ToArray(); } public InventoryFolder[] ParseInventorySkeleton(string key, OSDMap reply) { List folders = new List(); OSD skeleton; if (reply.TryGetValue(key, out skeleton) && skeleton.Type == OSDType.Array) { OSDArray array = (OSDArray)skeleton; for (int i = 0; i < array.Count; i++) { if (array[i].Type == OSDType.Map) { OSDMap map = (OSDMap)array[i]; InventoryFolder folder = new InventoryFolder(map["folder_id"].AsUUID()); folder.Name = map["name"].AsString(); folder.ParentUUID = map["parent_id"].AsUUID(); folder.PreferredType = (AssetType)map["type_default"].AsInteger(); folder.Version = map["version"].AsInteger(); folders.Add(folder); } } } return folders.ToArray(); } public InventoryFolder[] ParseInventorySkeleton(string key, Hashtable reply) { UUID ownerID; if (key.Equals("inventory-skel-lib")) ownerID = LibraryOwner; else ownerID = AgentID; List folders = new List(); if (reply.ContainsKey(key) && reply[key] is ArrayList) { ArrayList array = (ArrayList)reply[key]; for (int i = 0; i < array.Count; i++) { if (array[i] is Hashtable) { Hashtable map = (Hashtable)array[i]; InventoryFolder folder = new InventoryFolder(ParseUUID("folder_id", map)); folder.Name = ParseString("name", map); folder.ParentUUID = ParseUUID("parent_id", map); folder.PreferredType = (AssetType)ParseUInt("type_default", map); folder.Version = (int)ParseUInt("version", map); folder.OwnerID = ownerID; folders.Add(folder); } } } return folders.ToArray(); } #endregion Parsing Helpers } #endregion Structs /// /// Login Routines /// public partial class NetworkManager { #region Delegates //////LoginProgress //// LoginProgress /// The event subscribers, null of no subscribers private EventHandler m_LoginProgress; ///Raises the LoginProgress Event /// A LoginProgressEventArgs object containing /// the data sent from the simulator protected virtual void OnLoginProgress(LoginProgressEventArgs e) { EventHandler handler = m_LoginProgress; if (handler != null) handler(this, e); } /// Thread sync lock object private readonly object m_LoginProgressLock = new object(); /// Raised when the simulator sends us data containing /// ... public event EventHandler LoginProgress { add { lock (m_LoginProgressLock) { m_LoginProgress += value; } } remove { lock (m_LoginProgressLock) { m_LoginProgress -= value; } } } ///// The event subscribers, null of no subscribers //private EventHandler m_LoggedIn; /////Raises the LoggedIn Event ///// A LoggedInEventArgs object containing ///// the data sent from the simulator //protected virtual void OnLoggedIn(LoggedInEventArgs e) //{ // EventHandler handler = m_LoggedIn; // if (handler != null) // handler(this, e); //} ///// Thread sync lock object //private readonly object m_LoggedInLock = new object(); ///// Raised when the simulator sends us data containing ///// ... //public event EventHandler LoggedIn //{ // add { lock (m_LoggedInLock) { m_LoggedIn += value; } } // remove { lock (m_LoggedInLock) { m_LoggedIn -= value; } } //} /// /// /// /// /// /// /// /// public delegate void LoginResponseCallback(bool loginSuccess, bool redirect, string message, string reason, LoginResponseData replyData); #endregion Delegates #region Events /// Called when a reply is received from the login server, the /// login sequence will block until this event returns private event LoginResponseCallback OnLoginResponse; #endregion Events #region Public Members /// Seed CAPS URL returned from the login server public string LoginSeedCapability = String.Empty; /// Current state of logging in public LoginStatus LoginStatusCode { get { return InternalStatusCode; } } /// Upon login failure, contains a short string key for the /// type of login error that occurred public string LoginErrorKey { get { return InternalErrorKey; } } /// The raw XML-RPC reply from the login server, exactly as it /// was received (minus the HTTP header) public string RawLoginReply { get { return InternalRawLoginReply; } } /// During login this contains a descriptive version of /// LoginStatusCode. After a successful login this will contain the /// message of the day, and after a failed login a descriptive error /// message will be returned public string LoginMessage { get { return InternalLoginMessage; } } /// Maximum number of groups an agent can belong to, -1 for unlimited public int MaxAgentGroups = -1; /// Server side baking service URL public string AgentAppearanceServiceURL; /// Parsed login response data public LoginResponseData LoginResponseData; #endregion #region Private Members private LoginParams CurrentContext = null; private AutoResetEvent LoginEvent = new AutoResetEvent(false); private LoginStatus InternalStatusCode = LoginStatus.None; private string InternalErrorKey = String.Empty; private string InternalLoginMessage = String.Empty; private string InternalRawLoginReply = String.Empty; private Dictionary CallbackOptions = new Dictionary(); /// A list of packets obtained during the login process which /// networkmanager will log but not process private readonly List UDPBlacklist = new List(); #endregion #region Public Methods /// /// Generate sane default values for a login request /// /// Account first name /// Account last name /// Account password /// Client application name (channel) /// Client application name + version /// A populated struct containing /// sane defaults public LoginParams DefaultLoginParams(string firstName, string lastName, string password, string channel, string version) { return new LoginParams(Client, firstName, lastName, password, channel, version); } /// /// Simplified login that takes the most common and required fields /// /// Account first name /// Account last name /// Account password /// Client application name (channel) /// Client application name + version /// Whether the login was successful or not. On failure the /// LoginErrorKey string will contain the error code and LoginMessage /// will contain a description of the error public bool Login(string firstName, string lastName, string password, string channel, string version) { return Login(firstName, lastName, password, channel, "last", version); } /// /// Simplified login that takes the most common fields along with a /// starting location URI, and can accept an MD5 string instead of a /// plaintext password /// /// Account first name /// Account last name /// Account password or MD5 hash of the password /// such as $1$1682a1e45e9f957dcdf0bb56eb43319c /// Client application name (channel) /// Starting location URI that can be built with /// StartLocation() /// Client application name + version /// Whether the login was successful or not. On failure the /// LoginErrorKey string will contain the error code and LoginMessage /// will contain a description of the error public bool Login(string firstName, string lastName, string password, string channel, string start, string version) { LoginParams loginParams = DefaultLoginParams(firstName, lastName, password, channel, version); loginParams.Start = start; return Login(loginParams); } /// /// Login that takes a struct of all the values that will be passed to /// the login server /// /// The values that will be passed to the login /// server, all fields must be set even if they are String.Empty /// Whether the login was successful or not. On failure the /// LoginErrorKey string will contain the error code and LoginMessage /// will contain a description of the error public bool Login(LoginParams loginParams) { BeginLogin(loginParams); LoginEvent.WaitOne(loginParams.Timeout, false); if (CurrentContext != null) { CurrentContext = null; // Will force any pending callbacks to bail out early InternalStatusCode = LoginStatus.Failed; InternalLoginMessage = "Timed out"; return false; } return (InternalStatusCode == LoginStatus.Success); } public void BeginLogin(LoginParams loginParams) { // FIXME: Now that we're using CAPS we could cancel the current login and start a new one if (CurrentContext != null) throw new Exception("Login already in progress"); LoginEvent.Reset(); CurrentContext = loginParams; BeginLogin(); } public void RegisterLoginResponseCallback(LoginResponseCallback callback) { RegisterLoginResponseCallback(callback, null); } public void RegisterLoginResponseCallback(LoginResponseCallback callback, string[] options) { CallbackOptions.Add(callback, options); OnLoginResponse += callback; } public void UnregisterLoginResponseCallback(LoginResponseCallback callback) { CallbackOptions.Remove(callback); OnLoginResponse -= callback; } /// /// Build a start location URI for passing to the Login function /// /// Name of the simulator to start in /// X coordinate to start at /// Y coordinate to start at /// Z coordinate to start at /// String with a URI that can be used to login to a specified /// location public static string StartLocation(string sim, int x, int y, int z) { return String.Format("uri:{0}&{1}&{2}&{3}", sim, x, y, z); } public void AbortLogin() { LoginParams loginParams = CurrentContext; CurrentContext = null; // Will force any pending callbacks to bail out early // FIXME: Now that we're using CAPS we could cancel the current login and start a new one if (loginParams == null) { Logger.DebugLog("No Login was in progress: " + CurrentContext, Client); } else { InternalStatusCode = LoginStatus.Failed; InternalLoginMessage = "Aborted"; } UpdateLoginStatus(LoginStatus.Failed, "Abort Requested"); } #endregion #region Private Methods private void BeginLogin() { LoginParams loginParams = CurrentContext; // Generate a random ID to identify this login attempt loginParams.LoginID = UUID.Random(); CurrentContext = loginParams; #region Sanity Check loginParams if (loginParams.Options == null) loginParams.Options = new List().ToArray(); if (loginParams.Password == null) loginParams.Password = String.Empty; // Convert the password to MD5 if it isn't already if (loginParams.Password.Length != 35 && !loginParams.Password.StartsWith("$1$")) loginParams.Password = Utils.MD5(loginParams.Password); if (loginParams.ViewerDigest == null) loginParams.ViewerDigest = String.Empty; if (loginParams.Version == null) loginParams.Version = String.Empty; if (loginParams.UserAgent == null) loginParams.UserAgent = String.Empty; if (loginParams.Platform == null) loginParams.Platform = String.Empty; if (loginParams.MAC == null) loginParams.MAC = String.Empty; if (string.IsNullOrEmpty(loginParams.Channel)) { Logger.Log("Viewer channel not set. This is a TOS violation on some grids.", Helpers.LogLevel.Warning); loginParams.Channel = "libopenmetaverse generic client"; } if (loginParams.Author == null) loginParams.Author = String.Empty; #endregion // TODO: Allow a user callback to be defined for handling the cert ServicePointManager.CertificatePolicy = new TrustAllCertificatePolicy(); // Even though this will compile on Mono 2.4, it throws a runtime exception //ServicePointManager.ServerCertificateValidationCallback = TrustAllCertificatePolicy.TrustAllCertificateHandler; if (Client.Settings.USE_LLSD_LOGIN) { #region LLSD Based Login // Create the CAPS login structure OSDMap loginLLSD = new OSDMap(); loginLLSD["first"] = OSD.FromString(loginParams.FirstName); loginLLSD["last"] = OSD.FromString(loginParams.LastName); loginLLSD["passwd"] = OSD.FromString(loginParams.Password); loginLLSD["start"] = OSD.FromString(loginParams.Start); loginLLSD["channel"] = OSD.FromString(loginParams.Channel); loginLLSD["version"] = OSD.FromString(loginParams.Version); loginLLSD["platform"] = OSD.FromString(loginParams.Platform); loginLLSD["mac"] = OSD.FromString(loginParams.MAC); loginLLSD["agree_to_tos"] = OSD.FromBoolean(loginParams.AgreeToTos); loginLLSD["read_critical"] = OSD.FromBoolean(loginParams.ReadCritical); loginLLSD["viewer_digest"] = OSD.FromString(loginParams.ViewerDigest); loginLLSD["id0"] = OSD.FromString(loginParams.ID0); loginLLSD["last_exec_event"] = OSD.FromInteger((int)loginParams.LastExecEvent); // Create the options LLSD array OSDArray optionsOSD = new OSDArray(); for (int i = 0; i < loginParams.Options.Length; i++) optionsOSD.Add(OSD.FromString(loginParams.Options[i])); foreach (string[] callbackOpts in CallbackOptions.Values) { if (callbackOpts != null) { for (int i = 0; i < callbackOpts.Length; i++) { if (!optionsOSD.Contains(callbackOpts[i])) optionsOSD.Add(callbackOpts[i]); } } } loginLLSD["options"] = optionsOSD; // Make the CAPS POST for login Uri loginUri; try { loginUri = new Uri(loginParams.URI); } catch (Exception ex) { Logger.Log(String.Format("Failed to parse login URI {0}, {1}", loginParams.URI, ex.Message), Helpers.LogLevel.Error, Client); return; } CapsClient loginRequest = new CapsClient(loginUri); loginRequest.OnComplete += new CapsClient.CompleteCallback(LoginReplyLLSDHandler); loginRequest.UserData = CurrentContext; UpdateLoginStatus(LoginStatus.ConnectingToLogin, String.Format("Logging in as {0} {1}...", loginParams.FirstName, loginParams.LastName)); loginRequest.BeginGetResponse(loginLLSD, OSDFormat.Xml, Client.Settings.CAPS_TIMEOUT); #endregion } else { #region XML-RPC Based Login Code // Create the Hashtable for XmlRpcCs Hashtable loginXmlRpc = new Hashtable(); loginXmlRpc["first"] = loginParams.FirstName; loginXmlRpc["last"] = loginParams.LastName; loginXmlRpc["passwd"] = loginParams.Password; loginXmlRpc["start"] = loginParams.Start; loginXmlRpc["channel"] = loginParams.Channel; loginXmlRpc["version"] = loginParams.Version; loginXmlRpc["platform"] = loginParams.Platform; loginXmlRpc["mac"] = loginParams.MAC; if (loginParams.AgreeToTos) loginXmlRpc["agree_to_tos"] = "true"; if (loginParams.ReadCritical) loginXmlRpc["read_critical"] = "true"; loginXmlRpc["id0"] = loginParams.ID0; loginXmlRpc["last_exec_event"] = (int)loginParams.LastExecEvent; // Create the options array ArrayList options = new ArrayList(); for (int i = 0; i < loginParams.Options.Length; i++) options.Add(loginParams.Options[i]); foreach (string[] callbackOpts in CallbackOptions.Values) { if (callbackOpts != null) { for (int i = 0; i < callbackOpts.Length; i++) { if (!options.Contains(callbackOpts[i])) options.Add(callbackOpts[i]); } } } loginXmlRpc["options"] = options; try { ArrayList loginArray = new ArrayList(1); loginArray.Add(loginXmlRpc); XmlRpcRequest request = new XmlRpcRequest(CurrentContext.MethodName, loginArray); var cc = CurrentContext; // Start the request Thread requestThread = new Thread( delegate() { try { LoginReplyXmlRpcHandler( request.Send(cc.URI, cc.Timeout), loginParams); } catch (Exception e) { UpdateLoginStatus(LoginStatus.Failed, "Error opening the login server connection: " + e.Message); } }); requestThread.Name = "XML-RPC Login"; requestThread.Start(); } catch (Exception e) { UpdateLoginStatus(LoginStatus.Failed, "Error opening the login server connection: " + e); } #endregion } } private void UpdateLoginStatus(LoginStatus status, string message) { InternalStatusCode = status; InternalLoginMessage = message; Logger.DebugLog("Login status: " + status.ToString() + ": " + message, Client); // If we reached a login resolution trigger the event if (status == LoginStatus.Success || status == LoginStatus.Failed) { CurrentContext = null; LoginEvent.Set(); } // Fire the login status callback if (m_LoginProgress != null) { OnLoginProgress(new LoginProgressEventArgs(status, message, InternalErrorKey)); } } /// /// LoginParams and the initial login XmlRpcRequest were made on a remote machine. /// This method now initializes libomv with the results. /// public void RemoteLoginHandler(LoginResponseData response, LoginParams newContext) { CurrentContext = newContext; LoginReplyXmlRpcHandler(response, newContext); } /// /// Handles response from XML-RPC login replies /// private void LoginReplyXmlRpcHandler(XmlRpcResponse response, LoginParams context) { LoginResponseData reply = new LoginResponseData(); // Fetch the login response if (response == null || !(response.Value is Hashtable)) { UpdateLoginStatus(LoginStatus.Failed, "Invalid or missing login response from the server"); Logger.Log("Invalid or missing login response from the server", Helpers.LogLevel.Warning); return; } try { reply.Parse((Hashtable)response.Value); if (context.LoginID != CurrentContext.LoginID) { Logger.Log("Login response does not match login request. Only one login can be attempted at a time", Helpers.LogLevel.Error); return; } } catch (Exception e) { UpdateLoginStatus(LoginStatus.Failed, "Error retrieving the login response from the server: " + e.Message); Logger.Log("Login response failure: " + e.Message + " " + e.StackTrace, Helpers.LogLevel.Warning); return; } LoginReplyXmlRpcHandler(reply, context); } /// /// Handles response from XML-RPC login replies with already parsed LoginResponseData /// private void LoginReplyXmlRpcHandler(LoginResponseData reply, LoginParams context) { LoginResponseData = reply; ushort simPort = 0; uint regionX = 0; uint regionY = 0; string reason = reply.Reason; string message = reply.Message; if (reply.Login == "true") { // Remove the quotes around our first name. if (reply.FirstName[0] == '"') reply.FirstName = reply.FirstName.Remove(0, 1); if (reply.FirstName[reply.FirstName.Length - 1] == '"') reply.FirstName = reply.FirstName.Remove(reply.FirstName.Length - 1); #region Critical Information try { // Networking Client.Network.CircuitCode = (uint)reply.CircuitCode; regionX = (uint)reply.RegionX; regionY = (uint)reply.RegionY; simPort = (ushort)reply.SimPort; LoginSeedCapability = reply.SeedCapability; } catch (Exception) { UpdateLoginStatus(LoginStatus.Failed, "Login server failed to return critical information"); return; } #endregion Critical Information /* Add any blacklisted UDP packets to the blacklist * for exclusion from packet processing */ if (reply.UDPBlacklist != null) UDPBlacklist.AddRange(reply.UDPBlacklist.Split(',')); // Misc: MaxAgentGroups = reply.MaxAgentGroups; AgentAppearanceServiceURL = reply.AgentAppearanceServiceURL; //uint timestamp = (uint)reply.seconds_since_epoch; //DateTime time = Helpers.UnixTimeToDateTime(timestamp); // TODO: Do something with this? // Unhandled: // reply.gestures // reply.event_categories // reply.classified_categories // reply.event_notifications // reply.ui_config // reply.login_flags // reply.global_textures // reply.inventory_lib_root // reply.inventory_lib_owner // reply.inventory_skeleton // reply.inventory_skel_lib // reply.initial_outfit } bool redirect = (reply.Login == "indeterminate"); try { if (OnLoginResponse != null) { try { OnLoginResponse(reply.Success, redirect, message, reason, reply); } catch (Exception ex) { Logger.Log(ex.ToString(), Helpers.LogLevel.Error); } } } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, ex); } // Make the next network jump, if needed if (redirect) { UpdateLoginStatus(LoginStatus.Redirecting, "Redirecting login..."); LoginParams loginParams = CurrentContext; loginParams.URI = reply.NextUrl; loginParams.MethodName = reply.NextMethod; loginParams.Options = reply.NextOptions; // Sleep for some amount of time while the servers work int seconds = reply.NextDuration; Logger.Log("Sleeping for " + seconds + " seconds during a login redirect", Helpers.LogLevel.Info); Thread.Sleep(seconds * 1000); CurrentContext = loginParams; BeginLogin(); } else if (reply.Success) { UpdateLoginStatus(LoginStatus.ConnectingToSim, "Connecting to simulator..."); ulong handle = Utils.UIntsToLong(regionX, regionY); // Connect to the sim given in the login reply if (Connect(reply.SimIP, simPort, handle, true, LoginSeedCapability) != null) { // Request the economy data right after login SendPacket(new EconomyDataRequestPacket()); // Update the login message with the MOTD returned from the server UpdateLoginStatus(LoginStatus.Success, message); } else { UpdateLoginStatus(LoginStatus.Failed, "Unable to connect to simulator"); } } else { // Make sure a usable error key is set if (!String.IsNullOrEmpty(reason)) InternalErrorKey = reason; else InternalErrorKey = "unknown"; UpdateLoginStatus(LoginStatus.Failed, message); } } /// /// Handle response from LLSD login replies /// /// /// /// private void LoginReplyLLSDHandler(CapsClient client, OSD result, Exception error) { if (error == null) { if (result != null && result.Type == OSDType.Map) { OSDMap map = (OSDMap)result; OSD osd; LoginResponseData data = new LoginResponseData(); data.Parse(map); if (map.TryGetValue("login", out osd)) { bool loginSuccess = osd.AsBoolean(); bool redirect = (osd.AsString() == "indeterminate"); if (redirect) { // Login redirected // Make the next login URL jump UpdateLoginStatus(LoginStatus.Redirecting, data.Message); LoginParams loginParams = CurrentContext; loginParams.URI = LoginResponseData.ParseString("next_url", map); //CurrentContext.Params.MethodName = LoginResponseData.ParseString("next_method", map); // Sleep for some amount of time while the servers work int seconds = (int)LoginResponseData.ParseUInt("next_duration", map); Logger.Log("Sleeping for " + seconds + " seconds during a login redirect", Helpers.LogLevel.Info); Thread.Sleep(seconds * 1000); // Ignore next_options for now CurrentContext = loginParams; BeginLogin(); } else if (loginSuccess) { // Login succeeded // Fire the login callback if (OnLoginResponse != null) { try { OnLoginResponse(loginSuccess, redirect, data.Message, data.Reason, data); } catch (Exception ex) { Logger.Log(ex.Message, Helpers.LogLevel.Error, Client, ex); } } // These parameters are stored in NetworkManager, so instead of registering // another callback for them we just set the values here CircuitCode = (uint)data.CircuitCode; LoginSeedCapability = data.SeedCapability; UpdateLoginStatus(LoginStatus.ConnectingToSim, "Connecting to simulator..."); ulong handle = Utils.UIntsToLong((uint)data.RegionX, (uint)data.RegionY); if (data.SimIP != null && data.SimPort != 0) { // Connect to the sim given in the login reply if (Connect(data.SimIP, (ushort)data.SimPort, handle, true, LoginSeedCapability) != null) { // Request the economy data right after login SendPacket(new EconomyDataRequestPacket()); // Update the login message with the MOTD returned from the server UpdateLoginStatus(LoginStatus.Success, data.Message); } else { UpdateLoginStatus(LoginStatus.Failed, "Unable to establish a UDP connection to the simulator"); } } else { UpdateLoginStatus(LoginStatus.Failed, "Login server did not return a simulator address"); } } else { // Login failed // Make sure a usable error key is set if (data.Reason != String.Empty) InternalErrorKey = data.Reason; else InternalErrorKey = "unknown"; UpdateLoginStatus(LoginStatus.Failed, data.Message); } } else { // Got an LLSD map but no login value UpdateLoginStatus(LoginStatus.Failed, "login parameter missing in the response"); } } else { // No LLSD response InternalErrorKey = "bad response"; UpdateLoginStatus(LoginStatus.Failed, "Empty or unparseable login response"); } } else { // Connection error InternalErrorKey = "no connection"; UpdateLoginStatus(LoginStatus.Failed, error.Message); } } /// /// Get current OS /// /// Either "Win" or "Linux" public static string GetPlatform() { switch (Environment.OSVersion.Platform) { case PlatformID.Unix: return "Linux"; default: return "Win"; } } /// /// Get clients default Mac Address /// /// A string containing the first found Mac Address public static string GetMAC() { string mac = String.Empty; try { System.Net.NetworkInformation.NetworkInterface[] nics = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); if (nics != null && nics.Length > 0) { for (int i = 0; i < nics.Length; i++) { string adapterMac = nics[i].GetPhysicalAddress().ToString().ToUpper(); if (adapterMac.Length == 12 && adapterMac != "000000000000") { mac = adapterMac; continue; } } } } catch { } if (mac.Length < 12) mac = UUID.Random().ToString().Substring(24, 12); return String.Format("{0}:{1}:{2}:{3}:{4}:{5}", mac.Substring(0, 2), mac.Substring(2, 2), mac.Substring(4, 2), mac.Substring(6, 2), mac.Substring(8, 2), mac.Substring(10, 2)); } #endregion } #region EventArgs public class LoginProgressEventArgs : EventArgs { private readonly LoginStatus m_Status; private readonly String m_Message; private readonly String m_FailReason; public LoginStatus Status { get { return m_Status; } } public String Message { get { return m_Message; } } public string FailReason { get { return m_FailReason; } } public LoginProgressEventArgs(LoginStatus login, String message, String failReason) { this.m_Status = login; this.m_Message = message; this.m_FailReason = failReason; } } #endregion EventArgs }