Skip to content
Closed
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
1 change: 1 addition & 0 deletions src/llhttp/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export enum FLAGS {
export enum LENIENT_FLAGS {
HEADERS = 1 << 0,
CHUNKED_LENGTH = 1 << 1,
KEEP_ALIVE = 1 << 2,
}

export enum METHODS {
Expand Down
27 changes: 16 additions & 11 deletions src/llhttp/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,22 +787,27 @@ export class HTTP {
.otherwise(this.invokePausable('on_message_complete',
ERROR.CB_MESSAGE_COMPLETE, upgradeAfterDone));

const lenientClose = this.testLenientFlags(LENIENT_FLAGS.KEEP_ALIVE, {
1: n('restart'),
}, n('closed'));

// Check if we'd like to keep-alive
n('cleanup')
.otherwise(p.invoke(callback.afterMessageComplete, {
1: n('restart'),
}, this.update('finish', FINISH.SAFE, lenientClose)));

if (this.mode === 'strict') {
n('cleanup')
.otherwise(p.invoke(callback.afterMessageComplete, {
1: n('restart'),
}, this.update('finish', FINISH.SAFE, n('closed'))));
// Error on extra data after `Connection: close`
n('closed')
.match([ '\r', '\n' ], n('closed'))
.skipTo(p.error(ERROR.CLOSED_CONNECTION,
'Data after `Connection: close`'));
} else {
n('cleanup')
.otherwise(p.invoke(callback.afterMessageComplete, n('restart')));
// Discard all data after `Connection: close`
n('closed').skipTo(n('closed'));
}

n('closed')
.match([ '\r', '\n' ], n('closed'))
.skipTo(p.error(ERROR.CLOSED_CONNECTION,
'Data after `Connection: close`'));

n('restart')
.otherwise(this.update('finish', FINISH.SAFE, n('start')));
}
Expand Down
9 changes: 9 additions & 0 deletions src/native/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) {
}
}


void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_CHUNKED_LENGTH;
Expand All @@ -159,6 +160,14 @@ void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled) {
}


void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled) {
if (enabled) {
parser->lenient_flags |= LENIENT_KEEP_ALIVE;
} else {
parser->lenient_flags &= ~LENIENT_KEEP_ALIVE;
}
}

/* Callbacks */


Expand Down
14 changes: 14 additions & 0 deletions src/native/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ void llhttp_set_lenient_headers(llhttp_t* parser, int enabled);
*/
void llhttp_set_lenient_chunked_length(llhttp_t* parser, int enabled);


/* Enables/disables lenient handling of `Connection: close` and HTTP/1.0
* requests responses.
*
* Normally `llhttp` would error on (in strict mode) or discard (in loose mode)
* the HTTP request/response after the request/response with `Connection: close`
* and `Content-Length`. This is important to prevent cache poisoning attacks,
* but might interact badly with outdated and insecure clients. With this flag
* the extra request/response will be parsed normally.
*
* **(USE AT YOUR OWN RISK)**
*/
void llhttp_set_lenient_keep_alive(llhttp_t* parser, int enabled);

#ifdef __cplusplus
} /* extern "C" */
#endif
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/extra.c
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,25 @@ void llhttp__test_init_request_lenient_headers(llparse_t* s) {
s->lenient_flags |= LENIENT_HEADERS;
}


void llhttp__test_init_request_lenient_chunked_length(llparse_t* s) {
llhttp__test_init_request(s);
s->lenient_flags |= LENIENT_CHUNKED_LENGTH;
}


void llhttp__test_init_request_lenient_keep_alive(llparse_t* s) {
llhttp__test_init_request(s);
s->lenient_flags |= LENIENT_KEEP_ALIVE;
}


void llhttp__test_init_response_lenient_keep_alive(llparse_t* s) {
llhttp__test_init_response(s);
s->lenient_flags |= LENIENT_KEEP_ALIVE;
}


void llhttp__test_finish(llparse_t* s) {
llparse__print(NULL, NULL, "finish=%d", s->finish);
}
Expand Down
7 changes: 5 additions & 2 deletions test/fixtures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import * as path from 'path';
import * as llhttp from '../../src/llhttp';

export type TestType = 'request' | 'response' | 'request-lenient-headers' |
'request-lenient-chunked-length' | 'request-finish' | 'response-finish' |
'request-lenient-chunked-length' | 'request-lenient-keep-alive' |
'response-lenient-keep-alive' | 'request-finish' | 'response-finish' |
'none' | 'url';

export { FixtureResult };
Expand Down Expand Up @@ -61,7 +62,9 @@ export async function build(
const extra = options.extra === undefined ? [] : options.extra.slice();
if (ty === 'request' || ty === 'response' ||
ty === 'request-lenient-headers' ||
ty === 'request-lenient-chunked-length') {
ty === 'request-lenient-chunked-length' ||
ty === 'request-lenient-keep-alive' ||
ty === 'response-lenient-keep-alive') {
extra.push(
`-DLLPARSE__TEST_INIT=llhttp__test_init_${ty.replace(/-/g, '_')}`);
} else if (ty === 'request-finish' || ty === 'response-finish') {
Expand Down
12 changes: 12 additions & 0 deletions test/md-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,12 @@ const http: IFixtureMap = {
'request-lenient-chunked-length':
buildMode('loose', 'request-lenient-chunked-length'),
'request-lenient-headers': buildMode('loose', 'request-lenient-headers'),
'request-lenient-keep-alive': buildMode(
'loose', 'request-lenient-keep-alive'),
'response': buildMode('loose', 'response'),
'response-finish': buildMode('loose', 'response-finish'),
'response-lenient-keep-alive': buildMode(
'loose', 'response-lenient-keep-alive'),
'url': buildMode('loose', 'url'),
},
strict: {
Expand All @@ -93,8 +97,12 @@ const http: IFixtureMap = {
'request-lenient-chunked-length':
buildMode('strict', 'request-lenient-chunked-length'),
'request-lenient-headers': buildMode('strict', 'request-lenient-headers'),
'request-lenient-keep-alive': buildMode(
'strict', 'request-lenient-keep-alive'),
'response': buildMode('strict', 'response'),
'response-finish': buildMode('strict', 'response-finish'),
'response-lenient-keep-alive': buildMode(
'strict', 'response-lenient-keep-alive'),
'url': buildMode('strict', 'url'),
},
};
Expand Down Expand Up @@ -155,6 +163,10 @@ function run(name: string): void {
types = [ 'request-lenient-headers' ];
} else if (meta.type === 'request-lenient-chunked-length') {
types = [ 'request-lenient-chunked-length' ];
} else if (meta.type === 'request-lenient-keep-alive') {
types = [ 'request-lenient-keep-alive' ];
} else if (meta.type === 'response-lenient-keep-alive') {
types = [ 'response-lenient-keep-alive' ];
} else if (meta.type === 'response-only') {
types = [ 'response' ];
} else if (meta.type === 'request-finish') {
Expand Down
8 changes: 4 additions & 4 deletions test/request/connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ off=21 message complete
off=22 error code=5 reason="Data after `Connection: close`"
```

### Resetting flags when keep-alive is off (1.0) and parser is in loose mode
### Resetting flags when keep-alive is off (1.0) and parser is in lenient mode

Even though we allow restarts in loose mode, the flags should be still set to
`0` upon restart.

<!-- meta={"type": "request", "mode": "loose"} -->
<!-- meta={"type": "request-lenient-keep-alive"} -->
```http
PUT /url HTTP/1.0
Content-Length: 0
Expand Down Expand Up @@ -242,11 +242,11 @@ off=133 message complete
off=138 error code=5 reason="Data after `Connection: close`"
```

### CRLF between requests, explicit `close` (loose mode)
### CRLF between requests, explicit `close` (lenient mode)

Loose mode is more lenient, and allows further requests.

<!-- meta={"type": "request", "mode": "loose"} -->
<!-- meta={"type": "request-lenient-keep-alive"} -->
```http
POST / HTTP/1.1
Host: www.example.com
Expand Down
95 changes: 94 additions & 1 deletion test/response/connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,103 @@ off=46 message complete
off=47 error code=5 reason="Data after `Connection: close`"
```

## HTTP/1.1 with keep-alive disabled and 204 status in loose mode
## HTTP/1.1 with keep-alive disabled, content-length, and in loose mode

Parser should discard extra request in loose mode.

<!-- meta={"type": "response", "mode": "loose"} -->
```http
HTTP/1.1 200 No content
Content-Length: 5
Connection: close

2ad731e3-4dcd-4f70-b871-0ad284b29ffc
```

```log
off=0 message begin
off=13 len=10 span[status]="No content"
off=25 status complete
off=25 len=14 span[header_field]="Content-Length"
off=40 header_field complete
off=41 len=1 span[header_value]="5"
off=44 header_value complete
off=44 len=10 span[header_field]="Connection"
off=55 header_field complete
off=56 len=5 span[header_value]="close"
off=63 header_value complete
off=65 headers complete status=200 v=1/1 flags=22 content_length=5
off=65 len=5 span[body]="2ad73"
off=70 message complete
```

## HTTP/1.1 with keep-alive disabled, content-length, and in strict mode

Parser should discard extra request in strict mode.

<!-- meta={"type": "response", "mode": "strict"} -->
```http
HTTP/1.1 200 No content
Content-Length: 5
Connection: close

2ad731e3-4dcd-4f70-b871-0ad284b29ffc
```

```log
off=0 message begin
off=13 len=10 span[status]="No content"
off=25 status complete
off=25 len=14 span[header_field]="Content-Length"
off=40 header_field complete
off=41 len=1 span[header_value]="5"
off=44 header_value complete
off=44 len=10 span[header_field]="Connection"
off=55 header_field complete
off=56 len=5 span[header_value]="close"
off=63 header_value complete
off=65 headers complete status=200 v=1/1 flags=22 content_length=5
off=65 len=5 span[body]="2ad73"
off=70 message complete
off=71 error code=5 reason="Data after `Connection: close`"
```

## HTTP/1.1 with keep-alive disabled, content-length, and in lenient mode

Parser should process extra request in lenient mode.

<!-- meta={"type": "response-lenient-keep-alive"} -->
```http
HTTP/1.1 200 No content
Content-Length: 5
Connection: close

2ad73HTTP/1.1 200 OK
```

```log
off=0 message begin
off=13 len=10 span[status]="No content"
off=25 status complete
off=25 len=14 span[header_field]="Content-Length"
off=40 header_field complete
off=41 len=1 span[header_value]="5"
off=44 header_value complete
off=44 len=10 span[header_field]="Connection"
off=55 header_field complete
off=56 len=5 span[header_value]="close"
off=63 header_value complete
off=65 headers complete status=200 v=1/1 flags=22 content_length=5
off=65 len=5 span[body]="2ad73"
off=70 message complete
off=70 message begin
off=83 len=2 span[status]="OK"
```

## HTTP/1.1 with keep-alive disabled and 204 status in lenient mode

<!-- meta={"type": "response-lenient-keep-alive"} -->
```http
HTTP/1.1 204 No content
Connection: close

Expand Down