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/select.h>
39 #include <sys/socket.h>
40 #include <netdb.h>
42 #include <stdlib.h>
43 #include <unistd.h>
45 /*
46 * private data structures
47 */
49 struct junos_ssh_access {
50 junos_t *junos;
52 char *hostname;
53 char *username;
54 char *password;
56 struct addrinfo ai;
57 int sock_fd;
59 LIBSSH2_SESSION *session;
60 LIBSSH2_CHANNEL *channel;
62 char *banner;
63 };
65 /*
66 * private helper functions
67 */
69 static void
70 ssh_set_error(junos_ssh_access_t *ssh, int type, int error,
71 char *msg_prefix, ...)
72 {
73 va_list ap;
75 if (! ssh)
76 return;
78 va_start(ap, msg_prefix);
79 junos_set_verror(ssh->junos, type, error, msg_prefix, ap);
80 va_end(ap);
81 } /* ssh_set_error */
83 static int
84 ssh_select(junos_ssh_access_t *ssh)
85 {
86 int direction;
88 fd_set fd;
89 fd_set *read_fds = NULL;
90 fd_set *write_fds = NULL;
92 struct timeval timeout;
94 timeout.tv_sec = 10;
95 timeout.tv_usec = 0;
97 FD_ZERO(&fd);
98 FD_SET(ssh->sock_fd, &fd);
100 direction = libssh2_session_block_directions(ssh->session);
102 if (direction & LIBSSH2_SESSION_BLOCK_INBOUND)
103 read_fds = &fd;
105 if (direction & LIBSSH2_SESSION_BLOCK_OUTBOUND)
106 write_fds = &fd;
108 return select(ssh->sock_fd + 1, read_fds, write_fds,
109 /* error_fds = */ NULL, &timeout);
110 } /* ssh_select */
112 static int
113 ssh_connect(junos_ssh_access_t *ssh)
114 {
115 int status;
117 struct addrinfo *ai;
118 struct addrinfo *ai_ptr;
119 struct addrinfo ai_hints;
121 int have_error = 0;
123 memset(&ai_hints, 0, sizeof(ai_hints));
124 ai_hints.ai_flags = 0;
125 ai_hints.ai_family = AF_UNSPEC;
126 ai_hints.ai_socktype = SOCK_STREAM;
127 ai_hints.ai_protocol = IPPROTO_TCP;
129 status = getaddrinfo(ssh->hostname, /* servname = */ "22",
130 &ai_hints, &ai);
131 if (status) {
132 ssh_set_error(ssh, JUNOS_GAI_ERROR, status,
133 "Failed to resolve hostname '%s'", ssh->hostname);
134 return -1;
135 }
137 for (ai_ptr = ai; ai_ptr != NULL; ai_ptr = ai_ptr->ai_next) {
138 ssh->sock_fd = socket(ai_ptr->ai_family, ai_ptr->ai_socktype,
139 ai_ptr->ai_protocol);
140 if (ssh->sock_fd < 0) {
141 ssh_set_error(ssh, JUNOS_SYS_ERROR, errno,
142 "Failed to open socket");
143 have_error = 1;
144 continue;
145 }
147 status = connect(ssh->sock_fd, (struct sockaddr *)ai_ptr->ai_addr,
148 ai_ptr->ai_addrlen);
149 if (status) {
150 ssh_set_error(ssh, JUNOS_SYS_ERROR, errno,
151 "Failed to connect to host '%s'", ssh->hostname);
152 shutdown(ssh->sock_fd, SHUT_RDWR);
153 close(ssh->sock_fd);
154 ssh->sock_fd = -1;
155 have_error = 1;
156 continue;
157 }
159 /* succeeded to connect */
160 ssh->ai = *ai_ptr;
161 break;
162 }
164 if (ai_ptr && have_error)
165 junos_clear_error(ssh->junos);
166 else if (! ai_ptr) /* use error set above */
167 return -1;
169 freeaddrinfo(ai);
170 return 0;
171 } /* ssh_connect */
173 static int
174 ssh_authenticate_password(junos_ssh_access_t *ssh)
175 {
176 int status;
178 while ((status = libssh2_userauth_password_ex(ssh->session,
179 ssh->username, (unsigned int)strlen(ssh->username),
180 ssh->password, (unsigned int)strlen(ssh->password),
181 /* passwd_change_cb = */ NULL)) == LIBSSH2_ERROR_EAGAIN)
182 /* retry */;
184 if (status) {
185 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, status,
186 "Password authentication failed for user '%s'",
187 ssh->username);
188 return -1;
189 }
190 return 0;
191 } /* ssh_authenticate_password */
193 static int
194 ssh_authenticate_pubkey(junos_ssh_access_t *ssh)
195 {
196 int status;
198 char ssh_pub_key[256];
199 char ssh_priv_key[256];
201 char *home_dir;
203 home_dir = getenv("HOME");
204 if (! home_dir) {
205 ssh_set_error(ssh, JUNOS_SYS_ERROR, errno,
206 "Failed to determine home directory");
207 return -1;
208 }
210 snprintf(ssh_pub_key, sizeof(ssh_pub_key) - 1,
211 "%s/.ssh/id_rsa.pub", home_dir);
212 ssh_pub_key[sizeof(ssh_pub_key) - 1] = '\0';
213 snprintf(ssh_priv_key, sizeof(ssh_priv_key) - 1,
214 "%s/.ssh/id_rsa", home_dir);
215 ssh_priv_key[sizeof(ssh_priv_key) - 1] = '\0';
217 while ((status = libssh2_userauth_publickey_fromfile_ex(ssh->session,
218 ssh->username, (unsigned int)strlen(ssh->username),
219 ssh_pub_key, ssh_priv_key,
220 /* passphrase = */ "")) == LIBSSH2_ERROR_EAGAIN)
221 /* retry */;
223 if (status) {
224 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, status,
225 "Public key authentication failed for user '%s'",
226 ssh->username);
227 return -1;
228 }
229 return 0;
230 } /* ssh_authenticate_pubkey */
232 static int
233 ssh_authenticate(junos_ssh_access_t *ssh)
234 {
235 if (ssh->password && strlen(ssh->password))
236 return ssh_authenticate_password(ssh);
237 else
238 return ssh_authenticate_pubkey(ssh);
239 return -1;
240 } /* ssh_authenticate */
242 static void
243 ssh_access_disconnect(junos_ssh_access_t *ssh, char *msg)
244 {
245 int status;
247 if (! ssh)
248 return;
250 if (! msg)
251 msg = "SSH session terminated";
253 if (ssh->channel) {
254 while ((status = libssh2_channel_close(ssh->channel)) == LIBSSH2_ERROR_EAGAIN)
255 ssh_select(ssh);
257 libssh2_channel_free(ssh->channel);
258 ssh->channel = NULL;
259 }
261 if (ssh->session) {
262 libssh2_session_disconnect(ssh->session, msg);
263 libssh2_session_free(ssh->session);
264 ssh->session = NULL;
265 }
267 if (ssh->sock_fd >= 0) {
268 shutdown(ssh->sock_fd, SHUT_RDWR);
269 close(ssh->sock_fd);
270 ssh->sock_fd = -1;
271 }
273 if (ssh->banner)
274 free(ssh->banner);
275 ssh->banner = NULL;
276 } /* ssh_access_disconnect */
278 static void
279 ssh_access_free(junos_ssh_access_t *ssh, char *msg)
280 {
281 ssh_access_disconnect(ssh, msg);
283 if (ssh->hostname)
284 free(ssh->hostname);
285 if (ssh->username)
286 free(ssh->username);
287 if (ssh->password)
288 free(ssh->password);
289 ssh->hostname = ssh->username = ssh->password = NULL;
291 free(ssh);
292 } /* ssh_access_free */
294 /*
295 * public API
296 */
298 junos_ssh_access_t *
299 junos_ssh_new(junos_t *junos)
300 {
301 junos_ssh_access_t *ssh;
303 if (! junos) {
304 dprintf("SSH: Missing JUNOS object\n");
305 return NULL;
306 }
308 ssh = calloc(1, sizeof(*ssh));
309 if (! ssh) {
310 junos_set_error(junos, JUNOS_SYS_ERROR, errno,
311 "Failed to allocate a new JUNOS SSH object");
312 return NULL;
313 }
315 ssh->junos = junos;
317 ssh->hostname = junos_get_hostname(junos);
318 if (ssh->hostname)
319 ssh->hostname = strdup(ssh->hostname);
320 ssh->username = junos_get_username(junos);
321 if (ssh->username)
322 ssh->username = strdup(ssh->username);
323 ssh->password = junos_get_password(junos);
324 if (ssh->password)
325 ssh->password = strdup(ssh->password);
327 if ((! ssh->hostname) || (! ssh->username)) {
328 ssh_set_error(ssh, JUNOS_SYS_ERROR, errno,
329 "Failed to duplicate hostname/username strings");
330 ssh_access_free(ssh, "Internal error: strdup failed");
331 return NULL;
332 }
334 ssh->sock_fd = -1;
336 ssh->session = NULL;
337 ssh->channel = NULL;
339 ssh->banner = NULL;
341 return ssh;
342 } /* junos_ssh_new */
344 int
345 junos_ssh_connect(junos_ssh_access_t *ssh)
346 {
347 int status;
348 const char *ssh_banner;
350 if (! ssh)
351 return -1;
353 if (ssh_connect(ssh)) {
354 ssh_access_disconnect(ssh, "Failed to connect to host");
355 return -1;
356 }
358 ssh->session = libssh2_session_init();
359 if (! ssh->session) {
360 ssh_set_error(ssh, JUNOS_ACCESS_ERROR,
361 libssh2_session_last_error(ssh->session, NULL, NULL, 0),
362 "Failed to create libssh2 session object");
363 ssh_access_disconnect(ssh, "Failed to create libssh2 session object");
364 return -1;
365 }
367 /* do non-blocking I/O */
368 libssh2_session_set_blocking(ssh->session, 0);
370 while ((status = libssh2_session_handshake(ssh->session,
371 ssh->sock_fd)) == LIBSSH2_ERROR_EAGAIN)
372 /* retry */;
374 if (status) {
375 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, status,
376 "Failed to establish libssh2 session");
377 ssh_access_disconnect(ssh, "Failed to establish libssh2 session");
378 return -1;
379 }
381 /* XXX: verify host key */
383 if (ssh_authenticate(ssh)) {
384 ssh_access_disconnect(ssh, "Authentication failed");
385 return -1;
386 }
388 while ((! (ssh->channel = libssh2_channel_open_session(ssh->session)))
389 && (libssh2_session_last_error(ssh->session,
390 NULL, NULL, 0) == LIBSSH2_ERROR_EAGAIN))
391 ssh_select(ssh);
393 if (! ssh->channel) {
394 ssh_set_error(ssh, JUNOS_ACCESS_ERROR,
395 libssh2_session_last_error(ssh->session, NULL, NULL, 0),
396 "Failed to open libssh2 session");
397 ssh_access_disconnect(ssh, "Failed to open libssh2 session");
398 return -1;
399 }
401 ssh_banner = libssh2_session_banner_get(ssh->session);
402 if (ssh_banner) {
403 dprintf("SSH: Successfully connected to host '%s': %s\n",
404 ssh->hostname, ssh_banner);
405 ssh->banner = strdup(ssh_banner);
406 }
408 while ((status = libssh2_channel_exec(ssh->channel,
409 "junoscript")) == LIBSSH2_ERROR_EAGAIN)
410 ssh_select(ssh);
412 if (status) {
413 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, status,
414 "Failed to start 'junoscript'");
415 ssh_access_disconnect(ssh, "Failed to start 'junoscript'");
416 return -1;
417 }
419 return 0;
420 } /* junos_ssh_connect */
422 int
423 junos_ssh_disconnect(junos_ssh_access_t *ssh)
424 {
425 ssh_access_disconnect(ssh, NULL);
426 return 0;
427 } /* junos_ssh_disconnect */
429 int
430 junos_ssh_free(junos_ssh_access_t *ssh)
431 {
432 ssh_access_disconnect(ssh, NULL);
433 ssh_access_free(ssh, NULL);
434 return 0;
435 } /* junos_ssh_free */
437 ssize_t
438 junos_ssh_recv(junos_ssh_access_t *ssh, char *buf, size_t buf_len)
439 {
440 ssize_t status;
441 ssize_t count = 0;
443 if ((! ssh) || (! buf)) {
444 ssh_set_error(ssh, JUNOS_SYS_ERROR, EINVAL,
445 "junos_ssh_recv() requires the 'ssh' and 'buf' arguments");
446 return -1;
447 }
449 if (buf_len <= 1) {
450 ssh_set_error(ssh, JUNOS_SYS_ERROR, EINVAL,
451 "junos_ssh_recv() requires buffer >= 2 bytes");
452 return -1;
453 }
455 while (42) {
456 if ((size_t)count >= buf_len - 1)
457 break;
459 status = libssh2_channel_read(ssh->channel,
460 buf + count, buf_len - (size_t)count - 1);
462 if (! status)
463 break;
464 else if (status == LIBSSH2_ERROR_EAGAIN) {
465 if (! count) {
466 ssh_select(ssh);
467 continue;
468 }
469 break;
470 }
471 else if (status < 0) {
472 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, (int)status,
473 "Failed to read from remote host");
474 if (! count)
475 count = -1;
476 break;
477 }
479 count += status;
480 }
482 if (count >= 0)
483 buf[count] = '\0';
485 return count;
486 } /* junos_ssh_recv */
488 ssize_t
489 junos_ssh_send(junos_ssh_access_t *ssh, char *buf, size_t buf_len)
490 {
491 ssize_t status;
492 ssize_t count = 0;
494 if ((! ssh) || (! buf)) {
495 ssh_set_error(ssh, JUNOS_SYS_ERROR, EINVAL,
496 "junos_ssh_send() requires the 'ssh' and 'buf' arguments");
497 return -1;
498 }
500 while (42) {
501 if ((size_t)count >= buf_len)
502 break;
504 status = libssh2_channel_write(ssh->channel,
505 buf + count, buf_len - (size_t)count);
507 if (status == LIBSSH2_ERROR_EAGAIN)
508 continue;
509 else if (status < 0) {
510 ssh_set_error(ssh, JUNOS_ACCESS_ERROR, (int)status,
511 "Failed to write to remote host");
512 if (! count)
513 count = -1;
514 break;
515 }
517 count += status;
518 }
520 return count;
521 } /* junos_ssh_send */
523 int
524 junos_set_ssh_error(junos_error_t *err, junos_ssh_access_t *ssh,
525 char *msg_prefix, ...)
526 {
527 va_list ap;
528 int status;
530 va_start(ap, msg_prefix);
531 status = junos_set_ssh_verror(err, ssh, msg_prefix, ap);
532 va_end(ap);
534 return status;
535 } /* junos_set_ssh_error */
537 int
538 junos_set_ssh_verror(junos_error_t *err, junos_ssh_access_t *ssh,
539 char *msg_prefix, va_list ap)
540 {
541 char *err_msg = NULL;
543 char prefix[1024];
545 vsnprintf(prefix, sizeof(prefix), msg_prefix, ap);
546 prefix[sizeof(prefix) - 1] = '\0';
548 if (! ssh->session) {
549 err->type = JUNOS_ACCESS_ERROR;
550 err->error = -1;
551 snprintf(err->errmsg, sizeof(err->errmsg),
552 "%s: SSH session not initialized", prefix);
553 return 0;
554 }
556 err->type = JUNOS_ACCESS_ERROR;
557 err->error = libssh2_session_last_error(ssh->session, &err_msg,
558 /* errmsg_len = */ NULL, /* want_buf = */ 0);
560 if (! err->error) {
561 junos_clear_error(ssh->junos);
562 return 0;
563 }
565 snprintf(err->errmsg, sizeof(err->errmsg), "%s: %s", prefix, err_msg);
566 return 0;
567 } /* ssh_set_verror */
569 /* error handling */
571 /* vim: set tw=78 sw=4 ts=4 noexpandtab : */