@@ 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';
};