Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/cli/src/commands/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ export async function dashboard(ctx: ServiceContext): Promise<void> {
// "Provided Host header is not whitelisted". Same rewrite nginx does per location.
const upstream = http.request(
{ host: '127.0.0.1', port: route.target.port,
path: route.target.keepPrefix ? route.matched + route.rest : (route.rest || '/'),
path: upstreamPath(route, url.search),
method: req.method,
headers: { ...req.headers, host: `127.0.0.1:${route.target.port}` } },
(up) => {
Expand Down Expand Up @@ -471,7 +471,7 @@ export async function dashboard(ctx: ServiceContext): Promise<void> {
const headers = Object.entries({ ...req.headers, host: `127.0.0.1:${route.target.port}` })
.map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : v}`)
.join('\r\n');
upstream.write(`${req.method} ${route.target.keepPrefix ? route.matched + route.rest : (route.rest || '/')} HTTP/1.1\r\n${headers}\r\n\r\n`);
upstream.write(`${req.method} ${upstreamPath(route, url.search)} HTTP/1.1\r\n${headers}\r\n\r\n`);
if (head.length) upstream.write(head);
upstream.pipe(socket);
socket.pipe(upstream);
Expand All @@ -491,6 +491,11 @@ export async function dashboard(ctx: ServiceContext): Promise<void> {
await new Promise(() => {});
}

/** The path and query a matched request is forwarded upstream with. */
function upstreamPath(route: NonNullable<ReturnType<typeof matchRoute>>, search: string): string {
return (route.target.keepPrefix ? route.matched + route.rest : route.rest || '/') + search;
}

/** Longest-prefix route match; returns the remainder to forward upstream. */
function matchRoute(
routes: Map<string, { port: number; ws: boolean; keepPrefix: boolean }>,
Expand Down
27 changes: 27 additions & 0 deletions packages/cli/tests/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,4 +301,31 @@ describe('dashboard API', () => {
assert.ok(e.checkedAt, `${e.id}: no checkedAt`);
}
});

// A client finds a person on the DUB by `GET /dub/api/v1/usernames/search?prefix=`; without
// the query the backend answers an unfiltered page and the person never shows up.
it('proxies a request with its query string', async () => {
const { body } = await get('/api/network');
const dub = (body as { services: { id: string; port: number }[] }).services.find((s) => s.id === 'dub')!;
let seen: string | undefined;
const stub = http.createServer((req, res) => {
seen = req.url;
res.end('{}');
});
const bound = await new Promise<boolean>((resolve) => {
stub.once('error', () => resolve(false));
stub.listen(dub.port, '127.0.0.1', () => resolve(true));
});
if (!bound) {
console.log(`# skipped: port ${dub.port} is already in use`);
return;
}
try {
const res = await fetch(`${BASE}/dub/api/v1/usernames/search?prefix=alice&limit=5`);
assert.equal(res.status, 200);
assert.equal(seen, '/api/v1/usernames/search?prefix=alice&limit=5');
} finally {
stub.close();
}
});
});
Loading