summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.Web/Framework')
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs2
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs89
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs (renamed from src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs)19
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs2
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs54
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs (renamed from src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs)30
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs3
-rw-r--r--src/SMAPI.Web/Framework/Compression/GzipHelper.cs2
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs25
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs18
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs15
-rw-r--r--src/SMAPI.Web/Framework/Extensions.cs22
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogParser.cs6
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs54
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs58
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs54
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs47
-rw-r--r--src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs62
-rw-r--r--src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs57
19 files changed, 443 insertions, 176 deletions
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
index bcec8b36..08749f3b 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
@@ -4,7 +4,7 @@ using StardewModdingAPI.Web.Framework.ModRepositories;
namespace StardewModdingAPI.Web.Framework.Caching.Mods
{
- /// <summary>Encapsulates logic for accessing the mod data cache.</summary>
+ /// <summary>Manages cached mod data.</summary>
internal interface IModCacheRepository : ICacheRepository
{
/*********
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
new file mode 100644
index 00000000..9c5a217e
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+using StardewModdingAPI.Web.Framework.ModRepositories;
+
+namespace StardewModdingAPI.Web.Framework.Caching.Mods
+{
+ /// <summary>Manages cached mod data in-memory.</summary>
+ internal class ModCacheMemoryRepository : BaseCacheRepository, IModCacheRepository
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The cached mod data indexed by <c>{site key}:{ID}</c>.</summary>
+ private readonly IDictionary<string, CachedMod> Mods = new Dictionary<string, CachedMod>(StringComparer.InvariantCultureIgnoreCase);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get the cached mod data.</summary>
+ /// <param name="site">The mod site to search.</param>
+ /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
+ /// <param name="mod">The fetched mod.</param>
+ /// <param name="markRequested">Whether to update the mod's 'last requested' date.</param>
+ public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true)
+ {
+ // get mod
+ if (!this.Mods.TryGetValue(this.GetKey(site, id), out mod))
+ return false;
+
+ // bump 'last requested'
+ if (markRequested)
+ {
+ mod.LastRequested = DateTimeOffset.UtcNow;
+ mod = this.SaveMod(mod);
+ }
+
+ return true;
+ }
+
+ /// <summary>Save data fetched for a mod.</summary>
+ /// <param name="site">The mod site on which the mod is found.</param>
+ /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
+ /// <param name="mod">The mod data.</param>
+ /// <param name="cachedMod">The stored mod record.</param>
+ public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod)
+ {
+ string key = this.GetKey(site, id);
+ cachedMod = this.SaveMod(new CachedMod(site, id, mod));
+ }
+
+ /// <summary>Delete data for mods which haven't been requested within a given time limit.</summary>
+ /// <param name="age">The minimum age for which to remove mods.</param>
+ public void RemoveStaleMods(TimeSpan age)
+ {
+ DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age);
+
+ string[] staleKeys = this.Mods
+ .Where(p => p.Value.LastRequested < minDate)
+ .Select(p => p.Key)
+ .ToArray();
+
+ foreach (string key in staleKeys)
+ this.Mods.Remove(key);
+ }
+
+ /// <summary>Save data fetched for a mod.</summary>
+ /// <param name="mod">The mod data.</param>
+ public CachedMod SaveMod(CachedMod mod)
+ {
+ string key = this.GetKey(mod.Site, mod.ID);
+ return this.Mods[key] = mod;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get a cache key.</summary>
+ /// <param name="site">The mod site.</param>
+ /// <param name="id">The mod ID.</param>
+ public string GetKey(ModRepositoryKey site, string id)
+ {
+ return $"{site}:{id.Trim()}".ToLower();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs
index 2e7804a7..f105baab 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs
@@ -5,8 +5,8 @@ using StardewModdingAPI.Web.Framework.ModRepositories;
namespace StardewModdingAPI.Web.Framework.Caching.Mods
{
- /// <summary>Encapsulates logic for accessing the mod data cache.</summary>
- internal class ModCacheRepository : BaseCacheRepository, IModCacheRepository
+ /// <summary>Manages cached mod data in MongoDB.</summary>
+ internal class ModCacheMongoRepository : BaseCacheRepository, IModCacheRepository
{
/*********
** Fields
@@ -20,7 +20,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="database">The authenticated MongoDB database.</param>
- public ModCacheRepository(IMongoDatabase database)
+ public ModCacheMongoRepository(IMongoDatabase database)
{
// get collections
this.Mods = database.GetCollection<CachedMod>("mods");
@@ -29,6 +29,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
this.Mods.Indexes.CreateOne(new CreateIndexModel<CachedMod>(Builders<CachedMod>.IndexKeys.Ascending(p => p.ID).Ascending(p => p.Site)));
}
+
/*********
** Public methods
*********/
@@ -72,13 +73,9 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
public void RemoveStaleMods(TimeSpan age)
{
DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age);
- var result = this.Mods.DeleteMany(p => p.LastRequested < minDate);
+ this.Mods.DeleteMany(p => p.LastRequested < minDate);
}
-
- /*********
- ** Private methods
- *********/
/// <summary>Save data fetched for a mod.</summary>
/// <param name="mod">The mod data.</param>
public CachedMod SaveMod(CachedMod mod)
@@ -88,12 +85,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
this.Mods.ReplaceOne(
entry => entry.ID == id && entry.Site == mod.Site,
mod,
- new UpdateOptions { IsUpsert = true }
+ new ReplaceOptions { IsUpsert = true }
);
return mod;
}
+
+ /*********
+ ** Private methods
+ *********/
/// <summary>Normalize a mod ID for case-insensitive search.</summary>
/// <param name="id">The mod ID.</param>
public string NormalizeId(string id)
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
index b54c8a2f..02097f52 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
@@ -5,7 +5,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
{
- /// <summary>Encapsulates logic for accessing the wiki data cache.</summary>
+ /// <summary>Manages cached wiki data.</summary>
internal interface IWikiCacheRepository : ICacheRepository
{
/*********
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
new file mode 100644
index 00000000..4621f5e3
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.Framework.Caching.Wiki
+{
+ /// <summary>Manages cached wiki data in-memory.</summary>
+ internal class WikiCacheMemoryRepository : BaseCacheRepository, IWikiCacheRepository
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The saved wiki metadata.</summary>
+ private CachedWikiMetadata Metadata;
+
+ /// <summary>The cached wiki data.</summary>
+ private CachedWikiMod[] Mods = new CachedWikiMod[0];
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get the cached wiki metadata.</summary>
+ /// <param name="metadata">The fetched metadata.</param>
+ public bool TryGetWikiMetadata(out CachedWikiMetadata metadata)
+ {
+ metadata = this.Metadata;
+ return metadata != null;
+ }
+
+ /// <summary>Get the cached wiki mods.</summary>
+ /// <param name="filter">A filter to apply, if any.</param>
+ public IEnumerable<CachedWikiMod> GetWikiMods(Expression<Func<CachedWikiMod, bool>> filter = null)
+ {
+ return filter != null
+ ? this.Mods.Where(filter.Compile())
+ : this.Mods.ToArray();
+ }
+
+ /// <summary>Save data fetched from the wiki compatibility list.</summary>
+ /// <param name="stableVersion">The current stable Stardew Valley version.</param>
+ /// <param name="betaVersion">The current beta Stardew Valley version.</param>
+ /// <param name="mods">The mod data.</param>
+ /// <param name="cachedMetadata">The stored metadata record.</param>
+ /// <param name="cachedMods">The stored mod records.</param>
+ public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods)
+ {
+ this.Metadata = cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion);
+ this.Mods = cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs
index 1ae9d38f..07e7c721 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs
@@ -7,17 +7,17 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
{
- /// <summary>Encapsulates logic for accessing the wiki data cache.</summary>
- internal class WikiCacheRepository : BaseCacheRepository, IWikiCacheRepository
+ /// <summary>Manages cached wiki data in MongoDB.</summary>
+ internal class WikiCacheMongoRepository : BaseCacheRepository, IWikiCacheRepository
{
/*********
** Fields
*********/
/// <summary>The collection for wiki metadata.</summary>
- private readonly IMongoCollection<CachedWikiMetadata> WikiMetadata;
+ private readonly IMongoCollection<CachedWikiMetadata> Metadata;
/// <summary>The collection for wiki mod data.</summary>
- private readonly IMongoCollection<CachedWikiMod> WikiMods;
+ private readonly IMongoCollection<CachedWikiMod> Mods;
/*********
@@ -25,21 +25,21 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
*********/
/// <summary>Construct an instance.</summary>
/// <param name="database">The authenticated MongoDB database.</param>
- public WikiCacheRepository(IMongoDatabase database)
+ public WikiCacheMongoRepository(IMongoDatabase database)
{
// get collections
- this.WikiMetadata = database.GetCollection<CachedWikiMetadata>("wiki-metadata");
- this.WikiMods = database.GetCollection<CachedWikiMod>("wiki-mods");
+ this.Metadata = database.GetCollection<CachedWikiMetadata>("wiki-metadata");
+ this.Mods = database.GetCollection<CachedWikiMod>("wiki-mods");
// add indexes if needed
- this.WikiMods.Indexes.CreateOne(new CreateIndexModel<CachedWikiMod>(Builders<CachedWikiMod>.IndexKeys.Ascending(p => p.ID)));
+ this.Mods.Indexes.CreateOne(new CreateIndexModel<CachedWikiMod>(Builders<CachedWikiMod>.IndexKeys.Ascending(p => p.ID)));
}
/// <summary>Get the cached wiki metadata.</summary>
/// <param name="metadata">The fetched metadata.</param>
public bool TryGetWikiMetadata(out CachedWikiMetadata metadata)
{
- metadata = this.WikiMetadata.Find("{}").FirstOrDefault();
+ metadata = this.Metadata.Find("{}").FirstOrDefault();
return metadata != null;
}
@@ -48,8 +48,8 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
public IEnumerable<CachedWikiMod> GetWikiMods(Expression<Func<CachedWikiMod, bool>> filter = null)
{
return filter != null
- ? this.WikiMods.Find(filter).ToList()
- : this.WikiMods.Find("{}").ToList();
+ ? this.Mods.Find(filter).ToList()
+ : this.Mods.Find("{}").ToList();
}
/// <summary>Save data fetched from the wiki compatibility list.</summary>
@@ -63,11 +63,11 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion);
cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray();
- this.WikiMods.DeleteMany("{}");
- this.WikiMods.InsertMany(cachedMods);
+ this.Mods.DeleteMany("{}");
+ this.Mods.InsertMany(cachedMods);
- this.WikiMetadata.DeleteMany("{}");
- this.WikiMetadata.InsertOne(cachedMetadata);
+ this.Metadata.DeleteMany("{}");
+ this.Metadata.InsertOne(cachedMetadata);
}
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
index 140b854e..a6fd21fd 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
@@ -57,8 +57,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
if (!SemanticVersion.TryParse(raw, out version))
{
- if (invalidVersion == null)
- invalidVersion = raw;
+ invalidVersion ??= raw;
continue;
}
}
diff --git a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
index cc8f4737..676d660d 100644
--- a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
+++ b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
@@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Framework.Compression
return rawText;
// decompress
- using (MemoryStream memoryStream = new MemoryStream())
+ using MemoryStream memoryStream = new MemoryStream();
{
// read length prefix
int dataLength = BitConverter.ToInt32(zipBuffer, 0);
diff --git a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs
deleted file mode 100644
index c7b6cb00..00000000
--- a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace StardewModdingAPI.Web.Framework.ConfigModels
-{
- /// <summary>The config settings for mod compatibility list.</summary>
- internal class MongoDbConfig
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The MongoDB connection string.</summary>
- public string ConnectionString { get; set; }
-
- /// <summary>The database name.</summary>
- public string Database { get; set; }
-
-
- /*********
- ** Public method
- *********/
- /// <summary>Get whether a MongoDB instance is configured.</summary>
- public bool IsConfigured()
- {
- return !string.IsNullOrWhiteSpace(this.ConnectionString);
- }
- }
-}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs
new file mode 100644
index 00000000..61cc4855
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs
@@ -0,0 +1,18 @@
+namespace StardewModdingAPI.Web.Framework.ConfigModels
+{
+ /// <summary>The config settings for cache storage.</summary>
+ internal class StorageConfig
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The storage mechanism to use.</summary>
+ public StorageMode Mode { get; set; }
+
+ /// <summary>The connection string for the storage mechanism, if applicable.</summary>
+ public string ConnectionString { get; set; }
+
+ /// <summary>The database name for the storage mechanism, if applicable.</summary>
+ public string Database { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs b/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs
new file mode 100644
index 00000000..4c2ea801
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs
@@ -0,0 +1,15 @@
+namespace StardewModdingAPI.Web.Framework.ConfigModels
+{
+ /// <summary>Indicates a storage mechanism to use.</summary>
+ internal enum StorageMode
+ {
+ /// <summary>Store data in a hosted MongoDB instance.</summary>
+ Mongo,
+
+ /// <summary>Store data in an in-memory MongoDB instance. This is useful for testing MongoDB storage locally, but will likely fail when deployed since it needs permission to open a local port.</summary>
+ MongoInMemory,
+
+ /// <summary>Store data in-memory. This is suitable for local testing or single-instance servers, but will cause issues when distributed across multiple servers.</summary>
+ InMemory
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs
index e0da1424..ad7e645a 100644
--- a/src/SMAPI.Web/Framework/Extensions.cs
+++ b/src/SMAPI.Web/Framework/Extensions.cs
@@ -1,8 +1,12 @@
using System;
using JetBrains.Annotations;
+using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Razor;
+using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
+using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework
{
@@ -18,6 +22,7 @@ namespace StardewModdingAPI.Web.Framework
/// <returns>The generated URL.</returns>
public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null, bool absoluteUrl = false)
{
+ // get route values
RouteValueDictionary valuesDict = new RouteValueDictionary(values);
foreach (var value in helper.ActionContext.RouteData.Values)
{
@@ -25,14 +30,31 @@ namespace StardewModdingAPI.Web.Framework
valuesDict[value.Key] = null; // explicitly remove it from the URL
}
+ // get relative URL
string url = helper.Action(action, controller, valuesDict);
+ if (url == null && action.EndsWith("Async"))
+ url = helper.Action(action[..^"Async".Length], controller, valuesDict);
+
+ // get absolute URL
if (absoluteUrl)
{
HttpRequest request = helper.ActionContext.HttpContext.Request;
Uri baseUri = new Uri($"{request.Scheme}://{request.Host}");
url = new Uri(baseUri, url).ToString();
}
+
return url;
}
+
+ /// <summary>Get a serialized JSON representation of the value.</summary>
+ /// <param name="page">The page to extend.</param>
+ /// <param name="value">The value to serialize.</param>
+ /// <returns>The serialized JSON.</returns>
+ /// <remarks>This bypasses unnecessary validation (e.g. not allowing null values) in <see cref="IJsonHelper.Serialize"/>.</remarks>
+ public static IHtmlContent ForJson(this RazorPageBase page, object value)
+ {
+ string json = JsonConvert.SerializeObject(value);
+ return new HtmlString(json);
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index cce80816..227dcd89 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching SMAPI's update line.</summary>
- private readonly Regex SMAPIUpdatePattern = new Regex(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex SmapiUpdatePattern = new Regex(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/*********
@@ -181,9 +181,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
message.Section = LogSection.ModUpdateList;
}
- else if (message.Level == LogLevel.Alert && this.SMAPIUpdatePattern.IsMatch(message.Text))
+ else if (message.Level == LogLevel.Alert && this.SmapiUpdatePattern.IsMatch(message.Text))
{
- Match match = this.SMAPIUpdatePattern.Match(message.Text);
+ Match match = this.SmapiUpdatePattern.Match(message.Text);
string version = match.Groups["version"].Value;
string link = match.Groups["link"].Value;
smapiMod.UpdateVersion = version;
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
new file mode 100644
index 00000000..d75ee791
--- /dev/null
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Net;
+using Microsoft.AspNetCore.Rewrite;
+
+namespace StardewModdingAPI.Web.Framework.RedirectRules
+{
+ /// <summary>Redirect hostnames to a URL if they match a condition.</summary>
+ internal class RedirectHostsToUrlsRule : RedirectMatchRule
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Maps a lowercase hostname to the resulting redirect URL.</summary>
+ private readonly Func<string, string> Map;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="statusCode">The status code to use for redirects.</param>
+ /// <param name="map">Hostnames mapped to the resulting redirect URL.</param>
+ public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func<string, string> map)
+ {
+ this.StatusCode = statusCode;
+ this.Map = map ?? throw new ArgumentNullException(nameof(map));
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the new redirect URL.</summary>
+ /// <param name="context">The rewrite context.</param>
+ /// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
+ protected override string GetNewUrl(RewriteContext context)
+ {
+ // get requested host
+ string host = context.HttpContext.Request.Host.Host;
+ if (host == null)
+ return null;
+
+ // get new host
+ host = this.Map(host);
+ if (host == null)
+ return null;
+
+ // rewrite URL
+ UriBuilder uri = this.GetUrl(context.HttpContext.Request);
+ uri.Host = host;
+ return uri.ToString();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
new file mode 100644
index 00000000..6e81c4ca
--- /dev/null
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite;
+
+namespace StardewModdingAPI.Web.Framework.RedirectRules
+{
+ /// <summary>Redirect matching requests to a URL.</summary>
+ internal abstract class RedirectMatchRule : IRule
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The status code to use for redirects.</summary>
+ protected HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Redirect;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <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)
+ {
+ string newUrl = this.GetNewUrl(context);
+ if (newUrl == null)
+ return;
+
+ HttpResponse response = context.HttpContext.Response;
+ response.StatusCode = (int)HttpStatusCode.Redirect;
+ response.Headers["Location"] = newUrl;
+ context.Result = RuleResult.EndResponse;
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get the new redirect URL.</summary>
+ /// <param name="context">The rewrite context.</param>
+ /// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
+ protected abstract string GetNewUrl(RewriteContext context);
+
+ /// <summary>Get the full request URL.</summary>
+ /// <param name="request">The request.</param>
+ protected UriBuilder GetUrl(HttpRequest request)
+ {
+ return new UriBuilder
+ {
+ Scheme = request.Scheme,
+ Host = request.Host.Host,
+ Port = request.Host.Port ?? -1,
+ Path = request.PathBase + request.Path,
+ Query = request.QueryString.Value
+ };
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
new file mode 100644
index 00000000..16397c1e
--- /dev/null
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Rewrite;
+
+namespace StardewModdingAPI.Web.Framework.RedirectRules
+{
+ /// <summary>Redirect paths to URLs if they match a condition.</summary>
+ internal class RedirectPathsToUrlsRule : RedirectMatchRule
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Regex patterns matching the current URL mapped to the resulting redirect URL.</summary>
+ private readonly IDictionary<Regex, string> Map;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="map">Regex patterns matching the current URL mapped to the resulting redirect URL.</param>
+ public RedirectPathsToUrlsRule(IDictionary<string, string> map)
+ {
+ this.Map = map.ToDictionary(
+ p => new Regex(p.Key, RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ p => p.Value
+ );
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get the new redirect URL.</summary>
+ /// <param name="context">The rewrite context.</param>
+ /// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
+ protected override string GetNewUrl(RewriteContext context)
+ {
+ string path = context.HttpContext.Request.Path.Value;
+
+ if (!string.IsNullOrWhiteSpace(path))
+ {
+ foreach ((Regex pattern, string url) in this.Map)
+ {
+ if (pattern.IsMatch(path))
+ return pattern.Replace(path, url);
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
new file mode 100644
index 00000000..2a503ae3
--- /dev/null
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite;
+
+namespace StardewModdingAPI.Web.Framework.RedirectRules
+{
+ /// <summary>Redirect requests to HTTPS.</summary>
+ internal class RedirectToHttpsRule : RedirectMatchRule
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Matches requests which should be ignored.</summary>
+ private readonly Func<HttpRequest, bool> Except;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="except">Matches requests which should be ignored.</param>
+ public RedirectToHttpsRule(Func<HttpRequest, bool> except = null)
+ {
+ this.Except = except ?? (req => false);
+ this.StatusCode = HttpStatusCode.RedirectKeepVerb;
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get the new redirect URL.</summary>
+ /// <param name="context">The rewrite context.</param>
+ /// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
+ protected override string GetNewUrl(RewriteContext context)
+ {
+ HttpRequest request = context.HttpContext.Request;
+ if (request.IsHttps || this.Except(request))
+ return null;
+
+ UriBuilder uri = this.GetUrl(request);
+ uri.Scheme = "https";
+ return uri.ToString();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs
deleted file mode 100644
index 36effd82..00000000
--- a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-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
- {
- /*********
- ** Fields
- *********/
- /// <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/RedirectToUrlRule.cs b/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs
deleted file mode 100644
index ab9e019c..00000000
--- a/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-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
- {
- /*********
- ** Fields
- *********/
- /// <summary>Get the new URL to which to redirect (or <c>null</c> to skip).</summary>
- private readonly Func<HttpRequest, 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.NewUrl = req => shouldRewrite(req) ? url : null;
- }
-
- /// <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.NewUrl = req => req.Path.HasValue ? regex.Replace(req.Path.Value, url) : null;
- }
-
- /// <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 rewrite
- string newUrl = this.NewUrl(request);
- if (newUrl == null || newUrl == request.Path.Value)
- return;
-
- // redirect request
- HttpResponse response = context.HttpContext.Response;
- response.StatusCode = (int)HttpStatusCode.Redirect;
- response.Headers["Location"] = newUrl;
- context.Result = RuleResult.EndResponse;
- }
- }
-}