1
0
mirror of https://git.libssh.org/projects/libssh.git synced 2025-05-19 07:13:42 +03:00
libssh/libssh/client.c
2010-04-04 15:51:54 +02:00

822 lines
21 KiB
C

/*
* 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, void *user){
ssh_session session=(ssh_session)user;
enter_function();
ssh_log(session,SSH_LOG_RARE,"Socket connection callback: %d (%d)",code, errno);
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));
}
ssh_connection_callback(session);
leave_function();
}
/**
* @internal
* @brief Callback to be called when the socket received an exception code.
* @param user is a pointer to session
*/
static void socket_callback_exception(int code, int errno, void *user){
ssh_session session=(ssh_session)user;
enter_function();
ssh_log(session,SSH_LOG_RARE,"Socket exception callback: %d (%d)",code, errno);
session->session_state=SSH_SESSION_STATE_ERROR;
ssh_set_error(session,SSH_FATAL,"Socket error: %s",strerror(errno));
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);
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};
enter_function();
banner = session->version == 1 ? CLIENTBANNER1 : CLIENTBANNER2;
if (session->xbanner) {
banner = session->xbanner;
}
if (server) {
session->serverbanner = strdup(banner);
if (session->serverbanner == NULL) {
leave_function();
return -1;
}
} else {
session->clientbanner = strdup(banner);
if (session->clientbanner == NULL) {
leave_function();
return -1;
}
}
snprintf(buffer, 128, "%s\r\n", banner);
if (ssh_socket_write(session->socket, buffer, strlen(buffer)) == SSH_ERROR) {
leave_function();
return -1;
}
if (ssh_socket_blocking_flush(session->socket) != SSH_OK) {
leave_function();
return -1;
}
#ifdef WITH_PCAP
if(session->pcap_ctx)
ssh_pcap_context_write(session->pcap_ctx,SSH_PCAP_DIR_OUT,buffer,strlen(buffer),strlen(buffer));
#endif
leave_function();
return 0;
}
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;
}
string_burn(f);
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;
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 ... */
string_burn(signature);
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;
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 pubkey = 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;
}
string_burn(e);
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){
string_burn(e);
string_free(e);
}
if(f != NULL){
string_burn(f);
string_free(f);
}
if(pubkey != NULL){
string_burn(pubkey);
string_free(pubkey);
}
if(signature != NULL){
string_burn(signature);
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 = string_from_char(service);
if (service_s == NULL) {
break;
}
if (buffer_add_ssh_string(session->out_buffer,service_s) < 0) {
string_free(service_s);
break;
}
string_free(service_s);
if (packet_send(session) != SSH_OK) {
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.
*/
void ssh_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.4);
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.5);
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.6);
session->connected = 1;
break;
}
#endif
break;
case SSH_SESSION_STATE_KEXINIT_RECEIVED:
set_status(session,0.6);
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.8);
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.0);
session->connected = 1;
session->session_state=SSH_SESSION_STATE_AUTHENTICATING;
}
break;
case SSH_SESSION_STATE_AUTHENTICATING:
break;
case SSH_SESSION_STATE_ERROR:
break;
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 == -1 && session->host == NULL) {
ssh_set_error(session, SSH_FATAL, "Hostname required");
leave_function();
return SSH_ERROR;
}
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=socket_callback_exception;
session->socket_callbacks.userdata=session;
if (session->fd != -1) {
ssh_socket_set_fd(session->socket, session->fd);
ret=SSH_OK;
} else {
ret=ssh_socket_connect(session->socket, session->host, session->port,
session->bindaddr);
//, session->timeout * 1000 + session->timeout_usec);
}
if (ret != SSH_OK) {
leave_function();
return SSH_ERROR;
}
set_status(session, 0.2);
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 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 = string_from_char("Bye Bye");
if (str == NULL) {
goto error;
}
if (buffer_add_ssh_string(session->out_buffer,str) < 0) {
string_free(str);
goto error;
}
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: */