diff options
Diffstat (limited to 'src/client.c')
-rw-r--r-- | src/client.c | 814 |
1 files changed, 814 insertions, 0 deletions
diff --git a/src/client.c b/src/client.c new file mode 100644 index 00000000..d12aa117 --- /dev/null +++ b/src/client.c @@ -0,0 +1,814 @@ +/* + * 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 <arpa/inet.h> +#endif + +#include "libssh/priv.h" +#include "libssh/ssh2.h" +#include "libssh/buffer.h" +#include "libssh/packet.h" +#include "libssh/socket.h" +#include "libssh/session.h" +#include "libssh/dh.h" + +#define set_status(session, status) do {\ + if (session->callbacks && session->callbacks->connect_status_function) \ + session->callbacks->connect_status_function(session->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(); + 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(); + 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 Analyze the SSH banner to find out if we have a SSHv1 or SSHv2 + * server. + * + * @param session The session to analyze the banner from. + * @param ssh1 The variable which is set if it is a SSHv1 server. + * @param ssh2 The variable which is set if it is a SSHv2 server. + * + * @return 0 on success, < 0 on error. + * + * @see ssh_get_banner() + */ +static int ssh_analyze_banner(ssh_session session, int *ssh1, int *ssh2) { + const char *banner = session->serverbanner; + const char *openssh; + + ssh_log(session, SSH_LOG_RARE, "Analyzing banner: %s", banner); + + if (strncmp(banner, "SSH-", 4) != 0) { + ssh_set_error(session, SSH_FATAL, "Protocol mismatch: %s", banner); + return -1; + } + + /* + * Typical banners e.g. are: + * SSH-1.5-blah + * SSH-1.99-blah + * SSH-2.0-blah + */ + switch(banner[4]) { + case '1': + *ssh1 = 1; + if (banner[6] == '9') { + *ssh2 = 1; + } else { + *ssh2 = 0; + } + break; + case '2': + *ssh1 = 0; + *ssh2 = 1; + break; + default: + ssh_set_error(session, SSH_FATAL, "Protocol mismatch: %s", banner); + return -1; + } + + openssh = strstr(banner, "OpenSSH"); + if (openssh != NULL) { + int major, minor; + major = strtol(openssh + 8, (char **) NULL, 10); + minor = strtol(openssh + 10, (char **) NULL, 10); + session->openssh = SSH_VERSION_INT(major, minor, 0); + ssh_log(session, SSH_LOG_RARE, + "We are talking to an OpenSSH server version: %d.%d (%x)", + major, minor, session->openssh); + } + + return 0; +} + +/** @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 (session->xbanner) { + banner = session->xbanner; + } + + 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; + } + if (ssh_socket_nonblocking_flush(session->socket) == 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; +} + + + +SSH_PACKET_CALLBACK(ssh_packet_dh_reply){ + ssh_string f = NULL; + ssh_string pubkey = NULL; + ssh_string signature = NULL; + (void)type; + (void)user; + ssh_log(session,SSH_LOG_PROTOCOL,"Received SSH_KEXDH_REPLY"); + if(session->session_state!= SSH_SESSION_STATE_DH && + session->dh_handshake_state != DH_STATE_INIT_SENT){ + ssh_set_error(session,SSH_FATAL,"ssh_packet_dh_reply called in wrong state : %d:%d", + session->session_state,session->dh_handshake_state); + goto error; + } + + pubkey = buffer_get_ssh_string(packet); + if (pubkey == NULL){ + ssh_set_error(session,SSH_FATAL, "No public key in packet"); + goto error; + } + dh_import_pubkey(session, pubkey); + + f = buffer_get_ssh_string(packet); + if (f == NULL) { + ssh_set_error(session,SSH_FATAL, "No F number in packet"); + goto error; + } + if (dh_import_f(session, f) < 0) { + ssh_set_error(session, SSH_FATAL, "Cannot import f number"); + goto error; + } + ssh_string_burn(f); + ssh_string_free(f); + f=NULL; + signature = buffer_get_ssh_string(packet); + if (signature == NULL) { + ssh_set_error(session, SSH_FATAL, "No signature in packet"); + goto error; + } + session->dh_server_signature = signature; + signature=NULL; /* ownership changed */ + if (dh_build_k(session) < 0) { + ssh_set_error(session, SSH_FATAL, "Cannot build k number"); + goto error; + } + + /* Send the MSG_NEWKEYS */ + if (buffer_add_u8(session->out_buffer, SSH2_MSG_NEWKEYS) < 0) { + goto error; + } + + packet_send(session); + ssh_log(session, SSH_LOG_PROTOCOL, "SSH_MSG_NEWKEYS sent"); + + session->dh_handshake_state = DH_STATE_NEWKEYS_SENT; + return SSH_PACKET_USED; +error: + session->session_state=SSH_SESSION_STATE_ERROR; + return SSH_PACKET_USED; +} + +SSH_PACKET_CALLBACK(ssh_packet_newkeys){ + ssh_string signature = NULL; + int rc; + (void)packet; + (void)user; + (void)type; + ssh_log(session, SSH_LOG_PROTOCOL, "Received SSH_MSG_NEWKEYS"); + if(session->session_state!= SSH_SESSION_STATE_DH && + session->dh_handshake_state != DH_STATE_NEWKEYS_SENT){ + ssh_set_error(session,SSH_FATAL,"ssh_packet_newkeys called in wrong state : %d:%d", + session->session_state,session->dh_handshake_state); + goto error; + } + if(session->server){ + /* server things are done in server.c */ + session->dh_handshake_state=DH_STATE_FINISHED; + } else { + /* client */ + rc = make_sessionid(session); + if (rc != SSH_OK) { + goto error; + } + + /* + * Set the cryptographic functions for the next crypto + * (it is needed for generate_session_keys for key lengths) + */ + if (crypt_set_algorithms(session)) { + goto error; + } + + if (generate_session_keys(session) < 0) { + goto error; + } + + /* Verify the host's signature. FIXME do it sooner */ + signature = session->dh_server_signature; + session->dh_server_signature = NULL; + if (signature_verify(session, signature)) { + goto error; + } + + /* forget it for now ... */ + ssh_string_burn(signature); + ssh_string_free(signature); + signature=NULL; + /* + * Once we got SSH2_MSG_NEWKEYS we can switch next_crypto and + * current_crypto + */ + if (session->current_crypto) { + crypto_free(session->current_crypto); + session->current_crypto=NULL; + } + + /* FIXME later, include a function to change keys */ + session->current_crypto = session->next_crypto; + + session->next_crypto = crypto_new(); + if (session->next_crypto == NULL) { + ssh_set_error_oom(session); + goto error; + } + } + session->dh_handshake_state = DH_STATE_FINISHED; + session->ssh_connection_callback(session); + return SSH_PACKET_USED; +error: + session->session_state=SSH_SESSION_STATE_ERROR; + return SSH_PACKET_USED; +} + +/** @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) { + ssh_string e = NULL; + ssh_string f = NULL; + ssh_string signature = NULL; + int rc = SSH_ERROR; + + enter_function(); + + switch (session->dh_handshake_state) { + case DH_STATE_INIT: + if (buffer_add_u8(session->out_buffer, SSH2_MSG_KEXDH_INIT) < 0) { + goto error; + } + + if (dh_generate_x(session) < 0) { + goto error; + } + if (dh_generate_e(session) < 0) { + goto error; + } + + e = dh_get_e(session); + if (e == NULL) { + goto error; + } + + if (buffer_add_ssh_string(session->out_buffer, e) < 0) { + goto error; + } + ssh_string_burn(e); + ssh_string_free(e); + e=NULL; + + rc = packet_send(session); + 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; + } + + leave_function(); + return SSH_AGAIN; +error: + if(e != NULL){ + ssh_string_burn(e); + ssh_string_free(e); + } + if(f != NULL){ + ssh_string_burn(f); + ssh_string_free(f); + } + if(signature != NULL){ + ssh_string_burn(signature); + ssh_string_free(signature); + } + + leave_function(); + return rc; +} + +/** + * @internal + * @brief handles a SSH_SERVICE_ACCEPT packet + * + */ +SSH_PACKET_CALLBACK(ssh_packet_service_accept){ + (void)packet; + (void)type; + (void)user; + enter_function(); + session->auth_service_state=SSH_AUTH_SERVICE_ACCEPTED; + ssh_log(session, SSH_LOG_PACKET, + "Received SSH_MSG_SERVICE_ACCEPT"); + leave_function(); + return SSH_PACKET_USED; +} + +/** + * @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(); + switch(session->auth_service_state){ + case SSH_AUTH_SERVICE_NONE: + if (buffer_add_u8(session->out_buffer, SSH2_MSG_SERVICE_REQUEST) < 0) { + break; + } + service_s = ssh_string_from_char(service); + if (service_s == NULL) { + break; + } + + if (buffer_add_ssh_string(session->out_buffer,service_s) < 0) { + ssh_string_free(service_s); + break; + } + ssh_string_free(service_s); + + if (packet_send(session) == SSH_ERROR) { + ssh_set_error(session, SSH_FATAL, + "Sending SSH2_MSG_SERVICE_REQUEST failed."); + break; + } + + ssh_log(session, SSH_LOG_PACKET, + "Sent SSH_MSG_SERVICE_REQUEST (service %s)", service); + session->auth_service_state=SSH_AUTH_SERVICE_SENT; + rc=SSH_AGAIN; + break; + 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_USER_SENT: + /* Invalid state, SSH1 specific */ + rc=SSH_ERROR; + break; + } + + 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, &ssh1, &ssh2) < 0) { + goto error; + } + /* Here we decide which version of the protocol to use. */ + if (ssh2 && session->ssh2) { + session->version = 2; + } else if(ssh1 && session->ssh1) { + session->version = 1; + } else if(ssh1 && !session->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); + ssh_send_banner(session, 0); + set_status(session, 0.5f); + session->session_state=SSH_SESSION_STATE_INITIAL_KEX; + 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->server_kex); + if (set_kex(session) < 0) { + 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; + 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(); +} + +/** + * @brief Connect to the ssh server. + * + * @param[in] session The ssh session to connect. + * + * @returns SSH_OK on success, SSH_ERROR on error. + * + * @see ssh_new() + * @see ssh_disconnect() + */ +int ssh_connect(ssh_session session) { + int ret; + + if (session == NULL) { + ssh_set_error(session, SSH_FATAL, "Invalid session pointer"); + return SSH_ERROR; + } + + enter_function(); + + session->alive = 0; + session->client = 1; + + if (ssh_init() < 0) { + leave_function(); + return SSH_ERROR; + } + if (session->fd == SSH_INVALID_SOCKET && session->host == NULL && session->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; + } + + 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->fd != SSH_INVALID_SOCKET) { + ssh_socket_set_fd(session->socket, session->fd); + ret=SSH_OK; +#ifndef _WIN32 + } else if (session->ProxyCommand != NULL){ + ret=ssh_socket_connect_proxycommand(session->socket, session->ProxyCommand); +#endif + } else { + ret=ssh_socket_connect(session->socket, session->host, session->port, + session->bindaddr); + + /*, session->timeout * 1000 + session->timeout_usec); */ + } + 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"); + while(session->session_state != SSH_SESSION_STATE_ERROR && + session->session_state != SSH_SESSION_STATE_AUTHENTICATING && + session->session_state != SSH_SESSION_STATE_DISCONNECTED){ + /* loop until SSH_SESSION_STATE_BANNER_RECEIVED or + * SSH_SESSION_STATE_ERROR */ + ssh_handle_packets(session,-1); + ssh_log(session,SSH_LOG_PACKET,"ssh_connect: Actual state : %d",session->session_state); + } + leave_function(); + 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; + + 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); + } + session->alive = 0; + +error: + 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: */ |