~sircmpwn/hare-message

2aefb0c550c90f0ef568fdd71959d67a74d1c7cc — Drew DeVault 6 months ago 5579e92
message: implement read_header
1 files changed, 138 insertions(+), 0 deletions(-)

M message/header.ha
M message/header.ha => message/header.ha +138 -0
@@ 5,6 5,7 @@ use errors;
use fmt;
use hash::fnv;
use io;
use os;
use strings;
use strio;
use types;


@@ 297,6 298,97 @@ export fn header_set(head: *header, key: str, val: str) void = {
	assert(vals[0] == "text/x-hare");
};

// Reads a MIME [[header]] from an [[io::handle]]. The header is a sequence of
// key: value fields, possibly with continuation lines, terminated by an empty
// line.
//
// To prevent denial of service attacks when using untrusted input, use an
// [[io::limitreader]] bounded to the expected length of the headers or some
// reasonable maximum.
//
// The return value should be freed with [[header_finish]].
export fn read_header(
	in: io::handle,
) (header | io::error | errors::invalid) = {
	let buf: [os::BUFSIZ]u8 = [0...];
	let rd = bufio::buffered(in, buf, []);

	// TODO: leaks on error
	let head = new_header();

	match (bufio::scanbyte(&rd)?) {
	case let b: u8 =>
		// Cannot start with a leading space
		if (b == ' ' || b == '\t') {
			return errors::invalid;
		};
		bufio::unread(&rd, [b]);
	case io::EOF =>
		return errors::invalid;
	};

	for (true) {
		let kv = read_continued(&rd)?;
		if (len(kv) == 0) {
			return head;
		};

		const i = match (bytes::index(kv, ':')) {
		case let i: size =>
			yield i;
		case void =>
			// Malformed header line
			return errors::invalid;
		};

		const key = bytes::trim(kv[..i], ' ', '\t');
		for (let i = 0z; i < len(key); i += 1) {
			if (!valid_header_field(key[i])) {
				return errors::invalid;
			};
		};

		const key = canonical_mime_header_key(strings::fromutf8(key)!);
		if (key == "") {
			// Per RFC 7230, keys may not be empty, but we will be
			// liberal in what we accept here.
			continue;
		};

		const val = decode_header_value(kv[i+1..]);
		const field = alloc(header_field {
			raw = kv,
			key = key,
			val = val,
		});
		let mapkey = header_get_mapkey(&head, key);
		insert(head.fields[0], field);
		append(mapkey.fields, field);
	};

	abort(); // Unreachable
};

@test fn read_header() void = {
	const input =
		"To: Drew DeVault <sir@cmpwn.com>\r\n"
		"From: Harriet <harriet@harelang.org>\r\n"
		"Content-Type: text/plain\r\n"
		"DKIM-Signature: a=rsa-sha256;\r\n"
		" bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHOI=; c=simple/simple;\r\n"
		" d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;\r\n"
		"\r\n";

	const in = bufio::fixed(strings::toutf8(input), io::mode::READ);
	const head = read_header(&in)!;
	defer header_finish(&head);

	assert(header_get(&head, "To") == "Drew DeVault <sir@cmpwn.com>");
	assert(header_get(&head, "From") == "Harriet <harriet@harelang.org>");
	assert(header_get(&head, "Content-Type") == "text/plain");
	assert(header_get(&head, "Dkim-Signature") == "a=rsa-sha256; bh=uI/rVH7mLBSWkJVvQYKz3TbpdI2BLZWTIMKcuo0KHOI=; c=simple/simple; d=example.org; h=Subject:To:From; s=default; t=1577562184; v=1; b=;");
};

// Writes a MIME [[header]] to an [[io::handle]].
export fn write_header(sink: io::handle, head: *header) (size | io::error) = {
	let z = 0z;


@@ 516,3 608,49 @@ fn write_continued(sink: *strio::stream, v: []u8) void = {
	};
	io::write(sink, v)!;
};

// Reads a (possibly continued) line from a buffered stream. The caller must
// free the return value.
fn read_continued(
	rd: *bufio::bufstream,
) ([]u8 | errors::invalid | io::error) = {
	let line = match (bufio::scanline(rd)?) {
	case let line: []u8 =>
		yield bytes::rtrim(line, '\r');
	case io::EOF =>
		return errors::invalid;
	};

	if (len(line) == 0) {
		return [];
	};
	append(line, ['\r', '\n']...);

	for (has_continuation(rd)?) {
		const cont = match (bufio::scanline(rd)?) {
		case let line: []u8 =>
			yield bytes::rtrim(line, '\r');
		case io::EOF =>
			return errors::invalid;
		};
		defer free(cont);

		append(line, cont...);
		append(line, ['\r', '\n']...);
	};

	return line;
};

fn has_continuation(
	rd: *bufio::bufstream,
) (bool | io::error) = {
	const b = match (bufio::scanbyte(rd)?) {
	case let b: u8 =>
		yield b;
	case io::EOF =>
		return false;
	};
	bufio::unread(rd, [b]);
	return b == ' ' || b == '\t';
};