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.Clients.Pastebin; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.LogParsing; using StardewModdingAPI.Web.Framework.LogParsing.Models; 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 ContextConfig Config; /// <summary>The underlying Pastebin client.</summary> private readonly IPastebinClient Pastebin; /// <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="contextProvider">The context config settings.</param> /// <param name="pastebin">The Pastebin API client.</param> public LogParserController(IOptions<ContextConfig> contextProvider, IPastebinClient pastebin) { this.Config = contextProvider.Value; this.Pastebin = pastebin; } /*** ** Web UI ***/ /// <summary>Render the log parser UI.</summary> /// <param name="id">The paste ID.</param> [HttpGet] [Route("log")] [Route("log/{id}")] public async Task<ViewResult> Index(string id = null) { // fresh page if (string.IsNullOrWhiteSpace(id)) return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null)); // log page PasteInfo paste = await this.GetAsync(id); ParsedLog log = paste.Success ? new LogParser().Parse(paste.Content) : new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error }; return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log)); } /*** ** JSON ***/ /// <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<SavePasteResult> PostAsync([FromBody] string content) { content = this.CompressString(content); return await this.Pastebin.PostAsync(content); } /********* ** Private methods *********/ /// <summary>Fetch raw text from Pastebin.</summary> /// <param name="id">The Pastebin paste ID.</param> private async Task<PasteInfo> GetAsync(string id) { PasteInfo response = await this.Pastebin.GetAsync(id); response.Content = this.DecompressString(response.Content); return response; } /// <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); } } } }