/*
 * Copyright (C) 2018, 2022 Igalia S.L.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "XDGDBusProxy.h"

#if ENABLE(BUBBLEWRAP_SANDBOX)
#include "BubblewrapLauncher.h"
#include <wtf/UniStdExtras.h>
#include <wtf/glib/GRefPtr.h>
#include <wtf/glib/GUniquePtr.h>

#if PLATFORM(GTK)
#define BASE_DIRECTORY "webkitgtk"
#elif PLATFORM(WPE)
#define BASE_DIRECTORY "wpe"
#endif

namespace WebKit {

XDGDBusProxy::XDGDBusProxy(Type type, bool allowPortals)
    : m_type(type)
{
    switch (m_type) {
    case Type::SessionBus:
        m_dbusAddress = g_getenv("DBUS_SESSION_BUS_ADDRESS");
        break;
    case Type::AccessibilityBus:
        // FIXME: Avoid blocking IO... (It is at least a one-time cost)
        if (GRefPtr<GDBusConnection> sessionBus = adoptGRef(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr))) {
            GRefPtr<GDBusMessage> msg = adoptGRef(g_dbus_message_new_method_call("org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus", "GetAddress"));
            g_dbus_message_set_body(msg.get(), g_variant_new("()"));
            GRefPtr<GDBusMessage> reply = adoptGRef(g_dbus_connection_send_message_with_reply_sync(
                sessionBus.get(), msg.get(), G_DBUS_SEND_MESSAGE_FLAGS_NONE, 30000, nullptr, nullptr, nullptr));

            if (reply) {
                GUniqueOutPtr<GError> error;
                if (g_dbus_message_to_gerror(reply.get(), &error.outPtr())) {
                    if (!g_error_matches(error.get(), G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN))
                        g_warning("Can't find a11y bus: %s", error->message);
                } else {
                    GUniqueOutPtr<char> a11yAddress;
                    g_variant_get(g_dbus_message_get_body(reply.get()), "(s)", &a11yAddress.outPtr());
                    m_dbusAddress = a11yAddress.get();
                }
            }
        }
        break;
    }

    if (m_dbusAddress.isNull() || !g_str_has_prefix(m_dbusAddress.data(), "unix:"))
        return;

    m_proxyPath = makeProxy();
    if (m_proxyPath.isNull())
        return;

    if (const char* path = strstr(m_dbusAddress.data(), "path=")) {
        path += strlen("path=");
        const char* pathEnd = path;
        while (*pathEnd && *pathEnd != ',')
            pathEnd++;

        m_path = CString(path, pathEnd - path);
    }

    m_syncFD = launch(allowPortals);
}

XDGDBusProxy::~XDGDBusProxy()
{
    if (m_syncFD != -1)
        close(m_syncFD);
}

CString XDGDBusProxy::makeProxy() const
{
    GUniquePtr<char> appRunDir(g_build_filename(g_get_user_runtime_dir(), BASE_DIRECTORY, nullptr));
    if (g_mkdir_with_parents(appRunDir.get(), 0700) == -1) {
        g_warning("Failed to mkdir for dbus proxy (%s): %s", appRunDir.get(), g_strerror(errno));
        return { };
    }

    auto proxyTemplate = [](Type type) -> const char* {
        switch (type) {
        case Type::SessionBus:
            return "bus-proxy-XXXXXX";
        case Type::AccessibilityBus:
            return "a11y-proxy-XXXXXX";
        }
        RELEASE_ASSERT_NOT_REACHED();
    };

    GUniquePtr<char> proxySocketTemplate(g_build_filename(appRunDir.get(), proxyTemplate(m_type), nullptr));
    int fd;
    if ((fd = g_mkstemp(proxySocketTemplate.get())) == -1) {
        g_warning("Failed to make socket file %s for dbus proxy: %s", proxySocketTemplate.get(), g_strerror(errno));
        return { };
    }

    close(fd);
    return proxySocketTemplate.get();
}

int XDGDBusProxy::launch(bool allowPortals) const
{
    int syncFds[2];
    if (pipe(syncFds) == -1)
        g_error("Failed to make syncfds for dbus-proxy: %s", g_strerror(errno));
    setCloseOnExec(syncFds[0]);

    GUniquePtr<char> syncFdStr(g_strdup_printf("--fd=%d", syncFds[1]));
    Vector<CString> proxyArgs = {
        m_dbusAddress, m_proxyPath,
        "--filter",
        syncFdStr.get()
    };

    auto enableLogging = [](Type type) -> bool {
        switch (type) {
        case Type::SessionBus:
            return !g_strcmp0(g_getenv("WEBKIT_ENABLE_DBUS_PROXY_LOGGING"), "1");
        case Type::AccessibilityBus:
            return !g_strcmp0(g_getenv("WEBKIT_ENABLE_A11Y_DBUS_PROXY_LOGGING"), "1");
        }
        RELEASE_ASSERT_NOT_REACHED();
    };
    if (enableLogging(m_type))
        proxyArgs.append("--log");

    switch (m_type) {
    case Type::SessionBus: {
        // GStreamers plugin install helper.
        proxyArgs.append("--call=org.freedesktop.PackageKit=org.freedesktop.PackageKit.Modify2.InstallGStreamerResources@/org/freedesktop/PackageKit");

        if (allowPortals)
            proxyArgs.append("--talk=org.freedesktop.portal.Desktop");
        break;
    }
    case Type::AccessibilityBus:
        proxyArgs.appendVector(Vector<CString> {
            "--sloppy-names",
            "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Embed@/org/a11y/atspi/accessible/root",
            "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Unembed@/org/a11y/atspi/accessible/root",
            "--call=org.a11y.atspi.Registry=org.a11y.atspi.Registry.GetRegisteredEvents@/org/a11y/atspi/registry",
            "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetKeystrokeListeners@/org/a11y/atspi/registry/deviceeventcontroller",
            "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetDeviceEventListeners@/org/a11y/atspi/registry/deviceeventcontroller",
            "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersSync@/org/a11y/atspi/registry/deviceeventcontroller",
            "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersAsync@/org/a11y/atspi/registry/deviceeventcontroller",
        });
        break;
    }

    // We have to run xdg-dbus-proxy under bubblewrap because we need /.flatpak-info to exist in
    // xdg-dbus-proxy's mount namespace. Portals may use this as a trusted way to get the
    // sandboxed process's application ID, and will break if it's missing.
    int proxyFd = argumentsToFileDescriptor(proxyArgs, "dbus-proxy");
    GUniquePtr<char> proxyArgsStr(g_strdup_printf("--args=%d", proxyFd));
    Vector<CString> args = {
        DBUS_PROXY_EXECUTABLE,
        proxyArgsStr.get(),
    };
    int nargs = args.size() + 1;
    int i = 0;
    char** argv = g_newa(char*, nargs);
    for (const auto& arg : args)
        argv[i++] = const_cast<char*>(arg.data());
    argv[i] = nullptr;

    // Warning: do not set a child setup function, because we want GIO to be able to spawn with
    // posix_spawn() rather than fork()/exec(), in order to better accomodate applications that use
    // a huge amount of memory or address space in the UI process, like Eclipse.
    GRefPtr<GSubprocessLauncher> launcher = adoptGRef(g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_INHERIT_FDS));
    g_subprocess_launcher_take_fd(launcher.get(), proxyFd, proxyFd);
    g_subprocess_launcher_take_fd(launcher.get(), syncFds[1], syncFds[1]);

    // We are purposefully leaving syncFds[0] open here.
    // xdg-dbus-proxy will exit() itself once that is closed on our exit.

    ProcessLauncher::LaunchOptions launchOptions;
    launchOptions.processType = ProcessLauncher::ProcessType::DBusProxy;
    if (m_type == Type::AccessibilityBus && !m_path.isNull())
        launchOptions.extraSandboxPaths.add(m_path, SandboxPermission::ReadOnly);
    GUniqueOutPtr<GError> error;
    GRefPtr<GSubprocess> process = bubblewrapSpawn(launcher.get(), launchOptions, argv, &error.outPtr());
    if (!process)
        g_error("Failed to start dbus proxy: %s", error->message);

    // We need to ensure the proxy has created the socket.
    // FIXME: This is more blocking IO.
    char out;
    if (read(syncFds[0], &out, 1) != 1)
        g_error("Failed to fully launch dbus-proxy: %s", g_strerror(errno));

    return syncFds[0];
}

} // namespace WebKit

#endif // ENABLE(BUBBLEWRAP_SANDBOX)
