Initial commit.

This commit is contained in:
retoor 2025-07-17 00:51:02 +02:00
commit 563728ce4e
6 changed files with 1854 additions and 0 deletions

2
Makefile Normal file
View File

@ -0,0 +1,2 @@
install:
sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-webkit2-4.0

270
README.md Normal file
View File

@ -0,0 +1,270 @@
# WebKitGTK WebSocket Browser Automation Server
This project provides a lightweight, headless (or headful, depending on configuration) browser automation server built with WebKit2GTK and powered by a WebSocket interface. It allows external clients to control browser instances, navigate to URLs, execute JavaScript, take screenshots, and load custom HTML, making it suitable for web scraping, automated testing, or other browser-driven tasks.
The server is implemented in C using GTK3, WebKit2GTK-4.1, Libsoup, and Jansson. The provided client examples are in Python using `websockets` and `asyncio`.
## Features
* **Lightweight:** Built directly on WebKit2GTK, which can be more resource-efficient than full browser automation frameworks for certain tasks.
* **WebSocket Control:** A simple, message-based protocol over WebSockets for client-server communication.
* **JavaScript Execution:** Execute arbitrary JavaScript in the browser context.
* **Navigation:** Load URLs and local HTML content.
* **Screenshot Capability:** Capture PNG screenshots of the current page (requires additional server-side implementation).
* **Parallel Window Support:** Each WebSocket connection can control a separate browser window, enabling parallel automation.
## Server Setup (C Application)
### Prerequisites
You need the following libraries and their development headers installed on your Debian/Ubuntu-based system:
* `build-essential` (for `gcc`, `make`, etc.)
* `pkg-config`
* `libgtk-3-dev`
* `libwebkit2gtk-4.1-dev`
* `libsoup-2.4-dev`
* `libjansson-dev`
Install them using apt:
```bash
sudo apt update
sudo apt install build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.1-dev libsoup-2.4-dev libjansson-dev
```
### Compilation
Navigate to the directory containing `webapp.c` and compile using `gcc`:
```bash
gcc webapp.c $(pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.1 libsoup-2.4 jansson) -o webapp
```
If you encounter issues related to `webkit2gtk-4.1` package names, you might need to adjust the `pkg-config` name to `webkit2gtk-4.0` or another version if your system's package provides it under a different name. Use `pkg-config --list-all | grep webkit2gtk` to find available versions.
### Running the Server
Execute the compiled binary:
```bash
./webapp
```
The server will start listening for WebSocket connections on `ws://localhost:8080/`. You will also see a GTK window appear, which is the browser instance.
> **Note on Sandbox:** If you encounter issues related to the WebKit sandbox (e.g., crashes or non-functional browser), you might need to adjust your system's `sysctl` settings (often needed on Linux for unprivileged user namespaces):
>
> ```bash
> sudo sysctl -w kernel.unprivileged_userns_clone=1
> ```
>
> This is a common requirement for modern WebKit versions using sandboxing.
## Client Usage (Python Examples)
The provided `client.py` demonstrates how to interact with the server using `asyncio` and `websockets`.
### Prerequisites for Client
```bash
pip install websockets
```
### Running the Python Client Demos
```bash
python client.py
```
The client script will present a menu allowing you to choose from different automation scenarios:
1. **Parallel Browsers:** Opens multiple browser windows concurrently, navigates to different sites, and executes basic JavaScript in each.
2. **Automated Testing:** Navigates to a list of URLs, extracts specific content (e.g., heading text), and takes screenshots.
3. **Form Automation:** Loads a custom HTML page with a form, then uses JavaScript to programmatically fill and submit the form fields.
4. **Run All Demos:** Executes all the above demos sequentially.
## WebSocket Protocol
The communication between the client and the server happens over a standard WebSocket connection. All messages are JSON-encoded.
### 1. Initial Connection Handshake (Server to Client)
Upon a successful WebSocket connection, the server immediately sends a JSON message to the client acknowledging the connection.
**Server to Client:**
```json
{
"status": "connected",
"connection_id": "unique-uuid-for-this-connection"
}
```
* `status`: Always `"connected"`.
* `connection_id`: A unique identifier for the established WebSocket session. This can be used by the client for logging or internal tracking.
### 2. Client Commands (Client to Server)
The client sends JSON objects to the server to issue commands. Each command should include a `command` field and optionally other parameters, along with a `request_id` to correlate responses.
#### A. Navigate to URL
Loads a specified URL in the browser window.
**Client to Server:**
```json
{
"command": "navigate",
"url": "https://www.example.com",
"request_id": "my_navigation_request_123"
}
```
* `command`: `"navigate"`
* `url`: The URL to load.
* `request_id`: A unique ID for this request.
**Server to Client (Response):**
```json
{
"status": "success",
"command": "navigate",
"request_id": "my_navigation_request_123",
"message": "Navigation initiated"
}
```
* `status`: `"success"` or `"error"`.
* `command`: The original command, `"navigate"`.
* `request_id`: The `request_id` from the original client command.
* `message`: A descriptive message.
#### B. Execute JavaScript
Executes a JavaScript string in the context of the current page. The result of the JavaScript execution is returned.
**Client to Server:**
```json
{
"command": "execute_js",
"script": "document.body.style.backgroundColor = 'blue'; document.title;",
"request_id": "my_js_execution_request_456"
}
```
* `command`: `"execute_js"`
* `script`: The JavaScript code to execute. The last expression's value is returned.
* `request_id`: A unique ID for this request.
**Server to Client (Response):**
```json
{
"status": "success",
"command": "execute_js",
"request_id": "my_js_execution_request_456",
"result": "Your Page Title",
"result_type": "string"
}
```
Or for an error in the JavaScript:
```json
{
"status": "success",
"command": "execute_js",
"request_id": "my_js_execution_request_456",
"result_type": "javascript_error",
"result": {
"_error": true,
"message": "ReferenceError: nonExistentVar is not defined",
"stack": "..."
}
}
```
* `status`: `"success"` (even for JS errors, as the command itself executed successfully) or `"error"` (if the server couldn't execute the command at all).
* `command`: The original command, `"execute_js"`.
* `request_id`: The `request_id` from the original client command.
* `result`: The JSON-stringified result of the JavaScript execution. This can be a string, number, boolean, object, array, or `null`. If a JavaScript error occurred, `result` will be an object with `_error: true`, `message`, and `stack`.
* `result_type`: A string indicating the JSON type of the `result` field (e.g., `"string"`, `"number"`, `"object"`, `"javascript_error"`).
#### C. Set HTML Content
Replaces the entire content of the current page with the provided HTML string.
**Client to Server:**
```json
{
"command": "set_html",
"html": "<h1>Hello from Client!</h1><p>This is custom HTML.</p>",
"request_id": "my_set_html_request_789"
}
```
* `command`: `"set_html"`
* `html`: The full HTML content to load.
* `request_id`: A unique ID for this request.
**Server to Client (Response):**
```json
{
"status": "success",
"command": "set_html",
"request_id": "my_set_html_request_789",
"message": "HTML content set"
}
```
* `status`: `"success"` or `"error"`.
* `command`: The original command, `"set_html"`.
* `request_id`: The `request_id` from the original client command.
* `message`: A descriptive message.
#### D. Take Screenshot (Placeholder - Server-side implementation needed)
**Note:** The current C server implementation *does not yet include the code to actually capture and send a screenshot*. This command is a placeholder in the protocol. To implement this, you would need to use WebKitGTK's screenshot capabilities (e.g., `webkit_web_view_get_snapshot`) and encode the image (e.g., Base64) to send it over WebSocket.
**Client to Server:**
```json
{
"command": "screenshot",
"request_id": "my_screenshot_request_101"
}
```
* `command`: `"screenshot"`
* `request_id`: A unique ID for this request.
**Server to Client (Response - Current Placeholder):**
```json
{
"status": "success",
"command": "screenshot",
"request_id": "my_screenshot_request_101",
"result": "Placeholder: Screenshot functionality not fully implemented.",
"result_type": "string"
}
```
*When implemented, `result` would likely be a Base64-encoded PNG/JPEG string.*
## Contributing
Feel free to fork, contribute, and enhance this browser automation server. Ideas for improvement include:
* Full screenshot implementation and sending as Base64.
* Error handling for invalid JSON messages from client.
* More robust error reporting from the C server.
* Support for more browser interactions (e.g., mouse clicks, key presses, element selection).
* Headless mode option for the WebKitGTK window.
* Option to specify the port on server startup.

243
client.py Normal file
View File

@ -0,0 +1,243 @@
import asyncio
import websockets
import json
import base64
from typing import Optional, Dict, Any
class BrowserClient:
"""Client for controlling remote browser instances via WebSocket."""
def __init__(self, uri: str = "ws://localhost:8765"):
self.uri = uri
self.websocket = None
self.connection_id = None
self.request_counter = 0
self.pending_responses = {}
async def connect(self):
"""Connect to the browser server."""
self.websocket = await websockets.connect(self.uri)
# Get connection confirmation
response = await self.websocket.recv()
data = json.loads(response)
self.connection_id = data.get("connection_id")
print(f"Connected to browser: {self.connection_id}")
# Start response handler
asyncio.create_task(self._response_handler())
async def _response_handler(self):
"""Handle responses and events from the server."""
try:
async for message in self.websocket:
data = json.loads(message)
if data["type"] == "response":
request_id = data.get("request_id")
if request_id in self.pending_responses:
self.pending_responses[request_id].set_result(data)
elif data["type"] == "event":
print(f"Event: {data['event']} - {data['data']}")
except websockets.exceptions.ConnectionClosed:
print("Connection closed")
async def _send_command(self, command: str, **kwargs) -> Dict[str, Any]:
"""Send a command and wait for response."""
self.request_counter += 1
request_id = f"req_{self.request_counter}"
# Create future for response
future = asyncio.Future()
self.pending_responses[request_id] = future
# Send command
await self.websocket.send(json.dumps({
"command": command,
"request_id": request_id,
**kwargs
}))
# Wait for response
try:
response = await asyncio.wait_for(future, timeout=10.0)
del self.pending_responses[request_id]
if response.get("error"):
raise Exception(response["error"])
return response.get("result")
except asyncio.TimeoutError:
del self.pending_responses[request_id]
raise Exception("Command timeout")
async def navigate(self, url: str) -> Dict[str, Any]:
"""Navigate to a URL."""
return await self._send_command("navigate", url=url)
async def execute_js(self, script: str) -> Any:
"""Execute JavaScript and return result."""
return await self._send_command("execute_js", script=script)
async def go_back(self):
"""Go back in history."""
return await self._send_command("go_back")
async def go_forward(self):
"""Go forward in history."""
return await self._send_command("go_forward")
async def reload(self):
"""Reload the current page."""
return await self._send_command("reload")
async def stop(self):
"""Stop loading."""
return await self._send_command("stop")
async def get_info(self) -> Dict[str, Any]:
"""Get browser information."""
return await self._send_command("get_info")
async def screenshot(self, save_path: Optional[str] = None) -> str:
"""Take a screenshot. Returns base64 data or saves to file."""
result = await self._send_command("screenshot")
screenshot_b64 = result.get("screenshot")
if save_path and screenshot_b64:
with open(save_path, "wb") as f:
f.write(base64.b64decode(screenshot_b64))
return save_path
return screenshot_b64
async def set_html(self, html: str, base_uri: Optional[str] = None):
"""Load custom HTML content."""
return await self._send_command("set_html", html=html, base_uri=base_uri)
async def close(self):
"""Close the connection."""
if self.websocket:
await self.websocket.close()
# Example automation functions
async def scrape_page_title(client: BrowserClient, url: str) -> str:
"""Example: Scrape page title."""
await client.navigate(url)
await asyncio.sleep(2) # Wait for page load
title = await client.execute_js("document.title")
return title
async def fill_and_submit_form(client: BrowserClient, url: str):
"""Example: Fill and submit a form."""
await client.navigate(url)
await asyncio.sleep(2)
# Fill form fields
await client.execute_js("""
document.querySelector('#username').value = 'testuser';
document.querySelector('#email').value = 'test@example.com';
""")
# Submit form
await client.execute_js("document.querySelector('#submit-button').click()")
async def extract_all_links(client: BrowserClient, url: str) -> list:
"""Example: Extract all links from a page."""
await client.navigate(url)
await asyncio.sleep(2)
links = await client.execute_js("""
Array.from(document.querySelectorAll('a[href]')).map(a => ({
text: a.textContent.trim(),
href: a.href
}))
""")
return links
async def monitor_page_changes(client: BrowserClient, url: str, selector: str, interval: int = 5):
"""Example: Monitor a page element for changes."""
await client.navigate(url)
await asyncio.sleep(2)
last_value = None
while True:
try:
current_value = await client.execute_js(f"document.querySelector('{selector}')?.textContent")
if current_value != last_value:
print(f"Change detected: {last_value} -> {current_value}")
last_value = current_value
await asyncio.sleep(interval)
except Exception as e:
print(f"Monitoring error: {e}")
break
# Main example
async def main():
"""Example usage of the browser client."""
client = BrowserClient()
try:
# Connect to server
await client.connect()
# Example 1: Basic navigation and JS execution
print("\n1. Basic navigation:")
await client.navigate("https://www.example.com")
await asyncio.sleep(2)
title = await client.execute_js("document.title")
print(f"Page title: {title}")
# Example 2: Get page info
print("\n2. Browser info:")
info = await client.get_info()
print(f"Current URL: {info['url']}")
print(f"Can go back: {info['can_go_back']}")
# Example 3: Custom HTML
print("\n3. Loading custom HTML:")
await client.set_html("""
<html>
<head><title>Test Page</title></head>
<body>
<h1>WebSocket Browser Control</h1>
<p id="content">This page was loaded via WebSocket!</p>
<button onclick="alert('Clicked!')">Click Me</button>
</body>
</html>
""")
await asyncio.sleep(1)
content = await client.execute_js("document.getElementById('content').textContent")
print(f"Content: {content}")
# Example 4: Screenshot
print("\n4. Taking screenshot:")
await client.screenshot("screenshot.png")
print("Screenshot saved to screenshot.png")
# Example 5: Extract links from a real page
print("\n5. Extracting links:")
links = await extract_all_links(client, "https://www.python.org")
print(f"Found {len(links)} links")
for link in links[:5]: # Show first 5
print(f" - {link['text']}: {link['href']}")
# Keep connection open for a bit to see any events
print("\nWaiting for events...")
await asyncio.sleep(5)
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main())

232
demo.py Normal file
View File

@ -0,0 +1,232 @@
import asyncio
import websockets
import json
async def create_browser_window(window_id: int, url: str):
"""Create and control a single browser window."""
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
response = await websocket.recv()
conn_data = json.loads(response)
print(f"Window {window_id} connected: {conn_data['connection_id'][:8]}")
await websocket.send(json.dumps({
"command": "navigate",
"url": url,
"request_id": f"nav_{window_id}"
}))
await websocket.recv()
await asyncio.sleep(2)
await websocket.send(json.dumps({
"command": "execute_js",
"script": f"document.body.style.backgroundColor = '#{window_id:02x}0000'; document.title",
"request_id": f"js_{window_id}"
}))
response = await websocket.recv()
data = json.loads(response)
print(f"Window {window_id} - Title: {data.get('result')}")
await asyncio.sleep(10)
print(f"Window {window_id} closing...")
async def parallel_browser_demo():
"""Demo: Open multiple browser windows in parallel."""
urls = [
"https://www.python.org",
"https://www.github.com",
"https://www.example.com",
"https://www.wikipedia.org"
]
tasks = []
for i, url in enumerate(urls):
task = asyncio.create_task(create_browser_window(i + 1, url))
tasks.append(task)
await asyncio.sleep(0.5)
await asyncio.gather(*tasks)
print("All browser windows closed.")
async def automated_testing_demo():
"""Demo: Automated testing across multiple sites."""
test_sites = [
{"url": "https://www.example.com", "selector": "h1"},
{"url": "https://www.python.org", "selector": ".introduction h1"},
{"url": "https://httpbin.org/html", "selector": "h1"},
]
async def test_site(site_info):
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
await websocket.recv()
await websocket.send(json.dumps({
"command": "navigate",
"url": site_info["url"]
}))
await websocket.recv()
await asyncio.sleep(3)
await websocket.send(json.dumps({
"command": "execute_js",
"script": f"document.querySelector('{site_info['selector']}')?.textContent || 'Not found'"
}))
response = await websocket.recv()
data = json.loads(response)
heading = data.get("result", "Error")
await websocket.send(json.dumps({
"command": "screenshot"
}))
screenshot_response = await websocket.recv()
screenshot_data = json.loads(screenshot_response)
print(f"Site: {site_info['url']}")
print(f" Heading: {heading}")
print(f" Screenshot: {'' if screenshot_data.get('result') else ''}")
print()
tasks = [test_site(site) for site in test_sites]
await asyncio.gather(*tasks)
async def form_automation_demo():
"""Demo: Fill forms in multiple windows."""
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
await websocket.recv()
html = """
<html>
<head>
<title>Form Automation Demo</title>
<style>
body { font-family: Arial; padding: 20px; }
input, select { margin: 5px; padding: 5px; }
#result { margin-top: 20px; color: green; }
</style>
</head>
<body>
<h1>Automated Form Demo</h1>
<form id="demo-form">
<input type="text" id="name" placeholder="Name"><br>
<input type="email" id="email" placeholder="Email"><br>
<select id="country">
<option value="">Select Country</option>
<option value="US">United States</option>
<option value="UK">United Kingdom</option>
<option value="CA">Canada</option>
</select><br>
<button type="button" onclick="submitForm()">Submit</button>
</form>
<div id="result"></div>
<script>
function submitForm() {
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
const country = document.getElementById('country').value;
document.getElementById('result').innerHTML =
`Submitted: ${name} (${email}) from ${country}`;
}
</script>
</body>
</html>
"""
await websocket.send(json.dumps({
"command": "set_html",
"html": html
}))
await websocket.recv()
print("Form loaded. Automating form filling...")
await asyncio.sleep(1)
fields = [
("document.getElementById('name').value = 'John Doe'", "Filled name"),
("document.getElementById('email').value = 'john@example.com'", "Filled email"),
("document.getElementById('country').value = 'US'", "Selected country"),
("submitForm()", "Submitted form")
]
for script, message in fields:
await websocket.send(json.dumps({
"command": "execute_js",
"script": script
}))
await websocket.recv()
print(f"{message}")
await asyncio.sleep(1)
await websocket.send(json.dumps({
"command": "execute_js",
"script": "document.getElementById('result').textContent"
}))
response = await websocket.recv()
data = json.loads(response)
print(f"\nForm result: {data.get('result')}")
await asyncio.sleep(5)
async def main():
print("WebSocket Browser Control Demos")
print("=" * 40)
print("1. Parallel Browsers - Open 4 sites simultaneously")
print("2. Automated Testing - Test multiple sites")
print("3. Form Automation - Fill and submit forms")
print("4. Run All Demos")
choice = input("\nSelect demo (1-4): ")
if choice == "1":
await parallel_browser_demo()
elif choice == "2":
await automated_testing_demo()
elif choice == "3":
await form_automation_demo()
elif choice == "4":
print("\n--- Running Parallel Browsers Demo ---")
await parallel_browser_demo()
print("\n--- Running Automated Testing Demo ---")
await automated_testing_demo()
print("\n--- Running Form Automation Demo ---")
await form_automation_demo()
else:
print("Invalid choice")
if __name__ == "__main__":
asyncio.run(main())

485
server.py Normal file
View File

@ -0,0 +1,485 @@
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()

622
webapp.c Normal file
View File

@ -0,0 +1,622 @@
/* webapp.c GTK3 + WebKit2GTK-4.1 + WebSocket demo
*
* Build:
* gcc webapp.c $(pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.1 libsoup-2.4 jansson) -o webapp
*
* Runtime tip:
* WebKit sandbox disabled? sudo sysctl -w kernel.unprivileged_userns_clone=1
*/
#include <gtk/gtk.h>
#include <webkit2/webkit2.h>
#include <jsc/jsc.h>
#include <libsoup/soup.h>
#include <jansson.h>
/* Forward declaration for on_websocket */
static void on_websocket (SoupServer *server,
SoupServerMessage *msg,
const char *path,
SoupWebsocketConnection *conn,
gpointer user_data);
// Forward declaration for the explicit closed handler
static void on_websocket_closed (SoupWebsocketConnection *conn, gpointer user_data);
// Structure to hold context for JavaScript execution callbacks
typedef struct {
SoupWebsocketConnection *conn; // The WebSocket connection to send results back to
} JsExecutionCtx;
// Callback function for when innerHTML is ready after a JavaScript evaluation (from click handler)
static void
on_inner_html_ready (GObject *web_view,
GAsyncResult *res,
gpointer user_data) /* user_data = char* selector */
{
GError *error = NULL;
WebKitJavascriptResult *js_result = webkit_web_view_evaluate_javascript_finish (
WEBKIT_WEB_VIEW (web_view), res, &error);
// CORRECTED: js_result is already declared and assigned correctly above.
// JSCValue *val = NULL; // This line was problematic as it was initializing js_result with a JSCValue type
JSCValue *val; // Declare val here, assign it below.
if (!js_result || error) {
g_warning ("JavaScript eval failed for selector [%s]: %s",
(char *)user_data, error ? error->message : "unknown");
g_clear_error (&error);
g_free (user_data);
if (js_result) g_object_unref(js_result); // Unref here if we're exiting early
return;
}
val = webkit_javascript_result_get_js_value(js_result);
// In this specific callback (on_inner_html_ready), we are expecting a string or null/undefined
// It's not part of the WebSocket JSON fallback, but still needs a valid JSCValue
if (!JSC_IS_VALUE (val)) {
g_warning ("Invalid JSCValue for selector [%s]", (char *)user_data);
g_free (user_data);
g_object_unref(js_result); // Unref here as well if val is invalid
return;
}
gchar *html = jsc_value_is_null (val) || jsc_value_is_undefined (val)
? NULL : jsc_value_to_string (val);
g_print ("\n[C] innerHTML for selector [%s]:\n%s\n\n",
(char *)user_data, html ? html : "(null or undefined)");
g_free (html);
g_free (user_data);
// IMPORTANT: Unref js_result AFTER all processing of 'val'
g_object_unref(js_result);
}
// Callback function for JavaScript 'click' messages from the WebView
static void
on_js_click (WebKitUserContentManager *mgr,
WebKitJavascriptResult *res,
gpointer user_data) /* user_data = WebKitWebView* */
{
JSCValue *obj = webkit_javascript_result_get_js_value (res);
if (!jsc_value_is_object (obj)) {
g_warning ("Invalid JS object received from click handler.");
return;
}
JSCValue *sel_val = jsc_value_object_get_property (obj, "selector");
JSCValue *x_val = jsc_value_object_get_property (obj, "x");
JSCValue *y_val = jsc_value_object_get_property (obj, "y");
if (!jsc_value_is_string (sel_val) || !jsc_value_is_number (x_val) || !jsc_value_is_number (y_val)) {
g_warning ("Invalid JS property types received from click handler.");
if (sel_val) g_object_unref (sel_val);
if (x_val) g_object_unref (x_val);
if (y_val) g_object_unref (y_val);
return;
}
gchar *selector = jsc_value_to_string (sel_val);
int x = (int) jsc_value_to_int32 (x_val);
int y = (int) jsc_value_to_int32 (y_val);
g_print ("Clicked selector: %s at (%d,%d)\n", selector, x, y);
gchar *esc = g_strescape (selector, NULL);
gchar *script = g_strdup_printf (
"(function(){"
" try {"
" var e = document.querySelector('%s');"
" return e && e.innerHTML ? e.innerHTML : null;"
" } catch (err) {"
" return null;"
" }"
"})()", esc);
g_free (esc);
WebKitWebView *view = WEBKIT_WEB_VIEW (user_data);
webkit_web_view_evaluate_javascript (view,
script,
-1,
NULL,
NULL,
NULL,
on_inner_html_ready,
g_strdup (selector));
g_free (script);
g_free (selector);
g_object_unref (sel_val);
g_object_unref (x_val);
g_object_unref (y_val);
}
// Callback function for when JavaScript execution initiated from WebSocket completes
static void
on_js_execution_complete (GObject *web_view,
GAsyncResult *res,
gpointer user_data)
{
JsExecutionCtx *ctx = (JsExecutionCtx*)user_data;
SoupWebsocketConnection *conn = ctx->conn;
GError *error = NULL;
json_t *result_json_root = json_object(); // The root object for our JSON response
WebKitJavascriptResult *js_result = webkit_web_view_evaluate_javascript_finish(
WEBKIT_WEB_VIEW(web_view), res, &error);
JSCValue *val; // Declare val here, assign it below.
if (error) {
g_warning ("JavaScript execution failed: %s", error->message);
json_object_set_new(result_json_root, "status", json_string("error"));
json_object_set_new(result_json_root, "message", json_string(error->message));
g_clear_error(&error);
} else if (!js_result) {
g_warning ("JavaScript execution returned no result object.");
json_object_set_new(result_json_root, "status", json_string("error"));
json_object_set_new(result_json_root, "message", json_string("JavaScript execution returned no result."));
} else {
val = webkit_javascript_result_get_js_value(js_result);
// We expect the result to always be a JSON string due to the wrapper in on_websocket_message
if (JSC_IS_VALUE(val) && jsc_value_is_string(val)) {
gchar *json_string_from_js = jsc_value_to_string(val);
g_print("DEBUG: Received JSON string from JS: %s\n", json_string_from_js ? json_string_from_js : "(null)");
if (json_string_from_js) {
json_t *parsed_json_result = json_loads(json_string_from_js, 0, NULL);
if (parsed_json_result) {
json_object_set_new(result_json_root, "status", json_string("success"));
// Check if it's a function that was returned
if (json_is_object(parsed_json_result)) {
json_t *type_field = json_object_get(parsed_json_result, "type");
if (type_field && json_is_string(type_field) &&
strcmp(json_string_value(type_field), "function") == 0) {
json_object_set_new(result_json_root, "result_type", json_string("function"));
} else if (json_object_get(parsed_json_result, "_error")) {
json_object_set_new(result_json_root, "result_type", json_string("javascript_error"));
} else {
json_object_set_new(result_json_root, "result_type", json_string("object"));
}
} else if (json_is_array(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("array"));
} else if (json_is_string(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("string"));
} else if (json_is_integer(parsed_json_result) || json_is_real(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("number"));
} else if (json_is_boolean(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("boolean"));
} else if (json_is_null(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("null"));
} else {
json_object_set_new(result_json_root, "result_type", json_string("unknown_json_type"));
}
json_object_set_new(result_json_root, "result", parsed_json_result);
}
if (parsed_json_result) {
json_object_set_new(result_json_root, "status", json_string("success"));
// Check if it's our internal error object from JS (e.g., if a JS error occurred)
if (json_is_object(parsed_json_result) && json_object_get(parsed_json_result, "_error")) {
json_object_set_new(result_json_root, "result_type", json_string("javascript_error"));
} else if (json_is_object(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("object"));
} else if (json_is_array(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("array"));
} else if (json_is_string(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("string"));
} else if (json_is_integer(parsed_json_result) || json_is_real(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("number"));
} else if (json_is_boolean(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("boolean"));
} else if (json_is_null(parsed_json_result)) {
json_object_set_new(result_json_root, "result_type", json_string("null"));
} else {
json_object_set_new(result_json_root, "result_type", json_string("unknown_json_type"));
}
json_object_set_new(result_json_root, "result", parsed_json_result); // Jansson takes ownership
} else {
// This means JSON.stringify on JS side gave something invalid, or our handling is off
g_warning("Failed to parse JSON string from JavaScript: %s", json_string_from_js);
// CORRECTED: Use g_strdup_printf and json_string
gchar *msg = g_strdup_printf("Failed to parse JavaScript result as JSON: %s", json_string_from_js);
json_object_set_new(result_json_root, "status", json_string("error"));
json_object_set_new(result_json_root, "message", json_string(msg));
g_free(msg);
}
g_free(json_string_from_js);
} else {
g_warning("jsc_value_to_string returned NULL for expected JSON string.");
json_object_set_new(result_json_root, "status", json_string("error"));
json_object_set_new(result_json_root, "message", json_string("JavaScript result was not a string or could not be converted to string."));
}
} else {
g_warning("JSCValue was not a valid string as expected. Type assertion failed or not string.");
json_object_set_new(result_json_root, "status", json_string("error"));
gchar *msg = g_strdup_printf("Expected JavaScript result to be a JSON string, but it was not a valid JSCValue or not a string type. JSC_IS_VALUE: %d, jsc_value_is_string: %d",
(val != NULL && JSC_IS_VALUE(val)), (val != NULL && jsc_value_is_string(val)));
json_object_set_new(result_json_root, "message", json_string(msg));
g_free(msg);
}
// IMPORTANT: Unref js_result AFTER all processing
g_object_unref(js_result);
}
gchar *json_response_str = json_dumps(result_json_root, JSON_INDENT(2));
if (json_response_str) {
soup_websocket_connection_send_text(conn, json_response_str);
g_free(json_response_str);
} else {
g_warning("Failed to serialize JSON response to string.");
soup_websocket_connection_send_text(conn, "{\"status\":\"error\",\"message\":\"Internal server error: Failed to serialize response\"}");
}
json_decref(result_json_root);
g_object_unref(ctx->conn);
g_free(ctx);
}
/* WebSocket message handler: Called when a message is received on the WebSocket */
/* WebSocket message handler: Called when a message is received on the WebSocket */
static void
on_websocket_message (SoupWebsocketConnection *conn,
SoupWebsocketDataType type,
GBytes *message,
gpointer user_data) /* user_data = WebKitWebView* */
{
gsize size;
const gchar *data = g_bytes_get_data (message, &size);
WebKitWebView *view = WEBKIT_WEB_VIEW(user_data);
if (type == SOUP_WEBSOCKET_DATA_TEXT && data) {
gchar *incoming_javascript_code = g_strndup (data, size);
g_print ("Received raw JavaScript code via WebSocket:\n%s\n", incoming_javascript_code);
// Improved wrapper that properly handles return statements and expressions
gchar *wrapped_javascript_code = g_strdup_printf(
"(function() {\n"
" try {\n"
" // Wrap user code in eval to handle return statements\n"
" var _userResult_ = eval(\n"
" '(function() {\\n' +\n"
" %s +\n"
" '\\n})()'\n"
" );\n"
" // Handle different types of results\n"
" if (_userResult_ === undefined) {\n"
" return JSON.stringify(null);\n"
" } else if (typeof _userResult_ === 'function') {\n"
" // Convert function to string representation\n"
" return JSON.stringify({\n"
" type: 'function',\n"
" value: _userResult_.toString(),\n"
" name: _userResult_.name || 'anonymous'\n"
" });\n"
" } else {\n"
" return JSON.stringify(_userResult_);\n"
" }\n"
" } catch (e) {\n"
" // Catch JS errors and stringify them in a consistent error format\n"
" return JSON.stringify({ _error: true, message: e.message, stack: e.stack });\n"
" }\n"
"})()",
// Escape the incoming JavaScript code for embedding in a string
incoming_javascript_code
);
g_print ("Executing wrapped JavaScript (forcing JSON string return):\n%s\n", wrapped_javascript_code);
JsExecutionCtx *ctx = g_new(JsExecutionCtx, 1);
ctx->conn = g_object_ref(conn);
webkit_web_view_evaluate_javascript (view,
wrapped_javascript_code,
-1,
NULL,
NULL,
NULL,
on_js_execution_complete,
ctx);
g_free (incoming_javascript_code);
g_free (wrapped_javascript_code);
} else {
g_warning("Received non-text WebSocket message. Ignoring.");
}
}
static void
on_websocket_message4 (SoupWebsocketConnection *conn,
SoupWebsocketDataType type,
GBytes *message,
gpointer user_data) /* user_data = WebKitWebView* */
{
gsize size;
const gchar *data = g_bytes_get_data (message, &size);
WebKitWebView *view = WEBKIT_WEB_VIEW(user_data);
if (type == SOUP_WEBSOCKET_DATA_TEXT && data) {
gchar *incoming_javascript_code = g_strndup (data, size);
g_print ("Received raw JavaScript code via WebSocket:\n%s\n", incoming_javascript_code);
// Improved wrapper that properly handles function definitions and expressions
gchar *wrapped_javascript_code = g_strdup_printf(
"(function() {\n"
" %s \n"
" try {\n"
" // Execute the user's code and capture the result\n"
" var _userResult_ = (function() {\n"
" ""\n"
" return \"aaaa\";"
" });\n"
" // If the result is a function, we can optionally call it or just return it\n"
" // For now, we'll return the function itself (not call it)\n"
" if (typeof _userResult_ === 'function') {\n"
" // Convert function to string representation\n"
" return JSON.stringify({\n"
" type: 'function',\n"
" value: _userResult_().toString(),\n"
" name: _userResult_.name || 'anonymous'\n"
" });\n"
" }\n"
" return JSON.stringify(_userResult_);\n"
" } catch (e) {\n"
" // Catch JS errors and stringify them in a consistent error format\n"
" return JSON.stringify({ _error: true, message: e.message, stack: e.stack });\n"
" }\n"
"})()",
incoming_javascript_code
);
g_print ("Executing wrapped JavaScript (forcing JSON string return):\n%s\n", wrapped_javascript_code);
JsExecutionCtx *ctx = g_new(JsExecutionCtx, 1);
ctx->conn = g_object_ref(conn);
webkit_web_view_evaluate_javascript (view,
wrapped_javascript_code,
-1,
NULL,
NULL,
NULL,
on_js_execution_complete,
ctx);
g_free (incoming_javascript_code);
g_free (wrapped_javascript_code);
} else {
g_warning("Received non-text WebSocket message. Ignoring.");
}
}
static void
on_websocket_message3 (SoupWebsocketConnection *conn,
SoupWebsocketDataType type,
GBytes *message,
gpointer user_data) /* user_data = WebKitWebView* */
{
gsize size;
const gchar *data = g_bytes_get_data (message, &size);
WebKitWebView *view = WEBKIT_WEB_VIEW(user_data);
if (type == SOUP_WEBSOCKET_DATA_TEXT && data) {
gchar *incoming_javascript_code = g_strndup (data, size);
g_print ("Received raw JavaScript code via WebSocket:\n%s\n", incoming_javascript_code);
// New wrapper: always JSON.stringify the result
// The user's code is wrapped in an inner IIFE to prevent variable pollution
// and its return value is then JSON.stringify'd.
// CORRECTED: Multi-line string literal formatting for comments
gchar *wrapped_javascript_code = g_strdup_printf(
"(function() {\n"
" try {\n"
" %s\n"
" const _result_ = function xxx (){ %s };\n" // Execute user code in a sub-IIFE
" return JSON.stringify(xxx());\n" // Stringify the result
" } catch (e) {\n"
" // Catch JS errors and stringify them in a consistent error format\n"
" return JSON.stringify({ _error: true, message: e.message, stack: e.stack });\n"
" }\n"
"})()",
incoming_javascript_code,
incoming_javascript_code
);
g_print ("Executing wrapped JavaScript (forcing JSON string return):\n%s\n", wrapped_javascript_code);
JsExecutionCtx *ctx = g_new(JsExecutionCtx, 1);
ctx->conn = g_object_ref(conn);
webkit_web_view_evaluate_javascript (view,
wrapped_javascript_code,
-1,
NULL,
NULL,
NULL,
on_js_execution_complete,
ctx);
g_free (incoming_javascript_code);
g_free (wrapped_javascript_code);
} else {
g_warning("Received non-text WebSocket message. Ignoring.");
}
}
// NEW: Explicit callback function for when the WebSocket connection closes
static void
on_websocket_closed (SoupWebsocketConnection *conn,
gpointer user_data)
{
g_print("WebSocket connection closed.\n");
// This balances the g_object_ref done in on_websocket
g_object_unref(conn);
}
/* WebSocket connection handler: Called when a new WebSocket connection is established */
static void
on_websocket (SoupServer *server,
SoupServerMessage *msg,
const char *path,
SoupWebsocketConnection *conn,
gpointer user_data) /* user_data = WebKitWebView* */
{
g_print ("WebSocket connected on %s\n", path);
g_object_ref (conn); // Increment reference count to keep the connection alive
// Connect the 'message' signal to our handler (on_websocket_message)
g_signal_connect (G_OBJECT(conn), "message", G_CALLBACK (on_websocket_message), user_data);
// Connect the 'closed' signal to our explicit handler (on_websocket_closed)
g_signal_connect (G_OBJECT(conn), "closed", G_CALLBACK (on_websocket_closed), NULL); // No user_data needed for closed handler
}
// Main activation function for the GTK application
static void
activate (GtkApplication *app, gpointer unused)
{
GtkWidget *win = gtk_application_window_new (app);
gtk_window_set_default_size (GTK_WINDOW (win), 980, 720);
gtk_window_set_title (GTK_WINDOW (win), "JS Remote Execution Demo");
WebKitUserContentManager *mgr = webkit_user_content_manager_new ();
WebKitWebView *view = WEBKIT_WEB_VIEW (webkit_web_view_new_with_user_content_manager (mgr));
/* Set up WebSocket server */
SoupServer *server = soup_server_new (NULL, NULL);
GError *error = NULL;
// Attempt to listen on port 8080 locally
if (!soup_server_listen_local (server, 8080, 0, &error)) {
g_warning ("Failed to start WebSocket server: %s", error->message);
g_clear_error (&error);
g_object_unref (server);
// Continue application without WebSocket server if it fails
} else {
g_print("WebSocket server listening on ws://localhost:8080/\n");
// Add the WebSocket handler for the "/" path.
// The 'on_websocket' callback handles new connections.
soup_server_add_websocket_handler (server, "/", NULL, NULL, on_websocket, view, NULL);
g_object_ref (view); // Keep the WebKitWebView alive for the WebSocket handler's user_data
}
// JavaScript code to be injected into the WebView to capture clicks and get CSS paths
const gchar *click_js =
"(function(){"
" function getUniqueClass(el){"
" if(!el.classList||el.classList.length===0) return null;"
" for(let c of el.classList){"
" if(document.querySelectorAll('.'+CSS.escape(c)).length===1)"
" return c;"
" } return null;"
" }"
" function cssPath(el){"
" if(el.tagName.toLowerCase()==='html') return 'html';"
" if(el.id) return '#'+CSS.escape(el.id);"
" const parts=[];"
" while(el&&el.nodeType===1&&el!==document.body){"
" let sel=el.nodeName.toLowerCase();"
" let u=getUniqueClass(el);"
" if(u){ sel='.'+CSS.escape(u); parts.unshift(sel); break; }"
" else if(el.classList.length){"
" sel+='.'+Array.from(el.classList)"
" .map(c=>CSS.escape(c)).join('.');"
" }"
" let sibs=Array.from(el.parentNode.children);"
" if(sibs.length>1)"
" sel+=':nth-child('+(sibs.indexOf(el)+1)+')';"
" parts.unshift(sel); el=el.parentNode;"
" } return parts.join(' > ');"
" }"
" document.addEventListener('click',e=>{"
" let selector=cssPath(e.target);"
" if(!selector || !document.querySelector(selector)) return;"
" window.webkit.messageHandlers.click.postMessage({"
" selector: selector, x:e.clientX, y:e.clientY });"
" });"
"})();";
// Add the click tracking script as a user script
WebKitUserScript *us = webkit_user_script_new (
click_js,
WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END,
NULL, NULL);
webkit_user_content_manager_add_script (mgr, us);
webkit_user_script_unref (us);
/* Load embed.js from file (if it exists) */
GError *error_js = NULL;
gchar *embed_js_content = NULL;
if (!g_file_get_contents ("embed.js", &embed_js_content, NULL, &error_js)) {
g_warning ("Failed to load embed.js: %s", error_js->message);
g_clear_error (&error_js);
} else {
WebKitUserScript *embed_us = webkit_user_script_new (
embed_js_content,
WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END,
NULL, NULL);
webkit_user_content_manager_add_script (mgr, embed_us);
webkit_user_script_unref (embed_us);
g_free (embed_js_content);
}
// Example of another injected script (a simple red bar at the bottom)
const gchar *tools_js =
"let c=document.createElement('div');"
"c.style.cssText='position:fixed;left:0;bottom:0;"
"height:100px;width:100%;background:#cc0000;opacity:0.4;z-index:9999';"
"document.body.appendChild(c);"
"";
WebKitUserScript *tools_us = webkit_user_script_new (
tools_js,
WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_END,
NULL, NULL);
webkit_user_content_manager_add_script (mgr, tools_us);
webkit_user_script_unref (tools_us);
// Register a message handler for JavaScript messages sent via 'window.webkit.messageHandlers.click.postMessage'
webkit_user_content_manager_register_script_message_handler (mgr, "click");
g_signal_connect (mgr, "script-message-received::click",
G_CALLBACK (on_js_click), view);
// Load an initial URI in the WebView
webkit_web_view_load_uri (view, "https://google.nl");
// Add the WebView to the GTK window
gtk_container_add (GTK_CONTAINER (win), GTK_WIDGET (view));
// Show all widgets
gtk_widget_show_all (win);
}
// Main function: Initializes GTK application and runs the main loop
int main (int argc, char **argv)
{
GtkApplication *app =
gtk_application_new ("nl.demo.selector2html",
#if GLIB_CHECK_VERSION(2,74,0)
G_APPLICATION_DEFAULT_FLAGS);
#else
G_APPLICATION_FLAGS_NONE);
#endif
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
int status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return status;
}