From ad5bb5b49af49c4668fd30fb2a0e606dcefe4ec0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 19:39:13 -0400 Subject: proxy Pastebin requests through our API instead of third parties, improve error-handling (#358) --- .../Framework/AllowLargePostsAttribute.cs | 52 ++++++++++ .../Framework/ConfigModels/LogParserConfig.cs | 18 ++++ .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 2 +- .../Framework/LogParser/GetPasteResponse.cs | 15 +++ .../Framework/LogParser/PastebinClient.cs | 110 +++++++++++++++++++++ .../Framework/LogParser/SavePasteResponse.cs | 15 +++ 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs create mode 100644 src/SMAPI.Web/Framework/LogParser/GetPasteResponse.cs create mode 100644 src/SMAPI.Web/Framework/LogParser/PastebinClient.cs create mode 100644 src/SMAPI.Web/Framework/LogParser/SavePasteResponse.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs new file mode 100644 index 00000000..68ead3c2 --- /dev/null +++ b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace StardewModdingAPI.Web.Framework +{ + /// A filter which increases the maximum request size for an endpoint. + /// Derived from . + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class AllowLargePostsAttribute : Attribute, IAuthorizationFilter, IOrderedFilter + { + /********* + ** Properties + *********/ + /// The underlying form options. + private readonly FormOptions FormOptions; + + + /********* + ** Accessors + *********/ + /// The attribute order. + public int Order { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public AllowLargePostsAttribute() + { + this.FormOptions = new FormOptions + { + ValueLengthLimit = 200 * 1024 * 1024 // 200MB + }; + } + + /// Called early in the filter pipeline to confirm request is authorized. + /// The authorisation filter context. + public void OnAuthorization(AuthorizationFilterContext context) + { + IFeatureCollection features = context.HttpContext.Features; + IFormFeature formFeature = features.Get(); + + if (formFeature?.Form == null) + { + // Request form has not been read yet, so set the limits + features.Set(new FormFeature(context.HttpContext.Request, this.FormOptions)); + } + } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs new file mode 100644 index 00000000..5cb0cf95 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for the log parser. + internal class LogParserConfig + { + /********* + ** Accessors + *********/ + /// The base URL for the Pastebin API. + public string PastebinBaseUrl { get; set; } + + /// The user agent for the Pastebin API client, where {0} is the SMAPI version. + public string PastebinUserAgent { get; set; } + + /// The developer key used to authenticate with the Pastebin API. + public string PastebinDevKey { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 03de639e..2fb5b97e 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -1,7 +1,7 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels { /// The config settings for mod update checks. - public class ModUpdateCheckConfig + internal class ModUpdateCheckConfig { /********* ** Accessors diff --git a/src/SMAPI.Web/Framework/LogParser/GetPasteResponse.cs b/src/SMAPI.Web/Framework/LogParser/GetPasteResponse.cs new file mode 100644 index 00000000..4f8794db --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParser/GetPasteResponse.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.LogParser +{ + /// The response for a get-paste request. + internal class GetPasteResponse + { + /// Whether the log was successfully fetched. + public bool Success { get; set; } + + /// The fetched paste content (if is true). + public string Content { get; set; } + + /// The error message (if saving failed). + public string Error { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs new file mode 100644 index 00000000..8536f249 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Web.Framework.LogParser +{ + /// An API client for Pastebin. + internal class PastebinClient : IDisposable + { + /********* + ** Properties + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + /// The developer key used to authenticate with the Pastebin API. + private readonly string DevKey; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base URL for the Pastebin API. + /// The user agent for the API client. + /// The developer key used to authenticate with the Pastebin API. + public PastebinClient(string baseUrl, string userAgent, string devKey) + { + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.DevKey = devKey; + } + + /// Fetch a saved paste. + /// The paste ID. + public async Task GetAsync(string id) + { + try + { + // get from API + string content = await this.Client + .GetAsync($"raw/{id}") + .AsString(); + + // handle Pastebin errors + if (string.IsNullOrWhiteSpace(content)) + return new GetPasteResponse { Error = "Received an empty response from Pastebin." }; + if (content.StartsWith(" PostAsync(string content) + { + try + { + // validate + if (string.IsNullOrWhiteSpace(content)) + return new SavePasteResponse { Error = "The log content can't be empty." }; + + // post to API + string response = await this.Client + .PostAsync("api/api_post.php") + .WithBodyContent(new FormUrlEncodedContent(new Dictionary + { + ["api_dev_key"] = "b8219d942109d1e60ebb14fbb45f06f9", + ["api_option"] = "paste", + ["api_paste_private"] = "1", + ["api_paste_code"] = content, + ["api_paste_expire_date"] = "1W" + })) + .AsString(); + + // handle Pastebin errors + if (string.IsNullOrWhiteSpace(response)) + return new SavePasteResponse { Error = "Received an empty response from Pastebin." }; + if (response.StartsWith("Bad API request")) + return new SavePasteResponse { Error = response }; + if (!response.Contains("/")) + return new SavePasteResponse { Error = $"Received an unknown response: {response}" }; + + // return paste ID + string pastebinID = response.Split("/").Last(); + return new SavePasteResponse { Success = true, ID = pastebinID }; + } + catch (Exception ex) + { + return new SavePasteResponse { Success = false, Error = ex.ToString() }; + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client.Dispose(); + } + } +} diff --git a/src/SMAPI.Web/Framework/LogParser/SavePasteResponse.cs b/src/SMAPI.Web/Framework/LogParser/SavePasteResponse.cs new file mode 100644 index 00000000..1c0960a4 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParser/SavePasteResponse.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.LogParser +{ + /// The response for a save-log request. + internal class SavePasteResponse + { + /// Whether the log was successfully saved. + public bool Success { get; set; } + + /// The saved paste ID (if is true). + public string ID { get; set; } + + /// The error message (if saving failed). + public string Error { get; set; } + } +} -- cgit From 3f43ebcc0e31db523fa82a163374cebf2f577cde Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Oct 2017 21:10:36 -0400 Subject: fix issues with subdomain routing in log UI (#358) --- src/SMAPI.Web/Controllers/LogParserController.cs | 12 ++++++---- .../Framework/ConfigModels/LogParserConfig.cs | 3 +++ src/SMAPI.Web/Framework/RewriteSubdomainRule.cs | 22 ++++++++++++++++-- src/SMAPI.Web/Startup.cs | 9 +++++++- src/SMAPI.Web/ViewModels/LogParserModel.cs | 26 ++++++++++++++++++++++ src/SMAPI.Web/Views/LogParser/Index.cshtml | 6 +++++ src/SMAPI.Web/appsettings.Development.json | 5 ++++- src/SMAPI.Web/appsettings.json | 1 + src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 9 ++++---- 9 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 src/SMAPI.Web/ViewModels/LogParserModel.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 893d9a52..ee1d51cd 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.LogParser; +using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers { @@ -13,6 +14,9 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Properties *********/ + /// The log parser config settings. + private readonly LogParserConfig Config; + /// The underlying Pastebin client. private readonly PastebinClient PastebinClient; @@ -28,10 +32,10 @@ namespace StardewModdingAPI.Web.Controllers public LogParserController(IOptions configProvider) { // init Pastebin client - LogParserConfig config = configProvider.Value; + this.Config = configProvider.Value; string version = this.GetType().Assembly.GetName().Version.ToString(3); - string userAgent = string.Format(config.PastebinUserAgent, version); - this.PastebinClient = new PastebinClient(config.PastebinBaseUrl, userAgent, config.PastebinDevKey); + string userAgent = string.Format(this.Config.PastebinUserAgent, version); + this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinDevKey); } /*** @@ -42,7 +46,7 @@ namespace StardewModdingAPI.Web.Controllers [Route("log")] public ViewResult Index() { - return this.View("Index"); + return this.View("Index", new LogParserModel(this.Config.SectionUrl)); } /*** diff --git a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs index 5cb0cf95..18d8ff05 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs @@ -6,6 +6,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /********* ** Accessors *********/ + /// The root URL for the log parser controller. + public string SectionUrl { get; set; } + /// The base URL for the Pastebin API. public string PastebinBaseUrl { get; set; } diff --git a/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs index 5a56844f..cc183fe3 100644 --- a/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs +++ b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs @@ -1,4 +1,7 @@ using System; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Rewrite; namespace StardewModdingAPI.Web.Framework @@ -7,14 +10,29 @@ namespace StardewModdingAPI.Web.Framework /// Derived from . internal class RewriteSubdomainRule : IRule { + /********* + ** Accessors + *********/ + /// The paths (excluding the hostname portion) to not rewrite. + public Regex[] ExceptPaths { get; set; } + + + /********* + ** Public methods + *********/ /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). /// The rewrite context. public void ApplyRule(RewriteContext context) { context.Result = RuleResult.ContinueRules; + HttpRequest request = context.HttpContext.Request; + + // check ignores + if (this.ExceptPaths?.Any(pattern => pattern.IsMatch(request.Path)) == true) + return; // get host parts - string host = context.HttpContext.Request.Host.Host; + string host = request.Host.Host; string[] parts = host.Split('.'); // validate @@ -24,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework return; // prepend to path - context.HttpContext.Request.Path = $"/{parts[0]}{context.HttpContext.Request.Path}"; + request.Path = $"/{parts[0]}{request.Path}"; } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index c0ea90da..e19593c7 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -1,3 +1,4 @@ +using System.Text.RegularExpressions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; @@ -64,7 +65,13 @@ namespace StardewModdingAPI.Web loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); loggerFactory.AddDebug(); app - .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing + .UseRewriter( + new RewriteOptions() + .Add(new RewriteSubdomainRule + { + ExceptPaths = new[] { new Regex("^/Content", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase) } + }) + ) // convert subdomain.smapi.io => smapi.io/subdomain for routing .UseStaticFiles() // wwwroot folder .UseMvc(); } diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs new file mode 100644 index 00000000..ba31db87 --- /dev/null +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -0,0 +1,26 @@ +namespace StardewModdingAPI.Web.ViewModels +{ + /// The view model for the log parser page. + public class LogParserModel + { + /********* + ** Accessors + *********/ + /// The root URL for the log parser controller. + public string SectionUrl { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public LogParserModel() { } + + /// Construct an instance. + /// The root URL for the log parser controller. + public LogParserModel(string sectionUrl) + { + this.SectionUrl = sectionUrl; + } + } +} diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 87a3962b..bd47f2ff 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -1,12 +1,18 @@ @{ ViewData["Title"] = "SMAPI log parser"; } +@model StardewModdingAPI.Web.ViewModels.LogParserModel @section Head { + } @********* diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index fa8ce71a..e49eabb7 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -1,4 +1,4 @@ -{ +{ "Logging": { "IncludeScopes": false, "LogLevel": { @@ -6,5 +6,8 @@ "System": "Information", "Microsoft": "Information" } + }, + "LogParser": { + "SectionUrl": "http://localhost:59482/log/" } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index ca1299ce..1b5c35cd 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -28,6 +28,7 @@ "NexusModUrlFormat": "mods/{0}" }, "LogParser": { + "SectionUrl": "https://log.smapi.io/", "PastebinBaseUrl": "https://pastebin.com/", "PastebinUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", "PastebinDevKey": "b8219d942109d1e60ebb14fbb45f06f9" diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index b1f8f5c6..3949fabe 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -1,6 +1,7 @@ /* globals $, LZString */ -$(function() { +var smapi = smapi || {}; +smapi.logParser = function(sectionUrl) { /********* ** Initialisation *********/ @@ -75,7 +76,7 @@ $(function() { $ .ajax({ type: "POST", - url: "/log/save", + url: sectionUrl + "/save", data: JSON.stringify(paste), contentType: "application/json" // sent to API }) @@ -273,7 +274,7 @@ $(function() { function getData() { $("#uploader").attr("data-text", "Loading..."); $("#uploader").fadeIn(); - $.get("/log/fetch/" + location.search.substring(1), function(data) { + $.get(sectionUrl + "/fetch/" + location.search.substring(1), function(data) { if (data.success) { $("#input").val(LZString.decompressFromUTF16(data.content) || data.content); loadData(); @@ -284,4 +285,4 @@ $(function() { $("#uploader").fadeOut(); }); } -}); +}; -- cgit From c6d8333c7a28b752397e171540306ceccf74ca12 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Oct 2017 11:53:54 -0400 Subject: improve criteria for subdomain rewriting (#358) --- .../ConditionalRewriteSubdomainRule.cs | 48 ++++++++++++++++++++++ src/SMAPI.Web/Framework/RewriteSubdomainRule.cs | 48 ---------------------- src/SMAPI.Web/Startup.cs | 18 ++++---- 3 files changed, 58 insertions(+), 56 deletions(-) create mode 100644 src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs delete mode 100644 src/SMAPI.Web/Framework/RewriteSubdomainRule.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs new file mode 100644 index 00000000..83f23058 --- /dev/null +++ b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework.RewriteRules +{ + /// Rewrite requests to prepend the subdomain portion (if any) to the path. + /// Derived from . + internal class ConditionalRewriteSubdomainRule : IRule + { + /********* + ** Accessors + *********/ + /// A predicate which indicates when the rule should be applied. + private readonly Func ShouldRewrite; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A predicate which indicates when the rule should be applied. + public ConditionalRewriteSubdomainRule(Func shouldRewrite = null) + { + this.ShouldRewrite = shouldRewrite; + } + + /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). + /// The rewrite context. + public void ApplyRule(RewriteContext context) + { + HttpRequest request = context.HttpContext.Request; + + // check condition + if (this.ShouldRewrite != null && !this.ShouldRewrite(request)) + return; + + // get host parts + string host = request.Host.Host; + string[] parts = host.Split('.'); + if (parts.Length < 2) + return; + + // prepend to path + request.Path = $"/{parts[0]}{request.Path}"; + } + } +} diff --git a/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs deleted file mode 100644 index cc183fe3..00000000 --- a/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Rewrite; - -namespace StardewModdingAPI.Web.Framework -{ - /// Rewrite requests to prepend the subdomain portion (if any) to the path. - /// Derived from . - internal class RewriteSubdomainRule : IRule - { - /********* - ** Accessors - *********/ - /// The paths (excluding the hostname portion) to not rewrite. - public Regex[] ExceptPaths { get; set; } - - - /********* - ** Public methods - *********/ - /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). - /// The rewrite context. - public void ApplyRule(RewriteContext context) - { - context.Result = RuleResult.ContinueRules; - HttpRequest request = context.HttpContext.Request; - - // check ignores - if (this.ExceptPaths?.Any(pattern => pattern.IsMatch(request.Path)) == true) - return; - - // get host parts - string host = request.Host.Host; - string[] parts = host.Split('.'); - - // validate - if (parts.Length < 2) - return; - if (parts.Length < 3 && !"localhost".Equals(parts[1], StringComparison.InvariantCultureIgnoreCase)) - return; - - // prepend to path - request.Path = $"/{parts[0]}{request.Path}"; - } - } -} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index e19593c7..0f656e55 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; @@ -9,6 +8,7 @@ using Microsoft.Extensions.Logging; using Newtonsoft.Json; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.Framework.RewriteRules; namespace StardewModdingAPI.Web { @@ -65,13 +65,15 @@ namespace StardewModdingAPI.Web loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); loggerFactory.AddDebug(); app - .UseRewriter( - new RewriteOptions() - .Add(new RewriteSubdomainRule - { - ExceptPaths = new[] { new Regex("^/Content", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase) } - }) - ) // convert subdomain.smapi.io => smapi.io/subdomain for routing + .UseRewriter(new RewriteOptions() + // convert subdomain.smapi.io => smapi.io/subdomain for routing + .Add(new ConditionalRewriteSubdomainRule( + shouldRewrite: req => + req.Host.Host != "localhost" + && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log.")) + && !req.Path.StartsWithSegments("/content") + )) + ) .UseStaticFiles() // wwwroot folder .UseMvc(); } -- cgit From d545281ef3d83d4db43d5ca56eb59800c8a1b8d2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Oct 2017 12:24:50 -0400 Subject: redirect web views to HTTPS (#358) --- .../RewriteRules/ConditionalRedirectToHttpsRule.cs | 62 ++++++++++++++++++++++ .../ConditionalRewriteSubdomainRule.cs | 4 +- src/SMAPI.Web/Startup.cs | 7 +++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs new file mode 100644 index 00000000..d6a56bb7 --- /dev/null +++ b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs @@ -0,0 +1,62 @@ +using System; +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework.RewriteRules +{ + /// Redirect requests to HTTPS. + /// Derived from and . + internal class ConditionalRedirectToHttpsRule : IRule + { + /********* + ** Properties + *********/ + /// A predicate which indicates when the rule should be applied. + private readonly Func ShouldRewrite; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A predicate which indicates when the rule should be applied. + public ConditionalRedirectToHttpsRule(Func shouldRewrite = null) + { + this.ShouldRewrite = shouldRewrite ?? (req => true); + } + + /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). + /// The rewrite context. + public void ApplyRule(RewriteContext context) + { + HttpRequest request = context.HttpContext.Request; + + // check condition + if (this.IsSecure(request) || !this.ShouldRewrite(request)) + return; + + // redirect request + HttpResponse response = context.HttpContext.Response; + response.StatusCode = (int)HttpStatusCode.RedirectKeepVerb; + response.Headers["Location"] = new StringBuilder() + .Append("https://") + .Append(request.Host.Host) + .Append(request.PathBase) + .Append(request.Path) + .Append(request.QueryString) + .ToString(); + context.Result = RuleResult.EndResponse; + } + + /// Get whether the request was received over HTTPS. + /// The request to check. + public bool IsSecure(HttpRequest request) + { + return + request.IsHttps // HTTPS to server + || string.Equals(request.Headers["x-forwarded-proto"], "HTTPS", StringComparison.OrdinalIgnoreCase); // HTTPS to AWS load balancer + } + } +} diff --git a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs index 83f23058..920632ab 100644 --- a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs +++ b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Web.Framework.RewriteRules /// A predicate which indicates when the rule should be applied. public ConditionalRewriteSubdomainRule(Func shouldRewrite = null) { - this.ShouldRewrite = shouldRewrite; + this.ShouldRewrite = shouldRewrite ?? (req => true); } /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Web.Framework.RewriteRules HttpRequest request = context.HttpContext.Request; // check condition - if (this.ShouldRewrite != null && !this.ShouldRewrite(request)) + if (!this.ShouldRewrite(request)) return; // get host parts diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 0f656e55..b27ff9a5 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -66,6 +66,13 @@ namespace StardewModdingAPI.Web loggerFactory.AddDebug(); app .UseRewriter(new RewriteOptions() + // redirect to HTTPS (except API for Linux/Mac Mono compatibility) + .Add(new ConditionalRedirectToHttpsRule( + shouldRewrite: req => + req.Host.Host != "localhost" + && !req.Path.StartsWithSegments("/api") + )) + // convert subdomain.smapi.io => smapi.io/subdomain for routing .Add(new ConditionalRewriteSubdomainRule( shouldRewrite: req => -- cgit From f895fedc6aa12742842c97e536b5bf2e5ca3553c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Oct 2017 14:03:53 -0400 Subject: move credentials into git-ignored file (#358) --- .gitignore | 3 +++ src/SMAPI.Web/Framework/LogParser/PastebinClient.cs | 2 +- src/SMAPI.Web/appsettings.Development.json | 16 +++++++++++++++- src/SMAPI.Web/appsettings.json | 16 ++++++++++++---- 4 files changed, 31 insertions(+), 6 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/.gitignore b/.gitignore index f2d50778..7e0c1e9d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ _ReSharper*/ **/packages/* *.nuget.props *.nuget.targets + +# sensitive files +appsettings.Development.json diff --git a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs index 8536f249..e45a9eed 100644 --- a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs @@ -75,7 +75,7 @@ namespace StardewModdingAPI.Web.Framework.LogParser .PostAsync("api/api_post.php") .WithBodyContent(new FormUrlEncodedContent(new Dictionary { - ["api_dev_key"] = "b8219d942109d1e60ebb14fbb45f06f9", + ["api_dev_key"] = this.DevKey, ["api_option"] = "paste", ["api_paste_private"] = "1", ["api_paste_code"] = content, diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index e49eabb7..1080ee00 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -1,3 +1,12 @@ +/* + + + This file is committed to source control with the default settings, but added to .gitignore to + avoid accidentally committing login details. + + + +*/ { "Logging": { "IncludeScopes": false, @@ -7,7 +16,12 @@ "Microsoft": "Information" } }, + "ModUpdateCheck": { + "GitHubUsername": null, + "GitHubPassword": null + }, "LogParser": { - "SectionUrl": "http://localhost:59482/log/" + "SectionUrl": "http://localhost:59482/log/", + "PastebinDevKey": null } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index f99748d6..397765eb 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -1,3 +1,11 @@ +/* + + + This contains the default settings for the web app. Login credentials and contextual settings are + configured via appsettings.Development.json locally, or environment properties in AWS. + + +*/ { "Logging": { "IncludeScopes": false, @@ -19,8 +27,8 @@ "GitHubBaseUrl": "https://api.github.com", "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", "GitHubAcceptHeader": "application/vnd.github.v3+json", - "GitHubUsername": null, /* set via environment properties */ - "GitHubPassword": null, /* set via environment properties */ + "GitHubUsername": null, // see top note + "GitHubPassword": null, // see top note "NexusKey": "Nexus", "NexusUserAgent": "Nexus Client v0.63.15", @@ -28,9 +36,9 @@ "NexusModUrlFormat": "mods/{0}" }, "LogParser": { - "SectionUrl": null, /* set via environment properties */ + "SectionUrl": null, // see top note "PastebinBaseUrl": "https://pastebin.com/", "PastebinUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", - "PastebinDevKey": "b8219d942109d1e60ebb14fbb45f06f9" + "PastebinDevKey": null // see top note } } -- cgit From 790a62920b15f1f948724f5b2a70a937829355dd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 28 Oct 2017 14:05:29 -0400 Subject: link pastes to Pastebin account & tweak paste options (#358) --- src/SMAPI.Web/Controllers/LogParserController.cs | 2 +- src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs | 3 +++ src/SMAPI.Web/Framework/LogParser/PastebinClient.cs | 17 ++++++++++++----- src/SMAPI.Web/appsettings.Development.json | 1 + src/SMAPI.Web/appsettings.json | 1 + 5 files changed, 18 insertions(+), 6 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index f9943707..8e986196 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI.Web.Controllers this.Config = configProvider.Value; string version = this.GetType().Assembly.GetName().Version.ToString(3); string userAgent = string.Format(this.Config.PastebinUserAgent, version); - this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinDevKey); + this.PastebinClient = new PastebinClient(this.Config.PastebinBaseUrl, userAgent, this.Config.PastebinUserKey, this.Config.PastebinDevKey); } /*** diff --git a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs index 18d8ff05..df5d605d 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs @@ -15,6 +15,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The user agent for the Pastebin API client, where {0} is the SMAPI version. public string PastebinUserAgent { get; set; } + /// The user key used to authenticate with the Pastebin API. + public string PastebinUserKey { get; set; } + /// The developer key used to authenticate with the Pastebin API. public string PastebinDevKey { get; set; } } diff --git a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs index e45a9eed..738330d3 100644 --- a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Web.Framework.LogParser /// The underlying HTTP client. private readonly IClient Client; + /// The user key used to authenticate with the Pastebin API. + private readonly string UserKey; + /// The developer key used to authenticate with the Pastebin API. private readonly string DevKey; @@ -27,10 +30,12 @@ namespace StardewModdingAPI.Web.Framework.LogParser /// Construct an instance. /// The base URL for the Pastebin API. /// The user agent for the API client. + /// The user key used to authenticate with the Pastebin API. /// The developer key used to authenticate with the Pastebin API. - public PastebinClient(string baseUrl, string userAgent, string devKey) + public PastebinClient(string baseUrl, string userAgent, string userKey, string devKey) { this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.UserKey = userKey; this.DevKey = devKey; } @@ -75,11 +80,13 @@ namespace StardewModdingAPI.Web.Framework.LogParser .PostAsync("api/api_post.php") .WithBodyContent(new FormUrlEncodedContent(new Dictionary { - ["api_dev_key"] = this.DevKey, ["api_option"] = "paste", - ["api_paste_private"] = "1", - ["api_paste_code"] = content, - ["api_paste_expire_date"] = "1W" + ["api_user_key"] = this.UserKey, + ["api_dev_key"] = this.DevKey, + ["api_paste_private"] = "1", // unlisted + ["api_paste_name"] = $"SMAPI log {DateTime.UtcNow:s}", + ["api_paste_expire_date"] = "1W", // one week + ["api_paste_code"] = content })) .AsString(); diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 1080ee00..87c35ca9 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -22,6 +22,7 @@ }, "LogParser": { "SectionUrl": "http://localhost:59482/log/", + "PastebinUserKey": null, "PastebinDevKey": null } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 397765eb..eb6ecc9b 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -39,6 +39,7 @@ "SectionUrl": null, // see top note "PastebinBaseUrl": "https://pastebin.com/", "PastebinUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", + "PastebinUserKey": null, // see top note "PastebinDevKey": null // see top note } } -- cgit From 6638701d0221e82daa42581eddb3cf051d1f8de4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Oct 2017 13:15:05 -0400 Subject: fix config not being injected from Amazon Beanstalk env props --- .../Framework/BeanstalkEnvPropsConfigProvider.cs | 54 ++++++++++++++++++++++ src/SMAPI.Web/Startup.cs | 3 +- 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI.Web/Framework/BeanstalkEnvPropsConfigProvider.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/BeanstalkEnvPropsConfigProvider.cs b/src/SMAPI.Web/Framework/BeanstalkEnvPropsConfigProvider.cs new file mode 100644 index 00000000..b39a3b61 --- /dev/null +++ b/src/SMAPI.Web/Framework/BeanstalkEnvPropsConfigProvider.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Web.Framework +{ + /// Reads configuration values from the AWS Beanstalk environment properties file (if present). + /// This is a workaround for AWS Beanstalk injection not working with .NET Core apps. + internal class BeanstalkEnvPropsConfigProvider : ConfigurationProvider, IConfigurationSource + { + /********* + ** Properties + *********/ + /// The absolute path to the container configuration file on an Amazon EC2 instance. + private const string ContainerConfigPath = @"C:\Program Files\Amazon\ElasticBeanstalk\config\containerconfiguration"; + + + /********* + ** Public methods + *********/ + /// Build the configuration provider for this source. + /// The configuration builder. + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new BeanstalkEnvPropsConfigProvider(); + } + + /// Load the environment properties. + public override void Load() + { + this.Data = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // get Beanstalk config file + FileInfo file = new FileInfo(BeanstalkEnvPropsConfigProvider.ContainerConfigPath); + if (!file.Exists) + return; + + // parse JSON + JObject jsonRoot = (JObject)JsonConvert.DeserializeObject(File.ReadAllText(file.FullName)); + if (jsonRoot["iis"]?["env"] is JArray jsonProps) + { + foreach (string prop in jsonProps.Values()) + { + string[] parts = prop.Split('=', 2); // key=value + if (parts.Length == 2) + this.Data[parts[0]] = parts[1]; + } + } + } + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index b27ff9a5..860354f1 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -31,10 +31,9 @@ namespace StardewModdingAPI.Web { this.Configuration = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) - .AddEnvironmentVariables() .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables() + .Add(new BeanstalkEnvPropsConfigProvider()) //.AddEnvironmentVariables() .Build(); } -- cgit From 13baaf8920f4a80ac3c0cd41a16b9afb1b993048 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Oct 2017 22:18:08 -0400 Subject: add smapi.io shortcut URLs (#375) --- .../Framework/RewriteRules/RedirectToUrlRule.cs | 61 ++++++++++++++++++++++ src/SMAPI.Web/Startup.cs | 4 ++ 2 files changed, 65 insertions(+) create mode 100644 src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs b/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs new file mode 100644 index 00000000..0719e311 --- /dev/null +++ b/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs @@ -0,0 +1,61 @@ +using System; +using System.Net; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework.RewriteRules +{ + /// Redirect requests to an external URL if they match a condition. + internal class RedirectToUrlRule : IRule + { + /********* + ** Properties + *********/ + /// A predicate which indicates when the rule should be applied. + private readonly Func ShouldRewrite; + + /// The new URL to which to redirect. + private readonly string NewUrl; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A predicate which indicates when the rule should be applied. + /// The new URL to which to redirect. + public RedirectToUrlRule(Func shouldRewrite, string url) + { + this.ShouldRewrite = shouldRewrite ?? (req => true); + this.NewUrl = url; + } + + /// Construct an instance. + /// A case-insensitive regex to match against the path. + /// The external URL. + public RedirectToUrlRule(string pathRegex, string url) + { + Regex regex = new Regex(pathRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled); + this.ShouldRewrite = req => req.Path.HasValue && regex.IsMatch(req.Path.Value); + this.NewUrl = url; + } + + /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). + /// The rewrite context. + public void ApplyRule(RewriteContext context) + { + HttpRequest request = context.HttpContext.Request; + + // check condition + if (!this.ShouldRewrite(request)) + return; + + // redirect request + HttpResponse response = context.HttpContext.Response; + response.StatusCode = (int)HttpStatusCode.Redirect; + response.Headers["Location"] = this.NewUrl; + context.Result = RuleResult.EndResponse; + } + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 860354f1..bc491128 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -79,6 +79,10 @@ namespace StardewModdingAPI.Web && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log.")) && !req.Path.StartsWithSegments("/content") )) + + // shortcut redirects + .Add(new RedirectToUrlRule("^/docs$", "https://stardewvalleywiki.com/Modding:Index")) + .Add(new RedirectToUrlRule("^/install$", "https://stardewvalleywiki.com/Modding:Installing_SMAPI")) ) .UseStaticFiles() // wwwroot folder .UseMvc(); -- cgit