From 2f212fc2a6aaf698ce5040fffa14773de1755a6e Mon Sep 17 00:00:00 2001
From: Ludovico de Nittis <ludovico.denittis@collabora.com>
Date: Wed, 31 Mar 2021 14:44:52 +0200
Subject: [PATCH] urlopen: Add a more clever xdg-open executable

When inside a Steam Runtime container we currently make `xdg-open`
points to the `flatpak-xdg-utils` implementation that relies on
`xdg-desktop-portal`. But if an user doesn't have a working
`xdg-desktop-portal` it will fail.

Additionally the `steam://` URLs might end up opening another instance
of the Steam client, even if one instance was already running. This can
happen if more than one version are installed, e.g. the Steam client
from the distro repositories and the Flatpak version.

With this alternative implementation we try a more clever approach that
should workaround the `xdg-open` shortcomings that we experienced in our
Steam related use cases.

Signed-off-by: Ludovico de Nittis <ludovico.denittis@collabora.com>
[smcv: Install all /usr/bin/steam-runtime-* executables]
Signed-off-by: Simon McVittie <smcv@collabora.com>
---
 bin/meson.build                        |  11 ++
 bin/urlopen.c                          | 217 +++++++++++++++++++++++++
 bin/urlopen.md                         |  44 +++++
 debian/steam-runtime-tools-bin.install |   5 +-
 4 files changed, 273 insertions(+), 4 deletions(-)
 create mode 100644 bin/urlopen.c
 create mode 100644 bin/urlopen.md

diff --git a/bin/meson.build b/bin/meson.build
index 8604b4085..d0d197547 100644
--- a/bin/meson.build
+++ b/bin/meson.build
@@ -61,12 +61,23 @@ executable(
   install_rpath : bin_rpath,
 )
 
+executable(
+  'steam-runtime-urlopen',
+  'urlopen.c',
+  dependencies : [glib, gio_unix, libglnx_dep, libsteamrt_static_dep],
+  install : true,
+  # Use GLib from the adjacent libdir, ignoring LD_LIBRARY_PATH
+  build_rpath : bin_rpath,
+  install_rpath : bin_rpath,
+)
+
 if get_option('man')
   foreach bin_name : [
     'check-requirements',
     'input-monitor',
     'steam-remote',
     'system-info',
+    'urlopen',
   ]
     custom_target(
       'steam-runtime-' + bin_name + '.1',
diff --git a/bin/urlopen.c b/bin/urlopen.c
new file mode 100644
index 000000000..f9297d255
--- /dev/null
+++ b/bin/urlopen.c
@@ -0,0 +1,217 @@
+/*
+ * Copyright © 2017 Red Hat, Inc
+ * Copyright © 2021 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Florian Müllner <fmuellner@gnome.org>
+ *       Matthias Clasen <mclasen@redhat.com>
+ */
+
+/*
+ * Alternative executable to the canonical 'xdg-open' with a better handling
+ * of Steam's URLs.
+ * Loosely based on the xdg-open implementation of flatpak-xdg-utils.
+ */
+
+#include <libglnx.h>
+
+#include "steam-runtime-tools/glib-backports-internal.h"
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gunixfdlist.h>
+
+#include <steam-runtime-tools/utils-internal.h>
+
+typedef GUnixFDList AutoUnixFDList;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(AutoUnixFDList, g_object_unref)
+
+static gchar **uris = NULL;
+static gboolean opt_print_help = FALSE;
+static gboolean opt_print_version = FALSE;
+
+static const GOptionEntry option_entries[] =
+{
+  /* Imitate the allowed options of the real 'xdg-open' */
+  { "manual", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &opt_print_help,
+    NULL, NULL },
+  { "version", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_print_version,
+    "Print version number and exit", NULL },
+  { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME_ARRAY,
+    &uris, NULL, NULL },
+  { NULL }
+};
+
+static gboolean
+open_with_portal (const char *uri_or_filename,
+                  GError **error)
+{
+  g_autoptr(GDBusConnection) connection = NULL;
+
+  connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error);
+
+  if (connection != NULL)
+    {
+      const gchar *portal_bus_name = "org.freedesktop.portal.Desktop";
+      const gchar *portal_object_path = "/org/freedesktop/portal/desktop";
+      const gchar *portal_iface_name = "org.freedesktop.portal.OpenURI";
+      const gchar *method_name = NULL;
+      GVariant *arguments = NULL;   /* floating */
+      GVariantBuilder opt_builder;
+      g_autoptr(AutoUnixFDList) fd_list = NULL;
+      g_autoptr(GFile) file = NULL;
+      g_autoptr(GVariant) result = NULL;
+
+      g_debug ("Trying the D-Bus desktop portal");
+
+      g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
+      file = g_file_new_for_commandline_arg (uri_or_filename);
+
+      if (g_file_is_native (file))
+        {
+          /* The canonical 'xdg-open' also handles paths. We try to replicate
+           * that too, but it might not always work because the container
+           * inside and outside filesystem structure might be different. */
+          g_autofree gchar *path = NULL;
+          int fd;
+
+          path = g_file_get_path (file);
+          fd = open (path, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
+          if (fd < 0)
+            {
+              return glnx_throw_errno_prefix (error, "Failed to open '%s'", path);
+            }
+
+          fd_list = g_unix_fd_list_new_from_array (&fd, 1); /* adopts the fd */
+
+          arguments = g_variant_new ("(sh@a{sv})",
+                                     "", 0,
+                                     g_variant_builder_end (&opt_builder));
+          method_name = "OpenFile";
+        }
+      else
+        {
+          arguments = g_variant_new ("(ss@a{sv})",
+                                     "", uri_or_filename,
+                                     g_variant_builder_end (&opt_builder));
+          method_name = "OpenURI";
+        }
+
+      result = g_dbus_connection_call_with_unix_fd_list_sync (connection,
+                                                              portal_bus_name,
+                                                              portal_object_path,
+                                                              portal_iface_name,
+                                                              method_name,
+                                                              /* sinks floating reference */
+                                                              g_steal_pointer (&arguments),
+                                                              NULL,
+                                                              G_DBUS_CALL_FLAGS_NONE,
+                                                              -1,
+                                                              fd_list,
+                                                              NULL,
+                                                              NULL,
+                                                              error);
+
+      if (result == NULL)
+        return glnx_prefix_error (error,
+                                  "Unable to open URL with xdg-desktop-portal");
+      else
+        return TRUE;
+    }
+  else
+    {
+      return glnx_prefix_error (error,
+                                "Unable to connect to D-Bus session bus");
+    }
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  const gchar *uri;
+  g_autofree gchar *scheme = NULL;
+  g_autoptr(GOptionContext) option_context = NULL;
+  g_autoptr(GError) portal_error = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_set_prgname ("steam-runtime-urlopen");
+
+  option_context = g_option_context_new ("{ file | URL }");
+  g_option_context_add_main_entries (option_context, option_entries, NULL);
+
+  if (!g_option_context_parse (option_context, &argc, &argv, &error))
+    {
+      g_printerr ("%s: %s\n", g_get_prgname (), error->message);
+      return 1;
+    }
+
+  if (opt_print_version)
+    {
+      /* Simply print the version number, similarly to the real xdg-open */
+      g_print ("%s\n", VERSION);
+      return EXIT_SUCCESS;
+    }
+
+  if (uris == NULL || g_strv_length (uris) > 1)
+    {
+      g_autofree gchar *help = g_option_context_get_help (option_context, TRUE, NULL);
+      g_print ("%s\n", help);
+      return 1;
+    }
+
+  /* In reality this could also be a path, but we call it "uri" for simplicity */
+  uri = uris[0];
+
+  scheme = g_uri_parse_scheme (uri);
+
+  if (scheme != NULL && (g_ascii_strcasecmp (scheme, "steamlink") == 0
+                         || g_ascii_strcasecmp (scheme, "steam") == 0))
+    {
+      g_debug ("Passing the URL '%s' to the Steam pipe", uri);
+      if (!_srt_steam_command_via_pipe (&uri, 1, &error))
+        {
+          g_printerr ("%s: %s\n", g_get_prgname (), error->message);
+          return 4;
+        }
+
+      return EXIT_SUCCESS;
+    }
+
+  if (open_with_portal (uri, &portal_error))
+    return EXIT_SUCCESS;
+
+  if (scheme != NULL && (g_ascii_strcasecmp (scheme, "http") == 0
+                         || g_ascii_strcasecmp (scheme, "https") == 0))
+    {
+      g_autofree gchar *steam_url = NULL;
+      steam_url = g_strjoin ("/", "steam://openurl", uri, NULL);
+
+      g_debug ("Passing the URL '%s' to the Steam pipe", steam_url);
+      if (!_srt_steam_command_via_pipe ((const gchar **) &steam_url, 1, &error))
+        {
+          g_printerr ("%s: %s\n", g_get_prgname (), error->message);
+          return 4;
+        }
+
+      return EXIT_SUCCESS;
+    }
+
+  g_printerr ("%s: %s\n", g_get_prgname (), portal_error->message);
+  return 4;
+}
diff --git a/bin/urlopen.md b/bin/urlopen.md
new file mode 100644
index 000000000..3cf8ab923
--- /dev/null
+++ b/bin/urlopen.md
@@ -0,0 +1,44 @@
+---
+title: steam-runtime-urlopen
+section: 1
+...
+
+# NAME
+
+steam-runtime-urlopen - Alternative xdg-open executable
+
+# SYNOPSIS
+
+**steam-runtime-urlopen** [*OPTIONS*]... {*file* | *URL*}
+
+# DESCRIPTION
+
+**steam-runtime-urlopen** is an alternative xdg-open executable that provides
+better handling for Steam URLs. This tool is expected to be executed from
+inside a Steam Runtime container.
+
+# OPTIONS
+
+**--version**
+:   Instead of opening the file/URL, write in output the version number.
+
+# OUTPUT
+
+On success, the output will be empty.
+
+On error, a human-readable message is shown on standard error.
+
+# EXIT STATUS
+
+The exit status is intended to be the same as for **xdg-open**(1):
+
+0
+:   Success.
+
+1
+:   Error in command line syntax.
+
+4
+:   The action failed.
+
+<!-- vim:set sw=4 sts=4 et: -->
diff --git a/debian/steam-runtime-tools-bin.install b/debian/steam-runtime-tools-bin.install
index 6a6434d18..173b84854 100644
--- a/debian/steam-runtime-tools-bin.install
+++ b/debian/steam-runtime-tools-bin.install
@@ -1,5 +1,2 @@
-usr/bin/steam-runtime-check-requirements
-usr/bin/steam-runtime-input-monitor
-usr/bin/steam-runtime-steam-remote
-usr/bin/steam-runtime-system-info
+usr/bin/steam-runtime-*
 usr/share/man/man1/steam-runtime-*
-- 
GitLab