235 lines
5.9 KiB
Go
235 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// --- Config types ---
|
|
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server"`
|
|
TLS TLSConfig `yaml:"tls"`
|
|
Domain string `yaml:"domain"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Port int `yaml:"port"`
|
|
Host string `yaml:"host"`
|
|
}
|
|
|
|
type TLSConfig struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
Email string `yaml:"email"`
|
|
}
|
|
|
|
type Proxies struct {
|
|
Apps map[string]string `yaml:"apps"`
|
|
}
|
|
|
|
// --- Config loading ---
|
|
|
|
func loadConfig(configDir string) (*Config, *Proxies, error) {
|
|
configPath := filepath.Join(configDir, "config.yaml")
|
|
proxiesPath := filepath.Join(configDir, "proxies.yaml")
|
|
|
|
configData, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("reading config: %w", err)
|
|
}
|
|
proxiesData, err := os.ReadFile(proxiesPath)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("reading proxies: %w", err)
|
|
}
|
|
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(configData, &cfg); err != nil {
|
|
return nil, nil, fmt.Errorf("parsing config: %w", err)
|
|
}
|
|
var proxies Proxies
|
|
if err := yaml.Unmarshal(proxiesData, &proxies); err != nil {
|
|
return nil, nil, fmt.Errorf("parsing proxies: %w", err)
|
|
}
|
|
|
|
return &cfg, &proxies, nil
|
|
}
|
|
|
|
// --- Subdomain extraction ---
|
|
|
|
func extractSubdomain(host, domain string) string {
|
|
host = strings.ToLower(host)
|
|
|
|
// Strip port if present
|
|
if idx := strings.LastIndex(host, ":"); idx != -1 {
|
|
host = host[:idx]
|
|
}
|
|
|
|
// Bare domain — no subdomain (e.g. "nextwks.eu")
|
|
domainWithDot := "." + domain
|
|
if host == domain {
|
|
return ""
|
|
}
|
|
if !strings.HasSuffix(host, domainWithDot) {
|
|
return ""
|
|
}
|
|
|
|
return strings.TrimSuffix(host, domainWithDot)
|
|
}
|
|
|
|
// --- Handlers ---
|
|
|
|
func coreAppHandler(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 — App</title>
|
|
</head>
|
|
<body>
|
|
<h1>NextWorkspace — Your Workspace, Your Server</h1>
|
|
<p>Coming soon.</p>
|
|
</body>
|
|
</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 ---
|
|
|
|
func main() {
|
|
configDir := os.Getenv("CONFIG_DIR")
|
|
if configDir == "" {
|
|
configDir = "/opt/workspace/configs/core"
|
|
}
|
|
|
|
cfg, proxies, err := loadConfig(configDir)
|
|
if err != nil {
|
|
log.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Build subdomain -> handler map
|
|
handlers := make(map[string]http.HandlerFunc)
|
|
for subdomain, appName := range proxies.Apps {
|
|
switch appName {
|
|
case "core":
|
|
handlers[subdomain] = coreAppHandler
|
|
default:
|
|
log.Printf("Warning: unknown app %q for subdomain %q", appName, subdomain)
|
|
handlers[subdomain] = unknownSubdomainHandler
|
|
}
|
|
}
|
|
|
|
// Main router (for HTTPS or non-TLS mode)
|
|
router := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
subdomain := extractSubdomain(r.Host, cfg.Domain)
|
|
if handler, ok := handlers[subdomain]; ok {
|
|
handler(w, r)
|
|
return
|
|
}
|
|
unknownSubdomainHandler(w, r)
|
|
})
|
|
|
|
httpAddr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
|
|
|
if cfg.TLS.Enabled {
|
|
log.Println("NextWorkspace starting with TLS (certmagic)...")
|
|
|
|
certmagic.DefaultACME.Agreed = true
|
|
certmagic.DefaultACME.Email = cfg.TLS.Email
|
|
certmagic.Default.Storage = &certmagic.FileStorage{
|
|
Path: filepath.Join(configDir, "certs"),
|
|
}
|
|
|
|
// Collect domains from proxies.yaml only (no bare domain, no wildcard)
|
|
// Each subdomain gets its own certificate independently.
|
|
var domains []string
|
|
for subdomain := range proxies.Apps {
|
|
domains = append(domains, subdomain+"."+cfg.Domain)
|
|
}
|
|
|
|
// Obtain and manage certificates
|
|
magic := certmagic.NewDefault()
|
|
if err := magic.ManageSync(context.Background(), domains); err != nil {
|
|
log.Fatalf("Failed to manage certificates: %v", err)
|
|
}
|
|
|
|
// Extract ACME issuer for http-01 challenge handling
|
|
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()
|
|
http.Redirect(w, r, target, http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
unknownSubdomainHandler(w, r)
|
|
})
|
|
|
|
// Wrap with ACME challenge handler for Let's Encrypt http-01 validation
|
|
if acmeIssuer != nil {
|
|
httpHandler = acmeIssuer.HTTPChallengeHandler(httpHandler)
|
|
log.Println("ACME challenge handler enabled on :80")
|
|
}
|
|
|
|
httpServer := &http.Server{
|
|
Addr: httpAddr,
|
|
Handler: httpHandler,
|
|
}
|
|
|
|
// HTTPS server on :443 with TLS
|
|
tlsConfig := magic.TLSConfig()
|
|
tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") // HTTP/2 support
|
|
|
|
httpsServer := &http.Server{
|
|
Addr: ":443",
|
|
Handler: router,
|
|
TLSConfig: tlsConfig,
|
|
}
|
|
|
|
// Start HTTP server
|
|
go func() {
|
|
log.Printf("HTTP listening on %s", httpAddr)
|
|
if err := httpServer.ListenAndServe(); err != nil {
|
|
log.Fatalf("HTTP server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
log.Println("HTTPS listening on :443")
|
|
log.Fatal(httpsServer.ListenAndServeTLS("", "")) // certmagic provides certs via TLSConfig
|
|
} else {
|
|
log.Printf("NextWorkspace listening on %s (no TLS)", httpAddr)
|
|
log.Fatal(http.ListenAndServe(httpAddr, router))
|
|
}
|
|
}
|