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