mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-05-06 14:03:40 +08:00
fix(cert): restore WebSocket connection for certificate issuance (#1630)
`ObtainCert.job()` called `issueCert()` synchronously after `step.value++`, before Vue mounted `<ObtainCertLive>`, so `refObtainCertLive.value` was null and the optional-chain call silently no-oped — no log entry, no WebSocket connection, progress stuck at 0%. Add an `await nextTick()` so the live component is mounted before its method is invoked. Also harden the long-token WebSocket fallback: switch the frontend to URL-safe base64 (avoids `+` being decoded as a space in query strings) and accept both URL-safe and standard base64 in `getTokenWS` for backward compatibility. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,10 @@ import { useSettingsStore, useUserStore } from '@/pinia'
|
||||
export function buildWebSocketUrl(url: string, token: string, shortToken: string, nodeId?: number): string {
|
||||
const node_id = nodeId && nodeId > 0 ? `&x_node_id=${nodeId}` : ''
|
||||
|
||||
// Use shortToken if available (without base64 encoding), otherwise use regular token (with base64 encoding)
|
||||
const authParam = shortToken ? `token=${shortToken}` : `token=${btoa(token)}`
|
||||
// Use shortToken if available (without base64 encoding), otherwise use regular token (URL-safe base64).
|
||||
// URL-safe base64 avoids `+` chars that get decoded as spaces in query strings.
|
||||
const longTokenParam = btoa(token).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
const authParam = shortToken ? `token=${shortToken}` : `token=${longTokenParam}`
|
||||
|
||||
// In development mode, keep WebSocket same-origin so the browser
|
||||
// connects through the dev server instead of the private backend port.
|
||||
@@ -41,8 +43,15 @@ export function useWebSocket<T = any>(
|
||||
const settings = useSettingsStore()
|
||||
const { token, shortToken } = storeToRefs(userStore)
|
||||
|
||||
// Snapshot the URL at call time — must NOT be reactive to avoid
|
||||
// tearing down in-flight connections when shortToken arrives later.
|
||||
// Snapshot the URL at call time — must NOT be reactive to avoid tearing down
|
||||
// in-flight connections (e.g. terminal, log tail) when shortToken arrives later.
|
||||
// When shortToken is empty we fall back to the URL-safe base64 long token,
|
||||
// which the backend still accepts. We deliberately do NOT trigger
|
||||
// fetchShortToken() here: /token/short can return 403 if the secure-session
|
||||
// cookie is stale, and the global HTTP interceptor turns any 403 into a
|
||||
// forced logout — which would kick out otherwise-valid sessions on any
|
||||
// WebSocket-backed page. Short-token refresh is handled by the user store's
|
||||
// token watcher (see app/src/pinia/moudule/user.ts).
|
||||
const wsUrl = buildWebSocketUrl(url, token.value, shortToken.value, settings.node.id)
|
||||
|
||||
return vueUseWebSocket<T>(wsUrl, {
|
||||
|
||||
@@ -164,6 +164,11 @@ async function job() {
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for Vue to mount ObtainCertLive after step transitions to 2; without
|
||||
// this tick refObtainCertLive.value is still null and issueCert() silently
|
||||
// no-ops via its optional-chain call.
|
||||
await nextTick()
|
||||
|
||||
issueCert()
|
||||
}
|
||||
function toggle(status: boolean) {
|
||||
|
||||
@@ -46,6 +46,12 @@ func getTokenWS(c *gin.Context) (token string) {
|
||||
|
||||
if token = c.Query("token"); token != "" {
|
||||
if len(token) > 16 {
|
||||
// Try URL-safe base64 first (browsers send `+` -> `-`, `/` -> `_` to
|
||||
// avoid query-string corruption); fall back to standard base64 for
|
||||
// backward compatibility with older clients.
|
||||
if tokenBytes, err := base64.RawURLEncoding.DecodeString(token); err == nil {
|
||||
return string(tokenBytes)
|
||||
}
|
||||
tokenBytes, _ := base64.StdEncoding.DecodeString(token)
|
||||
return string(tokenBytes)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,22 @@ func TestGetTokenWS_NoCookieFallback(t *testing.T) {
|
||||
assert.Equal(t, jwt, token)
|
||||
})
|
||||
|
||||
t.Run("decodes URL-safe base64 token from query", func(t *testing.T) {
|
||||
// Pick a payload long enough that its encoded form is > 16 chars and
|
||||
// whose std-base64 encoding contains `+` / `/` so the URL-safe variant
|
||||
// differs (would have been corrupted by `c.Query` decoding `+` as space).
|
||||
jwt := "eyJhbGciOiJIUzI1NiJ9.test??>>>>>>>>"
|
||||
urlSafe := base64.RawURLEncoding.EncodeToString([]byte(jwt))
|
||||
std := base64.StdEncoding.EncodeToString([]byte(jwt))
|
||||
assert.NotEqual(t, urlSafe, std, "test payload must encode differently in std vs URL-safe")
|
||||
assert.Greater(t, len(urlSafe), 16, "encoded payload must be > 16 chars to hit the long-token branch")
|
||||
|
||||
c := newTestGinContext(t, "GET", "/ws?token="+urlSafe, nil)
|
||||
|
||||
token := getTokenWS(c)
|
||||
assert.Equal(t, jwt, token)
|
||||
})
|
||||
|
||||
t.Run("does NOT read from cookie", func(t *testing.T) {
|
||||
c := newTestGinContext(t, "GET", "/ws", nil)
|
||||
c.Request.AddCookie(&http.Cookie{Name: "token", Value: "cookie-jwt-token"})
|
||||
|
||||
Reference in New Issue
Block a user