change default portal domain to localhost for local development
Kim committed
Nov 6, 2025 at 18:46 UTC
a04eab5d1b649cd3ff7d69b20052daa837107026
7 files changed
+63
-24
Dockerfile
+3
-2
@@ -14,7 +14,8 @@ RUN go mod download
14
COPY . .
15
16
# Build WASM and server
17
-RUN make build-wasm
17
+ARG BOOTSTRAPS=""
18
+RUN make build-wasm BOOTSTRAPS="$BOOTSTRAPS"
19
20
# Build server
21
RUN make build-server
@@ -29,7 +30,7 @@ COPY --from=builder /src/dist /app/dist
30
31
# Set default environment variables
32
ENV STATIC_DIR=/app/dist
32
-ENV PORTAL_DOMAIN=portal.gosuda.org
33
+ENV PORTAL_DOMAIN=localhost
34
35
# Expose ports
36
# 4017: relay server and portal frontend
Makefile
+16
-7
@@ -1,9 +1,12 @@
1
SHELL := /bin/sh
2
3
-.PHONY: build build-wasm build-server clean
3
+.PHONY: run build build-wasm build-server clean
4
+
5
+run:
6
+ ./bin/relay-server
7
8
# Convenience target: build wasm then server
6
-build: build-protoc build-wasm build-server
9
+build: build-protoc build-wasm build-server build-tunnel
10
11
build-protoc:
12
protoc -I . \
@@ -14,12 +17,20 @@ build-protoc:
17
portal/core/proto/rdsec/rdsec.proto \
18
portal/core/proto/rdverb/rdverb.proto
19
20
+BOOTSTRAPS ?= ""
21
+
22
# Build WASM artifacts with wasm-opt optimization and generate manifest
23
build-wasm:
24
@echo "[wasm] building webclient WASM..."
25
@mkdir -p dist
21
-
22
- GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" -o dist/portal.wasm ./cmd/webclient
26
+
27
+ # Prepare optional link flags for bootstrap injection
28
+ @WASM_LDFLAGS=""; \
29
+ if [ -n "$(BOOTSTRAPS)" ]; then \
30
+ WASM_LDFLAGS="-X main.bootstrapServersCSV=$(BOOTSTRAPS)"; \
31
+ echo "[wasm] injecting bootstraps: $(BOOTSTRAPS)"; \
32
+ fi; \
33
+ GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w $$WASM_LDFLAGS" -o dist/portal.wasm ./cmd/webclient
34
35
@echo "[wasm] optimizing with wasm-opt..."
36
@if command -v wasm-opt >/dev/null 2>&1; then \
@@ -58,6 +69,4 @@ build-tunnel:
69
70
clean:
71
rm -rf bin
61
- rm -rf dist
62
- rm -rf sdk/wasm
63
- rm -rf portal/wasm/pkg
72
+ rm -rf dist
\ No newline at end of file
cmd/relay-server/frontend.go
+16
-4
@@ -20,8 +20,8 @@ import (
20
// staticDir is the directory where static files are located
21
var staticDir = "./dist"
22
23
-// portalDomain is the domain for portal frontend (e.g., "portal.gosuda.org")
24
-var portalDomain = "portal.gosuda.org"
23
+// portalDomain is the domain for portal frontend.
24
+var portalDomain = "localhost"
25
26
// wasmCache stores pre-compressed WASM files in memory
27
type wasmCacheEntry struct {
@@ -419,8 +419,20 @@ func getContentType(ext string) string {
419
420
// isPortalSubdomain checks if the host is a portal subdomain (*.{portalDomain})
421
func isPortalSubdomain(host string) bool {
422
- // Check if it ends with .{portalDomain} or is exactly {portalDomain}
423
- return strings.HasSuffix(host, "."+portalDomain)
422
+ if portalDomain == "" {
423
+ return false
424
+ }
425
+ // Trim port from request host
426
+ if i := strings.Index(host, ":"); i >= 0 {
427
+ host = host[:i]
428
+ }
429
+ // Allow optional ":port" in portalDomain for local dev
430
+ base := portalDomain
431
+ if j := strings.Index(base, ":"); j >= 0 {
432
+ base = base[:j]
433
+ }
434
+ // Check if it ends with .{base}
435
+ return strings.HasSuffix(host, "."+base)
436
}
437
438
// isHexString checks if a string contains only hexadecimal characters
cmd/relay-server/main.go
+1
-1
@@ -35,7 +35,7 @@ func main() {
35
}
36
defaultPortalDomain := os.Getenv("PORTAL_DOMAIN")
37
if defaultPortalDomain == "" {
38
- defaultPortalDomain = "portal.gosuda.org"
38
+ defaultPortalDomain = "localhost"
39
}
40
var flagBootstrapsCSV string
41
flag.StringVar(&flagBootstrapsCSV, "bootstraps", "ws://localhost:4017/relay", "bootstrap addresses (comma-separated)")
cmd/relay-server/static/index.html
+15
@@ -90,6 +90,21 @@
90
});
91
} catch (_) { }
92
})();
93
+ (function () {
94
+ try {
95
+ var port = window.location.port;
96
+ if (!port) return; // default ports (80/443)
97
+ document.querySelectorAll('a.card[href^="//"]').forEach(function (a) {
98
+ try {
99
+ var u = new URL(a.href);
100
+ if (!u.port) { // only if not already set
101
+ u.port = port;
102
+ a.href = u.toString();
103
+ }
104
+ } catch (_) { }
105
+ });
106
+ } catch (_) { }
107
+ })();
108
</script>
109
</body>
110
cmd/webclient/main_js.go
+3
-2
@@ -28,7 +28,7 @@ import (
28
)
29
30
var (
31
- bootstrapServers = []string{"wss://portal.gosuda.org/relay"}
31
+ bootstrapServers string = "ws://localhost:4017/relay"
32
rdClient *sdk.RDClient
33
)
34
@@ -538,9 +538,10 @@ func (p *Proxy) handleDisconnect(w http.ResponseWriter, r *http.Request, connID
538
func main() {
539
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
540
var err error
541
+ var bootstrapServerList = strings.Split(bootstrapServers, ",")
542
543
rdClient, err = sdk.NewClient(
543
- sdk.WithBootstrapServers(bootstrapServers),
544
+ sdk.WithBootstrapServers(bootstrapServerList),
545
sdk.WithDialer(WebSocketDialerJS()),
546
)
547
if err != nil {
cmd/webclient/service-worker.js
+9
-8
@@ -1,6 +1,7 @@
1
+const BASE_PATH = new URL('./', self.location).pathname;
2
// const wasm_exec_URL = "https://cdn.jsdelivr.net/gh/golang/go@go1.25.3/lib/wasm/wasm_exec.js";
2
-const wasm_exec_URL = "/wasm_exec.js";
3
-const manifest_URL = "/manifest.json";
3
+const wasm_exec_URL = BASE_PATH + "wasm_exec.js";
4
+const manifest_URL = BASE_PATH + "manifest.json";
5
6
importScripts(wasm_exec_URL);
7
@@ -68,8 +69,8 @@ async function runWASM() {
69
70
try {
71
const manifest = await fetchManifest();
71
- // Use content-addressed path: /static/<sha256>.wasm
72
- const wasm_URL = `/static/${manifest.wasmFile}`;
72
+ // Use content-addressed path under scope: <BASE_PATH>/static/<sha256>.wasm
73
+ const wasm_URL = `${BASE_PATH}static/${manifest.wasmFile}`;
74
75
const go = new Go();
76
@@ -204,7 +205,7 @@ self.addEventListener("fetch", (e) => {
205
}
206
207
// Serve portal.mp4 from cache or fetch from origin
207
- if (url.pathname === "/portal.mp4") {
208
+ if (url.pathname === BASE_PATH + "portal.mp4") {
209
console.log("[SW] Fetching portal.mp4");
210
e.respondWith(
211
(async () => {
@@ -212,7 +213,7 @@ self.addEventListener("fetch", (e) => {
213
await fetchManifest();
214
// Try to get from cache first
215
const cache = await caches.open(currentCacheVersion);
215
- const cachedResponse = await cache.match("/portal.mp4");
216
+ const cachedResponse = await cache.match(BASE_PATH + "portal.mp4");
217
if (cachedResponse) {
218
console.log("[SW] Found portal.mp4 in cache");
219
return cachedResponse;
@@ -222,7 +223,7 @@ self.addEventListener("fetch", (e) => {
223
const response = await fetch(e.request);
224
if (response.ok) {
225
console.log("[SW] Caching portal.mp4");
225
- cache.put("/portal.mp4", response.clone());
226
+ cache.put(BASE_PATH + "portal.mp4", response.clone());
227
}
228
return response;
229
} catch (error) {
@@ -242,7 +243,7 @@ self.addEventListener("fetch", (e) => {
243
// send auto refresh page
244
e.respondWith(
245
new Response(
245
- "<html><head><meta http-equiv='refresh' content='0'></head><body><h1>Sorry, Service Worker failed to process the request. Please refresh the page.</h1></body></html>",
246
+ "<html><head><meta http-equiv='refresh' content='0'></head><body>Service Worker failed to process the request. Please refresh the page.</body></html>",
247
{ status: 500, headers: { "Content-Type": "text/html" } }
248
)
249
);