From 8a7ae4ad6fca1d74d052625bcbb0d1c0d6709c12 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Fri, 31 Oct 2025 19:32:13 +0200 Subject: [PATCH] feat: add HTTPS configuration options and server setup - Introduced new configuration fields for enabling HTTPS, specifying a domain name, and setting a TLS cache directory in the gateway configuration. - Enhanced the main server logic to support HTTPS with ACME integration, including automatic HTTP to HTTPS redirection and error handling for server startup. - Added validation for HTTPS settings to ensure proper domain and cache directory configuration. - Implemented interactive prompts in the CLI for domain and HTTPS setup, including DNS verification and port availability checks. --- CHANGELOG.md | 26 ++++ Makefile | 2 +- cmd/gateway/config.go | 22 +++ cmd/gateway/main.go | 118 ++++++++++++++++ pkg/cli/setup.go | 241 ++++++++++++++++++++++++++++++++- pkg/gateway/config_validate.go | 50 +++++++ pkg/gateway/gateway.go | 5 + 7 files changed, 458 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc44467..7a47abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,32 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant ### Fixed +## [0.53.8] - 2025-10-31 + +### Added + +- **HTTPS/ACME Support**: Gateway now supports automatic HTTPS with Let's Encrypt certificates via ACME + - Interactive domain configuration during `network-cli setup` command + - Automatic port availability checking for ports 80 and 443 before enabling HTTPS + - DNS resolution verification to ensure domain points to the server IP + - TLS certificate cache directory management (`~/.debros/tls-cache`) + - Gateway automatically serves HTTP (port 80) for ACME challenges and HTTPS (port 443) for traffic + - New gateway config fields: `enable_https`, `domain_name`, `tls_cache_dir` +- **Domain Validation**: Added domain name validation and DNS verification helpers in setup CLI +- **Port Checking**: Added port availability checking utilities to detect conflicts before HTTPS setup + +### Changed + +- Updated `generateGatewayConfigDirect` to include HTTPS configuration fields +- Enhanced gateway config parsing to support HTTPS settings with validation +- Modified gateway startup to handle both HTTP-only and HTTPS+ACME modes +- Gateway now automatically manages ACME certificate acquisition and renewal + +### Fixed + +- Improved error handling during HTTPS setup with clear messaging when ports are unavailable +- Enhanced DNS verification flow with better user feedback during setup + ## [0.53.0] - 2025-10-31 ### Added diff --git a/Makefile b/Makefile index f2c8b36..2adc09c 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ test-e2e: .PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports -VERSION := 0.53.6 +VERSION := 0.53.8 COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)' diff --git a/cmd/gateway/config.go b/cmd/gateway/config.go index 76548da..e10763c 100644 --- a/cmd/gateway/config.go +++ b/cmd/gateway/config.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "path/filepath" "strings" "github.com/DeBrosOfficial/network/pkg/config" @@ -53,6 +54,9 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config { ClientNamespace string `yaml:"client_namespace"` RQLiteDSN string `yaml:"rqlite_dsn"` BootstrapPeers []string `yaml:"bootstrap_peers"` + EnableHTTPS bool `yaml:"enable_https"` + DomainName string `yaml:"domain_name"` + TLSCacheDir string `yaml:"tls_cache_dir"` } data, err := os.ReadFile(configPath) @@ -79,6 +83,9 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config { ClientNamespace: "default", BootstrapPeers: nil, RQLiteDSN: "", + EnableHTTPS: false, + DomainName: "", + TLSCacheDir: "", } if v := strings.TrimSpace(y.ListenAddr); v != "" { @@ -103,6 +110,21 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config { } } + // HTTPS configuration + cfg.EnableHTTPS = y.EnableHTTPS + if v := strings.TrimSpace(y.DomainName); v != "" { + cfg.DomainName = v + } + if v := strings.TrimSpace(y.TLSCacheDir); v != "" { + cfg.TLSCacheDir = v + } else if cfg.EnableHTTPS { + // Default TLS cache directory if HTTPS is enabled but not specified + homeDir, err := os.UserHomeDir() + if err == nil { + cfg.TLSCacheDir = filepath.Join(homeDir, ".debros", "tls-cache") + } + } + // Validate configuration if errs := cfg.ValidateConfig(); len(errs) > 0 { fmt.Fprintf(os.Stderr, "\nGateway configuration errors (%d):\n", len(errs)) diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index 02b9446..d700474 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -12,6 +12,7 @@ import ( "github.com/DeBrosOfficial/network/pkg/gateway" "github.com/DeBrosOfficial/network/pkg/logging" "go.uber.org/zap" + "golang.org/x/crypto/acme/autocert" ) func setupLogger() *logging.ColoredLogger { @@ -42,6 +43,123 @@ func main() { logger.ComponentInfo(logging.ComponentGeneral, "Creating HTTP server and routes...") + // Check if HTTPS is enabled + if cfg.EnableHTTPS && cfg.DomainName != "" { + logger.ComponentInfo(logging.ComponentGeneral, "HTTPS enabled with ACME", + zap.String("domain", cfg.DomainName), + zap.String("tls_cache_dir", cfg.TLSCacheDir), + ) + + // Set up ACME manager + manager := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(cfg.DomainName), + } + + // Set cache directory if specified + if cfg.TLSCacheDir != "" { + manager.Cache = autocert.DirCache(cfg.TLSCacheDir) + logger.ComponentInfo(logging.ComponentGeneral, "Using TLS certificate cache", + zap.String("cache_dir", cfg.TLSCacheDir), + ) + } + + // Create HTTP server for ACME challenge (port 80) + httpServer := &http.Server{ + Addr: ":80", + Handler: manager.HTTPHandler(nil), // Redirects all HTTP traffic to HTTPS except ACME challenge + } + + // Create HTTPS server (port 443) + httpsServer := &http.Server{ + Addr: ":443", + Handler: gw.Routes(), + TLSConfig: manager.TLSConfig(), + } + + // Start HTTP server for ACME challenge + logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTP server for ACME challenge on port 80...") + httpLn, err := net.Listen("tcp", ":80") + if err != nil { + logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTP listen address (port 80)", zap.Error(err)) + os.Exit(1) + } + logger.ComponentInfo(logging.ComponentGeneral, "HTTP listener bound", zap.String("listen_addr", httpLn.Addr().String())) + + // Start HTTPS server + logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTPS server on port 443...") + httpsLn, err := net.Listen("tcp", ":443") + if err != nil { + logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTPS listen address (port 443)", zap.Error(err)) + os.Exit(1) + } + logger.ComponentInfo(logging.ComponentGeneral, "HTTPS listener bound", zap.String("listen_addr", httpsLn.Addr().String())) + + // Serve HTTP in a goroutine + httpServeErrCh := make(chan error, 1) + go func() { + if err := httpServer.Serve(httpLn); err != nil && err != http.ErrServerClosed { + httpServeErrCh <- err + return + } + httpServeErrCh <- nil + }() + + // Serve HTTPS in a goroutine + httpsServeErrCh := make(chan error, 1) + go func() { + if err := httpsServer.ServeTLS(httpsLn, "", ""); err != nil && err != http.ErrServerClosed { + httpsServeErrCh <- err + return + } + httpsServeErrCh <- nil + }() + + // Wait for termination signal or server error + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt, syscall.SIGTERM) + + select { + case sig := <-quit: + logger.ComponentInfo(logging.ComponentGeneral, "shutdown signal received", zap.String("signal", sig.String())) + case err := <-httpServeErrCh: + if err != nil { + logger.ComponentError(logging.ComponentGeneral, "HTTP server error", zap.Error(err)) + } else { + logger.ComponentInfo(logging.ComponentGeneral, "HTTP server exited normally") + } + case err := <-httpsServeErrCh: + if err != nil { + logger.ComponentError(logging.ComponentGeneral, "HTTPS server error", zap.Error(err)) + } else { + logger.ComponentInfo(logging.ComponentGeneral, "HTTPS server exited normally") + } + } + + logger.ComponentInfo(logging.ComponentGeneral, "Shutting down gateway servers...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Shutdown HTTPS server + if err := httpsServer.Shutdown(ctx); err != nil { + logger.ComponentError(logging.ComponentGeneral, "HTTPS server shutdown error", zap.Error(err)) + } else { + logger.ComponentInfo(logging.ComponentGeneral, "HTTPS server shutdown complete") + } + + // Shutdown HTTP server + if err := httpServer.Shutdown(ctx); err != nil { + logger.ComponentError(logging.ComponentGeneral, "HTTP server shutdown error", zap.Error(err)) + } else { + logger.ComponentInfo(logging.ComponentGeneral, "HTTP server shutdown complete") + } + + logger.ComponentInfo(logging.ComponentGeneral, "Gateway shutdown complete") + return + } + + // Standard HTTP server (no HTTPS) server := &http.Server{ Addr: cfg.ListenAddr, Handler: gw.Routes(), diff --git a/pkg/cli/setup.go b/pkg/cli/setup.go index 8d8a718..073aca4 100644 --- a/pkg/cli/setup.go +++ b/pkg/cli/setup.go @@ -125,13 +125,57 @@ func HandleSetupCommand(args []string) { fmt.Printf(" network-cli service restart gateway\n\n") fmt.Printf("Access DeBros User:\n") fmt.Printf(" sudo -u debros bash\n\n") + + // Check if HTTPS is enabled + gatewayConfigPath := "/home/debros/.debros/gateway.yaml" + httpsEnabled := false + var domainName string + if data, err := os.ReadFile(gatewayConfigPath); err == nil { + var cfg config.Config + if err := config.DecodeStrict(strings.NewReader(string(data)), &cfg); err == nil { + // Try to parse as gateway config + if strings.Contains(string(data), "enable_https: true") { + httpsEnabled = true + // Extract domain name from config + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), "domain_name:") { + parts := strings.Split(line, ":") + if len(parts) > 1 { + domainName = strings.Trim(strings.TrimSpace(parts[1]), "\"") + } + break + } + } + } + } + } + fmt.Printf("Verify Installation:\n") - fmt.Printf(" curl http://localhost:6001/health\n") + if httpsEnabled && domainName != "" { + fmt.Printf(" curl https://%s/health\n", domainName) + fmt.Printf(" curl http://localhost:6001/health (HTTP fallback)\n") + } else { + fmt.Printf(" curl http://localhost:6001/health\n") + } fmt.Printf(" curl http://localhost:5001/status\n\n") + + if httpsEnabled && domainName != "" { + fmt.Printf("HTTPS Configuration:\n") + fmt.Printf(" Domain: %s\n", domainName) + fmt.Printf(" HTTPS endpoint: https://%s\n", domainName) + fmt.Printf(" Certificate cache: /home/debros/.debros/tls-cache\n") + fmt.Printf(" Certificates are automatically managed via Let's Encrypt (ACME)\n\n") + } + fmt.Printf("Anyone Relay (Anon):\n") fmt.Printf(" sudo systemctl status anon\n") fmt.Printf(" sudo tail -f /home/debros/.debros/logs/anon/notices.log\n") - fmt.Printf(" Proxy endpoint: POST http://localhost:6001/v1/proxy/anon\n\n") + if httpsEnabled && domainName != "" { + fmt.Printf(" Proxy endpoint: POST https://%s/v1/proxy/anon\n\n", domainName) + } else { + fmt.Printf(" Proxy endpoint: POST http://localhost:6001/v1/proxy/anon\n\n") + } } // extractIPFromMultiaddr extracts the IP address from a multiaddr string @@ -334,6 +378,86 @@ func isValidHostPort(s string) bool { return true } +// isPortAvailable checks if a port is available for binding +func isPortAvailable(port int) bool { + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return false + } + ln.Close() + return true +} + +// checkPorts80And443 checks if ports 80 and 443 are available +func checkPorts80And443() (bool, string) { + port80Available := isPortAvailable(80) + port443Available := isPortAvailable(443) + + if !port80Available || !port443Available { + var issues []string + if !port80Available { + issues = append(issues, "port 80") + } + if !port443Available { + issues = append(issues, "port 443") + } + return false, strings.Join(issues, " and ") + } + + return true, "" +} + +// isValidDomain validates a domain name format +func isValidDomain(domain string) bool { + domain = strings.TrimSpace(domain) + if domain == "" { + return false + } + + // Basic validation: domain should contain at least one dot + // and not start/end with dot or hyphen + if !strings.Contains(domain, ".") { + return false + } + + if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") { + return false + } + + if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") { + return false + } + + // Check for valid characters (letters, numbers, dots, hyphens) + for _, char := range domain { + if !((char >= 'a' && char <= 'z') || + (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') || + char == '.' || + char == '-') { + return false + } + } + + return true +} + +// verifyDNSResolution verifies that a domain resolves to the VPS IP +func verifyDNSResolution(domain, expectedIP string) bool { + ips, err := net.LookupIP(domain) + if err != nil { + return false + } + + for _, ip := range ips { + if ip.To4() != nil && ip.String() == expectedIP { + return true + } + } + + return false +} + func setupDebrosUser() { fmt.Printf("šŸ‘¤ Setting up 'debros' user...\n") @@ -973,9 +1097,100 @@ func generateConfigsInteractive(force bool) { } if !gatewayExists || force { + // Prompt for domain and HTTPS configuration + var domain string + var enableHTTPS bool + var tlsCacheDir string + + fmt.Printf("\n🌐 Domain and HTTPS Configuration\n") + fmt.Printf("Would you like to configure HTTPS with a domain name? (yes/no) [default: no]: ") + response, _ := reader.ReadString('\n') + response = strings.ToLower(strings.TrimSpace(response)) + + if response == "yes" || response == "y" { + // Check if ports 80 and 443 are available + portsAvailable, portIssues := checkPorts80And443() + if !portsAvailable { + fmt.Fprintf(os.Stderr, "\nāš ļø Cannot enable HTTPS: %s is already in use\n", portIssues) + fmt.Fprintf(os.Stderr, " You will need to configure HTTPS manually if you want to use a domain.\n") + fmt.Fprintf(os.Stderr, " Continuing without HTTPS configuration...\n\n") + enableHTTPS = false + } else { + enableHTTPS = true + + // Prompt for domain name + for { + fmt.Printf("\nEnter your domain name (e.g., example.com): ") + domainInput, _ := reader.ReadString('\n') + domain = strings.TrimSpace(domainInput) + + if domain == "" { + fmt.Printf(" Domain name cannot be empty. Skipping HTTPS configuration.\n") + enableHTTPS = false + break + } + + if !isValidDomain(domain) { + fmt.Printf(" āŒ Invalid domain format. Please enter a valid domain name.\n") + continue + } + + // Verify DNS is configured + fmt.Printf("\n Verifying DNS configuration...\n") + fmt.Printf(" Please ensure your domain %s points to this server's IP (%s)\n", domain, vpsIP) + fmt.Printf(" Have you configured the DNS record? (yes/no): ") + dnsResponse, _ := reader.ReadString('\n') + dnsResponse = strings.ToLower(strings.TrimSpace(dnsResponse)) + + if dnsResponse == "yes" || dnsResponse == "y" { + // Try to verify DNS resolution + fmt.Printf(" Checking DNS resolution...\n") + if verifyDNSResolution(domain, vpsIP) { + fmt.Printf(" āœ“ DNS is correctly configured\n") + break + } else { + fmt.Printf(" āš ļø DNS does not resolve to this server's IP (%s)\n", vpsIP) + fmt.Printf(" DNS may still be propagating. Continue anyway? (yes/no): ") + continueResponse, _ := reader.ReadString('\n') + continueResponse = strings.ToLower(strings.TrimSpace(continueResponse)) + if continueResponse == "yes" || continueResponse == "y" { + fmt.Printf(" Continuing with domain configuration (DNS may need time to propagate)\n") + break + } + // User chose not to continue, ask for domain again + fmt.Printf(" Please configure DNS and try again, or press Enter to skip HTTPS\n") + continue + } + } else { + fmt.Printf(" Please configure DNS first. Type 'skip' to skip HTTPS configuration: ") + skipResponse, _ := reader.ReadString('\n') + skipResponse = strings.ToLower(strings.TrimSpace(skipResponse)) + if skipResponse == "skip" { + enableHTTPS = false + domain = "" + break + } + continue + } + } + + // Set TLS cache directory if HTTPS is enabled + if enableHTTPS && domain != "" { + tlsCacheDir = "/home/debros/.debros/tls-cache" + // Create TLS cache directory + if err := os.MkdirAll(tlsCacheDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "āš ļø Failed to create TLS cache directory: %v\n", err) + } else { + exec.Command("chown", "-R", "debros:debros", tlsCacheDir).Run() + fmt.Printf(" āœ“ TLS cache directory created: %s\n", tlsCacheDir) + } + } + } + } + // Gateway config should include bootstrap peers if this is a regular node // (bootstrap nodes don't need bootstrap peers since they are the bootstrap) - gatewayConfig := generateGatewayConfigDirect(bootstrapPeers) + gatewayConfig := generateGatewayConfigDirect(bootstrapPeers, enableHTTPS, domain, tlsCacheDir) if err := os.WriteFile(gatewayPath, []byte(gatewayConfig), 0644); err != nil { fmt.Fprintf(os.Stderr, "āŒ Failed to write gateway config: %v\n", err) os.Exit(1) @@ -1107,7 +1322,7 @@ logging: } // generateGatewayConfigDirect generates gateway config directly -func generateGatewayConfigDirect(bootstrapPeers string) string { +func generateGatewayConfigDirect(bootstrapPeers string, enableHTTPS bool, domain, tlsCacheDir string) string { var peers []string if bootstrapPeers != "" { for _, p := range strings.Split(bootstrapPeers, ",") { @@ -1127,11 +1342,23 @@ func generateGatewayConfigDirect(bootstrapPeers string) string { } } + var httpsYAML strings.Builder + if enableHTTPS && domain != "" { + fmt.Fprintf(&httpsYAML, "enable_https: true\n") + fmt.Fprintf(&httpsYAML, "domain_name: \"%s\"\n", domain) + if tlsCacheDir != "" { + fmt.Fprintf(&httpsYAML, "tls_cache_dir: \"%s\"\n", tlsCacheDir) + } + } else { + fmt.Fprintf(&httpsYAML, "enable_https: false\n") + } + return fmt.Sprintf(`listen_addr: ":6001" client_namespace: "default" rqlite_dsn: "" %s -`, peersYAML.String()) +%s +`, peersYAML.String(), httpsYAML.String()) } func createSystemdServices() { @@ -1191,6 +1418,10 @@ StandardOutput=journal StandardError=journal SyslogIdentifier=debros-gateway +# Allow binding to privileged ports (80, 443) for HTTPS/ACME +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE + NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict diff --git a/pkg/gateway/config_validate.go b/pkg/gateway/config_validate.go index a185107..baae7be 100644 --- a/pkg/gateway/config_validate.go +++ b/pkg/gateway/config_validate.go @@ -70,6 +70,21 @@ func (c *Config) ValidateConfig() []error { } } + // Validate HTTPS configuration + if c.EnableHTTPS { + if c.DomainName == "" { + errs = append(errs, fmt.Errorf("gateway.domain_name: must be set when enable_https is true")) + } else { + // Basic domain validation + if !isValidDomainName(c.DomainName) { + errs = append(errs, fmt.Errorf("gateway.domain_name: invalid domain format")) + } + } + if c.TLSCacheDir == "" { + errs = append(errs, fmt.Errorf("gateway.tls_cache_dir: must be set when enable_https is true")) + } + } + return errs } @@ -135,3 +150,38 @@ func extractTCPPort(multiaddrStr string) string { return portPart[:firstSlashIndex] } + +// isValidDomainName validates a domain name format +func isValidDomainName(domain string) bool { + domain = strings.TrimSpace(domain) + if domain == "" { + return false + } + + // Basic validation: domain should contain at least one dot + // and not start/end with dot or hyphen + if !strings.Contains(domain, ".") { + return false + } + + if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") { + return false + } + + if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") { + return false + } + + // Check for valid characters (letters, numbers, dots, hyphens) + for _, char := range domain { + if !((char >= 'a' && char <= 'z') || + (char >= 'A' && char <= 'Z') || + (char >= '0' && char <= '9') || + char == '.' || + char == '-') { + return false + } + } + + return true +} diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index 6057a34..62354cb 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -26,6 +26,11 @@ type Config struct { // Optional DSN for rqlite database/sql driver, e.g. "http://localhost:4001" // If empty, defaults to "http://localhost:4001". RQLiteDSN string + + // HTTPS configuration + EnableHTTPS bool // Enable HTTPS with ACME (Let's Encrypt) + DomainName string // Domain name for HTTPS certificate + TLSCacheDir string // Directory to cache TLS certificates (default: ~/.debros/tls-cache) } type Gateway struct {