Code

Initial commit.
[libjunos.git] / src / access_ssh.c
1 /*
2  * libJUNOS - src/access_ssh.c
3  * Copyright (C) 2012 Sebastian 'tokkee' Harl <sh@tokkee.org>
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
17  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
19  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26  */
28 /*
29  * SSH access backend for the JUNOS object.
30  */
32 #include "junos.h"
34 #include <errno.h>
36 #include <libssh2.h>
38 #include <sys/socket.h>
39 #include <netdb.h>
41 #include <stdlib.h>
42 #include <unistd.h>
44 /*
45  * private data structures
46  */
48 struct junos_ssh_access {
49         junos_t *junos;
51         char *hostname;
52         char *username;
53         char *password;
55         struct addrinfo ai;
56         int sock_fd;
58         LIBSSH2_SESSION *session;
59         LIBSSH2_CHANNEL *channel;
61         char *banner;
62 };
64 /*
65  * private helper functions
66  */
68 static void
69 ssh_set_error(junos_ssh_access_t *ssh, int type, int error,
70                 char *msg_prefix, ...)
71 {
72         va_list ap;
74         if (! ssh)
75                 return;
77         va_start(ap, msg_prefix);
78         junos_set_verror(ssh->junos, type, error, msg_prefix, ap);
79         va_end(ap);
80 } /* ssh_set_error */
82 static int
83 ssh_select(junos_ssh_access_t *ssh)
84 {
85         int direction;
87         fd_set fd;
88         fd_set *read_fds  = NULL;
89         fd_set *write_fds = NULL;
91         struct timeval timeout;
93         timeout.tv_sec  = 10;
94         timeout.tv_usec = 0;
96         FD_ZERO(&fd);
97         FD_SET(ssh->sock_fd, &fd);
99         direction = libssh2_session_block_directions(ssh->session);
101         if (direction & LIBSSH2_SESSION_BLOCK_INBOUND)
102                 read_fds = &fd;
104         if (direction & LIBSSH2_SESSION_BLOCK_OUTBOUND)
105                 write_fds = &fd;
107         return select(ssh->sock_fd + 1, read_fds, write_fds,
108                         /* error_fds = */ NULL, &timeout);
109 } /* ssh_select */
111 static int
112 ssh_connect(junos_ssh_access_t *ssh)
114         int status;
116         struct addrinfo *ai;
117         struct addrinfo *ai_ptr;
118         struct addrinfo  ai_hints;
120         int have_error = 0;
122         memset(&ai_hints, 0, sizeof(ai_hints));
123         ai_hints.ai_flags    = 0;
124         ai_hints.ai_family   = AF_UNSPEC;
125         ai_hints.ai_socktype = SOCK_STREAM;
126         ai_hints.ai_protocol = IPPROTO_TCP;
128         status = getaddrinfo(ssh->hostname, /* servname = */ "22",
129                         &ai_hints, &ai);
130         if (status) {
131                 ssh_set_error(ssh, JUNOS_GAI_ERROR, status,
132                                 "Failed to resolve hostname '%s'", ssh->hostname);
133                 return -1;
134         }
136         for (ai_ptr = ai; ai_ptr != NULL; ai_ptr = ai_ptr->ai_next) {
137                 ssh->sock_fd = socket(ai_ptr->ai_family, ai_ptr->ai_socktype,
138                                 ai_ptr->ai_protocol);
139                 if (ssh->sock_fd < 0) {
140                         ssh_set_error(ssh, JUNOS_SYS_ERROR, errno,
141                                         "Failed to open socket");
142                         have_error = 1;
143                         continue;
144                 }
146                 status = connect(ssh->sock_fd, (struct sockaddr *)ai_ptr->ai_addr,
147                                 ai_ptr->ai_addrlen);
148                 if (status) {
149                         ssh_set_error(ssh, JUNOS_SYS_ERROR, errno,
150                                         "Failed to connect to host '%s'", ssh->hostname);
151                         shutdown(ssh->sock_fd, SHUT_RDWR);
152                         close(ssh->sock_fd);
153                         ssh->sock_fd = -1;
154                         have_error = 1;
155                         continue;
156                 }
158                 /* succeeded to connect */
159                 ssh->ai = *ai_ptr;
160                 break;
161         }
163         if (ai_ptr && have_error)
164                 junos_clear_error(ssh->junos);
165         else if (! ai_ptr) /* use error set above */
166                 return -1;
168         freeaddrinfo(ai);
169         return 0;
170 } /* ssh_connect */
172 static int
173 ssh_authenticate_password(junos_ssh_access_t *ssh)
175         int status;
177         while ((status = libssh2_userauth_password_ex(ssh->session,
178                                         ssh->username, (unsigned int)strlen(ssh->username),
179                                         ssh->password, (unsigned int)strlen(ssh->password),
180                                         /* passwd_change_cb = */ NULL)) == LIBSSH2_ERROR_EAGAIN)
181                 /* retry */;
183         if (status) {
184                 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, status,
185                                 "Password authentication failed for user '%s'",
186                                 ssh->username);
187                 return -1;
188         }
189         return 0;
190 } /* ssh_authenticate_password */
192 static int
193 ssh_authenticate_pubkey(junos_ssh_access_t *ssh)
195         int status;
197         char  ssh_pub_key[256];
198         char  ssh_priv_key[256];
200         char *home_dir;
202         home_dir = getenv("HOME");
203         if (! home_dir) {
204                 ssh_set_error(ssh, JUNOS_SYS_ERROR, errno,
205                                 "Failed to determine home directory");
206                 return -1;
207         }
209         snprintf(ssh_pub_key, sizeof(ssh_pub_key) - 1,
210                         "%s/.ssh/id_rsa.pub", home_dir);
211         ssh_pub_key[sizeof(ssh_pub_key) - 1] = '\0';
212         snprintf(ssh_priv_key, sizeof(ssh_priv_key) - 1,
213                         "%s/.ssh/id_rsa", home_dir);
214         ssh_priv_key[sizeof(ssh_priv_key) - 1] = '\0';
216         while ((status = libssh2_userauth_publickey_fromfile_ex(ssh->session,
217                                         ssh->username, (unsigned int)strlen(ssh->username),
218                                         ssh_pub_key, ssh_priv_key,
219                                         /* passphrase = */ "")) == LIBSSH2_ERROR_EAGAIN)
220                 /* retry */;
222         if (status) {
223                 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, status,
224                                 "Public key authentication failed for user '%s'",
225                                 ssh->username);
226                 return -1;
227         }
228         return 0;
229 } /* ssh_authenticate_pubkey */
231 static int
232 ssh_authenticate(junos_ssh_access_t *ssh)
234         if (ssh->password && strlen(ssh->password))
235                 return ssh_authenticate_password(ssh);
236         else
237                 return ssh_authenticate_pubkey(ssh);
238         return -1;
239 } /* ssh_authenticate */
241 static void
242 ssh_access_disconnect(junos_ssh_access_t *ssh, char *msg)
244         int status;
246         if (! ssh)
247                 return;
249         if (! msg)
250                 msg = "SSH session terminated";
252         if (ssh->channel) {
253                 while ((status = libssh2_channel_close(ssh->channel)) == LIBSSH2_ERROR_EAGAIN)
254                         ssh_select(ssh);
256                 libssh2_channel_free(ssh->channel);
257                 ssh->channel = NULL;
258         }
260         if (ssh->session) {
261                 libssh2_session_disconnect(ssh->session, msg);
262                 libssh2_session_free(ssh->session);
263                 ssh->session = NULL;
264         }
266         if (ssh->sock_fd >= 0) {
267                 shutdown(ssh->sock_fd, SHUT_RDWR);
268                 close(ssh->sock_fd);
269                 ssh->sock_fd = -1;
270         }
272         if (ssh->banner)
273                 free(ssh->banner);
274         ssh->banner = NULL;
275 } /* ssh_access_disconnect */
277 static void
278 ssh_access_free(junos_ssh_access_t *ssh, char *msg)
280         ssh_access_disconnect(ssh, msg);
282         if (ssh->hostname)
283                 free(ssh->hostname);
284         if (ssh->username)
285                 free(ssh->username);
286         if (ssh->password)
287                 free(ssh->password);
288         ssh->hostname = ssh->username = ssh->password = NULL;
290         free(ssh);
291 } /* ssh_access_free */
293 /*
294  * public API
295  */
297 junos_ssh_access_t *
298 junos_ssh_new(junos_t *junos)
300         junos_ssh_access_t *ssh;
302         if (! junos) {
303                 dprintf("SSH: Missing JUNOS object\n");
304                 return NULL;
305         }
307         ssh = calloc(1, sizeof(*ssh));
308         if (! ssh) {
309                 junos_set_error(junos, JUNOS_SYS_ERROR, errno,
310                                 "Failed to allocate a new JUNOS SSH object");
311                 return NULL;
312         }
314         ssh->junos = junos;
316         ssh->hostname = junos_get_hostname(junos);
317         if (ssh->hostname)
318                 ssh->hostname = strdup(ssh->hostname);
319         ssh->username = junos_get_username(junos);
320         if (ssh->username)
321                 ssh->username = strdup(ssh->username);
322         ssh->password = junos_get_password(junos);
323         if (ssh->password)
324                 ssh->password = strdup(ssh->password);
326         if ((! ssh->hostname) || (! ssh->username)) {
327                 ssh_set_error(ssh, JUNOS_SYS_ERROR, errno,
328                                 "Failed to duplicate hostname/username strings");
329                 ssh_access_free(ssh, "Internal error: strdup failed");
330                 return NULL;
331         }
333         ssh->sock_fd = -1;
335         ssh->session = NULL;
336         ssh->channel = NULL;
338         ssh->banner  = NULL;
340         return ssh;
341 } /* junos_ssh_new */
343 int
344 junos_ssh_connect(junos_ssh_access_t *ssh)
346         int status;
347         const char *ssh_banner;
349         if (! ssh)
350                 return -1;
352         if (ssh_connect(ssh)) {
353                 ssh_access_disconnect(ssh, "Failed to connect to host");
354                 return -1;
355         }
357         ssh->session = libssh2_session_init();
358         if (! ssh->session) {
359                 ssh_set_error(ssh, JUNOS_ACCESS_ERROR,
360                                 libssh2_session_last_error(ssh->session, NULL, NULL, 0),
361                                 "Failed to create libssh2 session object");
362                 ssh_access_disconnect(ssh, "Failed to create libssh2 session object");
363                 return -1;
364         }
366         /* do non-blocking I/O */
367         libssh2_session_set_blocking(ssh->session, 0);
369         while ((status = libssh2_session_handshake(ssh->session,
370                                         ssh->sock_fd)) == LIBSSH2_ERROR_EAGAIN)
371                 /* retry */;
373         if (status) {
374                 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, status,
375                                 "Failed to establish libssh2 session");
376                 ssh_access_disconnect(ssh, "Failed to establish libssh2 session");
377                 return -1;
378         }
380         /* XXX: verify host key */
382         if (ssh_authenticate(ssh)) {
383                 ssh_access_disconnect(ssh, "Authentication failed");
384                 return -1;
385         }
387         while ((! (ssh->channel = libssh2_channel_open_session(ssh->session)))
388                         && (libssh2_session_last_error(ssh->session,
389                                         NULL, NULL, 0) == LIBSSH2_ERROR_EAGAIN))
390                 ssh_select(ssh);
392         if (! ssh->channel) {
393                 ssh_set_error(ssh, JUNOS_ACCESS_ERROR,
394                                 libssh2_session_last_error(ssh->session, NULL, NULL, 0),
395                                 "Failed to open libssh2 session");
396                 ssh_access_disconnect(ssh, "Failed to open libssh2 session");
397                 return -1;
398         }
400         ssh_banner = libssh2_session_banner_get(ssh->session);
401         if (ssh_banner) {
402                 dprintf("SSH: Successfully connected to host '%s': %s\n",
403                                 ssh->hostname, ssh_banner);
404                 ssh->banner = strdup(ssh_banner);
405         }
407         while ((status = libssh2_channel_exec(ssh->channel,
408                                         "junoscript")) == LIBSSH2_ERROR_EAGAIN)
409                 ssh_select(ssh);
411         if (status) {
412                 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, status,
413                                 "Failed to start 'junoscript'");
414                 ssh_access_disconnect(ssh, "Failed to start 'junoscript'");
415                 return -1;
416         }
418         return 0;
419 } /* junos_ssh_connect */
421 int
422 junos_ssh_disconnect(junos_ssh_access_t *ssh)
424         ssh_access_disconnect(ssh, NULL);
425         return 0;
426 } /* junos_ssh_disconnect */
428 int
429 junos_ssh_free(junos_ssh_access_t *ssh)
431         ssh_access_disconnect(ssh, NULL);
432         ssh_access_free(ssh, NULL);
433         return 0;
434 } /* junos_ssh_free */
436 ssize_t
437 junos_ssh_recv(junos_ssh_access_t *ssh, char *buf, size_t buf_len)
439         ssize_t status;
440         ssize_t count = 0;
442         if ((! ssh) || (! buf)) {
443                 ssh_set_error(ssh, JUNOS_SYS_ERROR, EINVAL,
444                                 "junos_ssh_recv() requires the 'ssh' and 'buf' arguments");
445                 return -1;
446         }
448         if (buf_len <= 1) {
449                 ssh_set_error(ssh, JUNOS_SYS_ERROR, EINVAL,
450                                 "junos_ssh_recv() requires buffer >= 2 bytes");
451                 return -1;
452         }
454         while (42) {
455                 if ((size_t)count >= buf_len - 1)
456                         break;
458                 status = libssh2_channel_read(ssh->channel,
459                                 buf + count, buf_len - (size_t)count - 1);
461                 if (! status)
462                         break;
463                 else if (status == LIBSSH2_ERROR_EAGAIN) {
464                         if (! count) {
465                                 ssh_select(ssh);
466                                 continue;
467                         }
468                         break;
469                 }
470                 else if (status < 0) {
471                         ssh_set_error(ssh, JUNOS_ACCESS_ERROR, (int)status,
472                                         "Failed to read from remote host");
473                         if (! count)
474                                 count = -1;
475                         break;
476                 }
478                 count += status;
479         }
481         if (count >= 0)
482                 buf[count] = '\0';
484         return count;
485 } /* junos_ssh_recv */
487 ssize_t
488 junos_ssh_send(junos_ssh_access_t *ssh, char *buf, size_t buf_len)
490         ssize_t status;
491         ssize_t count = 0;
493         if ((! ssh) || (! buf)) {
494                 ssh_set_error(ssh, JUNOS_SYS_ERROR, EINVAL,
495                                 "junos_ssh_send() requires the 'ssh' and 'buf' arguments");
496                 return -1;
497         }
499         while (42) {
500                 if ((size_t)count >= buf_len)
501                         break;
503                 status = libssh2_channel_write(ssh->channel,
504                                 buf + count, buf_len - (size_t)count);
506                 if (status == LIBSSH2_ERROR_EAGAIN)
507                         continue;
508                 else if (status < 0) {
509                         ssh_set_error(ssh, JUNOS_ACCESS_ERROR, (int)status,
510                                         "Failed to write to remote host");
511                         if (! count)
512                                 count = -1;
513                         break;
514                 }
516                 count += status;
517         }
519         return count;
520 } /* junos_ssh_send */
522 int
523 junos_set_ssh_error(junos_error_t *err, junos_ssh_access_t *ssh,
524                 char *msg_prefix, ...)
526         va_list ap;
527         int status;
529         va_start(ap, msg_prefix);
530         status = junos_set_ssh_verror(err, ssh, msg_prefix, ap);
531         va_end(ap);
533         return status;
534 } /* junos_set_ssh_error */
536 int
537 junos_set_ssh_verror(junos_error_t *err, junos_ssh_access_t *ssh,
538                 char *msg_prefix, va_list ap)
540         char *err_msg = NULL;
542         char prefix[1024];
544         vsnprintf(prefix, sizeof(prefix), msg_prefix, ap);
545         prefix[sizeof(prefix) - 1] = '\0';
547         if (! ssh->session) {
548                 err->type  = JUNOS_ACCESS_ERROR;
549                 err->error = -1;
550                 snprintf(err->errmsg, sizeof(err->errmsg),
551                                 "%s: SSH session not initialized", prefix);
552                 return 0;
553         }
555         err->type  = JUNOS_ACCESS_ERROR;
556         err->error = libssh2_session_last_error(ssh->session, &err_msg,
557                         /* errmsg_len = */ NULL, /* want_buf = */ 0);
559         if (! err->error) {
560                 junos_clear_error(ssh->junos);
561                 return 0;
562         }
564         snprintf(err->errmsg, sizeof(err->errmsg), "%s: %s", prefix, err_msg);
565         return 0;
566 } /* ssh_set_verror */
568 /* error handling */
570 /* vim: set tw=78 sw=4 ts=4 noexpandtab : */