396 lines
15 KiB
C#
396 lines
15 KiB
C#
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<string> 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<string> 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<ILiteDbService, LiteDbService>();
|
|
builder.Services.AddSingleton<IJwtService, JwtService>();
|
|
builder.Services.AddSingleton<IPasswordService, PasswordService>();
|
|
builder.Services.AddSingleton<INotificationService, NotificationService>();
|
|
builder.Services.AddSingleton<IMessageService, MessageService>();
|
|
builder.Services.AddSingleton<ICdnService, CdnService>();
|
|
|
|
builder.Services.AddSingleton(new DiscordSocketClient(new DiscordSocketConfig
|
|
{
|
|
GatewayIntents = GatewayIntents.All,
|
|
AlwaysDownloadUsers = true
|
|
}));
|
|
|
|
builder.Services.AddSingleton(x => new InteractionService(x.GetRequiredService<DiscordSocketClient>()));
|
|
|
|
builder.Services.AddSingleton<DiscordBotService>();
|
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<DiscordBotService>());
|
|
|
|
|
|
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<DateOnly>(
|
|
serialize: value => value.ToDateTime(TimeOnly.MinValue),
|
|
deserialize: bson => DateOnly.FromDateTime(bson.AsDateTime)
|
|
);
|
|
|
|
BsonMapper.Global.RegisterType<DateOnly?>(
|
|
serialize: value => value?.ToDateTime(TimeOnly.MinValue),
|
|
deserialize: bson => bson.IsNull ? null : DateOnly.FromDateTime(bson.AsDateTime)
|
|
);
|
|
BsonMapper.Global.RegisterType<System.Drawing.Color>(
|
|
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<NotificationHub>("/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<List<AvatarItemDto>>(json));
|
|
}
|
|
app.Run();
|
|
}
|
|
|
|
}
|
|
}
|