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
5 changes: 5 additions & 0 deletions .changeset/rc-tunnel-base64-linear-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix Remote Control uploads larger than ~3.5MB always failing with a 400 error.
35 changes: 32 additions & 3 deletions packages/remote-control/src/remote-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,12 @@ class RemoteControlClient {
) {
throw new SyntaxError('invalid HTTP tunnel request message');
}
const chunk = decodeBase64(parsed['body_base64']);
const bodyBase64 = parsed['body_base64'];
const minDecodedBytes = Math.floor(bodyBase64.length / 4) * 3 - 2;
if (this.pendingHttpBytes + minDecodedBytes > MAX_HTTP_REQUEST_BYTES) {
throw new SyntaxError('HTTP tunnel request exceeds 10 MiB');
}
const chunk = decodeBase64(bodyBase64);
const pending = this.pendingHttpRequests.get(requestId) ?? { chunks: [], size: 0 };
if (this.pendingHttpBytes + chunk.length > MAX_HTTP_REQUEST_BYTES) {
throw new SyntaxError('HTTP tunnel request exceeds 10 MiB');
Expand All @@ -557,7 +562,8 @@ class RemoteControlClient {
} catch (error) {
if (requestId !== undefined) {
this.clearPendingHttpRequest(requestId);
this.sendHttpResponse(requestId, buildErrorResponse(400));
const status = error instanceof SyntaxError ? 400 : 502;
this.sendHttpResponse(requestId, buildErrorResponse(status));
}
this.stderr.write(`Remote Control HTTP message error: ${errorMessage(error)}\n`);
}
Expand Down Expand Up @@ -1023,7 +1029,30 @@ function stringField(
}

function decodeBase64(value: string): Buffer {
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
if (value.length % 4 !== 0) {
throw new SyntaxError('invalid HTTP tunnel request base64');
}
let paddingStart = -1;
for (let i = 0; i < value.length; i++) {
const c = value.codePointAt(i)!;
if (c === 0x3d) {
if (paddingStart === -1) paddingStart = i;
continue;
}
if (paddingStart !== -1) {
throw new SyntaxError('invalid HTTP tunnel request base64');
}
const ok =
(c >= 0x41 && c <= 0x5a) ||
(c >= 0x61 && c <= 0x7a) ||
(c >= 0x30 && c <= 0x39) ||
c === 0x2b ||
c === 0x2f;
if (!ok) {
throw new SyntaxError('invalid HTTP tunnel request base64');
}
}
if (paddingStart !== -1 && value.length - paddingStart > 2) {
throw new SyntaxError('invalid HTTP tunnel request base64');
}
return Buffer.from(value, 'base64');
Expand Down
39 changes: 33 additions & 6 deletions packages/remote-control/test/remote-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ describe('Remote Control tunnel', () => {
);

let localHttpRequest: IncomingMessage | undefined;
let localHttpBodyBytes = 0;
let localWsRequest: IncomingMessage | undefined;
const localWsServer = new WebSocketServer({ noServer: true });
const assetJs = `const boot = "/assets/boot.js";\n${'const chunk = "/assets/chunk.js";\n'.repeat(120)}`;
Expand Down Expand Up @@ -307,13 +308,20 @@ describe('Remote Control tunnel', () => {
response.end(assetText);
return;
}
response.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'public, max-age=31536000, immutable',
Connection: 'X-Remove',
'X-Remove': 'gone',
let bodyBytes = 0;
request.on('data', (chunk: Buffer) => {
bodyBytes += chunk.length;
});
request.on('end', () => {
localHttpBodyBytes = bodyBytes;
response.writeHead(200, {
'Content-Type': 'text/html',
'Cache-Control': 'public, max-age=31536000, immutable',
Connection: 'X-Remove',
'X-Remove': 'gone',
});
response.end('<html><head></head><script src="/boot.js"></script></html>');
});
response.end('<html><head></head><script src="/boot.js"></script></html>');
});
localServer.on('upgrade', (request, socket, head) => {
localWsRequest = request;
Expand Down Expand Up @@ -547,6 +555,25 @@ describe('Remote Control tunnel', () => {
expect(rangeHead).not.toContain('Content-Encoding');
expect(rangeResponse.subarray(rangeSeparator + 4).toString()).toBe(assetText);

const largeBody = Buffer.alloc(4 * 1024 * 1024 + 512 * 1024, 0x61);
const largeRequest = Buffer.concat([
Buffer.from(`POST /upload HTTP/1.1\r\nHost: relay.test\r\nContent-Length: ${largeBody.length}\r\n\r\n`),
largeBody,
]);
const largeResponsePromise = nextJsonMessage(httpConnections[0]!);
httpConnections[0]!.send(
JSON.stringify({
request_id: 'request-8',
type: 'request',
is_last: true,
body_base64: largeRequest.toString('base64'),
}),
);
const largeResponseMessage = await largeResponsePromise;
const largeResponse = Buffer.from(largeResponseMessage['body_base64'] as string, 'base64').toString();
expect(largeResponse).toContain('HTTP/1.1 200 OK');
expect(localHttpBodyBytes).toBe(largeBody.length);

managementConnections[0]!.send(
JSON.stringify({
type: 'open_ws',
Expand Down
Loading