view src/lib-mail/istream-binary-converter.c @ 21322:5ab8dc1a4a6f

global: Change string position/length from unsigned int to size_t Mainly to avoid truncating >4GB strings, which might potentially cause some security holes. Normally there are other limits, which prevent such excessive strings from being created in the first place. I'm sure this didn't find everything. Maybe everything could be found with compiler warnings. -Wconversion kind of does it, but it gives way too many unnecessary warnings. These were mainly found with: grep " = strlen" egrep "unsigned int.*(size|len)"
author Timo Sirainen <timo.sirainen@dovecot.fi>
date Mon, 12 Dec 2016 07:19:55 +0200
parents 57078d325d01
children 2e2563132d5f
line wrap: on
line source

/* Copyright (c) 2013-2016 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "buffer.h"
#include "base64.h"
#include "istream-private.h"
#include "message-parser.h"
#include "istream-binary-converter.h"

#define BASE64_BLOCK_INPUT_SIZE 3
#define BASE64_BLOCK_SIZE 4
#define BASE64_BLOCKS_PER_LINE (76/BASE64_BLOCK_SIZE)
#define MAX_HDR_BUFFER_SIZE (1024*32)

struct binary_converter_istream {
	struct istream_private istream;

	pool_t pool;
	struct message_parser_ctx *parser;
	struct message_part *convert_part;
	char base64_delayed[BASE64_BLOCK_INPUT_SIZE-1];
	unsigned int base64_delayed_len;
	unsigned int base64_block_pos;

	buffer_t *hdr_buf;
	size_t cte_header_len;
	unsigned int content_type_seen:1;
};

static void stream_add_data(struct binary_converter_istream *bstream,
			    const void *data, size_t size);

static bool part_can_convert(const struct message_part *part)
{
	/* some MUAs use "c-t-e: binary" for multiparts.
	   we don't want to convert them. */
	return (part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0;
}

static void
stream_finish_convert_decision(struct binary_converter_istream *bstream)
{
	buffer_t *buf = bstream->hdr_buf;
	const unsigned char *data;

	bstream->hdr_buf = NULL;
	if (!part_can_convert(bstream->convert_part)) {
		bstream->convert_part = NULL;
		stream_add_data(bstream, buf->data, buf->used);
	} else {
		stream_add_data(bstream,
			"Content-Transfer-Encoding: base64\r\n", 35);

		data = CONST_PTR_OFFSET(buf->data, bstream->cte_header_len);
		stream_add_data(bstream, data,
				buf->used - bstream->cte_header_len);
	}
	buffer_free(&buf);
}

static void stream_add_data(struct binary_converter_istream *bstream,
			    const void *data, size_t size)
{
	if (size == 0)
		return;

	if (bstream->hdr_buf != NULL) {
		if (bstream->hdr_buf->used + size <= MAX_HDR_BUFFER_SIZE) {
			buffer_append(bstream->hdr_buf, data, size);
			return;
		}
		/* buffer is getting too large. just finish the decision. */
		stream_finish_convert_decision(bstream);
	}

	memcpy(i_stream_alloc(&bstream->istream, size), data, size);
	bstream->istream.pos += size;
}

static void stream_encode_base64(struct binary_converter_istream *bstream,
				 const void *_data, size_t size)
{
	struct istream_private *stream = &bstream->istream;
	const unsigned char *data = _data;
	buffer_t buf;
	void *dest;
	size_t encode_size, max_encoded_size;
	unsigned char base64_block[BASE64_BLOCK_INPUT_SIZE];
	unsigned int base64_block_len, missing_len, encode_blocks;

	if (bstream->base64_delayed_len > 0) {
		if (bstream->base64_delayed_len == 1 && size == 1) {
			bstream->base64_delayed[1] = data[0];
			bstream->base64_delayed_len++;
			return;
		}
		memcpy(base64_block, bstream->base64_delayed,
		       bstream->base64_delayed_len);
		base64_block_len = bstream->base64_delayed_len;
		if (size == 0) {
			/* finish base64 */
		} else {
			missing_len = BASE64_BLOCK_INPUT_SIZE - base64_block_len;
			i_assert(size >= missing_len);
			memcpy(base64_block + base64_block_len,
			       data, missing_len);
			data += missing_len;
			size -= missing_len;
			base64_block_len = BASE64_BLOCK_INPUT_SIZE;
		}

		if (bstream->base64_block_pos == BASE64_BLOCKS_PER_LINE) {
			memcpy(i_stream_alloc(stream, 2), "\r\n", 2);
			stream->pos += 2;
			bstream->base64_block_pos = 0;
		}

		dest = i_stream_alloc(stream, BASE64_BLOCK_SIZE);
		buffer_create_from_data(&buf, dest, BASE64_BLOCK_SIZE);
		base64_encode(base64_block, base64_block_len, &buf);
		stream->pos += buf.used;
		bstream->base64_block_pos++;
		bstream->base64_delayed_len = 0;
	}

	while (size >= BASE64_BLOCK_INPUT_SIZE) {
		if (bstream->base64_block_pos == BASE64_BLOCKS_PER_LINE) {
			memcpy(i_stream_alloc(stream, 2), "\r\n", 2);
			stream->pos += 2;
			bstream->base64_block_pos = 0;
		}

		/* try to encode one full line of base64 blocks */
		encode_size = I_MIN(size, BASE64_BLOCKS_PER_LINE*BASE64_BLOCK_SIZE);
		if (encode_size % BASE64_BLOCK_INPUT_SIZE != 0)
			encode_size -= encode_size % BASE64_BLOCK_INPUT_SIZE;
		encode_blocks = encode_size/BASE64_BLOCK_INPUT_SIZE;
		if (bstream->base64_block_pos + encode_blocks > BASE64_BLOCKS_PER_LINE) {
			encode_blocks = BASE64_BLOCKS_PER_LINE -
				bstream->base64_block_pos;
			encode_size = encode_blocks * BASE64_BLOCK_INPUT_SIZE;
		}

		max_encoded_size = MAX_BASE64_ENCODED_SIZE(encode_size);
		dest = i_stream_alloc(stream, max_encoded_size);
		buffer_create_from_data(&buf, dest, max_encoded_size);
		base64_encode(data, encode_size, &buf);
		stream->pos += buf.used;
		bstream->base64_block_pos += encode_blocks;

		data += encode_size;
		size -= encode_size;
	}
	if (size > 0) {
		/* encode these when more data is available */
		i_assert(size < BASE64_BLOCK_INPUT_SIZE);
		memcpy(bstream->base64_delayed, data, size);
		bstream->base64_delayed_len = size;
	}
}

static void stream_add_hdr(struct binary_converter_istream *bstream,
			   const struct message_header_line *hdr)
{
	if (!hdr->continued) {
		stream_add_data(bstream, hdr->name, hdr->name_len);
		stream_add_data(bstream, hdr->middle, hdr->middle_len);
	}

	stream_add_data(bstream, hdr->value, hdr->value_len);
	if (!hdr->no_newline)
		stream_add_data(bstream, "\r\n", 2);
}

static ssize_t i_stream_binary_converter_read(struct istream_private *stream)
{
	/* @UNSAFE */
	struct binary_converter_istream *bstream =
		(struct binary_converter_istream *)stream;
	struct message_block block;
	size_t old_size, new_size;

	if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
		return -2;
	old_size = stream->pos - stream->skip;

	switch (message_parser_parse_next_block(bstream->parser, &block)) {
	case -1:
		/* done / error */
		if (bstream->convert_part != NULL &&
		    bstream->base64_delayed_len > 0) {
			/* flush any pending base64 output */
			stream_encode_base64(bstream, "", 0);
			new_size = stream->pos - stream->skip;
			i_assert(old_size != new_size);
			return new_size - old_size;
		}
		stream->istream.eof = TRUE;
		stream->istream.stream_errno = stream->parent->stream_errno;
		return -1;
	case 0:
		/* need more data */
		return 0;
	default:
		break;
	}

	if (block.part != bstream->convert_part &&
	    bstream->convert_part != NULL) {
		/* end of base64 encoded part */
		stream_encode_base64(bstream, "", 0);
	}

	if (block.hdr != NULL) {
		/* parsing a header */
		if (strcasecmp(block.hdr->name, "Content-Type") == 0)
			bstream->content_type_seen = TRUE;

		if (strcasecmp(block.hdr->name, "Content-Transfer-Encoding") == 0 &&
			 !block.hdr->continued && !block.hdr->continues &&
			 block.hdr->value_len == 6 &&
			 i_memcasecmp(block.hdr->value, "binary", 6) == 0 &&
			 part_can_convert(block.part) &&
			 bstream->convert_part != block.part) {
			/* looks like we want to convert this body part to
			   base64, but if we haven't seen Content-Type yet
			   delay the decision until we've read the rest of
			   the header */
			i_assert(block.part != NULL);
			bstream->convert_part = block.part;
			bstream->base64_block_pos = 0;
			if (!bstream->content_type_seen) {
				i_assert(bstream->hdr_buf == NULL);
				bstream->hdr_buf = buffer_create_dynamic(default_pool, 512);
				stream_add_hdr(bstream, block.hdr);
				bstream->cte_header_len = bstream->hdr_buf->used;
			} else {
				stream_add_data(bstream,
					"Content-Transfer-Encoding: base64\r\n", 35);
			}
		} else if (block.hdr->eoh && bstream->hdr_buf != NULL) {
			/* finish the decision about decoding */
			stream_finish_convert_decision(bstream);
			stream_add_data(bstream, "\r\n", 2);
		} else {
			stream_add_hdr(bstream, block.hdr);
		}
	} else if (block.size == 0) {
		/* end of header */
		if (bstream->hdr_buf != NULL) {
			/* message has no body */
			bstream->convert_part = NULL;
			stream_add_data(bstream, bstream->hdr_buf->data,
					bstream->hdr_buf->used);
			buffer_free(&bstream->hdr_buf);
		}
		bstream->content_type_seen = FALSE;
	} else if (block.part == bstream->convert_part) {
		/* convert body part to base64 */
		stream_encode_base64(bstream, block.data, block.size);
	} else {
		stream_add_data(bstream, block.data, block.size);
	}
	new_size = stream->pos - stream->skip;
	if (new_size == old_size)
		return i_stream_binary_converter_read(stream);
	return new_size - old_size;
}

static void i_stream_binary_converter_close(struct iostream_private *stream,
					    bool close_parent)
{
	struct binary_converter_istream *bstream =
		(struct binary_converter_istream *)stream;
	struct message_part *parts;

	if (bstream->parser != NULL) {
		if (message_parser_deinit(&bstream->parser, &parts) < 0)
			i_unreached(); /* we didn't use preparsed message_parts */
	}
	if (bstream->pool != NULL)
		pool_unref(&bstream->pool);
	if (close_parent)
		i_stream_close(bstream->istream.parent);
}

struct istream *i_stream_create_binary_converter(struct istream *input)
{
	struct binary_converter_istream *bstream;

	bstream = i_new(struct binary_converter_istream, 1);
	bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;

	bstream->istream.read = i_stream_binary_converter_read;
	bstream->istream.iostream.close = i_stream_binary_converter_close;

	bstream->istream.istream.readable_fd = FALSE;
	bstream->istream.istream.blocking = input->blocking;
	bstream->istream.istream.seekable = FALSE;

	bstream->pool = pool_alloconly_create("istream binary converter", 128);
	bstream->parser = message_parser_init(bstream->pool, input, 0,
				MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS |
				MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES);
	return i_stream_create(&bstream->istream, input,
			       i_stream_get_fd(input));
}