Skip to content

Commit 6d04465

Browse files
authored
feat: Drop strict mode in favor of leniency flags. (#234)
* feat: Drop strict mode in favor of leniency flags. * chore: Simplified build script. * fix: Removed leftover. * chore: Updated benchmark script. * chore: Updated docs.
1 parent 78af631 commit 6d04465

23 files changed

+786
-538
lines changed

README.md

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ protocol support to highly non-compliant clients/server.
287287
No `HPE_INVALID_HEADER_TOKEN` will be raised for incorrect header values when
288288
lenient parsing is "on".
289289
290-
**USE AT YOUR OWN RISK!**
290+
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
291291
292292
### `void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled)`
293293
@@ -300,23 +300,22 @@ conjunction with `Content-Length`.
300300
This error is important to prevent HTTP request smuggling, but may be less desirable
301301
for small number of cases involving legacy servers.
302302
303-
**USE AT YOUR OWN RISK!**
303+
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
304304
305305
### `void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled)`
306306
307307
Enables/disables lenient handling of `Connection: close` and HTTP/1.0
308308
requests responses.
309309
310-
Normally `llhttp` would error on (in strict mode) or discard (in loose mode)
311-
the HTTP request/response after the request/response with `Connection: close`
312-
and `Content-Length`.
310+
Normally `llhttp` would error the HTTP request/response
311+
after the request/response with `Connection: close` and `Content-Length`.
313312
314313
This is important to prevent cache poisoning attacks,
315314
but might interact badly with outdated and insecure clients.
316315
317316
With this flag the extra request/response will be parsed normally.
318317
319-
**USE AT YOUR OWN RISK!**
318+
**Enabling this flag can pose a security issue since you will be exposed to poisoning attacks. USE WITH CAUTION!**
320319
321320
### `void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, int enabled)`
322321
@@ -331,7 +330,48 @@ avoid request smuggling.
331330
332331
With this flag the extra value will be parsed normally.
333332
334-
**USE AT YOUR OWN RISK!**
333+
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
334+
335+
### `void llhttp_set_lenient_version(llhttp_t* parser, int enabled)`
336+
337+
Enables/disables lenient handling of HTTP version.
338+
339+
Normally `llhttp` would error when the HTTP version in the request or status line
340+
is not `0.9`, `1.0`, `1.1` or `2.0`.
341+
With this flag the extra value will be parsed normally.
342+
343+
**Enabling this flag can pose a security issue since you will allow unsupported HTTP versions. USE WITH CAUTION!**
344+
345+
### `void llhttp_set_lenient_data_after_close(llhttp_t* parser, int enabled)`
346+
347+
Enables/disables lenient handling of additional data received after a message ends
348+
and keep-alive is disabled.
349+
350+
Normally `llhttp` would error when additional unexpected data is received if the message
351+
contains the `Connection` header with `close` value.
352+
With this flag the extra data will discarded without throwing an error.
353+
354+
**Enabling this flag can pose a security issue since you will be exposed to poisoning attacks. USE WITH CAUTION!**
355+
356+
### `void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, int enabled)`
357+
358+
Enables/disables lenient handling of incomplete CRLF sequences.
359+
360+
Normally `llhttp` would error when a CR is not followed by LF when terminating the
361+
request line, the status line, the headers or a chunk header.
362+
With this flag only a CR is required to terminate such sections.
363+
364+
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
365+
366+
### `void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, int enabled)`
367+
368+
Enables/disables lenient handling of chunks not separated via CRLF.
369+
370+
Normally `llhttp` would error when after a chunk data a CRLF is missing before
371+
starting a new chunk.
372+
With this flag the new chunk can start immediately after the previous one.
373+
374+
**Enabling this flag can pose a security issue since you will be exposed to request smuggling attacks. USE WITH CAUTION!**
335375
336376
## Build Instructions
337377

bench/index.ts

Lines changed: 57 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,71 @@
1-
// NOTE: run `npm test` to build `./test/tmp/http-loose-request`
1+
import * as assert from "assert";
2+
import { spawnSync } from "child_process";
3+
import { existsSync } from "fs";
4+
import { resolve } from "path";
25

3-
import * as assert from 'assert';
4-
import { spawnSync } from 'child_process';
5-
import { existsSync } from 'fs';
6-
7-
const isURL = !process.argv[2] || process.argv[2] === 'url';
8-
const isHTTP = !process.argv[2] || process.argv[2] === 'http';
6+
function request(tpl: TemplateStringsArray): string {
7+
return tpl.raw[0].replace(/^\s+/gm, '').replace(/\n/gm, '').replace(/\\r/gm, '\r').replace(/\\n/gm, '\n')
8+
}
99

10-
const requests: Map<string, string> = new Map();
10+
const urlExecutable = resolve(__dirname, "../test/tmp/url-url-c");
11+
const httpExecutable = resolve(__dirname, "../test/tmp/http-request-c");
12+
13+
const httpRequests: Record<string, string> = {
14+
"seanmonstar/httparse": request`
15+
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n
16+
Host: www.kittyhell.com\r\n
17+
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n
18+
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
19+
Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n
20+
Accept-Encoding: gzip,deflate\r\n
21+
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n
22+
Keep-Alive: 115\r\n
23+
Connection: keep-alive\r\n
24+
Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral\r\n\r\n
25+
`,
26+
"nodejs/http-parser": request`
27+
POST /joyent/http-parser HTTP/1.1\r\n
28+
Host: github.com\r\n
29+
DNT: 1\r\n
30+
Accept-Encoding: gzip, deflate, sdch\r\n
31+
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n
32+
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)
33+
AppleWebKit/537.36 (KHTML, like Gecko)
34+
Chrome/39.0.2171.65 Safari/537.36\r\n
35+
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,
36+
image/webp,*/*;q=0.8\r\n
37+
Referer: https://github.com/joyent/http-parser\r\n
38+
Connection: keep-alive\r\n
39+
Transfer-Encoding: chunked\r\n
40+
Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n\r\n
41+
`
42+
}
43+
const urlRequest = "http://example.com/path/to/file?query=value#fragment";
1144

12-
if (!existsSync('./test/tmp/http-loose-request.c')) {
13-
console.error('Run npm test to build ./test/tmp/http-loose-request');
45+
if (!existsSync(urlExecutable) || !existsSync(urlExecutable)) {
46+
console.error(
47+
"\x1b[31m\x1b[1mPlease run npm test in order to create required executables."
48+
);
1449
process.exit(1);
1550
}
1651

17-
requests.set('seanmonstar/httparse',
18-
'GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n' +
19-
'Host: www.kittyhell.com\r\n' +
20-
'User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n' +
21-
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n' +
22-
'Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n' +
23-
'Accept-Encoding: gzip,deflate\r\n' +
24-
'Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n' +
25-
'Keep-Alive: 115\r\n' +
26-
'Connection: keep-alive\r\n' +
27-
'Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral\r\n\r\n');
28-
29-
requests.set('nodejs/http-parser',
30-
'POST /joyent/http-parser HTTP/1.1\r\n' +
31-
'Host: github.com\r\n' +
32-
'DNT: 1\r\n' +
33-
'Accept-Encoding: gzip, deflate, sdch\r\n' +
34-
'Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n' +
35-
'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) ' +
36-
'AppleWebKit/537.36 (KHTML, like Gecko) ' +
37-
'Chrome/39.0.2171.65 Safari/537.36\r\n' +
38-
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,' +
39-
'image/webp,*/*;q=0.8\r\n' +
40-
'Referer: https://github.com/joyent/http-parser\r\n' +
41-
'Connection: keep-alive\r\n' +
42-
'Transfer-Encoding: chunked\r\n' +
43-
'Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n\r\n');
44-
45-
if (process.argv[2] === 'loop') {
52+
if (process.argv[2] === "loop") {
4653
const reqName = process.argv[3];
47-
assert(requests.has(reqName), `Unknown request name: "${reqName}"`);
48-
49-
const request = requests.get(reqName)!;
50-
spawnSync('./test/tmp/http-loose-request', [
51-
'loop',
52-
request
53-
], { stdio: 'inherit' });
54+
const request = httpRequests[reqName]!;
55+
56+
assert(request, `Unknown request name: "${reqName}"`);
57+
spawnSync(httpExecutable, ["loop", request], { stdio: "inherit" });
5458
process.exit(0);
5559
}
5660

57-
if (isURL) {
58-
console.log('url loose (C)');
59-
60-
spawnSync('./test/tmp/url-loose-url-c', [
61-
'bench',
62-
'http://example.com/path/to/file?query=value#fragment'
63-
], { stdio: 'inherit' });
64-
65-
console.log('url strict (C)');
66-
67-
spawnSync('./test/tmp/url-strict-url-c', [
68-
'bench',
69-
'http://example.com/path/to/file?query=value#fragment'
70-
], { stdio: 'inherit' });
61+
if (!process.argv[2] || process.argv[2] === "url") {
62+
console.log("url (C)");
63+
spawnSync(urlExecutable, ["bench", urlRequest], { stdio: "inherit" });
7164
}
7265

73-
if (isHTTP) {
74-
for(const [name, request] of requests) {
75-
console.log('http loose: "%s" (C)', name);
76-
77-
spawnSync('./test/tmp/http-loose-request-c', [
78-
'bench',
79-
request
80-
], { stdio: 'inherit' });
81-
82-
console.log('http strict: "%s" (C)', name);
83-
84-
spawnSync('./test/tmp/http-strict-request-c', [
85-
'bench',
86-
request
87-
], { stdio: 'inherit' });
66+
if (!process.argv[2] || process.argv[2] === "http") {
67+
for (const [name, request] of Object.entries(httpRequests)) {
68+
console.log('http: "%s" (C)', name);
69+
spawnSync(httpExecutable, ["bench", request], { stdio: "inherit" });
8870
}
8971
}

bin/generate.ts

Lines changed: 35 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,47 @@
11
#!/usr/bin/env -S npx ts-node
2-
import * as fs from 'fs';
3-
import { LLParse } from 'llparse';
4-
import * as path from 'path';
5-
import * as semver from 'semver';
6-
7-
import * as llhttp from '../src/llhttp';
8-
9-
const pkgFile = path.join(__dirname, '..', 'package.json');
10-
const pkg = JSON.parse(fs.readFileSync(pkgFile).toString());
11-
12-
const BUILD_DIR = path.join(__dirname, '..', 'build');
13-
const C_DIR = path.join(BUILD_DIR, 'c');
14-
const SRC_DIR = path.join(__dirname, '..', 'src');
15-
16-
const C_FILE = path.join(C_DIR, 'llhttp.c');
17-
const HEADER_FILE = path.join(BUILD_DIR, 'llhttp.h');
18-
19-
for (const dir of [ BUILD_DIR, C_DIR ]) {
20-
try {
21-
fs.mkdirSync(dir);
22-
} catch (e) {
23-
// no-op
24-
}
25-
}
26-
27-
function build(mode: 'strict' | 'loose') {
28-
const llparse = new LLParse('llhttp__internal');
29-
const instance = new llhttp.HTTP(llparse, mode);
30-
31-
return llparse.build(instance.build().entry, {
32-
c: {
33-
header: 'llhttp',
34-
},
35-
debug: process.env.LLPARSE_DEBUG ? 'llhttp__debug' : undefined,
36-
headerGuard: 'INCLUDE_LLHTTP_ITSELF_H_',
37-
});
38-
}
39-
40-
function guard(strict: string, loose: string): string {
41-
let out = '';
422

43-
if (strict === loose) {
44-
return strict;
45-
}
46-
47-
out += '#if LLHTTP_STRICT_MODE\n';
48-
out += '\n';
49-
out += strict + '\n';
50-
out += '\n';
51-
out += '#else /* !LLHTTP_STRICT_MODE */\n';
52-
out += '\n';
53-
out += loose + '\n';
54-
out += '\n';
55-
out += '#endif /* LLHTTP_STRICT_MODE */\n';
56-
57-
return out;
58-
}
59-
60-
const artifacts = {
61-
loose: build('loose'),
62-
strict: build('strict'),
63-
};
64-
65-
let headers = '';
66-
67-
headers += '#ifndef INCLUDE_LLHTTP_H_\n';
68-
headers += '#define INCLUDE_LLHTTP_H_\n';
69-
70-
headers += '\n';
71-
72-
const version = semver.parse(pkg.version)!;
3+
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
4+
import { LLParse } from 'llparse';
5+
import { dirname, resolve } from 'path';
6+
import { parse } from 'semver';
7+
import { CHeaders, HTTP } from '../src/llhttp';
738

74-
headers += `#define LLHTTP_VERSION_MAJOR ${version.major}\n`;
75-
headers += `#define LLHTTP_VERSION_MINOR ${version.minor}\n`;
76-
headers += `#define LLHTTP_VERSION_PATCH ${version.patch}\n`;
77-
headers += '\n';
9+
const C_FILE = resolve(__dirname, '../build/c/llhttp.c');
10+
const HEADER_FILE = resolve(__dirname, '../build/llhttp.h');
7811

79-
headers += '#ifndef LLHTTP_STRICT_MODE\n';
80-
headers += '# define LLHTTP_STRICT_MODE 0\n';
81-
headers += '#endif\n';
82-
headers += '\n';
12+
const pkg = JSON.parse(
13+
readFileSync(resolve(__dirname, '..', 'package.json')).toString(),
14+
);
15+
const version = parse(pkg.version)!;
16+
const llparse = new LLParse('llhttp__internal');
8317

84-
const cHeaders = new llhttp.CHeaders();
18+
const cHeaders = new CHeaders();
19+
const nativeHeaders = readFileSync(resolve(__dirname, '../src/native/api.h'));
20+
const generated = llparse.build(new HTTP(llparse).build().entry, {
21+
c: {
22+
header: 'llhttp',
23+
},
24+
debug: process.env.LLPARSE_DEBUG ? 'llhttp__debug' : undefined,
25+
headerGuard: 'INCLUDE_LLHTTP_ITSELF_H_',
26+
});
8527

86-
headers += guard(artifacts.strict.header, artifacts.loose.header);
28+
const headers = `
29+
#ifndef INCLUDE_LLHTTP_H_
30+
#define INCLUDE_LLHTTP_H_
8731
88-
headers += '\n';
32+
#define LLHTTP_VERSION_MAJOR ${version.major}
33+
#define LLHTTP_VERSION_MINOR ${version.minor}
34+
#define LLHTTP_VERSION_PATCH ${version.patch}
8935
90-
headers += cHeaders.build();
36+
${generated.header}
9137
92-
headers += '\n';
38+
${cHeaders.build()}
9339
94-
headers += fs.readFileSync(path.join(SRC_DIR, 'native', 'api.h'));
40+
${nativeHeaders}
9541
96-
headers += '\n';
97-
headers += '#endif /* INCLUDE_LLHTTP_H_ */\n';
42+
#endif /* INCLUDE_LLHTTP_H_ */
43+
`;
9844

99-
fs.writeFileSync(C_FILE,
100-
guard(artifacts.strict.c || '', artifacts.loose.c || ''));
101-
fs.writeFileSync(HEADER_FILE, headers);
45+
mkdirSync(dirname(C_FILE), { recursive: true });
46+
writeFileSync(HEADER_FILE, headers);
47+
writeFileSync(C_FILE, generated.c);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"clean": "rm -rf lib && rm -rf test/tmp",
1919
"prepare": "npm run clean && npm run build-ts",
2020
"lint": "tslint -c tslint.json bin/*.ts src/*.ts src/**/*.ts test/*.ts test/**/*.ts",
21+
"lint-fix": "tslint --fix -c tslint.json bin/*.ts src/*.ts src/**/*.ts test/*.ts test/**/*.ts",
2122
"mocha": "mocha --timeout=10000 -r ts-node/register/type-check --reporter progress test/*-test.ts",
2223
"test": "npm run mocha && npm run lint",
2324
"postversion": "RELEASE=`node -e \"process.stdout.write(require('./package').version)\"` make -B postversion",

src/llhttp.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import * as constants from './llhttp/constants';
22

3-
import HTTPMode = constants.HTTPMode;
4-
53
export { constants };
6-
export { HTTPMode };
74

85
export { HTTP } from './llhttp/http';
96
export { URL } from './llhttp/url';

0 commit comments

Comments
 (0)