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