Explorar o código

修复桌面检测成功后登录失败的网络通道不一致

统一登录、注册和电脑授权使用 Chromium 网络请求,并隔离认证 Cookie。增加 Node 网络不可达时的真实桌面流程回归,验证会话保留与管理员授权失败后的继续登录。相关 16 项测试通过。
Samien hai 1 mes
pai
achega
f44672560a
Modificáronse 2 ficheiros con 39 adicións e 5 borrados
  1. 6 3
      desktop/main.mjs
  2. 33 2
      desktop/onboarding-flow.test.mjs

+ 6 - 3
desktop/main.mjs

@@ -30,6 +30,9 @@ else {
   let remoteMode = false, remoteURL = '', serverModeActive = false, switchingMode = false
   let remoteMode = false, remoteURL = '', serverModeActive = false, switchingMode = false
   let recoveringService = false
   let recoveringService = false
   const serviceURL = () => remoteMode ? remoteURL : manager?.state.url
   const serviceURL = () => remoteMode ? remoteURL : manager?.state.url
+  // Use the same Chromium networking as server detection/navigation. Auth owns
+  // its cookie jar: do not send or overwrite another account's browser cookies.
+  const authFetch = (url, options) => net.fetch(url, { ...options, credentials: 'omit' })
   const serviceOrigins = () => {
   const serviceOrigins = () => {
     try { return remoteMode && remoteURL ? [new URL(remoteURL).origin] : manager?.state.url ? [new URL(manager.state.url).origin] : [] }
     try { return remoteMode && remoteURL ? [new URL(remoteURL).origin] : manager?.state.url ? [new URL(manager.state.url).origin] : [] }
     catch { return [] }
     catch { return [] }
@@ -346,7 +349,7 @@ else {
     async function authorizeComputer(server,session){
     async function authorizeComputer(server,session){
       const previous=await hostManager.read().catch(()=>undefined)
       const previous=await hostManager.read().catch(()=>undefined)
       const previousHostId=previous&&new URL(previous.serverURL).origin===new URL(server).origin?previous.hostId:undefined
       const previousHostId=previous&&new URL(previous.serverURL).origin===new URL(server).origin?previous.hostId:undefined
-      const registration=await enrollDesktopHost(session,{name:hostname(),installId:await hostManager.installIdentity(),previousHostId})
+      const registration=await enrollDesktopHost(session,{name:hostname(),installId:await hostManager.installIdentity(),previousHostId},authFetch)
       await hostManager.adopt({protocol:1,serverURL:server,hostId:registration.host.id,token:registration.token,...(server.startsWith('http://')?{allowInsecureLan:true}:{})})
       await hostManager.adopt({protocol:1,serverURL:server,hostId:registration.host.id,token:registration.token,...(server.startsWith('http://')?{allowInsecureLan:true}:{})})
       return registration
       return registration
     }
     }
@@ -409,9 +412,9 @@ else {
         await runnerManager.start().catch(error => runnerManager.publish(error.message))
         await runnerManager.start().catch(error => runnerManager.publish(error.message))
         return manager.state.url
         return manager.state.url
       },
       },
-      register: ({ serverURL, username, password }) => remoteRegistration(serverURL, { username, password }),
+      register: ({ serverURL, username, password }) => remoteRegistration(serverURL, { username, password }, authFetch),
       authenticate: async ({ mode, serverURL, setup, username, password }) => {
       authenticate: async ({ mode, serverURL, setup, username, password }) => {
-        const auth = await remoteSession(serverURL, { setup, username, password })
+        const auth = await remoteSession(serverURL, { setup, username, password }, authFetch)
         for (const cookie of auth.cookieDetails) {
         for (const cookie of auth.cookieDetails) {
           await electronSession.defaultSession.cookies.set({ url: serverURL, ...cookie,
           await electronSession.defaultSession.cookies.set({ url: serverURL, ...cookie,
             httpOnly: true, secure: cookie.secure || serverURL.startsWith('https://') })
             httpOnly: true, secure: cookie.secure || serverURL.startsWith('https://') })

+ 33 - 2
desktop/onboarding-flow.test.mjs

@@ -11,6 +11,7 @@ test('first launch, failed connection and remote login all stay in the only main
   const home = await realpath(await mkdtemp(join(tmpdir(), 'yaoyao-onboarding-')))
   const home = await realpath(await mkdtemp(join(tmpdir(), 'yaoyao-onboarding-')))
   let loginCount = 0
   let loginCount = 0
   let registrationCount = 0
   let registrationCount = 0
+  let loginRole = 'user', enrollmentCount = 0
   let holdBootstrap = false, replyBootstrap, sessionValid = true
   let holdBootstrap = false, replyBootstrap, sessionValid = true
   const server = createServer((req, res) => {
   const server = createServer((req, res) => {
     const json = (value, status = 200) => { res.statusCode = status; res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(value)) }
     const json = (value, status = 200) => { res.statusCode = status; res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(value)) }
@@ -26,7 +27,7 @@ test('first launch, failed connection and remote login all stay in the only main
         assert.deepEqual(JSON.parse(raw), { username: 'new-child', password: 'child-password' })
         assert.deepEqual(JSON.parse(raw), { username: 'new-child', password: 'child-password' })
         assert.equal(req.headers['x-csrf-token'], 'fixture')
         assert.equal(req.headers['x-csrf-token'], 'fixture')
         assert.ok(String(req.headers.cookie).includes('csrf=fixture'))
         assert.ok(String(req.headers.cookie).includes('csrf=fixture'))
-        assert.ok(!String(req.headers.cookie).includes('session=fixture'))
+        assert.ok(!String(req.headers.cookie).includes('session='))
         json({ registrationStatus: 'pending', message: '注册成功,请等待管理员开通' }, 201)
         json({ registrationStatus: 'pending', message: '注册成功,请等待管理员开通' }, 201)
       }); return
       }); return
     }
     }
@@ -34,12 +35,21 @@ test('first launch, failed connection and remote login all stay in the only main
       let raw = ''; req.on('data', chunk => { raw += chunk }); req.on('end', () => {
       let raw = ''; req.on('data', chunk => { raw += chunk }); req.on('end', () => {
         loginCount++
         loginCount++
         const body = JSON.parse(raw)
         const body = JSON.parse(raw)
+        assert.ok(!String(req.headers.cookie).includes('session='))
         if (body.username !== 'user' || body.password !== 'correct-password') return json({ error: 'wrong' }, 401)
         if (body.username !== 'user' || body.password !== 'correct-password') return json({ error: 'wrong' }, 401)
         if (req.headers['x-csrf-token'] !== 'fixture' || !String(req.headers.cookie).includes('csrf=fixture')) return json({ error: 'csrf' }, 403)
         if (req.headers['x-csrf-token'] !== 'fixture' || !String(req.headers.cookie).includes('csrf=fixture')) return json({ error: 'csrf' }, 403)
         res.setHeader('set-cookie', 'session=fixture; Path=/; HttpOnly; Max-Age=3600; SameSite=Strict')
         res.setHeader('set-cookie', 'session=fixture; Path=/; HttpOnly; Max-Age=3600; SameSite=Strict')
-        json({ user: { id: 'fixture-user', username: 'user', role: 'user' }, csrfToken: 'fixture' })
+        json({ user: { id: 'fixture-user', username: 'user', role: loginRole }, csrfToken: 'fixture' })
       }); return
       }); return
     }
     }
+    if (req.url === '/api/app/admin/desktop-hosts') {
+      enrollmentCount++
+      assert.equal(req.headers.origin, url)
+      assert.equal(req.headers['x-csrf-token'], 'fixture')
+      assert.ok(String(req.headers.cookie).includes('session=fixture'))
+      // Exercise authorization without provisioning a real computer in the fixture.
+      return json({ error: 'enrollment unavailable' }, 503)
+    }
     res.setHeader('content-type', 'text/html; charset=utf-8'); res.end('<title>已登录远程夭夭</title><h1>远程工作区</h1>')
     res.setHeader('content-type', 'text/html; charset=utf-8'); res.end('<title>已登录远程夭夭</title><h1>远程工作区</h1>')
   })
   })
   await new Promise(done => server.listen(0, '127.0.0.1', done))
   await new Promise(done => server.listen(0, '127.0.0.1', done))
@@ -49,6 +59,12 @@ test('first launch, failed connection and remote login all stay in the only main
     app = await electron.launch({ args: [root], cwd: root, env: { ...process.env,
     app = await electron.launch({ args: [root], cwd: root, env: { ...process.env,
       HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_TEST_MODE: 'ask', HERMES_YAOYAO_DESKTOP_TEST_SYNC: '0' } })
       HERMES_YAOYAO_DESKTOP_TEST_HOME: home, HERMES_YAOYAO_DESKTOP_TEST_MODE: 'ask', HERMES_YAOYAO_DESKTOP_TEST_SYNC: '0' } })
     const page = await app.firstWindow()
     const page = await app.firstWindow()
+    // The connectivity check can succeed through Chromium while Node sockets
+    // are unavailable (for example, after a macOS local-network permission change).
+    await app.evaluate(({ session }, origin) => {
+      globalThis.fetch = async () => { throw new TypeError('fetch failed', { cause: { code: 'EHOSTUNREACH' } }) }
+      return session.defaultSession.cookies.set({ url: origin, name: 'session', value: 'previous-account', httpOnly: true })
+    }, url)
     const errors = []; page.on('pageerror', error => errors.push(error.message))
     const errors = []; page.on('pageerror', error => errors.push(error.message))
     await expect(page.getByRole('heading', { name: '开始使用夭夭' })).toBeVisible()
     await expect(page.getByRole('heading', { name: '开始使用夭夭' })).toBeVisible()
     assert.equal(app.windows().length, 1)
     assert.equal(app.windows().length, 1)
@@ -72,6 +88,9 @@ test('first launch, failed connection and remote login all stay in the only main
     await expect(page.locator('#confirmation-field')).toBeHidden()
     await expect(page.locator('#confirmation-field')).toBeHidden()
     assert.equal(registrationCount, 1)
     assert.equal(registrationCount, 1)
     assert.equal(loginCount, 0, 'registration never signs in or authorizes this computer')
     assert.equal(loginCount, 0, 'registration never signs in or authorizes this computer')
+    assert.equal(await app.evaluate(async ({ session }, origin) =>
+      (await session.defaultSession.cookies.get({ url: origin, name: 'session' }))[0]?.value, url),
+    'previous-account', 'registration does not replace the active browser session')
     assert.match(page.url(), /boot.html$/)
     assert.match(page.url(), /boot.html$/)
     await page.locator('#username').fill('user')
     await page.locator('#username').fill('user')
     await page.locator('#password').fill('wrong-password')
     await page.locator('#password').fill('wrong-password')
@@ -146,6 +165,18 @@ test('first launch, failed connection and remote login all stay in the only main
     await expect(reopened.locator('#login-form')).toBeVisible()
     await expect(reopened.locator('#login-form')).toBeVisible()
     await expect(reopened.locator('#session-loading')).toBeHidden()
     await expect(reopened.locator('#session-loading')).toBeHidden()
     assert.equal(app.windows().length, 1)
     assert.equal(app.windows().length, 1)
+    loginRole = 'admin'
+    await app.evaluate(() => {
+      globalThis.fetch = async () => { throw new TypeError('fetch failed', { cause: { code: 'EHOSTUNREACH' } }) }
+    })
+    await reopened.locator('#username').fill('user')
+    await reopened.locator('#password').fill('correct-password')
+    await reopened.locator('#submit').click()
+    await expect(reopened.locator('#login-error')).toContainText('账号已登录,但电脑授权未恢复')
+    assert.equal(enrollmentCount, 1, 'computer authorization also uses the reachable Chromium transport')
+    assert.equal((await reopened.evaluate(() => window.yaoyaoDesktop.status())).authenticated, true)
+    await reopened.locator('#submit').click()
+    await reopened.waitForURL(url + '/**')
   } finally {
   } finally {
     await app?.close().catch(() => {})
     await app?.close().catch(() => {})
     await new Promise(done => { server.close(done); server.closeAllConnections() })
     await new Promise(done => { server.close(done); server.closeAllConnections() })