using Microsoft.Extensions.Options; using System.Net; using System.IO; using System.Text.RegularExpressions; using sodoff.Configuration; using sodoff.Util; namespace sodoff.Middleware; public class AssetMiddleware { private readonly RequestDelegate _next; private readonly IOptions config; private readonly Regex autoEncryptRegexp; public AssetMiddleware(RequestDelegate next, IOptions config) { _next = next; this.config = config; if (config.Value.AutoEncryptRegexp != "") autoEncryptRegexp = new Regex(config.Value.AutoEncryptRegexp); else autoEncryptRegexp = null; } public async Task Invoke(HttpContext context) { if (context.Connection.LocalPort == config.Value.Port) await GetAssetAsync(context); else await _next(context); } private async Task GetAssetAsync(HttpContext context) { string path = context.Request.Path; if (path is null || config.Value.Mode == AssetServerMode.None) { context.Response.StatusCode = 400; return; } string assetPath; if (config.Value.UseAnyURLPrefix) { int firstSlash = path.IndexOf('/', 1); if (firstSlash < 0) { context.Response.StatusCode = 400; return; } assetPath = path.Remove(0, firstSlash + 1); } else { if (!string.IsNullOrEmpty(config.Value.URLPrefix) && !path.StartsWith("/" + config.Value.URLPrefix)) { context.Response.StatusCode = 400; return; } assetPath = path.Remove(0, config.Value.URLPrefix.Length + 1); } string localPath = GetLocalPath("assets/" + assetPath); if (localPath == string.Empty && config.Value.Mode == AssetServerMode.Partial && config.Value.UseCache) localPath = GetLocalPath("assets-cache/" + assetPath); if (localPath == string.Empty) { if (config.Value.Mode == AssetServerMode.Partial) await GetRemoteAsset(context, assetPath); else context.Response.StatusCode = 404; } else { context.Response.Headers["Content-Type"] = "application/octet-stream"; bool needEncrypt = autoEncryptRegexp != null && autoEncryptRegexp.IsMatch(localPath); if (needEncrypt) { await context.Response.WriteAsync( TripleDES.EncryptASCII( System.IO.File.ReadAllText(localPath), config.Value.AutoEncryptKey ) ); } else { await context.Response.SendFileAsync(Path.GetFullPath(localPath)); } } } private async Task GetRemoteAsset(HttpContext context, string path) { HttpClient client = new HttpClient(); string filePath = Path.GetFullPath("assets-cache/" + path); string filePathTmp = filePath + Path.GetRandomFileName().Substring(0, 8); try { using (var response = await client.GetAsync( config.Value.ProviderURL + path, HttpCompletionOption.ResponseHeadersRead )) { if (response.IsSuccessStatusCode) { if (response.Content.Headers.ContentType?.MediaType != null) context.Response.Headers["Content-Type"] = response.Content.Headers.ContentType?.MediaType; if (response.Content.Headers.ContentLength != null) context.Response.Headers["Content-Length"] = response.Content.Headers.ContentLength.ToString(); using (var inputStream = await response.Content.ReadAsStreamAsync()) { if (config.Value.UseCache) { string dirPath = Path.GetDirectoryName(filePath); if (!Directory.Exists(dirPath)) { Directory.CreateDirectory(dirPath); } // copy data retrieved from upstream server to file and to response for game client using (var fileStream = File.Open(filePathTmp, FileMode.Create)) { // read response from upstream server byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { // write to temporary file var task1 = fileStream.WriteAsync(buffer, 0, bytesRead); // send to client var task2 = context.Response.Body.WriteAsync(buffer, 0, bytesRead); // wait for finish both writes await Task.WhenAll(task1, task2); } } // after successfully write data to temporary file, rename it to proper asset filename try { File.Move(filePathTmp, filePath); } catch (System.IO.IOException) { // this can happen for example if two clients request the same file in the same time // TODO: avoid redundant download in those cases File.Delete(filePathTmp); } } else { await inputStream.CopyToAsync(context.Response.Body); } } } else { context.Response.StatusCode = 404; } } } catch (Exception) { if (File.Exists(filePathTmp)) File.Delete(filePathTmp); if (!context.Response.HasStarted) context.Response.StatusCode = 502; } } private string GetLocalPath(string path) { if (File.Exists(path)) return path; string[] qualityTiers = { "/High/", "/Mid/", "/Low/" }; if (config.Value.SubstituteMissingLocalAssets) { foreach (var tier in qualityTiers) { if (path.Contains(tier)) { foreach (var otherTier in qualityTiers) { if (otherTier != tier) { string otherPath = path.Replace(tier, otherTier); if (File.Exists(otherPath)) return otherPath; } } } } } return string.Empty; } }