From 0ac2b0cfa69a11e036d009703b14267fdc66fc4d Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Mon, 30 Jun 2025 19:03:43 +0530 Subject: [PATCH 1/7] support optional `accept_insecure_certs` and `proxy` params --- py/selenium/webdriver/common/bidi/browser.py | 20 +++++++++-- py/selenium/webdriver/common/proxy.py | 36 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/py/selenium/webdriver/common/bidi/browser.py b/py/selenium/webdriver/common/bidi/browser.py index c66bad1d3d96c..4ef5780a006a6 100644 --- a/py/selenium/webdriver/common/bidi/browser.py +++ b/py/selenium/webdriver/common/bidi/browser.py @@ -16,7 +16,10 @@ # under the License. +from typing import Optional + from selenium.webdriver.common.bidi.common import command_builder +from selenium.webdriver.common.proxy import Proxy class ClientWindowState: @@ -182,14 +185,27 @@ class Browser: def __init__(self, conn): self.conn = conn - def create_user_context(self) -> str: + def create_user_context(self, accept_insecure_certs: Optional[bool] = None, proxy: Optional[Proxy] = None) -> str: """Creates a new user context. + Parameters: + ----------- + accept_insecure_certs: Optional flag to accept insecure TLS certificates + proxy: Optional proxy configuration for the user context + Returns: ------- str: The ID of the created user context. """ - result = self.conn.execute(command_builder("browser.createUserContext", {})) + params = {} + + if accept_insecure_certs is not None: + params["acceptInsecureCerts"] = accept_insecure_certs + + if proxy is not None: + params["proxy"] = proxy.to_bidi_dict() + + result = self.conn.execute(command_builder("browser.createUserContext", params)) return result["userContext"] def get_user_contexts(self) -> list[str]: diff --git a/py/selenium/webdriver/common/proxy.py b/py/selenium/webdriver/common/proxy.py index 187f00418a421..f14c201542abc 100644 --- a/py/selenium/webdriver/common/proxy.py +++ b/py/selenium/webdriver/common/proxy.py @@ -325,3 +325,39 @@ def to_capabilities(self): if attr_value: proxy_caps[proxy] = attr_value return proxy_caps + + def to_bidi_dict(self): + """Convert proxy settings to BiDi format. + + Returns: + ------- + dict: Proxy configuration in BiDi format. + """ + proxy_type = self.proxyType["string"].lower() + result = {"proxyType": proxy_type} + + if proxy_type == "manual": + if self.httpProxy: + result["httpProxy"] = self.httpProxy + if self.sslProxy: + result["sslProxy"] = self.sslProxy + if self.socksProxy: + result["socksProxy"] = self.socksProxy + if self.socksVersion is not None: + result["socksVersion"] = self.socksVersion + if self.noProxy: + # Convert comma-separated string to list + if isinstance(self.noProxy, str): + result["noProxy"] = [host.strip() for host in self.noProxy.split(",") if host.strip()] + elif isinstance(self.noProxy, list): + if not all(isinstance(h, str) for h in self.noProxy): + raise TypeError("no_proxy list must contain only strings") + result["noProxy"] = self.noProxy + else: + raise TypeError("no_proxy must be a comma-separated string or a list of strings") + + elif proxy_type == "pac": + if self.proxyAutoconfigUrl: + result["proxyAutoconfigUrl"] = self.proxyAutoconfigUrl + + return result From 9287f496f5bcabcdbe6a1fd0515b27b11b3842cb Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Mon, 30 Jun 2025 19:04:08 +0530 Subject: [PATCH 2/7] add tests for `accept_insecure_certs` and `proxy` params --- .../webdriver/common/bidi_browser_tests.py | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index 2fec55c1e4f48..4daa2c434e41d 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -15,9 +15,38 @@ # specific language governing permissions and limitations # under the License. +import http.server +import socket +import socketserver +import threading + import pytest from selenium.webdriver.common.bidi.browser import ClientWindowInfo, ClientWindowState +from selenium.webdriver.common.by import By +from selenium.webdriver.common.proxy import Proxy, ProxyType +from selenium.webdriver.common.window import WindowTypes + + +def get_free_port(): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) + return s.getsockname()[1] + + +class FakeProxyHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + print(f"[Fake Proxy] Intercepted request to: {self.path}") + self.send_response(200) + self.end_headers() + self.wfile.write(b"proxied response") + + +def start_fake_proxy(port): + server = socketserver.TCPServer(("localhost", port), FakeProxyHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + return server def test_browser_initialized(driver): @@ -95,3 +124,116 @@ def test_client_window_state_constants(driver): assert ClientWindowState.MAXIMIZED == "maximized" assert ClientWindowState.MINIMIZED == "minimized" assert ClientWindowState.NORMAL == "normal" + + +def test_create_user_context_with_accept_insecure_certs(driver): + """Test creating a user context with accept_insecure_certs parameter.""" + INSECURE_TEST_SITE = "https://self-signed.badssl.com/" + user_context = driver.browser.create_user_context(accept_insecure_certs=True) + + bc = driver.browsing_context.create(type=WindowTypes.WINDOW, user_context=user_context) + driver.switch_to.window(bc) + assert user_context is not None + assert bc is not None + + driver.get(INSECURE_TEST_SITE) + + h1 = driver.find_element(By.TAG_NAME, "h1") + assert h1.text.strip() == "self-signed.\nbadssl.com" + + # Clean up + driver.browser.remove_user_context(user_context) + + +def test_create_user_context_with_direct_proxy(driver): + """Test creating a user context with direct proxy configuration.""" + proxy = Proxy() + proxy.proxy_type = ProxyType.DIRECT + + user_context = driver.browser.create_user_context(proxy=proxy) + assert user_context is not None + + bc = driver.browsing_context.create(type=WindowTypes.WINDOW, user_context=user_context) + driver.switch_to.window(bc) + + # Visiting a site should load directly without proxy + driver.get("http://example.com/") + body_text = driver.find_element(By.TAG_NAME, "body").text.lower() + assert "example domain" in body_text + + # Clean up + driver.browser.remove_user_context(user_context) + + +@pytest.mark.xfail_firefox(reason="Firefox proxy settings are different") +def test_create_user_context_with_manual_proxy_all_params(driver): + """Test creating a user context with manual proxy configuration.""" + # Start a fake proxy server + port = get_free_port() + fake_proxy_server = start_fake_proxy(port=port) + + proxy = Proxy() + proxy.proxy_type = ProxyType.MANUAL + proxy.http_proxy = f"localhost:{port}" + proxy.ssl_proxy = f"localhost:{port}" + proxy.socks_proxy = f"localhost:{port}" + proxy.socks_version = 5 + proxy.no_proxy = ["the-internet.herokuapp.com"] + + user_context = driver.browser.create_user_context(proxy=proxy) + + # Create and switch to a new browsing context using this proxy + bc = driver.browsing_context.create(type=WindowTypes.WINDOW, user_context=user_context) + driver.switch_to.window(bc) + + try: + # Visit no proxy site, it should bypass proxy + driver.get("http://the-internet.herokuapp.com/") + body_text = driver.find_element(By.TAG_NAME, "body").text.lower() + assert "welcome to the-internet" in body_text + + # Visit a site that should be proxied + driver.get("http://example.com/") + + body_text = driver.find_element("tag name", "body").text + assert "proxied response" in body_text.lower() + + finally: + driver.browser.remove_user_context(user_context) + fake_proxy_server.shutdown() + fake_proxy_server.server_close() + + +@pytest.mark.xfail_firefox(reason="Firefox proxy settings are different") +def test_create_user_context_with_both_params(driver): + """Test creating a user context with both acceptInsecureCerts and proxy parameters.""" + # Start fake proxy server + port = get_free_port() + fake_proxy_server = start_fake_proxy(port=port) + + proxy = Proxy() + proxy.proxy_type = ProxyType.MANUAL + proxy.http_proxy = f"localhost:{port}" + proxy.ssl_proxy = f"localhost:{port}" + proxy.no_proxy = ["self-signed.badssl.com"] + + user_context = driver.browser.create_user_context(accept_insecure_certs=True, proxy=proxy) + + bc = driver.browsing_context.create(type=WindowTypes.WINDOW, user_context=user_context) + driver.switch_to.window(bc) + + try: + # Visit a site with an invalid certificate + driver.get("https://self-signed.badssl.com/") + h1 = driver.find_element(By.TAG_NAME, "h1") + assert "badssl.com" in h1.text.lower() + + # Visit a site that should go through the fake proxy + driver.get("http://example.com/") + body_text = driver.find_element(By.TAG_NAME, "body").text + assert "proxied response" in body_text.lower() + + finally: + driver.browser.remove_user_context(user_context) + fake_proxy_server.shutdown() + fake_proxy_server.server_close() From 291061af7b6801dc6cfd9c3799fb90e6904d554a Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Tue, 1 Jul 2025 08:27:04 +0530 Subject: [PATCH 3/7] add xfail for remote --- py/test/selenium/webdriver/common/bidi_browser_tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index 4daa2c434e41d..d9d527e39372c 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -166,6 +166,7 @@ def test_create_user_context_with_direct_proxy(driver): @pytest.mark.xfail_firefox(reason="Firefox proxy settings are different") +@pytest.mark.xfail_remote def test_create_user_context_with_manual_proxy_all_params(driver): """Test creating a user context with manual proxy configuration.""" # Start a fake proxy server @@ -205,6 +206,7 @@ def test_create_user_context_with_manual_proxy_all_params(driver): @pytest.mark.xfail_firefox(reason="Firefox proxy settings are different") +@pytest.mark.xfail_remote def test_create_user_context_with_both_params(driver): """Test creating a user context with both acceptInsecureCerts and proxy parameters.""" # Start fake proxy server From 345c1bef391b1a0ef41d53d54516d908b8bcf183 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Thu, 10 Jul 2025 23:37:05 +0530 Subject: [PATCH 4/7] use `free_port` method from utils --- .../selenium/webdriver/common/bidi_browser_tests.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index d9d527e39372c..399dfe42439b3 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -16,7 +16,6 @@ # under the License. import http.server -import socket import socketserver import threading @@ -25,15 +24,10 @@ from selenium.webdriver.common.bidi.browser import ClientWindowInfo, ClientWindowState from selenium.webdriver.common.by import By from selenium.webdriver.common.proxy import Proxy, ProxyType +from selenium.webdriver.common.utils import free_port from selenium.webdriver.common.window import WindowTypes -def get_free_port(): - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("", 0)) - return s.getsockname()[1] - - class FakeProxyHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): print(f"[Fake Proxy] Intercepted request to: {self.path}") @@ -170,7 +164,7 @@ def test_create_user_context_with_direct_proxy(driver): def test_create_user_context_with_manual_proxy_all_params(driver): """Test creating a user context with manual proxy configuration.""" # Start a fake proxy server - port = get_free_port() + port = free_port() fake_proxy_server = start_fake_proxy(port=port) proxy = Proxy() @@ -210,7 +204,7 @@ def test_create_user_context_with_manual_proxy_all_params(driver): def test_create_user_context_with_both_params(driver): """Test creating a user context with both acceptInsecureCerts and proxy parameters.""" # Start fake proxy server - port = get_free_port() + port = free_port() fake_proxy_server = start_fake_proxy(port=port) proxy = Proxy() From 811ad19543873f270b8230331cba73bdcf5f8df9 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 11 Jul 2025 15:32:19 +0530 Subject: [PATCH 5/7] test extended proxy --- .../webdriver/common/bidi_browser_tests.py | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index 399dfe42439b3..e16eef606ff86 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -16,6 +16,7 @@ # under the License. import http.server +import socket import socketserver import threading @@ -28,16 +29,65 @@ from selenium.webdriver.common.window import WindowTypes -class FakeProxyHandler(http.server.SimpleHTTPRequestHandler): +class FakeProxyHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): - print(f"[Fake Proxy] Intercepted request to: {self.path}") + print(f"[Fake Proxy] Intercepted GET request to: {self.path}") self.send_response(200) + self.send_header("Content-Type", "text/html") self.end_headers() - self.wfile.write(b"proxied response") + self.wfile.write(b"proxied response") + + def do_POST(self): + print(f"[Fake Proxy] Intercepted POST request to: {self.path}") + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.wfile.write(b"proxied response") + + def do_CONNECT(self): + """Handle CONNECT requests for HTTPS tunneling.""" + print(f"[Fake Proxy] Intercepted CONNECT request to: {self.path}") + + self.send_response(200, "Connection established") + self.end_headers() + + try: + request_data = b"" + while True: + try: + chunk = self.rfile.read(1024) + if not chunk: + break + request_data += chunk + if b"\r\n\r\n" in request_data: + break + except socket.timeout: + break + + if request_data: + print(f"[Fake Proxy] Received tunneled request: {request_data[:100]}...") + response = ( + b"HTTP/1.1 200 OK\r\n" + b"Content-Type: text/html\r\n" + b"Content-Length: 37\r\n" + b"Connection: close\r\n" + b"\r\n" + b"proxied response" + ) + self.wfile.write(response) + self.wfile.flush() + except Exception as e: + print(f"[Fake Proxy] Error handling CONNECT: {e}") + finally: + self.wfile.close() + + def log_message(self, format, *args): + pass def start_fake_proxy(port): server = socketserver.TCPServer(("localhost", port), FakeProxyHandler) + server.timeout = 5 thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() return server From cd537252b33a6a6a16228d90a419c6152affc53e Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 11 Jul 2025 17:48:14 +0530 Subject: [PATCH 6/7] Revert "test extended proxy" This reverts commit 811ad19543873f270b8230331cba73bdcf5f8df9. --- .../webdriver/common/bidi_browser_tests.py | 56 +------------------ 1 file changed, 3 insertions(+), 53 deletions(-) diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index e16eef606ff86..399dfe42439b3 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -16,7 +16,6 @@ # under the License. import http.server -import socket import socketserver import threading @@ -29,65 +28,16 @@ from selenium.webdriver.common.window import WindowTypes -class FakeProxyHandler(http.server.BaseHTTPRequestHandler): +class FakeProxyHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): - print(f"[Fake Proxy] Intercepted GET request to: {self.path}") + print(f"[Fake Proxy] Intercepted request to: {self.path}") self.send_response(200) - self.send_header("Content-Type", "text/html") self.end_headers() - self.wfile.write(b"proxied response") - - def do_POST(self): - print(f"[Fake Proxy] Intercepted POST request to: {self.path}") - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.end_headers() - self.wfile.write(b"proxied response") - - def do_CONNECT(self): - """Handle CONNECT requests for HTTPS tunneling.""" - print(f"[Fake Proxy] Intercepted CONNECT request to: {self.path}") - - self.send_response(200, "Connection established") - self.end_headers() - - try: - request_data = b"" - while True: - try: - chunk = self.rfile.read(1024) - if not chunk: - break - request_data += chunk - if b"\r\n\r\n" in request_data: - break - except socket.timeout: - break - - if request_data: - print(f"[Fake Proxy] Received tunneled request: {request_data[:100]}...") - response = ( - b"HTTP/1.1 200 OK\r\n" - b"Content-Type: text/html\r\n" - b"Content-Length: 37\r\n" - b"Connection: close\r\n" - b"\r\n" - b"proxied response" - ) - self.wfile.write(response) - self.wfile.flush() - except Exception as e: - print(f"[Fake Proxy] Error handling CONNECT: {e}") - finally: - self.wfile.close() - - def log_message(self, format, *args): - pass + self.wfile.write(b"proxied response") def start_fake_proxy(port): server = socketserver.TCPServer(("localhost", port), FakeProxyHandler) - server.timeout = 5 thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() return server From c2fa49690253d415b9449b87de4871704a2745a1 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Fri, 11 Jul 2025 18:06:21 +0530 Subject: [PATCH 7/7] xfail chrome proxy tests --- py/test/selenium/webdriver/common/bidi_browser_tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index 399dfe42439b3..907bb126f9bad 100644 --- a/py/test/selenium/webdriver/common/bidi_browser_tests.py +++ b/py/test/selenium/webdriver/common/bidi_browser_tests.py @@ -159,6 +159,7 @@ def test_create_user_context_with_direct_proxy(driver): driver.browser.remove_user_context(user_context) +@pytest.mark.xfail_chrome(reason="Chrome auto upgrades HTTP to HTTPS in untrusted networks like CI environments") @pytest.mark.xfail_firefox(reason="Firefox proxy settings are different") @pytest.mark.xfail_remote def test_create_user_context_with_manual_proxy_all_params(driver): @@ -199,9 +200,10 @@ def test_create_user_context_with_manual_proxy_all_params(driver): fake_proxy_server.server_close() +@pytest.mark.xfail_chrome(reason="Chrome auto upgrades HTTP to HTTPS in untrusted networks like CI environments") @pytest.mark.xfail_firefox(reason="Firefox proxy settings are different") @pytest.mark.xfail_remote -def test_create_user_context_with_both_params(driver): +def test_create_user_context_with_proxy_and_accept_insecure_certs(driver): """Test creating a user context with both acceptInsecureCerts and proxy parameters.""" # Start fake proxy server port = free_port()