remote-login.test.mjs 4.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import {test} from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import {createServer} from 'node:http'
  4. import {randomUUID,randomBytes} from 'node:crypto'
  5. import {remoteSession,enrollDesktopHost,normalizeServerURL} from './remote-login.mjs'
  6. test('server addresses are normalized like browser URLs',()=>{
  7. assert.equal(normalizeServerURL('192.168.1.10:15300'),'http://192.168.1.10:15300')
  8. assert.equal(normalizeServerURL('http://192.168.1.10:15300/'),'http://192.168.1.10:15300')
  9. assert.equal(normalizeServerURL('https://yaoyao.example.com'),'https://yaoyao.example.com')
  10. assert.throws(()=>normalizeServerURL(''),/服务器地址/)
  11. assert.throws(()=>normalizeServerURL('https://user:pass@example.com'),/无效/)
  12. assert.throws(()=>normalizeServerURL('http://example.com/path'),/无效/)
  13. assert.throws(()=>normalizeServerURL('ftp://example.com'),/无效/)
  14. })
  15. async function fakeServer(){
  16. const csrf=randomUUID(),session=randomBytes(24).toString('base64url'),token=randomBytes(32).toString('base64url'),hostId=randomUUID()
  17. const requests=[]
  18. const server=createServer((req,res)=>{
  19. const chunks=[];req.on('data',chunk=>chunks.push(chunk));req.on('end',()=>{
  20. const body=Buffer.concat(chunks).toString()
  21. requests.push({method:req.method,url:req.url,origin:req.headers.origin,csrf:req.headers['x-csrf-token'],cookie:req.headers.cookie??'',body})
  22. const json=payload=>{res.setHeader('content-type','application/json');res.end(JSON.stringify(payload))}
  23. if(req.url==='/api/app/bootstrap'&&(req.method==='GET'||req.method==='HEAD')){res.setHeader('set-cookie','csrf=test-csrf; Path=/; HttpOnly');return json({authRequired:true,csrfToken:csrf})}
  24. if(req.url==='/api/app/login'&&req.method==='POST'){
  25. const credentials=JSON.parse(body||'{}')
  26. if(credentials.username!=='admin'||credentials.password!=='pass'){res.statusCode=401;return json({error:'invalid'})}
  27. if(req.headers['x-csrf-token']!==csrf||!String(req.headers.cookie).includes('csrf=')){res.statusCode=403;return json({error:'csrf'})}
  28. res.setHeader('set-cookie',[`session=${session}; Path=/; HttpOnly`,`csrf=${csrf}.sig; Path=/; HttpOnly`])
  29. return json({user:{id:randomUUID(),username:'admin',role:'admin'},csrfToken:csrf+'-rotated'})
  30. }
  31. if(req.url==='/api/app/admin/desktop-hosts'&&req.method==='POST'){
  32. if(req.headers['x-csrf-token']!==csrf+'-rotated'||!String(req.headers.cookie).includes(`session=${session}`)){res.statusCode=403;return json({error:'session'})}
  33. return json({host:{id:hostId,name:JSON.parse(body||'{}').name,enabled:true},token})
  34. }
  35. res.statusCode=404;json({error:'not found'})
  36. })
  37. })
  38. await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve))
  39. return {server,url:`http://127.0.0.1:${server.address().port}`,requests,csrf,session,token,hostId}
  40. }
  41. test('logs in with username and password and enrolls this Mac as a computer',async()=>{
  42. const fake=await fakeServer()
  43. try{
  44. const auth=await remoteSession(fake.url,{username:'admin',password:'pass'})
  45. assert.equal(auth.user.role,'admin')
  46. assert.equal(auth.origin,fake.url)
  47. assert.ok(auth.cookies.some(([name])=>name==='session'))
  48. const installId='22222222-2222-4222-8222-222222222222',previousHostId='33333333-3333-4333-8333-333333333333'
  49. const enrollment=await enrollDesktopHost(auth,{name:'书房 iMac',installId,previousHostId})
  50. assert.equal(enrollment.host.id,fake.hostId)
  51. assert.equal(enrollment.token,fake.token)
  52. const [bootstrap,login,enroll]=fake.requests
  53. assert.equal(bootstrap.url,'/api/app/bootstrap')
  54. assert.equal(login.origin,fake.url)
  55. assert.equal(login.csrf,fake.csrf)
  56. assert.equal(enroll.csrf,fake.csrf+'-rotated')
  57. assert.ok(enroll.cookie.includes(`session=${fake.session}`))
  58. assert.deepEqual(JSON.parse(enroll.body),{name:'书房 iMac',installId,previousHostId})
  59. }finally{
  60. fake.server.close();fake.server.closeAllConnections?.()
  61. }
  62. })
  63. test('wrong credentials and non-admin accounts surface clear errors',async()=>{
  64. const fake=await fakeServer()
  65. try{
  66. await assert.rejects(()=>remoteSession(fake.url,{username:'admin',password:'wrong'}),/用户名或密码不正确/)
  67. await assert.rejects(()=>remoteSession(fake.url,{username:'',password:'pass'}),/用户名和密码/)
  68. const auth={origin:fake.url,csrfToken:'stale',cookies:[['session','x']]}
  69. await assert.rejects(()=>enrollDesktopHost(auth,{name:'x'}),/管理员账号|注册电脑失败/)
  70. }finally{
  71. fake.server.close();fake.server.closeAllConnections?.()
  72. }
  73. })