Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update PhotonStateManager related configuration #865

Merged
merged 4 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .env.default
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,5 @@ POSTGRES_DB=DragaliaAPI

# Bearer token for admin endpoints e.g. manual save import
DEVELOPER_TOKEN=
# Hostname to add to logging context
HOSTNAME=
# Token used to authenticate incoming requests from co-op Photon Server
PHOTON_TOKEN=
PhotonOptions__Token=
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,6 @@ private async Task<string> StartDungeon(DungeonSession session)

private void SetupPhotonAuthentication()
{
Environment.SetEnvironmentVariable("PHOTON_TOKEN", "supersecrettoken");
this.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
"supersecrettoken"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using DragaliaAPI.Database;
using DragaliaAPI.Models.Options;
using DragaliaAPI.Shared.PlayerDetails;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
Expand All @@ -11,11 +12,12 @@
namespace DragaliaAPI.Middleware;

public class PhotonAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
IOptionsMonitor<AuthenticationSchemeOptions> authOptions,
IOptionsMonitor<PhotonOptions> photonOptions,
ILoggerFactory logger,
UrlEncoder encoder,
ApiContext apiContext
) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
) : AuthenticationHandler<AuthenticationSchemeOptions>(authOptions, logger, encoder)
{
public const string Role = "Photon";

Expand All @@ -40,11 +42,7 @@ out AuthenticationHeaderValue? authenticationHeader
return AuthenticateResult.NoResult();
}

string configuredToken =
Environment.GetEnvironmentVariable("PHOTON_TOKEN")
?? throw new InvalidOperationException("PHOTON_TOKEN environment variable not set!");

if (authenticationHeader.Parameter != configuredToken)
if (authenticationHeader.Parameter != photonOptions.CurrentValue.Token)
{
Logger.LogInformation(
"AuthenticationHeader.Parameter value {param} did not match configured token.",
Expand Down
6 changes: 4 additions & 2 deletions DragaliaAPI/DragaliaAPI/Models/Options/PhotonOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

public class PhotonOptions
{
public required string ServerUrl { get; set; }
public required string ServerUrl { get; init; }

public required string StateManagerUrl { get; set; }
public required string StateManagerUrl { get; init; }

public required string Token { get; init; }
}
6 changes: 6 additions & 0 deletions DragaliaAPI/DragaliaAPI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
reloadOnChange: true
);

string kpfPath = Path.Combine(Directory.GetCurrentDirectory(), "config");

builder.Configuration.AddKeyPerFile(directoryPath: kpfPath, optional: true, reloadOnChange: true);

builder.WebHost.UseStaticWebAssets();

builder.Logging.ClearProviders();
Expand Down Expand Up @@ -105,6 +109,8 @@

WebApplication app = builder.Build();

app.Logger.LogDebug("Using key-per-file configuration from path {KpfPath}", kpfPath);

Stopwatch watch = new();
app.Logger.LogInformation("Loading MasterAsset data.");

Expand Down
16 changes: 7 additions & 9 deletions DragaliaAPI/DragaliaAPI/ServiceConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,13 @@ IConfiguration configuration

services.AddHttpClient<IBaasApi, BaasApi>();

services.AddHttpClient<IPhotonStateApi, PhotonStateApi>(client =>
{
PhotonOptions? options = configuration
.GetRequiredSection(nameof(PhotonOptions))
.Get<PhotonOptions>();
ArgumentNullException.ThrowIfNull(options);

client.BaseAddress = new(options.StateManagerUrl);
});
services.AddHttpClient<IPhotonStateApi, PhotonStateApi>(
(sp, client) =>
{
IOptions<PhotonOptions> options = sp.GetRequiredService<IOptions<PhotonOptions>>();
client.BaseAddress = new(options.Value.StateManagerUrl);
}
);
services.AddScoped<IMatchingService, MatchingService>();

services.AddScoped<ResourceVersionActionFilter>().AddScoped<MaintenanceActionFilter>();
Expand Down
3 changes: 3 additions & 0 deletions DragaliaAPI/DragaliaAPI/appsettings.Testing.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"Android": "y2XM6giU6zz56wCm",
"Ios": "b1HyoeTFegeTexC0"
},
"PhotonOptions": {
"Token": "supersecrettoken"
},
"EventOptions": {
"EventList": [
{
Expand Down
8 changes: 4 additions & 4 deletions PhotonPlugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ The state manager application should be comparitively easy to deploy. It is a Do
deployed alongside a `redis/redis-stack` image. See the `docker-compose.yml` file in the repository for reference. It
does not need to be on the same server as Photon.

It expects an environment variable, `PHOTON_TOKEN`, to match that which is configured in `plugin.config` above, so as to
It expects an environment variable, `PhotonOptions__Token`, to match that which is configured in `plugin.config` above, so as to
authenticate requests from the Photon server. A sample Docker compose file could look like:

```yaml
Expand All @@ -132,7 +132,7 @@ services:
ports:
- "3000:80"
environment:
PHOTON_TOKEN: yourtoken
PhotonOptions__Token: yourtoken

redis:
hostname: redis
Expand All @@ -154,9 +154,9 @@ In the main API `appsettings.json`, configure the following values in `$.PhotonO
- `ServerUrl`: the Photon server URL. Must end with :5055 due to Dragalia using a legacy client.
- `StateManagerUrl` the Photon state manager URL.

Configure the following environment variables:
Configure the following environment variables (or other source of `IConfiguration`):

- `PHOTON_TOKEN`: the same photon token as the plugin and state manager use. Used to authenticate requests from Photon.
- `PhotonOptions__Token`: the same photon token as the plugin and state manager use. Used to authenticate requests from Photon.

#### Other servers

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;

namespace DragaliaAPI.Photon.StateManager.Test;

Expand All @@ -12,25 +13,19 @@ public CustomWebApplicationFactory()
this.testContainersHelper = new();
}

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
Environment.SetEnvironmentVariable(
"RedisOptions__Hostname",
this.testContainersHelper.RedisHost
);
Environment.SetEnvironmentVariable(
"RedisOptions__Port",
this.testContainersHelper.RedisPort.ToString()
protected override void ConfigureWebHost(IWebHostBuilder builder) =>
builder.ConfigureAppConfiguration(cfg =>
cfg.AddInMemoryCollection(
new Dictionary<string, string?>
{
["PhotonOptions:Token"] = "photontoken",
["RedisOptions:Hostname"] = this.testContainersHelper.RedisHost,
["RedisOptions:Port"] = this.testContainersHelper.RedisPort.ToString()
}
)
);
}

public Task InitializeAsync() => this.testContainersHelper.StartAsync();

Task IAsyncLifetime.DisposeAsync() => this.testContainersHelper.StopAsync();

protected override void Dispose(bool disposing)
{
Environment.SetEnvironmentVariable("RedisOptions__Hostname", null);
Environment.SetEnvironmentVariable("RedisOptions__Port", null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,23 @@ namespace DragaliaAPI.Photon.StateManager.Test;
[Collection(TestCollection.Name)]
public class TestFixture : IAsyncLifetime
{
private const string PhotonToken = "photontoken";

public TestFixture(CustomWebApplicationFactory factory, ITestOutputHelper outputHelper)
{
this.Client = factory
.WithWebHostBuilder(
(builder) =>
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddXUnit(outputHelper);
})
.WithWebHostBuilder(builder =>
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddXUnit(outputHelper);
})
)
.CreateClient();

this.Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
PhotonToken
"photontoken"
);

Environment.SetEnvironmentVariable("PHOTON_TOKEN", PhotonToken);

this.RedisConnectionProvider =
factory.Services.GetRequiredService<IRedisConnectionProvider>();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using DragaliaAPI.Photon.StateManager.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;

namespace DragaliaAPI.Photon.StateManager.Authentication;

public class PhotonAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
public class PhotonAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> authOptions,
IOptionsMonitor<PhotonOptions> photonOptions,
ILoggerFactory logger,
UrlEncoder encoder
) : AuthenticationHandler<AuthenticationSchemeOptions>(authOptions, logger, encoder)
{
public PhotonAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder
)
: base(options, logger, encoder) { }

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (
Expand All @@ -34,11 +33,7 @@ out AuthenticationHeaderValue? authenticationHeader
return Task.FromResult(AuthenticateResult.NoResult());
}

string configuredToken =
Environment.GetEnvironmentVariable("PHOTON_TOKEN")
?? throw new InvalidOperationException("PHOTON_TOKEN environment variable not set!");

if (authenticationHeader.Parameter != configuredToken)
if (authenticationHeader.Parameter != photonOptions.CurrentValue.Token)
{
this.Logger.LogInformation(
"AuthenticationHeader.Parameter value {param} did not match configured token.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace DragaliaAPI.Photon.StateManager.Models;

public class PhotonOptions
{
public required string Token { get; init; }
}
62 changes: 34 additions & 28 deletions PhotonStateManager/DragaliaAPI.Photon.StateManager/Program.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
using System.Security.Cryptography.X509Certificates;
using DragaliaAPI.Photon.StateManager;
using DragaliaAPI.Photon.StateManager.Authentication;
using DragaliaAPI.Photon.StateManager.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using Redis.OM;
using Redis.OM.Contracts;
using Serilog;
using StackExchange.Redis;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

if (Environment.GetEnvironmentVariable("ENABLE_HTTPS") != null)
{
X509Certificate2 certificate = X509Certificate2.CreateFromPemFile("cert.pem", "cert.key");

builder.WebHost.ConfigureKestrel(serverOptions =>
serverOptions.ConfigureHttpsDefaults(options => options.ServerCertificate = certificate)
);
}

builder.Services.AddControllers();

builder.Host.UseSerilog(
Expand All @@ -39,9 +30,20 @@
builder
.Services.AddOptions<RedisCachingOptions>()
.BindConfiguration(nameof(RedisCachingOptions))
.Validate(x => x.KeyExpiryTimeMins > 0)
.Validate(
x => x.KeyExpiryTimeMins > 0,
"RedisCachingOptions.KeyExpiryTime must be greater than 0!"
)
.ValidateOnStart();

builder
.Services.AddOptions<PhotonOptions>()
.BindConfiguration(nameof(PhotonOptions))
.Validate(x => !string.IsNullOrEmpty(x.Token), "Must specify a value for PhotonOptions.Token!")
.ValidateOnStart();

builder.Services.AddOptions<RedisOptions>().BindConfiguration(nameof(RedisOptions));

builder
.Services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, PhotonAuthenticationHandler>(
Expand All @@ -65,29 +67,33 @@
);
});

RedisOptions redisOptions =
builder.Configuration.GetRequiredSection(nameof(RedisOptions)).Get<RedisOptions>()
?? throw new InvalidOperationException("Failed to deserialize Redis configuration");
builder.Services.AddSingleton<IRedisConnectionProvider, RedisConnectionProvider>(sp =>
{
RedisOptions redisOptions = sp.GetRequiredService<IOptions<RedisOptions>>().Value;

Log.Logger.Information(
IConnectionMultiplexer multiplexer = ConnectionMultiplexer.Connect(
new ConfigurationOptions()
{
EndPoints = new() { { redisOptions.Hostname, redisOptions.Port } },
Password = redisOptions.Password,
AbortOnConnectFail = false,
}
);

return new RedisConnectionProvider(multiplexer);
});

WebApplication app = builder.Build();

RedisOptions redisOptions = app.Services.GetRequiredService<IOptions<RedisOptions>>().Value;

app.Logger.LogInformation(
"Connecting to Redis at {Hostname}:{Port}",
redisOptions.Hostname,
redisOptions.Port
);

IConnectionMultiplexer multiplexer = ConnectionMultiplexer.Connect(
new ConfigurationOptions()
{
EndPoints = new() { { redisOptions.Hostname, redisOptions.Port } },
Password = redisOptions.Password,
AbortOnConnectFail = false,
}
);

RedisConnectionProvider provider = new(multiplexer);
builder.Services.AddSingleton<IRedisConnectionProvider>(provider);

WebApplication app = builder.Build();
IRedisConnectionProvider provider = app.Services.GetRequiredService<IRedisConnectionProvider>();

bool created = await provider.Connection.CreateIndexAsync(typeof(RedisGame));
RedisIndexInfo? info = await provider.Connection.GetIndexInfoAsync(typeof(RedisGame));
Expand Down
6 changes: 5 additions & 1 deletion PhotonStateManager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ It operates by receiving webhook events from the Photon server, which trigger ro

PhotonStateManager exposes a very simple REST API that is divided into two halves: 'private' endpoints designed for consumption by the Photon server, and 'public' endpoints that are designed to be consumed by the main Dragalia Lost API server. The private endpoints are under the /event/ route group, and the public endpoints are under the /get/ route group.

The private endpoints are secured by bearer token authentication set by an environment variable `PHOTON_TOKEN`.
The private endpoints are secured by bearer token authentication set by an IConfiguration property `PhotonOptions.Token`. The simplest way to configure this is by setting the environment variable `PhotonOptions__Token`.

### /get/ endpoints

- `/get/gamelist`: Returns a list of currently open games that are available to join for public matchmaking.
- `/get/byid/{roomId}`: Searches for a room by its numeric passcode. This can include private rooms as well. If found, returns 200 OK, otherwise 404 Not Found.
- `/get/ishost/{viewerId}`: Returns a scalar boolean indicating whether a user with the provided player ID is a host in any room.
- `/get/byviewerid/{viewerId}`: Returns the room that a player is in, or 404 if they could not be found in a room.

## HTTPS

To configure the container to accept requests over HTTPS, follow the instructions in the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-8.0#configure-https-in-appsettingsjson).
Loading