fix: subdomain-based certs only, unknown subdomains return error

This commit is contained in:
Claus Lohmar 2026-07-06 15:52:51 +01:00
parent 672653f731
commit 897275c659

83
main.go
View file

@ -72,7 +72,7 @@ func extractSubdomain(host, domain string) string {
host = host[:idx] host = host[:idx]
} }
// Bare domain — no subdomain // Bare domain — no subdomain (e.g. "nextwks.eu")
domainWithDot := "." + domain domainWithDot := "." + domain
if host == domain { if host == domain {
return "" return ""
@ -86,22 +86,6 @@ func extractSubdomain(host, domain string) string {
// --- Handlers --- // --- Handlers ---
func boilerplateHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>NextWorkspace</title>
</head>
<body>
<h1>NextWorkspace</h1>
<p>The Self-Hosted Workspace for Startups</p>
</body>
</html>`)
}
func coreAppHandler(w http.ResponseWriter, r *http.Request) { func coreAppHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@ -118,6 +102,22 @@ func coreAppHandler(w http.ResponseWriter, r *http.Request) {
</html>`) </html>`)
} }
func unknownSubdomainHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Not Found</title>
</head>
<body>
<h1>Not Found</h1>
<p>No application registered for this subdomain.</p>
</body>
</html>`)
}
// --- Main --- // --- Main ---
func main() { func main() {
@ -138,19 +138,19 @@ func main() {
case "core": case "core":
handlers[subdomain] = coreAppHandler handlers[subdomain] = coreAppHandler
default: default:
log.Printf("Warning: unknown app %q for subdomain %q, using boilerplate", appName, subdomain) log.Printf("Warning: unknown app %q for subdomain %q", appName, subdomain)
handlers[subdomain] = boilerplateHandler handlers[subdomain] = unknownSubdomainHandler
} }
} }
// Main router // Main router (for HTTPS or non-TLS mode)
router := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { router := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
subdomain := extractSubdomain(r.Host, cfg.Domain) subdomain := extractSubdomain(r.Host, cfg.Domain)
if handler, ok := handlers[subdomain]; ok { if handler, ok := handlers[subdomain]; ok {
handler(w, r) handler(w, r)
return return
} }
boilerplateHandler(w, r) unknownSubdomainHandler(w, r)
}) })
httpAddr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) httpAddr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
@ -164,12 +164,12 @@ func main() {
Path: filepath.Join(configDir, "certs"), Path: filepath.Join(configDir, "certs"),
} }
// Collect domains (from proxies.yaml + bare domain) // Collect domains from proxies.yaml only (no bare domain, no wildcard)
// Each subdomain gets its own certificate independently.
var domains []string var domains []string
for subdomain := range proxies.Apps { for subdomain := range proxies.Apps {
domains = append(domains, subdomain+"."+cfg.Domain) domains = append(domains, subdomain+"."+cfg.Domain)
} }
domains = append(domains, cfg.Domain)
// Obtain and manage certificates // Obtain and manage certificates
magic := certmagic.NewDefault() magic := certmagic.NewDefault()
@ -177,19 +177,30 @@ func main() {
log.Fatalf("Failed to manage certificates: %v", err) log.Fatalf("Failed to manage certificates: %v", err)
} }
// HTTP redirect handler (on :80) // Extract ACME issuer for http-01 challenge handling
redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var acmeIssuer *certmagic.ACMEIssuer
target := "https://" + r.Host + r.URL.RequestURI() if len(magic.Issuers) > 0 {
http.Redirect(w, r, target, http.StatusMovedPermanently) acmeIssuer, _ = magic.Issuers[0].(*certmagic.ACMEIssuer)
}
// HTTP handler on :80
// - Known subdomains → redirect to HTTPS
// - Unknown subdomains → error directly on HTTP (no redirect to broken HTTPS)
// - ACME challenge paths → handled by certmagic before reaching us
var httpHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
subdomain := extractSubdomain(r.Host, cfg.Domain)
if _, ok := handlers[subdomain]; ok {
target := "https://" + r.Host + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusMovedPermanently)
return
}
unknownSubdomainHandler(w, r)
}) })
// Wrap with ACME challenge handler for Let's Encrypt http-01 validation // Wrap with ACME challenge handler for Let's Encrypt http-01 validation
var httpHandler http.Handler = redirectHandler if acmeIssuer != nil {
if len(magic.Issuers) > 0 { httpHandler = acmeIssuer.HTTPChallengeHandler(httpHandler)
if ai, ok := magic.Issuers[0].(*certmagic.ACMEIssuer); ok { log.Println("ACME challenge handler enabled on :80")
httpHandler = ai.HTTPChallengeHandler(redirectHandler)
log.Println("ACME challenge handler enabled on :80")
}
} }
httpServer := &http.Server{ httpServer := &http.Server{
@ -197,7 +208,7 @@ func main() {
Handler: httpHandler, Handler: httpHandler,
} }
// HTTPS server with TLS // HTTPS server on :443 with TLS
tlsConfig := magic.TLSConfig() tlsConfig := magic.TLSConfig()
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") // HTTP/2 support tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") // HTTP/2 support
@ -207,9 +218,9 @@ func main() {
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
} }
// Start HTTP redirect server // Start HTTP server
go func() { go func() {
log.Printf("HTTP redirect listening on %s", httpAddr) log.Printf("HTTP listening on %s", httpAddr)
if err := httpServer.ListenAndServe(); err != nil { if err := httpServer.ListenAndServe(); err != nil {
log.Fatalf("HTTP server error: %v", err) log.Fatalf("HTTP server error: %v", err)
} }