feat(serve): add remote browser control over TCP with token auth
Exposes a local browser over a TCP socket so remote machines can control it using the same CLI and Python API. Token auth (auto-generated via secrets.token_urlsafe) is on by default; --no-auth disables it. Profile routing via _route message field lets clients target specific browser instances on the remote host. BROWSER_CLI_PROFILE is forwarded automatically so --browser flag works transparently over remote. - browser-cli serve [--host] [--port] [--token] [--no-auth] - browser-cli --remote HOST:PORT --token TOKEN <command> - BrowserCLI(remote="host:port", token="...").tabs_list()
This commit is contained in:
+35
-35
@@ -122,7 +122,7 @@ class TestNavigation:
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.open",
|
||||
{"url": "https://example.com", "background": False, "window": None, "group": None},
|
||||
profile=None,
|
||||
profile=None, remote=None, token=None,
|
||||
)
|
||||
|
||||
def test_open_background(self, b, mock_send):
|
||||
@@ -136,33 +136,33 @@ class TestNavigation:
|
||||
|
||||
def test_reload(self, b, mock_send):
|
||||
b.reload(tab_id=5)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_hard_reload(self, b, mock_send):
|
||||
b.hard_reload(tab_id=7)
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None)
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_back(self, b, mock_send):
|
||||
b.back(tab_id=3)
|
||||
mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None)
|
||||
mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_forward(self, b, mock_send):
|
||||
b.forward(tab_id=3)
|
||||
mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None)
|
||||
mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_focus_url(self, b, mock_send):
|
||||
b.focus_url("github.com")
|
||||
mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None)
|
||||
mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_navigate_tab(self, b, mock_send):
|
||||
b.navigate_tab(5, "https://example.com")
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None
|
||||
"navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None, remote=None, token=None
|
||||
)
|
||||
|
||||
def test_profile_forwarded(self, b_profile, mock_send):
|
||||
b_profile.reload()
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave")
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, token=None)
|
||||
|
||||
|
||||
# ── Search ────────────────────────────────────────────────────────────────────
|
||||
@@ -195,12 +195,12 @@ class TestExtract:
|
||||
result = b.extract_markdown()
|
||||
|
||||
assert result == "# Title"
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None)
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_extract_markdown_selector(self, b, mock_send):
|
||||
b.extract_markdown("article")
|
||||
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None)
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, token=None)
|
||||
|
||||
|
||||
# ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||
@@ -235,7 +235,7 @@ class TestTabs:
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.close",
|
||||
{"tabId": 10, "inactive": False, "duplicates": False},
|
||||
profile=None,
|
||||
profile=None, remote=None, token=None,
|
||||
)
|
||||
|
||||
def test_tabs_move(self, b, mock_send):
|
||||
@@ -243,19 +243,19 @@ class TestTabs:
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.move",
|
||||
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
||||
profile=None,
|
||||
profile=None, remote=None, token=None,
|
||||
)
|
||||
|
||||
def test_tabs_active(self, b, mock_send):
|
||||
b.tabs_active(10)
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None)
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_window_active_tab(self, b, mock_send):
|
||||
mock_send.return_value = TAB_DATA
|
||||
tab = b.window_active_tab(1)
|
||||
assert isinstance(tab, Tab)
|
||||
assert tab.id == 10
|
||||
mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None)
|
||||
mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_window_active_tab_missing_raises(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
@@ -308,7 +308,7 @@ class TestTabs:
|
||||
assert mock_send.call_args_list == [
|
||||
call("tabs.list", {}, profile="default"),
|
||||
call("tabs.list", {}, profile="work"),
|
||||
call("tabs.close", {"tabId": 11}, profile="work"),
|
||||
call("tabs.close", {"tabId": 11}, profile="work", remote=None, token=None),
|
||||
]
|
||||
|
||||
def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||
@@ -351,7 +351,7 @@ class TestTabs:
|
||||
|
||||
def test_tabs_sort(self, b, mock_send):
|
||||
b.tabs_sort(by="title")
|
||||
mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None)
|
||||
mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_tabs_merge_windows(self, b, mock_send):
|
||||
mock_send.return_value = {"moved": 4}
|
||||
@@ -384,7 +384,7 @@ class TestGroups:
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
tabs = b.group_tabs(42)
|
||||
assert isinstance(tabs[0], Tab)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_group_count(self, b, mock_send):
|
||||
mock_send.return_value = 7
|
||||
@@ -412,7 +412,7 @@ class TestGroups:
|
||||
assert mock_send.call_args_list == [
|
||||
call("group.list", {}, profile="default"),
|
||||
call("group.list", {}, profile="work"),
|
||||
call("group.close", {"groupId": 99}, profile="work"),
|
||||
call("group.close", {"groupId": 99}, profile="work", remote=None, token=None),
|
||||
]
|
||||
|
||||
def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||
@@ -435,7 +435,7 @@ class TestGroups:
|
||||
|
||||
def test_group_close(self, b, mock_send):
|
||||
b.group_close(42)
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None)
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_group_create_dict_response(self, b, mock_send):
|
||||
mock_send.return_value = GROUP_DATA
|
||||
@@ -455,7 +455,7 @@ class TestGroups:
|
||||
tab_id = b.group_add_tab(42, "https://example.com")
|
||||
assert tab_id == 55
|
||||
mock_send.assert_called_once_with(
|
||||
"group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None
|
||||
"group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None, remote=None, token=None
|
||||
)
|
||||
|
||||
def test_group_add_tab_non_dict_response(self, b, mock_send):
|
||||
@@ -465,7 +465,7 @@ class TestGroups:
|
||||
def test_group_move_forward(self, b, mock_send):
|
||||
b.group_move(42, forward=True)
|
||||
mock_send.assert_called_once_with(
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None
|
||||
)
|
||||
|
||||
|
||||
@@ -495,7 +495,7 @@ class TestWindows:
|
||||
result = b.windows_open()
|
||||
|
||||
assert result == {"id": 5}
|
||||
mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None)
|
||||
mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_windows_open_with_url(self, b, mock_send):
|
||||
mock_send.return_value = {"id": 9}
|
||||
@@ -503,7 +503,7 @@ class TestWindows:
|
||||
result = b.windows_open("https://example.com")
|
||||
|
||||
assert result == {"id": 9}
|
||||
mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None)
|
||||
mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, token=None)
|
||||
|
||||
|
||||
class TestSession:
|
||||
@@ -513,7 +513,7 @@ class TestSession:
|
||||
result = b.session_list()
|
||||
|
||||
assert result == [{"name": "saved", "tabs": 3, "savedAt": 1712707200000}]
|
||||
mock_send.assert_called_once_with("session.list", {}, profile=None)
|
||||
mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_session_list_multi_browser_adds_browser(self, b, mock_send):
|
||||
with patch(
|
||||
@@ -548,26 +548,26 @@ class TestTabModel:
|
||||
|
||||
def test_close(self, tab, mock_send):
|
||||
tab.close()
|
||||
mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None)
|
||||
mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_activate(self, tab, mock_send):
|
||||
tab.activate()
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None)
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_reload(self, tab, mock_send):
|
||||
tab.reload()
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_hard_reload(self, tab, mock_send):
|
||||
tab.hard_reload()
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None)
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_move_forward(self, tab, mock_send):
|
||||
tab.move(forward=True)
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.move",
|
||||
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
||||
profile=None,
|
||||
profile=None, remote=None, token=None,
|
||||
)
|
||||
|
||||
def test_move_to_group(self, tab, mock_send):
|
||||
@@ -577,12 +577,12 @@ class TestTabModel:
|
||||
def test_html(self, tab, mock_send):
|
||||
mock_send.return_value = "<html/>"
|
||||
assert tab.html() == "<html/>"
|
||||
mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None)
|
||||
mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_open(self, tab, mock_send):
|
||||
tab.open("https://new.example.com")
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None
|
||||
"navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, token=None
|
||||
)
|
||||
|
||||
def test_open_background_changes_same_tab(self, tab, mock_send):
|
||||
@@ -590,7 +590,7 @@ class TestTabModel:
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.to",
|
||||
{"tabId": 10, "url": "https://new.example.com"},
|
||||
profile=None,
|
||||
profile=None, remote=None, token=None,
|
||||
)
|
||||
|
||||
def test_unbound_raises(self):
|
||||
@@ -608,18 +608,18 @@ class TestGroupModel:
|
||||
|
||||
def test_close(self, group, mock_send):
|
||||
group.close()
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None)
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_tabs(self, group, mock_send):
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
tabs = group.tabs()
|
||||
assert isinstance(tabs[0], Tab)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None)
|
||||
|
||||
def test_move_forward(self, group, mock_send):
|
||||
group.move(forward=True)
|
||||
mock_send.assert_called_once_with(
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None
|
||||
)
|
||||
|
||||
def test_move_backward(self, group, mock_send):
|
||||
|
||||
Reference in New Issue
Block a user