From fe6865050faf46e9c0ed7d9c22e1f36374bdaa31 Mon Sep 17 00:00:00 2001 From: JustArchi Date: Wed, 6 Dec 2017 08:14:12 +0100 Subject: [PATCH] Closes #667 --- ArchiSteamFarm/Bot.cs | 57 ++- ArchiSteamFarm/BotConfig.cs | 33 ++ ArchiSteamFarm/BotDatabase.cs | 19 + ArchiSteamFarm/IPC.cs | 451 ++++++++++++++---- .../Localization/Strings.Designer.cs | 9 - ArchiSteamFarm/Localization/Strings.resx | 4 - 6 files changed, 454 insertions(+), 119 deletions(-) diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index fc71a408b..fa58d78cc 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -90,8 +90,11 @@ namespace ArchiSteamFarm { private readonly Trading Trading; private string BotPath => Path.Combine(SharedInfo.ConfigDirectory, BotName); + private string ConfigFilePath => BotPath + ".json"; + private string DatabaseFilePath => BotPath + ".db"; private bool IsAccountLocked => AccountFlags.HasFlag(EAccountFlags.Lockdown); - private string SentryFile => BotPath + ".bin"; + private string MobileAuthenticatorFilePath => BotPath + ".maFile"; + private string SentryFilePath => BotPath + ".bin"; [JsonProperty] private ulong SteamID => SteamClient?.SteamID ?? 0; @@ -141,19 +144,15 @@ namespace ArchiSteamFarm { BotName = botName; ArchiLogger = new ArchiLogger(botName); - string botConfigFile = BotPath + ".json"; - - BotConfig = BotConfig.Load(botConfigFile); + BotConfig = BotConfig.Load(ConfigFilePath); if (BotConfig == null) { - ArchiLogger.LogGenericError(string.Format(Strings.ErrorBotConfigInvalid, botConfigFile)); + ArchiLogger.LogGenericError(string.Format(Strings.ErrorBotConfigInvalid, ConfigFilePath)); return; } - string botDatabaseFile = BotPath + ".db"; - - BotDatabase = BotDatabase.Load(botDatabaseFile); + BotDatabase = BotDatabase.Load(DatabaseFilePath); if (BotDatabase == null) { - ArchiLogger.LogGenericError(string.Format(Strings.ErrorDatabaseInvalid, botDatabaseFile)); + ArchiLogger.LogGenericError(string.Format(Strings.ErrorDatabaseInvalid, DatabaseFilePath)); return; } @@ -309,6 +308,33 @@ namespace ArchiSteamFarm { } } + internal async Task DeleteAllRelatedFiles() { + try { + await BotDatabase.MakeReadOnly().ConfigureAwait(false); + + if (File.Exists(ConfigFilePath)) { + File.Delete(ConfigFilePath); + } + + if (File.Exists(DatabaseFilePath)) { + File.Delete(DatabaseFilePath); + } + + if (File.Exists(MobileAuthenticatorFilePath)) { + File.Delete(MobileAuthenticatorFilePath); + } + + if (File.Exists(SentryFilePath)) { + File.Delete(SentryFilePath); + } + + return true; + } catch (Exception e) { + ArchiLogger.LogGenericException(e); + return false; + } + } + internal static string FormatBotResponse(string response, string botName) { if (!string.IsNullOrEmpty(response) && !string.IsNullOrEmpty(botName)) { return Environment.NewLine + "<" + botName + "> " + response; @@ -1538,9 +1564,9 @@ namespace ArchiSteamFarm { byte[] sentryFileHash = null; - if (File.Exists(SentryFile)) { + if (File.Exists(SentryFilePath)) { try { - byte[] sentryFileContent = File.ReadAllBytes(SentryFile); + byte[] sentryFileContent = File.ReadAllBytes(SentryFilePath); sentryFileHash = SteamKit2.CryptoHelper.SHAHash(sentryFileContent); } catch (Exception e) { ArchiLogger.LogGenericException(e); @@ -2008,7 +2034,7 @@ namespace ArchiSteamFarm { byte[] sentryHash; try { - using (FileStream fileStream = File.Open(SentryFile, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { + using (FileStream fileStream = File.Open(SentryFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { fileStream.Seek(callback.Offset, SeekOrigin.Begin); fileStream.Write(callback.Data, 0, callback.BytesToWrite); fileSize = (int) fileStream.Length; @@ -4578,11 +4604,8 @@ namespace ArchiSteamFarm { } // Support and convert 2FA files - if (!HasMobileAuthenticator) { - string maFilePath = BotPath + ".maFile"; - if (File.Exists(maFilePath)) { - await ImportAuthenticator(maFilePath).ConfigureAwait(false); - } + if (!HasMobileAuthenticator && File.Exists(MobileAuthenticatorFilePath)) { + await ImportAuthenticator(MobileAuthenticatorFilePath).ConfigureAwait(false); } await Connect().ConfigureAwait(false); diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index 0f2782fa8..71eb376f2 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -24,6 +24,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using ArchiSteamFarm.JSON; using ArchiSteamFarm.Localization; using Newtonsoft.Json; @@ -31,6 +33,8 @@ using Newtonsoft.Json; namespace ArchiSteamFarm { [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] internal sealed class BotConfig { + private static readonly SemaphoreSlim WriteSemaphore = new SemaphoreSlim(1, 1); + [JsonProperty(Required = Required.DisallowNull)] internal readonly bool AcceptGifts; @@ -186,6 +190,35 @@ namespace ArchiSteamFarm { return botConfig; } + internal static async Task Write(string filePath, BotConfig botConfig) { + if (string.IsNullOrEmpty(filePath) || (botConfig == null)) { + ASF.ArchiLogger.LogNullError(nameof(filePath) + " || " + nameof(botConfig)); + return false; + } + + string json = JsonConvert.SerializeObject(botConfig, Formatting.Indented); + string newFilePath = filePath + ".new"; + + await WriteSemaphore.WaitAsync().ConfigureAwait(false); + + try { + await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); + + if (File.Exists(filePath)) { + File.Replace(newFilePath, filePath, null); + } else { + File.Move(newFilePath, filePath); + } + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + return false; + } finally { + WriteSemaphore.Release(); + } + + return true; + } + internal enum EFarmingOrder : byte { Unordered, AppIDsAscending, diff --git a/ArchiSteamFarm/BotDatabase.cs b/ArchiSteamFarm/BotDatabase.cs index 451e6d4b3..9ec49606e 100644 --- a/ArchiSteamFarm/BotDatabase.cs +++ b/ArchiSteamFarm/BotDatabase.cs @@ -46,6 +46,7 @@ namespace ArchiSteamFarm { internal MobileAuthenticator MobileAuthenticator { get; private set; } private string FilePath; + private bool ReadOnly; // This constructor is used when creating new database private BotDatabase(string filePath) { @@ -174,6 +175,16 @@ namespace ArchiSteamFarm { return botDatabase; } + internal async Task MakeReadOnly() { + await FileSemaphore.WaitAsync().ConfigureAwait(false); + + try { + ReadOnly = true; + } finally { + FileSemaphore.Release(); + } + } + internal async Task RemoveBlacklistedFromTradesSteamIDs(IReadOnlyCollection steamIDs) { if ((steamIDs == null) || (steamIDs.Count == 0)) { ASF.ArchiLogger.LogNullError(nameof(steamIDs)); @@ -226,6 +237,10 @@ namespace ArchiSteamFarm { } private async Task Save() { + if (ReadOnly) { + return; + } + string json = JsonConvert.SerializeObject(this); if (string.IsNullOrEmpty(json)) { ASF.ArchiLogger.LogNullError(nameof(json)); @@ -236,6 +251,10 @@ namespace ArchiSteamFarm { await FileSemaphore.WaitAsync().ConfigureAwait(false); + if (ReadOnly) { + return; + } + try { await File.WriteAllTextAsync(newFilePath, json).ConfigureAwait(false); diff --git a/ArchiSteamFarm/IPC.cs b/ArchiSteamFarm/IPC.cs index 3502d0835..4a5f5d249 100644 --- a/ArchiSteamFarm/IPC.cs +++ b/ArchiSteamFarm/IPC.cs @@ -20,6 +20,7 @@ // limitations under the License. using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; @@ -27,6 +28,7 @@ using System.Net; using System.Text; using System.Threading.Tasks; using ArchiSteamFarm.Localization; +using Newtonsoft.Json; namespace ArchiSteamFarm { internal static class IPC { @@ -96,66 +98,177 @@ namespace ArchiSteamFarm { httpListener.Stop(); } - private static async Task BaseResponse(this HttpListenerContext context, byte[] response, HttpStatusCode statusCode = HttpStatusCode.OK) { - if ((context == null) || (response == null) || (response.Length == 0)) { - ASF.ArchiLogger.LogNullError(nameof(context) + " || " + nameof(response)); - return; + private static async Task HandleApi(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) { + if ((request == null) || (response == null) || (arguments == null) || (argumentsIndex == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); + return false; } - try { - if (context.Response.StatusCode != (ushort) statusCode) { - context.Response.StatusCode = (ushort) statusCode; - } + if (arguments.Length <= argumentsIndex) { + return false; + } - context.Response.AppendHeader("Access-Control-Allow-Origin", "*"); - - string acceptEncoding = context.Request.Headers["Accept-Encoding"]; - - if (!string.IsNullOrEmpty(acceptEncoding)) { - if (acceptEncoding.Contains("gzip")) { - context.Response.AddHeader("Content-Encoding", "gzip"); - using (MemoryStream ms = new MemoryStream()) { - using (GZipStream stream = new GZipStream(ms, CompressionMode.Compress)) { - await stream.WriteAsync(response, 0, response.Length).ConfigureAwait(false); - } - - response = ms.ToArray(); - } - } else if (acceptEncoding.Contains("deflate")) { - context.Response.AddHeader("Content-Encoding", "deflate"); - using (MemoryStream ms = new MemoryStream()) { - using (DeflateStream stream = new DeflateStream(ms, CompressionMode.Compress)) { - await stream.WriteAsync(response, 0, response.Length).ConfigureAwait(false); - } - - response = ms.ToArray(); - } - } - } - - context.Response.ContentLength64 = response.Length; - await context.Response.OutputStream.WriteAsync(response, 0, response.Length).ConfigureAwait(false); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); + switch (arguments[argumentsIndex]) { + case "Bot/": + return await HandleApiBot(request, response, arguments, ++argumentsIndex).ConfigureAwait(false); + case "Command": + case "Command/": + return await HandleApiCommand(request, response, arguments, ++argumentsIndex).ConfigureAwait(false); + default: + return false; } } - private static async Task ExecuteCommand(HttpListenerContext context) { - if (context == null) { - ASF.ArchiLogger.LogNullError(nameof(context)); - return; + private static async Task HandleApiBot(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) { + if ((request == null) || (response == null) || (arguments == null) || (argumentsIndex == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); + return false; } - string command = context.Request.GetQueryStringValue("command"); + switch (request.HttpMethod) { + case HttpMethods.Delete: + return await HandleApiBotDelete(request, response, arguments, argumentsIndex).ConfigureAwait(false); + case HttpMethods.Get: + return await HandleApiBotGet(request, response, arguments, argumentsIndex).ConfigureAwait(false); + case HttpMethods.Post: + return await HandleApiBotPost(request, response, arguments, argumentsIndex).ConfigureAwait(false); + default: + await ResponseStatusCode(request, response, HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); + return true; + } + } + + private static async Task HandleApiBotDelete(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) { + if ((request == null) || (response == null) || (arguments == null) || (argumentsIndex == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); + return false; + } + + if (arguments.Length <= argumentsIndex) { + return false; + } + + string botName = WebUtility.UrlDecode(arguments[argumentsIndex]); + + if (!Bot.Bots.TryGetValue(botName, out Bot bot)) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.BotNotFound, botName)), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + if (!await bot.DeleteAllRelatedFiles().ConfigureAwait(false)) { + await ResponseJsonObject(request, response, new GenericResponse(false, "Removing one or more files failed, check ASF log for details"), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + await ResponseJsonObject(request, response, new GenericResponse(true, "OK")).ConfigureAwait(false); + return true; + } + + private static async Task HandleApiBotGet(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) { + if ((request == null) || (response == null) || (arguments == null) || (argumentsIndex == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); + return false; + } + + if (arguments.Length <= argumentsIndex) { + return false; + } + + string botName = WebUtility.UrlDecode(arguments[argumentsIndex]); + + if (!Bot.Bots.TryGetValue(botName, out Bot bot)) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.BotNotFound, botName)), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + await ResponseJsonObject(request, response, new GenericResponse(true, "OK", bot)).ConfigureAwait(false); + return true; + } + + private static async Task HandleApiBotPost(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) { + if ((request == null) || (response == null) || (arguments == null) || (argumentsIndex == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); + return false; + } + + if (arguments.Length <= argumentsIndex) { + return false; + } + + const string requiredContentType = "application/json"; + + if (request.ContentType != requiredContentType) { + await ResponseJsonObject(request, response, new GenericResponse(false, nameof(request.ContentType) + " must be declared as " + requiredContentType), HttpStatusCode.NotAcceptable).ConfigureAwait(false); + return true; + } + + string body; + using (StreamReader reader = new StreamReader(request.InputStream)) { + body = await reader.ReadToEndAsync().ConfigureAwait(false); + } + + if (string.IsNullOrEmpty(body)) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(body))), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + BotConfig botConfig; + + try { + botConfig = JsonConvert.DeserializeObject(body); + } catch (Exception e) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorParsingObject, nameof(botConfig)) + Environment.NewLine + e), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + string filePath = Path.Combine(SharedInfo.ConfigDirectory, WebUtility.UrlDecode(arguments[argumentsIndex]) + ".json"); + + if (!await BotConfig.Write(filePath, botConfig).ConfigureAwait(false)) { + await ResponseJsonObject(request, response, new GenericResponse(false, "Writing bot config failed, check ASF log for details"), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + await ResponseJsonObject(request, response, new GenericResponse(true, "OK")).ConfigureAwait(false); + return true; + } + + private static async Task HandleApiCommand(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) { + if ((request == null) || (response == null) || (arguments == null) || (argumentsIndex == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); + return false; + } + + switch (request.HttpMethod) { + case HttpMethods.Get: + return await HandleApiCommandGet(request, response, arguments, argumentsIndex).ConfigureAwait(false); + case HttpMethods.Post: + return await HandleApiCommandPost(request, response, arguments, argumentsIndex).ConfigureAwait(false); + default: + await ResponseStatusCode(request, response, HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); + return true; + } + } + + private static async Task HandleApiCommandGet(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) { + if ((request == null) || (response == null) || (arguments == null) || (argumentsIndex == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); + return false; + } + + if (arguments.Length <= argumentsIndex) { + return false; + } + + string command = WebUtility.UrlDecode(arguments[argumentsIndex]); if (string.IsNullOrEmpty(command)) { - await context.StringResponse(string.Format(Strings.ErrorIsEmpty, nameof(command)), statusCode: HttpStatusCode.BadRequest).ConfigureAwait(false); - return; + await ResponseText(request, response, string.Format(Strings.ErrorIsEmpty, nameof(command)), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; } Bot targetBot = Bot.Bots.OrderBy(bot => bot.Key).Select(bot => bot.Value).FirstOrDefault(); if (targetBot == null) { - await context.StringResponse(Strings.ErrorNoBotsDefined, statusCode: HttpStatusCode.BadRequest).ConfigureAwait(false); - return; + await ResponseText(request, response, Strings.ErrorNoBotsDefined, HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; } if (command[0] != '!') { @@ -164,32 +277,87 @@ namespace ArchiSteamFarm { string content = await targetBot.Response(Program.GlobalConfig.SteamOwnerID, command).ConfigureAwait(false); - ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.IPCAnswered, command, content)); - - await context.StringResponse(content).ConfigureAwait(false); + await ResponseText(request, response, content).ConfigureAwait(false); + return true; } - private static string GetQueryStringValue(this HttpListenerRequest request, string requestKey) { - if ((request == null) || string.IsNullOrEmpty(requestKey)) { - ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(requestKey)); - return null; + private static async Task HandleApiCommandPost(HttpListenerRequest request, HttpListenerResponse response, string[] arguments, byte argumentsIndex) { + if ((request == null) || (response == null) || (arguments == null) || (argumentsIndex == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); + return false; } - string result = (from string key in request.QueryString where requestKey.Equals(key) select request.QueryString[key]).FirstOrDefault(); - return result; + const string requiredContentType = "application/json"; + + if (request.ContentType != requiredContentType) { + await ResponseJsonObject(request, response, new GenericResponse(false, nameof(request.ContentType) + " must be declared as " + requiredContentType), HttpStatusCode.NotAcceptable).ConfigureAwait(false); + return true; + } + + string body; + using (StreamReader reader = new StreamReader(request.InputStream)) { + body = await reader.ReadToEndAsync().ConfigureAwait(false); + } + + if (string.IsNullOrEmpty(body)) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(body))), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + ApiCommandRequest apiCommandRequest; + + try { + apiCommandRequest = JsonConvert.DeserializeObject(body); + } catch (Exception e) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorParsingObject, nameof(apiCommandRequest)) + Environment.NewLine + e), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + string command = apiCommandRequest.Command; + + Bot targetBot = Bot.Bots.OrderBy(bot => bot.Key).Select(bot => bot.Value).FirstOrDefault(); + if (targetBot == null) { + await ResponseJsonObject(request, response, new GenericResponse(false, Strings.ErrorNoBotsDefined), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + if (command[0] != '!') { + command = "!" + command; + } + + string content = await targetBot.Response(Program.GlobalConfig.SteamOwnerID, command).ConfigureAwait(false); + + await ResponseJsonObject(request, response, new GenericResponse(true, "OK", content)).ConfigureAwait(false); + return true; } - private static async Task HandleGetRequest(HttpListenerContext context) { - if (context == null) { - ASF.ArchiLogger.LogNullError(nameof(context)); - return; + private static async Task HandleAuthenticatedRequest(HttpListenerRequest request, HttpListenerResponse response) { + if ((request == null) || (response == null)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response)); + return false; } - switch (context.Request.Url.LocalPath.ToUpperInvariant()) { - case "/IPC": - await ExecuteCommand(context).ConfigureAwait(false); - break; + if (request.Url.Segments.Length < 2) { + return await HandleMainPage(request, response).ConfigureAwait(false); } + + switch (request.Url.Segments[1]) { + case "Api/": + return await HandleApi(request, response, request.Url.Segments, 2).ConfigureAwait(false); + default: + return false; + } + } + + private static async Task HandleMainPage(HttpListenerRequest request, HttpListenerResponse response) { + if ((request == null) || (response == null)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response)); + return false; + } + + // In the future we'll probably have some friendly admin panel here, for now this is 501 + await ResponseStatusCode(request, response, HttpStatusCode.NotImplemented).ConfigureAwait(false); + return true; } private static async Task HandleRequest(HttpListenerContext context) { @@ -200,23 +368,24 @@ namespace ArchiSteamFarm { try { if (Program.GlobalConfig.SteamOwnerID == 0) { - await context.StatusCodeResponse(HttpStatusCode.Forbidden).ConfigureAwait(false); + await ResponseStatusCode(context.Request, context.Response, HttpStatusCode.Forbidden).ConfigureAwait(false); return; } - if (!string.IsNullOrEmpty(Program.GlobalConfig.IPCPassword) && (context.Request.GetQueryStringValue("password") != Program.GlobalConfig.IPCPassword)) { - await context.StatusCodeResponse(HttpStatusCode.Unauthorized).ConfigureAwait(false); - return; + if (!string.IsNullOrEmpty(Program.GlobalConfig.IPCPassword)) { + string password = context.Request.Headers.Get("Authentication"); + if (string.IsNullOrEmpty(password)) { + password = context.Request.QueryString.Get("password"); + } + + if (password != Program.GlobalConfig.IPCPassword) { + await ResponseStatusCode(context.Request, context.Response, HttpStatusCode.Unauthorized).ConfigureAwait(false); + return; + } } - switch (context.Request.HttpMethod) { - case WebRequestMethods.Http.Get: - await HandleGetRequest(context).ConfigureAwait(false); - break; - } - - if (context.Response.ContentLength64 == 0) { - await context.StatusCodeResponse(HttpStatusCode.NotFound); + if (!await HandleAuthenticatedRequest(context.Request, context.Response).ConfigureAwait(false)) { + await ResponseStatusCode(context.Request, context.Response, HttpStatusCode.NotFound).ConfigureAwait(false); } } finally { context.Response.Close(); @@ -260,30 +429,134 @@ namespace ArchiSteamFarm { } } - private static async Task StatusCodeResponse(this HttpListenerContext context, HttpStatusCode statusCode) { - if (context == null) { - ASF.ArchiLogger.LogNullError(nameof(context)); + private static async Task ResponseBase(HttpListenerRequest request, HttpListenerResponse response, byte[] content, HttpStatusCode statusCode = HttpStatusCode.OK) { + if ((request == null) || (response == null) || (content == null) || (content.Length == 0)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(content)); return; } - string content = (ushort) statusCode + " - " + statusCode; - await context.StringResponse(content, statusCode: statusCode).ConfigureAwait(false); + try { + if (response.StatusCode != (ushort) statusCode) { + response.StatusCode = (ushort) statusCode; + } + + response.AppendHeader("Access-Control-Allow-Origin", "*"); + + string acceptEncoding = request.Headers["Accept-Encoding"]; + + if (!string.IsNullOrEmpty(acceptEncoding)) { + if (acceptEncoding.Contains("gzip")) { + response.AddHeader("Content-Encoding", "gzip"); + using (MemoryStream ms = new MemoryStream()) { + using (GZipStream stream = new GZipStream(ms, CompressionMode.Compress)) { + await stream.WriteAsync(content, 0, content.Length).ConfigureAwait(false); + } + + content = ms.ToArray(); + } + } else if (acceptEncoding.Contains("deflate")) { + response.AddHeader("Content-Encoding", "deflate"); + using (MemoryStream ms = new MemoryStream()) { + using (DeflateStream stream = new DeflateStream(ms, CompressionMode.Compress)) { + await stream.WriteAsync(content, 0, content.Length).ConfigureAwait(false); + } + + content = ms.ToArray(); + } + } + } + + response.ContentLength64 = content.Length; + await response.OutputStream.WriteAsync(content, 0, content.Length).ConfigureAwait(false); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericDebuggingException(e); + } } - private static async Task StringResponse(this HttpListenerContext context, string content, string textType = "text/plain", HttpStatusCode statusCode = HttpStatusCode.OK) { - if ((context == null) || string.IsNullOrEmpty(content) || string.IsNullOrEmpty(textType)) { - ASF.ArchiLogger.LogNullError(nameof(context) + " || " + nameof(content) + " || " + nameof(textType)); + private static async Task ResponseJson(HttpListenerRequest request, HttpListenerResponse response, string json, HttpStatusCode statusCode = HttpStatusCode.OK) { + if ((request == null) || (response == null) || string.IsNullOrEmpty(json)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(json)); return; } - if (context.Response.ContentEncoding == null) { - context.Response.ContentEncoding = Encoding.UTF8; + await ResponseString(request, response, json, "text/json", statusCode).ConfigureAwait(false); + } + + private static async Task ResponseJsonObject(HttpListenerRequest request, HttpListenerResponse response, object obj, HttpStatusCode statusCode = HttpStatusCode.OK) { + if ((request == null) || (response == null) || (obj == null)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(obj)); + return; } - context.Response.ContentType = textType + "; charset=" + context.Response.ContentEncoding.WebName; + await ResponseJson(request, response, JsonConvert.SerializeObject(obj), statusCode).ConfigureAwait(false); + } - byte[] response = context.Response.ContentEncoding.GetBytes(content + Environment.NewLine); - await BaseResponse(context, response, statusCode).ConfigureAwait(false); + private static async Task ResponseStatusCode(HttpListenerRequest request, HttpListenerResponse response, HttpStatusCode statusCode) { + if ((request == null) || (response == null)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response)); + return; + } + + string text = (ushort) statusCode + " - " + statusCode; + await ResponseText(request, response, text, statusCode).ConfigureAwait(false); + } + + private static async Task ResponseString(HttpListenerRequest request, HttpListenerResponse response, string text, string textType, HttpStatusCode statusCode) { + if ((request == null) || (response == null) || string.IsNullOrEmpty(text) || string.IsNullOrEmpty(textType)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(text) + " || " + nameof(textType)); + return; + } + + if (response.ContentEncoding == null) { + response.ContentEncoding = Encoding.UTF8; + } + + response.ContentType = textType + "; charset=" + response.ContentEncoding.WebName; + + byte[] content = response.ContentEncoding.GetBytes(text + Environment.NewLine); + await ResponseBase(request, response, content, statusCode).ConfigureAwait(false); + } + + private static async Task ResponseText(HttpListenerRequest request, HttpListenerResponse response, string text, HttpStatusCode statusCode = HttpStatusCode.OK) { + if ((request == null) || (response == null) || string.IsNullOrEmpty(text)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(text)); + return; + } + + await ResponseString(request, response, text, "text/plain", statusCode).ConfigureAwait(false); + } + + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + private sealed class ApiCommandRequest { +#pragma warning disable 649 + [JsonProperty(Required = Required.Always)] + internal readonly string Command; +#pragma warning restore 649 + + private ApiCommandRequest() { } + } + + private sealed class GenericResponse { + [JsonProperty] + internal readonly string Message; + + [JsonProperty] + internal readonly object Result; + + [JsonProperty] + internal readonly bool Success; + + internal GenericResponse(bool success, string message = null, object result = null) { + Success = success; + Message = message; + Result = result; + } + } + + private static class HttpMethods { + internal const string Delete = "DELETE"; + internal const string Get = "GET"; + internal const string Post = "POST"; } } } \ No newline at end of file diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs index a4782030b..acdcc40b1 100644 --- a/ArchiSteamFarm/Localization/Strings.Designer.cs +++ b/ArchiSteamFarm/Localization/Strings.Designer.cs @@ -1062,15 +1062,6 @@ namespace ArchiSteamFarm.Localization { } } - /// - /// Looks up a localized string similar to Answered to IPC command: {0} with: {1}. - /// - internal static string IPCAnswered { - get { - return ResourceManager.GetString("IPCAnswered", resourceCulture); - } - } - /// /// Looks up a localized string similar to IPC server ready!. /// diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx index c95e9f13c..f610ecc46 100644 --- a/ArchiSteamFarm/Localization/Strings.resx +++ b/ArchiSteamFarm/Localization/Strings.resx @@ -292,10 +292,6 @@ StackTrace: Playing more than {0} games concurrently is not possible, only first {0} entries from {1} will be used! {0} will be replaced by max number of games, {1} will be replaced by name of the configuration property - - Answered to IPC command: {0} with: {1} - {0} will be replaced by IPC command, {1} will be replaced by IPC answer - IPC server ready!