#include <config.h>

#ifndef WITH_TLS
#include <stdio.h>

int main(void)
{
    puts("Please compile the server with --with-tls to use this feature.\n"
         "Thank you.");

    return 0;
}
#else

#include "ftpd.h"
#include "tls_extcert.h"
#include "pure-certd_p.h"
#include "safe_rw.h"

#ifdef WITH_DMALLOC
# include <dmalloc.h>
#endif

static void setcloexec(const int fd)
{
    fcntl(fd, F_SETFD, FD_CLOEXEC);
}

static int closedesc_all(const int closestdin)
{
    int fodder;

    if (closestdin != 0) {
        (void) close(0);
        if ((fodder = open("/dev/null", O_RDONLY)) == -1) {
            return -1;
        }
        (void) dup2(fodder, 0);
        if (fodder > 0) {
            (void) close(fodder);
        }
    }
    if ((fodder = open("/dev/null", O_WRONLY)) == -1) {
        return -1;
    }
    (void) dup2(fodder, 1);
    (void) dup2(1, 2);
    if (fodder > 2) {
        (void) close(fodder);
    }
    return 0;
}

static void dodaemonize(void)
{
    pid_t child;

    if (daemonize != 0) {
        if ((child = fork()) == (pid_t) -1) {
            perror("Daemonization failed - fork");
            return;
        } else if (child != (pid_t) 0) {
            _exit(EXIT_SUCCESS);
        } else if (setsid() == (pid_t) -1) {
            perror("Daemonization failed : setsid");
        }
        (void) chdir("/");
#ifdef HAVE_CLOSEFROM
        (void) closefrom(3);
#endif
        (void) closedesc_all(1);
    }
}

static int init(void)
{
#ifndef NON_ROOT_FTP
    if (geteuid() != (uid_t) 0) {
        fprintf(stderr,
        "Sorry, but you have to be root to run this program\n");
        return -1;
    }
#endif

    return 0;
}

static void usage(void)
{
#ifndef NO_GETOPT_LONG
    const struct option *options = long_options;

    do {
        printf("-%c\t--%s\t%s\n", options->val, options->name,
               options->has_arg ? "<opt>" : "");
        options++;
    } while (options->name != NULL);
#endif
    exit(EXIT_SUCCESS);
}

static int parseoptions(int argc, char *argv[])
{
#ifndef NO_GETOPT_LONG
    int option_index = 0;
#endif
    int fodder;

    while ((fodder =
#ifndef NO_GETOPT_LONG
            getopt_long(argc, argv, GETOPT_OPTIONS, long_options,
            &option_index)
#else
            getopt(argc, argv, GETOPT_OPTIONS)
#endif
            ) != -1) {
        switch (fodder) {
        case 'B': {
            daemonize = 1;
            break;
        }
        case 'g': {
            const char *nptr;
            char *endptr;

            nptr = optarg;
            endptr = NULL;
            gid = (gid_t) strtoul(nptr, &endptr, 10);
            if (!nptr || !*nptr || !endptr || *endptr) {
                perror("Illegal GID - Must be a number\n");
            }
            break;
        }
        case 'p': {
            if ((certd_pid_file = strdup(optarg)) == NULL) {
                perror("Oh no ! More memory !");
            }
            break;
        }
#ifndef NO_GETOPT_LONG
        case 'h': {
            usage();
        }
#endif
        case 'r': {
            if (script == NULL && (script = strdup(optarg)) == NULL) {
                perror("Oh no ! More memory !");
            }
            break;
        }
        case 's': {
            if (socketpath == NULL && (socketpath = strdup(optarg)) == NULL) {
                perror("Oh no ! More memory !");
            }
            break;
        }
        case 'u': {
            const char *nptr;
            char *endptr;

            nptr = optarg;
            endptr = NULL;
            uid = (uid_t) strtoul(nptr, &endptr, 10);
            if (!*nptr || !endptr || *endptr) {
                perror("Illegal UID - Must be a number\n");
            }
            break;
        }
        default:
            usage();
        }
    }
    return 0;
}

static int changeuidgid(void)
{
#ifndef NON_ROOT_FTP
    if (
# ifdef HAVE_SETGROUPS
        setgroups(1U, &gid) ||
# endif
        setgid(gid) || setegid(gid) ||
        setuid(uid) || seteuid(uid) || chdir("/")) {
        return -1;
    }
#endif
    return 0;
}

static void newenv_str(const char * const var, const char * const str)
{
    size_t s;
    char *v;

    if (str == NULL || *str == 0) {
        return;
    }
    s = strlen(var) + strlen(str) + (size_t) 2U;
    if ((v = malloc(s)) == NULL) {
        return;
    }
    if (SNCHECK(snprintf(v, s, "%s=%s", var, str), s)) {
        free(v);
        return;
    }
#ifdef HAVE_PUTENV
    putenv(v);
#endif
}

static void updatepidfile(void)
{
    int fd;
    char buf[42];
    size_t buf_len;

    if (SNCHECK(snprintf(buf, sizeof buf, "%lu\n",
                         (unsigned long) getpid()), sizeof buf)) {
        return;
    }
    if (unlink(certd_pid_file) != 0 && errno != ENOENT) {
        return;
    }
    if ((fd = open(certd_pid_file, O_CREAT | O_WRONLY | O_TRUNC |
                   O_NOFOLLOW, (mode_t) 0644)) == -1) {
        return;
    }
    buf_len = strlen(buf);
    if (safe_write(fd, buf, buf_len, -1) != (ssize_t) buf_len) {
        ftruncate(fd, (off_t) 0);
    }
    close(fd);
}

static void callback_client_sni_name(const char *str)
{
    newenv_str(ENV_CERTD_SNI_NAME, str);
}

static void callback_client_end(const char *str)
{
    (void) str;
    ended = 1;
}

static void process(const int clientfd)
{
    ssize_t readnb;
    char *linepnt;
    char *crpoint;
    pid_t pid;
    int pfds[2];
    char line[4096];

    while ((readnb = read(clientfd, line, sizeof line - 1U)) < (ssize_t) 0 &&
           (errno == EINTR || errno == EIO));
    if (readnb <= (ssize_t) 0) {
        return;
    }
    line[readnb] = 0;
    if (pipe(pfds) != 0) {
        return;
    }
    pid = fork();
    if (pid == (pid_t) -1) {
        close(pfds[0]);
        close(pfds[1]);
        return;
    }
    if (pid != (pid_t) 0) {
        close(pfds[1]);         /* close the output side of the pipe */
        if ((readnb = safe_read(pfds[0], line,
                                sizeof line - 1U)) > (ssize_t) 0) {
            (void) safe_write(clientfd, line, readnb, -1);
        }
#ifdef HAVE_WAITPID
        (void) waitpid(pid, NULL, 0);
#else
        while (wait3(NULL, 0, NULL) != pid);
#endif
        close(pfds[0]);
        return;
    }
    /* now, we are in the child */
    close(clientfd);
    close(kindy);
    close(pfds[0]);                    /* close the input side of the pipe */
    closedesc_all(1);
    linepnt = line;
    while ((crpoint = strchr(linepnt, '\n')) != NULL) {
        const CertdCallBack *scanned;
        size_t keyword_len;

        *crpoint = 0;
        scanned = certd_callbacks;
        while (scanned->keyword != NULL) {
            keyword_len = strlen(scanned->keyword);
            if (strncmp(scanned->keyword, linepnt, keyword_len) == 0) {
                scanned->func(linepnt + keyword_len);
                break;
            }
            scanned++;
        }
        linepnt = crpoint + 1;
    }
    if (ended == 0) {
        close(pfds[1]);
        _exit(EXIT_FAILURE);
    }
    if (dup2(pfds[1], 1) == -1) {
        close(pfds[1]);
        _exit(EXIT_FAILURE);
    }
    close(pfds[1]);
#ifdef DO_CERTD_TIMEOUT
    (void) alarm(CERTD_SCRIPT_TIMEOUT);
#endif
    (void) execl(script, script, (char *) NULL);

    _exit(EXIT_SUCCESS);
}

int listencnx(void)
{
    struct sockaddr_un *saddr;
    int clientfd;
    int ret = -1;
    const size_t socketpath_len = strlen(socketpath);

    if ((saddr = malloc(sizeof(*saddr) + socketpath_len +
                        (size_t) 1U)) == NULL) {
        perror("No more memory to listen to anything");
        goto bye;
    }
    memcpy(saddr->sun_path, socketpath, socketpath_len + (size_t) 1U);
    saddr->sun_family = AF_UNIX;
    (void) unlink(socketpath);
    (void) umask(077);
    if ((kindy = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
        perror("Unable to create a local socket");
        goto bye;
    }
    setcloexec(kindy);
    if (bind(kindy, (struct sockaddr *) saddr, SUN_LEN(saddr)) != 0) {
        perror("Unable to bind a local socket");
        goto bye;
    }
    if (chmod(socketpath, 0600) != 0) {
        perror("Unable to change perms on the local socket");
        goto bye;
    }
    if (listen(kindy, CERTD_BACKLOG) != 0) {
        perror("Unable to listen the local socket");
        goto bye;
    }
    if (changeuidgid() < 0) {
        perror("Identity change");
        goto bye;
    }
    do {
        if ((clientfd = accept(kindy, NULL, NULL)) == -1) {
            if (exit_certd != 0) {
                break;
            }
            (void) sleep(1);
            continue;
        }
        setcloexec(clientfd);
        process(clientfd);
        close(clientfd);
    } while (exit_certd == 0);
    ret = 0;

    bye:
    if (kindy != -1) {
        close(kindy);
        kindy = -1;
    }
    (void) unlink(socketpath);
    free(saddr);

    return ret;
}

static RETSIGTYPE sigterm(int sig)
{
    (void) sig;

    exit_certd = 1;
    if (kindy != -1) {
        close(kindy);
        kindy = -1;
    }
}

int main(int argc, char *argv[])
{
    int err;

#ifdef HAVE_SETLOCALE
# ifdef LC_MESSAGES
    (void) setlocale(LC_MESSAGES, "");
# endif
# ifdef LC_CTYPE
    (void) setlocale(LC_CTYPE, "");
# endif
# ifdef LC_COLLATE
    (void) setlocale(LC_COLLATE, "");
# endif
#endif
    if (init() < 0) {
        return -1;
    }
    (void) signal(SIGTERM, sigterm);
    (void) signal(SIGQUIT, sigterm);
    (void) signal(SIGINT, sigterm);
#ifdef SIGXCPU
    (void) signal(SIGXCPU, sigterm);
#endif
    if (parseoptions(argc, argv) < 0) {
        return -1;
    }
    if (script == NULL || *script != '/') {
        fprintf(stderr, "You must give -r /path/to/cert/program\n");
        return -2;
    }
    if (socketpath == NULL || *socketpath == 0) {
        fprintf(stderr, "You must give -s /path/to/socket\n");
        return -2;
    }
    if (daemonize != 0) {
        dodaemonize();
    }
    updatepidfile();
#ifdef SIGPIPE
    signal(SIGPIPE, SIG_IGN);
#endif
#ifdef SIGCHLD
    signal(SIGCHLD, SIG_DFL);
#endif
    err = listencnx();
    (void) unlink(certd_pid_file);

    return err;
}

#endif
