diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/python/urllib3/dummyserver | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/python/urllib3/dummyserver')
-rw-r--r-- | third_party/python/urllib3/dummyserver/__init__.py | 0 | ||||
-rw-r--r-- | third_party/python/urllib3/dummyserver/certs/README.rst | 17 | ||||
-rw-r--r-- | third_party/python/urllib3/dummyserver/certs/cacert.key | 27 | ||||
-rw-r--r-- | third_party/python/urllib3/dummyserver/certs/cacert.pem | 21 | ||||
-rw-r--r-- | third_party/python/urllib3/dummyserver/certs/server.crt | 21 | ||||
-rw-r--r-- | third_party/python/urllib3/dummyserver/certs/server.key | 27 | ||||
-rw-r--r-- | third_party/python/urllib3/dummyserver/handlers.py | 328 | ||||
-rwxr-xr-x | third_party/python/urllib3/dummyserver/proxy.py | 139 | ||||
-rwxr-xr-x | third_party/python/urllib3/dummyserver/server.py | 188 | ||||
-rw-r--r-- | third_party/python/urllib3/dummyserver/testcase.py | 210 |
10 files changed, 978 insertions, 0 deletions
diff --git a/third_party/python/urllib3/dummyserver/__init__.py b/third_party/python/urllib3/dummyserver/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/third_party/python/urllib3/dummyserver/__init__.py diff --git a/third_party/python/urllib3/dummyserver/certs/README.rst b/third_party/python/urllib3/dummyserver/certs/README.rst new file mode 100644 index 0000000000..7c712b6e15 --- /dev/null +++ b/third_party/python/urllib3/dummyserver/certs/README.rst @@ -0,0 +1,17 @@ +Generating new certificates +--------------------------- + +Here's how you can regenerate the certificates:: + + import trustme + + ca = trustme.CA() + server_cert = ca.issue_cert(u"localhost") + + ca.cert_pem.write_to_path("cacert.pem") + ca.private_key_pem.write_to_path("cacert.key") + server_cert.cert_chain_pems[0].write_to_path("server.crt") + server_cert.private_key_pem.write_to_path("server.key") + +This will break a number of tests: you will need to update the +relevant fingerprints and hashes. diff --git a/third_party/python/urllib3/dummyserver/certs/cacert.key b/third_party/python/urllib3/dummyserver/certs/cacert.key new file mode 100644 index 0000000000..58e1c20167 --- /dev/null +++ b/third_party/python/urllib3/dummyserver/certs/cacert.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAn330NEISY7w+GeZR2jh8Of1x2CtpenWRKuuD2u4FOaN0G1SN +pm6Owum6xzhv93jmj+tZrp9kYvC9HcvGrgzH9yP566pLLfY2SEEAJYNNeVdqegY5 +9W1wa6vEDN5UYruVEbymgPHoItiYhfndgEDbagPN5dhrdNvaRNI2c5zMmBDlzzhC +G7AZbXuthTp6OwTVye71f2lnNhKi6rzWtre/ji88fq8Qm406l29O1RTzmNttN5BZ +nPWU9v4GnCYKXdY6BN1Ub6z8C9hna/oRgBqa0Zbv7kEuAuqhsFQeuFCfBhQm7NdP +d/7Kh6LS+VIiCu3AOccZHOuFOjtHb/KyndmgCwIDAQABAoIBAGQg9wc308O5kmNA +LXMKszLU4nwMBRRUaua/JPB1LeKZs3LVCnjKP+YuRox76g87X8RKxOrUNnnHGXNz +UzBB5ehKNcS2DKy2Pi3uYOEsJZ9gOgCRmCF0q3dtRo+tpNy3V0bjYMTjGhGGWXsC ++wRhs15DNShvTkb3H3jFYFoEvo1YUKsvImBWJGwDbdDMfMZv4aeBWXlOrF+2fwt2 +TM3G1o8xzEEWBB4FLZBW+tq2zfUSa1KwqqyQ4ZIqXepjQcN6nNfuHADA+nxuruVV +LPUhz4ZmsBEnJ7CL9zWJkLUw/al9/6Q14tleRmiZTTztqAlFgZUpNhaKSzVdsIc/ +Xz3+OgECgYEAzgNu7eFJAOzq+ZVFRrrbA7tu+f318FTCaADJ1kFgAJyj6b9yOGan +LNL4TfXVjzgqtfQ4AIVqEHwXO7yS+YgTprvgzqRxqNRZ6ikuo2IPkIwXIAXZAlwd +JsWLPBXOlOFW6LHvhYxjY2xF+A9y4AbuZ3UDRUQ+tp226VfEaeY80+ECgYEAxjDV +cJqeBO06YRVGmcXfAYwRLJGT4hvIZeiSbxX/kJ0rx+cYLT/XZbAguJYQ5ZK2lkeA +YneXYDlSTxmBxHxiWwWe3mcmctdE4Jbw8oIZ8a49a4KE/F2ojC4gmisIt3/OqGOw +C4e/pDCE/QV64LWdazgUWHPGoVEmZx9/oMm/MWsCgYEAsLtlSJFB7ZdRpTcXLSxT +gwoilDf36mrsNAipHjMLRrsaKwbf197If72k4kyJHspSabHO8TOC4A10aPzHIWZJ +ZXo7y0prbyhs0mLt7Z/MNnbXx9L8bffT0lUZszwJ8tK1mf47utfK05opFDs8k0+e +6gYJ/jwjiMoYBmoSx76KZEECgYBagJxHAmQcbdQV1yhZOhFe3H5PMt8sBnHZj32m ++o2slQkUDQRuTVPoHKikgeqPWxLDxzzqOiBHEYXzlvs6JW6okAV/G+1jzcenI2Y9 +54k/YsirWnut3nsEIGBE5lfhq5xMKtGOQlwR9xITlLgK+wQ6nO41ghD3Q15dAvY+ +D0KepwKBgQChHvbyzw0t76J2gLxUSyuG7VsId651bpqTYUsbSDFlRo4g8UbBAkHd +fdv5BOon3ALJFreSK+a78es0kpiLwrS2SqG/y3mb9aUoLpCVB1haDmmP4Rn4AYXz +OCfUkusuSoXOR8CMjqkXYl5QjeJAUAt9GTsZnXIbOQKbaZwkeV0HEg== +-----END RSA PRIVATE KEY----- diff --git a/third_party/python/urllib3/dummyserver/certs/cacert.pem b/third_party/python/urllib3/dummyserver/certs/cacert.pem new file mode 100644 index 0000000000..710b8fbd98 --- /dev/null +++ b/third_party/python/urllib3/dummyserver/certs/cacert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfzCCAmegAwIBAgIUVGEi+7bkaGRIPoQrp9zFFAT5JYQwDQYJKoZIhvcNAQEL +BQAwQDEXMBUGA1UECgwOdHJ1c3RtZSB2MC41LjMxJTAjBgNVBAsMHFRlc3Rpbmcg +Q0EgIzdEUWJWOTBzV2xZSEstY0wwHhcNMDAwMTAxMDAwMDAwWhcNMzgwMTAxMDAw +MDAwWjBAMRcwFQYDVQQKDA50cnVzdG1lIHYwLjUuMzElMCMGA1UECwwcVGVzdGlu +ZyBDQSAjN0RRYlY5MHNXbFlISy1jTDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAJ999DRCEmO8PhnmUdo4fDn9cdgraXp1kSrrg9ruBTmjdBtUjaZujsLp +usc4b/d45o/rWa6fZGLwvR3Lxq4Mx/cj+euqSy32NkhBACWDTXlXanoGOfVtcGur +xAzeVGK7lRG8poDx6CLYmIX53YBA22oDzeXYa3Tb2kTSNnOczJgQ5c84QhuwGW17 +rYU6ejsE1cnu9X9pZzYSouq81ra3v44vPH6vEJuNOpdvTtUU85jbbTeQWZz1lPb+ +BpwmCl3WOgTdVG+s/AvYZ2v6EYAamtGW7+5BLgLqobBUHrhQnwYUJuzXT3f+yoei +0vlSIgrtwDnHGRzrhTo7R2/ysp3ZoAsCAwEAAaNxMG8wHQYDVR0OBBYEFHWf39Hn +rdChKjsOBoBGn1U+0VgxMBIGA1UdEwEB/wQIMAYBAf8CAQkwDgYDVR0PAQH/BAQD +AgEGMCoGA1UdJQEB/wQgMB4GCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMw +DQYJKoZIhvcNAQELBQADggEBAIAylnWX2WTB+mrVVpE2W8i0HollTMJIPJA9Jq3Q +/t2uPjXDEAVAcBmQju8qy2tHpamvzpQseVm3EF3UFNlGxwOGKsTzU4J45qOJITZk +eLRAcWNEt6cgqj8ml8PuMHU7oDnp7pP6VPe5KQH1a0FYQnDNEwg7MyX+GjnXeRwd +re6y9nMC+XKCYUAd1/nQcrZdnSsws1M5lzXir2vuyyN9EUkf2xMMKA2E1s0f+5he +3eNghAXtZw616ITBoMb7ckG6a0+YobbiQ0tKgB8D3MG2544Gx6xhCXf7pX4q4g// +1nTPeYFsBDyqEOEhcW1o9/MSSbjpUJC+QUmCb2Y1wYeum+w= +-----END CERTIFICATE----- diff --git a/third_party/python/urllib3/dummyserver/certs/server.crt b/third_party/python/urllib3/dummyserver/certs/server.crt new file mode 100644 index 0000000000..24026c367a --- /dev/null +++ b/third_party/python/urllib3/dummyserver/certs/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDeTCCAmGgAwIBAgIUQeadxkH6YMoSecB2rNbFEr1u1kUwDQYJKoZIhvcNAQEL +BQAwQDEXMBUGA1UECgwOdHJ1c3RtZSB2MC41LjMxJTAjBgNVBAsMHFRlc3Rpbmcg +Q0EgIzdEUWJWOTBzV2xZSEstY0wwHhcNMDAwMTAxMDAwMDAwWhcNMzgwMTAxMDAw +MDAwWjBCMRcwFQYDVQQKDA50cnVzdG1lIHYwLjUuMzEnMCUGA1UECwweVGVzdGlu +ZyBjZXJ0ICMtSGRsMnMyTEYyeVp0NDFOMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArRLZX+5dyCh4N7q90sH2Q4Ea6QLK8OfoUQPWtpzAtINDUAdfSXCC +/qYTtGeSCGjB4W0LfvRTI8afHoD/M+YpaCRnx7T1sy1taA2rnGrEVXEHalVP+RI4 +t4ZWtX56aez2M0Fs6o4MtzAuP6fKgSdWzIvOmtCxqn0Zf2KbfEHnQylsy2LgPa/x +Lg50fbZ195+h4EAB3d2/jqaeFGGhN+7zrrv4+L1eeW3bzOkvPEkTNepq3Gy/8r5e +0i2icEnM+eBfl8NYgQ1toJYvDIy5Qi2TRzaFxBVmqUOc8EFtHpL7E9YLbTTW15xd +oLVLdXI5igGxkwPYoeiiAJWxIsC/hL1RRQIDAQABo2kwZzAdBgNVHQ4EFgQUMU6+ +uwNmL8TxLwjrj7jzzlwDPiowDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBR1n9/R +563QoSo7DgaARp9VPtFYMTAXBgNVHREBAf8EDTALgglsb2NhbGhvc3QwDQYJKoZI +hvcNAQELBQADggEBAJ6w5neQKw+/efA/I3IHzt8GaSHQ/YehMHx8GxCViJUmLg6P +Vf74k856Knvh7IsVaqF1uRi6qQaFPik6CwtBCj7/ZftdseOCDljd+8EWyQ+ZWie7 ++tzMIdQWZxYSdR9Ov42VD++a6oWJtfJhWV5eyDit99FFK31/M1ZXoceiDS5AsIG6 +wfsxrFj1qV9pLNSIlfrnycYhYx7avVJTf+2mfZgTO9Tx+VPapkZrfCnP/2jpN39u +zblFFjP9Ir0QqBw7MXjVX+Y1HkQ2TQnEeSsp1HuFRIZYx72Cttnckv1Lxcx/HiQB +oebTDYiRfxOAEeIMgIhX88Jca8vNIRcXDeGK9mU= +-----END CERTIFICATE----- diff --git a/third_party/python/urllib3/dummyserver/certs/server.key b/third_party/python/urllib3/dummyserver/certs/server.key new file mode 100644 index 0000000000..592ee6bea2 --- /dev/null +++ b/third_party/python/urllib3/dummyserver/certs/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEArRLZX+5dyCh4N7q90sH2Q4Ea6QLK8OfoUQPWtpzAtINDUAdf +SXCC/qYTtGeSCGjB4W0LfvRTI8afHoD/M+YpaCRnx7T1sy1taA2rnGrEVXEHalVP ++RI4t4ZWtX56aez2M0Fs6o4MtzAuP6fKgSdWzIvOmtCxqn0Zf2KbfEHnQylsy2Lg +Pa/xLg50fbZ195+h4EAB3d2/jqaeFGGhN+7zrrv4+L1eeW3bzOkvPEkTNepq3Gy/ +8r5e0i2icEnM+eBfl8NYgQ1toJYvDIy5Qi2TRzaFxBVmqUOc8EFtHpL7E9YLbTTW +15xdoLVLdXI5igGxkwPYoeiiAJWxIsC/hL1RRQIDAQABAoIBAQCZ/62f6G9WDHx7 +yhPhlmjTw+r37l45YYCbpbjFoFDvzeR1LzogFJbak1fxLD8KcHwjY23ZNvlLWg53 +i/yIZ4Hsgog9cM0283LoJVHPykiMZhhdCzAvxYDl/AjnUXUHD6w6CzsoseCql5pv +VZOgvCpFsxjRNGUB+HJZoJoNRG7MmHaY638pGHGMiVbskT9Ww3emtMLdTKy1rQcj +9XO/4mlaBGD79wYxy5Hlysbh2UYuQRv8XN5V46Uugk6sC/i2G7VC8KkqPQ2pM+rA +LaeWSuN9dfBwiKcHtJssMP95ilsXsjqh3NoVuFODDXHv3u+nBAxtg2TnLZFkDZXH +FvxPJu8BAoGBANwWWzvl/dnTtVDFbhF/61v3AVZV4vVpokXRJKny05CZPZWROTc4 +LXMVw9kxeecNdo0yR4jn1yyNUmKnQpTmpsR9Yo9NYH+Z1CLxswpc7ILfVRZBK6bK +cCG43lM5xZprG6FXhqkHN2u9z5Y8/PuaMzC8iVs402/gakgPKmn8OjdhAoGBAMlQ +mmrx24n9YY/dOn55XC5V/iN3Z6mIsHThnDIU515bwLwZVG7chOLSiWHAh4JzUH+v +bV3NnlE1jhf5ln0WAadCtIeVprJG6msNTQlbTMTTV5kVNdbfYX6sFQEI+hC1LCiV +yJtuNIa4P5W/PtoC3FWUlcAH8C91S/M4CeZZ0HhlAoGBAIxflgE2SBrO9S53PiTb +OfqGKMwwK3nrzhxJsODUiCwKEUV8Qsn9gr+MekXlUKMV6y9Tily/wnYgDRPvKoBe +PK/GaT6NU6cPLka7cj6B1jgCyfpPxs+y/qIDj4n1pxs+hXj6omDcwXRutCBW9eRk +DZJgLhuIuxL4R9F+GsdOoLMBAoGAKQn1cLe9OXQd32YJ9p5m3EtLc49z4murDSiw +3sTEJcgukinXvIHX1SV2PCczeLRpRJ5OfUDddVCllt2agAVscNx4UOuA//bU8t3T +RoUGMVmkEeDxCMyg42HRJlTeJWnJhryCGK1up8gHrk8+UNMkd43CuVLk88fFo99Y +pUzJ4sECgYEAvBDTo3k3sD18qV6p6tQwy+MVjvQb9V81GHP18tYcVKta3LkkqUFa +3qSyVxi7gl3JtynG7NJ7+GDx6zxW2xUR72NTcJwWvesLI+1orM288pyNDVw9MJ/j +AyVFnW5SEYEqdizTnQxL+rQB4CyeHfwZx2/1/Qr0ezLGUJv51lnk4mQ= +-----END RSA PRIVATE KEY----- diff --git a/third_party/python/urllib3/dummyserver/handlers.py b/third_party/python/urllib3/dummyserver/handlers.py new file mode 100644 index 0000000000..696dbab076 --- /dev/null +++ b/third_party/python/urllib3/dummyserver/handlers.py @@ -0,0 +1,328 @@ +from __future__ import print_function + +import collections +import contextlib +import gzip +import json +import logging +import sys +import time +import zlib + +from io import BytesIO +from tornado.web import RequestHandler +from tornado import httputil +from datetime import datetime +from datetime import timedelta + +from urllib3.packages.six.moves.http_client import responses +from urllib3.packages.six.moves.urllib.parse import urlsplit +from urllib3.packages.six import binary_type, ensure_str + +log = logging.getLogger(__name__) + + +class Response(object): + def __init__(self, body="", status="200 OK", headers=None): + self.body = body + self.status = status + self.headers = headers or [("Content-type", "text/plain")] + + def __call__(self, request_handler): + status, reason = self.status.split(" ", 1) + request_handler.set_status(int(status), reason) + for header, value in self.headers: + request_handler.add_header(header, value) + + # chunked + if isinstance(self.body, list): + for item in self.body: + if not isinstance(item, bytes): + item = item.encode("utf8") + request_handler.write(item) + request_handler.flush() + else: + body = self.body + if not isinstance(body, bytes): + body = body.encode("utf8") + + request_handler.write(body) + + +RETRY_TEST_NAMES = collections.defaultdict(int) + + +class TestingApp(RequestHandler): + """ + Simple app that performs various operations, useful for testing an HTTP + library. + + Given any path, it will attempt to load a corresponding local method if + it exists. Status code 200 indicates success, 400 indicates failure. Each + method has its own conditions for success/failure. + """ + + def get(self): + """ Handle GET requests """ + self._call_method() + + def post(self): + """ Handle POST requests """ + self._call_method() + + def put(self): + """ Handle PUT requests """ + self._call_method() + + def options(self): + """ Handle OPTIONS requests """ + self._call_method() + + def head(self): + """ Handle HEAD requests """ + self._call_method() + + def _call_method(self): + """ Call the correct method in this class based on the incoming URI """ + req = self.request + req.params = {} + for k, v in req.arguments.items(): + req.params[k] = next(iter(v)) + + path = req.path[:] + if not path.startswith("/"): + path = urlsplit(path).path + + target = path[1:].split("/", 1)[0] + method = getattr(self, target, self.index) + + resp = method(req) + + if dict(resp.headers).get("Connection") == "close": + # FIXME: Can we kill the connection somehow? + pass + + resp(self) + + def index(self, _request): + "Render simple message" + return Response("Dummy server!") + + def certificate(self, request): + """Return the requester's certificate.""" + cert = request.get_ssl_certificate() + subject = dict() + if cert is not None: + subject = dict((k, v) for (k, v) in [y for z in cert["subject"] for y in z]) + return Response(json.dumps(subject)) + + def source_address(self, request): + """Return the requester's IP address.""" + return Response(request.remote_ip) + + def set_up(self, request): + test_type = request.params.get("test_type") + test_id = request.params.get("test_id") + if test_id: + print("\nNew test %s: %s" % (test_type, test_id)) + else: + print("\nNew test %s" % test_type) + return Response("Dummy server is ready!") + + def specific_method(self, request): + "Confirm that the request matches the desired method type" + method = request.params.get("method") + if method and not isinstance(method, str): + method = method.decode("utf8") + + if request.method != method: + return Response( + "Wrong method: %s != %s" % (method, request.method), + status="400 Bad Request", + ) + return Response() + + def upload(self, request): + "Confirm that the uploaded file conforms to specification" + # FIXME: This is a huge broken mess + param = request.params.get("upload_param", b"myfile").decode("ascii") + filename = request.params.get("upload_filename", b"").decode("utf-8") + size = int(request.params.get("upload_size", "0")) + files_ = request.files.get(param) + + if len(files_) != 1: + return Response( + "Expected 1 file for '%s', not %d" % (param, len(files_)), + status="400 Bad Request", + ) + file_ = files_[0] + + data = file_["body"] + if int(size) != len(data): + return Response( + "Wrong size: %d != %d" % (size, len(data)), status="400 Bad Request" + ) + + got_filename = file_["filename"] + if isinstance(got_filename, binary_type): + got_filename = got_filename.decode("utf-8") + + # Tornado can leave the trailing \n in place on the filename. + if filename != got_filename: + return Response( + u"Wrong filename: %s != %s" % (filename, file_.filename), + status="400 Bad Request", + ) + + return Response() + + def redirect(self, request): + "Perform a redirect to ``target``" + target = request.params.get("target", "/") + status = request.params.get("status", "303 See Other") + if len(status) == 3: + status = "%s Redirect" % status.decode("latin-1") + + headers = [("Location", target)] + return Response(status=status, headers=headers) + + def not_found(self, request): + return Response("Not found", status="404 Not Found") + + def multi_redirect(self, request): + "Performs a redirect chain based on ``redirect_codes``" + codes = request.params.get("redirect_codes", b"200").decode("utf-8") + head, tail = codes.split(",", 1) if "," in codes else (codes, None) + status = "{0} {1}".format(head, responses[int(head)]) + if not tail: + return Response("Done redirecting", status=status) + + headers = [("Location", "/multi_redirect?redirect_codes=%s" % tail)] + return Response(status=status, headers=headers) + + def keepalive(self, request): + if request.params.get("close", b"0") == b"1": + headers = [("Connection", "close")] + return Response("Closing", headers=headers) + + headers = [("Connection", "keep-alive")] + return Response("Keeping alive", headers=headers) + + def echo_params(self, request): + params = sorted( + [(ensure_str(k), ensure_str(v)) for k, v in request.params.items()] + ) + return Response(repr(params)) + + def sleep(self, request): + "Sleep for a specified amount of ``seconds``" + # DO NOT USE THIS, IT'S DEPRECATED. + # FIXME: Delete this once appengine tests are fixed to not use this handler. + seconds = float(request.params.get("seconds", "1")) + time.sleep(seconds) + return Response() + + def echo(self, request): + "Echo back the params" + if request.method == "GET": + return Response(request.query) + + return Response(request.body) + + def echo_uri(self, request): + "Echo back the requested URI" + return Response(request.uri) + + def encodingrequest(self, request): + "Check for UA accepting gzip/deflate encoding" + data = b"hello, world!" + encoding = request.headers.get("Accept-Encoding", "") + headers = None + if encoding == "gzip": + headers = [("Content-Encoding", "gzip")] + file_ = BytesIO() + with contextlib.closing( + gzip.GzipFile("", mode="w", fileobj=file_) + ) as zipfile: + zipfile.write(data) + data = file_.getvalue() + elif encoding == "deflate": + headers = [("Content-Encoding", "deflate")] + data = zlib.compress(data) + elif encoding == "garbage-gzip": + headers = [("Content-Encoding", "gzip")] + data = "garbage" + elif encoding == "garbage-deflate": + headers = [("Content-Encoding", "deflate")] + data = "garbage" + return Response(data, headers=headers) + + def headers(self, request): + return Response(json.dumps(dict(request.headers))) + + def successful_retry(self, request): + """ Handler which will return an error and then success + + It's not currently very flexible as the number of retries is hard-coded. + """ + test_name = request.headers.get("test-name", None) + if not test_name: + return Response("test-name header not set", status="400 Bad Request") + + RETRY_TEST_NAMES[test_name] += 1 + + if RETRY_TEST_NAMES[test_name] >= 2: + return Response("Retry successful!") + else: + return Response("need to keep retrying!", status="418 I'm A Teapot") + + def chunked(self, request): + return Response(["123"] * 4) + + def chunked_gzip(self, request): + chunks = [] + compressor = zlib.compressobj(6, zlib.DEFLATED, 16 + zlib.MAX_WBITS) + + for uncompressed in [b"123"] * 4: + chunks.append(compressor.compress(uncompressed)) + + chunks.append(compressor.flush()) + + return Response(chunks, headers=[("Content-Encoding", "gzip")]) + + def nbytes(self, request): + length = int(request.params.get("length")) + data = b"1" * length + return Response(data, headers=[("Content-Type", "application/octet-stream")]) + + def status(self, request): + status = request.params.get("status", "200 OK") + + return Response(status=status) + + def retry_after(self, request): + if datetime.now() - self.application.last_req < timedelta(seconds=1): + status = request.params.get("status", b"429 Too Many Requests") + return Response( + status=status.decode("utf-8"), headers=[("Retry-After", "1")] + ) + + self.application.last_req = datetime.now() + + return Response(status="200 OK") + + def redirect_after(self, request): + "Perform a redirect to ``target``" + date = request.params.get("date") + if date: + retry_after = str( + httputil.format_timestamp(datetime.fromtimestamp(float(date))) + ) + else: + retry_after = "1" + target = request.params.get("target", "/") + headers = [("Location", target), ("Retry-After", retry_after)] + return Response(status="303 See Other", headers=headers) + + def shutdown(self, request): + sys.exit() diff --git a/third_party/python/urllib3/dummyserver/proxy.py b/third_party/python/urllib3/dummyserver/proxy.py new file mode 100755 index 0000000000..c4f0b824f7 --- /dev/null +++ b/third_party/python/urllib3/dummyserver/proxy.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# +# Simple asynchronous HTTP proxy with tunnelling (CONNECT). +# +# GET/POST proxying based on +# http://groups.google.com/group/python-tornado/msg/7bea08e7a049cf26 +# +# Copyright (C) 2012 Senko Rasic <senko.rasic@dobarkod.hr> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import sys +import socket + +import tornado.gen +import tornado.httpserver +import tornado.ioloop +import tornado.iostream +import tornado.web +import tornado.httpclient + +__all__ = ["ProxyHandler", "run_proxy"] + + +class ProxyHandler(tornado.web.RequestHandler): + SUPPORTED_METHODS = ["GET", "POST", "CONNECT"] + + @tornado.gen.coroutine + def get(self): + def handle_response(response): + if response.error and not isinstance( + response.error, tornado.httpclient.HTTPError + ): + self.set_status(500) + self.write("Internal server error:\n" + str(response.error)) + self.finish() + else: + self.set_status(response.code) + for header in ( + "Date", + "Cache-Control", + "Server", + "Content-Type", + "Location", + ): + v = response.headers.get(header) + if v: + self.set_header(header, v) + if response.body: + self.write(response.body) + self.finish() + + req = tornado.httpclient.HTTPRequest( + url=self.request.uri, + method=self.request.method, + body=self.request.body, + headers=self.request.headers, + follow_redirects=False, + allow_nonstandard_methods=True, + ) + + client = tornado.httpclient.AsyncHTTPClient() + try: + response = yield client.fetch(req) + yield handle_response(response) + except tornado.httpclient.HTTPError as e: + if hasattr(e, "response") and e.response: + yield handle_response(e.response) + else: + self.set_status(500) + self.write("Internal server error:\n" + str(e)) + self.finish() + + @tornado.gen.coroutine + def post(self): + yield self.get() + + @tornado.gen.coroutine + def connect(self): + host, port = self.request.uri.split(":") + client = self.request.connection.stream + + @tornado.gen.coroutine + def start_forward(reader, writer): + while True: + try: + data = yield reader.read_bytes(4096, partial=True) + except tornado.iostream.StreamClosedError: + break + if not data: + break + writer.write(data) + writer.close() + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + upstream = tornado.iostream.IOStream(s) + yield upstream.connect((host, int(port))) + + client.write(b"HTTP/1.0 200 Connection established\r\n\r\n") + fu1 = start_forward(client, upstream) + fu2 = start_forward(upstream, client) + yield [fu1, fu2] + + +def run_proxy(port, start_ioloop=True): + """ + Run proxy on the specified port. If start_ioloop is True (default), + the tornado IOLoop will be started immediately. + """ + app = tornado.web.Application([(r".*", ProxyHandler)]) + app.listen(port) + ioloop = tornado.ioloop.IOLoop.instance() + if start_ioloop: + ioloop.start() + + +if __name__ == "__main__": + port = 8888 + if len(sys.argv) > 1: + port = int(sys.argv[1]) + + print("Starting HTTP proxy on port %d" % port) + run_proxy(port) diff --git a/third_party/python/urllib3/dummyserver/server.py b/third_party/python/urllib3/dummyserver/server.py new file mode 100755 index 0000000000..68f383534e --- /dev/null +++ b/third_party/python/urllib3/dummyserver/server.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +""" +Dummy server used for unit testing. +""" +from __future__ import print_function + +import logging +import os +import sys +import threading +import socket +import warnings +import ssl +from datetime import datetime + +from urllib3.exceptions import HTTPWarning + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +import tornado.httpserver +import tornado.ioloop +import tornado.netutil +import tornado.web +import trustme + + +log = logging.getLogger(__name__) + +CERTS_PATH = os.path.join(os.path.dirname(__file__), "certs") +DEFAULT_CERTS = { + "certfile": os.path.join(CERTS_PATH, "server.crt"), + "keyfile": os.path.join(CERTS_PATH, "server.key"), + "cert_reqs": ssl.CERT_OPTIONAL, + "ca_certs": os.path.join(CERTS_PATH, "cacert.pem"), +} +DEFAULT_CA = os.path.join(CERTS_PATH, "cacert.pem") +DEFAULT_CA_KEY = os.path.join(CERTS_PATH, "cacert.key") + + +def _resolves_to_ipv6(host): + """ Returns True if the system resolves host to an IPv6 address by default. """ + resolves_to_ipv6 = False + try: + for res in socket.getaddrinfo(host, None, socket.AF_UNSPEC): + af, _, _, _, _ = res + if af == socket.AF_INET6: + resolves_to_ipv6 = True + except socket.gaierror: + pass + + return resolves_to_ipv6 + + +def _has_ipv6(host): + """ Returns True if the system can bind an IPv6 address. """ + sock = None + has_ipv6 = False + + if socket.has_ipv6: + # has_ipv6 returns true if cPython was compiled with IPv6 support. + # It does not tell us if the system has IPv6 support enabled. To + # determine that we must bind to an IPv6 address. + # https://github.com/urllib3/urllib3/pull/611 + # https://bugs.python.org/issue658327 + try: + sock = socket.socket(socket.AF_INET6) + sock.bind((host, 0)) + has_ipv6 = _resolves_to_ipv6("localhost") + except Exception: + pass + + if sock: + sock.close() + return has_ipv6 + + +# Some systems may have IPv6 support but DNS may not be configured +# properly. We can not count that localhost will resolve to ::1 on all +# systems. See https://github.com/urllib3/urllib3/pull/611 and +# https://bugs.python.org/issue18792 +HAS_IPV6_AND_DNS = _has_ipv6("localhost") +HAS_IPV6 = _has_ipv6("::1") + + +# Different types of servers we have: + + +class NoIPv6Warning(HTTPWarning): + "IPv6 is not available" + pass + + +class SocketServerThread(threading.Thread): + """ + :param socket_handler: Callable which receives a socket argument for one + request. + :param ready_event: Event which gets set when the socket handler is + ready to receive requests. + """ + + USE_IPV6 = HAS_IPV6_AND_DNS + + def __init__(self, socket_handler, host="localhost", port=8081, ready_event=None): + threading.Thread.__init__(self) + self.daemon = True + + self.socket_handler = socket_handler + self.host = host + self.ready_event = ready_event + + def _start_server(self): + if self.USE_IPV6: + sock = socket.socket(socket.AF_INET6) + else: + warnings.warn("No IPv6 support. Falling back to IPv4.", NoIPv6Warning) + sock = socket.socket(socket.AF_INET) + if sys.platform != "win32": + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((self.host, 0)) + self.port = sock.getsockname()[1] + + # Once listen() returns, the server socket is ready + sock.listen(1) + + if self.ready_event: + self.ready_event.set() + + self.socket_handler(sock) + sock.close() + + def run(self): + self.server = self._start_server() + + +def run_tornado_app(app, io_loop, certs, scheme, host): + assert io_loop == tornado.ioloop.IOLoop.current() + + # We can't use fromtimestamp(0) because of CPython issue 29097, so we'll + # just construct the datetime object directly. + app.last_req = datetime(1970, 1, 1) + + if scheme == "https": + http_server = tornado.httpserver.HTTPServer(app, ssl_options=certs) + else: + http_server = tornado.httpserver.HTTPServer(app) + + sockets = tornado.netutil.bind_sockets(None, address=host) + port = sockets[0].getsockname()[1] + http_server.add_sockets(sockets) + return http_server, port + + +def run_loop_in_thread(io_loop): + t = threading.Thread(target=io_loop.start) + t.start() + return t + + +def get_unreachable_address(): + # reserved as per rfc2606 + return ("something.invalid", 54321) + + +if __name__ == "__main__": + # For debugging dummyserver itself - python -m dummyserver.server + from .testcase import TestingApp + + host = "127.0.0.1" + + io_loop = tornado.ioloop.IOLoop.current() + app = tornado.web.Application([(r".*", TestingApp)]) + server, port = run_tornado_app(app, io_loop, None, "http", host) + server_thread = run_loop_in_thread(io_loop) + + print("Listening on http://{host}:{port}".format(host=host, port=port)) + + +def encrypt_key_pem(private_key_pem, password): + private_key = serialization.load_pem_private_key( + private_key_pem.bytes(), password=None, backend=default_backend() + ) + encrypted_key = private_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.BestAvailableEncryption(password), + ) + return trustme.Blob(encrypted_key) diff --git a/third_party/python/urllib3/dummyserver/testcase.py b/third_party/python/urllib3/dummyserver/testcase.py new file mode 100644 index 0000000000..90c6b2240c --- /dev/null +++ b/third_party/python/urllib3/dummyserver/testcase.py @@ -0,0 +1,210 @@ +import threading + +import pytest +from tornado import ioloop, web + +from dummyserver.server import ( + SocketServerThread, + run_tornado_app, + run_loop_in_thread, + DEFAULT_CERTS, + HAS_IPV6, +) +from dummyserver.handlers import TestingApp +from dummyserver.proxy import ProxyHandler + + +def consume_socket(sock, chunks=65536): + consumed = bytearray() + while True: + b = sock.recv(chunks) + consumed += b + if b.endswith(b"\r\n\r\n"): + break + return consumed + + +class SocketDummyServerTestCase(object): + """ + A simple socket-based server is created for this class that is good for + exactly one request. + """ + + scheme = "http" + host = "localhost" + + @classmethod + def _start_server(cls, socket_handler): + ready_event = threading.Event() + cls.server_thread = SocketServerThread( + socket_handler=socket_handler, ready_event=ready_event, host=cls.host + ) + cls.server_thread.start() + ready_event.wait(5) + if not ready_event.is_set(): + raise Exception("most likely failed to start server") + cls.port = cls.server_thread.port + + @classmethod + def start_response_handler(cls, response, num=1, block_send=None): + ready_event = threading.Event() + + def socket_handler(listener): + for _ in range(num): + ready_event.set() + + sock = listener.accept()[0] + consume_socket(sock) + if block_send: + block_send.wait() + block_send.clear() + sock.send(response) + sock.close() + + cls._start_server(socket_handler) + return ready_event + + @classmethod + def start_basic_handler(cls, **kw): + return cls.start_response_handler( + b"HTTP/1.1 200 OK\r\n" b"Content-Length: 0\r\n" b"\r\n", **kw + ) + + @classmethod + def teardown_class(cls): + if hasattr(cls, "server_thread"): + cls.server_thread.join(0.1) + + def assert_header_received( + self, received_headers, header_name, expected_value=None + ): + header_name = header_name.encode("ascii") + if expected_value is not None: + expected_value = expected_value.encode("ascii") + header_titles = [] + for header in received_headers: + key, value = header.split(b": ") + header_titles.append(key) + if key == header_name and expected_value is not None: + assert value == expected_value + assert header_name in header_titles + + +class IPV4SocketDummyServerTestCase(SocketDummyServerTestCase): + @classmethod + def _start_server(cls, socket_handler): + ready_event = threading.Event() + cls.server_thread = SocketServerThread( + socket_handler=socket_handler, ready_event=ready_event, host=cls.host + ) + cls.server_thread.USE_IPV6 = False + cls.server_thread.start() + ready_event.wait(5) + if not ready_event.is_set(): + raise Exception("most likely failed to start server") + cls.port = cls.server_thread.port + + +class HTTPDummyServerTestCase(object): + """ A simple HTTP server that runs when your test class runs + + Have your test class inherit from this one, and then a simple server + will start when your tests run, and automatically shut down when they + complete. For examples of what test requests you can send to the server, + see the TestingApp in dummyserver/handlers.py. + """ + + scheme = "http" + host = "localhost" + host_alt = "127.0.0.1" # Some tests need two hosts + certs = DEFAULT_CERTS + + @classmethod + def _start_server(cls): + cls.io_loop = ioloop.IOLoop.current() + app = web.Application([(r".*", TestingApp)]) + cls.server, cls.port = run_tornado_app( + app, cls.io_loop, cls.certs, cls.scheme, cls.host + ) + cls.server_thread = run_loop_in_thread(cls.io_loop) + + @classmethod + def _stop_server(cls): + cls.io_loop.add_callback(cls.server.stop) + cls.io_loop.add_callback(cls.io_loop.stop) + cls.server_thread.join() + + @classmethod + def setup_class(cls): + cls._start_server() + + @classmethod + def teardown_class(cls): + cls._stop_server() + + +class HTTPSDummyServerTestCase(HTTPDummyServerTestCase): + scheme = "https" + host = "localhost" + certs = DEFAULT_CERTS + + +class HTTPDummyProxyTestCase(object): + + http_host = "localhost" + http_host_alt = "127.0.0.1" + + https_host = "localhost" + https_host_alt = "127.0.0.1" + https_certs = DEFAULT_CERTS + + proxy_host = "localhost" + proxy_host_alt = "127.0.0.1" + + @classmethod + def setup_class(cls): + cls.io_loop = ioloop.IOLoop.current() + + app = web.Application([(r".*", TestingApp)]) + cls.http_server, cls.http_port = run_tornado_app( + app, cls.io_loop, None, "http", cls.http_host + ) + + app = web.Application([(r".*", TestingApp)]) + cls.https_server, cls.https_port = run_tornado_app( + app, cls.io_loop, cls.https_certs, "https", cls.http_host + ) + + app = web.Application([(r".*", ProxyHandler)]) + cls.proxy_server, cls.proxy_port = run_tornado_app( + app, cls.io_loop, None, "http", cls.proxy_host + ) + + cls.server_thread = run_loop_in_thread(cls.io_loop) + + @classmethod + def teardown_class(cls): + cls.io_loop.add_callback(cls.http_server.stop) + cls.io_loop.add_callback(cls.https_server.stop) + cls.io_loop.add_callback(cls.proxy_server.stop) + cls.io_loop.add_callback(cls.io_loop.stop) + cls.server_thread.join() + + +@pytest.mark.skipif(not HAS_IPV6, reason="IPv6 not available") +class IPv6HTTPDummyServerTestCase(HTTPDummyServerTestCase): + host = "::1" + + +@pytest.mark.skipif(not HAS_IPV6, reason="IPv6 not available") +class IPv6HTTPDummyProxyTestCase(HTTPDummyProxyTestCase): + + http_host = "localhost" + http_host_alt = "127.0.0.1" + + https_host = "localhost" + https_host_alt = "127.0.0.1" + https_certs = DEFAULT_CERTS + + proxy_host = "::1" + proxy_host_alt = "127.0.0.1" |