1import os 2import sys 3import tempfile 4import unittest 5from collections import namedtuple 6from io import StringIO, BytesIO 7from test import support 8from test.support import warnings_helper 9 10cgi = warnings_helper.import_deprecated("cgi") 11 12 13class HackedSysModule: 14 # The regression test will have real values in sys.argv, which 15 # will completely confuse the test of the cgi module 16 argv = [] 17 stdin = sys.stdin 18 19cgi.sys = HackedSysModule() 20 21class ComparableException: 22 def __init__(self, err): 23 self.err = err 24 25 def __str__(self): 26 return str(self.err) 27 28 def __eq__(self, anExc): 29 if not isinstance(anExc, Exception): 30 return NotImplemented 31 return (self.err.__class__ == anExc.__class__ and 32 self.err.args == anExc.args) 33 34 def __getattr__(self, attr): 35 return getattr(self.err, attr) 36 37def do_test(buf, method): 38 env = {} 39 if method == "GET": 40 fp = None 41 env['REQUEST_METHOD'] = 'GET' 42 env['QUERY_STRING'] = buf 43 elif method == "POST": 44 fp = BytesIO(buf.encode('latin-1')) # FieldStorage expects bytes 45 env['REQUEST_METHOD'] = 'POST' 46 env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' 47 env['CONTENT_LENGTH'] = str(len(buf)) 48 else: 49 raise ValueError("unknown method: %s" % method) 50 try: 51 return cgi.parse(fp, env, strict_parsing=1) 52 except Exception as err: 53 return ComparableException(err) 54 55parse_strict_test_cases = [ 56 ("", {}), 57 ("&", ValueError("bad query field: ''")), 58 ("&&", ValueError("bad query field: ''")), 59 # Should the next few really be valid? 60 ("=", {}), 61 ("=&=", {}), 62 # This rest seem to make sense 63 ("=a", {'': ['a']}), 64 ("&=a", ValueError("bad query field: ''")), 65 ("=a&", ValueError("bad query field: ''")), 66 ("=&a", ValueError("bad query field: 'a'")), 67 ("b=a", {'b': ['a']}), 68 ("b+=a", {'b ': ['a']}), 69 ("a=b=a", {'a': ['b=a']}), 70 ("a=+b=a", {'a': [' b=a']}), 71 ("&b=a", ValueError("bad query field: ''")), 72 ("b&=a", ValueError("bad query field: 'b'")), 73 ("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}), 74 ("a=a+b&a=b+a", {'a': ['a b', 'b a']}), 75 ("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}), 76 ("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env", 77 {'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'], 78 'cuyer': ['r'], 79 'expire': ['964546263'], 80 'kid': ['130003.300038'], 81 'lobale': ['en-US'], 82 'order_id': ['0bb2e248638833d48cb7fed300000f1b'], 83 'ss': ['env'], 84 'view': ['bustomer'], 85 }), 86 87 ("group_id=5470&set=custom&_assigned_to=31392&_status=1&_category=100&SUBMIT=Browse", 88 {'SUBMIT': ['Browse'], 89 '_assigned_to': ['31392'], 90 '_category': ['100'], 91 '_status': ['1'], 92 'group_id': ['5470'], 93 'set': ['custom'], 94 }) 95 ] 96 97def norm(seq): 98 return sorted(seq, key=repr) 99 100def first_elts(list): 101 return [p[0] for p in list] 102 103def first_second_elts(list): 104 return [(p[0], p[1][0]) for p in list] 105 106def gen_result(data, environ): 107 encoding = 'latin-1' 108 fake_stdin = BytesIO(data.encode(encoding)) 109 fake_stdin.seek(0) 110 form = cgi.FieldStorage(fp=fake_stdin, environ=environ, encoding=encoding) 111 112 result = {} 113 for k, v in dict(form).items(): 114 result[k] = isinstance(v, list) and form.getlist(k) or v.value 115 116 return result 117 118class CgiTests(unittest.TestCase): 119 120 def test_parse_multipart(self): 121 fp = BytesIO(POSTDATA.encode('latin1')) 122 env = {'boundary': BOUNDARY.encode('latin1'), 123 'CONTENT-LENGTH': '558'} 124 result = cgi.parse_multipart(fp, env) 125 expected = {'submit': [' Add '], 'id': ['1234'], 126 'file': [b'Testing 123.\n'], 'title': ['']} 127 self.assertEqual(result, expected) 128 129 def test_parse_multipart_without_content_length(self): 130 POSTDATA = '''--JfISa01 131Content-Disposition: form-data; name="submit-name" 132 133just a string 134 135--JfISa01-- 136''' 137 fp = BytesIO(POSTDATA.encode('latin1')) 138 env = {'boundary': 'JfISa01'.encode('latin1')} 139 result = cgi.parse_multipart(fp, env) 140 expected = {'submit-name': ['just a string\n']} 141 self.assertEqual(result, expected) 142 143 def test_parse_multipart_invalid_encoding(self): 144 BOUNDARY = "JfISa01" 145 POSTDATA = """--JfISa01 146Content-Disposition: form-data; name="submit-name" 147Content-Length: 3 148 149\u2603 150--JfISa01""" 151 fp = BytesIO(POSTDATA.encode('utf8')) 152 env = {'boundary': BOUNDARY.encode('latin1'), 153 'CONTENT-LENGTH': str(len(POSTDATA.encode('utf8')))} 154 result = cgi.parse_multipart(fp, env, encoding="ascii", 155 errors="surrogateescape") 156 expected = {'submit-name': ["\udce2\udc98\udc83"]} 157 self.assertEqual(result, expected) 158 self.assertEqual("\u2603".encode('utf8'), 159 result["submit-name"][0].encode('utf8', 'surrogateescape')) 160 161 def test_fieldstorage_properties(self): 162 fs = cgi.FieldStorage() 163 self.assertFalse(fs) 164 self.assertIn("FieldStorage", repr(fs)) 165 self.assertEqual(list(fs), list(fs.keys())) 166 fs.list.append(namedtuple('MockFieldStorage', 'name')('fieldvalue')) 167 self.assertTrue(fs) 168 169 def test_fieldstorage_invalid(self): 170 self.assertRaises(TypeError, cgi.FieldStorage, "not-a-file-obj", 171 environ={"REQUEST_METHOD":"PUT"}) 172 self.assertRaises(TypeError, cgi.FieldStorage, "foo", "bar") 173 fs = cgi.FieldStorage(headers={'content-type':'text/plain'}) 174 self.assertRaises(TypeError, bool, fs) 175 176 def test_strict(self): 177 for orig, expect in parse_strict_test_cases: 178 # Test basic parsing 179 d = do_test(orig, "GET") 180 self.assertEqual(d, expect, "Error parsing %s method GET" % repr(orig)) 181 d = do_test(orig, "POST") 182 self.assertEqual(d, expect, "Error parsing %s method POST" % repr(orig)) 183 184 env = {'QUERY_STRING': orig} 185 fs = cgi.FieldStorage(environ=env) 186 if isinstance(expect, dict): 187 # test dict interface 188 self.assertEqual(len(expect), len(fs)) 189 self.assertCountEqual(expect.keys(), fs.keys()) 190 ##self.assertEqual(norm(expect.values()), norm(fs.values())) 191 ##self.assertEqual(norm(expect.items()), norm(fs.items())) 192 self.assertEqual(fs.getvalue("nonexistent field", "default"), "default") 193 # test individual fields 194 for key in expect.keys(): 195 expect_val = expect[key] 196 self.assertIn(key, fs) 197 if len(expect_val) > 1: 198 self.assertEqual(fs.getvalue(key), expect_val) 199 else: 200 self.assertEqual(fs.getvalue(key), expect_val[0]) 201 202 def test_separator(self): 203 parse_semicolon = [ 204 ("x=1;y=2.0", {'x': ['1'], 'y': ['2.0']}), 205 ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}), 206 (";", ValueError("bad query field: ''")), 207 (";;", ValueError("bad query field: ''")), 208 ("=;a", ValueError("bad query field: 'a'")), 209 (";b=a", ValueError("bad query field: ''")), 210 ("b;=a", ValueError("bad query field: 'b'")), 211 ("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}), 212 ("a=a+b;a=b+a", {'a': ['a b', 'b a']}), 213 ] 214 for orig, expect in parse_semicolon: 215 env = {'QUERY_STRING': orig} 216 fs = cgi.FieldStorage(separator=';', environ=env) 217 if isinstance(expect, dict): 218 for key in expect.keys(): 219 expect_val = expect[key] 220 self.assertIn(key, fs) 221 if len(expect_val) > 1: 222 self.assertEqual(fs.getvalue(key), expect_val) 223 else: 224 self.assertEqual(fs.getvalue(key), expect_val[0]) 225 226 @warnings_helper.ignore_warnings(category=DeprecationWarning) 227 def test_log(self): 228 cgi.log("Testing") 229 230 cgi.logfp = StringIO() 231 cgi.initlog("%s", "Testing initlog 1") 232 cgi.log("%s", "Testing log 2") 233 self.assertEqual(cgi.logfp.getvalue(), "Testing initlog 1\nTesting log 2\n") 234 if os.path.exists(os.devnull): 235 cgi.logfp = None 236 cgi.logfile = os.devnull 237 cgi.initlog("%s", "Testing log 3") 238 self.addCleanup(cgi.closelog) 239 cgi.log("Testing log 4") 240 241 def test_fieldstorage_readline(self): 242 # FieldStorage uses readline, which has the capacity to read all 243 # contents of the input file into memory; we use readline's size argument 244 # to prevent that for files that do not contain any newlines in 245 # non-GET/HEAD requests 246 class TestReadlineFile: 247 def __init__(self, file): 248 self.file = file 249 self.numcalls = 0 250 251 def readline(self, size=None): 252 self.numcalls += 1 253 if size: 254 return self.file.readline(size) 255 else: 256 return self.file.readline() 257 258 def __getattr__(self, name): 259 file = self.__dict__['file'] 260 a = getattr(file, name) 261 if not isinstance(a, int): 262 setattr(self, name, a) 263 return a 264 265 f = TestReadlineFile(tempfile.TemporaryFile("wb+")) 266 self.addCleanup(f.close) 267 f.write(b'x' * 256 * 1024) 268 f.seek(0) 269 env = {'REQUEST_METHOD':'PUT'} 270 fs = cgi.FieldStorage(fp=f, environ=env) 271 self.addCleanup(fs.file.close) 272 # if we're not chunking properly, readline is only called twice 273 # (by read_binary); if we are chunking properly, it will be called 5 times 274 # as long as the chunksize is 1 << 16. 275 self.assertGreater(f.numcalls, 2) 276 f.close() 277 278 def test_fieldstorage_multipart(self): 279 #Test basic FieldStorage multipart parsing 280 env = { 281 'REQUEST_METHOD': 'POST', 282 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 283 'CONTENT_LENGTH': '558'} 284 fp = BytesIO(POSTDATA.encode('latin-1')) 285 fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1") 286 self.assertEqual(len(fs.list), 4) 287 expect = [{'name':'id', 'filename':None, 'value':'1234'}, 288 {'name':'title', 'filename':None, 'value':''}, 289 {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'}, 290 {'name':'submit', 'filename':None, 'value':' Add '}] 291 for x in range(len(fs.list)): 292 for k, exp in expect[x].items(): 293 got = getattr(fs.list[x], k) 294 self.assertEqual(got, exp) 295 296 def test_fieldstorage_multipart_leading_whitespace(self): 297 env = { 298 'REQUEST_METHOD': 'POST', 299 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 300 'CONTENT_LENGTH': '560'} 301 # Add some leading whitespace to our post data that will cause the 302 # first line to not be the innerboundary. 303 fp = BytesIO(b"\r\n" + POSTDATA.encode('latin-1')) 304 fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1") 305 self.assertEqual(len(fs.list), 4) 306 expect = [{'name':'id', 'filename':None, 'value':'1234'}, 307 {'name':'title', 'filename':None, 'value':''}, 308 {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'}, 309 {'name':'submit', 'filename':None, 'value':' Add '}] 310 for x in range(len(fs.list)): 311 for k, exp in expect[x].items(): 312 got = getattr(fs.list[x], k) 313 self.assertEqual(got, exp) 314 315 def test_fieldstorage_multipart_non_ascii(self): 316 #Test basic FieldStorage multipart parsing 317 env = {'REQUEST_METHOD':'POST', 318 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 319 'CONTENT_LENGTH':'558'} 320 for encoding in ['iso-8859-1','utf-8']: 321 fp = BytesIO(POSTDATA_NON_ASCII.encode(encoding)) 322 fs = cgi.FieldStorage(fp, environ=env,encoding=encoding) 323 self.assertEqual(len(fs.list), 1) 324 expect = [{'name':'id', 'filename':None, 'value':'\xe7\xf1\x80'}] 325 for x in range(len(fs.list)): 326 for k, exp in expect[x].items(): 327 got = getattr(fs.list[x], k) 328 self.assertEqual(got, exp) 329 330 def test_fieldstorage_multipart_maxline(self): 331 # Issue #18167 332 maxline = 1 << 16 333 self.maxDiff = None 334 def check(content): 335 data = """---123 336Content-Disposition: form-data; name="upload"; filename="fake.txt" 337Content-Type: text/plain 338 339%s 340---123-- 341""".replace('\n', '\r\n') % content 342 environ = { 343 'CONTENT_LENGTH': str(len(data)), 344 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', 345 'REQUEST_METHOD': 'POST', 346 } 347 self.assertEqual(gen_result(data, environ), 348 {'upload': content.encode('latin1')}) 349 check('x' * (maxline - 1)) 350 check('x' * (maxline - 1) + '\r') 351 check('x' * (maxline - 1) + '\r' + 'y' * (maxline - 1)) 352 353 def test_fieldstorage_multipart_w3c(self): 354 # Test basic FieldStorage multipart parsing (W3C sample) 355 env = { 356 'REQUEST_METHOD': 'POST', 357 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY_W3), 358 'CONTENT_LENGTH': str(len(POSTDATA_W3))} 359 fp = BytesIO(POSTDATA_W3.encode('latin-1')) 360 fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1") 361 self.assertEqual(len(fs.list), 2) 362 self.assertEqual(fs.list[0].name, 'submit-name') 363 self.assertEqual(fs.list[0].value, 'Larry') 364 self.assertEqual(fs.list[1].name, 'files') 365 files = fs.list[1].value 366 self.assertEqual(len(files), 2) 367 expect = [{'name': None, 'filename': 'file1.txt', 'value': b'... contents of file1.txt ...'}, 368 {'name': None, 'filename': 'file2.gif', 'value': b'...contents of file2.gif...'}] 369 for x in range(len(files)): 370 for k, exp in expect[x].items(): 371 got = getattr(files[x], k) 372 self.assertEqual(got, exp) 373 374 def test_fieldstorage_part_content_length(self): 375 BOUNDARY = "JfISa01" 376 POSTDATA = """--JfISa01 377Content-Disposition: form-data; name="submit-name" 378Content-Length: 5 379 380Larry 381--JfISa01""" 382 env = { 383 'REQUEST_METHOD': 'POST', 384 'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY), 385 'CONTENT_LENGTH': str(len(POSTDATA))} 386 fp = BytesIO(POSTDATA.encode('latin-1')) 387 fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1") 388 self.assertEqual(len(fs.list), 1) 389 self.assertEqual(fs.list[0].name, 'submit-name') 390 self.assertEqual(fs.list[0].value, 'Larry') 391 392 def test_field_storage_multipart_no_content_length(self): 393 fp = BytesIO(b"""--MyBoundary 394Content-Disposition: form-data; name="my-arg"; filename="foo" 395 396Test 397 398--MyBoundary-- 399""") 400 env = { 401 "REQUEST_METHOD": "POST", 402 "CONTENT_TYPE": "multipart/form-data; boundary=MyBoundary", 403 "wsgi.input": fp, 404 } 405 fields = cgi.FieldStorage(fp, environ=env) 406 407 self.assertEqual(len(fields["my-arg"].file.read()), 5) 408 409 def test_fieldstorage_as_context_manager(self): 410 fp = BytesIO(b'x' * 10) 411 env = {'REQUEST_METHOD': 'PUT'} 412 with cgi.FieldStorage(fp=fp, environ=env) as fs: 413 content = fs.file.read() 414 self.assertFalse(fs.file.closed) 415 self.assertTrue(fs.file.closed) 416 self.assertEqual(content, 'x' * 10) 417 with self.assertRaisesRegex(ValueError, 'I/O operation on closed file'): 418 fs.file.read() 419 420 _qs_result = { 421 'key1': 'value1', 422 'key2': ['value2x', 'value2y'], 423 'key3': 'value3', 424 'key4': 'value4' 425 } 426 def testQSAndUrlEncode(self): 427 data = "key2=value2x&key3=value3&key4=value4" 428 environ = { 429 'CONTENT_LENGTH': str(len(data)), 430 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 431 'QUERY_STRING': 'key1=value1&key2=value2y', 432 'REQUEST_METHOD': 'POST', 433 } 434 v = gen_result(data, environ) 435 self.assertEqual(self._qs_result, v) 436 437 def test_max_num_fields(self): 438 # For application/x-www-form-urlencoded 439 data = '&'.join(['a=a']*11) 440 environ = { 441 'CONTENT_LENGTH': str(len(data)), 442 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 443 'REQUEST_METHOD': 'POST', 444 } 445 446 with self.assertRaises(ValueError): 447 cgi.FieldStorage( 448 fp=BytesIO(data.encode()), 449 environ=environ, 450 max_num_fields=10, 451 ) 452 453 # For multipart/form-data 454 data = """---123 455Content-Disposition: form-data; name="a" 456 4573 458---123 459Content-Type: application/x-www-form-urlencoded 460 461a=4 462---123 463Content-Type: application/x-www-form-urlencoded 464 465a=5 466---123-- 467""" 468 environ = { 469 'CONTENT_LENGTH': str(len(data)), 470 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', 471 'QUERY_STRING': 'a=1&a=2', 472 'REQUEST_METHOD': 'POST', 473 } 474 475 # 2 GET entities 476 # 1 top level POST entities 477 # 1 entity within the second POST entity 478 # 1 entity within the third POST entity 479 with self.assertRaises(ValueError): 480 cgi.FieldStorage( 481 fp=BytesIO(data.encode()), 482 environ=environ, 483 max_num_fields=4, 484 ) 485 cgi.FieldStorage( 486 fp=BytesIO(data.encode()), 487 environ=environ, 488 max_num_fields=5, 489 ) 490 491 def testQSAndFormData(self): 492 data = """---123 493Content-Disposition: form-data; name="key2" 494 495value2y 496---123 497Content-Disposition: form-data; name="key3" 498 499value3 500---123 501Content-Disposition: form-data; name="key4" 502 503value4 504---123-- 505""" 506 environ = { 507 'CONTENT_LENGTH': str(len(data)), 508 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', 509 'QUERY_STRING': 'key1=value1&key2=value2x', 510 'REQUEST_METHOD': 'POST', 511 } 512 v = gen_result(data, environ) 513 self.assertEqual(self._qs_result, v) 514 515 def testQSAndFormDataFile(self): 516 data = """---123 517Content-Disposition: form-data; name="key2" 518 519value2y 520---123 521Content-Disposition: form-data; name="key3" 522 523value3 524---123 525Content-Disposition: form-data; name="key4" 526 527value4 528---123 529Content-Disposition: form-data; name="upload"; filename="fake.txt" 530Content-Type: text/plain 531 532this is the content of the fake file 533 534---123-- 535""" 536 environ = { 537 'CONTENT_LENGTH': str(len(data)), 538 'CONTENT_TYPE': 'multipart/form-data; boundary=-123', 539 'QUERY_STRING': 'key1=value1&key2=value2x', 540 'REQUEST_METHOD': 'POST', 541 } 542 result = self._qs_result.copy() 543 result.update({ 544 'upload': b'this is the content of the fake file\n' 545 }) 546 v = gen_result(data, environ) 547 self.assertEqual(result, v) 548 549 def test_parse_header(self): 550 self.assertEqual( 551 cgi.parse_header("text/plain"), 552 ("text/plain", {})) 553 self.assertEqual( 554 cgi.parse_header("text/vnd.just.made.this.up ; "), 555 ("text/vnd.just.made.this.up", {})) 556 self.assertEqual( 557 cgi.parse_header("text/plain;charset=us-ascii"), 558 ("text/plain", {"charset": "us-ascii"})) 559 self.assertEqual( 560 cgi.parse_header('text/plain ; charset="us-ascii"'), 561 ("text/plain", {"charset": "us-ascii"})) 562 self.assertEqual( 563 cgi.parse_header('text/plain ; charset="us-ascii"; another=opt'), 564 ("text/plain", {"charset": "us-ascii", "another": "opt"})) 565 self.assertEqual( 566 cgi.parse_header('attachment; filename="silly.txt"'), 567 ("attachment", {"filename": "silly.txt"})) 568 self.assertEqual( 569 cgi.parse_header('attachment; filename="strange;name"'), 570 ("attachment", {"filename": "strange;name"})) 571 self.assertEqual( 572 cgi.parse_header('attachment; filename="strange;name";size=123;'), 573 ("attachment", {"filename": "strange;name", "size": "123"})) 574 self.assertEqual( 575 cgi.parse_header('form-data; name="files"; filename="fo\\"o;bar"'), 576 ("form-data", {"name": "files", "filename": 'fo"o;bar'})) 577 578 def test_all(self): 579 not_exported = { 580 "logfile", "logfp", "initlog", "dolog", "nolog", "closelog", "log", 581 "maxlen", "valid_boundary"} 582 support.check__all__(self, cgi, not_exported=not_exported) 583 584 585BOUNDARY = "---------------------------721837373350705526688164684" 586 587POSTDATA = """-----------------------------721837373350705526688164684 588Content-Disposition: form-data; name="id" 589 5901234 591-----------------------------721837373350705526688164684 592Content-Disposition: form-data; name="title" 593 594 595-----------------------------721837373350705526688164684 596Content-Disposition: form-data; name="file"; filename="test.txt" 597Content-Type: text/plain 598 599Testing 123. 600 601-----------------------------721837373350705526688164684 602Content-Disposition: form-data; name="submit" 603 604 Add\x20 605-----------------------------721837373350705526688164684-- 606""" 607 608POSTDATA_NON_ASCII = """-----------------------------721837373350705526688164684 609Content-Disposition: form-data; name="id" 610 611\xe7\xf1\x80 612-----------------------------721837373350705526688164684 613""" 614 615# http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4 616BOUNDARY_W3 = "AaB03x" 617POSTDATA_W3 = """--AaB03x 618Content-Disposition: form-data; name="submit-name" 619 620Larry 621--AaB03x 622Content-Disposition: form-data; name="files" 623Content-Type: multipart/mixed; boundary=BbC04y 624 625--BbC04y 626Content-Disposition: file; filename="file1.txt" 627Content-Type: text/plain 628 629... contents of file1.txt ... 630--BbC04y 631Content-Disposition: file; filename="file2.gif" 632Content-Type: image/gif 633Content-Transfer-Encoding: binary 634 635...contents of file2.gif... 636--BbC04y-- 637--AaB03x-- 638""" 639 640if __name__ == '__main__': 641 unittest.main() 642