test/proxy.test.js
Rama main · réplica de lectura del repositorio original.
// Running behind a reverse proxy or tunnel (PUBLIC_URL, docs/REMOTE.md): the
// origin it lets in, the forwarded headers it believes, and the links it builds.
import { describe, it, expect, afterEach, vi } from 'vitest';
import express from 'express';
import { createServer } from 'http';
import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { parsePublicUrl } from '../server/state.js';
import { noteProxy, proxySeen, accessEmail } from '../server/proxy.js';
import { appLink } from '../server/rounds.js';
import { setupProxy } from '../server/remote-setup.js';
import { remoteLines } from '../server/service.js';
import { removeTempDir } from './temp-dir.js';
describe('PUBLIC_URL', () => {
afterEach(() => { delete process.env.PUBLIC_URL; vi.resetModules(); });
// state.js reads process.env when it loads.
async function load(mod, val) {
process.env.PUBLIC_URL = val;
vi.resetModules();
return import(mod);
}
it('is the origin of an http(s) URL, else null', () => {
expect(parsePublicUrl('https://agent.example.com')).toBe('https://agent.example.com');
expect(parsePublicUrl(' https://agent.example.com/some/path/ ')).toBe('https://agent.example.com');
expect(parsePublicUrl('https://agent.example.com:8443')).toBe('https://agent.example.com:8443');
expect(parsePublicUrl('http://10.0.0.1:8080')).toBe('http://10.0.0.1:8080');
for (const bad of [undefined, '', 'agent.example.com', 'ftp://agent.example.com', 'not a url']) expect(parsePublicUrl(bad)).toBeNull();
});
it('lets its hostname in as an origin, and no other', async () => {
const { isAllowedOrigin, PUBLIC_URL } = await load('../server/state.js', 'https://agent.example.com');
expect(PUBLIC_URL).toBe('https://agent.example.com');
expect(isAllowedOrigin('https://agent.example.com')).toBe(true);
expect(isAllowedOrigin('http://localhost:7007')).toBe(true);
expect(isAllowedOrigin('https://evil.example.com')).toBe(false);
expect(isAllowedOrigin('https://agent.example.com.evil.com')).toBe(false);
const off = await load('../server/state.js', '');
expect(off.isAllowedOrigin('https://agent.example.com')).toBe(false);
});
it("counts its page as the owner's browser even when the proxy sends its own Host", async () => {
const { fromBrowser } = await load('../server/ws.js', 'https://agent.example.com');
const req = (origin, host) => ({ headers: { origin, host } });
expect(fromBrowser(req('https://agent.example.com', '127.0.0.1:7007'))).toBe(true);
expect(fromBrowser(req('https://agent.example.com', 'agent.example.com'))).toBe(true);
expect(fromBrowser(req('http://localhost:7007', 'localhost:7007'))).toBe(true);
expect(fromBrowser(req('https://evil.example.com', '127.0.0.1:7007'))).toBe(false);
expect(fromBrowser(req(undefined, '127.0.0.1:7007'))).toBe(false);
});
it('is where Telegram links point, ahead of APP_URL and ALLOWED_ORIGINS', () => {
expect(appLink({ PUBLIC_URL: 'https://agent.example.com/', APP_URL: 'https://old', ALLOWED_ORIGINS: 'mini.ts.net' })).toBe('https://agent.example.com');
expect(appLink({ PUBLIC_URL: 'garbage', APP_URL: 'https://old' })).toBe('https://old');
});
});
describe('forwarded headers (server/proxy.js)', () => {
// A real Express app with the server's trust setting, on loopback.
async function serve() {
const app = express();
app.set('trust proxy', 'loopback');
app.use(noteProxy);
app.get('/', (req, res) => res.json({ proto: req.protocol, ip: req.ip, email: accessEmail(req) }));
const server = createServer(app);
await new Promise(r => server.listen(0, '127.0.0.1', r));
return { url: `http://127.0.0.1:${server.address().port}/`, close: () => new Promise(r => server.close(r)) };
}
it("believes a loopback proxy's X-Forwarded-Proto/For and notes the newest request", async () => {
const s = await serve();
try {
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
const headers = { 'X-Forwarded-Proto': 'https', 'X-Forwarded-For': '203.0.113.9', 'Cf-Access-Authenticated-User-Email': 'ada@example.com' };
expect(await (await fetch(s.url, { headers })).json()).toEqual({ proto: 'https', ip: '203.0.113.9', email: 'ada@example.com' });
expect(proxySeen()).toMatchObject({ proto: 'https', ip: '203.0.113.9', email: 'ada@example.com' });
await fetch(s.url, { headers });
expect(log.mock.calls.filter(([l]) => l.includes('ada@example.com'))).toHaveLength(1);
log.mockRestore();
// A plain request (no proxy) changes nothing it noted.
const before = proxySeen();
expect(await (await fetch(s.url)).json()).toMatchObject({ proto: 'http', email: null });
expect(proxySeen()).toBe(before);
} finally { await s.close(); }
});
it("says which mode status is in and what the proxy last sent", () => {
const p = { at: 0, proto: 'https', ip: '203.0.113.9', email: 'ada@example.com' };
expect(remoteLines({ publicUrl: 'https://a.example', proxy: p }, 120_000)).toEqual([
' remote: reverse proxy at https://a.example (PUBLIC_URL)',
' proxy headers: last forwarded request 2m ago: https from 203.0.113.9, Cloudflare Access user ada@example.com']);
expect(remoteLines({ publicUrl: 'https://a.example', proxy: null })[1]).toContain('none since the start');
expect(remoteLines({ publicUrl: null, proxy: null })).toEqual([' remote: no PUBLIC_URL (local only, or Tailscale, which doctor checks)']);
});
});
describe('install --public-url (setupProxy)', () => {
function ctx(over = {}) {
const home = mkdtempSync(join(tmpdir(), 'a007-proxy-'));
const out = [];
return { home, out, cfg: join(home, '.agent-007'), port: 7123, host: '127.0.0.1', root: home, env: { HOME: home, AGENT007_CONFIG_DIR: join(home, '.agent-007') }, log: (s) => out.push(s), err: (s) => out.push(`ERR ${s}`), ...over };
}
it('writes PUBLIC_URL to the settings file, keeping what is there', () => {
const c = ctx();
try {
mkdirSync(c.cfg, { recursive: true });
writeFileSync(join(c.cfg, '.env'), 'PORT=7123\n');
expect(setupProxy(c, 'https://agent.example.com/')).toBe(0);
expect(readFileSync(join(c.cfg, '.env'), 'utf8')).toMatch(/PORT=7123\n[\s\S]*PUBLIC_URL=https:\/\/agent\.example\.com\n/);
expect(c.out.join('\n')).toContain('http://127.0.0.1:7123');
expect(c.out.join('\n')).not.toContain('ERR');
expect(setupProxy(c, 'https://agent.example.com')).toBe(0);
expect(c.out.at(-2)).toContain('already https://agent.example.com');
} finally { removeTempDir(c.home); }
});
it('refuses a non-URL, and warns about http, a wildcard HOST and an overriding setting', () => {
const c = ctx({ host: '0.0.0.0' });
c.env.PUBLIC_URL = 'https://other';
try {
expect(setupProxy(c, 'agent.example.com')).toBe(2);
expect(c.out.join('\n')).toContain('Nothing was changed');
expect(setupProxy({ ...c, 'dry-run': true }, 'http://agent.example.com')).toBe(0);
const said = c.out.join('\n');
expect(said).toContain('Would set PUBLIC_URL=http://agent.example.com');
expect(said).toContain('ERR ! PUBLIC_URL is also set in your environment');
expect(said).toContain('ERR ! That is not https');
expect(said).toContain('ERR ! HOST=0.0.0.0');
} finally { removeTempDir(c.home); }
});
});