using AspNetCoreRateLimit; using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; using LightlessSync.API.SignalR; using LightlessSyncServer.Configuration; using LightlessSyncServer.Controllers; using LightlessSyncServer.Hubs; using LightlessSyncServer.Services; using LightlessSyncServer.Services.Interfaces; using LightlessSyncServer.Worker; using LightlessSyncShared.Data; using LightlessSyncShared.Metrics; using LightlessSyncShared.RequirementHandlers; using LightlessSyncShared.Services; using LightlessSyncShared.Utils; using LightlessSyncShared.Utils.Configuration; using MessagePack; using MessagePack.Resolvers; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Prometheus; using StackExchange.Redis; using StackExchange.Redis.Extensions.Core.Configuration; using StackExchange.Redis.Extensions.System.Text.Json; using System.Net; using System.Text; namespace LightlessSyncServer; public class Startup { private readonly ILogger _logger; public Startup(IConfiguration configuration, ILogger logger) { Configuration = configuration; _logger = logger; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddTransient(_ => Configuration); var lightlessConfig = Configuration.GetRequiredSection("LightlessSync"); // configure metrics ConfigureMetrics(services); // configure database ConfigureDatabase(services, lightlessConfig); // configure authentication and authorization ConfigureAuthorization(services); // configure rate limiting ConfigureIpRateLimiting(services); // configure SignalR ConfigureSignalR(services, lightlessConfig); // configure lightless specific services ConfigureLightlessServices(services, lightlessConfig); services.AddHealthChecks(); services.AddControllers().ConfigureApplicationPartManager(a => { a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); if (lightlessConfig.GetValue(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null) { a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(LightlessServerConfigurationController), typeof(LightlessBaseConfigurationController), typeof(ClientMessageController), typeof(UserController), typeof(GroupController))); } else { a.FeatureProviders.Add(new AllowedControllersFeatureProvider()); } }); } private void ConfigureLightlessServices(IServiceCollection services, IConfigurationSection lightlessConfig) { bool isMainServer = lightlessConfig.GetValue(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null; services.Configure(Configuration.GetRequiredSection("LightlessSync")); services.Configure(Configuration.GetRequiredSection("LightlessSync")); services.Configure(Configuration.GetSection("Broadcast")); services.Configure(Configuration.GetSection("ChatZoneOverrides")); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddHostedService(provider => provider.GetService()); // configure services based on main server status ConfigureServicesBasedOnShardType(services, lightlessConfig, isMainServer); services.AddSingleton(s => new LightlessCensus(s.GetRequiredService>())); services.AddHostedService(p => p.GetRequiredService()); if (isMainServer) { services.AddSingleton(); services.AddHostedService(provider => provider.GetService()); services.AddSingleton(); services.AddHostedService(provider => provider.GetService()); services.AddHostedService(); services.AddScoped(); services.AddScoped(); } services.AddSingleton(); services.AddHostedService(); services.AddHostedService(provider => provider.GetService()); } private static void ConfigureSignalR(IServiceCollection services, IConfigurationSection lightlessConfig) { services.AddSingleton(); services.AddSingleton(); var msgpackOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block).WithResolver(ContractlessStandardResolver.Instance); var signalRServiceBuilder = services.AddSignalR(hubOptions => { hubOptions.MaximumReceiveMessageSize = long.MaxValue; hubOptions.EnableDetailedErrors = true; hubOptions.MaximumParallelInvocationsPerClient = 10; hubOptions.StreamBufferCapacity = 200; hubOptions.AddFilter(); hubOptions.AddFilter(); }).AddMessagePackProtocol(opt => { opt.SerializerOptions = msgpackOptions; var dummy = new GroupPruneSettingsDto(new GroupData("TEST-GID", null), true, 14); MessagePackSerializer.Serialize(dummy, msgpackOptions); }); // configure redis for SignalR var redisConnection = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty); signalRServiceBuilder.AddStackExchangeRedis(redisConnection, options => { }); var options = ConfigurationOptions.Parse(redisConnection); var endpoint = options.EndPoints[0]; string address = ""; int port = 0; if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; } if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; } var redisConfiguration = new RedisConfiguration() { AbortOnConnectFail = true, KeyPrefix = "", Hosts = new RedisHost[] { new(){ Host = address, Port = port }, }, AllowAdmin = true, ConnectTimeout = options.ConnectTimeout, Database = 0, Ssl = false, Password = options.Password, ServerEnumerationStrategy = new ServerEnumerationStrategy() { Mode = ServerEnumerationStrategy.ModeOptions.All, TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any, UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw, }, MaxValueLength = 1024, PoolSize = lightlessConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), SyncTimeout = options.SyncTimeout, }; services.AddStackExchangeRedisExtensions(redisConfiguration); } private void ConfigureIpRateLimiting(IServiceCollection services) { services.Configure(Configuration.GetSection("IpRateLimiting")); services.Configure(Configuration.GetSection("IpRateLimitPolicies")); services.AddSingleton(); services.AddMemoryCache(); services.AddInMemoryRateLimiting(); } private static void ConfigureAuthorization(IServiceCollection services) { services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddOptions(JwtBearerDefaults.AuthenticationScheme) .Configure>((options, config) => { options.TokenValidationParameters = new() { ValidateIssuer = false, ValidateLifetime = true, ValidateAudience = false, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue(nameof(LightlessConfigurationBase.Jwt)))), }; }); services.AddAuthentication(o => { o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(); services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser().Build(); options.AddPolicy("Authenticated", policy => { policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); policy.RequireAuthenticatedUser(); policy.AddRequirements(new ValidTokenRequirement()); }); options.AddPolicy("Identified", policy => { policy.AddRequirements(new UserRequirement(UserRequirements.Identified)); policy.AddRequirements(new ValidTokenRequirement()); }); options.AddPolicy("Admin", policy => { policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator)); policy.AddRequirements(new ValidTokenRequirement()); }); options.AddPolicy("Moderator", policy => { policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator)); policy.AddRequirements(new ValidTokenRequirement()); }); options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(LightlessClaimTypes.Internal, "true").Build()); }); } private void ConfigureDatabase(IServiceCollection services, IConfigurationSection lightlessConfig) { services.AddDbContextPool(options => { options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => { builder.MigrationsHistoryTable("_efmigrationshistory", "public"); builder.MigrationsAssembly("LightlessSyncShared"); }).UseSnakeCaseNamingConvention(); options.EnableThreadSafetyChecks(false); }, lightlessConfig.GetValue(nameof(LightlessConfigurationBase.DbContextPoolSize), 1024)); services.AddDbContextFactory(options => { options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"), builder => { builder.MigrationsHistoryTable("_efmigrationshistory", "public"); builder.MigrationsAssembly("LightlessSyncShared"); }).UseSnakeCaseNamingConvention(); options.EnableThreadSafetyChecks(false); }); } private static void ConfigureMetrics(IServiceCollection services) { services.AddSingleton(m => new LightlessMetrics(m.GetService>(), new List { MetricsAPI.CounterInitializedConnections, MetricsAPI.CounterUserPushData, MetricsAPI.CounterUserPushDataTo, MetricsAPI.CounterUsersRegisteredDeleted, MetricsAPI.CounterAuthenticationCacheHits, MetricsAPI.CounterAuthenticationFailures, MetricsAPI.CounterAuthenticationRequests, MetricsAPI.CounterAuthenticationSuccesses, MetricsAPI.CounterUserPairCacheHit, MetricsAPI.CounterUserPairCacheMiss, MetricsAPI.CounterUserPairCacheNewEntries, MetricsAPI.CounterUserPairCacheUpdatedEntries, }, [ MetricsAPI.GaugeAuthorizedConnections, MetricsAPI.GaugeLightFinderConnections, MetricsAPI.GaugeLightFinderGroups, MetricsAPI.GaugeGroupAutoPrunesEnabled, MetricsAPI.GaugeConnections, MetricsAPI.GaugePairs, MetricsAPI.GaugePairsPaused, MetricsAPI.GaugeAvailableIOWorkerThreads, MetricsAPI.GaugeAvailableWorkerThreads, MetricsAPI.GaugeGroups, MetricsAPI.GaugeGroupPairs, MetricsAPI.GaugeUsersRegistered, MetricsAPI.GaugeAuthenticationCacheEntries, MetricsAPI.GaugeUserPairCacheEntries, MetricsAPI.GaugeUserPairCacheUsers, MetricsAPI.GaugeGposeLobbies, MetricsAPI.GaugeGposeLobbyUsers, MetricsAPI.GaugeHubConcurrency, MetricsAPI.GaugeHubQueuedConcurrency, ])); } private static void ConfigureServicesBasedOnShardType(IServiceCollection services, IConfigurationSection lightlessConfig, bool isMainServer) { if (!isMainServer) { services.AddSingleton, LightlessConfigurationServiceClient>(); services.AddSingleton, LightlessConfigurationServiceClient>(); services.AddHostedService(p => (LightlessConfigurationServiceClient)p.GetService>()); services.AddHostedService(p => (LightlessConfigurationServiceClient)p.GetService>()); } else { services.AddSingleton, LightlessConfigurationServiceServer>(); services.AddSingleton, LightlessConfigurationServiceServer>(); } } public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) { logger.LogInformation("Running Configure"); var config = app.ApplicationServices.GetRequiredService>(); app.UseIpRateLimiting(); app.UseRouting(); app.UseWebSockets(); app.UseHttpMetrics(); var metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(LightlessConfigurationBase.MetricsPort), 4980)); metricServer.Start(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapHub(ILightlessHub.Path, options => { options.ApplicationMaxBufferSize = 5242880; options.TransportMaxBufferSize = 5242880; options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; }); endpoints.MapHealthChecks("/health").AllowAnonymous(); endpoints.MapControllers(); foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast()) { if (source == null) continue; _logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText); } }); } }