fix: subdomain-based certs only, unknown subdomains return error
This commit is contained in:
parent
672653f731
commit
897275c659
1 changed files with 47 additions and 36 deletions
77
main.go
77
main.go
|
|
@ -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,27 +177,38 @@ 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
|
||||||
|
if len(magic.Issuers) > 0 {
|
||||||
|
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()
|
target := "https://" + r.Host + r.URL.RequestURI()
|
||||||
http.Redirect(w, r, target, http.StatusMovedPermanently)
|
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 {
|
|
||||||
httpHandler = ai.HTTPChallengeHandler(redirectHandler)
|
|
||||||
log.Println("ACME challenge handler enabled on :80")
|
log.Println("ACME challenge handler enabled on :80")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: httpAddr,
|
Addr: httpAddr,
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue