using DeluxeBackend.Hubs; using DeluxeBackend.Services; using Discord; using Discord.Interactions; using Discord.WebSocket; using LiteDB; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.HttpLogging; using Microsoft.AspNetCore.SignalR; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Scalar.AspNetCore; namespace DeluxeBackend { public class Program { private static readonly SemaphoreSlim FileLock = new SemaphoreSlim(1, 1); private static Task GenerateSignatureBytes(string signingKey, string uri, byte[] bodyBytes) { byte[] signingKeyBytes = Convert.FromBase64String(signingKey); byte[] uriBytes = Encoding.ASCII.GetBytes(uri); using (IncrementalHash incrementalHash = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA256, signingKeyBytes)) { incrementalHash.AppendData(uriBytes); if (bodyBytes != null && bodyBytes.Length != 0) { incrementalHash.AppendData(BitConverter.GetBytes(bodyBytes.Length)); if (bodyBytes.Length > 2048) { //Console.WriteLine("Body length > 2048, doing large payload sampling."); int sectionSize = bodyBytes.Length / 16; //Console.WriteLine($"Appending the first 128 bytes of 16 sections each sized {sectionSize} bytes."); for (int i = 0; i < 16; i++) { int currentSection = i * sectionSize; incrementalHash.AppendData(bodyBytes, currentSection, 128); } } else { incrementalHash.AppendData(bodyBytes); } } else { //Console.WriteLine("Body Length is 0, not appending any body data."); } string base64Hash = Convert.ToBase64String(incrementalHash.GetHashAndReset()); return Task.FromResult(base64Hash); } } private static Task GenerateResponseSignatureRsa(RSAParameters rsaParams, string url, int statusCode, byte[] bodyBytes) { byte[] urlBytes = Encoding.UTF8.GetBytes(url); byte[] statusBytes = Encoding.UTF8.GetBytes(statusCode.ToString()); using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); incrementalHash.AppendData(urlBytes); incrementalHash.AppendData(statusBytes); if (bodyBytes != null && bodyBytes.Length != 0) { incrementalHash.AppendData(BitConverter.GetBytes(bodyBytes.Length)); if (bodyBytes.Length > 2048) { int sectionSize = bodyBytes.Length / 16; for (int i = 0; i < 16; i++) { int currentSection = i * sectionSize; incrementalHash.AppendData(bodyBytes, currentSection, 128); } } else { incrementalHash.AppendData(bodyBytes); } } byte[] hashBytes = incrementalHash.GetHashAndReset(); using RSA rsa = RSA.Create(); rsa.ImportParameters(rsaParams); byte[] signatureBytes = rsa.SignHash(hashBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return Task.FromResult(Convert.ToBase64String(signatureBytes)); } public static void Main(string[] args) { //JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); var builder = WebApplication.CreateBuilder(args); var corsPolicyName = "AllowAll"; string base64Key = "RXq0pIk5tTHbKnvWVoUTGUWW3dWt5hdqcTyZ8V71lvI="; builder.Configuration["JwtConfig:Key"] = base64Key; builder.Services.AddHttpLogging(logging => { logging.LoggingFields = HttpLoggingFields.All; logging.RequestHeaders.Add("Authorization"); logging.RequestHeaders.Add("Cookie"); logging.ResponseHeaders.Add("WWW-Authenticate"); }); builder.Services.AddHttpClient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(new DiscordSocketClient(new DiscordSocketConfig { GatewayIntents = GatewayIntents.All, AlwaysDownloadUsers = true })); builder.Services.AddSingleton(x => new InteractionService(x.GetRequiredService())); builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); builder.Services.AddControllers().AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; }); builder.Services.AddRazorPages(); builder.Services.AddSignalR().AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.CheckAdditionalContent = false; }); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromHours(1); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); builder.Services.AddCors(options => { options.AddPolicy(name: corsPolicyName, policy => { policy.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); }); #if DEBUG builder.Services.AddOpenApi(options => { options.AddDocumentTransformer((document, context, cancellationToken) => { document.Info.Title = "API"; document.Info.Version = "v1"; return Task.CompletedTask; }); }); #endif builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = builder.Configuration["JwtConfig:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(builder.Configuration["JwtConfig:Key"])), ValidateIssuer = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidateAudience = false }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { var path = context.HttpContext.Request.Path; if (path.StartsWithSegments("/Notifications/hub/v1")) { var accessToken = context.Request.Query["access_token"]; if (!string.IsNullOrEmpty(accessToken)) context.Token = accessToken; } return Task.CompletedTask; }, OnAuthenticationFailed = ctx => { Console.WriteLine(ctx.Exception.ToString()); return Task.CompletedTask; } }; }); BsonMapper.Global.RegisterType( serialize: value => value.ToDateTime(TimeOnly.MinValue), deserialize: bson => DateOnly.FromDateTime(bson.AsDateTime) ); BsonMapper.Global.RegisterType( serialize: value => value?.ToDateTime(TimeOnly.MinValue), deserialize: bson => bson.IsNull ? null : DateOnly.FromDateTime(bson.AsDateTime) ); BsonMapper.Global.RegisterType( serialize: (color) => color.ToArgb(), deserialize: (bson) => System.Drawing.Color.FromArgb(bson.AsInt32) ); builder.Services.AddAuthorization(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseHttpLogging(); /*app.MapOpenApi(); app.MapScalarApiReference("/docs", options => { options.HideDownloadButton = true; options.Theme = ScalarTheme.DeepSpace; options.Layout = ScalarLayout.Classic; options.HideDarkModeToggle = true; options.DarkMode = true; });*/ } app.Use(async (context, next) =>//this is for when rr fucks the uri builder in 2018 builds { var path = context.Request.Path.Value; if (!string.IsNullOrEmpty(path) && path.Contains("//")) { context.Request.Path = "/" + path.TrimStart('/'); } await next(); }); app.UseCors(corsPolicyName); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); /*app.Use(async (context, next) => { string userAgent = context.Request.Headers["User-Agent"].ToString(); if (userAgent != "BestHTTP/Deluxe") { await next(); return; } var originalBodyStream = context.Response.Body; using (var responseBodyMemoryStream = new MemoryStream()) { context.Response.Body = responseBodyMemoryStream; await next(); string url = context.Request.Path + context.Request.QueryString; int statusCode = context.Response.StatusCode; var headersBuilder = new StringBuilder(); foreach (var header in context.Request.Headers.OrderBy(h => h.Key)) { headersBuilder.Append($"{header.Key}:{header.Value}\n"); } string serializedHeaders = headersBuilder.ToString(); responseBodyMemoryStream.Position = 0; byte[] responseBodyBytes = responseBodyMemoryStream.ToArray(); string signature = await GenerateResponseSignatureRsa( ServerConfig.RsaParams, url, statusCode, responseBodyBytes ); context.Response.Headers["X-Server-Signature"] = signature; responseBodyMemoryStream.Position = 0; await responseBodyMemoryStream.CopyToAsync(originalBodyStream); } });*/ app.Use(async (context, next) => { if (context.User.Identity?.IsAuthenticated == true && context.User.IsInRole("gameClient")) { if (HttpMethods.IsPost(context.Request.Method) || HttpMethods.IsPut(context.Request.Method)) { if (!context.Request.Headers.TryGetValue("X-RNSIG", out var clientSignature)) { context.Response.StatusCode = 403; return; } context.Request.EnableBuffering(); byte[] bodyBytes; using (var ms = new MemoryStream()) { await context.Request.Body.CopyToAsync(ms); bodyBytes = ms.ToArray(); context.Request.Body.Position = 0; } string uri = context.Request.Path; string expectedSignature = await GenerateSignatureBytes(ServerConfig.SigKey, uri, bodyBytes); if (clientSignature != expectedSignature) { Console.WriteLine($"Signature Mismatch! Client: {clientSignature}, Expected: {expectedSignature}"); context.Response.StatusCode = 403; return; } } } await next(); }); app.Use(async (context, next) => { await next(); if (context.Response.StatusCode >= 400) { try { await FileLock.WaitAsync(); string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); string method = context.Request.Method; string url = context.Request.Path + context.Request.QueryString; int status = context.Response.StatusCode; string logLine = $"[{timestamp}] {status} | {method} {url}{Environment.NewLine}"; await File.AppendAllTextAsync("error_urls.txt", logLine); } catch (Exception ex) { Console.WriteLine($"Failed to write error log to file: {ex.Message}"); } finally { FileLock.Release(); } } }); app.MapControllers(); app.MapHub("/Notifications/hub/v1"); if (ServerConfig.avatarItems.Count == 0) { var json = File.ReadAllText(Path.Combine("data", "AvatarItems.json")); ServerConfig.avatarItems.AddRange(System.Text.Json.JsonSerializer.Deserialize>(json)); } app.Run(); } } }