package main import ( "context" "crypto/tls" "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 domainWithDot := "." + domain if host == domain { return "" } if !strings.HasSuffix(host, domainWithDot) { return "" } return strings.TrimSuffix(host, domainWithDot) } // --- 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, `
The Self-Hosted Workspace for Startups
`) } 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, `Coming soon.
`) } // --- 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, using boilerplate", appName, subdomain) handlers[subdomain] = boilerplateHandler } } // Main router 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 } boilerplateHandler(w, r) }) addr := 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"), } var domains []string for subdomain := range proxies.Apps { domains = append(domains, subdomain+"."+cfg.Domain) } domains = append(domains, cfg.Domain) magic := certmagic.NewDefault() if err := magic.ManageSync(context.Background(), domains); err != nil { log.Fatalf("Failed to manage certificates: %v", err) } tlsConfig := magic.TLSConfig() listener, err := tls.Listen("tcp", addr, tlsConfig) if err != nil { log.Fatalf("Failed to start TLS listener: %v", err) } log.Printf("NextWorkspace listening on %s (TLS)", addr) log.Fatal(http.Serve(listener, router)) } else { log.Printf("NextWorkspace listening on %s (no TLS)", addr) log.Fatal(http.ListenAndServe(addr, router)) } }