Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
999d92d
Import order
julien-lang Jul 16, 2025
9777db0
Remove __future__ imports
julien-lang Jul 16, 2025
e3e72cd
Cleanup super prototype
julien-lang Jul 16, 2025
2e3b54b
six.iter....
julien-lang Jul 16, 2025
64fa354
Remove calls to six.text_type and six.binary_type
julien-lang Jul 16, 2025
d014745
Remove calls to ensure_bytes, ensure_text, ensure_strings
julien-lang Jul 16, 2025
153f179
test fixed
eduardoChaucaGallegos Jul 17, 2025
2de2822
fixup! test fixed
julien-lang Jul 17, 2025
24835e3
Black
julien-lang Jul 17, 2025
60bb0bc
Update shotgun_api3/shotgun.py
julien-lang Jul 17, 2025
473b811
six.moves imports
julien-lang Jul 16, 2025
f68f1ae
Cleanup BytesIO import from six
julien-lang Jul 16, 2025
0b956cc
simple json
julien-lang Jul 16, 2025
b94da1a
Cleanup Py2-3 compat with ImportError
julien-lang Jul 16, 2025
6e89b98
Simplify Base64
julien-lang Jul 16, 2025
439d0b3
fixup! six.moves imports
julien-lang Jul 17, 2025
adbc0da
fixup! six.moves imports
julien-lang Jul 17, 2025
c4d1e30
Remove deprecated custome mimetype module
julien-lang Jul 16, 2025
e93ed1e
Remove deprecated backported mock module
julien-lang Jul 16, 2025
12f1abe
Fixup assert_called_once
julien-lang Jul 17, 2025
ea953d2
Fixup CI tests
julien-lang Jul 17, 2025
f45faac
fixup! Fixup CI tests
julien-lang Jul 17, 2025
6a2b3e2
provides debug info
julien-lang Jul 17, 2025
cb800c1
fixup! provides debug info
julien-lang Jul 17, 2025
a4afffa
fixup! Remove deprecated backported mock module
julien-lang Jul 17, 2025
77c9cd3
Remove python2 from httplib2 module
julien-lang Jul 16, 2025
6124681
fixup issue with ssl_error_classes
julien-lang Jul 17, 2025
10119ec
Cleanup six.PY2/six.PY3 conditions
julien-lang Jul 16, 2025
c9648c1
Remove useless test in Python 3
julien-lang Jul 16, 2025
e7ca1eb
Replace sgsix.file_types by io.IOBase
julien-lang Jul 16, 2025
3cbc5ff
Replace ShotgunSSLError by ssl.SSLError
julien-lang Jul 16, 2025
5d8b18d
Cleanup Python-2 related comments and workarounds
julien-lang Jul 17, 2025
b90a64b
fixup! Cleanup six.PY2/six.PY3 conditions
julien-lang Jul 17, 2025
f6275cd
Remove deprecated ensure_ascii parameter from SG object
julien-lang Jul 16, 2025
5c4dc5b
fixup! Remove deprecated ensure_ascii parameter from SG object
julien-lang Jul 17, 2025
c321734
Test CI
julien-lang Jul 17, 2025
1f370fe
fixup! Test CI
julien-lang Jul 17, 2025
2eb8306
fixup! fixup! Test CI
julien-lang Jul 17, 2025
1463852
fixup! fixup! fixup! Test CI
julien-lang Jul 17, 2025
da922f9
fixup! fixup! fixup! fixup! Test CI
julien-lang Jul 17, 2025
c03452a
Remove now unsused sgutils
julien-lang Jul 16, 2025
dc934f5
Remove now useless sgsix module
julien-lang Jul 16, 2025
9a131fa
Remove six module
julien-lang Jul 16, 2025
92c4429
SSL
julien-lang Feb 25, 2025
74a0494
More SSL cleanups
julien-lang Jul 17, 2025
99d3a1e
fixup! More SSL cleanups
julien-lang Jul 17, 2025
cfc0c98
fixup! fixup! More SSL cleanups
julien-lang Jul 17, 2025
f9f9a67
Fixup/restore
julien-lang Jul 18, 2025
2a56e5e
More cleanup
julien-lang Jul 18, 2025
29b6e0b
pulling from master and conflicts fixed
eduardoChaucaGallegos Sep 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/advanced/iron_python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ that we will be compatible with future releases of IronPython. While we don't of
IronPython, we certainly will do our best to figure out any issues that come up while using it and
how to avoid them.


Legacy Information
------------------

This following information is provided for historical purposes only.

As of July 9, 2015 you can look at this fork of the repo to see what changes were needed as of that
date to make things work. The original fork was as of v3.0.20 of the API. Big thanks to our awesome
clients Pixomondo for making their work public and letting us refer to it:
Expand All @@ -20,12 +26,6 @@ v3.0.20 can be used with IronPython with a little bit of added work:
https://bitbucket.org/jdhardy/ironpythonzlib/src/. And the blog post about it here
http://blog.jdhardy.ca/2008/12/solving-zlib-problem-ironpythonzlib.html

- If you encounter any SSL errors like
``unknown field: SERIALNUMBER=0123456789`` or ``:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed``.
For now you can workaround this problem by disabling ssl certificate validation which we've
encountered some intermittent issues with. Set ``NO_SSL_VALIDATION = True`` for either case.
See :const:`shotgun_api3.shotgun.NO_SSL_VALIDATION`

- If you encounter ``LookupError: unknown encoding: idna``, you can force utf-8 by changing
iri2uri.py ~ln 71 from ``authority = authority.encode('idna')`` to
``authority = authority.encode('utf-8')``
Expand Down
2 changes: 1 addition & 1 deletion docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The :mod:`~shotgun_api3.shotgun` module is a container for the :class:`~shotgun.
class. There are a couple of useful attributes to note.

.. automodule:: shotgun_api3.shotgun
:members: NO_SSL_VALIDATION, LOG
:members: LOG
:private-members:
:special-members:

Expand Down
111 changes: 11 additions & 100 deletions shotgun_api3/shotgun.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,6 @@

SHOTGUN_API_DISABLE_ENTITY_OPTIMIZATION = False

NO_SSL_VALIDATION = False
"""
Turns off hostname matching validation for SSL certificates

Sometimes there are cases where certificate validation should be disabled. For example, if you
have a self-signed internal certificate that isn't included in our certificate bundle, you may
not require the added security provided by enforcing this.
"""

# ----------------------------------------------------------------------------
# Version
Expand Down Expand Up @@ -327,9 +319,7 @@ class ClientCapabilities(object):
:ivar str local_path_field: The PTR field used for local file paths. This is calculated using
the value of ``platform``. Ex. ``local_path_mac``.
:ivar str py_version: Simple version of Python executable as a string. Eg. ``3.9``.
:ivar str ssl_version: Version of OpenSSL installed. Eg. ``OpenSSL 1.0.2g 1 Mar 2016``. This
info is only available in Python 2.7+ if the ssl module was imported successfully.
Defaults to ``unknown``
:ivar str ssl_version: Version of OpenSSL installed. Eg. ``OpenSSL 1.0.2g 1 Mar 2016``.
"""

def __init__(self):
Expand All @@ -350,14 +340,7 @@ def __init__(self):
self.local_path_field = None

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

# extract the OpenSSL version if we can. The version is only available in Python 2.7 and
# only if we successfully imported ssl
self.ssl_version = "unknown"
try:
self.ssl_version = ssl.OPENSSL_VERSION
except (AttributeError, NameError):
pass
self.ssl_version = ssl.OPENSSL_VERSION

def __str__(self):
return (
Expand Down Expand Up @@ -425,7 +408,6 @@ def __init__(self, sg):
self.proxy_pass = None
self.session_token = None
self.authorization = None
self.no_ssl_validation = False
self.localized = False

def set_server_params(self, base_url):
Expand Down Expand Up @@ -616,7 +598,6 @@ def __init__(
self.config.session_token = session_token
self.config.sudo_as_login = sudo_as_login
self.config.convert_datetimes_to_utc = convert_datetimes_to_utc
self.config.no_ssl_validation = NO_SSL_VALIDATION
self.config.raw_http_proxy = http_proxy

try:
Expand Down Expand Up @@ -2264,14 +2245,10 @@ def reset_user_agent(self):
ua_platform = self.client_caps.platform.capitalize()

# create ssl validation string based on settings
validation_str = "validate"
if self.config.no_ssl_validation:
validation_str = "no-validate"

self._user_agents = [
"shotgun-json (%s)" % __version__,
"Python %s (%s)" % (self.client_caps.py_version, ua_platform),
"ssl %s (%s)" % (self.client_caps.ssl_version, validation_str),
"ssl %s" % (self.client_caps.ssl_version),
]

def set_session_uuid(self, session_uuid):
Expand Down Expand Up @@ -3543,8 +3520,14 @@ def _build_opener(self, handler):
Build urllib2 opener with appropriate proxy handler.
"""
handlers = []
if self.__ca_certs and not NO_SSL_VALIDATION:
handlers.append(CACertsHTTPSHandler(self.__ca_certs))
if self.__ca_certs:
handlers.append(
urllib.request.HTTPSHandler(
context=ssl.create_default_context(
cafile=self.__ca_certs,
),
),
)

if self.config.proxy_handler:
handlers.append(self.config.proxy_handler)
Expand Down Expand Up @@ -3613,23 +3596,6 @@ def _get_certs_file(cls, ca_certs):
cert_file = os.path.join(cur_dir, "lib", "certifi", "cacert.pem")
return cert_file

def _turn_off_ssl_validation(self):
"""
Turn off SSL certificate validation.
"""
global NO_SSL_VALIDATION
self.config.no_ssl_validation = True
NO_SSL_VALIDATION = True
# reset ssl-validation in user-agents
self._user_agents = [
(
"ssl %s (no-validate)" % self.client_caps.ssl_version
if ua.startswith("ssl ")
else ua
)
for ua in self._user_agents
]

# Deprecated methods from old wrapper
def schema(self, entity_type):
"""
Expand Down Expand Up @@ -3843,44 +3809,7 @@ def _make_call(self, verb, path, body, headers):
if attempt == max_rpc_attempts:
LOG.debug("Request failed. Giving up after %d attempts." % attempt)
raise
# This is the exact same block as the "except Exception" bellow.
# We need to do it here because the next except will match it
# otherwise and will not re-attempt.
# When we drop support of Python 2 and we will probably drop the
# next except, we might want to remove this except too.
Comment on lines -3846 to -3850
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not accurate anymore after #379. So we remove the comment but keep the block.

except (ssl.SSLError, ssl.CertificateError) as e:
# Test whether the exception is due to the fact that this is an older version of
# Python that cannot validate certificates encrypted with SHA-2. If it is, then
# fall back on disabling the certificate validation and try again - unless the
# SHOTGUN_FORCE_CERTIFICATE_VALIDATION environment variable has been set by the
# user. In that case we simply raise the exception. Any other exceptions simply
# get raised as well.
#
# For more info see:
# https://www.shotgridsoftware.com/blog/important-ssl-certificate-renewal-and-sha-2/
#
# SHA-2 errors look like this:
# [Errno 1] _ssl.c:480: error:0D0C50A1:asn1 encoding routines:ASN1_item_verify:
# unknown message digest algorithm
#
# Any other exceptions simply get raised.
if (
"unknown message digest algorithm" not in str(e)
or "SHOTGUN_FORCE_CERTIFICATE_VALIDATION" in os.environ
):
raise

if self.config.no_ssl_validation is False:
LOG.warning(
"SSL Error: this Python installation is incompatible with "
"certificates signed with SHA-2. Disabling certificate validation. "
"For more information, see https://www.shotgridsoftware.com/blog/"
"important-ssl-certificate-renewal-and-sha-2/"
)
self._turn_off_ssl_validation()
# reload user agent to reflect that we have turned off ssl validation
req_headers["user-agent"] = "; ".join(self._user_agents)

self._close_connection()
if attempt == max_rpc_attempts:
LOG.debug("Request failed. Giving up after %d attempts." % attempt)
Expand Down Expand Up @@ -4142,14 +4071,12 @@ def _get_connection(self):
timeout=self.config.timeout_secs,
ca_certs=self.__ca_certs,
proxy_info=pi,
disable_ssl_certificate_validation=self.config.no_ssl_validation,
)
else:
self._connection = Http(
timeout=self.config.timeout_secs,
ca_certs=self.__ca_certs,
proxy_info=None,
disable_ssl_certificate_validation=self.config.no_ssl_validation,
)

return self._connection
Expand Down Expand Up @@ -4613,22 +4540,6 @@ def connect(self):
)


class CACertsHTTPSHandler(urllib.request.HTTPHandler):
"""
Handler that ensures https connections are created with the custom CA certs.
"""

def __init__(self, cacerts):
super().__init__(self)
self.__ca_certs = cacerts

def https_open(self, req):
return self.do_open(self.create_https_connection, req)

def create_https_connection(self, *args, **kwargs):
return CACertsHTTPSConnection(*args, ca_certs=self.__ca_certs, **kwargs)


# Helpers from the previous API, left as is.
# Based on http://code.activestate.com/recipes/146306/
class FormPostHandler(urllib.request.BaseHandler):
Expand Down
71 changes: 0 additions & 71 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2214,77 +2214,6 @@ def my_side_effect2(*args, **kwargs):
finally:
self.sg.config.rpc_attempt_interval = bak_rpc_attempt_interval

@unittest.mock.patch("shotgun_api3.shotgun.Http.request")
def test_sha2_error(self, mock_request):
# Simulate the exception raised with SHA-2 errors
mock_request.side_effect = ssl.SSLError(
"[Errno 1] _ssl.c:480: error:0D0C50A1:asn1 "
"encoding routines:ASN1_item_verify: unknown message digest "
"algorithm"
)

# save the original state
original_env_val = os.environ.pop("SHOTGUN_FORCE_CERTIFICATE_VALIDATION", None)

# ensure we're starting with the right values
self.sg.reset_user_agent()

# ensure the initial settings are correct. These will be different depending on whether
# the ssl module imported successfully or not.
if "ssl" in sys.modules:
self.assertFalse(self.sg.config.no_ssl_validation)
self.assertFalse(shotgun_api3.shotgun.NO_SSL_VALIDATION)
self.assertTrue("(validate)" in " ".join(self.sg._user_agents))
self.assertFalse("(no-validate)" in " ".join(self.sg._user_agents))
else:
self.assertTrue(self.sg.config.no_ssl_validation)
self.assertTrue(shotgun_api3.shotgun.NO_SSL_VALIDATION)
self.assertFalse("(validate)" in " ".join(self.sg._user_agents))
self.assertTrue("(no-validate)" in " ".join(self.sg._user_agents))

try:
self.sg.info()
except ssl.SSLError:
# ensure the api has reset the values in the correct fallback behavior
self.assertTrue(self.sg.config.no_ssl_validation)
self.assertTrue(shotgun_api3.shotgun.NO_SSL_VALIDATION)
self.assertFalse("(validate)" in " ".join(self.sg._user_agents))
self.assertTrue("(no-validate)" in " ".join(self.sg._user_agents))

if original_env_val is not None:
os.environ["SHOTGUN_FORCE_CERTIFICATE_VALIDATION"] = original_env_val

@unittest.mock.patch("shotgun_api3.shotgun.Http.request")
def test_sha2_error_with_strict(self, mock_request):
# Simulate the exception raised with SHA-2 errors
mock_request.side_effect = ssl.SSLError(
"[Errno 1] _ssl.c:480: error:0D0C50A1:asn1 "
"encoding routines:ASN1_item_verify: unknown message digest "
"algorithm"
)

# save the original state
original_env_val = os.environ.pop("SHOTGUN_FORCE_CERTIFICATE_VALIDATION", None)
os.environ["SHOTGUN_FORCE_CERTIFICATE_VALIDATION"] = "1"

# ensure we're starting with the right values
self.sg.config.no_ssl_validation = False
shotgun_api3.shotgun.NO_SSL_VALIDATION = False
self.sg.reset_user_agent()

try:
self.sg.info()
except ssl.SSLError:
# ensure the api has NOT reset the values in the fallback behavior because we have
# set the env variable to force validation
self.assertFalse(self.sg.config.no_ssl_validation)
self.assertFalse(shotgun_api3.shotgun.NO_SSL_VALIDATION)
self.assertFalse("(no-validate)" in " ".join(self.sg._user_agents))
self.assertTrue("(validate)" in " ".join(self.sg._user_agents))

if original_env_val is not None:
os.environ["SHOTGUN_FORCE_CERTIFICATE_VALIDATION"] = original_env_val

@unittest.mock.patch.object(urllib.request.OpenerDirector, "open")
def test_sanitized_auth_params(self, mock_open):
# Simulate the server blowing up and giving us a 500 error
Expand Down
9 changes: 3 additions & 6 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,11 @@ def test_user_agent(self):
args, _ = self.sg._http_request.call_args
(_, _, _, headers) = args
ssl_validate_lut = {True: "no-validate", False: "validate"}
expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s)" % (
expected = "shotgun-json (%s); Python %s (%s); ssl %s" % (
api.__version__,
client_caps.py_version,
client_caps.platform.capitalize(),
client_caps.ssl_version,
ssl_validate_lut[config.no_ssl_validation],
)
self.assertEqual(expected, headers.get("user-agent"))

Expand All @@ -283,12 +282,11 @@ def test_user_agent(self):
self.sg.info()
args, _ = self.sg._http_request.call_args
(_, _, _, headers) = args
expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s); test-agent" % (
expected = "shotgun-json (%s); Python %s (%s); ssl %s; test-agent" % (
api.__version__,
client_caps.py_version,
client_caps.platform.capitalize(),
client_caps.ssl_version,
ssl_validate_lut[config.no_ssl_validation],
)
self.assertEqual(expected, headers.get("user-agent"))

Expand All @@ -297,12 +295,11 @@ def test_user_agent(self):
self.sg.info()
args, _ = self.sg._http_request.call_args
(_, _, _, headers) = args
expected = "shotgun-json (%s); Python %s (%s); ssl %s (%s)" % (
expected = "shotgun-json (%s); Python %s (%s); ssl %s" % (
api.__version__,
client_caps.py_version,
client_caps.platform.capitalize(),
client_caps.ssl_version,
ssl_validate_lut[config.no_ssl_validation],
)
self.assertEqual(expected, headers.get("user-agent"))

Expand Down
Loading