using System;
using System.Diagnostics;
using System.Text;
using System.Net;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Reflection;
using System.IO;
using System.Net.Http;
using RageCoop.Core;
using Newtonsoft.Json;
using Lidgren.Network;
using System.Timers;
using System.Security.Cryptography;
using RageCoop.Server.Scripting;
using System.Net.Sockets;
using System.Threading.Tasks;
using RageCoop.Core.Scripting;
namespace RageCoop.Server
{
///
/// The instantiable RageCoop server class
///
public partial class Server
{
///
/// The API for controlling server and hooking events.
///
public API API { get; private set; }
internal readonly BaseScript BaseScript;
internal readonly Settings Settings;
internal NetServer MainNetServer;
internal ServerEntities Entities;
internal readonly Dictionary> Commands = new();
internal readonly Dictionary ClientsByNetHandle = new();
internal readonly Dictionary ClientsByName = new();
internal readonly Dictionary ClientsByID = new();
internal Client _hostClient;
private Dictionary InProgressFileTransfers=new();
internal Resources Resources;
internal Logger Logger;
private Security Security;
private bool _stopping = false;
private Thread _listenerThread;
private Thread _announceThread;
private Thread _latencyThread;
private Worker _worker;
private HashSet _allowedCharacterSet;
private Dictionary> PendingResponses=new();
internal Dictionary> RequestHandlers=new();
private readonly string _compatibleVersion = "V0_5";
///
/// Instantiate a server.
///
///
///
///
public Server(Settings settings,Logger logger=null)
{
Settings = settings;
if (settings==null) { throw new ArgumentNullException("Server settings cannot be null!"); }
Logger=logger;
if (Logger!=null) { Logger.LogLevel=Settings.LogLevel;}
API=new API(this);
Resources=new Resources(this);
Security=new Security(Logger);
Entities=new ServerEntities(this);
BaseScript=new BaseScript(this);
_allowedCharacterSet=new HashSet(Settings.AllowedUsernameChars.ToCharArray());
_worker=new Worker("ServerWorker", Logger);
_listenerThread=new Thread(() => Listen());
_latencyThread=new Thread(() =>
{
while (!_stopping)
{
foreach(var c in ClientsByNetHandle.Values.ToArray())
{
try
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
new Packets.PlayerInfoUpdate()
{
PedID=c.Player.ID,
Username=c.Username,
Latency=c.Latency,
Position=c.Player.Position
}.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, NetDeliveryMethod.ReliableSequenced, (byte)ConnectionChannel.Default);
}
catch(Exception ex)
{
Logger?.Error(ex);
}
}
Thread.Sleep(1000);
}
});
_announceThread=new Thread(async () =>
{
try
{
// TLS only
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls13 | SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
HttpClient httpClient = new();
IpInfo info;
try
{
info = CoreUtils.GetIPInfo();
Logger?.Info($"Your public IP is {info.Address}, announcing to master server...");
}
catch (Exception ex)
{
Logger?.Error(ex.InnerException?.Message ?? ex.Message);
return;
}
while (!_stopping)
{
HttpResponseMessage response = null;
try
{
var serverInfo = new ServerInfo
{
address = info.Address,
port=Settings.Port.ToString(),
country=info.Country,
name=Settings.Name,
version=_compatibleVersion.Replace("_", "."),
players=MainNetServer.ConnectionsCount.ToString(),
maxPlayers=Settings.MaxPlayers.ToString(),
description=Settings.Description,
website=Settings.Website,
gameMode=Settings.GameMode,
language=Settings.Language,
useP2P=Settings.UseP2P,
useZT=Settings.UseZeroTier,
ztID=Settings.UseZeroTier ? Settings.ZeroTierNetworkID : "",
ztAddress=Settings.UseZeroTier ? ZeroTierHelper.Networks[Settings.ZeroTierNetworkID].Addresses.Where(x => !x.Contains(":")).First() : "0.0.0.0",
};
string msg = JsonConvert.SerializeObject(serverInfo);
var realUrl = Util.GetFinalRedirect(Settings.MasterServer);
response = await httpClient.PostAsync(realUrl, new StringContent(msg, Encoding.UTF8, "application/json"));
}
catch (Exception ex)
{
Logger?.Error($"MasterServer: {ex.Message}");
// Sleep for 5s
Thread.Sleep(5000);
continue;
}
if (response == null)
{
Logger?.Error("MasterServer: Something went wrong!");
}
else if (response.StatusCode != HttpStatusCode.OK)
{
if (response.StatusCode == HttpStatusCode.BadRequest)
{
string requestContent = await response.Content.ReadAsStringAsync();
Logger?.Error($"MasterServer: [{(int)response.StatusCode}], {requestContent}");
}
else
{
Logger?.Error($"MasterServer: [{(int)response.StatusCode}]");
Logger?.Error($"MasterServer: [{await response.Content.ReadAsStringAsync()}]");
}
}
// Sleep for 10s
for (int i = 0; i<10; i++)
{
if (_stopping)
{
break;
}
else
{
Thread.Sleep(1000);
}
}
}
}
catch (HttpRequestException ex)
{
Logger?.Error($"MasterServer: {ex.InnerException.Message}");
}
catch (Exception ex)
{
Logger?.Error($"MasterServer: {ex.Message}");
}
});
}
///
/// Spawn threads and start the server
///
public void Start()
{
Logger?.Info("================");
Logger?.Info($"Server bound to: 0.0.0.0:{Settings.Port}");
Logger?.Info($"Server version: {Assembly.GetCallingAssembly().GetName().Version}");
Logger?.Info($"Compatible RAGECOOP versions: {_compatibleVersion.Replace('_', '.')}.x");
Logger?.Info("================");
if (Settings.UseZeroTier)
{
Logger?.Info($"Joining ZeroTier network: "+Settings.ZeroTierNetworkID);
if (ZeroTierHelper.Join(Settings.ZeroTierNetworkID)==null)
{
throw new Exception("Failed to obtain ZeroTier network IP");
}
}
else if (Settings.UseP2P)
{
Logger?.Warning("ZeroTier is not enabled, P2P connection may not work as expected.");
}
// 623c92c287cc392406e7aaaac1c0f3b0 = RAGECOOP
NetPeerConfiguration config = new("623c92c287cc392406e7aaaac1c0f3b0")
{
Port = Settings.Port,
MaximumConnections = Settings.MaxPlayers,
EnableUPnP = false,
AutoFlushSendQueue = true,
PingInterval=5
};
config.EnableMessageType(NetIncomingMessageType.ConnectionApproval);
config.EnableMessageType(NetIncomingMessageType.UnconnectedData);
MainNetServer = new NetServer(config);
MainNetServer.Start();
Logger?.Info(string.Format("Server listening on {0}:{1}", config.LocalAddress.ToString(), config.Port));
BaseScript.API=API;
BaseScript.OnStart();
Resources.LoadAll();
_listenerThread.Start();
_latencyThread.Start();
if (Settings.AnnounceSelf)
{
_announceThread.Start();
}
Logger?.Info("Listening for clients");
}
///
/// Terminate threads and stop the server
///
public void Stop()
{
_stopping = true;
Logger?.Flush();
_listenerThread.Join();
_latencyThread.Join();
if (_announceThread.IsAlive)
{
_announceThread.Join();
}
_worker.Dispose();
}
private void Listen()
{
NetIncomingMessage msg=null;
while (!_stopping)
{
try
{
msg=MainNetServer.WaitMessage(200);
ProcessMessage(msg);
}
catch(Exception ex)
{
Logger?.Error("Error processing message");
Logger?.Error(ex);
if (msg!=null)
{
DisconnectAndLog(msg.SenderConnection, PacketType.Unknown, ex);
}
}
}
Logger?.Info("Server is shutting down!");
MainNetServer.Shutdown("Server is shutting down!");
BaseScript.OnStop();
Resources.UnloadAll();
}
private void ProcessMessage(NetIncomingMessage message)
{
Client sender;
if (message == null) { return; }
switch (message.MessageType)
{
case NetIncomingMessageType.ConnectionApproval:
{
Logger?.Info($"New incoming connection from: [{message.SenderConnection.RemoteEndPoint}]");
if (message.ReadByte() != (byte)PacketType.Handshake)
{
Logger?.Info($"IP [{message.SenderConnection.RemoteEndPoint.Address}] was blocked, reason: Wrong packet!");
message.SenderConnection.Deny("Wrong packet!");
}
else
{
try
{
int len = message.ReadInt32();
byte[] data = message.ReadBytes(len);
GetHandshake(message.SenderConnection, data.GetPacket());
}
catch (Exception e)
{
Logger?.Info($"IP [{message.SenderConnection.RemoteEndPoint.Address}] was blocked, reason: {e.Message}");
Logger?.Error(e);
message.SenderConnection.Deny(e.Message);
}
}
break;
}
case NetIncomingMessageType.StatusChanged:
{
// Get sender client
if (!ClientsByNetHandle.TryGetValue(message.SenderConnection.RemoteUniqueIdentifier, out sender))
{
break;
}
NetConnectionStatus status = (NetConnectionStatus)message.ReadByte();
if (status == NetConnectionStatus.Disconnected)
{
PlayerDisconnected(sender);
}
else if (status == NetConnectionStatus.Connected)
{
PlayerConnected(sender);
_worker.QueueJob(() => API.Events.InvokePlayerConnected(sender));
Resources.SendTo(sender);
}
break;
}
case NetIncomingMessageType.Data:
{
// Get sender client
if (ClientsByNetHandle.TryGetValue(message.SenderConnection.RemoteUniqueIdentifier, out sender))
{
// Get packet type
var type = (PacketType)message.ReadByte();
switch (type)
{
case PacketType.Response:
{
int id = message.ReadInt32();
if (PendingResponses.TryGetValue(id, out var callback))
{
callback((PacketType)message.ReadByte(), message.ReadBytes(message.ReadInt32()));
PendingResponses.Remove(id);
}
break;
}
case PacketType.Request:
{
int id = message.ReadInt32();
if (RequestHandlers.TryGetValue((PacketType)message.ReadByte(), out var handler))
{
var response=MainNetServer.CreateMessage();
response.Write((byte)PacketType.Response);
response.Write(id);
handler(message.ReadBytes(message.ReadInt32()),sender).Pack(response);
MainNetServer.SendMessage(response,message.SenderConnection,NetDeliveryMethod.ReliableOrdered);
}
break;
}
default:
{
byte[] data = message.ReadBytes(message.ReadInt32());
if (type.IsSyncEvent())
{
// Sync Events
if (Settings.UseP2P) { break; }
try
{
var toSend = MainNetServer.Connections.Exclude(message.SenderConnection);
if (toSend.Count!=0)
{
var outgoingMessage = MainNetServer.CreateMessage();
outgoingMessage.Write((byte)type);
outgoingMessage.Write(data.Length);
outgoingMessage.Write(data);
MainNetServer.SendMessage(outgoingMessage, toSend, NetDeliveryMethod.UnreliableSequenced, (byte)ConnectionChannel.SyncEvents);
}
}
catch (Exception e)
{
DisconnectAndLog(message.SenderConnection, type, e);
}
}
else
{
HandlePacket(type, data, sender);
}
break;
}
}
}
break;
}
case NetIncomingMessageType.ErrorMessage:
Logger?.Error(message.ReadString());
break;
case NetIncomingMessageType.WarningMessage:
Logger?.Warning(message.ReadString());
break;
case NetIncomingMessageType.DebugMessage:
case NetIncomingMessageType.VerboseDebugMessage:
Logger?.Debug(message.ReadString());
break;
case NetIncomingMessageType.UnconnectedData:
{
if (message.ReadByte()==(byte)PacketType.PublicKeyRequest)
{
var msg = MainNetServer.CreateMessage();
var p=new Packets.PublicKeyResponse();
Security.GetPublicKey(out p.Modulus,out p.Exponent);
p.Pack(msg);
Logger?.Debug($"Sending public key to {message.SenderEndPoint}, length:{msg.LengthBytes}");
MainNetServer.SendUnconnectedMessage(msg, message.SenderEndPoint);
}
}
break;
default:
Logger?.Error(string.Format("Unhandled type: {0} {1} bytes {2} | {3}", message.MessageType, message.LengthBytes, message.DeliveryMethod, message.SequenceChannel));
break;
}
MainNetServer.Recycle(message);
}
internal void QueueJob(Action job)
{
_worker.QueueJob(job);
}
private void HandlePacket(PacketType type,byte[] data,Client sender)
{
try
{
switch (type)
{
case PacketType.PedSync:
PedSync(data.GetPacket(), sender);
break;
case PacketType.VehicleSync:
VehicleSync(data.GetPacket(), sender);
break;
case PacketType.ProjectileSync:
ProjectileSync(data.GetPacket(), sender);
break;
case PacketType.ChatMessage:
{
Packets.ChatMessage packet = new((b) =>
{
return Security.Decrypt(b,sender.EndPoint);
});
packet.Deserialize(data);
ChatMessageReceived(packet.Username,packet.Message, sender);
}
break;
case PacketType.Voice:
{
if (Settings.UseVoice)
{
Forward(data.GetPacket(), sender, ConnectionChannel.Voice);
}
}
break;
case PacketType.CustomEvent:
{
Packets.CustomEvent packet = new Packets.CustomEvent();
packet.Deserialize(data);
_worker.QueueJob(() => API.Events.InvokeCustomEventReceived(packet, sender));
}
break;
default:
Logger?.Error("Unhandled Data / Packet type");
break;
}
}
catch (Exception e)
{
DisconnectAndLog(sender.Connection, type, e);
}
}
// Send a message to targets or all players
internal void ChatMessageReceived(string name, string message,Client sender=null)
{
if (message.StartsWith('/'))
{
string[] cmdArgs = message.Split(" ");
string cmdName = cmdArgs[0].Remove(0, 1);
_worker.QueueJob(()=>API.Events.InvokeOnCommandReceived(cmdName, cmdArgs, sender));
return;
}
message = message.Replace("~", "");
_worker.QueueJob(() => API.Events.InvokeOnChatMessage(message, sender));
foreach(var c in ClientsByNetHandle.Values)
{
var msg = MainNetServer.CreateMessage();
var crypt = new Func((s) =>
{
return Security.Encrypt(s.GetBytes(), c.EndPoint);
});
new Packets.ChatMessage(crypt)
{
Username=name,
Message=message
}.Pack(msg);
MainNetServer.SendMessage(msg,c.Connection, NetDeliveryMethod.ReliableOrdered, (int)ConnectionChannel.Chat);
}
Logger?.Info(name + ": " + message);
}
internal void SendChatMessage(string name, string message, Client target)
{
if(target == null) { return; }
var msg = MainNetServer.CreateMessage();
new Packets.ChatMessage(new Func((s) =>
{
return Security.Encrypt(s.GetBytes(), target.EndPoint);
}))
{
Username= name,
Message=message,
}.Pack(msg);
MainNetServer.SendMessage(msg, target.Connection, NetDeliveryMethod.ReliableOrdered, (int)ConnectionChannel.Chat);
}
internal void RegisterCommand(string name, string usage, short argsLength, Action callback)
{
Command command = new(name) { Usage = usage, ArgsLength = argsLength };
if (Commands.ContainsKey(command))
{
throw new Exception("Command \"" + command.Name + "\" was already been registered!");
}
Commands.Add(command, callback);
}
internal void RegisterCommand(string name, Action callback)
{
Command command = new(name);
if (Commands.ContainsKey(command))
{
throw new Exception("Command \"" + command.Name + "\" was already been registered!");
}
Commands.Add(command, callback);
}
internal void RegisterCommands()
{
IEnumerable commands = typeof(T).GetMethods().Where(method => method.GetCustomAttributes(typeof(Command), false).Any());
foreach (MethodInfo method in commands)
{
Command attribute = method.GetCustomAttribute(true);
RegisterCommand(attribute.Name, attribute.Usage, attribute.ArgsLength, (Action)Delegate.CreateDelegate(typeof(Action), method));
}
}
internal T GetResponse(Client client,Packet request, ConnectionChannel channel = ConnectionChannel.RequestResponse,int timeout=5000) where T:Packet, new()
{
if (Thread.CurrentThread==_listenerThread)
{
throw new InvalidOperationException("Cannot wait for response from the listener thread!");
}
var received=new AutoResetEvent(false);
byte[] response=null;
var id = NewRequestID();
PendingResponses.Add(id, (type,p) =>
{
response=p;
received.Set();
});
var msg = MainNetServer.CreateMessage();
msg.Write((byte)PacketType.Request);
msg.Write(id);
request.Pack(msg);
MainNetServer.SendMessage(msg,client.Connection,NetDeliveryMethod.ReliableOrdered,(int)channel);
if (received.WaitOne(timeout))
{
var p = new T();
p.Deserialize(response);
return p;
}
else
{
return null;
}
}
internal void SendFile(string path,string name,Client client,Action updateCallback=null)
{
SendFile(File.OpenRead(path), name,client,NewFileID(),updateCallback);
}
internal void SendFile(Stream stream, string name, Client client,int id=default, Action updateCallback = null)
{
id = id ==default? NewFileID(): id ;
stream.Seek(0, SeekOrigin.Begin);
var total = stream.Length;
if (GetResponse(client, new Packets.FileTransferRequest()
{
FileLength= total,
Name=name,
ID=id,
}, ConnectionChannel.File)?.Response!=FileResponse.NeedToDownload)
{
Logger?.Info($"Skipping file transfer \"{name}\" to {client.Username}");
stream.Close();
stream.Dispose();
return;
}
Logger?.Debug($"Initiating file transfer:{name}, {total}");
FileTransfer transfer = new()
{
ID=id,
Name = name,
};
InProgressFileTransfers.Add(id, transfer);
int read = 0;
int thisRead;
do
{
// 4 KB chunk
byte[] chunk = new byte[4096];
read += thisRead=stream.Read(chunk, 0, 4096);
if (thisRead!=chunk.Length)
{
if (thisRead==0) { break; }
Logger?.Trace($"Purging chunk:{thisRead}");
Array.Resize(ref chunk, thisRead);
}
Send(
new Packets.FileTransferChunk()
{
ID=id,
FileChunk=chunk,
},
client, ConnectionChannel.File, NetDeliveryMethod.ReliableOrdered);
transfer.Progress=read/stream.Length;
if (updateCallback!=null) { updateCallback(transfer.Progress); }
} while (thisRead>0);
if (GetResponse(client, new Packets.FileTransferComplete()
{
ID= id,
}, ConnectionChannel.File)?.Response!=FileResponse.Completed)
{
Logger.Warning($"File trasfer to {client.Username} failed: "+name);
}
stream.Close();
stream.Dispose();
Logger?.Debug($"All file chunks sent:{name}");
InProgressFileTransfers.Remove(id);
}
internal int NewFileID()
{
int ID = 0;
while ((ID==0)
|| InProgressFileTransfers.ContainsKey(ID))
{
byte[] rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
return ID;
}
private int NewRequestID()
{
int ID = 0;
while ((ID==0)
|| PendingResponses.ContainsKey(ID))
{
byte[] rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
return ID;
}
internal void Send(Packet p,Client client, ConnectionChannel channel = ConnectionChannel.Default, NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, client.Connection,method,(int)channel);
}
internal void Forward(Packet p, Client except, ConnectionChannel channel = ConnectionChannel.Default, NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, except.Connection, method, (int)channel);
}
internal void SendToAll(Packet p, ConnectionChannel channel = ConnectionChannel.Default, NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, method, (int)channel);
}
}
}