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.
This commit is contained in:
anonpenguin23 2025-10-31 19:32:13 +02:00
parent f2d6254b7b
commit 8a7ae4ad6f
7 changed files with 458 additions and 6 deletions

View File

@ -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

View File

@ -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)'

View File

@ -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))

View File

@ -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(),

View File

@ -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

View File

@ -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
}

View File

@ -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 {