Skip to content

Commit e9d8c90

Browse files
jeffreyparkerDenitsaTHAaronAtDuo
authored
Drop python2 support (#62)
* Drop Python 2 support (#53) * drop python 2 support * Remove python 3.7 from CI (#54) it's not available on Ubuntu anymore since it's EOL --------- Co-authored-by: Aaron McConnell <[email protected]> * Fix missing 'not' * add Python 3.11, 3.12 to CI (#56) --------- Co-authored-by: Denitsa Hristova <[email protected]> Co-authored-by: Aaron McConnell <[email protected]> Co-authored-by: Denitsa Hristova <[email protected]>
1 parent 194fc5b commit e9d8c90

File tree

4 files changed

+61
-72
lines changed

4 files changed

+61
-72
lines changed

.github/workflows/openvpn-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414

1515
strategy:
1616
matrix:
17-
python-version: ["3.8", "3.9", "3.10"]
17+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
1818

1919
steps:
2020
- uses: actions/checkout@v2

duo_openvpn.py

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@
1616
import socket
1717
import sys
1818
import syslog
19+
import http.client as http_client
20+
import urllib.parse as urllib_parse
1921

20-
import six
21-
from six.moves import http_client
22-
from six.moves.urllib.parse import quote, urlencode
2322

2423
def log(msg):
2524
msg = 'Duo OpenVPN: %s' % msg
@@ -50,8 +49,8 @@ def canon_params(params):
5049
# http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
5150
args = []
5251
for (key, vals) in sorted(
53-
(quote(key, '~'), vals) for (key, vals) in params.items()):
54-
for val in sorted(quote(val, '~') for val in vals):
52+
(urllib_parse.quote(key, '~'), vals) for (key, vals) in params.items()):
53+
for val in sorted(urllib_parse.quote(val, '~') for val in vals):
5554
args.append('%s=%s' % (key, val))
5655
return '&'.join(args)
5756

@@ -80,19 +79,19 @@ def sign(ikey, skey, method, host, uri, date, sig_version, params):
8079
"""
8180
canonical = canonicalize(method, host, uri, params, date, sig_version)
8281

83-
if isinstance(skey, six.text_type):
82+
if isinstance(skey, str):
8483
skey = skey.encode('utf-8')
85-
if isinstance(canonical, six.text_type):
84+
if isinstance(canonical, str):
8685
canonical = canonical.encode('utf-8')
8786

8887
sig = hmac.new(skey, canonical, hashlib.sha512)
8988
auth = '%s:%s' % (ikey, sig.hexdigest())
9089

91-
if isinstance(auth, six.text_type):
90+
if isinstance(auth, str):
9291
auth = auth.encode('utf-8')
9392

9493
b64 = base64.b64encode(auth)
95-
if not isinstance(b64, six.text_type):
94+
if not isinstance(b64, str):
9695
b64 = b64.decode('utf-8')
9796

9897
return 'Basic %s' % b64
@@ -106,12 +105,12 @@ def normalize_params(params):
106105
# urllib cannot handle unicode strings properly. quote() excepts,
107106
# and urlencode() replaces them with '?'.
108107
def encode(value):
109-
if isinstance(value, six.text_type):
108+
if isinstance(value, str):
110109
return value.encode("utf-8")
111110
return value
112111

113112
def to_list(value):
114-
if value is None or isinstance(value, six.string_types):
113+
if value is None or isinstance(value, str):
115114
return [value]
116115
return value
117116

@@ -185,11 +184,11 @@ def api_call(self, method, path, params):
185184

186185
if method in ['POST', 'PUT']:
187186
headers['Content-type'] = 'application/x-www-form-urlencoded'
188-
body = urlencode(params, doseq=True)
187+
body = urllib_parse.urlencode(params, doseq=True)
189188
uri = path
190189
else:
191190
body = None
192-
uri = path + '?' + urlencode(params, doseq=True)
191+
uri = path + '?' + urllib_parse.urlencode(params, doseq=True)
193192

194193
return self._make_request(method, uri, body, headers)
195194

@@ -278,7 +277,7 @@ def raise_error(msg):
278277
error.data = data
279278
raise error
280279

281-
if not isinstance(data, six.text_type):
280+
if not isinstance(data, str):
282281
data = data.decode('utf-8')
283282

284283
if response.status != 200:
@@ -293,14 +292,14 @@ def raise_error(msg):
293292
))
294293
else:
295294
raise_error('Received %s %s' % (
296-
response.status,
295+
response.status,
297296
data['message'],
298297
))
299298
except (ValueError, KeyError, TypeError):
300299
pass
301300
raise_error('Received %s %s' % (
302-
response.status,
303-
response.reason,
301+
response.status,
302+
response.reason,
304303
))
305304
try:
306305
data = json.loads(data)
@@ -313,18 +312,16 @@ def raise_error(msg):
313312
def success(control):
314313
log('writing success code to %s' % control)
315314

316-
f = open(control, 'w')
317-
f.write('1')
318-
f.close()
315+
with open(control, 'w') as f:
316+
f.write('1')
319317

320318
sys.exit(0)
321319

322320
def failure(control):
323321
log('writing failure code to %s' % control)
324322

325-
f = open(control, 'w')
326-
f.write('0')
327-
f.close()
323+
with open(control, 'w') as f:
324+
f.write('0')
328325

329326
sys.exit(1)
330327

https_wrapper.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
import re
2323
import socket
2424
import ssl
25-
26-
from six.moves import http_client
27-
from six.moves import urllib
25+
import http.client as http_client
26+
import urllib.request
27+
import urllib.error
2828

2929

3030
class InvalidCertificateException(http_client.HTTPException):
@@ -37,7 +37,7 @@ def __init__(self, host, cert, reason):
3737
host: The hostname the connection was made to.
3838
cert: The SSL certificate (as a dictionary) the host returned.
3939
"""
40-
http_client.HTTPException.__init__(self)
40+
super().__init__()
4141
self.host = host
4242
self.cert = cert
4343
self.reason = reason
@@ -68,7 +68,7 @@ def __init__(self, host, port=None, key_file=None, cert_file=None,
6868
strict: When true, causes BadStatusLine to be raised if the status line
6969
can't be parsed as a valid HTTP/1.0 or 1.1 status line.
7070
"""
71-
http_client.HTTPConnection.__init__(self, host, port, strict, **kwargs)
71+
super().__init__(host, port, strict, **kwargs)
7272
self.key_file = key_file
7373
self.cert_file = cert_file
7474
self.ca_certs = ca_certs
@@ -136,7 +136,7 @@ class CertValidatingHTTPSHandler(urllib.request.HTTPSHandler):
136136

137137
def __init__(self, **kwargs):
138138
"""Constructor. Any keyword args are passed to the http_client handler."""
139-
urllib.request.HTTPSHandler.__init__(self)
139+
super().__init__()
140140
self._connection_args = kwargs
141141

142142
def https_open(self, req):
@@ -147,9 +147,9 @@ def http_class_wrapper(host, **kwargs):
147147
try:
148148
return self.do_open(http_class_wrapper, req)
149149
except urllib.error.URLError as e:
150-
if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1:
150+
if isinstance(e.reason, ssl.SSLError) and e.reason.args[0] == 1:
151151
raise InvalidCertificateException(req.host, '',
152152
e.reason.args[1])
153153
raise
154154

155-
https_request = urllib.request.HTTPSHandler.do_request_
155+
https_request = urllib.request.HTTPSHandler.do_request_

test_duo_openvpn.py

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import email.utils
22
import json
3-
import os
43
import tempfile
54
import unittest
65

7-
from mox3 import mox
8-
import six
6+
from unittest.mock import MagicMock
7+
import io
98

109
import duo_openvpn
1110

@@ -19,22 +18,22 @@ def mock_client_factory(mock):
1918
class MockClient(duo_openvpn.Client):
2019
def __init__(self, *args, **kwargs):
2120
mock.duo_client_init(*args, **kwargs)
22-
super(MockClient, self).__init__(*args, **kwargs)
21+
super().__init__(*args, **kwargs)
2322

2423
def set_proxy(self, *args, **kwargs):
2524
mock.duo_client_set_proxy(*args, **kwargs)
26-
return super(MockClient, self).set_proxy(*args, **kwargs)
25+
return super().set_proxy(*args, **kwargs)
2726

2827
def _connect(self):
2928
return mock
3029

3130
return MockClient
3231

33-
class MockResponse(six.StringIO, object):
32+
class MockResponse(io.StringIO):
3433
def __init__(self, status, body, reason='some reason'):
3534
self.status = status
3635
self.reason = reason
37-
super(MockResponse, self).__init__(body)
36+
super().__init__(body)
3837

3938
class TestIntegration(unittest.TestCase):
4039
IKEY = 'expected ikey'
@@ -60,13 +59,9 @@ class TestIntegration(unittest.TestCase):
6059
)
6160

6261
def setUp(self):
63-
self.mox = mox.Mox()
64-
self.expected_calls = self.mox.CreateMockAnything()
65-
66-
def assert_auth(self, environ, expected_control,
67-
send_control=True):
68-
self.mox.ReplayAll()
62+
self.expected_calls = MagicMock()
6963

64+
def assert_auth(self, environ, expected_control, send_control=True):
7065
with tempfile.NamedTemporaryFile() as control:
7166
if send_control:
7267
environ['control'] = control.name
@@ -76,12 +71,9 @@ def assert_auth(self, environ, expected_control,
7671
environ=environ,
7772
Client=mock_client_factory(self.expected_calls),
7873
)
79-
self.mox.VerifyAll()
8074

81-
control.seek(0, os.SEEK_SET)
82-
output = control.read()
83-
if not isinstance(output, six.text_type):
84-
output = output.decode('utf-8')
75+
control.seek(0)
76+
output = control.read().decode('utf-8')
8577
self.assertEqual(expected_control, output)
8678
if expected_control == '1':
8779
self.assertEqual(0, cm.exception.args[0])
@@ -101,7 +93,7 @@ def normal_environ(self):
10193
ikey=self.IKEY,
10294
skey=self.SKEY,
10395
host=self.HOST,
104-
user_agent=('duo_openvpn/' + duo_openvpn.__version__),
96+
user_agent=self.EXPECTED_USER_AGENT,
10597
)
10698
self.expected_calls.duo_client_set_proxy(
10799
host=None,
@@ -114,22 +106,22 @@ def compare_params(self, recv_params, sent_params):
114106
return len(recv_params.split('&')) == len(stanzas) and all([s in recv_params for s in stanzas])
115107

116108
def expect_request(self, method, path, params, params_func=None, response=None, raises=None):
117-
if params_func == None:
109+
if params_func is None:
118110
params_func = lambda p: self.compare_params(p, self.EXPECTED_PREAUTH_PARAMS)
119-
self.expected_calls.request(method, path, mox.Func(params_func), {
111+
112+
self.expected_calls.request(
113+
method, path, params_func, {
120114
'User-Agent': self.EXPECTED_USER_AGENT,
121115
'Host': self.HOST,
122116
'Content-type': 'application/x-www-form-urlencoded',
123-
'Authorization': mox.Func((lambda s: s.startswith('Basic ') and not s.startswith('Basic b\''))),
124-
'Date': mox.Func((lambda s: bool(email.utils.parsedate_tz(s))))
125-
},
117+
'Authorization': MagicMock(side_effect=lambda s: s.startswith('Basic ') and not s.startswith('Basic b\'')),
118+
'Date': MagicMock(side_effect=lambda s: bool(email.utils.parsedate_tz(s)))
119+
}
126120
)
127-
meth = self.expected_calls.getresponse()
128-
if raises is not None:
129-
meth.AndRaise(raises)
121+
if raises:
122+
self.expected_calls.getresponse.side_effect = raises
130123
else:
131-
meth.AndReturn(response)
132-
self.expected_calls.close()
124+
self.expected_calls.getresponse.return_value = response
133125

134126
def expect_preauth(self, result, path=EXPECTED_PREAUTH_PATH, factor='push1'):
135127
self.expect_request(
@@ -139,12 +131,12 @@ def expect_preauth(self, result, path=EXPECTED_PREAUTH_PATH, factor='push1'):
139131
response=MockResponse(
140132
status=200,
141133
body=json.dumps({
142-
'stat': 'OK',
143-
'response': {
144-
'result': result,
145-
'status': 'expected status',
146-
'factors': {'default': factor},
147-
},
134+
'stat': 'OK',
135+
'response': {
136+
'result': result,
137+
'status': 'expected status',
138+
'factors': {'default': factor},
139+
},
148140
}),
149141
),
150142
)
@@ -158,11 +150,11 @@ def expect_auth(self, result, path=EXPECTED_AUTH_PATH):
158150
response=MockResponse(
159151
status=200,
160152
body=json.dumps({
161-
'stat': 'OK',
162-
'response': {
163-
'result': result,
164-
'status': 'expected status',
165-
},
153+
'stat': 'OK',
154+
'response': {
155+
'result': result,
156+
'status': 'expected status',
157+
},
166158
}),
167159
),
168160
)

0 commit comments

Comments
 (0)