using System.Collections.Generic; 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.Logging; using Newtonsoft.Json; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.RewriteRules; 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) .Add(new BeanstalkEnvPropsConfigProvider()) .Build(); } /// The method called by the runtime to add services to the container. /// The service injection container. public void ConfigureServices(IServiceCollection services) { // init configuration services .Configure(this.Configuration.GetSection("ModUpdateCheck")) .Configure(this.Configuration.GetSection("Context")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddMemoryCache() .AddMvc() .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) .AddJsonOptions(options => { options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); // 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 GitHubClient( baseUrl: api.GitHubBaseUrl, releaseUrlFormat: api.GitHubReleaseUrlFormat, userAgent: userAgent, acceptHeader: api.GitHubAcceptHeader, username: api.GitHubUsername, password: api.GitHubPassword )); services.AddSingleton(new NexusClient( userAgent: api.NexusUserAgent, baseUrl: api.NexusBaseUrl, modUrlFormat: api.NexusModUrlFormat )); services.AddSingleton(new PastebinClient( baseUrl: api.PastebinBaseUrl, userAgent: userAgent, userKey: api.PastebinUserKey, devKey: api.PastebinDevKey )); } } /// The method called by the runtime to configure the HTTP request pipeline. /// The application builder. /// The hosting environment. /// The logger factory. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); loggerFactory.AddDebug(); if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); app .UseCors(policy => policy .AllowAnyHeader() .AllowAnyMethod() .WithOrigins("https://smapi.io", "https://*.smapi.io", "https://*.edge.smapi.io") .SetIsOriginAllowedToAllowWildcardSubdomains() ) .UseRewriter(this.GetRedirectRules()) .UseStaticFiles() // wwwroot folder .UseMvc(); } /********* ** 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") && !req.Host.Host.StartsWith("api.") )); // convert subdomain.smapi.io => smapi.io/subdomain for routing redirects.Add(new ConditionalRewriteSubdomainRule( shouldRewrite: req => req.Host.Host != "localhost" && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log.")) && !req.Path.StartsWithSegments("/content") && !req.Path.StartsWithSegments("/favicon.ico") )); // shortcut redirects redirects.Add(new RedirectToUrlRule("^/docs$", "https://stardewvalleywiki.com/Modding:Index")); // redirect legacy canimod.com URLs var wikiRedirects = new Dictionary { ["Modding:Creating_a_SMAPI_mod"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod" }, ["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:Installing_SMAPI"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods" }, ["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" }, ["Modding:Player_FAQs"] = new[] { "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" }, ["Modding:SMAPI_APIs"] = new[] { "^/for-devs/creating-a-smapi-mod-advanced-config" }, ["Modding:Updating_deprecated_SMAPI_code"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" }, ["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; } } }