diff options
Diffstat (limited to 'libssh/src/client.c')
-rw-r--r-- | libssh/src/client.c | 696 |
1 files changed, 696 insertions, 0 deletions
diff --git a/libssh/src/client.c b/libssh/src/client.c new file mode 100644 index 00000000..ac1b83db --- /dev/null +++ b/libssh/src/client.c @@ -0,0 +1,696 @@ +/* + * client.c - SSH client functions + * + * This file is part of the SSH Library + * + * Copyright (c) 2003-2008 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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifndef _WIN32 +#include <netinet/in.h> +#include <arpa/inet.h> +#endif + +#include "libssh/priv.h" +#include "libssh/ssh2.h" +#include "libssh/buffer.h" +#include "libssh/packet.h" +#include "libssh/options.h" +#include "libssh/socket.h" +#include "libssh/session.h" +#include "libssh/dh.h" +#include "libssh/ecdh.h" +#include "libssh/threads.h" +#include "libssh/misc.h" +#include "libssh/pki.h" +#include "libssh/kex.h" + +#define set_status(session, status) do {\ + if (session->common.callbacks && session->common.callbacks->connect_status_function) \ + session->common.callbacks->connect_status_function(session->common.callbacks->userdata, status); \ + } while (0) + +/** + * @internal + * @brief Callback to be called when the socket is connected or had a + * connection error. Changes the state of the session and updates the error + * message. + * @param code one of SSH_SOCKET_CONNECTED_OK or SSH_SOCKET_CONNECTED_ERROR + * @param user is a pointer to session + */ +static void socket_callback_connected(int code, int errno_code, void *user){ + ssh_session session=(ssh_session)user; + enter_function(); + if(session->session_state != SSH_SESSION_STATE_CONNECTING){ + ssh_set_error(session,SSH_FATAL, "Wrong state in socket_callback_connected : %d", + session->session_state); + leave_function(); + return; + } + ssh_log(session,SSH_LOG_RARE,"Socket connection callback: %d (%d)",code, errno_code); + if(code == SSH_SOCKET_CONNECTED_OK) + session->session_state=SSH_SESSION_STATE_SOCKET_CONNECTED; + else { + session->session_state=SSH_SESSION_STATE_ERROR; + ssh_set_error(session,SSH_FATAL,"%s",strerror(errno_code)); + } + session->ssh_connection_callback(session); + leave_function(); +} + +/** + * @internal + * + * @brief Gets the banner from socket and saves it in session. + * Updates the session state + * + * @param data pointer to the beginning of header + * @param len size of the banner + * @param user is a pointer to session + * @returns Number of bytes processed, or zero if the banner is not complete. + */ +static int callback_receive_banner(const void *data, size_t len, void *user) { + char *buffer = (char *)data; + ssh_session session=(ssh_session) user; + char *str = NULL; + size_t i; + int ret=0; + enter_function(); + if(session->session_state != SSH_SESSION_STATE_SOCKET_CONNECTED){ + ssh_set_error(session,SSH_FATAL,"Wrong state in callback_receive_banner : %d",session->session_state); + leave_function(); + return SSH_ERROR; + } + for(i=0;i<len;++i){ +#ifdef WITH_PCAP + if(session->pcap_ctx && buffer[i] == '\n'){ + ssh_pcap_context_write(session->pcap_ctx,SSH_PCAP_DIR_IN,buffer,i+1,i+1); + } +#endif + if(buffer[i]=='\r') + buffer[i]='\0'; + if(buffer[i]=='\n'){ + buffer[i]='\0'; + str=strdup(buffer); + /* number of bytes read */ + ret=i+1; + session->serverbanner=str; + session->session_state=SSH_SESSION_STATE_BANNER_RECEIVED; + ssh_log(session,SSH_LOG_PACKET,"Received banner: %s",str); + session->ssh_connection_callback(session); + leave_function(); + return ret; + } + if(i>127){ + /* Too big banner */ + session->session_state=SSH_SESSION_STATE_ERROR; + ssh_set_error(session,SSH_FATAL,"Receiving banner: too large banner"); + leave_function(); + return 0; + } + } + leave_function(); + return ret; +} + +/** @internal + * @brief Sends a SSH banner to the server. + * + * @param session The SSH session to use. + * + * @param server Send client or server banner. + * + * @return 0 on success, < 0 on error. + */ +int ssh_send_banner(ssh_session session, int server) { + const char *banner = NULL; + char buffer[128] = {0}; + int err=SSH_ERROR; + + enter_function(); + + banner = session->version == 1 ? CLIENTBANNER1 : CLIENTBANNER2; + + if (server) { + session->serverbanner = strdup(banner); + if (session->serverbanner == NULL) { + goto end; + } + } else { + session->clientbanner = strdup(banner); + if (session->clientbanner == NULL) { + goto end; + } + } + + snprintf(buffer, 128, "%s\n", banner); + + if (ssh_socket_write(session->socket, buffer, strlen(buffer)) == SSH_ERROR) { + goto end; + } +#ifdef WITH_PCAP + if(session->pcap_ctx) + ssh_pcap_context_write(session->pcap_ctx,SSH_PCAP_DIR_OUT,buffer,strlen(buffer),strlen(buffer)); +#endif + err=SSH_OK; +end: + leave_function(); + return err; +} + +/** @internal + * @brief launches the DH handshake state machine + * @param session session handle + * @returns SSH_OK or SSH_ERROR + * @warning this function returning is no proof that DH handshake is + * completed + */ +static int dh_handshake(ssh_session session) { + + int rc = SSH_AGAIN; + + enter_function(); + + switch (session->dh_handshake_state) { + case DH_STATE_INIT: + switch(session->next_crypto->kex_type){ + case SSH_KEX_DH_GROUP1_SHA1: + case SSH_KEX_DH_GROUP14_SHA1: + rc = ssh_client_dh_init(session); + break; +#ifdef HAVE_ECDH + case SSH_KEX_ECDH_SHA2_NISTP256: + rc = ssh_client_ecdh_init(session); + break; +#endif + default: + rc=SSH_ERROR; + goto error; + } + + if (rc == SSH_ERROR) { + goto error; + } + + session->dh_handshake_state = DH_STATE_INIT_SENT; + case DH_STATE_INIT_SENT: + /* wait until ssh_packet_dh_reply is called */ + break; + case DH_STATE_NEWKEYS_SENT: + /* wait until ssh_packet_newkeys is called */ + break; + case DH_STATE_FINISHED: + leave_function(); + return SSH_OK; + default: + ssh_set_error(session, SSH_FATAL, "Invalid state in dh_handshake(): %d", + session->dh_handshake_state); + leave_function(); + return SSH_ERROR; + } +error: + leave_function(); + return rc; +} + +static int ssh_service_request_termination(void *s){ + ssh_session session = (ssh_session)s; + if(session->session_state == SSH_SESSION_STATE_ERROR || + session->auth_service_state != SSH_AUTH_SERVICE_SENT) + return 1; + else + return 0; +} + +/** + * @internal + * + * @brief Request a service from the SSH server. + * + * Service requests are for example: ssh-userauth, ssh-connection, etc. + * + * @param session The session to use to ask for a service request. + * @param service The service request. + * + * @return SSH_OK on success + * @return SSH_ERROR on error + * @return SSH_AGAIN No response received yet + * @bug actually only works with ssh-userauth + */ +int ssh_service_request(ssh_session session, const char *service) { + ssh_string service_s = NULL; + int rc=SSH_ERROR; + enter_function(); + if(session->auth_service_state != SSH_AUTH_SERVICE_NONE) + goto pending; + if (buffer_add_u8(session->out_buffer, SSH2_MSG_SERVICE_REQUEST) < 0) { + goto error; + } + service_s = ssh_string_from_char(service); + if (service_s == NULL) { + goto error; + } + + if (buffer_add_ssh_string(session->out_buffer,service_s) < 0) { + ssh_string_free(service_s); + goto error; + } + ssh_string_free(service_s); + session->auth_service_state=SSH_AUTH_SERVICE_SENT; + if (packet_send(session) == SSH_ERROR) { + ssh_set_error(session, SSH_FATAL, + "Sending SSH2_MSG_SERVICE_REQUEST failed."); + goto error; + } + + ssh_log(session, SSH_LOG_PACKET, + "Sent SSH_MSG_SERVICE_REQUEST (service %s)", service); +pending: + rc=ssh_handle_packets_termination(session,SSH_TIMEOUT_USER, + ssh_service_request_termination, session); + if(rc == SSH_ERROR) + goto error; + switch(session->auth_service_state){ + case SSH_AUTH_SERVICE_DENIED: + ssh_set_error(session,SSH_FATAL,"ssh_auth_service request denied"); + break; + case SSH_AUTH_SERVICE_ACCEPTED: + rc=SSH_OK; + break; + case SSH_AUTH_SERVICE_SENT: + rc=SSH_AGAIN; + break; + case SSH_AUTH_SERVICE_NONE: + case SSH_AUTH_SERVICE_USER_SENT: + /* Invalid state, SSH1 specific */ + rc=SSH_ERROR; + break; + } +error: + leave_function(); + return rc; +} + +/** + * @addtogroup libssh_session + * + * @{ + */ + +/** + * @internal + * + * @brief A function to be called each time a step has been done in the + * connection. + */ +static void ssh_client_connection_callback(ssh_session session){ + int ssh1,ssh2; + enter_function(); + switch(session->session_state){ + case SSH_SESSION_STATE_NONE: + case SSH_SESSION_STATE_CONNECTING: + case SSH_SESSION_STATE_SOCKET_CONNECTED: + break; + case SSH_SESSION_STATE_BANNER_RECEIVED: + if (session->serverbanner == NULL) { + goto error; + } + set_status(session, 0.4f); + ssh_log(session, SSH_LOG_RARE, + "SSH server banner: %s", session->serverbanner); + + /* Here we analyze the different protocols the server allows. */ + if (ssh_analyze_banner(session, 0, &ssh1, &ssh2) < 0) { + goto error; + } + /* Here we decide which version of the protocol to use. */ + if (ssh2 && session->opts.ssh2) { + session->version = 2; +#ifdef WITH_SSH1 + } else if(ssh1 && session->opts.ssh1) { + session->version = 1; +#endif + } else if(ssh1 && !session->opts.ssh1){ +#ifdef WITH_SSH1 + ssh_set_error(session, SSH_FATAL, + "SSH-1 protocol not available (configure session to allow SSH-1)"); + goto error; +#else + ssh_set_error(session, SSH_FATAL, + "SSH-1 protocol not available (libssh compiled without SSH-1 support)"); + goto error; +#endif + } else { + ssh_set_error(session, SSH_FATAL, + "No version of SSH protocol usable (banner: %s)", + session->serverbanner); + goto error; + } + /* from now, the packet layer is handling incoming packets */ + if(session->version==2) + session->socket_callbacks.data=ssh_packet_socket_callback; +#ifdef WITH_SSH1 + else + session->socket_callbacks.data=ssh_packet_socket_callback1; +#endif + ssh_packet_set_default_callbacks(session); + session->session_state=SSH_SESSION_STATE_INITIAL_KEX; + ssh_send_banner(session, 0); + set_status(session, 0.5f); + break; + case SSH_SESSION_STATE_INITIAL_KEX: + /* TODO: This state should disappear in favor of get_key handle */ +#ifdef WITH_SSH1 + if(session->version==1){ + if (ssh_get_kex1(session) < 0) + goto error; + set_status(session,0.6f); + session->connected = 1; + break; + } +#endif + break; + case SSH_SESSION_STATE_KEXINIT_RECEIVED: + set_status(session,0.6f); + ssh_list_kex(session, &session->next_crypto->server_kex); + if (set_client_kex(session) < 0) { + goto error; + } + if (ssh_kex_select_methods(session) == SSH_ERROR) + goto error; + if (ssh_send_kex(session, 0) < 0) { + goto error; + } + set_status(session,0.8f); + session->session_state=SSH_SESSION_STATE_DH; + if (dh_handshake(session) == SSH_ERROR) { + goto error; + } + case SSH_SESSION_STATE_DH: + if(session->dh_handshake_state==DH_STATE_FINISHED){ + set_status(session,1.0f); + session->connected = 1; + if (session->flags & SSH_SESSION_FLAG_AUTHENTICATED) + session->session_state = SSH_SESSION_STATE_AUTHENTICATED; + else + session->session_state=SSH_SESSION_STATE_AUTHENTICATING; + } + break; + case SSH_SESSION_STATE_AUTHENTICATING: + break; + case SSH_SESSION_STATE_ERROR: + goto error; + default: + ssh_set_error(session,SSH_FATAL,"Invalid state %d",session->session_state); + } + leave_function(); + return; + error: + ssh_socket_close(session->socket); + session->alive = 0; + session->session_state=SSH_SESSION_STATE_ERROR; + leave_function(); +} + +/** @internal + * @brief describe under which conditions the ssh_connect function may stop + */ +static int ssh_connect_termination(void *user){ + ssh_session session = (ssh_session)user; + switch(session->session_state){ + case SSH_SESSION_STATE_ERROR: + case SSH_SESSION_STATE_AUTHENTICATING: + case SSH_SESSION_STATE_DISCONNECTED: + return 1; + default: + return 0; + } +} + +/** + * @brief Connect to the ssh server. + * + * @param[in] session The ssh session to connect. + * + * @returns SSH_OK on success, SSH_ERROR on error. + * @returns SSH_AGAIN, if the session is in nonblocking mode, + * and call must be done again. + * + * @see ssh_new() + * @see ssh_disconnect() + */ +int ssh_connect(ssh_session session) { + int ret; + + if (session == NULL) { + return SSH_ERROR; + } + + enter_function(); + switch(session->pending_call_state){ + case SSH_PENDING_CALL_NONE: + break; + case SSH_PENDING_CALL_CONNECT: + goto pending; + default: + ssh_set_error(session,SSH_FATAL,"Bad call during pending SSH call in ssh_connect"); + leave_function(); + return SSH_ERROR; + } + session->alive = 0; + session->client = 1; + + if (ssh_init() < 0) { + leave_function(); + return SSH_ERROR; + } + if (session->opts.fd == SSH_INVALID_SOCKET && + session->opts.host == NULL && + session->opts.ProxyCommand == NULL) { + ssh_set_error(session, SSH_FATAL, "Hostname required"); + leave_function(); + return SSH_ERROR; + } + + ret = ssh_options_apply(session); + if (ret < 0) { + ssh_set_error(session, SSH_FATAL, "Couldn't apply options"); + leave_function(); + return SSH_ERROR; + } + ssh_log(session,SSH_LOG_RARE,"libssh %s, using threading %s", ssh_copyright(), ssh_threads_get_type()); + session->ssh_connection_callback = ssh_client_connection_callback; + session->session_state=SSH_SESSION_STATE_CONNECTING; + ssh_socket_set_callbacks(session->socket,&session->socket_callbacks); + session->socket_callbacks.connected=socket_callback_connected; + session->socket_callbacks.data=callback_receive_banner; + session->socket_callbacks.exception=ssh_socket_exception_callback; + session->socket_callbacks.userdata=session; + if (session->opts.fd != SSH_INVALID_SOCKET) { + ssh_socket_set_fd(session->socket, session->opts.fd); + ret=SSH_OK; +#ifndef _WIN32 + } else if (session->opts.ProxyCommand != NULL){ + ret = ssh_socket_connect_proxycommand(session->socket, + session->opts.ProxyCommand); +#endif + } else { + ret=ssh_socket_connect(session->socket, + session->opts.host, + session->opts.port, + session->opts.bindaddr); + } + if (ret == SSH_ERROR) { + leave_function(); + return SSH_ERROR; + } + + set_status(session, 0.2f); + + session->alive = 1; + ssh_log(session,SSH_LOG_PROTOCOL,"Socket connecting, now waiting for the callbacks to work"); +pending: + session->pending_call_state=SSH_PENDING_CALL_CONNECT; + if(ssh_is_blocking(session)) { + int timeout = (session->opts.timeout * 1000) + + (session->opts.timeout_usec / 1000); + if (timeout == 0) { + timeout = 10 * 1000; + } + ssh_log(session,SSH_LOG_PACKET,"ssh_connect: Actual timeout : %d", timeout); + ret = ssh_handle_packets_termination(session, timeout, ssh_connect_termination, session); + if (ret == SSH_ERROR || !ssh_connect_termination(session)) { + ssh_set_error(session, SSH_FATAL, + "Timeout connecting to %s", session->opts.host); + session->session_state = SSH_SESSION_STATE_ERROR; + } + } + else { + ret = ssh_handle_packets_termination(session, + SSH_TIMEOUT_NONBLOCKING, + ssh_connect_termination, + session); + if (ret == SSH_ERROR) { + session->session_state = SSH_SESSION_STATE_ERROR; + } + } + ssh_log(session,SSH_LOG_PACKET,"ssh_connect: Actual state : %d",session->session_state); + if(!ssh_is_blocking(session) && !ssh_connect_termination(session)){ + leave_function(); + return SSH_AGAIN; + } + leave_function(); + session->pending_call_state=SSH_PENDING_CALL_NONE; + if(session->session_state == SSH_SESSION_STATE_ERROR || session->session_state == SSH_SESSION_STATE_DISCONNECTED) + return SSH_ERROR; + return SSH_OK; +} + +/** + * @brief Get the issue banner from the server. + * + * This is the banner showing a disclaimer to users who log in, + * typically their right or the fact that they will be monitored. + * + * @param[in] session The SSH session to use. + * + * @return A newly allocated string with the banner, NULL on error. + */ +char *ssh_get_issue_banner(ssh_session session) { + if (session == NULL || session->banner == NULL) { + return NULL; + } + + return ssh_string_to_char(session->banner); +} + +/** + * @brief Get the version of the OpenSSH server, if it is not an OpenSSH server + * then 0 will be returned. + * + * You can use the SSH_VERSION_INT macro to compare version numbers. + * + * @param[in] session The SSH session to use. + * + * @return The version number if available, 0 otherwise. + */ +int ssh_get_openssh_version(ssh_session session) { + if (session == NULL) { + return 0; + } + + return session->openssh; +} + +/** + * @brief Disconnect from a session (client or server). + * The session can then be reused to open a new session. + * + * @param[in] session The SSH session to use. + */ +void ssh_disconnect(ssh_session session) { + ssh_string str = NULL; + struct ssh_iterator *it; + + if (session == NULL) { + return; + } + + enter_function(); + + if (ssh_socket_is_open(session->socket)) { + if (buffer_add_u8(session->out_buffer, SSH2_MSG_DISCONNECT) < 0) { + goto error; + } + if (buffer_add_u32(session->out_buffer, + htonl(SSH2_DISCONNECT_BY_APPLICATION)) < 0) { + goto error; + } + + str = ssh_string_from_char("Bye Bye"); + if (str == NULL) { + goto error; + } + + if (buffer_add_ssh_string(session->out_buffer,str) < 0) { + ssh_string_free(str); + goto error; + } + ssh_string_free(str); + + packet_send(session); + ssh_socket_close(session->socket); + } +error: + session->alive = 0; + if(session->socket){ + ssh_socket_reset(session->socket); + } + session->opts.fd = SSH_INVALID_SOCKET; + session->session_state=SSH_SESSION_STATE_DISCONNECTED; + + while ((it=ssh_list_get_iterator(session->channels)) != NULL) { + ssh_channel_free(ssh_iterator_value(ssh_channel,it)); + ssh_list_remove(session->channels, it); + } + if(session->current_crypto){ + crypto_free(session->current_crypto); + session->current_crypto=NULL; + } + if(session->in_buffer) + buffer_reinit(session->in_buffer); + if(session->out_buffer) + buffer_reinit(session->out_buffer); + if(session->in_hashbuf) + buffer_reinit(session->in_hashbuf); + if(session->out_hashbuf) + buffer_reinit(session->out_hashbuf); + session->auth_methods = 0; + SAFE_FREE(session->serverbanner); + SAFE_FREE(session->clientbanner); + + if(session->ssh_message_list){ + ssh_message msg; + while((msg=ssh_list_pop_head(ssh_message ,session->ssh_message_list)) + != NULL){ + ssh_message_free(msg); + } + ssh_list_free(session->ssh_message_list); + session->ssh_message_list=NULL; + } + + if (session->packet_callbacks){ + ssh_list_free(session->packet_callbacks); + session->packet_callbacks=NULL; + } + + leave_function(); +} + +const char *ssh_copyright(void) { + return SSH_STRINGIFY(LIBSSH_VERSION) " (c) 2003-2010 Aris Adamantiadis " + "(aris@0xbadc0de.be) Distributed under the LGPL, please refer to COPYING " + "file for information about your rights"; +} +/** @} */ + +/* vim: set ts=4 sw=4 et cindent: */ |