From e66abe2e0cc2002ca2c4577ee94cbff1c1c057be Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 13 Jul 2025 01:55:35 +0800 Subject: [PATCH] :recycle: Mix things up --- .gitignore | 1 + DysonNetwork.Pass/AppDatabase.cs | 4 + .../Services/OidcProviderService.cs | 1 + DysonNetwork.Pass/Auth/Session.cs | 3 +- DysonNetwork.Pass/Developer/CustomApp.cs | 68 +++++++++++ DysonNetwork.Pass/Email/EmailService.cs | 113 ++++++------------ DysonNetwork.Pass/Program.cs | 4 + .../ServiceRegistrationHostedService.cs | 56 +++++++++ DysonNetwork.Pusher/Program.cs | 4 + .../Startup/ServiceCollectionExtensions.cs | 14 +++ .../ServiceRegistrationHostedService.cs | 56 +++++++++ DysonNetwork.Shared/Auth/AuthConstants.cs | 22 ++++ .../DysonNetwork.Shared.csproj | 4 + .../Middleware/AuthMiddleware.cs | 107 +++++++++++++++++ DysonNetwork.Shared/Proto/GrpcClientHelper.cs | 104 ++++++++++++++++ .../Registry/ServiceRegistry.cs | 28 +++++ DysonNetwork.Shared/Registry/Startup.cs | 23 ++++ DysonNetwork.Sphere/appsettings.json | 8 +- DysonNetwork.sln.DotSettings.user | 4 + 19 files changed, 546 insertions(+), 78 deletions(-) create mode 100644 DysonNetwork.Pass/Developer/CustomApp.cs create mode 100644 DysonNetwork.Pass/Startup/ServiceRegistrationHostedService.cs create mode 100644 DysonNetwork.Pusher/Startup/ServiceRegistrationHostedService.cs create mode 100644 DysonNetwork.Shared/Auth/AuthConstants.cs create mode 100644 DysonNetwork.Shared/Middleware/AuthMiddleware.cs create mode 100644 DysonNetwork.Shared/Proto/GrpcClientHelper.cs create mode 100644 DysonNetwork.Shared/Registry/ServiceRegistry.cs create mode 100644 DysonNetwork.Shared/Registry/Startup.cs diff --git a/.gitignore b/.gitignore index 30f23d9..c7242c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ bin/ obj/ /packages/ +/Certificates/ riderModule.iml /_ReSharper.Caches/ .idea diff --git a/DysonNetwork.Pass/AppDatabase.cs b/DysonNetwork.Pass/AppDatabase.cs index f666e8f..695c3b2 100644 --- a/DysonNetwork.Pass/AppDatabase.cs +++ b/DysonNetwork.Pass/AppDatabase.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using System.Reflection; using DysonNetwork.Pass.Account; using DysonNetwork.Pass.Auth; +using DysonNetwork.Pass.Developer; using DysonNetwork.Pass.Permission; using DysonNetwork.Pass.Wallet; using DysonNetwork.Shared.Data; @@ -46,6 +47,9 @@ public class AppDatabase( public DbSet PaymentTransactions { get; set; } public DbSet WalletSubscriptions { get; set; } public DbSet WalletCoupons { get; set; } + + public DbSet CustomApps { get; set; } + public DbSet CustomAppSecrets { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/DysonNetwork.Pass/Auth/OidcProvider/Services/OidcProviderService.cs b/DysonNetwork.Pass/Auth/OidcProvider/Services/OidcProviderService.cs index 0c1d86e..2ddb88d 100644 --- a/DysonNetwork.Pass/Auth/OidcProvider/Services/OidcProviderService.cs +++ b/DysonNetwork.Pass/Auth/OidcProvider/Services/OidcProviderService.cs @@ -5,6 +5,7 @@ using System.Text; using DysonNetwork.Pass.Auth.OidcProvider.Models; using DysonNetwork.Pass.Auth.OidcProvider.Options; using DysonNetwork.Pass.Auth.OidcProvider.Responses; +using DysonNetwork.Pass.Developer; using DysonNetwork.Shared.Cache; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; diff --git a/DysonNetwork.Pass/Auth/Session.cs b/DysonNetwork.Pass/Auth/Session.cs index d5f935d..149fb65 100644 --- a/DysonNetwork.Pass/Auth/Session.cs +++ b/DysonNetwork.Pass/Auth/Session.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; using DysonNetwork.Pass; +using DysonNetwork.Pass.Developer; using DysonNetwork.Shared.Data; using NodaTime; using NodaTime.Serialization.Protobuf; @@ -21,7 +22,7 @@ public class AuthSession : ModelBase public Guid ChallengeId { get; set; } public AuthChallenge Challenge { get; set; } = null!; public Guid? AppId { get; set; } - // public CustomApp? App { get; set; } + public CustomApp? App { get; set; } public Shared.Proto.AuthSession ToProtoValue() => new() { diff --git a/DysonNetwork.Pass/Developer/CustomApp.cs b/DysonNetwork.Pass/Developer/CustomApp.cs new file mode 100644 index 0000000..731d8cc --- /dev/null +++ b/DysonNetwork.Pass/Developer/CustomApp.cs @@ -0,0 +1,68 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using DysonNetwork.Pass.Account; +using DysonNetwork.Shared.Data; +using NodaTime; + +namespace DysonNetwork.Pass.Developer; + +public enum CustomAppStatus +{ + Developing, + Staging, + Production, + Suspended +} + +public class CustomApp : ModelBase, IIdentifiedResource +{ + public Guid Id { get; set; } = Guid.NewGuid(); + [MaxLength(1024)] public string Slug { get; set; } = null!; + [MaxLength(1024)] public string Name { get; set; } = null!; + [MaxLength(4096)] public string? Description { get; set; } + public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing; + + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; } + [Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; } + + [Column(TypeName = "jsonb")] public VerificationMark? Verification { get; set; } + [Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; } + [Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; } + + [JsonIgnore] public ICollection Secrets { get; set; } = new List(); + + // TODO: Publisher + + [NotMapped] public string ResourceIdentifier => "custom-app/" + Id; +} + +public class CustomAppLinks +{ + [MaxLength(8192)] public string? HomePage { get; set; } + [MaxLength(8192)] public string? PrivacyPolicy { get; set; } + [MaxLength(8192)] public string? TermsOfService { get; set; } +} + +public class CustomAppOauthConfig +{ + [MaxLength(1024)] public string? ClientUri { get; set; } + [MaxLength(4096)] public string[] RedirectUris { get; set; } = []; + [MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; } + [MaxLength(256)] public string[]? AllowedScopes { get; set; } = ["openid", "profile", "email"]; + [MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"]; + public bool RequirePkce { get; set; } = true; + public bool AllowOfflineAccess { get; set; } = false; +} + +public class CustomAppSecret : ModelBase +{ + public Guid Id { get; set; } = Guid.NewGuid(); + [MaxLength(1024)] public string Secret { get; set; } = null!; + [MaxLength(4096)] public string? Description { get; set; } = null!; + public Instant? ExpiredAt { get; set; } + public bool IsOidc { get; set; } = false; // Indicates if this secret is for OIDC/OAuth + + public Guid AppId { get; set; } + public CustomApp App { get; set; } = null!; +} diff --git a/DysonNetwork.Pass/Email/EmailService.cs b/DysonNetwork.Pass/Email/EmailService.cs index 2051cf4..b8bb428 100644 --- a/DysonNetwork.Pass/Email/EmailService.cs +++ b/DysonNetwork.Pass/Email/EmailService.cs @@ -1,92 +1,56 @@ -using MailKit.Net.Smtp; +using dotnet_etcd; +using DysonNetwork.Shared.Proto; using Microsoft.AspNetCore.Components; -using MimeKit; namespace DysonNetwork.Pass.Email; -public class EmailServiceConfiguration -{ - public string Server { get; set; } = null!; - public int Port { get; set; } - public bool UseSsl { get; set; } - public string Username { get; set; } = null!; - public string Password { get; set; } = null!; - public string FromAddress { get; set; } = null!; - public string FromName { get; set; } = null!; - public string SubjectPrefix { get; set; } = null!; -} - public class EmailService { - private readonly EmailServiceConfiguration _configuration; + private readonly PusherService.PusherServiceClient _client; private readonly RazorViewRenderer _viewRenderer; private readonly ILogger _logger; - public EmailService(IConfiguration configuration, RazorViewRenderer viewRenderer, ILogger logger) + public EmailService( + EtcdClient etcd, + RazorViewRenderer viewRenderer, + IConfiguration configuration, + ILogger logger, + PusherService.PusherServiceClient client + ) { - var cfg = configuration.GetSection("Email").Get(); - _configuration = cfg ?? throw new ArgumentException("Email service was not configured."); + _client = GrpcClientHelper.CreatePusherServiceClient( + etcd, + configuration["Service:CertPath"]!, + configuration["Service:KeyPath"]! + ).GetAwaiter().GetResult(); _viewRenderer = viewRenderer; _logger = logger; + _client = client; } - - public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody) + + public async Task SendEmailAsync( + string? recipientName, + string recipientEmail, + string subject, + string htmlBody + ) { - await SendEmailAsync(recipientName, recipientEmail, subject, textBody, null); + subject = $"[Solarpass] {subject}"; + + await _client.SendEmailAsync( + new SendEmailRequest() + { + Email = new EmailMessage() + { + ToName = recipientName, + ToAddress = recipientEmail, + Subject = subject, + Body = htmlBody + } + } + ); } - public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody, - string? htmlBody) - { - subject = $"[{_configuration.SubjectPrefix}] {subject}"; - - var emailMessage = new MimeMessage(); - emailMessage.From.Add(new MailboxAddress(_configuration.FromName, _configuration.FromAddress)); - emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail)); - emailMessage.Subject = subject; - - var bodyBuilder = new BodyBuilder - { - TextBody = textBody - }; - - if (!string.IsNullOrEmpty(htmlBody)) - bodyBuilder.HtmlBody = htmlBody; - - emailMessage.Body = bodyBuilder.ToMessageBody(); - - using var client = new SmtpClient(); - await client.ConnectAsync(_configuration.Server, _configuration.Port, _configuration.UseSsl); - await client.AuthenticateAsync(_configuration.Username, _configuration.Password); - await client.SendAsync(emailMessage); - await client.DisconnectAsync(true); - } - - private static string _ConvertHtmlToPlainText(string html) - { - // Remove style tags and their contents - html = System.Text.RegularExpressions.Regex.Replace(html, "]*>.*?", "", - System.Text.RegularExpressions.RegexOptions.Singleline); - - // Replace header tags with text + newlines - html = System.Text.RegularExpressions.Regex.Replace(html, "]*>(.*?)", "$1\n\n", - System.Text.RegularExpressions.RegexOptions.IgnoreCase); - - // Replace line breaks - html = html.Replace("
", "\n").Replace("
", "\n").Replace("
", "\n"); - - // Remove all remaining HTML tags - html = System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", ""); - - // Decode HTML entities - html = System.Net.WebUtility.HtmlDecode(html); - - // Remove excess whitespace - html = System.Text.RegularExpressions.Regex.Replace(html, @"\s+", " ").Trim(); - - return html; - } - public async Task SendTemplatedEmailAsync(string? recipientName, string recipientEmail, string subject, TModel model) where TComponent : IComponent @@ -94,8 +58,7 @@ public class EmailService try { var htmlBody = await _viewRenderer.RenderComponentToStringAsync(model); - var fallbackTextBody = _ConvertHtmlToPlainText(htmlBody); - await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody); + await SendEmailAsync(recipientName, recipientEmail, subject, htmlBody); } catch (Exception err) { diff --git a/DysonNetwork.Pass/Program.cs b/DysonNetwork.Pass/Program.cs index e52a3c3..c6b9257 100644 --- a/DysonNetwork.Pass/Program.cs +++ b/DysonNetwork.Pass/Program.cs @@ -1,6 +1,7 @@ using DysonNetwork.Pass; using DysonNetwork.Pass.Account; using DysonNetwork.Pass.Startup; +using DysonNetwork.Shared.Registry; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -12,6 +13,7 @@ builder.ConfigureAppKestrel(); builder.Services.AddAppMetrics(); // Add application services +builder.Services.AddEtcdService(builder.Configuration); builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppRateLimiting(); builder.Services.AddAppAuthentication(); @@ -26,6 +28,8 @@ builder.Services.AddAppBusinessServices(builder.Configuration); // Add scheduled jobs builder.Services.AddAppScheduledJobs(); +builder.Services.AddHostedService(); + var app = builder.Build(); // Run database migrations diff --git a/DysonNetwork.Pass/Startup/ServiceRegistrationHostedService.cs b/DysonNetwork.Pass/Startup/ServiceRegistrationHostedService.cs new file mode 100644 index 0000000..7abe7fc --- /dev/null +++ b/DysonNetwork.Pass/Startup/ServiceRegistrationHostedService.cs @@ -0,0 +1,56 @@ +using DysonNetwork.Shared.Registry; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; + +namespace DysonNetwork.Pass.Startup; + +public class ServiceRegistrationHostedService : IHostedService +{ + private readonly ServiceRegistry _serviceRegistry; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public ServiceRegistrationHostedService( + ServiceRegistry serviceRegistry, + IConfiguration configuration, + ILogger logger) + { + _serviceRegistry = serviceRegistry; + _configuration = configuration; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var serviceName = "DysonNetwork.Pass"; // Preset service name + var serviceUrl = _configuration["Service:Url"]; + + if (string.IsNullOrEmpty(serviceName) || string.IsNullOrEmpty(serviceUrl)) + { + _logger.LogWarning("Service name or URL not configured. Skipping Etcd registration."); + return; + } + + _logger.LogInformation("Registering service {ServiceName} at {ServiceUrl} with Etcd.", serviceName, serviceUrl); + try + { + await _serviceRegistry.RegisterService(serviceName, serviceUrl); + _logger.LogInformation("Service {ServiceName} registered successfully.", serviceName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to register service {ServiceName} with Etcd.", serviceName); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + // The lease will expire automatically if the service stops. + // For explicit unregistration, you would implement it here. + _logger.LogInformation("Service registration hosted service is stopping."); + return Task.CompletedTask; + } +} diff --git a/DysonNetwork.Pusher/Program.cs b/DysonNetwork.Pusher/Program.cs index 744c79d..6f8fc57 100644 --- a/DysonNetwork.Pusher/Program.cs +++ b/DysonNetwork.Pusher/Program.cs @@ -23,6 +23,8 @@ builder.Services.AddAppBusinessServices(); // Add scheduled jobs builder.Services.AddAppScheduledJobs(); +builder.Services.AddHostedService(); + var app = builder.Build(); // Run database migrations @@ -35,6 +37,8 @@ using (var scope = app.Services.CreateScope()) // Configure application middleware pipeline app.ConfigureAppMiddleware(builder.Configuration); +app.UseMiddleware(); + // Configure gRPC app.ConfigureGrpcServices(); diff --git a/DysonNetwork.Pusher/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Pusher/Startup/ServiceCollectionExtensions.cs index 033a98b..bbcfe8e 100644 --- a/DysonNetwork.Pusher/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Pusher/Startup/ServiceCollectionExtensions.cs @@ -1,9 +1,11 @@ using System.Text.Json; using System.Threading.RateLimiting; +using dotnet_etcd.interfaces; using DysonNetwork.Pusher.Email; using DysonNetwork.Pusher.Notification; using DysonNetwork.Pusher.Services; using DysonNetwork.Shared.Cache; +using DysonNetwork.Shared.Proto; using Microsoft.AspNetCore.RateLimiting; using Microsoft.OpenApi.Models; using NodaTime; @@ -42,6 +44,18 @@ public static class ServiceCollectionExtensions // Register gRPC services services.AddScoped(); + // Register AuthService.AuthServiceClient for AuthMiddleware + services.AddSingleton(sp => + { + var etcdClient = sp.GetRequiredService(); + var configuration = sp.GetRequiredService(); + var clientCertPath = configuration["ClientCert:Path"]; + var clientKeyPath = configuration["ClientKey:Path"]; + var clientCertPassword = configuration["ClientCert:Password"]; + + return GrpcClientHelper.CreateAuthServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword); + }); + // Register OIDC services services.AddControllers().AddJsonOptions(options => { diff --git a/DysonNetwork.Pusher/Startup/ServiceRegistrationHostedService.cs b/DysonNetwork.Pusher/Startup/ServiceRegistrationHostedService.cs new file mode 100644 index 0000000..c68d3fb --- /dev/null +++ b/DysonNetwork.Pusher/Startup/ServiceRegistrationHostedService.cs @@ -0,0 +1,56 @@ +using DysonNetwork.Shared.Registry; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Threading; +using System.Threading.Tasks; + +namespace DysonNetwork.Pusher.Startup; + +public class ServiceRegistrationHostedService : IHostedService +{ + private readonly ServiceRegistry _serviceRegistry; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public ServiceRegistrationHostedService( + ServiceRegistry serviceRegistry, + IConfiguration configuration, + ILogger logger) + { + _serviceRegistry = serviceRegistry; + _configuration = configuration; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var serviceName = "DysonNetwork.Pusher"; // Preset service name + var serviceUrl = _configuration["Service:Url"]; + + if (string.IsNullOrEmpty(serviceUrl)) + { + _logger.LogWarning("Service URL not configured. Skipping Etcd registration."); + return; + } + + _logger.LogInformation("Registering service {ServiceName} at {ServiceUrl} with Etcd.", serviceName, serviceUrl); + try + { + await _serviceRegistry.RegisterService(serviceName, serviceUrl); + _logger.LogInformation("Service {ServiceName} registered successfully.", serviceName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to register service {ServiceName} with Etcd.", serviceName); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + // The lease will expire automatically if the service stops. + // For explicit unregistration, you would implement it here. + _logger.LogInformation("Service registration hosted service is stopping."); + return Task.CompletedTask; + } +} diff --git a/DysonNetwork.Shared/Auth/AuthConstants.cs b/DysonNetwork.Shared/Auth/AuthConstants.cs new file mode 100644 index 0000000..8b84521 --- /dev/null +++ b/DysonNetwork.Shared/Auth/AuthConstants.cs @@ -0,0 +1,22 @@ +namespace DysonNetwork.Shared.Auth; + +public static class AuthConstants +{ + public const string SchemeName = "DysonToken"; + public const string TokenQueryParamName = "tk"; + public const string CookieTokenName = "AuthToken"; +} + +public enum TokenType +{ + AuthKey, + ApiKey, + OidcKey, + Unknown +} + +public class TokenInfo +{ + public string Token { get; set; } = string.Empty; + public TokenType Type { get; set; } = TokenType.Unknown; +} diff --git a/DysonNetwork.Shared/DysonNetwork.Shared.csproj b/DysonNetwork.Shared/DysonNetwork.Shared.csproj index 5d993e2..0c82947 100644 --- a/DysonNetwork.Shared/DysonNetwork.Shared.csproj +++ b/DysonNetwork.Shared/DysonNetwork.Shared.csproj @@ -7,6 +7,7 @@ + @@ -17,6 +18,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -24,6 +27,7 @@ + diff --git a/DysonNetwork.Shared/Middleware/AuthMiddleware.cs b/DysonNetwork.Shared/Middleware/AuthMiddleware.cs new file mode 100644 index 0000000..6356427 --- /dev/null +++ b/DysonNetwork.Shared/Middleware/AuthMiddleware.cs @@ -0,0 +1,107 @@ +using Grpc.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using DysonNetwork.Shared.Proto; +using System.Threading.Tasks; +using DysonNetwork.Shared.Auth; + +namespace DysonNetwork.Shared.Middleware; + +public class AuthMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public AuthMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context, AuthService.AuthServiceClient authServiceClient) + { + var tokenInfo = _ExtractToken(context.Request); + + if (tokenInfo == null || string.IsNullOrEmpty(tokenInfo.Token)) + { + await _next(context); + return; + } + + try + { + var authSession = await authServiceClient.AuthenticateAsync(new AuthenticateRequest { Token = tokenInfo.Token }); + context.Items["AuthSession"] = authSession; + context.Items["CurrentTokenType"] = tokenInfo.Type.ToString(); + // Assuming AuthSession contains Account information or can be retrieved + // context.Items["CurrentUser"] = authSession.Account; // You might need to fetch Account separately if not embedded + } + catch (RpcException ex) + { + _logger.LogWarning(ex, "Authentication failed for token: {Token}", tokenInfo.Token); + // Optionally, you can return an unauthorized response here + // context.Response.StatusCode = StatusCodes.Status401Unauthorized; + // return; + } + + await _next(context); + } + + private TokenInfo? _ExtractToken(HttpRequest request) + { + // Check for token in query parameters + if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken)) + { + return new TokenInfo + { + Token = queryToken.ToString(), + Type = TokenType.AuthKey + }; + } + + // Check for token in Authorization header + var authHeader = request.Headers["Authorization"].ToString(); + if (!string.IsNullOrEmpty(authHeader)) + { + if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + var token = authHeader["Bearer ".Length..].Trim(); + var parts = token.Split('.'); + + return new TokenInfo + { + Token = token, + Type = parts.Length == 3 ? TokenType.OidcKey : TokenType.AuthKey + }; + } + else if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase)) + { + return new TokenInfo + { + Token = authHeader["AtField ".Length..].Trim(), + Type = TokenType.AuthKey + }; + } + else if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase)) + { + return new TokenInfo + { + Token = authHeader["AkField ".Length..].Trim(), + Type = TokenType.ApiKey + }; + } + } + + // Check for token in cookies + if (request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out var cookieToken)) + { + return new TokenInfo + { + Token = cookieToken, + Type = cookieToken.Count(c => c == '.') == 2 ? TokenType.OidcKey : TokenType.AuthKey + }; + } + + return null; + } +} diff --git a/DysonNetwork.Shared/Proto/GrpcClientHelper.cs b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs new file mode 100644 index 0000000..f891317 --- /dev/null +++ b/DysonNetwork.Shared/Proto/GrpcClientHelper.cs @@ -0,0 +1,104 @@ +using Grpc.Net.Client; +using System.Security.Cryptography.X509Certificates; +using Grpc.Core; +using dotnet_etcd.interfaces; + +namespace DysonNetwork.Shared.Proto; + +public static class GrpcClientHelper +{ + private static CallInvoker CreateCallInvoker( + string url, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + var handler = new HttpClientHandler(); + handler.ClientCertificates.Add( + clientCertPassword is null ? + X509Certificate2.CreateFromPemFile(clientCertPath, clientKeyPath) : + X509Certificate2.CreateFromEncryptedPemFile(clientCertPath, clientCertPassword, clientKeyPath) + ); + return GrpcChannel.ForAddress(url, new GrpcChannelOptions { HttpHandler = handler }).CreateCallInvoker(); + } + + private static async Task GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName) + { + var response = await etcdClient.GetAsync($"/services/{serviceName}"); + if (response.Kvs.Count == 0) + { + throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd."); + } + return response.Kvs[0].Value.ToStringUtf8(); + } + + public static AccountService.AccountServiceClient CreateAccountServiceClient( + string url, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, + clientCertPassword)); + } + + public static async Task CreateAccountServiceClient( + IEtcdClient etcdClient, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + var url = await GetServiceUrlFromEtcd(etcdClient, "AccountService"); + return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, + clientCertPassword)); + } + + public static AuthService.AuthServiceClient CreateAuthServiceClient( + string url, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, + clientCertPassword)); + } + + public static async Task CreateAuthServiceClient( + IEtcdClient etcdClient, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + var url = await GetServiceUrlFromEtcd(etcdClient, "AuthService"); + return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, + clientCertPassword)); + } + + public static PusherService.PusherServiceClient CreatePusherServiceClient( + string url, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, + clientCertPassword)); + } + + public static async Task CreatePusherServiceClient( + IEtcdClient etcdClient, + string clientCertPath, + string clientKeyPath, + string? clientCertPassword = null + ) + { + var url = await GetServiceUrlFromEtcd(etcdClient, "PusherService"); + return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath, + clientCertPassword)); + } +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Registry/ServiceRegistry.cs b/DysonNetwork.Shared/Registry/ServiceRegistry.cs new file mode 100644 index 0000000..8d3e7f4 --- /dev/null +++ b/DysonNetwork.Shared/Registry/ServiceRegistry.cs @@ -0,0 +1,28 @@ +using System.Text; +using dotnet_etcd.interfaces; +using Etcdserverpb; +using Google.Protobuf; + +namespace DysonNetwork.Shared.Registry; + +public class ServiceRegistry(IEtcdClient etcd) +{ + public async Task RegisterService(string serviceName, string serviceUrl, long leaseTtlSeconds = 60) + { + var key = $"/services/{serviceName}"; + var leaseResponse = await etcd.LeaseGrantAsync(new LeaseGrantRequest { TTL = leaseTtlSeconds }); + await etcd.PutAsync(new PutRequest + { + Key = ByteString.CopyFrom(key, Encoding.UTF8), + Value = ByteString.CopyFrom(serviceUrl, Encoding.UTF8), + Lease = leaseResponse.ID + }); + await etcd.LeaseKeepAlive(leaseResponse.ID, CancellationToken.None); + } + + public async Task UnregisterService(string serviceName) + { + var key = $"/services/{serviceName}"; + await etcd.DeleteAsync(key); + } +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Registry/Startup.cs b/DysonNetwork.Shared/Registry/Startup.cs new file mode 100644 index 0000000..cceef2d --- /dev/null +++ b/DysonNetwork.Shared/Registry/Startup.cs @@ -0,0 +1,23 @@ +using dotnet_etcd.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace DysonNetwork.Shared.Registry; + +public static class EtcdStartup +{ + public static IServiceCollection AddEtcdService( + this IServiceCollection services, + IConfiguration configuration + ) + { + services.AddEtcdClient(options => + { + options.ConnectionString = configuration.GetConnectionString("Etcd"); + options.UseInsecureChannel = configuration.GetValue("Etcd:Insecure"); + }); + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/appsettings.json b/DysonNetwork.Sphere/appsettings.json index 82c1088..5cb458b 100644 --- a/DysonNetwork.Sphere/appsettings.json +++ b/DysonNetwork.Sphere/appsettings.json @@ -10,7 +10,8 @@ "AllowedHosts": "*", "ConnectionStrings": { "App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60", - "FastRetrieve": "localhost:6379" + "FastRetrieve": "localhost:6379", + "Etcd": "localhost:2379" }, "Authentication": { "Schemes": { @@ -125,5 +126,8 @@ "KnownProxies": [ "127.0.0.1", "::1" - ] + ], + "Etcd": { + "Insecure": true + } } diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 41ec08d..95dd6c2 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -10,6 +10,8 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -39,8 +41,10 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded