Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit c5a0631

Browse files
committed
Merge branch 'development' of https://github.com/mylh/hyper into mylh-development
2 parents aa459b9 + f70ae6f commit c5a0631

File tree

5 files changed

+137
-11
lines changed

5 files changed

+137
-11
lines changed

hyper/contrib.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from hyper.common.connection import HTTPConnection
1818
from hyper.compat import urlparse
19+
from hyper.tls import init_context
1920

2021

2122
class HTTP20Adapter(HTTPAdapter):
@@ -28,31 +29,42 @@ def __init__(self, *args, **kwargs):
2829
#: A mapping between HTTP netlocs and ``HTTP20Connection`` objects.
2930
self.connections = {}
3031

31-
def get_connection(self, host, port, scheme):
32+
def get_connection(self, host, port, scheme, cert=None):
3233
"""
33-
Gets an appropriate HTTP/2 connection object based on host/port/scheme
34-
tuples.
34+
Gets an appropriate HTTP/2 connection object based on
35+
host/port/scheme/cert tuples.
3536
"""
3637
secure = (scheme == 'https')
3738

3839
if port is None: # pragma: no cover
3940
port = 80 if not secure else 443
4041

42+
ssl_context = None
43+
if cert is not None:
44+
ssl_context = init_context(cert=cert)
45+
4146
try:
42-
conn = self.connections[(host, port, scheme)]
47+
conn = self.connections[(host, port, scheme, cert)]
4348
except KeyError:
44-
conn = HTTPConnection(host, port, secure=secure)
45-
self.connections[(host, port, scheme)] = conn
49+
conn = HTTPConnection(
50+
host,
51+
port,
52+
secure=secure,
53+
ssl_context=ssl_context)
54+
self.connections[(host, port, scheme, cert)] = conn
4655

4756
return conn
4857

49-
def send(self, request, stream=False, **kwargs):
58+
def send(self, request, stream=False, cert=None, **kwargs):
5059
"""
5160
Sends a HTTP message to the server.
5261
"""
5362
parsed = urlparse(request.url)
54-
55-
conn = self.get_connection(parsed.hostname, parsed.port, parsed.scheme)
63+
conn = self.get_connection(
64+
parsed.hostname,
65+
parsed.port,
66+
parsed.scheme,
67+
cert=cert)
5668

5769
# Build the selector.
5870
selector = parsed.path

hyper/tls.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def wrap_socket(sock, server_hostname, ssl_context=None, force_proto=None):
6565
return (ssl_sock, proto)
6666

6767

68-
def init_context(cert_path=None):
68+
def init_context(cert_path=None, cert=None, cert_password=None):
6969
"""
7070
Create a new ``SSLContext`` that is correctly set up for an HTTP/2 connection.
7171
This SSL context object can be customized and passed as a parameter to the
@@ -74,7 +74,24 @@ def init_context(cert_path=None):
7474
certificate. The path to the certificate can be absolute or relative
7575
to your working directory.
7676
77-
:param cert_path: (optional) The path to the certificate file.
77+
:param cert_path: (optional) The path to the certificate file of
78+
“certification authority” (CA) certificates
79+
:param cert: (optional) if string, path to ssl client cert file (.pem).
80+
If tuple, ('cert', 'key') pair.
81+
The certfile string must be the path to a single file in PEM format
82+
containing the certificate as well as any number of CA certificates
83+
needed to establish the certificate’s authenticity. The keyfile string,
84+
if present, must point to a file containing the private key in.
85+
Otherwise the private key will be taken from certfile as well.
86+
:param cert_password: (optional) The password argument may be a function to
87+
call to get the password for decrypting the private key. It will only
88+
be called if the private key is encrypted and a password is necessary.
89+
It will be called with no arguments, and it should return a string,
90+
bytes, or bytearray. If the return value is a string it will be
91+
encoded as UTF-8 before using it to decrypt the key. Alternatively a
92+
string, bytes, or bytearray value may be supplied directly as the
93+
password argument. It will be ignored if the private key is not
94+
encrypted and no password is needed.
7895
:returns: An ``SSLContext`` correctly set up for HTTP/2.
7996
"""
8097
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
@@ -92,4 +109,14 @@ def init_context(cert_path=None):
92109
# required by the spec
93110
context.options |= ssl.OP_NO_COMPRESSION
94111

112+
if cert is not None:
113+
try:
114+
basestring
115+
except NameError:
116+
basestring = str
117+
if not isinstance(cert, basestring):
118+
context.load_cert_chain(cert[0], cert[1], cert_password)
119+
else:
120+
context.load_cert_chain(cert, password=cert_password)
121+
95122
return context

test/certs/nopassword.pem

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6Xa4TY1JPp6Ta
3+
PdiBsCBIxCDOOUZWsAb+F6p0VDCocOv3Qx8NOHalFHjTAJIHdv0oyCwv99z+z3rc
4+
NZY+5DAaNQxw3mtaSoQVK+yVLQqfqkze/Qm5RoDFqubrFCGBSI4XQ3RW88JLIuZw
5+
b2do8MhIUwMo7xkVpsA/C+KBeKP1cchvoDwmO6MDLQGLozfARQpD1Y/kxAsfFRe+
6+
tsGFhyHuiwFed7IaWt4aUbBsFCw6O5zEg6tK+tJh2O9LEN8PyG1mojYQk3e1v5/6
7+
sSxoEuE2xTqFruZ+cuT+iSq0upF8o2bLfPZalY5NICKF3sKY0xCdnVabxUbxB+Z1
8+
5Nf5ZFCpAgMBAAECggEBAJWRm6R5wNSm0fpJSlqC9NYRedaoRthJu8LvUWC9NLPq
9+
tKYkG2ar2ySPsox9V7Vf/LtfM39n6NgjwhG7fBKLZkOSMaLgDr5PMYQgVWY/2Nfd
10+
gIYyBDzK5Yw+pccix+UPSuJGw7cJOPS+VL0F27NwEv1gihevFK24v2+Z5TZNkSDo
11+
yvI8Y0QNEZS36RGP1mlRGB1IAD61gOYPrnkkpYL+N1ZiGHUdqxLbMjxqmoFRJjhj
12+
qYfll0I6x5o4QydO7qXnSdpQugBWdRP2H/IHHB/QQAwro09t6B3C34jq4mTs6Oeu
13+
eTXk6ug6oPkqpCjoIk78W32Cd1SdCyeTkD7VTTLPN2UCgYEA3igGKKC7EffZqTYG
14+
6QXqj2hdl+YXmOy9gKxSnbGCO9uqefJTpqlmY7g9JfV/ijhJlMk4ZI9Hr/eCqCCp
15+
r6oIcGtwB1GqkHnVUgPaf6qc52r9ag+jXPJdWmL5Dc7GK3iosD4a7xXtY6obf5cX
16+
qzGczFbRlbBzmwI++/PFwaIBPv8CgYEA1sHZggxt1aOBfzJaYafdiC/NT/7DMAJ+
17+
qVCgoFU3IaexxAcaHpsPT3sMu/kEsA574V4PAojvR3qlzSMpBsCWNtgrOdA9WNyS
18+
G0RI/+gA0XCmDZaCGrzBlLGKidY48G+OozLRf3fBR8IbVeHfkocw65KQVKwfG4Ah
19+
Zc/bj0LwGFcCgYEA1xM4oyy473RcrY04s3Ce3afUtLJ2Nf88l849TZ4Ez56jNNx+
20+
T+PA1NoRmSZMC6ziz8Dfb7unU5z0SYEVxpN/CBd7phpSXv0UoQpKBz9OGF1kacIq
21+
Dlo2NsOLCuscwAlYhwgZW06HPO37IVNN/tdRTiLfVWQ3B+Lsx1ACLKyDOFECgYB1
22+
EsJPWhU6RONggwOwfwGOr3h+poSjlIiWJsUaArqGV1PaaIC9tIw5KPx9MLh0fcDc
23+
0BjgqeO/lMX0Obmw26ZICbouzy3SVpQz1xrwnvprMrzjZWxRxRrGw66hi64IrNgW
24+
caqxkYhFZTTfsb3etGJf2ctizV478LLEPPcVd0lKCwKBgG0q1+0FHJ8jsgi1pj+Y
25+
jP6n8LQ9fLlUVhjFHfNEA3gSanVHdsWmW+7pMUIbDSnDlifS5/afnZfPpW3lQXB4
26+
yoTmYYSt4mq8Jo5VItRWm5xsWqcbdqKVuuwzTi6m2F2Lvc4tp/E8GXGhSw3UM2qy
27+
CRHH9H+8ktCv/GnW5MiAF5Jz
28+
-----END PRIVATE KEY-----
29+
-----BEGIN CERTIFICATE-----
30+
MIIDezCCAmOgAwIBAgIJAJmioIcuQec4MA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV
31+
BAYTAlVTMRAwDgYDVQQIDAdOZXdZb3JrMRAwDgYDVQQHDAdOZXdZb3JrMSEwHwYD
32+
VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYwMzIzMTE1MzUxWhcN
33+
MTYwNDIyMTE1MzUxWjBUMQswCQYDVQQGEwJVUzEQMA4GA1UECAwHTmV3WW9yazEQ
34+
MA4GA1UEBwwHTmV3WW9yazEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkg
35+
THRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAul2uE2NST6ek2j3Y
36+
gbAgSMQgzjlGVrAG/heqdFQwqHDr90MfDTh2pRR40wCSB3b9KMgsL/fc/s963DWW
37+
PuQwGjUMcN5rWkqEFSvslS0Kn6pM3v0JuUaAxarm6xQhgUiOF0N0VvPCSyLmcG9n
38+
aPDISFMDKO8ZFabAPwvigXij9XHIb6A8JjujAy0Bi6M3wEUKQ9WP5MQLHxUXvrbB
39+
hYch7osBXneyGlreGlGwbBQsOjucxIOrSvrSYdjvSxDfD8htZqI2EJN3tb+f+rEs
40+
aBLhNsU6ha7mfnLk/okqtLqRfKNmy3z2WpWOTSAihd7CmNMQnZ1Wm8VG8QfmdeTX
41+
+WRQqQIDAQABo1AwTjAdBgNVHQ4EFgQUtdlqFdvfUI7mkDH62Q6BE9DKyqYwHwYD
42+
VR0jBBgwFoAUtdlqFdvfUI7mkDH62Q6BE9DKyqYwDAYDVR0TBAUwAwEB/zANBgkq
43+
hkiG9w0BAQsFAAOCAQEAh/el7sxFudIVHcAti7DxkqJEiOs7bPwAwnaRiKfLZEYo
44+
NVGQ+GtMarCO/j/EO6STlkoFqI8XA2Jn9WYsoV8OD6lcU9DoJKlupIIRH+PVYhSb
45+
q6WTqbB0o12Se9EGJ/NmsXFn2KqSRah6TTcJSVNAvcF8qMilnPjbJjCxdoMVT34b
46+
EmdyZxIFmKMrth+zKTqDzs+FK+49JTnjomm7A3h9iErGzsiM3BZzZLGEcG52IYuC
47+
P/fcJs24014NqPqXl+JVnFk8sLBRZKYA9yYZ4C/XH/ZF/Y2pVhgwxZN8OtcaydR4
48+
pyGNdgG1ml9CpqZQfj4WMS+x2LmCWpCRH+pXcqg/kQ==
49+
-----END CERTIFICATE-----

test/test_SSLContext.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@
22
"""
33
Tests the hyper SSLContext.
44
"""
5+
import os
6+
57
import hyper
68
from hyper.common.connection import HTTPConnection
79
from hyper.compat import ssl
10+
811
import pytest
912

13+
14+
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
15+
TEST_CERTS_DIR = os.path.join(TEST_DIR, 'certs')
16+
CLIENT_CERT_FILE = os.path.join(TEST_CERTS_DIR, 'client.crt')
17+
CLIENT_KEY_FILE = os.path.join(TEST_CERTS_DIR, 'client.key')
18+
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')
19+
20+
1021
class TestSSLContext(object):
1122
"""
1223
Tests default and custom SSLContext
@@ -47,3 +58,10 @@ def test_HTTPConnection_with_custom_context(self):
4758
assert conn.ssl_context.check_hostname == True
4859
assert conn.ssl_context.verify_mode == ssl.CERT_REQUIRED
4960
assert conn.ssl_context.options & ssl.OP_NO_COMPRESSION != 0
61+
62+
63+
def test_client_certificates(self):
64+
context = hyper.tls.init_context(
65+
cert=(CLIENT_CERT_FILE, CLIENT_KEY_FILE),
66+
cert_password=b'abc123')
67+
context = hyper.tls.init_context(cert=CLIENT_PEM_FILE)

test/test_hyper.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
import hyper
2929

3030

31+
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
32+
TEST_CERTS_DIR = os.path.join(TEST_DIR, 'certs')
33+
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')
34+
35+
3136
def decode_frame(frame_data):
3237
f, length = Frame.parse_frame_header(frame_data[:9])
3338
f.parse_body(memoryview(frame_data[9:9 + length]))
@@ -784,6 +789,21 @@ def test_adapter_reuses_connections(self):
784789

785790
assert conn1 is conn2
786791

792+
def test_adapter_accept_client_certificate(self):
793+
a = HTTP20Adapter()
794+
conn1 = a.get_connection(
795+
'http2bin.org',
796+
80,
797+
'http',
798+
cert=CLIENT_PEM_FILE)
799+
conn2 = a.get_connection(
800+
'http2bin.org',
801+
80,
802+
'http',
803+
cert=CLIENT_PEM_FILE)
804+
assert conn1 is conn2
805+
806+
787807

788808
class TestUtilities(object):
789809
def test_combining_repeated_headers(self):

0 commit comments

Comments
 (0)