486 lines
17 KiB
Python
486 lines
17 KiB
Python
|
import gi
|
||
|
gi.require_version('Gtk', '3.0')
|
||
|
|
||
|
|
||
|
webkit_loaded = False
|
||
|
webkit_version = None
|
||
|
for version in ['4.1', '4.0', '3.0']:
|
||
|
try:
|
||
|
gi.require_version('WebKit2', version)
|
||
|
webkit_loaded = True
|
||
|
webkit_version = version
|
||
|
break
|
||
|
except ValueError:
|
||
|
continue
|
||
|
|
||
|
if not webkit_loaded:
|
||
|
print("Error: WebKit2 is not installed or available.")
|
||
|
print("Please install it using:")
|
||
|
print(" Ubuntu/Debian: sudo apt-get install gir1.2-webkit2-4.0")
|
||
|
print(" Fedora: sudo dnf install webkit2gtk3")
|
||
|
print(" Arch: sudo pacman -S webkit2gtk")
|
||
|
exit(1)
|
||
|
|
||
|
from gi.repository import Gtk, WebKit2, GLib, Gdk
|
||
|
print(f"Successfully loaded WebKit2 version {webkit_version}")
|
||
|
|
||
|
import asyncio
|
||
|
import websockets
|
||
|
import json
|
||
|
import threading
|
||
|
import uuid
|
||
|
from typing import Dict, Optional, Callable, Any
|
||
|
import time
|
||
|
import base64
|
||
|
|
||
|
|
||
|
class RemoteBrowser:
|
||
|
"""
|
||
|
A WebKit2GTK browser instance that can be controlled via WebSocket.
|
||
|
Each connection gets its own browser window.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, connection_id: str, websocket, server):
|
||
|
self.connection_id = connection_id
|
||
|
self.websocket = websocket
|
||
|
self.server = server
|
||
|
self.window = None
|
||
|
self.webview = None
|
||
|
self.pending_callbacks = {}
|
||
|
self.is_closing = False
|
||
|
|
||
|
def create_browser(self):
|
||
|
"""Create the browser window in the GTK thread."""
|
||
|
self.window = Gtk.Window(title=f"Remote Browser - {self.connection_id[:8]}")
|
||
|
self.window.set_default_size(1024, 768)
|
||
|
self.window.connect("destroy", self.on_window_destroy)
|
||
|
|
||
|
|
||
|
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||
|
self.window.add(main_box)
|
||
|
|
||
|
|
||
|
nav_bar = self.create_navigation_bar()
|
||
|
main_box.pack_start(nav_bar, False, False, 0)
|
||
|
|
||
|
|
||
|
self.webview = WebKit2.WebView()
|
||
|
self.setup_webview()
|
||
|
|
||
|
|
||
|
scrolled_window = Gtk.ScrolledWindow()
|
||
|
scrolled_window.add(self.webview)
|
||
|
main_box.pack_start(scrolled_window, True, True, 0)
|
||
|
|
||
|
|
||
|
self.status_bar = Gtk.Statusbar()
|
||
|
self.status_bar.push(0, f"Connected: {self.connection_id[:8]}")
|
||
|
main_box.pack_start(self.status_bar, False, False, 0)
|
||
|
|
||
|
self.window.show_all()
|
||
|
|
||
|
def create_navigation_bar(self):
|
||
|
"""Create a simple navigation bar."""
|
||
|
nav_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||
|
nav_bar.set_margin_top(5)
|
||
|
nav_bar.set_margin_bottom(5)
|
||
|
nav_bar.set_margin_left(5)
|
||
|
nav_bar.set_margin_right(5)
|
||
|
|
||
|
|
||
|
self.url_label = Gtk.Label()
|
||
|
self.url_label.set_ellipsize(True)
|
||
|
self.url_label.set_max_width_chars(50)
|
||
|
nav_bar.pack_start(self.url_label, True, True, 0)
|
||
|
|
||
|
return nav_bar
|
||
|
|
||
|
def setup_webview(self):
|
||
|
"""Configure WebKit view settings."""
|
||
|
settings = self.webview.get_settings()
|
||
|
settings.set_enable_developer_extras(True)
|
||
|
settings.set_enable_javascript(True)
|
||
|
settings.set_allow_file_access_from_file_urls(True)
|
||
|
|
||
|
|
||
|
self.webview.connect("load-changed", self.on_load_changed)
|
||
|
self.webview.connect("notify::uri", self.on_uri_changed)
|
||
|
self.webview.connect("load-failed", self.on_load_failed)
|
||
|
|
||
|
def on_window_destroy(self, widget):
|
||
|
"""Handle window close event."""
|
||
|
if not self.is_closing:
|
||
|
self.is_closing = True
|
||
|
asyncio.create_task(self.close())
|
||
|
|
||
|
def on_load_changed(self, webview, load_event):
|
||
|
"""Handle page load events."""
|
||
|
if load_event == WebKit2.LoadEvent.FINISHED:
|
||
|
asyncio.create_task(self.send_event("load_finished", {
|
||
|
"url": webview.get_uri(),
|
||
|
"title": webview.get_title()
|
||
|
}))
|
||
|
elif load_event == WebKit2.LoadEvent.STARTED:
|
||
|
asyncio.create_task(self.send_event("load_started", {
|
||
|
"url": webview.get_uri()
|
||
|
}))
|
||
|
|
||
|
def on_uri_changed(self, webview, param):
|
||
|
"""Update URL label when URI changes."""
|
||
|
uri = webview.get_uri()
|
||
|
if uri:
|
||
|
self.url_label.set_text(uri)
|
||
|
|
||
|
def on_load_failed(self, webview, load_event, failing_uri, error):
|
||
|
"""Handle load failures."""
|
||
|
asyncio.create_task(self.send_event("load_failed", {
|
||
|
"url": failing_uri,
|
||
|
"error": error.message if error else "Unknown error"
|
||
|
}))
|
||
|
|
||
|
async def send_event(self, event_type: str, data: dict):
|
||
|
"""Send an event to the WebSocket client."""
|
||
|
try:
|
||
|
await self.websocket.send(json.dumps({
|
||
|
"type": "event",
|
||
|
"event": event_type,
|
||
|
"data": data
|
||
|
}))
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
async def send_response(self, request_id: str, result: Any, error: Optional[str] = None):
|
||
|
"""Send a response to a command."""
|
||
|
try:
|
||
|
await self.websocket.send(json.dumps({
|
||
|
"type": "response",
|
||
|
"request_id": request_id,
|
||
|
"result": result,
|
||
|
"error": error
|
||
|
}))
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
def execute_javascript(self, script: str, request_id: str):
|
||
|
"""Execute JavaScript and send result back via WebSocket."""
|
||
|
def js_finished(webview, task, user_data):
|
||
|
try:
|
||
|
result = webview.run_javascript_finish(task)
|
||
|
js_result = result.get_js_value()
|
||
|
|
||
|
|
||
|
if js_result.is_string():
|
||
|
value = js_result.to_string()
|
||
|
elif js_result.is_number():
|
||
|
value = js_result.to_double()
|
||
|
elif js_result.is_boolean():
|
||
|
value = js_result.to_boolean()
|
||
|
elif js_result.is_object():
|
||
|
|
||
|
self.webview.run_javascript(
|
||
|
f"JSON.stringify({script})",
|
||
|
None,
|
||
|
lambda wv, t, ud: self._handle_json_result(wv, t, request_id)
|
||
|
)
|
||
|
return
|
||
|
else:
|
||
|
value = None
|
||
|
|
||
|
asyncio.create_task(self.send_response(request_id, value))
|
||
|
except Exception as e:
|
||
|
asyncio.create_task(self.send_response(request_id, None, str(e)))
|
||
|
|
||
|
self.webview.run_javascript(script, None, js_finished, None)
|
||
|
|
||
|
def _handle_json_result(self, webview, task, request_id):
|
||
|
"""Handle JSON stringified JavaScript results."""
|
||
|
try:
|
||
|
result = webview.run_javascript_finish(task)
|
||
|
js_result = result.get_js_value()
|
||
|
json_str = js_result.to_string()
|
||
|
value = json.loads(json_str) if json_str else None
|
||
|
asyncio.create_task(self.send_response(request_id, value))
|
||
|
except Exception as e:
|
||
|
asyncio.create_task(self.send_response(request_id, None, str(e)))
|
||
|
|
||
|
async def handle_command(self, command: dict):
|
||
|
"""Handle commands from WebSocket client."""
|
||
|
cmd_type = command.get("command")
|
||
|
request_id = command.get("request_id", str(uuid.uuid4()))
|
||
|
|
||
|
try:
|
||
|
if cmd_type == "navigate":
|
||
|
url = command.get("url")
|
||
|
GLib.idle_add(self.webview.load_uri, url)
|
||
|
await self.send_response(request_id, {"status": "navigating", "url": url})
|
||
|
|
||
|
elif cmd_type == "execute_js":
|
||
|
script = command.get("script")
|
||
|
GLib.idle_add(self.execute_javascript, script, request_id)
|
||
|
|
||
|
elif cmd_type == "go_back":
|
||
|
GLib.idle_add(self.webview.go_back)
|
||
|
await self.send_response(request_id, {"status": "ok"})
|
||
|
|
||
|
elif cmd_type == "go_forward":
|
||
|
GLib.idle_add(self.webview.go_forward)
|
||
|
await self.send_response(request_id, {"status": "ok"})
|
||
|
|
||
|
elif cmd_type == "reload":
|
||
|
GLib.idle_add(self.webview.reload)
|
||
|
await self.send_response(request_id, {"status": "reloading"})
|
||
|
|
||
|
elif cmd_type == "stop":
|
||
|
GLib.idle_add(self.webview.stop_loading)
|
||
|
await self.send_response(request_id, {"status": "stopped"})
|
||
|
|
||
|
elif cmd_type == "get_info":
|
||
|
def get_browser_info():
|
||
|
info = {
|
||
|
"url": self.webview.get_uri(),
|
||
|
"title": self.webview.get_title(),
|
||
|
"can_go_back": self.webview.can_go_back(),
|
||
|
"can_go_forward": self.webview.can_go_forward(),
|
||
|
"is_loading": self.webview.is_loading()
|
||
|
}
|
||
|
asyncio.create_task(self.send_response(request_id, info))
|
||
|
GLib.idle_add(get_browser_info)
|
||
|
|
||
|
elif cmd_type == "screenshot":
|
||
|
def take_screenshot():
|
||
|
def on_snapshot(webview, task, data):
|
||
|
try:
|
||
|
surface = webview.get_snapshot_finish(task)
|
||
|
|
||
|
import io
|
||
|
import cairo
|
||
|
|
||
|
buf = io.BytesIO()
|
||
|
surface.write_to_png(buf)
|
||
|
buf.seek(0)
|
||
|
|
||
|
|
||
|
screenshot_b64 = base64.b64encode(buf.read()).decode('utf-8')
|
||
|
asyncio.create_task(self.send_response(request_id, {
|
||
|
"screenshot": screenshot_b64,
|
||
|
"format": "png"
|
||
|
}))
|
||
|
except Exception as e:
|
||
|
asyncio.create_task(self.send_response(request_id, None, str(e)))
|
||
|
|
||
|
self.webview.get_snapshot(
|
||
|
WebKit2.SnapshotRegion.FULL_DOCUMENT,
|
||
|
WebKit2.SnapshotOptions.NONE,
|
||
|
None,
|
||
|
on_snapshot,
|
||
|
None
|
||
|
)
|
||
|
GLib.idle_add(take_screenshot)
|
||
|
|
||
|
elif cmd_type == "set_html":
|
||
|
html = command.get("html", "")
|
||
|
base_uri = command.get("base_uri")
|
||
|
GLib.idle_add(self.webview.load_html, html, base_uri)
|
||
|
await self.send_response(request_id, {"status": "html_loaded"})
|
||
|
|
||
|
else:
|
||
|
await self.send_response(request_id, None, f"Unknown command: {cmd_type}")
|
||
|
|
||
|
except Exception as e:
|
||
|
await self.send_response(request_id, None, str(e))
|
||
|
|
||
|
async def close(self):
|
||
|
"""Close the browser window and cleanup."""
|
||
|
if not self.is_closing:
|
||
|
self.is_closing = True
|
||
|
|
||
|
def close_window():
|
||
|
if self.window:
|
||
|
self.window.destroy()
|
||
|
|
||
|
GLib.idle_add(close_window)
|
||
|
|
||
|
|
||
|
if self.connection_id in self.server.connections:
|
||
|
del self.server.connections[self.connection_id]
|
||
|
|
||
|
|
||
|
class BrowserServer:
|
||
|
"""
|
||
|
WebSocket server that manages multiple browser instances.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, host: str = "localhost", port: int = 8765):
|
||
|
self.host = host
|
||
|
self.port = port
|
||
|
self.connections: Dict[str, RemoteBrowser] = {}
|
||
|
self.gtk_thread = None
|
||
|
self.loop = None
|
||
|
|
||
|
def start_gtk_thread(self):
|
||
|
"""Start GTK main loop in a separate thread."""
|
||
|
def gtk_main():
|
||
|
Gtk.main()
|
||
|
|
||
|
self.gtk_thread = threading.Thread(target=gtk_main, daemon=True)
|
||
|
self.gtk_thread.start()
|
||
|
|
||
|
async def handle_connection(self, websocket, path):
|
||
|
"""Handle a new WebSocket connection."""
|
||
|
connection_id = str(uuid.uuid4())
|
||
|
browser = RemoteBrowser(connection_id, websocket, self)
|
||
|
self.connections[connection_id] = browser
|
||
|
|
||
|
|
||
|
GLib.idle_add(browser.create_browser)
|
||
|
|
||
|
|
||
|
await websocket.send(json.dumps({
|
||
|
"type": "connected",
|
||
|
"connection_id": connection_id,
|
||
|
"message": "Browser window created"
|
||
|
}))
|
||
|
|
||
|
try:
|
||
|
async for message in websocket:
|
||
|
try:
|
||
|
command = json.loads(message)
|
||
|
await browser.handle_command(command)
|
||
|
except json.JSONDecodeError:
|
||
|
await websocket.send(json.dumps({
|
||
|
"type": "error",
|
||
|
"error": "Invalid JSON"
|
||
|
}))
|
||
|
except Exception as e:
|
||
|
await websocket.send(json.dumps({
|
||
|
"type": "error",
|
||
|
"error": str(e)
|
||
|
}))
|
||
|
finally:
|
||
|
|
||
|
await browser.close()
|
||
|
|
||
|
async def start_server(self):
|
||
|
"""Start the WebSocket server."""
|
||
|
print(f"Starting WebSocket server on ws://{self.host}:{self.port}")
|
||
|
await websockets.serve(self.handle_connection, self.host, self.port)
|
||
|
await asyncio.Future()
|
||
|
|
||
|
async def run(self):
|
||
|
"""Run the server."""
|
||
|
|
||
|
self.start_gtk_thread()
|
||
|
|
||
|
|
||
|
await self.start_server()
|
||
|
|
||
|
|
||
|
|
||
|
CLIENT_EXAMPLE = '''
|
||
|
import asyncio
|
||
|
import websockets
|
||
|
import json
|
||
|
|
||
|
async def test_browser():
|
||
|
"""Example client that controls the browser."""
|
||
|
uri = "ws://localhost:8765"
|
||
|
|
||
|
async with websockets.connect(uri) as websocket:
|
||
|
# Wait for connection confirmation
|
||
|
response = await websocket.recv()
|
||
|
print(f"Connected: {response}")
|
||
|
|
||
|
# Navigate to a website
|
||
|
await websocket.send(json.dumps({
|
||
|
"command": "navigate",
|
||
|
"url": "https://www.example.com",
|
||
|
"request_id": "nav1"
|
||
|
}))
|
||
|
|
||
|
# Wait for navigation response
|
||
|
response = await websocket.recv()
|
||
|
print(f"Navigation response: {response}")
|
||
|
|
||
|
# Wait a bit for page to load
|
||
|
await asyncio.sleep(2)
|
||
|
|
||
|
# Execute JavaScript
|
||
|
await websocket.send(json.dumps({
|
||
|
"command": "execute_js",
|
||
|
"script": "document.title",
|
||
|
"request_id": "js1"
|
||
|
}))
|
||
|
|
||
|
response = await websocket.recv()
|
||
|
print(f"Page title: {response}")
|
||
|
|
||
|
# Get browser info
|
||
|
await websocket.send(json.dumps({
|
||
|
"command": "get_info",
|
||
|
"request_id": "info1"
|
||
|
}))
|
||
|
|
||
|
response = await websocket.recv()
|
||
|
print(f"Browser info: {response}")
|
||
|
|
||
|
# Take screenshot
|
||
|
await websocket.send(json.dumps({
|
||
|
"command": "screenshot",
|
||
|
"request_id": "screenshot1"
|
||
|
}))
|
||
|
|
||
|
response = await websocket.recv()
|
||
|
result = json.loads(response)
|
||
|
if result.get("result") and result["result"].get("screenshot"):
|
||
|
print("Screenshot captured!")
|
||
|
# You can save the base64 image here
|
||
|
|
||
|
# Load custom HTML
|
||
|
await websocket.send(json.dumps({
|
||
|
"command": "set_html",
|
||
|
"html": "<h1>Hello from WebSocket!</h1><p>This is custom HTML</p>",
|
||
|
"request_id": "html1"
|
||
|
}))
|
||
|
|
||
|
response = await websocket.recv()
|
||
|
print(f"HTML loaded: {response}")
|
||
|
|
||
|
# Keep connection open for events
|
||
|
while True:
|
||
|
try:
|
||
|
message = await asyncio.wait_for(websocket.recv(), timeout=30)
|
||
|
print(f"Event: {message}")
|
||
|
except asyncio.TimeoutError:
|
||
|
break
|
||
|
|
||
|
asyncio.run(test_browser())
|
||
|
'''
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
|
||
|
server = BrowserServer(host="localhost", port=8765)
|
||
|
|
||
|
print("WebSocket Browser Server")
|
||
|
print("=" * 50)
|
||
|
print(f"Server will run on ws://localhost:8765")
|
||
|
print("\nAvailable commands:")
|
||
|
print("- navigate: Load a URL")
|
||
|
print("- execute_js: Execute JavaScript and get result")
|
||
|
print("- go_back/go_forward: Navigate history")
|
||
|
print("- reload/stop: Reload or stop loading")
|
||
|
print("- get_info: Get browser state info")
|
||
|
print("- screenshot: Take a screenshot (base64)")
|
||
|
print("- set_html: Load custom HTML")
|
||
|
print("\nExample client code:")
|
||
|
print("-" * 50)
|
||
|
print(CLIENT_EXAMPLE)
|
||
|
print("-" * 50)
|
||
|
|
||
|
try:
|
||
|
asyncio.run(server.run())
|
||
|
except KeyboardInterrupt:
|
||
|
print("\nShutting down server...")
|
||
|
Gtk.main_quit()
|
||
|
|