Преглед изворни кода

fix: 修复桌面登录 Cookie 合并导致八小时后失效

Samien пре 1 месец
родитељ
комит
446f5a917d
2 измењених фајлова са 46 додато и 1 уклоњено
  1. 4 1
      desktop/remote-login.mjs
  2. 42 0
      desktop/remote-login.test.mjs

+ 4 - 1
desktop/remote-login.mjs

@@ -24,7 +24,10 @@ async function requestServer(fetchImpl,url,options){
 class CookieJar{
   constructor(){this.cookies=new Map();this.details=new Map()}
   absorb(response){
-    for(const line of response.headers.getSetCookie?.()??[]){
+    // Electron net.fetch can fold multiple cookies into one getSetCookie entry.
+    // Split only before a cookie name, preserving commas inside Expires dates.
+    const lines=(response.headers.getSetCookie?.()??[]).flatMap(value=>value.split(/,(?=\s*[^;,\s]+=)/g))
+    for(const line of lines){
       const pair=line.split(';')[0],eq=pair.indexOf('=')
       if(eq<=0)continue
       const name=pair.slice(0,eq).trim(),value=pair.slice(eq+1).trim()

+ 42 - 0
desktop/remote-login.test.mjs

@@ -101,6 +101,48 @@ test('native login preserves cookie expiry, scope and same-site policy', async (
   assert.equal(auth.cookieDetails.find(cookie => cookie.name === 'csrf').expirationDate, undefined)
 })
 
+test('folded Electron response cookies preserve the 30-day login and rotated CSRF token', async () => {
+  let request = 0
+  const before = Date.now() / 1000
+  const auth = await remoteSession('https://server.test', { username: 'admin', password: 'password' }, async () => {
+    if (++request === 1) return Response.json({ csrfToken: 'initial' }, { headers: { 'set-cookie': 'csrf=initial; Path=/; Max-Age=28800; HttpOnly; SameSite=Strict' } })
+    // Electron net.fetch joins the server's Set-Cookie array before creating Headers.
+    return Response.json({ user: { id: 'admin', role: 'admin' }, csrfToken: 'rotated' }, { headers: { 'set-cookie': [
+      'session=login; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=Strict',
+      'csrf=rotated; Path=/; Max-Age=28800; HttpOnly; Secure; SameSite=Lax',
+    ].join(', ') } })
+  })
+  const session = auth.cookieDetails.find(cookie => cookie.name === 'session')
+  const csrf = auth.cookieDetails.find(cookie => cookie.name === 'csrf')
+  assert.ok(session.expirationDate >= before + 2592000 && session.expirationDate <= Date.now() / 1000 + 2592000)
+  assert.ok(csrf.expirationDate >= before + 28800 && csrf.expirationDate <= Date.now() / 1000 + 28800)
+  assert.equal(session.sameSite, 'strict'); assert.equal(csrf.sameSite, 'lax')
+  assert.equal(session.secure, true); assert.equal(csrf.secure, true)
+  assert.deepEqual(Object.fromEntries(auth.cookies), { csrf: 'rotated', session: 'login' })
+})
+
+test('folded cookies keep Expires date commas and separate cookie values and paths', async () => {
+  let request = 0
+  const expires = 'Wed, 21 Oct 2037 07:28:00 GMT'
+  const auth = await remoteSession('https://server.test', { username: 'admin', password: 'password' }, async () => {
+    if (++request === 1) return Response.json({ csrfToken: 'initial' })
+    return Response.json({ user: { id: 'admin', role: 'admin' } }, { headers: { 'set-cookie': [
+      `session=login==; Expires=${expires}; Path=/; HttpOnly; Secure; SameSite=Strict`,
+      'csrf=rotated; Path=/api; HttpOnly; SameSite=Lax',
+      'device=fixture; Path=/device; Max-Age=0; HttpOnly',
+    ].join(', ') } })
+  })
+  const session = auth.cookieDetails.find(cookie => cookie.name === 'session')
+  const csrf = auth.cookieDetails.find(cookie => cookie.name === 'csrf')
+  const device = auth.cookieDetails.find(cookie => cookie.name === 'device')
+  assert.equal(session.expirationDate, Date.parse(expires) / 1000)
+  assert.equal(session.value, 'login=='); assert.equal(session.path, '/')
+  assert.equal(csrf.path, '/api'); assert.equal(csrf.expirationDate, undefined)
+  assert.equal(csrf.secure, false); assert.equal(csrf.sameSite, 'lax')
+  assert.equal(device.path, '/device'); assert.ok(device.expirationDate <= Date.now() / 1000)
+  assert.equal(auth.cookieDetails.length, 3)
+})
+
 test('pending approval is not mislabeled as a bad password', async () => {
   let calls = 0
   await assert.rejects(() => remoteSession('https://server.test', { username: 'child', password: 'password' }, async () => {