Skip to content

Commit 6492b21

Browse files
committed
SSL
1 parent 56b37c3 commit 6492b21

File tree

4 files changed

+26
-181
lines changed

4 files changed

+26
-181
lines changed

docs/advanced/iron_python.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ v3.0.20 can be used with IronPython with a little bit of added work:
2222

2323
- If you encounter any SSL errors like
2424
``unknown field: SERIALNUMBER=0123456789`` or ``:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed``.
25-
For now you can workaround this problem by disabling ssl certificate validation which we've
26-
encountered some intermittent issues with. Set ``NO_SSL_VALIDATION = True`` for either case.
27-
See :const:`shotgun_api3.shotgun.NO_SSL_VALIDATION`
25+
Inject you own CA cert <<TODO>>
2826

2927
- If you encounter ``LookupError: unknown encoding: idna``, you can force utf-8 by changing
3028
iri2uri.py ~ln 71 from ``authority = authority.encode('idna')`` to

shotgun_api3/shotgun.py

Lines changed: 13 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,6 @@
7777

7878
SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION = False
7979

80-
NO_SSL_VALIDATION = False
81-
"""
82-
Turns off hostname matching validation for SSL certificates
83-
84-
Sometimes there are cases where certificate validation should be disabled. For example, if you
85-
have a self-signed internal certificate that isn't included in our certificate bundle, you may
86-
not require the added security provided by enforcing this.
87-
"""
8880

8981
# ----------------------------------------------------------------------------
9082
# Version
@@ -350,12 +342,11 @@ def __init__(self):
350342

351343
self.py_version = ".".join(str(x) for x in sys.version_info[:2])
352344

353-
# extract the OpenSSL version if we can. The version is only available in Python 2.7 and
354-
# only if we successfully imported ssl
345+
# extract the OpenSSL version if we can.
355346
self.ssl_version = "unknown"
356347
try:
357348
self.ssl_version = ssl.OPENSSL_VERSION
358-
except (AttributeError, NameError):
349+
except AttributeError:
359350
pass
360351

361352
def __str__(self):
@@ -424,7 +415,6 @@ def __init__(self, sg):
424415
self.proxy_pass = None
425416
self.session_token = None
426417
self.authorization = None
427-
self.no_ssl_validation = False
428418
self.localized = False
429419

430420
def set_server_params(self, base_url):
@@ -633,7 +623,6 @@ def __init__(
633623
self.config.session_token = session_token
634624
self.config.sudo_as_login = sudo_as_login
635625
self.config.convert_datetimes_to_utc = convert_datetimes_to_utc
636-
self.config.no_ssl_validation = NO_SSL_VALIDATION
637626
self.config.raw_http_proxy = http_proxy
638627

639628
try:
@@ -2281,14 +2270,10 @@ def reset_user_agent(self):
22812270
ua_platform = self.client_caps.platform.capitalize()
22822271

22832272
# create ssl validation string based on settings
2284-
validation_str = "validate"
2285-
if self.config.no_ssl_validation:
2286-
validation_str = "no-validate"
2287-
22882273
self._user_agents = [
2289-
"shotgun-json (%s)" % __version__,
2290-
"Python %s (%s)" % (self.client_caps.py_version, ua_platform),
2291-
"ssl %s (%s)" % (self.client_caps.ssl_version, validation_str),
2274+
f"shotgun-json ({__version__})",
2275+
f"Python {self.client_caps.py_version} ({ua_platform})",
2276+
f"ssl {self.client_caps.ssl_version}",
22922277
]
22932278

22942279
def set_session_uuid(self, session_uuid):
@@ -3560,8 +3545,14 @@ def _build_opener(self, handler):
35603545
Build urllib2 opener with appropriate proxy handler.
35613546
"""
35623547
handlers = []
3563-
if self.__ca_certs and not NO_SSL_VALIDATION:
3564-
handlers.append(CACertsHTTPSHandler(self.__ca_certs))
3548+
if self.__ca_certs:
3549+
handlers.append(
3550+
urllib.request.HTTPSHandler(
3551+
context=ssl.create_default_context(
3552+
cafile=self.__ca_certs,
3553+
),
3554+
),
3555+
)
35653556

35663557
if self.config.proxy_handler:
35673558
handlers.append(self.config.proxy_handler)
@@ -3630,23 +3621,6 @@ def _get_certs_file(cls, ca_certs):
36303621
cert_file = os.path.join(cur_dir, "lib", "certifi", "cacert.pem")
36313622
return cert_file
36323623

3633-
def _turn_off_ssl_validation(self):
3634-
"""
3635-
Turn off SSL certificate validation.
3636-
"""
3637-
global NO_SSL_VALIDATION
3638-
self.config.no_ssl_validation = True
3639-
NO_SSL_VALIDATION = True
3640-
# reset ssl-validation in user-agents
3641-
self._user_agents = [
3642-
(
3643-
"ssl %s (no-validate)" % self.client_caps.ssl_version
3644-
if ua.startswith("ssl ")
3645-
else ua
3646-
)
3647-
for ua in self._user_agents
3648-
]
3649-
36503624
# Deprecated methods from old wrapper
36513625
def schema(self, entity_type):
36523626
"""
@@ -3849,59 +3823,6 @@ def _make_call(self, verb, path, body, headers):
38493823
attempt += 1
38503824
try:
38513825
return self._http_request(verb, path, body, req_headers)
3852-
except ssl.SSLEOFError as e:
3853-
# SG-34910 - EOF occurred in violation of protocol (_ssl.c:2426)
3854-
# This issue seems to be related to proxy and keep alive.
3855-
# It looks like, sometimes, the proxy drops the connection on
3856-
# the TCP/TLS level despites the keep-alive. So we need to close
3857-
# the connection and make a new attempt.
3858-
LOG.debug("SSLEOFError: {}".format(e))
3859-
self._close_connection()
3860-
if attempt == max_rpc_attempts:
3861-
LOG.debug("Request failed. Giving up after %d attempts." % attempt)
3862-
raise
3863-
# This is the exact same block as the "except Exception" bellow.
3864-
# We need to do it here because the next except will match it
3865-
# otherwise and will not re-attempt.
3866-
# When we drop support of Python 2 and we will probably drop the
3867-
# next except, we might want to remove this except too.
3868-
except ssl_error_classes as e:
3869-
# Test whether the exception is due to the fact that this is an older version of
3870-
# Python that cannot validate certificates encrypted with SHA-2. If it is, then
3871-
# fall back on disabling the certificate validation and try again - unless the
3872-
# SHOTGUN_FORCE_CERTIFICATE_VALIDATION environment variable has been set by the
3873-
# user. In that case we simply raise the exception. Any other exceptions simply
3874-
# get raised as well.
3875-
#
3876-
# For more info see:
3877-
# https://www.shotgridsoftware.com/blog/important-ssl-certificate-renewal-and-sha-2/
3878-
#
3879-
# SHA-2 errors look like this:
3880-
# [Errno 1] _ssl.c:480: error:0D0C50A1:asn1 encoding routines:ASN1_item_verify:
3881-
# unknown message digest algorithm
3882-
#
3883-
# Any other exceptions simply get raised.
3884-
if (
3885-
"unknown message digest algorithm" not in str(e)
3886-
or "SHOTGUN_FORCE_CERTIFICATE_VALIDATION" in os.environ
3887-
):
3888-
raise
3889-
3890-
if self.config.no_ssl_validation is False:
3891-
LOG.warning(
3892-
"SSL Error: this Python installation is incompatible with "
3893-
"certificates signed with SHA-2. Disabling certificate validation. "
3894-
"For more information, see https://www.shotgridsoftware.com/blog/"
3895-
"important-ssl-certificate-renewal-and-sha-2/"
3896-
)
3897-
self._turn_off_ssl_validation()
3898-
# reload user agent to reflect that we have turned off ssl validation
3899-
req_headers["user-agent"] = "; ".join(self._user_agents)
3900-
3901-
self._close_connection()
3902-
if attempt == max_rpc_attempts:
3903-
LOG.debug("Request failed. Giving up after %d attempts." % attempt)
3904-
raise
39053826
except Exception:
39063827
self._close_connection()
39073828
if attempt == max_rpc_attempts:
@@ -4160,14 +4081,12 @@ def _get_connection(self):
41604081
timeout=self.config.timeout_secs,
41614082
ca_certs=self.__ca_certs,
41624083
proxy_info=pi,
4163-
disable_ssl_certificate_validation=self.config.no_ssl_validation,
41644084
)
41654085
else:
41664086
self._connection = Http(
41674087
timeout=self.config.timeout_secs,
41684088
ca_certs=self.__ca_certs,
41694089
proxy_info=None,
4170-
disable_ssl_certificate_validation=self.config.no_ssl_validation,
41714090
)
41724091

41734092
return self._connection

tests/test_api.py

Lines changed: 6 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def test_upload_to_sg(self, mock_send_form):
276276
Upload an attachment tests for _upload_to_sg()
277277
"""
278278
self.sg.server_info["s3_direct_uploads_enabled"] = False
279-
mock_send_form.method.assert_called_once()
279+
280280
mock_send_form.return_value = "1\n:123\nasd"
281281
this_dir, _ = os.path.split(__file__)
282282
u_path = os.path.abspath(
@@ -289,6 +289,8 @@ def test_upload_to_sg(self, mock_send_form):
289289
"attachments",
290290
tag_list="monkeys, everywhere, send, help",
291291
)
292+
293+
mock_send_form.assert_called_once()
292294
mock_send_form_args, _ = mock_send_form.call_args
293295
display_name_to_send = mock_send_form_args[1].get("display_name", "")
294296
self.assertTrue(isinstance(upload_id, int))
@@ -311,7 +313,7 @@ def test_upload_to_sg(self, mock_send_form):
311313
display_name_to_send.startswith("b'") and display_name_to_send.endswith("'")
312314
)
313315

314-
mock_send_form.method.assert_called_once()
316+
# mock_send_form.method.assert_called_once() ## CANOT work because was already called earlier....
315317
mock_send_form.return_value = "2\nIt can't be upload"
316318
self.assertRaises(
317319
shotgun_api3.ShotgunError,
@@ -674,7 +676,6 @@ def share_thumbnail_retry(*args, **kwargs):
674676
def test_share_thumbnail_not_ready(self, mock_send_form):
675677
"""throw an exception if trying to share a transient thumbnail"""
676678

677-
mock_send_form.method.assert_called_once()
678679
mock_send_form.return_value = (
679680
"2"
680681
"\nsource_entity image is a transient thumbnail that cannot be shared. "
@@ -692,7 +693,7 @@ def test_share_thumbnail_not_ready(self, mock_send_form):
692693
def test_share_thumbnail_returns_error(self, mock_send_form):
693694
"""throw an exception if server returns an error code"""
694695

695-
mock_send_form.method.assert_called_once()
696+
# mock_send_form.method.assert_called_once() # never worked.....
696697
mock_send_form.return_value = "1\nerror message.\n"
697698

698699
self.assertRaises(
@@ -2245,78 +2246,7 @@ def my_side_effect2(*args, **kwargs):
22452246
finally:
22462247
self.sg.config.rpc_attempt_interval = bak_rpc_attempt_interval
22472248

2248-
@unittest.mock.patch("shotgun_api3.shotgun.Http.request")
2249-
def test_sha2_error(self, mock_request):
2250-
# Simulate the exception raised with SHA-2 errors
2251-
mock_request.side_effect = ssl.SSLError(
2252-
"[Errno 1] _ssl.c:480: error:0D0C50A1:asn1 "
2253-
"encoding routines:ASN1_item_verify: unknown message digest "
2254-
"algorithm"
2255-
)
2256-
2257-
# save the original state
2258-
original_env_val = os.environ.pop("SHOTGUN_FORCE_CERTIFICATE_VALIDATION", None)
2259-
2260-
# ensure we're starting with the right values
2261-
self.sg.reset_user_agent()
2262-
2263-
# ensure the initial settings are correct. These will be different depending on whether
2264-
# the ssl module imported successfully or not.
2265-
if "ssl" in sys.modules:
2266-
self.assertFalse(self.sg.config.no_ssl_validation)
2267-
self.assertFalse(shotgun_api3.shotgun.NO_SSL_VALIDATION)
2268-
self.assertTrue("(validate)" in " ".join(self.sg._user_agents))
2269-
self.assertFalse("(no-validate)" in " ".join(self.sg._user_agents))
2270-
else:
2271-
self.assertTrue(self.sg.config.no_ssl_validation)
2272-
self.assertTrue(shotgun_api3.shotgun.NO_SSL_VALIDATION)
2273-
self.assertFalse("(validate)" in " ".join(self.sg._user_agents))
2274-
self.assertTrue("(no-validate)" in " ".join(self.sg._user_agents))
2275-
2276-
try:
2277-
self.sg.info()
2278-
except ssl.SSLError:
2279-
# ensure the api has reset the values in the correct fallback behavior
2280-
self.assertTrue(self.sg.config.no_ssl_validation)
2281-
self.assertTrue(shotgun_api3.shotgun.NO_SSL_VALIDATION)
2282-
self.assertFalse("(validate)" in " ".join(self.sg._user_agents))
2283-
self.assertTrue("(no-validate)" in " ".join(self.sg._user_agents))
2284-
2285-
if original_env_val is not None:
2286-
os.environ["SHOTGUN_FORCE_CERTIFICATE_VALIDATION"] = original_env_val
2287-
2288-
@unittest.mock.patch("shotgun_api3.shotgun.Http.request")
2289-
def test_sha2_error_with_strict(self, mock_request):
2290-
# Simulate the exception raised with SHA-2 errors
2291-
mock_request.side_effect = ssl.SSLError(
2292-
"[Errno 1] _ssl.c:480: error:0D0C50A1:asn1 "
2293-
"encoding routines:ASN1_item_verify: unknown message digest "
2294-
"algorithm"
2295-
)
2296-
2297-
# save the original state
2298-
original_env_val = os.environ.pop("SHOTGUN_FORCE_CERTIFICATE_VALIDATION", None)
2299-
os.environ["SHOTGUN_FORCE_CERTIFICATE_VALIDATION"] = "1"
2300-
2301-
# ensure we're starting with the right values
2302-
self.sg.config.no_ssl_validation = False
2303-
shotgun_api3.shotgun.NO_SSL_VALIDATION = False
2304-
self.sg.reset_user_agent()
2305-
2306-
try:
2307-
self.sg.info()
2308-
except ssl.SSLError:
2309-
# ensure the api has NOT reset the values in the fallback behavior because we have
2310-
# set the env variable to force validation
2311-
self.assertFalse(self.sg.config.no_ssl_validation)
2312-
self.assertFalse(shotgun_api3.shotgun.NO_SSL_VALIDATION)
2313-
self.assertFalse("(no-validate)" in " ".join(self.sg._user_agents))
2314-
self.assertTrue("(validate)" in " ".join(self.sg._user_agents))
2315-
2316-
if original_env_val is not None:
2317-
os.environ["SHOTGUN_FORCE_CERTIFICATE_VALIDATION"] = original_env_val
2318-
2319-
@unittest.mock.patch.object(urllib.request.OpenerDirector, "open")
2249+
@unittest.mock.patch.object(urllib.request.OpenerDirector, 'open')
23202250
def test_sanitized_auth_params(self, mock_open):
23212251
# Simulate the server blowing up and giving us a 500 error
23222252
mock_open.side_effect = urllib.error.HTTPError("url", 500, "message", {}, None)

tests/test_client.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
import shotgun_api3.lib.httplib2 as httplib2
2929
import shotgun_api3 as api
3030
from shotgun_api3.shotgun import ServerCapabilities, SG_TIMEZONE
31-
from . import base
31+
import shotgun_api3 as api
32+
import shotgun_api3.lib.httplib2 as httplib2
3233

3334

3435
def b64encode(val):
@@ -268,12 +269,11 @@ def test_user_agent(self):
268269
args, _ = self.sg._http_request.call_args
269270
(_, _, _, headers) = args
270271
ssl_validate_lut = {True: "no-validate", False: "validate"}
271-
expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s)" % (
272+
expected = "shotgun-json (%s); Python %s (%s); ssl %s" % (
272273
api.__version__,
273274
client_caps.py_version,
274275
client_caps.platform.capitalize(),
275276
client_caps.ssl_version,
276-
ssl_validate_lut[config.no_ssl_validation],
277277
)
278278
self.assertEqual(expected, headers.get("user-agent"))
279279

@@ -282,12 +282,11 @@ def test_user_agent(self):
282282
self.sg.info()
283283
args, _ = self.sg._http_request.call_args
284284
(_, _, _, headers) = args
285-
expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s); test-agent" % (
285+
expected = "shotgun-json (%s); Python %s (%s); ssl %s; test-agent" % (
286286
api.__version__,
287287
client_caps.py_version,
288288
client_caps.platform.capitalize(),
289289
client_caps.ssl_version,
290-
ssl_validate_lut[config.no_ssl_validation],
291290
)
292291
self.assertEqual(expected, headers.get("user-agent"))
293292

@@ -296,12 +295,11 @@ def test_user_agent(self):
296295
self.sg.info()
297296
args, _ = self.sg._http_request.call_args
298297
(_, _, _, headers) = args
299-
expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s)" % (
298+
expected = "shotgun-json (%s); Python %s (%s); ssl %s" % (
300299
api.__version__,
301300
client_caps.py_version,
302301
client_caps.platform.capitalize(),
303302
client_caps.ssl_version,
304-
ssl_validate_lut[config.no_ssl_validation],
305303
)
306304
self.assertEqual(expected, headers.get("user-agent"))
307305

@@ -325,7 +323,7 @@ def test_network_retry(self):
325323
)
326324
# Ensure that sleep was called with the retry interval between each attempt
327325
attempt_interval = self.sg.config.rpc_attempt_interval / 1000.0
328-
calls = [unittest.mock.callargs(((attempt_interval,), {}))]
326+
calls = [(attempt_interval,)]
329327
calls *= self.sg.config.max_rpc_attempts - 1
330328
self.assertTrue(
331329
mock_sleep.call_args_list == calls,

0 commit comments

Comments
 (0)