pax_global_header00006660000000000000000000000064147710247160014523gustar00rootroot0000000000000052 comment=6ed8dc156702494471e7dab4c03b9bdbe48d4573 xdg-native-messaging-proxy-0.1.0/000077500000000000000000000000001477102471600167215ustar00rootroot00000000000000xdg-native-messaging-proxy-0.1.0/.githooks/000077500000000000000000000000001477102471600206265ustar00rootroot00000000000000xdg-native-messaging-proxy-0.1.0/.githooks/pre-commit000077500000000000000000000011261477102471600226300ustar00rootroot00000000000000#!/usr/bin/env bash # File generated by pre-commit: https://pre-commit.com # ID: 138fd403232d2ddd5efb44317e38bf03 # start templated INSTALL_PYTHON=/usr/bin/python3 ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit) # end templated HERE="$(cd "$(dirname "$0")" && pwd)" ARGS+=(--hook-dir "$HERE" -- "$@") if [ -x "$INSTALL_PYTHON" ]; then exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}" elif command -v pre-commit > /dev/null; then exec pre-commit "${ARGS[@]}" else echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2 exit 1 fi xdg-native-messaging-proxy-0.1.0/.github/000077500000000000000000000000001477102471600202615ustar00rootroot00000000000000xdg-native-messaging-proxy-0.1.0/.github/workflows/000077500000000000000000000000001477102471600223165ustar00rootroot00000000000000xdg-native-messaging-proxy-0.1.0/.github/workflows/Containerfile000066400000000000000000000021311477102471600250200ustar00rootroot00000000000000# This Containerfile builds the image that we use in all github workflows. # When this file is changed, or one needs to rebuild the image for another # reason, bump the `IMAGE_TAG` in the container.yml workflow. FROM ubuntu:latest RUN apt update RUN apt upgrade -y # Install dependencies RUN apt install -y --no-install-recommends \ # build tools meson \ gcc clang \ ca-certificates \ git \ libclang-rt-18-dev \ # python tools python3-pip \ python-is-python3 \ # deps libglib2.0-dev \ libjson-glib-dev \ # testing deps python3-gi \ python3-pytest \ python3-dbusmock \ python3-dbus \ # ci deps jq # Install pre-commit RUN pip install --user --break-system-packages pre-commit # Install latest libdex; required because it contains fixes for cancellation RUN git clone https://gitlab.gnome.org/GNOME/libdex.git && \ cd libdex && \ meson setup _build \ --prefix=/usr \ --libdir=lib \ -Dexamples=false \ -Dtests=false \ -Dvapi=false \ -Dintrospection=disabled && \ meson install -C _build xdg-native-messaging-proxy-0.1.0/.github/workflows/build-and-test.yml000066400000000000000000000037471477102471600256700ustar00rootroot00000000000000env: TESTS_TIMEOUT: 10 # in minutes on: workflow_call: inputs: image: required: true type: string image_options: required: true type: string jobs: build-and-test: name: Build and Test runs-on: ubuntu-latest strategy: matrix: compiler: ['gcc', 'clang'] sanitizer: ['address'] container: image: ${{ inputs.image }} env: CFLAGS: -Wp,-D_FORTIFY_SOURCE=2 CC: ${{ matrix.compiler }} G_MESSAGES_DEBUG: all options: ${{ inputs.image_options }} steps: - name: Configure environment run: | git config --global --add safe.directory $GITHUB_WORKSPACE echo XDG_DATA_DIRS=$GITHUB_WORKSPACE/tests/share:/usr/local/share:/usr/share | tee -a $GITHUB_ENV - name: Check out sources uses: actions/checkout@v4 - name: Run pre-commit hooks run: | export PYTHONPATH="/root/.local/lib/python$(python3 -c 'import sys; print("{}.{}".format(*sys.version_info))')/site-packages:$PYTHONPATH" export PATH="/root/.local/bin:$PATH" pre-commit run --show-diff-on-failure --color=always --all-files - name: Build run: | meson setup _build \ -Db_sanitize=${{ matrix.sanitizer }} \ -Db_lundef=false meson compile -C _build - name: Run tests run: timeout --signal=KILL -v ${TESTS_TIMEOUT}m meson test -C _build - name: Install run: meson install -C _build - name: Create dist tarball run: | ls -la timeout --signal=KILL -v ${TESTS_TIMEOUT}m meson dist -C _build - name: Upload test logs uses: actions/upload-artifact@v4 if: success() || failure() with: name: test logs (${{ matrix.compiler }}, ${{ matrix.sanitizer }}) path: | tests/*.log test-*.log installed-test-logs/ _build/meson-logs/testlog.txt xdg-native-messaging-proxy-0.1.0/.github/workflows/container.yml000066400000000000000000000035011477102471600250220ustar00rootroot00000000000000env: IMAGE_TAG: 2025.03.26-6 on: workflow_call: outputs: image: description: "The build image" value: ${{ jobs.build-container.outputs.image }} image_options: description: "The options to use with the image" value: --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined --privileged jobs: build-container: name: Create build container runs-on: ubuntu-latest outputs: image: ${{ steps.check.outputs.image }} steps: - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Check if image already exists on GHCR id: check run: | ACTOR="${{ github.actor }}" OWNER="${{ github.repository_owner }}" image_actor="ghcr.io/${ACTOR,,}/xdg-native-messaging-proxy:${{ env.IMAGE_TAG }}" image_owner="ghcr.io/${OWNER,,}/xdg-native-messaging-proxy:${{ env.IMAGE_TAG }}" if docker manifest inspect "${image_owner}" >/dev/null ; then echo "exists=true" >> "$GITHUB_OUTPUT" echo "image=${image_owner}" >> "$GITHUB_OUTPUT" exit 0 fi if docker manifest inspect "${image_actor}" >/dev/null ; then echo "exists=true" >> "$GITHUB_OUTPUT" echo "image=${image_actor}" >> "$GITHUB_OUTPUT" exit 0 fi echo "exists=false" >> "$GITHUB_OUTPUT" echo "image=${image_owner}" >> "$GITHUB_OUTPUT" - name: Build and push if: ${{ steps.check.outputs.exists == 'false' }} uses: docker/build-push-action@v5 with: push: true file: .github/workflows/Containerfile tags: ${{ steps.check.outputs.image }} xdg-native-messaging-proxy-0.1.0/.github/workflows/main.yml000066400000000000000000000006451477102471600237720ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: build-container: name: Container uses: ./.github/workflows/container.yml permissions: packages: write build-and-test: name: Build and Test uses: ./.github/workflows/build-and-test.yml needs: build-container with: image: ${{ needs.build-container.outputs.image }} image_options: ${{ needs.build-container.outputs.image_options }}xdg-native-messaging-proxy-0.1.0/.github/workflows/release.yml000066400000000000000000000030151477102471600244600ustar00rootroot00000000000000name: Release new version on: push: tags: - '[0-9]+.[0-9]+.[0-9]+' jobs: build-container: name: Container uses: ./.github/workflows/container.yml permissions: packages: write release: name: Build and publish a release runs-on: ubuntu-latest needs: build-container permissions: contents: write container: image: ${{ needs.build-container.outputs.image }} options: ${{ needs.build-container.outputs.image_options }} steps: - name: Configure environment run: | git config --global --add safe.directory $GITHUB_WORKSPACE - name: Checkout the repository uses: actions/checkout@v4 - name: Build run: | meson setup . _build meson dist -C _build - name: Extract release information run: | # Extract the release version releaseVersion=`meson introspect --projectinfo _build/ | jq -r .version` echo "releaseVersion=$releaseVersion" | tee -a $GITHUB_ENV echo $releaseVersion # Extract the changelog { echo "releaseChangelog< Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! xdg-native-messaging-proxy-0.1.0/NEWS.md000066400000000000000000000002061477102471600200150ustar00rootroot00000000000000Changes in 0.1.0 ================= Released: 2025-03-26 Initial release. * Entire required feature-set is implemented and tested xdg-native-messaging-proxy-0.1.0/README.md000066400000000000000000000040301477102471600201750ustar00rootroot00000000000000# xdg-native-messaging-proxy This is a small service which can be used to find native messaging host manifests, as well as start and stop those native messaging hosts. Applications running inside a sandbox might have a limited view of the host which might prevent them from finding and executing native messaging hosts which exist outside of the sandbox. This proxy is supposed to run outside of any sandbox, which will make the native messaging hosts outside the sandbox available to anyone with access to the dbus service. It is critical to understand that exposing this proxy, or any other scheme which makes the native messaging hosts from outside a sandbox available to a sandboxed process, is potentially *INSECURE*! Native messaging is a form of IPC but the specifics of which services get exposed over this IPC mechanism are unknown. Any of the services could provide some functionality that allows the caller to escape the sandbox. An obvious example is the native messaging host for installing GNOME Shell extensions, which allows sandboxed callers to run arbitrary code outside of the sandbox. A more subtle example are native messaging hosts which allow writing files to arbitrary locations, which can be used to, for example, add code to .bashrc or change the permissions of a sandboxed flatpak application. Additionally, users will not be able to judge if a native messaging host API provides the tools to escape the sandbox, making it impossible to offload this problem to the users. Previously, the functionality that xdg-native-messaging-proxy provides was implemented in a merge request for xdg-desktop-portal and was shipped by Ubuntu for a number of releases. However, xdg-desktop-portal APIs are supposed to be generically useful and secure for sandboxed applications which is not the case for any native messaging proxy. By moving the functionality to its own dbus name, we do not need to provide a secure API and applications which want to make use of the proxy have to explicitly request talk permission to this dbus name in their manifest. xdg-native-messaging-proxy-0.1.0/data/000077500000000000000000000000001477102471600176325ustar00rootroot00000000000000xdg-native-messaging-proxy-0.1.0/data/meson.build000066400000000000000000000010241477102471600217710ustar00rootroot00000000000000dbus_interfaces = files( 'org.freedesktop.NativeMessagingProxy.xml', ) install_data( dbus_interfaces, install_dir: get_option('datadir') / 'dbus-1' / 'interfaces', ) configure_file( input: 'xdg-native-messaging-proxy.service.in', output: '@BASENAME@', configuration: base_config, install: true, install_dir: systemd_userunit_dir, ) configure_file( input: 'org.freedesktop.NativeMessagingProxy.service.in', output: '@BASENAME@', configuration: base_config, install: true, install_dir: dbus_service_dir, )xdg-native-messaging-proxy-0.1.0/data/org.freedesktop.NativeMessagingProxy.service.in000066400000000000000000000002711477102471600310470ustar00rootroot00000000000000[D-BUS Service] Name=org.freedesktop.NativeMessagingProxy Exec=@libexecdir@/xdg-native-messaging-proxy SystemdService=xdg-native-messaging-proxy.service AssumedAppArmorLabel=unconfined xdg-native-messaging-proxy-0.1.0/data/org.freedesktop.NativeMessagingProxy.xml000066400000000000000000000075451477102471600276150ustar00rootroot00000000000000 xdg-native-messaging-proxy-0.1.0/data/xdg-native-messaging-proxy.service.in000066400000000000000000000003321477102471600270170ustar00rootroot00000000000000[Unit] Description=Native messaging proxy service PartOf=graphical-session.target [Service] Type=dbus BusName=org.freedesktop.NativeMessagingProxy ExecStart=@libexecdir@/xdg-native-messaging-proxy Slice=session.slice xdg-native-messaging-proxy-0.1.0/meson.build000066400000000000000000000034531477102471600210700ustar00rootroot00000000000000project( 'xdg-native-messaging-proxy', 'c', version: '0.1.0', meson_version: '>= 1.1', license: 'LGPL-2.0-or-later', ) gnome = import('gnome') glib_dep = dependency('glib-2.0', version: '>= 2.72') gio_dep = dependency('gio-2.0') gio_unix_dep = dependency('gio-unix-2.0') libdex_dep = dependency('libdex-1') json_glib_dep = dependency('json-glib-1.0') prefix = get_option('prefix') datadir = prefix / get_option('datadir') libdir = prefix / get_option('libdir') libexecdir = prefix / get_option('libexecdir') sysconfdir = prefix / get_option('sysconfdir') localedir = prefix / get_option('localedir') dbus_service_dir = get_option('dbus-service-dir') if dbus_service_dir == '' dbus_service_dir = datadir / 'dbus-1' / 'services' endif systemd_userunit_dir = get_option('systemd-user-unit-dir') if systemd_userunit_dir == '' # This is deliberately not ${libdir}: systemd units always go in # .../lib, never .../lib64 or .../lib/x86_64-linux-gnu systemd_userunit_dir = prefix / 'lib' / 'systemd' / 'user' endif common_incs = include_directories('.') # config.h src_incs = include_directories('src') config_h = configuration_data() config_h.set('_GNU_SOURCE', 1) config_h.set_quoted('G_LOG_DOMAIN', 'xdg-native-messaging-proxy') config_h.set_quoted('GETTEXT_PACKAGE', 'xdg-native-messaging-proxy') config_h.set_quoted('DATADIR', datadir) config_h.set_quoted('LIBDIR', libdir) config_h.set_quoted('LIBEXECDIR', libexecdir) config_h.set_quoted('SYSCONFDIR', sysconfdir) config_h.set_quoted('LOCALEDIR', localedir) config_h.set_quoted( 'PACKAGE_STRING', 'xdg-native-messaging-proxy @0@'.format(meson.project_version()), ) configure_file(output: 'config.h', configuration: config_h) base_config = configuration_data() base_config.set('libexecdir', libexecdir) subdir('data') subdir('src') subdir('tests')xdg-native-messaging-proxy-0.1.0/meson.options000066400000000000000000000005171477102471600214620ustar00rootroot00000000000000option('dbus-service-dir', type: 'string', value: '', description: 'directory for dbus service files (default: PREFIX/share/dbus-1/services)') option('systemd-user-unit-dir', type: 'string', value: '', description: 'directory for systemd user service files (default: PREFIX/lib/systemd/user)')xdg-native-messaging-proxy-0.1.0/src/000077500000000000000000000000001477102471600175105ustar00rootroot00000000000000xdg-native-messaging-proxy-0.1.0/src/meson.build000066400000000000000000000014231477102471600216520ustar00rootroot00000000000000dbus_sources = gnome.gdbus_codegen( 'xdg-native-messaging-proxy-dbus', sources: dbus_interfaces, interface_prefix: 'org.freedesktop', namespace: 'XnmpDbus', ) xdg_native_messaging_proxy_sources = files( 'xdg-native-messaging-proxy.c', 'xnmp-impl.c', 'xnmp-service.c', ) xdg_native_messaging_proxy_sources += dbus_sources xdg_native_messaging_proxy_deps = [ glib_dep, gio_dep, gio_unix_dep, libdex_dep, json_glib_dep, ] xdg_native_messaging_proxy_incs = [ common_incs, src_incs, ] xdg_native_messaging_proxy = executable( 'xdg-native-messaging-proxy', xdg_native_messaging_proxy_sources, dependencies: xdg_native_messaging_proxy_deps, include_directories: xdg_native_messaging_proxy_incs, install: true, install_dir: get_option('libexecdir'), )xdg-native-messaging-proxy-0.1.0/src/xdg-native-messaging-proxy.c000066400000000000000000000120761477102471600250620ustar00rootroot00000000000000/* * Copyright © 2025 Red Hat, Inc * * SPDX-License-Identifier: LGPL-2.1-or-later * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "config.h" #include #include #include #include #include #include "xnmp-impl.h" #include "xnmp-service.h" static int global_exit_status = 0; static GMainLoop *loop = NULL; static gboolean opt_replace; static gboolean opt_show_version; static GOptionEntry entries[] = { { "replace", 'r', 0, G_OPTION_ARG_NONE, &opt_replace, "Replace a running instance", NULL }, { "version", 0, 0, G_OPTION_ARG_NONE, &opt_show_version, "Show program version.", NULL}, { NULL } }; static void exit_with_status (int status) { g_debug ("Exiting with status %d", status); global_exit_status = status; g_main_loop_quit (loop); } static gboolean on_sighub_signal (gpointer user_data) { g_debug ("Received SIGHUB"); exit_with_status (0); return G_SOURCE_REMOVE; } static void on_name_acquired (GDBusConnection *connection, const char *name, gpointer user_data) { g_debug ("Bus name %s acquired", name); } static void on_name_lost (GDBusConnection *connection, const char *name, gpointer user_data) { g_debug ("Bus name %s lost", name); exit_with_status (0); } static void on_bus_acquired (GDBusConnection *connection, const char *name, gpointer user_data) { g_autoptr(GError) error = NULL; g_debug ("Bus %s acquired", name); if (!init_xnmp_service (connection, &error)) { g_critical ("No document portal: %s", error->message); return exit_with_status (1); } } int main (int argc, char *argv[]) { g_autoptr(GOptionContext) context = NULL; g_autoptr(GSource) signal_handler_source = NULL; g_autoptr(GDBusConnection) session_bus = NULL; GBusNameOwnerFlags flags; guint owner_id; g_autoptr(GError) error = NULL; if (g_getenv ("XNMP_WAIT_FOR_DEBUGGER") != NULL) { g_printerr ("\nnative messaging proxy (PID %d) is waiting for a debugger. " "Use `gdb -p %d` to connect. \n", getpid (), getpid ()); if (raise (SIGSTOP) == -1) { g_printerr ("Failed waiting for debugger\n"); exit (1); } raise (SIGCONT); } setlocale (LC_ALL, ""); bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); textdomain (GETTEXT_PACKAGE); context = g_option_context_new ("- native messaging proxy"); g_option_context_set_summary (context, "A proxy for native messaging IPC"); g_option_context_set_description (context, "native messaging proxy allows sandboxed applications to retrieve " "manifests and start those native messaging hosts. This proxy is not secure " "Any native messaging host might provide functionality to escape the " "sandbox."); g_option_context_add_main_entries (context, entries, NULL); if (!g_option_context_parse (context, &argc, &argv, &error)) { g_printerr ("%s: %s", g_get_application_name (), error->message); g_printerr ("\n"); g_printerr ("Try \"%s --help\" for more information.", g_get_prgname ()); g_printerr ("\n"); return 1; } if (opt_show_version) { g_print (PACKAGE_STRING "\n"); return 0; } g_set_prgname (argv[0]); dex_init (); loop = g_main_loop_new (NULL, FALSE); signal_handler_source = g_unix_signal_source_new (SIGHUP); g_source_set_callback (signal_handler_source, G_SOURCE_FUNC (on_sighub_signal), NULL, NULL); g_source_attach (signal_handler_source, g_main_loop_get_context (loop)); session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); if (session_bus == NULL) { g_printerr ("No session bus: %s", error->message); return 2; } flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | (opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0); owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, XNMP_BUS_NAME, flags, on_bus_acquired, on_name_acquired, on_name_lost, NULL, NULL); g_main_loop_run (loop); g_bus_unown_name (owner_id); g_main_loop_unref (loop); return global_exit_status; } xdg-native-messaging-proxy-0.1.0/src/xnmp-impl.c000066400000000000000000000517001477102471600216000ustar00rootroot00000000000000/* * Copyright © 2022-2025 Canonical Ltd * Copyright © 2025 Red Hat, Inc * * SPDX-License-Identifier: LGPL-2.1-or-later * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "config.h" #include #include #include #include #include #include "xnmp-impl.h" typedef enum _XnmpImplMode { XNMP_IMPL_MODE_CHROMIUM, XNMP_IMPL_MODE_MOZILLA, } XnmpImplMode; struct _XnmpImpl { GObject parent_instance; XnmpDbusNativeMessagingProxy *dbus_object; GHashTable *running; /* handle -> GCancellable */ GStrv chromium_search_paths; GStrv mozilla_search_paths; }; G_DEFINE_FINAL_TYPE (XnmpImpl, xnmp_impl, G_TYPE_OBJECT); typedef struct _XnmpImplGetManifestData { XnmpImpl *impl; GDBusMethodInvocation *invocation; char *messaging_host_name; char *mode; } XnmpImplGetManifestData; XnmpImplGetManifestData * xnmp_impl_get_manifest_data_new (XnmpImpl *impl, GDBusMethodInvocation *invocation, const char *messaging_host_name, const char *mode) { XnmpImplGetManifestData *d = g_new0 (XnmpImplGetManifestData, 1); d->impl = g_object_ref (impl); d->invocation = g_object_ref (invocation); d->messaging_host_name = g_strdup (messaging_host_name); d->mode = g_strdup (mode); return d; } void xnmp_impl_get_manifest_data_free (XnmpImplGetManifestData *d) { g_clear_object (&d->impl); g_clear_object (&d->invocation); g_clear_pointer (&d->messaging_host_name, g_free); g_clear_pointer (&d->mode, g_free); g_free (d); } typedef struct _XnmpImplStartData { XnmpImpl *impl; GDBusMethodInvocation *invocation; char *messaging_host_name; char *extension_or_origin; char *mode; } XnmpImplStartData; XnmpImplStartData * xnmp_impl_start_data_new (XnmpImpl *impl, GDBusMethodInvocation *invocation, const char *messaging_host_name, const char *extension_or_origin, const char *mode) { XnmpImplStartData *d = g_new0 (XnmpImplStartData, 1); d->impl = g_object_ref (impl); d->invocation = g_object_ref (invocation); d->messaging_host_name = g_strdup (messaging_host_name); d->extension_or_origin = g_strdup (extension_or_origin); d->mode = g_strdup (mode); return d; } void xnmp_impl_start_data_free (XnmpImplStartData *d) { g_clear_object (&d->impl); g_clear_object (&d->invocation); g_clear_pointer (&d->messaging_host_name, g_free); g_clear_pointer (&d->extension_or_origin, g_free); g_clear_pointer (&d->mode, g_free); g_free (d); } typedef struct _XnmpImplCloseData { XnmpImpl *impl; GDBusMethodInvocation *invocation; char *handle; } XnmpImplCloseData; XnmpImplCloseData * xnmp_impl_close_data_new (XnmpImpl *impl, GDBusMethodInvocation *invocation, const char *handle) { XnmpImplCloseData *d = g_new0 (XnmpImplCloseData, 1); d->impl = g_object_ref (impl); d->invocation = g_object_ref (invocation); d->handle = g_strdup (handle); return d; } void xnmp_impl_close_data_free (XnmpImplCloseData *d) { g_clear_object (&d->impl); g_clear_object (&d->invocation); g_clear_pointer (&d->handle, g_free); g_free (d); } static GStrv xnmp_impl_get_search_paths (XnmpImpl *impl, XnmpImplMode mode) { switch (mode) { case XNMP_IMPL_MODE_CHROMIUM: return impl->chromium_search_paths; case XNMP_IMPL_MODE_MOZILLA: return impl->mozilla_search_paths; break; } g_assert_not_reached (); } XnmpImplMode get_mode_from_str (const char *mode) { if (g_strcmp0 (mode, "mozilla") == 0) return XNMP_IMPL_MODE_MOZILLA; if (g_strcmp0 (mode, "chromium") == 0) return XNMP_IMPL_MODE_CHROMIUM; return XNMP_IMPL_MODE_MOZILLA; } static gboolean is_valid_name (const char *name) { /* This regexp comes from the Mozilla documentation on valid native messaging host names: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#native_messaging_manifests That is, one or more dot-separated groups composed of alphanumeric characters and underscores. */ return g_regex_match_simple ("^\\w+(\\.\\w+)*$", name, 0, 0); } static gboolean is_valid_manifest (JsonParser *parser, const char *messaging_host_name, GError **error) { JsonObject *metadata_root; const char *value; metadata_root = json_node_get_object (json_parser_get_root (parser)); value = json_object_get_string_member (metadata_root, "name"); if (g_strcmp0 (value, messaging_host_name) != 0) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Metadata contains an unexpected name"); return FALSE; } value = json_object_get_string_member (metadata_root, "type"); if (g_strcmp0 (value, "stdio") != 0) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Not a \"stdio\" type native messaging host"); return FALSE; } value = json_object_get_string_member (metadata_root, "path"); if (!g_path_is_absolute (value)) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "Native messaging host path is not absolute"); return FALSE; } return TRUE; } static GBytes * find_manifest (XnmpImpl *impl, const char *messaging_host_name, XnmpImplMode mode, char **manifest_filename_out, JsonParser **json_parser_out, GError **error) { GStrv search_paths = NULL; g_autofree char *metadata_basename = NULL; /* Check that the we have a valid native messaging host name */ if (!is_valid_name (messaging_host_name)) { g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Invalid native messaging host name"); return NULL; } search_paths = xnmp_impl_get_search_paths (impl, mode); metadata_basename = g_strconcat (messaging_host_name, ".json", NULL); for (size_t i = 0; search_paths[i] != NULL; i++) { g_autoptr (GFile) metadata_file = g_file_new_build_filename (search_paths[i], metadata_basename, NULL); g_autoptr (GBytes) contents = NULL; g_autoptr(JsonParser) parser = NULL; gconstpointer data; gsize size; g_autoptr (GError) local_error = NULL; contents = dex_await_boxed (dex_file_load_contents_bytes (metadata_file), &local_error); if (!contents) { if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_warning ("Loading file %s failed: %s", g_file_peek_path (metadata_file), local_error->message); } g_debug ("Skipping file %s", g_file_peek_path (metadata_file)); continue; } parser = json_parser_new (); data = g_bytes_get_data (contents, &size); if (!json_parser_load_from_data (parser, data, size, &local_error)) { g_warning ("Manifest %s is not a valid JSON file: %s", g_file_peek_path (metadata_file), local_error->message); g_debug ("Skipping file %s", g_file_peek_path (metadata_file)); continue; } if (!is_valid_manifest (parser, messaging_host_name, &local_error)) { g_warning ("Manifest %s is invalid: %s", g_file_peek_path (metadata_file), local_error->message); g_debug ("Skipping file %s", g_file_peek_path (metadata_file)); continue; } g_debug ("Found manifest %s", g_file_peek_path (metadata_file)); if (manifest_filename_out) *manifest_filename_out = g_file_get_path (metadata_file); if (json_parser_out) *json_parser_out = g_steal_pointer (&parser); return g_steal_pointer (&contents); } g_debug ("Requested manifest not found"); g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FILE_NOT_FOUND, "Could not find native messaging host"); return NULL; } DexFuture * xnmp_impl_handle_get_manifest (XnmpImplGetManifestData *data) { XnmpImpl *impl = data->impl; GDBusMethodInvocation *invocation = data->invocation; const char *messaging_host_name = data->messaging_host_name; XnmpImplMode mode = get_mode_from_str (data->mode); g_autoptr (GBytes) manifest = NULL; g_autoptr (GError) error = NULL; g_print ("Handling GetManifest %s (%s)\n", messaging_host_name, data->mode); manifest = find_manifest (impl, messaging_host_name, mode, NULL, NULL, &error); if (!manifest) { g_dbus_method_invocation_return_gerror (invocation, error); return NULL; } xnmp_dbus_native_messaging_proxy_complete_get_manifest (impl->dbus_object, invocation, g_bytes_get_data (manifest, NULL)); return NULL; } static GSubprocess * subprocess_new_with_pipes (const char * const *argv, int *stdin_fd_out, int *stdout_fd_out, int *stderr_fd_out, GError **error) { g_autoptr (GSubprocess) subp = NULL; GUnixInputStream *stream_stdout, *stream_stderr; GUnixOutputStream *stream_stdin; subp = g_subprocess_newv ((const char * const *)argv, G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE, error); if (!subp) return NULL; /* take ownership over the FDs */ stream_stdin = G_UNIX_OUTPUT_STREAM (g_subprocess_get_stdin_pipe (subp)); g_unix_output_stream_set_close_fd (stream_stdin, FALSE); *stdin_fd_out = g_unix_output_stream_get_fd (stream_stdin); stream_stdout = G_UNIX_INPUT_STREAM (g_subprocess_get_stdout_pipe (subp)); g_unix_input_stream_set_close_fd (stream_stdout, FALSE); *stdout_fd_out = g_unix_input_stream_get_fd (stream_stdout); stream_stderr = G_UNIX_INPUT_STREAM (g_subprocess_get_stderr_pipe (subp)); g_unix_input_stream_set_close_fd (stream_stderr, FALSE); *stderr_fd_out = g_unix_input_stream_get_fd (stream_stderr); return g_steal_pointer (&subp); } static void register_running (XnmpImpl *impl, const char **object_path_out, GCancellable **cancellable_out) { g_autofree char *object_path = NULL; g_autoptr (GCancellable) cancellable = NULL; do { uint64_t key; g_clear_pointer (&object_path, g_free); key = g_random_int (); key = (key << 32) | g_random_int (); object_path = g_strdup_printf (XNMP_OBJECT_PATH "/%" G_GUINT64_FORMAT, key); } while (g_hash_table_contains (impl->running, object_path)); g_debug ("registering running messaging host handle: %s", object_path); cancellable = g_cancellable_new (); *cancellable_out = cancellable; *object_path_out = object_path; g_hash_table_insert (impl->running, g_steal_pointer (&object_path), g_steal_pointer (&cancellable)); } static void unregister_running (XnmpImpl *impl, const char *object_path) { g_debug ("unregistering running messaging host handle: %s", object_path); g_hash_table_remove (impl->running, object_path); } static void cancel_running (XnmpImpl *impl, const char *object_path) { GCancellable *cancellable; cancellable = g_hash_table_lookup (impl->running, object_path); if (cancellable) { g_debug ("canceling %s\n", object_path); g_cancellable_cancel (cancellable); } } DexFuture * xnmp_impl_handle_start (XnmpImplStartData *data) { XnmpImpl *impl = data->impl; GDBusMethodInvocation *invocation = data->invocation; const char *messaging_host_name = data->messaging_host_name; const char *extension_or_origin = data->extension_or_origin; XnmpImplMode mode = get_mode_from_str (data->mode); g_autoptr (GBytes) manifest = NULL; g_autoptr (JsonParser) manifest_json = NULL; g_autofree char *manifest_filename = NULL; JsonObject *metadata_root; const char *argv[4]; size_t i = 0; gboolean success; GSubprocess *subp; int subp_pipes[3]; g_autoptr(GUnixFDList) fd_list = NULL; g_auto(GVariantBuilder) closed_options_builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); const char *handle; GCancellable *cancellable; g_autoptr (GError) error = NULL; g_print ("Handling Start %s (%s)\n", messaging_host_name, data->mode); manifest = find_manifest (impl, messaging_host_name, mode, &manifest_filename, &manifest_json, &error); if (!manifest) { g_dbus_method_invocation_return_gerror (invocation, error); return NULL; } /* Chromium: * https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging * * Mozilla: * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging * https://searchfox.org/mozilla-central/rev/9fcc11127fbfbdc88cbf37489dac90542e141c77/toolkit/components/extensions/NativeMessaging.sys.mjs#104-110 */ metadata_root = json_node_get_object (json_parser_get_root (manifest_json)); argv[i++] = json_object_get_string_member (metadata_root, "path"); if (mode == XNMP_IMPL_MODE_MOZILLA) argv[i++] = manifest_filename; argv[i++] = extension_or_origin; argv[i++] = NULL; g_assert (i <= G_N_ELEMENTS (argv)); g_debug ("Spawning native messaging host %s\n", argv[0]); subp = subprocess_new_with_pipes ((const char * const *)argv, &subp_pipes[0], &subp_pipes[1], &subp_pipes[2], &error); if (!subp) { g_dbus_method_invocation_return_gerror (invocation, error); return NULL; } register_running (impl, &handle, &cancellable); fd_list = g_unix_fd_list_new_from_array (subp_pipes, G_N_ELEMENTS (subp_pipes)); xnmp_dbus_native_messaging_proxy_complete_start (impl->dbus_object, invocation, fd_list, g_variant_new_handle (0), g_variant_new_handle (1), g_variant_new_handle (2), handle); /* close the stdin, stdout, stderr fds, so the client owns them */ g_clear_object (&fd_list); success = dex_await (dex_future_all_race (dex_subprocess_wait_check (subp), dex_cancellable_new_from_cancellable (cancellable), NULL), &error); if (!success) g_debug ("native messaging host failed: %s\n", error->message); g_clear_error (&error); g_subprocess_force_exit (subp); g_debug ("Emitting Closed signal on %s\n", g_dbus_method_invocation_get_sender (invocation)); if (!g_dbus_connection_emit_signal (g_dbus_method_invocation_get_connection (invocation), g_dbus_method_invocation_get_sender (invocation), XNMP_OBJECT_PATH, XNMP_IFACE, "Closed", g_variant_new ("(oa{sv})", handle, &closed_options_builder), &error)) g_warning ("Failed emitting Closed signal: %s", error->message); g_clear_error (&error); unregister_running (impl, handle); return NULL; } DexFuture * xnmp_impl_handle_close (XnmpImplCloseData *data) { XnmpImpl *impl = data->impl; GDBusMethodInvocation *invocation = data->invocation; const char *handle = data->handle; g_print ("Handling Close %s\n", handle); cancel_running (impl, handle); xnmp_dbus_native_messaging_proxy_complete_close (impl->dbus_object, invocation); return NULL; } static void ensure_manifest_search_paths (XnmpImpl *impl) { const char *hosts_path_str; g_autoptr(GPtrArray) search_paths = NULL; hosts_path_str = g_getenv ("XNMP_HOST_LOCATIONS"); if (hosts_path_str != NULL) { impl->chromium_search_paths = g_strsplit (hosts_path_str, ":", -1); impl->mozilla_search_paths = g_strsplit (hosts_path_str, ":", -1); return; } /* Chrome and Chromium search paths documented here: * https://developer.chrome.com/docs/extensions/nativeMessaging/#native-messaging-host-location */ search_paths = g_ptr_array_new_with_free_func (g_free); /* Add per-user directories */ g_ptr_array_add (search_paths, g_build_filename (g_get_user_config_dir (), "google-chrome", "NativeMessagingHosts", NULL)); g_ptr_array_add (search_paths, g_build_filename (g_get_user_config_dir (), "chromium", "NativeMessagingHosts", NULL)); /* Add system wide directories */ g_ptr_array_add (search_paths, g_strdup ("/etc/opt/chrome/native-messaging-hosts")); g_ptr_array_add (search_paths, g_strdup ("/etc/chromium/native-messaging-hosts")); /* And the same for the configured prefix */ g_ptr_array_add (search_paths, g_strdup (SYSCONFDIR "/opt/chrome/native-messaging-hosts")); g_ptr_array_add (search_paths, g_strdup (SYSCONFDIR "/chromium/native-messaging-hosts")); /* NULL terminated */ g_ptr_array_add (search_paths, NULL); impl->chromium_search_paths = (GStrv) g_ptr_array_free (g_steal_pointer (&search_paths), FALSE); /* Firefox search paths documented here: * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#manifest_location */ search_paths = g_ptr_array_new_with_free_func (g_free); /* Add per-user directories */ g_ptr_array_add (search_paths, g_build_filename (g_get_home_dir (), ".mozilla", "native-messaging-hosts", NULL)); g_ptr_array_add (search_paths, g_build_filename (g_get_user_config_dir (), "mozilla", "native-messaging-hosts", NULL)); /* Add system wide directories */ g_ptr_array_add (search_paths, g_strdup ("/usr/lib/mozilla/native-messaging-hosts")); g_ptr_array_add (search_paths, g_strdup ("/usr/lib64/mozilla/native-messaging-hosts")); /* And the same for the configured prefix. This is helpful on Debian-based systems where LIBDIR is suffixed with 'dpkg-architecture -qDEB_HOST_MULTIARCH', e.g. '/usr/lib/x86_64-linux-gnu'. https://salsa.debian.org/debian/debhelper/-/blob/5b96b19b456fe5e094f2870327a753b4b3ece0dc/lib/Debian/Debhelper/Buildsystem/meson.pm#L78 */ g_ptr_array_add (search_paths, g_strdup (LIBDIR "/mozilla/native-messaging-hosts")); /* NULL terminated */ g_ptr_array_add (search_paths, NULL); impl->mozilla_search_paths = (GStrv) g_ptr_array_free (g_steal_pointer (&search_paths), FALSE); } static void xnmp_impl_dispose (GObject *object) { XnmpImpl *impl = XNMP_IMPL (object); g_clear_object (&impl->dbus_object); g_clear_pointer (&impl->chromium_search_paths, g_strfreev); g_clear_pointer (&impl->mozilla_search_paths, g_strfreev); g_clear_pointer (&impl->running, g_hash_table_unref); G_OBJECT_CLASS (xnmp_impl_parent_class)->dispose (object); } static void xnmp_impl_init (XnmpImpl *impl) { } static void xnmp_impl_class_init (XnmpImplClass *klass) { GObjectClass *gobject_class; gobject_class = G_OBJECT_CLASS (klass); gobject_class->dispose = xnmp_impl_dispose; } XnmpImpl * xnmp_impl_new (XnmpDbusNativeMessagingProxy *dbus_object) { XnmpImpl *impl = g_object_new (XNMP_TYPE_IMPL, NULL); impl->dbus_object = g_object_ref (dbus_object); impl->running = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); ensure_manifest_search_paths (impl); return impl; } xdg-native-messaging-proxy-0.1.0/src/xnmp-impl.h000066400000000000000000000056421477102471600216110ustar00rootroot00000000000000/* * Copyright © 2025 Red Hat, Inc * * SPDX-License-Identifier: LGPL-2.1-or-later * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #pragma once #include #include #include "xdg-native-messaging-proxy-dbus.h" #define DBUS_BUS_NAME "org.freedesktop.DBus" #define DBUS_OBJECT_PATH "/org/freedesktop/DBus" #define DBUS_IFACE DBUS_BUS_NAME #define XNMP_BUS_NAME "org.freedesktop.NativeMessagingProxy" #define XNMP_OBJECT_PATH "/org/freedesktop/nativemessagingproxy" #define XNMP_IFACE XNMP_BUS_NAME #define XNMP_TYPE_IMPL (xnmp_impl_get_type ()) G_DECLARE_FINAL_TYPE (XnmpImpl, xnmp_impl, XNMP, IMPL, GObject) XnmpImpl * xnmp_impl_new (XnmpDbusNativeMessagingProxy *dbus_object); typedef struct _XnmpImplGetManifestData XnmpImplGetManifestData; typedef struct _XnmpImplStartData XnmpImplStartData; typedef struct _XnmpImplCloseData XnmpImplCloseData; XnmpImplGetManifestData * xnmp_impl_get_manifest_data_new (XnmpImpl *impl, GDBusMethodInvocation *invocation, const char *messaging_host_name, const char *mode); void xnmp_impl_get_manifest_data_free (XnmpImplGetManifestData *d); DexFuture * xnmp_impl_handle_get_manifest (XnmpImplGetManifestData *data); XnmpImplStartData * xnmp_impl_start_data_new (XnmpImpl *impl, GDBusMethodInvocation *invocation, const char *messaging_host_name, const char *extension_or_origin, const char *mode); void xnmp_impl_start_data_free (XnmpImplStartData *d); DexFuture * xnmp_impl_handle_start (XnmpImplStartData *data); XnmpImplCloseData * xnmp_impl_close_data_new (XnmpImpl *impl, GDBusMethodInvocation *invocation, const char *handle); void xnmp_impl_close_data_free (XnmpImplCloseData *d); DexFuture * xnmp_impl_handle_close (XnmpImplCloseData *data); xdg-native-messaging-proxy-0.1.0/src/xnmp-service.c000066400000000000000000000233521477102471600223010ustar00rootroot00000000000000/* * Copyright © 2025 Red Hat, Inc * * SPDX-License-Identifier: LGPL-2.1-or-later * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "config.h" #include #include "xdg-native-messaging-proxy-dbus.h" #include "xnmp-impl.h" #include "xnmp-service.h" struct _XnmpService { XnmpDbusNativeMessagingProxySkeleton parent_instance; XnmpImpl *impl; GHashTable *cancellables; /* dbus unique name -> GCancellable */ }; #define XNMP_TYPE_SERVICE (xnmp_service_get_type ()) G_DECLARE_FINAL_TYPE (XnmpService, xnmp_service, XNMP, SERVICE, XnmpDbusNativeMessagingProxySkeleton) static void xnmp_native_messaging_proxy_iface_init (XnmpDbusNativeMessagingProxyIface *iface); G_DEFINE_FINAL_TYPE_WITH_CODE (XnmpService, xnmp_service, XNMP_DBUS_TYPE_NATIVE_MESSAGING_PROXY_SKELETON, G_IMPLEMENT_INTERFACE (XNMP_DBUS_TYPE_NATIVE_MESSAGING_PROXY, xnmp_native_messaging_proxy_iface_init)) static GCancellable * ensure_cancellable (XnmpService *service, GDBusMethodInvocation *invocation) { const char *sender; g_autofree char *owned_sender = NULL; g_autoptr(GCancellable) cancellable = NULL; sender = g_dbus_method_invocation_get_sender (invocation); if (!g_hash_table_steal_extended (service->cancellables, sender, (gpointer *)&owned_sender, (gpointer *)&cancellable)) { owned_sender = g_strdup (sender); cancellable = g_cancellable_new (); } g_hash_table_insert (service->cancellables, g_steal_pointer (&owned_sender), g_object_ref (cancellable)); return cancellable; } static gboolean handle_get_manifest (XnmpDbusNativeMessagingProxy *object, GDBusMethodInvocation *invocation, const char *arg_messaging_host_name, const char *arg_mode, GVariant *arg_options) { XnmpService *service = XNMP_SERVICE (object); DexFuture *fiber; DexFuture *cancellable; XnmpImplGetManifestData *data; data = xnmp_impl_get_manifest_data_new (service->impl, invocation, arg_messaging_host_name, arg_mode); fiber = dex_scheduler_spawn (NULL, 0, (DexFiberFunc) xnmp_impl_handle_get_manifest, data, (GDestroyNotify) xnmp_impl_get_manifest_data_free); cancellable = dex_cancellable_new_from_cancellable (ensure_cancellable (service, invocation)); dex_future_disown (dex_future_all_race (fiber, cancellable, NULL)); return G_DBUS_METHOD_INVOCATION_HANDLED; } static gboolean handle_start (XnmpDbusNativeMessagingProxy *object, GDBusMethodInvocation *invocation, GUnixFDList *fd_list, const char *arg_messaging_host_name, const char *arg_extension_or_origin, const char *arg_mode, GVariant *arg_options) { XnmpService *service = XNMP_SERVICE (object); DexFuture *fiber; DexFuture *cancellable; XnmpImplStartData *data; data = xnmp_impl_start_data_new (service->impl, invocation, arg_messaging_host_name, arg_extension_or_origin, arg_mode); fiber = dex_scheduler_spawn (NULL, 0, (DexFiberFunc) xnmp_impl_handle_start, data, (GDestroyNotify) xnmp_impl_start_data_free); cancellable = dex_cancellable_new_from_cancellable (ensure_cancellable (service, invocation)); dex_future_disown (dex_future_all_race (fiber, cancellable, NULL)); return G_DBUS_METHOD_INVOCATION_HANDLED; } static gboolean handle_close (XnmpDbusNativeMessagingProxy *object, GDBusMethodInvocation *invocation, const char *arg_handle, GVariant *arg_options) { XnmpService *service = XNMP_SERVICE (object); DexFuture *fiber; DexFuture *cancellable; XnmpImplCloseData *data; data = xnmp_impl_close_data_new (service->impl, invocation, arg_handle); fiber = dex_scheduler_spawn (NULL, 0, (DexFiberFunc) xnmp_impl_handle_close, data, (GDestroyNotify) xnmp_impl_close_data_free); cancellable = dex_cancellable_new_from_cancellable (ensure_cancellable (service, invocation)); dex_future_disown (dex_future_all_race (fiber, cancellable, NULL)); return G_DBUS_METHOD_INVOCATION_HANDLED; } static void xnmp_native_messaging_proxy_iface_init (XnmpDbusNativeMessagingProxyIface *iface) { iface->handle_get_manifest = handle_get_manifest; iface->handle_start = handle_start; iface->handle_close = handle_close; } static void xnmp_service_dispose (GObject *object) { XnmpService *service = XNMP_SERVICE (object); GHashTableIter iter; GCancellable *cancellable; g_hash_table_iter_init (&iter, service->cancellables); while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &cancellable)) g_cancellable_cancel (cancellable); g_clear_object (&service->impl); g_clear_pointer (&service->cancellables, g_hash_table_unref); G_OBJECT_CLASS (xnmp_service_parent_class)->dispose (object); } static void xnmp_service_class_init (XnmpServiceClass *klass) { GObjectClass *gobject_class; gobject_class = G_OBJECT_CLASS (klass); gobject_class->dispose = xnmp_service_dispose; } static void xnmp_service_init (XnmpService *service) { } static void on_name_owner_changed (GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { const char *name, *from, *to; XnmpService *service = XNMP_SERVICE (user_data); g_autofree char *owned_sender = NULL; g_autoptr(GCancellable) cancellable = NULL; g_variant_get (parameters, "(&s&s&s)", &name, &from, &to); if (name[0] != ':' || strcmp (name, from) != 0 || strcmp (to, "") != 0) return; if (g_hash_table_steal_extended (service->cancellables, name, (gpointer *)&owned_sender, (gpointer *)&cancellable)) { g_info ("cancelling future for client %s", owned_sender); g_cancellable_cancel (cancellable); } } gboolean init_xnmp_service (GDBusConnection *connection, GError **error) { g_autoptr(XnmpService) service = NULL; XnmpDbusNativeMessagingProxy *dbus_object; GDBusInterfaceSkeleton *skeleton; g_return_val_if_fail (g_object_get_data (G_OBJECT (connection), "-xnmp-service") == NULL, FALSE); service = g_object_new (XNMP_TYPE_SERVICE, NULL); dbus_object = XNMP_DBUS_NATIVE_MESSAGING_PROXY (service); skeleton = G_DBUS_INTERFACE_SKELETON (service); service->impl = xnmp_impl_new (dbus_object); service->cancellables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); g_dbus_connection_signal_subscribe (connection, DBUS_BUS_NAME, DBUS_IFACE, "NameOwnerChanged", DBUS_OBJECT_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, on_name_owner_changed, service, NULL); xnmp_dbus_native_messaging_proxy_set_version (dbus_object, 1); if (!g_dbus_interface_skeleton_export (skeleton, connection, XNMP_OBJECT_PATH, error)) return FALSE; g_object_set_data_full (G_OBJECT (connection), "-xnmp-service", g_steal_pointer (&service), (GDestroyNotify)g_object_unref); return TRUE; } xdg-native-messaging-proxy-0.1.0/src/xnmp-service.h000066400000000000000000000016161477102471600223050ustar00rootroot00000000000000/* * Copyright © 2025 Red Hat, Inc * * SPDX-License-Identifier: LGPL-2.1-or-later * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #pragma once #include gboolean init_xnmp_service (GDBusConnection *connection, GError **error); xdg-native-messaging-proxy-0.1.0/tests/000077500000000000000000000000001477102471600200635ustar00rootroot00000000000000xdg-native-messaging-proxy-0.1.0/tests/conftest.py000066400000000000000000000110271477102471600222630ustar00rootroot00000000000000# SPDX-License-Identifier: LGPL-2.1-or-later from typing import Iterator import pytest import dbus import dbusmock import os import tempfile import subprocess import time import signal import json from pathlib import Path from dbus.mainloop.glib import DBusGMainLoop def pytest_configure() -> None: ensure_environment_set() DBusGMainLoop(set_as_default=True) def pytest_sessionfinish(session, exitstatus): # Meson and ginsttest-runner expect tests to exit with status 77 if all # tests were skipped if exitstatus == pytest.ExitCode.NO_TESTS_COLLECTED: session.exitstatus = 77 def ensure_environment_set() -> None: env_vars = [ "XDG_NATIVE_MESSAGING_PROXY_PATH", ] for env_var in env_vars: if not os.getenv(env_var): raise Exception(f"{env_var} must be set") @pytest.fixture(autouse=True) def create_test_dirs() -> Iterator[None]: env_dirs = [ "HOME", "TMPDIR", ] test_root = tempfile.TemporaryDirectory( prefix="xnmp-testroot-", ignore_cleanup_errors=True ) for env_dir in env_dirs: directory = Path(test_root.name) / env_dir.lower() directory.mkdir(mode=0o700, parents=True) os.environ[env_dir] = directory.absolute().as_posix() yield test_root.cleanup() @pytest.fixture def create_test_dbus() -> Iterator[dbusmock.DBusTestCase]: bus = dbusmock.DBusTestCase() bus.setUp() bus.start_session_bus() bus.start_system_bus() yield bus bus.tearDown() bus.tearDownClass() @pytest.fixture def dbus_con(create_test_dbus: dbusmock.DBusTestCase) -> dbus.Bus: """ Default fixture which provides the python-dbus session bus of the test. """ con = create_test_dbus.get_dbus(system_bus=False) assert con return con @pytest.fixture(autouse=True) def create_dbus_monitor(create_test_dbus) -> Iterator[subprocess.Popen | None]: if not os.getenv("XNMP_DBUS_MONITOR"): yield None return dbus_monitor = subprocess.Popen(["dbus-monitor", "--session"]) yield dbus_monitor dbus_monitor.terminate() dbus_monitor.wait() def test_dir() -> Path: return Path(__file__).resolve().parent @pytest.fixture def xnmp_host_locations(create_test_dirs) -> Path | None: return Path(os.environ["TMPDIR"]) / "native-messaging-hosts" @pytest.fixture(autouse=True) def manifests(xnmp_host_locations): nmhd = test_dir() / "native-messaging-hosts" manifests = {} xnmp_host_locations.mkdir(parents=True) for manifest_path in nmhd.glob("*.json"): manifest = json.loads(manifest_path.read_text()) assert manifest["name"] == manifest_path.stem path = manifest["path"] if path[0] != "/": manifest["path"] = (nmhd / path).absolute().as_posix() destination = xnmp_host_locations / manifest_path.name destination.write_text(json.dumps(manifest)) manifests[manifest_path.stem] = manifest return manifests @pytest.fixture def xdg_native_messaging_proxy_path() -> Path: return Path(os.environ["XDG_NATIVE_MESSAGING_PROXY_PATH"]) @pytest.fixture def xnmp_overwrite_env() -> dict[str, str]: return {} @pytest.fixture def xnmp_env( xnmp_overwrite_env: dict[str, str], xnmp_host_locations: Path | None, ) -> dict[str, str]: env = os.environ.copy() env["G_DEBUG"] = "fatal-criticals" env["G_MESSAGES_DEBUG"] = "all" env["XDG_CURRENT_DESKTOP"] = "test" if xnmp_host_locations: env["XNMP_HOST_LOCATIONS"] = xnmp_host_locations.absolute().as_posix() for key, val in xnmp_overwrite_env.items(): env[key] = val return env @pytest.fixture def xdg_native_messaging_proxy( dbus_con: dbus.Bus, xdg_native_messaging_proxy_path: Path, xnmp_env: dict[str, str], ) -> Iterator[subprocess.Popen]: if not xdg_native_messaging_proxy_path.exists(): raise FileNotFoundError(f"{xdg_native_messaging_proxy_path} does not exist") xdg_native_messaging_proxy = subprocess.Popen( [xdg_native_messaging_proxy_path], env=xnmp_env, ) while not dbus_con.name_has_owner("org.freedesktop.NativeMessagingProxy"): returncode = xdg_native_messaging_proxy.poll() if returncode is not None: raise subprocess.SubprocessError( f"xdg-native-messaging-proxy exited with {returncode}" ) time.sleep(0.1) yield xdg_native_messaging_proxy xdg_native_messaging_proxy.send_signal(signal.SIGHUP) returncode = xdg_native_messaging_proxy.wait() assert returncode == 0 xdg-native-messaging-proxy-0.1.0/tests/meson.build000066400000000000000000000004561477102471600222320ustar00rootroot00000000000000run_test = find_program('run-test.sh') pytest_args = [ meson.current_source_dir(), '--verbose', '--log-level=DEBUG', '-s', ] pytest_env = environment() pytest_env.set('BUILDDIR', meson.project_build_root()) test( 'xnmp', run_test, args:pytest_args, env: pytest_env, timeout: 120, )xdg-native-messaging-proxy-0.1.0/tests/native-messaging-hosts/000077500000000000000000000000001477102471600244625ustar00rootroot00000000000000xdg-native-messaging-proxy-0.1.0/tests/native-messaging-hosts/echo.sh000077500000000000000000000000341477102471600257340ustar00rootroot00000000000000#!/usr/bin/env sh exec cat -xdg-native-messaging-proxy-0.1.0/tests/native-messaging-hosts/org.example.cat.json000066400000000000000000000002441477102471600303440ustar00rootroot00000000000000{ "name": "org.example.cat", "description": "Cat", "path": "/usr/bin/cat", "type": "stdio", "allowed_extensions": [ "some-extension@example.org" ] }xdg-native-messaging-proxy-0.1.0/tests/native-messaging-hosts/org.example.echo.json000066400000000000000000000002411477102471600305100ustar00rootroot00000000000000{ "name": "org.example.echo", "description": "Echo", "path": "echo.sh", "type": "stdio", "allowed_extensions": [ "some-extension@example.org" ] }xdg-native-messaging-proxy-0.1.0/tests/native-messaging-hosts/org.example.writeonclose.json000066400000000000000000000003621477102471600323130ustar00rootroot00000000000000{ "name": "org.example.writeonclose", "description": "Writes a file to $TMPDIR/xnmp-write-on-close when stdin gets closed", "path": "write-on-close.py", "type": "stdio", "allowed_extensions": [ "some-extension@example.org" ] }xdg-native-messaging-proxy-0.1.0/tests/native-messaging-hosts/write-on-close.py000077500000000000000000000003221477102471600277030ustar00rootroot00000000000000#!/usr/bin/env python import sys import os from pathlib import Path while True: r = sys.stdin.buffer.read(1) if len(r) == 0: break (Path(os.environ["TMPDIR"]) / "xnmp-write-on-close").touch() xdg-native-messaging-proxy-0.1.0/tests/run-test.sh000077500000000000000000000020161477102471600222020ustar00rootroot00000000000000#!/usr/bin/env bash # # - Runs pytest with the required environment to run tests on an x-n-m-p build # - By default, the tests run on the firstbuild directory that is found inside # the source tree # - The BUILDDIR environment variable can be set to a specific build directory # - All arguments are passed along to pytest # # Examples: # # ./run-test.sh ./test_xnmp.py -k test_cat -s # set -euo pipefail function fail() { sed -n '/^#$/,/^$/p' "${BASH_SOURCE[0]}" echo "$1" exit 1 } SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) PYTEST=$(command -v "pytest-3" || command -v "pytest") || fail "pytest is missing" BUILDDIR=${BUILDDIR:-$(find "${SCRIPT_DIR}/.." -maxdepth 2 -name "build.ninja" -printf "%h\n" -quit)} [ ! -f "${BUILDDIR}/build.ninja" ] && fail "Path '${BUILDDIR}' does not appear to be a build dir" echo "Running tests on build dir: $(readlink -f "${BUILDDIR}")" echo "" export XDG_NATIVE_MESSAGING_PROXY_PATH="$BUILDDIR/src/xdg-native-messaging-proxy" exec "$PYTEST" "$@" xdg-native-messaging-proxy-0.1.0/tests/test_xnmp.py000077500000000000000000000076341477102471600224730ustar00rootroot00000000000000import os import json import xnmp import errno from pathlib import Path class TestXnmp: def test_cat(self, xdg_native_messaging_proxy, manifests, dbus_con): iface = xnmp.get_iface(dbus_con) manifest_name = "org.example.cat" manifest = manifests[manifest_name] extension = "some-extension@example.org" mode = "firefox" manifest_str = iface.GetManifest(manifest_name, mode, {}) assert json.loads(manifest_str) == manifest (stdin, stdout, stderr, handle) = iface.Start( manifest_name, extension, mode, {} ) closed_received = False def on_closed(closed_handle, options): nonlocal closed_received nonlocal handle assert closed_handle == handle closed_received = True iface.connect_to_signal("Closed", on_closed) xnmp.wait_for(lambda: closed_received) stdout_fd = stdout.take() try: result = os.read(stdout_fd, 1024) assert json.loads(result) == manifest finally: os.close(stdout_fd) def test_echo(self, xdg_native_messaging_proxy, manifests, dbus_con): iface = xnmp.get_iface(dbus_con) manifest_name = "org.example.echo" manifest = manifests[manifest_name] extension = "some-extension@example.org" mode = "firefox" manifest_str = iface.GetManifest(manifest_name, mode, {}) assert json.loads(manifest_str) == manifest (stdin, stdout, stderr, handle) = iface.Start( manifest_name, extension, mode, {} ) stdout_fd = stdout.take() stdin_fd = stdin.take() try: msg = b"this is a test" os.write(stdin_fd, msg) result = os.read(stdout_fd, 1024) assert result == msg finally: os.close(stdout_fd) os.close(stdin_fd) def test_close(self, xdg_native_messaging_proxy, manifests, dbus_con): iface = xnmp.get_iface(dbus_con) manifest_name = "org.example.echo" extension = "some-extension@example.org" mode = "firefox" (stdin, stdout, stderr, handle) = iface.Start( manifest_name, extension, mode, {} ) iface.Close(handle, {}) closed_received = False def on_closed(closed_handle, options): nonlocal closed_received nonlocal handle assert closed_handle == handle closed_received = True iface.connect_to_signal("Closed", on_closed) xnmp.wait_for(lambda: closed_received) def test_dbus_close(self, xdg_native_messaging_proxy, manifests, dbus_con): iface = xnmp.get_iface(dbus_con) manifest_name = "org.example.echo" extension = "some-extension@example.org" mode = "firefox" (stdin, stdout, stderr, handle) = iface.Start( manifest_name, extension, mode, {} ) dbus_con.close() stdin_fd = stdin.take() def fd_closed(): try: msg = b"1" os.write(stdin_fd, msg) except IOError as e: return e.errno == errno.EPIPE except Exception: return False return False try: xnmp.wait_for(fd_closed) finally: os.close(stdin_fd) def test_close_stdin(self, xdg_native_messaging_proxy, manifests, dbus_con): iface = xnmp.get_iface(dbus_con) manifest_name = "org.example.writeonclose" extension = "some-extension@example.org" mode = "firefox" (stdin, stdout, stderr, handle) = iface.Start( manifest_name, extension, mode, {} ) fpath = Path(os.environ["TMPDIR"]) / "xnmp-write-on-close" assert not fpath.exists() stdin_fd = stdin.take() os.close(stdin_fd) xnmp.wait_for(lambda: fpath.exists()) xdg-native-messaging-proxy-0.1.0/tests/xnmp.py000066400000000000000000000017701477102471600214240ustar00rootroot00000000000000from typing import Callable from gi.repository import GLib import dbus def wait(ms: int): """ Waits for the specified amount of milliseconds. """ mainloop = GLib.MainLoop() GLib.timeout_add(ms, mainloop.quit) mainloop.run() def wait_for(fn: Callable[[], bool]): """ Waits and dispatches to mainloop until the function fn returns true. This is useful in combination with a lambda which captures a variable: my_var = False def callback(): my_var = True do_something_later(callback) xdp.wait_for(lambda: my_var) """ mainloop = GLib.MainLoop() while not fn(): GLib.timeout_add(50, mainloop.quit) mainloop.run() def get_iface(dbus_con: dbus.Bus) -> dbus.Interface: return dbus.Interface( dbus_con.get_object( "org.freedesktop.NativeMessagingProxy", "/org/freedesktop/nativemessagingproxy", ), dbus_interface="org.freedesktop.NativeMessagingProxy", )