From: js6pak Date: Sun, 17 Dec 2023 19:19:27 +0000 (+0100) Subject: Merge Impostor.Http into core (#533) X-Git-Tag: v1.9.0~8 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=5325a81cb6b50d2f2bef30ba6bd43b8b23d94f70;p=rhonda%2Fimpostor.git Merge Impostor.Http into core (#533) --- diff --git a/Dockerfile b/Dockerfile index 7490b1c..9792a1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/src/Impostor.Api/Config/DisconnectMessages.cs b/src/Impostor.Api/Config/DisconnectMessages.cs index 5a40559..e722f32 100644 --- a/src/Impostor.Api/Config/DisconnectMessages.cs +++ b/src/Impostor.Api/Config/DisconnectMessages.cs @@ -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 Impostor documentation on how to migrate to HTTP Matchmaking + """; } } diff --git a/src/Impostor.Api/Config/HttpServerConfig.cs b/src/Impostor.Api/Config/HttpServerConfig.cs new file mode 100644 index 0000000..fd3cda2 --- /dev/null +++ b/src/Impostor.Api/Config/HttpServerConfig.cs @@ -0,0 +1,27 @@ +namespace Impostor.Api.Config; + +/// +/// Configuration for HttpServer. +/// +public class HttpServerConfig +{ + /// + /// Gets the name of this config section. + /// + public const string Section = "HttpServer"; + + public bool Enabled { get; set; } = true; + + /// + /// Gets or sets the IP address the HTTP Matchmaking server will listen on. + /// + /// 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"; + + /// + /// Gets or sets the port the HTTP Matchmaking server will listen on. + /// + /// For port forwarding purposes, this is a TCP port. + public ushort ListenPort { get; set; } = 22023; +} diff --git a/src/Impostor.Api/Http/IListingFilter.cs b/src/Impostor.Api/Http/IListingFilter.cs new file mode 100644 index 0000000..ce3768a --- /dev/null +++ b/src/Impostor.Api/Http/IListingFilter.cs @@ -0,0 +1,18 @@ +using System; +using Impostor.Api.Games; +using Microsoft.AspNetCore.Http; + +namespace Impostor.Api.Http; + +/// +/// Register a method to filter listings on. +/// +public interface IListingFilter +{ + /// + /// Return a filter to filter listings on. + /// + /// HTTP Context of this request. + /// A function that looks at a game and returns true iff the connecting player is compatible with this game. + Func GetFilter(HttpContext context); +} diff --git a/src/Impostor.Api/Impostor.Api.csproj b/src/Impostor.Api/Impostor.Api.csproj index 0b3ab69..29025dc 100644 --- a/src/Impostor.Api/Impostor.Api.csproj +++ b/src/Impostor.Api/Impostor.Api.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Impostor.Api/Plugins/IPluginHttpStartup.cs b/src/Impostor.Api/Plugins/IPluginHttpStartup.cs new file mode 100644 index 0000000..bdb7dd6 --- /dev/null +++ b/src/Impostor.Api/Plugins/IPluginHttpStartup.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Builder; + +namespace Impostor.Api.Plugins; + +public interface IPluginHttpStartup : IPluginStartup +{ + void ConfigureWebApplication(IApplicationBuilder builder); +} diff --git a/src/Impostor.Api/packages.lock.json b/src/Impostor.Api/packages.lock.json index d7113bf..0cdaf87 100644 --- a/src/Impostor.Api/packages.lock.json +++ b/src/Impostor.Api/packages.lock.json @@ -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==" } } } diff --git a/src/Impostor.Benchmarks/packages.lock.json b/src/Impostor.Benchmarks/packages.lock.json index 2a0b829..bd54502 100644 --- a/src/Impostor.Benchmarks/packages.lock.json +++ b/src/Impostor.Benchmarks/packages.lock.json @@ -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, )", diff --git a/src/Impostor.Client.App/packages.lock.json b/src/Impostor.Client.App/packages.lock.json index 98ba9b6..8b24be1 100644 --- a/src/Impostor.Client.App/packages.lock.json +++ b/src/Impostor.Client.App/packages.lock.json @@ -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, )" } } diff --git a/src/Impostor.Client/packages.lock.json b/src/Impostor.Client/packages.lock.json index 166d277..0e4e032 100644 --- a/src/Impostor.Client/packages.lock.json +++ b/src/Impostor.Client/packages.lock.json @@ -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, )" } diff --git a/src/Impostor.Plugins.Debugger/packages.lock.json b/src/Impostor.Plugins.Debugger/packages.lock.json index c18e493..a88d4ea 100644 --- a/src/Impostor.Plugins.Debugger/packages.lock.json +++ b/src/Impostor.Plugins.Debugger/packages.lock.json @@ -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, )" } diff --git a/src/Impostor.Plugins.Example/packages.lock.json b/src/Impostor.Plugins.Example/packages.lock.json index c18e493..a88d4ea 100644 --- a/src/Impostor.Plugins.Example/packages.lock.json +++ b/src/Impostor.Plugins.Example/packages.lock.json @@ -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, )" } diff --git a/src/Impostor.Server/Http/GamesController.cs b/src/Impostor.Server/Http/GamesController.cs new file mode 100644 index 0000000..e64a498 --- /dev/null +++ b/src/Impostor.Server/Http/GamesController.cs @@ -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; + +/// +/// This controller has method to get a list of public games, join by game and create new games. +/// +[Route("/api/games")] +[ApiController] +public sealed class GamesController : ControllerBase +{ + private readonly IGameManager _gameManager; + private readonly ListingManager _listingManager; + private readonly HostServer _hostServer; + + /// + /// Initializes a new instance of the class. + /// + /// GameManager containing a list of games. + /// ListingManager responsible for filtering. + /// Impostor configuration section containing the public ip address of this server. + public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions serverConfig) + { + _gameManager = gameManager; + _listingManager = listingManager; + var config = serverConfig.Value; + _hostServer = HostServer.From(IPAddress.Parse(config.ResolvePublicIp()), config.PublicPort); + } + + /// + /// Get a list of active games. + /// + /// Maps that are requested. + /// Preferred chat language. + /// Amount of impostors. 0 is any. + /// Authorization header containing the matchmaking token. + /// An array of game listings. + [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(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)); + } + + /// + /// Get the address a certain game is hosted at. + /// + /// The id of the game that should be retrieved. + /// The server this game is hosted on. + [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)); + } + + /// + /// Get the address to host a new game on. + /// + /// The address of this server. + [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, + }; + } + } +} diff --git a/src/Impostor.Server/Http/ListingManager.cs b/src/Impostor.Server/Http/ListingManager.cs new file mode 100644 index 0000000..27867d7 --- /dev/null +++ b/src/Impostor.Server/Http/ListingManager.cs @@ -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; + +/// +/// Perform game listing filtering. +/// +public sealed class ListingManager +{ + private readonly IGameManager _gameManager; + private readonly IEnumerable _listingFilters; + private readonly ICompatibilityManager _compatibilityManager; + private readonly CompatibilityConfig _compatibilityConfig; + + public ListingManager(IGameManager gameManager, IEnumerable listingFilters, ICompatibilityManager compatibilityManager, IOptions compatibilityConfig) + { + _gameManager = gameManager; + _listingFilters = listingFilters; + _compatibilityManager = compatibilityManager; + _compatibilityConfig = compatibilityConfig.Value; + } + + /// + /// Find listings that match the requested settings. + /// + /// The context of this http request. + /// The selected maps. + /// The amount of impostors. 0 is any. + /// Chat language of the game. + /// Game version of the client. + /// Maximum amount of games to return. + /// Listings that match the required criteria. + public IEnumerable 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(); + + // 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; + } +} diff --git a/src/Impostor.Server/Http/TokenController.cs b/src/Impostor.Server/Http/TokenController.cs new file mode 100644 index 0000000..4345cb8 --- /dev/null +++ b/src/Impostor.Server/Http/TokenController.cs @@ -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; + +/// +/// This controller has a method to get an auth token. +/// +[Route("/api/user")] +[ApiController] +public sealed class TokenController : ControllerBase +{ + /// + /// Get an authentication token. + /// + /// Token parameters that need to be put into the token. + /// A bare minimum authentication token that the client will accept. + [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)); + } + + /// + /// Body of the token request endpoint. + /// + 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; } + } + + /// + /// Token that is returned to the user with a "signature". + /// + public sealed class Token + { + [JsonPropertyName("Content")] + public required TokenPayload Content { get; init; } + + [JsonPropertyName("Hash")] + public required string Hash { get; init; } + } + + /// + /// Actual token contents. + /// + 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; + } +} diff --git a/src/Impostor.Server/Impostor.Server.csproj b/src/Impostor.Server/Impostor.Server.csproj index 6e708b0..b16ea53 100644 --- a/src/Impostor.Server/Impostor.Server.csproj +++ b/src/Impostor.Server/Impostor.Server.csproj @@ -1,4 +1,4 @@ - + Exe @@ -30,28 +30,10 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - Always - PreserveNewest - true - - - Never - PreserveNewest - true - - - Never - Never - true - - - diff --git a/src/Impostor.Server/Net/Client.cs b/src/Impostor.Server/Net/Client.cs index b2a928d..0debdd5 100644 --- a/src/Impostor.Server/Net/Client.cs +++ b/src/Impostor.Server/Net/Client.cs @@ -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; } - /// - /// Triggered when the connected client requests the game listing. - /// - /// Options specific to the game mode. At this moment, the client can only specify the map, impostor count and chat language. - /// Filter options not specific to the game mode. - 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); - } - /// /// Triggered when the connected client requests the PlatformSpecificData. /// diff --git a/src/Impostor.Server/Net/Manager/GameManager.cs b/src/Impostor.Server/Net/Manager/GameManager.cs index 40c8610..c8371d5 100644 --- a/src/Impostor.Server/Net/Manager/GameManager.cs +++ b/src/Impostor.Server/Net/Manager/GameManager.cs @@ -61,56 +61,6 @@ namespace Impostor.Server.Net.Manager return game; } - public IEnumerable FindListings( - MapFlags map, - int impostorCount, - GameKeywords language, - GameVersion gameVersion, - HashSet 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) diff --git a/src/Impostor.Server/Plugins/PluginLoader.cs b/src/Impostor.Server/Plugins/PluginLoader.cs index 76c10e6..4148c6d 100644 --- a/src/Impostor.Server/Plugins/PluginLoader.cs +++ b/src/Impostor.Server/Plugins/PluginLoader.cs @@ -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(provider, orderedPlugins)); + services.AddSingleton(provider => ActivatorUtilities.CreateInstance(provider, orderedPlugins)); + services.AddSingleton(p => p.GetRequiredService()); foreach (var plugin in orderedPlugins) { diff --git a/src/Impostor.Server/Plugins/PluginLoaderService.cs b/src/Impostor.Server/Plugins/PluginLoaderService.cs index 40e13b5..9ec5d00 100644 --- a/src/Impostor.Server/Plugins/PluginLoaderService.cs +++ b/src/Impostor.Server/Plugins/PluginLoaderService.cs @@ -22,6 +22,8 @@ namespace Impostor.Server.Plugins _plugins = plugins; } + public IReadOnlyList Plugins => _plugins; + public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Loading plugins."); diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index 584629f..fdcd44d 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -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() ?? new PluginConfig(); + var httpConfig = configuration.GetSection(HttpServerConfig.Section) + .Get() ?? 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(host.Configuration.GetSection(CompatibilityConfig.Section)); services.Configure(host.Configuration.GetSection(ServerConfig.Section)); services.Configure(host.Configuration.GetSection(TimeoutConfig.Section)); + services.Configure(host.Configuration.GetSection(HttpServerConfig.Section)); services.AddSingleton(); services.AddSingleton(); @@ -126,6 +135,7 @@ namespace Impostor.Server services.AddSingleton(); services.AddSingleton(p => p.GetRequiredService()); + services.AddSingleton(); 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(); + 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; } } } diff --git a/src/Impostor.Server/config-full.json b/src/Impostor.Server/config-full.json index 562e290..9ffb353 100644 --- a/src/Impostor.Server/config-full.json +++ b/src/Impostor.Server/config-full.json @@ -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 diff --git a/src/Impostor.Server/config.json b/src/Impostor.Server/config.json index 1f68f96..fbad991 100644 --- a/src/Impostor.Server/config.json +++ b/src/Impostor.Server/config.json @@ -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 diff --git a/src/Impostor.Server/packages.lock.json b/src/Impostor.Server/packages.lock.json index b28bd0f..2182e59 100644 --- a/src/Impostor.Server/packages.lock.json +++ b/src/Impostor.Server/packages.lock.json @@ -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, )" } diff --git a/src/Impostor.Tests/packages.lock.json b/src/Impostor.Tests/packages.lock.json index fcf2b2f..536a439 100644 --- a/src/Impostor.Tests/packages.lock.json +++ b/src/Impostor.Tests/packages.lock.json @@ -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, )", diff --git a/src/Impostor.Tools.ServerReplay/packages.lock.json b/src/Impostor.Tools.ServerReplay/packages.lock.json index 7e02969..5f0e27a 100644 --- a/src/Impostor.Tools.ServerReplay/packages.lock.json +++ b/src/Impostor.Tools.ServerReplay/packages.lock.json @@ -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, )",