1 /* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
2 
3 #include "lib.h"
4 #include "array.h"
5 #include "str.h"
6 #include "sha1.h"
7 #include "hash-format.h"
8 #include "safe-mkstemp.h"
9 #include "istream.h"
10 #include "istream-crlf.h"
11 #include "istream-attachment-extractor.h"
12 #include "istream-attachment-connector.h"
13 #include "ostream.h"
14 #include "test-common.h"
15 
16 #include <stdio.h>
17 #include <unistd.h>
18 
19 #define BINARY_TEXT_LONG "we have\ra lot \nof \0binary stuff in here\n" \
20 "b adjig sadjg jasidgjiaehga3wht8a3w8ghxjc dsgad hasdghsd gasd ds" \
21 "jdsoga sjdga0w3tjhawjgsertniq3n5oqerjqw2r89q23h awhrqh835r8a"
22 #define BINARY_TEXT_LONG_BASE64 \
23 "d2UgaGF2ZQ1hIGxvdCAKb2YgAGJpbmFyeSBzdHVmZiBpbiBoZXJlCmIgYWRqaWcgc2FkamcgamFz\r\n" \
24 "aWRnamlhZWhnYTN3aHQ4YTN3OGdoeGpjIGRzZ2FkIGhhc2RnaHNkIGdhc2QgZHNqZHNvZ2Egc2pk\r\n" \
25 "Z2EwdzN0amhhd2pnc2VydG5pcTNuNW9xZXJqcXcycjg5cTIzaCBhd2hycWg4MzVyOGE="
26 
27 #define BINARY_TEXT_SHORT "eh"
28 #define BINARY_TEXT_SHORT_BASE64 "ZWg="
29 
30 static const char mail_input[] =
31 "MIME-Version: 1.0\r\n"
32 "Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
33 "\r\n"
34 "mime header\r\n"
35 "\r\n--bound\r\n"
36 "Content-Transfer-Encoding: base64\r\n"
37 "Content-Type: text/plain\r\n"
38 "\r\n"
39 BINARY_TEXT_LONG_BASE64
40 "\r\n--bound\r\n"
41 "Content-Type: text/plain\r\n"
42 "Content-Transfer-Encoding: base64\r\n"
43 "\r\n"
44 BINARY_TEXT_SHORT_BASE64
45 "\r\n--bound--\r\n";
46 
47 static const char mail_output[] =
48 "MIME-Version: 1.0\r\n"
49 "Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
50 "\r\n"
51 "mime header\r\n"
52 "\r\n--bound\r\n"
53 "Content-Transfer-Encoding: base64\r\n"
54 "Content-Type: text/plain\r\n"
55 "\r\n"
56 "\r\n--bound\r\n"
57 "Content-Type: text/plain\r\n"
58 "Content-Transfer-Encoding: base64\r\n"
59 "\r\n"
60 "\r\n--bound--\r\n";
61 
62 static const char *mail_broken_input_body_prefix =
63 "MIME-Version: 1.0\r\n"
64 "Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
65 "\r\n"
66 "--bound\r\n"
67 "Content-Transfer-Encoding: base64\r\n"
68 "Content-Type: text/plain\r\n"
69 "\r\n";
70 
71 static const char *mail_broken_input_bodies[] = {
72 	/* broken base64 input */
73 	"Zm9vCg=\n",
74 	"Zm9vCg\n",
75 	"Zm9vC\n",
76 	/* extra whitespace */
77 	"Zm9v\n Zm9v\n",
78 	"Zm9v \nZm9v\n",
79 	/* mixed LF vs CRLFs */
80 	"Zm9vYmFy\r\nZm9vYmFy\n",
81 	"Zm9vYmFy\nZm9vYmFy\r\n",
82 	/* line length increases */
83 	"Zm9v\nZm9vYmFy\n",
84 	"Zm9v\nZm9vCg==",
85 	"Zm9v\nZm9vYgo="
86 };
87 
88 static const char *mail_nonbroken_input_bodies[] = {
89 	/* suffixes with explicit '=' end */
90 	"Zm9vCg==",
91 	"Zm9vCg==\n",
92 	"Zm9vCg==\r\n",
93 	"Zm9vCg==\nfoo\n",
94 	"Zm9vCg==\r\nfoo\n",
95 	"Zm9vCg==  \t\t\n\n",
96 	/* suffixes with shorter line length */
97 	"Zm9vYmFy\nZm9v\n",
98 	"Zm9vYmFy\r\nZm9v\r\n",
99 	"Zm9vYmFy\nZm9v\nfoo\n",
100 	"Zm9vYmFy\r\nZm9v\r\nfoo\n",
101 	"Zm9vYmFy\nZm9v\n  \t\t\n\n",
102 	/* suffixes with empty line */
103 	"Zm9v\n\n",
104 	"Zm9v\r\n\r\n",
105 	"Zm9v\n\nfoo\n"
106 	"Zm9v\r\n\nfoo\n"
107 	"Zm9v\r\n\r\nfoo\n"
108 #if 0
109 	/* the whitespace here could be handled as suffixes, but for now
110 	   they're not: */
111 	"Zm9v ",
112 	"Zm9v \n"
113 #endif
114 };
115 
116 struct attachment {
117 	size_t buffer_offset;
118 	uoff_t start_offset;
119 	uoff_t encoded_size, decoded_size;
120 	unsigned int base64_blocks_per_line;
121 };
122 
123 static buffer_t *attachment_data;
124 static ARRAY(struct attachment) attachments;
125 
test_open_temp_fd(void * context ATTR_UNUSED)126 static int test_open_temp_fd(void *context ATTR_UNUSED)
127 {
128 	string_t *str = t_str_new(128);
129 	int fd;
130 
131 	str_append(str, "/tmp/dovecot-test.");
132 	fd = safe_mkstemp(str, 0600, (uid_t)-1, (gid_t)-1);
133 	if (fd == -1)
134 		i_fatal("safe_mkstemp(%s) failed: %m", str_c(str));
135 	i_unlink(str_c(str));
136 	return fd;
137 }
138 
test_open_attachment_ostream(struct istream_attachment_info * info,struct ostream ** output_r,const char ** error_r ATTR_UNUSED,void * context ATTR_UNUSED)139 static int test_open_attachment_ostream(struct istream_attachment_info *info,
140 					struct ostream **output_r,
141 					const char **error_r ATTR_UNUSED,
142 					void *context ATTR_UNUSED)
143 {
144 	struct attachment *a;
145 
146 	if (attachment_data == NULL)
147 		attachment_data = buffer_create_dynamic(default_pool, 1024);
148 	if (!array_is_created(&attachments))
149 		i_array_init(&attachments, 8);
150 	a = array_append_space(&attachments);
151 	a->buffer_offset = attachment_data->used;
152 	a->start_offset = info->start_offset;
153 	a->encoded_size = info->encoded_size;
154 	a->base64_blocks_per_line = info->base64_blocks_per_line;
155 	test_assert(strlen(info->hash) == 160/8*2); /* sha1 size */
156 
157 	*output_r = o_stream_create_buffer(attachment_data);
158 	if (o_stream_seek(*output_r, a->buffer_offset) < 0)
159 		i_unreached();
160 	return 0;
161 }
162 
163 static int
test_open_attachment_ostream_error(struct istream_attachment_info * info ATTR_UNUSED,struct ostream ** output_r ATTR_UNUSED,const char ** error_r,void * context ATTR_UNUSED)164 test_open_attachment_ostream_error(struct istream_attachment_info *info ATTR_UNUSED,
165 				   struct ostream **output_r ATTR_UNUSED,
166 				   const char **error_r,
167 				   void *context ATTR_UNUSED)
168 {
169 	*error_r = "test open error";
170 	return -1;
171 }
172 
test_close_attachment_ostream(struct ostream * output,bool success,const char ** error_r ATTR_UNUSED,void * context ATTR_UNUSED)173 static int test_close_attachment_ostream(struct ostream *output, bool success,
174 					 const char **error_r ATTR_UNUSED,
175 					 void *context ATTR_UNUSED)
176 {
177 	struct attachment *a;
178 
179 	i_assert(success);
180 
181 	a = array_back_modifiable(&attachments);
182 	a->decoded_size = output->offset - a->buffer_offset;
183 
184 	if (o_stream_finish(output) < 0)
185 		i_unreached();
186 	o_stream_destroy(&output);
187 	return 0;
188 }
189 
190 static int
test_close_attachment_ostream_error(struct ostream * output,bool success,const char ** error,void * context ATTR_UNUSED)191 test_close_attachment_ostream_error(struct ostream *output,
192 				    bool success, const char **error,
193 				    void *context ATTR_UNUSED)
194 {
195 	if (success)
196 		*error = "test output error";
197 	o_stream_abort(output);
198 	o_stream_destroy(&output);
199 	return -1;
200 }
201 
202 static struct istream *
test_build_original_istream(struct istream * base_input,uoff_t msg_size)203 test_build_original_istream(struct istream *base_input, uoff_t msg_size)
204 {
205 	struct istream_attachment_connector *conn;
206 	const unsigned char *data = attachment_data->data;
207 	const struct attachment *a;
208 	struct istream *input;
209 	uoff_t data_size = attachment_data->used;
210 	const char *error;
211 
212 	conn = istream_attachment_connector_begin(base_input, msg_size);
213 	array_foreach(&attachments, a) {
214 		input = i_stream_create_from_data(data, a->decoded_size);
215 		if (istream_attachment_connector_add(conn, input,
216 				a->start_offset, a->encoded_size,
217 				a->base64_blocks_per_line, TRUE, &error) < 0)
218 			i_unreached();
219 		i_stream_unref(&input);
220 
221 		i_assert(a->decoded_size <= data_size);
222 		data += a->decoded_size;
223 		data_size -= a->decoded_size;
224 	}
225 	i_assert(data_size == 0);
226 	return istream_attachment_connector_finish(&conn);
227 }
228 
229 static void
get_istream_attachment_settings(struct istream_attachment_settings * set_r)230 get_istream_attachment_settings(struct istream_attachment_settings *set_r)
231 {
232 	const char *error;
233 
234 	i_zero(set_r);
235 	set_r->min_size = 1;
236 	set_r->drain_parent_input = TRUE;
237 	set_r->open_temp_fd = test_open_temp_fd;
238 	set_r->open_attachment_ostream = test_open_attachment_ostream;
239 	set_r->close_attachment_ostream= test_close_attachment_ostream;
240 	if (hash_format_init("%{sha1}", &set_r->hash_format, &error) < 0)
241 		i_unreached();
242 }
243 
test_input_stream(struct istream * file_input)244 static int test_input_stream(struct istream *file_input)
245 {
246 	struct istream_attachment_settings set;
247 	struct istream *input, *input2;
248 	const unsigned char *data;
249 	size_t size;
250 	struct sha1_ctxt hash;
251 	uoff_t msg_size, orig_msg_size;
252 	buffer_t *base_buf;
253 	unsigned char hash_file[SHA1_RESULTLEN], hash_attached[SHA1_RESULTLEN];
254 	int ret = 0;
255 
256 	/* get hash when directly reading input */
257 	input = i_stream_create_crlf(file_input);
258 	sha1_init(&hash);
259 	while (i_stream_read_more(input, &data, &size) > 0) {
260 		sha1_loop(&hash, data, size);
261 		i_stream_skip(input, size);
262 	}
263 	sha1_result(&hash, hash_file);
264 	msg_size = orig_msg_size = input->v_offset;
265 	i_stream_unref(&input);
266 
267 	/* read through attachment extractor */
268 	get_istream_attachment_settings(&set);
269 
270 	i_stream_seek(file_input, 0);
271 	input = i_stream_create_crlf(file_input);
272 	input2 = i_stream_create_attachment_extractor(input, &set, NULL);
273 	i_stream_unref(&input);
274 	base_buf = buffer_create_dynamic(default_pool, 1024);
275 	while (i_stream_read_more(input2, &data, &size) > 0) {
276 		buffer_append(base_buf, data, size);
277 		i_stream_skip(input2, size);
278 	}
279 	i_stream_unref(&input2);
280 
281 	/* rebuild the original stream and see if the hash matches */
282 	for (unsigned int i = 0; i < 2; i++) {
283 		input2 = i_stream_create_from_data(base_buf->data, base_buf->used);
284 		input = test_build_original_istream(input2, msg_size);
285 		i_stream_unref(&input2);
286 
287 		sha1_init(&hash);
288 		while (i_stream_read_more(input, &data, &size) > 0) {
289 			sha1_loop(&hash, data, size);
290 			i_stream_skip(input, size);
291 		}
292 		test_assert_idx(input->eof && input->stream_errno == 0, i);
293 		sha1_result(&hash, hash_attached);
294 		i_stream_unref(&input);
295 
296 		if (memcmp(hash_file, hash_attached, SHA1_RESULTLEN) != 0)
297 			ret = -1;
298 
299 		/* try again without knowing the message's size */
300 		msg_size = UOFF_T_MAX;
301 	}
302 
303 	/* try with a wrong message size */
304 	for (int i = 0; i < 2; i++) {
305 		input2 = i_stream_create_from_data(base_buf->data, base_buf->used);
306 		input = test_build_original_istream(input2,
307 				i == 0 ? orig_msg_size + 1 : orig_msg_size - 1);
308 		i_stream_unref(&input2);
309 		while (i_stream_read_more(input, &data, &size) > 0)
310 			i_stream_skip(input, size);
311 		test_assert(input->stream_errno == (i == 0 ? EPIPE : EINVAL));
312 		i_stream_unref(&input);
313 	}
314 
315 	buffer_free(&base_buf);
316 	buffer_free(&attachment_data);
317 	if (array_is_created(&attachments))
318 		array_free(&attachments);
319 	return ret;
320 }
321 
test_istream_attachment(void)322 static void test_istream_attachment(void)
323 {
324 	struct istream_attachment_settings set;
325 	struct istream *datainput, *input;
326 	const unsigned char *data;
327 	size_t i, size;
328 	int ret;
329 
330 	test_begin("istream attachment");
331 	datainput = test_istream_create_data(mail_input, sizeof(mail_input));
332 	test_istream_set_allow_eof(datainput, FALSE);
333 
334 	get_istream_attachment_settings(&set);
335 	input = i_stream_create_attachment_extractor(datainput, &set, NULL);
336 
337 	for (i = 1; i <= sizeof(mail_input); i++) {
338 		test_istream_set_size(datainput, i);
339 		while ((ret = i_stream_read(input)) > 0) ;
340 		test_assert(ret == 0);
341 	}
342 	test_istream_set_allow_eof(datainput, TRUE);
343 	while ((ret = i_stream_read(input)) > 0) ;
344 	test_assert(ret == -1);
345 
346 	data = i_stream_get_data(input, &size);
347 	test_assert(size == sizeof(mail_output) &&
348 		    memcmp(data, mail_output, size) == 0);
349 
350 	data = attachment_data->data;
351 	test_assert(attachment_data->used ==
352 		    sizeof(BINARY_TEXT_LONG)-1 + strlen(BINARY_TEXT_SHORT));
353 	test_assert(memcmp(data, BINARY_TEXT_LONG, sizeof(BINARY_TEXT_LONG)-1) == 0);
354 	test_assert(memcmp(data + sizeof(BINARY_TEXT_LONG)-1,
355 			   BINARY_TEXT_SHORT, strlen(BINARY_TEXT_SHORT)) == 0);
356 	i_stream_unref(&input);
357 	i_stream_unref(&datainput);
358 
359 	buffer_free(&attachment_data);
360 	if (array_is_created(&attachments))
361 		array_free(&attachments);
362 	test_end();
363 }
364 
test_istream_attachment_extractor_one(const char * body,int err_type)365 static bool test_istream_attachment_extractor_one(const char *body, int err_type)
366 {
367 	const size_t prefix_len = strlen(mail_broken_input_body_prefix);
368 	struct istream_attachment_settings set;
369 	struct istream *datainput, *input;
370 	char *mail_text;
371 	const unsigned char *data;
372 	size_t size;
373 	int ret;
374 	bool unchanged;
375 
376 	mail_text = i_strconcat(mail_broken_input_body_prefix, body, NULL);
377 	datainput = test_istream_create_data(mail_text, strlen(mail_text));
378 
379 	get_istream_attachment_settings(&set);
380 	if (err_type == 1)
381 		set.open_attachment_ostream = test_open_attachment_ostream_error;
382 	else if (err_type == 2)
383 		set.close_attachment_ostream = test_close_attachment_ostream_error;
384 	input = i_stream_create_attachment_extractor(datainput, &set, NULL);
385 
386 	while ((ret = i_stream_read(input)) > 0) ;
387 	if (err_type != 0) {
388 		test_assert(ret == -1 && input->stream_errno == EIO);
389 		unchanged = FALSE;
390 		goto cleanup;
391 	}
392 	test_assert(ret == -1 && input->stream_errno == 0);
393 
394 	data = i_stream_get_data(input, &size);
395 	i_assert(size >= prefix_len &&
396 		 memcmp(data, mail_broken_input_body_prefix, prefix_len) == 0);
397 	data += prefix_len;
398 	size -= prefix_len;
399 
400 	i_assert(attachment_data != NULL);
401 	unchanged = attachment_data->used <= strlen(body) &&
402 		memcmp(attachment_data->data, body, attachment_data->used) == 0 &&
403 		strlen(body) - attachment_data->used == size &&
404 		memcmp(data, body + attachment_data->used, size) == 0;
405 
406 cleanup:
407 	buffer_free(&attachment_data);
408 	if (array_is_created(&attachments))
409 		array_free(&attachments);
410 
411 	i_stream_unref(&input);
412 	i_stream_unref(&datainput);
413 	i_free(mail_text);
414 	return unchanged;
415 }
416 
test_istream_attachment_extractor(void)417 static void test_istream_attachment_extractor(void)
418 {
419 	unsigned int i;
420 
421 	test_begin("istream attachment extractor");
422 	for (i = 0; i < N_ELEMENTS(mail_broken_input_bodies); i++)
423 		test_assert(test_istream_attachment_extractor_one(mail_broken_input_bodies[i], 0));
424 	for (i = 0; i < N_ELEMENTS(mail_nonbroken_input_bodies); i++)
425 		test_assert(!test_istream_attachment_extractor_one(mail_nonbroken_input_bodies[i], 0));
426 	test_end();
427 }
428 
test_istream_attachment_extractor_error(void)429 static void test_istream_attachment_extractor_error(void)
430 {
431 	unsigned int i;
432 
433 	test_begin("istream attachment extractor error");
434 	for (int err_type = 1; err_type <= 2; err_type++) {
435 		for (i = 0; i < N_ELEMENTS(mail_broken_input_bodies); i++)
436 			test_istream_attachment_extractor_one(mail_broken_input_bodies[i], err_type);
437 		for (i = 0; i < N_ELEMENTS(mail_nonbroken_input_bodies); i++)
438 			test_istream_attachment_extractor_one(mail_nonbroken_input_bodies[i], err_type);
439 	}
440 	test_end();
441 }
442 
test_istream_attachment_connector(void)443 static void test_istream_attachment_connector(void)
444 {
445 	struct istream *input;
446 
447 	test_begin("istream attachment connector");
448 	input = i_stream_create_from_data(mail_input, sizeof(mail_input));
449 	test_assert(test_input_stream(input) == 0);
450 	i_stream_unref(&input);
451 	test_end();
452 }
453 
test_input_file(const char * path)454 static int test_input_file(const char *path)
455 {
456 	struct istream *file_input;
457 	int ret = 0;
458 
459 	lib_init();
460 
461 	file_input = i_stream_create_file(path, 64);
462 	if (test_input_stream(file_input) < 0) {
463 		fprintf(stderr, "istream-attachment-extractor: mismatch on file %s\n",
464 			path);
465 		ret = -1;
466 	}
467 	i_stream_unref(&file_input);
468 
469 	lib_deinit();
470 	return ret;
471 }
472 
main(int argc,char * argv[])473 int main(int argc, char *argv[])
474 {
475 	static void (*const test_functions[])(void) = {
476 		test_istream_attachment,
477 		test_istream_attachment_extractor,
478 		test_istream_attachment_extractor_error,
479 		test_istream_attachment_connector,
480 		NULL
481 	};
482 	if (argc > 1)
483 		return test_input_file(argv[1]) < 0 ? 1 : 0;
484 	else
485 		return test_run(test_functions);
486 }
487