Skip to content

Commit 7b58fae

Browse files
committed
http: lenient header value parsing flag
Introduce `llhttp_set_lenient` API method for enabling/disabling lenient parsing mode for header value. With lenient parsing on - no token check would be performed on the header value. This mode was originally introduced to http-parser in nodejs/http-parser@e2e467b9 in order to provide a fallback mode for less compliant clients/servers.
1 parent d8d480a commit 7b58fae

8 files changed

Lines changed: 91 additions & 8 deletions

File tree

src/llhttp/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export enum FLAGS {
4848
CONTENT_LENGTH = 1 << 5,
4949
SKIPBODY = 1 << 6,
5050
TRAILING = 1 << 7,
51+
LENIENT = 1 << 8,
5152
}
5253

5354
export enum METHODS {

src/llhttp/http.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const NODES: ReadonlyArray<string> = [
5454
'header_value_start',
5555
'header_value',
5656
'header_value_otherwise',
57+
'header_value_lenient',
5758
'header_value_lws',
5859
'header_value_te_chunked',
5960
'header_value_content_length_once',
@@ -177,7 +178,7 @@ export class HTTP {
177178
p.property('i8', 'http_major');
178179
p.property('i8', 'http_minor');
179180
p.property('i8', 'header_state');
180-
p.property('i8', 'flags');
181+
p.property('i16', 'flags');
181182
p.property('i8', 'upgrade');
182183
p.property('i16', 'status_code');
183184
p.property('i8', 'finish');
@@ -491,12 +492,19 @@ export class HTTP {
491492
.match(HEADER_CHARS, n('header_value'))
492493
.otherwise(n('header_value_otherwise'));
493494

495+
const checkLenient = this.testFlags(FLAGS.LENIENT, {
496+
1: n('header_value_lenient'),
497+
}, p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header value char'));
498+
494499
n('header_value_otherwise')
495500
.peek('\r', span.headerValue.end().skipTo(n('header_value_almost_done')))
496501
.peek('\n', span.headerValue.end(n('header_value_almost_done')))
497-
// TODO(indutny): do we need `lenient` option? (it is always off now)
498-
.otherwise(p.error(ERROR.INVALID_HEADER_TOKEN,
499-
'Invalid header value char'));
502+
.otherwise(checkLenient);
503+
504+
n('header_value_lenient')
505+
.peek('\r', span.headerValue.end().skipTo(n('header_value_almost_done')))
506+
.peek('\n', span.headerValue.end(n('header_value_almost_done')))
507+
.skipTo(n('header_value_lenient'));
500508

501509
n('header_value_almost_done')
502510
.match('\n', n('header_value_lws'))

src/native/api.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ const char* llhttp_method_name(llhttp_method_t method) {
127127
}
128128

129129

130+
void llhttp_set_lenient(llhttp_t* parser, int enabled) {
131+
if (enabled) {
132+
parser->flags |= F_LENIENT;
133+
} else {
134+
parser->flags &= ~F_LENIENT;
135+
}
136+
}
137+
138+
130139
/* Callbacks */
131140

132141

src/native/api.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,18 @@ const char* llhttp_errno_name(llhttp_errno_t err);
141141
/* Returns textual name of HTTP method */
142142
const char* llhttp_method_name(llhttp_method_t method);
143143

144+
145+
/* Enables/disables lenient header value parsing (disabled by default).
146+
*
147+
* Lenient parsing disables header value token checks, extending llhttp's
148+
* protocol support to highly non-compliant clients/server. No
149+
* `HPE_INVALID_HEADER_TOKEN` will be raised for incorrect header values when
150+
* lenient parsing is "on".
151+
*
152+
* **(USE AT YOUR OWN RISK)**
153+
*/
154+
void llhttp_set_lenient(llhttp_t* parser, int enabled);
155+
144156
#ifdef __cplusplus
145157
} /* extern "C" */
146158
#endif

test/fixtures/extra.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ void llhttp__test_init_response(llparse_t* s) {
5757
}
5858

5959

60+
void llhttp__test_init_request_lenient(llparse_t* s) {
61+
llhttp__test_init_request(s);
62+
s->flags |= F_LENIENT;
63+
}
64+
65+
6066
void llhttp__test_finish(llparse_t* s) {
6167
llparse__print(NULL, NULL, "finish=%d", s->finish);
6268
}

test/fixtures/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import * as path from 'path';
88

99
import * as llhttp from '../../src/llhttp';
1010

11-
export type TestType = 'request' | 'response' | 'request-finish' |
12-
'response-finish' | 'none' | 'url';
11+
export type TestType = 'request' | 'response' | 'request-lenient' |
12+
'request-finish' | 'response-finish' | 'none' | 'url';
1313

1414
export { FixtureResult };
1515

@@ -58,8 +58,9 @@ export function build(llparse: LLParse, node: any, outFile: string,
5858
}
5959

6060
const extra = options.extra === undefined ? [] : options.extra.slice();
61-
if (ty === 'request' || ty === 'response') {
62-
extra.push(`-DLLPARSE__TEST_INIT=llhttp__test_init_${ty}`);
61+
if (ty === 'request' || ty === 'response' || ty === 'request-lenient') {
62+
extra.push(
63+
`-DLLPARSE__TEST_INIT=llhttp__test_init_${ty.replace(/-/g, '_')}`);
6364
} else if (ty === 'request-finish' || ty === 'response-finish') {
6465
if (ty === 'request-finish') {
6566
extra.push('-DLLPARSE__TEST_INIT=llhttp__test_init_request');

test/md-test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const http: IFixtureMap = {
7878
'none': buildMode('loose', 'none'),
7979
'request': buildMode('loose', 'request'),
8080
'request-finish': buildMode('loose', 'request-finish'),
81+
'request-lenient': buildMode('loose', 'request-lenient'),
8182
'response': buildMode('loose', 'response'),
8283
'response-finish': buildMode('loose', 'response-finish'),
8384
'url': buildMode('loose', 'url'),
@@ -86,6 +87,7 @@ const http: IFixtureMap = {
8687
'none': buildMode('strict', 'none'),
8788
'request': buildMode('strict', 'request'),
8889
'request-finish': buildMode('strict', 'request-finish'),
90+
'request-lenient': buildMode('strict', 'request-lenient'),
8991
'response': buildMode('strict', 'response'),
9092
'response-finish': buildMode('strict', 'response-finish'),
9193
'url': buildMode('strict', 'url'),
@@ -143,6 +145,8 @@ function run(name: string): void {
143145
types.push('response');
144146
} else if (meta.type === 'request-only') {
145147
types = [ 'request' ];
148+
} else if (meta.type === 'request-lenient') {
149+
types = [ 'request-lenient' ];
146150
} else if (meta.type === 'response-only') {
147151
types = [ 'response' ];
148152
} else if (meta.type === 'request-finish') {
@@ -231,6 +235,7 @@ function run(name: string): void {
231235
}
232236

233237
run('request/sample');
238+
run('request/lenient');
234239
run('request/method');
235240
run('request/uri');
236241
run('request/connection');

test/request/lenient.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
Lenient header value parsing
2+
============================
3+
4+
Parsing with header value token checks off.
5+
6+
## Header value with lenient
7+
8+
<!-- meta={"type": "request-lenient"} -->
9+
```http
10+
GET /url HTTP/1.1
11+
Header1: \f
12+
13+
14+
```
15+
16+
```log
17+
off=0 message begin
18+
off=4 len=4 span[url]="/url"
19+
off=19 len=7 span[header_field]="Header1"
20+
off=28 len=1 span[header_value]="\f"
21+
off=33 headers complete method=1 v=1/1 flags=100 content_length=0
22+
off=33 message complete
23+
```
24+
25+
## Header value without lenient
26+
27+
<!-- meta={"type": "request"} -->
28+
```http
29+
GET /url HTTP/1.1
30+
Header1: \f
31+
32+
33+
34+
```
35+
36+
```log
37+
off=0 message begin
38+
off=4 len=4 span[url]="/url"
39+
off=19 len=7 span[header_field]="Header1"
40+
off=28 error code=10 reason="Invalid header value char"
41+
```

0 commit comments

Comments
 (0)