From 37564d0cfee94dd412e7bab8001ebad83b02197d Mon Sep 17 00:00:00 2001 From: JustArchi Date: Thu, 22 Feb 2018 16:08:50 +0100 Subject: [PATCH] Add GlobalConfig to /Api/ASF, and POST /Api/ASF, #750 --- ArchiSteamFarm/BotDatabase.cs | 8 ++++ ArchiSteamFarm/GlobalConfig.cs | 33 +++++++++++++ ArchiSteamFarm/IPC.cs | 85 ++++++++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 5 deletions(-) diff --git a/ArchiSteamFarm/BotDatabase.cs b/ArchiSteamFarm/BotDatabase.cs index 78df1df6e..ebfbd29e7 100644 --- a/ArchiSteamFarm/BotDatabase.cs +++ b/ArchiSteamFarm/BotDatabase.cs @@ -201,9 +201,17 @@ namespace ArchiSteamFarm { } internal async Task MakeReadOnly() { + if (ReadOnly) { + return; + } + await FileSemaphore.WaitAsync().ConfigureAwait(false); try { + if (ReadOnly) { + return; + } + ReadOnly = true; } finally { FileSemaphore.Release(); diff --git a/ArchiSteamFarm/GlobalConfig.cs b/ArchiSteamFarm/GlobalConfig.cs index d51ae3e15..107f9ab09 100644 --- a/ArchiSteamFarm/GlobalConfig.cs +++ b/ArchiSteamFarm/GlobalConfig.cs @@ -23,6 +23,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading; +using System.Threading.Tasks; using ArchiSteamFarm.Localization; using Newtonsoft.Json; using SteamKit2; @@ -36,6 +38,8 @@ namespace ArchiSteamFarm { internal static readonly HashSet SalesBlacklist = new HashSet { 267420, 303700, 335590, 368020, 425280, 480730, 566020, 639900, 762800 }; // Steam Summer/Winter sales + private static readonly SemaphoreSlim WriteSemaphore = new SemaphoreSlim(1, 1); + [JsonProperty(Required = Required.DisallowNull)] internal readonly bool AutoRestart = true; @@ -170,6 +174,35 @@ namespace ArchiSteamFarm { return result; } + internal static async Task Write(string filePath, GlobalConfig globalConfig) { + if (string.IsNullOrEmpty(filePath) || (globalConfig == null)) { + ASF.ArchiLogger.LogNullError(nameof(filePath) + " || " + nameof(globalConfig)); + return false; + } + + string json = JsonConvert.SerializeObject(globalConfig, 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 EOptimizationMode : byte { MaxPerformance, MinMemoryUsage diff --git a/ArchiSteamFarm/IPC.cs b/ArchiSteamFarm/IPC.cs index 5078be631..cd84db78b 100644 --- a/ArchiSteamFarm/IPC.cs +++ b/ArchiSteamFarm/IPC.cs @@ -43,9 +43,9 @@ namespace ArchiSteamFarm { private static readonly HashSet CompressableContentTypes = new HashSet { "application/javascript", + "application/json", "text/css", "text/html", - "text/json", "text/plain" }; @@ -55,7 +55,7 @@ namespace ArchiSteamFarm { { ".ico", "image/x-icon" }, { ".jpg", "image/jpeg" }, { ".js", "application/javascript" }, - { ".json", "text/json" }, + { ".json", "application/json" }, { ".png", "image/png" }, { ".txt", "text/plain" } }; @@ -174,6 +174,8 @@ namespace ArchiSteamFarm { switch (request.HttpMethod) { case HttpMethods.Get: return await HandleApiASFGet(request, response, arguments, argumentsIndex).ConfigureAwait(false); + case HttpMethods.Post: + return await HandleApiASFPost(request, response, arguments, argumentsIndex).ConfigureAwait(false); default: await ResponseStatusCode(request, response, HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); return true; @@ -194,12 +196,60 @@ namespace ArchiSteamFarm { processStartTime = process.StartTime; } - ASFResponse asfResponse = new ASFResponse(memoryUsage, processStartTime, SharedInfo.Version); + ASFResponse asfResponse = new ASFResponse(Program.GlobalConfig, memoryUsage, processStartTime, SharedInfo.Version); await ResponseJsonObject(request, response, new GenericResponse(true, "OK", asfResponse)).ConfigureAwait(false); return true; } + private static async Task HandleApiASFPost(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; + } + + 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; + } + + ASFRequest jsonRequest; + + try { + jsonRequest = JsonConvert.DeserializeObject(body); + } catch (Exception e) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorParsingObject, nameof(jsonRequest)) + Environment.NewLine + e), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + if (jsonRequest == null) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorObjectIsNull, nameof(jsonRequest))), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + + string filePath = Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalConfigFileName); + + if (!await GlobalConfig.Write(filePath, jsonRequest.GlobalConfig).ConfigureAwait(false)) { + await ResponseJsonObject(request, response, new GenericResponse(false, "Writing global 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 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)); @@ -320,6 +370,11 @@ namespace ArchiSteamFarm { return true; } + if (jsonRequest == null) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorObjectIsNull, nameof(jsonRequest))), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + string botName = WebUtility.UrlDecode(arguments[argumentsIndex]); if (jsonRequest.KeepSensitiveDetails && Bot.Bots.TryGetValue(botName, out Bot bot)) { @@ -459,6 +514,11 @@ namespace ArchiSteamFarm { return true; } + if (jsonRequest == null) { + await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorObjectIsNull, nameof(jsonRequest))), HttpStatusCode.BadRequest).ConfigureAwait(false); + return true; + } + if (jsonRequest.GamesToRedeemInBackground.Count == 0) { await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(jsonRequest.GamesToRedeemInBackground))), HttpStatusCode.BadRequest).ConfigureAwait(false); return true; @@ -932,7 +992,21 @@ namespace ArchiSteamFarm { await ResponseString(request, response, text, "text/plain", statusCode).ConfigureAwait(false); } + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + private sealed class ASFRequest { +#pragma warning disable 649 + [JsonProperty(Required = Required.Always)] + internal readonly GlobalConfig GlobalConfig; +#pragma warning restore 649 + + // Deserialized from JSON + private ASFRequest() { } + } + private sealed class ASFResponse { + [JsonProperty] + private readonly GlobalConfig GlobalConfig; + [JsonProperty] private readonly uint MemoryUsage; @@ -942,11 +1016,12 @@ namespace ArchiSteamFarm { [JsonProperty] private readonly Version Version; - internal ASFResponse(uint memoryUsage, DateTime processStartTime, Version version) { - if ((memoryUsage == 0) || (processStartTime == DateTime.MinValue) || (version == null)) { + internal ASFResponse(GlobalConfig globalConfig, uint memoryUsage, DateTime processStartTime, Version version) { + if ((globalConfig == null) || (memoryUsage == 0) || (processStartTime == DateTime.MinValue) || (version == null)) { throw new ArgumentNullException(nameof(memoryUsage) + " || " + nameof(processStartTime) + " || " + nameof(version)); } + GlobalConfig = globalConfig; MemoryUsage = memoryUsage; ProcessStartTime = processStartTime; Version = version;