fix: force refresh connect, support native language url.
Hee Sung Son committed
Nov 6, 2025 at 21:47 UTC
c9cf7f60931a337c825ba69dcdb0e7104c2612e9
4 files changed
+156
-53
Makefile
+2
@@ -45,6 +45,8 @@ build-wasm:
45
@echo "[wasm] calculating SHA256 hash..."
46
@WASM_HASH=$$(shasum -a 256 dist/portal.wasm | awk '{print $$1}'); \
47
echo "[wasm] SHA256: $$WASM_HASH"; \
48
+ echo "[wasm] cleaning old hash files..."; \
49
+ find dist -name '[0-9a-f]*.wasm' ! -name "$$WASM_HASH.wasm" -type f -delete 2>/dev/null || true; \
50
cp dist/portal.wasm dist/$$WASM_HASH.wasm; \
51
echo "{\"wasmFile\":\"$$WASM_HASH.wasm\",\"hash\":\"$$WASM_HASH\"}" > dist/manifest.json; \
52
echo "[wasm] manifest created"
cmd/webclient/index.html
+55
-20
@@ -320,21 +320,30 @@
320
321
const waitForController = () => {
322
return new Promise((resolve, reject) => {
323
- if (navigator.serviceWorker.controller) {
324
- console.log("[Portal] Service Worker already active");
325
- resolve();
323
+ const checkController = () => {
324
+ if (navigator.serviceWorker.controller) {
325
+ resolve();
326
+ return true;
327
+ }
328
+ return false;
329
+ };
330
+
331
+ if (checkController()) {
332
return;
333
}
334
335
let timeoutId;
336
+ let pollIntervalId;
337
+
338
const onControllerChange = () => {
331
- console.log("[Portal] Service Worker activated");
332
- clearTimeout(timeoutId);
333
- navigator.serviceWorker.removeEventListener(
334
- "controllerchange",
335
- onControllerChange
336
- );
337
- resolve();
339
+ if (checkController()) {
340
+ clearTimeout(timeoutId);
341
+ clearInterval(pollIntervalId);
342
+ navigator.serviceWorker.removeEventListener(
343
+ "controllerchange",
344
+ onControllerChange
345
+ );
346
+ }
347
};
348
349
navigator.serviceWorker.addEventListener(
@@ -342,15 +351,32 @@
351
onControllerChange
352
);
353
354
+ pollIntervalId = setInterval(() => {
355
+ if (checkController()) {
356
+ clearTimeout(timeoutId);
357
+ clearInterval(pollIntervalId);
358
+ navigator.serviceWorker.removeEventListener(
359
+ "controllerchange",
360
+ onControllerChange
361
+ );
362
+ }
363
+ }, 100);
364
+
365
timeoutId = setTimeout(() => {
366
+ clearInterval(pollIntervalId);
367
navigator.serviceWorker.removeEventListener(
368
"controllerchange",
369
onControllerChange
370
);
350
- reject(new Error("Service Worker activation timeout"));
351
- setTimeout(() => {
352
- location.reload();
353
- }, 500);
371
+
372
+ if (navigator.serviceWorker.controller) {
373
+ resolve();
374
+ } else {
375
+ reject(new Error("Service Worker activation timeout"));
376
+ setTimeout(() => {
377
+ location.reload();
378
+ }, 500);
379
+ }
380
}, 3000);
381
});
382
};
@@ -366,22 +392,31 @@
392
"/service-worker.js",
393
{
394
scope: "/",
395
+ updateViaCache: "none",
396
}
397
);
398
372
- console.log("[Portal] Service Worker registered");
399
+ if (!navigator.serviceWorker.controller && registration.active) {
400
+ registration.active.postMessage({ type: "CLAIM_CLIENTS" });
401
+ }
402
403
updateLoadingText("Activating Service Worker...");
404
405
await navigator.serviceWorker.ready;
377
- console.log("[Portal] Service Worker ready");
378
-
379
- updateLoadingText("Waiting for activation...");
406
381
- await waitForController();
407
+ if (!navigator.serviceWorker.controller) {
408
+ updateLoadingText("Waiting for activation...");
409
+ try {
410
+ await Promise.race([
411
+ waitForController(),
412
+ new Promise((resolve) => setTimeout(resolve, 500))
413
+ ]);
414
+ } catch (error) {
415
+ // Ignore timeout, proceed anyway
416
+ }
417
+ }
418
419
updateLoadingText("Connecting to Portal Network...");
384
- console.log("[Portal] Checking WASM initialization...");
420
421
setTimeout(checkWASMReady, 100);
422
} catch (error) {
cmd/webclient/main_js.go
+45
-7
@@ -28,23 +28,48 @@ import (
28
)
29
30
var (
31
- bootstrapServers string = "ws://localhost:4017/relay"
32
- rdClient *sdk.RDClient
31
+ // Default bootstrap server, can be overridden at build time with -ldflags
32
+ // Example: make build-wasm BOOTSTRAPS=wss://portal.gosuda.org/relay
33
+ bootstrapServersCSV string = "ws://localhost:4017/relay"
34
+ rdClient *sdk.RDClient
35
)
36
37
var rdDialer = func(ctx context.Context, network, address string) (net.Conn, error) {
38
+ originalAddr := address
39
address = strings.TrimSuffix(address, ":80")
40
address = strings.TrimSuffix(address, ":443")
41
42
+ // Decode URL-encoded address (e.g., %ED%8E%98%EC%9D%B8%ED%8A%B8 -> 페인트)
43
+ decodedAddr, err := url.QueryUnescape(address)
44
+ if err != nil {
45
+ log.Debug().Err(err).Str("address", address).Msg("[Dialer] Failed to unescape address")
46
+ decodedAddr = address
47
+ }
48
+ address = decodedAddr
49
+
50
+ // Convert Punycode to Unicode (e.g., xn--lu5bu9rfta -> 페인트)
51
+ unicodeAddr, err := idna.ToUnicode(address)
52
+ if err != nil {
53
+ log.Debug().Err(err).Str("address", address).Msg("[Dialer] Failed to convert punycode")
54
+ unicodeAddr = address
55
+ } else if unicodeAddr != address {
56
+ log.Debug().Str("punycode", address).Str("unicode", unicodeAddr).Msg("[Dialer] Converted punycode to unicode")
57
+ }
58
+ address = unicodeAddr
59
+
60
lease, err := rdClient.LookupName(address)
61
if err == nil && lease != nil {
62
+ log.Debug().Str("name", address).Str("id", lease.Identity.Id).Msg("[Dialer] Found lease")
63
address = lease.Identity.Id
42
- log.Debug().Str("name", address).Str("id", lease.Identity.Id).Msg("[SDK] Found lease")
64
+ } else {
65
+ log.Debug().Err(err).Str("name", address).Msg("[Dialer] Lease lookup failed")
66
}
67
68
+ log.Debug().Str("original_addr", originalAddr).Str("final_addr", address).Msg("[Dialer] Attempting dial")
69
cred := sdk.NewCredential()
70
conn, err := rdClient.Dial(cred, address, "http/1.1")
71
if err != nil {
72
+ log.Error().Err(err).Str("address", address).Msg("[Dialer] Dial failed")
73
return nil, err
74
}
75
return conn, nil
@@ -301,10 +326,18 @@ func IsHTMLContentType(contentType string) bool {
326
}
327
328
func getLeaseID(hostname string) string {
304
- host, err := idna.ToUnicode(hostname)
329
+ // First, decode URL-encoded characters (e.g., %ED%8E%98%EC%9D%B8%ED%8A%B8 -> 페인트)
330
+ decoded, err := url.QueryUnescape(hostname)
331
if err != nil {
306
- host = hostname
332
+ decoded = hostname
333
}
334
+
335
+ // Then, convert punycode to unicode (e.g., xn--v9jub -> 日本語)
336
+ host, err := idna.ToUnicode(decoded)
337
+ if err != nil {
338
+ host = decoded
339
+ }
340
+
341
id := strings.Split(host, ".")[0]
342
id = strings.TrimSpace(id)
343
id = strings.ToUpper(id)
@@ -321,7 +354,10 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
354
log.Info().Msgf("Proxying request to %s", r.URL.String())
355
356
r = r.Clone(context.Background())
324
- r.URL.Host = getLeaseID(r.URL.Hostname())
357
+
358
+ // Decode hostname properly for Korean/multi-language domains
359
+ decodedHost := getLeaseID(r.URL.Hostname())
360
+ r.URL.Host = decodedHost
361
r.URL.Scheme = "http"
362
363
resp, err := client.Do(r)
@@ -538,7 +574,9 @@ func (p *Proxy) handleDisconnect(w http.ResponseWriter, r *http.Request, connID
574
func main() {
575
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
576
var err error
541
- var bootstrapServerList = strings.Split(bootstrapServers, ",")
577
+
578
+ var bootstrapServerList = strings.Split(bootstrapServersCSV, ",")
579
+ log.Info().Strs("servers", bootstrapServerList).Msg("Initializing RDClient with bootstrap servers")
580
581
rdClient, err = sdk.NewClient(
582
sdk.WithBootstrapServers(bootstrapServerList),
cmd/webclient/service-worker.js
+54
-26
@@ -110,15 +110,12 @@ async function runWASM() {
110
}
111
112
self.addEventListener("install", (e) => {
113
- console.log("[SW] Install event");
113
async function LoadCache() {
115
- console.log("[SW] Loading cache");
114
try {
115
const manifest = await fetchManifest();
116
// Use content-addressed path: /static/<sha256>.wasm
117
const wasm_URL = `/static/${manifest.wasmFile}`;
118
const cache = await caches.open(currentCacheVersion);
121
-
119
await cache.addAll([wasm_URL, wasm_exec_URL, manifest_URL]);
120
} catch (error) {
121
console.error("[SW] Cache loading failed:", error);
@@ -134,7 +131,6 @@ self.addEventListener("activate", (e) => {
131
(async () => {
132
try {
133
// Delete old caches
137
- const manifest = await fetchManifest();
134
const cacheNames = await caches.keys();
135
await Promise.all(
136
cacheNames.map((cacheName) => {
@@ -142,7 +138,6 @@ self.addEventListener("activate", (e) => {
138
cacheName !== currentCacheVersion &&
139
cacheName.startsWith("WASM_Cache_")
140
) {
145
- console.log("[SW] Deleting old cache:", cacheName);
141
return caches.delete(cacheName);
142
}
143
})
@@ -167,6 +162,20 @@ self.addEventListener("activate", (e) => {
162
);
163
});
164
165
+self.addEventListener("message", (event) => {
166
+ if (event.data && event.data.type === "CLAIM_CLIENTS") {
167
+ self.clients.claim().then(() => {
168
+ self.clients.matchAll().then((clients) => {
169
+ clients.forEach((client) => {
170
+ client.postMessage({ type: "CLAIMED" });
171
+ });
172
+ });
173
+ }).catch((error) => {
174
+ console.error("[SW] Manual clients.claim() failed:", error);
175
+ });
176
+ }
177
+});
178
+
179
self.addEventListener("fetch", (e) => {
180
const url = new URL(e.request.url);
181
@@ -206,7 +215,6 @@ self.addEventListener("fetch", (e) => {
215
216
// Serve portal.mp4 from cache or fetch from origin
217
if (url.pathname === BASE_PATH + "portal.mp4") {
209
- console.log("[SW] Fetching portal.mp4");
218
e.respondWith(
219
(async () => {
220
try {
@@ -215,14 +223,12 @@ self.addEventListener("fetch", (e) => {
223
const cache = await caches.open(currentCacheVersion);
224
const cachedResponse = await cache.match(BASE_PATH + "portal.mp4");
225
if (cachedResponse) {
218
- console.log("[SW] Found portal.mp4 in cache");
226
return cachedResponse;
227
}
228
229
// Fetch from network and cache it
230
const response = await fetch(e.request);
231
if (response.ok) {
225
- console.log("[SW] Caching portal.mp4");
232
cache.put(BASE_PATH + "portal.mp4", response.clone());
233
}
234
return response;
@@ -235,28 +241,51 @@ self.addEventListener("fetch", (e) => {
241
return;
242
}
243
238
- if (typeof __go_jshttp === "undefined") {
239
- if (Date.now() - _lastReload > 1000) {
240
- _lastReload = Date.now();
241
- loading = false;
242
- } else {
243
- // send auto refresh page
244
- e.respondWith(
245
- new Response(
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
- );
250
- return;
251
- }
252
- }
253
-
244
e.respondWith(
245
(async () => {
246
+ // WASM 초기화 대기
247
+ if (typeof __go_jshttp === "undefined" && !loading) {
248
+ try {
249
+ await init();
250
+ } catch (error) {
251
+ console.error('[SW] Init failed:', error);
252
+ return new Response("WASM initialization failed. Please refresh the page.", {
253
+ status: 503,
254
+ statusText: 'Service Unavailable'
255
+ });
256
+ }
257
+ }
258
+
259
+ // 로딩 중이면 대기 (최대 5초)
260
+ let waitCount = 0;
261
+ while (loading && waitCount < 50) {
262
+ await new Promise(resolve => setTimeout(resolve, 100));
263
+ waitCount++;
264
+ }
265
+
266
+ // 여전히 undefined면 새로고침 시도
267
+ if (typeof __go_jshttp === "undefined") {
268
+ if (Date.now() - _lastReload > 1000) {
269
+ _lastReload = Date.now();
270
+ loading = false;
271
+ return new Response(
272
+ "<html><head><meta http-equiv='refresh' content='0'></head><body><h1>Initializing WASM... Please wait.</h1></body></html>",
273
+ { status: 503, headers: { "Content-Type": "text/html" } }
274
+ );
275
+ } else {
276
+ return new Response(
277
+ "<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>",
278
+ { status: 503, headers: { "Content-Type": "text/html" } }
279
+ );
280
+ }
281
+ }
282
+
283
+ // 요청 처리
284
try {
285
const resp = await __go_jshttp(e.request);
286
return resp;
259
- } catch {
287
+ } catch (error) {
288
+ console.error('[SW] Request handling error:', error);
289
__go_jshttp = undefined;
290
await init();
291
const resp = await __go_jshttp(e.request);
@@ -264,5 +293,4 @@ self.addEventListener("fetch", (e) => {
293
}
294
})()
295
);
267
- return;
296
});