You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
757 lines
23 KiB
757 lines
23 KiB
from __future__ import print_function
|
|
|
|
import base64
|
|
import contextlib
|
|
import copy
|
|
import email.utils
|
|
import functools
|
|
import gzip
|
|
import hashlib
|
|
import httplib2
|
|
import os
|
|
import random
|
|
import re
|
|
import shutil
|
|
import six
|
|
import socket
|
|
import ssl
|
|
import struct
|
|
import sys
|
|
import threading
|
|
import time
|
|
import traceback
|
|
import zlib
|
|
from six.moves import http_client, queue
|
|
|
|
|
|
DUMMY_URL = "http://127.0.0.1:1"
|
|
DUMMY_HTTPS_URL = "https://127.0.0.1:2"
|
|
|
|
tls_dir = os.path.join(os.path.dirname(__file__), "tls")
|
|
CA_CERTS = os.path.join(tls_dir, "ca.pem")
|
|
CA_UNUSED_CERTS = os.path.join(tls_dir, "ca_unused.pem")
|
|
CLIENT_PEM = os.path.join(tls_dir, "client.pem")
|
|
CLIENT_ENCRYPTED_PEM = os.path.join(tls_dir, "client_encrypted.pem")
|
|
SERVER_PEM = os.path.join(tls_dir, "server.pem")
|
|
SERVER_CHAIN = os.path.join(tls_dir, "server_chain.pem")
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def assert_raises(exc_type):
|
|
def _name(t):
|
|
return getattr(t, "__name__", None) or str(t)
|
|
|
|
if not isinstance(exc_type, tuple):
|
|
exc_type = (exc_type,)
|
|
names = ", ".join(map(_name, exc_type))
|
|
|
|
try:
|
|
yield
|
|
except exc_type:
|
|
pass
|
|
else:
|
|
assert False, "Expected exception(s) {0}".format(names)
|
|
|
|
|
|
class BufferedReader(object):
|
|
"""io.BufferedReader with \r\n support
|
|
"""
|
|
|
|
def __init__(self, sock):
|
|
self._buf = b""
|
|
self._end = False
|
|
self._newline = b"\r\n"
|
|
self._sock = sock
|
|
if isinstance(sock, bytes):
|
|
self._sock = None
|
|
self._buf = sock
|
|
|
|
def _fill(self, target=1, more=None, untilend=False):
|
|
if more:
|
|
target = len(self._buf) + more
|
|
while untilend or (len(self._buf) < target):
|
|
# crutch to enable HttpRequest.from_bytes
|
|
if self._sock is None:
|
|
chunk = b""
|
|
else:
|
|
chunk = self._sock.recv(8 << 10)
|
|
# print("!!! recv", chunk)
|
|
if not chunk:
|
|
self._end = True
|
|
if untilend:
|
|
return
|
|
else:
|
|
raise EOFError
|
|
self._buf += chunk
|
|
|
|
def peek(self, size):
|
|
self._fill(target=size)
|
|
return self._buf[:size]
|
|
|
|
def read(self, size):
|
|
self._fill(target=size)
|
|
chunk, self._buf = self._buf[:size], self._buf[size:]
|
|
return chunk
|
|
|
|
def readall(self):
|
|
self._fill(untilend=True)
|
|
chunk, self._buf = self._buf, b""
|
|
return chunk
|
|
|
|
def readline(self):
|
|
while True:
|
|
i = self._buf.find(self._newline)
|
|
if i >= 0:
|
|
break
|
|
self._fill(more=1)
|
|
inext = i + len(self._newline)
|
|
line, self._buf = self._buf[:inext], self._buf[inext:]
|
|
return line
|
|
|
|
|
|
def parse_http_message(kind, buf):
|
|
if buf._end:
|
|
return None
|
|
try:
|
|
start_line = buf.readline()
|
|
except EOFError:
|
|
return None
|
|
msg = kind()
|
|
msg.raw = start_line
|
|
if kind is HttpRequest:
|
|
assert re.match(
|
|
br".+ HTTP/\d\.\d\r\n$", start_line
|
|
), "Start line does not look like HTTP request: " + repr(start_line)
|
|
msg.method, msg.uri, msg.proto = start_line.rstrip().decode().split(" ", 2)
|
|
assert msg.proto.startswith("HTTP/"), repr(start_line)
|
|
elif kind is HttpResponse:
|
|
assert re.match(
|
|
br"^HTTP/\d\.\d \d+ .+\r\n$", start_line
|
|
), "Start line does not look like HTTP response: " + repr(start_line)
|
|
msg.proto, msg.status, msg.reason = start_line.rstrip().decode().split(" ", 2)
|
|
msg.status = int(msg.status)
|
|
assert msg.proto.startswith("HTTP/"), repr(start_line)
|
|
else:
|
|
raise Exception("Use HttpRequest or HttpResponse .from_{bytes,buffered}")
|
|
msg.version = msg.proto[5:]
|
|
|
|
while True:
|
|
line = buf.readline()
|
|
msg.raw += line
|
|
line = line.rstrip()
|
|
if not line:
|
|
break
|
|
t = line.decode().split(":", 1)
|
|
msg.headers[t[0].lower()] = t[1].lstrip()
|
|
|
|
content_length_string = msg.headers.get("content-length", "")
|
|
if content_length_string.isdigit():
|
|
content_length = int(content_length_string)
|
|
msg.body = msg.body_raw = buf.read(content_length)
|
|
elif msg.headers.get("transfer-encoding") == "chunked":
|
|
raise NotImplemented
|
|
elif msg.version == "1.0":
|
|
msg.body = msg.body_raw = buf.readall()
|
|
else:
|
|
msg.body = msg.body_raw = b""
|
|
|
|
msg.raw += msg.body_raw
|
|
return msg
|
|
|
|
|
|
class HttpMessage(object):
|
|
def __init__(self):
|
|
self.headers = {}
|
|
|
|
@classmethod
|
|
def from_bytes(cls, bs):
|
|
buf = BufferedReader(bs)
|
|
return parse_http_message(cls, buf)
|
|
|
|
@classmethod
|
|
def from_buffered(cls, buf):
|
|
return parse_http_message(cls, buf)
|
|
|
|
def __repr__(self):
|
|
return "{} {}".format(self.__class__, repr(vars(self)))
|
|
|
|
|
|
class HttpRequest(HttpMessage):
|
|
pass
|
|
|
|
|
|
class HttpResponse(HttpMessage):
|
|
pass
|
|
|
|
|
|
class MockResponse(six.BytesIO):
|
|
def __init__(self, body, **kwargs):
|
|
six.BytesIO.__init__(self, body)
|
|
self.headers = kwargs
|
|
|
|
def items(self):
|
|
return self.headers.items()
|
|
|
|
def iteritems(self):
|
|
return six.iteritems(self.headers)
|
|
|
|
|
|
class MockHTTPConnection(object):
|
|
"""This class is just a mock of httplib.HTTPConnection used for testing
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
host,
|
|
port=None,
|
|
key_file=None,
|
|
cert_file=None,
|
|
strict=None,
|
|
timeout=None,
|
|
proxy_info=None,
|
|
):
|
|
self.host = host
|
|
self.port = port
|
|
self.timeout = timeout
|
|
self.log = ""
|
|
self.sock = None
|
|
|
|
def set_debuglevel(self, level):
|
|
pass
|
|
|
|
def connect(self):
|
|
"Connect to a host on a given port."
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def request(self, method, request_uri, body, headers):
|
|
pass
|
|
|
|
def getresponse(self):
|
|
return MockResponse(b"the body", status="200")
|
|
|
|
|
|
class MockHTTPBadStatusConnection(object):
|
|
"""Mock of httplib.HTTPConnection that raises BadStatusLine.
|
|
"""
|
|
|
|
num_calls = 0
|
|
|
|
def __init__(
|
|
self,
|
|
host,
|
|
port=None,
|
|
key_file=None,
|
|
cert_file=None,
|
|
strict=None,
|
|
timeout=None,
|
|
proxy_info=None,
|
|
):
|
|
self.host = host
|
|
self.port = port
|
|
self.timeout = timeout
|
|
self.log = ""
|
|
self.sock = None
|
|
MockHTTPBadStatusConnection.num_calls = 0
|
|
|
|
def set_debuglevel(self, level):
|
|
pass
|
|
|
|
def connect(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def request(self, method, request_uri, body, headers):
|
|
pass
|
|
|
|
def getresponse(self):
|
|
MockHTTPBadStatusConnection.num_calls += 1
|
|
raise http_client.BadStatusLine("")
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def server_socket(fun, request_count=1, timeout=5, scheme="", tls=None):
|
|
"""Base socket server for tests.
|
|
Likely you want to use server_request or other higher level helpers.
|
|
All arguments except fun can be passed to other server_* helpers.
|
|
|
|
:param fun: fun(client_sock, tick) called after successful accept().
|
|
:param request_count: test succeeds after exactly this number of requests, triggered by tick(request)
|
|
:param timeout: seconds.
|
|
:param scheme: affects yielded value
|
|
"" - build normal http/https URI.
|
|
string - build normal URI using supplied scheme.
|
|
None - yield (addr, port) tuple.
|
|
:param tls:
|
|
None (default) - plain HTTP.
|
|
True - HTTPS with reasonable defaults. Likely you want httplib2.Http(ca_certs=tests.CA_CERTS)
|
|
string - path to custom server cert+key PEM file.
|
|
callable - function(context, listener, skip_errors) -> ssl_wrapped_listener
|
|
"""
|
|
gresult = [None]
|
|
gcounter = [0]
|
|
tls_skip_errors = [
|
|
"TLSV1_ALERT_UNKNOWN_CA",
|
|
]
|
|
|
|
def tick(request):
|
|
gcounter[0] += 1
|
|
keep = True
|
|
keep &= gcounter[0] < request_count
|
|
if request is not None:
|
|
keep &= request.headers.get("connection", "").lower() != "close"
|
|
return keep
|
|
|
|
def server_socket_thread(srv):
|
|
try:
|
|
while gcounter[0] < request_count:
|
|
try:
|
|
client, _ = srv.accept()
|
|
except ssl.SSLError as e:
|
|
if e.reason in tls_skip_errors:
|
|
return
|
|
raise
|
|
|
|
try:
|
|
client.settimeout(timeout)
|
|
fun(client, tick)
|
|
finally:
|
|
try:
|
|
client.shutdown(socket.SHUT_RDWR)
|
|
except (IOError, socket.error):
|
|
pass
|
|
# FIXME: client.close() introduces connection reset by peer
|
|
# at least in other/connection_close test
|
|
# should not be a problem since socket would close upon garbage collection
|
|
if gcounter[0] > request_count:
|
|
gresult[0] = Exception(
|
|
"Request count expected={0} actual={1}".format(
|
|
request_count, gcounter[0]
|
|
)
|
|
)
|
|
except Exception as e:
|
|
# traceback.print_exc caused IOError: concurrent operation on sys.stderr.close() under setup.py test
|
|
print(traceback.format_exc(), file=sys.stderr)
|
|
gresult[0] = e
|
|
|
|
bind_hostname = "localhost"
|
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server.bind((bind_hostname, 0))
|
|
try:
|
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
except socket.error as ex:
|
|
print("non critical error on SO_REUSEADDR", ex)
|
|
server.listen(10)
|
|
server.settimeout(timeout)
|
|
server_port = server.getsockname()[1]
|
|
if tls is True:
|
|
tls = SERVER_CHAIN
|
|
if tls:
|
|
context = ssl_context()
|
|
if callable(tls):
|
|
context.load_cert_chain(SERVER_CHAIN)
|
|
server = tls(context, server, tls_skip_errors)
|
|
else:
|
|
context.load_cert_chain(tls)
|
|
server = context.wrap_socket(server, server_side=True)
|
|
if scheme == "":
|
|
scheme = "https" if tls else "http"
|
|
|
|
t = threading.Thread(target=server_socket_thread, args=(server,))
|
|
t.daemon = True
|
|
t.start()
|
|
if scheme is None:
|
|
yield (bind_hostname, server_port)
|
|
else:
|
|
yield u"{scheme}://{host}:{port}/".format(scheme=scheme, host=bind_hostname, port=server_port)
|
|
server.close()
|
|
t.join()
|
|
if gresult[0] is not None:
|
|
raise gresult[0]
|
|
|
|
|
|
def server_yield(fun, **kwargs):
|
|
q = queue.Queue(1)
|
|
g = fun(q.get)
|
|
|
|
def server_yield_socket_handler(sock, tick):
|
|
buf = BufferedReader(sock)
|
|
i = 0
|
|
while True:
|
|
request = HttpRequest.from_buffered(buf)
|
|
if request is None:
|
|
break
|
|
i += 1
|
|
request.client_sock = sock
|
|
request.number = i
|
|
q.put(request)
|
|
response = six.next(g)
|
|
sock.sendall(response)
|
|
request.client_sock = None
|
|
if not tick(request):
|
|
break
|
|
|
|
return server_socket(server_yield_socket_handler, **kwargs)
|
|
|
|
|
|
def server_request(request_handler, **kwargs):
|
|
def server_request_socket_handler(sock, tick):
|
|
buf = BufferedReader(sock)
|
|
i = 0
|
|
while True:
|
|
request = HttpRequest.from_buffered(buf)
|
|
if request is None:
|
|
break
|
|
# print("--- debug request\n" + request.raw.decode("ascii", "replace"))
|
|
i += 1
|
|
request.client_sock = sock
|
|
request.number = i
|
|
response = request_handler(request=request)
|
|
# print("--- debug response\n" + response.decode("ascii", "replace"))
|
|
sock.sendall(response)
|
|
request.client_sock = None
|
|
if not tick(request):
|
|
break
|
|
|
|
return server_socket(server_request_socket_handler, **kwargs)
|
|
|
|
|
|
def server_const_bytes(response_content, **kwargs):
|
|
return server_request(lambda request: response_content, **kwargs)
|
|
|
|
|
|
_http_kwargs = (
|
|
"proto",
|
|
"status",
|
|
"headers",
|
|
"body",
|
|
"add_content_length",
|
|
"add_date",
|
|
"add_etag",
|
|
"undefined_body_length",
|
|
)
|
|
|
|
|
|
def http_response_bytes(
|
|
proto="HTTP/1.1",
|
|
status="200 OK",
|
|
headers=None,
|
|
body=b"",
|
|
add_content_length=True,
|
|
add_date=False,
|
|
add_etag=False,
|
|
undefined_body_length=False,
|
|
**kwargs
|
|
):
|
|
if undefined_body_length:
|
|
add_content_length = False
|
|
if headers is None:
|
|
headers = {}
|
|
if add_content_length:
|
|
headers.setdefault("content-length", str(len(body)))
|
|
if add_date:
|
|
headers.setdefault("date", email.utils.formatdate())
|
|
if add_etag:
|
|
headers.setdefault("etag", '"{0}"'.format(hashlib.md5(body).hexdigest()))
|
|
header_string = "".join("{0}: {1}\r\n".format(k, v) for k, v in headers.items())
|
|
if (
|
|
not undefined_body_length
|
|
and proto != "HTTP/1.0"
|
|
and "content-length" not in headers
|
|
):
|
|
raise Exception(
|
|
"httplib2.tests.http_response_bytes: client could not figure response body length"
|
|
)
|
|
if str(status).isdigit():
|
|
status = "{} {}".format(status, http_client.responses[status])
|
|
response = (
|
|
"{proto} {status}\r\n{headers}\r\n".format(
|
|
proto=proto, status=status, headers=header_string
|
|
).encode()
|
|
+ body
|
|
)
|
|
return response
|
|
|
|
|
|
def make_http_reflect(**kwargs):
|
|
assert "body" not in kwargs, "make_http_reflect will overwrite response " "body"
|
|
|
|
def fun(request):
|
|
kw = copy.deepcopy(kwargs)
|
|
kw["body"] = request.raw
|
|
response = http_response_bytes(**kw)
|
|
return response
|
|
|
|
return fun
|
|
|
|
|
|
def server_route(routes, **kwargs):
|
|
response_404 = http_response_bytes(status="404 Not Found")
|
|
response_wildcard = routes.get("")
|
|
|
|
def handler(request):
|
|
target = routes.get(request.uri, response_wildcard) or response_404
|
|
if callable(target):
|
|
response = target(request=request)
|
|
else:
|
|
response = target
|
|
return response
|
|
|
|
return server_request(handler, **kwargs)
|
|
|
|
|
|
def server_const_http(**kwargs):
|
|
response_kwargs = {k: kwargs.pop(k) for k in dict(kwargs) if k in _http_kwargs}
|
|
response = http_response_bytes(**response_kwargs)
|
|
return server_const_bytes(response, **kwargs)
|
|
|
|
|
|
def server_list_http(responses, **kwargs):
|
|
i = iter(responses)
|
|
|
|
def handler(request):
|
|
return next(i)
|
|
|
|
kwargs.setdefault("request_count", len(responses))
|
|
return server_request(handler, **kwargs)
|
|
|
|
|
|
def server_reflect(**kwargs):
|
|
response_kwargs = {k: kwargs.pop(k) for k in dict(kwargs) if k in _http_kwargs}
|
|
http_handler = make_http_reflect(**response_kwargs)
|
|
return server_request(http_handler, **kwargs)
|
|
|
|
|
|
def http_parse_auth(s):
|
|
"""https://tools.ietf.org/html/rfc7235#section-2.1
|
|
"""
|
|
scheme, rest = s.split(" ", 1)
|
|
result = {}
|
|
while True:
|
|
m = httplib2.WWW_AUTH_RELAXED.search(rest)
|
|
if not m:
|
|
break
|
|
if len(m.groups()) == 3:
|
|
key, value, rest = m.groups()
|
|
result[key.lower()] = httplib2.UNQUOTE_PAIRS.sub(r"\1", value)
|
|
return result
|
|
|
|
|
|
def store_request_response(out):
|
|
def wrapper(fun):
|
|
@functools.wraps(fun)
|
|
def wrapped(request, *a, **kw):
|
|
response_bytes = fun(request, *a, **kw)
|
|
if out is not None:
|
|
response = HttpResponse.from_bytes(response_bytes)
|
|
out.append((request, response))
|
|
return response_bytes
|
|
|
|
return wrapped
|
|
|
|
return wrapper
|
|
|
|
|
|
def http_reflect_with_auth(
|
|
allow_scheme, allow_credentials, out_renew_nonce=None, out_requests=None
|
|
):
|
|
"""allow_scheme - 'basic', 'digest', etc allow_credentials - sequence of ('name', 'password') out_renew_nonce - None | [function]
|
|
|
|
Way to return nonce renew function to caller.
|
|
Kind of `out` parameter in some programming languages.
|
|
Allows to keep same signature for all handler builder functions.
|
|
out_requests - None | []
|
|
If set to list, every parsed request will be appended here.
|
|
"""
|
|
glastnc = [None]
|
|
gnextnonce = [None]
|
|
gserver_nonce = [gen_digest_nonce(salt=b"n")]
|
|
realm = "httplib2 test"
|
|
server_opaque = gen_digest_nonce(salt=b"o")
|
|
|
|
def renew_nonce():
|
|
if gnextnonce[0]:
|
|
assert False, (
|
|
"previous nextnonce was not used, probably bug in " "test code"
|
|
)
|
|
gnextnonce[0] = gen_digest_nonce()
|
|
return gserver_nonce[0], gnextnonce[0]
|
|
|
|
if out_renew_nonce:
|
|
out_renew_nonce[0] = renew_nonce
|
|
|
|
def deny(**kwargs):
|
|
nonce_stale = kwargs.pop("nonce_stale", False)
|
|
if nonce_stale:
|
|
kwargs.setdefault("body", b"nonce stale")
|
|
if allow_scheme == "basic":
|
|
authenticate = 'basic realm="{realm}"'.format(realm=realm)
|
|
elif allow_scheme == "digest":
|
|
authenticate = (
|
|
'digest realm="{realm}", qop="auth"'
|
|
+ ', nonce="{nonce}", opaque="{opaque}"'
|
|
+ (", stale=true" if nonce_stale else "")
|
|
).format(realm=realm, nonce=gserver_nonce[0], opaque=server_opaque)
|
|
else:
|
|
raise Exception("unknown allow_scheme={0}".format(allow_scheme))
|
|
deny_headers = {"www-authenticate": authenticate}
|
|
kwargs.setdefault("status", 401)
|
|
# supplied headers may overwrite generated ones
|
|
deny_headers.update(kwargs.get("headers", {}))
|
|
kwargs["headers"] = deny_headers
|
|
kwargs.setdefault("body", b"HTTP authorization required")
|
|
return http_response_bytes(**kwargs)
|
|
|
|
@store_request_response(out_requests)
|
|
def http_reflect_with_auth_handler(request):
|
|
auth_header = request.headers.get("authorization", "")
|
|
if not auth_header:
|
|
return deny()
|
|
if " " not in auth_header:
|
|
return http_response_bytes(
|
|
status=400, body=b"authorization header syntax error"
|
|
)
|
|
scheme, data = auth_header.split(" ", 1)
|
|
scheme = scheme.lower()
|
|
if scheme != allow_scheme:
|
|
return deny(body=b"must use different auth scheme")
|
|
if scheme == "basic":
|
|
decoded = base64.b64decode(data).decode()
|
|
username, password = decoded.split(":", 1)
|
|
if (username, password) in allow_credentials:
|
|
return make_http_reflect()(request)
|
|
else:
|
|
return deny(body=b"supplied credentials are not allowed")
|
|
elif scheme == "digest":
|
|
server_nonce_old = gserver_nonce[0]
|
|
nextnonce = gnextnonce[0]
|
|
if nextnonce:
|
|
# server decided to change nonce, in this case, guided by caller test code
|
|
gserver_nonce[0] = nextnonce
|
|
gnextnonce[0] = None
|
|
server_nonce_current = gserver_nonce[0]
|
|
auth_info = http_parse_auth(data)
|
|
client_cnonce = auth_info.get("cnonce", "")
|
|
client_nc = auth_info.get("nc", "")
|
|
client_nonce = auth_info.get("nonce", "")
|
|
client_opaque = auth_info.get("opaque", "")
|
|
client_qop = auth_info.get("qop", "auth").strip('"')
|
|
|
|
# TODO: auth_info.get('algorithm', 'md5')
|
|
hasher = hashlib.md5
|
|
|
|
# TODO: client_qop auth-int
|
|
ha2 = hasher(":".join((request.method, request.uri)).encode()).hexdigest()
|
|
|
|
if client_nonce != server_nonce_current:
|
|
if client_nonce == server_nonce_old:
|
|
return deny(nonce_stale=True)
|
|
return deny(body=b"invalid nonce")
|
|
if not client_nc:
|
|
return deny(body=b"auth-info nc missing")
|
|
if client_opaque != server_opaque:
|
|
return deny(
|
|
body="auth-info opaque mismatch expected={} actual={}".format(
|
|
server_opaque, client_opaque
|
|
).encode()
|
|
)
|
|
for allow_username, allow_password in allow_credentials:
|
|
ha1 = hasher(
|
|
":".join((allow_username, realm, allow_password)).encode()
|
|
).hexdigest()
|
|
allow_response = hasher(
|
|
":".join(
|
|
(ha1, client_nonce, client_nc, client_cnonce, client_qop, ha2)
|
|
).encode()
|
|
).hexdigest()
|
|
rspauth_ha2 = hasher(":{}".format(request.uri).encode()).hexdigest()
|
|
rspauth = hasher(
|
|
":".join(
|
|
(
|
|
ha1,
|
|
client_nonce,
|
|
client_nc,
|
|
client_cnonce,
|
|
client_qop,
|
|
rspauth_ha2,
|
|
)
|
|
).encode()
|
|
).hexdigest()
|
|
if auth_info.get("response", "") == allow_response:
|
|
# TODO: fix or remove doubtful comment
|
|
# do we need to save nc only on success?
|
|
glastnc[0] = client_nc
|
|
allow_headers = {
|
|
"authentication-info": " ".join(
|
|
(
|
|
'nextnonce="{}"'.format(nextnonce) if nextnonce else "",
|
|
"qop={}".format(client_qop),
|
|
'rspauth="{}"'.format(rspauth),
|
|
'cnonce="{}"'.format(client_cnonce),
|
|
"nc={}".format(client_nc),
|
|
)
|
|
).strip()
|
|
}
|
|
return make_http_reflect(headers=allow_headers)(request)
|
|
return deny(body=b"supplied credentials are not allowed")
|
|
else:
|
|
return http_response_bytes(
|
|
status=400,
|
|
body="unknown authorization scheme={0}".format(scheme).encode(),
|
|
)
|
|
|
|
return http_reflect_with_auth_handler
|
|
|
|
|
|
def get_cache_path():
|
|
default = "./_httplib2_test_cache"
|
|
path = os.environ.get("httplib2_test_cache_path") or default
|
|
if os.path.exists(path):
|
|
shutil.rmtree(path)
|
|
return path
|
|
|
|
|
|
def gen_digest_nonce(salt=b""):
|
|
t = struct.pack(">Q", int(time.time() * 1e9))
|
|
return base64.b64encode(t + b":" + hashlib.sha1(t + salt).digest()).decode()
|
|
|
|
|
|
def gen_password():
|
|
length = random.randint(8, 64)
|
|
return "".join(six.unichr(random.randint(0, 127)) for _ in range(length))
|
|
|
|
|
|
def gzip_compress(bs):
|
|
# gzipobj = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16)
|
|
# result = gzipobj.compress(text) + gzipobj.flush()
|
|
buf = six.BytesIO()
|
|
gf = gzip.GzipFile(fileobj=buf, mode="wb", compresslevel=6)
|
|
gf.write(bs)
|
|
gf.close()
|
|
return buf.getvalue()
|
|
|
|
|
|
def gzip_decompress(bs):
|
|
return zlib.decompress(bs, zlib.MAX_WBITS | 16)
|
|
|
|
|
|
def deflate_compress(bs):
|
|
do = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS)
|
|
return do.compress(bs) + do.flush()
|
|
|
|
|
|
def deflate_decompress(bs):
|
|
return zlib.decompress(bs, -zlib.MAX_WBITS)
|
|
|
|
|
|
def ssl_context(protocol=None):
|
|
"""Workaround for old SSLContext() required protocol argument.
|
|
"""
|
|
if sys.version_info < (3, 5, 3):
|
|
return ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
|
return ssl.SSLContext()
|