diff options
| author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-11-01 17:42:18 -0400 |
|---|---|---|
| committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-11-01 17:42:18 -0400 |
| commit | e0b72374cd14298aacc6f71dc391fdc9814be37c (patch) | |
| tree | 2e5d85937c34539c1a0df48423b5136508693ca8 /src/SMAPI.Web | |
| parent | 79118316065a01322d8ea12a14589ec016794c32 (diff) | |
| parent | 089e6de749ae7cb109af00164d2597c6644c255e (diff) | |
| download | SMAPI-e0b72374cd14298aacc6f71dc391fdc9814be37c.tar.gz SMAPI-e0b72374cd14298aacc6f71dc391fdc9814be37c.tar.bz2 SMAPI-e0b72374cd14298aacc6f71dc391fdc9814be37c.zip | |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI.Web')
26 files changed, 1593 insertions, 50 deletions
diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs new file mode 100644 index 00000000..454440bb --- /dev/null +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -0,0 +1,158 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +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 +{ + /// <summary>Provides a web UI and API for parsing SMAPI log files.</summary> + internal class LogParserController : Controller + { + /********* + ** Properties + *********/ + /// <summary>The log parser config settings.</summary> + private readonly LogParserConfig Config; + + /// <summary>The underlying Pastebin client.</summary> + private readonly PastebinClient PastebinClient; + + /// <summary>The first bytes in a valid zip file.</summary> + /// <remarks>See <a href="https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers"/>.</remarks> + private const uint GzipLeadBytes = 0x8b1f; + + + /********* + ** Public methods + *********/ + /*** + ** Constructor + ***/ + /// <summary>Construct an instance.</summary> + /// <param name="configProvider">The log parser config settings.</param> + public LogParserController(IOptions<LogParserConfig> configProvider) + { + // init Pastebin client + 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.PastebinUserKey, this.Config.PastebinDevKey); + } + + /*** + ** Web UI + ***/ + /// <summary>Render the log parser UI.</summary> + /// <param name="id">The paste ID.</param> + [HttpGet] + [Route("")] + [Route("log")] + [Route("log/{id}")] + public ViewResult Index(string id = null) + { + return this.View("Index", new LogParserModel(this.Config.SectionUrl, id)); + } + + /*** + ** JSON + ***/ + /// <summary>Fetch raw text from Pastebin.</summary> + /// <param name="id">The Pastebin paste ID.</param> + [HttpGet, Produces("application/json")] + [Route("log/fetch/{id}")] + public async Task<GetPasteResponse> GetAsync(string id) + { + GetPasteResponse response = await this.PastebinClient.GetAsync(id); + response.Content = this.DecompressString(response.Content); + return response; + } + + /// <summary>Save raw log data.</summary> + /// <param name="content">The log content to save.</param> + [HttpPost, Produces("application/json"), AllowLargePosts] + [Route("log/save")] + public async Task<SavePasteResponse> PostAsync([FromBody] string content) + { + content = this.CompressString(content); + return await this.PastebinClient.PostAsync(content); + } + + + /********* + ** Private methods + *********/ + /// <summary>Compress a string.</summary> + /// <param name="text">The text to compress.</param> + /// <remarks>Derived from <a href="https://stackoverflow.com/a/17993002/262123"/>.</remarks> + private string CompressString(string text) + { + // get raw bytes + byte[] buffer = Encoding.UTF8.GetBytes(text); + + // compressed + byte[] compressedData; + using (MemoryStream stream = new MemoryStream()) + { + using (GZipStream zipStream = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true)) + zipStream.Write(buffer, 0, buffer.Length); + + stream.Position = 0; + compressedData = new byte[stream.Length]; + stream.Read(compressedData, 0, compressedData.Length); + } + + // prefix length + var zipBuffer = new byte[compressedData.Length + 4]; + Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length); + Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4); + + // return string representation + return Convert.ToBase64String(zipBuffer); + } + + /// <summary>Decompress a string.</summary> + /// <param name="rawText">The compressed text.</param> + /// <remarks>Derived from <a href="https://stackoverflow.com/a/17993002/262123"/>.</remarks> + private string DecompressString(string rawText) + { + // get raw bytes + byte[] zipBuffer; + try + { + zipBuffer = Convert.FromBase64String(rawText); + } + catch + { + return rawText; // not valid base64, wasn't compressed by the log parser + } + + // skip if not gzip + if (BitConverter.ToUInt16(zipBuffer, 4) != LogParserController.GzipLeadBytes) + return rawText; + + // decompress + using (MemoryStream memoryStream = new MemoryStream()) + { + // read length prefix + int dataLength = BitConverter.ToInt32(zipBuffer, 0); + memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4); + + // read data + var buffer = new byte[dataLength]; + memoryStream.Position = 0; + using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + gZipStream.Read(buffer, 0, buffer.Length); + + // return original string + return Encoding.UTF8.GetString(buffer); + } + } + } +} diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index a671ddca..a600662c 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -14,8 +14,8 @@ namespace StardewModdingAPI.Web.Controllers { /// <summary>Provides an API to perform mod update checks.</summary> [Produces("application/json")] - [Route("api/{version:semanticVersion}/[controller]")] - internal class ModsController : Controller + [Route("api/v{version:semanticVersion}/mods")] + internal class ModsApiController : Controller { /********* ** Properties @@ -39,7 +39,7 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>Construct an instance.</summary> /// <param name="cache">The cache in which to store mod metadata.</param> /// <param name="configProvider">The config settings for mod update checks.</param> - public ModsController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider) + public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider) { ModUpdateCheckConfig config = configProvider.Value; 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 +{ + /// <summary>A filter which increases the maximum request size for an endpoint.</summary> + /// <remarks>Derived from <a href="https://stackoverflow.com/a/38360093/262123"/>.</remarks> + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class AllowLargePostsAttribute : Attribute, IAuthorizationFilter, IOrderedFilter + { + /********* + ** Properties + *********/ + /// <summary>The underlying form options.</summary> + private readonly FormOptions FormOptions; + + + /********* + ** Accessors + *********/ + /// <summary>The attribute order.</summary> + public int Order { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public AllowLargePostsAttribute() + { + this.FormOptions = new FormOptions + { + ValueLengthLimit = 200 * 1024 * 1024 // 200MB + }; + } + + /// <summary>Called early in the filter pipeline to confirm request is authorized.</summary> + /// <param name="context">The authorisation filter context.</param> + public void OnAuthorization(AuthorizationFilterContext context) + { + IFeatureCollection features = context.HttpContext.Features; + IFormFeature formFeature = features.Get<IFormFeature>(); + + if (formFeature?.Form == null) + { + // Request form has not been read yet, so set the limits + features.Set<IFormFeature>(new FormFeature(context.HttpContext.Request, this.FormOptions)); + } + } + } +} 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 +{ + /// <summary>Reads configuration values from the AWS Beanstalk environment properties file (if present).</summary> + /// <remarks>This is a workaround for AWS Beanstalk injection not working with .NET Core apps.</remarks> + internal class BeanstalkEnvPropsConfigProvider : ConfigurationProvider, IConfigurationSource + { + /********* + ** Properties + *********/ + /// <summary>The absolute path to the container configuration file on an Amazon EC2 instance.</summary> + private const string ContainerConfigPath = @"C:\Program Files\Amazon\ElasticBeanstalk\config\containerconfiguration"; + + + /********* + ** Public methods + *********/ + /// <summary>Build the configuration provider for this source.</summary> + /// <param name="builder">The configuration builder.</param> + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new BeanstalkEnvPropsConfigProvider(); + } + + /// <summary>Load the environment properties.</summary> + public override void Load() + { + this.Data = new Dictionary<string, string>(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>()) + { + string[] parts = prop.Split('=', 2); // key=value + if (parts.Length == 2) + this.Data[parts[0]] = parts[1]; + } + } + } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs new file mode 100644 index 00000000..df5d605d --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/LogParserConfig.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// <summary>The config settings for the log parser.</summary> + internal class LogParserConfig + { + /********* + ** Accessors + *********/ + /// <summary>The root URL for the log parser controller.</summary> + public string SectionUrl { get; set; } + + /// <summary>The base URL for the Pastebin API.</summary> + public string PastebinBaseUrl { get; set; } + + /// <summary>The user agent for the Pastebin API client, where {0} is the SMAPI version.</summary> + public string PastebinUserAgent { get; set; } + + /// <summary>The user key used to authenticate with the Pastebin API.</summary> + public string PastebinUserKey { get; set; } + + /// <summary>The developer key used to authenticate with the Pastebin API.</summary> + 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 { /// <summary>The config settings for mod update checks.</summary> - 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 +{ + /// <summary>The response for a get-paste request.</summary> + internal class GetPasteResponse + { + /// <summary>Whether the log was successfully fetched.</summary> + public bool Success { get; set; } + + /// <summary>The fetched paste content (if <see cref="Success"/> is <c>true</c>).</summary> + public string Content { get; set; } + + /// <summary>The error message (if saving failed).</summary> + 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..738330d3 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs @@ -0,0 +1,117 @@ +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 +{ + /// <summary>An API client for Pastebin.</summary> + internal class PastebinClient : IDisposable + { + /********* + ** Properties + *********/ + /// <summary>The underlying HTTP client.</summary> + private readonly IClient Client; + + /// <summary>The user key used to authenticate with the Pastebin API.</summary> + private readonly string UserKey; + + /// <summary>The developer key used to authenticate with the Pastebin API.</summary> + private readonly string DevKey; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="baseUrl">The base URL for the Pastebin API.</param> + /// <param name="userAgent">The user agent for the API client.</param> + /// <param name="userKey">The user key used to authenticate with the Pastebin API.</param> + /// <param name="devKey">The developer key used to authenticate with the Pastebin API.</param> + public PastebinClient(string baseUrl, string userAgent, string userKey, string devKey) + { + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.UserKey = userKey; + this.DevKey = devKey; + } + + /// <summary>Fetch a saved paste.</summary> + /// <param name="id">The paste ID.</param> + public async Task<GetPasteResponse> 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("<!DOCTYPE")) + return new GetPasteResponse { Error = $"Received a captcha challenge from Pastebin. Please visit https://pastebin.com/{id} in a new window to solve it." }; + return new GetPasteResponse { Success = true, Content = content }; + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new GetPasteResponse { Error = "There's no log with that ID." }; + } + catch (Exception ex) + { + return new GetPasteResponse { Error = ex.ToString() }; + } + } + + public async Task<SavePasteResponse> 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<string, string> + { + ["api_option"] = "paste", + ["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(); + + // 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() }; + } + } + + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + 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 +{ + /// <summary>The response for a save-log request.</summary> + internal class SavePasteResponse + { + /// <summary>Whether the log was successfully saved.</summary> + public bool Success { get; set; } + + /// <summary>The saved paste ID (if <see cref="Success"/> is <c>true</c>).</summary> + public string ID { get; set; } + + /// <summary>The error message (if saving failed).</summary> + public string Error { get; set; } + } +} 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 +{ + /// <summary>Redirect requests to HTTPS.</summary> + /// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" /> and <see cref="Microsoft.AspNetCore.Rewrite.Internal.RedirectToHttpsRule"/>.</remarks> + internal class ConditionalRedirectToHttpsRule : IRule + { + /********* + ** Properties + *********/ + /// <summary>A predicate which indicates when the rule should be applied.</summary> + private readonly Func<HttpRequest, bool> ShouldRewrite; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param> + public ConditionalRedirectToHttpsRule(Func<HttpRequest, bool> shouldRewrite = null) + { + this.ShouldRewrite = shouldRewrite ?? (req => true); + } + + /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary> + /// <param name="context">The rewrite context.</param> + 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; + } + + /// <summary>Get whether the request was received over HTTPS.</summary> + /// <param name="request">The request to check.</param> + 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 new file mode 100644 index 00000000..920632ab --- /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 +{ + /// <summary>Rewrite requests to prepend the subdomain portion (if any) to the path.</summary> + /// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" />.</remarks> + internal class ConditionalRewriteSubdomainRule : IRule + { + /********* + ** Accessors + *********/ + /// <summary>A predicate which indicates when the rule should be applied.</summary> + private readonly Func<HttpRequest, bool> ShouldRewrite; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param> + public ConditionalRewriteSubdomainRule(Func<HttpRequest, bool> shouldRewrite = null) + { + this.ShouldRewrite = shouldRewrite ?? (req => true); + } + + /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary> + /// <param name="context">The rewrite context.</param> + public void ApplyRule(RewriteContext context) + { + HttpRequest request = context.HttpContext.Request; + + // check condition + if (!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/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 +{ + /// <summary>Redirect requests to an external URL if they match a condition.</summary> + internal class RedirectToUrlRule : IRule + { + /********* + ** Properties + *********/ + /// <summary>A predicate which indicates when the rule should be applied.</summary> + private readonly Func<HttpRequest, bool> ShouldRewrite; + + /// <summary>The new URL to which to redirect.</summary> + private readonly string NewUrl; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param> + /// <param name="url">The new URL to which to redirect.</param> + public RedirectToUrlRule(Func<HttpRequest, bool> shouldRewrite, string url) + { + this.ShouldRewrite = shouldRewrite ?? (req => true); + this.NewUrl = url; + } + + /// <summary>Construct an instance.</summary> + /// <param name="pathRegex">A case-insensitive regex to match against the path.</param> + /// <param name="url">The external URL.</param> + 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; + } + + /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary> + /// <param name="context">The rewrite context.</param> + 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/Framework/RewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs deleted file mode 100644 index 5a56844f..00000000 --- a/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Micro |
