NextWks/main.go

224 lines
5.4 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
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, `<!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) {
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>`)
}
// --- 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)
})
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 + bare domain)
var domains []string
for subdomain := range proxies.Apps {
domains = append(domains, subdomain+"."+cfg.Domain)
}
domains = append(domains, 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)
}
// HTTP redirect handler (on :80)
redirectHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
target := "https://" + r.Host + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusMovedPermanently)
})
// Wrap with ACME challenge handler for Let's Encrypt http-01 validation
var httpHandler http.Handler = redirectHandler
if len(magic.Issuers) > 0 {
if ai, ok := magic.Issuers[0].(*certmagic.ACMEIssuer); ok {
httpHandler = ai.HTTPChallengeHandler(redirectHandler)
log.Println("ACME challenge handler enabled on :80")
}
}
httpServer := &http.Server{
Addr: httpAddr,
Handler: httpHandler,
}
// HTTPS server 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 redirect server
go func() {
log.Printf("HTTP redirect 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))
}
}