Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
97 changes: 96 additions & 1 deletion GSRequestTest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import unittest
from GSSDK import GSRequest
import ssl
import socket
from unittest.mock import MagicMock, patch
from GSSDK import GSRequest, ValidHTTPSConnection, SSLUtils


class TestResolveMtlsDomain(unittest.TestCase):
Expand Down Expand Up @@ -117,5 +120,97 @@ def test_regular_request_uses_namespace_domain(self):
self.assertEqual(request._domain, "accounts.eu1.gigya.com")


class TestValidHTTPSConnectionSNI(unittest.TestCase):
"""
Tests for the SNI fix in ValidHTTPSConnection.connect().

When routing through an HTTPS proxy (CONNECT tunnel), urllib sets:
self.host = proxy address
self._tunnel_host = real destination hostname

The fix ensures the real destination is used as SNI, not the proxy.
"""

def _make_connection(self, host, tunnel_host=None):
ctx = SSLUtils.createSSLContext(False, None, None)
conn = ValidHTTPSConnection(host, context=ctx)
conn._tunnel_host = tunnel_host
conn.timeout = 5
conn.source_address = None
return conn

def _mock_socket(self):
sock = MagicMock()
sock.setsockopt = MagicMock()
return sock

def test_sni_uses_tunnel_host_when_proxy_is_set(self):
"""When going through a proxy, SNI must be the destination host, not the proxy."""
conn = self._make_connection("127.0.0.1", tunnel_host="accounts.us1.gigya.com")
mock_sock = self._mock_socket()
captured_sni = []

def fake_wrap(sock, server_hostname=None):
captured_sni.append(server_hostname)
raise ConnectionAbortedError("stop after SNI capture")

conn._context.wrap_socket = fake_wrap

with patch("socket.create_connection", return_value=mock_sock):
with patch.object(conn, "_tunnel"):
try:
conn.connect()
except ConnectionAbortedError:
pass

self.assertEqual(captured_sni, ["accounts.us1.gigya.com"],
"SNI must be the tunnel destination, not the proxy address")

def test_sni_uses_host_when_no_proxy(self):
"""Without a proxy, SNI must be self.host (direct connection, no tunnel)."""
conn = self._make_connection("accounts.us1.gigya.com", tunnel_host=None)
mock_sock = self._mock_socket()
captured_sni = []

def fake_wrap(sock, server_hostname=None):
captured_sni.append(server_hostname)
raise ConnectionAbortedError("stop after SNI capture")

conn._context.wrap_socket = fake_wrap

with patch("socket.create_connection", return_value=mock_sock):
try:
conn.connect()
except ConnectionAbortedError:
pass

self.assertEqual(captured_sni, ["accounts.us1.gigya.com"],
"SNI must be self.host for direct connections")

def test_sni_not_proxy_ip(self):
"""Regression: the proxy IP must never appear as SNI."""
conn = self._make_connection("10.0.0.1", tunnel_host="accounts.eu1.gigya.com")
mock_sock = self._mock_socket()
captured_sni = []

def fake_wrap(sock, server_hostname=None):
captured_sni.append(server_hostname)
raise ConnectionAbortedError("stop after SNI capture")

conn._context.wrap_socket = fake_wrap

with patch("socket.create_connection", return_value=mock_sock):
with patch.object(conn, "_tunnel"):
try:
conn.connect()
except ConnectionAbortedError:
pass

self.assertNotEqual(captured_sni[0], "10.0.0.1",
"Proxy IP must never be sent as SNI")
self.assertEqual(captured_sni[0], "accounts.eu1.gigya.com",
"SNI must be the real destination hostname")


if __name__ == '__main__':
unittest.main()
3 changes: 2 additions & 1 deletion GSSDK.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,8 @@ def connect(self):
self.sock = sock
self._tunnel()

self.sock = self._context.wrap_socket(sock, server_hostname=self.host)
server_hostname = self._tunnel_host if self._tunnel_host else self.host
self.sock = self._context.wrap_socket(sock, server_hostname=server_hostname)


class ValidHTTPSHandler(HTTPSHandler):
Expand Down