mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-11 01:25:03 +00:00
fix(sharing): answer malformed request URLs instead of crashing the server
handle() is dispatched via `void`, so a throw before its try/catch is an unhandled rejection on a LAN-facing server. A request target the HTTP parser accepts but the WHATWG URL parser rejects (unterminated IPv6 host like //[::1) threw at new URL() and hung/killed the process. Parse inside a guard and answer 400. Mutation-checked: the test times out with an unhandled error before the fix, passes after.
This commit is contained in:
parent
2c3319b286
commit
2b49608fd2
2 changed files with 70 additions and 1 deletions
|
|
@ -73,11 +73,22 @@ export class ShareServer {
|
|||
}
|
||||
|
||||
private async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
const url = new URL(req.url ?? '/', 'https://localhost')
|
||||
const json = (code: number, body: unknown): void => {
|
||||
res.writeHead(code, { 'content-type': 'application/json' })
|
||||
res.end(JSON.stringify(body))
|
||||
}
|
||||
// handle() is dispatched with `void` (see the createServer callback), so a
|
||||
// throw here is an UNHANDLED rejection, not a caught 500. A request target
|
||||
// the HTTP parser accepts but the WHATWG URL parser rejects - e.g. an
|
||||
// unterminated IPv6 host like `//[::1` - would otherwise crash this
|
||||
// LAN-facing server. Parse inside the guard and answer 400 instead.
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(req.url ?? '/', 'https://localhost')
|
||||
} catch {
|
||||
json(400, { error: 'malformed request URL' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
await this.route(url, req, res, json)
|
||||
} catch (err) {
|
||||
|
|
|
|||
58
tests/sharing/malformed-request.test.ts
Normal file
58
tests/sharing/malformed-request.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { connect as tlsConnect } from 'tls'
|
||||
|
||||
import { generateIdentity, type Identity } from '../../src/sharing/identity.js'
|
||||
import { PeerStore } from '../../src/sharing/pairing.js'
|
||||
import { ShareServer } from '../../src/sharing/share-server.js'
|
||||
|
||||
// The share server listens on the LAN for device pairing and is dispatched via
|
||||
// `void this.handle(...)`, so a throw inside handle() is an UNHANDLED rejection.
|
||||
// A request target the HTTP parser accepts but the WHATWG URL parser rejects
|
||||
// (an unterminated IPv6 host) used to throw at `new URL(...)` before the
|
||||
// try/catch, which could crash the host process. The server must instead answer
|
||||
// and stay alive.
|
||||
describe('share server: malformed request URL does not crash the process', () => {
|
||||
let server: ShareServer
|
||||
let serverId: Identity
|
||||
let clientId: Identity
|
||||
let port: number
|
||||
|
||||
beforeAll(async () => {
|
||||
serverId = await generateIdentity('Server')
|
||||
clientId = await generateIdentity('Client')
|
||||
server = new ShareServer({ identity: serverId, peers: new PeerStore(), getUsage: async () => ({ current: { cost: 1 } }) })
|
||||
port = await server.listen(0, '127.0.0.1')
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close()
|
||||
})
|
||||
|
||||
// Send one raw HTTP request line over mTLS and resolve with the response head.
|
||||
function rawRequest(line: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = tlsConnect(
|
||||
{ host: '127.0.0.1', port, key: clientId.key, cert: clientId.cert, rejectUnauthorized: false },
|
||||
() => socket.write(`${line}\r\nHost: localhost\r\nConnection: close\r\n\r\n`),
|
||||
)
|
||||
let buf = ''
|
||||
socket.setTimeout(4000, () => { socket.destroy(); reject(new Error('timed out (server hung)')) })
|
||||
socket.on('data', (d) => { buf += d.toString() })
|
||||
socket.on('end', () => resolve(buf))
|
||||
socket.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
it('answers an unterminated-IPv6 target instead of hanging or crashing', async () => {
|
||||
// `new URL('//[::1', 'https://localhost')` throws TypeError; llhttp accepts
|
||||
// the target, so this exercises the exact pre-try throw path.
|
||||
const res = await rawRequest('GET //[::1 HTTP/1.1')
|
||||
expect(res).toMatch(/^HTTP\/1\.1 400/)
|
||||
})
|
||||
|
||||
it('is still alive for a valid request afterward', async () => {
|
||||
const res = await rawRequest('GET /api/peer/hello HTTP/1.1')
|
||||
expect(res).toMatch(/^HTTP\/1\.1 200/)
|
||||
expect(res).toContain(serverId.fingerprint)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue