diff --git a/cmd/gateway/config.go b/cmd/gateway/config.go index 76548da..54fa8c9 100644 --- a/cmd/gateway/config.go +++ b/cmd/gateway/config.go @@ -53,6 +53,7 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config { ClientNamespace string `yaml:"client_namespace"` RQLiteDSN string `yaml:"rqlite_dsn"` BootstrapPeers []string `yaml:"bootstrap_peers"` + Domain string `yaml:"domain"` } data, err := os.ReadFile(configPath) @@ -103,6 +104,10 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config { } } + if v := strings.TrimSpace(y.Domain); v != "" { + cfg.Domain = v + } + // 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..4152b16 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -6,14 +6,19 @@ import ( "net/http" "os" "os/signal" + "path/filepath" + "strings" "syscall" "time" "github.com/DeBrosOfficial/network/pkg/gateway" "github.com/DeBrosOfficial/network/pkg/logging" + "github.com/caddyserver/certmagic" "go.uber.org/zap" ) +const acmeEmail = "dev@debros.io" + func setupLogger() *logging.ColoredLogger { logger, err := logging.NewColoredLogger(logging.ComponentGeneral, true) if err != nil { @@ -42,9 +47,143 @@ func main() { logger.ComponentInfo(logging.ComponentGeneral, "Creating HTTP server and routes...") + // Wrap handler with host enforcement if domain is set + handler := gw.Routes() + if cfg.Domain != "" { + d := cfg.Domain + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + host := r.Host + if i := strings.IndexByte(host, ':'); i >= 0 { + host = host[:i] + } + if !strings.EqualFold(host, d) { + http.NotFound(w, r) + return + } + gw.Routes().ServeHTTP(w, r) + }) + } + + // If domain is configured, use ACME TLS on :443 and :80 for challenges + if cfg.Domain != "" { + logger.ComponentInfo(logging.ComponentGeneral, "Production ACME TLS enabled", + zap.String("domain", cfg.Domain), + zap.String("acme_email", acmeEmail), + ) + + // Setup CertMagic with file storage + certDir := filepath.Join(os.ExpandEnv("$HOME"), ".debros", "certmagic") + if home, err := os.UserHomeDir(); err == nil { + certDir = filepath.Join(home, ".debros", "certmagic") + } + + if err := os.MkdirAll(certDir, 0700); err != nil { + logger.ComponentError(logging.ComponentGeneral, "failed to create certmagic directory", zap.Error(err)) + os.Exit(1) + } + + // Configure CertMagic for ACME + logger.ComponentInfo(logging.ComponentGeneral, "Provisioning ACME certificate...", + zap.String("domain", cfg.Domain), + ) + + // Use the default CertMagic instance and configure storage + certmagic.Default.Storage = &certmagic.FileStorage{Path: certDir} + + // Setup ACME issuer + acmeIssuer := certmagic.ACMEIssuer{ + CA: certmagic.LetsEncryptProductionCA, + Email: acmeEmail, + Agreed: true, + } + certmagic.Default.Issuers = []certmagic.Issuer{&acmeIssuer} + + // Manage the domain + if err := certmagic.ManageSync(context.Background(), []string{cfg.Domain}); err != nil { + logger.ComponentError(logging.ComponentGeneral, "ACME ManageSync failed", zap.Error(err)) + os.Exit(1) + } + + // Get TLS config + tlsCfg := certmagic.Default.TLSConfig() + + // Start HTTP server on :80 for ACME challenges and redirect to HTTPS + logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTP server on :80 for ACME challenges") + go func() { + httpMux := http.NewServeMux() + httpMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Redirect all HTTP to HTTPS + u := *r.URL + u.Scheme = "https" + u.Host = cfg.Domain + http.Redirect(w, r, u.String(), http.StatusMovedPermanently) + }) + // HTTP server for ACME challenges and redirects + if err := http.ListenAndServe(":80", httpMux); err != nil && err != http.ErrServerClosed { + logger.ComponentError(logging.ComponentGeneral, "HTTP :80 server error", zap.Error(err)) + } + logger.ComponentInfo(logging.ComponentGeneral, "HTTP :80 server stopped") + }() + + // Start HTTPS server on :443 + logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTPS server on :443 with ACME certificate", + zap.String("domain", cfg.Domain), + ) + + httpsServer := &http.Server{ + Addr: ":443", + Handler: handler, + TLSConfig: tlsCfg, + } + + ln, err := net.Listen("tcp", httpsServer.Addr) + if err != nil { + logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTPS listen address", zap.Error(err)) + os.Exit(1) + } + logger.ComponentInfo(logging.ComponentGeneral, "HTTPS listener bound", zap.String("listen_addr", ln.Addr().String())) + + // Serve in a goroutine so we can handle graceful shutdown on signals. + serveErrCh := make(chan error, 1) + go func() { + if err := httpsServer.ServeTLS(ln, "", ""); err != nil && err != http.ErrServerClosed { + serveErrCh <- err + return + } + serveErrCh <- 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 := <-serveErrCh: + 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 HTTPS server...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := httpsServer.Shutdown(ctx); err != nil { + logger.ComponentError(logging.ComponentGeneral, "HTTPS server shutdown error", zap.Error(err)) + } else { + logger.ComponentInfo(logging.ComponentGeneral, "Gateway shutdown complete") + } + return + } + + // Fallback: HTTP server on configured listen_addr when no domain server := &http.Server{ Addr: cfg.ListenAddr, - Handler: gw.Routes(), + Handler: handler, } // Try to bind listener explicitly so binding failures are visible immediately. diff --git a/go.mod b/go.mod index 0b1a4b6..d67266f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.8 toolchain go1.24.1 require ( + github.com/caddyserver/certmagic v0.20.0 github.com/ethereum/go-ethereum v1.13.14 github.com/gorilla/websocket v1.5.3 github.com/libp2p/go-libp2p v0.41.1 @@ -46,6 +47,7 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/koron/go-ssdp v0.0.5 // indirect + github.com/libdns/libdns v0.2.1 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-flow-metrics v0.2.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect @@ -55,6 +57,7 @@ require ( github.com/libp2p/go-yamux/v5 v5.0.0 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mholt/acmez v1.2.0 // indirect github.com/miekg/dns v1.1.66 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect @@ -104,6 +107,7 @@ require ( github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/wlynxg/anet v0.0.5 // indirect + github.com/zeebo/blake3 v0.2.3 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/fx v1.23.0 // indirect go.uber.org/mock v0.5.0 // indirect diff --git a/go.sum b/go.sum index 33dd50c..9594e52 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFA github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= +github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= @@ -123,6 +125,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk= @@ -136,6 +139,8 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= +github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= @@ -165,6 +170,8 @@ github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= +github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= @@ -342,6 +349,12 @@ github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguH github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= +github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= diff --git a/pkg/gateway/config_validate.go b/pkg/gateway/config_validate.go index a185107..2bc676a 100644 --- a/pkg/gateway/config_validate.go +++ b/pkg/gateway/config_validate.go @@ -70,6 +70,13 @@ func (c *Config) ValidateConfig() []error { } } + // Validate domain if provided (optional for TLS in production) + if c.Domain != "" { + if err := validateDomain(c.Domain); err != nil { + errs = append(errs, fmt.Errorf("gateway.domain: %v", err)) + } + } + return errs } @@ -135,3 +142,31 @@ func extractTCPPort(multiaddrStr string) string { return portPart[:firstSlashIndex] } + +// validateDomain checks if a domain is valid (basic hostname validation). +// Forbids schemes, slashes, and leading dots. +func validateDomain(domain string) error { + if strings.Contains(domain, "://") { + return fmt.Errorf("domain must not contain a scheme (e.g., 'http://')") + } + if strings.Contains(domain, "/") { + return fmt.Errorf("domain must not contain slashes") + } + if strings.HasPrefix(domain, ".") { + return fmt.Errorf("domain must not start with a dot") + } + // Basic length check + if len(domain) > 253 { + return fmt.Errorf("domain is too long (max 253 characters)") + } + if len(domain) < 1 { + return fmt.Errorf("domain must not be empty") + } + // Allow alphanumeric, dots, hyphens (basic hostname check) + for _, ch := range domain { + if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '.' || ch == '-') { + return fmt.Errorf("domain contains invalid characters; only alphanumeric, dots, and hyphens allowed") + } + } + return nil +} diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index 8887237..ea5c1b0 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -25,6 +25,9 @@ type Config struct { // Optional DSN for rqlite database/sql driver, e.g. "http://localhost:4001" // If empty, defaults to "http://localhost:4001". RQLiteDSN string + + // Production domain for HTTPS/ACME + Domain string } type Gateway struct { diff --git a/scripts/install-debros-network.sh b/scripts/install-debros-network.sh index abeeeb3..7ee5951 100755 --- a/scripts/install-debros-network.sh +++ b/scripts/install-debros-network.sh @@ -251,7 +251,7 @@ install_rqlite() { } check_ports() { - local ports=($NODE_PORT $RQLITE_PORT $RAFT_PORT $GATEWAY_PORT) + local ports=($NODE_PORT $RQLITE_PORT $RAFT_PORT $GATEWAY_PORT 80 443) for port in "${ports[@]}"; do if sudo netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; then error "Port $port is already in use. Please free it up and try again." @@ -279,23 +279,31 @@ setup_directories() { sudo -u "$DEBROS_USER" mkdir -p "$DEBROS_HOME/.debros" sudo chmod 0700 "$DEBROS_HOME/.debros" + # Create ~/.debros/certmagic for ACME certificate storage + sudo -u "$DEBROS_USER" mkdir -p "$DEBROS_HOME/.debros/certmagic" + sudo chmod 0700 "$DEBROS_HOME/.debros/certmagic" + success "Directory structure ready" } setup_source_code() { log "Setting up source code..." if [ -d "$INSTALL_DIR/src/.git" ]; then - log "Updating existing repository..." + log "Updating existing repository (on 'nightly' branch)..." cd "$INSTALL_DIR/src" - sudo -u "$DEBROS_USER" git pull + # Always ensure we're on 'nightly' before pulling + sudo -u "$DEBROS_USER" git fetch + sudo -u "$DEBROS_USER" git checkout nightly || sudo -u "$DEBROS_USER" git checkout -b nightly origin/nightly + sudo -u "$DEBROS_USER" git pull origin nightly else - log "Cloning repository..." - sudo -u "$DEBROS_USER" git clone "$REPO_URL" "$INSTALL_DIR/src" + log "Cloning repository (branch: nightly)..." + sudo -u "$DEBROS_USER" git clone --branch nightly --single-branch "$REPO_URL" "$INSTALL_DIR/src" cd "$INSTALL_DIR/src" fi success "Source code ready" } + build_binaries() { log "Building DeBros Network binaries..." cd "$INSTALL_DIR/src" @@ -354,7 +362,7 @@ configure_firewall() { log "Configuring firewall rules..." if command -v ufw &> /dev/null; then log "Adding UFW rules for DeBros Network ports..." - for port in $NODE_PORT $RQLITE_PORT $RAFT_PORT $GATEWAY_PORT; do + for port in $NODE_PORT $RQLITE_PORT $RAFT_PORT $GATEWAY_PORT 80 443; do if ! sudo ufw allow $port 2>/dev/null; then error "Failed to allow port $port" exit 1 @@ -375,6 +383,8 @@ configure_firewall() { log " - Port $RQLITE_PORT (RQLite HTTP)" log " - Port $RAFT_PORT (RQLite Raft)" log " - Port $GATEWAY_PORT (Gateway)" + log " - Port 80 (HTTP - ACME challenges)" + log " - Port 443 (HTTPS - Production TLS)" fi } @@ -455,6 +465,10 @@ ProtectSystem=strict ProtectHome=yes ReadWritePaths=/opt/debros +# Allow binding to privileged ports (80, 443) for ACME TLS +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE + [Install] WantedBy=multi-user.target EOF @@ -538,8 +552,16 @@ main() { log "${GREEN}Config Directory:${NOCOLOR} ${CYAN}$DEBROS_HOME/.debros${NOCOLOR}" log "${GREEN}LibP2P Port:${NOCOLOR} ${CYAN}$NODE_PORT${NOCOLOR}" log "${GREEN}RQLite Port:${NOCOLOR} ${CYAN}$RQLITE_PORT${NOCOLOR}" - log "${GREEN}Gateway Port:${NOCOLOR} ${CYAN}$GATEWAY_PORT${NOCOLOR}" + log "${GREEN}Gateway Port (Dev):${NOCOLOR} ${CYAN}$GATEWAY_PORT${NOCOLOR}" log "${GREEN}Raft Port:${NOCOLOR} ${CYAN}$RAFT_PORT${NOCOLOR}" + log "${GREEN}HTTP/ACME (Production):${NOCOLOR} ${CYAN}80${NOCOLOR}" + log "${GREEN}HTTPS/TLS (Production):${NOCOLOR} ${CYAN}443${NOCOLOR}" + log "${BLUE}==================================================${NOCOLOR}" + log "${GREEN}Production ACME Setup:${NOCOLOR}" + log "${CYAN} Edit $DEBROS_HOME/.debros/gateway.yaml and add:${NOCOLOR}" + log "${CYAN} domain: \"api.example.com\"${NOCOLOR}" + log "${CYAN} Ensure DNS A record points to this server IP${NOCOLOR}" + log "${CYAN} Certificate will auto-provision on first gateway start${NOCOLOR}" log "${BLUE}==================================================${NOCOLOR}" log "${GREEN}Service Management:${NOCOLOR}" log "${CYAN} - sudo systemctl status debros-node${NOCOLOR} (Check node status)"