feat: subdomain router with certmagic integration
This commit is contained in:
parent
2d3832df0b
commit
cd15d294a1
6 changed files with 296 additions and 25 deletions
9
configs/core/config.yaml
Normal file
9
configs/core/config.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
server:
|
||||
port: 80
|
||||
host: "0.0.0.0"
|
||||
|
||||
tls:
|
||||
enabled: false # Flip to true for Let's Encrypt HTTPS
|
||||
email: "" # Required when enabled (e.g. admin@nextwks.eu)
|
||||
|
||||
domain: "nextwks.eu" # Base domain for subdomain routing + certs
|
||||
5
configs/core/proxies.yaml
Normal file
5
configs/core/proxies.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Subdomain → registered app mapping
|
||||
# app : app handler name (reserved)
|
||||
# future: mail, drive, calendar, etc.
|
||||
apps:
|
||||
app: core
|
||||
36
deploy.sh
36
deploy.sh
|
|
@ -6,7 +6,7 @@ HEALTH_CHECK_RETRIES=10
|
|||
HEALTH_CHECK_INTERVAL=2
|
||||
|
||||
REPO_DIR="/opt/NextWks"
|
||||
TARGET_DIR="/opt/nextworkspace"
|
||||
TARGET_DIR="/opt/workspace"
|
||||
SERVICE_NAME="nextworkspace"
|
||||
BINARY_NAME="nextworkspace"
|
||||
|
||||
|
|
@ -14,36 +14,40 @@ echo "=== NextWorkspace Deploy ==="
|
|||
|
||||
# 1. Navigate to repo and pull latest
|
||||
cd "$REPO_DIR"
|
||||
echo "[1/8] Pulling latest code..."
|
||||
echo "[1/9] Pulling latest code..."
|
||||
git pull
|
||||
|
||||
# 2. Build
|
||||
echo "[2/8] Building binary..."
|
||||
echo "[2/9] Building binary..."
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
go build -o "$BINARY_NAME" .
|
||||
|
||||
# 3. Remove old deployment
|
||||
echo "[3/8] Removing old deployment..."
|
||||
echo "[3/9] Removing old deployment..."
|
||||
rm -rf "$TARGET_DIR"
|
||||
|
||||
# 4. Create target directories
|
||||
echo "[4/8] Creating target directories..."
|
||||
mkdir -p "$TARGET_DIR/app/data"
|
||||
mkdir -p "$TARGET_DIR/app/static"
|
||||
# 4. Create target directory structure
|
||||
echo "[4/9] Creating target directories..."
|
||||
mkdir -p "$TARGET_DIR/configs/core/certs"
|
||||
|
||||
# 5. Copy binary
|
||||
echo "[5/8] Copying binary..."
|
||||
echo "[5/9] Copying binary..."
|
||||
cp "$BINARY_NAME" "$TARGET_DIR/$BINARY_NAME"
|
||||
|
||||
# 6. Write systemd service
|
||||
echo "[6/8] Writing systemd service..."
|
||||
# 6. Copy config files
|
||||
echo "[6/9] Copying config files..."
|
||||
cp configs/core/config.yaml "$TARGET_DIR/configs/core/config.yaml"
|
||||
cp configs/core/proxies.yaml "$TARGET_DIR/configs/core/proxies.yaml"
|
||||
|
||||
# 7. Write systemd service
|
||||
echo "[7/9] Writing systemd service..."
|
||||
cat > /etc/systemd/system/$SERVICE_NAME.service <<UNIT
|
||||
[Unit]
|
||||
Description=NextWorkspace
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Environment=PORT=80
|
||||
Environment=CONFIG_DIR=$TARGET_DIR/configs/core
|
||||
ExecStart=$TARGET_DIR/$BINARY_NAME
|
||||
WorkingDirectory=$TARGET_DIR
|
||||
Restart=always
|
||||
|
|
@ -54,14 +58,14 @@ Group=root
|
|||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
# 7. Reload systemd and restart
|
||||
echo "[7/8] Reloading systemd and restarting service..."
|
||||
# 8. Reload systemd and restart
|
||||
echo "[8/9] Reloading systemd and restarting service..."
|
||||
systemctl daemon-reload
|
||||
systemctl enable $SERVICE_NAME
|
||||
systemctl restart $SERVICE_NAME
|
||||
|
||||
# 8. Health check
|
||||
echo "[8/8] Running health check..."
|
||||
# 9. Health check
|
||||
echo "[9/9] Running health check..."
|
||||
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
|
||||
if curl -sf http://localhost:80/ > /dev/null 2>&1; then
|
||||
echo "[OK] NextWorkspace is serving on http://localhost:80/"
|
||||
|
|
|
|||
26
go.mod
26
go.mod
|
|
@ -1,3 +1,27 @@
|
|||
module nextworkspace
|
||||
|
||||
go 1.22
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/caddyserver/certmagic v0.25.4
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
||||
github.com/miekg/dns v1.1.72 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
)
|
||||
|
|
|
|||
60
go.sum
Normal file
60
go.sum
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
github.com/caddyserver/certmagic v0.25.4 h1:8eIXh0HC3MsGnNo8One+BCxMGTbe5zb/oz+2KsxBFQg=
|
||||
github.com/caddyserver/certmagic v0.25.4/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
||||
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
||||
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
185
main.go
185
main.go
|
|
@ -1,22 +1,191 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
// --- 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)
|
||||
}
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello World from NextWorkspace!")
|
||||
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)
|
||||
})
|
||||
|
||||
log.Printf("NextWorkspace listening on :%s", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue