/*
20140214
20241208 - reformated using clang-format
Jan Mojzis
Public domain.
*/

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <pwd.h>
#include "str.h"
#include "open.h"
#include "dropuidgid.h"
#include "global.h"
#include "e.h"
#include "log.h"
#include "sshcrypto.h"
#include "getln.h"
#include "bug.h"
#include "limit.h"
#include "subprocess.h"

static int findnameandkey(const char *keyname, const char *key, char *x) {

    if (!str_start(x, keyname)) return 0;
    x += str_len(keyname);
    if (*x != ' ') return 0;
    x += 1;
    if (!str_start(x, key)) return 0;
    x += str_len(key);
    if (*x == ' ') return 1;
    if (*x == '\n') return 1;
    if (*x == '\r') return 1;
    if (*x == 0) return 1;
    return 0;
}

static void check(uid_t uid, const char *d, const char *f, long long *err) {

    struct stat st;
    int e = 0;

    if (f) {
        if (stat(f, &st) == -1) {
            log_w4("auth: unable to stat file: ", d, "/", f);
            e = 1;
        }
        if (e == 0 && !S_ISREG(st.st_mode)) {
            errno = EACCES;
            log_w4("auth: non-regular file: ", d, "/", f);
            e = 1;
        }
    }
    else {
        if (stat(d, &st) == -1) {
            log_w2("auth: unable to stat directory: ", d);
            e = 1;
        }
        if (e == 0 && !S_ISDIR(st.st_mode)) {
            errno = ENOTDIR;
            log_w2("auth: not a directory: ", d);
            e = 1;
        }
    }
    if (e == 0 && (st.st_mode & 022) != 0) {
        errno = EACCES;
        if (f)
            log_w4("auth: bad mode: writable by group or others: ", d, "/",
                   f);
        else
            log_w2("auth: bad mode: writable by group or others: ", d);
        e = 1;
    }
    if (e == 0 && st.st_uid != uid && st.st_uid != 0) {
        errno = EACCES;
        if (f) log_w4("auth: bad owner: ", d, "/", f);
        else log_w2("auth: bad owner: ", d);
        e = 1;
    }

    if (e)
        *err = 1;
    else if (f)
        log_d4("auth: path: ok: ", d, "/", f);
    else
        log_d2("auth: path: ok: ", d);
}

int subprocess_auth_checkpath_(char *path, long long pathlen, uid_t uid,
                               int fd) {

    long long err = 0, j, i;
    char *real;
    struct stat fst, st;

    if (fstat(fd, &fst) == -1) return 0;
    if (!S_ISREG(fst.st_mode) || (fst.st_mode & 022) != 0 ||
        (fst.st_uid != uid && fst.st_uid != 0))
        return 0;

    real = realpath("authorized_keys", 0);
    if (!real) return 0;
    if (!str_copyn(path, pathlen, real)) {
        free(real);
        return 0;
    }
    free(real);

    /* realpath() is name-based; tie its result to the open descriptor */
    if (stat(path, &st) == -1 || st.st_dev != fst.st_dev ||
        st.st_ino != fst.st_ino)
        return 0;

    i = str_len(path);
    for (j = i; j >= 0; --j) {
        if (path[j] == '/') {
            path[j] = 0;
            i = j;
            break;
        }
    }

    if (i > 0) {
        do {
            check(uid, path, 0, &err);
            for (j = i; j >= 0; --j) {
                if (path[j] == '/') {
                    path[j] = 0;
                    i = j;
                    break;
                }
            }
        } while (j > 0);
    }

    check(uid, "/", 0, &err);

    return (err == 0);
}

int subprocess_auth_authorizedkeys_(const char *keyname, const char *key,
                                    const char *dir, char *buf,
                                    long long bufmax) {

    int fd = -1;
    int r;

    fd = open_read("authorized_keys");
    if (fd == -1) {
        log_w3("auth: unable to open file: ", dir, "/.ssh/authorized_keys");
        return 0;
    }

    if (!subprocess_auth_checkpath_(buf, bufmax, geteuid(), fd)) {
        errno = EACCES;
        log_w3("auth: unable to read file: ", dir, "/.ssh/authorized_keys");
        close(fd);
        return 0;
    }

    do {
        r = getln_nul_as_newline(fd, buf, bufmax);
        if (r == -1) {
            log_w3("auth: unable to read from file ", dir,
                   "/.ssh/authorized_keys");
            close(fd);
            return 0;
        }
        if (findnameandkey(keyname, key, buf)) {
            close(fd);
            return 1; /* authorized */
        }
    } while (r > 0);
    close(fd);
    log_w1("auth: unable to authorize using authorized_keys: key not found");
    return 0;
}

/*
The 'subprocess_auth' is used for authorization using
~/.ssh/authorized_keys file.
*/
int subprocess_auth(const char *account, const char *keyname, const char *key) {

    pid_t pid, r;
    int status;

    pid = fork();
    if (pid == -1) return -1;
    if (pid == 0) {

#define buf global_bspace2 /* reusing global buffer */
        struct passwd *pw;

        if (!account || !keyname || !key) bug_inval();
        if (sshcrypto_sign_BASE64PUBLICKEYMIN > str_len(key) + 1) bug_inval();

        /* drop privileges */
        pw = getpwnam(account);
        if (!pw) {
            log_w3("auth: account ", account, ": not exist");
            global_die(111);
        }
        if (!dropuidgid(pw->pw_name, pw->pw_uid, pw->pw_gid)) {
            log_w2("auth: unable to drop privileges to account ", account);
            global_die(111);
        }

        /* change directory to ~/.ssh */
        if (chdir(pw->pw_dir) == -1) {
            log_w2("auth: unable to change directory to ", pw->pw_dir);
            global_die(111);
        }
        if (chdir(".ssh") == -1) {
            log_w3("auth: unable to change directory to ", pw->pw_dir, "/.ssh");
            global_die(111);
        }

        /* authorization starts here */
        if (!subprocess_auth_authorizedkeys_(keyname, key, pw->pw_dir,
                                             (char *) buf, sizeof buf))
            global_die(111);
        /* authorization ends here */

        global_die(0);
    }

    do {
        r = waitpid(pid, &status, 0);
    } while (r == -1 && errno == EINTR);
    if (r != pid) return -1;
    if (!WIFEXITED(status)) return -1;
    return WEXITSTATUS(status);
}
