Skip to content

Commit 13541bd

Browse files
authored
http: added chunked extensions parsing and related callbacks (#193)
1 parent 6bb6744 commit 13541bd

File tree

11 files changed

+688
-34
lines changed

11 files changed

+688
-34
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ The following callbacks can return `0` (proceed normally), `-1` (error) or `HPE_
109109
* `on_header_field_complete`: Invoked after a header name has been parsed.
110110
* `on_header_value_complete`: Invoked after a header value has been parsed.
111111
* `on_chunk_header`: Invoked after a new chunk is started. The current chunk length is stored in `parser->content_length`.
112+
* `on_chunk_extension_name_complete`: Invoked after a chunk extension name is started.
113+
* `on_chunk_extension_value_complete`: Invoked after a chunk extension value is started.
112114
* `on_chunk_complete`: Invoked after a new chunk is received.
113115
* `on_reset`: Invoked after `on_message_complete` and before `on_message_begin` when a new message
114116
is received on the same parser. This is not invoked for the first message of the parser.
@@ -123,6 +125,8 @@ The following callbacks can return `0` (proceed normally), `-1` (error) or `HPE_
123125
* `on_version`: Invoked when another character of the version is received.
124126
* `on_header_field`: Invoked when another character of a header name is received.
125127
* `on_header_value`: Invoked when another character of a header value is received.
128+
* `on_chunk_extension_name`: Invoked when another character of a chunk extension name is received.
129+
* `on_chunk_extension_value`: Invoked when another character of a extension value is received.
126130
127131
The callback `on_headers_complete`, invoked when headers are completed, can return:
128132

src/llhttp/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export enum ERROR {
4242
CB_VERSION_COMPLETE = 33,
4343
CB_HEADER_FIELD_COMPLETE = 28,
4444
CB_HEADER_VALUE_COMPLETE = 29,
45+
CB_CHUNK_EXTENSION_NAME_COMPLETE = 34,
46+
CB_CHUNK_EXTENSION_VALUE_COMPLETE = 35,
4547
CB_RESET = 31,
4648
}
4749

@@ -430,6 +432,13 @@ for (let i = 32; i <= 255; i++) {
430432
export const CONNECTION_TOKEN_CHARS: CharList =
431433
HEADER_CHARS.filter((c: string | number) => c !== 44);
432434

435+
export const QUOTED_STRING: CharList = [ '\t', ' ' ];
436+
for (let i = 0x21; i <= 0xff; i++) {
437+
if (i !== 0x22 && i !== 0x5c) { // All characters in ASCII except \ and "
438+
QUOTED_STRING.push(i);
439+
}
440+
}
441+
433442
export const MAJOR = NUM_MAP;
434443
export const MINOR = MAJOR;
435444

src/llhttp/http.ts

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
HTTPMode,
1212
LENIENT_FLAGS,
1313
MAJOR, METHOD_MAP, METHODS, METHODS_HTTP, METHODS_ICE, METHODS_RTSP,
14-
MINOR, NUM_MAP, SPECIAL_HEADERS, STRICT_TOKEN,
15-
TOKEN, TYPE,
14+
MINOR, NUM_MAP, QUOTED_STRING, SPECIAL_HEADERS,
15+
STRICT_TOKEN, TOKEN, TYPE,
1616
} from './constants';
1717
import { URL } from './url';
1818

@@ -88,7 +88,11 @@ const NODES: ReadonlyArray<string> = [
8888
'chunk_size_otherwise',
8989
'chunk_size_almost_done',
9090
'chunk_size_almost_done_lf',
91-
'chunk_parameters',
91+
'chunk_extensions',
92+
'chunk_extension_name',
93+
'chunk_extension_value',
94+
'chunk_extension_quoted_value',
95+
'chunk_extension_quoted_value_done',
9296
'chunk_data',
9397
'chunk_data_almost_done',
9498
'chunk_data_almost_done_skip',
@@ -105,29 +109,33 @@ const NODES: ReadonlyArray<string> = [
105109
];
106110

107111
interface ISpanMap {
108-
readonly body: source.Span;
109-
readonly headerField: source.Span;
110-
readonly headerValue: source.Span;
111112
readonly status: source.Span;
112113
readonly method: source.Span;
113114
readonly version: source.Span;
115+
readonly headerField: source.Span;
116+
readonly headerValue: source.Span;
117+
readonly chunkExtensionName: source.Span;
118+
readonly chunkExtensionValue: source.Span;
119+
readonly body: source.Span;
114120
}
115121

116122
interface ICallbackMap {
117-
readonly afterHeadersComplete: source.code.Code;
118-
readonly afterMessageComplete: source.code.Code;
119-
readonly beforeHeadersComplete: source.code.Code;
120-
readonly onChunkComplete: source.code.Code;
121-
readonly onChunkHeader: source.code.Code;
122-
readonly onHeadersComplete: source.code.Code;
123123
readonly onMessageBegin: source.code.Code;
124-
readonly onMessageComplete: source.code.Code;
125124
readonly onUrlComplete: source.code.Code;
126125
readonly onMethodComplete: source.code.Code;
127126
readonly onVersionComplete: source.code.Code;
128127
readonly onStatusComplete: source.code.Code;
128+
readonly beforeHeadersComplete: source.code.Code;
129129
readonly onHeaderFieldComplete: source.code.Code;
130130
readonly onHeaderValueComplete: source.code.Code;
131+
readonly onHeadersComplete: source.code.Code;
132+
readonly afterHeadersComplete: source.code.Code;
133+
readonly onChunkHeader: source.code.Code;
134+
readonly onChunkExtensionName: source.code.Code;
135+
readonly onChunkExtensionValue: source.code.Code;
136+
readonly onChunkComplete: source.code.Code;
137+
readonly onMessageComplete: source.code.Code;
138+
readonly afterMessageComplete: source.code.Code;
131139
readonly onReset: source.code.Code;
132140
}
133141

@@ -167,6 +175,8 @@ export class HTTP {
167175

168176
this.span = {
169177
body: p.span(p.code.span('llhttp__on_body')),
178+
chunkExtensionName: p.span(p.code.span('llhttp__on_chunk_extension_name')),
179+
chunkExtensionValue: p.span(p.code.span('llhttp__on_chunk_extension_value')),
170180
headerField: p.span(p.code.span('llhttp__on_header_field')),
171181
headerValue: p.span(p.code.span('llhttp__on_header_value')),
172182
method: p.span(p.code.span('llhttp__on_method')),
@@ -187,6 +197,8 @@ export class HTTP {
187197
onMessageBegin: p.code.match('llhttp__on_message_begin'),
188198
onMessageComplete: p.code.match('llhttp__on_message_complete'),
189199
onChunkHeader: p.code.match('llhttp__on_chunk_header'),
200+
onChunkExtensionName: p.code.match('llhttp__on_chunk_extension_name_complete'),
201+
onChunkExtensionValue: p.code.match('llhttp__on_chunk_extension_value_complete'),
190202
onChunkComplete: p.code.match('llhttp__on_chunk_complete'),
191203
onReset: p.code.match('llhttp__on_reset'),
192204

@@ -855,15 +867,69 @@ export class HTTP {
855867

856868
n('chunk_size_otherwise')
857869
.match('\r', n('chunk_size_almost_done'))
858-
.match('; ', n('chunk_parameters'))
870+
.match(';', n('chunk_extensions'))
859871
.otherwise(p.error(ERROR.INVALID_CHUNK_SIZE,
860872
'Invalid character in chunk size'));
861873

862-
n('chunk_parameters')
874+
const onChunkExtensionNameCompleted = (destination: Node) => {
875+
return this.invokePausable(
876+
'on_chunk_extension_name', ERROR.CB_CHUNK_EXTENSION_NAME_COMPLETE, destination);
877+
};
878+
879+
const onChunkExtensionValueCompleted = (destination: Node) => {
880+
return this.invokePausable(
881+
'on_chunk_extension_value', ERROR.CB_CHUNK_EXTENSION_VALUE_COMPLETE, destination);
882+
};
883+
884+
n('chunk_extensions')
885+
.match(' ', p.error(ERROR.STRICT, 'Invalid character in chunk extensions'))
886+
.match('\r', p.error(ERROR.STRICT, 'Invalid character in chunk extensions'))
887+
.otherwise(this.span.chunkExtensionName.start(n('chunk_extension_name')));
888+
889+
n('chunk_extension_name')
890+
.match(STRICT_TOKEN, n('chunk_extension_name'))
891+
.peek('=', this.span.chunkExtensionName.end().skipTo(
892+
this.span.chunkExtensionValue.start(
893+
onChunkExtensionNameCompleted(n('chunk_extension_value')),
894+
),
895+
))
896+
.peek(';', this.span.chunkExtensionName.end().skipTo(
897+
onChunkExtensionNameCompleted(n('chunk_extensions')),
898+
))
899+
.peek('\r', this.span.chunkExtensionName.end().skipTo(
900+
onChunkExtensionNameCompleted(n('chunk_size_almost_done')),
901+
))
902+
.otherwise(this.span.chunkExtensionName.end().skipTo(
903+
p.error(ERROR.STRICT, 'Invalid character in chunk extensions name'),
904+
));
905+
906+
n('chunk_extension_value')
907+
.match('"', n('chunk_extension_quoted_value'))
908+
.match(STRICT_TOKEN, n('chunk_extension_value'))
909+
.peek(';', this.span.chunkExtensionValue.end().skipTo(
910+
onChunkExtensionValueCompleted(n('chunk_size_otherwise')),
911+
))
912+
.peek('\r', this.span.chunkExtensionValue.end().skipTo(
913+
onChunkExtensionValueCompleted(n('chunk_size_almost_done')),
914+
))
915+
.otherwise(this.span.chunkExtensionValue.end().skipTo(
916+
p.error(ERROR.STRICT, 'Invalid character in chunk extensions value'),
917+
));
918+
919+
n('chunk_extension_quoted_value')
920+
.match(QUOTED_STRING, n('chunk_extension_quoted_value'))
921+
.match('"', this.span.chunkExtensionValue.end(
922+
onChunkExtensionValueCompleted(n('chunk_extension_quoted_value_done')),
923+
))
924+
.otherwise(this.span.chunkExtensionValue.end().skipTo(
925+
p.error(ERROR.STRICT, 'Invalid character in chunk extensions quoted value'),
926+
));
927+
928+
n('chunk_extension_quoted_value_done')
929+
.match(';', n('chunk_extensions'))
863930
.match('\r', n('chunk_size_almost_done'))
864-
.match(HEADER_CHARS, n('chunk_parameters'))
865931
.otherwise(p.error(ERROR.STRICT,
866-
'Invalid character in chunk parameters'));
932+
'Invalid character in chunk extensions quote value'));
867933

868934
if (this.mode === 'strict') {
869935
n('chunk_size_almost_done')
@@ -1094,6 +1160,12 @@ export class HTTP {
10941160
case 'on_chunk_header':
10951161
cb = this.callback.onChunkHeader;
10961162
break;
1163+
case 'on_chunk_extension_name':
1164+
cb = this.callback.onChunkExtensionName;
1165+
break;
1166+
case 'on_chunk_extension_value':
1167+
cb = this.callback.onChunkExtensionValue;
1168+
break;
10971169
case 'on_chunk_complete':
10981170
cb = this.callback.onChunkComplete;
10991171
break;

src/native/api.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ const llhttp_settings_t wasm_settings = {
6464
NULL,
6565
wasm_on_header_field,
6666
wasm_on_header_value,
67+
NULL,
68+
NULL,
6769
wasm_on_headers_complete_wrap,
6870
wasm_on_body,
6971
wasm_on_message_complete,
@@ -76,6 +78,8 @@ const llhttp_settings_t wasm_settings = {
7678
NULL,
7779
NULL,
7880
NULL,
81+
NULL,
82+
NULL,
7983
};
8084

8185

@@ -401,12 +405,41 @@ int llhttp__on_chunk_header(llhttp_t* s, const char* p, const char* endp) {
401405
}
402406

403407

408+
int llhttp__on_chunk_extension_name(llhttp_t* s, const char* p, const char* endp) {
409+
int err;
410+
SPAN_CALLBACK_MAYBE(s, on_chunk_extension_name, p, endp - p);
411+
return err;
412+
}
413+
414+
415+
int llhttp__on_chunk_extension_name_complete(llhttp_t* s, const char* p, const char* endp) {
416+
int err;
417+
CALLBACK_MAYBE(s, on_chunk_extension_name_complete);
418+
return err;
419+
}
420+
421+
422+
int llhttp__on_chunk_extension_value(llhttp_t* s, const char* p, const char* endp) {
423+
int err;
424+
SPAN_CALLBACK_MAYBE(s, on_chunk_extension_value, p, endp - p);
425+
return err;
426+
}
427+
428+
429+
int llhttp__on_chunk_extension_value_complete(llhttp_t* s, const char* p, const char* endp) {
430+
int err;
431+
CALLBACK_MAYBE(s, on_chunk_extension_value_complete);
432+
return err;
433+
}
434+
435+
404436
int llhttp__on_chunk_complete(llhttp_t* s, const char* p, const char* endp) {
405437
int err;
406438
CALLBACK_MAYBE(s, on_chunk_complete);
407439
return err;
408440
}
409441

442+
410443
int llhttp__on_reset(llhttp_t* s, const char* p, const char* endp) {
411444
int err;
412445
CALLBACK_MAYBE(s, on_reset);

src/native/api.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ struct llhttp_settings_s {
2828
llhttp_data_cb on_version;
2929
llhttp_data_cb on_header_field;
3030
llhttp_data_cb on_header_value;
31+
llhttp_data_cb on_chunk_extension_name;
32+
llhttp_data_cb on_chunk_extension_value;
3133

3234
/* Possible return values:
3335
* 0 - Proceed normally
@@ -51,6 +53,8 @@ struct llhttp_settings_s {
5153
llhttp_cb on_version_complete;
5254
llhttp_cb on_header_field_complete;
5355
llhttp_cb on_header_value_complete;
56+
llhttp_cb on_chunk_extension_name_complete;
57+
llhttp_cb on_chunk_extension_value_complete;
5458

5559
/* When on_chunk_header is called, the current chunk length is stored
5660
* in parser->content_length.

test/fixtures/extra.c

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,50 @@ int llhttp__on_chunk_header(llparse_t* s, const char* p, const char* endp) {
305305
}
306306

307307

308+
int llhttp__on_chunk_extension_name(llparse_t* s, const char* p, const char* endp) {
309+
if (llparse__in_bench)
310+
return 0;
311+
312+
return llparse__print_span("chunk_extension_name", p, endp);
313+
}
314+
315+
316+
int llhttp__on_chunk_extension_name_complete(llparse_t* s, const char* p, const char* endp) {
317+
if (llparse__in_bench)
318+
return 0;
319+
320+
llparse__print(p, endp, "chunk_extension_name complete");
321+
322+
#ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_EXTENSION_NAME
323+
return LLPARSE__ERROR_PAUSE;
324+
#else
325+
return 0;
326+
#endif
327+
}
328+
329+
330+
int llhttp__on_chunk_extension_value(llparse_t* s, const char* p, const char* endp) {
331+
if (llparse__in_bench)
332+
return 0;
333+
334+
return llparse__print_span("chunk_extension_value", p, endp);
335+
}
336+
337+
338+
int llhttp__on_chunk_extension_value_complete(llparse_t* s, const char* p, const char* endp) {
339+
if (llparse__in_bench)
340+
return 0;
341+
342+
llparse__print(p, endp, "chunk_extension_value complete");
343+
344+
#ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_EXTENSION_VALUE
345+
return LLPARSE__ERROR_PAUSE;
346+
#else
347+
return 0;
348+
#endif
349+
}
350+
351+
308352
int llhttp__on_chunk_complete(llparse_t* s, const char* p, const char* endp) {
309353
if (llparse__in_bench)
310354
return 0;

test/md-test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,10 @@ function run(name: string): void {
261261

262262
for (const mode of modes) {
263263
for (const ty of types) {
264+
if (meta.skip === true || (process.env.ONLY === 'true' && !meta.only)) {
265+
continue;
266+
}
267+
264268
runSingleTest(mode, ty, meta, input, fullExpected);
265269
}
266270
}

0 commit comments

Comments
 (0)