summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Controllers/LogParserController.cs
blob: 454440bb98e9131a72faf10dfa09813db88d3757 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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);
            }
        }
    }
}