453 lines
16 KiB
C#
453 lines
16 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Google.Protobuf;
|
|
|
|
namespace Tadah.Arbiter
|
|
{
|
|
internal class Client
|
|
{
|
|
public string IpAddress { get; private set; }
|
|
public int Port { get; private set; }
|
|
public Socket Socket { get; set; }
|
|
|
|
public Client(Socket socket)
|
|
{
|
|
this.IpAddress = null;
|
|
this.Port = 0;
|
|
this.Socket = socket;
|
|
|
|
IPEndPoint remote = socket.RemoteEndPoint as IPEndPoint;
|
|
IPEndPoint local = socket.LocalEndPoint as IPEndPoint;
|
|
|
|
if (remote != null)
|
|
{
|
|
this.IpAddress = remote.Address.ToString();
|
|
this.Port = remote.Port;
|
|
}
|
|
|
|
if (local != null)
|
|
{
|
|
this.IpAddress = local.Address.ToString();
|
|
this.Port = local.Port;
|
|
}
|
|
|
|
if (local == null && remote == null)
|
|
{
|
|
throw new("Failed to resolve information from socket");
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class StateObject
|
|
{
|
|
public byte[] Buffer = new byte[1024];
|
|
public ushort Size = 0;
|
|
public Socket WorkSocket = null;
|
|
public Client Client;
|
|
}
|
|
|
|
public class Service
|
|
{
|
|
private static readonly ManualResetEvent Finished = new(false);
|
|
private static Socket Listener;
|
|
|
|
public static int Start()
|
|
{
|
|
IPEndPoint localEndPoint = new(IPAddress.Any, Configuration.ServicePort);
|
|
|
|
Listener = new(IPAddress.Any.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
|
|
|
try
|
|
{
|
|
Listener.Bind(localEndPoint);
|
|
Listener.Listen(100);
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
Task.Run(() => ListenForConnections());
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.Error($"ListenForConnections failed: {exception}");
|
|
}
|
|
}
|
|
|
|
return Configuration.ServicePort;
|
|
}
|
|
|
|
public static void Stop()
|
|
{
|
|
Listener.Close();
|
|
}
|
|
|
|
private static void ListenForConnections()
|
|
{
|
|
while (true)
|
|
{
|
|
Finished.Reset();
|
|
Listener.BeginAccept(new(AcceptCallback), Listener);
|
|
Finished.WaitOne();
|
|
}
|
|
}
|
|
|
|
private static void AcceptCallback(IAsyncResult result)
|
|
{
|
|
Client client = null;
|
|
Socket handler = null;
|
|
|
|
try
|
|
{
|
|
Finished.Set();
|
|
|
|
Socket listener = (Socket)result.AsyncState;
|
|
handler = listener.EndAccept(result);
|
|
client = new(handler);
|
|
|
|
Log.Write($"[ArbiterService] '{client.IpAddress}' Connected on port {client.Port}", LogSeverity.Event);
|
|
}
|
|
finally
|
|
{
|
|
StateObject state = new();
|
|
state.WorkSocket = handler;
|
|
state.Client = client;
|
|
|
|
handler.BeginReceive(state.Buffer, 0, 1024, 0, new(ReadCallback), state);
|
|
}
|
|
}
|
|
|
|
private static void ReadCallback(IAsyncResult result)
|
|
{
|
|
StateObject state = (StateObject)result.AsyncState;
|
|
Socket handler = state.WorkSocket;
|
|
|
|
try
|
|
{
|
|
int read = handler.EndReceive(result);
|
|
|
|
if (read > 0)
|
|
{
|
|
/*
|
|
* TadahMessage format:
|
|
*
|
|
* 0x02 0x00 0x00 0x00 0x00 0x02 0x00 0x00 .. .. 0x02 0x00 0x00 .. ..
|
|
* (STX) (UINT16) (UINT16) (STX) (UINT16) (DATA) (STX) (UINT16) (DATA)
|
|
* (MSGREAD) (MSGSIZE) (CHKSUM) (SIGREAD) (SIGSIZE) (SIGDATA) (BUFREAD) (BUFSIZE) (BUFDATA)
|
|
*
|
|
* From this, we can do some sanity checks while getting a complete read *and* fending off potential attackers;
|
|
* - See if the message begins with our MSGREAD
|
|
* - If it does, then recieve next 2 bytes; try parsing as a uint8 and use that as message size
|
|
* - Read message up to the specified message size
|
|
* - Try parsing the entire message
|
|
* - Verify buffer with given signature
|
|
* - Send buffer to ProcessData
|
|
*/
|
|
|
|
if (state.Buffer[0] != 0x02)
|
|
{
|
|
Log.Write($"[ArbiterService::ReadCallback] '{state.Client.IpAddress}' - Not a TadahMessage", LogSeverity.Warning);
|
|
handler.Close();
|
|
}
|
|
|
|
if (read == 3)
|
|
{
|
|
if (state.Size == 0)
|
|
{
|
|
ushort parsed = 0;
|
|
|
|
try
|
|
{
|
|
parsed = BitConverter.ToUInt16(state.Buffer, 1); // Offset by 1 (MSGREAD[STX])
|
|
}
|
|
catch
|
|
{
|
|
Log.Write($"[ArbiterService::ReadCallback] '{state.Client.IpAddress}' - Not a TadahMessage", LogSeverity.Warning);
|
|
handler.Close();
|
|
}
|
|
finally
|
|
{
|
|
if (parsed == 0)
|
|
{
|
|
Log.Write($"[ArbiterService::ReadCallback] '{state.Client.IpAddress}' - Not a TadahMessage", LogSeverity.Warning);
|
|
handler.Close();
|
|
}
|
|
|
|
state.Size = parsed;
|
|
}
|
|
}
|
|
|
|
handler.BeginReceive(state.Buffer, 0, state.Size - 3, 0, new AsyncCallback(ReadCallback), state); // state.Size - 3 for 3 bytes already read (MSGREAD[STX], MSGSIZE[UINT8])
|
|
}
|
|
|
|
// Parse given data
|
|
if (!Message.TryParse(state.Buffer, out Message message))
|
|
{
|
|
Log.Write($"[ArbiterService::ReadCallback] '{state.Client.IpAddress}' - Not a TadahMessage", LogSeverity.Warning);
|
|
handler.Close();
|
|
}
|
|
|
|
// Verify signature
|
|
if (!Signature.Verify(message.Data, message.Signature))
|
|
{
|
|
Log.Write($"[ArbiterService::ReadCallback] '{state.Client.IpAddress}' - Bad or invalid signature", LogSeverity.Warning);
|
|
handler.Close();
|
|
}
|
|
|
|
// See if signal is older than ten seconds (so that people can't re-send signed destructive commands, such as "CloseAllJobs")
|
|
if (Unix.Now() + 10 > Unix.From(message.Signal.Nonce.ToDateTime()))
|
|
{
|
|
Log.Write($"[ArbiterService::ReadCallback] '{state.Client.IpAddress}' - Too old message received", LogSeverity.Warning);
|
|
handler.Close();
|
|
}
|
|
|
|
// Process data
|
|
byte[] response = ProcessSignal(message.Signal, state.Client);
|
|
SendData(handler, response);
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.Write($"[ArbiterService::ReadCallback] - {exception}", LogSeverity.Error);
|
|
}
|
|
}
|
|
|
|
private static void SendCallback(IAsyncResult result)
|
|
{
|
|
try
|
|
{
|
|
Socket handler = (Socket)result.AsyncState;
|
|
|
|
int bytesSent = handler.EndSend(result);
|
|
handler.Shutdown(SocketShutdown.Both);
|
|
handler.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Write($"[ArbiterService::SendCallback] {ex.Message}", LogSeverity.Error);
|
|
}
|
|
}
|
|
|
|
private static void SendData(Socket handler, byte[] data)
|
|
{
|
|
try
|
|
{
|
|
handler.BeginSend(data, 0, data.Length, 0, new AsyncCallback(SendCallback), handler);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Write($"[ArbiterService::SendData] {ex.Message}", LogSeverity.Error);
|
|
}
|
|
}
|
|
|
|
private static byte[] SignalError(Proto.Signal signal, Client client, string reason)
|
|
{
|
|
byte[] stream = new byte[1024];
|
|
|
|
try
|
|
{
|
|
Log.Write($"[ArbiterService::{client.IpAddress}] Tried '{Enum.GetName(signal.Operation)}' with '{signal.JobId}' - {reason}", LogSeverity.Warning);
|
|
}
|
|
finally
|
|
{
|
|
new Proto.Response
|
|
{
|
|
Operation = signal.Operation,
|
|
Success = false,
|
|
Message = reason
|
|
}.WriteTo(stream);
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
private static byte[] ProcessSignal(Proto.Signal signal, Client client)
|
|
{
|
|
byte[] stream = new byte[1024];
|
|
bool success = false;
|
|
object responseData = null;
|
|
|
|
try
|
|
{
|
|
switch (signal.Operation)
|
|
{
|
|
case Proto.Operation.OpenJob:
|
|
{
|
|
if (signal.Place.Count == 0)
|
|
{
|
|
return SignalError(signal, client, "No place specified");
|
|
}
|
|
|
|
Proto.Signal.Types.Place place = signal.Place[0];
|
|
|
|
if (JobManager.JobExists(signal.JobId))
|
|
{
|
|
return SignalError(signal, client, "Job already exists");
|
|
}
|
|
|
|
if (!JobManager.IsValidVersion(signal.Version))
|
|
{
|
|
return SignalError(signal, client, "Invalid ClientVersion");
|
|
}
|
|
|
|
if (JobManager.OpenJobs.Count >= Configuration.MaximumPlaceJobs)
|
|
{
|
|
return SignalError(signal, client, "Maximum amount of jobs reached on this arbiter -- please try another");
|
|
}
|
|
|
|
Task.Run(() => JobManager.OpenJob(signal.JobId, place.PlaceId, signal.Version));
|
|
success = true;
|
|
|
|
break;
|
|
}
|
|
|
|
case Proto.Operation.CloseJob:
|
|
{
|
|
if (JobManager.JobExists(signal.JobId))
|
|
{
|
|
return SignalError(signal, client, "Job doesn't exist");
|
|
}
|
|
|
|
Task.Run(() => JobManager.CloseJob(signal.JobId));
|
|
success = true;
|
|
|
|
break;
|
|
}
|
|
|
|
case Proto.Operation.ExecuteScript:
|
|
{
|
|
Job job = JobManager.GetJobFromId(signal.JobId);
|
|
|
|
if (job == null)
|
|
{
|
|
return SignalError(signal, client, "Job does not exist");
|
|
}
|
|
|
|
if (job is Taipei.Job)
|
|
{
|
|
return SignalError(signal, client, "Job does not support such functionality (is TaipeiJob)");
|
|
}
|
|
|
|
if (signal.Place.Count == 0)
|
|
{
|
|
return SignalError(signal, client, "No place specified");
|
|
}
|
|
|
|
Proto.Signal.Types.Place place = signal.Place[0];
|
|
Task.Run(() => { job.ExecuteScript(place.Script); });
|
|
|
|
success = true;
|
|
|
|
break;
|
|
}
|
|
|
|
case Proto.Operation.RenewTampaJobLease:
|
|
{
|
|
Job job = JobManager.GetJobFromId(signal.JobId);
|
|
|
|
if (job == null)
|
|
{
|
|
return SignalError(signal, client, "Job doesn't exist");
|
|
}
|
|
|
|
if (job is not Tampa.Job)
|
|
{
|
|
return SignalError(signal, client, "Job does not support such functionality (is TampaJob)");
|
|
}
|
|
|
|
if (signal.Place.Count == 0)
|
|
{
|
|
return SignalError(signal, client, "No place specified");
|
|
}
|
|
|
|
Tampa.Job taJob = (Tampa.Job)job;
|
|
Task.Run(() => { taJob.RenewLease(signal.Place[0].ExpirationInSeconds); });
|
|
|
|
success = true;
|
|
|
|
break;
|
|
}
|
|
|
|
case Proto.Operation.CloseAllJobs:
|
|
{
|
|
int jobs = JobManager.OpenJobs.Count;
|
|
|
|
Task.Run(() => { JobManager.CloseAllJobs(); });
|
|
|
|
success = true;
|
|
responseData = jobs;
|
|
|
|
break;
|
|
}
|
|
|
|
case Proto.Operation.CloseAllTampaProcesses:
|
|
{
|
|
int processes = Tampa.ProcessManager.OpenProcesses.Count;
|
|
|
|
Task.Run(() => { Tampa.ProcessManager.CloseAllProcesses(); });
|
|
|
|
success = true;
|
|
responseData = processes;
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
new Proto.Response
|
|
{
|
|
Success = false,
|
|
Message = "No such operation exists"
|
|
}.WriteTo(stream);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.Write($"[ArbiterService::ProcessData] '{client.IpAddress}' - {exception.Message}", LogSeverity.Error);
|
|
|
|
#if DEBUG
|
|
new Proto.Response
|
|
{
|
|
Operation = signal.Operation,
|
|
Success = false,
|
|
Message = exception.Message,
|
|
}.WriteTo(stream);
|
|
#else
|
|
// Will just give a blank error
|
|
#endif
|
|
}
|
|
|
|
if (success)
|
|
{
|
|
Proto.Response response = new()
|
|
{
|
|
Operation = signal.Operation,
|
|
Success = true
|
|
};
|
|
|
|
if (responseData != null)
|
|
{
|
|
response.Data = (string)responseData;
|
|
}
|
|
|
|
response.WriteTo(stream);
|
|
}
|
|
else
|
|
{
|
|
// If it wasn't successful, the stream has already been written to
|
|
// In the case of an exception or the "default" fallthrough in the switch statement.
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
}
|
|
}
|