From 9b3648ded00b05412e63a3260c7baf605d03e75f Mon Sep 17 00:00:00 2001 From: Aris Adamantiadis Date: Mon, 10 Oct 2011 23:04:06 +0200 Subject: connector: Implement ssh_connector_except() Signed-off-by: Aris Adamantiadis Reviewed-by: Andreas Schneider --- include/libssh/libssh.h | 24 ++ include/libssh/priv.h | 4 + src/CMakeLists.txt | 1 + src/channels.c | 17 ++ src/connector.c | 619 ++++++++++++++++++++++++++++++++++++++++++++++++ src/poll.c | 38 ++- 6 files changed, 698 insertions(+), 5 deletions(-) create mode 100644 src/connector.c diff --git a/include/libssh/libssh.h b/include/libssh/libssh.h index e384ba04..5b4d4efa 100644 --- a/include/libssh/libssh.h +++ b/include/libssh/libssh.h @@ -123,6 +123,7 @@ typedef struct ssh_scp_struct* ssh_scp; typedef struct ssh_session_struct* ssh_session; typedef struct ssh_string_struct* ssh_string; typedef struct ssh_event_struct* ssh_event; +typedef struct ssh_connector_struct * ssh_connector; typedef void* ssh_gssapi_creds; /* Socket type */ @@ -373,6 +374,15 @@ enum ssh_scp_request_types { SSH_SCP_REQUEST_WARNING }; +enum ssh_connector_flags_e { + /** Only the standard stream of the channel */ + SSH_CONNECTOR_STDOUT = 1, + /** Only the exception stream of the channel */ + SSH_CONNECTOR_STDERR = 2, + /** Merge both standard and exception streams */ + SSH_CONNECTOR_BOTH = 3 +}; + LIBSSH_API int ssh_blocking_flush(ssh_session session, int timeout); LIBSSH_API ssh_channel ssh_channel_accept_x11(ssh_channel channel, int timeout_ms); LIBSSH_API int ssh_channel_change_pty_size(ssh_channel channel,int cols,int rows); @@ -422,6 +432,18 @@ LIBSSH_API uint32_t ssh_channel_window_size(ssh_channel channel); LIBSSH_API char *ssh_basename (const char *path); LIBSSH_API void ssh_clean_pubkey_hash(unsigned char **hash); LIBSSH_API int ssh_connect(ssh_session session); + +LIBSSH_API ssh_connector ssh_connector_new(ssh_session session); +LIBSSH_API void ssh_connector_free(ssh_connector connector); +LIBSSH_API int ssh_connector_set_in_channel(ssh_connector connector, + ssh_channel channel, + enum ssh_connector_flags_e flags); +LIBSSH_API int ssh_connector_set_out_channel(ssh_connector connector, + ssh_channel channel, + enum ssh_connector_flags_e flags); +LIBSSH_API void ssh_connector_set_in_fd(ssh_connector connector, socket_t fd); +LIBSSH_API void ssh_connector_set_out_fd(ssh_connector connector, socket_t fd); + LIBSSH_API const char *ssh_copyright(void); LIBSSH_API void ssh_disconnect(ssh_session session); LIBSSH_API char *ssh_dirname (const char *path); @@ -672,9 +694,11 @@ LIBSSH_API ssh_event ssh_event_new(void); LIBSSH_API int ssh_event_add_fd(ssh_event event, socket_t fd, short events, ssh_event_callback cb, void *userdata); LIBSSH_API int ssh_event_add_session(ssh_event event, ssh_session session); +LIBSSH_API int ssh_event_add_connector(ssh_event event, ssh_connector connector); LIBSSH_API int ssh_event_dopoll(ssh_event event, int timeout); LIBSSH_API int ssh_event_remove_fd(ssh_event event, socket_t fd); LIBSSH_API int ssh_event_remove_session(ssh_event event, ssh_session session); +LIBSSH_API int ssh_event_remove_connector(ssh_event event, ssh_connector connector); LIBSSH_API void ssh_event_free(ssh_event event); LIBSSH_API const char* ssh_get_clientbanner(ssh_session session); LIBSSH_API const char* ssh_get_serverbanner(ssh_session session); diff --git a/include/libssh/priv.h b/include/libssh/priv.h index 9b34b247..a4e08cac 100644 --- a/include/libssh/priv.h +++ b/include/libssh/priv.h @@ -259,6 +259,10 @@ int decompress_buffer(ssh_session session,ssh_buffer buf, size_t maxlen); /* match.c */ int match_hostname(const char *host, const char *pattern, unsigned int len); +/* connector.c */ +int ssh_connector_set_event(ssh_connector connector, ssh_event event); +int ssh_connector_remove_event(ssh_connector connector); + #ifndef MIN #define MIN(a,b) ((a) < (b) ? (a) : (b)) #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1012ddf0..a3e08a61 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -115,6 +115,7 @@ set(libssh_SRCS client.c config.c connect.c + connector.c curve25519.c dh.c ecdh.c diff --git a/src/channels.c b/src/channels.c index c804bb46..f7cc086e 100644 --- a/src/channels.c +++ b/src/channels.c @@ -1363,6 +1363,23 @@ error: return SSH_ERROR; } +/** + * @brief Get the remote window size. + * + * This is the maximum amounts of bytes the remote side expects us to send + * before growing the window again. + * + * @param[in] channel The channel to query. + * + * @return The remote window size + * + * @warning A nonzero return value does not guarantee the socket is ready + * to send that much data. Buffering may happen in the local SSH + * packet buffer, so beware of really big window sizes. + * + * @warning A zero return value means ssh_channel_write (default settings) + * will block until the window grows back. + */ uint32_t ssh_channel_window_size(ssh_channel channel) { return channel->remote_window; } diff --git a/src/connector.c b/src/connector.c new file mode 100644 index 00000000..6f05008c --- /dev/null +++ b/src/connector.c @@ -0,0 +1,619 @@ +/* + * This file is part of the SSH Library + * + * Copyright (c) 2015 by Aris Adamantiadis + * + * The SSH Library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or (at your + * option) any later version. + * + * The SSH Library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with the SSH Library; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, + * MA 02111-1307, USA. + */ + +#include "libssh/priv.h" +#include "libssh/poll.h" +#include "libssh/callbacks.h" +#include "libssh/session.h" +#include +#define CHUNKSIZE 4096 + +struct ssh_connector_struct { + ssh_session session; + + ssh_channel in_channel; + ssh_channel out_channel; + + socket_t in_fd; + socket_t out_fd; + + ssh_poll_handle in_poll; + ssh_poll_handle out_poll; + + ssh_event event; + + int in_available; + int out_wontblock; + + struct ssh_channel_callbacks_struct in_channel_cb; + struct ssh_channel_callbacks_struct out_channel_cb; + + enum ssh_connector_flags_e in_flags; + enum ssh_connector_flags_e out_flags; +}; + +static int ssh_connector_channel_data_cb(ssh_session session, + ssh_channel channel, + void *data, + uint32_t len, + int is_stderr, + void *userdata); +static int ssh_connector_channel_write_wontblock_cb(ssh_session session, + ssh_channel channel, + size_t bytes, + void *userdata); + +ssh_connector ssh_connector_new(ssh_session session) +{ + ssh_connector connector; + + connector = calloc(1, sizeof(struct ssh_connector_struct)); + if (connector == NULL){ + ssh_set_error_oom(session); + return NULL; + } + + connector->session = session; + connector->in_fd = SSH_INVALID_SOCKET; + connector->out_fd = SSH_INVALID_SOCKET; + + ssh_callbacks_init(&connector->in_channel_cb); + ssh_callbacks_init(&connector->out_channel_cb); + + connector->in_channel_cb.userdata = connector; + connector->in_channel_cb.channel_data_function = ssh_connector_channel_data_cb; + + connector->out_channel_cb.userdata = connector; + connector->out_channel_cb.channel_write_wontblock_function = + ssh_connector_channel_write_wontblock_cb; + + return connector; +} + +void ssh_connector_free (ssh_connector connector) +{ + if (connector->event != NULL){ + ssh_connector_remove_event(connector); + } + + if (connector->in_poll != NULL) { + ssh_poll_free(connector->in_poll); + connector->in_poll = NULL; + } + + if (connector->out_poll != NULL) { + ssh_poll_free(connector->out_poll); + connector->out_poll = NULL; + } + + if (connector->in_channel != NULL) { + ssh_remove_channel_callbacks(connector->in_channel, + &connector->in_channel_cb); + } + if (connector->out_channel != NULL) { + ssh_remove_channel_callbacks(connector->out_channel, + &connector->out_channel_cb); + } + + free(connector); +} + +int ssh_connector_set_in_channel(ssh_connector connector, + ssh_channel channel, + enum ssh_connector_flags_e flags) +{ + connector->in_channel = channel; + connector->in_fd = SSH_INVALID_SOCKET; + connector->in_flags = flags; + + /* Fallback to default value for invalid flags */ + if (!(flags & SSH_CONNECTOR_STDOUT) && !(flags & SSH_CONNECTOR_STDERR)) { + connector->in_flags = SSH_CONNECTOR_STDOUT; + } + + return ssh_add_channel_callbacks(channel, &connector->in_channel_cb); +} + +int ssh_connector_set_out_channel(ssh_connector connector, + ssh_channel channel, + enum ssh_connector_flags_e flags) +{ + connector->out_channel = channel; + connector->out_fd = SSH_INVALID_SOCKET; + connector->out_flags = flags; + + /* Fallback to default value for invalid flags */ + if (!(flags & SSH_CONNECTOR_STDOUT) && !(flags & SSH_CONNECTOR_STDERR)) { + connector->in_flags = SSH_CONNECTOR_STDOUT; + } + + return ssh_add_channel_callbacks(channel, &connector->out_channel_cb); +} + +void ssh_connector_set_in_fd(ssh_connector connector, socket_t fd) +{ + connector->in_fd = fd; + connector->in_channel = NULL; +} + +void ssh_connector_set_out_fd(ssh_connector connector, socket_t fd) +{ + connector->out_fd = fd; + connector->out_channel = NULL; +} + +/* TODO */ +static void ssh_connector_except(ssh_connector connector, socket_t fd) +{ + (void) connector; + (void) fd; +} + +/* TODO */ +static void ssh_connector_except_channel(ssh_connector connector, + ssh_channel channel) +{ + (void) connector; + (void) channel; +} + +/** + * @internal + * + * @brief Reset the poll events to be followed for each file descriptors. + */ +static void ssh_connector_reset_pollevents(ssh_connector connector) +{ + if (connector->in_fd != SSH_INVALID_SOCKET) { + if (connector->in_available) { + ssh_poll_remove_events(connector->in_poll, POLLIN); + } else { + ssh_poll_add_events(connector->in_poll, POLLIN); + } + } + + if (connector->out_fd != SSH_INVALID_SOCKET) { + if (connector->out_wontblock) { + ssh_poll_remove_events(connector->out_poll, POLLOUT); + } else { + ssh_poll_add_events(connector->out_poll, POLLOUT); + } + } +} + +/** + * @internal + * + * @brief Callback called when a poll event is received on an input fd. + */ +static void ssh_connector_fd_in_cb(ssh_connector connector) +{ + unsigned char buffer[CHUNKSIZE]; + int r; + int toread = CHUNKSIZE; + int w; + int total = 0; + int rc; + + SSH_LOG(SSH_LOG_TRACE, "connector POLLIN event for fd %d", connector->in_fd); + + if (connector->out_wontblock) { + if (connector->out_channel != NULL) { + size_t size = ssh_channel_window_size(connector->out_channel); + + /* Don't attempt reading more than the window */ + toread = MIN(size, CHUNKSIZE); + } + + r = read(connector->in_fd, buffer, toread); + if (r < 0) { + ssh_connector_except(connector, connector->in_fd); + return; + } + + if (connector->out_channel != NULL) { + if (r == 0) { + rc = ssh_channel_send_eof(connector->out_channel); + (void)rc; /* TODO Handle rc? */ + } else if (r> 0) { + /* loop around ssh_channel_write in case our window reduced due to a race */ + while (total != r){ + if (connector->out_flags & SSH_CONNECTOR_STDOUT) { + w = ssh_channel_write(connector->out_channel, + buffer + total, + r - total); + } else { + w = ssh_channel_write_stderr(connector->out_channel, + buffer + total, + r - total); + } + if (w == SSH_ERROR) { + return; + } + total += w; + } + } + } else if (connector->out_fd != SSH_INVALID_SOCKET) { + if (r == 0){ + close (connector->out_fd); + connector->out_fd = SSH_INVALID_SOCKET; + } else { + /* + * Loop around write in case the write blocks even for CHUNKSIZE + * bytes + */ + while (total != r) { + w = write(connector->out_fd, buffer + total, r - total); + if (w < 0){ + ssh_connector_except(connector, connector->out_fd); + return; + } + total += w; + } + } + } else { + ssh_set_error(connector->session, SSH_FATAL, "output socket or channel closed"); + return; + } + connector->out_wontblock = 0; + connector->in_available = 0; + } else { + connector->in_available = 1; + } +} + +/** @internal + * @brief Callback called when a poll event is received on an output fd + */ +static void ssh_connector_fd_out_cb(ssh_connector connector){ + unsigned char buffer[CHUNKSIZE]; + int r; + int w; + int total = 0; + SSH_LOG(SSH_LOG_TRACE, "connector POLLOUT event for fd %d", connector->out_fd); + + if(connector->in_available){ + if (connector->in_channel != NULL){ + r = ssh_channel_read_nonblocking(connector->in_channel, buffer, CHUNKSIZE, 0); + if(r == SSH_ERROR){ + ssh_connector_except_channel(connector, connector->in_channel); + return; + } else if(r == 0 && ssh_channel_is_eof(connector->in_channel)){ + close(connector->out_fd); + connector->out_fd = SSH_INVALID_SOCKET; + return; + } else if(r>0) { + /* loop around write in case the write blocks even for CHUNKSIZE bytes */ + while (total != r){ + w = write(connector->out_fd, buffer + total, r - total); + if (w < 0){ + ssh_connector_except(connector, connector->out_fd); + return; + } + total += w; + } + } + } else if (connector->in_fd != SSH_INVALID_SOCKET){ + /* fallback on the socket input callback */ + connector->out_wontblock = 1; + ssh_connector_fd_in_cb(connector); + } else { + ssh_set_error(connector->session, + SSH_FATAL, + "Output socket or channel closed"); + return; + } + connector->in_available = 0; + connector->out_wontblock = 0; + } else { + connector->out_wontblock = 1; + } +} + +/** + * @internal + * + * @brief Callback called when a poll event is received on a file descriptor. + * + * This is for (input or output. + * + * @param[in] fd file descriptor receiving the event + * + * @param[in] revents received Poll(2) events + * + * @param[in] userdata connector + * + * @returns 0 + */ +static int ssh_connector_fd_cb(ssh_poll_handle p, + socket_t fd, + int revents, + void *userdata) +{ + ssh_connector connector = userdata; + + (void)p; + + if (revents & POLLERR) { + ssh_connector_except(connector, fd); + } else if((revents & POLLIN) && fd == connector->in_fd) { + ssh_connector_fd_in_cb(connector); + } else if((revents & POLLOUT) && fd == connector->out_fd) { + ssh_connector_fd_out_cb(connector); + } + ssh_connector_reset_pollevents(connector); + + return 0; +} + +/** + * @internal + * + * @brief Callback called when data is received on channel. + * + * @param[in] data Pointer to the data + * + * @param[in] len Length of data + * + * @param[in] is_stderr Set to 1 if the data are out of band + * + * @param[in] userdata The ssh connector + * + * @returns Amount of data bytes consumed + */ +static int ssh_connector_channel_data_cb(ssh_session session, + ssh_channel channel, + void *data, + uint32_t len, + int is_stderr, + void *userdata) +{ + ssh_connector connector = userdata; + int w; + size_t window; + + (void) session; + (void) channel; + (void) is_stderr; + + SSH_LOG(SSH_LOG_TRACE,"connector data on channel"); + + if (is_stderr && !(connector->in_flags & SSH_CONNECTOR_STDERR)) { + /* ignore stderr */ + return 0; + } else if (!is_stderr && !(connector->in_flags & SSH_CONNECTOR_STDOUT)) { + /* ignore stdout */ + return 0; + } + + if (connector->out_wontblock) { + if (connector->out_channel != NULL) { + int window_len; + + window = ssh_channel_window_size(connector->out_channel); + window_len = MIN(window, len); + + /* Route the data to the right exception channel */ + if (is_stderr && (connector->out_flags & SSH_CONNECTOR_STDERR)) { + w = ssh_channel_write_stderr(connector->out_channel, + data, + window_len); + } else if (!is_stderr && + (connector->out_flags & SSH_CONNECTOR_STDOUT)) { + w = ssh_channel_write(connector->out_channel, + data, + window_len); + } else if (connector->out_flags & SSH_CONNECTOR_STDOUT) { + w = ssh_channel_write(connector->out_channel, + data, + window_len); + } else { + w = ssh_channel_write_stderr(connector->out_channel, + data, + window_len); + } + if (w == SSH_ERROR) { + ssh_connector_except_channel(connector, connector->out_channel); + } + } else if (connector->out_fd != SSH_INVALID_SOCKET) { + w = write(connector->out_fd, data, len); + if (w < 0) + ssh_connector_except(connector, connector->out_fd); + } else { + ssh_set_error(session, SSH_FATAL, "output socket or channel closed"); + return SSH_ERROR; + } + + connector->out_wontblock = 0; + connector->in_available = 0; + if ((unsigned int)w < len) { + connector->in_available = 1; + } + ssh_connector_reset_pollevents(connector); + + return w; + } else { + connector->in_available = 1; + + return 0; + } +} + +/** + * @internal + * + * @brief Callback called when the channel is free to write. + * + * @param[in] bytes Amount of bytes that can be written without blocking + * + * @param[in] userdata The ssh connector + * + * @returns Amount of data bytes consumed + */ +static int ssh_connector_channel_write_wontblock_cb(ssh_session session, + ssh_channel channel, + size_t bytes, + void *userdata) +{ + ssh_connector connector = userdata; + uint8_t buffer[CHUNKSIZE]; + int r, w; + + (void) channel; + + SSH_LOG(SSH_LOG_TRACE, "Channel write won't block"); + if (connector->in_available) { + if (connector->in_channel != NULL) { + size_t len = MIN(CHUNKSIZE, bytes); + + r = ssh_channel_read_nonblocking(connector->in_channel, + buffer, + len, + 0); + if (r == SSH_ERROR) { + ssh_connector_except_channel(connector, connector->in_channel); + } else if(r == 0 && ssh_channel_is_eof(connector->in_channel)){ + ssh_channel_send_eof(connector->out_channel); + } else if (r > 0) { + w = ssh_channel_write(connector->out_channel, buffer, r); + if (w == SSH_ERROR) { + ssh_connector_except_channel(connector, + connector->out_channel); + } + } + } else if (connector->in_fd != SSH_INVALID_SOCKET) { + /* fallback on on the socket input callback */ + connector->out_wontblock = 1; + ssh_connector_fd_in_cb(connector); + ssh_connector_reset_pollevents(connector); + } else { + ssh_set_error(session, + SSH_FATAL, + "Output socket or channel closed"); + + return 0; + } + connector->in_available = 0; + connector->out_wontblock = 0; + } else { + connector->out_wontblock = 1; + } + + return 0; +} + +int ssh_connector_set_event(ssh_connector connector, ssh_event event) +{ + int rc = SSH_OK; + + if ((connector->in_fd == SSH_INVALID_SOCKET && + connector->in_channel == NULL) + || (connector->out_fd == SSH_INVALID_SOCKET && + connector->out_channel == NULL)) { + rc = SSH_ERROR; + ssh_set_error(connector->session,SSH_FATAL,"Connector not complete"); + goto error; + } + + connector->event = event; + if (connector->in_fd != SSH_INVALID_SOCKET) { + if (connector->in_poll == NULL) { + connector->in_poll = ssh_poll_new(connector->in_fd, + POLLIN|POLLERR, + ssh_connector_fd_cb, + connector); + } + rc = ssh_event_add_poll(event, connector->in_poll); + if (rc != SSH_OK) { + goto error; + } + } + + if (connector->out_fd != SSH_INVALID_SOCKET) { + if (connector->out_poll == NULL) { + connector->out_poll = ssh_poll_new(connector->out_fd, + POLLOUT|POLLERR, + ssh_connector_fd_cb, + connector); + } + + rc = ssh_event_add_poll(event, connector->out_poll); + if (rc != SSH_OK) { + goto error; + } + } + if (connector->in_channel != NULL) { + rc = ssh_event_add_session(event, + ssh_channel_get_session(connector->in_channel)); + if (rc != SSH_OK) + goto error; + if (ssh_channel_poll_timeout(connector->in_channel, 0, 0) > 0){ + connector->in_available = 1; + } + } + if(connector->out_channel != NULL) { + ssh_session session = ssh_channel_get_session(connector->out_channel); + + rc = ssh_event_add_session(event, session); + if (rc != SSH_OK) { + goto error; + } + if (ssh_channel_window_size(connector->out_channel) > 0) { + connector->out_wontblock = 1; + } + } + +error: + return rc; +} + +int ssh_connector_remove_event(ssh_connector connector) { + ssh_session session; + + if (connector->in_poll != NULL) { + ssh_event_remove_poll(connector->event, connector->in_poll); + ssh_poll_free(connector->in_poll); + connector->in_poll = NULL; + } + + if (connector->out_poll != NULL) { + ssh_event_remove_poll(connector->event, connector->out_poll); + ssh_poll_free(connector->out_poll); + connector->out_poll = NULL; + } + + if (connector->in_channel != NULL) { + session = ssh_channel_get_session(connector->in_channel); + + ssh_event_remove_session(connector->event, session); + connector->in_channel = NULL; + } + + if (connector->out_channel != NULL) { + session = ssh_channel_get_session(connector->out_channel); + + ssh_event_remove_session(connector->event, session); + connector->out_channel = NULL; + } + connector->event = NULL; + + return SSH_OK; +} diff --git a/src/poll.c b/src/poll.c index 142fb644..c698c13f 100644 --- a/src/poll.c +++ b/src/poll.c @@ -850,17 +850,34 @@ int ssh_event_add_session(ssh_event event, ssh_session session) { } /** - * @brief Poll all the sockets and sessions associated through an event object. - * If any of the events are set after the poll, the - * call back functions of the sessions or sockets will be called. - * This function should be called once within the programs main loop. + * @brief Add a connector to the SSH event loop + * + * @param[in] event The SSH event loop + * + * @param[in] connector The connector object + * + * @return SSH_OK + * + * @return SSH_ERROR in case of error + */ +int ssh_event_add_connector(ssh_event event, ssh_connector connector){ + return ssh_connector_set_event(connector, event); +} + +/** + * @brief Poll all the sockets and sessions associated through an event object.i + * + * If any of the events are set after the poll, the call back functions of the + * sessions or sockets will be called. + * This function should be called once within the programs main loop. * * @param event The ssh_event object to poll. + * * @param timeout An upper limit on the time for which the poll will * block, in milliseconds. Specifying a negative value * means an infinite timeout. This parameter is passed to * the poll() function. - * @returns SSH_OK No error. + * @returns SSH_OK on success. * SSH_ERROR Error happened during the poll. */ int ssh_event_dopoll(ssh_event event, int timeout) { @@ -966,6 +983,17 @@ int ssh_event_remove_session(ssh_event event, ssh_session session) { return rc; } +/** @brief Remove a connector from an event context + * @param[in] event The ssh_event object. + * @param[in] connector connector object to remove + * @return SSH_OK on success + * @return SSH_ERROR on failure + */ +int ssh_event_remove_connector(ssh_event event, ssh_connector connector){ + (void)event; + return ssh_connector_remove_event(connector); +} + /** * @brief Free an event context. * -- cgit v1.2.3