From 542b66363cfcd0af7b7c79a0ec1b8a0edc42ec28 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 27 Nov 2025 19:17:38 -0800 Subject: [PATCH 01/10] Tests --- azure-pipelines-templates/run-tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/azure-pipelines-templates/run-tests.yml b/azure-pipelines-templates/run-tests.yml index c1a1a4ef8..61be2c18a 100644 --- a/azure-pipelines-templates/run-tests.yml +++ b/azure-pipelines-templates/run-tests.yml @@ -50,6 +50,12 @@ jobs: python.version: '3.10' Python 3.11: python.version: '3.11' + Python 3.13: + python.version: '3.13' + + # Keep compat + Python 3.7: + python.version: '3.7' maxParallel: 4 From e430f0da6501e9ca8b1322cd0153e32bc0f483f2 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 17 Dec 2025 12:46:56 -0800 Subject: [PATCH 02/10] Tests --- azure-pipelines-templates/run-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/azure-pipelines-templates/run-tests.yml b/azure-pipelines-templates/run-tests.yml index 61be2c18a..c166c24ff 100644 --- a/azure-pipelines-templates/run-tests.yml +++ b/azure-pipelines-templates/run-tests.yml @@ -57,7 +57,6 @@ jobs: Python 3.7: python.version: '3.7' - maxParallel: 4 variables: group: sg-credentials From 384e968fab392d916ab0528173ecdc4588fbc9a6 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 17 Dec 2025 12:56:11 -0800 Subject: [PATCH 03/10] ee --- azure-pipelines-templates/run-tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-templates/run-tests.yml b/azure-pipelines-templates/run-tests.yml index c166c24ff..6eeda2fb0 100644 --- a/azure-pipelines-templates/run-tests.yml +++ b/azure-pipelines-templates/run-tests.yml @@ -55,8 +55,10 @@ jobs: # Keep compat Python 3.7: - python.version: '3.7' + python.version: pypy3.7 + maxParallel: 2 + # Looks like too much parallelle creates race condition issues between tests..... :( variables: group: sg-credentials @@ -71,7 +73,6 @@ jobs: - task: UsePythonVersion@0 inputs: versionSpec: '$(python.version)' - addToPath: True # Install all dependencies needed for running the tests. This command is good # for all OSes From 37f17054e1fa0f0dc2e624218d12c453372e3784 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 17 Dec 2025 14:39:23 -0800 Subject: [PATCH 04/10] Tests --- azure-pipelines-templates/run-tests.yml | 14 +++++++------- azure-pipelines.yml | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/azure-pipelines-templates/run-tests.yml b/azure-pipelines-templates/run-tests.yml index 6eeda2fb0..675ad5af4 100644 --- a/azure-pipelines-templates/run-tests.yml +++ b/azure-pipelines-templates/run-tests.yml @@ -44,18 +44,18 @@ jobs: strategy: matrix: # We support these versions of Python. - Python 3.9: - python.version: '3.9' - Python 3.10: - python.version: '3.10' + # Python 3.9: + # python.version: '3.9' + # Python 3.10: + # python.version: '3.10' Python 3.11: python.version: '3.11' Python 3.13: python.version: '3.13' - # Keep compat - Python 3.7: - python.version: pypy3.7 + # # Keep compat + # Python 3.7: + # python.version: pypy3.7 maxParallel: 2 # Looks like too much parallelle creates race condition issues between tests..... :( diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b9be5ab7e..e20fac629 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -79,12 +79,12 @@ jobs: name: Linux vm_image: 'ubuntu-latest' -- template: azure-pipelines-templates/run-tests.yml - parameters: - name: macOS - vm_image: 'macOS-latest' +# - template: azure-pipelines-templates/run-tests.yml +# parameters: +# name: macOS +# vm_image: 'macOS-latest' -- template: azure-pipelines-templates/run-tests.yml - parameters: - name: Windows - vm_image: 'windows-latest' +# - template: azure-pipelines-templates/run-tests.yml +# parameters: +# name: Windows +# vm_image: 'windows-latest' From c9e7a5b23602521d972bc6a0f656255439fc2aa0 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 17 Dec 2025 15:37:48 -0800 Subject: [PATCH 05/10] tests --- azure-pipelines-templates/run-tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-templates/run-tests.yml b/azure-pipelines-templates/run-tests.yml index 675ad5af4..0345e0f3a 100644 --- a/azure-pipelines-templates/run-tests.yml +++ b/azure-pipelines-templates/run-tests.yml @@ -77,8 +77,9 @@ jobs: # Install all dependencies needed for running the tests. This command is good # for all OSes - script: | + python -m pip install --upgrade pip python -m pip install --upgrade pip setuptools wheel - python -m pip install -r tests/ci_requirements.txt + python -m pip install --upgrade -r tests/ci_requirements.txt displayName: Install tools # The {{}} syntax is meant for the the pre-processor of Azure pipeline. Every statement inside @@ -98,9 +99,10 @@ jobs: # Runs the tests and generates test coverage. The tests results are uploaded to Azure Pipelines in the # Tests tab of the build and each test run will be named after the --test-run-title argument to pytest, # for example 'Windows - 2.7' + ## pytest --durations=0 -v --cov shotgun_api3 --cov-report xml --test-run-title="${{parameters.name}}-$(python.version)" - bash: | cp ./tests/example_config ./tests/config - pytest --durations=0 -v --cov shotgun_api3 --cov-report xml --test-run-title="${{parameters.name}}-$(python.version)" + python -m pytest -v ./tests/test_api.py -k test_info displayName: Running tests env: # Pass the values needed to authenticate with the Flow Production Tracking site and create some entities. From a2b77e7fff9c1916739f10fcc1de190b9e81284d Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 17 Dec 2025 15:48:59 -0800 Subject: [PATCH 06/10] add logs --- tests/test_api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index d0e8407ea..1ac95d5e9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -35,6 +35,20 @@ from . import base +import logging + +root = logging.getLogger() +root.setLevel(logging.DEBUG) + +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +root.addHandler(handler) + +logger = logging.getLogger("shotgun_api3") +logger.setLevel(logging.DEBUG) + class TestShotgunApi(base.LiveTestBase): def setUp(self): From 397a06e45e12874bd6fe6845164f111c1be45602 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 17 Dec 2025 15:54:10 -0800 Subject: [PATCH 07/10] Try updating httplib2 ?!?! --- shotgun_api3/lib/httplib2/__init__.py | 13 +- shotgun_api3/lib/httplib2/auth.py | 9 +- shotgun_api3/lib/httplib2/socks.py | 518 -------------------------- update_httplib2.py | 2 +- 4 files changed, 13 insertions(+), 529 deletions(-) delete mode 100644 shotgun_api3/lib/httplib2/socks.py diff --git a/shotgun_api3/lib/httplib2/__init__.py b/shotgun_api3/lib/httplib2/__init__.py index ba5fa2f23..e9350f684 100644 --- a/shotgun_api3/lib/httplib2/__init__.py +++ b/shotgun_api3/lib/httplib2/__init__.py @@ -16,7 +16,7 @@ "Lai Han", ] __license__ = "MIT" -__version__ = "0.22.0" +__version__ = "0.31.0" import base64 import calendar @@ -47,9 +47,7 @@ try: import socks except ImportError: - # TODO: remove this fallback and copypasted socksipy module upon py2/3 merge, - # idea is to have soft-dependency on any compatible module called socks - from . import socks + socks = None from . import auth from .error import * from .iri2uri import iri2uri @@ -180,7 +178,8 @@ def _build_ssl_context( if hasattr(context, "check_hostname"): context.check_hostname = not disable_ssl_certificate_validation - context.load_verify_locations(ca_certs) + if not disable_ssl_certificate_validation: + context.load_verify_locations(ca_certs) if cert_file: context.load_cert_chain(cert_file, key_file, key_password) @@ -955,6 +954,10 @@ def proxy_info_from_url(url, method="http", noproxy=None): url = urllib.parse.urlparse(url) proxy_type = 3 # socks.PROXY_TYPE_HTTP + if url.scheme == "socks4": + proxy_type = 1 # socks.PROXY_TYPE_SOCKS4 + elif url.scheme == "socks5" or url.scheme == "socks": + proxy_type = 2 # socks.PROXY_TYPE_SOCKS5 pi = ProxyInfo( proxy_type=proxy_type, proxy_host=url.hostname, diff --git a/shotgun_api3/lib/httplib2/auth.py b/shotgun_api3/lib/httplib2/auth.py index fd75e61bd..6b45fc484 100644 --- a/shotgun_api3/lib/httplib2/auth.py +++ b/shotgun_api3/lib/httplib2/auth.py @@ -1,9 +1,8 @@ -import base64 import re from .. import pyparsing as pp -from .error import * +from .error import MalformedHeader try: # pyparsing>=3.0.0 @@ -12,7 +11,7 @@ downcaseTokens = pp.downcaseTokens UNQUOTE_PAIRS = re.compile(r"\\(.)") -unquote = lambda s, l, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1]) +unquote = lambda s, _, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1]) # https://tools.ietf.org/html/rfc7235#section-1.2 # https://tools.ietf.org/html/rfc7235#appendix-B @@ -42,7 +41,7 @@ def _parse_authentication_info(headers, headername="authentication-info"): return {} try: parsed = authentication_info.parseString(header) - except pp.ParseException as ex: + except pp.ParseException: # print(ex.explain(ex)) raise MalformedHeader(headername) @@ -56,7 +55,7 @@ def _parse_www_authenticate(headers, headername="www-authenticate"): return {} try: parsed = www_authenticate.parseString(header) - except pp.ParseException as ex: + except pp.ParseException: # print(ex.explain(ex)) raise MalformedHeader(headername) diff --git a/shotgun_api3/lib/httplib2/socks.py b/shotgun_api3/lib/httplib2/socks.py deleted file mode 100644 index cc68e634c..000000000 --- a/shotgun_api3/lib/httplib2/socks.py +++ /dev/null @@ -1,518 +0,0 @@ -"""SocksiPy - Python SOCKS module. - -Version 1.00 - -Copyright 2006 Dan-Haim. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of Dan Haim nor the names of his contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. - -This module provides a standard socket-like interface for Python -for tunneling connections through SOCKS proxies. - -Minor modifications made by Christopher Gilbert (http://motomastyle.com/) for -use in PyLoris (http://pyloris.sourceforge.net/). - -Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) -mainly to merge bug fixes found in Sourceforge. -""" - -import base64 -import socket -import struct -import sys - -if getattr(socket, "socket", None) is None: - raise ImportError("socket.socket missing, proxy support unusable") - -PROXY_TYPE_SOCKS4 = 1 -PROXY_TYPE_SOCKS5 = 2 -PROXY_TYPE_HTTP = 3 -PROXY_TYPE_HTTP_NO_TUNNEL = 4 - -_defaultproxy = None -_orgsocket = socket.socket - - -class ProxyError(Exception): - pass - - -class GeneralProxyError(ProxyError): - pass - - -class Socks5AuthError(ProxyError): - pass - - -class Socks5Error(ProxyError): - pass - - -class Socks4Error(ProxyError): - pass - - -class HTTPError(ProxyError): - pass - - -_generalerrors = ( - "success", - "invalid data", - "not connected", - "not available", - "bad proxy type", - "bad input", -) - -_socks5errors = ( - "succeeded", - "general SOCKS server failure", - "connection not allowed by ruleset", - "Network unreachable", - "Host unreachable", - "Connection refused", - "TTL expired", - "Command not supported", - "Address type not supported", - "Unknown error", -) - -_socks5autherrors = ( - "succeeded", - "authentication is required", - "all offered authentication methods were rejected", - "unknown username or invalid password", - "unknown error", -) - -_socks4errors = ( - "request granted", - "request rejected or failed", - "request rejected because SOCKS server cannot connect to identd on the client", - "request rejected because the client program and identd report different " - "user-ids", - "unknown error", -) - - -def setdefaultproxy( - proxytype=None, addr=None, port=None, rdns=True, username=None, password=None -): - """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets a default proxy which all further socksocket objects will use, - unless explicitly changed. - """ - global _defaultproxy - _defaultproxy = (proxytype, addr, port, rdns, username, password) - - -def wrapmodule(module): - """wrapmodule(module) - - Attempts to replace a module's socket library with a SOCKS socket. Must set - a default proxy using setdefaultproxy(...) first. - This will only work on modules that import socket directly into the - namespace; - most of the Python Standard Library falls into this category. - """ - if _defaultproxy != None: - module.socket.socket = socksocket - else: - raise GeneralProxyError((4, "no proxy specified")) - - -class socksocket(socket.socket): - """socksocket([family[, type[, proto]]]) -> socket object - Open a SOCKS enabled socket. The parameters are the same as - those of the standard socket init. In order for SOCKS to work, - you must specify family=AF_INET, type=SOCK_STREAM and proto=0. - """ - - def __init__( - self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None - ): - _orgsocket.__init__(self, family, type, proto, _sock) - if _defaultproxy != None: - self.__proxy = _defaultproxy - else: - self.__proxy = (None, None, None, None, None, None) - self.__proxysockname = None - self.__proxypeername = None - self.__httptunnel = True - - def __recvall(self, count): - """__recvall(count) -> data - Receive EXACTLY the number of bytes requested from the socket. - Blocks until the required number of bytes have been received. - """ - data = self.recv(count) - while len(data) < count: - d = self.recv(count - len(data)) - if not d: - raise GeneralProxyError((0, "connection closed unexpectedly")) - data = data + d - return data - - def sendall(self, content, *args): - """ override socket.socket.sendall method to rewrite the header - for non-tunneling proxies if needed - """ - if not self.__httptunnel: - content = self.__rewriteproxy(content) - return super(socksocket, self).sendall(content, *args) - - def __rewriteproxy(self, header): - """ rewrite HTTP request headers to support non-tunneling proxies - (i.e. those which do not support the CONNECT method). - This only works for HTTP (not HTTPS) since HTTPS requires tunneling. - """ - host, endpt = None, None - hdrs = header.split("\r\n") - for hdr in hdrs: - if hdr.lower().startswith("host:"): - host = hdr - elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): - endpt = hdr - if host and endpt: - hdrs.remove(host) - hdrs.remove(endpt) - host = host.split(" ")[1] - endpt = endpt.split(" ") - if self.__proxy[4] != None and self.__proxy[5] != None: - hdrs.insert(0, self.__getauthheader()) - hdrs.insert(0, "Host: %s" % host) - hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) - return "\r\n".join(hdrs) - - def __getauthheader(self): - auth = self.__proxy[4] + b":" + self.__proxy[5] - return "Proxy-Authorization: Basic " + base64.b64encode(auth).decode() - - def setproxy( - self, - proxytype=None, - addr=None, - port=None, - rdns=True, - username=None, - password=None, - headers=None, - ): - """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - - Sets the proxy to be used. - proxytype - The type of the proxy to be used. Three types - are supported: PROXY_TYPE_SOCKS4 (including socks4a), - PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP - addr - The address of the server (IP or DNS). - port - The port of the server. Defaults to 1080 for SOCKS - servers and 8080 for HTTP proxy servers. - rdns - Should DNS queries be preformed on the remote side - (rather than the local side). The default is True. - Note: This has no effect with SOCKS4 servers. - username - Username to authenticate with to the server. - The default is no authentication. - password - Password to authenticate with to the server. - Only relevant when username is also provided. - headers - Additional or modified headers for the proxy connect - request. - """ - self.__proxy = ( - proxytype, - addr, - port, - rdns, - username.encode() if username else None, - password.encode() if password else None, - headers, - ) - - def __negotiatesocks5(self, destaddr, destport): - """__negotiatesocks5(self,destaddr,destport) - Negotiates a connection through a SOCKS5 server. - """ - # First we'll send the authentication packages we support. - if (self.__proxy[4] != None) and (self.__proxy[5] != None): - # The username/password details were supplied to the - # setproxy method so we support the USERNAME/PASSWORD - # authentication (in addition to the standard none). - self.sendall(struct.pack("BBBB", 0x05, 0x02, 0x00, 0x02)) - else: - # No username/password were entered, therefore we - # only support connections with no authentication. - self.sendall(struct.pack("BBB", 0x05, 0x01, 0x00)) - # We'll receive the server's response to determine which - # method was selected - chosenauth = self.__recvall(2) - if chosenauth[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - # Check the chosen authentication method - if chosenauth[1:2] == chr(0x00).encode(): - # No authentication is required - pass - elif chosenauth[1:2] == chr(0x02).encode(): - # Okay, we need to perform a basic username/password - # authentication. - packet = bytearray() - packet.append(0x01) - packet.append(len(self.__proxy[4])) - packet.extend(self.__proxy[4]) - packet.append(len(self.__proxy[5])) - packet.extend(self.__proxy[5]) - self.sendall(packet) - authstat = self.__recvall(2) - if authstat[0:1] != chr(0x01).encode(): - # Bad response - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if authstat[1:2] != chr(0x00).encode(): - # Authentication failed - self.close() - raise Socks5AuthError((3, _socks5autherrors[3])) - # Authentication succeeded - else: - # Reaching here is always bad - self.close() - if chosenauth[1] == chr(0xFF).encode(): - raise Socks5AuthError((2, _socks5autherrors[2])) - else: - raise GeneralProxyError((1, _generalerrors[1])) - # Now we can request the actual connection - req = struct.pack("BBB", 0x05, 0x01, 0x00) - # If the given destination address is an IP address, we'll - # use the IPv4 address request even if remote resolving was specified. - try: - ipaddr = socket.inet_aton(destaddr) - req = req + chr(0x01).encode() + ipaddr - except socket.error: - # Well it's not an IP number, so it's probably a DNS name. - if self.__proxy[3]: - # Resolve remotely - ipaddr = None - req = ( - req - + chr(0x03).encode() - + chr(len(destaddr)).encode() - + destaddr.encode() - ) - else: - # Resolve locally - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - req = req + chr(0x01).encode() + ipaddr - req = req + struct.pack(">H", destport) - self.sendall(req) - # Get the response - resp = self.__recvall(4) - if resp[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - elif resp[1:2] != chr(0x00).encode(): - # Connection failed - self.close() - if ord(resp[1:2]) <= 8: - raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) - else: - raise Socks5Error((9, _socks5errors[9])) - # Get the bound address/port - elif resp[3:4] == chr(0x01).encode(): - boundaddr = self.__recvall(4) - elif resp[3:4] == chr(0x03).encode(): - resp = resp + self.recv(1) - boundaddr = self.__recvall(ord(resp[4:5])) - else: - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - boundport = struct.unpack(">H", self.__recvall(2))[0] - self.__proxysockname = (boundaddr, boundport) - if ipaddr != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def getproxysockname(self): - """getsockname() -> address info - Returns the bound IP address and port number at the proxy. - """ - return self.__proxysockname - - def getproxypeername(self): - """getproxypeername() -> address info - Returns the IP and port number of the proxy. - """ - return _orgsocket.getpeername(self) - - def getpeername(self): - """getpeername() -> address info - Returns the IP address and port number of the destination - machine (note: getproxypeername returns the proxy) - """ - return self.__proxypeername - - def __negotiatesocks4(self, destaddr, destport): - """__negotiatesocks4(self,destaddr,destport) - Negotiates a connection through a SOCKS4 server. - """ - # Check if the destination address provided is an IP address - rmtrslv = False - try: - ipaddr = socket.inet_aton(destaddr) - except socket.error: - # It's a DNS name. Check where it should be resolved. - if self.__proxy[3]: - ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) - rmtrslv = True - else: - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - # Construct the request packet - req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr - # The username parameter is considered userid for SOCKS4 - if self.__proxy[4] != None: - req = req + self.__proxy[4] - req = req + chr(0x00).encode() - # DNS name if remote resolving is required - # NOTE: This is actually an extension to the SOCKS4 protocol - # called SOCKS4A and may not be supported in all cases. - if rmtrslv: - req = req + destaddr + chr(0x00).encode() - self.sendall(req) - # Get the response from the server - resp = self.__recvall(8) - if resp[0:1] != chr(0x00).encode(): - # Bad data - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if resp[1:2] != chr(0x5A).encode(): - # Server returned an error - self.close() - if ord(resp[1:2]) in (91, 92, 93): - self.close() - raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) - else: - raise Socks4Error((94, _socks4errors[4])) - # Get the bound address/port - self.__proxysockname = ( - socket.inet_ntoa(resp[4:]), - struct.unpack(">H", resp[2:4])[0], - ) - if rmtrslv != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def __negotiatehttp(self, destaddr, destport): - """__negotiatehttp(self,destaddr,destport) - Negotiates a connection through an HTTP server. - """ - # If we need to resolve locally, we do this now - if not self.__proxy[3]: - addr = socket.gethostbyname(destaddr) - else: - addr = destaddr - headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] - wrote_host_header = False - wrote_auth_header = False - if self.__proxy[6] != None: - for key, val in self.__proxy[6].iteritems(): - headers += [key, ": ", val, "\r\n"] - wrote_host_header = key.lower() == "host" - wrote_auth_header = key.lower() == "proxy-authorization" - if not wrote_host_header: - headers += ["Host: ", destaddr, "\r\n"] - if not wrote_auth_header: - if self.__proxy[4] != None and self.__proxy[5] != None: - headers += [self.__getauthheader(), "\r\n"] - headers.append("\r\n") - self.sendall("".join(headers).encode()) - # We read the response until we get the string "\r\n\r\n" - resp = self.recv(1) - while resp.find("\r\n\r\n".encode()) == -1: - resp = resp + self.recv(1) - # We just need the first line to check if the connection - # was successful - statusline = resp.splitlines()[0].split(" ".encode(), 2) - if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - try: - statuscode = int(statusline[1]) - except ValueError: - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if statuscode != 200: - self.close() - raise HTTPError((statuscode, statusline[2])) - self.__proxysockname = ("0.0.0.0", 0) - self.__proxypeername = (addr, destport) - - def connect(self, destpair): - """connect(self, despair) - Connects to the specified destination through a proxy. - destpar - A tuple of the IP/DNS address and the port number. - (identical to socket's connect). - To select the proxy server use setproxy(). - """ - # Do a minimal input check first - if ( - (not type(destpair) in (list, tuple)) - or (len(destpair) < 2) - or (not isinstance(destpair[0], (str, bytes))) - or (type(destpair[1]) != int) - ): - raise GeneralProxyError((5, _generalerrors[5])) - if self.__proxy[0] == PROXY_TYPE_SOCKS5: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - self.__negotiatesocks5(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_SOCKS4: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - self.__negotiatesocks4(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_HTTP: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 8080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - self.__negotiatehttp(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 8080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - if destpair[1] == 443: - self.__negotiatehttp(destpair[0], destpair[1]) - else: - self.__httptunnel = False - elif self.__proxy[0] == None: - _orgsocket.connect(self, (destpair[0], destpair[1])) - else: - raise GeneralProxyError((4, _generalerrors[4])) diff --git a/update_httplib2.py b/update_httplib2.py index 7124860e6..8e114082a 100755 --- a/update_httplib2.py +++ b/update_httplib2.py @@ -93,7 +93,7 @@ def main(temp_path, repo_root, version): # Copies a new version into place. print("Copying new version of httplib2") root_folder = unzipped_folder / f"httplib2-{version[1:]}" - utilities.copy_folder(str(root_folder / "python3" / "httplib2"), httplib2_dir) + utilities.copy_folder(str(root_folder / "httplib2"), httplib2_dir) utilities.remove_folder(f"{httplib2_dir}/test") # Patches the httplib2 imports so they are relative instead of absolute. From 70552ae11206cd0fa148f3f8fd5acf5730b7275e Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 18 Dec 2025 15:52:53 -0800 Subject: [PATCH 08/10] no more logs --- tests/test_api.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 1ac95d5e9..70ccfb79d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -37,17 +37,17 @@ import logging -root = logging.getLogger() -root.setLevel(logging.DEBUG) +# root = logging.getLogger() +# root.setLevel(logging.DEBUG) -handler = logging.StreamHandler(sys.stdout) -handler.setLevel(logging.DEBUG) -formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") -handler.setFormatter(formatter) -root.addHandler(handler) +# handler = logging.StreamHandler(sys.stdout) +# handler.setLevel(logging.DEBUG) +# formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +# handler.setFormatter(formatter) +# root.addHandler(handler) -logger = logging.getLogger("shotgun_api3") -logger.setLevel(logging.DEBUG) +# logger = logging.getLogger("shotgun_api3") +# logger.setLevel(logging.DEBUG) class TestShotgunApi(base.LiveTestBase): From 3411426ca5e694dfa61a853987d1b942fb320a68 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 18 Dec 2025 15:53:20 -0800 Subject: [PATCH 09/10] Revert "Try updating httplib2 ?!?!" This reverts commit 397a06e45e12874bd6fe6845164f111c1be45602. --- shotgun_api3/lib/httplib2/__init__.py | 13 +- shotgun_api3/lib/httplib2/auth.py | 9 +- shotgun_api3/lib/httplib2/socks.py | 518 ++++++++++++++++++++++++++ update_httplib2.py | 2 +- 4 files changed, 529 insertions(+), 13 deletions(-) create mode 100644 shotgun_api3/lib/httplib2/socks.py diff --git a/shotgun_api3/lib/httplib2/__init__.py b/shotgun_api3/lib/httplib2/__init__.py index e9350f684..ba5fa2f23 100644 --- a/shotgun_api3/lib/httplib2/__init__.py +++ b/shotgun_api3/lib/httplib2/__init__.py @@ -16,7 +16,7 @@ "Lai Han", ] __license__ = "MIT" -__version__ = "0.31.0" +__version__ = "0.22.0" import base64 import calendar @@ -47,7 +47,9 @@ try: import socks except ImportError: - socks = None + # TODO: remove this fallback and copypasted socksipy module upon py2/3 merge, + # idea is to have soft-dependency on any compatible module called socks + from . import socks from . import auth from .error import * from .iri2uri import iri2uri @@ -178,8 +180,7 @@ def _build_ssl_context( if hasattr(context, "check_hostname"): context.check_hostname = not disable_ssl_certificate_validation - if not disable_ssl_certificate_validation: - context.load_verify_locations(ca_certs) + context.load_verify_locations(ca_certs) if cert_file: context.load_cert_chain(cert_file, key_file, key_password) @@ -954,10 +955,6 @@ def proxy_info_from_url(url, method="http", noproxy=None): url = urllib.parse.urlparse(url) proxy_type = 3 # socks.PROXY_TYPE_HTTP - if url.scheme == "socks4": - proxy_type = 1 # socks.PROXY_TYPE_SOCKS4 - elif url.scheme == "socks5" or url.scheme == "socks": - proxy_type = 2 # socks.PROXY_TYPE_SOCKS5 pi = ProxyInfo( proxy_type=proxy_type, proxy_host=url.hostname, diff --git a/shotgun_api3/lib/httplib2/auth.py b/shotgun_api3/lib/httplib2/auth.py index 6b45fc484..fd75e61bd 100644 --- a/shotgun_api3/lib/httplib2/auth.py +++ b/shotgun_api3/lib/httplib2/auth.py @@ -1,8 +1,9 @@ +import base64 import re from .. import pyparsing as pp -from .error import MalformedHeader +from .error import * try: # pyparsing>=3.0.0 @@ -11,7 +12,7 @@ downcaseTokens = pp.downcaseTokens UNQUOTE_PAIRS = re.compile(r"\\(.)") -unquote = lambda s, _, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1]) +unquote = lambda s, l, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1]) # https://tools.ietf.org/html/rfc7235#section-1.2 # https://tools.ietf.org/html/rfc7235#appendix-B @@ -41,7 +42,7 @@ def _parse_authentication_info(headers, headername="authentication-info"): return {} try: parsed = authentication_info.parseString(header) - except pp.ParseException: + except pp.ParseException as ex: # print(ex.explain(ex)) raise MalformedHeader(headername) @@ -55,7 +56,7 @@ def _parse_www_authenticate(headers, headername="www-authenticate"): return {} try: parsed = www_authenticate.parseString(header) - except pp.ParseException: + except pp.ParseException as ex: # print(ex.explain(ex)) raise MalformedHeader(headername) diff --git a/shotgun_api3/lib/httplib2/socks.py b/shotgun_api3/lib/httplib2/socks.py new file mode 100644 index 000000000..cc68e634c --- /dev/null +++ b/shotgun_api3/lib/httplib2/socks.py @@ -0,0 +1,518 @@ +"""SocksiPy - Python SOCKS module. + +Version 1.00 + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) for +use in PyLoris (http://pyloris.sourceforge.net/). + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge. +""" + +import base64 +import socket +import struct +import sys + +if getattr(socket, "socket", None) is None: + raise ImportError("socket.socket missing, proxy support unusable") + +PROXY_TYPE_SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = 2 +PROXY_TYPE_HTTP = 3 +PROXY_TYPE_HTTP_NO_TUNNEL = 4 + +_defaultproxy = None +_orgsocket = socket.socket + + +class ProxyError(Exception): + pass + + +class GeneralProxyError(ProxyError): + pass + + +class Socks5AuthError(ProxyError): + pass + + +class Socks5Error(ProxyError): + pass + + +class Socks4Error(ProxyError): + pass + + +class HTTPError(ProxyError): + pass + + +_generalerrors = ( + "success", + "invalid data", + "not connected", + "not available", + "bad proxy type", + "bad input", +) + +_socks5errors = ( + "succeeded", + "general SOCKS server failure", + "connection not allowed by ruleset", + "Network unreachable", + "Host unreachable", + "Connection refused", + "TTL expired", + "Command not supported", + "Address type not supported", + "Unknown error", +) + +_socks5autherrors = ( + "succeeded", + "authentication is required", + "all offered authentication methods were rejected", + "unknown username or invalid password", + "unknown error", +) + +_socks4errors = ( + "request granted", + "request rejected or failed", + "request rejected because SOCKS server cannot connect to identd on the client", + "request rejected because the client program and identd report different " + "user-ids", + "unknown error", +) + + +def setdefaultproxy( + proxytype=None, addr=None, port=None, rdns=True, username=None, password=None +): + """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. + """ + global _defaultproxy + _defaultproxy = (proxytype, addr, port, rdns, username, password) + + +def wrapmodule(module): + """wrapmodule(module) + + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using setdefaultproxy(...) first. + This will only work on modules that import socket directly into the + namespace; + most of the Python Standard Library falls into this category. + """ + if _defaultproxy != None: + module.socket.socket = socksocket + else: + raise GeneralProxyError((4, "no proxy specified")) + + +class socksocket(socket.socket): + """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + """ + + def __init__( + self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None + ): + _orgsocket.__init__(self, family, type, proto, _sock) + if _defaultproxy != None: + self.__proxy = _defaultproxy + else: + self.__proxy = (None, None, None, None, None, None) + self.__proxysockname = None + self.__proxypeername = None + self.__httptunnel = True + + def __recvall(self, count): + """__recvall(count) -> data + Receive EXACTLY the number of bytes requested from the socket. + Blocks until the required number of bytes have been received. + """ + data = self.recv(count) + while len(data) < count: + d = self.recv(count - len(data)) + if not d: + raise GeneralProxyError((0, "connection closed unexpectedly")) + data = data + d + return data + + def sendall(self, content, *args): + """ override socket.socket.sendall method to rewrite the header + for non-tunneling proxies if needed + """ + if not self.__httptunnel: + content = self.__rewriteproxy(content) + return super(socksocket, self).sendall(content, *args) + + def __rewriteproxy(self, header): + """ rewrite HTTP request headers to support non-tunneling proxies + (i.e. those which do not support the CONNECT method). + This only works for HTTP (not HTTPS) since HTTPS requires tunneling. + """ + host, endpt = None, None + hdrs = header.split("\r\n") + for hdr in hdrs: + if hdr.lower().startswith("host:"): + host = hdr + elif hdr.lower().startswith("get") or hdr.lower().startswith("post"): + endpt = hdr + if host and endpt: + hdrs.remove(host) + hdrs.remove(endpt) + host = host.split(" ")[1] + endpt = endpt.split(" ") + if self.__proxy[4] != None and self.__proxy[5] != None: + hdrs.insert(0, self.__getauthheader()) + hdrs.insert(0, "Host: %s" % host) + hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2])) + return "\r\n".join(hdrs) + + def __getauthheader(self): + auth = self.__proxy[4] + b":" + self.__proxy[5] + return "Proxy-Authorization: Basic " + base64.b64encode(auth).decode() + + def setproxy( + self, + proxytype=None, + addr=None, + port=None, + rdns=True, + username=None, + password=None, + headers=None, + ): + """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) + + Sets the proxy to be used. + proxytype - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be preformed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + headers - Additional or modified headers for the proxy connect + request. + """ + self.__proxy = ( + proxytype, + addr, + port, + rdns, + username.encode() if username else None, + password.encode() if password else None, + headers, + ) + + def __negotiatesocks5(self, destaddr, destport): + """__negotiatesocks5(self,destaddr,destport) + Negotiates a connection through a SOCKS5 server. + """ + # First we'll send the authentication packages we support. + if (self.__proxy[4] != None) and (self.__proxy[5] != None): + # The username/password details were supplied to the + # setproxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + self.sendall(struct.pack("BBBB", 0x05, 0x02, 0x00, 0x02)) + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + self.sendall(struct.pack("BBB", 0x05, 0x01, 0x00)) + # We'll receive the server's response to determine which + # method was selected + chosenauth = self.__recvall(2) + if chosenauth[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + # Check the chosen authentication method + if chosenauth[1:2] == chr(0x00).encode(): + # No authentication is required + pass + elif chosenauth[1:2] == chr(0x02).encode(): + # Okay, we need to perform a basic username/password + # authentication. + packet = bytearray() + packet.append(0x01) + packet.append(len(self.__proxy[4])) + packet.extend(self.__proxy[4]) + packet.append(len(self.__proxy[5])) + packet.extend(self.__proxy[5]) + self.sendall(packet) + authstat = self.__recvall(2) + if authstat[0:1] != chr(0x01).encode(): + # Bad response + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if authstat[1:2] != chr(0x00).encode(): + # Authentication failed + self.close() + raise Socks5AuthError((3, _socks5autherrors[3])) + # Authentication succeeded + else: + # Reaching here is always bad + self.close() + if chosenauth[1] == chr(0xFF).encode(): + raise Socks5AuthError((2, _socks5autherrors[2])) + else: + raise GeneralProxyError((1, _generalerrors[1])) + # Now we can request the actual connection + req = struct.pack("BBB", 0x05, 0x01, 0x00) + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + ipaddr = socket.inet_aton(destaddr) + req = req + chr(0x01).encode() + ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. + if self.__proxy[3]: + # Resolve remotely + ipaddr = None + req = ( + req + + chr(0x03).encode() + + chr(len(destaddr)).encode() + + destaddr.encode() + ) + else: + # Resolve locally + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + req = req + chr(0x01).encode() + ipaddr + req = req + struct.pack(">H", destport) + self.sendall(req) + # Get the response + resp = self.__recvall(4) + if resp[0:1] != chr(0x05).encode(): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + elif resp[1:2] != chr(0x00).encode(): + # Connection failed + self.close() + if ord(resp[1:2]) <= 8: + raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) + else: + raise Socks5Error((9, _socks5errors[9])) + # Get the bound address/port + elif resp[3:4] == chr(0x01).encode(): + boundaddr = self.__recvall(4) + elif resp[3:4] == chr(0x03).encode(): + resp = resp + self.recv(1) + boundaddr = self.__recvall(ord(resp[4:5])) + else: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + boundport = struct.unpack(">H", self.__recvall(2))[0] + self.__proxysockname = (boundaddr, boundport) + if ipaddr != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def getproxysockname(self): + """getsockname() -> address info + Returns the bound IP address and port number at the proxy. + """ + return self.__proxysockname + + def getproxypeername(self): + """getproxypeername() -> address info + Returns the IP and port number of the proxy. + """ + return _orgsocket.getpeername(self) + + def getpeername(self): + """getpeername() -> address info + Returns the IP address and port number of the destination + machine (note: getproxypeername returns the proxy) + """ + return self.__proxypeername + + def __negotiatesocks4(self, destaddr, destport): + """__negotiatesocks4(self,destaddr,destport) + Negotiates a connection through a SOCKS4 server. + """ + # Check if the destination address provided is an IP address + rmtrslv = False + try: + ipaddr = socket.inet_aton(destaddr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if self.__proxy[3]: + ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) + rmtrslv = True + else: + ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) + # Construct the request packet + req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr + # The username parameter is considered userid for SOCKS4 + if self.__proxy[4] != None: + req = req + self.__proxy[4] + req = req + chr(0x00).encode() + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if rmtrslv: + req = req + destaddr + chr(0x00).encode() + self.sendall(req) + # Get the response from the server + resp = self.__recvall(8) + if resp[0:1] != chr(0x00).encode(): + # Bad data + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if resp[1:2] != chr(0x5A).encode(): + # Server returned an error + self.close() + if ord(resp[1:2]) in (91, 92, 93): + self.close() + raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + else: + raise Socks4Error((94, _socks4errors[4])) + # Get the bound address/port + self.__proxysockname = ( + socket.inet_ntoa(resp[4:]), + struct.unpack(">H", resp[2:4])[0], + ) + if rmtrslv != None: + self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + else: + self.__proxypeername = (destaddr, destport) + + def __negotiatehttp(self, destaddr, destport): + """__negotiatehttp(self,destaddr,destport) + Negotiates a connection through an HTTP server. + """ + # If we need to resolve locally, we do this now + if not self.__proxy[3]: + addr = socket.gethostbyname(destaddr) + else: + addr = destaddr + headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"] + wrote_host_header = False + wrote_auth_header = False + if self.__proxy[6] != None: + for key, val in self.__proxy[6].iteritems(): + headers += [key, ": ", val, "\r\n"] + wrote_host_header = key.lower() == "host" + wrote_auth_header = key.lower() == "proxy-authorization" + if not wrote_host_header: + headers += ["Host: ", destaddr, "\r\n"] + if not wrote_auth_header: + if self.__proxy[4] != None and self.__proxy[5] != None: + headers += [self.__getauthheader(), "\r\n"] + headers.append("\r\n") + self.sendall("".join(headers).encode()) + # We read the response until we get the string "\r\n\r\n" + resp = self.recv(1) + while resp.find("\r\n\r\n".encode()) == -1: + resp = resp + self.recv(1) + # We just need the first line to check if the connection + # was successful + statusline = resp.splitlines()[0].split(" ".encode(), 2) + if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + try: + statuscode = int(statusline[1]) + except ValueError: + self.close() + raise GeneralProxyError((1, _generalerrors[1])) + if statuscode != 200: + self.close() + raise HTTPError((statuscode, statusline[2])) + self.__proxysockname = ("0.0.0.0", 0) + self.__proxypeername = (addr, destport) + + def connect(self, destpair): + """connect(self, despair) + Connects to the specified destination through a proxy. + destpar - A tuple of the IP/DNS address and the port number. + (identical to socket's connect). + To select the proxy server use setproxy(). + """ + # Do a minimal input check first + if ( + (not type(destpair) in (list, tuple)) + or (len(destpair) < 2) + or (not isinstance(destpair[0], (str, bytes))) + or (type(destpair[1]) != int) + ): + raise GeneralProxyError((5, _generalerrors[5])) + if self.__proxy[0] == PROXY_TYPE_SOCKS5: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks5(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_SOCKS4: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 1080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatesocks4(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + self.__negotiatehttp(destpair[0], destpair[1]) + elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL: + if self.__proxy[2] != None: + portnum = self.__proxy[2] + else: + portnum = 8080 + _orgsocket.connect(self, (self.__proxy[1], portnum)) + if destpair[1] == 443: + self.__negotiatehttp(destpair[0], destpair[1]) + else: + self.__httptunnel = False + elif self.__proxy[0] == None: + _orgsocket.connect(self, (destpair[0], destpair[1])) + else: + raise GeneralProxyError((4, _generalerrors[4])) diff --git a/update_httplib2.py b/update_httplib2.py index 8e114082a..7124860e6 100755 --- a/update_httplib2.py +++ b/update_httplib2.py @@ -93,7 +93,7 @@ def main(temp_path, repo_root, version): # Copies a new version into place. print("Copying new version of httplib2") root_folder = unzipped_folder / f"httplib2-{version[1:]}" - utilities.copy_folder(str(root_folder / "httplib2"), httplib2_dir) + utilities.copy_folder(str(root_folder / "python3" / "httplib2"), httplib2_dir) utilities.remove_folder(f"{httplib2_dir}/test") # Patches the httplib2 imports so they are relative instead of absolute. From 64aa32afa58268a1ffe9bd9a607fcc5769c9cfca Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 18 Dec 2025 15:57:19 -0800 Subject: [PATCH 10/10] Tests --- azure-pipelines-templates/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-templates/run-tests.yml b/azure-pipelines-templates/run-tests.yml index 0345e0f3a..361c3194a 100644 --- a/azure-pipelines-templates/run-tests.yml +++ b/azure-pipelines-templates/run-tests.yml @@ -102,7 +102,7 @@ jobs: ## pytest --durations=0 -v --cov shotgun_api3 --cov-report xml --test-run-title="${{parameters.name}}-$(python.version)" - bash: | cp ./tests/example_config ./tests/config - python -m pytest -v ./tests/test_api.py -k test_info + pytest --durations=0 -v --cov shotgun_api3 --cov-report xml --test-run-title="${{parameters.name}}-$(python.version)" displayName: Running tests env: # Pass the values needed to authenticate with the Flow Production Tracking site and create some entities.