python (3.11.7)
1 import os
2 import sys
3 import tempfile
4 import unittest
5 from collections import namedtuple
6 from io import StringIO, BytesIO
7 from test import support
8 from test.support import warnings_helper
9
10 cgi = warnings_helper.import_deprecated("cgi")
11
12
13 class ESC[4;38;5;81mHackedSysModule:
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
19 cgi.sys = HackedSysModule()
20
21 class ESC[4;38;5;81mComparableException:
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
37 def 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
55 parse_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
97 def norm(seq):
98 return sorted(seq, key=repr)
99
100 def first_elts(list):
101 return [p[0] for p in list]
102
103 def first_second_elts(list):
104 return [(p[0], p[1][0]) for p in list]
105
106 def 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
118 class ESC[4;38;5;81mCgiTests(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
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
131 Content-Disposition: form-data; name="submit-name"
132
133 just 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
146 Content-Disposition: form-data; name="submit-name"
147 Content-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 ESC[4;38;5;81mTestReadlineFile:
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
336 Content-Disposition: form-data; name="upload"; filename="fake.txt"
337 Content-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
377 Content-Disposition: form-data; name="submit-name"
378 Content-Length: 5
379
380 Larry
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
394 Content-Disposition: form-data; name="my-arg"; filename="foo"
395
396 Test
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
455 Content-Disposition: form-data; name="a"
456
457 3
458 ---123
459 Content-Type: application/x-www-form-urlencoded
460
461 a=4
462 ---123
463 Content-Type: application/x-www-form-urlencoded
464
465 a=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
493 Content-Disposition: form-data; name="key2"
494
495 value2y
496 ---123
497 Content-Disposition: form-data; name="key3"
498
499 value3
500 ---123
501 Content-Disposition: form-data; name="key4"
502
503 value4
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
517 Content-Disposition: form-data; name="key2"
518
519 value2y
520 ---123
521 Content-Disposition: form-data; name="key3"
522
523 value3
524 ---123
525 Content-Disposition: form-data; name="key4"
526
527 value4
528 ---123
529 Content-Disposition: form-data; name="upload"; filename="fake.txt"
530 Content-Type: text/plain
531
532 this 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
585 BOUNDARY = "---------------------------721837373350705526688164684"
586
587 POSTDATA = """-----------------------------721837373350705526688164684
588 Content-Disposition: form-data; name="id"
589
590 1234
591 -----------------------------721837373350705526688164684
592 Content-Disposition: form-data; name="title"
593
594
595 -----------------------------721837373350705526688164684
596 Content-Disposition: form-data; name="file"; filename="test.txt"
597 Content-Type: text/plain
598
599 Testing 123.
600
601 -----------------------------721837373350705526688164684
602 Content-Disposition: form-data; name="submit"
603
604 Add\x20
605 -----------------------------721837373350705526688164684--
606 """
607
608 POSTDATA_NON_ASCII = """-----------------------------721837373350705526688164684
609 Content-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
616 BOUNDARY_W3 = "AaB03x"
617 POSTDATA_W3 = """--AaB03x
618 Content-Disposition: form-data; name="submit-name"
619
620 Larry
621 --AaB03x
622 Content-Disposition: form-data; name="files"
623 Content-Type: multipart/mixed; boundary=BbC04y
624
625 --BbC04y
626 Content-Disposition: file; filename="file1.txt"
627 Content-Type: text/plain
628
629 ... contents of file1.txt ...
630 --BbC04y
631 Content-Disposition: file; filename="file2.gif"
632 Content-Type: image/gif
633 Content-Transfer-Encoding: binary
634
635 ...contents of file2.gif...
636 --BbC04y--
637 --AaB03x--
638 """
639
640 if __name__ == '__main__':
641 unittest.main()