Finalized the client's TCP IP address verification process for HG1.5.
parent
4b755c6d80
commit
2a1e45f657
|
@ -36,6 +36,7 @@ namespace OpenSim.Framework
|
||||||
public class AgentCircuitManager
|
public class AgentCircuitManager
|
||||||
{
|
{
|
||||||
public Dictionary<uint, AgentCircuitData> AgentCircuits = new Dictionary<uint, AgentCircuitData>();
|
public Dictionary<uint, AgentCircuitData> AgentCircuits = new Dictionary<uint, AgentCircuitData>();
|
||||||
|
public Dictionary<UUID, AgentCircuitData> AgentCircuitsByUUID = new Dictionary<UUID, AgentCircuitData>();
|
||||||
|
|
||||||
public virtual AuthenticateResponse AuthenticateSession(UUID sessionID, UUID agentID, uint circuitcode)
|
public virtual AuthenticateResponse AuthenticateSession(UUID sessionID, UUID agentID, uint circuitcode)
|
||||||
{
|
{
|
||||||
|
@ -86,10 +87,12 @@ namespace OpenSim.Framework
|
||||||
if (AgentCircuits.ContainsKey(circuitCode))
|
if (AgentCircuits.ContainsKey(circuitCode))
|
||||||
{
|
{
|
||||||
AgentCircuits[circuitCode] = agentData;
|
AgentCircuits[circuitCode] = agentData;
|
||||||
|
AgentCircuitsByUUID[agentData.AgentID] = agentData;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AgentCircuits.Add(circuitCode, agentData);
|
AgentCircuits.Add(circuitCode, agentData);
|
||||||
|
AgentCircuitsByUUID.Add(agentData.AgentID, agentData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,10 +102,26 @@ namespace OpenSim.Framework
|
||||||
lock (AgentCircuits)
|
lock (AgentCircuits)
|
||||||
{
|
{
|
||||||
if (AgentCircuits.ContainsKey(circuitCode))
|
if (AgentCircuits.ContainsKey(circuitCode))
|
||||||
|
{
|
||||||
|
UUID agentID = AgentCircuits[circuitCode].AgentID;
|
||||||
AgentCircuits.Remove(circuitCode);
|
AgentCircuits.Remove(circuitCode);
|
||||||
|
AgentCircuitsByUUID.Remove(agentID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual void RemoveCircuit(UUID agentID)
|
||||||
|
{
|
||||||
|
lock (AgentCircuits)
|
||||||
|
{
|
||||||
|
if (AgentCircuitsByUUID.ContainsKey(agentID))
|
||||||
|
{
|
||||||
|
uint circuitCode = AgentCircuitsByUUID[agentID].circuitcode;
|
||||||
|
AgentCircuits.Remove(circuitCode);
|
||||||
|
AgentCircuitsByUUID.Remove(agentID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
public AgentCircuitData GetAgentCircuitData(uint circuitCode)
|
public AgentCircuitData GetAgentCircuitData(uint circuitCode)
|
||||||
{
|
{
|
||||||
AgentCircuitData agentCircuit = null;
|
AgentCircuitData agentCircuit = null;
|
||||||
|
@ -110,6 +129,13 @@ namespace OpenSim.Framework
|
||||||
return agentCircuit;
|
return agentCircuit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AgentCircuitData GetAgentCircuitData(UUID agentID)
|
||||||
|
{
|
||||||
|
AgentCircuitData agentCircuit = null;
|
||||||
|
AgentCircuitsByUUID.TryGetValue(agentID, out agentCircuit);
|
||||||
|
return agentCircuit;
|
||||||
|
}
|
||||||
|
|
||||||
public void UpdateAgentData(AgentCircuitData agentData)
|
public void UpdateAgentData(AgentCircuitData agentData)
|
||||||
{
|
{
|
||||||
if (AgentCircuits.ContainsKey((uint) agentData.circuitcode))
|
if (AgentCircuits.ContainsKey((uint) agentData.circuitcode))
|
||||||
|
|
|
@ -99,6 +99,7 @@ namespace OpenSim.Framework.Capabilities
|
||||||
// private static readonly string m_remoteParcelRequestPath = "0009/";// This is in the LandManagementModule.
|
// private static readonly string m_remoteParcelRequestPath = "0009/";// This is in the LandManagementModule.
|
||||||
|
|
||||||
//private string eventQueue = "0100/";
|
//private string eventQueue = "0100/";
|
||||||
|
private IScene m_Scene;
|
||||||
private IHttpServer m_httpListener;
|
private IHttpServer m_httpListener;
|
||||||
private UUID m_agentID;
|
private UUID m_agentID;
|
||||||
private IAssetService m_assetCache;
|
private IAssetService m_assetCache;
|
||||||
|
@ -130,9 +131,10 @@ namespace OpenSim.Framework.Capabilities
|
||||||
public FetchInventoryDescendentsCAPS CAPSFetchInventoryDescendents = null;
|
public FetchInventoryDescendentsCAPS CAPSFetchInventoryDescendents = null;
|
||||||
public GetClientDelegate GetClient = null;
|
public GetClientDelegate GetClient = null;
|
||||||
|
|
||||||
public Caps(IAssetService assetCache, IHttpServer httpServer, string httpListen, uint httpPort, string capsPath,
|
public Caps(IScene scene, IAssetService assetCache, IHttpServer httpServer, string httpListen, uint httpPort, string capsPath,
|
||||||
UUID agent, bool dumpAssetsToFile, string regionName)
|
UUID agent, bool dumpAssetsToFile, string regionName)
|
||||||
{
|
{
|
||||||
|
m_Scene = scene;
|
||||||
m_assetCache = assetCache;
|
m_assetCache = assetCache;
|
||||||
m_capsObjectPath = capsPath;
|
m_capsObjectPath = capsPath;
|
||||||
m_httpListener = httpServer;
|
m_httpListener = httpServer;
|
||||||
|
@ -278,7 +280,13 @@ namespace OpenSim.Framework.Capabilities
|
||||||
public string CapsRequest(string request, string path, string param,
|
public string CapsRequest(string request, string path, string param,
|
||||||
OSHttpRequest httpRequest, OSHttpResponse httpResponse)
|
OSHttpRequest httpRequest, OSHttpResponse httpResponse)
|
||||||
{
|
{
|
||||||
//m_log.Debug("[CAPS]: Seed Caps Request in region: " + m_regionName);
|
m_log.Debug("[CAPS]: Seed Caps Request in region: " + m_regionName);
|
||||||
|
|
||||||
|
if (!m_Scene.CheckClient(m_agentID, httpRequest.RemoteIPEndPoint))
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("[CAPS]: Unauthorized CAPS client");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
string result = LLSDHelpers.SerialiseLLSDReply(m_capsHandlers.CapsDetails);
|
string result = LLSDHelpers.SerialiseLLSDReply(m_capsHandlers.CapsDetails);
|
||||||
|
|
||||||
|
|
|
@ -102,5 +102,7 @@ namespace OpenSim.Framework
|
||||||
void AddCommand(object module, string command, string shorthelp, string longhelp, CommandDelegate callback);
|
void AddCommand(object module, string command, string shorthelp, string longhelp, CommandDelegate callback);
|
||||||
|
|
||||||
ISceneObject DeserializeObject(string representation);
|
ISceneObject DeserializeObject(string representation);
|
||||||
|
|
||||||
|
bool CheckClient(UUID agentID, System.Net.IPEndPoint ep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ namespace OpenSim.Region.CoreModules.Agent.Capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
Caps caps
|
Caps caps
|
||||||
= new Caps(
|
= new Caps(m_scene,
|
||||||
m_scene.AssetService, MainServer.Instance, m_scene.RegionInfo.ExternalHostName,
|
m_scene.AssetService, MainServer.Instance, m_scene.RegionInfo.ExternalHostName,
|
||||||
MainServer.Instance.Port,
|
MainServer.Instance.Port,
|
||||||
capsObjectPath, agentId, m_scene.DumpAssetsToFile, m_scene.RegionInfo.RegionName);
|
capsObjectPath, agentId, m_scene.DumpAssetsToFile, m_scene.RegionInfo.RegionName);
|
||||||
|
|
|
@ -2629,17 +2629,10 @@ namespace OpenSim.Region.Framework.Scenes
|
||||||
AgentCircuitData aCircuit = m_authenticateHandler.GetAgentCircuitData(client.CircuitCode);
|
AgentCircuitData aCircuit = m_authenticateHandler.GetAgentCircuitData(client.CircuitCode);
|
||||||
|
|
||||||
// Do the verification here
|
// Do the verification here
|
||||||
System.Net.EndPoint ep = client.GetClientEP();
|
System.Net.IPEndPoint ep = (System.Net.IPEndPoint)client.GetClientEP();
|
||||||
if (aCircuit != null)
|
if (aCircuit != null)
|
||||||
{
|
{
|
||||||
if ((aCircuit.teleportFlags & (uint)Constants.TeleportFlags.ViaLogin) != 0)
|
if (!VerifyClient(aCircuit, ep, out vialogin))
|
||||||
{
|
|
||||||
m_log.DebugFormat("[Scene]: Incoming client {0} {1} in region {2} via Login", aCircuit.firstname, aCircuit.lastname, RegionInfo.RegionName);
|
|
||||||
vialogin = true;
|
|
||||||
IUserAgentVerificationModule userVerification = RequestModuleInterface<IUserAgentVerificationModule>();
|
|
||||||
if (userVerification != null && ep != null)
|
|
||||||
{
|
|
||||||
if (!userVerification.VerifyClient(aCircuit, ep.ToString()))
|
|
||||||
{
|
{
|
||||||
// uh-oh, this is fishy
|
// uh-oh, this is fishy
|
||||||
m_log.WarnFormat("[Scene]: Agent {0} with session {1} connecting with unidentified end point {2}. Refusing service.",
|
m_log.WarnFormat("[Scene]: Agent {0} with session {1} connecting with unidentified end point {2}. Refusing service.",
|
||||||
|
@ -2654,10 +2647,6 @@ namespace OpenSim.Region.Framework.Scenes
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
m_log.DebugFormat("[Scene]: User Client Verification for {0} {1} returned true", aCircuit.firstname, aCircuit.lastname);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_log.Debug("[Scene] Adding new agent " + client.Name + " to scene " + RegionInfo.RegionName);
|
m_log.Debug("[Scene] Adding new agent " + client.Name + " to scene " + RegionInfo.RegionName);
|
||||||
|
@ -2682,7 +2671,65 @@ namespace OpenSim.Region.Framework.Scenes
|
||||||
EventManager.TriggerOnClientLogin(client);
|
EventManager.TriggerOnClientLogin(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool VerifyClient(AgentCircuitData aCircuit, System.Net.IPEndPoint ep, out bool vialogin)
|
||||||
|
{
|
||||||
|
vialogin = false;
|
||||||
|
|
||||||
|
// Do the verification here
|
||||||
|
if ((aCircuit.teleportFlags & (uint)Constants.TeleportFlags.ViaLogin) != 0)
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("[Scene]: Incoming client {0} {1} in region {2} via Login", aCircuit.firstname, aCircuit.lastname, RegionInfo.RegionName);
|
||||||
|
vialogin = true;
|
||||||
|
IUserAgentVerificationModule userVerification = RequestModuleInterface<IUserAgentVerificationModule>();
|
||||||
|
if (userVerification != null && ep != null)
|
||||||
|
{
|
||||||
|
if (!userVerification.VerifyClient(aCircuit, ep.Address.ToString()))
|
||||||
|
{
|
||||||
|
// uh-oh, this is fishy
|
||||||
|
m_log.DebugFormat("[Scene]: User Client Verification for {0} {1} in {2} returned false", aCircuit.firstname, aCircuit.lastname, RegionInfo.RegionName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
m_log.DebugFormat("[Scene]: User Client Verification for {0} {1} in {2} returned true", aCircuit.firstname, aCircuit.lastname, RegionInfo.RegionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by Caps, on the first HTTP contact from the client
|
||||||
|
public override bool CheckClient(UUID agentID, System.Net.IPEndPoint ep)
|
||||||
|
{
|
||||||
|
AgentCircuitData aCircuit = m_authenticateHandler.GetAgentCircuitData(agentID);
|
||||||
|
if (aCircuit != null)
|
||||||
|
{
|
||||||
|
bool vialogin = false;
|
||||||
|
if (!VerifyClient(aCircuit, ep, out vialogin))
|
||||||
|
{
|
||||||
|
// if it doesn't pass, we remove the agentcircuitdata altogether
|
||||||
|
// and the scene presence and the client, if they exist
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ScenePresence sp = GetScenePresence(agentID);
|
||||||
|
if (sp != null)
|
||||||
|
sp.ControllingClient.Close();
|
||||||
|
|
||||||
|
// BANG! SLASH!
|
||||||
|
m_authenticateHandler.RemoveCircuit(agentID);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("[Scene]: Exception while closing aborted client: {0}", e.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register for events from the client
|
/// Register for events from the client
|
||||||
|
|
|
@ -536,5 +536,6 @@ namespace OpenSim.Region.Framework.Scenes
|
||||||
get { return false; }
|
get { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract bool CheckClient(UUID agentID, System.Net.IPEndPoint ep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,11 @@ namespace OpenSim.Region.Framework.Scenes.Tests
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool CheckClient(UUID agentID, System.Net.IPEndPoint ep)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
@ -204,6 +204,11 @@ namespace OpenSim.Services.Connectors.Hypergrid
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetClientToken(UUID sessionID, string token)
|
||||||
|
{
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
public GridRegion GetHomeRegion(UUID userID, out Vector3 position, out Vector3 lookAt)
|
public GridRegion GetHomeRegion(UUID userID, out Vector3 position, out Vector3 lookAt)
|
||||||
{
|
{
|
||||||
position = Vector3.UnitY; lookAt = Vector3.UnitY;
|
position = Vector3.UnitY; lookAt = Vector3.UnitY;
|
||||||
|
|
|
@ -148,6 +148,15 @@ namespace OpenSim.Services.HypergridService
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetClientToken(UUID sessionID, string token)
|
||||||
|
{
|
||||||
|
if (m_TravelingAgents.ContainsKey(sessionID))
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("[USER AGENT SERVICE]: Setting token {0} for session {1}", token, sessionID);
|
||||||
|
m_TravelingAgents[sessionID].ClientToken = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TravelingAgentInfo UpdateTravelInfo(AgentCircuitData agentCircuit, GridRegion region)
|
TravelingAgentInfo UpdateTravelInfo(AgentCircuitData agentCircuit, GridRegion region)
|
||||||
{
|
{
|
||||||
TravelingAgentInfo travel = new TravelingAgentInfo();
|
TravelingAgentInfo travel = new TravelingAgentInfo();
|
||||||
|
@ -203,22 +212,16 @@ namespace OpenSim.Services.HypergridService
|
||||||
|
|
||||||
public bool VerifyClient(UUID sessionID, string token)
|
public bool VerifyClient(UUID sessionID, string token)
|
||||||
{
|
{
|
||||||
return true;
|
m_log.DebugFormat("[USER AGENT SERVICE]: Verifying Client session {0} with token {1}", sessionID, token);
|
||||||
|
//return true;
|
||||||
|
|
||||||
// Commenting this for now until I understand better what part of a sender's
|
// Commenting this for now until I understand better what part of a sender's
|
||||||
// info stays unchanged throughout a session
|
// info stays unchanged throughout a session
|
||||||
//
|
//
|
||||||
//if (m_TravelingAgents.ContainsKey(sessionID))
|
if (m_TravelingAgents.ContainsKey(sessionID))
|
||||||
//{
|
return m_TravelingAgents[sessionID].ClientToken == token;
|
||||||
// // Aquiles heel. Must trust the first grid upon login
|
|
||||||
// if (m_TravelingAgents[sessionID].ClientToken == string.Empty)
|
return false;
|
||||||
// {
|
|
||||||
// m_TravelingAgents[sessionID].ClientToken = token;
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// return m_TravelingAgents[sessionID].ClientToken == token;
|
|
||||||
//}
|
|
||||||
//return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool VerifyAgent(UUID sessionID, string token)
|
public bool VerifyAgent(UUID sessionID, string token)
|
||||||
|
|
|
@ -49,6 +49,7 @@ namespace OpenSim.Services.Interfaces
|
||||||
public interface IUserAgentService
|
public interface IUserAgentService
|
||||||
{
|
{
|
||||||
bool LoginAgentToGrid(AgentCircuitData agent, GridRegion gatekeeper, GridRegion finalDestination, out string reason);
|
bool LoginAgentToGrid(AgentCircuitData agent, GridRegion gatekeeper, GridRegion finalDestination, out string reason);
|
||||||
|
void SetClientToken(UUID sessionID, string token);
|
||||||
void LogoutAgent(UUID userID, UUID sessionID);
|
void LogoutAgent(UUID userID, UUID sessionID);
|
||||||
GridRegion GetHomeRegion(UUID userID, out Vector3 position, out Vector3 lookAt);
|
GridRegion GetHomeRegion(UUID userID, out Vector3 position, out Vector3 lookAt);
|
||||||
|
|
||||||
|
|
|
@ -329,7 +329,7 @@ namespace OpenSim.Services.LLLoginService
|
||||||
// Instantiate/get the simulation interface and launch an agent at the destination
|
// Instantiate/get the simulation interface and launch an agent at the destination
|
||||||
//
|
//
|
||||||
string reason = string.Empty;
|
string reason = string.Empty;
|
||||||
AgentCircuitData aCircuit = LaunchAgentAtGrid(gatekeeper, destination, account, avatar, session, secureSession, position, where, clientVersion, out where, out reason);
|
AgentCircuitData aCircuit = LaunchAgentAtGrid(gatekeeper, destination, account, avatar, session, secureSession, position, where, clientVersion, clientIP, out where, out reason);
|
||||||
|
|
||||||
if (aCircuit == null)
|
if (aCircuit == null)
|
||||||
{
|
{
|
||||||
|
@ -589,7 +589,7 @@ namespace OpenSim.Services.LLLoginService
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AgentCircuitData LaunchAgentAtGrid(GridRegion gatekeeper, GridRegion destination, UserAccount account, AvatarData avatar,
|
protected AgentCircuitData LaunchAgentAtGrid(GridRegion gatekeeper, GridRegion destination, UserAccount account, AvatarData avatar,
|
||||||
UUID session, UUID secureSession, Vector3 position, string currentWhere, string viewer, out string where, out string reason)
|
UUID session, UUID secureSession, Vector3 position, string currentWhere, string viewer, IPEndPoint clientIP, out string where, out string reason)
|
||||||
{
|
{
|
||||||
where = currentWhere;
|
where = currentWhere;
|
||||||
ISimulationService simConnector = null;
|
ISimulationService simConnector = null;
|
||||||
|
@ -655,7 +655,7 @@ namespace OpenSim.Services.LLLoginService
|
||||||
{
|
{
|
||||||
circuitCode = (uint)Util.RandomClass.Next(); ;
|
circuitCode = (uint)Util.RandomClass.Next(); ;
|
||||||
aCircuit = MakeAgent(destination, account, avatar, session, secureSession, circuitCode, position, viewer);
|
aCircuit = MakeAgent(destination, account, avatar, session, secureSession, circuitCode, position, viewer);
|
||||||
success = LaunchAgentIndirectly(gatekeeper, destination, aCircuit, out reason);
|
success = LaunchAgentIndirectly(gatekeeper, destination, aCircuit, clientIP, out reason);
|
||||||
if (!success && m_GridService != null)
|
if (!success && m_GridService != null)
|
||||||
{
|
{
|
||||||
// Try the fallback regions
|
// Try the fallback regions
|
||||||
|
@ -664,7 +664,7 @@ namespace OpenSim.Services.LLLoginService
|
||||||
{
|
{
|
||||||
foreach (GridRegion r in fallbacks)
|
foreach (GridRegion r in fallbacks)
|
||||||
{
|
{
|
||||||
success = LaunchAgentIndirectly(gatekeeper, r, aCircuit, out reason);
|
success = LaunchAgentIndirectly(gatekeeper, r, aCircuit, clientIP, out reason);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
where = "safe";
|
where = "safe";
|
||||||
|
@ -741,10 +741,15 @@ namespace OpenSim.Services.LLLoginService
|
||||||
return simConnector.CreateAgent(region, aCircuit, (int)Constants.TeleportFlags.ViaLogin, out reason);
|
return simConnector.CreateAgent(region, aCircuit, (int)Constants.TeleportFlags.ViaLogin, out reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool LaunchAgentIndirectly(GridRegion gatekeeper, GridRegion destination, AgentCircuitData aCircuit, out string reason)
|
private bool LaunchAgentIndirectly(GridRegion gatekeeper, GridRegion destination, AgentCircuitData aCircuit, IPEndPoint clientIP, out string reason)
|
||||||
{
|
{
|
||||||
m_log.Debug("[LLOGIN SERVICE] Launching agent at " + destination.RegionName);
|
m_log.Debug("[LLOGIN SERVICE] Launching agent at " + destination.RegionName);
|
||||||
return m_UserAgentService.LoginAgentToGrid(aCircuit, gatekeeper, destination, out reason);
|
if (m_UserAgentService.LoginAgentToGrid(aCircuit, gatekeeper, destination, out reason))
|
||||||
|
{
|
||||||
|
m_UserAgentService.SetClientToken(aCircuit.SessionID, clientIP.Address.ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Console Commands
|
#region Console Commands
|
||||||
|
|
Loading…
Reference in New Issue