222 lines
10 KiB
Python
222 lines
10 KiB
Python
"""TEST_ONLY: real Firefox webapp-first capture under a private Sway and profile.
|
|
|
|
No user browser, profile, bus, socket, checkpoint, or extension is modified.
|
|
Marionette system access is enabled only for this owned disposable browser.
|
|
"""
|
|
import argparse
|
|
import http.server
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import socket
|
|
import threading
|
|
import time
|
|
from common import BACKEND
|
|
from lab import Lab, until
|
|
from session_v2.ipc import Sway
|
|
from session_v2.firefox_identity import FirefoxProvider
|
|
from session_v2.errors import Failure
|
|
|
|
APP_UUID = "fcd72ebe-94c5-4209-b3f6-bbaa7784cded"
|
|
WEBAPP = "firefox.webapp-" + APP_UUID
|
|
WINDOW_UUID = "11111111-2222-4333-8444-555555555555"
|
|
KEY = "extension:session-v2@labfy.org:labfy.session.window_uuid"
|
|
|
|
|
|
class TestPage(http.server.BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
body = b"<!doctype html><title>TEST_ONLY</title><p>TEST_ONLY</p>"
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/html")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, *_):
|
|
pass
|
|
|
|
|
|
class Marionette:
|
|
def __init__(self, port):
|
|
end = time.monotonic() + 20
|
|
while True:
|
|
try:
|
|
self.connection = socket.create_connection(("127.0.0.1", port), timeout=2)
|
|
break
|
|
except OSError:
|
|
if time.monotonic() > end:
|
|
raise RuntimeError("TEST_FIREFOX_NOT_READY")
|
|
time.sleep(.1)
|
|
self.connection.settimeout(25)
|
|
hello = self.receive()
|
|
assert hello == {"applicationType": "gecko", "marionetteProtocol": 3}
|
|
self.sequence = 0
|
|
self.call("WebDriver:NewSession", {"capabilities": {}})
|
|
self.call("Marionette:SetContext", {"value": "chrome"})
|
|
|
|
def receive(self):
|
|
header = bytearray()
|
|
while not header.endswith(b":"):
|
|
part = self.connection.recv(1)
|
|
if not part:
|
|
raise RuntimeError("MARIONETTE_CLOSED")
|
|
header.extend(part)
|
|
assert len(header) < 12
|
|
length = int(header[:-1])
|
|
assert 0 < length < 4 * 1024 * 1024
|
|
data = bytearray()
|
|
while len(data) < length:
|
|
part = self.connection.recv(length - len(data))
|
|
if not part:
|
|
raise RuntimeError("MARIONETTE_CLOSED")
|
|
data.extend(part)
|
|
return json.loads(data)
|
|
|
|
def call(self, name, arguments):
|
|
self.sequence += 1
|
|
raw = json.dumps([0, self.sequence, name, arguments]).encode()
|
|
self.connection.sendall(str(len(raw)).encode() + b":" + raw)
|
|
response = self.receive()
|
|
assert response[:2] == [1, self.sequence], response
|
|
if response[2] is not None:
|
|
raise RuntimeError(response[2])
|
|
return response[3]
|
|
|
|
def script(self, body, arguments=()):
|
|
script = """const done = arguments[arguments.length - 1];
|
|
const args = Array.from(arguments).slice(0, -1);
|
|
(async () => { BODY })().then(
|
|
value => done({ok: true, value}),
|
|
error => done({ok: false, error: String(error), stack: error.stack})
|
|
);""".replace("BODY", body)
|
|
result = self.call("WebDriver:ExecuteAsyncScript", {
|
|
"script": script, "args": list(arguments), "newSandbox": True,
|
|
"sandbox": "default", "line": 1, "filename": "TEST_ONLY"
|
|
})["value"]
|
|
assert result["ok"], result
|
|
return result.get("value")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--extension", type=Path, required=True)
|
|
parser.add_argument("--expect", choices=("success", "unavailable"), required=True)
|
|
args = parser.parse_args()
|
|
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), TestPage)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
with Lab() as lab:
|
|
print("TEST_ONLY_DIRECTORY=" + str(lab.directory), flush=True)
|
|
profile = lab.directory / "firefox-profile"
|
|
profile.mkdir(mode=0o700)
|
|
extension = lab.directory / "companion.xpi"
|
|
shutil.copyfile(args.extension, extension)
|
|
reservation = socket.socket()
|
|
reservation.bind(("127.0.0.1", 0))
|
|
port = reservation.getsockname()[1]
|
|
reservation.close()
|
|
url = f"http://127.0.0.1:{server.server_port}/TEST_ONLY"
|
|
prefs = {
|
|
"marionette.port": port, "browser.taskbarTabs.enabled": True,
|
|
"browser.startup.page": 0, "browser.startup.homepage": "about:blank",
|
|
"browser.shell.checkDefaultBrowser": False,
|
|
"browser.aboutwelcome.enabled": False,
|
|
"startup.homepage_welcome_url": "",
|
|
"datareporting.policy.dataSubmissionEnabled": False,
|
|
"toolkit.telemetry.reportingpolicy.firstRun": False,
|
|
}
|
|
(profile / "user.js").write_text("".join(
|
|
"user_pref(" + json.dumps(k) + ", " + json.dumps(v) + ");\n"
|
|
for k, v in prefs.items()))
|
|
taskbars = profile / "taskbartabs"
|
|
taskbars.mkdir(mode=0o700)
|
|
(taskbars / "taskbartabs.json").write_text(json.dumps({
|
|
"version": 1, "taskbarTabs": [{
|
|
"id": APP_UUID, "scopes": [{"hostname": "127.0.0.1"}],
|
|
"startUrl": url, "name": "TEST_ONLY", "userContextId": 0,
|
|
}]
|
|
}))
|
|
native_dir = Path(lab.env["XDG_CONFIG_HOME"]) / "mozilla/native-messaging-hosts"
|
|
native_dir.mkdir(parents=True, mode=0o700)
|
|
(Path(lab.env["HOME"]) / ".mozilla").symlink_to(native_dir.parent)
|
|
(native_dir / "org.labfy.session_v2_identity.json").write_text(json.dumps({
|
|
"name": "org.labfy.session_v2_identity",
|
|
"description": "TEST_ONLY", "type": "stdio",
|
|
"path": str(BACKEND / "session-v2-native-host.py"),
|
|
"allowed_extensions": ["session-v2@labfy.org"],
|
|
}))
|
|
# Existing state prevents automatic baseline mint in BOTH versions.
|
|
automatic = Path(lab.env["XDG_STATE_HOME"]) / "labfy-sway/session-v2/automatic"
|
|
automatic.mkdir(parents=True, mode=0o700)
|
|
(automatic / "last.json").write_text('{"TEST_ONLY":true}\n')
|
|
env = dict(lab.env, MOZ_ENABLE_WAYLAND="1", MOZ_CRASHREPORTER_DISABLE="1")
|
|
lab.spawn([
|
|
"/usr/bin/dbus-run-session", "--", "/usr/lib/firefox/firefox",
|
|
"--no-remote", "--profile", str(profile), "--marionette",
|
|
"-remote-allow-system-access", "-taskbar-tab", APP_UUID,
|
|
"-new-window", url, "-container", "0",
|
|
], env=env)
|
|
control = Marionette(port)
|
|
sway = Sway(lab.socket)
|
|
until(lambda: any(n["app_id"] == WEBAPP for n in sway.windows()), timeout=10)
|
|
initial = [n["app_id"] for n in sway.windows()]
|
|
assert initial == [WEBAPP], initial
|
|
created = control.script("""
|
|
const {BrowserWindowTracker} = ChromeUtils.importESModule("resource:///modules/BrowserWindowTracker.sys.mjs");
|
|
const {SessionStore} = ChromeUtils.importESModule("moz-src:///browser/components/sessionstore/SessionStore.sys.mjs");
|
|
const win = await BrowserWindowTracker.promiseOpenWindow({});
|
|
await SessionStore.promiseInitialized;
|
|
SessionStore.setCustomWindowValue(win, args[0], JSON.stringify(args[1]));
|
|
return {normalID: win.docShell.browsingContext.id};
|
|
""", [KEY, WINDOW_UUID])
|
|
assert isinstance(created.get("normalID"), int), created
|
|
until(lambda: any(n["app_id"] == "firefox" for n in sway.windows()), timeout=10)
|
|
installed = control.script("""
|
|
const {AddonManager} = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs");
|
|
const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
|
file.initWithPath(args[0]);
|
|
const addon = await AddonManager.installTemporaryAddon(file);
|
|
return {id: addon.id, version: addon.version};
|
|
""", [str(extension)])
|
|
assert installed["id"] == "session-v2@labfy.org"
|
|
print("INSTALLED_TEST_ONLY=" + json.dumps(installed), flush=True)
|
|
time.sleep(2)
|
|
nodes = [n for n in sway.windows() if n["app_id"] == "firefox"]
|
|
assert len(nodes) == 1
|
|
runtime = Path(lab.env["XDG_RUNTIME_DIR"]) / "labfy-sway"
|
|
provider = FirefoxProvider(sway, runtime)
|
|
result = {}
|
|
try:
|
|
mapping = provider.capture(nodes, timeout=6)
|
|
result = {"status": "success", "mapping_matches": mapping == {WINDOW_UUID: nodes[0]["id"]}}
|
|
assert result["mapping_matches"], result
|
|
except Failure as error:
|
|
result = {"status": "unavailable", "reason": error.code}
|
|
assert error.code == "FIREFOX_IDENTITY_PROVIDER_UNAVAILABLE"
|
|
metadata = control.script("""
|
|
const {SessionStore} = ChromeUtils.importESModule("moz-src:///browser/components/sessionstore/SessionStore.sys.mjs");
|
|
const windows = Array.from(Services.wm.getEnumerator("navigator:browser"));
|
|
return windows.map(win => ({
|
|
runtimeWindowID: win.docShell.browsingContext.id,
|
|
value: SessionStore.getCustomWindowValue(win, args[0]) || null
|
|
}));
|
|
""", [KEY])
|
|
assert len(metadata) == 2, metadata
|
|
assert [r["value"] for r in metadata if r["runtimeWindowID"] != created["normalID"]] == [None], metadata
|
|
assert [r["value"] for r in metadata if r["runtimeWindowID"] == created["normalID"]] == [json.dumps(WINDOW_UUID)], metadata
|
|
assert result["status"] == args.expect, result
|
|
result.update(extension_version=installed["version"], webapp_first=True,
|
|
webapp_never_enrolled=True, browser_uuid_unchanged=True,
|
|
isolated_profile=True, isolated_sway=True)
|
|
(lab.directory / "firefox-webapp-result.json").write_text(json.dumps(result, indent=2))
|
|
print("FIREFOX_WEBAPP_RUNTIME=" + json.dumps(result), flush=True)
|
|
control.connection.close()
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|