Merge Impostor.Http into core (#533)

This commit is contained in:
js6pak authored and GitHub committed 2023-12-17 20:19:27 +01:00
1 parent c6121eed49
commit 5325a81cb6
26 files changed
+750 -118

No files matched your search

+2 -6
View File
@@ -40,14 +40,10 @@ FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app ./
# Add Impostor.Http as a default built-in plugin
ADD https://github.com/Impostor/Impostor.Http/releases/download/v0.5.0/Impostor.Http.dll /app/builtin-plugins/
# Make it listen to 0.0.0.0 to expose it to the outside world.
ENV IMPOSTOR_HTTP_HttpServer__ListenIp=0.0.0.0
# Make the HttpServer listen to 0.0.0.0 to expose it to the outside world.
ENV IMPOSTOR_HttpServer__ListenIp=0.0.0.0
# Override ASPNETCORE_URLS to stop warning.
ENV ASPNETCORE_URLS=
# Enable the built-in plugin folder. Use a high number to prevent conflicts with existing configurations
ENV IMPOSTOR_PluginLoader__Paths__76=/app/builtin-plugins
EXPOSE 22023/tcp 22023/udp
ENTRYPOINT ["./Impostor.Server"]
@@ -23,5 +23,12 @@
public const string VersionServerTooOld = "Your client is too new, please update your Impostor server to play.";
public const string VersionUnsupported = "Your client version is unsupported, please update your Game and/or Impostor server.";
private const string UpgradingDocsLink = "https://github.com/Impostor/Impostor/blob/master/docs/Upgrading.md";
public const string UdpMatchmakingUnsupported = $"""
Sorry, UDP Matchmaking is no longer supported.
See <link={UpgradingDocsLink}#impostor-190>Impostor documentation</link> on how to migrate to HTTP Matchmaking
""";
}
}
@@ -0,0 +1,27 @@
namespace Impostor.Api.Config;
/// <summary>
/// Configuration for HttpServer.
/// </summary>
public class HttpServerConfig
{
/// <summary>
/// Gets the name of this config section.
/// </summary>
public const string Section = "HttpServer";
public bool Enabled { get; set; } = true;
/// <summary>
/// Gets or sets the IP address the HTTP Matchmaking server will listen on.
/// </summary>
/// Use "127.0.0.1" if you are running behind a reverse proxy or just testing locally.
/// Use "0.0.0.0" if you are directly exposing this server to the internet (not recommended).
public string ListenIp { get; set; } = "127.0.0.1";
/// <summary>
/// Gets or sets the port the HTTP Matchmaking server will listen on.
/// </summary>
/// For port forwarding purposes, this is a TCP port.
public ushort ListenPort { get; set; } = 22023;
}
+18
View File
@@ -0,0 +1,18 @@
using System;
using Impostor.Api.Games;
using Microsoft.AspNetCore.Http;
namespace Impostor.Api.Http;
/// <summary>
/// Register a method to filter listings on.
/// </summary>
public interface IListingFilter
{
/// <summary>
/// Return a filter to filter listings on.
/// </summary>
/// <param name="context">HTTP Context of this request.</param>
/// <returns>A function that looks at a game and returns true iff the connecting player is compatible with this game.</returns>
Func<IGame, bool> GetFilter(HttpContext context);
}
+1
View File
@@ -25,6 +25,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="all" />
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Builder;
namespace Impostor.Api.Plugins;
public interface IPluginHttpStartup : IPluginStartup
{
void ConfigureWebApplication(IApplicationBuilder builder);
}
+23
View File
@@ -8,6 +8,16 @@
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Direct",
"requested": "[2.2.0, )",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Direct",
"requested": "[7.0.0, )",
@@ -34,6 +44,14 @@
"StyleCop.Analyzers.Unstable": "1.2.0.435"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
@@ -64,6 +82,11 @@
"type": "Transitive",
"resolved": "1.2.0.435",
"contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
}
}
}
+19 -1
View File
@@ -53,6 +53,23 @@
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
"resolved": "1.1.0",
@@ -523,6 +540,7 @@
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
@@ -530,7 +548,7 @@
"impostor.server": {
"type": "Project",
"dependencies": {
"Impostor.Api": "[1.7.3-dev, )",
"Impostor.Api": "[1.8.4-dev, )",
"Impostor.Hazel": "[1.0.0, )",
"Microsoft.Extensions.FileSystemGlobbing": "[7.0.0, )",
"Microsoft.Extensions.Hosting": "[7.0.0, )",
+24 -1
View File
@@ -27,6 +27,23 @@
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
@@ -78,10 +95,16 @@
"resolved": "2.12.0",
"contentHash": "xaiJLIdu6rYMKfQMYUZgTy8YK7SMZjB4Yk50C/u//Z4OsvxkUfSPJy4nknfvwAC34yr13q7kcyh4grbwhSxyZg=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
@@ -89,7 +112,7 @@
"impostor.client": {
"type": "Project",
"dependencies": {
"Impostor.Api": "[1.7.3-dev, )",
"Impostor.Api": "[1.8.4-dev, )",
"Impostor.Hazel": "[1.0.0, )"
}
}
+23
View File
@@ -19,6 +19,23 @@
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
@@ -70,10 +87,16 @@
"resolved": "2.12.0",
"contentHash": "xaiJLIdu6rYMKfQMYUZgTy8YK7SMZjB4Yk50C/u//Z4OsvxkUfSPJy4nknfvwAC34yr13q7kcyh4grbwhSxyZg=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
@@ -7,6 +7,23 @@
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
@@ -48,10 +65,16 @@
"resolved": "7.0.0",
"contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
@@ -7,6 +7,23 @@
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
@@ -48,10 +65,16 @@
"resolved": "7.0.0",
"contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"impostor.api": {
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
+213
View File
@@ -0,0 +1,213 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using Impostor.Api.Config;
using Impostor.Api.Games;
using Impostor.Api.Games.Managers;
using Impostor.Api.Innersloth;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Impostor.Server.Http;
/// <summary>
/// This controller has method to get a list of public games, join by game and create new games.
/// </summary>
[Route("/api/games")]
[ApiController]
public sealed class GamesController : ControllerBase
{
private readonly IGameManager _gameManager;
private readonly ListingManager _listingManager;
private readonly HostServer _hostServer;
/// <summary>
/// Initializes a new instance of the <see cref="GamesController"/> class.
/// </summary>
/// <param name="gameManager">GameManager containing a list of games.</param>
/// <param name="listingManager">ListingManager responsible for filtering.</param>
/// <param name="serverConfig">Impostor configuration section containing the public ip address of this server.</param>
public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions<ServerConfig> serverConfig)
{
_gameManager = gameManager;
_listingManager = listingManager;
var config = serverConfig.Value;
_hostServer = HostServer.From(IPAddress.Parse(config.ResolvePublicIp()), config.PublicPort);
}
/// <summary>
/// Get a list of active games.
/// </summary>
/// <param name="mapId">Maps that are requested.</param>
/// <param name="lang">Preferred chat language.</param>
/// <param name="numImpostors">Amount of impostors. 0 is any.</param>
/// <param name="authorization">Authorization header containing the matchmaking token.</param>
/// <returns>An array of game listings.</returns>
[HttpGet]
public IActionResult Index(int mapId, GameKeywords lang, int numImpostors, [FromHeader] AuthenticationHeaderValue authorization)
{
if (authorization.Scheme != "Bearer" || authorization.Parameter == null)
{
return BadRequest();
}
var token = JsonSerializer.Deserialize<TokenController.Token>(Convert.FromBase64String(authorization.Parameter));
if (token == null)
{
return BadRequest();
}
var clientVersion = new GameVersion(token.Content.ClientVersion);
var listings = _listingManager.FindListings(HttpContext, mapId, numImpostors, lang, clientVersion);
return Ok(listings.Select(GameListing.From));
}
/// <summary>
/// Get the address a certain game is hosted at.
/// </summary>
/// <param name="gameId">The id of the game that should be retrieved.</param>
/// <returns>The server this game is hosted on.</returns>
[HttpPost]
public IActionResult Post(int gameId)
{
var code = new GameCode(gameId);
var game = _gameManager.Find(code);
// If the game was not found, print an error message.
if (game == null)
{
return NotFound(new MatchmakerResponse(new MatchmakerError(DisconnectReason.GameNotFound)));
}
return Ok(HostServer.From(game.PublicIp));
}
/// <summary>
/// Get the address to host a new game on.
/// </summary>
/// <returns>The address of this server.</returns>
[HttpPut]
public IActionResult Put()
{
return Ok(_hostServer);
}
private static uint ConvertAddressToNumber(IPAddress address)
{
#pragma warning disable CS0618 // Among Us only supports IPv4
return (uint)address.Address;
#pragma warning restore CS0618
}
private class HostServer
{
[JsonPropertyName("Ip")]
public required long Ip { get; init; }
[JsonPropertyName("Port")]
public required ushort Port { get; init; }
public static HostServer From(IPAddress ipAddress, ushort port)
{
return new HostServer
{
Ip = ConvertAddressToNumber(ipAddress),
Port = port,
};
}
public static HostServer From(IPEndPoint endPoint)
{
return From(endPoint.Address, (ushort)endPoint.Port);
}
}
private class MatchmakerResponse
{
[SetsRequiredMembers]
public MatchmakerResponse(MatchmakerError error)
{
Errors = new[] { error };
}
[JsonPropertyName("Errors")]
public required MatchmakerError[] Errors { get; init; }
}
private class MatchmakerError
{
[SetsRequiredMembers]
public MatchmakerError(DisconnectReason reason)
{
Reason = reason;
}
[JsonPropertyName("Reason")]
public required DisconnectReason Reason { get; init; }
}
private class GameListing
{
[JsonPropertyName("IP")]
public required uint Ip { get; init; }
[JsonPropertyName("Port")]
public required ushort Port { get; init; }
[JsonPropertyName("GameId")]
public required int GameId { get; init; }
[JsonPropertyName("PlayerCount")]
public required int PlayerCount { get; init; }
[JsonPropertyName("HostName")]
public required string HostName { get; init; }
[JsonPropertyName("HostPlatformName")]
public required string HostPlatformName { get; init; }
[JsonPropertyName("Platform")]
public required Platforms Platform { get; init; }
[JsonPropertyName("Age")]
public required int Age { get; init; }
[JsonPropertyName("MaxPlayers")]
public required int MaxPlayers { get; init; }
[JsonPropertyName("NumImpostors")]
public required int NumImpostors { get; init; }
[JsonPropertyName("MapId")]
public required MapTypes MapId { get; init; }
[JsonPropertyName("Language")]
public required GameKeywords Language { get; init; }
public static GameListing From(IGame game)
{
var platform = game.Host?.Client.PlatformSpecificData;
return new GameListing
{
Ip = ConvertAddressToNumber(game.PublicIp.Address),
Port = (ushort)game.PublicIp.Port,
GameId = game.Code,
PlayerCount = game.PlayerCount,
HostName = game.DisplayName ?? game.Host?.Client.Name ?? "Unknown host",
HostPlatformName = platform?.PlatformName ?? string.Empty,
Platform = platform?.Platform ?? Platforms.Unknown,
Age = 0,
MaxPlayers = game.Options.MaxPlayers,
NumImpostors = game.Options.NumImpostors,
MapId = game.Options.Map,
Language = game.Options.Keywords,
};
}
}
}
+125
View File
@@ -0,0 +1,125 @@
using System.Collections.Generic;
using System.Linq;
using Impostor.Api.Config;
using Impostor.Api.Games;
using Impostor.Api.Games.Managers;
using Impostor.Api.Http;
using Impostor.Api.Innersloth;
using Impostor.Api.Net.Manager;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Impostor.Server.Http;
/// <summary>
/// Perform game listing filtering.
/// </summary>
public sealed class ListingManager
{
private readonly IGameManager _gameManager;
private readonly IEnumerable<IListingFilter> _listingFilters;
private readonly ICompatibilityManager _compatibilityManager;
private readonly CompatibilityConfig _compatibilityConfig;
public ListingManager(IGameManager gameManager, IEnumerable<IListingFilter> listingFilters, ICompatibilityManager compatibilityManager, IOptions<CompatibilityConfig> compatibilityConfig)
{
_gameManager = gameManager;
_listingFilters = listingFilters;
_compatibilityManager = compatibilityManager;
_compatibilityConfig = compatibilityConfig.Value;
}
/// <summary>
/// Find listings that match the requested settings.
/// </summary>
/// <param name="ctx">The context of this http request.</param>
/// <param name="map">The selected maps.</param>
/// <param name="impostorCount">The amount of impostors. 0 is any.</param>
/// <param name="language">Chat language of the game.</param>
/// <param name="gameVersion">Game version of the client.</param>
/// <param name="maxListings">Maximum amount of games to return.</param>
/// <returns>Listings that match the required criteria.</returns>
public IEnumerable<IGame> FindListings(HttpContext ctx, int map, int impostorCount, GameKeywords language, GameVersion gameVersion, int maxListings = 10)
{
var resultCount = 0;
var filters = _listingFilters.Select(f => f.GetFilter(ctx)).ToArray();
var compatibleGames = new List<IGame>();
// We want to add 2 types of games
// 1. Desireable games that the player wants to play (right language, right map, desired impostor amount)
// 2. Failing that, display compatible games the player could join (public games with spots available)
// .Where filters out games that can't be joined.
foreach (var game in this._gameManager.Games)
{
if (!game.IsPublic || game.GameState != GameStates.NotStarted || game.PlayerCount >= game.Options.MaxPlayers)
{
continue;
}
if (!_compatibilityConfig.AllowVersionMixing &&
game.Host != null &&
_compatibilityManager.CanJoinGame(game.Host.Client.GameVersion, gameVersion) != GameJoinError.None)
{
continue;
}
if (!filters.All(filter => filter(game)))
{
continue;
}
if (IsGameDesired(game, map, impostorCount, language))
{
// Add to result immediately.
yield return game;
// Break out if we have enough.
if (++resultCount == maxListings)
{
yield break;
}
}
else
{
// Add to result to add afterwards. Adding is pointless if we already have enough compatible games to fill the list
if (compatibleGames.Count < (maxListings - resultCount))
{
compatibleGames.Add(game);
}
}
}
foreach (var game in compatibleGames)
{
yield return game;
if (++resultCount == maxListings)
{
yield break;
}
}
}
private static bool IsGameDesired(IGame game, int map, int impostorCount, GameKeywords language)
{
if ((map & (1 << (int)game.Options.Map)) == 0)
{
return false;
}
if (language != game.Options.Keywords)
{
return false;
}
if (impostorCount != 0 && game.Options.NumImpostors != impostorCount)
{
return false;
}
return true;
}
}
@@ -0,0 +1,85 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Impostor.Api.Innersloth;
using Microsoft.AspNetCore.Mvc;
namespace Impostor.Server.Http;
/// <summary>
/// This controller has a method to get an auth token.
/// </summary>
[Route("/api/user")]
[ApiController]
public sealed class TokenController : ControllerBase
{
/// <summary>
/// Get an authentication token.
/// </summary>
/// <param name="request">Token parameters that need to be put into the token.</param>
/// <returns>A bare minimum authentication token that the client will accept.</returns>
[HttpPost]
public IActionResult GetToken([FromBody] TokenRequest request)
{
var token = new Token
{
Content = new TokenPayload
{
ProductUserId = request.ProductUserId,
ClientVersion = request.ClientVersion,
},
Hash = "impostor_was_here",
};
// Wrap into a Base64 sandwich
var serialized = JsonSerializer.SerializeToUtf8Bytes(token);
return this.Ok(Convert.ToBase64String(serialized));
}
/// <summary>
/// Body of the token request endpoint.
/// </summary>
public class TokenRequest
{
[JsonPropertyName("Puid")]
public required string ProductUserId { get; init; }
[JsonPropertyName("Username")]
public required string Username { get; init; }
[JsonPropertyName("ClientVersion")]
public required int ClientVersion { get; init; }
[JsonPropertyName("Language")]
public required Language Language { get; init; }
}
/// <summary>
/// Token that is returned to the user with a "signature".
/// </summary>
public sealed class Token
{
[JsonPropertyName("Content")]
public required TokenPayload Content { get; init; }
[JsonPropertyName("Hash")]
public required string Hash { get; init; }
}
/// <summary>
/// Actual token contents.
/// </summary>
public sealed class TokenPayload
{
private static readonly DateTime DefaultExpiryDate = new(2012, 12, 21);
[JsonPropertyName("Puid")]
public required string ProductUserId { get; init; }
[JsonPropertyName("ClientVersion")]
public required int ClientVersion { get; init; }
[JsonPropertyName("ExpiresAt")]
public DateTime ExpiresAt { get; init; } = DefaultExpiryDate;
}
}
+2 -20
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -30,28 +30,10 @@
<PackageReference Include="Serilog.Extensions.Hosting" Version="5.0.1" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.435">
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.507">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Content Include="config.json">
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Include="config.*.json">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Include="config-full.json">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
</Project>
+2 -20
View File
@@ -5,7 +5,6 @@ using Impostor.Api;
using Impostor.Api.Config;
using Impostor.Api.Games;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
using Impostor.Api.Net;
using Impostor.Api.Net.Custom;
using Impostor.Api.Net.Messages;
@@ -260,9 +259,8 @@ namespace Impostor.Server.Net
case MessageFlags.GetGameListV2:
{
Message16GetGameListC2S.Deserialize(reader, out var options, out _, out _, out var filterOptions);
await OnRequestGameListAsync(options, filterOptions);
break;
await DisconnectAsync(DisconnectReason.Custom, DisconnectMessages.UdpMatchmakingUnsupported);
return;
}
case MessageFlags.SetActivePodType:
@@ -352,22 +350,6 @@ namespace Impostor.Server.Net
return true;
}
/// <summary>
/// Triggered when the connected client requests the game listing.
/// </summary>
/// <param name="options">Options specific to the game mode. At this moment, the client can only specify the map, impostor count and chat language.</param>
/// <param name="filterOptions">Filter options not specific to the game mode.</param>
private ValueTask OnRequestGameListAsync(IGameOptions options, GameFilterOptions filterOptions)
{
using var message = MessageWriter.Get(MessageType.Reliable);
var games = _gameManager.FindListings((MapFlags)options.Map, options.NumImpostors, options.Keywords, this.GameVersion, filterOptions.FilterTags);
Message16GetGameListS2C.Serialize(message, games);
return Connection.SendAsync(message);
}
/// <summary>
/// Triggered when the connected client requests the PlatformSpecificData.
/// </summary>
@@ -61,56 +61,6 @@ namespace Impostor.Server.Net.Manager
return game;
}
public IEnumerable<Game> FindListings(
MapFlags map,
int impostorCount,
GameKeywords language,
GameVersion gameVersion,
HashSet<string> filterTags,
int count = 10)
{
var results = 0;
// Find games that have not started yet.
foreach (var (_, game) in _games.Where(x =>
x.Value.IsPublic &&
x.Value.GameState == GameStates.NotStarted &&
x.Value.PlayerCount < x.Value.Options.MaxPlayers &&
(_compatibilityConfig.AllowVersionMixing || x.Value.Host == null ||
this._compatibilityManager.CanJoinGame(x.Value.Host.Client.GameVersion, gameVersion) == GameJoinError.None)))
{
// Check for options.
if (!map.HasFlag((MapFlags)(1 << (byte)game.Options.Map)))
{
continue;
}
if (!language.HasFlag(game.Options.Keywords))
{
continue;
}
if (impostorCount != 0 && game.Options.NumImpostors != impostorCount)
{
continue;
}
if (!game.FilterOptions.FilterTags.SetEquals(filterTags))
{
continue;
}
// Add to result.
yield return game;
// Break out if we have enough.
if (++results == count)
{
yield break;
}
}
}
public async ValueTask RemoveAsync(GameCode gameCode)
{
if (_games.TryGetValue(gameCode, out var game) && game.PlayerCount > 0)
+2 -14
View File
@@ -27,19 +27,6 @@ namespace Impostor.Server.Plugins
CheckPaths(pluginPaths);
CheckPaths(libraryPaths);
// Add library path for ASP.NET Core. This is useful for plugins that depend on ASP.NET Core,
// like Impostor.Http. The path contains the .NET version number, so it changes regularly.
var aspNetPath = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()
.Replace("Microsoft.NETCore.App", "Microsoft.AspNetCore.App");
if (Directory.Exists(aspNetPath))
{
libraryPaths.Add(aspNetPath);
}
else
{
Logger.Information("ASP.NET Core not installed, this may cause issues if you have plugins that depend on it");
}
var rootFolder = Directory.GetCurrentDirectory();
pluginPaths.Add(Path.Combine(rootFolder, "plugins"));
@@ -126,7 +113,8 @@ namespace Impostor.Server.Plugins
builder.ConfigureServices(services =>
{
services.AddHostedService(provider => ActivatorUtilities.CreateInstance<PluginLoaderService>(provider, orderedPlugins));
services.AddSingleton<PluginLoaderService>(provider => ActivatorUtilities.CreateInstance<PluginLoaderService>(provider, orderedPlugins));
services.AddSingleton<IHostedService>(p => p.GetRequiredService<PluginLoaderService>());
foreach (var plugin in orderedPlugins)
{
@@ -22,6 +22,8 @@ namespace Impostor.Server.Plugins
_plugins = plugins;
}
public IReadOnlyList<PluginInformation> Plugins => _plugins;
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Loading plugins.");
+52 -1
View File
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.Loader;
using Impostor.Api.Config;
@@ -9,9 +10,11 @@ using Impostor.Api.Games;
using Impostor.Api.Games.Managers;
using Impostor.Api.Net.Custom;
using Impostor.Api.Net.Manager;
using Impostor.Api.Plugins;
using Impostor.Api.Utils;
using Impostor.Hazel.Extensions;
using Impostor.Server.Events;
using Impostor.Server.Http;
using Impostor.Server.Net;
using Impostor.Server.Net.Custom;
using Impostor.Server.Net.Factories;
@@ -20,6 +23,9 @@ using Impostor.Server.Net.Messages;
using Impostor.Server.Plugins;
using Impostor.Server.Recorder;
using Impostor.Server.Utils;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -73,8 +79,10 @@ namespace Impostor.Server
var configuration = CreateConfiguration(args);
var pluginConfig = configuration.GetSection("PluginLoader")
.Get<PluginConfig>() ?? new PluginConfig();
var httpConfig = configuration.GetSection(HttpServerConfig.Section)
.Get<HttpServerConfig>() ?? new HttpServerConfig();
return Host.CreateDefaultBuilder(args)
var hostBuilder = Host.CreateDefaultBuilder(args)
.UseContentRoot(Directory.GetCurrentDirectory())
#if DEBUG
.UseEnvironment(Environment.GetEnvironmentVariable("IMPOSTOR_ENV") ?? "Development")
@@ -100,6 +108,7 @@ namespace Impostor.Server
services.Configure<CompatibilityConfig>(host.Configuration.GetSection(CompatibilityConfig.Section));
services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
services.Configure<TimeoutConfig>(host.Configuration.GetSection(TimeoutConfig.Section));
services.Configure<HttpServerConfig>(host.Configuration.GetSection(HttpServerConfig.Section));
services.AddSingleton<ICompatibilityManager, CompatibilityManager>();
services.AddSingleton<ClientManager>();
@@ -126,6 +135,7 @@ namespace Impostor.Server
services.AddSingleton<GameManager>();
services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>());
services.AddSingleton<ListingManager>();
services.AddEventPools();
services.AddHazel();
@@ -180,6 +190,7 @@ namespace Impostor.Server
#else
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
#endif
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console()
.ReadFrom.Configuration(context.Configuration, ConfigurationAssemblySource.AlwaysScanDllFiles);
@@ -188,6 +199,46 @@ namespace Impostor.Server
})
.UseConsoleLifetime()
.UsePluginLoader(pluginConfig);
if (httpConfig.Enabled)
{
hostBuilder.ConfigureWebHostDefaults(builder =>
{
builder.ConfigureServices(services =>
{
services.AddControllers();
});
builder.Configure(app =>
{
var pluginLoaderService = app.ApplicationServices.GetRequiredService<PluginLoaderService>();
foreach (var pluginInformation in pluginLoaderService.Plugins)
{
if (pluginInformation.Startup is IPluginHttpStartup httpStartup)
{
httpStartup.ConfigureWebApplication(app);
}
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
});
builder.ConfigureKestrel(serverOptions =>
{
serverOptions.Listen(IPAddress.Parse(httpConfig.ListenIp), httpConfig.ListenPort, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
});
});
});
}
return hostBuilder;
}
}
}
+5
View File
@@ -5,6 +5,11 @@
"ListenIp": "0.0.0.0",
"ListenPort": 22023
},
"HttpServer": {
"Enabled": true,
"ListenIp": "127.0.0.1",
"ListenPort": 22023
},
"AntiCheat": {
"Enabled": true,
"BanIpFromGame": true
+5
View File
@@ -5,6 +5,11 @@
"ListenIp": "0.0.0.0",
"ListenPort": 22023
},
"HttpServer": {
"Enabled": true,
"ListenIp": "127.0.0.1",
"ListenPort": 22023
},
"AntiCheat": {
"Enabled": true,
"BanIpFromGame": true
+21 -3
View File
@@ -90,15 +90,32 @@
},
"StyleCop.Analyzers.Unstable": {
"type": "Direct",
"requested": "[1.2.0.435, )",
"resolved": "1.2.0.435",
"contentHash": "ouwPWZxbOV3SmCZxIRqHvljkSzkCyi1tDoMzQtDb/bRP8ctASV/iRJr+A2Gdj0QLaLmWnqTWDrH82/iP+X80Lg=="
"requested": "[1.2.0.507, )",
"resolved": "1.2.0.507",
"contentHash": "gTY3IQdRqDJ4hbhSA3e/R48oE8b/OiKfvwkt1QdNVfrJK2gMHBV8ldaHJ885jxWZfllK66soa/sdcjh9bX49Tw=="
},
"Impostor.Hazel.Abstractions": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "7.0.0",
@@ -367,6 +384,7 @@
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
+19 -1
View File
@@ -61,6 +61,23 @@
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "17.4.1",
@@ -1398,6 +1415,7 @@
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
@@ -1405,7 +1423,7 @@
"impostor.server": {
"type": "Project",
"dependencies": {
"Impostor.Api": "[1.7.3-dev, )",
"Impostor.Api": "[1.8.4-dev, )",
"Impostor.Hazel": "[1.0.0, )",
"Microsoft.Extensions.FileSystemGlobbing": "[7.0.0, )",
"Microsoft.Extensions.Hosting": "[7.0.0, )",
@@ -27,6 +27,23 @@
"resolved": "1.0.0",
"contentHash": "x56AEsY5fKU2Z5O7ZrzEqKfr62kq/C05Aevcciwz5yMfOQlpwuFI1awgNGJsoe+bcx+XnFc3+l+eadtY72aZsg=="
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "7.0.0",
@@ -355,6 +372,7 @@
"type": "Project",
"dependencies": {
"Impostor.Hazel.Abstractions": "[1.0.0, )",
"Microsoft.AspNetCore.Http.Abstractions": "[2.2.0, )",
"Microsoft.Extensions.Hosting.Abstractions": "[7.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
@@ -362,7 +380,7 @@
"impostor.server": {
"type": "Project",
"dependencies": {
"Impostor.Api": "[1.7.3-dev, )",
"Impostor.Api": "[1.8.4-dev, )",
"Impostor.Hazel": "[1.0.0, )",
"Microsoft.Extensions.FileSystemGlobbing": "[7.0.0, )",
"Microsoft.Extensions.Hosting": "[7.0.0, )",