diff --git a/src/Tool/src/Boost.Abstractions/Web/ILocalProxyServer.cs b/src/Tool/src/Boost.Abstractions/Web/ILocalProxyServer.cs new file mode 100644 index 0000000..e7ae765 --- /dev/null +++ b/src/Tool/src/Boost.Abstractions/Web/ILocalProxyServer.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Boost.Web.Proxy +{ + public interface ILocalProxyServer : IDisposable + { + Task StartAsync(LocalProxyOptions options, CancellationToken cancellationToken); + Task StopAsync(); + } +} diff --git a/src/Tool/src/Boost.Abstractions/Web/LocalProxyOptions.cs b/src/Tool/src/Boost.Abstractions/Web/LocalProxyOptions.cs new file mode 100644 index 0000000..21efd53 --- /dev/null +++ b/src/Tool/src/Boost.Abstractions/Web/LocalProxyOptions.cs @@ -0,0 +1,9 @@ +namespace Boost.Web +{ + public class LocalProxyOptions + { + public int Port { get; set; } + + public string DestinationAddress { get; set; } + } +} diff --git a/src/Tool/src/Boost.Core/Boost.Core.csproj b/src/Tool/src/Boost.Core/Boost.Core.csproj index 35d6b4b..4271e5d 100644 --- a/src/Tool/src/Boost.Core/Boost.Core.csproj +++ b/src/Tool/src/Boost.Core/Boost.Core.csproj @@ -1,4 +1,4 @@ - + net5.0 false @@ -28,6 +28,7 @@ + diff --git a/src/Tool/src/Boost.Core/BoostServiceCollectionExtensions.cs b/src/Tool/src/Boost.Core/BoostServiceCollectionExtensions.cs index 8801e69..1040328 100644 --- a/src/Tool/src/Boost.Core/BoostServiceCollectionExtensions.cs +++ b/src/Tool/src/Boost.Core/BoostServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Boost.Security; using Boost.Settings; using Boost.Utils; +using Boost.Web.Proxy; using Boost.Workspace; using HotChocolate.Execution.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -51,6 +52,7 @@ public static IServiceCollection AddBoost(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddUserDataProtection(); diff --git a/src/Tool/src/Boost.Core/Commands/LocalProxyCommand.cs b/src/Tool/src/Boost.Core/Commands/LocalProxyCommand.cs new file mode 100644 index 0000000..46765d6 --- /dev/null +++ b/src/Tool/src/Boost.Core/Commands/LocalProxyCommand.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading.Tasks; +using Boost.Infrastructure; +using Boost.Web; +using Boost.Web.Proxy; +using McMaster.Extensions.CommandLineUtils; + +namespace Boost.Commands +{ + [Command( + Name = "lp", + FullName = "Local proxy", + Description = "Starts a local proxy server")] + public class LocalProxyCommand : CommandBase + { + private readonly ILocalProxyServer _proxyServer; + + public LocalProxyCommand(ILocalProxyServer proxyServer) + { + _proxyServer = proxyServer; + } + + [Option("--port ", Description = "Webserver port")] + public int Port { get; set; } = 5001; + + [Argument(0, "DestinationAddress", ShowInHelpText = true)] + public string DestinationAddress { get; set; } = default!; + + public async Task OnExecute(IConsole console) + { + console.WriteLine("Starting local proxy"); + var port = NetworkExtensions.GetAvailablePort(Port); + + if (port != Port) + { + console.WriteLine($"Port {Port} is allready in use.", ConsoleColor.Yellow); + var useOther = Prompt.GetYesNo($"Start proxy on port: {port}", true); + + if (useOther) + { + Port = port; + } + else + { + return; + } + } + var options = new LocalProxyOptions + { + Port = Port, + DestinationAddress = DestinationAddress + }; + + string url = await _proxyServer.StartAsync( + options, + CommandAborded); + + ProcessHelpers.OpenBrowser(url); + + var stopMessage = "Press 'q' or 'esc' to stop"; + console.WriteLine(stopMessage); + + while (true) + { + ConsoleKeyInfo key = Console.ReadKey(); + if (key.KeyChar == 'q' || key.Key == ConsoleKey.Escape) + { + break; + } + else + { + console.ClearLine(); + console.WriteLine("Unknown command", ConsoleColor.Red); + Console.WriteLine(stopMessage); + } + } + + console.WriteLine("Stopping proxy...."); + await _proxyServer.StopAsync(); + + _proxyServer.Dispose(); + } + } +} diff --git a/src/Tool/src/Boost.Core/Web/Proxy/LocalProxyServer.cs b/src/Tool/src/Boost.Core/Web/Proxy/LocalProxyServer.cs new file mode 100644 index 0000000..bef24fe --- /dev/null +++ b/src/Tool/src/Boost.Core/Web/Proxy/LocalProxyServer.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Yarp.ReverseProxy.Abstractions; + +namespace Boost.Web.Proxy +{ + public class LocalProxyServer : ILocalProxyServer, IDisposable + { + private IHost _host = default!; + + public async Task StartAsync( + LocalProxyOptions options, + CancellationToken cancellationToken) + { + var url = $"https://localhost:{options.Port}"; + + _host = Host.CreateDefaultBuilder() + .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseUrls(url); + webBuilder.UseStartup(); + }) + .ConfigureServices((ctx, services) => + { + ProxyRoute[]? routes = new[] + { + new ProxyRoute() + { + RouteId = "route1", + ClusterId = "cluster1", + Match = new RouteMatch + { + Path = "{**catch-all}" + } + } + }; + + Cluster[] clusters = new[] + { + new Cluster() + { + Id = "cluster1", + Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "destination1", new Destination() { Address = options.DestinationAddress} } + } + } + }; + + services.AddReverseProxy() + .LoadFromMemory(routes, clusters); + }) + + .Build(); + + await _host.StartAsync(cancellationToken); + + return url; + } + + public Task StopAsync() + { + return _host.StopAsync(); + } + + public void Dispose() + { + if (_host is { }) + { + _host.Dispose(); + } + } + } +} diff --git a/src/Tool/src/Boost.Core/Web/Proxy/ProxyConfigProvider.cs b/src/Tool/src/Boost.Core/Web/Proxy/ProxyConfigProvider.cs new file mode 100644 index 0000000..0ee6398 --- /dev/null +++ b/src/Tool/src/Boost.Core/Web/Proxy/ProxyConfigProvider.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Yarp.ReverseProxy.Abstractions; +using Yarp.ReverseProxy.Service; + +namespace Boost.Web.Proxy +{ + public static class InMemoryConfigProviderExtensions + { + public static IReverseProxyBuilder LoadFromMemory( + this IReverseProxyBuilder builder, + IReadOnlyList routes, + IReadOnlyList clusters) + { + builder.Services.AddSingleton( + new InMemoryConfigProvider(routes, clusters)); + + return builder; + } + } + public class InMemoryConfigProvider : IProxyConfigProvider + { + private volatile InMemoryConfig _config; + + public InMemoryConfigProvider( + IReadOnlyList routes, + IReadOnlyList clusters) + { + _config = new InMemoryConfig(routes, clusters); + } + + public IProxyConfig GetConfig() => _config; + + public void Update(IReadOnlyList routes, IReadOnlyList clusters) + { + InMemoryConfig oldConfig = _config; + _config = new InMemoryConfig(routes, clusters); + oldConfig.SignalChange(); + } + + private class InMemoryConfig : IProxyConfig + { + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + + public InMemoryConfig(IReadOnlyList routes, IReadOnlyList clusters) + { + Routes = routes; + Clusters = clusters; + ChangeToken = new CancellationChangeToken(_cts.Token); + } + + public IReadOnlyList Routes { get; } + + public IReadOnlyList Clusters { get; } + + public IChangeToken ChangeToken { get; } + + internal void SignalChange() + { + _cts.Cancel(); + } + } + } +} diff --git a/src/Tool/src/Boost.Core/Web/Proxy/Startup.cs b/src/Tool/src/Boost.Core/Web/Proxy/Startup.cs new file mode 100644 index 0000000..016c22e --- /dev/null +++ b/src/Tool/src/Boost.Core/Web/Proxy/Startup.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Builder; + +namespace Boost.Web.Proxy +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapReverseProxy(); + }); + } + } +} diff --git a/src/Tool/src/Boost.Tool/Boost.Tool.csproj b/src/Tool/src/Boost.Tool/Boost.Tool.csproj index b02dd30..268e83e 100644 --- a/src/Tool/src/Boost.Tool/Boost.Tool.csproj +++ b/src/Tool/src/Boost.Tool/Boost.Tool.csproj @@ -1,4 +1,4 @@ - + Exe net5.0 diff --git a/src/Tool/src/Boost.Tool/Program.cs b/src/Tool/src/Boost.Tool/Program.cs index e62277c..668ba61 100644 --- a/src/Tool/src/Boost.Tool/Program.cs +++ b/src/Tool/src/Boost.Tool/Program.cs @@ -30,6 +30,7 @@ namespace Boost.Tool typeof(QuickActionsCommand), typeof(VersionCommand), typeof(SwitchRepositoryCommand), + typeof(LocalProxyCommand), typeof(IndexRepositoriesCommand))] class Program { diff --git a/src/Tool/src/Boost.Tool/Properties/launchSettings.json b/src/Tool/src/Boost.Tool/Properties/launchSettings.json index ff7a4cd..cb739c0 100644 --- a/src/Tool/src/Boost.Tool/Properties/launchSettings.json +++ b/src/Tool/src/Boost.Tool/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Boost.Tool": { "commandName": "Project", - "commandLineArgs": "ui" + "commandLineArgs": "lp https://c4c-sidecar-blueprint.a.portals.swisslife.ch" } } } \ No newline at end of file