[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[PATCH V2] examples: Add ssh_server_fork example
[Thread Prev] | [Thread Next]
- Subject: [PATCH V2] examples: Add ssh_server_fork example
- From: Audrius Butkevicius <audrius.butkevicius@xxxxxxxxxxxxxxxx>
- Reply-to: libssh@xxxxxxxxxx
- Date: Sun, 09 Feb 2014 19:25:49 +0000
- To: libssh@xxxxxxxxxx
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 ptyexample, 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
Re: [PATCH V2] examples: Add ssh_server_fork example | Raphael Kubo da Costa <rakuco@xxxxxxxxxxx> |