// SPDX-License-Identifier: BSL-1.0
#include "SshServer.h"

/* Based on sample by Audrius Butkevicius */

#include <stdlib.h>
#include <sys/wait.h>
#include <stdio.h>
#include <malloc.h>

#include <libssh/callbacks.h>
#include <libssh/server.h>

#ifdef Q_CC_GNU
#pragma GCC diagnostic ignored "-Winvalid-offsetof"
#endif
#define container_of(ptr, type, member) ((type *)((char *)ptr - offsetof(type, member)))

bool pty_requested = false;
bool start_requested = false;

/* A userdata struct for session. */
struct session_data_struct {
    /* Pointer to the channel the session will allocate. */
    ssh_channel channel;
    int auth_attempts;
    int authenticated;
};

static int data_function(ssh_session session, ssh_channel channel, void *data,
                         uint32_t len, int is_stderr, void *userdata) {
    SshServer *cdata = (SshServer*) userdata;

    (void) session;
    (void) channel;
    (void) is_stderr;

    termpaint_terminal_add_input_data(cdata->terminal, (const char*)data, len);
    cdata->newInput = true;

    return len;
}

static int pty_request(ssh_session session, ssh_channel channel,
                       const char *term, int cols, int rows, int py, int px,
                       void *userdata) {
    pty_requested = true;
    return SSH_OK;
}

static int pty_resize(ssh_session session, ssh_channel channel, int cols,
                      int rows, int py, int px, void *userdata) {
    return SSH_OK;
}

static int exec_request(ssh_session session, ssh_channel channel,
                        const char *command, void *userdata) {

    (void) session;
    start_requested = true;
    return SSH_OK;
}

static int shell_request(ssh_session session, ssh_channel channel,
                         void *userdata) {
    (void) session;
    start_requested = true;
    return SSH_OK;
}

static int auth_none(ssh_session session, const char *user, void *userdata) {
    struct session_data_struct *sdata = (struct session_data_struct *) userdata;

    (void) session;

    sdata->authenticated = 1;
    return SSH_AUTH_SUCCESS;
}

static int auth_password(ssh_session session, const char *user,
                         const char *pass, void *userdata) {
    struct session_data_struct *sdata = (struct session_data_struct *) userdata;

    (void) session;

    sdata->authenticated = 1;
    return SSH_AUTH_SUCCESS;
}

static ssh_channel channel_open(ssh_session session, void *userdata) {
    struct session_data_struct *sdata = (struct session_data_struct *) userdata;

    sdata->channel = ssh_channel_new(session);
    return sdata->channel;
}

/* SIGCHLD handler for cleaning up dead children. */
static void sigchld_handler(int signo) {
    (void) signo;
    while (waitpid(-1, NULL, WNOHANG) > 0);
}

SshServer::SshServer(int port, std::string serverKeyFile)
    : port(port), serverKeyFile(serverKeyFile)
{
}

void SshServer::run() {
    ssh_bind sshbind;
    ssh_session session;
    ssh_event event;
    struct sigaction sa;

    /* Set up SIGCHLD handler. */
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    if (sigaction(SIGCHLD, &sa, NULL) != 0) {
        fprintf(stderr, "Failed to register SIGCHLD handler\n");
        return;
    }

    ssh_init();
    sshbind = ssh_bind_new();

    ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY,
                         serverKeyFile.c_str());

    ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT_STR, std::to_string(port).c_str());

    if(ssh_bind_listen(sshbind) < 0) {
        fprintf(stderr, "%s\n", ssh_get_error(sshbind));
        return;
    }

    while (1) {
        session = ssh_new();
        if (session == NULL) {
            fprintf(stderr, "Failed to allocate session\n");
            continue;
        }

        /* Blocks until there is a new incoming connection. */
        if(ssh_bind_accept(sshbind, session) != SSH_ERROR) {
            switch(fork()) {
                case 0:
                    /* Remove the SIGCHLD handler inherited from parent. */
                    sa.sa_handler = SIG_DFL;
                    sigaction(SIGCHLD, &sa, NULL);
                    /* Remove socket binding, which allows us to restart the
                     * parent process, without terminating existing sessions. */
                    ssh_bind_free(sshbind);

                    event = ssh_event_new();
                    if (event != NULL) {
                        /* Blocks until the SSH session ends by either
                         * child process exiting, or client disconnecting. */
                        handleSession(event, session);
                        ssh_event_free(event);
                    } else {
                        fprintf(stderr, "Could not create polling context\n");
                    }
                    ssh_disconnect(session);
                    ssh_free(session);

                    exit(0);
                case -1:
                    fprintf(stderr, "Failed to fork\n");
            }
        } else {
            fprintf(stderr, "%s\n", ssh_get_error(sshbind));
        }
        /* Since the session has been passed to a child fork, do some cleaning
         * up at the parent process. */
        ssh_disconnect(session);
        ssh_free(session);
    }

    ssh_bind_free(sshbind);
    ssh_finalize();
}

void SshServer::outStr(const char *s) {
    ssh_channel_write(channel, s, strlen(s));
}

void SshServer::handleSession(ssh_event event, ssh_session session) {
    int n;

    struct session_data_struct sdata;
        sdata.channel = NULL;
        sdata.auth_attempts = 0;
        sdata.authenticated = 0;

    struct ssh_channel_callbacks_struct channel_cb { 0 };
        channel_cb.userdata = this;
        channel_cb.channel_pty_request_function = pty_request;
        channel_cb.channel_pty_window_change_function = pty_resize;
        channel_cb.channel_shell_request_function = shell_request;
        channel_cb.channel_exec_request_function = exec_request;
        channel_cb.channel_data_function = data_function;

    struct ssh_server_callbacks_struct server_cb { 0 };
        server_cb.userdata = &sdata;
        server_cb.auth_password_function = auth_password;
        server_cb.auth_none_function = auth_none;
        server_cb.channel_open_request_session_function = channel_open;

    ssh_callbacks_init(&server_cb);
    ssh_callbacks_init(&channel_cb);

    ssh_set_server_callbacks(session, &server_cb);

    if (ssh_handle_key_exchange(session) != SSH_OK) {
        fprintf(stderr, "%s\n", ssh_get_error(session));
        return;
    }

    ssh_set_auth_methods(session, SSH_AUTH_METHOD_PASSWORD | SSH_AUTH_METHOD_NONE | SSH_AUTH_METHOD_HOSTBASED);
    ssh_event_add_session(event, session);

    n = 0;
    while (sdata.authenticated == 0 || sdata.channel == NULL) {
        if (sdata.auth_attempts >= 3 || n >= 100) {
            return;
        }

        if (ssh_event_dopoll(event, 100) == SSH_ERROR) {
            fprintf(stderr, "%s\n", ssh_get_error(session));
            return;
        }
        n++;
    }

    channel = sdata.channel;

    memset(&integration, 0, sizeof(integration));

    auto free = [] (termpaint_integration* ptr) {
    };
    auto write = [] (termpaint_integration* ptr, const char *data, int length) {
        auto t = container_of(ptr, SshServer, integration);
        t->outputBuffer += std::string(data, length);
        if (t->outputBuffer.size() > 1000) {
            int written = ssh_channel_write(t->channel, t->outputBuffer.data(), t->outputBuffer.size());
            if (written >= 0) {
                t->outputBuffer.erase(t->outputBuffer.begin(), t->outputBuffer.begin() + written);
            }
        }
    };
    auto flush = [] (termpaint_integration* ptr) {
        auto t = container_of(ptr, SshServer, integration);
        while (t->outputBuffer.size() > 0) {
            int written = ssh_channel_write(t->channel, t->outputBuffer.data(), t->outputBuffer.size());
            if (written >= 0) {
                t->outputBuffer.erase(t->outputBuffer.begin(), t->outputBuffer.begin() + written);
            } else {
                break; // error
            }
        }
    };

    termpaint_integration_init(&integration, free, write, flush);

    termpaint_integration_set_is_bad(&integration, [] (termpaint_integration* ptr) {
        return false;
    });

    termpaint_integration_set_request_callback(&integration, [] (termpaint_integration* ptr) {
        auto t = container_of(ptr, SshServer, integration);
        t->callback_requested = true;
    });

    terminal = termpaint_terminal_new(&integration);
    //termpaint_auto_detect(surface);

    ssh_set_channel_callbacks(sdata.channel, &channel_cb);

    while (!start_requested && ssh_channel_is_open(sdata.channel)) {
        int res = ssh_event_dopoll(event, -1);
        if (res == SSH_ERROR) {
            ssh_channel_close(sdata.channel);
        }
    }

    main([&] () -> bool {
        newInput = false;
        flush(&integration);
        do {
            int timeout = -1;
            if (callback_requested) {
                 timeout = 100;
            }
            int res = ssh_event_dopoll(event, timeout);
            if (res == SSH_ERROR) {
                ssh_channel_close(sdata.channel);
            } else if (res == SSH_AGAIN) {
                callback_requested = false;
                termpaint_terminal_callback(terminal);
            }
        } while(ssh_channel_is_open(sdata.channel) && !newInput);
        return ssh_channel_is_open(sdata.channel);
    });

    ssh_channel_send_eof(sdata.channel);
    ssh_channel_close(sdata.channel);

    /* Wait up to 5 seconds for the client to terminate the session. */
    for (n = 0; n < 50 && (ssh_get_status(session) & (SSH_CLOSED | SSH_CLOSED_ERROR)) == 0; n++) {
        ssh_event_dopoll(event, 100);
    }
    termpaint_integration_deinit(&integration);
}
