diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index c230028e7..e9ddc9b6b 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -31,13 +31,15 @@ - + + + diff --git a/ArchiSteamFarm/IPC.cs b/ArchiSteamFarm/IPC.cs index 09ad5a4de..6abb11c81 100644 --- a/ArchiSteamFarm/IPC.cs +++ b/ArchiSteamFarm/IPC.cs @@ -23,66 +23,32 @@ */ using System; -using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; using System.Linq; using System.Net; using System.Text; -using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Localization; -using Unosquare.Labs.EmbedIO; -using Unosquare.Labs.EmbedIO.Constants; -using Unosquare.Labs.EmbedIO.Modules; -using Unosquare.Swan; namespace ArchiSteamFarm { - [SuppressMessage("ReSharper", "UnusedMember.Global")] - internal sealed class IPCWebApiController : WebApiController { - [WebApiHandler(HttpVerbs.Get, "/ipc")] - public async Task ExecuteCommandObsolete(WebServer server, HttpListenerContext context) { - if ((server == null) || (context == null)) { - ASF.ArchiLogger.LogNullError(nameof(server) + " || " + nameof(context)); - return false; - } - - string command = context.QueryString("command"); - if (string.IsNullOrEmpty(command)) { - await context.PlainTextResponse(string.Format(Strings.ErrorIsEmpty, nameof(command)), HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - Bot targetBot = Bot.Bots.OrderBy(bot => bot.Key).Select(bot => bot.Value).FirstOrDefault(); - if (targetBot == null) { - await context.PlainTextResponse(Strings.ErrorNoBotsDefined, HttpStatusCode.BadRequest).ConfigureAwait(false); - return true; - } - - if (command[0] != '!') { - command = "!" + command; - } - - string content = await targetBot.Response(Program.GlobalConfig.SteamOwnerID, command).ConfigureAwait(false); - - ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.IPCAnswered, command, content)); - - await context.PlainTextResponse(content).ConfigureAwait(false); - return true; - } - } - internal static class IPC { - internal static bool IsRunning => WebServerCancellationToken?.IsCancellationRequested == false; + internal static bool IsRunning => HttpListener?.IsListening == true; - private static WebServer WebServer; - private static CancellationTokenSource WebServerCancellationToken; + private static HttpListener HttpListener; - internal static void Initialize(string host, ushort port) { + internal static void Init(string host, ushort port) { if (string.IsNullOrEmpty(host) || (port == 0)) { ASF.ArchiLogger.LogNullError(nameof(host) + " || " + nameof(port)); return; } - if (WebServer != null) { + if (HttpListener != null) { + return; + } + + if (!HttpListener.IsSupported) { + ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningFailedWithError, "!HttpListener.IsSupported")); return; } @@ -96,19 +62,49 @@ namespace ArchiSteamFarm { string url = "http://" + host + ":" + port + "/"; - Terminal.Settings.DisplayLoggingMessageType = LogMessageType.None; + try { + HttpListener = new HttpListener { + IgnoreWriteExceptions = true + }; - Terminal.OnLogMessageReceived += OnLogMessageReceived; - - WebServer = new WebServer(url); - WebServer.RegisterModule(new WebApiModule()); - WebServer.Module().RegisterController(); + HttpListener.Prefixes.Add(url); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + HttpListener = null; + } } - internal static async Task PlainTextResponse(this HttpListenerContext context, string content, HttpStatusCode statusCode = HttpStatusCode.OK) { - if ((context == null) || string.IsNullOrEmpty(content)) { - ASF.ArchiLogger.LogNullError(nameof(context) + " || " + nameof(content)); - return false; + internal static void Start() { + if ((HttpListener?.IsListening != false) || (HttpListener.Prefixes.Count == 0)) { + return; + } + + ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.IPCStarting, HttpListener.Prefixes.First())); + + try { + HttpListener.Start(); + } catch (HttpListenerException e) { + ASF.ArchiLogger.LogGenericException(e); + return; + } + + Utilities.StartBackgroundFunction(Run); + + ASF.ArchiLogger.LogGenericInfo(Strings.IPCReady); + } + + internal static void Stop() { + if (HttpListener?.IsListening != true) { + return; + } + + HttpListener.Stop(); + } + + private static async Task BaseResponse(this HttpListenerContext context, byte[] response, HttpStatusCode statusCode = HttpStatusCode.OK) { + if ((context == null) || (response == null) || (response.Length == 0)) { + ASF.ArchiLogger.LogNullError(nameof(context) + " || " + nameof(response)); + return; } try { @@ -118,113 +114,145 @@ namespace ArchiSteamFarm { context.Response.AppendHeader("Access-Control-Allow-Origin", "null"); - Encoding encoding = Encoding.UTF8; + string acceptEncoding = context.Request.Headers["Accept-Encoding"]; - context.Response.ContentEncoding = encoding; - context.Response.ContentType = "text/plain; charset=" + encoding.WebName; + if (!string.IsNullOrEmpty(acceptEncoding)) { + if (acceptEncoding.Contains("gzip")) { + context.Response.AddHeader("Content-Encoding", "gzip"); + using (MemoryStream ms = new MemoryStream()) { + using (GZipStream stream = new GZipStream(ms, CompressionMode.Compress)) { + await stream.WriteAsync(response, 0, response.Length).ConfigureAwait(false); + } - byte[] buffer = encoding.GetBytes(content + Environment.NewLine); - context.Response.ContentLength64 = buffer.Length; + response = ms.ToArray(); + } + } else if (acceptEncoding.Contains("deflate")) { + context.Response.AddHeader("Content-Encoding", "deflate"); + using (MemoryStream ms = new MemoryStream()) { + using (DeflateStream stream = new DeflateStream(ms, CompressionMode.Compress)) { + await stream.WriteAsync(response, 0, response.Length).ConfigureAwait(false); + } - await context.Response.OutputStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericDebugException(e); - return false; - } - - return true; - } - - internal static void Start() { - if (IsRunning || (WebServer == null) || (WebServer.UrlPrefixes.Count == 0)) { - return; - } - - ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.IPCStarting, WebServer.UrlPrefixes.First())); - - // Fail early if we're not able to start our listener - - try { - WebServer.Listener.Start(); - } catch (HttpListenerException e) { - ASF.ArchiLogger.LogGenericException(e); - return; - } - - WebServerCancellationToken = new CancellationTokenSource(); - Utilities.StartBackgroundFunction(() => Run(WebServerCancellationToken)); - - ASF.ArchiLogger.LogGenericInfo(Strings.IPCReady); - } - - internal static void Stop() { - if (WebServerCancellationToken == null) { - return; - } - - Release(WebServerCancellationToken); - } - - private static void OnLogMessageReceived(object sender, LogMessageReceivedEventArgs e) { - // Note: it's valid for sender to be null in this function - if (e == null) { - ASF.ArchiLogger.LogNullError(nameof(e)); - return; - } - - if (string.IsNullOrEmpty(e.Message)) { - return; - } - - string message = e.Source + " | " + e.Message; - - switch (e.MessageType) { - case LogMessageType.Error: - case LogMessageType.Warning: - ASF.ArchiLogger.LogGenericWarning(message); - break; - case LogMessageType.Info: - ASF.ArchiLogger.LogGenericDebug(message); - break; - default: - ASF.ArchiLogger.LogGenericTrace(message); - break; - } - } - - private static void Release(CancellationTokenSource cts) { - if (cts == null) { - ASF.ArchiLogger.LogNullError(nameof(cts)); - return; - } - - if (cts == WebServerCancellationToken) { - WebServerCancellationToken = null; - } - - if (!cts.IsCancellationRequested) { - cts.Cancel(); - } - - WebServer.Listener.Stop(); - } - - private static async Task Run(CancellationTokenSource cts) { - if (cts == null) { - ASF.ArchiLogger.LogNullError(nameof(cts)); - return; - } - - using (cts) { - while ((cts == WebServerCancellationToken) && !cts.IsCancellationRequested) { - try { - await WebServer.RunAsync(cts.Token).ConfigureAwait(false); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - Release(cts); + response = ms.ToArray(); + } } } + + context.Response.ContentLength64 = response.Length; + await context.Response.OutputStream.WriteAsync(response, 0, response.Length).ConfigureAwait(false); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericDebugException(e); } } + + private static async Task ExecuteCommand(HttpListenerContext context) { + if (context == null) { + ASF.ArchiLogger.LogNullError(nameof(context)); + return; + } + + string command = context.Request.GetQueryStringValue("command"); + if (string.IsNullOrEmpty(command)) { + await context.StringResponse(string.Format(Strings.ErrorIsEmpty, nameof(command)), statusCode: HttpStatusCode.BadRequest).ConfigureAwait(false); + return; + } + + Bot targetBot = Bot.Bots.OrderBy(bot => bot.Key).Select(bot => bot.Value).FirstOrDefault(); + if (targetBot == null) { + await context.StringResponse(Strings.ErrorNoBotsDefined, statusCode: HttpStatusCode.BadRequest).ConfigureAwait(false); + return; + } + + if (command[0] != '!') { + command = "!" + command; + } + + string content = await targetBot.Response(Program.GlobalConfig.SteamOwnerID, command).ConfigureAwait(false); + + ASF.ArchiLogger.LogGenericInfo(string.Format(Strings.IPCAnswered, command, content)); + + await context.StringResponse(content).ConfigureAwait(false); + } + + private static string GetQueryStringValue(this HttpListenerRequest request, string requestKey) { + if ((request == null) || string.IsNullOrEmpty(requestKey)) { + ASF.ArchiLogger.LogNullError(nameof(request) + " || " + nameof(requestKey)); + return null; + } + + string result = (from string key in request.QueryString where requestKey.Equals(key) select request.QueryString[key]).FirstOrDefault(); + return result; + } + + private static async Task HandleGetRequest(HttpListenerContext context) { + if (context == null) { + ASF.ArchiLogger.LogNullError(nameof(context)); + return; + } + + switch (context.Request.Url.LocalPath.ToUpperInvariant()) { + case "/IPC": + await ExecuteCommand(context).ConfigureAwait(false); + break; + } + } + + private static async Task HandleRequest(HttpListenerContext context) { + if (context == null) { + ASF.ArchiLogger.LogNullError(nameof(context)); + return; + } + + try { + if (Program.GlobalConfig.SteamOwnerID == 0) { + ASF.ArchiLogger.LogGenericWarning(Strings.ErrorIPCAccessDenied); + await context.StringResponse(Strings.ErrorIPCAccessDenied, statusCode: HttpStatusCode.Forbidden).ConfigureAwait(false); + return; + } + + switch (context.Request.HttpMethod) { + case WebRequestMethods.Http.Get: + await HandleGetRequest(context).ConfigureAwait(false); + break; + } + + if (context.Response.ContentLength64 == 0) { + await context.StringResponse("404 - Not Found", statusCode: HttpStatusCode.NotFound); + } + } finally { + context.Response.Close(); + } + } + + private static async Task Run() { + while (HttpListener.IsListening) { + HttpListenerContext context; + + try { + context = await HttpListener.GetContextAsync().ConfigureAwait(false); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + return; + } + + Utilities.StartBackgroundFunction(() => HandleRequest(context), false); + } + } + + private static async Task StringResponse(this HttpListenerContext context, string content, string textType = "text/plain", HttpStatusCode statusCode = HttpStatusCode.OK) { + if ((context == null) || string.IsNullOrEmpty(content) || string.IsNullOrEmpty(textType)) { + ASF.ArchiLogger.LogNullError(nameof(context) + " || " + nameof(content) + " || " + nameof(textType)); + return; + } + + if (context.Response.ContentEncoding == null) { + context.Response.ContentEncoding = Encoding.UTF8; + } + + context.Response.ContentType = textType + "; charset=" + context.Response.ContentEncoding.WebName; + + byte[] response = context.Response.ContentEncoding.GetBytes(content + Environment.NewLine); + await BaseResponse(context, response, statusCode).ConfigureAwait(false); + } } } \ No newline at end of file diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs index 161b57c09..3bd0f1c15 100644 --- a/ArchiSteamFarm/Program.cs +++ b/ArchiSteamFarm/Program.cs @@ -336,7 +336,6 @@ namespace ArchiSteamFarm { return; } - IPC.Initialize(GlobalConfig.IPCHost, GlobalConfig.IPCPort); OS.Init(GlobalConfig.Headless); WebBrowser.Init(); @@ -437,6 +436,7 @@ namespace ArchiSteamFarm { goto default; } + IPC.Init(GlobalConfig.IPCHost, GlobalConfig.IPCPort); IPC.Start(); break; case "--service":