corrade-vassal – Rev 16
?pathlinks?
/*
* 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
/// <summary>
///
/// </summary>
public enum LoginStatus
{
/// <summary></summary>
Failed = -1,
/// <summary></summary>
None = 0,
/// <summary></summary>
ConnectingToLogin,
/// <summary></summary>
ReadingResponse,
/// <summary></summary>
ConnectingToSim,
/// <summary></summary>
Redirecting,
/// <summary></summary>
Success
}
/// <summary>
/// Status of the last application run.
/// Used for error reporting to the grid login service for statistical purposes.
/// </summary>
public enum LastExecStatus
{
/// <summary> Application exited normally </summary>
Normal = 0,
/// <summary> Application froze </summary>
Froze,
/// <summary> Application detected error and exited abnormally </summary>
ForcedCrash,
/// <summary> Other crash </summary>
OtherCrash,
/// <summary> Application froze during logout </summary>
LogoutFroze,
/// <summary> Application crashed during logout </summary>
LogoutCrash
}
#endregion Enums
#region Structs
/// <summary>
/// Login Request Parameters
/// </summary>
public class LoginParams
{
/// <summary>The URL of the Login Server</summary>
public string URI;
/// <summary>The number of milliseconds to wait before a login is considered
/// failed due to timeout</summary>
public int Timeout;
/// <summary>The request method</summary>
/// <remarks>login_to_simulator is currently the only supported method</remarks>
public string MethodName;
/// <summary>The Agents First name</summary>
public string FirstName;
/// <summary>The Agents Last name</summary>
public string LastName;
/// <summary>A md5 hashed password</summary>
/// <remarks>plaintext password will be automatically hashed</remarks>
public string Password;
/// <summary>The agents starting location once logged in</summary>
/// <remarks>Either "last", "home", or a string encoded URI
/// containing the simulator name and x/y/z coordinates e.g: uri:hooper&128&152&17</remarks>
public string Start;
/// <summary>A string containing the client software channel information</summary>
/// <example>Second Life Release</example>
public string Channel;
/// <summary>The client software version information</summary>
/// <remarks>The official viewer uses: Second Life Release n.n.n.n
/// where n is replaced with the current version of the viewer</remarks>
public string Version;
/// <summary>A string containing the platform information the agent is running on</summary>
public string Platform;
/// <summary>A string hash of the network cards Mac Address</summary>
public string MAC;
/// <summary>Unknown or deprecated</summary>
public string ViewerDigest;
/// <summary>A string hash of the first disk drives ID used to identify this clients uniqueness</summary>
public string ID0;
/// <summary>A string containing the viewers Software, this is not directly sent to the login server but
/// instead is used to generate the Version string</summary>
public string UserAgent;
/// <summary>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</summary>
public string Author;
/// <summary>If true, this agent agrees to the Terms of Service of the grid its connecting to</summary>
public bool AgreeToTos;
/// <summary>Unknown</summary>
public bool ReadCritical;
/// <summary>Status of the last application run sent to the grid login server for statistical purposes</summary>
public LastExecStatus LastExecEvent = LastExecStatus.Normal;
/// <summary>An array of string sent to the login server to enable various options</summary>
public string[] Options;
/// <summary>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</summary>
internal UUID LoginID;
/// <summary>
/// Default constuctor, initializes sane default values
/// </summary>
public LoginParams()
{
List<string> options = new List<string>(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;
}
/// <summary>
/// Instantiates new LoginParams object and fills in the values
/// </summary>
/// <param name="client">Instance of GridClient to read settings from</param>
/// <param name="firstName">Login first name</param>
/// <param name="lastName">Login last name</param>
/// <param name="password">Password</param>
/// <param name="channel">Login channnel (application name)</param>
/// <param name="version">Client version, should be application name + version number</param>
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;
}
/// <summary>
/// Instantiates new LoginParams object and fills in the values
/// </summary>
/// <param name="client">Instance of GridClient to read settings from</param>
/// <param name="firstName">Login first name</param>
/// <param name="lastName">Login last name</param>
/// <param name="password">Password</param>
/// <param name="channel">Login channnel (application name)</param>
/// <param name="version">Client version, should be application name + version number</param>
/// <param name="loginURI">URI of the login server</param>
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;
}
/// <summary>
/// The decoded data returned from the login server after a successful login
/// </summary>
public struct LoginResponseData
{
/// <summary>true, false, indeterminate</summary>
//[XmlRpcMember("login")]
public string Login;
public bool Success;
public string Reason;
/// <summary>Login message of the day</summary>
public string Message;
public UUID AgentID;
public UUID SessionID;
public UUID SecureSessionID;
public string FirstName;
public string LastName;
public string StartLocation;
/// <summary>M or PG, also agent_region_access and agent_access_max</summary>
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;
/// <summary>
/// Parse LLSD Login Reply Data
/// </summary>
/// <param name="reply">An <seealso cref="OSDMap"/>
/// contaning the login response data</param>
/// <remarks>XML-RPC logins do not require this as XML-RPC.NET
/// automatically populates the struct properly using attributes</remarks>
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<BuddyListEntry> buddys = new List<BuddyListEntry>();
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<BuddyListEntry> buddys = new List<BuddyListEntry>();
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<InventoryFolder> folders = new List<InventoryFolder>();
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<InventoryFolder> folders = new List<InventoryFolder>();
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<InventoryFolder> folders = new List<InventoryFolder>();
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
/// <summary>
/// Login Routines
/// </summary>
public partial class NetworkManager
{
#region Delegates
//////LoginProgress
//// LoginProgress
/// <summary>The event subscribers, null of no subscribers</summary>
private EventHandler<LoginProgressEventArgs> m_LoginProgress;
///<summary>Raises the LoginProgress Event</summary>
/// <param name="e">A LoginProgressEventArgs object containing
/// the data sent from the simulator</param>
protected virtual void OnLoginProgress(LoginProgressEventArgs e)
{
EventHandler<LoginProgressEventArgs> handler = m_LoginProgress;
if (handler != null)
handler(this, e);
}
/// <summary>Thread sync lock object</summary>
private readonly object m_LoginProgressLock = new object();
/// <summary>Raised when the simulator sends us data containing
/// ...</summary>
public event EventHandler<LoginProgressEventArgs> LoginProgress
{
add { lock (m_LoginProgressLock) { m_LoginProgress += value; } }
remove { lock (m_LoginProgressLock) { m_LoginProgress -= value; } }
}
///// <summary>The event subscribers, null of no subscribers</summary>
//private EventHandler<LoggedInEventArgs> m_LoggedIn;
/////<summary>Raises the LoggedIn Event</summary>
///// <param name="e">A LoggedInEventArgs object containing
///// the data sent from the simulator</param>
//protected virtual void OnLoggedIn(LoggedInEventArgs e)
//{
// EventHandler<LoggedInEventArgs> handler = m_LoggedIn;
// if (handler != null)
// handler(this, e);
//}
///// <summary>Thread sync lock object</summary>
//private readonly object m_LoggedInLock = new object();
///// <summary>Raised when the simulator sends us data containing
///// ...</summary>
//public event EventHandler<LoggedInEventArgs> LoggedIn
//{
// add { lock (m_LoggedInLock) { m_LoggedIn += value; } }
// remove { lock (m_LoggedInLock) { m_LoggedIn -= value; } }
//}
/// <summary>
///
/// </summary>
/// <param name="loginSuccess"></param>
/// <param name="redirect"></param>
/// <param name="replyData"></param>
/// <param name="message"></param>
/// <param name="reason"></param>
public delegate void LoginResponseCallback(bool loginSuccess, bool redirect, string message, string reason, LoginResponseData replyData);
#endregion Delegates
#region Events
/// <summary>Called when a reply is received from the login server, the
/// login sequence will block until this event returns</summary>
private event LoginResponseCallback OnLoginResponse;
#endregion Events
#region Public Members
/// <summary>Seed CAPS URL returned from the login server</summary>
public string LoginSeedCapability = String.Empty;
/// <summary>Current state of logging in</summary>
public LoginStatus LoginStatusCode { get { return InternalStatusCode; } }
/// <summary>Upon login failure, contains a short string key for the
/// type of login error that occurred</summary>
public string LoginErrorKey { get { return InternalErrorKey; } }
/// <summary>The raw XML-RPC reply from the login server, exactly as it
/// was received (minus the HTTP header)</summary>
public string RawLoginReply { get { return InternalRawLoginReply; } }
/// <summary>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</summary>
public string LoginMessage { get { return InternalLoginMessage; } }
/// <summary>Maximum number of groups an agent can belong to, -1 for unlimited</summary>
public int MaxAgentGroups = -1;
/// <summary>Server side baking service URL</summary>
public string AgentAppearanceServiceURL;
/// <summary>Parsed login response data</summary>
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<LoginResponseCallback, string[]> CallbackOptions = new Dictionary<LoginResponseCallback, string[]>();
/// <summary>A list of packets obtained during the login process which
/// networkmanager will log but not process</summary>
private readonly List<string> UDPBlacklist = new List<string>();
#endregion
#region Public Methods
/// <summary>
/// Generate sane default values for a login request
/// </summary>
/// <param name="firstName">Account first name</param>
/// <param name="lastName">Account last name</param>
/// <param name="password">Account password</param>
/// <param name="channel">Client application name (channel)</param>
/// <param name="version">Client application name + version</param>
/// <returns>A populated <seealso cref="LoginParams"/> struct containing
/// sane defaults</returns>
public LoginParams DefaultLoginParams(string firstName, string lastName, string password,
string channel, string version)
{
return new LoginParams(Client, firstName, lastName, password, channel, version);
}
/// <summary>
/// Simplified login that takes the most common and required fields
/// </summary>
/// <param name="firstName">Account first name</param>
/// <param name="lastName">Account last name</param>
/// <param name="password">Account password</param>
/// <param name="channel">Client application name (channel)</param>
/// <param name="version">Client application name + version</param>
/// <returns>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</returns>
public bool Login(string firstName, string lastName, string password, string channel, string version)
{
return Login(firstName, lastName, password, channel, "last", version);
}
/// <summary>
/// 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
/// </summary>
/// <param name="firstName">Account first name</param>
/// <param name="lastName">Account last name</param>
/// <param name="password">Account password or MD5 hash of the password
/// such as $1$1682a1e45e9f957dcdf0bb56eb43319c</param>
/// <param name="channel">Client application name (channel)</param>
/// <param name="start">Starting location URI that can be built with
/// StartLocation()</param>
/// <param name="version">Client application name + version</param>
/// <returns>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</returns>
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);
}
/// <summary>
/// Login that takes a struct of all the values that will be passed to
/// the login server
/// </summary>
/// <param name="loginParams">The values that will be passed to the login
/// server, all fields must be set even if they are String.Empty</param>
/// <returns>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</returns>
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;
}
/// <summary>
/// Build a start location URI for passing to the Login function
/// </summary>
/// <param name="sim">Name of the simulator to start in</param>
/// <param name="x">X coordinate to start at</param>
/// <param name="y">Y coordinate to start at</param>
/// <param name="z">Z coordinate to start at</param>
/// <returns>String with a URI that can be used to login to a specified
/// location</returns>
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<string>().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));
}
}
/// <summary>
/// LoginParams and the initial login XmlRpcRequest were made on a remote machine.
/// This method now initializes libomv with the results.
/// </summary>
public void RemoteLoginHandler(LoginResponseData response, LoginParams newContext)
{
CurrentContext = newContext;
LoginReplyXmlRpcHandler(response, newContext);
}
/// <summary>
/// Handles response from XML-RPC login replies
/// </summary>
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);
}
/// <summary>
/// Handles response from XML-RPC login replies with already parsed LoginResponseData
/// </summary>
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);
}
}
/// <summary>
/// Handle response from LLSD login replies
/// </summary>
/// <param name="client"></param>
/// <param name="result"></param>
/// <param name="error"></param>
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);
}
}
/// <summary>
/// Get current OS
/// </summary>
/// <returns>Either "Win" or "Linux"</returns>
public static string GetPlatform()
{
switch (Environment.OSVersion.Platform)
{
case PlatformID.Unix:
return "Linux";
default:
return "Win";
}
}
/// <summary>
/// Get clients default Mac Address
/// </summary>
/// <returns>A string containing the first found Mac Address</returns>
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
}
Generated by GNU Enscript 1.6.5.90.