Ctrl K
ComunidadGuías…

test/proxy.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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); }
  });
});