diff options
Diffstat (limited to 'src/SMAPI.Web/Startup.cs')
-rw-r--r-- | src/SMAPI.Web/Startup.cs | 220 |
1 files changed, 148 insertions, 72 deletions
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 56ef9a79..dee2edc2 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using Hangfire; using Hangfire.MemoryStorage; using Hangfire.Mongo; @@ -27,7 +28,7 @@ using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.Compression; using StardewModdingAPI.Web.Framework.ConfigModels; -using StardewModdingAPI.Web.Framework.RewriteRules; +using StardewModdingAPI.Web.Framework.RedirectRules; using StardewModdingAPI.Web.Framework.Storage; namespace StardewModdingAPI.Web @@ -47,7 +48,7 @@ namespace StardewModdingAPI.Web *********/ /// <summary>Construct an instance.</summary> /// <param name="env">The hosting environment.</param> - public Startup(IHostingEnvironment env) + public Startup(IWebHostEnvironment env) { this.Configuration = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) @@ -67,70 +68,91 @@ namespace StardewModdingAPI.Web .Configure<BackgroundServicesConfig>(this.Configuration.GetSection("BackgroundServices")) .Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList")) .Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck")) - .Configure<MongoDbConfig>(this.Configuration.GetSection("MongoDB")) + .Configure<StorageConfig>(this.Configuration.GetSection("Storage")) .Configure<SiteConfig>(this.Configuration.GetSection("Site")) .Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddLogging() - .AddMemoryCache() - .AddMvc() - .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) - .AddJsonOptions(options => - { - foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters) - options.SerializerSettings.Converters.Add(converter); + .AddMemoryCache(); + StorageConfig storageConfig = this.Configuration.GetSection("Storage").Get<StorageConfig>(); + StorageMode storageMode = storageConfig.Mode; - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); - MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get<MongoDbConfig>(); + // init MVC + services + .AddControllers() + .AddNewtonsoftJson(options => this.ConfigureJsonNet(options.SerializerSettings)) + .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())); + services + .AddRazorPages(); - // init background service + // init storage + switch (storageMode) { - BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get<BackgroundServicesConfig>(); - if (config.Enabled) - services.AddHostedService<BackgroundService>(); - } + case StorageMode.InMemory: + services.AddSingleton<IModCacheRepository>(new ModCacheMemoryRepository()); + services.AddSingleton<IWikiCacheRepository>(new WikiCacheMemoryRepository()); + break; - // init MongoDB - services.AddSingleton<MongoDbRunner>(serv => !mongoConfig.IsConfigured() - ? MongoDbRunner.Start() - : throw new InvalidOperationException("The MongoDB connection is configured, so the local development version should not be used.") - ); - services.AddSingleton<IMongoDatabase>(serv => - { - // get connection string - string connectionString = mongoConfig.IsConfigured() - ? mongoConfig.ConnectionString - : serv.GetRequiredService<MongoDbRunner>().ConnectionString; - - // get client - BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer()); - return new MongoClient(connectionString).GetDatabase(mongoConfig.Database); - }); - services.AddSingleton<IModCacheRepository>(serv => new ModCacheRepository(serv.GetRequiredService<IMongoDatabase>())); - services.AddSingleton<IWikiCacheRepository>(serv => new WikiCacheRepository(serv.GetRequiredService<IMongoDatabase>())); + case StorageMode.Mongo: + case StorageMode.MongoInMemory: + { + // local MongoDB instance + services.AddSingleton<MongoDbRunner>(_ => storageMode == StorageMode.MongoInMemory + ? MongoDbRunner.Start() + : throw new NotSupportedException($"The in-memory MongoDB runner isn't available in storage mode {storageMode}.") + ); + + // MongoDB + services.AddSingleton<IMongoDatabase>(serv => + { + BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer()); + return new MongoClient(this.GetMongoDbConnectionString(serv, storageConfig)) + .GetDatabase(storageConfig.Database); + }); + + // repositories + services.AddSingleton<IModCacheRepository>(serv => new ModCacheMongoRepository(serv.GetRequiredService<IMongoDatabase>())); + services.AddSingleton<IWikiCacheRepository>(serv => new WikiCacheMongoRepository(serv.GetRequiredService<IMongoDatabase>())); + } + break; + + default: + throw new NotSupportedException($"Unhandled storage mode '{storageMode}'."); + } // init Hangfire services - .AddHangfire(config => + .AddHangfire((serv, config) => { config .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings(); - if (mongoConfig.IsConfigured()) + switch (storageMode) { - config.UseMongoStorage(mongoConfig.ConnectionString, $"{mongoConfig.Database}-hangfire", new MongoStorageOptions - { - MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop), - CheckConnection = false // error on startup takes down entire process - }); + case StorageMode.InMemory: + config.UseMemoryStorage(); + break; + + case StorageMode.MongoInMemory: + case StorageMode.Mongo: + string connectionString = this.GetMongoDbConnectionString(serv, storageConfig); + config.UseMongoStorage(MongoClientSettings.FromConnectionString(connectionString), $"{storageConfig.Database}-hangfire", new MongoStorageOptions + { + MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop), + CheckConnection = false // error on startup takes down entire process + }); + break; } - else - config.UseMemoryStorage(); }); + // init background service + { + BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get<BackgroundServicesConfig>(); + if (config.Enabled) + services.AddHostedService<BackgroundService>(); + } + // init API clients { ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>(); @@ -142,6 +164,7 @@ namespace StardewModdingAPI.Web baseUrl: api.ChucklefishBaseUrl, modPageUrlFormat: api.ChucklefishModPageUrlFormat )); + services.AddSingleton<ICurseForgeClient>(new CurseForgeClient( userAgent: userAgent, apiUrl: api.CurseForgeBaseUrl @@ -188,8 +211,7 @@ namespace StardewModdingAPI.Web /// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary> /// <param name="app">The application builder.</param> - /// <param name="env">The hosting environment.</param> - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app) { // basic config app.UseDeveloperExceptionPage(); @@ -201,7 +223,13 @@ namespace StardewModdingAPI.Web ) .UseRewriter(this.GetRedirectRules()) .UseStaticFiles() // wwwroot folder - .UseMvc(); + .UseRouting() + .UseAuthorization() + .UseEndpoints(p => + { + p.MapControllers(); + p.MapRazorPages(); + }); // enable Hangfire dashboard app.UseHangfireDashboard("/tasks", new DashboardOptions @@ -215,29 +243,77 @@ namespace StardewModdingAPI.Web /********* ** Private methods *********/ + /// <summary>Configure a Json.NET serializer.</summary> + /// <param name="settings">The serializer settings to edit.</param> + private void ConfigureJsonNet(JsonSerializerSettings settings) + { + foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters) + settings.Converters.Add(converter); + + settings.Formatting = Formatting.Indented; + settings.NullValueHandling = NullValueHandling.Ignore; + } + + /// <summary>Get the MongoDB connection string for the given storage configuration.</summary> + /// <param name="services">The service provider.</param> + /// <param name="storageConfig">The storage configuration</param> + /// <exception cref="NotSupportedException">There's no MongoDB instance in the given storage mode.</exception> + private string GetMongoDbConnectionString(IServiceProvider services, StorageConfig storageConfig) + { + return storageConfig.Mode switch + { + StorageMode.Mongo => storageConfig.ConnectionString, + StorageMode.MongoInMemory => services.GetRequiredService<MongoDbRunner>().ConnectionString, + _ => throw new NotSupportedException($"There's no MongoDB instance in storage mode {storageConfig.Mode}.") + }; + } + /// <summary>Get the redirect rules to apply.</summary> private RewriteOptions GetRedirectRules() { - var redirects = new RewriteOptions(); - - // redirect to HTTPS (except API for Linux/Mac Mono compatibility) - redirects.Add(new ConditionalRedirectToHttpsRule( - shouldRewrite: req => - req.Host.Host != "localhost" - && !req.Path.StartsWithSegments("/api") - )); - - // shortcut redirects - redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0")); - redirects.Add(new RedirectToUrlRule(@"^/(?:buildmsg|package)(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1")); // buildmsg deprecated, remove when SDV 1.4 is released - redirects.Add(new RedirectToUrlRule(@"^/community\.?$", "https://stardewvalleywiki.com/Modding:Community")); - redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://smapi.io/mods")); - redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index")); - redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI")); - redirects.Add(new RedirectToUrlRule(@"^/troubleshoot(.*)$", "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1")); - redirects.Add(new RedirectToUrlRule(@"^/xnb\.?$", "https://stardewvalleywiki.com/Modding:Using_XNB_mods")); - - // redirect legacy canimod.com URLs + var redirects = new RewriteOptions() + // shortcut paths + .Add(new RedirectPathsToUrlsRule(new Dictionary<string, string> + { + [@"^/3\.0\.?$"] = "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0", + [@"^/(?:buildmsg|package)(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1", // buildmsg deprecated, remove when SDV 1.4 is released + [@"^/community\.?$"] = "https://stardewvalleywiki.com/Modding:Community", + [@"^/compat\.?$"] = "https://smapi.io/mods", + [@"^/docs\.?$"] = "https://stardewvalleywiki.com/Modding:Index", + [@"^/install\.?$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI", + [@"^/troubleshoot(.*)$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1", + [@"^/xnb\.?$"] = "https://stardewvalleywiki.com/Modding:Using_XNB_mods" + })) + + // legacy paths + .Add(new RedirectPathsToUrlsRule(this.GetLegacyPathRedirects())) + + // subdomains + .Add(new RedirectHostsToUrlsRule(HttpStatusCode.PermanentRedirect, host => host switch + { + "api.smapi.io" => "smapi.io/api", + "json.smapi.io" => "smapi.io/json", + "log.smapi.io" => "smapi.io/log", + "mods.smapi.io" => "smapi.io/mods", + _ => host.EndsWith(".smapi.io") + ? "smapi.io" + : null + })) + + // redirect to HTTPS (except API for Linux/Mac Mono compatibility) + .Add( + new RedirectToHttpsRule(except: req => req.Host.Host == "localhost" || req.Path.StartsWithSegments("/api")) + ); + + return redirects; + } + + /// <summary>Get the redirects for legacy paths that have been moved elsewhere.</summary> + private IDictionary<string, string> GetLegacyPathRedirects() + { + var redirects = new Dictionary<string, string>(); + + // canimod.com => wiki var wikiRedirects = new Dictionary<string, string[]> { ["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" }, @@ -251,10 +327,10 @@ namespace StardewModdingAPI.Web ["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" }, ["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" } }; - foreach (KeyValuePair<string, string[]> pair in wikiRedirects) + foreach ((string page, string[] patterns) in wikiRedirects) { - foreach (string pattern in pair.Value) - redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + pair.Key)); + foreach (string pattern in patterns) + redirects.Add(pattern, "https://stardewvalleywiki.com/" + page); } return redirects; |