[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH V2] examples: Add ssh_server_fork example


Hi,

Patch attached, but a few points/questions:
1. I haven't used CMake before, but I have been told to add:
check_include_file(libutil.h HAVE_LIBUTIL_H)

to ConfigureChecks.cmake but I've noticed that there is already:
check_library_exists(util forkpty "" HAVE_LIBUTIL)

And this example is only build `if (HAVE_LIBUTIL)`, just like the old pty
example, so I am not sure if this check_include_file is actually relevant, as I
assume the old example was never built for FreeBSD?

Perhaps we should instead have:
check_library_exists(libutil forkpty "" HAVE_ALTLIBUTIL)

and build only `if (HAVE_LIBUTIL OR HAVE_ALTLIBUTIL)`?

2. The build is currently not dependent on HAVE_PTY_H or HAVE_LIBUTIL_H, but I
feel that it should be?

3. Regarding signals, since we are only using waitpid inside the handler, I
could just set the handler to SIG_IGN, but according to the internet, having
an explicit handler with waitpid is more compatible. What would you prefer?

4. This example although works needs a bit of change to make it work like
OpenSSH (V3 to follow hopefully next weekend).
It doesn't behave as OpenSSH when you start using -T/-t options on the client. This is because pty requests actually don't actually open a pty, and only shell requests do. Regardless of that I'd prefer to get more detailed feedback before
I send out a V3 so I could keep the amount of emails minimal.

Thanks,
Audrius.
From fb3c360c5c1d501d0381545a867029a2426fefbc Mon Sep 17 00:00:00 2001
From: Audrius Butkevicius <audrius.butkevicius@xxxxxxxxx>
Date: Sun, 9 Feb 2014 19:16:47 +0000
Subject: [PATCH V2] examples: Add ssh_server_fork example

Signed-off-by: Audrius Butkevicius <audrius.butkevicius@xxxxxxxxx>
---
 ConfigureChecks.cmake      |    1 +
 config.h.cmake             |    3 +
 examples/CMakeLists.txt    |    3 +
 examples/ssh_server_fork.c |  587 ++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 594 insertions(+), 0 deletions(-)
 create mode 100644 examples/ssh_server_fork.c

diff --git a/ConfigureChecks.cmake b/ConfigureChecks.cmake
index 8f76af8..43b1300 100644
--- a/ConfigureChecks.cmake
+++ b/ConfigureChecks.cmake
@@ -51,6 +51,7 @@ check_include_file(pty.h HAVE_PTY_H)
 check_include_file(termios.h HAVE_TERMIOS_H)
 check_include_file(unistd.h HAVE_UNISTD_H)
 check_include_file(util.h HAVE_UTIL_H)
+check_include_file(libutil.h HAVE_LIBUTIL_H)
 
 if (WIN32)
   check_include_files("winsock2.h;ws2tcpip.h;wspiapi.h" HAVE_WSPIAPI_H)
diff --git a/config.h.cmake b/config.h.cmake
index 7e8cb6a..c76b928 100644
--- a/config.h.cmake
+++ b/config.h.cmake
@@ -23,6 +23,9 @@
 /* Define to 1 if you have the <util.h> header file. */
 #cmakedefine HAVE_UTIL_H 1
 
+/* Define to 1 if you have the <libutil.h> header file. */
+#cmakedefine HAVE_LIBUTIL_H 1
+
 /* Define to 1 if you have the <termios.h> header file. */
 #cmakedefine HAVE_TERMIOS_H 1
 
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index c155e09..0a28327 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -29,6 +29,9 @@ if (UNIX AND NOT WIN32)
         if (HAVE_LIBUTIL)
             add_executable(samplesshd-tty samplesshd-tty.c)
             target_link_libraries(samplesshd-tty ${LIBSSH_SHARED_LIBRARY} ${ARGP_LIBRARIES} util)
+
+            add_executable(ssh_server_fork ssh_server_fork.c)
+            target_link_libraries(ssh_server_fork ${LIBSSH_SHARED_LIBRARY} ${ARGP_LIBRARIES} util)
         endif (HAVE_LIBUTIL)
     endif (WITH_SERVER)
 
diff --git a/examples/ssh_server_fork.c b/examples/ssh_server_fork.c
new file mode 100644
index 0000000..4c3bd92
--- /dev/null
+++ b/examples/ssh_server_fork.c
@@ -0,0 +1,587 @@
+/* This is a sample implementation of a libssh based SSH server */
+/*
+Copyright 2014 Audrius Butkevicius
+
+This file is part of the SSH Library
+
+You are free to copy this file, modify it in any way, consider it being public
+domain. This does not apply to the rest of the library though, but it is
+allowed to cut-and-paste working code from this file to any license of
+program.
+The goal is to show the API in action.
+*/
+
+#include "config.h"
+
+#include <libssh/callbacks.h>
+#include <libssh/poll.h>
+#include <libssh/server.h>
+
+#ifdef HAVE_ARGP_H
+#include <argp.h>
+#endif
+#include <fcntl.h>
+#ifdef HAVE_LIBUTIL_H
+#include <libutil.h>
+#endif
+#ifdef HAVE_PTY_H
+#include <pty.h>
+#endif
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+
+#ifndef KEYS_FOLDER
+#ifdef _WIN32
+#define KEYS_FOLDER
+#else
+#define KEYS_FOLDER "/etc/ssh/"
+#endif
+#endif
+
+#define USER "myuser"
+#define PASS "mypassword"
+#define BUF_SIZE 1048576
+#define SESSION_END (SSH_CLOSED | SSH_CLOSED_ERROR)
+#define SFTP_SERVER_PATH "/usr/lib/sftp-server"
+
+#ifdef HAVE_ARGP_H
+const char *argp_program_version = "libssh server example "
+SSH_STRINGIFY(LIBSSH_VERSION);
+const char *argp_program_bug_address = "<libssh@xxxxxxxxxx>";
+
+/* Program documentation. */
+static char doc[] = "libssh -- a Secure Shell protocol implementation";
+
+/* A description of the arguments we accept. */
+static char args_doc[] = "BINDADDR";
+
+/* The options we understand. */
+static struct argp_option options[] = {
+    {
+        .name  = "port",
+        .key   = 'p',
+        .arg   = "PORT",
+        .flags = 0,
+        .doc   = "Set the port to bind.",
+        .group = 0
+    },
+    {
+        .name  = "hostkey",
+        .key   = 'k',
+        .arg   = "FILE",
+        .flags = 0,
+        .doc   = "Set the host key.",
+        .group = 0
+    },
+    {
+        .name  = "dsakey",
+        .key   = 'd',
+        .arg   = "FILE",
+        .flags = 0,
+        .doc   = "Set the dsa key.",
+        .group = 0
+    },
+    {
+        .name  = "rsakey",
+        .key   = 'r',
+        .arg   = "FILE",
+        .flags = 0,
+        .doc   = "Set the rsa key.",
+        .group = 0
+    },
+    {
+        .name  = "verbose",
+        .key   = 'v',
+        .arg   = NULL,
+        .flags = 0,
+        .doc   = "Get verbose output.",
+        .group = 0
+    },
+    {NULL, 0, NULL, 0, NULL, 0}
+};
+
+/* Parse a single option. */
+static error_t parse_opt (int key, char *arg, struct argp_state *state) {
+    /* Get the input argument from argp_parse, which we
+     * know is a pointer to our arguments structure. */
+    ssh_bind sshbind = state->input;
+
+    switch (key) {
+        case 'p':
+            ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT_STR, arg);
+            break;
+        case 'd':
+            ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_DSAKEY, arg);
+            break;
+        case 'k':
+            ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HOSTKEY, arg);
+            break;
+        case 'r':
+            ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, arg);
+            break;
+        case 'v':
+            ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY_STR,
+                                 "3");
+            break;
+        case ARGP_KEY_ARG:
+            if (state->arg_num >= 1) {
+                /* Too many arguments. */
+                argp_usage (state);
+            }
+            ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDADDR, arg);
+            break;
+        case ARGP_KEY_END:
+            if (state->arg_num < 1) {
+                /* Not enough arguments. */
+                argp_usage (state);
+            }
+            break;
+        default:
+            return ARGP_ERR_UNKNOWN;
+    }
+    return 0;
+}
+
+/* Our argp parser. */
+static struct argp argp = {options, parse_opt, args_doc, doc, NULL, NULL, NULL};
+#endif /* HAVE_ARGP_H */
+
+/* A userdata struct for channel. */
+struct channel_data_struct {
+    /* pid of the child process the channel will spawn. */
+    pid_t pid;
+    /* For communication with the child process. */
+    socket_t stdin;
+    socket_t stdout;
+    /* Only used for subsystem and exec requests. */
+    socket_t stderr;
+    /* Event which is used to poll the above descriptors. */
+    ssh_event event;
+    /* Terminal size struct. */
+    struct winsize *winsize;
+};
+
+/* 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) {
+    struct channel_data_struct *cdata = (struct channel_data_struct *) userdata;
+
+    (void) session;
+    (void) channel;
+    (void) is_stderr;
+
+    if (len == 0 || cdata->pid == 0 || kill(cdata->pid, 0) < 0) {
+        return 0;
+    }
+
+    return write(cdata->stdin, (char *) data, len);
+}
+
+static int pty_request(ssh_session session, ssh_channel channel,
+                       const char *term, int cols, int rows, int py, int px,
+                       void *userdata) {
+    struct channel_data_struct *cdata = (struct channel_data_struct *)userdata;
+
+    (void) session;
+    (void) channel;
+    (void) term;
+
+    cdata->winsize->ws_row = rows;
+    cdata->winsize->ws_col = cols;
+    cdata->winsize->ws_xpixel = px;
+    cdata->winsize->ws_ypixel = py;
+
+    return SSH_OK;
+}
+
+static int pty_resize(ssh_session session, ssh_channel channel, int cols,
+                      int rows, int py, int px, void *userdata) {
+    struct channel_data_struct *cdata = (struct channel_data_struct *)userdata;
+
+    (void) session;
+    (void) channel;
+
+    cdata->winsize->ws_row = rows;
+    cdata->winsize->ws_col = cols;
+    cdata->winsize->ws_xpixel = px;
+    cdata->winsize->ws_ypixel = py;
+
+    return ioctl(cdata->stdin, TIOCSWINSZ, cdata->winsize);
+}
+
+static int shell_request(ssh_session session, ssh_channel channel,
+                         void *userdata) {
+    struct channel_data_struct *cdata = (struct channel_data_struct *) userdata;
+
+    (void) session;
+    (void) channel;
+
+    switch(cdata->pid = forkpty(&cdata->stdin, NULL, NULL, cdata->winsize)) {
+        case -1:
+            close(cdata->stdin);
+            fprintf(stderr, "Failed to allocate pty\n");
+            return SSH_ERROR;
+        case 0:
+            execl("/bin/sh", "sh", "-l", NULL);
+            exit(0);
+    }
+    /* forkpty returns a bi-directional pipe/socket. */
+    cdata->stdout = cdata->stdin;
+
+    return SSH_OK;
+}
+
+static int exec_request(ssh_session session, ssh_channel channel,
+                        const char *command, void *userdata) {
+    struct channel_data_struct *cdata = (struct channel_data_struct *) userdata;
+    int in[2], out[2], err[2];
+
+    (void) session;
+    (void) channel;
+
+    /* Do the plumbing to be able to talk with the child process. */
+    if (pipe(in) != 0) {
+        goto stdin_failed;
+    }
+    if (pipe(out) != 0) {
+        goto stdout_failed;
+    }
+    if (pipe(err) != 0) {
+        goto stderr_failed;
+    }
+
+    switch(cdata->pid = fork()) {
+        case -1:
+            goto fork_failed;
+        case 0:
+            /* Finish the plumbing in the child process. */
+            close(in[1]);
+            close(out[0]);
+            close(err[0]);
+            dup2(in[0], STDIN_FILENO);
+            dup2(out[1], STDOUT_FILENO);
+            dup2(err[1], STDERR_FILENO);
+            close(in[0]);
+            close(out[1]);
+            close(err[1]);
+            /* exec the requested command. */
+            execl("/bin/sh", "sh", "-c", command, NULL);
+            exit(0);
+    }
+
+    close(in[0]);
+    close(out[1]);
+    close(err[1]);
+
+    cdata->stdin = in[1];
+    cdata->stdout = out[0];
+    cdata->stderr = err[0];
+
+    return SSH_OK;
+
+fork_failed:
+    close(err[0]);
+    close(err[1]);
+stderr_failed:
+    close(out[0]);
+    close(out[1]);
+stdout_failed:
+    close(in[0]);
+    close(in[1]);
+stdin_failed:
+    return SSH_ERROR;
+}
+
+static int subsystem_request(ssh_session session, ssh_channel channel,
+                             const char *subsystem, void *userdata) {
+    /* subsystem requests behave simillarly to exec requests. */
+    if (strcmp(subsystem, "sftp") == 0) {
+        return exec_request(session, channel, SFTP_SERVER_PATH, userdata);
+    }
+    return SSH_ERROR;
+}
+
+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;
+
+    if (strcmp(user, USER) == 0 && strcmp(pass, PASS) == 0) {
+        sdata->authenticated = 1;
+        return SSH_AUTH_SUCCESS;
+    }
+
+    sdata->auth_attempts++;
+    return SSH_AUTH_DENIED;
+}
+
+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;
+}
+
+static int process_stdout(socket_t fd, int revents, void *userdata) {
+    char buf[BUF_SIZE];
+    int n = -1;
+    ssh_channel channel = (ssh_channel) userdata;
+
+    if (channel != NULL && (revents & POLLIN) != 0) {
+        n = read(fd, buf, BUF_SIZE);
+        if (n > 0) {
+            ssh_channel_write(channel, buf, n);
+        }
+    }
+
+    return n;
+}
+
+static int process_stderr(socket_t fd, int revents, void *userdata) {
+    char buf[BUF_SIZE];
+    int n = -1;
+    ssh_channel channel = (ssh_channel) userdata;
+
+    if (channel != NULL && (revents & POLLIN) != 0) {
+        n = read(fd, buf, BUF_SIZE);
+        if (n > 0) {
+            ssh_channel_write_stderr(channel, buf, n);
+        }
+    }
+
+    return n;
+}
+
+static void handle_session(ssh_event event, ssh_session session) {
+    int n, rc;
+
+    /* Structure for storing the pty size. */
+    struct winsize wsize = {
+        .ws_row = 0,
+        .ws_col = 0,
+        .ws_xpixel = 0,
+        .ws_ypixel = 0
+    };
+
+    /* Our struct holding information about the channel. */
+    struct channel_data_struct cdata = {
+        .pid = 0,
+        .stdin = -1,
+        .stdout = -1,
+        .stderr = -1,
+        .event = NULL,
+        .winsize = &wsize
+    };
+
+    /* Our struct holding information about the session. */
+    struct session_data_struct sdata = {
+        .channel = NULL,
+        .auth_attempts = 0,
+        .authenticated = 0
+    };
+
+    struct ssh_channel_callbacks_struct channel_cb = {
+        .userdata = &cdata,
+        .channel_pty_request_function = pty_request,
+        .channel_pty_window_change_function = pty_resize,
+        .channel_shell_request_function = shell_request,
+        .channel_exec_request_function = exec_request,
+        .channel_data_function = data_function,
+        .channel_subsystem_request_function = subsystem_request
+    };
+
+    struct ssh_server_callbacks_struct server_cb = {
+        .userdata = &sdata,
+        .auth_password_function = auth_password,
+        .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_event_add_session(event, session);
+
+    n = 0;
+    while (sdata.authenticated == 0 || sdata.channel == NULL) {
+        /* If the user has used up all attempts, or if he hasn't been able to
+         * authenticate in 10 seconds (n * 100ms), disconnect. */
+        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++;
+    }
+
+    ssh_set_channel_callbacks(sdata.channel, &channel_cb);
+
+    do {
+        /* Poll the main event which takes care of the session, the channel and
+         * even our child process's stdout/stderr (once it's started). */
+        if (ssh_event_dopoll(event, -1) == SSH_ERROR) {
+          ssh_channel_close(sdata.channel);
+        }
+
+        /* If child process's stdout/stderr has been registered with the event,
+         * or the child process hasn't started yet, continue. */
+        if (cdata.event != NULL || cdata.pid == 0) {
+            continue;
+        }
+        /* Executed only once, once the child process starts. */
+        cdata.event = event;
+        /* If stdout valid, add stdout to be monitored by the poll event. */
+        if (cdata.stdout != -1) {
+            if (ssh_event_add_fd(event, cdata.stdout, POLLIN, process_stdout,
+                                 sdata.channel) != SSH_OK) {
+                fprintf(stderr, "Failed to register stdout to poll context\n");
+                ssh_channel_close(sdata.channel);
+            }
+        }
+
+        /* If stderr valid, add stderr to be monitored by the poll event. */
+        if (cdata.stderr != -1){
+            if (ssh_event_add_fd(event, cdata.stderr, POLLIN, process_stderr,
+                                 sdata.channel) != SSH_OK) {
+                fprintf(stderr, "Failed to register stderr to poll context\n");
+                ssh_channel_close(sdata.channel);
+            }
+        }
+    } while(ssh_channel_is_open(sdata.channel) &&
+            (cdata.pid == 0 || waitpid(cdata.pid, &rc, WNOHANG) == 0));
+
+    close(cdata.stdin);
+    close(cdata.stdout);
+    close(cdata.stderr);
+
+    /* Remove the descriptors from the polling context, since they are now
+     * closed, they will always trigger during the poll calls. */
+    ssh_event_remove_fd(event, cdata.stdout);
+    ssh_event_remove_fd(event, cdata.stderr);
+
+    /* If the child process exited. */
+    if (kill(cdata.pid, 0) < 0 && WIFEXITED(rc)) {
+        rc = WEXITSTATUS(rc);
+        ssh_channel_request_send_exit_status(sdata.channel, rc);
+    /* If client terminated the channel or the process did not exit nicely. */
+    } else {
+        kill(cdata.pid, SIGKILL);
+    }
+
+    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) & SESSION_END) == 0; n++) {
+        ssh_event_dopoll(event, 100);
+    }
+}
+
+/* SIGCHLD handler for cleaning up dead children. */
+static void sigchld_handler(int signo) {
+    (void) signo;
+    while (waitpid(-1, NULL, WNOHANG) > 0);
+}
+
+int main(int argc, char **argv) {
+    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 1;
+    }
+
+    ssh_init();
+    sshbind = ssh_bind_new();
+
+    ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_DSAKEY,
+                         KEYS_FOLDER "ssh_host_dsa_key");
+    ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY,
+                         KEYS_FOLDER "ssh_host_rsa_key");
+
+#ifdef HAVE_ARGP_H
+    argp_parse(&argp, argc, argv, 0, 0, sshbind);
+#else
+    (void) argc;
+    (void) argv;
+#endif /* HAVE_ARGP_H */
+
+    if(ssh_bind_listen(sshbind) < 0) {
+        fprintf(stderr, "%s\n", ssh_get_error(sshbind));
+        return 1;
+    }
+
+    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. */
+                        handle_session(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();
+    return 0;
+}
-- 
1.7.2.5


Follow-Ups:
Re: [PATCH V2] examples: Add ssh_server_fork exampleRaphael Kubo da Costa <rakuco@xxxxxxxxxxx>
Archive administrator: postmaster@lists.cynapses.org