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")