diff --git a/Protest-Tests/ListenerTests.cs b/Protest-Tests/ListenerTests.cs index 7976cd8c..100d70fa 100644 --- a/Protest-Tests/ListenerTests.cs +++ b/Protest-Tests/ListenerTests.cs @@ -7,12 +7,16 @@ public class ListenerTests { private readonly DirectoryInfo front; public ListenerTests() { - if (OperatingSystem.IsWindows()) + if (OperatingSystem.IsWindows()) { front = new DirectoryInfo(@"..\..\..\..\..\Protest\front"); - else + } + else { front = new DirectoryInfo(@"../../../../../Protest/front"); + } - if (!front.Exists) Assert.Fail($"\"front\" directory not found: {front.FullName}"); + if (!front.Exists) { + Assert.Fail($"\"front\" directory not found: {front.FullName}"); + } } [SetUp] @@ -21,8 +25,6 @@ public void Setup() { Http.Listener listener = new Http.Listener("127.0.0.1", 8080, front.FullName); listener.Start(); }); - - //Thread.Sleep(100); } [Test] @@ -46,7 +48,7 @@ public void Listener_NoneExistingPage_ReturnNotFound() { } [Test] - public void CsrfCheck_NoHostInReferer_ReturnOk() { + public void CsrfCheck_NoHostInReferrer_ReturnOk() { using HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://127.0.0.1:8080/"); using HttpClient httpClient = new HttpClient(); @@ -56,7 +58,7 @@ public void CsrfCheck_NoHostInReferer_ReturnOk() { } [Test] - public void CsrfCheck_SameHostInReferer_ReturnOk() { + public void CsrfCheck_SameHostInReferrer_ReturnOk() { using HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://127.0.0.1:8080/"); requestMessage.Headers.Add("Referer", "http://127.0.0.1:8080/"); @@ -67,9 +69,9 @@ public void CsrfCheck_SameHostInReferer_ReturnOk() { } [Test] - public void CsrfCheck_DifferentHostInReferer_ReturnImaTeapot() { + public void CsrfCheck_DifferentHostInReferrer_ReturnImaTeapot() { using HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, "http://127.0.0.1:8080/"); - requestMessage.Headers.Add("Referer", "http://127.0.0.2:8080"); + requestMessage.Headers.Add("Referer", "http://127.0.0.2:8080/"); using HttpClient httpClient = new HttpClient(); HttpResponseMessage result = httpClient.Send(requestMessage); diff --git a/Protest/Front/automation.js b/Protest/Front/automation.js index ae5e1285..85cef32d 100644 --- a/Protest/Front/automation.js +++ b/Protest/Front/automation.js @@ -3,9 +3,8 @@ class Automation extends List { super(); this.AddCssDependencies("list.css"); - //this.AddCssDependencies("automation.css"); - const columns = ["name", "status", "progress"]; + const columns = ["name", "status", "start", "task"]; this.SetupColumns(columns); this.columnsOptions.style.display = "none"; @@ -21,97 +20,10 @@ class Automation extends List { this.pauseButton = this.AddToolbarButton("Pause", "mono/pause.svg?light"); this.stopButton = this.AddToolbarButton("Stop", "mono/stop.svg?light"); - this.createButton.disabled = true; //TODO: <- + this.createButton.disabled = true; this.deleteButton.disabled = true; this.startButton.disabled = true; this.pauseButton.disabled = true; this.stopButton.disabled = true; - - this.ListTasks(); - } - - async ListTasks() { - try { - const response = await fetch("automation/list"); - if (response.status !== 200) LOADER.HttpErrorHandler(response.status); - - const json = await response.json(); - if (json.error) throw (json.error); - - this.link = json; - - for (let task in this.link.data) { - const element = document.createElement("div"); - element.id = task; - element.className = "list-element"; - this.list.appendChild(element); - - this.InflateElement(element, this.link.data[task]); - - element.addEventListener("click", event=>this.Entry_onclick(event)); - } - } - catch (ex) { - this.ConfirmBox(ex, true, "mono/error.svg"); - } - } - - InflateElement(element, entry) { //overrides - let icon; - switch (entry.name.v.toLowerCase()) { - case "lifeline": icon = "mono/lifeline.svg"; break; - case "lastseen": icon = "mono/lastseen.svg"; break; - case "watchdog": icon = "mono/watchdog.svg"; break; - case "issues" : icon = "mono/issues.svg"; break; - case "fetch" : icon = "mono/fetch.svg"; break; - default : icon = "mono/automation.svg"; break; - } - - const iconBox = document.createElement("div"); - iconBox.className = "list-element-icon"; - iconBox.style.backgroundImage = `url(${icon})`; - element.appendChild(iconBox); - - super.InflateElement(element, entry, null); - - if (!element.ondblclick) { - element.ondblclick = event=> { - event.stopPropagation(); - this.Entry_ondblclick(event); - }; - } - } - - Entry_onclick(event) { - this.deleteButton.disabled = true; - this.startButton.disabled = true; - this.stopButton.disabled = true; - - if (!(this.args.select in this.link.data)) { - return; - } - - if (this.link.data[this.args.select].name.v.toLowerCase() === "lifeline" || - this.link.data[this.args.select].name.v.toLowerCase() === "watchdog" || - this.link.data[this.args.select].name.v.toLowerCase() === "fetch") { - this.deleteButton.disabled = true; - } - else { - //this.deleteButton.disabled = false; //TODO: <- - } - - if (this.link.data[this.args.select].status.v.toLowerCase() === "stopped") { - //this.startButton.disabled = false; - this.stopButton.disabled = true; - } - else { - this.startButton.disabled = true; - //this.stopButton.disabled = false; - } - - } - - Entry_ondblclick(event) { - } } \ No newline at end of file diff --git a/Protest/Front/certificates.js b/Protest/Front/certificates.js new file mode 100644 index 00000000..5bc7f2c1 --- /dev/null +++ b/Protest/Front/certificates.js @@ -0,0 +1,24 @@ +class Certificates extends List { + constructor() { + super(); + + this.AddCssDependencies("list.css"); + + const columns = ["name", "status", "start", "task"]; + this.SetupColumns(columns); + + this.columnsOptions.style.display = "none"; + + this.SetTitle("Certificates"); + this.SetIcon("mono/certificate.svg"); + + this.SetupToolbar(); + this.createButton = this.AddToolbarButton("Create task", "mono/add.svg?light"); + this.deleteButton = this.AddToolbarButton("Delete", "mono/delete.svg?light"); + this.downloadButton = this.AddToolbarButton("Delete", "mono/download.svg?light"); + + this.createButton.disabled = true; + this.deleteButton.disabled = true; + this.downloadButton.disabled = true; + } +} \ No newline at end of file diff --git a/Protest/Front/keyboardtester.js b/Protest/Front/keyboardtester.js index b28dcd9a..8b28ec1c 100644 --- a/Protest/Front/keyboardtester.js +++ b/Protest/Front/keyboardtester.js @@ -742,7 +742,7 @@ class KeyboardTester extends Window { let gamepads = navigator.getGamepads(); - for (var j = 0; j < gamepads.length; j++) { + for (let j=0; j + + + + + \ No newline at end of file diff --git a/Protest/Front/mono/task.svg b/Protest/Front/mono/task.svg new file mode 100644 index 00000000..7b2266fe --- /dev/null +++ b/Protest/Front/mono/task.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Protest/Front/tasks.js b/Protest/Front/tasks.js new file mode 100644 index 00000000..8db75f4c --- /dev/null +++ b/Protest/Front/tasks.js @@ -0,0 +1,116 @@ +class Tasks extends List { + constructor() { + super(); + + this.AddCssDependencies("list.css"); + + const columns = ["name", "status", "progress"]; + this.SetupColumns(columns); + + this.columnsOptions.style.display = "none"; + + this.SetTitle("Tasks"); + this.SetIcon("mono/task.svg"); + + this.SetupToolbar(); + this.createButton = this.AddToolbarButton("Create task", "mono/add.svg?light"); + this.deleteButton = this.AddToolbarButton("Delete", "mono/delete.svg?light"); + this.toolbar.appendChild(this.AddToolbarSeparator()); + this.startButton = this.AddToolbarButton("Start", "mono/play.svg?light"); + this.pauseButton = this.AddToolbarButton("Pause", "mono/pause.svg?light"); + this.stopButton = this.AddToolbarButton("Stop", "mono/stop.svg?light"); + + this.createButton.disabled = true; //TODO: <- + this.deleteButton.disabled = true; + this.startButton.disabled = true; + this.pauseButton.disabled = true; + this.stopButton.disabled = true; + + this.ListTasks(); + } + + async ListTasks() { + try { + const response = await fetch("tasks/list"); + if (response.status !== 200) LOADER.HttpErrorHandler(response.status); + + const json = await response.json(); + if (json.error) throw (json.error); + + this.link = json; + + for (let task in this.link.data) { + const element = document.createElement("div"); + element.id = task; + element.className = "list-element"; + this.list.appendChild(element); + + this.InflateElement(element, this.link.data[task]); + + element.addEventListener("click", event=>this.Entry_onclick(event)); + } + } + catch (ex) { + this.ConfirmBox(ex, true, "mono/error.svg"); + } + } + + InflateElement(element, entry) { //overrides + let icon; + switch (entry.name.v.toLowerCase()) { + case "lifeline": icon = "mono/lifeline.svg"; break; + case "lastseen": icon = "mono/lastseen.svg"; break; + case "watchdog": icon = "mono/watchdog.svg"; break; + case "issues" : icon = "mono/issues.svg"; break; + case "fetch" : icon = "mono/fetch.svg"; break; + default : icon = "mono/task.svg"; break; + } + + const iconBox = document.createElement("div"); + iconBox.className = "list-element-icon"; + iconBox.style.backgroundImage = `url(${icon})`; + element.appendChild(iconBox); + + super.InflateElement(element, entry, null); + + if (!element.ondblclick) { + element.ondblclick = event=> { + event.stopPropagation(); + this.Entry_ondblclick(event); + }; + } + } + + Entry_onclick(event) { + this.deleteButton.disabled = true; + this.startButton.disabled = true; + this.stopButton.disabled = true; + + if (!(this.args.select in this.link.data)) { + return; + } + + if (this.link.data[this.args.select].name.v.toLowerCase() === "lifeline" || + this.link.data[this.args.select].name.v.toLowerCase() === "watchdog" || + this.link.data[this.args.select].name.v.toLowerCase() === "fetch") { + this.deleteButton.disabled = true; + } + else { + //this.deleteButton.disabled = false; //TODO: <- + } + + if (this.link.data[this.args.select].status.v.toLowerCase() === "stopped") { + //this.startButton.disabled = false; + this.stopButton.disabled = true; + } + else { + this.startButton.disabled = true; + //this.stopButton.disabled = false; + } + + } + + Entry_ondblclick(event) { + + } +} \ No newline at end of file diff --git a/Protest/Front/ui.js b/Protest/Front/ui.js index f7bd0f1d..7bafd0a7 100644 --- a/Protest/Front/ui.js +++ b/Protest/Front/ui.js @@ -346,7 +346,9 @@ const MENU = { { t:"RBAC", i:"mono/rbac.svg?light", g:"manage", h:false, f:()=> new AccessControl("rbac"), k:"rbac acl role based users access control list permissions" }, { t:"Open sessions", i:"mono/hourglass.svg?light", g:"manage", h:true, f:()=> new AccessControl("sessions"), k:"alive connections" }, + { t:"Tasks", i:"mono/task.svg?light", g:"manage", h:false, f:()=> new Tasks(), k:"" }, { t:"Automation", i:"mono/automation.svg?light", g:"manage", h:false, f:()=> new Automation(), k:"" }, + { t:"Certificates", i:"mono/certificate.svg?light", g:"manage", h:false, f:()=> new Certificates(), k: "" }, { t:"Backup", i:"mono/backup.svg?light", g:"manage", h:false, f:()=> new Backup() }, { t:"Log", i:"mono/log.svg?light", g:"manage", h:false, f:()=> new Log() }, diff --git a/Protest/Http/Auth.cs b/Protest/Http/Auth.cs index 2178b5b0..183a90bf 100644 --- a/Protest/Http/Auth.cs +++ b/Protest/Http/Auth.cs @@ -126,12 +126,13 @@ public static bool AttemptAuthentication(HttpListenerContext ctx, out string ses public static string GrandAccess(HttpListenerContext ctx, string username) { string sessionId = Cryptography.RandomStringGenerator(64); + string userHostName = ctx.Request.UserHostName.Split(':')[0]; - //RFC6265: no port allowed in the Domain attribute + //RFC6265: no port in the "Domain" attribute #if DEBUG - ctx.Response.AddHeader("Set-Cookie", $"sessionid={sessionId}; Domain={ctx.Request.UserHostName.Split(':')[0]}; Max-age=604800; HttpOnly; SameSite=Strict;"); + ctx.Response.AddHeader("Set-Cookie", $"sessionid={sessionId}; Domain={userHostName}; Max-age=604800; HttpOnly; SameSite=Strict;"); #else - ctx.Response.AddHeader("Set-Cookie", $"sessionid={sessionId}; Domain={ctx.Request.UserHostName.Split(':')[0]}; Max-age=604800; HttpOnly; SameSite=Strict; Secure;"); + ctx.Response.AddHeader("Set-Cookie", $"sessionid={sessionId}; Domain={userHostName}; Max-age=604800; HttpOnly; SameSite=Strict; Secure;"); #endif Session newSession = new Session() { diff --git a/Protest/Http/Cache.cs b/Protest/Http/Cache.cs index 72e86974..d6b764fa 100644 --- a/Protest/Http/Cache.cs +++ b/Protest/Http/Cache.cs @@ -292,11 +292,16 @@ private Entry ConstructEntry(string name, byte[] bytes, bool isGzipped, string e headers.Add(new KeyValuePair("Last-Modified", birthdate)); headers.Add(new KeyValuePair("Referrer-Policy", "no-referrer")); + if (name == "/" || name == "/login") { + headers.Add(new KeyValuePair("Cache-Control", "no-store")); + } + else { #if DEBUG - headers.Add(new KeyValuePair("Cache-Control", "no-store")); + headers.Add(new KeyValuePair("Cache-Control", "no-store")); #else - headers.Add(new KeyValuePair("Cache-Control", name == "//" ? "no-store" : $"max-age={CACHE_CONTROL_MAX_AGE}")); + headers.Add(new KeyValuePair("Cache-Control", $"max-age={CACHE_CONTROL_MAX_AGE}")); #endif + } Entry entry = new Entry() { bytes = raw, diff --git a/Protest/Http/Listener.cs b/Protest/Http/Listener.cs index c78b133f..82bc0cf6 100644 --- a/Protest/Http/Listener.cs +++ b/Protest/Http/Listener.cs @@ -47,15 +47,15 @@ private static readonly Dictionary Tools.PasswordStrength.GandalfThreadWrapper(ctx, username) }, { "/fetch/networkinfo", (ctx, parameters, username) => Protocols.Kerberos.NetworkInfo() }, - { "/fetch/singledevice", (ctx, parameters, username) => Tasks.Fetch.SingleDeviceSerialize(parameters, true) }, - { "/fetch/singleuser", (ctx, parameters, username) => Tasks.Fetch.SingleUserSerialize(parameters) }, - { "/fetch/status", (ctx, parameters, username) => Tasks.Fetch.Status() }, - { "/fetch/devices", (ctx, parameters, username) => Tasks.Fetch.DevicesTask(ctx, parameters, username) }, - { "/fetch/users", (ctx, parameters, username) => Tasks.Fetch.UsersTask(parameters, username) }, - { "/fetch/approve", (ctx, parameters, username) => Tasks.Fetch.ApproveLastTask(parameters, username) }, - { "/fetch/abort", (ctx, parameters, username) => Tasks.Fetch.CancelTask(username) }, - { "/fetch/discard", (ctx, parameters, username) => Tasks.Fetch.DiscardLastTask(username) }, - { "/fetch/import", (ctx, parameters, username) => Tasks.Import.ImportTask(parameters, username) }, + { "/fetch/singledevice", (ctx, parameters, username) => Workers.Fetch.SingleDeviceSerialize(parameters, true) }, + { "/fetch/singleuser", (ctx, parameters, username) => Workers.Fetch.SingleUserSerialize(parameters) }, + { "/fetch/status", (ctx, parameters, username) => Workers.Fetch.Status() }, + { "/fetch/devices", (ctx, parameters, username) => Workers.Fetch.DevicesTask(ctx, parameters, username) }, + { "/fetch/users", (ctx, parameters, username) => Workers.Fetch.UsersTask(parameters, username) }, + { "/fetch/approve", (ctx, parameters, username) => Workers.Fetch.ApproveLastTask(parameters, username) }, + { "/fetch/abort", (ctx, parameters, username) => Workers.Fetch.CancelTask(username) }, + { "/fetch/discard", (ctx, parameters, username) => Workers.Fetch.DiscardLastTask(username) }, + { "/fetch/import", (ctx, parameters, username) => Workers.Import.ImportTask(parameters, username) }, { "/manage/device/wol", (ctx, parameters, username) => Protocols.Wol.Wakeup(parameters) }, { "/manage/device/shutdown", (ctx, parameters, username) => OperatingSystem.IsWindows() ? Protocols.Wmi.Wmi_Win32PowerHandler(parameters, 12) : null }, @@ -82,20 +82,20 @@ private static readonly Dictionary Tools.DebitNotes.ListTemplate() }, { "/debit/banners", (ctx, parameters, username) => Tools.DebitNotes.ListBanners() }, - { "/watchdog/list", (ctx, parameters, username) => Tasks.Watchdog.List() }, - { "/watchdog/view", (ctx, parameters, username) => Tasks.Watchdog.View(parameters) }, - { "/watchdog/create", (ctx, parameters, username) => Tasks.Watchdog.Create(ctx, parameters, username) }, - { "/watchdog/delete", (ctx, parameters, username) => Tasks.Watchdog.Delete(parameters, username) }, + { "/watchdog/list", (ctx, parameters, username) => Workers.Watchdog.List() }, + { "/watchdog/view", (ctx, parameters, username) => Workers.Watchdog.View(parameters) }, + { "/watchdog/create", (ctx, parameters, username) => Workers.Watchdog.Create(ctx, parameters, username) }, + { "/watchdog/delete", (ctx, parameters, username) => Workers.Watchdog.Delete(parameters, username) }, - { "/notifications/list", (ctx, parameters, username) => Tasks.Watchdog.ListNotifications() }, - { "/notifications/save", (ctx, parameters, username) => Tasks.Watchdog.SaveNotifications(ctx, username) }, + { "/notifications/list", (ctx, parameters, username) => Workers.Watchdog.ListNotifications() }, + { "/notifications/save", (ctx, parameters, username) => Workers.Watchdog.SaveNotifications(ctx, username) }, - { "/lifeline/ping/view", (ctx, parameters, username) => Tasks.Lifeline.ViewPing(parameters) }, - { "/lifeline/memory/view", (ctx, parameters, username) => Tasks.Lifeline.ViewFile(parameters, "memory") }, - { "/lifeline/cpu/view", (ctx, parameters, username) => Tasks.Lifeline.ViewFile(parameters, "cpu") }, - { "/lifeline/disk/view", (ctx, parameters, username) => Tasks.Lifeline.ViewFile(parameters, "disk") }, - { "/lifeline/diskusage/view", (ctx, parameters, username) => Tasks.Lifeline.ViewFile(parameters, "diskusage") }, - { "/lifeline/printcount/view", (ctx, parameters, username) => Tasks.Lifeline.ViewFile(parameters, "printcount") }, + { "/lifeline/ping/view", (ctx, parameters, username) => Workers.Lifeline.ViewPing(parameters) }, + { "/lifeline/memory/view", (ctx, parameters, username) => Workers.Lifeline.ViewFile(parameters, "memory") }, + { "/lifeline/cpu/view", (ctx, parameters, username) => Workers.Lifeline.ViewFile(parameters, "cpu") }, + { "/lifeline/disk/view", (ctx, parameters, username) => Workers.Lifeline.ViewFile(parameters, "disk") }, + { "/lifeline/diskusage/view", (ctx, parameters, username) => Workers.Lifeline.ViewFile(parameters, "diskusage") }, + { "/lifeline/printcount/view", (ctx, parameters, username) => Workers.Lifeline.ViewFile(parameters, "printcount") }, { "/tools/bulkping", (ctx, parameters, username) => Protocols.Icmp.BulkPing(parameters) }, { "/tools/dnslookup", (ctx, parameters, username) => Protocols.Dns.Resolve(parameters) }, @@ -118,7 +118,7 @@ private static readonly Dictionary Auth.ListSessions() }, { "/rbac/kickuser", (ctx, parameters, username) => Auth.KickUser(parameters, username) }, - { "/automation/list", (ctx, parameters, username) => Tasks.Automation.ListTasks() }, + { "/tasks/list", (ctx, parameters, username) => Workers.Tasks.ListTasks() }, { "/config/checkupdate", (ctx, parameters, username) => Update.CheckLatestRelease() }, @@ -195,15 +195,21 @@ private void ListenerCallback(IAsyncResult result) { HttpListenerContext ctx = listener.EndGetContext(result); //Cross Site Request Forgery protection - if (ctx.Request.UrlReferrer != null) { - if (!String.Equals(ctx.Request.UrlReferrer.Host, ctx.Request.UserHostName.Split(':')[0], StringComparison.Ordinal) || - Uri.IsWellFormedUriString(ctx.Request.UrlReferrer.Host, UriKind.Absolute)) { + if (ctx.Request.UrlReferrer is not null) { + string userHostName = ctx.Request.UserHostName; + string referrerHost = ctx.Request.UrlReferrer.Host; + int referrerPort = ctx.Request.UrlReferrer.Port; + + bool isSameHost = String.Equals(referrerHost, userHostName, StringComparison.Ordinal); + bool isWellFormedUri = Uri.IsWellFormedUriString(referrerHost, UriKind.Absolute); + + if (!isSameHost && !String.Equals($"{referrerHost}:{referrerPort}", userHostName, StringComparison.Ordinal) || isWellFormedUri) { ctx.Response.StatusCode = 418; //I'm a teapot ctx.Response.Close(); return; } - UriHostNameType type = Uri.CheckHostName(ctx.Request.UrlReferrer.Host); + UriHostNameType type = Uri.CheckHostName(referrerHost); if (type != UriHostNameType.Dns && type != UriHostNameType.IPv4 && type != UriHostNameType.IPv6) { ctx.Response.StatusCode = 418; //I'm a teapot ctx.Response.Close(); @@ -213,16 +219,22 @@ private void ListenerCallback(IAsyncResult result) { //handle X-Forwarded-For header if (Configuration.accept_xff_header) { - string xff = ctx.Request.Headers.Get("X-Forwarded-For"); - if (IPAddress.TryParse(xff, out IPAddress xffIp)) { - if (!IPAddress.IsLoopback(xffIp) && - (Configuration.accept_xff_only_from is null || IPAddress.Equals(ctx.Request.RemoteEndPoint.Address, Configuration.accept_xff_only_from))) { - ctx.Request.RemoteEndPoint.Address = xffIp; - } - else { - ctx.Response.StatusCode = 418; //I'm a teapot - ctx.Response.Close(); - return; + string xffHeader = ctx.Request.Headers.Get("X-Forwarded-For"); + + if (xffHeader != null) { + int delimiterIndex = xffHeader.LastIndexOf(','); + if (delimiterIndex > 0) { xffHeader.Substring(delimiterIndex + 1).Trim(); } + + if (IPAddress.TryParse(xffHeader, out IPAddress xffIp)) { + if (!IPAddress.IsLoopback(xffIp) && + (Configuration.accept_xff_only_from is null || IPAddress.Equals(ctx.Request.RemoteEndPoint.Address, Configuration.accept_xff_only_from))) { + ctx.Response.StatusCode = 418; //I'm a teapot + ctx.Response.Close(); + return; + } + else { + ctx.Request.RemoteEndPoint.Address = xffIp; + } } } } @@ -243,7 +255,7 @@ private void ListenerCallback(IAsyncResult result) { ctx.Response.Close(); return; } - + if (String.Equals(path, "/contacts", StringComparison.Ordinal)) { byte[] buffer = DatabaseInstances.users.SerializeContacts(); ctx.Response.StatusCode = (int)HttpStatusCode.OK; diff --git a/Protest/Http/ReverseProxy.cs b/Protest/Http/ReverseProxy.cs new file mode 100644 index 00000000..9f4419c4 --- /dev/null +++ b/Protest/Http/ReverseProxy.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Yarp.ReverseProxy.Configuration; +using Yarp.ReverseProxy.Transforms; +using Protest.Protocols; +using System.Threading; + +namespace Protest.Http; + +internal sealed class ReverseProxy : IDisposable { + private IHostBuilder hostBuilder; + private IHost host; + + public ReverseProxy(IPEndPoint listener, IPEndPoint destination, string certificate = null, string password = null) + : this(listener, new IPEndPoint[] { destination }, certificate, password) { + } + + public ReverseProxy(IPEndPoint listener, IPEndPoint[] destinations, string certificate = null, string password = null) { + int counter = 0; + Dictionary dictionary = destinations.ToDictionary( + key=> $"d{++counter}", + value => new DestinationConfig { Address = $"http://{value.Address}:{value.Port}" } + ); + + ClusterConfig cluster = new ClusterConfig { + ClusterId = "c1", + //LoadBalancingPolicy = "", + Destinations = dictionary + }; + + Initialize(listener, cluster, certificate, password); + } + + public ReverseProxy(IPEndPoint listener, ClusterConfig cluster, string certificate = null, string password = null) { + Initialize(listener, cluster, certificate, password); + } + + private bool Initialize(IPEndPoint listener, ClusterConfig cluster, string certificate, string password) { + hostBuilder = Host.CreateDefaultBuilder(); + + hostBuilder.ConfigureLogging(logger => this.ConfigureLogging(logger)); + + hostBuilder.ConfigureWebHostDefaults(webHost => { + webHost.ConfigureKestrel(options => this.ConfigureKestrel(options, listener, certificate, password)); + webHost.Configure(application => this.Configure(application)); + + RouteConfig[] routes = new RouteConfig[] { + new RouteConfig { + RouteId = "r1", + ClusterId = "c1", + Match = new RouteMatch { Path = "/{**all}" } + } + }; + + webHost.ConfigureServices(services => this.ConfigureServices(services, routes, new ClusterConfig[] { cluster })); + }); + + string destinations = cluster.Destinations.Values + .Select(o=>o.Address.ToString()) + .Aggregate((destination, accumulator)=> String.IsNullOrEmpty(accumulator) ? destination : $"{accumulator}, {destination}"); + + Console.WriteLine($"Start proxying from {listener} to {destinations}"); + + this.host = hostBuilder.Build(); + this.host.Run(); + + //Console.WriteLine($"Stop proxying from {listener} to {destinations}"); + + return true; + } + + public void Stop() { + this.host?.StopAsync(); + this.host = null; + } + + private void ConfigureLogging(ILoggingBuilder logger) { + logger.ClearProviders(); + //logger.AddConsole(); + //logger.SetMinimumLevel(LogLevel.Warning); + //logger.AddFilter("Microsoft", LogLevel.Warning); + //logger.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Information); + //logger.AddFilter("Yarp.ReverseProxy", LogLevel.Warning); + } + + private void ConfigureKestrel(KestrelServerOptions options, IPEndPoint endPoint, string certificate = null, string password = null) { + if (String.IsNullOrEmpty(certificate)) { + options.Listen(endPoint); + } + else if (String.IsNullOrEmpty(password)) { + options.Listen(endPoint, options => options.UseHttps(certificate)); + } + else { + options.Listen(endPoint, options => options.UseHttps(certificate, password)); + } + } + + private void Configure(IApplicationBuilder application) { + application.UseRouting(); + application.UseEndpoints(endpoints => endpoints.MapReverseProxy()); + } + + private void ConfigureServices(IServiceCollection services, IReadOnlyList routes, IReadOnlyList clusters) { + services.AddSingleton(); + + IReverseProxyBuilder rpBuilder = services.AddReverseProxy(); + + rpBuilder.LoadFromMemory(routes, clusters); + + rpBuilder.AddTransforms(builderContext => { + builderContext.AddRequestTransform(transformContext => { + string remoteIpAddress = transformContext.HttpContext.Connection.RemoteIpAddress?.ToString(); + string existingXForwardedFor = transformContext.HttpContext.Request.Headers["X-Forwarded-For"].ToString(); + string newXForwardedFor = string.IsNullOrEmpty(existingXForwardedFor) ? remoteIpAddress : $"{existingXForwardedFor}, {remoteIpAddress}"; + + transformContext.ProxyRequest.Headers.Remove("X-Forwarded-For"); + transformContext.ProxyRequest.Headers.Add("X-Forwarded-For", newXForwardedFor); + //transformContext.ProxyRequest.Headers.Add("X-Forwarded-Host", transformContext.HttpContext.Request.Host.Value); + //transformContext.ProxyRequest.Headers.Add("X-Forwarded-Proto", transformContext.HttpContext.Request.Scheme); + + transformContext.HttpContext.Request.Headers.Remove("Host"); + //transformContext.ProxyRequest.Headers.Host = transformContext.HttpContext.Request.Headers.Host; + + return ValueTask.CompletedTask; + }); + }); + } + + public void Dispose() { + this.Stop(); + } +} + +file class CustomHostLifetime : IHostLifetime { + /* Custom Host Lifetime: overrides the default behavior, + * so the reverse proxy will not terminate on Ctrl+C + */ + public Task StopAsync(CancellationToken cancellationToken) => + Task.CompletedTask; + + public Task WaitForStartAsync(CancellationToken cancellationToken) => + Task.CompletedTask; +} \ No newline at end of file diff --git a/Protest/Misc/Configuration.cs b/Protest/Misc/Configuration.cs index ef4ba97f..c521f7a8 100644 --- a/Protest/Misc/Configuration.cs +++ b/Protest/Misc/Configuration.cs @@ -12,9 +12,11 @@ internal static class Configuration { internal static byte[] DB_KEY_IV; internal static bool force_registry_keys = false; - internal static bool accept_xff_header = false; + internal static bool accept_xff_header = true; internal static IPAddress accept_xff_only_from = null; + internal static readonly string[] alternativeUriPrefixes = new string[] { "http://127.0.0.1:8080/" }; + internal static string front_path = $"{Data.DIR_ROOT}{Data.DELIMITER}front"; internal static string[] http_prefixes = new string[] { "http://127.0.0.1:8080/" }; @@ -138,7 +140,7 @@ internal static void CreateDefault() { #endif builder.AppendLine($"force_registry_keys = {force_registry_keys.ToString().ToLower()}"); - builder.AppendLine("accept_xff_header = false"); + builder.AppendLine("accept_xff_header = true"); builder.AppendLine("#accept_xff_header_only_from = [reverse proxy ip address]"); builder.AppendLine(); diff --git a/Protest/Misc/Data.cs b/Protest/Misc/Data.cs index 77a8051f..47f5219a 100644 --- a/Protest/Misc/Data.cs +++ b/Protest/Misc/Data.cs @@ -35,11 +35,12 @@ public static class Data { public static readonly ArraySegment CODE_OTHER_TASK_IN_PROGRESS = new ArraySegment("{\"error\":\"another task is already in progress\"}"u8.ToArray()); public static readonly ArraySegment CODE_TASK_DONT_EXITSTS = new ArraySegment("{\"error\":\"this task no longer exists\"}"u8.ToArray()); - public static readonly string DIR_ROOT = $"{Directory.GetCurrentDirectory()}{DELIMITER}protest"; - public static readonly string DIR_KNOWLADGE = $"{DIR_ROOT}{DELIMITER}knowledge"; - public static readonly string DIR_RBAC = $"{DIR_ROOT}{DELIMITER}rbac"; - public static readonly string DIR_LOG = $"{DIR_ROOT}{DELIMITER}log"; - public static readonly string DIR_BACKUP = $"{DIR_ROOT}{DELIMITER}backup"; + public static readonly string DIR_ROOT = $"{Directory.GetCurrentDirectory()}{DELIMITER}protest"; + public static readonly string DIR_KNOWLADGE = $"{DIR_ROOT}{DELIMITER}knowledge"; + public static readonly string DIR_RBAC = $"{DIR_ROOT}{DELIMITER}rbac"; + public static readonly string DIR_LOG = $"{DIR_ROOT}{DELIMITER}log"; + public static readonly string DIR_BACKUP = $"{DIR_ROOT}{DELIMITER}backup"; + public static readonly string DIR_CERTIFICATES = $"{DIR_ROOT}{DELIMITER}certificates"; public static readonly string DIR_DATA = $"{DIR_ROOT}{DELIMITER}data"; public static readonly string DIR_DEVICES = $"{DIR_DATA}{DELIMITER}devices"; diff --git a/Protest/Program.cs b/Protest/Program.cs index edbabcd5..72b6d217 100644 --- a/Protest/Program.cs +++ b/Protest/Program.cs @@ -21,9 +21,7 @@ Released into the public domain under the GPL v3 namespace Protest; internal class Program { - internal static readonly string[] alternativeUriPrefixes = new string[] { "http://127.0.0.1:8080/" }; - - static void Main(string[] args) { + static void Main(string[] args) { Console.Title = "Pro-test"; Console.WriteLine(@" _____"); @@ -63,25 +61,34 @@ static void Main(string[] args) { Console.WriteLine(String.Format("{0, -23} {1, -10}", "Loading RBAC", loadRbac ? "OK " : "Failed")); Console.Write("Launching tasks"); - Tasks.Automation.Initialize(); + Workers.Automation.Initialize(); Console.WriteLine(" OK"); Console.WriteLine(); + new System.Threading.Thread(() => { + Protest.Http.ReverseProxy rp = new Protest.Http.ReverseProxy( + new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 8443), + new System.Net.IPEndPoint(System.Net.IPAddress.Parse("127.0.0.1"), 8080), + $"{Data.DIR_CERTIFICATES}{Data.DELIMITER}certificate.pfx", + "your_password" + ); + }).Start(); + try { - Http.Listener listener = new Http.Listener(Configuration.http_prefixes, Configuration.front_path); - Console.WriteLine(listener.ToString()); - Console.WriteLine(); - listener.Start(); + StartServer(Configuration.http_prefixes); } catch (System.Net.HttpListenerException ex) { if (ex.ErrorCode != 5) return; //access denied Console.WriteLine("Switching to alternative prefix"); - - Http.Listener listener = new Http.Listener(alternativeUriPrefixes, Configuration.front_path); - Console.WriteLine(listener.ToString()); - Console.WriteLine(); - listener.Start(); + StartServer(Configuration.alternativeUriPrefixes); } } + + private static void StartServer(string[] prefixes) { + Http.Listener listener = new Http.Listener(prefixes, Configuration.front_path); + Console.WriteLine(listener.ToString()); + Console.WriteLine(); + listener.Start(); + } } \ No newline at end of file diff --git a/Protest/Protest.csproj b/Protest/Protest.csproj index 78ea4bb8..4b911a78 100644 --- a/Protest/Protest.csproj +++ b/Protest/Protest.csproj @@ -42,7 +42,7 @@ False True - false + true Pro-test false @@ -52,10 +52,11 @@ - - + + + diff --git a/Protest/Tools/LiveStats.cs b/Protest/Tools/LiveStats.cs index f632a51a..4c3a881b 100644 --- a/Protest/Tools/LiveStats.cs +++ b/Protest/Tools/LiveStats.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; using System.Timers; using Protest.Protocols; -using Protest.Tasks; +using Protest.Workers; using Lextm.SharpSnmpLib; namespace Protest.Tools; diff --git a/Protest/Workers/Automation.cs b/Protest/Workers/Automation.cs new file mode 100644 index 00000000..c9cb7b15 --- /dev/null +++ b/Protest/Workers/Automation.cs @@ -0,0 +1,14 @@ +using System.Collections.Concurrent; +using System.Text; + +namespace Protest.Workers; + +internal static class Automation { + + //static readonly ConcurrentDictionary tasks = new ConcurrentDictionary(); + + static public void Initialize() { + Watchdog.Initialize(); + Lifeline.Initialize(); + } +} \ No newline at end of file diff --git a/Protest/Tasks/Fetch.cs b/Protest/Workers/Fetch.cs similarity index 99% rename from Protest/Tasks/Fetch.cs rename to Protest/Workers/Fetch.cs index 4195eee2..d4e892bb 100644 --- a/Protest/Tasks/Fetch.cs +++ b/Protest/Workers/Fetch.cs @@ -16,7 +16,7 @@ using Lextm.SharpSnmpLib; using Protest.Protocols.Snmp; -namespace Protest.Tasks; +namespace Protest.Workers; internal static class Fetch { diff --git a/Protest/Tasks/Import.cs b/Protest/Workers/Import.cs similarity index 99% rename from Protest/Tasks/Import.cs rename to Protest/Workers/Import.cs index 8f0caa93..8791514a 100644 --- a/Protest/Tasks/Import.cs +++ b/Protest/Workers/Import.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Protest.Tools; -namespace Protest.Tasks; +namespace Protest.Workers; internal class Import { public static byte[] ImportTask(Dictionary parameters, string origin) { diff --git a/Protest/Tasks/Issues.cs b/Protest/Workers/Issues.cs similarity index 96% rename from Protest/Tasks/Issues.cs rename to Protest/Workers/Issues.cs index 3a9f602e..8a9c57ac 100644 --- a/Protest/Tasks/Issues.cs +++ b/Protest/Workers/Issues.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Protest.Tasks; +namespace Protest.Workers; internal static class Issues { diff --git a/Protest/Tasks/LastSeen.cs b/Protest/Workers/LastSeen.cs similarity index 98% rename from Protest/Tasks/LastSeen.cs rename to Protest/Workers/LastSeen.cs index 98706b7f..3f1c6bbb 100644 --- a/Protest/Tasks/LastSeen.cs +++ b/Protest/Workers/LastSeen.cs @@ -3,7 +3,7 @@ using System.Net.NetworkInformation; using System.Threading; -namespace Protest.Tasks { +namespace Protest.Workers { internal static class LastSeen { private static ConcurrentDictionary mutexes = new ConcurrentDictionary(); diff --git a/Protest/Tasks/Lifeline.cs b/Protest/Workers/Lifeline.cs similarity index 99% rename from Protest/Tasks/Lifeline.cs rename to Protest/Workers/Lifeline.cs index 5245383c..8390de28 100644 --- a/Protest/Tasks/Lifeline.cs +++ b/Protest/Workers/Lifeline.cs @@ -11,7 +11,7 @@ using Protest.Tools; using Lextm.SharpSnmpLib; -namespace Protest.Tasks; +namespace Protest.Workers; internal static partial class Lifeline { private const long FOUR_HOURS_IN_TICKS = 144_000_000_000L; diff --git a/Protest/Tasks/TaskWrapper.cs b/Protest/Workers/TaskWrapper.cs similarity index 99% rename from Protest/Tasks/TaskWrapper.cs rename to Protest/Workers/TaskWrapper.cs index 807192c7..9e7aff59 100644 --- a/Protest/Tasks/TaskWrapper.cs +++ b/Protest/Workers/TaskWrapper.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Protest.Tasks; +namespace Protest.Workers; internal sealed class TaskWrapper : IDisposable { diff --git a/Protest/Tasks/Automation.cs b/Protest/Workers/Tasks.cs similarity index 80% rename from Protest/Tasks/Automation.cs rename to Protest/Workers/Tasks.cs index 552cae4c..b27b69b6 100644 --- a/Protest/Tasks/Automation.cs +++ b/Protest/Workers/Tasks.cs @@ -1,18 +1,12 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; +using System.Threading.Tasks; -namespace Protest.Tasks; - -internal static class Automation { - - //static readonly ConcurrentDictionary tasks = new ConcurrentDictionary(); - - static public void Initialize() { - Tasks.Watchdog.Initialize(); - Tasks.Lifeline.Initialize(); - } - - public static byte[] ListTasks() { +namespace Protest.Workers; +internal class Tasks { + public static byte[] ListTasks() { StringBuilder builder = new StringBuilder(); builder.Append('{'); @@ -57,4 +51,5 @@ public static byte[] ListTasks() { return Encoding.UTF8.GetBytes(builder.ToString()); } -} \ No newline at end of file + +} diff --git a/Protest/Tasks/Watchdog.cs b/Protest/Workers/Watchdog.cs similarity index 99% rename from Protest/Tasks/Watchdog.cs rename to Protest/Workers/Watchdog.cs index d14fa639..32abfe53 100644 --- a/Protest/Tasks/Watchdog.cs +++ b/Protest/Workers/Watchdog.cs @@ -13,7 +13,7 @@ using System.Net.Mail; using Protest.Tools; -namespace Protest.Tasks; +namespace Protest.Workers; internal static class Watchdog { private const long WEEK_IN_TICKS = 6_048_000_000_000L;