diff --git a/ArchiSteamFarm.sln.DotSettings b/ArchiSteamFarm.sln.DotSettings index 97b4c09dd..24d3d074d 100644 --- a/ArchiSteamFarm.sln.DotSettings +++ b/ArchiSteamFarm.sln.DotSettings @@ -197,6 +197,8 @@ False False True + True + 1 END_OF_LINE NEVER @@ -528,4 +530,8 @@ limitations under the License. True True True - 8 \ No newline at end of file + 8 + True + True + True + True \ No newline at end of file diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index 6731b5aff..79d1444bf 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -40,9 +40,17 @@ + + + + + + + + diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index d10f1457c..7704e6747 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -40,7 +40,7 @@ using SteamKit2.Discovery; using SteamKit2.Unified.Internal; namespace ArchiSteamFarm { - internal sealed class Bot : IDisposable { + public sealed class Bot : IDisposable { internal const ushort CallbackSleep = 500; // In miliseconds internal const ushort MaxMessagePrefixLength = MaxMessageLength - ReservedMessageLength - 2; // 2 for a minimum of 2 characters (escape one and real one) internal const byte MinPlayingBlockedTTL = 60; // Delay in seconds added when account was occupied during our disconnect, to not disconnect other Steam client session too soon diff --git a/ArchiSteamFarm/GlobalConfig.cs b/ArchiSteamFarm/GlobalConfig.cs index 805a47a1f..dc8f8a2c3 100644 --- a/ArchiSteamFarm/GlobalConfig.cs +++ b/ArchiSteamFarm/GlobalConfig.cs @@ -32,7 +32,7 @@ using SteamKit2; namespace ArchiSteamFarm { [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - internal sealed class GlobalConfig { + public sealed class GlobalConfig { private const bool DefaultAutoRestart = true; private const string DefaultCommandPrefix = "!"; private const byte DefaultConfirmationsLimiterDelay = 10; @@ -46,7 +46,6 @@ namespace ArchiSteamFarm { private const byte DefaultInventoryLimiterDelay = 3; private const bool DefaultIPC = false; private const string DefaultIPCPassword = null; - private const ushort DefaultIPCPort = 1242; private const byte DefaultLoginLimiterDelay = 10; private const byte DefaultMaxFarmingTime = 10; private const byte DefaultMaxTradeHoldDuration = 15; @@ -63,7 +62,6 @@ namespace ArchiSteamFarm { private const string DefaultWebProxyUsername = null; internal static readonly ImmutableHashSet SalesBlacklist = ImmutableHashSet.Create(267420, 303700, 335590, 368020, 425280, 480730, 566020, 639900, 762800, 876740); - private static readonly ImmutableHashSet DefaultIPCPrefixes = ImmutableHashSet.Create("http://127.0.0.1:" + DefaultIPCPort + "/"); private static readonly ImmutableHashSet DefaultBlacklist = ImmutableHashSet.Create(); private static readonly SemaphoreSlim WriteSemaphore = new SemaphoreSlim(1, 1); @@ -110,9 +108,6 @@ namespace ArchiSteamFarm { [JsonProperty] internal readonly string IPCPassword = DefaultIPCPassword; - [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace, Required = Required.DisallowNull)] - internal readonly ImmutableHashSet IPCPrefixes = DefaultIPCPrefixes; - [JsonProperty(Required = Required.DisallowNull)] internal readonly byte LoginLimiterDelay = DefaultLoginLimiterDelay; @@ -336,7 +331,6 @@ namespace ArchiSteamFarm { public bool ShouldSerializeInventoryLimiterDelay() => ShouldSerializeEverything || (InventoryLimiterDelay != DefaultInventoryLimiterDelay); public bool ShouldSerializeIPC() => ShouldSerializeEverything || (IPC != DefaultIPC); public bool ShouldSerializeIPCPassword() => ShouldSerializeEverything || (IPCPassword != DefaultIPCPassword); - public bool ShouldSerializeIPCPrefixes() => ShouldSerializeEverything || ((IPCPrefixes != DefaultIPCPrefixes) && !IPCPrefixes.SetEquals(DefaultIPCPrefixes)); public bool ShouldSerializeLoginLimiterDelay() => ShouldSerializeEverything || (LoginLimiterDelay != DefaultLoginLimiterDelay); public bool ShouldSerializeMaxFarmingTime() => ShouldSerializeEverything || (MaxFarmingTime != DefaultMaxFarmingTime); public bool ShouldSerializeMaxTradeHoldDuration() => ShouldSerializeEverything || (MaxTradeHoldDuration != DefaultMaxTradeHoldDuration); diff --git a/ArchiSteamFarm/IPC.cs b/ArchiSteamFarm/IPC.cs deleted file mode 100644 index 6cc0997c8..000000000 --- a/ArchiSteamFarm/IPC.cs +++ /dev/null @@ -1,1359 +0,0 @@ -// _ _ _ ____ _ _____ -// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ -// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ -// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | -// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| -// -// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki -// Contact: JustArchi@JustArchi.net -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; -using System.Net.WebSockets; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using ArchiSteamFarm.Localization; -using Newtonsoft.Json; - -namespace ArchiSteamFarm { - internal static class IPC { - private const byte FailedAuthorizationsCooldown = 1; // In hours - private const byte MaxFailedAuthorizationAttempts = 5; - - internal static bool IsRunning => IsHandlingRequests || IsListening; - - private static readonly ConcurrentDictionary ActiveLogWebSockets = new ConcurrentDictionary(); - private static readonly SemaphoreSlim AuthorizationSemaphore = new SemaphoreSlim(1, 1); - - private static readonly HashSet CompressableContentTypes = new HashSet { - "application/javascript", - "application/json", - "text/css", - "text/html", - "text/plain" - }; - - private static readonly ConcurrentDictionary FailedAuthorizations = new ConcurrentDictionary(); - - private static readonly Dictionary MimeTypes = new Dictionary(8) { - { ".css", "text/css" }, - { ".html", "text/html" }, - { ".ico", "image/x-icon" }, - { ".jpg", "image/jpeg" }, - { ".js", "application/javascript" }, - { ".json", "application/json" }, - { ".png", "image/png" }, - { ".txt", "text/plain" } - }; - - private static bool IsListening { - get { - try { - return HttpListener?.IsListening == true; - } catch (ObjectDisposedException) { - // HttpListener can dispose itself on error - return false; - } - } - } - - private static Timer ClearFailedAuthorizationsTimer; - private static HistoryTarget HistoryTarget; - private static HttpListener HttpListener; - private static bool IsHandlingRequests; - - internal static void OnNewHistoryTarget(HistoryTarget historyTarget = null) { - if (HistoryTarget != null) { - HistoryTarget.NewHistoryEntry -= OnNewHistoryEntry; - HistoryTarget = null; - } - - if (historyTarget != null) { - historyTarget.NewHistoryEntry += OnNewHistoryEntry; - HistoryTarget = historyTarget; - } - } - - internal static void Start(IReadOnlyCollection prefixes) { - if ((prefixes == null) || (prefixes.Count == 0)) { - ASF.ArchiLogger.LogNullError(nameof(prefixes)); - return; - } - - if (!HttpListener.IsSupported) { - ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningFailedWithError, "!HttpListener.IsSupported")); - return; - } - - if (IsListening) { - return; - } - - HttpListener = new HttpListener { IgnoreWriteExceptions = true }; - - try { - foreach (string prefix in prefixes) { - ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.IPCStarting, prefix)); - HttpListener.Prefixes.Add(prefix); - } - - HttpListener.Start(); - } catch (Exception e) { - // HttpListener can dispose itself on error, so don't keep it around - HttpListener = null; - ASF.ArchiLogger.LogGenericException(e); - return; - } - - if (ClearFailedAuthorizationsTimer == null) { - ClearFailedAuthorizationsTimer = new Timer( - e => FailedAuthorizations.Clear(), - null, - TimeSpan.FromHours(FailedAuthorizationsCooldown), // Delay - TimeSpan.FromHours(FailedAuthorizationsCooldown) // Period - ); - } - - Logging.InitHistoryLogger(); - Utilities.InBackground(HandleRequests, true); - ASF.ArchiLogger.LogGenericInfo(Strings.IPCReady); - } - - internal static void Stop() { - if (!HttpListener.IsSupported) { - ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningFailedWithError, "!HttpListener.IsSupported")); - return; - } - - if (!IsListening) { - return; - } - - if (ClearFailedAuthorizationsTimer != null) { - ClearFailedAuthorizationsTimer.Dispose(); - ClearFailedAuthorizationsTimer = null; - } - - // We must set HttpListener to null before stopping it, so HandleRequests() knows that exception is expected - HttpListener httpListener = HttpListener; - HttpListener = null; - - httpListener.Stop(); - } - - private static async Task HandleApi(HttpListenerContext context, string[] arguments, byte argumentsIndex) { - if ((context == null) || (arguments == null) || (argumentsIndex == 0)) { - ASF.ArchiLogger.LogNullError(nameof(context) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); - return false; - } - - if (arguments.Length <= argumentsIndex) { - return false; - } - - switch (arguments[argumentsIndex]) { - case "ASF" when context.Request.HttpMethod == HttpMethods.Get: - return await HandleApiASFGet(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "ASF" when context.Request.HttpMethod == HttpMethods.Post: - return await HandleApiASFPost(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "Bot/" when context.Request.HttpMethod == HttpMethods.Delete: - return await HandleApiBotDelete(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "Bot/" when context.Request.HttpMethod == HttpMethods.Get: - return await HandleApiBotGet(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "Bot/" when context.Request.HttpMethod == HttpMethods.Post: - return await HandleApiBotPost(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "Command/" when context.Request.HttpMethod == HttpMethods.Post: - return await HandleApiCommandPost(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "GamesToRedeemInBackground/" when context.Request.HttpMethod == HttpMethods.Delete: - return await HandleApiGamesToRedeemInBackgroundDelete(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "GamesToRedeemInBackground/" when context.Request.HttpMethod == HttpMethods.Get: - return await HandleApiGamesToRedeemInBackgroundGet(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "GamesToRedeemInBackground/" when context.Request.HttpMethod == HttpMethods.Post: - return await HandleApiGamesToRedeemInBackgroundPost(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "Log" when context.Request.HttpMethod == HttpMethods.Get: - return await HandleApiLogGet(context, arguments, ++argumentsIndex).ConfigureAwait(false); - case "Structure/" when context.Request.HttpMethod == HttpMethods.Get: - return await HandleApiStructureGet(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "Type/" when context.Request.HttpMethod == HttpMethods.Get: - return await HandleApiTypeGet(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "WWW/" when arguments.Length > ++argumentsIndex: - switch (arguments[argumentsIndex]) { - case "Directory/" when context.Request.HttpMethod == HttpMethods.Get: - return await HandleApiWWWDirectoryGet(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - case "Send" when context.Request.HttpMethod == HttpMethods.Post: - return await HandleApiWWWSendPost(context.Request, context.Response, arguments, ++argumentsIndex).ConfigureAwait(false); - default: - return false; - } - default: - return false; - } - } - - private static async Task HandleApiASFGet(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; - } - - uint memoryUsage = (uint) GC.GetTotalMemory(false) / 1024; - - DateTime processStartTime; - - using (Process process = Process.GetCurrentProcess()) { - processStartTime = process.StartTime; - } - - ASFResponse asfResponse = new ASFResponse(SharedInfo.BuildInfo.Variant, 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 (string.IsNullOrEmpty(request.ContentType) || ((request.ContentType != requiredContentType) && !request.ContentType.StartsWith(requiredContentType + ";", StringComparison.Ordinal))) { - 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; - } - - if (jsonRequest.KeepSensitiveDetails) { - if (string.IsNullOrEmpty(jsonRequest.GlobalConfig.WebProxyPassword) && !string.IsNullOrEmpty(Program.GlobalConfig.WebProxyPassword)) { - jsonRequest.GlobalConfig.WebProxyPassword = Program.GlobalConfig.WebProxyPassword; - } - } - - jsonRequest.GlobalConfig.ShouldSerializeEverything = false; - - 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 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 argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - - HashSet bots = Bot.GetBots(argument); - if ((bots == null) || (bots.Count == 0)) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.BotNotFound, argument)), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - IEnumerable> tasks = bots.Select(bot => bot.DeleteAllRelatedFiles()); - ICollection results; - - switch (Program.GlobalConfig.OptimizationMode) { - case GlobalConfig.EOptimizationMode.MinMemoryUsage: - results = new List(bots.Count); - foreach (Task task in tasks) { - results.Add(await task.ConfigureAwait(false)); - } - - break; - default: - results = await Task.WhenAll(tasks).ConfigureAwait(false); - break; - } - - if (results.Any(result => !result)) { - 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 argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - - HashSet bots = Bot.GetBots(argument); - if ((bots == null) || (bots.Count == 0)) { - await ResponseJsonObject(request, response, new GenericResponse>(false, string.Format(Strings.BotNotFound, argument)), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - await ResponseJsonObject(request, response, new GenericResponse>(true, "OK", bots)).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 (string.IsNullOrEmpty(request.ContentType) || ((request.ContentType != requiredContentType) && !request.ContentType.StartsWith(requiredContentType + ";", StringComparison.Ordinal))) { - 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; - } - - BotRequest 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 botName = WebUtility.UrlDecode(arguments[argumentsIndex]); - - if (jsonRequest.KeepSensitiveDetails && Bot.Bots.TryGetValue(botName, out Bot bot)) { - if (string.IsNullOrEmpty(jsonRequest.BotConfig.SteamLogin) && !string.IsNullOrEmpty(bot.BotConfig.SteamLogin)) { - jsonRequest.BotConfig.SteamLogin = bot.BotConfig.SteamLogin; - } - - if (string.IsNullOrEmpty(jsonRequest.BotConfig.SteamParentalPIN) && !string.IsNullOrEmpty(bot.BotConfig.SteamParentalPIN)) { - jsonRequest.BotConfig.SteamParentalPIN = bot.BotConfig.SteamParentalPIN; - } - - if (string.IsNullOrEmpty(jsonRequest.BotConfig.SteamPassword) && !string.IsNullOrEmpty(bot.BotConfig.SteamPassword)) { - jsonRequest.BotConfig.SteamPassword = bot.BotConfig.SteamPassword; - } - } - - jsonRequest.BotConfig.ShouldSerializeEverything = false; - - string filePath = Path.Combine(SharedInfo.ConfigDirectory, botName + SharedInfo.ConfigExtension); - - if (!await BotConfig.Write(filePath, jsonRequest.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 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; - } - - if (arguments.Length <= argumentsIndex) { - return false; - } - - string argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - if (string.IsNullOrEmpty(argument)) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(argument))), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - if (Program.GlobalConfig.SteamOwnerID == 0) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, nameof(Program.GlobalConfig.SteamOwnerID))), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - 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 (!string.IsNullOrEmpty(Program.GlobalConfig.CommandPrefix) && !argument.StartsWith(Program.GlobalConfig.CommandPrefix, StringComparison.Ordinal)) { - argument = Program.GlobalConfig.CommandPrefix + argument; - } - - string content = await targetBot.Response(Program.GlobalConfig.SteamOwnerID, argument).ConfigureAwait(false); - - await ResponseJsonObject(request, response, new GenericResponse(true, "OK", content)).ConfigureAwait(false); - return true; - } - - private static async Task HandleApiGamesToRedeemInBackgroundDelete(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 argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - - HashSet bots = Bot.GetBots(argument); - if ((bots == null) || (bots.Count == 0)) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.BotNotFound, argument)), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - IEnumerable> tasks = bots.Select(bot => Task.Run(() => bot.DeleteRedeemedKeysFiles())); - ICollection results; - - switch (Program.GlobalConfig.OptimizationMode) { - case GlobalConfig.EOptimizationMode.MinMemoryUsage: - results = new List(bots.Count); - foreach (Task task in tasks) { - results.Add(await task.ConfigureAwait(false)); - } - - break; - default: - results = await Task.WhenAll(tasks).ConfigureAwait(false); - break; - } - - if (results.Any(result => !result)) { - 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 HandleApiGamesToRedeemInBackgroundGet(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 argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - - HashSet bots = Bot.GetBots(argument); - if ((bots == null) || (bots.Count == 0)) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.BotNotFound, argument)), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - IEnumerable<(string BotName, Task<(Dictionary UnusedKeys, Dictionary UsedKeys)> Task)> tasks = bots.Select(bot => (bot.BotName, bot.GetUsedAndUnusedKeys())); - ICollection<(string BotName, (Dictionary UnusedKeys, Dictionary UsedKeys))> results; - - switch (Program.GlobalConfig.OptimizationMode) { - case GlobalConfig.EOptimizationMode.MinMemoryUsage: - results = new List<(string BotName, (Dictionary UnusedKeys, Dictionary UsedKeys))>(bots.Count); - foreach ((string botName, Task<(Dictionary UnusedKeys, Dictionary UsedKeys)> task) in tasks) { - results.Add((botName, await task.ConfigureAwait(false))); - } - - break; - default: - results = await Task.WhenAll(tasks.Select(async task => (task.BotName, await task.Task.ConfigureAwait(false)))).ConfigureAwait(false); - - break; - } - - Dictionary jsonResponse = new Dictionary(); - - foreach ((string botName, (Dictionary UnusedKeys, Dictionary UsedKeys) taskResult) in results) { - jsonResponse[botName] = new GamesToRedeemInBackgroundResponse(taskResult.UnusedKeys, taskResult.UsedKeys); - } - - await ResponseJsonObject(request, response, new GenericResponse>(true, "OK", jsonResponse)).ConfigureAwait(false); - return true; - } - - private static async Task HandleApiGamesToRedeemInBackgroundPost(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 argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - - if (!Bot.Bots.TryGetValue(argument, out Bot bot)) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.BotNotFound, argument)), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - const string requiredContentType = "application/json"; - - if (string.IsNullOrEmpty(request.ContentType) || ((request.ContentType != requiredContentType) && !request.ContentType.StartsWith(requiredContentType + ";", StringComparison.Ordinal))) { - 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; - } - - GamesToRedeemInBackgroundRequest 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; - } - - 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; - } - - await bot.ValidateAndAddGamesToRedeemInBackground(jsonRequest.GamesToRedeemInBackground).ConfigureAwait(false); - - await ResponseJsonObject(request, response, new GenericResponse(true, "OK", jsonRequest.GamesToRedeemInBackground)).ConfigureAwait(false); - return true; - } - - private static async Task HandleApiLogGet(HttpListenerContext context, string[] arguments, byte argumentsIndex) { - if ((context == null) || (arguments == null) || (argumentsIndex == 0)) { - ASF.ArchiLogger.LogNullError(nameof(context) + " || " + nameof(arguments) + " || " + nameof(argumentsIndex)); - return false; - } - - if (!context.Request.IsWebSocketRequest) { - await ResponseStatusCode(context.Request, context.Response, HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); - return true; - } - - try { - HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null).ConfigureAwait(false); - - SemaphoreSlim sendSemaphore = new SemaphoreSlim(1, 1); - if (!ActiveLogWebSockets.TryAdd(webSocketContext.WebSocket, sendSemaphore)) { - sendSemaphore.Dispose(); - return true; - } - - try { - // Push initial history if available - if (HistoryTarget != null) { - await Task.WhenAll(HistoryTarget.ArchivedMessages.Select(archivedMessage => PostLoggedMessageUpdate(webSocketContext.WebSocket, sendSemaphore, archivedMessage))).ConfigureAwait(false); - } - - while (webSocketContext.WebSocket.State == WebSocketState.Open) { - WebSocketReceiveResult result = await webSocketContext.WebSocket.ReceiveAsync(new byte[0], CancellationToken.None).ConfigureAwait(false); - - if (result.MessageType != WebSocketMessageType.Close) { - await webSocketContext.WebSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "You're not supposed to be sending any message but Close!", CancellationToken.None).ConfigureAwait(false); - break; - } - - await webSocketContext.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); - break; - } - } finally { - if (ActiveLogWebSockets.TryRemove(webSocketContext.WebSocket, out SemaphoreSlim closedSemaphore)) { - await closedSemaphore.WaitAsync().ConfigureAwait(false); // Ensure that our semaphore is truly closed by now - closedSemaphore.Dispose(); - } - } - - return true; - } catch (HttpListenerException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - return true; - } catch (WebSocketException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - return true; - } - } - - private static async Task HandleApiStructureGet(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 argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - Type targetType = Type.GetType(argument); - - if (targetType == null) { - // We can try one more time by trying to smartly guess the assembly name from the namespace, this will work for custom libraries like SteamKit2 - int index = argument.IndexOf('.'); - - if ((index <= 0) || (index >= argument.Length - 1)) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, nameof(argument))), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - targetType = Type.GetType(argument + "," + argument.Substring(0, index)); - - if (targetType == null) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, nameof(argument))), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - } - - object obj; - - try { - obj = Activator.CreateInstance(targetType, true); - } catch (Exception e) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorParsingObject, targetType) + Environment.NewLine + e), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - await ResponseJsonObject(request, response, new GenericResponse(true, "OK", obj)).ConfigureAwait(false); - return true; - } - - private static async Task HandleApiTypeGet(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 argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - Type targetType = Type.GetType(argument); - - if (targetType == null) { - // We can try one more time by trying to smartly guess the assembly name from the namespace, this will work for custom libraries like SteamKit2 - int index = argument.IndexOf('.'); - - if ((index <= 0) || (index >= argument.Length - 1)) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, nameof(argument))), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - targetType = Type.GetType(argument + "," + argument.Substring(0, index)); - - if (targetType == null) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, nameof(argument))), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - } - - string baseType = targetType.BaseType?.GetUnifiedName(); - HashSet customAttributes = targetType.CustomAttributes.Select(attribute => attribute.AttributeType.GetUnifiedName()).ToHashSet(); - string underlyingType = null; - - Dictionary body = new Dictionary(); - - if (targetType.IsClass) { - foreach (FieldInfo field in targetType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(field => !field.IsPrivate)) { - JsonPropertyAttribute jsonProperty = field.GetCustomAttribute(); - - if (jsonProperty != null) { - body[jsonProperty.PropertyName ?? field.Name] = field.FieldType.GetUnifiedName(); - } - } - - foreach (PropertyInfo property in targetType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(property => property.CanRead && !property.GetMethod.IsPrivate)) { - JsonPropertyAttribute jsonProperty = property.GetCustomAttribute(); - - if (jsonProperty != null) { - body[jsonProperty.PropertyName ?? property.Name] = property.PropertyType.GetUnifiedName(); - } - } - } else if (targetType.IsEnum) { - Type enumType = Enum.GetUnderlyingType(targetType); - underlyingType = enumType.GetUnifiedName(); - - foreach (object value in Enum.GetValues(targetType)) { - body[value.ToString()] = Convert.ChangeType(value, enumType).ToString(); - } - } - - TypeResponse.TypeProperties properties = new TypeResponse.TypeProperties(baseType, customAttributes.Count > 0 ? customAttributes : null, underlyingType); - - await ResponseJsonObject(request, response, new GenericResponse(true, "OK", new TypeResponse(body, properties))).ConfigureAwait(false); - return true; - } - - private static async Task HandleApiWWWDirectoryGet(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 argument = WebUtility.UrlDecode(string.Join("", arguments.Skip(argumentsIndex))); - - string directory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.WebsiteDirectory, argument); - if (!Directory.Exists(directory)) { - await ResponseJsonObject(request, response, new GenericResponse>(false, string.Format(Strings.ErrorIsInvalid, nameof(directory))), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - string[] files; - - try { - files = Directory.GetFiles(directory); - } catch (Exception e) { - await ResponseJsonObject(request, response, new GenericResponse>(false, string.Format(Strings.ErrorParsingObject, nameof(files)) + Environment.NewLine + e), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - HashSet result = files.Select(Path.GetFileName).ToHashSet(); - - await ResponseJsonObject(request, response, new GenericResponse>(true, "OK", result)).ConfigureAwait(false); - return true; - } - - private static async Task HandleApiWWWSendPost(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 (string.IsNullOrEmpty(request.ContentType) || ((request.ContentType != requiredContentType) && !request.ContentType.StartsWith(requiredContentType + ";", StringComparison.Ordinal))) { - 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; - } - - WWWSendRequest 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; - } - - if (string.IsNullOrEmpty(jsonRequest.URL) || !Uri.TryCreate(jsonRequest.URL, UriKind.Absolute, out Uri uri) || !uri.Scheme.Equals(Uri.UriSchemeHttps)) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, nameof(jsonRequest.URL))), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - WebBrowser.HtmlDocumentResponse urlResponse = await Program.WebBrowser.UrlGetToHtmlDocument(jsonRequest.URL).ConfigureAwait(false); - if (urlResponse?.Content == null) { - await ResponseJsonObject(request, response, new GenericResponse(false, string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries)), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - await ResponseJsonObject(request, response, new GenericResponse(true, "OK", urlResponse.Content.DocumentNode.InnerHtml)).ConfigureAwait(false); - return true; - } - - private static async Task HandleFile(HttpListenerRequest request, HttpListenerResponse response, string absolutePath) { - if ((request == null) || (response == null) || string.IsNullOrEmpty(absolutePath)) { - ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(absolutePath)); - return false; - } - - switch (request.HttpMethod) { - case HttpMethods.Get: - return await HandleFileGet(request, response, absolutePath).ConfigureAwait(false); - default: - await ResponseStatusCode(request, response, HttpStatusCode.MethodNotAllowed).ConfigureAwait(false); - return true; - } - } - - private static async Task HandleFileGet(HttpListenerRequest request, HttpListenerResponse response, string absolutePath) { - if ((request == null) || (response == null) || string.IsNullOrEmpty(absolutePath)) { - ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(absolutePath)); - return false; - } - - string filePath = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.WebsiteDirectory) + Path.DirectorySeparatorChar + absolutePath.Replace('/', Path.DirectorySeparatorChar); - if (Directory.Exists(filePath)) { - filePath = Path.Combine(filePath, "index.html"); - } - - if (!File.Exists(filePath)) { - return false; - } - - await ResponseFile(request, response, filePath).ConfigureAwait(false); - return true; - } - - private static async Task HandleRequest(HttpListenerContext context) { - if (context == null) { - ASF.ArchiLogger.LogNullError(nameof(context)); - return; - } - - try { - bool handled; - - if ((context.Request.Url.Segments.Length >= 2) && context.Request.Url.Segments[1].Equals("Api/")) { - if (!await IsAuthorized(context).ConfigureAwait(false)) { - return; - } - - handled = await HandleApi(context, context.Request.Url.Segments, 2).ConfigureAwait(false); - } else { - handled = await HandleFile(context.Request, context.Response, context.Request.Url.AbsolutePath).ConfigureAwait(false); - } - - if (!handled) { - await ResponseStatusCode(context.Request, context.Response, HttpStatusCode.NotFound).ConfigureAwait(false); - } - } finally { - context.Response.Close(); - } - } - - private static async Task HandleRequests() { - if (IsHandlingRequests) { - return; - } - - IsHandlingRequests = true; - - try { - while (IsListening) { - Task task = HttpListener?.GetContextAsync(); - if (task == null) { - return; - } - - HttpListenerContext context; - - try { - context = await task.ConfigureAwait(false); - } catch (HttpListenerException e) { - // If HttpListener is null then we're stopping HttpListener, so this exception is expected, ignore it - if (HttpListener == null) { - return; - } - - // Otherwise this is an error, and HttpListener can dispose itself in this situation, so don't keep it around - HttpListener = null; - ASF.ArchiLogger.LogGenericException(e); - return; - } - - Utilities.InBackground(() => HandleRequest(context)); - } - } catch (ObjectDisposedException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } finally { - IsHandlingRequests = false; - } - } - - private static async Task IsAuthorized(HttpListenerContext context) { - if (string.IsNullOrEmpty(Program.GlobalConfig.IPCPassword)) { - return true; - } - - IPAddress ipAddress = context.Request.RemoteEndPoint?.Address; - - bool authorized; - - if (ipAddress != null) { - if (FailedAuthorizations.TryGetValue(ipAddress, out byte attempts)) { - if (attempts >= MaxFailedAuthorizationAttempts) { - await ResponseStatusCode(context.Request, context.Response, HttpStatusCode.Forbidden).ConfigureAwait(false); - return false; - } - } - - await AuthorizationSemaphore.WaitAsync().ConfigureAwait(false); - - try { - if (FailedAuthorizations.TryGetValue(ipAddress, out attempts)) { - if (attempts >= MaxFailedAuthorizationAttempts) { - await ResponseStatusCode(context.Request, context.Response, HttpStatusCode.Forbidden).ConfigureAwait(false); - return false; - } - } - - string password = context.Request.Headers.Get("Authentication"); - if (string.IsNullOrEmpty(password)) { - password = context.Request.QueryString.Get("password"); - } - - authorized = password == Program.GlobalConfig.IPCPassword; - - if (authorized) { - FailedAuthorizations.TryRemove(ipAddress, out _); - } else { - FailedAuthorizations[ipAddress] = FailedAuthorizations.TryGetValue(ipAddress, out attempts) ? ++attempts : (byte) 1; - } - } finally { - AuthorizationSemaphore.Release(); - } - } else { - string password = context.Request.Headers.Get("Authentication"); - if (string.IsNullOrEmpty(password)) { - password = context.Request.QueryString.Get("password"); - } - - authorized = password == Program.GlobalConfig.IPCPassword; - } - - if (authorized) { - return true; - } - - await ResponseStatusCode(context.Request, context.Response, HttpStatusCode.Unauthorized).ConfigureAwait(false); - return false; - } - - private static async void OnNewHistoryEntry(object sender, HistoryTarget.NewHistoryEntryArgs newHistoryEntryArgs) { - if ((sender == null) || (newHistoryEntryArgs == null)) { - ASF.ArchiLogger.LogNullError(nameof(sender) + " || " + nameof(newHistoryEntryArgs)); - return; - } - - if (ActiveLogWebSockets.Count == 0) { - return; - } - - string json = JsonConvert.SerializeObject(new GenericResponse(true, "OK", newHistoryEntryArgs.Message)); - await Task.WhenAll(ActiveLogWebSockets.Where(kv => kv.Key.State == WebSocketState.Open).Select(kv => PostLoggedJsonUpdate(kv.Key, kv.Value, json))).ConfigureAwait(false); - } - - private static async Task PostLoggedJsonUpdate(WebSocket webSocket, SemaphoreSlim sendSemaphore, string json) { - if ((webSocket == null) || (sendSemaphore == null) || string.IsNullOrEmpty(json)) { - ASF.ArchiLogger.LogNullError(nameof(webSocket) + " || " + nameof(sendSemaphore) + " || " + nameof(json)); - return; - } - - if (webSocket.State != WebSocketState.Open) { - return; - } - - await sendSemaphore.WaitAsync().ConfigureAwait(false); - - try { - if (webSocket.State != WebSocketState.Open) { - return; - } - - await webSocket.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); - } catch (HttpListenerException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } catch (WebSocketException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } finally { - sendSemaphore.Release(); - } - } - - private static async Task PostLoggedMessageUpdate(WebSocket webSocket, SemaphoreSlim sendSemaphore, string loggedMessage) { - if ((webSocket == null) || (sendSemaphore == null) || string.IsNullOrEmpty(loggedMessage)) { - ASF.ArchiLogger.LogNullError(nameof(webSocket) + " || " + nameof(sendSemaphore) + " || " + nameof(loggedMessage)); - return; - } - - if (webSocket.State != WebSocketState.Open) { - return; - } - - string response = JsonConvert.SerializeObject(new GenericResponse(true, "OK", loggedMessage)); - await PostLoggedJsonUpdate(webSocket, sendSemaphore, response).ConfigureAwait(false); - } - - 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; - } - - try { - if (response.StatusCode != (ushort) statusCode) { - response.StatusCode = (ushort) statusCode; - } - - response.AddHeader("Access-Control-Allow-Origin", "*"); - response.AddHeader("Date", DateTime.UtcNow.ToString("R")); - - if (CompressableContentTypes.Contains(response.ContentType)) { - 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 (HttpListenerException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } catch (ObjectDisposedException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - await ResponseStatusCode(request, response, HttpStatusCode.ServiceUnavailable).ConfigureAwait(false); - } - } - - private static async Task ResponseFile(HttpListenerRequest request, HttpListenerResponse response, string filePath) { - if ((request == null) || (response == null) || string.IsNullOrEmpty(filePath)) { - ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(response) + " || " + nameof(filePath)); - return; - } - - try { - response.ContentType = MimeTypes.TryGetValue(Path.GetExtension(filePath), out string mimeType) ? mimeType : "application/octet-stream"; - - byte[] content = await RuntimeCompatibility.File.ReadAllBytesAsync(filePath).ConfigureAwait(false); - await ResponseBase(request, response, content).ConfigureAwait(false); - } catch (FileNotFoundException) { - await ResponseStatusCode(request, response, HttpStatusCode.NotFound).ConfigureAwait(false); - } catch (HttpListenerException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } catch (ObjectDisposedException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - await ResponseStatusCode(request, response, HttpStatusCode.ServiceUnavailable).ConfigureAwait(false); - } - } - - 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; - } - - await ResponseString(request, response, json, "application/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; - } - - await ResponseJson(request, response, JsonConvert.SerializeObject(obj), 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; - } - - try { - 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); - } catch (HttpListenerException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } catch (ObjectDisposedException e) { - ASF.ArchiLogger.LogGenericDebuggingException(e); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - await ResponseStatusCode(request, response, HttpStatusCode.ServiceUnavailable).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 ASFRequest { -#pragma warning disable 649 - [JsonProperty(Required = Required.Always)] - internal readonly GlobalConfig GlobalConfig; -#pragma warning restore 649 - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool KeepSensitiveDetails = true; - - // Deserialized from JSON - private ASFRequest() { } - } - - private sealed class ASFResponse { - [JsonProperty] - private readonly string BuildVariant; - - [JsonProperty] - private readonly GlobalConfig GlobalConfig; - - [JsonProperty] - private readonly uint MemoryUsage; - - [JsonProperty] - private readonly DateTime ProcessStartTime; - - [JsonProperty] - private readonly Version Version; - - internal ASFResponse(string buildVariant, GlobalConfig globalConfig, uint memoryUsage, DateTime processStartTime, Version version) { - if (string.IsNullOrEmpty(buildVariant) || (globalConfig == null) || (memoryUsage == 0) || (processStartTime == DateTime.MinValue) || (version == null)) { - throw new ArgumentNullException(nameof(buildVariant) + " || " + nameof(globalConfig) + " || " + nameof(memoryUsage) + " || " + nameof(processStartTime) + " || " + nameof(version)); - } - - BuildVariant = buildVariant; - GlobalConfig = globalConfig; - MemoryUsage = memoryUsage; - ProcessStartTime = processStartTime; - Version = version; - } - } - - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - private sealed class BotRequest { -#pragma warning disable 649 - [JsonProperty(Required = Required.Always)] - internal readonly BotConfig BotConfig; -#pragma warning restore 649 - - [JsonProperty(Required = Required.DisallowNull)] - internal readonly bool KeepSensitiveDetails = true; - - // Deserialized from JSON - private BotRequest() { } - } - - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - private sealed class GamesToRedeemInBackgroundRequest { -#pragma warning disable 649 - [JsonProperty(Required = Required.Always)] - internal readonly OrderedDictionary GamesToRedeemInBackground; -#pragma warning restore 649 - - // Deserialized from JSON - private GamesToRedeemInBackgroundRequest() { } - } - - private sealed class GamesToRedeemInBackgroundResponse { - [JsonProperty] - internal readonly Dictionary UnusedKeys; - - [JsonProperty] - internal readonly Dictionary UsedKeys; - - internal GamesToRedeemInBackgroundResponse(Dictionary unusedKeys = null, Dictionary usedKeys = null) { - UnusedKeys = unusedKeys; - UsedKeys = usedKeys; - } - } - - private sealed class GenericResponse where T : class { - [JsonProperty] - private readonly string Message; - - [JsonProperty] - private readonly T Result; - - [JsonProperty] - private readonly bool Success; - - internal GenericResponse(bool success, string message = null, T 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"; - } - - private sealed class TypeResponse { - [JsonProperty] - private readonly Dictionary Body; - - [JsonProperty] - private readonly TypeProperties Properties; - - internal TypeResponse(Dictionary body, TypeProperties properties) { - if ((body == null) || (properties == null)) { - throw new ArgumentNullException(nameof(body) + " || " + nameof(properties)); - } - - Body = body; - Properties = properties; - } - - internal sealed class TypeProperties { - [JsonProperty] - private readonly string BaseType; - - [JsonProperty] - private readonly HashSet CustomAttributes; - - [JsonProperty] - private readonly string UnderlyingType; - - internal TypeProperties(string baseType = null, HashSet customAttributes = null, string underlyingType = null) { - BaseType = baseType; - CustomAttributes = customAttributes; - UnderlyingType = underlyingType; - } - } - } - - [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] - private sealed class WWWSendRequest { -#pragma warning disable 649 - [JsonProperty(Required = Required.Always)] - internal readonly string URL; -#pragma warning restore 649 - - // Deserialized from JSON - private WWWSendRequest() { } - } - } -} diff --git a/ArchiSteamFarm/IPC/ArchiKestrel.cs b/ArchiSteamFarm/IPC/ArchiKestrel.cs new file mode 100644 index 000000000..a3682626c --- /dev/null +++ b/ArchiSteamFarm/IPC/ArchiKestrel.cs @@ -0,0 +1,112 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.IO; +using System.Threading.Tasks; +using ArchiSteamFarm.IPC.Controllers.Api; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using NLog.Web; + +namespace ArchiSteamFarm.IPC { + internal static class ArchiKestrel { + private const string ConfigurationFile = nameof(IPC) + ".config"; + + internal static HistoryTarget HistoryTarget { get; private set; } + + private static IWebHost KestrelWebHost; + + internal static void OnNewHistoryTarget(HistoryTarget historyTarget = null) { + if (HistoryTarget != null) { + HistoryTarget.NewHistoryEntry -= LogController.OnNewHistoryEntry; + HistoryTarget = null; + } + + if (historyTarget != null) { + historyTarget.NewHistoryEntry += LogController.OnNewHistoryEntry; + HistoryTarget = historyTarget; + } + } + + internal static async Task Start() { + if (KestrelWebHost != null) { + return; + } + + // The order of dependency injection matters, pay attention to it + IWebHostBuilder builder = new WebHostBuilder(); + + // Set default directories + builder.UseContentRoot(SharedInfo.HomeDirectory); + builder.UseWebRoot(SharedInfo.WebsiteDirectory); + + // Check if custom config is available + string absoluteConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.ConfigDirectory); + + if (File.Exists(Path.Combine(absoluteConfigDirectory, ConfigurationFile))) { + // Set up custom config to be used + builder.UseConfiguration(new ConfigurationBuilder().SetBasePath(absoluteConfigDirectory).AddJsonFile(ConfigurationFile, false, true).Build()); + + // Use custom config for Kestrel and Logging configuration + builder.UseKestrel((builderContext, options) => options.Configure(builderContext.Configuration.GetSection("Kestrel"))); + builder.ConfigureLogging((hostingContext, logging) => logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"))); + } else { + // Use ASF defaults for Kestrel and Logging + builder.UseKestrel(options => options.ListenLocalhost(1242)); + builder.ConfigureLogging(logging => logging.SetMinimumLevel(Debugging.IsUserDebugging ? LogLevel.Trace : LogLevel.Warning)); + } + + // Enable NLog integration for logging + builder.UseNLog(); + + // Specify Startup class for IPC + builder.UseStartup(); + + // Init history logger for /Api/Log usage + Logging.InitHistoryLogger(); + + // Start the server + IWebHost kestrelWebHost = builder.Build(); + + try { + await kestrelWebHost.StartAsync().ConfigureAwait(false); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + kestrelWebHost.Dispose(); + return; + } + + KestrelWebHost = kestrelWebHost; + } + + internal static async Task Stop() { + if (KestrelWebHost == null) { + return; + } + + await KestrelWebHost.StopAsync().ConfigureAwait(false); + KestrelWebHost.Dispose(); + KestrelWebHost = null; + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs new file mode 100644 index 000000000..5f31551ba --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/ASFController.cs @@ -0,0 +1,74 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using ArchiSteamFarm.IPC.Requests; +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/ASF")] + public sealed class ASFController : ControllerBase { + [HttpGet] + public ActionResult> Get() { + uint memoryUsage = (uint) GC.GetTotalMemory(false) / 1024; + + DateTime processStartTime; + + using (Process process = Process.GetCurrentProcess()) { + processStartTime = process.StartTime; + } + + ASFResponse response = new ASFResponse(SharedInfo.BuildInfo.Variant, Program.GlobalConfig, memoryUsage, processStartTime, SharedInfo.Version); + + return Ok(new GenericResponse(response)); + } + + [HttpPost] + public async Task> Post([FromBody] ASFRequest request) { + if (request == null) { + ASF.ArchiLogger.LogNullError(nameof(request)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(request)))); + } + + if (request.KeepSensitiveDetails) { + if (string.IsNullOrEmpty(request.GlobalConfig.WebProxyPassword) && !string.IsNullOrEmpty(Program.GlobalConfig.WebProxyPassword)) { + request.GlobalConfig.WebProxyPassword = Program.GlobalConfig.WebProxyPassword; + } + } + + request.GlobalConfig.ShouldSerializeEverything = false; + + string filePath = Path.Combine(SharedInfo.ConfigDirectory, SharedInfo.GlobalConfigFileName); + + if (!await GlobalConfig.Write(filePath, request.GlobalConfig).ConfigureAwait(false)) { + return BadRequest(new GenericResponse(false, Strings.WarningFailed)); + } + + return Ok(new GenericResponse(true)); + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs new file mode 100644 index 000000000..f09a6c249 --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -0,0 +1,117 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ArchiSteamFarm.IPC.Requests; +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/Bot")] + public sealed class BotController : ControllerBase { + [HttpDelete("{botNames:required}")] + public async Task> Delete(string botNames) { + if (string.IsNullOrEmpty(botNames)) { + ASF.ArchiLogger.LogNullError(nameof(botNames)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(botNames)))); + } + + HashSet bots = Bot.GetBots(botNames); + if ((bots == null) || (bots.Count == 0)) { + return BadRequest(new GenericResponse(false, string.Format(Strings.BotNotFound, botNames))); + } + + IEnumerable> tasks = bots.Select(bot => bot.DeleteAllRelatedFiles()); + ICollection results; + + switch (Program.GlobalConfig.OptimizationMode) { + case GlobalConfig.EOptimizationMode.MinMemoryUsage: + results = new List(bots.Count); + foreach (Task task in tasks) { + results.Add(await task.ConfigureAwait(false)); + } + + break; + default: + results = await Task.WhenAll(tasks).ConfigureAwait(false); + break; + } + + if (results.Any(result => !result)) { + return BadRequest(new GenericResponse(false, Strings.WarningFailed)); + } + + return Ok(new GenericResponse(true)); + } + + [HttpGet("{botNames:required}")] + public ActionResult>> Get(string botNames) { + if (string.IsNullOrEmpty(botNames)) { + ASF.ArchiLogger.LogNullError(nameof(botNames)); + return BadRequest(new GenericResponse>(false, string.Format(Strings.ErrorIsEmpty, nameof(botNames)))); + } + + HashSet bots = Bot.GetBots(botNames); + if ((bots == null) || (bots.Count == 0)) { + return BadRequest(new GenericResponse>(false, string.Format(Strings.BotNotFound, botNames))); + } + + return Ok(new GenericResponse>(bots)); + } + + [HttpPost("{botName:required}")] + public async Task> Post(string botName, [FromBody] BotRequest request) { + if (string.IsNullOrEmpty(botName) || (request == null)) { + ASF.ArchiLogger.LogNullError(nameof(botName) + " || " + nameof(request)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(botName) + " || " + nameof(request)))); + } + + if (request.KeepSensitiveDetails && Bot.Bots.TryGetValue(botName, out Bot bot)) { + if (string.IsNullOrEmpty(request.BotConfig.SteamLogin) && !string.IsNullOrEmpty(bot.BotConfig.SteamLogin)) { + request.BotConfig.SteamLogin = bot.BotConfig.SteamLogin; + } + + if (string.IsNullOrEmpty(request.BotConfig.SteamParentalPIN) && !string.IsNullOrEmpty(bot.BotConfig.SteamParentalPIN)) { + request.BotConfig.SteamParentalPIN = bot.BotConfig.SteamParentalPIN; + } + + if (string.IsNullOrEmpty(request.BotConfig.SteamPassword) && !string.IsNullOrEmpty(bot.BotConfig.SteamPassword)) { + request.BotConfig.SteamPassword = bot.BotConfig.SteamPassword; + } + } + + request.BotConfig.ShouldSerializeEverything = false; + + string filePath = Path.Combine(SharedInfo.ConfigDirectory, botName + SharedInfo.ConfigExtension); + + if (!await BotConfig.Write(filePath, request.BotConfig).ConfigureAwait(false)) { + return BadRequest(new GenericResponse(false, Strings.WarningFailed)); + } + + return Ok(new GenericResponse(true)); + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/CommandController.cs b/ArchiSteamFarm/IPC/Controllers/Api/CommandController.cs new file mode 100644 index 000000000..18fbefd71 --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/CommandController.cs @@ -0,0 +1,57 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Linq; +using System.Threading.Tasks; +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/Command")] + public sealed class CommandController : ControllerBase { + [HttpPost("{command:required}")] + public async Task>> Post(string command) { + if (string.IsNullOrEmpty(command)) { + ASF.ArchiLogger.LogNullError(nameof(command)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(command)))); + } + + if (Program.GlobalConfig.SteamOwnerID == 0) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, nameof(Program.GlobalConfig.SteamOwnerID)))); + } + + Bot targetBot = Bot.Bots.OrderBy(bot => bot.Key).Select(bot => bot.Value).FirstOrDefault(); + if (targetBot == null) { + return BadRequest(new GenericResponse(false, Strings.ErrorNoBotsDefined)); + } + + if (!string.IsNullOrEmpty(Program.GlobalConfig.CommandPrefix) && !command.StartsWith(Program.GlobalConfig.CommandPrefix, StringComparison.Ordinal)) { + command = Program.GlobalConfig.CommandPrefix + command; + } + + string content = await targetBot.Response(Program.GlobalConfig.SteamOwnerID, command).ConfigureAwait(false); + return Ok(new GenericResponse(content)); + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/GamesToRedeemInBackgroundController.cs b/ArchiSteamFarm/IPC/Controllers/Api/GamesToRedeemInBackgroundController.cs new file mode 100644 index 000000000..7fbf6475b --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/GamesToRedeemInBackgroundController.cs @@ -0,0 +1,126 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Threading.Tasks; +using ArchiSteamFarm.IPC.Requests; +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/GamesToRedeemInBackgroundController")] + public sealed class GamesToRedeemInBackgroundController : ControllerBase { + [HttpDelete("{botNames:required}")] + public async Task> Delete(string botNames) { + if (string.IsNullOrEmpty(botNames)) { + ASF.ArchiLogger.LogNullError(nameof(botNames)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(botNames)))); + } + + HashSet bots = Bot.GetBots(botNames); + if ((bots == null) || (bots.Count == 0)) { + return BadRequest(new GenericResponse(false, string.Format(Strings.BotNotFound, botNames))); + } + + IEnumerable> tasks = bots.Select(bot => Task.Run(bot.DeleteRedeemedKeysFiles)); + ICollection results; + + switch (Program.GlobalConfig.OptimizationMode) { + case GlobalConfig.EOptimizationMode.MinMemoryUsage: + results = new List(bots.Count); + foreach (Task task in tasks) { + results.Add(await task.ConfigureAwait(false)); + } + + break; + default: + results = await Task.WhenAll(tasks).ConfigureAwait(false); + break; + } + + if (results.Any(result => !result)) { + return BadRequest(new GenericResponse(false, Strings.WarningFailed)); + } + + return Ok(new GenericResponse(true)); + } + + [HttpGet("{botNames:required}")] + public async Task>>> Get(string botNames) { + if (string.IsNullOrEmpty(botNames)) { + ASF.ArchiLogger.LogNullError(nameof(botNames)); + return BadRequest(new GenericResponse>(false, string.Format(Strings.ErrorIsEmpty, nameof(botNames)))); + } + + HashSet bots = Bot.GetBots(botNames); + if ((bots == null) || (bots.Count == 0)) { + return BadRequest(new GenericResponse>(false, string.Format(Strings.BotNotFound, botNames))); + } + + IEnumerable<(string BotName, Task<(Dictionary UnusedKeys, Dictionary UsedKeys)> Task)> tasks = bots.Select(bot => (bot.BotName, bot.GetUsedAndUnusedKeys())); + ICollection<(string BotName, (Dictionary UnusedKeys, Dictionary UsedKeys))> results; + + switch (Program.GlobalConfig.OptimizationMode) { + case GlobalConfig.EOptimizationMode.MinMemoryUsage: + results = new List<(string BotName, (Dictionary UnusedKeys, Dictionary UsedKeys))>(bots.Count); + foreach ((string botName, Task<(Dictionary UnusedKeys, Dictionary UsedKeys)> task) in tasks) { + results.Add((botName, await task.ConfigureAwait(false))); + } + + break; + default: + results = await Task.WhenAll(tasks.Select(async task => (task.BotName, await task.Task.ConfigureAwait(false)))).ConfigureAwait(false); + break; + } + + Dictionary result = new Dictionary(); + + foreach ((string botName, (Dictionary UnusedKeys, Dictionary UsedKeys) taskResult) in results) { + result[botName] = new GamesToRedeemInBackgroundResponse(taskResult.UnusedKeys, taskResult.UsedKeys); + } + + return Ok(new GenericResponse>(result)); + } + + [HttpPost("{botName:required}")] + public async Task>> Post(string botName, [FromBody] GamesToRedeemInBackgroundRequest request) { + if (string.IsNullOrEmpty(botName) || (request == null)) { + ASF.ArchiLogger.LogNullError(nameof(botName) + " || " + nameof(request)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(botName) + " || " + nameof(request)))); + } + + if (request.GamesToRedeemInBackground.Count == 0) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(request.GamesToRedeemInBackground)))); + } + + if (!Bot.Bots.TryGetValue(botName, out Bot bot)) { + return BadRequest(new GenericResponse(false, string.Format(Strings.BotNotFound, botName))); + } + + await bot.ValidateAndAddGamesToRedeemInBackground(request.GamesToRedeemInBackground).ConfigureAwait(false); + return Ok(new GenericResponse(request.GamesToRedeemInBackground)); + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/LogController.cs b/ArchiSteamFarm/IPC/Controllers/Api/LogController.cs new file mode 100644 index 000000000..1ad84fbe8 --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/LogController.cs @@ -0,0 +1,140 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Concurrent; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/Log")] + public sealed class LogController : ControllerBase { + private static readonly ConcurrentDictionary ActiveLogWebSockets = new ConcurrentDictionary(); + + [HttpGet] + public async Task Get() { + if (!HttpContext.WebSockets.IsWebSocketRequest) { + return BadRequest(new GenericResponse(false, string.Format(Strings.WarningFailedWithError, nameof(HttpContext.WebSockets.IsWebSocketRequest) + ": " + HttpContext.WebSockets.IsWebSocketRequest))); + } + + // From now on we can return only EmptyResult as the response stream is already being used by existing websocket connection + + try { + using (WebSocket webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false)) { + SemaphoreSlim sendSemaphore = new SemaphoreSlim(1, 1); + if (!ActiveLogWebSockets.TryAdd(webSocket, sendSemaphore)) { + sendSemaphore.Dispose(); + return new EmptyResult(); + } + + try { + // Push initial history if available + if (ArchiKestrel.HistoryTarget != null) { + // ReSharper disable once AccessToDisposedClosure - we're waiting for completion with Task.WhenAll(), we're not going to exit using block + await Task.WhenAll(ArchiKestrel.HistoryTarget.ArchivedMessages.Select(archivedMessage => PostLoggedMessageUpdate(webSocket, sendSemaphore, archivedMessage))).ConfigureAwait(false); + } + + while (webSocket.State == WebSocketState.Open) { + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new byte[0], CancellationToken.None).ConfigureAwait(false); + + if (result.MessageType != WebSocketMessageType.Close) { + await webSocket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "You're not supposed to be sending any message but Close!", CancellationToken.None).ConfigureAwait(false); + break; + } + + await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); + break; + } + } finally { + if (ActiveLogWebSockets.TryRemove(webSocket, out SemaphoreSlim closedSemaphore)) { + await closedSemaphore.WaitAsync().ConfigureAwait(false); // Ensure that our semaphore is truly closed by now + closedSemaphore.Dispose(); + } + } + } + } catch (WebSocketException e) { + ASF.ArchiLogger.LogGenericDebuggingException(e); + } + + return new EmptyResult(); + } + + internal static async void OnNewHistoryEntry(object sender, HistoryTarget.NewHistoryEntryArgs newHistoryEntryArgs) { + if ((sender == null) || (newHistoryEntryArgs == null)) { + ASF.ArchiLogger.LogNullError(nameof(sender) + " || " + nameof(newHistoryEntryArgs)); + return; + } + + if (ActiveLogWebSockets.Count == 0) { + return; + } + + string json = JsonConvert.SerializeObject(new GenericResponse(newHistoryEntryArgs.Message)); + await Task.WhenAll(ActiveLogWebSockets.Where(kv => kv.Key.State == WebSocketState.Open).Select(kv => PostLoggedJsonUpdate(kv.Key, kv.Value, json))).ConfigureAwait(false); + } + + private static async Task PostLoggedJsonUpdate(WebSocket webSocket, SemaphoreSlim sendSemaphore, string json) { + if ((webSocket == null) || (sendSemaphore == null) || string.IsNullOrEmpty(json)) { + ASF.ArchiLogger.LogNullError(nameof(webSocket) + " || " + nameof(sendSemaphore) + " || " + nameof(json)); + return; + } + + if (webSocket.State != WebSocketState.Open) { + return; + } + + await sendSemaphore.WaitAsync().ConfigureAwait(false); + + try { + if (webSocket.State != WebSocketState.Open) { + return; + } + + await webSocket.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); + } catch (WebSocketException e) { + ASF.ArchiLogger.LogGenericDebuggingException(e); + } finally { + sendSemaphore.Release(); + } + } + + private static async Task PostLoggedMessageUpdate(WebSocket webSocket, SemaphoreSlim sendSemaphore, string loggedMessage) { + if ((webSocket == null) || (sendSemaphore == null) || string.IsNullOrEmpty(loggedMessage)) { + ASF.ArchiLogger.LogNullError(nameof(webSocket) + " || " + nameof(sendSemaphore) + " || " + nameof(loggedMessage)); + return; + } + + if (webSocket.State != WebSocketState.Open) { + return; + } + + string response = JsonConvert.SerializeObject(new GenericResponse(loggedMessage)); + await PostLoggedJsonUpdate(webSocket, sendSemaphore, response).ConfigureAwait(false); + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/StructureController.cs b/ArchiSteamFarm/IPC/Controllers/Api/StructureController.cs new file mode 100644 index 000000000..3e4e4eb30 --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/StructureController.cs @@ -0,0 +1,55 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/Structure")] + public sealed class StructureController : ControllerBase { + [HttpGet("{structure:required}")] + public ActionResult> Get(string structure) { + if (string.IsNullOrEmpty(structure)) { + ASF.ArchiLogger.LogNullError(nameof(structure)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(structure)))); + } + + Type targetType = Utilities.ParseType(structure); + + if (targetType == null) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, structure))); + } + + object obj; + + try { + obj = Activator.CreateInstance(targetType, true); + } catch (Exception e) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorParsingObject, nameof(targetType)) + Environment.NewLine + e)); + } + + return Ok(new GenericResponse(obj)); + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs b/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs new file mode 100644 index 000000000..2e0edd7b5 --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/TypeController.cs @@ -0,0 +1,85 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/Type")] + public sealed class TypeController : ControllerBase { + [HttpGet("{type:required}")] + public ActionResult> Get(string type) { + if (string.IsNullOrEmpty(type)) { + ASF.ArchiLogger.LogNullError(nameof(type)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(type)))); + } + + Type targetType = Utilities.ParseType(type); + + if (targetType == null) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, type))); + } + + string baseType = targetType.BaseType?.GetUnifiedName(); + HashSet customAttributes = targetType.CustomAttributes.Select(attribute => attribute.AttributeType.GetUnifiedName()).ToHashSet(); + string underlyingType = null; + + Dictionary body = new Dictionary(); + + if (targetType.IsClass) { + foreach (FieldInfo field in targetType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(field => !field.IsPrivate)) { + JsonPropertyAttribute jsonProperty = field.GetCustomAttribute(); + + if (jsonProperty != null) { + body[jsonProperty.PropertyName ?? field.Name] = field.FieldType.GetUnifiedName(); + } + } + + foreach (PropertyInfo property in targetType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).Where(property => property.CanRead && !property.GetMethod.IsPrivate)) { + JsonPropertyAttribute jsonProperty = property.GetCustomAttribute(); + + if (jsonProperty != null) { + body[jsonProperty.PropertyName ?? property.Name] = property.PropertyType.GetUnifiedName(); + } + } + } else if (targetType.IsEnum) { + Type enumType = Enum.GetUnderlyingType(targetType); + underlyingType = enumType.GetUnifiedName(); + + foreach (object value in Enum.GetValues(targetType)) { + body[value.ToString()] = Convert.ChangeType(value, enumType).ToString(); + } + } + + TypeResponse.TypeProperties properties = new TypeResponse.TypeProperties(baseType, customAttributes.Count > 0 ? customAttributes : null, underlyingType); + TypeResponse response = new TypeResponse(body, properties); + + return Ok(new GenericResponse(response)); + } + } +} diff --git a/ArchiSteamFarm/IPC/Controllers/Api/WWWController.cs b/ArchiSteamFarm/IPC/Controllers/Api/WWWController.cs new file mode 100644 index 000000000..271e485fd --- /dev/null +++ b/ArchiSteamFarm/IPC/Controllers/Api/WWWController.cs @@ -0,0 +1,79 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ArchiSteamFarm.IPC.Requests; +using ArchiSteamFarm.IPC.Responses; +using ArchiSteamFarm.Localization; +using Microsoft.AspNetCore.Mvc; + +namespace ArchiSteamFarm.IPC.Controllers.Api { + [ApiController] + [Route("Api/WWW")] + public sealed class WWWController : ControllerBase { + [HttpGet("Directory/{directory:required}")] + public ActionResult>> DirectoryGet(string directory) { + if (string.IsNullOrEmpty(directory)) { + ASF.ArchiLogger.LogNullError(nameof(directory)); + return BadRequest(new GenericResponse>(false, string.Format(Strings.ErrorIsEmpty, nameof(directory)))); + } + + string directoryPath = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.WebsiteDirectory, directory); + if (!Directory.Exists(directoryPath)) { + return BadRequest(new GenericResponse>(false, string.Format(Strings.ErrorIsInvalid, directory))); + } + + string[] files; + + try { + files = Directory.GetFiles(directoryPath); + } catch (Exception e) { + return BadRequest(new GenericResponse>(false, string.Format(Strings.ErrorParsingObject, nameof(files)) + Environment.NewLine + e)); + } + + HashSet result = files.Select(Path.GetFileName).ToHashSet(); + return Ok(new GenericResponse>(result)); + } + + [HttpPost("Send")] + public async Task>> SendPost([FromBody] WWWSendRequest request) { + if (request == null) { + ASF.ArchiLogger.LogNullError(nameof(request)); + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsEmpty, nameof(request)))); + } + + if (string.IsNullOrEmpty(request.URL) || !Uri.TryCreate(request.URL, UriKind.Absolute, out Uri uri) || !uri.Scheme.Equals(Uri.UriSchemeHttps)) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorIsInvalid, nameof(request.URL)))); + } + + WebBrowser.HtmlDocumentResponse urlResponse = await Program.WebBrowser.UrlGetToHtmlDocument(request.URL).ConfigureAwait(false); + if (urlResponse?.Content == null) { + return BadRequest(new GenericResponse(false, string.Format(Strings.ErrorRequestFailedTooManyTimes, WebBrowser.MaxTries))); + } + + return Ok(new GenericResponse(urlResponse.Content.DocumentNode.InnerHtml)); + } + } +} diff --git a/ArchiSteamFarm/IPC/Middleware/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Middleware/ApiAuthenticationMiddleware.cs new file mode 100644 index 000000000..701d6f548 --- /dev/null +++ b/ArchiSteamFarm/IPC/Middleware/ApiAuthenticationMiddleware.cs @@ -0,0 +1,114 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace ArchiSteamFarm.IPC.Middleware { + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + internal sealed class ApiAuthenticationMiddleware { + private const byte FailedAuthorizationsCooldownInHours = 1; + private const byte MaxFailedAuthorizationAttempts = 5; + + private static readonly SemaphoreSlim AuthorizationSemaphore = new SemaphoreSlim(1, 1); + + [SuppressMessage("ReSharper", "UnusedMember.Local")] + private static readonly Timer ClearFailedAuthorizationsTimer = new Timer( + e => FailedAuthorizations.Clear(), + null, + TimeSpan.FromHours(FailedAuthorizationsCooldownInHours), // Delay + TimeSpan.FromHours(FailedAuthorizationsCooldownInHours) // Period + ); + + private static readonly ConcurrentDictionary FailedAuthorizations = new ConcurrentDictionary(); + + private readonly RequestDelegate Next; + + public ApiAuthenticationMiddleware(RequestDelegate next) => Next = next ?? throw new ArgumentNullException(nameof(next)); + + [SuppressMessage("ReSharper", "UnusedMember.Global")] + public async Task InvokeAsync(HttpContext context) { + if (context == null) { + ASF.ArchiLogger.LogNullError(nameof(context)); + return; + } + + HttpStatusCode authenticationStatus = await GetAuthenticationStatus(context).ConfigureAwait(false); + + if (authenticationStatus != HttpStatusCode.OK) { + await context.Response.Generate(authenticationStatus).ConfigureAwait(false); + return; + } + + await Next(context).ConfigureAwait(false); + } + + private static async Task GetAuthenticationStatus(HttpContext context) { + if (context == null) { + ASF.ArchiLogger.LogNullError(nameof(context)); + return HttpStatusCode.InternalServerError; + } + + if (string.IsNullOrEmpty(Program.GlobalConfig.IPCPassword)) { + return HttpStatusCode.OK; + } + + IPAddress clientIP = context.Connection.RemoteIpAddress; + + if (FailedAuthorizations.TryGetValue(clientIP, out byte attempts)) { + if (attempts >= MaxFailedAuthorizationAttempts) { + return HttpStatusCode.Forbidden; + } + } + + if (!context.Request.Headers.TryGetValue("Authentication", out StringValues passwords) && !context.Request.Query.TryGetValue("password", out passwords)) { + return HttpStatusCode.Unauthorized; + } + + bool authorized = passwords.First() == Program.GlobalConfig.IPCPassword; + + await AuthorizationSemaphore.WaitAsync().ConfigureAwait(false); + + try { + if (FailedAuthorizations.TryGetValue(clientIP, out attempts)) { + if (attempts >= MaxFailedAuthorizationAttempts) { + return HttpStatusCode.Forbidden; + } + } + + if (!authorized) { + FailedAuthorizations[clientIP] = FailedAuthorizations.TryGetValue(clientIP, out attempts) ? ++attempts : (byte) 1; + } + } finally { + AuthorizationSemaphore.Release(); + } + + return authorized ? HttpStatusCode.OK : HttpStatusCode.Unauthorized; + } + } +} diff --git a/ArchiSteamFarm/IPC/Requests/ASFRequest.cs b/ArchiSteamFarm/IPC/Requests/ASFRequest.cs new file mode 100644 index 000000000..a03e9e4e7 --- /dev/null +++ b/ArchiSteamFarm/IPC/Requests/ASFRequest.cs @@ -0,0 +1,34 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Requests { + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + public sealed class ASFRequest : SensitiveDetailsRequest { + [JsonProperty(Required = Required.Always)] + internal readonly GlobalConfig GlobalConfig; + + // Deserialized from JSON + private ASFRequest() { } + } +} diff --git a/ArchiSteamFarm/IPC/Requests/BotRequest.cs b/ArchiSteamFarm/IPC/Requests/BotRequest.cs new file mode 100644 index 000000000..ba62ef41a --- /dev/null +++ b/ArchiSteamFarm/IPC/Requests/BotRequest.cs @@ -0,0 +1,34 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Requests { + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + public sealed class BotRequest : SensitiveDetailsRequest { + [JsonProperty(Required = Required.Always)] + internal readonly BotConfig BotConfig; + + // Deserialized from JSON + private BotRequest() { } + } +} diff --git a/ArchiSteamFarm/IPC/Requests/GamesToRedeemInBackgroundRequest.cs b/ArchiSteamFarm/IPC/Requests/GamesToRedeemInBackgroundRequest.cs new file mode 100644 index 000000000..d6034e937 --- /dev/null +++ b/ArchiSteamFarm/IPC/Requests/GamesToRedeemInBackgroundRequest.cs @@ -0,0 +1,35 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Requests { + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + public sealed class GamesToRedeemInBackgroundRequest { + [JsonProperty(Required = Required.Always)] + internal readonly OrderedDictionary GamesToRedeemInBackground; + + // Deserialized from JSON + private GamesToRedeemInBackgroundRequest() { } + } +} diff --git a/ArchiSteamFarm/IPC/Requests/SensitiveDetailsRequest.cs b/ArchiSteamFarm/IPC/Requests/SensitiveDetailsRequest.cs new file mode 100644 index 000000000..7591002eb --- /dev/null +++ b/ArchiSteamFarm/IPC/Requests/SensitiveDetailsRequest.cs @@ -0,0 +1,29 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Requests { + public abstract class SensitiveDetailsRequest { + [JsonProperty(Required = Required.DisallowNull)] + internal readonly bool KeepSensitiveDetails = true; + } +} diff --git a/ArchiSteamFarm/IPC/Requests/WWWSendRequest.cs b/ArchiSteamFarm/IPC/Requests/WWWSendRequest.cs new file mode 100644 index 000000000..416c14aa1 --- /dev/null +++ b/ArchiSteamFarm/IPC/Requests/WWWSendRequest.cs @@ -0,0 +1,34 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Requests { + [SuppressMessage("ReSharper", "ClassCannotBeInstantiated")] + public sealed class WWWSendRequest { + [JsonProperty(Required = Required.Always)] + internal readonly string URL; + + // Deserialized from JSON + private WWWSendRequest() { } + } +} diff --git a/ArchiSteamFarm/IPC/Responses/ASFResponse.cs b/ArchiSteamFarm/IPC/Responses/ASFResponse.cs new file mode 100644 index 000000000..de10ead13 --- /dev/null +++ b/ArchiSteamFarm/IPC/Responses/ASFResponse.cs @@ -0,0 +1,54 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Responses { + public sealed class ASFResponse { + [JsonProperty] + private readonly string BuildVariant; + + [JsonProperty] + private readonly GlobalConfig GlobalConfig; + + [JsonProperty] + private readonly uint MemoryUsage; + + [JsonProperty] + private readonly DateTime ProcessStartTime; + + [JsonProperty] + private readonly Version Version; + + internal ASFResponse(string buildVariant, GlobalConfig globalConfig, uint memoryUsage, DateTime processStartTime, Version version) { + if (string.IsNullOrEmpty(buildVariant) || (globalConfig == null) || (memoryUsage == 0) || (processStartTime == DateTime.MinValue) || (version == null)) { + throw new ArgumentNullException(nameof(buildVariant) + " || " + nameof(globalConfig) + " || " + nameof(memoryUsage) + " || " + nameof(processStartTime) + " || " + nameof(version)); + } + + BuildVariant = buildVariant; + GlobalConfig = globalConfig; + MemoryUsage = memoryUsage; + ProcessStartTime = processStartTime; + Version = version; + } + } +} diff --git a/ArchiSteamFarm/IPC/Responses/GamesToRedeemInBackgroundResponse.cs b/ArchiSteamFarm/IPC/Responses/GamesToRedeemInBackgroundResponse.cs new file mode 100644 index 000000000..6829c96b1 --- /dev/null +++ b/ArchiSteamFarm/IPC/Responses/GamesToRedeemInBackgroundResponse.cs @@ -0,0 +1,38 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Responses { + public sealed class GamesToRedeemInBackgroundResponse { + [JsonProperty] + private readonly Dictionary UnusedKeys; + + [JsonProperty] + private readonly Dictionary UsedKeys; + + internal GamesToRedeemInBackgroundResponse(Dictionary unusedKeys = null, Dictionary usedKeys = null) { + UnusedKeys = unusedKeys; + UsedKeys = usedKeys; + } + } +} diff --git a/ArchiSteamFarm/IPC/Responses/GenericResponse.cs b/ArchiSteamFarm/IPC/Responses/GenericResponse.cs new file mode 100644 index 000000000..4ea6fff51 --- /dev/null +++ b/ArchiSteamFarm/IPC/Responses/GenericResponse.cs @@ -0,0 +1,60 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Responses { + public sealed class GenericResponse : GenericResponse where T : class { + [JsonProperty] + private readonly T Result; + + internal GenericResponse(T result) : base(true, "OK") => Result = result ?? throw new ArgumentNullException(nameof(result)); + internal GenericResponse(bool success, string message) : base(success, message) { } + } + + public class GenericResponse { + [JsonProperty] + private readonly string Message; + + [JsonProperty] + private readonly bool Success; + + internal GenericResponse(bool success) { + if (!success) { + // Returning failed generic response without a message should never happen + throw new ArgumentException(nameof(success)); + } + + Success = true; + Message = "OK"; + } + + internal GenericResponse(bool success, string message) { + if (string.IsNullOrEmpty(message)) { + throw new ArgumentNullException(nameof(message)); + } + + Success = success; + Message = message; + } + } +} diff --git a/ArchiSteamFarm/IPC/Responses/TypeResponse.cs b/ArchiSteamFarm/IPC/Responses/TypeResponse.cs new file mode 100644 index 000000000..22f204287 --- /dev/null +++ b/ArchiSteamFarm/IPC/Responses/TypeResponse.cs @@ -0,0 +1,60 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Responses { + public sealed class TypeResponse { + [JsonProperty] + private readonly Dictionary Body; + + [JsonProperty] + private readonly TypeProperties Properties; + + internal TypeResponse(Dictionary body, TypeProperties properties) { + if ((body == null) || (properties == null)) { + throw new ArgumentNullException(nameof(body) + " || " + nameof(properties)); + } + + Body = body; + Properties = properties; + } + + internal sealed class TypeProperties { + [JsonProperty] + private readonly string BaseType; + + [JsonProperty] + private readonly HashSet CustomAttributes; + + [JsonProperty] + private readonly string UnderlyingType; + + internal TypeProperties(string baseType = null, HashSet customAttributes = null, string underlyingType = null) { + BaseType = baseType; + CustomAttributes = customAttributes; + UnderlyingType = underlyingType; + } + } + } +} diff --git a/ArchiSteamFarm/IPC/Startup.cs b/ArchiSteamFarm/IPC/Startup.cs new file mode 100644 index 000000000..c1d03312e --- /dev/null +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -0,0 +1,108 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using ArchiSteamFarm.IPC.Middleware; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace ArchiSteamFarm.IPC { + internal sealed class Startup { + private readonly IConfiguration Configuration; + + public Startup(IConfiguration configuration) => Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) { + if ((app == null) || (env == null)) { + ASF.ArchiLogger.LogNullError(nameof(app) + " || " + nameof(env)); + return; + } + + // The order of dependency injection matters, pay attention to it + + // Add workaround for missing PathBase feature, https://github.com/aspnet/Hosting/issues/1120 + PathString pathBase = Configuration.GetSection("Kestrel").GetValue("PathBase"); + if (!string.IsNullOrEmpty(pathBase)) { + app.UsePathBase(pathBase); + } + + // Add support for response compression + app.UseResponseCompression(); + + if (!string.IsNullOrEmpty(Program.GlobalConfig.IPCPassword)) { + // We need ApiAuthenticationMiddleware for IPCPassword + app.UseWhen(context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), appBuilder => appBuilder.UseMiddleware()); + } + + // We need WebSockets support for /Api/Log + app.UseWebSockets(); + + // We need static files support for IPC GUI + app.UseDefaultFiles(); + app.UseStaticFiles(); + + // We need MVC for /Api + app.UseMvcWithDefaultRoute(); + } + + public void ConfigureServices(IServiceCollection services) { + if (services == null) { + ASF.ArchiLogger.LogNullError(nameof(services)); + return; + } + + // The order of dependency injection matters, pay attention to it + + // Add support for response compression + services.AddResponseCompression(); + + // We need MVC for /Api, but we're going to use only a small subset of all available features + IMvcCoreBuilder mvc = services.AddMvcCore(); + + // Use latest compatibility version for MVC + mvc.SetCompatibilityVersion(CompatibilityVersion.Latest); + + // Add standard formatters that can be used for serializing/deserializing requests/responses, they're already available in the core + mvc.AddFormatterMappings(); + + // Add JSON formatters that will be used as default ones if no specific formatters are asked for + mvc.AddJsonFormatters(); + + // Fix default contract resolver to use original names and not a camel case + // Also add debugging aid while we're at it + mvc.AddJsonOptions( + options => { + options.SerializerSettings.ContractResolver = new DefaultContractResolver(); + + if (Debugging.IsUserDebugging) { + options.SerializerSettings.Formatting = Formatting.Indented; + } + } + ); + } + } +} diff --git a/ArchiSteamFarm/IPC/Utilities.cs b/ArchiSteamFarm/IPC/Utilities.cs new file mode 100644 index 000000000..802580925 --- /dev/null +++ b/ArchiSteamFarm/IPC/Utilities.cs @@ -0,0 +1,72 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// +// Copyright 2015-2018 Łukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace ArchiSteamFarm.IPC { + internal static class Utilities { + internal static async Task Generate(this HttpResponse httpResponse, HttpStatusCode statusCode) { + if (httpResponse == null) { + ASF.ArchiLogger.LogNullError(nameof(httpResponse)); + return; + } + + ushort statusCodeNumber = (ushort) statusCode; + + httpResponse.StatusCode = statusCodeNumber; + await httpResponse.WriteAsync(statusCodeNumber + " - " + statusCode).ConfigureAwait(false); + } + + internal static string GetUnifiedName(this Type type) { + if (type == null) { + ASF.ArchiLogger.LogNullError(nameof(type)); + return null; + } + + return type.GenericTypeArguments.Length == 0 ? type.FullName : type.Namespace + "." + type.Name + string.Join("", type.GenericTypeArguments.Select(innerType => '[' + innerType.GetUnifiedName() + ']')); + } + + internal static Type ParseType(string typeText) { + if (string.IsNullOrEmpty(typeText)) { + ASF.ArchiLogger.LogNullError(nameof(typeText)); + return null; + } + + Type targetType = Type.GetType(typeText); + if (targetType != null) { + return targetType; + } + + // We can try one more time by trying to smartly guess the assembly name from the namespace, this will work for custom libraries like SteamKit2 + int index = typeText.IndexOf('.'); + + if ((index <= 0) || (index >= typeText.Length - 1)) { + return null; + } + + return Type.GetType(typeText + "," + typeText.Substring(0, index)); + } + } +} diff --git a/ArchiSteamFarm/Logging.cs b/ArchiSteamFarm/Logging.cs index 2deafd205..0933a3d7b 100644 --- a/ArchiSteamFarm/Logging.cs +++ b/ArchiSteamFarm/Logging.cs @@ -20,6 +20,7 @@ // limitations under the License. using System.Linq; +using ArchiSteamFarm.IPC; using NLog; using NLog.Config; using NLog.Targets; @@ -99,7 +100,7 @@ namespace ArchiSteamFarm { LogManager.ReconfigExistingLoggers(); } - IPC.OnNewHistoryTarget(historyTarget); + ArchiKestrel.OnNewHistoryTarget(historyTarget); } internal static void OnUserInputEnd() { @@ -109,14 +110,14 @@ namespace ArchiSteamFarm { return; } - bool reconfig = false; + bool reconfigure = false; foreach (LoggingRule consoleLoggingRule in ConsoleLoggingRules.Where(consoleLoggingRule => !LogManager.Configuration.LoggingRules.Contains(consoleLoggingRule))) { LogManager.Configuration.LoggingRules.Add(consoleLoggingRule); - reconfig = true; + reconfigure = true; } - if (reconfig) { + if (reconfigure) { LogManager.ReconfigExistingLoggers(); } } @@ -128,15 +129,15 @@ namespace ArchiSteamFarm { return; } - bool reconfig = false; + bool reconfigure = false; foreach (LoggingRule consoleLoggingRule in ConsoleLoggingRules) { if (LogManager.Configuration.LoggingRules.Remove(consoleLoggingRule)) { - reconfig = true; + reconfigure = true; } } - if (reconfig) { + if (reconfigure) { LogManager.ReconfigExistingLoggers(); } } @@ -162,7 +163,7 @@ namespace ArchiSteamFarm { } HistoryTarget historyTarget = LogManager.Configuration.AllTargets.OfType().FirstOrDefault(); - IPC.OnNewHistoryTarget(historyTarget); + ArchiKestrel.OnNewHistoryTarget(historyTarget); } } -} \ No newline at end of file +} diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs index 6301684b7..01c1c1556 100644 --- a/ArchiSteamFarm/Program.cs +++ b/ArchiSteamFarm/Program.cs @@ -28,6 +28,7 @@ using System.IO; using System.Linq; using System.Resources; using System.Threading.Tasks; +using ArchiSteamFarm.IPC; using ArchiSteamFarm.Localization; using Newtonsoft.Json; using NLog; @@ -341,8 +342,8 @@ namespace ArchiSteamFarm { WebBrowser.Init(); WebBrowser = new WebBrowser(ASF.ArchiLogger, GlobalConfig.WebProxy, true); - if (GlobalConfig.IPC && (GlobalConfig.IPCPrefixes.Count > 0)) { - IPC.Start(GlobalConfig.IPCPrefixes); + if (GlobalConfig.IPC) { + await ArchiKestrel.Start().ConfigureAwait(false); } } @@ -353,15 +354,9 @@ namespace ArchiSteamFarm { ShutdownSequenceInitialized = true; - // Sockets created by HttpListener might still be running for a short while after complete app shutdown + // Sockets created by IPC might still be running for a short while after complete app shutdown // Ensure that IPC is stopped before we finalize shutdown sequence - if (IPC.IsRunning) { - IPC.Stop(); - - for (byte i = 0; (i < WebBrowser.MaxTries) && IPC.IsRunning; i++) { - await Task.Delay(1000).ConfigureAwait(false); - } - } + await ArchiKestrel.Stop().ConfigureAwait(false); if (Bot.Bots.Count > 0) { IEnumerable tasks = Bot.Bots.Values.Select(bot => Task.Run(() => bot.Stop(true))); diff --git a/ArchiSteamFarm/Utilities.cs b/ArchiSteamFarm/Utilities.cs index 7df6d276e..1bc2f879a 100644 --- a/ArchiSteamFarm/Utilities.cs +++ b/ArchiSteamFarm/Utilities.cs @@ -73,15 +73,6 @@ namespace ArchiSteamFarm { return cookies.Count > 0 ? (from Cookie cookie in cookies where cookie.Name.Equals(name) select cookie.Value).FirstOrDefault() : null; } - internal static string GetUnifiedName(this Type type) { - if (type == null) { - ASF.ArchiLogger.LogNullError(nameof(type)); - return null; - } - - return type.GenericTypeArguments.Length == 0 ? type.FullName : type.Namespace + "." + type.Name + string.Join("", type.GenericTypeArguments.Select(innerType => '[' + innerType.GetUnifiedName() + ']')); - } - internal static uint GetUnixTime() => (uint) DateTimeOffset.UtcNow.ToUnixTimeSeconds(); internal static void InBackground(Action action, bool longRunning = false) { @@ -185,4 +176,4 @@ namespace ArchiSteamFarm { internal static string ToHumanReadable(this TimeSpan timeSpan) => timeSpan.Humanize(3, maxUnit: TimeUnit.Year, minUnit: TimeUnit.Second); } -} \ No newline at end of file +} diff --git a/ArchiSteamFarm/config/ASF.json b/ArchiSteamFarm/config/ASF.json index d07dcf019..2ea16cb29 100644 --- a/ArchiSteamFarm/config/ASF.json +++ b/ArchiSteamFarm/config/ASF.json @@ -13,9 +13,6 @@ "InventoryLimiterDelay": 3, "IPC": false, "IPCPassword": null, - "IPCPrefixes": [ - "http://127.0.0.1:1242/" - ], "LoginLimiterDelay": 10, "MaxFarmingTime": 10, "MaxTradeHoldDuration": 15,