Skip to content

Commit

Permalink
feat: Added support for RTSP and ICE response and the on_protocol_com…
Browse files Browse the repository at this point in the history
…plete callback. (#477)

* feat: Added support for RTSP and ICE response and the on_protocol_complete callback.

* chore: Reduce instructions.

* fix: Minor fixes.
  • Loading branch information
ShogunPanda authored Aug 27, 2024
1 parent f9d9465 commit 8371b40
Show file tree
Hide file tree
Showing 29 changed files with 671 additions and 23 deletions.
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

0 comments on commit 8371b40

Please sign in to comment.