allowedHostsConfiguration.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import request from 'supertest'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import type { ApplicationRuntime } from '../../src/server/app.js'
  7. import type { ServerConfig } from '../../src/server/config.js'
  8. import {
  9. AllowedHostsConfigurationManager,
  10. allowedHostsConfigurationPath,
  11. loadAllowedHostsConfiguration,
  12. normalizeAllowedHost,
  13. } from '../../src/server/allowedHostsConfiguration.js'
  14. import { createAuthenticatedApplication } from './authenticatedApplication.js'
  15. const roots: string[] = []
  16. const runtimes: ApplicationRuntime[] = []
  17. afterEach(() => {
  18. for (const runtime of runtimes.splice(0)) runtime.close()
  19. for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
  20. })
  21. function temporaryHome(): string {
  22. const home = mkdtempSync(join(tmpdir(), 'yaoyao-allowed-hosts-'))
  23. roots.push(home)
  24. return home
  25. }
  26. function config(home: string, allowedHosts = new Set<string>()): ServerConfig {
  27. return {
  28. host: '127.0.0.1', port: 15300, upstream: new URL('http://127.0.0.1:9119'),
  29. allowedHosts, home, mediaRoot: home, attachmentsRoot: home, imagesRoot: home,
  30. mediaOwner: 'tester', allowInsecureLan: false, insecureLan: false, production: false,
  31. }
  32. }
  33. describe('allowed hosts configuration', () => {
  34. it('normalizes domains and IP addresses and rejects URLs or ports', () => {
  35. expect(normalizeAllowedHost('YAOYAO.Example.COM.')).toBe('yaoyao.example.com')
  36. expect(normalizeAllowedHost('[2001:db8::10]')).toBe('2001:db8::10')
  37. expect(normalizeAllowedHost('例子.测试')).toBe('xn--fsqu00a.xn--0zwm56d')
  38. expect(() => normalizeAllowedHost('https://yaoyao.example.com')).toThrow(/不能包含协议/)
  39. expect(() => normalizeAllowedHost('203.0.113.10:15300')).toThrow(/不能附加端口/)
  40. })
  41. it('persists Web entries with restrictive permissions and merges environment entries after restart', () => {
  42. const home = temporaryHome()
  43. const initial = loadAllowedHostsConfiguration(home, {
  44. HERMES_YAOYAO_ALLOWED_HOSTS: 'localhost,127.0.0.1',
  45. })
  46. const runtimeHosts = new Set(initial.hosts)
  47. const manager = new AllowedHostsConfigurationManager(home, initial, runtimeHosts)
  48. const saved = manager.update(['YAOYAO.Example.COM.', '203.0.113.10'])
  49. expect(saved).toMatchObject({
  50. editableHosts: ['203.0.113.10', 'yaoyao.example.com'],
  51. environmentHosts: ['127.0.0.1', 'localhost'],
  52. })
  53. expect([...runtimeHosts].sort()).toEqual([
  54. '127.0.0.1', '203.0.113.10', 'localhost', 'yaoyao.example.com',
  55. ])
  56. expect(statSync(allowedHostsConfigurationPath(home)).mode & 0o777).toBe(0o600)
  57. expect(JSON.parse(readFileSync(allowedHostsConfigurationPath(home), 'utf8'))).toMatchObject({
  58. schemaVersion: 1,
  59. hosts: ['203.0.113.10', 'yaoyao.example.com'],
  60. })
  61. expect(loadAllowedHostsConfiguration(home, {
  62. HERMES_YAOYAO_ALLOWED_HOSTS: 'localhost,proxy.example.com',
  63. })).toMatchObject({
  64. hosts: ['203.0.113.10', 'localhost', 'proxy.example.com', 'yaoyao.example.com'],
  65. editableHosts: ['203.0.113.10', 'yaoyao.example.com'],
  66. environmentHosts: ['localhost', 'proxy.example.com'],
  67. })
  68. })
  69. it('updates the running Host guard immediately and keeps the current public host reachable', async () => {
  70. const home = temporaryHome()
  71. const serverConfig = config(home)
  72. serverConfig.allowedHostsSettings = {
  73. source: 'none', hosts: [], editableHosts: [], environmentHosts: [],
  74. }
  75. const runtime = createAuthenticatedApplication({ config: serverConfig })
  76. runtimes.push(runtime)
  77. const agent = request.agent(runtime.app.callback())
  78. const bootstrap = await agent.get('/api/app/bootstrap').set('Host', '127.0.0.1:15300').expect(200)
  79. const saved = await agent.put('/api/app/system/allowed-hosts')
  80. .set('Host', '127.0.0.1:15300')
  81. .set('Origin', 'http://127.0.0.1:15300')
  82. .set('X-CSRF-Token', bootstrap.body.csrfToken)
  83. .send({ hosts: ['yaoyao.example.com', '203.0.113.10'] })
  84. .expect(200)
  85. expect(saved.body).toMatchObject({
  86. hosts: ['203.0.113.10', 'yaoyao.example.com'],
  87. editableHosts: ['203.0.113.10', 'yaoyao.example.com'],
  88. })
  89. await agent.get('/healthz').set('Host', 'yaoyao.example.com').expect(200)
  90. await agent.get('/healthz').set('Host', 'not-allowed.example.com').expect(421)
  91. const publicBootstrap = await agent.get('/api/app/bootstrap').set('Host', 'yaoyao.example.com').expect(200)
  92. const rejected = await agent.put('/api/app/system/allowed-hosts')
  93. .set('Host', 'yaoyao.example.com')
  94. .set('Origin', 'http://yaoyao.example.com')
  95. .set('X-CSRF-Token', publicBootstrap.body.csrfToken)
  96. .send({ hosts: ['203.0.113.10'] })
  97. .expect(409)
  98. expect(rejected.body.code).toBe('current_host_required')
  99. })
  100. })