#include "forward-tunnel.h"
#include "thpool.h"

const char *keyfile1 = "/home/aixiao/.ssh/id_rsa.pub";
const char *keyfile2 = "/home/aixiao/.ssh/id_rsa";

char *server_ssh_ip = "127.0.0.1";
int server_ssh_port = 22;
char *server_ssh_user = "aixiao";
char *server_ssh_passwd = "123456";

const char *local_listenip = "0.0.0.0";
unsigned int local_listenport = 3306;

char *remote_desthost = "0.0.0.0"; /* resolved by the server */
unsigned int remote_destport = 3306;

enum {
    AUTH_NONE = 0,
    AUTH_PASSWORD,
    AUTH_PUBLICKEY
};

void forward_tunnel(void *sock_)
{
    int forwardsock = *(int *)sock_;
    int rc, i, auth = AUTH_NONE;
    struct sockaddr_in sin;
    const char *fingerprint;
    char *userauthlist;
    LIBSSH2_SESSION *session;
    LIBSSH2_CHANNEL *channel = NULL;
    const char *shost;
    unsigned int sport;
    fd_set fds;
    struct timeval tv;
    ssize_t len, wr;
    char buffer[16384];
    int sock;

    bzero(buffer, 0);
    rc = libssh2_init(0);

    if (rc) {
        fprintf(stderr, "libssh2 initialization failed (%d)\n", rc);
        return;
    }

    /* Connect to SSH server */
    sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock == -1) {
        perror("socket");
        return;
    }

    int optval = SO_REUSEADDR;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) {
        perror("setsockopt");
        return;
    }

    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = inet_addr(server_ssh_ip);
    if (INADDR_NONE == sin.sin_addr.s_addr) {
        perror("inet_addr");
        return;
    }
    sin.sin_port = htons(server_ssh_port);
    if (connect(sock, (struct sockaddr *)(&sin), sizeof(struct sockaddr_in)) != 0) {
        fprintf(stderr, "failed to connect!\n");
        return;
    }

    /* Create a session instance */
    session = libssh2_session_init();

    if (!session) {
        fprintf(stderr, "Could not initialize SSH session!\n");
        return;
    }

    /* ... start it up. This will trade welcome banners, exchange keys,
     * and setup crypto, compression, and MAC layers
     */
    rc = libssh2_session_handshake(session, sock);
    if (rc) {
        fprintf(stderr, "Error when starting up SSH session: %d\n", rc);
        return;
    }

    /* At this point we havn't yet authenticated.  The first thing to do
     * is check the hostkey's fingerprint against our known hosts Your app
     * may have it hard coded, may go to a file, may present it to the
     * user, that's your call
     */
    fingerprint = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA1);

    fprintf(stderr, "Fingerprint: ");
    for (i = 0; i < 20; i++)
        fprintf(stderr, "%02X ", (unsigned char)fingerprint[i]);
    fprintf(stderr, "\n");

    /* check what authentication methods are available */
    userauthlist = libssh2_userauth_list(session, server_ssh_user, strlen(server_ssh_user));

    fprintf(stderr, "Authentication methods: %s\n", userauthlist);
    if (strstr(userauthlist, "password"))
        auth |= AUTH_PASSWORD;
    if (strstr(userauthlist, "publickey"))
        auth |= AUTH_PUBLICKEY;

    if (auth & AUTH_PASSWORD) {
        if (libssh2_userauth_password(session, server_ssh_user, server_ssh_passwd)) {

            fprintf(stderr, "Authentication by password failed.\n");
            goto shutdown;
        }
    } else if (auth & AUTH_PUBLICKEY) {
        if (libssh2_userauth_publickey_fromfile(session, server_ssh_user, keyfile1, keyfile2, server_ssh_passwd)) {
            fprintf(stderr, "\tAuthentication by public key failed!\n");
            goto shutdown;
        }
        fprintf(stderr, "\tAuthentication by public key succeeded.\n");
    } else {
        fprintf(stderr, "No supported authentication methods found!\n");
        goto shutdown;
    }

    shost = inet_ntoa(sin.sin_addr);
    sport = ntohs(sin.sin_port);

    fprintf(stderr, "Forwarding connection from %s:%d here to remote %s:%d\n", shost, sport, remote_desthost, remote_destport);
    channel = libssh2_channel_direct_tcpip_ex(session, remote_desthost, remote_destport, shost, sport);
    if (!channel) {
        fprintf(stderr, "Could not open the direct-tcpip channel!\n" "(Note that this can be a problem at the server!" " Please review the server logs.)\n");
        goto shutdown;
    }

    /* Setting session to non-blocking IO */
    libssh2_session_set_blocking(session, 0);

    while (1) {
        FD_ZERO(&fds);
        FD_SET(forwardsock, &fds); // 设置一个描述符
        tv.tv_sec = 0;
        tv.tv_usec = 100000;
        rc = select(forwardsock + 1, &fds, NULL, NULL, &tv);
        if (-1 == rc) {         // 返回-1表示出错
            perror("select");
            goto shutdown;
        }

        if (rc && FD_ISSET(forwardsock, &fds)) { // 测试某一个描述符返回值: 若fd在描述符集中则返回非0,否则返回0
            len = read(forwardsock, buffer, sizeof(buffer));
            if (len < 0) {
                perror("read");
                goto shutdown;
            } else if (0 == len) {
                fprintf(stderr, "The client at %s:%d disconnected!\n", shost, sport);
                goto shutdown;
            }

            wr = 0;
            while (wr < len) {
                i = libssh2_channel_write(channel, buffer + wr, len - wr);

                if (LIBSSH2_ERROR_EAGAIN == i) {
                    continue;
                }
                if (i < 0) {
                    fprintf(stderr, "libssh2_channel_write: %d\n", i);
                    goto shutdown;
                }

                wr += i;
            }
        }

        while (1) {
            len = libssh2_channel_read(channel, buffer, sizeof(buffer));

            if (LIBSSH2_ERROR_EAGAIN == len)
                break;
            else if (len < 0) {
                fprintf(stderr, "libssh2_channel_read: %d", (int)len);
                goto shutdown;
            }

            wr = 0;
            while (wr < len) {
                i = write(forwardsock, buffer + wr, len - wr);
                if (i <= 0) {
                    perror("write");
                    goto shutdown;
                }

                wr += i;
            }

            if (libssh2_channel_eof(channel)) {
                fprintf(stderr, "The server at %s:%d disconnected!\n", remote_desthost, remote_destport);
                goto shutdown;
            }
        }
    }

shutdown:
    close(forwardsock);
    if (channel)
        libssh2_channel_free(channel);
    libssh2_session_set_blocking(session, 1);
    libssh2_session_disconnect(session, "Client disconnecting normally");
    libssh2_session_free(session);
    close(sock);
    libssh2_exit();

    return;
}

static char help_info(void)
{
    int l;
    static const char name[] = "STunnel";
    static const char subject[] = "SSH forward tunnel";
    static const struct {
        const char *email;
        const char *version;
    } author = {
        "AIXIAO@AIXIAO.ME",
        "1.0",
    };

    static const char usage[] = "Usage: [-d] [-rsplueh?]";
    static const char *s_help[] = {
        "",
        "Options:",
        "    -d                     : Background running",
        "    -r                     : SSH Server IP",
        "    -p                     : SSH Server Port (default: 22)",
        "    -u                     : SSH Server User",
        "    -e                     : SSH Server User Passwd",
        "    -s                     : Remote Dest Port (default: 3306 1521)",
        "    -l                     : Local_Listen Port",
        "    -? -h                  : help information",
        "",
        "",
        0
    };

    fprintf(stderr, "       %s %s\n", name, subject);
    fprintf(stderr, "Author: %s\n", author.email);
    fprintf(stderr, "Version: %s\n", author.version);
    fprintf(stderr, "%s\n", usage);

    for (l = 0; s_help[l]; l++) {
        fprintf(stderr, "%s\n", s_help[l]);
    }

    BUILD("Compile、link.\n");
    puts("");

    return 0;
}

int nice_(int increment)
{
    int oldprio = getpriority(PRIO_PROCESS, getpid());
    printf("%d\n", oldprio);
    return setpriority(PRIO_PROCESS, getpid(), oldprio + increment);
}

int modif_argv(int argc, char *argv[])
{
    int i, j;

    // 加密参数(账号密码)
    for (i = 1; i < argc; i++) {

        if (0 == strcmp(argv[i], "-e") || 0 == strcmp(argv[i], "-u") || 0 == strcmp(argv[i], "-p")) {
            for (j = strlen(argv[i + 1]) - 1; j >= 0; j--) {
                argv[i + 1][j] = 'x';
            }
        }

    }

    return 0;
}

int get_threads()
{
    pid_t pid;
    char line[256];
    char path[256];
    int threads = 0;

    pid = getpid();
    snprintf(path, sizeof(path), "/proc/%d/status", pid);
    FILE *file = fopen(path, "r");
    if (file == NULL) {
        perror("Failed to open file");
        return EXIT_FAILURE;
    }

    while (fgets(line, sizeof(line), file)) {
        if (strncmp(line, "Threads:", 8) == 0) {
            sscanf(line, "Threads:\t%d", &threads);
            break;
        }
    }

    return threads;
}

int main(int argc, char *argv[], char **env)
{
    struct sockaddr_in sin;
    struct sockaddr_in server_addr;
    socklen_t server_addr_len;
    int sockopt = 1;
    int listensock = -1, forwardsock = -1;
    int _daemon = 0;
    int opt;
    char *p = NULL;

    char optstring[] = ":dr:s:p:l:u:e:h?";
    while (-1 != (opt = getopt(argc, argv, optstring))) {
        switch (opt) {
        case 'd':
            _daemon = 1;
            break;
        case 'r':
            server_ssh_ip = strdup(optarg);
            break;
        case 's':
            p = strchr(optarg, ':');
            if (p != NULL) {
                remote_destport = atoi(p + 1);

                *p = '\0';
                remote_desthost = optarg;

            } else {
                if (NULL == (p = strchr(optarg, '.'))) {
                    remote_destport = atoi(optarg);
                    remote_desthost = "0.0.0.0";
                } else {
                    help_info();
                    exit(0);
                }
            }

            break;
        case 'p':
            server_ssh_port = atoi(optarg);
            break;
        case 'l':
            local_listenport = atoi(optarg);
            break;
        case 'u':
            server_ssh_user = strdup(optarg);
            break;
        case 'e':
            server_ssh_passwd = strdup(optarg);;
            break;
        case 'h':
        case '?':
            help_info();
            exit(0);
            break;
        default:
            ;
        }
    }

    modif_argv(argc, argv);
    printf("SSH Server: %s:%d, Local listen: %s:%d, User&Passwd: [%s]['%s']\n", server_ssh_ip, remote_destport, local_listenip, local_listenport, server_ssh_user, server_ssh_passwd);

    listensock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listensock == -1) {
        perror("socket");
        return -1;
    }

    int optval = SO_REUSEADDR;
    if (setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) {
        perror("setsockopt");
        return -1;
    }

    sin.sin_family = AF_INET;
    sin.sin_port = htons(local_listenport);
    sin.sin_addr.s_addr = inet_addr(local_listenip);
    if (INADDR_NONE == sin.sin_addr.s_addr) {
        perror("inet_addr");
        goto shutdown;
    }

    if (setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt)) < 0) {
        perror("setsockopt");
        return -1;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(local_listenport);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(listensock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0) {
        perror("bind");
        goto shutdown;
    }

    if (listen(listensock, 500) < 0) {
        perror("listen");
        goto shutdown;
    }

    fprintf(stderr, "Waiting for TCP connection on %s:%d ...\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));

    // 后台运行
    if (_daemon == 1) {
        if (daemon(1, 1)) {
            perror("daemon");
            return -1;
        }
    }
    // 进程优先级
    if (-1 == (nice_(-20)))
        perror("nice_");

    threadpool thpool = thpool_init(1024);
    while (1) {
        printf("%d\n", get_threads());

        int *forwardsock = (int *)malloc(sizeof(int)); // 分配内存空间保存线程编号
        *forwardsock = accept(listensock, (struct sockaddr *)&sin, &server_addr_len);
        // 接受连接
        if (*forwardsock == -1) {
            perror("accept");
            goto shutdown;
        }

        thpool_add_work(thpool, forward_tunnel, (void *)(uintptr_t) forwardsock);

    }
    thpool_wait(thpool);
    thpool_destroy(thpool);
shutdown:
    
    close(forwardsock);
    close(listensock);
    libssh2_exit();
    if (server_ssh_ip)
        free(server_ssh_ip);
    if (server_ssh_user)
        free(server_ssh_user);
    if (server_ssh_passwd)
        free(server_ssh_passwd);

    return 0;
}
