Skip to content

Commit 82f6499

Browse files
committed
http: normalize CONNECT request paths
Signed-off-by: Efe Karasakal <hi@efe.dev>
1 parent a46087d commit 82f6499

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

doc/api/http.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4113,6 +4113,9 @@ changes:
41134113
E.G. `'/index.html?page=12'`. An exception is thrown when the request path
41144114
contains illegal characters. Currently, only spaces are rejected but that
41154115
may change in the future. **Default:** `'/'`.
4116+
With `method` set to `'CONNECT'`, `path` must be an authority in the form
4117+
`host:port`, such as `'www.example.com:80'` or `'[2001:db8::1]:443'`.
4118+
Invalid values throw an `ERR_INVALID_ARG_VALUE` error.
41164119
The content in `path` is sent as the [request target][] in the HTTP 1.1 message.
41174120
When `path` is an absolute URL, this means the request target in the message in [absolute form][].
41184121
If the receiving server is a proxy, the server typically forwards the request to the

lib/_http_client.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
121121
});
122122

123123
const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
124+
const CONNECT_PATH_REGEX = /^(\[[^\]]+\]|[^:]+):(\d+)$/;
124125
const kError = Symbol('kError');
125126
const kPath = Symbol('kPath');
126127
const kAuthority = Symbol('kAuthority');
@@ -193,6 +194,27 @@ function authoritiesMatch(canonicalHost, hostFromHeader) {
193194
return parsed.host === canonicalHost;
194195
}
195196

197+
function isValidConnectPath(path) {
198+
const match = CONNECT_PATH_REGEX.exec(path);
199+
if (match === null) {
200+
return false;
201+
}
202+
203+
try {
204+
validatePort(match[2], 'options.path', false);
205+
const url = new URL(`http://${path}`);
206+
207+
return url.hostname !== '' &&
208+
url.username === '' &&
209+
url.password === '' &&
210+
url.pathname === '/' &&
211+
url.search === '' &&
212+
url.hash === '';
213+
} catch {
214+
return false;
215+
}
216+
}
217+
196218
// https://datatracker.ietf.org/doc/html/rfc9112#section-3.2
197219
// When the request target is in absolute-form, ensure it is consistent with
198220
// the request authority: same scheme, no userinfo, and an authority
@@ -466,7 +488,23 @@ function ClientRequest(input, options, cb) {
466488

467489
this.joinDuplicateHeaders = options.joinDuplicateHeaders;
468490

469-
this[kPath] = options.path || '/';
491+
let path = options.path || '/';
492+
if (method === 'CONNECT' && options.path != null) {
493+
path = String(options.path);
494+
if (path[0] === '/') {
495+
path = path.slice(1) || '/';
496+
}
497+
498+
if (!isValidConnectPath(path)) {
499+
throw new ERR_INVALID_ARG_VALUE(
500+
'options.path',
501+
path,
502+
'must be a valid host:port combo',
503+
);
504+
}
505+
}
506+
507+
this[kPath] = path;
470508
if (cb) {
471509
this.once('response', cb);
472510
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
7+
for (const path of [
8+
'',
9+
'example.com',
10+
'example.com:0',
11+
'example.com:65536',
12+
'example.com:8080/example',
13+
'evil.com:666/good.org:777',
14+
'/example.com',
15+
]) {
16+
assert.throws(() => http.request({
17+
method: 'CONNECT',
18+
path,
19+
}), {
20+
code: 'ERR_INVALID_ARG_VALUE',
21+
name: 'TypeError',
22+
message: /^The property 'options\.path' must be a valid host:port combo\./,
23+
});
24+
}
25+
26+
{
27+
const server = http.createServer(common.mustNotCall());
28+
29+
server.on('connect', common.mustCall((req, socket) => {
30+
assert.strictEqual(req.url, 'example.com:80');
31+
socket.end('HTTP/1.1 501 Not Implemented\r\n\r\n');
32+
}));
33+
34+
server.listen(0, common.mustCall(() => {
35+
const port = server.address().port;
36+
const req = http.request(
37+
new URL(`http://localhost:${port}/example.com:80`),
38+
{ method: 'CONNECT' },
39+
);
40+
41+
req.on('connect', common.mustCall((res, socket) => {
42+
assert.strictEqual(res.statusCode, 501);
43+
socket.destroy();
44+
server.close();
45+
}));
46+
47+
req.end();
48+
}));
49+
}
50+
51+
{
52+
const server = http.createServer(common.mustNotCall());
53+
54+
server.on('connect', common.mustCall((req, socket) => {
55+
assert.strictEqual(req.url, '[2001:db8::1]:111');
56+
socket.end('HTTP/1.1 501 Not Implemented\r\n\r\n');
57+
}));
58+
59+
server.listen(0, common.mustCall(() => {
60+
const req = http.request({
61+
host: 'localhost',
62+
port: server.address().port,
63+
method: 'CONNECT',
64+
path: '[2001:db8::1]:111',
65+
});
66+
67+
req.on('connect', common.mustCall((res, socket) => {
68+
assert.strictEqual(res.statusCode, 501);
69+
socket.destroy();
70+
server.close();
71+
}));
72+
73+
req.end();
74+
}));
75+
}

0 commit comments

Comments
 (0)