Ctrl K
ComunidadGuías…

main.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// quicgate: a single-binary reverse proxy manager. NPM's workflow, a native
// Go engine (HTTP/1.1, HTTP/2, HTTP/3 via quic-go, ACME via certmagic), and
// every advanced option typed instead of free-text config.
package main

import (
	"context"
	"embed"
	"io/fs"
	"log"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"quicgate/internal/admin"
	"quicgate/internal/docker"
	"quicgate/internal/engine"
	"quicgate/internal/store"
)

// dockerEndpoints resolves the Docker hosts to watch. A JSON list in
// QG_DOCKER_ENDPOINTS or the docker_endpoints setting is authoritative when
// present; otherwise a single local endpoint is derived from the socket env.
// A list that does not parse or validate names no endpoint at all: it is
// logged and nothing is watched, rather than guessing at what was meant or
// quietly watching the local socket instead of the daemons that were named.
func dockerEndpoints(st *store.Store) []docker.Endpoint {
	raw := os.Getenv("QG_DOCKER_ENDPOINTS")
	if strings.TrimSpace(raw) == "" {
		raw = st.GetSetting("docker_endpoints", "")
	}
	if strings.TrimSpace(raw) != "" {
		eps, err := docker.ParseEndpoints(raw)
		if err != nil {
			log.Printf("docker: %v; no Docker host is watched until the list is fixed", err)
			return nil
		}
		return eps
	}
	return []docker.Endpoint{{
		Name:    "local",
		Connect: env("QG_DOCKER_SOCKET", "/var/run/docker.sock"),
		Address: env("QG_DOCKER_HOST_ADDR", "127.0.0.1"),
	}}
}

//go:embed web
var webEmbed embed.FS

// version is stamped at build time via -ldflags "-X main.version=...".
// Defaults to "dev" for `go run`/unstamped builds.
var version = "dev"

func env(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func main() {
	dataDir := env("QG_DATA", "./data")
	if err := os.MkdirAll(dataDir, 0o700); err != nil {
		log.Fatalf("data dir: %v", err)
	}

	st, err := store.Open(dataDir + "/quicgate.db")
	if err != nil {
		log.Fatalf("store: %v", err)
	}
	defer st.Close()

	// "quicgate -unseal" prepares a rollback to a version from before secrets
	// were sealed: it writes them back as plaintext and exits. Any later start
	// of this version seals them again.
	if len(os.Args) > 1 && os.Args[1] == "-unseal" {
		n, err := st.Unseal()
		if err != nil {
			log.Fatalf("unseal: %v", err)
		}
		log.Printf("unseal: %d secrets are stored in PLAINTEXT again, readable by older versions. Start the older version now; this version would seal them again.", n)
		return
	}
	if ss := st.SealStatus(); ss.Locked {
		log.Printf("quicgate: the secret store is LOCKED: %s", ss.Reason)
	} else {
		log.Printf("quicgate: secrets are sealed with key %s from %s", ss.KeyID, ss.Source)
	}

	eng := engine.New(engine.Config{
		HTTPAddr:   env("QG_HTTP", ":80"),
		HTTPSAddr:  env("QG_HTTPS", ":443"),
		DataDir:    dataDir,
		ACMEEmail:  os.Getenv("QG_ACME_EMAIL"),
		ACMEStage:  os.Getenv("QG_ACME_STAGING") == "1",
		DisableTLS: os.Getenv("QG_TLS") == "off",
		DisableH3:  os.Getenv("QG_H3") == "off",
		UPnP:       os.Getenv("QG_UPNP") == "1",
		AdminAddr:  env("QG_ADMIN", ":81"),
		Version:    version,
	}, st)
	log.Printf("quicgate %s starting", version)

	webFS, err := fs.Sub(webEmbed, "web")
	if err != nil {
		log.Fatalf("web assets: %v", err)
	}
	adm := admin.New(st, eng, webFS, dataDir)
	if err := adm.EnsureAdmin(); err != nil {
		log.Fatalf("admin seed: %v", err)
	}

	// Docker label provider (opt-in): derives hosts and streams from container
	// labels and merges them into the engine. Enabled with QG_DOCKER=1 or the
	// docker_enabled setting; the daemon socket must be mounted into the
	// container (read-only is sufficient, the provider only ever reads).
	var dockerProvider *docker.Provider
	if os.Getenv("QG_DOCKER") == "1" || st.GetSetting("docker_enabled", "") == "1" {
		dockerProvider = docker.NewProvider(docker.Options{
			Endpoints:     dockerEndpoints(st),
			DefaultDomain: os.Getenv("QG_DOCKER_DOMAIN"),
			LabelPrefix:   env("QG_DOCKER_LABEL_PREFIX", "quicgate"),
		}, docker.Hooks{
			Apply: eng.SetDockerRoutes,
			ResolveACL: func(name string) (int64, bool) {
				lists, err := st.ListAccessLists()
				if err != nil {
					return 0, false
				}
				for _, l := range lists {
					if strings.EqualFold(l.Name, name) {
						return l.ID, true
					}
				}
				return 0, false
			},
			ExistingDomains: func() map[string]bool {
				hosts, err := st.ListHosts()
				if err != nil {
					return nil
				}
				m := map[string]bool{}
				for _, h := range hosts {
					// A configured name is configured whether or not its host
					// is switched on: a container must not take it over while
					// the host is disabled.
					for _, d := range h.Domains {
						m[strings.ToLower(d)] = true
					}
				}
				return m
			},
			UsedPorts: func() map[int]bool {
				used := map[int]bool{}
				for _, p := range eng.ReservedPorts() {
					used[p] = true
				}
				streams, err := st.ListStreams()
				if err != nil {
					return used
				}
				for _, s := range streams {
					last := max(s.ListenPort, s.ListenPortEnd)
					for p := s.ListenPort; p <= last; p++ {
						used[p] = true
					}
				}
				return used
			},
			Setting: st.GetSetting,
		})
		adm.SetDocker(dockerProvider)
	}

	// SIGTERM is what `docker stop` and most service managers send; without it
	// the process died without shutting listeners down, releasing UPnP
	// mappings or flushing the access log.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	if dockerProvider != nil {
		go dockerProvider.Run(ctx)
	}

	// The admin port is bound here, before the engine starts, so a stream that
	// is configured on it is the one that fails to listen, not the admin UI
	// (which would otherwise die in a crash loop, taking every site with it).
	// ReadTimeout bounds how long a request body may trickle in (the restore
	// upload extends its own deadline); IdleTimeout ends keep-alive
	// connections nobody uses. There is no WriteTimeout: a backup download or
	// a log read may legitimately take longer than any fixed limit.
	adminAddr := env("QG_ADMIN", ":81")
	adminLn, err := net.Listen("tcp", adminAddr)
	if err != nil {
		log.Fatalf("admin listener: %v", err)
	}
	adminSrv := &http.Server{
		Addr:              adminAddr,
		Handler:           adm.Handler(),
		ReadHeaderTimeout: 10 * time.Second,
		ReadTimeout:       30 * time.Second,
		IdleTimeout:       120 * time.Second,
	}
	go func() {
		log.Printf("admin: ui listening on %s", adminAddr)
		if err := adminSrv.Serve(adminLn); err != http.ErrServerClosed {
			log.Fatalf("admin listener: %v", err)
		}
	}()

	if err := eng.Run(ctx); err != nil {
		log.Fatalf("engine: %v", err)
	}
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_ = adminSrv.Shutdown(shutdownCtx)
}