Skip to content
Open
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
15 changes: 6 additions & 9 deletions GenOnlineService/Controllers/CheckLogin/CheckLoginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,15 @@ public async Task<APIResult> Post()
{
string jsonData = await reader.ReadToEndAsync();

bool bSecureWS = true;
//if (ipCountry2LISO.ToLower() == "ru")
{
//bSecureWS = false;
}

POST_CheckLogin_Result result = (POST_CheckLogin_Result)await Post_InternalHandler(jsonData, IPHelpers.NormalizeIP(HttpContext.Connection.RemoteIpAddress?.ToString()), bSecureWS);
POST_CheckLogin_Result result = (POST_CheckLogin_Result)await Post_InternalHandler(
jsonData,
IPHelpers.NormalizeIP(HttpContext.Connection.RemoteIpAddress?.ToString()),
Program.BuildWebSocketUrl(Request));
return result;
}
}

public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr, bool bSecureWS, bool bIsMonitor = false)
public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr, string webSocketUrl, bool bIsMonitor = false)
{
POST_CheckLogin_Result result = new POST_CheckLogin_Result();

Expand Down Expand Up @@ -218,7 +215,7 @@ public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr
result.refresh_token = refreshtoken;
result.user_id = user_id;
result.display_name = strDisplayName;
result.ws_uri = Program.GetWebSocketAddress(bSecureWS);
result.ws_uri = webSocketUrl;

// clear cached data, its a new session and the client reconnects its
// websocket using the ws_uri below - must be awaited so the teardown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,15 @@ public async Task<APIResult> Post()
{
string jsonData = await reader.ReadToEndAsync();

bool bSecureWS = true;
//if (ipCountry2LISO.ToLower() == "ru")
{
//bSecureWS = false;
}

POST_LoginWithToken_Result result = (POST_LoginWithToken_Result)await Post_InternalHandler(jsonData, IPHelpers.NormalizeIP(HttpContext.Connection.RemoteIpAddress?.ToString()), bSecureWS);
POST_LoginWithToken_Result result = (POST_LoginWithToken_Result)await Post_InternalHandler(
jsonData,
IPHelpers.NormalizeIP(HttpContext.Connection.RemoteIpAddress?.ToString()),
Program.BuildWebSocketUrl(Request));
return result;
}
}

public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr, bool bSecureWS, bool bWasMonitor = false)
public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr, string webSocketUrl, bool bWasMonitor = false)
{
if (bWasMonitor)
{
Expand Down Expand Up @@ -164,7 +161,7 @@ public async Task<APIResult> Post_InternalHandler(string jsonData, string ipAddr
result.user_id = user_id;
result.display_name = strDisplayName;

result.ws_uri = Program.GetWebSocketAddress(bSecureWS);
result.ws_uri = webSocketUrl;

// This endpoint re-establishes a session, so tear down any state the previous
// one left behind (lobby membership, matchmaking, cached session data). Must
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ public async Task<APIResult> Monitor_Database()
var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();

GenOnlineService.Controllers.LoginWithToken.LoginWithToken loginWithTokenController = new GenOnlineService.Controllers.LoginWithToken.LoginWithToken(factory);
GenOnlineService.Controllers.LoginWithToken.POST_LoginWithToken_Result internalResult = (GenOnlineService.Controllers.LoginWithToken.POST_LoginWithToken_Result)await loginWithTokenController.Post_InternalHandler("{\"challenge\": \"abc\", \"token\": \"iamatest\", \"client_id\": \"gen_online_60hz\"}", IPAddress.Loopback.ToString(), true);
GenOnlineService.Controllers.LoginWithToken.POST_LoginWithToken_Result internalResult = (GenOnlineService.Controllers.LoginWithToken.POST_LoginWithToken_Result)await loginWithTokenController.Post_InternalHandler("{\"challenge\": \"abc\", \"token\": \"iamatest\", \"client_id\": \"gen_online_60hz\"}", IPAddress.Loopback.ToString(), Program.BuildWebSocketUrl(Request));
return internalResult;
}
catch
Expand Down Expand Up @@ -302,7 +302,7 @@ public APIResult Monitor_Uptime()
try
{
GenOnlineService.Controllers.CheckLoginController checkLoginController = new GenOnlineService.Controllers.CheckLoginController(_dbFactory);
APIResult internalResult = await checkLoginController.Post_InternalHandler("{\"challenge\": \"abc\", \"nonce\": \"def\", \"code\": \"iamatest\", \"client_id\": \"gen_online_30hz\"}", IPAddress.Loopback.ToString(), true);
APIResult internalResult = await checkLoginController.Post_InternalHandler("{\"challenge\": \"abc\", \"nonce\": \"def\", \"code\": \"iamatest\", \"client_id\": \"gen_online_30hz\"}", IPAddress.Loopback.ToString(), Program.BuildWebSocketUrl(Request));
return internalResult;
}
catch
Expand Down
197 changes: 144 additions & 53 deletions GenOnlineService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
using MaxMind.GeoIP2;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.AspNetCore.WebSockets;
Expand All @@ -36,6 +39,7 @@
using System.Configuration;
using System.Drawing;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Security.Claims;
Expand Down Expand Up @@ -756,31 +760,131 @@ public string GenerateToken(string displayname, Int64 userID, string ipAddr, ETo
}
}

public static string GetWebSocketAddress(bool bSecure)
private static CorsPolicy BuildCorsPolicy(IConfiguration configuration)
{
if (Program.g_Config == null)
string[] configuredOrigins = configuration
.GetSection("AllowedOrigins")
.Get<string[]>() ?? Array.Empty<string>();

string[] allowedOrigins = configuredOrigins
.Select(origin => origin.Trim())
.Where(origin => origin.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();

bool allowAnyOrigin = allowedOrigins.Contains("*");
if (allowAnyOrigin && allowedOrigins.Length > 1)
{
throw new Exception("g_Config is null.");
throw new InvalidOperationException("AllowedOrigins cannot combine '*' with explicit origins.");
}

IConfiguration? coreSettings = Program.g_Config.GetSection("Core");
var policyBuilder = new CorsPolicyBuilder()
.AllowAnyHeader()
.AllowAnyMethod();

if (coreSettings == null)
if (allowAnyOrigin)
{
throw new Exception("Core section of config is null.");
// CORS forbids combining any-origin with credential sharing.
policyBuilder.AllowAnyOrigin();
}
else if (allowedOrigins.Length > 0)
{
policyBuilder.WithOrigins(allowedOrigins)
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowCredentials();
}

return policyBuilder.Build();
}

private static bool TryConfigureForwardedHeaders(IServiceCollection services, IConfiguration configuration)
{
string[] trustedProxyValues = (configuration.GetSection("TrustedProxies").Get<string[]>() ?? Array.Empty<string>())
.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(value => value.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();

if (trustedProxyValues.Length == 0)
{
return false;
}

bool trustAnyProxy = trustedProxyValues.Contains("*");
if (trustAnyProxy && trustedProxyValues.Length > 1)
{
throw new InvalidOperationException("TrustedProxies cannot combine '*' with IP addresses or CIDR networks.");
}

var trustedProxyAddresses = new List<IPAddress>();
var trustedProxyNetworks = new List<System.Net.IPNetwork>();

foreach (string value in trustedProxyValues.Where(value => value != "*"))
{
if (IPAddress.TryParse(value, out IPAddress? address))
{
trustedProxyAddresses.Add(address);
}
else if (System.Net.IPNetwork.TryParse(value, out System.Net.IPNetwork network))
{
trustedProxyNetworks.Add(network);
}
else
{
throw new InvalidOperationException($"TrustedProxies contains invalid IP address or CIDR network '{value}'.");
}
}

services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
| ForwardedHeaders.XForwardedProto
| ForwardedHeaders.XForwardedHost
| ForwardedHeaders.XForwardedPrefix;
options.KnownProxies.Clear();
options.KnownIPNetworks.Clear();

if (trustAnyProxy)
{
return;
}

foreach (IPAddress trustedProxyAddress in trustedProxyAddresses)
{
options.KnownProxies.Add(trustedProxyAddress);
}

foreach (System.Net.IPNetwork trustedProxyNetwork in trustedProxyNetworks)
{
options.KnownIPNetworks.Add(trustedProxyNetwork);
}
});

string configKey = bSecure ? "ws_address" : "ws_address_insecure";
return true;
}

string? ws_address = coreSettings.GetValue<string>(configKey);
public static string BuildWebSocketUrl(HttpRequest request)
{
string webSocketScheme;
if (request.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
{
webSocketScheme = "wss";
}
else if (request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
{
webSocketScheme = "ws";
}
else
{
throw new InvalidOperationException($"Cannot build a WebSocket URL from request scheme '{request.Scheme}'.");
}

if (ws_address == null)
if (!request.Host.HasValue)
{
throw new Exception(String.Format("{0} in Core section of config is null.", configKey));
throw new InvalidOperationException("Cannot build a WebSocket URL because the request Host is empty.");
}

return ws_address;
return UriHelper.BuildAbsolute(webSocketScheme, request.Host, request.PathBase, new PathString("/ws"));
}

public static async Task Main(string[] args)
Expand All @@ -798,6 +902,10 @@ public static async Task Main(string[] args)

g_Config = builder.Configuration;

// Process the original client IP, request scheme, and host only when
// one or more trusted reverse proxies are configured.
bool useForwardedHeaders = TryConfigureForwardedHeaders(builder.Services, builder.Configuration);

ShowLogo();

IConfigurationSection? sentrySettings = Program.g_Config.GetSection("Sentry");
Expand Down Expand Up @@ -891,19 +999,11 @@ public static async Task Main(string[] args)
});
}

CorsPolicy corsPolicy = BuildCorsPolicy(builder.Configuration);

builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(
"https://localhost:9000",
"http://localhost:9001",
"https://*.playgenerals.online"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
options.AddDefaultPolicy(corsPolicy);
});


Expand Down Expand Up @@ -1105,35 +1205,6 @@ public static async Task Main(string[] args)
}
}

var kestrelSettings = Program.g_Config.GetSection("Kestrel");
var endpointSettings = kestrelSettings.GetSection("Endpoints");
var httpsSettings = endpointSettings.GetSection("HTTPS");
string? serverURI = httpsSettings.GetValue<string>("Url");

if (serverURI == null)
{
Console.WriteLine("FATAL ERROR: serverURI is not set in the config");
Console.ReadKey(true);
return;
}

// Parse the port number out of serverURI
int port = -1;
try
{
if (!string.IsNullOrEmpty(serverURI))
{
var uri = new Uri(serverURI);
port = uri.Port;
}
}
catch
{
Console.WriteLine("ERROR: Failed to parse port from serverURI: " + serverURI);
}



// options
builder.WebHost.ConfigureKestrel(options =>
{
Expand Down Expand Up @@ -1162,6 +1233,12 @@ public static async Task Main(string[] args)
var app = builder.Build();
ServiceLocator.Services = app.Services;

if (useForwardedHeaders)
{
// Must run before anything that consumes client IP or request scheme.
app.UseForwardedHeaders();
}

if (bUseBuiltinRateLimiter)
{
app.UseRateLimiter();
Expand All @@ -1179,6 +1256,21 @@ public static async Task Main(string[] args)

app.UseWebSockets(webSocketOptions);

// WebSockets do not use CORS, so apply the same origin policy to browser
// handshakes explicitly. Native clients may omit Origin.
app.Use(async (context, next) =>
{
if (context.WebSockets.IsWebSocketRequest
&& context.Request.Headers.TryGetValue("Origin", out var originHeaders)
&& (originHeaders.Count != 1 || !corsPolicy.IsOriginAllowed(originHeaders[0]!)))
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
return;
}

await next(context);
});

// end websocket

// Configure the HTTP request pipeline.
Expand All @@ -1202,8 +1294,7 @@ public static async Task Main(string[] args)
{
app.UseHsts();

// Websocket upgrades are exempt: ws_address_insecure is an intentional fallback for
// clients that can't do TLS, and redirecting the upgrade request would break it.
// WebSocket upgrades are exempt because redirecting an upgrade request breaks the handshake.
app.UseWhen(context => !context.WebSockets.IsWebSocketRequest, branch =>
{
branch.UseHttpsRedirection();
Expand Down
4 changes: 2 additions & 2 deletions GenOnlineService/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
}
},
"AllowedHosts": "*",
"AllowedOrigins": [],
"TrustedProxies": [],
"JwtSettings": {
"Key": "TODO_CHANGE_ME",
"Issuer": "YourIssuer",
Expand All @@ -27,8 +29,6 @@
"ExpiresInMinutes_Refresh": 43200
},
"Core": {
"ws_address": "wss://127.0.0.1:9000/ws",
"ws_address_insecure": "ws://127.0.0.1:9000/ws",
"use_os_cert_store": true,
"cert_pem_path": null,
"cert_key_path": null,
Expand Down
Loading