Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,26 @@ The following callbacks can return `0` (proceed normally), `-1` (error) or `HPE_
* `on_message_begin`: Invoked when a new request/response starts.
* `on_message_complete`: Invoked when a request/response has been completedly parsed.
* `on_url_complete`: Invoked after the URL has been parsed.
* `on_method_complete`: Invoked after the HTTP method has been parsed.
* `on_version_complete`: Invoked after the HTTP version has been parsed.
* `on_status_complete`: Invoked after the status code has been parsed.
* `on_header_field_complete`: Invoked after a header name has been parsed.
* `on_header_value_complete`: Invoked after a header value has been parsed.
* `on_chunk_header`: Invoked after a new chunk is started. The current chunk length is stored
`parser->content_length`.
* `on_chunk_header`: Invoked after a new chunk is started. The current chunk length is stored in `parser->content_length`.
* `on_chunk_complete`: Invoked after a new chunk is received.
* `on_reset`: Invoked after `on_message_complete` and before `on_message_begin` when a new message is received on the same parser. This is not invoked for the first message of the parser.
* `on_reset`: Invoked after `on_message_complete` and before `on_message_begin` when a new message
is received on the same parser. This is not invoked for the first message of the parser.

The following callbacks can return `0` (proceed normally), `-1` (error) or `HPE_USER` (error from the callback):

* `on_url`: Invoked when the URL starts.
* `on_status`: Invoked when the status starts.
* `on_header_field`: Invoked when a new header name starts.
* `on_header_value`: Invoked when a new header value starts.
* `on_url`: Invoked when another character of the URL is received.
* `on_status`: Invoked when another character of the status is received.
* `on_method`: Invoked when another character of the method is received.
When parser is created with `HTTP_BOTH` and the input is a response, this also invoked for the sequence `HTTP/`
of the first message.
* `on_version`: Invoked when another character of the version is received.
* `on_header_field`: Invoked when another character of a header name is received.
* `on_header_value`: Invoked when another character of a header value is received.

The callback `on_headers_complete`, invoked when headers are completed, can return:

Expand Down
2 changes: 2 additions & 0 deletions src/llhttp/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export enum ERROR {

CB_URL_COMPLETE = 26,
CB_STATUS_COMPLETE = 27,
CB_METHOD_COMPLETE = 32,
CB_VERSION_COMPLETE = 33,
CB_HEADER_FIELD_COMPLETE = 28,
CB_HEADER_VALUE_COMPLETE = 29,
CB_RESET = 31,
Expand Down
95 changes: 67 additions & 28 deletions src/llhttp/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const NODES: ReadonlyArray<string> = [
'start',
'after_start',
'start_req',
'after_start_req',
'start_res',
'start_req_or_res',

Expand All @@ -31,6 +32,7 @@ const NODES: ReadonlyArray<string> = [
'res_http_dot',
'res_http_minor',
'res_http_end',
'res_after_version',
'res_status_code',
'res_status_code_otherwise',
'res_status_start',
Expand All @@ -40,6 +42,7 @@ const NODES: ReadonlyArray<string> = [
'req_first_space_before_url',
'req_spaces_before_url',
'req_http_start',
'req_http_version',
'req_http_major',
'req_http_dot',
'req_http_minor',
Expand Down Expand Up @@ -106,6 +109,8 @@ interface ISpanMap {
readonly headerField: source.Span;
readonly headerValue: source.Span;
readonly status: source.Span;
readonly method: source.Span;
readonly version: source.Span;
}

interface ICallbackMap {
Expand All @@ -118,6 +123,8 @@ interface ICallbackMap {
readonly onMessageBegin: source.code.Code;
readonly onMessageComplete: source.code.Code;
readonly onUrlComplete: source.code.Code;
readonly onMethodComplete: source.code.Code;
readonly onVersionComplete: source.code.Code;
readonly onStatusComplete: source.code.Code;
readonly onHeaderFieldComplete: source.code.Code;
readonly onHeaderValueComplete: source.code.Code;
Expand Down Expand Up @@ -162,14 +169,18 @@ export class HTTP {
body: p.span(p.code.span('llhttp__on_body')),
headerField: p.span(p.code.span('llhttp__on_header_field')),
headerValue: p.span(p.code.span('llhttp__on_header_value')),
method: p.span(p.code.span('llhttp__on_method')),
status: p.span(p.code.span('llhttp__on_status')),
version: p.span(p.code.span('llhttp__on_version')),
};

/* tslint:disable:object-literal-sort-keys */
this.callback = {
// User callbacks
onUrlComplete: p.code.match('llhttp__on_url_complete'),
onStatusComplete: p.code.match('llhttp__on_status_complete'),
onMethodComplete: p.code.match('llhttp__on_method_complete'),
onVersionComplete: p.code.match('llhttp__on_version_complete'),
onHeaderFieldComplete: p.code.match('llhttp__on_header_field_complete'),
onHeaderValueComplete: p.code.match('llhttp__on_header_value_complete'),
onHeadersComplete: p.code.match('llhttp__on_headers_complete'),
Expand Down Expand Up @@ -252,53 +263,65 @@ export class HTTP {
);

n('start_req_or_res')
.peek('H', n('req_or_res_method'))
.peek('H', this.span.method.start(n('req_or_res_method')))
.otherwise(this.update('type', TYPE.REQUEST, 'start_req'));

n('req_or_res_method')
.select(H_METHOD_MAP, this.store('method',
this.update('type', TYPE.REQUEST, 'req_first_space_before_url')))
.match('HTTP/', this.update('type', TYPE.RESPONSE, 'res_http_major'))
this.update('type', TYPE.REQUEST, this.span.method.end(
this.invokePausable('on_method_complete', ERROR.CB_METHOD_COMPLETE, n('req_first_space_before_url')),
)),
))
.match('HTTP/', this.span.method.end(this.update('type', TYPE.RESPONSE,
this.span.version.start(n('res_http_major')))))
.otherwise(p.error(ERROR.INVALID_CONSTANT, 'Invalid word encountered'));

const checkVersion = (destination: string): Node => {
const node = n(destination);
const errorNode = this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version'));

return this.testLenientFlags(LENIENT_FLAGS.VERSION,
{
1: n(destination),
1: node,
},
this.load('http_major', {
0: this.load('http_minor', {
9: n(destination),
}, p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version')),
9: node,
}, errorNode),
1: this.load('http_minor', {
0: n(destination),
1: n(destination),
}, p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version')),
0: node,
1: node,
}, errorNode),
2: this.load('http_minor', {
0: n(destination),
}, p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version')),
}, p.error(ERROR.INVALID_VERSION, 'Invalid HTTP version')),
0: node,
}, errorNode),
}, errorNode),
);
};

// Response
n('start_res')
.match('HTTP/', n('res_http_major'))
.match('HTTP/', span.version.start(n('res_http_major')))
.otherwise(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/'));

n('res_http_major')
.select(MAJOR, this.store('http_major', 'res_http_dot'))
.otherwise(p.error(ERROR.INVALID_VERSION, 'Invalid major version'));
.otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid major version')));

n('res_http_dot')
.match('.', n('res_http_minor'))
.otherwise(p.error(ERROR.INVALID_VERSION, 'Expected dot'));
.otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Expected dot')));

n('res_http_minor')
.select(MINOR, this.store('http_minor', checkVersion('res_http_end')))
.otherwise(p.error(ERROR.INVALID_VERSION, 'Invalid minor version'));
.otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid minor version')));

n('res_http_end')
.otherwise(this.span.version.end().otherwise(
this.invokePausable('on_version_complete', ERROR.CB_VERSION_COMPLETE, 'res_after_version'),
));

n('res_after_version')
.match(' ', this.update('status_code', 0, 'res_status_code'))
.otherwise(p.error(ERROR.INVALID_VERSION,
'Expected space after version'));
Expand Down Expand Up @@ -339,10 +362,12 @@ export class HTTP {
}

// Request
n('start_req').otherwise(this.span.method.start(n('after_start_req')));

n('start_req')
.select(METHOD_MAP,
this.store('method', 'req_first_space_before_url'))
n('after_start_req')
.select(METHOD_MAP, this.store('method', this.span.method.end(
this.invokePausable('on_method_complete', ERROR.CB_METHOD_COMPLETE, n('req_first_space_before_url'),
))))
.otherwise(p.error(ERROR.INVALID_METHOD, 'Invalid method encountered'));

n('req_first_space_before_url')
Expand Down Expand Up @@ -374,7 +399,7 @@ export class HTTP {
);

const checkMethod = (methods: METHODS[], error: string): Node => {
const success = n('req_http_major');
const success = n('req_http_version');
const failure = p.error(ERROR.INVALID_CONSTANT, error);

const map: { [key: number]: Node } = {};
Expand All @@ -395,21 +420,30 @@ export class HTTP {
.match(' ', n('req_http_start'))
.otherwise(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/'));

n('req_http_version').otherwise(span.version.start(n('req_http_major')));

n('req_http_major')
.select(MAJOR, this.store('http_major', 'req_http_dot'))
.otherwise(p.error(ERROR.INVALID_VERSION, 'Invalid major version'));
.otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid major version')));

n('req_http_dot')
.match('.', n('req_http_minor'))
.otherwise(p.error(ERROR.INVALID_VERSION, 'Expected dot'));
.otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Expected dot')));

n('req_http_minor')
.select(MINOR, this.store('http_minor', checkVersion('req_http_end')))
.otherwise(p.error(ERROR.INVALID_VERSION, 'Invalid minor version'));

n('req_http_end').otherwise(this.load('method', {
[METHODS.PRI]: n('req_pri_upgrade'),
}, n('req_http_complete')));
.otherwise(this.span.version.end(p.error(ERROR.INVALID_VERSION, 'Invalid minor version')));

n('req_http_end').otherwise(
span.version.end().otherwise(
this.invokePausable(
'on_version_complete',
ERROR.CB_VERSION_COMPLETE,
this.load('method', {
[METHODS.PRI]: n('req_pri_upgrade'),
}, n('req_http_complete')),
)),
);

n('req_http_complete')
.match([ '\r\n', '\n' ], n('headers_start'))
Expand Down Expand Up @@ -1028,7 +1062,6 @@ export class HTTP {
return res;
}

// TODO(indutny): use type for `name`
private invokePausable(name: string, errorCode: ERROR, next: string | Node)
: Node {
let cb;
Expand All @@ -1043,6 +1076,12 @@ export class HTTP {
case 'on_status_complete':
cb = this.callback.onStatusComplete;
break;
case 'on_method_complete':
cb = this.callback.onMethodComplete;
break;
case 'on_version_complete':
cb = this.callback.onVersionComplete;
break;
case 'on_header_field_complete':
cb = this.callback.onHeaderFieldComplete;
break;
Expand Down
36 changes: 36 additions & 0 deletions src/native/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const llhttp_settings_t wasm_settings = {
wasm_on_message_begin,
wasm_on_url,
wasm_on_status,
NULL,
NULL,
wasm_on_header_field,
wasm_on_header_value,
wasm_on_headers_complete_wrap,
Expand All @@ -68,6 +70,12 @@ const llhttp_settings_t wasm_settings = {
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
};


Expand Down Expand Up @@ -309,6 +317,34 @@ int llhttp__on_status_complete(llhttp_t* s, const char* p, const char* endp) {
}


int llhttp__on_method(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_method, p, endp - p);
return err;
}


int llhttp__on_method_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_method_complete);
return err;
}


int llhttp__on_version(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_version, p, endp - p);
return err;
}


int llhttp__on_version_complete(llhttp_t* s, const char* p, const char* endp) {
int err;
CALLBACK_MAYBE(s, on_version_complete);
return err;
}


int llhttp__on_header_field(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_header_field, p, endp - p);
Expand Down
4 changes: 4 additions & 0 deletions src/native/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ struct llhttp_settings_s {
/* Possible return values 0, -1, HPE_USER */
llhttp_data_cb on_url;
llhttp_data_cb on_status;
llhttp_data_cb on_method;
llhttp_data_cb on_version;
llhttp_data_cb on_header_field;
llhttp_data_cb on_header_value;

Expand All @@ -45,6 +47,8 @@ struct llhttp_settings_s {
llhttp_cb on_message_complete;
llhttp_cb on_url_complete;
llhttp_cb on_status_complete;
llhttp_cb on_method_complete;
llhttp_cb on_version_complete;
llhttp_cb on_header_field_complete;
llhttp_cb on_header_value_complete;

Expand Down
43 changes: 43 additions & 0 deletions test/fixtures/extra.c
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,49 @@ int llhttp__on_status_complete(llparse_t* s, const char* p, const char* endp) {
}


int llhttp__on_method(llparse_t* s, const char* p, const char* endp) {
if (llparse__in_bench || s->type != HTTP_REQUEST)
return 0;

return llparse__print_span("method", p, endp);
}


int llhttp__on_method_complete(llparse_t* s, const char* p, const char* endp) {
if (llparse__in_bench)
return 0;

llparse__print(p, endp, "method complete");

#ifdef LLHTTP__TEST_PAUSE_ON_METHOD_COMPLETE
return LLPARSE__ERROR_PAUSE;
#else
return 0;
#endif
}


int llhttp__on_version(llparse_t* s, const char* p, const char* endp) {
if (llparse__in_bench)
return 0;

return llparse__print_span("version", p, endp);
}


int llhttp__on_version_complete(llparse_t* s, const char* p, const char* endp) {
if (llparse__in_bench)
return 0;

llparse__print(p, endp, "version complete");

#ifdef LLHTTP__TEST_PAUSE_ON_VERSION_COMPLETE
return LLPARSE__ERROR_PAUSE;
#else
return 0;
#endif
}

int llhttp__on_header_field(llparse_t* s, const char* p, const char* endp) {
if (llparse__in_bench)
return 0;
Expand Down
Loading