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 });