diff --git a/DysonNetwork.Gateway/Controllers/WellKnownController.cs b/DysonNetwork.Gateway/Controllers/WellKnownController.cs
new file mode 100644
index 0000000..7e94544
--- /dev/null
+++ b/DysonNetwork.Gateway/Controllers/WellKnownController.cs
@@ -0,0 +1,16 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace DysonNetwork.Gateway.Controllers;
+
+[ApiController]
+[Route("/.well-known")]
+public class WellKnownController(IConfiguration configuration) : ControllerBase
+{
+ [HttpGet("domains")]
+ public IActionResult GetDomainMappings()
+ {
+ var domainMappings = configuration.GetSection("DomainMappings").GetChildren()
+ .ToDictionary(x => x.Key, x => x.Value);
+ return Ok(domainMappings);
+ }
+}
\ No newline at end of file
diff --git a/DysonNetwork.Gateway/Dockerfile b/DysonNetwork.Gateway/Dockerfile
new file mode 100644
index 0000000..b9273da
--- /dev/null
+++ b/DysonNetwork.Gateway/Dockerfile
@@ -0,0 +1,23 @@
+FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["DysonNetwork.Gateway/DysonNetwork.Gateway.csproj", "DysonNetwork.Gateway/"]
+RUN dotnet restore "DysonNetwork.Gateway/DysonNetwork.Gateway.csproj"
+COPY . .
+WORKDIR "/src/DysonNetwork.Gateway"
+RUN dotnet build "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "DysonNetwork.Gateway.dll"]
\ No newline at end of file
diff --git a/DysonNetwork.Gateway/DysonNetwork.Gateway.csproj b/DysonNetwork.Gateway/DysonNetwork.Gateway.csproj
new file mode 100644
index 0000000..1e7d39a
--- /dev/null
+++ b/DysonNetwork.Gateway/DysonNetwork.Gateway.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DysonNetwork.Gateway/EtcdProxyConfigProvider.cs b/DysonNetwork.Gateway/EtcdProxyConfigProvider.cs
new file mode 100644
index 0000000..5984aea
--- /dev/null
+++ b/DysonNetwork.Gateway/EtcdProxyConfigProvider.cs
@@ -0,0 +1,108 @@
+using System.Text;
+using dotnet_etcd.interfaces;
+using Yarp.ReverseProxy.Configuration;
+
+namespace DysonNetwork.Gateway;
+
+public class EtcdProxyConfigProvider : IProxyConfigProvider, IDisposable
+{
+ private readonly IEtcdClient _etcdClient;
+ private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
+ private readonly CancellationTokenSource _watchCts = new();
+ private CancellationTokenSource _cts = new();
+
+ public EtcdProxyConfigProvider(IEtcdClient etcdClient, IConfiguration configuration, ILogger logger)
+ {
+ _etcdClient = etcdClient;
+ _configuration = configuration;
+ _logger = logger;
+
+ // Watch for changes in etcd
+ _etcdClient.WatchRange("/services/", _ =>
+ {
+ _logger.LogInformation("Etcd configuration changed. Reloading proxy config.");
+ _cts.Cancel();
+ _cts = new CancellationTokenSource();
+ }, cancellationToken: _watchCts.Token);
+ }
+
+ public IProxyConfig GetConfig()
+ {
+ // This will be called by YARP when it needs a new config
+ _logger.LogInformation("Generating new proxy config.");
+ var response = _etcdClient.GetRange("/services/");
+ var kvs = response.Kvs;
+
+ var clusters = new List();
+ var routes = new List();
+
+ var domainMappings = _configuration.GetSection("DomainMappings").GetChildren()
+ .ToDictionary(x => x.Key, x => x.Value);
+
+ _logger.LogInformation("Indexing {ServiceCount} services from Etcd.", kvs.Count);
+
+ foreach (var kv in kvs)
+ {
+ var serviceName = Encoding.UTF8.GetString(kv.Key.ToByteArray()).Replace("/services/", "");
+ var serviceUrl = Encoding.UTF8.GetString(kv.Value.ToByteArray());
+
+ _logger.LogInformation(" Service: {ServiceName}, URL: {ServiceUrl}", serviceName, serviceUrl);
+
+ var cluster = new ClusterConfig
+ {
+ ClusterId = serviceName,
+ Destinations = new Dictionary
+ {
+ { "destination1", new DestinationConfig { Address = serviceUrl } }
+ }
+ };
+ clusters.Add(cluster);
+
+ // Host-based routing
+ if (domainMappings.TryGetValue(serviceName, out var domain))
+ {
+ var hostRoute = new RouteConfig
+ {
+ RouteId = $"{serviceName}-host",
+ ClusterId = serviceName,
+ Match = new RouteMatch
+ {
+ Hosts = new[] { domain },
+ Path = "/{**catch-all}"
+ }
+ };
+ routes.Add(hostRoute);
+ _logger.LogInformation(" Added Host-based Route: {Host}", domain);
+ }
+
+ // Path-based routing
+ var pathRoute = new RouteConfig
+ {
+ RouteId = $"{serviceName}-path",
+ ClusterId = serviceName,
+ Match = new RouteMatch { Path = $"/{serviceName}/{{**catch-all}}" }
+ };
+ routes.Add(pathRoute);
+ _logger.LogInformation(" Added Path-based Route: {Path}", pathRoute.Match.Path);
+ }
+
+ return new CustomProxyConfig(routes, clusters);
+ }
+
+ private class CustomProxyConfig(IReadOnlyList routes, IReadOnlyList clusters)
+ : IProxyConfig
+ {
+ public IReadOnlyList Routes { get; } = routes;
+ public IReadOnlyList Clusters { get; } = clusters;
+ public Microsoft.Extensions.Primitives.IChangeToken ChangeToken { get; } = new Microsoft.Extensions.Primitives.CancellationChangeToken(CancellationToken.None);
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _cts.Dispose();
+ _watchCts.Cancel();
+ _watchCts.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/DysonNetwork.Gateway/Program.cs b/DysonNetwork.Gateway/Program.cs
new file mode 100644
index 0000000..df34149
--- /dev/null
+++ b/DysonNetwork.Gateway/Program.cs
@@ -0,0 +1,15 @@
+using DysonNetwork.Gateway.Startup;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddGateway(builder.Configuration);
+builder.Services.AddControllers();
+
+var app = builder.Build();
+
+// app.UseHttpsRedirection();
+
+app.MapReverseProxy();
+
+app.Run();
diff --git a/DysonNetwork.Gateway/Properties/launchSettings.json b/DysonNetwork.Gateway/Properties/launchSettings.json
new file mode 100644
index 0000000..bb5bb2e
--- /dev/null
+++ b/DysonNetwork.Gateway/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5094",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:7034;http://localhost:5094",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/DysonNetwork.Gateway/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Gateway/Startup/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..ba1f598
--- /dev/null
+++ b/DysonNetwork.Gateway/Startup/ServiceCollectionExtensions.cs
@@ -0,0 +1,16 @@
+using DysonNetwork.Shared.Registry;
+using Yarp.ReverseProxy.Configuration;
+
+namespace DysonNetwork.Gateway.Startup;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddGateway(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddReverseProxy();
+ services.AddRegistryService(configuration);
+ services.AddSingleton();
+
+ return services;
+ }
+}
diff --git a/DysonNetwork.Gateway/appsettings.json b/DysonNetwork.Gateway/appsettings.json
new file mode 100644
index 0000000..0e7c15b
--- /dev/null
+++ b/DysonNetwork.Gateway/appsettings.json
@@ -0,0 +1,25 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "Etcd": "etcd.orb.local:2379"
+ },
+ "Etcd": {
+ "Insecure": true
+ },
+ "Service": {
+ "Name": "DysonNetwork.Gateway",
+ "Url": "https://localhost:7034"
+ },
+ "DomainMappings": {
+ "DysonNetwork.Pass": "id.solsynth.dev",
+ "DysonNetwork.Drive": "drive.solsynth.dev",
+ "DysonNetwork.Pusher": "push.solsynth.dev",
+ "DysonNetwork.Sphere": "sphere.solsynth.dev"
+ }
+}
diff --git a/DysonNetwork.Shared/Auth/Startup.cs b/DysonNetwork.Shared/Auth/Startup.cs
index 103aac2..3cd02ed 100644
--- a/DysonNetwork.Shared/Auth/Startup.cs
+++ b/DysonNetwork.Shared/Auth/Startup.cs
@@ -15,8 +15,8 @@ public static class DysonAuthStartup
{
var etcdClient = sp.GetRequiredService();
var config = sp.GetRequiredService();
- var clientCertPath = config["Service:ClientCert"];
- var clientKeyPath = config["Service:ClientKey"];
+ var clientCertPath = config["Service:ClientCert"]!;
+ var clientKeyPath = config["Service:ClientKey"]!;
var clientCertPassword = config["Service:CertPassword"];
return GrpcClientHelper
@@ -24,6 +24,20 @@ public static class DysonAuthStartup
.GetAwaiter()
.GetResult();
});
+
+ services.AddSingleton(sp =>
+ {
+ var etcdClient = sp.GetRequiredService();
+ var config = sp.GetRequiredService();
+ var clientCertPath = config["Service:ClientCert"]!;
+ var clientKeyPath = config["Service:ClientKey"]!;
+ var clientCertPassword = config["Service:CertPassword"];
+
+ return GrpcClientHelper
+ .CreatePermissionServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
+ .GetAwaiter()
+ .GetResult();
+ });
services.AddAuthentication(options =>
{
diff --git a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs
index f254575..05b3213 100644
--- a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs
+++ b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs
@@ -61,6 +61,18 @@ public static class GrpcClientHelper
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
+
+ public static async Task CreatePermissionServiceClient(
+ IEtcdClient etcdClient,
+ string clientCertPath,
+ string clientKeyPath,
+ string? clientCertPassword = null
+ )
+ {
+ var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
+ return new PermissionService.PermissionServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
+ clientCertPassword));
+ }
public static async Task CreatePusherServiceClient(
IEtcdClient etcdClient,
diff --git a/DysonNetwork.sln b/DysonNetwork.sln
index 1ef9fd7..0ce0170 100644
--- a/DysonNetwork.sln
+++ b/DysonNetwork.sln
@@ -1,5 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
+#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Sphere", "DysonNetwork.Sphere\DysonNetwork.Sphere.csproj", "{CFF62EFA-F4C2-4FC7-8D97-25570B4DB452}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A444D180-5B51-49C3-A35D-AA55832BBC66}"
@@ -15,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Pusher", "Dyso
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Drive", "DysonNetwork.Drive\DysonNetwork.Drive.csproj", "{8DE0B783-8852-494D-B90A-201ABBB71202}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Gateway", "DysonNetwork.Gateway\DysonNetwork.Gateway.csproj", "{19EB0086-4049-4B78-91C4-EAC37130A006}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,5 +44,9 @@ Global
{8DE0B783-8852-494D-B90A-201ABBB71202}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DE0B783-8852-494D-B90A-201ABBB71202}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DE0B783-8852-494D-B90A-201ABBB71202}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19EB0086-4049-4B78-91C4-EAC37130A006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19EB0086-4049-4B78-91C4-EAC37130A006}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19EB0086-4049-4B78-91C4-EAC37130A006}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19EB0086-4049-4B78-91C4-EAC37130A006}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/compose.yaml b/compose.yaml
index 00ec211..fbfb60d 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,7 +1,96 @@
-services:
- sphere:
- image: xsheep2010/dyson-sphere:latest
+services:
+ etcd:
+ image: bitnami/etcd:latest
+ ports:
+ - "2379:2379"
+ - "2380:2380"
+ environment:
+ - ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379
+ - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
+ - ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
+ - ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd:2380
+ - ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster
+ - ETCD_INITIAL_CLUSTER_STATE=new
+ - ETCD_INITIAL_CLUSTER=etcd=http://etcd:2380
+ healthcheck:
+ test: ["CMD", "etcdctl", "get", "/health"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+
+ gateway:
+ build:
+ context: .
+ dockerfile: DysonNetwork.Gateway/Dockerfile
+ ports:
+ - "8000:8080"
+ environment:
+ - ConnectionStrings__Etcd=http://etcd:2379
+ - Etcd__Insecure=true
+ - Service__Name=DysonNetwork.Gateway
+ - Service__Url=http://gateway:8080
+ depends_on:
+ etcd:
+ condition: service_healthy
+
+ drive:
+ build:
+ context: .
+ dockerfile: DysonNetwork.Drive/Dockerfile
ports:
- "8001:8080"
+ environment:
+ - ConnectionStrings__Etcd=http://etcd:2379
+ - Etcd__Insecure=true
+ - Service__Name=DysonNetwork.Drive
+ - Service__Url=http://drive:8080
+ depends_on:
+ etcd:
+ condition: service_healthy
+
+ pass:
+ build:
+ context: .
+ dockerfile: DysonNetwork.Pass/Dockerfile
+ ports:
+ - "8002:8080"
+ environment:
+ - ConnectionStrings__Etcd=http://etcd:2379
+ - Etcd__Insecure=true
+ - Service__Name=DysonNetwork.Pass
+ - Service__Url=http://pass:8080
+ depends_on:
+ etcd:
+ condition: service_healthy
+
+ pusher:
+ build:
+ context: .
+ dockerfile: DysonNetwork.Pusher/Dockerfile
+ ports:
+ - "8003:8080"
+ environment:
+ - ConnectionStrings__Etcd=http://etcd:2379
+ - Etcd__Insecure=true
+ - Service__Name=DysonNetwork.Pusher
+ - Service__Url=http://pusher:8080
+ depends_on:
+ etcd:
+ condition: service_healthy
+
+ sphere:
+ build:
+ context: .
+ dockerfile: DysonNetwork.Sphere/Dockerfile
+ ports:
+ - "8004:8080"
+ environment:
+ - ConnectionStrings__Etcd=http://etcd:2379
+ - Etcd__Insecure=true
+ - Service__Name=DysonNetwork.Sphere
+ - Service__Url=http://sphere:8080
volumes:
- "./keys:/app/keys"
+ depends_on:
+ etcd:
+ condition: service_healthy
\ No newline at end of file