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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ The following callbacks can return `0` (proceed normally), `-1` (error) or `HPE_
* `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_protocol_complete`: Invoked after the HTTP version 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.
Expand All @@ -130,6 +131,7 @@ The following callbacks can return `0` (proceed normally), `-1` (error) or `HPE_
* `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_protocol`: Invoked when another character of the protocol is received.
* `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.
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import tseslint from 'typescript-eslint';
import globals from 'globals';

export default tseslint.config(
{ ignores: ["lib", "examples", "bench"] },
{ ignores: ["build", "lib", "examples", "bench"] },
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
Expand Down
1 change: 1 addition & 0 deletions src/llhttp/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const ERROR: IntDict = {
CB_CHUNK_EXTENSION_NAME_COMPLETE: 34,
CB_CHUNK_EXTENSION_VALUE_COMPLETE: 35,
CB_RESET: 31,
CB_PROTOCOL_COMPLETE: 38,
};

export const TYPE: IntDict = {
Expand Down
53 changes: 43 additions & 10 deletions src/llhttp/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const NODES: readonly string[] = [

'req_or_res_method',

'res_after_start',
'res_after_protocol',
'res_http_major',
'res_http_dot',
'res_http_minor',
Expand All @@ -41,6 +43,8 @@ const NODES: readonly string[] = [
'req_first_space_before_url',
'req_spaces_before_url',
'req_http_start',
'req_after_http_start',
'req_after_protocol',
'req_http_version',
'req_http_major',
'req_http_dot',
Expand Down Expand Up @@ -109,6 +113,7 @@ const NODES: readonly string[] = [
];

interface ISpanMap {
readonly protocol: source.Span;
readonly status: source.Span;
readonly method: source.Span;
readonly version: source.Span;
Expand All @@ -121,6 +126,7 @@ interface ISpanMap {

interface ICallbackMap {
readonly onMessageBegin: source.code.Code;
readonly onProtocolComplete: source.code.Code;
readonly onUrlComplete: source.code.Code;
readonly onMethodComplete: source.code.Code;
readonly onVersionComplete: source.code.Code;
Expand Down Expand Up @@ -178,13 +184,15 @@ export class HTTP {
chunkExtensionValue: p.span(p.code.span('llhttp__on_chunk_extension_value')),
headerField: p.span(p.code.span('llhttp__on_header_field')),
headerValue: p.span(p.code.span('llhttp__on_header_value')),
protocol: p.span(p.code.span('llhttp__on_protocol')),
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')),
};

this.callback = {
// User callbacks
onProtocolComplete: p.code.match('llhttp__on_protocol_complete'),
onUrlComplete: p.code.match('llhttp__on_url_complete'),
onStatusComplete: p.code.match('llhttp__on_status_complete'),
onMethodComplete: p.code.match('llhttp__on_method_complete'),
Expand All @@ -206,7 +214,7 @@ export class HTTP {
afterHeadersComplete: p.code.match('llhttp__after_headers_complete'),
afterMessageComplete: p.code.match('llhttp__after_message_complete'),
};

for (const name of NODES) {
this.nodes.set(name, p.node(name) as Match);
}
Expand Down Expand Up @@ -313,9 +321,22 @@ export class HTTP {
};

// Response
const endResponseProtocol = () => {
return this.span.protocol.end(
this.invokePausable('on_protocol_complete', ERROR.CB_PROTOCOL_COMPLETE, n('res_after_protocol'),
));
}

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

n('res_after_start')
.match([ 'HTTP', 'RTSP', 'ICE' ], endResponseProtocol())
.otherwise(this.span.protocol.end(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/, RTSP/ or ICE/')));

n('res_after_protocol')
.match('/', span.version.start(n('res_http_major')))
.otherwise(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/, RTSP/ or ICE/'));

n('res_http_major')
.select(MAJOR, this.store('http_major', 'res_http_dot'))
Expand Down Expand Up @@ -436,26 +457,35 @@ export class HTTP {
);

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

const map: Record<number, Node> = {};
for (const method of methods) {
map[method] = success;
}

return this.load('method', map, failure);
return this.span.protocol.end(
this.invokePausable('on_protocol_complete', ERROR.CB_PROTOCOL_COMPLETE, this.load('method', map, failure)),
);
};

n('req_http_start')
.match('HTTP/', checkMethod(METHODS_HTTP,
.match(' ', n('req_http_start'))
.otherwise(this.span.protocol.start(n('req_after_http_start')));

n('req_after_http_start')
.match('HTTP', checkMethod(METHODS_HTTP,
'Invalid method for HTTP/x.x request'))
.match('RTSP/', checkMethod(METHODS_RTSP,
.match('RTSP', checkMethod(METHODS_RTSP,
'Invalid method for RTSP/x.x request'))
.match('ICE/', checkMethod(METHODS_ICE,
.match('ICE', checkMethod(METHODS_ICE,
'Expected SOURCE method for ICE/x.x request'))
.match(' ', n('req_http_start'))
.otherwise(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/'));
.otherwise(this.span.protocol.end(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/, RTSP/ or ICE/')));

n('req_after_protocol')
.match('/', n('req_http_version'))
.otherwise(p.error(ERROR.INVALID_CONSTANT, 'Expected HTTP/, RTSP/ or ICE/'));

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

Expand Down Expand Up @@ -1248,6 +1278,9 @@ export class HTTP {
case 'on_message_begin':
cb = this.callback.onMessageBegin;
break;
case 'on_protocol_complete':
cb = this.callback.onProtocolComplete;
break;
case 'on_url_complete':
cb = this.callback.onUrlComplete;
break;
Expand Down
14 changes: 14 additions & 0 deletions src/native/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,20 @@ int llhttp__on_message_begin(llhttp_t* s, const char* p, const char* endp) {
}


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


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


int llhttp__on_url(llhttp_t* s, const char* p, const char* endp) {
int err;
SPAN_CALLBACK_MAYBE(s, on_url, p, endp - p);
Expand Down
2 changes: 2 additions & 0 deletions src/native/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct llhttp_settings_s {
llhttp_cb on_message_begin;

/* Possible return values 0, -1, HPE_USER */
llhttp_data_cb on_protocol;
llhttp_data_cb on_url;
llhttp_data_cb on_status;
llhttp_data_cb on_method;
Expand All @@ -49,6 +50,7 @@ struct llhttp_settings_s {

/* Possible return values 0, -1, `HPE_PAUSED` */
llhttp_cb on_message_complete;
llhttp_cb on_protocol_complete;
llhttp_cb on_url_complete;
llhttp_cb on_status_complete;
llhttp_cb on_method_complete;
Expand Down
22 changes: 22 additions & 0 deletions test/fixtures/extra.c
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,28 @@ int llhttp__on_message_complete(llparse_t* s, const char* p, const char* endp) {
}


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

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


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

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

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


int llhttp__on_status(llparse_t* s, const char* p, const char* endp) {
if (llparse__in_bench)
return 0;
Expand Down
13 changes: 8 additions & 5 deletions test/md-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,11 @@ function run(name: string): void {
const raw = fs.readFileSync(path.join(__dirname, name + '.md')).toString();
const groups = md.parse(raw);

function runSingleTest(ty: TestType, meta: Metadata,
input: string,
expected: readonly (string | RegExp)[]): void {
test(`should pass for type="${ty}"`, { timeout: 60000 }, async () => {
function runSingleTest(
location: string, ty: TestType, meta: Metadata,
input: string, expected: readonly (string | RegExp)[]
): void {
test(`should pass for type="${ty}" (location=${location})`, { timeout: 60000 }, async () => {
const binary = await buildMode(ty, meta);
await binary.check(input, expected, {
noScan: meta.noScan === true,
Expand All @@ -103,6 +104,8 @@ function run(name: string): void {

function runTest(test: Test) {
describe(test.name + ` at ${name}.md:${test.line + 1}`, () => {
const location = `${name}.md:${test.line + 1}`

let types: TestType[] = [];

const isURL = test.values.has('url');
Expand Down Expand Up @@ -202,7 +205,7 @@ function run(name: string): void {
continue;
}

runSingleTest(ty, meta, input, fullExpected);
runSingleTest(location, ty, meta, input, fullExpected);
}
});
}
Expand Down
Loading