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"]
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
+ """;
}
}
--- /dev/null
+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;
+}
--- /dev/null
+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);
+}
</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" />
--- /dev/null
+using Microsoft.AspNetCore.Builder;
+
+namespace Impostor.Api.Plugins;
+
+public interface IPluginHttpStartup : IPluginStartup
+{
+ void ConfigureWebApplication(IApplicationBuilder builder);
+}
"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, )",
"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",
"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=="
}
}
}
"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",
"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, )"
}
"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, )",
"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",
"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, )"
}
"impostor.client": {
"type": "Project",
"dependencies": {
- "Impostor.Api": "[1.7.3-dev, )",
+ "Impostor.Api": "[1.8.4-dev, )",
"Impostor.Hazel": "[1.0.0, )"
}
}
"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",
"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, )"
}
"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",
"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, )"
}
"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",
"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, )"
}
--- /dev/null
+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,
+ };
+ }
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<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>
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;
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:
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>
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)
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"));
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)
{
_plugins = plugins;
}
+ public IReadOnlyList<PluginInformation> Plugins => _plugins;
+
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Loading plugins.");
using System;
using System.IO;
using System.Linq;
+using System.Net;
using System.Reflection;
using System.Runtime.Loader;
using Impostor.Api.Config;
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;
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;
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")
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>();
services.AddSingleton<GameManager>();
services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>());
+ services.AddSingleton<ListingManager>();
services.AddEventPools();
services.AddHazel();
#else
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
#endif
+ .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console()
.ReadFrom.Configuration(context.Configuration, ConfigurationAssemblySource.AlwaysScanDllFiles);
})
.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;
}
}
}
"ListenIp": "0.0.0.0",
"ListenPort": 22023
},
+ "HttpServer": {
+ "Enabled": true,
+ "ListenIp": "127.0.0.1",
+ "ListenPort": 22023
+ },
"AntiCheat": {
"Enabled": true,
"BanIpFromGame": true
"ListenIp": "0.0.0.0",
"ListenPort": 22023
},
+ "HttpServer": {
+ "Enabled": true,
+ "ListenIp": "127.0.0.1",
+ "ListenPort": 22023
+ },
"AntiCheat": {
"Enabled": true,
"BanIpFromGame": true
},
"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",
"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, )"
}
"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",
"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, )"
}
"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, )",
"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",
"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, )"
}
"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, )",