NextWks/main.go

191 lines
4.4 KiB
Go

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, `<!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)
})
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))
}
}