using EmailMicroservice.Models; using EmailMicroservice.Services; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https; using System.Net.Mime; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System; using System.IO; var builder = WebApplication.CreateBuilder(args); string currentDirectory = Directory.GetCurrentDirectory(); //-------------------------------------------LOGS BORRAR -------------------------------------------------------- //builder.Logging.AddConsole(); //builder.Logging.SetMinimumLevel(LogLevel.Debug); // Verás todo lo que pasa //// Específicamente para Kestrel y TLS //builder.Services.AddLogging(logging => //{ // logging.AddFilter("Microsoft.AspNetCore.Server.Kestrel", LogLevel.Trace); // logging.AddFilter("Microsoft.AspNetCore.Server.Kestrel.Https", LogLevel.Trace); //}); //-------------------------------------------LOGS BORRAR -------------------------------------------------------- static X509Certificate2 LoadCertificateWithKey(string certPath, string keyPath) { try { // 1. Verificación de existencia (Crucial en K8s por montado de volúmenes) if (!File.Exists(certPath)) throw new FileNotFoundException("No se encontró el CRT", certPath); if (!File.Exists(keyPath)) throw new FileNotFoundException("No se encontró la KEY", keyPath); // 2. Carga inicial desde PEM (Llave efímera nativa de .NET 10) using var tempCert = X509Certificate2.CreateFromPemFile(certPath, keyPath); // 3. Persistencia de Llave Privada (El "Truco" de Compatibilidad) // Exportamos a PFX y re-importamos para que el motor de seguridad (Schannel o OpenSSL) // vincule correctamente la llave privada al objeto en memoria. X509Certificate2 finalCert; if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) { // Windows requiere flags específicos para no perder el acceso a la llave privada finalCert = new X509Certificate2(tempCert.Export(X509ContentType.Pfx), (string?)null, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet); } else { // Linux maneja la exportación de forma más directa finalCert = new X509Certificate2(tempCert.Export(X509ContentType.Pfx)); } // 4. Logs de inspección (Se ejecutan ANTES del return final) //Console.WriteLine("-------------------------------------------"); //Console.WriteLine("--- INSPECCIÓN DE CERTIFICADO ---"); //Console.WriteLine($"Subject: {finalCert.Subject}"); //Console.WriteLine($"Thumbprint: {finalCert.Thumbprint}"); //Console.WriteLine($"Issuer: {finalCert.Issuer}"); //Console.WriteLine($"Válido hasta: {finalCert.NotAfter}"); //Console.WriteLine($"Tiene Llave Privada: {finalCert.HasPrivateKey}"); //Console.WriteLine("-------------------------------------------"); return finalCert; } catch (Exception ex) { // Error común en Linux/K8s: permisos de lectura (600 vs 644) Console.WriteLine($"[FATAL] Error crítico cargando certificados: {ex.Message}"); throw; } } // ----------------------------------------------------------------------------- // 1. CONFIGURACIÓN DE SERVIDOR KESTREL (PUERTOS Y CERTIFICADOS) // ----------------------------------------------------------------------------- builder.WebHost.ConfigureKestrel(options => { // Puerto 8080: HTTP/2 (Texto plano - Comunicación interna rápida) options.ListenAnyIP(8080, listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; }); // Puerto 8081: HTTPS (mTLS - Seguridad y Certificados) options.ListenAnyIP(8081, listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; listenOptions.UseHttps(httpsOptions => { httpsOptions.ServerCertificate = LoadCertificateWithKey(currentDirectory +"/certs/server.pem", currentDirectory +"/certs/server.key"); httpsOptions.ClientCertificateMode = ClientCertificateMode.NoCertificate; var caCert = X509CertificateLoader.LoadCertificate(File.ReadAllBytes(currentDirectory +"/certs/ca.crt")); httpsOptions.ClientCertificateValidation = (certificate, chain, errors) => { //-------------------------------------------LOGS BORRAR -------------------------------------------------------- // Logs de identidad detallados //Console.WriteLine("================================================="); //Console.WriteLine("[mTLS] NUEVA CONEXIÓN RECIBIDA"); //Console.WriteLine($"Cliente: {certificate.Subject}"); //Console.WriteLine($"Emisor: {certificate.Issuer}"); //Console.WriteLine($"Serial: {certificate.SerialNumber}"); //Console.WriteLine($"Válido: Desde {certificate.NotBefore} hasta {certificate.NotAfter}"); //Console.WriteLine($"Errores de Validación: {errors}"); //Console.WriteLine("================================================="); //if (errors != System.Net.Security.SslPolicyErrors.None && // errors != System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) //{ // Console.WriteLine($"[CRÍTICO] Fallo de SSL real: {errors}"); //} //else if (errors == System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) //{ // Console.WriteLine("[INFO] Error de cadena detectado (Esperado por usar CA propia)."); //} //-------------------------------------------LOGS BORRAR -------------------------------------------------------- using var chainToValidate = new X509Chain(); // Configuramos la política de validación chainToValidate.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; chainToValidate.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; // Agregamos nuestra CA al almacén de confianza de esta petición chainToValidate.ChainPolicy.CustomTrustStore.Add(caCert); // .NET 10 maneja mejor la validación de SAN y fechas internamente aquí bool isValid = chainToValidate.Build((X509Certificate2)certificate); if (!isValid) { // Log de por qué falló var errors2 = string.Join(", ", chainToValidate.ChainStatus.Select(s => s.StatusInformation)); //Console.WriteLine($"[mTLS] Fallo de validación: {errors2}"); return false; } // En lugar de comparar RawData, verificamos que el certificado raíz sea nuestra CA // usando el Thumbprint, que es más rápido y seguro. var chainRoot = chainToValidate.ChainElements[^1].Certificate; // Usamos índice ^1 (moderno) return chainRoot.Thumbprint == caCert.Thumbprint; }; }); }); }); // Configuración global para validar certificados salientes (Exchange) //ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; // ----------------------------------------------------------------------------- // 2. REGISTRO DE SERVICIOS (DEPENDENCY INJECTION) // ----------------------------------------------------------------------------- // Configuración y Cliente HTTP builder.Services.Configure(builder.Configuration.GetSection("MailSettings")); builder.Services.AddHttpClient("ExchangeClient") // Puedes ponerle un nombre o dejarlo vacío .ConfigurePrimaryHttpMessageHandler(() => { var handler = new HttpClientHandler(); // Esta línea permite certificados inválidos (como los self-signed de Exchange) handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; return handler; }); // A. Registrar la lógica de Salud (Tu clase ExchangeHealthCheck) builder.Services.AddHealthChecks() // 1.Check General(Liveness)->Tag: "liveness" .AddCheck("General_Check", tags: new[] { "liveness" }) // 2. Check Exchange (Readiness) -> Tag: "readiness" .AddCheck("Exchange_EWS", tags: new[] { "readiness" }); // B. Servicios gRPC builder.Services.AddGrpc(); builder.Services.AddGrpcReflection(); // C. Habilitar soporte nativo de Salud para gRPC builder.Services.AddGrpcHealthChecks(); var app = builder.Build(); //-------------------------------------------LOGS BORRAR -------------------------------------------------------- // ----------------------------------------------------------------------------- // 3. PIPELINE (MAPEO DE ENDPOINTS) // ----------------------------------------------------------------------------- var jsonWriterOptions = new HealthCheckOptions { Predicate = _ => true, ResponseWriter = async (context, report) => { var result = JsonSerializer.Serialize( new { status = report.Status.ToString(), totalDuration = report.TotalDuration.TotalMilliseconds + " ms", checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description ?? "OK", error = e.Value.Exception?.Message ?? "none" }) }); context.Response.ContentType = MediaTypeNames.Application.Json; await context.Response.WriteAsync(result); } }; // RUTA 1: ¿Estoy vivo? (Solo ejecuta GeneralHealthCheck) app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = (check) => check.Tags.Contains("liveness"), ResponseWriter = jsonWriterOptions.ResponseWriter }); // RUTA 2: ¿Estoy listo? (Ejecuta ExchangeHealthCheck) app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = (check) => check.Tags.Contains("readiness"), ResponseWriter = jsonWriterOptions.ResponseWriter }); // Servicio principal de correo app.MapGrpcService(); // Servicio de Salud (grpc.health.v1.Health) app.MapGrpcHealthChecksService(); // Servicio de Reflection (Para Postman/grpcurl) app.MapGrpcReflectionService(); // Endpoint informativo raíz app.MapGet("/", () => $"Microservicio corriendo en: {builder.Environment.EnvironmentName}"); app.Run();