This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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;
}