relay-server: use utils in sdk
Kim committed
Nov 18, 2025 at 18:15 UTC
e0d239c721f7844df3981b3d58ae2106e062a0c6
2 files changed
+31
-86
cmd/relay-server/view.go
+3
-13
@@ -9,13 +9,11 @@ import (
9
"strings"
10
"time"
11
12
- "github.com/gorilla/websocket"
12
"github.com/rs/zerolog/log"
13
14
pathpkg "path"
15
16
"gosuda.org/portal/portal"
18
- "gosuda.org/portal/portal/utils/wsstream"
17
"gosuda.org/portal/sdk"
18
)
19
@@ -55,7 +53,7 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
53
54
// Portal app assets (JS, CSS, etc.) - served from /app/
55
appMux.HandleFunc("/app/", func(w http.ResponseWriter, r *http.Request) {
58
- setCORSHeaders(w)
56
+ sdk.SetCORSHeaders(w)
57
if r.Method == http.MethodOptions {
58
w.WriteHeader(http.StatusOK)
59
return
@@ -66,7 +64,7 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
64
65
// Portal frontend files (for unified caching)
66
appMux.HandleFunc("/frontend/", func(w http.ResponseWriter, r *http.Request) {
69
- setCORSHeaders(w)
67
+ sdk.SetCORSHeaders(w)
68
if r.Method == http.MethodOptions {
69
w.WriteHeader(http.StatusOK)
70
return
@@ -89,13 +87,11 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
87
return
88
}
89
92
- wsConn, err := wsUpgrader.Upgrade(w, r, nil)
90
+ stream, wsConn, err := sdk.UpgradeToWSStream(w, r, nil)
91
if err != nil {
92
log.Error().Err(err).Msg("[server] websocket upgrade failed")
93
return
94
}
97
-
98
- stream := &wsstream.WsStream{Conn: wsConn}
95
if err := serv.HandleConnection(stream); err != nil {
96
log.Error().Err(err).Msg("[server] websocket relay connection error")
97
wsConn.Close()
@@ -285,9 +281,3 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
281
282
return rows
283
}
288
-
289
-var wsUpgrader = websocket.Upgrader{
290
- CheckOrigin: func(r *http.Request) bool {
291
- return true
292
- },
293
-}
cmd/relay-server/wasm.go
+28
-73
@@ -10,6 +10,7 @@ import (
10
11
"github.com/rs/zerolog/log"
12
"gosuda.org/portal/portal"
13
+ "gosuda.org/portal/sdk"
14
)
15
16
// portalHost is the host for portal frontend.
@@ -49,10 +50,10 @@ func initWasmCache() error {
50
}
51
52
name := entry.Name()
52
- // Look for content-addressed WASM files: <64-char-hex>.wasm.br
53
- if strings.HasSuffix(name, ".wasm.br") && len(name) == 72 { // 64 + len(".wasm.br")
53
+ // Look for content-addressed WASM files: <hex>.wasm.br
54
+ if strings.HasSuffix(name, ".wasm.br") {
55
hash := strings.TrimSuffix(name, ".wasm.br")
55
- if isHexString(hash) && len(hash) == 64 {
56
+ if sdk.IsHexString(hash) {
57
fullPath := pathpkg.Join("dist", "wasm", name)
58
// Cache under the URL path (<hash>.wasm) while reading the
59
// brotli-compressed artifact (<hash>.wasm.br) from embed.FS.
@@ -71,9 +72,9 @@ func initWasmCache() error {
72
73
// cacheWasmFile reads and caches a WASM file and its pre-compressed variant (brotli).
74
func cacheWasmFile(name, fullPath string) error {
74
- // Verify SHA256 hash matches filename (name is <hash>.wasm).
75
+ // Verify name looks like a hex hash (name is <hash>.wasm).
76
hashHex := strings.TrimSuffix(name, ".wasm")
76
- if !isHexString(hashHex) || len(hashHex) != 64 {
77
+ if !sdk.IsHexString(hashHex) {
78
log.Warn().Str("file", name).Msg("WASM file name is not a valid SHA256 hex string")
79
}
80
@@ -103,13 +104,6 @@ func cacheWasmFile(name, fullPath string) error {
104
return nil
105
}
106
106
-// setCORSHeaders sets CORS headers for static file serving
107
-func setCORSHeaders(w http.ResponseWriter) {
108
- w.Header().Set("Access-Control-Allow-Origin", "*")
109
- w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
110
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Encoding")
111
-}
112
-
107
// createPortalMux creates a new HTTP mux for portal frontend
108
func createPortalMux() *http.ServeMux {
109
// Initialize WASM cache on startup
@@ -121,7 +115,7 @@ func createPortalMux() *http.ServeMux {
115
116
// Static file handler for /frontend/ (for unified caching)
117
mux.HandleFunc("/frontend/", func(w http.ResponseWriter, r *http.Request) {
124
- setCORSHeaders(w)
118
+ sdk.SetCORSHeaders(w)
119
if r.Method == http.MethodOptions {
120
w.WriteHeader(http.StatusOK)
121
return
@@ -139,7 +133,7 @@ func createPortalMux() *http.ServeMux {
133
134
// Root handler for portal frontend
135
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
142
- setCORSHeaders(w)
136
+ sdk.SetCORSHeaders(w)
137
if r.Method == http.MethodOptions {
138
w.WriteHeader(http.StatusOK)
139
return
@@ -158,7 +152,7 @@ func createPortalMux() *http.ServeMux {
152
153
// servePortalHTMLWithSSR serves portal.html with SSR data injection
154
func servePortalHTMLWithSSR(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer) {
161
- setCORSHeaders(w)
155
+ sdk.SetCORSHeaders(w)
156
157
// Read portal.html from embedded FS
158
fullPath := pathpkg.Join("dist", "app", "portal.html")
@@ -212,9 +206,9 @@ func injectServerData(htmlContent string, serv *portal.RelayServer) string {
206
// servePortalStaticFile serves static files for portal frontend with caching
207
func servePortalStaticFile(w http.ResponseWriter, r *http.Request, filePath string) {
208
// Check if this is a content-addressed WASM file
215
- if strings.HasSuffix(filePath, ".wasm") && len(filePath) == 69 { // 64 + len(".wasm")
209
+ if strings.HasSuffix(filePath, ".wasm") {
210
hash := strings.TrimSuffix(filePath, ".wasm")
217
- if isHexString(hash) && len(hash) == 64 {
211
+ if sdk.IsHexString(hash) {
212
serveCompressedWasm(w, r, filePath)
213
return
214
}
@@ -243,7 +237,7 @@ func serveCompressedWasm(w http.ResponseWriter, r *http.Request, filePath string
237
}
238
239
// Serve uncompressed WASM
246
- setCORSHeaders(w)
240
+ sdk.SetCORSHeaders(w)
241
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
242
w.Header().Set("Content-Type", "application/wasm")
243
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
@@ -257,7 +251,7 @@ func serveCompressedWasm(w http.ResponseWriter, r *http.Request, filePath string
251
}
252
253
// Set immutable cache headers for content-addressed files
260
- setCORSHeaders(w)
254
+ sdk.SetCORSHeaders(w)
255
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
256
w.Header().Set("Content-Type", "application/wasm")
257
@@ -294,7 +288,7 @@ func serveAppStatic(w http.ResponseWriter, r *http.Request, path string, serv *p
288
return
289
}
290
297
- setCORSHeaders(w)
291
+ sdk.SetCORSHeaders(w)
292
293
// If path is empty or "/", serve portal.html with SSR
294
if path == "" || path == "/" {
@@ -314,7 +308,7 @@ func serveAppStatic(w http.ResponseWriter, r *http.Request, path string, serv *p
308
309
// Set content type based on extension
310
ext := pathpkg.Ext(path)
317
- contentType := getContentType(ext)
311
+ contentType := sdk.GetContentType(ext)
312
if contentType != "" {
313
w.Header().Set("Content-Type", contentType)
314
}
@@ -372,7 +366,7 @@ func servePortalStatic(w http.ResponseWriter, r *http.Request) {
366
367
// serveStaticFile reads and serves a file from the static directory
368
func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, contentType string) {
375
- setCORSHeaders(w)
369
+ sdk.SetCORSHeaders(w)
370
371
fullPath := pathpkg.Join("dist", "wasm", path)
372
data, err := distFS.ReadFile(fullPath)
@@ -387,7 +381,7 @@ func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, conten
381
w.Header().Set("Content-Type", contentType)
382
} else {
383
ext := pathpkg.Ext(path)
390
- ct := getContentType(ext)
384
+ ct := sdk.GetContentType(ext)
385
if ct != "" {
386
w.Header().Set("Content-Type", ct)
387
}
@@ -405,7 +399,7 @@ func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, conten
399
// serveStaticFileWithFallback reads and serves a file from the static directory
400
// If the file is not found, it falls back to portal.html for SPA routing
401
func serveStaticFileWithFallback(w http.ResponseWriter, r *http.Request, path string, contentType string) {
408
- setCORSHeaders(w)
402
+ sdk.SetCORSHeaders(w)
403
404
fullPath := pathpkg.Join("dist", "wasm", path)
405
data, err := distFS.ReadFile(fullPath)
@@ -422,7 +416,7 @@ func serveStaticFileWithFallback(w http.ResponseWriter, r *http.Request, path st
416
w.Header().Set("Content-Type", contentType)
417
} else {
418
ext := pathpkg.Ext(path)
425
- ct := getContentType(ext)
419
+ ct := sdk.GetContentType(ext)
420
if ct != "" {
421
w.Header().Set("Content-Type", ct)
422
}
@@ -438,36 +432,13 @@ func serveStaticFileWithFallback(w http.ResponseWriter, r *http.Request, path st
432
}
433
434
// getContentType returns the MIME type for a file extension
441
-func getContentType(ext string) string {
442
- switch ext {
443
- case ".html":
444
- return "text/html; charset=utf-8"
445
- case ".js":
446
- return "application/javascript"
447
- case ".json":
448
- return "application/json"
449
- case ".wasm":
450
- return "application/wasm"
451
- case ".css":
452
- return "text/css"
453
- case ".mp4":
454
- return "video/mp4"
455
- case ".svg":
456
- return "image/svg+xml"
457
- case ".png":
458
- return "image/png"
459
- case ".ico":
460
- return "image/x-icon"
461
- default:
462
- return ""
463
- }
464
-}
435
+// content types are provided via sdk.GetContentType
436
437
// isPortalSubdomain checks if the host matches the portal frontend pattern
438
func isPortalSubdomain(host string) bool {
439
// If we have a frontend pattern, use it
440
if portalFrontendPattern != "" {
470
- return matchesWildcardPattern(host, portalFrontendPattern)
441
+ return sdk.MatchesWildcardPattern(host, portalFrontendPattern)
442
}
443
444
// Fallback to checking if it ends with .{portalHost}
@@ -479,30 +450,14 @@ func isPortalSubdomain(host string) bool {
450
}
451
452
// matchesWildcardPattern checks if a host matches a wildcard pattern (e.g., *.localhost:4017)
482
-func matchesWildcardPattern(host, pattern string) bool {
483
- // Handle wildcard pattern (e.g., *.localhost:4017)
484
- if strings.HasPrefix(pattern, "*.") {
485
- suffix := strings.TrimPrefix(pattern, "*")
486
- return strings.HasSuffix(host, suffix)
487
- }
488
-
489
- // Exact match
490
- return host == pattern
491
-}
453
+// wildcard matching is provided via sdk.MatchesWildcardPattern
454
455
// isHexString checks if a string contains only hexadecimal characters
494
-func isHexString(s string) bool {
495
- for _, c := range s {
496
- if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
497
- return false
498
- }
499
- }
500
- return true
501
-}
456
+// hex string check is provided via sdk.IsHexString
457
458
// serveDynamicManifest generates and serves manifest.json dynamically
459
func serveDynamicManifest(w http.ResponseWriter) {
505
- setCORSHeaders(w)
460
+ sdk.SetCORSHeaders(w)
461
462
// Find the content-addressed WASM file
463
wasmCacheMu.RLock()
@@ -524,10 +479,10 @@ func serveDynamicManifest(w http.ResponseWriter) {
479
continue
480
}
481
name := entry.Name()
527
- // Look for content-addressed WASM files: <64-char-hex>.wasm.br
528
- if strings.HasSuffix(name, ".wasm.br") && len(name) == 72 {
482
+ // Look for content-addressed WASM files: <hex>.wasm.br
483
+ if strings.HasSuffix(name, ".wasm.br") {
484
hash := strings.TrimSuffix(name, ".wasm.br")
530
- if isHexString(hash) && len(hash) == 64 {
485
+ if sdk.IsHexString(hash) {
486
wasmHash = hash
487
wasmFile = hash + ".wasm"
488
break
@@ -570,7 +525,7 @@ func serveDynamicManifest(w http.ResponseWriter) {
525
526
// serveDynamicServiceWorker serves service-worker.js with injected manifest and config
527
func serveDynamicServiceWorker(w http.ResponseWriter, r *http.Request) {
573
- setCORSHeaders(w)
528
+ sdk.SetCORSHeaders(w)
529
530
// Read the service-worker.js template
531
fullPath := pathpkg.Join("dist", "wasm", "service-worker.js")