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 { /// <summary>The web app startup configuration.</summary> internal class Startup { /********* ** Accessors *********/ /// <summary>The web app configuration.</summary> public IConfigurationRoot Configuration { get; } /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="env">The hosting environment.</param> 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(); } /// <summary>The method called by the runtime to add services to the container.</summary> /// <param name="services">The service injection container.</param> public void ConfigureServices(IServiceCollection services) { // init basic services services .Configure<ApiClientsConfig>(this.Configuration.GetSection("ApiClients")) .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<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); options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get<MongoDbConfig>(); // init background service { BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get<BackgroundServicesConfig>(); if (config.Enabled) services.AddHostedService<BackgroundService>(); } // 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>())); // 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<ApiClientsConfig>(); string version = this.GetType().Assembly.GetName().Version.ToString(3); string userAgent = string.Format(api.UserAgent, version); services.AddSingleton<IChucklefishClient>(new ChucklefishClient( userAgent: userAgent, baseUrl: api.ChucklefishBaseUrl, modPageUrlFormat: api.ChucklefishModPageUrlFormat )); services.AddSingleton<ICurseForgeClient>(new CurseForgeClient( userAgent: userAgent, apiUrl: api.CurseForgeBaseUrl )); services.AddSingleton<IGitHubClient>(new GitHubClient( baseUrl: api.GitHubBaseUrl, userAgent: userAgent, acceptHeader: api.GitHubAcceptHeader, username: api.GitHubUsername, password: api.GitHubPassword )); services.AddSingleton<IModDropClient>(new ModDropClient( userAgent: userAgent, apiUrl: api.ModDropApiUrl, modUrlFormat: api.ModDropModPageUrl )); services.AddSingleton<INexusClient>(new NexusClient( webUserAgent: userAgent, webBaseUrl: api.NexusBaseUrl, webModUrlFormat: api.NexusModUrlFormat, webModScrapeUrlFormat: api.NexusModScrapeUrlFormat, apiAppVersion: version, apiKey: api.NexusApiKey )); services.AddSingleton<IPastebinClient>(new PastebinClient( baseUrl: api.PastebinBaseUrl, userAgent: userAgent )); } // init helpers services .AddSingleton<IGzipHelper>(new GzipHelper()) .AddSingleton<IStorageProvider>(serv => new StorageProvider( serv.GetRequiredService<IOptions<ApiClientsConfig>>(), serv.GetRequiredService<IPastebinClient>(), serv.GetRequiredService<IGzipHelper>() )); } /// <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) { // 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 *********/ /// <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 wikiRedirects = new Dictionary<string, string[]> { ["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<string, string[]> pair in wikiRedirects) { foreach (string pattern in pair.Value) redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + pair.Key)); } return redirects; } } }