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
114 changes: 114 additions & 0 deletions test/fixtures/header-value-relaxed.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "llhttp.h"

static void check(llhttp_type_t type, int mode, const char* input, size_t len,
size_t split, size_t value_offset, unsigned char byte) {
llhttp_t parser;
llhttp_settings_t settings;
llhttp_errno_t err;
int invalid;

llhttp_settings_init(&settings);
llhttp_init(&parser, type, &settings);
if (mode & 1)
llhttp_set_lenient_header_value_relaxed(&parser, 1);
if (mode & 2)
llhttp_set_lenient_headers(&parser, 1);

invalid = !(mode & 2) &&
(byte == 0 || (!(mode & 1) &&
((byte < 0x20 && byte != '\t') || byte == 0x7f)));

err = llhttp_execute(&parser, input, split);
if (err == HPE_OK)
err = llhttp_execute(&parser, input + split, len - split);

if (invalid) {
assert(err == HPE_INVALID_HEADER_TOKEN);
assert(llhttp_get_error_pos(&parser) == input + value_offset);
} else {
assert(err == HPE_OK);
assert(llhttp_finish(&parser) == HPE_OK);
}
}

int main(void) {
static const struct {
llhttp_type_t type;
const char* prefix;
const char* suffix;
} messages[] = {
{ HTTP_REQUEST, "GET / HTTP/1.1\r\nX: ", "\r\n\r\n" },
{ HTTP_REQUEST, "GET / HTTP/1.1\r\nHost: ", "\r\n\r\n" },
{ HTTP_RESPONSE, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nX: ",
"\r\n\r\n" },
{ HTTP_REQUEST,
"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nX: ",
"\r\n\r\n" },
{ HTTP_RESPONSE,
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nX: ",
"\r\n\r\n" },
};
static const size_t lengths[] = { 0, 1, 15, 16, 17, 200 };
char input[512];
size_t message;
size_t length;
size_t prefix_len;
size_t value_offset;
size_t len;
size_t split;
unsigned int byte;
int mode;
int trailing;

for (message = 0; message < sizeof(messages) / sizeof(messages[0]); message++) {
for (length = 0; length < sizeof(lengths) / sizeof(lengths[0]); length++) {
for (trailing = 0; trailing <= 1; trailing++) {
prefix_len = strlen(messages[message].prefix);
memcpy(input, messages[message].prefix, prefix_len);
memset(input + prefix_len, 'a', lengths[length]);
value_offset = prefix_len + lengths[length];
len = value_offset + 1;
if (trailing) {
/* Leave enough bytes after the test byte for vectorized scanning. */
memset(input + len, 'b', 32);
len += 32;
}
memcpy(input + len, messages[message].suffix,
strlen(messages[message].suffix));
len += strlen(messages[message].suffix);
assert(len <= sizeof(input));

for (mode = 0; mode < 4; mode++) {
for (byte = 0; byte <= 0xff; byte++) {
/* CR and LF delimit headers; their handling has separate tests. */
if (byte == '\r' || byte == '\n')
continue;
input[value_offset] = (char) byte;

check(messages[message].type, mode, input, len, len,
value_offset, (unsigned char) byte);
check(messages[message].type, mode, input, len, value_offset,
value_offset, (unsigned char) byte);
check(messages[message].type, mode, input, len, value_offset + 1,
value_offset, (unsigned char) byte);

/* Exercise every two-buffer split for the NUL regression. */
if (byte == 0) {
for (split = 0; split <= len; split++) {
check(messages[message].type, mode, input, len, split,
value_offset, (unsigned char) byte);
}
}
}
}
}
}
}

puts("header value byte and buffer-boundary checks passed");
return 0;
}
54 changes: 54 additions & 0 deletions test/header-value-relaxed-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import assert from 'node:assert';
import { spawnSync } from 'node:child_process';
import { mkdirSync } from 'node:fs';
import path from 'node:path';
import { test } from 'node:test';

// The markdown fixtures pass inputs via argv, which cannot contain NUL bytes.
// Use a native test with explicit buffer lengths and an external watchdog.
test('header value validation rejects NUL without looping', () => {
const root = path.join(__dirname, '..');
const build = path.join(root, 'build');
const tmp = path.join(__dirname, 'tmp');
mkdirSync(tmp, { recursive: true });

const generated = spawnSync(process.execPath,
[ '--import', 'tsx', 'bin/generate.ts' ], {
cwd: root,
encoding: 'utf8',
timeout: 60000,
});
assert.ifError(generated.error);
assert.strictEqual(generated.status, 0, generated.stderr);

const variants = [ { name: 'scalar', flags: [] as string[] } ];
if (process.arch === 'x64' || process.arch === 'ia32') {
variants.push({ name: 'sse42', flags: [ '-msse4.2' ] });
}

for (const { name, flags } of variants) {
const executable = path.join(tmp,
`header-value-relaxed-${name}${process.platform === 'win32' ? '.exe' : ''}`);
const compiled = spawnSync(process.env.CLANG || 'clang', [
'-O2', '-Wall', '-Wextra', '-Wno-unused-parameter',
...flags,
'-I', build,
path.join(build, 'c', 'llhttp.c'),
path.join(root, 'src', 'native', 'api.c'),
path.join(root, 'src', 'native', 'http.c'),
path.join(__dirname, 'fixtures', 'header-value-relaxed.c'),
'-o', executable,
], { encoding: 'utf8', timeout: 60000 });
assert.ifError(compiled.error);
assert.strictEqual(compiled.status, 0, compiled.stderr);

const result = spawnSync(executable, [], {
encoding: 'utf8',
timeout: 10000,
killSignal: 'SIGKILL',
});
assert.ifError(result.error);
assert.strictEqual(result.status, 0, `${name}: ${result.stderr}`);
assert.match(result.stdout, /header value byte and buffer-boundary checks passed/);
}
});
2 changes: 2 additions & 0 deletions test/md-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Group, MDGator, Metadata, Test } from 'mdgator';
import path from 'path';
import vm from 'vm';

import './header-value-relaxed-test';

import llhttp from '../src/llhttp';
import { allowedTypes, build, FixtureResult, Node, TestType } from './fixtures';

Expand Down
Loading