diff options
Diffstat (limited to 'src/SMAPI.Web/Controllers')
-rw-r--r-- | src/SMAPI.Web/Controllers/LogParserController.cs | 158 | ||||
-rw-r--r-- | src/SMAPI.Web/Controllers/ModsApiController.cs (renamed from src/SMAPI.Web/Controllers/ModsController.cs) | 6 |
2 files changed, 161 insertions, 3 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; |