/*
 * Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org
 * See the file COPYING for copying conditions.
 */

/*
 * This is the backend half of an org.freedesktop.FileManager1 D-Bus interface
 * handler. It catches method calls intended for that interface, and executes
 * the frontend with arguments from the calls. This is intended to work around
 * issues with PCManFM-Qt's D-Bus handling.
 *
 * Notes for reviewers:
 *
 * - Local denial-of-service is out of scope. There are easier ways for
 *   malware to pop up lots of windows or consume huge amounts of RAM.
 * - The environment from systemd's user manager is considered trusted.
 *   Attackers that can modify its environment via D-Bus can set
 *   LD_LIBRARY_PATH in the user manager, which cannot be meaningfully
 *   defended against.
 * - The program intentionally return a "success" D-Bus reply even if no
 *   frontend popup is shown. The org.freedesktop.FileManager1 spec does not
 *   specify what errors (if any) are safe to return, so always returning
 *   empty response messages is the safest option. We do however return errors
 *   if a D-Bus call points to the wrong D-Bus object path, interface, or
 *   method.
 */

#define _GNU_SOURCE

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <err.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/wait.h>
#include <signal.h>
#include <linux/close_range.h>
#include <dbus/dbus.h>
#include <systemd/sd-daemon.h>

/*
 * 4096 bytes per URI * 256 URIs = 1 MiB of data + 256 bytes of NULLs
 * that can be passed to the frontend at once.
 */
#define MAX_OPEN_PATHS 256
#define MAX_URI_LEN 4096

static const char *introspection_xml =
  "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
  " \"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
  "<node xmlns:doc=\"http://www.freedesktop.org/dbus/1.0/doc.dtd\">\n"
  "  <interface name=\"org.freedesktop.DBus.Introspectable\">\n"
  "    <method name=\"Introspect\">\n"
  "      <arg type=\"s\" name=\"data\" direction=\"out\"/>\n"
  "    </method>\n"
  "  </interface>\n"
  "  <interface name=\"org.freedesktop.FileManager1\">\n"
  "    <method name=\"ShowFolders\">\n"
  "      <arg type=\"as\" name=\"URIs\" direction=\"in\"/>\n"
  "      <arg type=\"s\" name=\"StartupId\" direction=\"in\"/>\n"
  "    </method>\n"
  "    <method name=\"ShowItems\">\n"
  "      <arg type=\"as\" name=\"URIs\" direction=\"in\"/>\n"
  "      <arg type=\"s\" name=\"StartupId\" direction=\"in\"/>\n"
  "    </method>\n"
  "    <method name=\"ShowItemProperties\">\n"
  "      <arg type=\"as\" name=\"URIs\" direction=\"in\"/>\n"
  "      <arg type=\"s\" name=\"StartupId\" direction=\"in\"/>\n"
  "    </method>\n"
  "  </interface>\n"
  "</node>\n";

DBusError error_data = { 0 };
DBusConnection *dbus_conn = NULL;

void launch_frontend_process(const char *mode_opt, char **uri_list,
  size_t uri_list_len) {
  /*
   * Most of the code here is inspired heavily by the qubes-gui-runuser
   * function 'augment_pam_env_with_systemd_env()'. We need to get systemd's
   * current set of environment variables to expose those to the frontend
   * process, otherwise we won't know what display server to use and may run
   * into theming issues.
   *
   * TODO: The environment in Non-Qubes-Whonix's user manager has some
   * worrying discrepancies when compared to the environment of bash running
   * in a qterminal window. In particular, 'PATH' is different. We may need to
   * add a 'dbus-update-activation-environment --systemd --all' call to
   * /usr/libexec/desktop-config-dist/start-lxqt-session.
   */

  DBusMessage *env_request = NULL;
  const char *systemd_manager_str = "org.freedesktop.systemd1.Manager";
  const char *environment_str = "Environment";
  dbus_bool_t ret = FALSE;
  DBusMessage *env_reply = NULL;
  int reply_type = 0;
  DBusMessageIter reply_iter = { 0 };
  DBusMessageIter reply_inner_iter = { 0 };
  char *inner_iter_typesig = NULL;
  DBusMessageIter reply_arr_iter = { 0 };
  const char *env_val = NULL;
  const char **env_arr = NULL;
  size_t env_arr_len = 0;
  const char **arg_arr = NULL;
  int current_type = 0;
  size_t uri_list_idx = 0;
  pid_t fork_pid = 0;

  env_request = dbus_message_new_method_call("org.freedesktop.systemd1",
    "/org/freedesktop/systemd1",
    "org.freedesktop.DBus.Properties",
    "Get");
  if (env_request == NULL) {
    warnx("launch_frontend_process: Failed to create D-Bus method call object!");
    goto dbus_cleanup;
  }

  ret = dbus_message_append_args(env_request,
    DBUS_TYPE_STRING, &systemd_manager_str,
    DBUS_TYPE_STRING, &environment_str,
    DBUS_TYPE_INVALID);
  if (ret == FALSE) {
    warnx("launch_frontend_process: Failed to append arguments to D-Bus method call object!");
    goto dbus_cleanup;
  }

  env_reply = dbus_connection_send_with_reply_and_block(dbus_conn,
    env_request, 500, &error_data);
  if (env_reply == NULL) {
    warnx("launch_frontend_process: Failed to request environment data from system! Error name: '%s'. Error contents: '%s'.",
      error_data.name ? error_data.name : "(null)",
      error_data.message ? error_data.message : "(null)");
    dbus_error_free(&error_data);
    goto dbus_cleanup;
  }

  reply_type = dbus_message_get_type(env_reply);
  if (reply_type != DBUS_MESSAGE_TYPE_METHOD_RETURN) {
    warnx("launch_frontend_process: Did not get method call return object from systemd!");
    goto dbus_cleanup;
  }

  ret = dbus_message_iter_init(env_reply, &reply_iter);
  if (ret == FALSE) {
    warnx("launch_frontend_process: systemd returned an empty method call return object!");
    goto dbus_cleanup;
  }

  if (dbus_message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_VARIANT) {
    warnx("launch_frontend_process: systemd did not return a variant object!");
    goto dbus_cleanup;
  }
  dbus_message_iter_recurse(&reply_iter, &reply_inner_iter);

  inner_iter_typesig = dbus_message_iter_get_signature(&reply_inner_iter);
  if (inner_iter_typesig == NULL) {
    err(1, "launch_frontend_process: Failed to allocate memory for type signature!");
  }
  if (strcmp(inner_iter_typesig, "as") != 0) {
    warnx("launch_frontend_process: Variant object from systemd does not contain a string array!");
    goto dbus_cleanup;
  }
  if (dbus_message_iter_get_arg_type(&reply_inner_iter) != DBUS_TYPE_ARRAY) {
    warnx("launch_frontend_process: Variant object from systemd reported itself as a string array, but is not an array!");
    goto dbus_cleanup;
  }
  dbus_message_iter_recurse(&reply_inner_iter, &reply_arr_iter);

  while ((current_type = dbus_message_iter_get_arg_type(&reply_arr_iter))
    != DBUS_TYPE_INVALID) {
    if (current_type != DBUS_TYPE_STRING) {
      warnx("launch_frontend_process: Non-string item found in string array!");
      goto dbus_cleanup;
    }
    dbus_message_iter_get_basic(&reply_arr_iter, &env_val);
    env_arr_len++;
    env_arr = reallocarray(env_arr, env_arr_len, sizeof(const char *));
    if (env_arr == NULL) {
      err(1, "launch_frontend_process: Failed to allocate memory for environment array!");
    }
    env_arr[env_arr_len - 1] = env_val;
    dbus_message_iter_next(&reply_arr_iter);
  }

  /* execve requires the environment array to be null terminated */
  env_arr_len++;
  env_arr = reallocarray(env_arr, env_arr_len, sizeof(const char *));
  if (env_arr == NULL) {
    err(1, "launch_frontend_process: Failed to allocate memory for environment array!");
  }
  env_arr[env_arr_len - 1] = NULL;

  /*
   * This should be impossible since uri_list_len is originally a signed
   * integer in handle_filemanager_method_call and is then cast to a size_t,
   * but we check for this anyway.
   */
  if (uri_list_len > SIZE_MAX - 4) {
    warnx("launch_frontend_process: URI list too large!");
    goto dbus_cleanup;
  }

  /* Now that we have the environment array, we can build the arg array. */
  arg_arr = reallocarray(arg_arr, uri_list_len + 4, sizeof(const char *));
  if (arg_arr == NULL) {
    err(1, "launch_frontend_process: Failed to allocate memory for argument array!");
  }
  arg_arr[0] = "/usr/bin/fm-shim-frontend";
  arg_arr[1] = mode_opt;
  arg_arr[2] = "--";
  for (uri_list_idx = 0; uri_list_idx < uri_list_len; uri_list_idx++) {
    arg_arr[uri_list_idx + 3] = uri_list[uri_list_idx];
  }
  arg_arr[uri_list_len + 3] = NULL;

  /* Fork and exec the frontend */
  fork_pid = fork();
  if (fork_pid == -1) {
    err(1, "launch_frontend_process: fork failed");
  } else if (fork_pid == 0) {
    /* child - close inherited fds (except stdin/stdout/stderr) */
    if (close_range(3, ~0U, CLOSE_RANGE_UNSHARE) == -1) {
      warn("launch_frontend_process: close_range failed");
      _exit(1);
    }
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
    if (execve(arg_arr[0], (char **)arg_arr, (char **)env_arr) == -1) {
      warn("launch_frontend_process: Could not exec '/usr/bin/fm-shim-frontend'");
      _exit(1);
    }
#pragma GCC diagnostic pop
  }

  /* parent */

dbus_cleanup:
  /* Don't free the elements of arg_arr, they are not freeable. */
  if (arg_arr != NULL) {
    free(arg_arr);
  }
  /* Ditto for env_arr */
  if (env_arr != NULL) {
    free(env_arr);
  }
  if (inner_iter_typesig != NULL) {
    dbus_free(inner_iter_typesig);
  }
  if (env_reply != NULL) {
    dbus_message_unref(env_reply);
  }
  if (env_request != NULL) {
    dbus_message_unref(env_request);
  }
}

static void handle_introspect(DBusMessage *dbus_msg) {
  DBusMessage *method_return = NULL;

  method_return = dbus_message_new_method_return(dbus_msg);
  if (method_return == NULL) {
    errx(1, "handle_introspect: Could not allocate memory for a method return!");
  }
  if (dbus_message_append_args(method_return,
    DBUS_TYPE_STRING, &introspection_xml,
    DBUS_TYPE_INVALID) == FALSE) {
    errx(1, "handle_introspect: Could not append introspection XML to method return!");
  }
  if (dbus_connection_send(dbus_conn, method_return, NULL) == FALSE) {
    errx(1, "handle_introspect: Could not queue method return!");
  }
  dbus_connection_flush(dbus_conn);
  dbus_message_unref(method_return);
}

/*
 * All three of the file manager method calls take the same arguments, and we
 * do the hard work in the frontend, so we can use the same function here to
 * handle all the calls.
 */
void handle_filemanager_method_call(DBusMessage *dbus_msg,
  const char *method_call_name, const char *mode_opt) {
  char **uri_list = NULL;
  int uri_list_len_int = 0;
  size_t uri_list_len = 0;
  dbus_bool_t did_extract_args = FALSE;
  const char *startup_id = NULL;
  DBusMessage *method_return = NULL;
  size_t uri_list_idx = 0;
  bool do_launch_frontend = false;

  did_extract_args = dbus_message_get_args(dbus_msg,
    &error_data,
    DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &uri_list, &uri_list_len_int,
    DBUS_TYPE_STRING, &startup_id,
    DBUS_TYPE_INVALID);
  if (!did_extract_args) {
    warnx("handle_filemanager_method_call: Received a D-Bus method call for method '%s', but could not get arguments. Error name: '%s'. Error contents: '%s'.",
      method_call_name,
      error_data.name ? error_data.name : "(null)",
      error_data.message ? error_data.message : "(null)");
    dbus_error_free(&error_data);
    goto method_cleanup;
  }

  if (uri_list_len_int < 0) {
    warnx("handle_filemanager_method_call: Negative URI list length received!");
    goto method_send_reply;
  }
  if (uri_list_len_int > MAX_OPEN_PATHS) {
    warnx("handle_filemanager_method_call: Received a D-Bus method call attempting to open more than %d paths at once, ignoring.",
      MAX_OPEN_PATHS);
    goto method_send_reply;
  }
  uri_list_len = (size_t)uri_list_len_int;

  for (uri_list_idx = 0; uri_list_idx < uri_list_len; uri_list_idx++) {
    if (uri_list[uri_list_idx] == NULL) {
      warnx("handle_filemanager_method_call: Received a D-Bus method call with a NULL string, ignoring.");
      goto method_send_reply;
    }
    if (strnlen(uri_list[uri_list_idx], MAX_URI_LEN + 1) > MAX_URI_LEN) {
      warnx("handle_filemanager_method_call: Received a D-Bus method call attempting to open a path URI longer than %d bytes, ignoring.",
        MAX_URI_LEN);
      goto method_send_reply;
    }
  }

  do_launch_frontend = true;

method_send_reply:
  if (dbus_message_get_no_reply(dbus_msg) == TRUE) {
    goto launch_frontend;
  }

  method_return = dbus_message_new_method_return(dbus_msg);
  if (method_return == NULL) {
    errx(1, "handle_filemanager_method_call: Could not allocate memory for a method return!");
  }
  if (dbus_connection_send(dbus_conn, method_return, NULL) == FALSE) {
    errx(1, "handle_filemanager_method_call: Could not queue method return!");
  }
  /*
   * We could probably get away with omitting the flush, but browsers will
   * attempt to launch the file manager by themselves if they don't get a
   * return message quickly enough, so push this out as quickly as possible.
   */
  dbus_connection_flush(dbus_conn);

launch_frontend:
  if (do_launch_frontend) {
    launch_frontend_process(mode_opt, uri_list, uri_list_len);
  }

method_cleanup:
  if (method_return != NULL) {
    dbus_message_unref(method_return);
  }
  if (uri_list != NULL) {
    dbus_free_string_array(uri_list);
  }
}

void send_error_message_maybe(DBusMessage *dbus_msg, const char *error_name,
  const char *error_message) {
  if (dbus_message_get_no_reply(dbus_msg) == FALSE) {
    DBusMessage *err_reply = dbus_message_new_error(dbus_msg,
      error_name, error_message);
    if (err_reply == NULL) {
      errx(1, "send_error_message_maybe: Could not allocate memory for an unknown object error!");
    }
    if (dbus_connection_send(dbus_conn, err_reply, NULL) == FALSE) {
      errx(1, "send_error_message_maybe: Could not queue unknown object error!");
    }
    dbus_connection_flush(dbus_conn);
    dbus_message_unref(err_reply);
  }
}

int main(void) {
  struct sigaction sigchld_act = { 0 };
  int dbus_name_request_rslt = 0;

  sigchld_act.sa_handler = SIG_IGN;
  if (sigaction(SIGCHLD, &sigchld_act, NULL) == -1) {
    err(1, "Failed to enable automatic child process reaping");
  }

  dbus_error_init(&error_data);

  dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &error_data);
  if (dbus_conn == NULL) {
    errx(1, "Failed to connect to D-Bus session bus. Error name: '%s'. Error contents: '%s'.",
      error_data.name ? error_data.name : "(null)",
      error_data.message ? error_data.message : "(null)");
  }

  dbus_name_request_rslt = dbus_bus_request_name(dbus_conn,
    "org.freedesktop.FileManager1",
    DBUS_NAME_FLAG_REPLACE_EXISTING,
    &error_data);
  if (dbus_name_request_rslt == -1) {
    errx(1, "Failed to request 'org.freedesktop.FileManager1' name from D-Bus. Error name: '%s'. Error contents: '%s'.",
      error_data.name ? error_data.name : "(null)",
      error_data.message ? error_data.message : "(null)");
  }
  switch(dbus_name_request_rslt) {
  case DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER:
    printf("Successfully obtained 'org.freedesktop.FileManager1' name from D-Bus.\n");
    sd_notify(0, "READY=1");
    sd_notify(0, "STATUS=Running");
    break;
  case DBUS_REQUEST_NAME_REPLY_IN_QUEUE:
    warnx("Requested 'org.freedesktop.FileManager1' name from D-Bus, but was placed in queue.");
    warnx("This may be a security risk! Please report this bug!");
    /*
     * sd_notify(0, "READY=1") is intentionally omitted here, so that
     * systemcheck will show a warning about the system not being ready
     */
    sd_notify(0, "STATUS=Running, security issue detected");
    break;
  case DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER:
    warnx("Possible bug, we already own 'org.freedesktop.FileManager1' name?");
    sd_notify(0, "READY=1");
    sd_notify(0, "STATUS=Running, possible bug detected");
    break;
  default:
    errx(1, "Unexpected name request result '%d'!", dbus_name_request_rslt);
    break;
  }

  while (true) {
    DBusMessage *dbus_msg = NULL;
    int msg_type = 0;
    const char *dbus_msg_obj_path = NULL;
    const char *dbus_msg_iface = NULL;
    const char *dbus_msg_method = NULL;

    if ((dbus_msg = dbus_connection_pop_message(dbus_conn)) == NULL) {
      if (!dbus_connection_read_write(dbus_conn, -1)) {
        break;
      }
      continue;
    }

    msg_type = dbus_message_get_type(dbus_msg);
    if (msg_type != DBUS_MESSAGE_TYPE_METHOD_CALL) {
      /* Commented out because this happens normally during launch. */
      /*warnx("Received a D-Bus message that was not a method call!");*/
      goto cleanup;
    }

    dbus_msg_obj_path = dbus_message_get_path(dbus_msg);
    if (dbus_msg_obj_path == NULL) {
      warnx("Received a D-Bus method call with a NULL object path!");
      goto cleanup;
    }
    if (strcmp(dbus_msg_obj_path, "/org/freedesktop/FileManager1") != 0) {
      warnx("Received a D-Bus method call for object path '%s', rather than '/org/freedesktop/FileManager1'!",
        dbus_msg_obj_path);
      send_error_message_maybe(dbus_msg, DBUS_ERROR_UNKNOWN_OBJECT,
        "Unknown object path");
      goto cleanup;
    }

    dbus_msg_iface = dbus_message_get_interface(dbus_msg);
    if (dbus_msg_iface == NULL) {
      warnx("Received a D-Bus method call with a NULL interface!");
      goto cleanup;
    }

    dbus_msg_method = dbus_message_get_member(dbus_msg);
    if (dbus_msg_method == NULL) {
      warnx("Received a D-Bus method call with a NULL method!");
      goto cleanup;
    }

    if (strcmp(dbus_msg_iface, "org.freedesktop.DBus.Introspectable") == 0) {
      if (strcmp(dbus_msg_method, "Introspect") == 0) {
        handle_introspect(dbus_msg);
      } else {
        warnx("Received a D-Bus method call for unrecognized method '%s' on interface '%s'!",
          dbus_msg_method, dbus_msg_iface);
        send_error_message_maybe(dbus_msg, DBUS_ERROR_UNKNOWN_METHOD,
          "Unknown method");
      }
      goto cleanup;
    }

    if (strcmp(dbus_msg_iface, "org.freedesktop.FileManager1") == 0) {
      if (strcmp(dbus_msg_method, "ShowFolders") == 0) {
        handle_filemanager_method_call(dbus_msg, "ShowFolders",
          "--show-folders");
      } else if (strcmp(dbus_msg_method, "ShowItems") == 0) {
        handle_filemanager_method_call(dbus_msg, "ShowItems", "--show-items");
      } else if (strcmp(dbus_msg_method, "ShowItemProperties") == 0) {
        handle_filemanager_method_call(dbus_msg, "ShowItemProperties",
          "--show-item-properties");
      } else {
        warnx("Received a D-Bus method call for unrecognized method '%s' on interface '%s'!",
          dbus_msg_method, dbus_msg_iface);
        send_error_message_maybe(dbus_msg, DBUS_ERROR_UNKNOWN_METHOD,
          "Unknown method");
      }
      goto cleanup;
    }

    warnx("Received a D-Bus method call for unknown interface '%s'!",
      dbus_msg_iface);
    send_error_message_maybe(dbus_msg, DBUS_ERROR_UNKNOWN_INTERFACE,
      "Unknown interface");

cleanup:
    if (dbus_msg != NULL) {
      dbus_message_unref(dbus_msg);
      dbus_msg = NULL;
    }
    dbus_msg_obj_path = NULL;
    dbus_msg_iface = NULL;
    dbus_msg_method = NULL;
  }

  dbus_connection_unref(dbus_conn);
  dbus_error_free(&error_data);
}
