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)
113 {
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)
174 {
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)
194 {
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)
233 {
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)
243 {
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)
279 {
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)
299 {
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)
345 {
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)
423 {
424 ssh_access_disconnect(ssh, NULL);
425 return 0;
426 } /* junos_ssh_disconnect */
428 int
429 junos_ssh_free(junos_ssh_access_t *ssh)
430 {
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)
438 {
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)
489 {
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, ...)
525 {
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)
539 {
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 : */