#include "desktop_platform_channel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct _DesktopPlatformChannel { MyApplication* application; GtkWindow* window; FlView* view; FlMethodChannel* channel; GtkStatusIcon* status_icon; GtkWidget* menu; std::string preferred_mode = "proxy"; std::string vpn_connection_name = "XWorkmate Tunnel"; std::string proxy_host = "127.0.0.1"; int proxy_port = 7890; bool tray_enabled = true; bool tray_available = true; bool autostart_enabled = false; bool network_manager_available = false; std::string desktop_environment = "unknown"; std::string status_message; }; namespace { constexpr char kChannelName[] = "plus.svc.xworkmate/desktop_platform"; constexpr char kDesktopFileId[] = "plus.svc.xworkmate.desktop"; struct CommandResult { bool ok = false; int exit_status = -1; std::string stdout_text; std::string stderr_text; }; std::string json_escape(const std::string& input) { std::ostringstream escaped; for (const char ch : input) { switch (ch) { case '\\': escaped << "\\\\"; break; case '"': escaped << "\\\""; break; case '\n': escaped << "\\n"; break; default: escaped << ch; break; } } return escaped.str(); } std::string trim_quotes(const std::string& value) { if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { return value.substr(1, value.size() - 2); } return value; } std::optional json_string(const std::string& payload, const std::string& key) { const std::regex pattern("\"" + key + "\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\""); std::smatch match; if (!std::regex_search(payload, match, pattern) || match.size() < 2) { return std::nullopt; } std::string value = match[1].str(); value = std::regex_replace(value, std::regex("\\\\\""), "\""); value = std::regex_replace(value, std::regex("\\\\\\\\"), "\\"); return value; } std::optional json_int(const std::string& payload, const std::string& key) { const std::regex pattern("\"" + key + "\"\\s*:\\s*(\\d+)"); std::smatch match; if (!std::regex_search(payload, match, pattern) || match.size() < 2) { return std::nullopt; } return std::stoi(match[1].str()); } std::optional json_bool(const std::string& payload, const std::string& key) { const std::regex pattern("\"" + key + "\"\\s*:\\s*(true|false)"); std::smatch match; if (!std::regex_search(payload, match, pattern) || match.size() < 2) { return std::nullopt; } return match[1].str() == "true"; } CommandResult run_command(const std::vector& args) { std::vector> quoted; quoted.reserve(args.size()); std::ostringstream command; for (size_t index = 0; index < args.size(); index++) { if (index > 0) { command << ' '; } quoted.emplace_back(g_shell_quote(args[index].c_str()), g_free); command << quoted.back().get(); } gchar* stdout_text = nullptr; gchar* stderr_text = nullptr; gint exit_status = -1; GError* error = nullptr; const gboolean ok = g_spawn_command_line_sync(command.str().c_str(), &stdout_text, &stderr_text, &exit_status, &error); CommandResult result; result.ok = ok && error == nullptr; result.exit_status = exit_status; if (stdout_text != nullptr) { result.stdout_text = stdout_text; } if (stderr_text != nullptr) { result.stderr_text = stderr_text; } if (error != nullptr) { result.stderr_text = error->message; g_error_free(error); } g_free(stdout_text); g_free(stderr_text); return result; } bool command_succeeds(const std::vector& args) { const CommandResult result = run_command(args); return result.ok && result.exit_status == 0; } std::string detect_desktop_environment() { const char* current = g_getenv("XDG_CURRENT_DESKTOP"); const std::string desktop = current == nullptr ? "" : current; std::unique_ptr lowered_raw( g_ascii_strdown(desktop.c_str(), -1), g_free); const std::string lowered = lowered_raw == nullptr ? std::string() : lowered_raw.get(); if (lowered.find("gnome") != std::string::npos) { return "gnome"; } if (lowered.find("kde") != std::string::npos || lowered.find("plasma") != std::string::npos) { return "kde"; } if (g_getenv("KDE_FULL_SESSION") != nullptr) { return "kde"; } return "unknown"; } std::string autostart_path() { const char* config_home = g_get_user_config_dir(); std::ostringstream path; path << config_home << "/autostart/" << kDesktopFileId; return path.str(); } std::string executable_path() { gchar buffer[PATH_MAX]; const ssize_t size = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1); if (size <= 0) { return "xworkmate"; } buffer[size] = '\0'; return buffer; } bool write_autostart_file() { const std::string path = autostart_path(); const std::string directory = path.substr(0, path.find_last_of('/')); if (g_mkdir_with_parents(directory.c_str(), 0755) != 0) { return false; } std::ostringstream contents; contents << "[Desktop Entry]\n"; contents << "Type=Application\n"; contents << "Version=1.0\n"; contents << "Name=XWorkmate\n"; contents << "Exec=" << executable_path() << "\n"; contents << "Icon=xworkmate\n"; contents << "Terminal=false\n"; contents << "Categories=Network;Utility;\n"; contents << "StartupNotify=true\n"; return g_file_set_contents(path.c_str(), contents.str().c_str(), -1, nullptr); } bool remove_autostart_file() { return g_remove(autostart_path().c_str()) == 0 || errno == ENOENT; } bool autostart_enabled() { return g_file_test(autostart_path().c_str(), G_FILE_TEST_EXISTS); } bool network_manager_available() { return command_succeeds({"nmcli", "--version"}); } bool tunnel_profile_exists(const std::string& connection_name) { const CommandResult result = run_command( {"nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"}); if (!result.ok || result.exit_status != 0) { return false; } std::istringstream lines(result.stdout_text); std::string line; while (std::getline(lines, line)) { if (line.rfind(connection_name + ":", 0) == 0) { return true; } } return false; } bool tunnel_connected(const std::string& connection_name) { const CommandResult result = run_command( {"nmcli", "-t", "-f", "NAME", "connection", "show", "--active"}); if (!result.ok || result.exit_status != 0) { return false; } std::istringstream lines(result.stdout_text); std::string line; while (std::getline(lines, line)) { if (line == connection_name) { return true; } } return false; } std::string gsettings_read(const std::vector& args) { const CommandResult result = run_command(args); if (!result.ok || result.exit_status != 0) { return ""; } std::string value = result.stdout_text; value.erase(value.find_last_not_of(" \n\r\t") + 1); return trim_quotes(value); } bool apply_gnome_proxy(const DesktopPlatformChannel* self) { const bool mode_ok = command_succeeds({ "gsettings", "set", "org.gnome.system.proxy", "mode", "manual"}); const bool http_host = command_succeeds({ "gsettings", "set", "org.gnome.system.proxy.http", "host", self->proxy_host}); const bool http_port = command_succeeds({ "gsettings", "set", "org.gnome.system.proxy.http", "port", std::to_string(self->proxy_port)}); const bool https_host = command_succeeds({ "gsettings", "set", "org.gnome.system.proxy.https", "host", self->proxy_host}); const bool https_port = command_succeeds({ "gsettings", "set", "org.gnome.system.proxy.https", "port", std::to_string(self->proxy_port)}); const bool socks_host = command_succeeds({ "gsettings", "set", "org.gnome.system.proxy.socks", "host", self->proxy_host}); const bool socks_port = command_succeeds({ "gsettings", "set", "org.gnome.system.proxy.socks", "port", std::to_string(self->proxy_port)}); return mode_ok && http_host && http_port && https_host && https_port && socks_host && socks_port; } bool disable_gnome_proxy() { return command_succeeds( {"gsettings", "set", "org.gnome.system.proxy", "mode", "none"}); } bool apply_kde_proxy(const DesktopPlatformChannel* self) { const bool type_ok = command_succeeds({ "kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1"}); const std::string proxy_value = "http://" + self->proxy_host + " " + std::to_string(self->proxy_port); const bool http_ok = command_succeeds({ "kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "httpProxy", proxy_value}); const bool https_ok = command_succeeds({ "kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "httpsProxy", proxy_value}); const bool socks_ok = command_succeeds({ "kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "socksProxy", proxy_value}); command_succeeds({"qdbus", "org.kde.KIO", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseConfiguration", ""}); return type_ok && http_ok && https_ok && socks_ok; } bool disable_kde_proxy() { const bool ok = command_succeeds({ "kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0"}); command_succeeds({"qdbus", "org.kde.KIO", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseConfiguration", ""}); return ok; } bool apply_proxy_mode(DesktopPlatformChannel* self) { if (self->desktop_environment == "gnome") { return apply_gnome_proxy(self); } if (self->desktop_environment == "kde") { return apply_kde_proxy(self); } return false; } bool disable_system_proxy(DesktopPlatformChannel* self) { if (self->desktop_environment == "gnome") { return disable_gnome_proxy(); } if (self->desktop_environment == "kde") { return disable_kde_proxy(); } return false; } std::string gnome_proxy_mode() { return gsettings_read( {"gsettings", "get", "org.gnome.system.proxy", "mode"}); } std::string gnome_proxy_host(const std::string& group) { const std::string schema = "org.gnome.system.proxy." + group; return gsettings_read({"gsettings", "get", schema, "host"}); } int gnome_proxy_port(const std::string& group) { const std::string schema = "org.gnome.system.proxy." + group; const std::string value = gsettings_read({"gsettings", "get", schema, "port"}); return value.empty() ? 0 : std::atoi(value.c_str()); } std::string kde_proxy_value(const char* key) { const CommandResult result = run_command({"kreadconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", key}); if (!result.ok || result.exit_status != 0) { return ""; } std::string value = result.stdout_text; value.erase(value.find_last_not_of(" \n\r\t") + 1); return value; } void refresh_runtime_state(DesktopPlatformChannel* self) { self->desktop_environment = detect_desktop_environment(); self->network_manager_available = network_manager_available(); self->autostart_enabled = autostart_enabled(); } std::string state_json(DesktopPlatformChannel* self) { refresh_runtime_state(self); bool proxy_enabled = false; std::string proxy_backend; std::string proxy_host = self->proxy_host; int proxy_port = self->proxy_port; if (self->desktop_environment == "gnome") { proxy_backend = "gsettings"; proxy_enabled = gnome_proxy_mode() == "manual"; if (proxy_enabled) { const std::string detected_host = gnome_proxy_host("http"); const int detected_port = gnome_proxy_port("http"); if (!detected_host.empty()) { proxy_host = detected_host; } if (detected_port > 0) { proxy_port = detected_port; } } } else if (self->desktop_environment == "kde") { proxy_backend = "kioslaverc"; const std::string detected = kde_proxy_value("httpProxy"); proxy_enabled = !detected.empty(); if (proxy_enabled) { const std::regex pattern(R"(http://([^ ]+)\s+(\d+))"); std::smatch match; if (std::regex_search(detected, match, pattern) && match.size() >= 3) { proxy_host = match[1].str(); proxy_port = std::stoi(match[2].str()); } } } const bool tunnel_available = self->network_manager_available && tunnel_profile_exists(self->vpn_connection_name); const bool tunnel_is_connected = tunnel_available && tunnel_connected(self->vpn_connection_name); const std::string mode = tunnel_is_connected ? "tunnel" : (proxy_enabled ? "proxy" : self->preferred_mode); std::ostringstream json; json << "{"; json << "\"isSupported\":true,"; json << "\"environment\":\"" << json_escape(self->desktop_environment) << "\","; json << "\"mode\":\"" << json_escape(mode) << "\","; json << "\"trayAvailable\":" << (self->tray_available ? "true" : "false") << ","; json << "\"trayEnabled\":" << (self->tray_enabled ? "true" : "false") << ","; json << "\"autostartEnabled\":" << (self->autostart_enabled ? "true" : "false") << ","; json << "\"networkManagerAvailable\":" << (self->network_manager_available ? "true" : "false") << ","; json << "\"systemProxy\":{"; json << "\"enabled\":" << (proxy_enabled ? "true" : "false") << ","; json << "\"host\":\"" << json_escape(proxy_host) << "\","; json << "\"port\":" << proxy_port << ","; json << "\"backend\":\"" << json_escape(proxy_backend) << "\","; json << "\"lastAppliedMode\":\"" << json_escape(self->preferred_mode) << "\""; json << "},"; json << "\"tunnel\":{"; json << "\"available\":" << (tunnel_available ? "true" : "false") << ","; json << "\"connected\":" << (tunnel_is_connected ? "true" : "false") << ","; json << "\"connectionName\":\"" << json_escape(self->vpn_connection_name) << "\","; json << "\"backend\":\"nmcli\","; json << "\"lastError\":\"" << json_escape(self->status_message) << "\""; json << "},"; json << "\"statusMessage\":\"" << json_escape(self->status_message) << "\""; json << "}"; return json.str(); } void update_status_icon(DesktopPlatformChannel* self) { if (self->status_icon == nullptr) { return; } gtk_status_icon_set_visible(self->status_icon, self->tray_enabled); gtk_status_icon_set_from_icon_name(self->status_icon, "network-vpn-symbolic"); const std::string json = state_json(self); const std::string tooltip = "XWorkmate • " + self->desktop_environment + " • " + self->preferred_mode; gtk_status_icon_set_tooltip_text(self->status_icon, tooltip.c_str()); } void show_window(DesktopPlatformChannel* self) { gtk_widget_show_all(GTK_WIDGET(self->window)); gtk_window_present(self->window); } void on_open_activate(GtkMenuItem*, gpointer user_data) { show_window(static_cast(user_data)); } void on_status_icon_activate(GtkStatusIcon*, gpointer user_data) { show_window(static_cast(user_data)); } void on_quit_activate(GtkMenuItem*, gpointer user_data) { auto* self = static_cast(user_data); g_application_quit(G_APPLICATION(self->application)); } void on_use_proxy_activate(GtkMenuItem*, gpointer user_data) { auto* self = static_cast(user_data); self->preferred_mode = "proxy"; if (!apply_proxy_mode(self)) { self->status_message = "Failed to apply system proxy; verify gsettings/kwriteconfig5"; } else { self->status_message = "System proxy enabled"; } update_status_icon(self); } void on_use_tunnel_activate(GtkMenuItem*, gpointer user_data) { auto* self = static_cast(user_data); self->preferred_mode = "tunnel"; if (!disable_system_proxy(self)) { self->status_message = "Tunnel mode selected; proxy disable may require manual follow-up"; } else { self->status_message = "Tunnel mode selected"; } update_status_icon(self); } void on_connect_tunnel_activate(GtkMenuItem*, gpointer user_data) { auto* self = static_cast(user_data); self->preferred_mode = "tunnel"; disable_system_proxy(self); if (!command_succeeds({"nmcli", "connection", "up", "id", self->vpn_connection_name})) { self->status_message = "Failed to connect NetworkManager tunnel"; } else { self->status_message = "Tunnel connected"; } update_status_icon(self); } void on_disconnect_tunnel_activate(GtkMenuItem*, gpointer user_data) { auto* self = static_cast(user_data); if (!command_succeeds({"nmcli", "connection", "down", "id", self->vpn_connection_name})) { self->status_message = "Failed to disconnect tunnel"; } else { self->status_message = "Tunnel disconnected"; } update_status_icon(self); } void on_status_icon_popup(GtkStatusIcon* status_icon, guint button, guint activate_time, gpointer user_data) { auto* self = static_cast(user_data); gtk_menu_popup(GTK_MENU(self->menu), nullptr, nullptr, gtk_status_icon_position_menu, status_icon, button, activate_time); } GtkWidget* build_menu(DesktopPlatformChannel* self) { GtkWidget* menu = gtk_menu_new(); GtkWidget* open_item = gtk_menu_item_new_with_label("Open XWorkmate"); GtkWidget* connect_item = gtk_menu_item_new_with_label("Connect Tunnel"); GtkWidget* disconnect_item = gtk_menu_item_new_with_label("Disconnect Tunnel"); GtkWidget* proxy_item = gtk_menu_item_new_with_label("Use Proxy Mode"); GtkWidget* tunnel_item = gtk_menu_item_new_with_label("Use Tunnel Mode"); GtkWidget* quit_item = gtk_menu_item_new_with_label("Quit"); g_signal_connect(open_item, "activate", G_CALLBACK(on_open_activate), self); g_signal_connect(connect_item, "activate", G_CALLBACK(on_connect_tunnel_activate), self); g_signal_connect(disconnect_item, "activate", G_CALLBACK(on_disconnect_tunnel_activate), self); g_signal_connect(proxy_item, "activate", G_CALLBACK(on_use_proxy_activate), self); g_signal_connect(tunnel_item, "activate", G_CALLBACK(on_use_tunnel_activate), self); g_signal_connect(quit_item, "activate", G_CALLBACK(on_quit_activate), self); gtk_menu_shell_append(GTK_MENU_SHELL(menu), open_item); gtk_menu_shell_append(GTK_MENU_SHELL(menu), connect_item); gtk_menu_shell_append(GTK_MENU_SHELL(menu), disconnect_item); gtk_menu_shell_append(GTK_MENU_SHELL(menu), proxy_item); gtk_menu_shell_append(GTK_MENU_SHELL(menu), tunnel_item); gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); gtk_menu_shell_append(GTK_MENU_SHELL(menu), quit_item); gtk_widget_show_all(menu); return menu; } void ensure_status_icon(DesktopPlatformChannel* self) { if (self->status_icon == nullptr) { self->status_icon = gtk_status_icon_new(); self->menu = build_menu(self); g_signal_connect(self->status_icon, "popup-menu", G_CALLBACK(on_status_icon_popup), self); g_signal_connect(self->status_icon, "activate", G_CALLBACK(on_status_icon_activate), self); } update_status_icon(self); } FlMethodResponse* success_response_with_json(const std::string& payload) { g_autoptr(FlValue) result = fl_value_new_string(payload.c_str()); return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); } FlMethodResponse* method_error(const char* code, const std::string& message) { return FL_METHOD_RESPONSE( fl_method_error_response_new(code, message.c_str(), nullptr)); } FlMethodResponse* handle_method_call(DesktopPlatformChannel* self, FlMethodCall* method_call) { const gchar* method = fl_method_call_get_name(method_call); FlValue* args = fl_method_call_get_args(method_call); if (strcmp(method, "getState") == 0) { return success_response_with_json(state_json(self)); } if (strcmp(method, "configure") == 0) { const char* payload = args == nullptr ? nullptr : fl_value_get_string(args); const std::string json = payload == nullptr ? "" : payload; if (const auto value = json_string(json, "preferredMode"); value.has_value()) { self->preferred_mode = *value; } if (const auto value = json_string(json, "vpnConnectionName"); value.has_value()) { self->vpn_connection_name = *value; } if (const auto value = json_string(json, "proxyHost"); value.has_value()) { self->proxy_host = *value; } if (const auto value = json_int(json, "proxyPort"); value.has_value()) { self->proxy_port = *value; } if (const auto value = json_bool(json, "trayEnabled"); value.has_value()) { self->tray_enabled = *value; } ensure_status_icon(self); return success_response_with_json(state_json(self)); } if (strcmp(method, "setMode") == 0) { const char* value = args == nullptr ? nullptr : fl_value_get_string(args); if (value == nullptr) { return method_error("INVALID_ARGS", "mode is required"); } self->preferred_mode = value; if (self->preferred_mode == "proxy") { if (!apply_proxy_mode(self)) { self->status_message = "Failed to apply system proxy"; } else { self->status_message = "System proxy enabled"; } } else { disable_system_proxy(self); self->status_message = "Tunnel mode selected"; } update_status_icon(self); return success_response_with_json(state_json(self)); } if (strcmp(method, "connectTunnel") == 0) { self->preferred_mode = "tunnel"; disable_system_proxy(self); if (!command_succeeds({"nmcli", "connection", "up", "id", self->vpn_connection_name})) { return method_error("NM_CONNECT_FAILED", "Failed to connect NetworkManager tunnel"); } self->status_message = "Tunnel connected"; update_status_icon(self); return success_response_with_json(state_json(self)); } if (strcmp(method, "disconnectTunnel") == 0) { if (!command_succeeds({"nmcli", "connection", "down", "id", self->vpn_connection_name})) { return method_error("NM_DISCONNECT_FAILED", "Failed to disconnect tunnel"); } self->status_message = "Tunnel disconnected"; update_status_icon(self); return success_response_with_json(state_json(self)); } if (strcmp(method, "setAutostart") == 0) { const bool enabled = args != nullptr && fl_value_get_bool(args); const bool ok = enabled ? write_autostart_file() : remove_autostart_file(); if (!ok) { return method_error("AUTOSTART_FAILED", "Failed to update autostart"); } self->status_message = enabled ? "Autostart enabled" : "Autostart disabled"; update_status_icon(self); return success_response_with_json(state_json(self)); } if (strcmp(method, "showWindow") == 0) { show_window(self); return success_response_with_json(state_json(self)); } return FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) { auto* self = static_cast(user_data); g_autoptr(FlMethodResponse) response = handle_method_call(self, method_call); GError* error = nullptr; if (!fl_method_call_respond(method_call, response, &error) && error != nullptr) { g_warning("Failed to send response: %s", error->message); g_error_free(error); } } } // namespace DesktopPlatformChannel* desktop_platform_channel_new(MyApplication* application, GtkWindow* window, FlView* view) { auto* self = new DesktopPlatformChannel(); self->application = application; self->window = window; self->view = view; self->desktop_environment = detect_desktop_environment(); ensure_status_icon(self); FlEngine* engine = fl_view_get_engine(view); FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(engine); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); self->channel = fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec)); fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self, nullptr); return self; } void desktop_platform_channel_free(DesktopPlatformChannel* channel) { if (channel == nullptr) { return; } if (channel->status_icon != nullptr) { gtk_status_icon_set_visible(channel->status_icon, FALSE); g_object_unref(channel->status_icon); } if (channel->menu != nullptr) { gtk_widget_destroy(channel->menu); } if (channel->channel != nullptr) { g_object_unref(channel->channel); } delete channel; }