network/cmd/gateway/main.go
anonpenguin23 d2b671b335
feat: add domain configuration and ACME TLS support for gateway
- Introduced a new `Domain` field in the gateway configuration to support HTTPS and ACME certificate provisioning.
- Implemented domain validation to ensure proper format.
- Enhanced the main gateway logic to handle ACME challenges and manage TLS certificates using CertMagic.
- Updated installation script to create necessary directories for ACME certificate storage and configure firewall rules for HTTP/HTTPS ports.
2025-10-25 13:45:53 +03:00

242 lines
7.6 KiB
Go

package main
import (
"context"
"net"
"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 {
panic(err)
}
return logger
}
func main() {
logger := setupLogger()
// Load gateway config (flags/env)
cfg := parseGatewayConfig(logger)
logger.ComponentInfo(logging.ComponentGeneral, "Starting gateway initialization...")
// Initialize gateway (connect client, prepare routes)
gw, err := gateway.New(logger, cfg)
if err != nil {
logger.ComponentError(logging.ComponentGeneral, "failed to initialize gateway", zap.Error(err))
os.Exit(1)
}
defer gw.Close()
logger.ComponentInfo(logging.ComponentGeneral, "Gateway initialization completed successfully")
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: handler,
}
// Try to bind listener explicitly so binding failures are visible immediately.
logger.ComponentInfo(logging.ComponentGeneral, "Gateway HTTP server starting",
zap.String("addr", cfg.ListenAddr),
zap.String("namespace", cfg.ClientNamespace),
zap.Int("bootstrap_peer_count", len(cfg.BootstrapPeers)),
)
logger.ComponentInfo(logging.ComponentGeneral, "Attempting to bind HTTP listener...")
ln, err := net.Listen("tcp", cfg.ListenAddr)
if err != nil {
logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTP listen address", zap.Error(err))
// exit because server cannot function without a listener
os.Exit(1)
}
logger.ComponentInfo(logging.ComponentGeneral, "HTTP 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 := server.Serve(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, "HTTP server error", zap.Error(err))
// continue to shutdown path so we close resources cleanly
} else {
logger.ComponentInfo(logging.ComponentGeneral, "HTTP server exited normally")
}
}
logger.ComponentInfo(logging.ComponentGeneral, "Shutting down gateway HTTP server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.ComponentError(logging.ComponentGeneral, "HTTP server shutdown error", zap.Error(err))
} else {
logger.ComponentInfo(logging.ComponentGeneral, "Gateway shutdown complete")
}
}