relay-server: tidy configs wip
Kim committed
Nov 18, 2025 at 19:05 UTC
897db660d2bffbe3aabfa2a2f13cfc607b766116
7 files changed
+99
-127
Dockerfile
+2
-2
@@ -25,8 +25,8 @@ FROM gcr.io/distroless/static-debian12:nonroot
25
26
COPY --from=builder /src/bin/relay-server /usr/bin/relay-server
27
28
-ENV PORTAL_UI_URL=http://localhost:4017
29
-ENV PORTAL_FRONTEND_URL=http://*.localhost:4017
28
+ENV PORTAL_URL=http://localhost:4017
29
+ENV PORTAL_SUBDOMAIN_URL=http://*.localhost:4017
30
ENV BOOTSTRAP_URIS=ws://localhost:4017/relay
31
32
EXPOSE 4017
README.md
+3
-3
@@ -48,9 +48,9 @@ http://localhost:4017
48
# A (wildcard) for *.example.com (or *.portal.example.com) → server IP
49
#
50
# Then edit docker-compose.yml environment for your domain:
51
-PORTAL_UI_URL: https://yourservice.com
52
-PORTAL_FRONTEND_URL: https://*.yourservice.com
53
-BOOTSTRAP_URIS: wss://yourservice.com/relay
51
+PORTAL_URL: https://portal.example.com
52
+PORTAL_SUBDOMAIN_URL: https://*.example.com
53
+BOOTSTRAP_URIS: wss://portal.example.com/relay
54
55
```
56
cmd/relay-server/main.go
+25
-62
@@ -18,53 +18,46 @@ import (
18
)
19
20
var (
21
- flagBootstraps []string
22
- flagALPN string
23
- flagPort int
24
- flagPortalHost string
25
- flagMaxLease int
26
- flagLeaseBPS int
21
+ flagPortalURL string
22
+ flagPortalSubdomainURL string
23
+ flagBootstraps []string
24
+ flagALPN string
25
+ flagPort int
26
+ flagMaxLease int
27
+ flagLeaseBPS int
28
+ rootHost string
29
)
30
31
func main() {
32
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
33
32
- // Parse PORTAL_UI_URL or PORTAL_FRONTEND_URL to extract portal host
33
- defaultPortalHost := os.Getenv("PORTAL_UI_URL")
34
- if defaultPortalHost == "" {
35
- defaultPortalHost = os.Getenv("PORTAL_FRONTEND_URL")
34
+ defaultPortalURL := strings.TrimSuffix(os.Getenv("PORTAL_URL"), "/")
35
+ if defaultPortalURL == "" {
36
+ defaultPortalURL = "localhost:4017"
37
}
37
- if defaultPortalHost != "" {
38
- // Extract host from URL (supports wildcard patterns like http://*.localhost:4017)
39
- defaultPortalHost = strings.TrimPrefix(defaultPortalHost, "http://")
40
- defaultPortalHost = strings.TrimPrefix(defaultPortalHost, "https://")
41
- defaultPortalHost = strings.TrimPrefix(defaultPortalHost, "*.")
42
- } else {
43
- defaultPortalHost = "localhost:4017"
38
+ defaultSubdomain := os.Getenv("PORTAL_SUBDOMAIN_URL")
39
+ if defaultSubdomain == "" {
40
+ defaultSubdomain = "*.localhost:4017"
41
}
42
defaultBootstraps := os.Getenv("BOOTSTRAP_URIS")
43
if defaultBootstraps == "" {
44
defaultBootstraps = "ws://localhost:4017/relay"
45
}
46
+
47
var flagBootstrapsCSV string
48
+ flag.StringVar(&flagPortalURL, "portal-url", defaultPortalURL, "base URL for portal frontend (env: PORTAL_URL)")
49
+ flag.StringVar(&flagPortalSubdomainURL, "portal-subdomain-url", defaultSubdomain, "subdomain wildcard URL (env: PORTAL_SUBDOMAIN_URL)")
50
flag.StringVar(&flagBootstrapsCSV, "bootstraps", defaultBootstraps, "bootstrap addresses (comma-separated)")
51
flag.StringVar(&flagALPN, "alpn", "http/1.1", "ALPN identifier for this service")
52
flag.IntVar(&flagPort, "port", 4017, "app UI and HTTP proxy port")
53
- flag.StringVar(&flagPortalHost, "portal-host", defaultPortalHost, "portal host for frontend serving (env: PORTAL_HOST)")
53
flag.IntVar(&flagMaxLease, "max-lease", 0, "maximum active relayed connections per lease (0 = unlimited)")
54
flag.IntVar(&flagLeaseBPS, "lease-bps", 0, "default bytes-per-second limit per lease (0 = unlimited)")
56
-
55
flag.Parse()
56
59
- // Parse bootstrap list
60
- parts := strings.Split(flagBootstrapsCSV, ",")
61
- flagBootstraps = make([]string, 0, len(parts))
62
- for _, p := range parts {
63
- s := strings.TrimSpace(p)
64
- if s != "" {
65
- flagBootstraps = append(flagBootstraps, s)
66
- }
67
- }
57
+ flagBootstraps = sdk.ParseURLs(flagBootstrapsCSV)
58
+ flagPortalURL = sdk.StripScheme(flagPortalURL)
59
+ flagPortalSubdomainURL = sdk.StripScheme(flagPortalSubdomainURL)
60
+ rootHost = sdk.StripPort(flagPortalURL)
61
62
if err := runServer(); err != nil {
63
log.Fatal().Err(err).Msg("execute root command")
@@ -75,41 +68,11 @@ func runServer() error {
68
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
69
defer stop()
70
78
- // Set portal host
79
- portalHost = flagPortalHost
80
-
81
- // Set portal UI URL from environment or construct from portal host
82
- portalUIURL = os.Getenv("PORTAL_UI_URL")
83
- if portalUIURL == "" {
84
- portalUIURL = os.Getenv("PORTAL_FRONTEND_URL")
85
- }
86
- if portalUIURL == "" {
87
- portalUIURL = "http://" + portalHost
88
- }
89
- // Trim trailing slashes
90
- portalUIURL = strings.TrimSuffix(portalUIURL, "/")
91
-
92
- // Set portal frontend pattern from PORTAL_FRONTEND_URL
93
- portalFrontendURL := os.Getenv("PORTAL_FRONTEND_URL")
94
- if portalFrontendURL != "" {
95
- // Extract host pattern from URL (e.g., http://*.localhost:4017 -> *.localhost:4017)
96
- portalFrontendURL = strings.TrimPrefix(portalFrontendURL, "http://")
97
- portalFrontendURL = strings.TrimPrefix(portalFrontendURL, "https://")
98
- portalFrontendPattern = portalFrontendURL
99
- }
100
-
101
- // Set bootstrap URIs from environment
102
- bootstrapURIs = os.Getenv("BOOTSTRAP_URIS")
103
- if bootstrapURIs == "" {
104
- // Use flagBootstraps as fallback
105
- bootstrapURIs = strings.Join(flagBootstraps, ",")
106
- }
107
-
71
log.Info().
109
- Str("portal_host", portalHost).
110
- Str("portal_ui_url", portalUIURL).
111
- Str("portal_frontend_pattern", portalFrontendPattern).
112
- Str("bootstrap_uris", bootstrapURIs).
72
+ Str("root_host", rootHost).
73
+ Str("frontend_base_url", flagPortalURL).
74
+ Str("subdomain_pattern", flagPortalSubdomainURL).
75
+ Str("bootstrap_uris", strings.Join(flagBootstraps, ",")).
76
Msg("[server] frontend configuration")
77
78
cred := sdk.NewCredential()
cmd/relay-server/view.go
+10
-25
@@ -6,13 +6,12 @@ import (
6
"encoding/json"
7
"fmt"
8
"net/http"
9
+ "path"
10
"strings"
11
"time"
12
13
"github.com/rs/zerolog/log"
14
14
- pathpkg "path"
15
-
15
"gosuda.org/portal/portal"
16
"gosuda.org/portal/sdk"
17
)
@@ -23,7 +22,7 @@ var distFS embed.FS
22
func serveAsset(mux *http.ServeMux, route, assetPath, contentType string) {
23
mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
24
// Read from dist/app subdirectory of the embedded FS
26
- fullPath := pathpkg.Join("dist", "app", assetPath)
25
+ fullPath := path.Join("dist", "app", assetPath)
26
b, err := distFS.ReadFile(fullPath)
27
if err != nil {
28
http.NotFound(w, r)
@@ -58,8 +57,8 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
57
w.WriteHeader(http.StatusOK)
58
return
59
}
61
- path := strings.TrimPrefix(r.URL.Path, "/app/")
62
- serveAppStatic(w, r, path, serv)
60
+ p := strings.TrimPrefix(r.URL.Path, "/app/")
61
+ serveAppStatic(w, r, p, serv)
62
})
63
64
// Portal frontend files (for unified caching)
@@ -69,15 +68,13 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
68
w.WriteHeader(http.StatusOK)
69
return
70
}
72
- path := strings.TrimPrefix(r.URL.Path, "/frontend/")
73
-
74
- // Special handling for manifest.json - generate dynamically
75
- if path == "manifest.json" {
71
+ p := strings.TrimPrefix(r.URL.Path, "/frontend/")
72
+ if p == "manifest.json" {
73
serveDynamicManifest(w)
74
return
75
}
76
80
- servePortalStaticFile(w, r, path)
77
+ servePortalStaticFile(w, r, p)
78
})
79
80
appMux.HandleFunc("/relay", func(w http.ResponseWriter, r *http.Request) {
@@ -102,8 +99,8 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
99
// App UI index page - serve React frontend with SSR (delegates to serveAppStatic)
100
appMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
101
// serveAppStatic handles both "/" and 404 fallback with SSR
105
- path := strings.TrimPrefix(r.URL.Path, "/")
106
- serveAppStatic(w, r, path, serv)
102
+ p := strings.TrimPrefix(r.URL.Path, "/")
103
+ serveAppStatic(w, r, p, serv)
104
})
105
106
appMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
@@ -245,19 +242,7 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
242
dnsLabel = dnsLabel[:8] + "..."
243
}
244
248
- // Use frontend pattern if available, otherwise fall back to portalHost
249
- var link string
250
- if portalFrontendPattern != "" {
251
- // For wildcard patterns like *.localhost:4017, replace * with lease name
252
- if strings.HasPrefix(portalFrontendPattern, "*.") {
253
- link = fmt.Sprintf("//%s%s", lease.Name, strings.TrimPrefix(portalFrontendPattern, "*"))
254
- } else {
255
- // For non-wildcard patterns, construct URL with lease name as subdomain
256
- link = fmt.Sprintf("//%s.%s/", lease.Name, portalFrontendPattern)
257
- }
258
- } else {
259
- link = fmt.Sprintf("//%s.%s/", lease.Name, portalHost)
260
- }
245
+ link := fmt.Sprintf("//%s.%s/", lease.Name, flagPortalURL)
246
247
row := leaseRow{
248
Peer: identityID,
cmd/relay-server/wasm.go
+15
-33
@@ -13,18 +13,6 @@ import (
13
"gosuda.org/portal/sdk"
14
)
15
16
-// portalHost is the host for portal frontend.
17
-var portalHost = "localhost"
18
-
19
-// portalUIURL is the base URL for portal frontend
20
-var portalUIURL = "http://localhost:4017"
21
-
22
-// portalFrontendPattern is the wildcard pattern for portal frontend URLs (e.g., *.localhost:4017)
23
-var portalFrontendPattern = ""
24
-
25
-// bootstrapURIs stores the relay bootstrap server URIs
26
-var bootstrapURIs = "ws://localhost:4017/relay"
27
-
16
// wasmCache stores pre-loaded WASM files in memory (optional)
17
type wasmCacheEntry struct {
18
brotli []byte
@@ -337,9 +325,8 @@ func servePortalStatic(w http.ResponseWriter, r *http.Request) {
325
// Special handling for specific files
326
switch path {
327
case "manifest.json":
340
- w.Header().Set("Cache-Control", "no-cache, must-revalidate")
341
- w.Header().Set("Content-Type", "application/json")
342
- serveStaticFileWithFallback(w, r, path, "application/json")
328
+ // Serve dynamic manifest regardless of static presence
329
+ serveDynamicManifest(w)
330
return
331
332
case "service-worker.js":
@@ -431,30 +418,25 @@ func serveStaticFileWithFallback(w http.ResponseWriter, r *http.Request, path st
418
w.Write(data)
419
}
420
434
-// getContentType returns the MIME type for a file extension
435
-// content types are provided via sdk.GetContentType
436
-
421
// isPortalSubdomain checks if the host matches the portal frontend pattern
422
func isPortalSubdomain(host string) bool {
439
- // If we have a frontend pattern, use it
440
- if portalFrontendPattern != "" {
441
- return sdk.MatchesWildcardPattern(host, portalFrontendPattern)
423
+ // If we have a frontend pattern (already normalized in main), use it
424
+ if flagPortalSubdomainURL != "" {
425
+ p := flagPortalSubdomainURL
426
+ if strings.HasPrefix(p, "*.") {
427
+ return strings.HasSuffix(host, strings.TrimPrefix(p, "*"))
428
+ }
429
+ return host == p
430
}
431
444
- // Fallback to checking if it ends with .{portalHost}
445
- if portalHost == "" {
432
+ // Fallback to checking if it ends with .{rootHost}
433
+ if rootHost == "" {
434
return false
435
}
436
449
- return strings.HasSuffix(host, "."+portalHost)
437
+ return strings.HasSuffix(sdk.StripPort(host), "."+rootHost)
438
}
439
452
-// matchesWildcardPattern checks if a host matches a wildcard pattern (e.g., *.localhost:4017)
453
-// wildcard matching is provided via sdk.MatchesWildcardPattern
454
-
455
-// isHexString checks if a string contains only hexadecimal characters
456
-// hex string check is provided via sdk.IsHexString
457
-
440
// serveDynamicManifest generates and serves manifest.json dynamically
441
func serveDynamicManifest(w http.ResponseWriter) {
442
sdk.SetCORSHeaders(w)
@@ -493,14 +475,14 @@ func serveDynamicManifest(w http.ResponseWriter) {
475
}
476
477
// Generate WASM URL
496
- wasmURL := portalUIURL + "/frontend/" + wasmFile
478
+ wasmURL := flagPortalURL + "/frontend/" + wasmFile
479
480
// Create manifest structure
481
manifest := map[string]string{
482
"wasmFile": wasmFile,
483
"wasmUrl": wasmURL,
484
"hash": wasmHash,
503
- "bootstraps": bootstrapURIs,
485
+ "bootstraps": strings.Join(flagBootstraps, ","),
486
}
487
488
// Set headers for no caching
@@ -519,7 +501,7 @@ func serveDynamicManifest(w http.ResponseWriter) {
501
Str("wasmFile", wasmFile).
502
Str("wasmUrl", wasmURL).
503
Str("hash", wasmHash).
522
- Str("bootstraps", bootstrapURIs).
504
+ Str("bootstraps", strings.Join(flagBootstraps, ",")).
505
Msg("Served dynamic manifest")
506
}
507
docker-compose.yml
+2
-2
@@ -8,8 +8,8 @@ services:
8
- "--port"
9
- "${PORTAL_PORT:-4017}"
10
environment:
11
- PORTAL_UI_URL: ${PORTAL_UI_URL:-http://localhost:${PORTAL_PORT:-4017}}
12
- PORTAL_FRONTEND_URL: ${PORTAL_FRONTEND_URL:-http://*.localhost:${PORTAL_PORT:-4017}}
11
+ PORTAL_URL: ${PORTAL_URL:-http://localhost:${PORTAL_PORT:-4017}}
12
+ PORTAL_SUBDOMAIN_URL: ${PORTAL_SUBDOMAIN_URL:-http://*.localhost:${PORTAL_PORT:-4017}}
13
BOOTSTRAP_URIS: ${BOOTSTRAP_URIS:-ws://localhost:${PORTAL_PORT:-4017}/relay}
14
ports:
15
- "4017:4017"
sdk/utils.go
+42
@@ -202,3 +202,45 @@ func SetCORSHeaders(w http.ResponseWriter) {
202
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
203
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Encoding")
204
}
205
+
206
+func StripScheme(s string) string {
207
+ s = strings.TrimSpace(s)
208
+ if s == "" {
209
+ return s
210
+ }
211
+ s = strings.TrimPrefix(s, "http://")
212
+ s = strings.TrimPrefix(s, "https://")
213
+
214
+ return s
215
+}
216
+
217
+// TrimAfterFirstSlash returns s up to but not including the first '/'.
218
+func TrimAfterFirstSlash(s string) string {
219
+ if s == "" {
220
+ return s
221
+ }
222
+ if idx := strings.IndexByte(s, '/'); idx >= 0 {
223
+ return s[:idx]
224
+ }
225
+ return s
226
+}
227
+
228
+func StripPort(s string) string {
229
+ if s == "" {
230
+ return s
231
+ }
232
+ if idx := strings.LastIndexByte(s, ':'); idx >= 0 && idx+1 < len(s) {
233
+ port := s[idx+1:]
234
+ digits := true
235
+ for _, ch := range port {
236
+ if ch < '0' || ch > '9' {
237
+ digits = false
238
+ break
239
+ }
240
+ }
241
+ if digits {
242
+ return s[:idx]
243
+ }
244
+ }
245
+ return s
246
+}