623 lines
27 KiB
C
Raw Normal View History

2025-07-17 00:51:02 +02:00
/* 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;
}