mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 22:17:20 +00:00
fix(docker): harden cliproxy local proxy with auth guard, dynamic port, and body handling
Move reverse proxy under /api/cliproxy-local so it sits behind auth middleware. Enforce localhost-only access when dashboard auth is disabled. Resolve the target port from unified config instead of hardcoding 8317. Re-serialize parsed JSON bodies before forwarding so writes still work behind express.json(). Clean up proxy connections on client abort. On the frontend, point the iframe and health check at the new same-origin API path, probe the proxy directly, and tighten iframe origin/path validation before sending the auto-login secret. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
881b061dfe
commit
6471cc55d7
4 files changed
+279
-66
No files matched your search
@@ -0,0 +1,121 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import type { AddressInfo } from 'net';
|
||||
|
||||
import {
|
||||
createCliproxyLocalProxyRouter,
|
||||
type CliproxyLocalProxyDeps,
|
||||
} from '../../../src/web-server/routes/cliproxy-local-proxy';
|
||||
|
||||
const servers: http.Server[] = [];
|
||||
|
||||
async function listen(server: http.Server): Promise<number> {
|
||||
servers.push(server);
|
||||
|
||||
return await new Promise<number>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
server.off('error', reject);
|
||||
resolve((server.address() as AddressInfo).port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createBackendServer(
|
||||
handler: http.RequestListener
|
||||
): Promise<{ port: number; server: http.Server }> {
|
||||
const server = http.createServer(handler);
|
||||
const port = await listen(server);
|
||||
return { port, server };
|
||||
}
|
||||
|
||||
async function createProxyServer(options: {
|
||||
enforceAccess?: CliproxyLocalProxyDeps['enforceAccess'];
|
||||
resolveTargetPort: () => number;
|
||||
}): Promise<{ baseUrl: string; server: http.Server }> {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(
|
||||
'/api/cliproxy-local',
|
||||
createCliproxyLocalProxyRouter({
|
||||
enforceAccess: options.enforceAccess,
|
||||
resolveTargetPort: options.resolveTargetPort,
|
||||
})
|
||||
);
|
||||
|
||||
const server = http.createServer(app);
|
||||
const port = await listen(server);
|
||||
return { baseUrl: `http://127.0.0.1:${port}`, server };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
while (servers.length > 0) {
|
||||
const server = servers.pop();
|
||||
if (!server) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
describe('cliproxy local proxy route', () => {
|
||||
it('blocks requests when local-access enforcement fails', async () => {
|
||||
let backendHit = false;
|
||||
const backend = await createBackendServer((_req, res) => {
|
||||
backendHit = true;
|
||||
res.writeHead(200).end('ok');
|
||||
});
|
||||
const proxy = await createProxyServer({
|
||||
resolveTargetPort: () => backend.port,
|
||||
enforceAccess: (_req, res) => {
|
||||
res.status(403).json({ error: 'blocked' });
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const response = await fetch(`${proxy.baseUrl}/api/cliproxy-local/management.html`);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(await response.json()).toEqual({ error: 'blocked' });
|
||||
expect(backendHit).toBe(false);
|
||||
});
|
||||
|
||||
it('forwards JSON request bodies after express.json has parsed them', async () => {
|
||||
const backend = await createBackendServer((req, res) => {
|
||||
let body = '';
|
||||
req.setEncoding('utf8');
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
body: JSON.parse(body),
|
||||
method: req.method,
|
||||
path: req.url,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
const proxy = await createProxyServer({
|
||||
resolveTargetPort: () => backend.port,
|
||||
enforceAccess: () => true,
|
||||
});
|
||||
|
||||
const response = await fetch(`${proxy.baseUrl}/api/cliproxy-local/v0/management/test`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: true, ids: ['a', 'b'] }),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({
|
||||
body: { enabled: true, ids: ['a', 'b'] },
|
||||
method: 'PATCH',
|
||||
path: '/v0/management/test',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user