Initial commit.
This commit is contained in:
commit
563728ce4e
2
Makefile
Normal file
2
Makefile
Normal 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
270
README.md
Normal 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
243
client.py
Normal 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
232
demo.py
Normal 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
485
server.py
Normal 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
622
webapp.c
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user