diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -92,11 +92,14 @@ import re import socket import sys +import stat import time import collections import tempfile import contextlib +import mimetypes import warnings +import threading from urllib.error import URLError, HTTPError, ContentTooShortError @@ -701,6 +704,106 @@ "lead to an infinite loop.\n" \ "The last 30x error message was:\n" +# Utility function for MultipartPostHandler + +_counter_lock = threading._allocate_lock() + +_counter = 0 +def _get_next_counter(): + global _counter + _counter_lock.acquire() + _counter += 1 + result = _counter + _counter_lock.release() + return result + +_prefix = None + +def _choose_boundary(): + """Return a string usable as a multipart boundary. + + The string chosen is unique within a single program run, and + incorporates the user id (if available), process id (if available), + and current time. So it's very unlikely the returned string appears + in message text, but there's no guarantee. + + The boundary contains dots so you have to quote it in the header.""" + + global _prefix + if _prefix is None: + import socket + try: + hostid = socket.gethostbyname(socket.gethostname()) + except socket.gaierror: + hostid = '127.0.0.1' + try: + uid = repr(os.getuid()) + except AttributeError: + uid = '1' + try: + pid = repr(os.getpid()) + except AttributeError: + pid = '1' + _prefix = hostid + '.' + uid + '.' + pid + return "%s.%.3f.%d" % (_prefix, time.time(), _get_next_counter()) + + +class MultipartPostHandler(BaseHandler): + + handler_order = 480 + + def _multipart_encode(self, parts, files, boundary = None, buf=None): + if boundary is None: + boundary = _choose_boundary() + if buf is None: + buf = '' + for (key, value) in parts: + buf += '--%s\r\n' % boundary + buf += 'Content-Disposition: form-data; name="%s"' % key + buf += '\r\n\r\n' + value + '\r\n' + for (key, fd) in files: + file_size = os.fstat(fd.fileno())[stat.ST_SIZE] + filename = fd.name.split('/')[-1] + contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + buf += '--%s\r\n' % boundary + buf += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) + buf += 'Content-Type: %s\r\n' % contenttype + fd.seek(0) + buf += '\r\n' + fd.read() + '\r\n' + buf += '--%s--\r\n\r\n' % boundary + + return boundary, buf + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for (key, value) in data.items(): + if type(value) == file: + v_files.append((key, value)) + else: + v_vars.append((key, value)) + except TypeError: + systype, value, traceback = sys.exc_info() + raise TypeError("Not a Valid non-string sequence" + " or a mapping object") + if len(v_files) == 0: + data = urllib.parse.urlencode(v_vars, doseq) + else: + boundary, data = self._multipart_encode(v_vars, v_files) + contenttype = 'multipart/form-data: boundary=%s' % boundary + if (request.has_header('Content-Type') and\ + request.get_header('Content-Type').find('multipart/form-data') != 0): + # Setting Content-Type to multipart/form-data + request.add_unredirected_header('Content-Type', contenttype) + + request.add_data(data) + return request + + https_request = http_request + def _parse_proxy(proxy): """Return (scheme, user, password, host/port) given a URL or an authority.