foxygit / TransTUI Log in
commits tags

/src/daemon.c · 6.6 KB

raw
#include "daemon.h"
#include "../third_party/cJSON.h"
#include "util.h"

#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

int daemon_host_is_local(const char *host)
{
    return strcmp(host, "127.0.0.1") == 0 || strcmp(host, "localhost") == 0 ||
           strcmp(host, "::1") == 0;
}

int daemon_start(char *err, size_t errlen)
{
    pid_t pid = fork();
    if (pid < 0) {
        snprintf(err, errlen, "fork failed: %s", strerror(errno));
        return -1;
    }

    if (pid == 0) {
        /* transmission-daemon daemonizes (double-forks) itself by default,
         * so this child just execs it and lets it detach on its own. Give
         * it a clean session and /dev/null fds so it doesn't inherit our
         * controlling terminal or fight ncurses for it. */
        setsid();
        int devnull = open("/dev/null", O_RDWR);
        if (devnull >= 0) {
            dup2(devnull, STDIN_FILENO);
            dup2(devnull, STDOUT_FILENO);
            dup2(devnull, STDERR_FILENO);
            if (devnull > STDERR_FILENO)
                close(devnull);
        }
        execlp("transmission-daemon", "transmission-daemon", (char *)NULL);
        _exit(127); /* only reached if execlp failed */
    }

    int status = 0;
    if (waitpid(pid, &status, 0) < 0) {
        snprintf(err, errlen, "waitpid failed: %s", strerror(errno));
        return -1;
    }
    if (WIFEXITED(status) && WEXITSTATUS(status) == 127) {
        if (access("/etc/debian_version", F_OK) == 0)
            snprintf(err, errlen,
                      "transmission-daemon is not installed. Run: sudo apt install transmission-daemon");
        else
            snprintf(err, errlen, "transmission-daemon is not installed (install it via your package manager)");
        return -1;
    }
    if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
        snprintf(err, errlen, "transmission-daemon exited with code %d", WEXITSTATUS(status));
        return -1;
    }
    if (WIFSIGNALED(status)) {
        snprintf(err, errlen, "transmission-daemon was killed by signal %d", WTERMSIG(status));
        return -1;
    }
    return 0;
}

/* Runs argv[0] with argv as its arguments (no shell involved - the caller's
 * values, e.g. a user-chosen password, never pass through a shell so there's
 * nothing to escape or inject). Inherits our stdin/stdout/stderr so sudo can
 * prompt on the real terminal. */
static int run_argv(char *const argv[], char *err, size_t errlen)
{
    pid_t pid = fork();
    if (pid < 0) {
        if (err)
            snprintf(err, errlen, "fork failed: %s", strerror(errno));
        return -1;
    }
    if (pid == 0) {
        execvp(argv[0], argv);
        _exit(127);
    }
    int status = 0;
    if (waitpid(pid, &status, 0) < 0) {
        if (err)
            snprintf(err, errlen, "waitpid failed: %s", strerror(errno));
        return -1;
    }
    if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
        if (err)
            snprintf(err, errlen, "%s failed (cancelled at sudo?)", argv[0]);
        return -1;
    }
    return 0;
}

#define TRANSMISSION_SETTINGS_PATH "/etc/transmission-daemon/settings.json"
#define TRANSMISSION_SERVICE "transmission-daemon"

int daemon_fix_auth(const char *username, const char *password, char *err, size_t errlen)
{
    size_t flen = 0;
    unsigned char *data = read_file_all(TRANSMISSION_SETTINGS_PATH, &flen);
    if (!data) {
        snprintf(err, errlen, "cannot find %s - are you running Debian's transmission-daemon package?",
                  TRANSMISSION_SETTINGS_PATH);
        return -1;
    }
    cJSON *root = cJSON_ParseWithLength((const char *)data, flen);
    free(data);
    if (!root) {
        snprintf(err, errlen, "could not parse %s", TRANSMISSION_SETTINGS_PATH);
        return -1;
    }

    if (password && password[0]) {
        cJSON_ReplaceItemInObjectCaseSensitive(root, "rpc-username", cJSON_CreateString(username));
        cJSON_ReplaceItemInObjectCaseSensitive(root, "rpc-password", cJSON_CreateString(password));
        cJSON_ReplaceItemInObjectCaseSensitive(root, "rpc-authentication-required", cJSON_CreateBool(1));
    } else {
        cJSON_ReplaceItemInObjectCaseSensitive(root, "rpc-authentication-required", cJSON_CreateBool(0));
    }

    char *out = cJSON_Print(root);
    cJSON_Delete(root);
    if (!out) {
        snprintf(err, errlen, "out of memory");
        return -1;
    }

    char tmp_path[] = "/tmp/transtui-settings-XXXXXX";
    int fd = mkstemp(tmp_path);
    if (fd < 0) {
        snprintf(err, errlen, "mkstemp failed: %s", strerror(errno));
        cJSON_free(out);
        return -1;
    }
    size_t outlen = strlen(out);
    ssize_t wr = write(fd, out, outlen);
    close(fd);
    cJSON_free(out);
    if (wr < 0 || (size_t)wr != outlen) {
        snprintf(err, errlen, "could not write temp file");
        unlink(tmp_path);
        return -1;
    }

    char *stop_argv[] = {"sudo", "systemctl", "stop", TRANSMISSION_SERVICE, NULL};
    char *cp_argv[] = {"sudo", "cp", tmp_path, TRANSMISSION_SETTINGS_PATH, NULL};
    char *start_argv[] = {"sudo", "systemctl", "start", TRANSMISSION_SERVICE, NULL};

    int rc = run_argv(stop_argv, err, errlen);
    if (rc == 0)
        rc = run_argv(cp_argv, err, errlen);

    /* Always try to bring the service back up, even if the copy failed,
     * so we don't leave it stopped - but don't let a start failure here
     * clobber a more specific error from the steps above. */
    char start_err[128];
    int start_rc = run_argv(start_argv, start_err, sizeof(start_err));
    if (rc == 0 && start_rc != 0) {
        snprintf(err, errlen, "%s", start_err);
        rc = -1;
    }

    unlink(tmp_path);
    return rc;
}

#define TRANSMISSION_DAEMON_USER "debian-transmission"

int daemon_fix_download_dir_perms(const char *dir, char *err, size_t errlen)
{
    struct stat dst;
    if (stat(dir, &dst) != 0) {
        snprintf(err, errlen, "cannot stat %s: %s", dir, strerror(errno));
        return -1;
    }
    struct group *gr = getgrgid(dst.st_gid);
    if (!gr) {
        snprintf(err, errlen, "cannot resolve group for %s", dir);
        return -1;
    }

    char *usermod_argv[] = {"sudo", "usermod", "-aG", gr->gr_name, TRANSMISSION_DAEMON_USER, NULL};
    if (run_argv(usermod_argv, err, errlen) != 0)
        return -1;

    if (chmod(dir, (dst.st_mode & 07777) | S_IRGRP | S_IWGRP | S_IXGRP) != 0) {
        snprintf(err, errlen, "chmod %s failed: %s", dir, strerror(errno));
        return -1;
    }

    char *restart_argv[] = {"sudo", "systemctl", "restart", TRANSMISSION_SERVICE, NULL};
    return run_argv(restart_argv, err, errlen);
}