using System; using System.Collections.Generic; using Hangfire; using Hangfire.MemoryStorage; using Hangfire.Mongo; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Mongo2Go; using MongoDB.Bson.Serialization; using MongoDB.Driver; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; 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.Storage; namespace StardewModdingAPI.Web { /// The web app startup configuration. internal class Startup { /********* ** Accessors *********/ /// The web app configuration. public IConfigurationRoot Configuration { get; } /********* ** Public methods *********/ /// Construct an instance. /// The hosting environment. public Startup(IHostingEnvironment env) { this.Configuration = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables() .Build(); } /// The method called by the runtime to add services to the container. /// The service injection container. public void ConfigureServices(IServiceCollection services) { // init basic services services .Configure(this.Configuration.GetSection("ApiClients")) .Configure(this.Configuration.GetSection("BackgroundServices")) .Configure(this.Configuration.GetSection("ModCompatibilityList")) .Configure(this.Configuration.GetSection("ModUpdateCheck")) .Configure(this.Configuration.GetSection("MongoDB")) .Configure(this.Configuration.GetSection("Site")) .Configure(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); options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); // init background service { BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get(); if (config.Enabled) services.AddHostedService(); } // init MongoDB services.AddSingleton(serv => !mongoConfig.IsConfigured() ? MongoDbRunner.Start() : throw new InvalidOperationException("The MongoDB connection is configured, so the local development version should not be used.") ); services.AddSingleton(serv => { // get connection string string connectionString = mongoConfig.IsConfigured() ? mongoConfig.ConnectionString : serv.GetRequiredService().ConnectionString; // get client BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer()); return new MongoClient(connectionString).GetDatabase(mongoConfig.Database); }); services.AddSingleton(serv => new ModCacheRepository(serv.GetRequiredService())); services.AddSingleton(serv => new WikiCacheRepository(serv.GetRequiredService())); // init Hangfire services .AddHangfire(config => { config .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings(); if (mongoConfig.IsConfigured()) { config.UseMongoStorage(mongoConfig.ConnectionString, $"{mongoConfig.Database}-hangfire", new MongoStorageOptions { MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop), CheckConnection = false // error on startup takes down entire process }); } else config.UseMemoryStorage(); }); // init API clients { ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get(); string version = this.GetType().Assembly.GetName().Version.ToString(3); string userAgent = string.Format(api.UserAgent, version); services.AddSingleton(new ChucklefishClient( userAgent: userAgent, baseUrl: api.ChucklefishBaseUrl, modPageUrlFormat: api.ChucklefishModPageUrlFormat )); services.AddSingleton(new CurseForgeClient( userAgent: userAgent, apiUrl: api.CurseForgeBaseUrl )); services.AddSingleton(new GitHubClient( baseUrl: api.GitHubBaseUrl, userAgent: userAgent, acceptHeader: api.GitHubAcceptHeader, username: api.GitHubUsername, password: api.GitHubPassword )); services.AddSingleton(new ModDropClient( userAgent: userAgent, apiUrl: api.ModDropApiUrl, modUrlFormat: api.ModDropModPageUrl )); services.AddSingleton(new NexusClient( webUserAgent: userAgent, webBaseUrl: api.NexusBaseUrl, webModUrlFormat: api.NexusModUrlFormat, webModScrapeUrlFormat: api.NexusModScrapeUrlFormat, apiAppVersion: version, apiKey: api.NexusApiKey )); services.AddSingleton(new PastebinClient( baseUrl: api.PastebinBaseUrl, userAgent: userAgent )); } // init helpers services .AddSingleton(new GzipHelper()) .AddSingleton(serv => new StorageProvider( serv.GetRequiredService>(), serv.GetRequiredService(), serv.GetRequiredService() )); } /// The method called by the runtime to configure the HTTP request pipeline. /// The application builder. /// The hosting environment. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // basic config app.UseDeveloperExceptionPage(); app .UseCors(policy => policy .AllowAnyHeader() .AllowAnyMethod() .WithOrigins("https://smapi.io") ) .UseRewriter(this.GetRedirectRules()) .UseStaticFiles() // wwwroot folder .UseMvc(); // enable Hangfire dashboard app.UseHangfireDashboard("/tasks", new DashboardOptions { IsReadOnlyFunc = context => !JobDashboardAuthorizationFilter.IsLocalRequest(context), Authorization = new[] { new JobDashboardAuthorizationFilter() } }); } /********* ** Private methods *********/ /// Get the redirect rules to apply. 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 wikiRedirects = new Dictionary { ["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" }, ["Modding:Modder_Guide"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod", "^/for-devs/creating-a-smapi-mod-advanced-config" }, ["Modding:Player_Guide"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods", "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" }, ["Modding:Editing_XNB_files"] = new[] { "^/for-devs/creating-an-xnb-mod", "^/guides/creating-an-xnb-mod" }, ["Modding:Event_data"] = new[] { "^/for-devs/events", "^/guides/events" }, ["Modding:Gift_taste_data"] = new[] { "^/for-devs/npc-gift-tastes", "^/guides/npc-gift-tastes" }, ["Modding:IDE_reference"] = new[] { "^/for-devs/creating-a-smapi-mod-ide-primer" }, ["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" }, ["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" } }; foreach (KeyValuePair pair in wikiRedirects) { foreach (string pattern in pair.Value) redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + pair.Key)); } return redirects; } } }