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 diff --git a/py/test/selenium/webdriver/common/bidi_browser_tests.py b/py/test/selenium/webdriver/common/bidi_browser_tests.py index 2fec55c1e4f48..d9d527e39372c 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,118 @@ 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") +@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 + 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") +@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 + 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()