clean up rcon, fix a bunch of little things

This commit is contained in:
RaidMax
2018-09-29 14:52:22 -05:00
parent 5d93e7ac57
commit d45729d7e1
33 changed files with 993 additions and 813 deletions

View File

@ -13,145 +13,44 @@ namespace SharedLibraryCore.RCon
{
class ConnectionState
{
public Socket Client { get; private set; }
public int BufferSize { get; private set; }
public byte[] Buffer { get; private set; }
public StringBuilder ResponseString { get; }
public ConnectionState(Socket cl)
{
BufferSize = 8192;
Buffer = new byte[BufferSize];
Client = cl;
ResponseString = new StringBuilder();
}
public int ConnectionAttempts { get; set; }
const int BufferSize = 4096;
public readonly byte[] ReceiveBuffer = new byte[BufferSize];
public readonly SemaphoreSlim OnComplete = new SemaphoreSlim(1, 1);
}
public class Connection
{
static readonly ConcurrentDictionary<EndPoint, ConnectionState> ActiveQueries = new ConcurrentDictionary<EndPoint, ConnectionState>();
public IPEndPoint Endpoint { get; private set; }
public string RConPassword { get; private set; }
ILogger Log;
int FailedSends;
int FailedReceives;
string response;
ManualResetEvent OnConnected;
ManualResetEvent OnSent;
ManualResetEvent OnReceived;
public Connection(string ipAddress, int port, string password, ILogger log)
{
Endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);
RConPassword = password;
Log = log;
OnConnected = new ManualResetEvent(false);
OnSent = new ManualResetEvent(false);
OnReceived = new ManualResetEvent(false);
}
private void OnConnectedCallback(IAsyncResult ar)
{
var serverSocket = (Socket)ar.AsyncState;
try
{
serverSocket.EndConnect(ar);
#if DEBUG
Log.WriteDebug($"Successfully initialized socket to {serverSocket.RemoteEndPoint}");
#endif
OnConnected.Set();
}
catch (SocketException e)
{
throw new NetworkException($"Could not initialize socket for RCon - {e.Message}");
}
}
private void OnSentCallback(IAsyncResult ar)
{
Socket serverConnection = (Socket)ar.AsyncState;
try
{
int sentByteNum = serverConnection.EndSend(ar);
#if DEBUG
Log.WriteDebug($"Sent {sentByteNum} bytes to {serverConnection.RemoteEndPoint}");
#endif
// this is where we override our await to make it
OnSent.Set();
}
catch (Exception)
{
}
}
private void OnReceivedCallback(IAsyncResult ar)
{
var connectionState = (ConnectionState)ar.AsyncState;
var serverConnection = connectionState.Client;
try
{
int bytesRead = serverConnection.EndReceive(ar);
if (bytesRead > 0)
{
#if DEBUG
Log.WriteDebug($"Received {bytesRead} bytes from {serverConnection.RemoteEndPoint}");
#endif
FailedReceives = 0;
connectionState.ResponseString.Append(Utilities.EncodingType.GetString(connectionState.Buffer, 0, bytesRead).TrimEnd('\0') + '\n');
if (!connectionState.Buffer.Take(4).ToArray().SequenceEqual(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }))
throw new NetworkException("Unexpected packet received");
/* if (FailedReceives == 0 && serverConnection.Available > 0)
{
serverConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0,
new AsyncCallback(OnReceivedCallback), connectionState);
}
else*/
{
response = connectionState.ResponseString.ToString();
OnReceived.Set();
}
}
else
{
response = connectionState.ResponseString.ToString();
OnReceived.Set();
}
}
catch (SocketException)
{
}
catch (ObjectDisposedException)
{
// Log.WriteWarning($"Tried to check for more available bytes for disposed socket on {Endpoint}");
}
}
public async Task<string[]> SendQueryAsync(StaticHelpers.QueryType type, string parameters = "", bool waitForResponse = true)
{
//// will this really prevent flooding?
//if ((DateTime.Now - LastQuery).TotalMilliseconds < 350)
//{
// Thread.Sleep(350);
// //await Task.Delay(350);
//}
if (!ActiveQueries.ContainsKey(this.Endpoint))
{
ActiveQueries.TryAdd(this.Endpoint, new ConnectionState());
}
// LastQuery = DateTime.Now;
var connectionState = ActiveQueries[this.Endpoint];
#if DEBUG == true
Log.WriteDebug($"Waiting for semaphore to be released [${this.Endpoint}]");
#endif
await connectionState.OnComplete.WaitAsync();
#if DEBUG == true
Log.WriteDebug($"Semaphore has been released [${this.Endpoint}]");
#endif
OnSent.Reset();
OnReceived.Reset();
byte[] payload = null;
switch (type)
@ -171,130 +70,103 @@ namespace SharedLibraryCore.RCon
break;
}
using (var socketConnection = new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp) { ExclusiveAddressUse = true } )
byte[] response = null;
retrySend:
#if DEBUG == true
Log.WriteDebug($"Sending {payload.Length} bytes to [${this.Endpoint}] ({connectionState.ConnectionAttempts++}/{StaticHelpers.AllowedConnectionFails})");
#endif
try
{
socketConnection.BeginConnect(Endpoint, new AsyncCallback(OnConnectedCallback), socketConnection);
retrySend:
try
{
#if DEBUG
Console.WriteLine($"Sending Command {parameters}");
#endif
if (!OnConnected.WaitOne(StaticHelpers.SocketTimeout))
throw new SocketException((int)SocketError.TimedOut);
socketConnection.BeginSend(payload, 0, payload.Length, 0, new AsyncCallback(OnSentCallback), socketConnection);
bool success = await Task.FromResult(OnSent.WaitOne(StaticHelpers.SocketTimeout));
if (!success)
{
FailedSends++;
#if DEBUG
Log.WriteDebug($"{FailedSends} failed sends to {socketConnection.RemoteEndPoint.ToString()}");
#endif
if (FailedSends < 4)
goto retrySend;
else if (FailedSends == 4)
Log.WriteError($"Failed to send data to {socketConnection.RemoteEndPoint}");
}
else
{
if (FailedSends >= 4)
{
Log.WriteVerbose($"Resumed send RCon connection with {socketConnection.RemoteEndPoint}");
FailedSends = 0;
}
}
}
catch (SocketException e)
{
// this result is normal if the server is not listening
if (e.NativeErrorCode != (int)SocketError.ConnectionReset &&
e.NativeErrorCode != (int)SocketError.TimedOut)
throw new NetworkException($"Unexpected error while sending data to server - {e.Message}");
}
if (!waitForResponse)
return await Task.FromResult(new string[] { "" });
var connectionState = new ConnectionState(socketConnection);
retryReceive:
try
{
socketConnection.BeginReceive(connectionState.Buffer, 0, connectionState.Buffer.Length, 0,
new AsyncCallback(OnReceivedCallback), connectionState);
bool success = await Task.FromResult(OnReceived.WaitOne(StaticHelpers.SocketTimeout));
if (!success)
{
FailedReceives++;
#if DEBUG
Log.WriteDebug($"{FailedReceives} failed receives from {socketConnection.RemoteEndPoint.ToString()}");
#endif
if (FailedReceives < 4)
goto retrySend;
else if (FailedReceives == 4)
{
// Log.WriteError($"Failed to receive data from {socketConnection.RemoteEndPoint} after {FailedReceives} tries");
}
if (FailedReceives >= 4)
{
throw new NetworkException($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]} {socketConnection.RemoteEndPoint.ToString()}");
}
}
else
{
if (FailedReceives >= 4)
{
Log.WriteVerbose($"Resumed receive RCon connection from {socketConnection.RemoteEndPoint.ToString()}");
FailedReceives = 0;
}
}
}
catch (SocketException e)
{
// this result is normal if the server is not listening
if (e.NativeErrorCode != (int)SocketError.ConnectionReset &&
e.NativeErrorCode != (int)SocketError.TimedOut)
throw new NetworkException($"Unexpected error while receiving data from server - {e.Message}");
else if (FailedReceives < 4)
{
goto retryReceive;
}
else if (FailedReceives == 4)
{
// Log.WriteError($"Failed to receive data from {socketConnection.RemoteEndPoint} after {FailedReceives} tries");
}
if (FailedReceives >= 4)
{
throw new NetworkException(e.Message);
}
}
string queryResponse = response;
if (queryResponse.Contains("Invalid password"))
throw new NetworkException("RCON password is invalid");
if (queryResponse.ToString().Contains("rcon_password"))
throw new NetworkException("RCON password has not been set");
string[] splitResponse = queryResponse.Split(new char[]
{
'\n'
}, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim()).ToArray();
return splitResponse;
response = await SendPayloadAsync(payload);
}
catch (Exception ex)
{
if(connectionState.ConnectionAttempts < StaticHelpers.AllowedConnectionFails)
{
connectionState.ConnectionAttempts++;
Log.WriteWarning($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]} [{this.Endpoint}] ({connectionState.ConnectionAttempts++}/{StaticHelpers.AllowedConnectionFails})");
await Task.Delay(StaticHelpers.SocketTimeout.Milliseconds);
goto retrySend;
}
ActiveQueries.TryRemove(this.Endpoint, out _);
Log.WriteDebug(ex.GetExceptionInfo());
throw new NetworkException($"{Utilities.CurrentLocalization.LocalizationIndex["SERVER_ERROR_COMMUNICATION"]} [{this.Endpoint}]");
}
ActiveQueries.TryRemove(this.Endpoint, out _);
string responseString = Utilities.EncodingType.GetString(response, 0, response.Length).TrimEnd('\0') + '\n';
if (responseString.Contains("Invalid password"))
{
// todo: localize this
throw new NetworkException("RCON password is invalid");
}
if (responseString.ToString().Contains("rcon_password"))
{
throw new NetworkException("RCON password has not been set");
}
string[] splitResponse = responseString.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim()).ToArray();
return splitResponse;
}
private async Task<byte[]> SendPayloadAsync(byte[] payload)
{
var rconSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{
DontFragment = true,
Ttl = 42,
ExclusiveAddressUse = true
};
var connectionState = ActiveQueries[this.Endpoint];
var outgoingDataArgs = new SocketAsyncEventArgs()
{
RemoteEndPoint = this.Endpoint
};
outgoingDataArgs.SetBuffer(payload);
outgoingDataArgs.Completed += OnDataSent;
// send the data to the server
bool sendDataPending = rconSocket.SendToAsync(outgoingDataArgs);
var incomingDataArgs = new SocketAsyncEventArgs()
{
RemoteEndPoint = this.Endpoint
};
incomingDataArgs.SetBuffer(connectionState.ReceiveBuffer);
incomingDataArgs.Completed += OnDataReceived;
// get our response back
rconSocket.ReceiveFromAsync(incomingDataArgs);
if (!await connectionState.OnComplete.WaitAsync(StaticHelpers.SocketTimeout.Milliseconds))
{
throw new NetworkException("Timed out waiting for response", rconSocket);
}
byte[] response = connectionState.ReceiveBuffer;
return response;
}
private void OnDataReceived(object sender, SocketAsyncEventArgs e)
{
#if DEBUG == true
Log.WriteDebug($"Read {e.BytesTransferred} bytes from {e.RemoteEndPoint.ToString()}");
#endif
ActiveQueries[this.Endpoint].OnComplete.Release(1);
}
private void OnDataSent(object sender, SocketAsyncEventArgs e)
{
#if DEBUG == true
Log.WriteDebug($"Sent {e.Buffer.Length} bytes to {e.ConnectSocket.RemoteEndPoint.ToString()}");
#endif
}
}
}

View File

@ -39,10 +39,11 @@ namespace SharedLibraryCore.RCon
/// <summary>
/// timeout in seconds to wait for a socket send or receive before giving up
/// </summary>
public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 10);
public static readonly TimeSpan SocketTimeout = new TimeSpan(0, 0, 0, 0, 150);
/// <summary>
/// interval in milliseconds to wait before sending the next RCon request
/// </summary>
public static readonly int FloodProtectionInterval = 350;
public static readonly int AllowedConnectionFails = 3;
}
}