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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ where X.Y.Z is the semver of the most recent choreographer release.

### Added
- Add `enable_extensions` option to control browser extension loading [[#303](https://github.com/plotly/choreographer/pull/303)], with thanks to @hirohira9119 for the contribution!
- Add `proxy_server` browser configuration with a `CHOREO_PROXY_SERVER` environment fallback [[#301](https://github.com/plotly/choreographer/issues/301)]

### Fixed
- Improve platform architecture detection for arm on Linux and Windows [[#290](https://github.com/plotly/choreographer/pull/290)], with thanks to @juliabeliaeva for the contribution!
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ not absolutely required. The synchronous functions in this package are intended
as building blocks for other asynchronous strategies that Python may favor over
async/await in the future.

## Proxy Configuration

Pass `proxy_server` when constructing a browser to route Chromium traffic
through a proxy:

```python
browser = await choreo.Browser(proxy_server="http://proxy.example:8080")
```

For callers such as Kaleido and Plotly that construct the browser internally,
set `CHOREO_PROXY_SERVER` before starting the Python process:

```shell
CHOREO_PROXY_SERVER=http://proxy.example:8080 python app.py
```

The value is passed directly to Chromium's `--proxy-server` command-line flag.
Avoid including credentials in the URL because command-line arguments may be
visible to other users on the system.

## Testing

### Process Control Tests
Expand Down
21 changes: 19 additions & 2 deletions src/choreographer/browsers/chromium.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class Chromium:
"""True if we should use the gpu. False by default for compatibility."""
headless: bool
"""True if we should not show the browser. True by default."""
proxy_server: str | None
"""Proxy server passed to Chromium, if configured."""
sandbox_enabled: bool
"""True to enable the sandbox. False by default."""
skip_local: bool
Expand Down Expand Up @@ -147,6 +149,8 @@ def __init__(
enable_extensions (default True): Enable extensions?
enable_gpu (default False): Turn on GPU? Doesn't work in all envs.
headless (default True): Run the browser in headless mode?
proxy_server (default None): Proxy server URL passed to Chromium.
Falls back to the `CHOREO_PROXY_SERVER` environment variable.
enable_sandbox (default False): Enable sandbox-
a persnickety thing depending on environment, OS, user, etc
tmp_dir (default None): Manually set the temporary directory
Expand All @@ -156,11 +160,18 @@ def __init__(
NotImplementedError: Pipe is the only channel type it'll accept right now.

"""
_logger.info(f"Chromium init'ed with kwargs {kwargs}")
log_kwargs = kwargs.copy()
if "proxy_server" in log_kwargs:
log_kwargs["proxy_server"] = "<redacted>"
_logger.info(f"Chromium init'ed with kwargs {log_kwargs}")
self.path = path
self.extensions_enabled = kwargs.pop("enable_extensions", True)
self.gpu_enabled = kwargs.pop("enable_gpu", False)
self.headless = kwargs.pop("headless", True)
self.proxy_server = kwargs.pop(
"proxy_server",
os.environ.get("CHOREO_PROXY_SERVER"),
)
self.sandbox_enabled = kwargs.pop("enable_sandbox", False)
self._tmp_dir_path = kwargs.pop("tmp_dir", None)
if kwargs:
Expand Down Expand Up @@ -247,6 +258,8 @@ def get_cli(self) -> Sequence[str]:
cli.append("--disable-gpu")
if self.headless:
cli.append("--headless")
if self.proxy_server:
cli.append(f"--proxy-server={self.proxy_server}")
if not self.sandbox_enabled:
cli.append("--no-sandbox")

Expand Down Expand Up @@ -291,7 +304,11 @@ def get_cli(self) -> Sequence[str]:
cli += [
f"--remote-debugging-io-pipes={r_handle!s},{w_handle!s}",
]
_logger.debug(f"Returning cli: {cli}")
log_cli = [
"--proxy-server=<redacted>" if arg.startswith("--proxy-server=") else arg
for arg in cli
]
_logger.debug(f"Returning cli: {log_cli}")
return cli

def get_env(self) -> MutableMapping[str, str]:
Expand Down
71 changes: 71 additions & 0 deletions tests/test_chromium.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

import logging

from choreographer.browsers.chromium import Chromium
from choreographer.channels import Pipe


def _get_cli(tmp_path, **kwargs) -> list[str]:
executable = tmp_path / "chrome"
executable.touch()
channel = Pipe()
browser = Chromium(channel, executable, **kwargs)
browser.pre_open()
try:
return list(browser.get_cli())
finally:
browser.clean()
channel.close()


def test_proxy_server_argument(tmp_path, monkeypatch):
monkeypatch.delenv("CHOREO_PROXY_SERVER", raising=False)

cli = _get_cli(tmp_path, proxy_server="http://proxy.example:8080")

assert "--proxy-server=http://proxy.example:8080" in cli


def test_proxy_server_environment_variable(tmp_path, monkeypatch):
monkeypatch.setenv("CHOREO_PROXY_SERVER", "socks5://proxy.example:1080")

cli = _get_cli(tmp_path)

assert "--proxy-server=socks5://proxy.example:1080" in cli


def test_proxy_server_argument_overrides_environment(tmp_path, monkeypatch):
monkeypatch.setenv("CHOREO_PROXY_SERVER", "http://environment.example:8080")

cli = _get_cli(tmp_path, proxy_server="http://argument.example:8080")

assert "--proxy-server=http://argument.example:8080" in cli
assert "--proxy-server=http://environment.example:8080" not in cli


def test_proxy_server_credentials_are_redacted_from_logs(tmp_path, caplog):
proxy_server = "http://user:secret@proxy.example:8080"
caplog.set_level(logging.DEBUG)

cli = _get_cli(tmp_path, proxy_server=proxy_server)

assert f"--proxy-server={proxy_server}" in cli
assert proxy_server not in caplog.text
assert "--proxy-server=<redacted>" in caplog.text


def test_proxy_server_is_not_added_by_default(tmp_path, monkeypatch):
monkeypatch.delenv("CHOREO_PROXY_SERVER", raising=False)

cli = _get_cli(tmp_path)

assert not any(arg.startswith("--proxy-server=") for arg in cli)


def test_none_proxy_server_disables_environment_fallback(tmp_path, monkeypatch):
monkeypatch.setenv("CHOREO_PROXY_SERVER", "http://environment.example:8080")

cli = _get_cli(tmp_path, proxy_server=None)

assert not any(arg.startswith("--proxy-server=") for arg in cli)