def test_iri_support(self): self.assertEqual( urls.uri_to_iri('http://xn--n3h.net/'), 'http://\u2603.net/' ) self.assertEqual( urls.uri_to_iri( 'http://%C3%BCser:p%C3%[email protected]/p%C3%A5th' ), 'http://\xfcser:p\xe4ssword@\u2603.net/p\xe5th' ) self.assertEqual( urls.iri_to_uri('http://☃.net/'), 'http://xn--n3h.net/' ) self.assertEqual( urls.iri_to_uri('http://üser:pässword@☃.net/påth'), 'http://%C3%BCser:p%C3%[email protected]/p%C3%A5th' ) self.assertEqual( urls.uri_to_iri('http://test.com/%3Fmeh?foo=%26%2F'), 'http://test.com/%3Fmeh?foo=%26%2F' ) self.assertEqual(urls.iri_to_uri('/foo'), '/foo') self.assertEqual( urls.iri_to_uri('http://föö.com:8080/bam/baz'), 'http://xn--f-1gaa.com:8080/bam/baz' )
def test_iri_safe_conversion(self): self.assertEqual( urls.iri_to_uri('magnet:?foo=bar'), 'magnet:?foo=bar' ) self.assertEqual( urls.iri_to_uri('itms-service://?foo=bar'), 'itms-service:?foo=bar' ) self.assertEqual( urls.iri_to_uri('itms-service://?foo=bar', safe_conversion=True), 'itms-service://?foo=bar' )
def redirect(location, code=302, Response=None): """Returns a response object (a WSGI application) that, if called, redirects the client to the target location. Supported codes are 301, 302, 303, 305, and 307. 300 is not supported because it's not a real redirect and 304 because it's the answer for a request with a request with defined If-Modified-Since headers. :param location: The location the response should redirect to. :param code: The redirect status code. defaults to 302. :param class Response: A Response class to use when instantiating a response. The default is :class:`verktyg.responses.Response` if unspecified. """ if Response is None: from verktyg.responses import Response display_location = escape(location) if isinstance(location, str): # Safe conversion is necessary here as we might redirect # to a broken URI scheme (for instance itms-services). from verktyg.urls import iri_to_uri location = iri_to_uri(location, safe_conversion=True) response = Response( '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n' '<title>Redirecting...</title>\n' '<h1>Redirecting...</h1>\n' '<p>You should be redirected automatically to target URL: ' '<a href="%s">%s</a>. If not click the link.' % (escape(location), display_location), code, mimetype='text/html') response.headers['Location'] = location return response
def redirect(location, code=302, Response=None): """Returns a response object (a WSGI application) that, if called, redirects the client to the target location. Supported codes are 301, 302, 303, 305, and 307. 300 is not supported because it's not a real redirect and 304 because it's the answer for a request with a request with defined If-Modified-Since headers. :param location: The location the response should redirect to. :param code: The redirect status code. defaults to 302. :param class Response: A Response class to use when instantiating a response. The default is :class:`verktyg.responses.Response` if unspecified. """ if Response is None: from verktyg.responses import Response display_location = escape(location) if isinstance(location, str): # Safe conversion is necessary here as we might redirect # to a broken URI scheme (for instance itms-services). from verktyg.urls import iri_to_uri location = iri_to_uri(location) response = Response( '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n' '<title>Redirecting...</title>\n' '<h1>Redirecting...</h1>\n' '<p>You should be redirected automatically to target URL: ' '<a href="%s">%s</a>. If not click the link.' % (escape(location), display_location), code, mimetype='text/html' ) response.headers['Location'] = location return response
def __init__( self, path='/', base_url=None, query_string=None, method='GET', input_stream=None, content_type=None, content_length=None, errors_stream=None, multithread=False, multiprocess=False, run_once=False, headers=None, data=None, environ_base=None, environ_overrides=None, charset='utf-8', ): if query_string is None and '?' in path: path, query_string = path.split('?', 1) self.charset = charset self.path = iri_to_uri(path) if base_url is not None: base_url = url_fix(base_url) self.base_url = base_url if isinstance(query_string, (bytes, str)): self.query_string = query_string else: if query_string is None: query_string = MultiDict() elif not isinstance(query_string, MultiDict): query_string = MultiDict(query_string) self.args = query_string self.method = method if headers is None: headers = Headers() elif not isinstance(headers, Headers): headers = Headers(headers) self.headers = headers if content_type is not None: self.content_type = content_type if errors_stream is None: errors_stream = sys.stderr self.errors_stream = errors_stream self.multithread = multithread self.multiprocess = multiprocess self.run_once = run_once self.environ_base = environ_base self.environ_overrides = environ_overrides self.input_stream = input_stream self.content_length = content_length self.closed = False if data: if input_stream is not None: raise TypeError('can\'t provide input stream and data') if isinstance(data, str): data = data.encode(self.charset) if isinstance(data, bytes): self.input_stream = BytesIO(data) if self.content_length is None: self.content_length = len(data) else: for key, value in _iter_data(data): if ( isinstance(value, (tuple, dict)) or hasattr(value, 'read') ): self._add_file_from_data(key, value) else: self.form.setlistdefault(key).append(value)
def test_uri_iri_normalization(self): uri = 'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93' iri = 'http://föñ.com/\N{BALLOT BOX}/fred?utf8=\u2713' tests = [ 'http://föñ.com/\N{BALLOT BOX}/fred?utf8=\u2713', 'http://xn--f-rgao.com/\u2610/fred?utf8=\N{CHECK MARK}', 'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93', 'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93', 'http://föñ.com/\u2610/fred?utf8=%E2%9C%93', ] for test in tests: self.assertEqual(urls.uri_to_iri(test), iri) self.assertEqual(urls.iri_to_uri(test), uri) self.assertEqual(urls.uri_to_iri(urls.iri_to_uri(test)), iri) self.assertEqual(urls.iri_to_uri(urls.uri_to_iri(test)), uri) self.assertEqual(urls.uri_to_iri(urls.uri_to_iri(test)), iri) self.assertEqual(urls.iri_to_uri(urls.iri_to_uri(test)), uri)
def __init__(self, path='/', base_url=None, query_string=None, method='GET', input_stream=None, content_type=None, content_length=None, errors_stream=None, multithread=False, multiprocess=False, run_once=False, headers=None, data=None, environ_base=None, environ_overrides=None, charset='utf-8'): if query_string is None and '?' in path: path, query_string = path.split('?', 1) self.charset = charset self.path = iri_to_uri(path) if base_url is not None: base_url = url_fix(iri_to_uri(base_url, charset), charset) self.base_url = base_url if isinstance(query_string, (bytes, str)): self.query_string = query_string else: if query_string is None: query_string = MultiDict() elif not isinstance(query_string, MultiDict): query_string = MultiDict(query_string) self.args = query_string self.method = method if headers is None: headers = Headers() elif not isinstance(headers, Headers): headers = Headers(headers) self.headers = headers if content_type is not None: self.content_type = content_type if errors_stream is None: errors_stream = sys.stderr self.errors_stream = errors_stream self.multithread = multithread self.multiprocess = multiprocess self.run_once = run_once self.environ_base = environ_base self.environ_overrides = environ_overrides self.input_stream = input_stream self.content_length = content_length self.closed = False if data: if input_stream is not None: raise TypeError('can\'t provide input stream and data') if isinstance(data, str): data = data.encode(self.charset) if isinstance(data, bytes): self.input_stream = BytesIO(data) if self.content_length is None: self.content_length = len(data) else: for key, value in _iter_data(data): if (isinstance(value, (tuple, dict)) or hasattr(value, 'read')): self._add_file_from_data(key, value) else: self.form.setlistdefault(key).append(value)
def test_iri_safe_quoting(self): uri = 'http://xn--f-1gaa.com/%2F%25?q=%C3%B6&x=%3D%25#%25' iri = 'http://föö.com/%2F%25?q=ö&x=%3D%25#%25' self.assertEqual(urls.uri_to_iri(uri), iri) self.assertEqual(urls.iri_to_uri(urls.uri_to_iri(uri)), uri)
def test_uri_to_iri_to_uri(self): uri = 'http://xn--f-rgao.com/%C3%9E' iri = urls.uri_to_iri(uri) self.assertEqual(urls.iri_to_uri(iri), uri)
def test_iri_to_uri_to_iri(self): iri = 'http://föö.com/' uri = urls.iri_to_uri(iri) self.assertEqual(urls.uri_to_iri(uri), iri)
def test_iri_to_uri_idempotence_non_ascii(self): uri = 'http://\N{SNOWMAN}/\N{SNOWMAN}' uri = urls.iri_to_uri(uri) self.assertEqual(urls.iri_to_uri(uri), uri)
def dump_cookie( key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, charset='utf-8', sync_expires=True, ): """Creates a new Set-Cookie header without the ``Set-Cookie`` prefix The parameters are the same as in the cookie Morsel object in the Python standard library but it accepts unicode data, too. The return value of this function will be a unicode string tunneled through latin1 as required by PEP 3333. The return value is not ASCII safe if the key contains unicode characters. This is technically against the specification but happens in the wild. It's strongly recommended to not use non-ASCII values for the keys. :param max_age: Should be a number of seconds, or `None` (default) if the cookie should last only as long as the client's browser session. Additionally `timedelta` objects are accepted, too. :param expires: Should be a `datetime` object or unix timestamp. :param path: Limits the cookie to a given path, per default it will span the whole domain. :param domain: Use this if you want to set a cross-domain cookie. For example, ``domain=".example.com"`` will set a cookie that is readable by the domain ``www.example.com``, ``foo.example.com`` etc. Otherwise, a cookie will only be readable by the domain that set it. :param secure: The cookie will only be available via HTTPS. :param httponly: Disallow JavaScript to access the cookie. This is an extension to the cookie standard and probably not supported by all browsers. :param charset: The encoding for unicode values. :param sync_expires: Automatically set expires if max_age is defined but expires not. """ key = key.encode(charset) value = value.encode(charset) if path is not None: path = iri_to_uri(path) domain = _make_cookie_domain(domain) if isinstance(max_age, timedelta): max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds if expires is not None: if not isinstance(expires, str): expires = cookie_date(expires) elif max_age is not None and sync_expires: expires = cookie_date(time() + max_age).encode('ascii') buf = [key + b'=' + _cookie_quote(value)] # XXX: In theory all of these parameters that are not marked with `None` # should be quoted. Because stdlib did not quote it before I did not # want to introduce quoting there now. for k, v, q in ( (b'Domain', domain, True), (b'Expires', expires, False,), (b'Max-Age', max_age, False), (b'Secure', secure, None), (b'HttpOnly', httponly, None), (b'Path', path, False), ): if q is None: if v: buf.append(k) continue if v is None: continue tmp = bytearray(k) if not isinstance(v, (bytes, bytearray)): v = str(v).encode(charset) if q: v = _cookie_quote(v) tmp += b'=' + v buf.append(bytes(tmp)) # The return value will be an incorrectly encoded latin1 header on # for consistency with the headers object rv = b'; '.join(buf) rv = rv.decode('latin1') return rv
def get_wsgi_headers(self, environ): """This is automatically called right before the response is started and returns headers modified for the given environment. It returns a copy of the headers from the response with some modifications applied if necessary. For example the location header (if present) is joined with the root URL of the environment. Also the content length is automatically set to zero here for certain status codes. :param environ: The WSGI environment of the request. :return: Returns a new :class:`~verktyg.http.Headers` object. """ headers = Headers(self.headers) location = None content_location = None content_length = None status = self.status_code # iterate over the headers to find all values in one go. Because # get_wsgi_headers is used each response that gives us a tiny # speedup. for key, value in headers: ikey = key.lower() if ikey == u'location': location = value elif ikey == u'content-location': content_location = value elif ikey == u'content-length': content_length = value # make sure the location header is an absolute URL if location is not None: old_location = location if isinstance(location, str): # Safe conversion is necessary here as we might redirect # to a broken URI scheme (for instance itms-services). location = iri_to_uri(location) if self.autocorrect_location_header: current_url = get_current_url(environ, root_only=True) if isinstance(current_url, str): current_url = iri_to_uri(current_url) location = urljoin(current_url, location) if location != old_location: headers['Location'] = location # make sure the content location is a URL if ( content_location is not None and isinstance(content_location, str) ): headers['Content-Location'] = iri_to_uri(content_location) # remove entity headers and set content length to zero if needed. # Also update content_length accordingly so that the automatic # content length detection does not trigger in the following # code. if 100 <= status < 200 or status == 204: headers['Content-Length'] = content_length = u'0' elif status == 304: remove_entity_headers(headers) # if we can determine the content length automatically, we # should try to do that. But only if this does not involve # flattening the iterator or encoding of unicode strings in # the response. We however should not do that if we have a 304 # response. if ( self.automatically_set_content_length and self.is_sequence and content_length is None and status != 304 ): content_length = sum(len(x) for x in self.response) headers['Content-Length'] = str(content_length) return headers
def test_iri_to_uri_idempotence_ascii_only(self): uri = 'http://www.idempoten.ce' uri = urls.iri_to_uri(uri) self.assertEqual(urls.iri_to_uri(uri), uri)
def dump_cookie(key, value='', max_age=None, expires=None, path='/', domain=None, secure=False, httponly=False, charset='utf-8', sync_expires=True): """Creates a new Set-Cookie header without the ``Set-Cookie`` prefix The parameters are the same as in the cookie Morsel object in the Python standard library but it accepts unicode data, too. The return value of this function will be a unicode string tunneled through latin1 as required by PEP 3333. The return value is not ASCII safe if the key contains unicode characters. This is technically against the specification but happens in the wild. It's strongly recommended to not use non-ASCII values for the keys. :param max_age: Should be a number of seconds, or `None` (default) if the cookie should last only as long as the client's browser session. Additionally `timedelta` objects are accepted, too. :param expires: Should be a `datetime` object or unix timestamp. :param path: Limits the cookie to a given path, per default it will span the whole domain. :param domain: Use this if you want to set a cross-domain cookie. For example, ``domain=".example.com"`` will set a cookie that is readable by the domain ``www.example.com``, ``foo.example.com`` etc. Otherwise, a cookie will only be readable by the domain that set it. :param secure: The cookie will only be available via HTTPS. :param httponly: Disallow JavaScript to access the cookie. This is an extension to the cookie standard and probably not supported by all browsers. :param charset: The encoding for unicode values. :param sync_expires: Automatically set expires if max_age is defined but expires not. """ key = key.encode(charset) value = value.encode(charset) if path is not None: path = iri_to_uri(path, charset) domain = _make_cookie_domain(domain) if isinstance(max_age, timedelta): max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds if expires is not None: if not isinstance(expires, str): expires = cookie_date(expires) elif max_age is not None and sync_expires: expires = cookie_date(time() + max_age).encode('ascii') buf = [key + b'=' + _cookie_quote(value)] # XXX: In theory all of these parameters that are not marked with `None` # should be quoted. Because stdlib did not quote it before I did not # want to introduce quoting there now. for k, v, q in ((b'Domain', domain, True), ( b'Expires', expires, False, ), (b'Max-Age', max_age, False), (b'Secure', secure, None), (b'HttpOnly', httponly, None), (b'Path', path, False)): if q is None: if v: buf.append(k) continue if v is None: continue tmp = bytearray(k) if not isinstance(v, (bytes, bytearray)): v = str(v).encode(charset) if q: v = _cookie_quote(v) tmp += b'=' + v buf.append(bytes(tmp)) # The return value will be an incorrectly encoded latin1 header on # for consistency with the headers object rv = b'; '.join(buf) rv = rv.decode('latin1') return rv
def get_wsgi_headers(self, environ): """This is automatically called right before the response is started and returns headers modified for the given environment. It returns a copy of the headers from the response with some modifications applied if necessary. For example the location header (if present) is joined with the root URL of the environment. Also the content length is automatically set to zero here for certain status codes. :param environ: the WSGI environment of the request. :return: returns a new :class:`~verktyg.http.Headers` object. """ headers = Headers(self.headers) location = None content_location = None content_length = None status = self.status_code # iterate over the headers to find all values in one go. Because # get_wsgi_headers is used each response that gives us a tiny # speedup. for key, value in headers: ikey = key.lower() if ikey == u'location': location = value elif ikey == u'content-location': content_location = value elif ikey == u'content-length': content_length = value # make sure the location header is an absolute URL if location is not None: old_location = location if isinstance(location, str): # Safe conversion is necessary here as we might redirect # to a broken URI scheme (for instance itms-services). location = iri_to_uri(location, safe_conversion=True) if self.autocorrect_location_header: current_url = get_current_url(environ, root_only=True) if isinstance(current_url, str): current_url = iri_to_uri(current_url) location = urljoin(current_url, location) if location != old_location: headers['Location'] = location # make sure the content location is a URL if ( content_location is not None and isinstance(content_location, str) ): headers['Content-Location'] = iri_to_uri(content_location) # remove entity headers and set content length to zero if needed. # Also update content_length accordingly so that the automatic # content length detection does not trigger in the following # code. if 100 <= status < 200 or status == 204: headers['Content-Length'] = content_length = u'0' elif status == 304: remove_entity_headers(headers) # if we can determine the content length automatically, we # should try to do that. But only if this does not involve # flattening the iterator or encoding of unicode strings in # the response. We however should not do that if we have a 304 # response. if ( self.automatically_set_content_length and self.is_sequence and content_length is None and status != 304 ): try: content_length = sum(len(to_bytes(x, 'ascii')) for x in self.response) except UnicodeError: # aha, something non-bytestringy in there, too bad, we # can't safely figure out the length of the response. pass else: headers['Content-Length'] = str(content_length) return headers