- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: 3.1.301
+ dotnet-version: 3.1.402
- name: Install dependencies
run: dotnet restore ./src
- name: Build Impostor.Client
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
- dotnet-version: 3.1.301
+ dotnet-version: 3.1.402
- name: Install dependencies
run: dotnet restore ./src
- name: Build Impostor.Client
run: dotnet build --no-restore -c Release -f netcoreapp3.1 ./src/Impostor.Server/Impostor.Server.csproj /p:PublishTrimmed=false
- name: Test
run: dotnet test --no-restore -v normal -f netcoreapp3.1 ./src
- deploy:
- name: Deploy
- runs-on: windows-latest
- needs: build
- if: github.event_name == 'release'
- steps:
- - name: Publish Impostor.Server (win-x64)
- run: dotnet publish -c release -o /build/linux-x64 -f netcoreapp3.1 -r win-x64 --self-contained --no-restore ./src/Impostor.Server/Impostor.Server.csproj
- - name: Publish Impostor.Server (linux-x64)
- run: dotnet publish -c release -o /build/linux-x64 -f netcoreapp3.1 -r linux-x64 --self-contained --no-restore ./src/Impostor.Server/Impostor.Server.csproj
\ No newline at end of file
+config.*.json
+
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
public const string Destroyed = "The game you tried to join is being destroyed. " +
"Please create a new game.";
+
+ public const string NotImplemented = "Game listing has not been implemented in Impostor yet for servers " +
+ "running in server redirection mode.";
}
}
\ No newline at end of file
namespace Impostor.Server.Data
{
- public class ServerConfig
+ internal class ServerConfig
{
+ public const string Section = "Server";
+
public string PublicIp { get; set; } = "127.0.0.1";
public ushort PublicPort { get; set; } = 22023;
public string ListenIp { get; set; } = "127.0.0.1";
--- /dev/null
+using System.Collections.Generic;
+
+namespace Impostor.Server.Data
+{
+ internal class ServerRedirectorConfig
+ {
+ public const string Section = "ServerRedirector";
+
+ public bool Enabled { get; set; }
+ public bool Master { get; set; }
+ public string Redis { get; set; }
+ public List<ServerRedirectorNode> Nodes { get; set; }
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Data
+{
+ internal class ServerRedirectorNode
+ {
+ public string Ip { get; set; }
+ public ushort Port { get; set; }
+ }
+}
\ No newline at end of file
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.8" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<None Update="config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
+ <None Update="config.*.json">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
</ItemGroup>
</Project>
-using System;
+using System;
using Hazel;
using Impostor.Server.Data;
using Impostor.Server.Net.Manager;
namespace Impostor.Server.Net
{
- public class Client
+ internal class Client
{
private static readonly ILogger Logger = Log.ForContext<Client>();
using System.Collections.Concurrent;
+using Hazel;
using Impostor.Server.Exceptions;
-using Serilog;
+using Microsoft.Extensions.Logging;
namespace Impostor.Server.Net.Manager
{
- public class ClientManager
+ internal class ClientManager : IClientManager
{
- private static readonly ILogger Logger = Log.ForContext<ClientManager>();
-
+ private readonly ILogger<ClientManager> _clientManager;
+ private readonly GameManager _gameManager;
private readonly ConcurrentDictionary<int, Client> _clients;
private readonly object _idLock;
private int _idLast;
- public ClientManager()
+ public ClientManager(ILogger<ClientManager> clientManager, GameManager gameManager)
{
+ _clientManager = clientManager;
+ _gameManager = gameManager;
_clients = new ConcurrentDictionary<int, Client>();
_idLock = new object();
_idLast = 0;
}
// No idea what a good way for this is.
- public int NextId()
+ private int NextId()
{
lock (_idLock)
{
_idLast = 0;
}
- if (_clients.ContainsKey(_idLast))
+ if (_clients.ContainsKey(result))
{
continue;
}
}
}
- public void Add(Client client)
+ public void Create(string name, Connection connection)
{
- Logger.Information("Client connected.");
-
- _clients.TryAdd(client.Id, client);
+ var clientId = NextId();
+
+ _clientManager.LogInformation("Client connected.");
+ _clients.TryAdd(clientId, new Client(this, _gameManager, clientId, name, connection));
}
public void Remove(Client client)
{
- Logger.Information("Client disconnected.");
-
+ _clientManager.LogInformation("Client disconnected.");
_clients.TryRemove(client.Id, out _);
}
}
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Impostor.Server.Data;
+using Impostor.Server.Net.Redirector;
using Impostor.Server.Net.State;
using Impostor.Shared.Innersloth;
using Impostor.Shared.Innersloth.Data;
namespace Impostor.Server.Net.Manager
{
- public class GameManager
+ internal class GameManager
{
private readonly ILogger<GameManager> _logger;
+ private readonly INodeProvider _nodeProvider;
private readonly IPEndPoint _publicIp;
private readonly ConcurrentDictionary<int, Game> _games;
- public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config)
+ public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeProvider nodeProvider)
{
_logger = logger;
+ _nodeProvider = nodeProvider;
_publicIp = new IPEndPoint(IPAddress.Parse(config.Value.PublicIp), config.Value.PublicPort);
_games = new ConcurrentDictionary<int, Game>();
}
public Game Create(GameOptionsData options)
{
+ // TODO: Prevent duplicates when using server redirector using INodeProvider.
+
var gameCode = GameCode.GenerateCode(6);
- var game = new Game(this, _publicIp, gameCode, options);
+ var gameCodeStr = GameCode.IntToGameName(gameCode);
+ var game = new Game(this, _nodeProvider, _publicIp, gameCode, options);
- if (_games.TryAdd(gameCode, game))
+ if (_nodeProvider.Find(gameCodeStr) == null &&
+ _games.TryAdd(gameCode, game))
{
+ _nodeProvider.Save(gameCodeStr, _publicIp);
_logger.LogDebug("Created game with code {0} ({1}).", game.CodeStr, gameCode);
return game;
}
foreach (var (code, game) in _games.Where(x =>
x.Value.IsPublic &&
x.Value.GameState == GameStates.NotStarted &&
- x.Value.PlayerCount < 10))
+ x.Value.PlayerCount < 10)) // TODO: Do "< x.Value.Options.MaxPlayers" when GameData packets are done.
{
// Check for options.
// TODO: Re-enable map filter when GameData packets are done.
public void Remove(int gameCode)
{
- _logger.LogDebug("Remove game with code {0} ({1}).", GameCode.IntToGameName(gameCode), gameCode);
+ _logger.LogDebug("Remove game with code {0} ({1}).", GameCode.IntToGameName(gameCode), gameCode);
+ _nodeProvider.Remove(GameCode.IntToGameName(gameCode));
_games.TryRemove(gameCode, out _);
}
}
--- /dev/null
+using Hazel;
+
+namespace Impostor.Server.Net.Manager
+{
+ internal interface IClientManager
+ {
+ void Create(string name, Connection connection);
+ }
+}
\ No newline at end of file
namespace Impostor.Server.Net
{
- public class Matchmaker
+ internal class Matchmaker
{
private readonly ILogger<Matchmaker> _logger;
- private readonly ServerConfig _config;
- private readonly GameManager _gameManager;
- private readonly ClientManager _clientManager;
+ private readonly ServerConfig _serverConfig;
+ private readonly IClientManager _clientManager;
private readonly UdpConnectionListener _connection;
- public Matchmaker(ILogger<Matchmaker> logger, IOptions<ServerConfig> configOptions, GameManager gameManager)
+ public Matchmaker(
+ ILogger<Matchmaker> logger,
+ IOptions<ServerConfig> serverConfig,
+ IClientManager clientManager)
{
_logger = logger;
- _config = configOptions.Value;
- _gameManager = gameManager;
- _clientManager = new ClientManager();
- _connection = new UdpConnectionListener(new IPEndPoint(IPAddress.Parse(_config.ListenIp), _config.ListenPort), IPMode.IPv4, s =>
+ _serverConfig = serverConfig.Value;
+ _clientManager = clientManager;
+ _connection = new UdpConnectionListener(new IPEndPoint(IPAddress.Parse(_serverConfig.ListenIp), _serverConfig.ListenPort), IPMode.IPv4, s =>
{
_logger.LogWarning("Log from Hazel: {0}", s);
});
return;
}
- // Register client.
- _clientManager.Add(new Client(_clientManager, _gameManager, _clientManager.NextId(), clientName, e.Connection));
+ // Create client.
+ _clientManager.Create(clientName, e.Connection);
}
public void Start()
using System.Threading;
using System.Threading.Tasks;
+using Impostor.Server.Data;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
namespace Impostor.Server.Net
{
- public class MatchmakerService : IHostedService
+ internal class MatchmakerService : IHostedService
{
private readonly ILogger<MatchmakerService> _logger;
+ private readonly ServerConfig _serverConfig;
+ private readonly ServerRedirectorConfig _redirectorConfig;
private readonly Matchmaker _matchmaker;
- public MatchmakerService(ILogger<MatchmakerService> logger, Matchmaker matchmaker)
+ public MatchmakerService(
+ ILogger<MatchmakerService> logger,
+ IOptions<ServerConfig> serverConfig,
+ IOptions<ServerRedirectorConfig> redirectorConfig,
+ Matchmaker matchmaker)
{
_logger = logger;
+ _serverConfig = serverConfig.Value;
+ _redirectorConfig = redirectorConfig.Value;
_matchmaker = matchmaker;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_matchmaker.Start();
- _logger.LogInformation("Matchmaker is running on {0}:{1}.",
+
+ _logger.LogInformation("Matchmaker is listening on {0}:{1}, the public server ip is {2}:{3}.",
_matchmaker.EndPoint.Address,
- _matchmaker.EndPoint.Port);
+ _matchmaker.EndPoint.Port,
+ _serverConfig.PublicIp,
+ _serverConfig.PublicPort);
+
+ if (_redirectorConfig.Enabled)
+ {
+ _logger.LogWarning(_redirectorConfig.Master
+ ? "Server redirection is enabled as master, this instance will redirect clients to other nodes."
+ : "Server redirection is enabled as node, this instance will accept clients.");
+ }
return Task.CompletedTask;
}
--- /dev/null
+using System.Net;
+using Hazel;
+
+namespace Impostor.Server.Net.Messages
+{
+ internal static class Message13Redirect
+ {
+ public static void Serialize(MessageWriter writer, bool clear, IPEndPoint ipEndPoint)
+ {
+ if (clear)
+ {
+ writer.Clear(SendOption.Reliable);
+ }
+
+ writer.StartMessage(MessageFlags.Redirect);
+ writer.Write(ipEndPoint.Address.GetAddressBytes());
+ writer.Write((ushort) ipEndPoint.Port);
+ writer.EndMessage();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.Collections.Generic;
+using Hazel;
+using Impostor.Server.Net.Manager;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Net.Redirector
+{
+ internal class ClientManagerRedirector : IClientManager
+ {
+ private readonly ILogger<ClientManagerRedirector> _logger;
+ private readonly INodeProvider _nodeProvider;
+ private readonly HashSet<ClientRedirector> _clients;
+
+ public ClientManagerRedirector(ILogger<ClientManagerRedirector> logger, INodeProvider nodeProvider)
+ {
+ _logger = logger;
+ _nodeProvider = nodeProvider;
+ _clients = new HashSet<ClientRedirector>();
+ }
+
+ public void Create(string name, Connection connection)
+ {
+ _logger.LogInformation("Client connected.");
+ _clients.Add(new ClientRedirector(name, connection, this, _nodeProvider));
+ }
+
+ public void Remove(ClientRedirector client)
+ {
+ _logger.LogInformation("Client disconnected.");
+ _clients.Remove(client);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using Hazel;
+using Impostor.Server.Data;
+using Impostor.Server.Net.Messages;
+using Impostor.Shared.Innersloth;
+using Impostor.Shared.Innersloth.Data;
+using Serilog;
+using ILogger = Serilog.ILogger;
+
+namespace Impostor.Server.Net.Redirector
+{
+ internal class ClientRedirector
+ {
+ private static readonly ILogger Logger = Log.ForContext<ClientRedirector>();
+
+ private readonly string _name;
+ private readonly Connection _connection;
+ private readonly ClientManagerRedirector _clientManager;
+ private readonly INodeProvider _nodeProvider;
+
+ public ClientRedirector(string name, Connection connection, ClientManagerRedirector clientManager, INodeProvider nodeProvider)
+ {
+ _name = name;
+ _connection = connection;
+ _connection.DataReceived += OnDataReceived;
+ _connection.Disconnected += OnDisconnected;
+ _clientManager = clientManager;
+ _nodeProvider = nodeProvider;
+ }
+
+ private void OnDataReceived(DataReceivedEventArgs e)
+ {
+ try
+ {
+ while (true)
+ {
+ if (e.Message.Position >= e.Message.Length)
+ {
+ break;
+ }
+
+ OnMessageReceived(e.Message.ReadMessage());
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(ex, "Exception caught in client data handler.");
+ }
+ }
+
+ private void OnMessageReceived(MessageReader message)
+ {
+ var flag = message.Tag;
+
+ Logger.Verbose("Server got {0}.", flag);
+
+ switch (flag)
+ {
+ case MessageFlags.HostGame:
+ {
+ using (var packet = MessageWriter.Get(SendOption.Reliable))
+ {
+ Message13Redirect.Serialize(packet, false, _nodeProvider.Get());
+ _connection.Send(packet);
+ }
+ break;
+ }
+
+ case MessageFlags.JoinGame:
+ {
+ Message01JoinGame.Deserialize(message,
+ out var gameCode,
+ out var unknown);
+
+ using (var packet = MessageWriter.Get(SendOption.Reliable))
+ {
+ var endpoint = _nodeProvider.Find(GameCode.IntToGameName(gameCode));
+ if (endpoint == null)
+ {
+ Message01JoinGame.SerializeError(packet, false, DisconnectReason.GameMissing);
+ }
+ else
+ {
+ Message13Redirect.Serialize(packet, false, endpoint);
+ }
+
+ _connection.Send(packet);
+ }
+ break;
+ }
+
+ case MessageFlags.GetGameListV2:
+ {
+ // TODO: Implement.
+ using (var packet = MessageWriter.Get(SendOption.Reliable))
+ {
+ Message01JoinGame.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.NotImplemented);
+ _connection.Send(packet);
+ }
+ break;
+ }
+
+ default:
+ {
+ Logger.Warning("Received unsupported message flag on the redirector ({0}).", flag);
+ break;
+ }
+ }
+ }
+
+ private void OnDisconnected(object sender, DisconnectedEventArgs e)
+ {
+ _clientManager.Remove(this);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.Net;
+
+namespace Impostor.Server.Net.Redirector
+{
+ internal interface INodeProvider
+ {
+ IPEndPoint Get();
+ IPEndPoint Find(string gameCode);
+ void Save(string gameCode, IPEndPoint endPoint);
+ void Remove(string gameCode);
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Net;
+
+namespace Impostor.Server.Net.Redirector
+{
+ public class NodeProviderNoOp : INodeProvider
+ {
+ public IPEndPoint Get()
+ {
+ throw new NotImplementedException();
+ }
+
+ public IPEndPoint Find(string gameCode)
+ {
+ // Do nothing.
+ return null;
+ }
+
+ public void Save(string gameCode, IPEndPoint endPoint)
+ {
+ // Do nothing.
+ }
+
+ public void Remove(string gameCode)
+ {
+ // Do nothing.
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Net;
+using Impostor.Server.Data;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Net.Redirector
+{
+ internal class NodeProviderRedis : INodeProvider
+ {
+ private readonly IDistributedCache _cache;
+ private readonly List<IPEndPoint> _nodes;
+ private readonly object _lock;
+ private int _currentIndex;
+
+ public NodeProviderRedis(IOptions<ServerRedirectorConfig> redirectorConfig, IDistributedCache cache)
+ {
+ _cache = cache;
+ _nodes = new List<IPEndPoint>();
+ _lock = new object();
+
+ foreach (var node in redirectorConfig.Value.Nodes)
+ {
+ _nodes.Add(new IPEndPoint(IPAddress.Parse(node.Ip), node.Port));
+ }
+ }
+
+ public IPEndPoint Get()
+ {
+ lock (_lock)
+ {
+ var node = _nodes[_currentIndex++];
+
+ if (_currentIndex == _nodes.Count)
+ {
+ _currentIndex = 0;
+ }
+
+ return node;
+ }
+ }
+
+ public IPEndPoint Find(string gameCode)
+ {
+ var entry = _cache.GetString(gameCode);
+ if (entry == null)
+ {
+ return null;
+ }
+
+ return IPEndPoint.Parse(entry);
+ }
+
+ public void Save(string gameCode, IPEndPoint endPoint)
+ {
+ _cache.SetString(gameCode, endPoint.ToString(), new DistributedCacheEntryOptions
+ {
+ SlidingExpiration = TimeSpan.FromHours(1)
+ });
+ }
+
+ public void Remove(string gameCode)
+ {
+ _cache.Remove(gameCode);
+ }
+ }
+}
\ No newline at end of file
namespace Impostor.Server.Net.State
{
- public partial class ClientPlayer
+ internal partial class ClientPlayer
{
/// <summary>
/// Triggered when the connected client requests the game listing.
namespace Impostor.Server.Net.State
{
- public partial class ClientPlayer
+ internal partial class ClientPlayer
{
private readonly GameManager _gameManager;
namespace Impostor.Server.Net.State
{
- public partial class Game
+ internal partial class Game
{
public void HandleStartGame(MessageReader message)
{
namespace Impostor.Server.Net.State
{
- public partial class Game
+ internal partial class Game
{
private void WriteRemovePlayerMessage(MessageWriter message, bool clear, int playerId, DisconnectReason reason)
{
using Hazel;
using Impostor.Server.Net.Manager;
using Impostor.Server.Net.Messages;
+using Impostor.Server.Net.Redirector;
using Impostor.Shared.Innersloth;
using Impostor.Shared.Innersloth.Data;
using Serilog;
namespace Impostor.Server.Net.State
{
- public partial class Game
+ internal partial class Game
{
private static readonly ILogger Logger = Log.ForContext<Game>();
private readonly GameManager _gameManager;
+ private readonly INodeProvider _nodeProvider;
private readonly ConcurrentDictionary<int, ClientPlayer> _players;
private readonly HashSet<IPAddress> _bannedIps;
- public Game(GameManager gameManager, IPEndPoint publicIp, int code, GameOptionsData options)
+ public Game(GameManager gameManager, INodeProvider nodeProvider, IPEndPoint publicIp, int code, GameOptionsData options)
{
_gameManager = gameManager;
+ _nodeProvider = nodeProvider;
_players = new ConcurrentDictionary<int, ClientPlayer>();
_bannedIps = new HashSet<IPAddress>();
using Impostor.Server.Data;
using Impostor.Server.Net;
using Impostor.Server.Net.Manager;
+using Impostor.Server.Net.Redirector;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
-using Serilog.Events;
namespace Impostor.Server
{
.MinimumLevel.Verbose()
#else
.MinimumLevel.Information()
-
-#endif
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
+#endif
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
.ConfigureAppConfiguration(builder =>
{
builder.AddJsonFile("config.json", true);
+ builder.AddJsonFile("config.Development.json", true);
builder.AddEnvironmentVariables(prefix: "IMPOSTOR_");
builder.AddCommandLine(args);
})
.ConfigureServices((host, services) =>
{
- services.Configure<ServerConfig>(host.Configuration.GetSection("Server"));
+ var redirector = host.Configuration
+ .GetSection(ServerRedirectorConfig.Section)
+ .Get<ServerRedirectorConfig>();
+
+ services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
+ services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
- services.AddSingleton<GameManager>();
+ if (redirector.Enabled)
+ {
+ services.AddSingleton<INodeProvider, NodeProviderRedis>();
+ services.AddStackExchangeRedisCache(options =>
+ {
+ options.Configuration = redirector.Redis;
+ options.InstanceName = "ImpostorRedis";
+ });
+ }
+ else
+ {
+ services.AddSingleton<INodeProvider, NodeProviderNoOp>();
+ }
+
+ if (redirector.Enabled && redirector.Master)
+ {
+ services.AddSingleton<IClientManager, ClientManagerRedirector>();
+ }
+ else
+ {
+ services.AddSingleton<IClientManager, ClientManager>();
+ services.AddSingleton<GameManager>();
+ }
+
services.AddSingleton<Matchmaker>();
services.AddHostedService<MatchmakerService>();
"PublicPort": 22023,
"ListenIp": "0.0.0.0",
"ListenPort": 22023
+ },
+ "ServerRedirector": {
+ "Enabled": false,
+ "Master": true,
+ "Redis": "127.0.0.1:6379",
+ "Nodes": [
+ {
+ "Ip": "127.0.0.1",
+ "Port": 22024
+ },
+ {
+ "Ip": "127.0.0.1",
+ "Port": 22025
+ }
+ ]
}
}
\ No newline at end of file