using Immense.RemoteControl.Server.Extensions; using Immense.SimpleMessenger; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.StaticFiles; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; using Remotely.Server.Auth; using Remotely.Server.Components; using Remotely.Server.Components.Account; using Remotely.Server.Data; using Remotely.Server.Extensions; using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Options; using Remotely.Server.Services; using Remotely.Server.Services.RcImplementations; using Remotely.Server.Services.Stores; using Remotely.Shared.Entities; using Remotely.Shared.Services; using Serilog; using System.Net; using RatePolicyNames = Remotely.Server.RateLimiting.PolicyNames; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; var services = builder.Services; configuration.AddEnvironmentVariables("Remotely_"); services.Configure( configuration.GetSection(ApplicationOptions.SectionKey)); var appOptions = configuration .GetSection(ApplicationOptions.SectionKey) .Get(); services .AddRazorComponents() .AddInteractiveServerComponents(); services.AddRazorPages(); services.AddCascadingAuthenticationState(); services.AddScoped(); services.AddScoped(); services.AddScoped(); var dbProvider = appOptions?.DbProvider?.ToLower(); switch (dbProvider) { case "sqlite": services.AddDbContext( contextLifetime: ServiceLifetime.Transient, optionsLifetime: ServiceLifetime.Transient); break; case "sqlserver": services.AddDbContext( contextLifetime: ServiceLifetime.Transient, optionsLifetime: ServiceLifetime.Transient); break; case "postgresql": services.AddDbContext( contextLifetime: ServiceLifetime.Transient, optionsLifetime: ServiceLifetime.Transient); break; default: throw new InvalidOperationException( $"Invalid DBProvider: {dbProvider}. Ensure a valid value " + $"is set in appsettings.json or environment variables."); } using AppDb appDb = dbProvider switch { "sqlite" => new SqliteDbContext(builder.Configuration, builder.Environment), "sqlserver" => new SqlServerDbContext(builder.Configuration, builder.Environment), "postgresql" => new PostgreSqlDbContext(builder.Configuration, builder.Environment), _ => throw new InvalidOperationException($"Invalid DBProvider: {dbProvider}") }; await appDb.Database.MigrateAsync(); var settings = await appDb.GetAppSettings(); ConfigureSerilog(builder, settings); builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); if (OperatingSystem.IsWindows() && settings.EnableWindowsEventLog) { builder.Logging.AddEventLog(); } builder.Services.AddAuthentication(options => { options.DefaultScheme = IdentityConstants.ApplicationScheme; options.DefaultSignInScheme = IdentityConstants.ExternalScheme; }) .AddIdentityCookies(); services.AddIdentityCore(options => { options.Stores.MaxLengthForKeys = 128; options.Password.RequireNonAlphanumeric = false; }) .AddEntityFrameworkStores() .AddSignInManager() .AddDefaultTokenProviders(); services.AddScoped(); services.AddScoped(); services.AddScoped(); builder.Services.AddSingleton, IdentityNoOpEmailSender>(); services.AddAuthorization(options => { options.AddPolicy(PolicyNames.TwoFactorRequired, builder => { builder.Requirements.Add(new TwoFactorRequiredRequirement()); }); options.AddPolicy(PolicyNames.OrganizationAdminRequired, builder => { builder.Requirements.Add(new OrganizationAdminRequirement()); }); options.AddPolicy(PolicyNames.ServerAdminRequired, builder => { builder.Requirements.Add(new ServerAdminRequirement()); }); }); services.AddDatabaseDeveloperPageExceptionFilter(); if (settings.UseHttpLogging) { services.AddHttpLogging(options => { options.RequestHeaders.Add("X-Forwarded-For"); options.RequestHeaders.Add("X-Forwarded-Proto"); options.RequestHeaders.Add("X-Forwarded-Host"); options.RequestHeaders.Add("X-Original-For"); options.RequestHeaders.Add("X-Original-Proto"); options.RequestHeaders.Add("X-Original-Host"); options.RequestHeaders.Add("Host"); }); } services.AddCors(options => { if (settings.TrustedCorsOrigins is { Count: > 0 } trustedOrigins) { options.AddPolicy("TrustedOriginPolicy", builder => builder .WithOrigins(trustedOrigins.ToArray()) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials() ); } }); services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.All; options.ForwardLimit = null; // Default Docker host. We want to allow forwarded headers from this address. if (IPAddress.TryParse(appOptions?.DockerGatewayIp, out var dockerGatewayIp)) { options.KnownProxies.Add(dockerGatewayIp); } if (settings.KnownProxies is { Count: > 0 } knownProxies) { foreach (var proxy in knownProxies) { if (IPAddress.TryParse(proxy, out var ip)) { options.KnownProxies.Add(ip); } } } }); services.AddSignalR(options => { options.EnableDetailedErrors = builder.Environment.IsDevelopment(); options.MaximumParallelInvocationsPerClient = 5; options.MaximumReceiveMessageSize = 100_000; }) .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; }) .AddMessagePackProtocol(); services.AddRateLimiter(options => { options.AddConcurrencyLimiter(RatePolicyNames.AgentUpdateDownloads, clOptions => { clOptions.QueueLimit = int.MaxValue; clOptions.PermitLimit = settings.MaxConcurrentUpdates <= 0 ? 10 : settings.MaxConcurrentUpdates; }); }); services.AddHttpClient(); services.AddLogging(); services.AddScoped(); if (builder.Environment.IsDevelopment()) { services.AddScoped(); } else { services.AddScoped, EmailSenderEx>(); services.AddScoped(); } services.AddSingleton(); services.AddTransient(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddHostedService(); services.AddHostedService(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(x => (CircuitHandler)x.GetRequiredService()); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(WeakReferenceMessenger.Default); services.AddRemoteControlServer(config => { config.AddHubEventHandler(); config.AddViewerAuthorizer(); config.AddViewerPageDataProvider(); config.AddViewerOptionsProvider(); config.AddSessionRecordingSink(); }); services.AddSingleton(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); app.UseForwardedHeaders(); app.UseRateLimiter(); if (settings.UseHttpLogging) { app.UseHttpLogging(); } if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseMigrationsEndPoint(); app.UseSwagger(); app.UseSwaggerUI(); } else { app.UseExceptionHandler("/Error"); if (settings.UseHsts) { app.UseHsts(); } if (settings.RedirectToHttps) { app.UseHttpsRedirection(); } } ConfigureStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseCors("TrustedOriginPolicy"); app.UseAntiforgery(); app.UseRemoteControlServer(); app.MapHub("/hubs/service"); app.MapControllers(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); app.MapAdditionalIdentityEndpoints(); using (var scope = app.Services.CreateScope()) { var dataService = scope.ServiceProvider.GetRequiredService(); await dataService.SetAllDevicesNotOnline(); await dataService.CleanupOldRecords(); } await app.RunAsync(); void ConfigureStaticFiles() { var provider = new FileExtensionContentTypeProvider(); // Add new mappings provider.Mappings[".ps1"] = "application/octet-stream"; provider.Mappings[".exe"] = "application/octet-stream"; provider.Mappings[".dll"] = "application/octet-stream"; provider.Mappings[".appimage"] = "application/octet-stream"; provider.Mappings[".zip"] = "application/octet-stream"; provider.Mappings[".config"] = "application/octet-stream"; app.UseStaticFiles(); var contentPath = Path.Combine(app.Environment.WebRootPath, "Content"); if (Directory.Exists(contentPath)) { app.UseStaticFiles(new StaticFileOptions() { FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.WebRootPath, "Content")), ServeUnknownFileTypes = true, RequestPath = new PathString("/Content"), ContentTypeProvider = provider, DefaultContentType = "application/octet-stream" }); } // Needed for Let's Encrypt. if (Directory.Exists(Path.Combine(app.Environment.ContentRootPath, ".well-known"))) { app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, @".well-known")), RequestPath = new PathString("/.well-known"), ServeUnknownFileTypes = true }); } } void ConfigureSerilog(WebApplicationBuilder webAppBuilder, SettingsModel settings) { try { var dataRetentionDays = settings.DataRetentionInDays; if (dataRetentionDays <= 0) { dataRetentionDays = 7; } var logPath = LogsManager.DefaultLogsDirectory; void ApplySharedLoggerConfig(LoggerConfiguration loggerConfiguration) { loggerConfiguration .Enrich.FromLogContext() .Enrich.WithThreadId() .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}") .WriteTo.File($"{logPath}/Remotely_Server.log", rollingInterval: RollingInterval.Day, retainedFileTimeLimit: TimeSpan.FromDays(dataRetentionDays), outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}", shared: true); } // https://github.com/serilog/serilog-aspnetcore#two-stage-initialization var loggerConfig = new LoggerConfiguration(); ApplySharedLoggerConfig(loggerConfig); Log.Logger = loggerConfig.CreateBootstrapLogger(); builder.Host.UseSerilog((context, services, configuration) => { configuration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services); ApplySharedLoggerConfig(configuration); }); } catch (Exception ex) { Console.WriteLine($"Failed to configure Serilog file logging. Error: {ex.Message}"); } }