ssh: Remove left over comment
[nbdkit.git] / plugins / ssh / ssh.c
blobaaa7c2b9fc18881b59233731513caa07192d7d02
1 /* nbdkit
2 * Copyright (C) 2014-2020 Red Hat Inc.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
11 * * 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.
15 * * Neither the name of Red Hat nor the names of its contributors may be
16 * used to endorse or promote products derived from this software without
17 * specific prior written permission.
19 * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
26 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
27 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
29 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
30 * SUCH DAMAGE.
33 #include <config.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <stdbool.h>
38 #include <stdarg.h>
39 #include <stdint.h>
40 #include <inttypes.h>
41 #include <limits.h>
42 #include <string.h>
43 #include <unistd.h>
44 #include <fcntl.h>
45 #include <sys/stat.h>
47 #include <pthread.h>
49 #include <libssh/libssh.h>
50 #include <libssh/sftp.h>
51 #include <libssh/callbacks.h>
53 #include <nbdkit-plugin.h>
55 #include "array-size.h"
56 #include "cleanup.h"
57 #include "const-string-vector.h"
58 #include "minmax.h"
60 static const char *host = NULL;
61 static const char *path = NULL;
62 static const char *port = NULL;
63 static const char *user = NULL;
64 static char *password = NULL;
65 static bool verify_remote_host = true;
66 static const char *known_hosts = NULL;
67 static const_string_vector identities = empty_vector;
68 static uint32_t timeout = 0;
69 static bool compression = false;
70 static bool create = false;
71 static int64_t create_size = -1;
72 static unsigned create_mode = S_IRUSR | S_IWUSR /* 0600 */;
74 /* config can be:
75 * NULL => parse options from default file
76 * "" => do NOT parse options
77 * some filename => parse options from filename
79 static const char *config = NULL;
81 /* Use '-D ssh.log=N' to set. */
82 NBDKIT_DLL_PUBLIC int ssh_debug_log = 0;
84 /* If ssh_debug_log > 0 then the library will call this function with
85 * log messages.
87 static void
88 log_callback (int priority, const char *function, const char *message, void *vp)
90 const char *levels[] =
91 { "none", "warning", "protocol", "packet", "function" };
92 const char *level;
94 if (priority >= 0 && priority < ARRAY_SIZE (levels))
95 level = levels[priority];
96 else
97 level = "unknown";
99 /* NB We don't need to print the function parameter because it is
100 * always prefixed to the message.
102 nbdkit_debug ("libssh: %s: %s", level, message);
105 static void
106 ssh_unload (void)
108 free (identities.ptr);
109 free (password);
112 /* Called for each key=value passed on the command line. */
113 static int
114 ssh_config (const char *key, const char *value)
116 int r;
118 if (strcmp (key, "host") == 0)
119 host = value;
121 else if (strcmp (key, "path") == 0)
122 path = value;
124 else if (strcmp (key, "port") == 0)
125 port = value;
127 else if (strcmp (key, "user") == 0)
128 user = value;
130 else if (strcmp (key, "password") == 0) {
131 free (password);
132 if (nbdkit_read_password (value, &password) == -1)
133 return -1;
136 else if (strcmp (key, "config") == 0)
137 config = value; /* %-expanded, cannot use nbdkit_absolute_path */
139 else if (strcmp (key, "known-hosts") == 0)
140 known_hosts = value; /* %-expanded, cannot use nbdkit_absolute_path */
142 else if (strcmp (key, "identity") == 0) {
143 /* %-expanded, cannot use nbdkit_absolute_path on value */
144 if (const_string_vector_append (&identities, value) == -1) {
145 nbdkit_error ("realloc: %m");
146 return -1;
150 else if (strcmp (key, "verify-remote-host") == 0) {
151 r = nbdkit_parse_bool (value);
152 if (r == -1)
153 return -1;
154 verify_remote_host = r;
157 else if (strcmp (key, "timeout") == 0) {
158 if (nbdkit_parse_uint32_t ("timeout", value, &timeout) == -1)
159 return -1;
160 #if LONG_MAX < UINT32_MAX
161 /* C17 5.2.4.2.1 requires that LONG_MAX is at least 2^31 - 1.
162 * However a large positive number might still exceed the limit.
164 if (timeout > LONG_MAX) {
165 nbdkit_error ("timeout is too large");
166 return -1;
168 #endif
170 else if (strcmp (key, "compression") == 0) {
171 r = nbdkit_parse_bool (value);
172 if (r == -1)
173 return -1;
174 compression = r;
176 else if (strcmp (key, "create") == 0) {
177 r = nbdkit_parse_bool (value);
178 if (r == -1)
179 return -1;
180 create = r;
182 else if (strcmp (key, "create-size") == 0) {
183 create_size = nbdkit_parse_size (value);
184 if (create_size == -1)
185 return -1;
187 else if (strcmp (key, "create-mode") == 0) {
188 r = nbdkit_parse_unsigned (key, value, &create_mode);
189 if (r == -1)
190 return -1;
191 /* OpenSSH checks this too. */
192 if (create_mode > 0777) {
193 nbdkit_error ("create-mode must be <= 0777");
194 return -1;
198 else {
199 nbdkit_error ("unknown parameter '%s'", key);
200 return -1;
203 return 0;
206 /* The host and path parameters are mandatory. */
207 static int
208 ssh_config_complete (void)
210 if (host == NULL || path == NULL) {
211 nbdkit_error ("you must supply the host and path parameters "
212 "after the plugin name on the command line");
213 return -1;
216 /* If create=true, create-size must be supplied. */
217 if (create && create_size == -1) {
218 nbdkit_error ("if using create=true, you must specify the size "
219 "of the new remote file using create-size=SIZE");
220 return -1;
223 return 0;
226 #define ssh_config_help \
227 "host=<HOST> (required) SSH server hostname.\n" \
228 "[path=]<PATH> (required) SSH remote path.\n" \
229 "port=<PORT> SSH protocol port number.\n" \
230 "user=<USER> SSH user name.\n" \
231 "password=<PASSWORD> SSH password.\n" \
232 "config=<CONFIG> Alternate local SSH configuration file.\n" \
233 "known-hosts=<FILENAME> Set location of known_hosts file.\n" \
234 "identity=<FILENAME> Prepend private key (identity) file.\n" \
235 "timeout=SECS Set SSH connection timeout.\n" \
236 "verify-remote-host=false Ignore known_hosts.\n" \
237 "compression=true Enable compression.\n" \
238 "create=true Create the remote file.\n" \
239 "create-mode=MODE Set the permissions of the remote file.\n" \
240 "create-size=SIZE Set the size of the remote file."
242 /* Since we must simulate atomic pread and pwrite using seek +
243 * read/write, calls on each handle must be serialized.
245 #define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_REQUESTS
247 /* The per-connection handle. */
248 struct ssh_handle {
249 ssh_session session;
250 sftp_session sftp;
251 sftp_file file;
254 /* Verify the remote host.
255 * See: http://api.libssh.org/master/libssh_tutor_guided_tour.html
257 static int
258 do_verify_remote_host (struct ssh_handle *h)
260 enum ssh_known_hosts_e state;
262 state = ssh_session_is_known_server (h->session);
263 switch (state) {
264 case SSH_KNOWN_HOSTS_OK:
265 /* OK */
266 break;
268 case SSH_KNOWN_HOSTS_CHANGED:
269 nbdkit_error ("host key for server changed");
270 return -1;
272 case SSH_KNOWN_HOSTS_OTHER:
273 nbdkit_error ("host key for server was not found "
274 "but another type of key exists");
275 return -1;
277 case SSH_KNOWN_HOSTS_NOT_FOUND:
278 /* This is not actually an error, but the user must ensure the
279 * host key is set up before using nbdkit so we error out here.
281 nbdkit_error ("could not find known_hosts file");
282 return -1;
284 case SSH_KNOWN_HOSTS_UNKNOWN:
285 /* See: https://gitlab.com/nbdkit/nbdkit/-/issues/10 */
286 nbdkit_error ("either the host key is unknown, you must use ssh "
287 "first and accept the host key; or the known_hosts "
288 "file (usually ~/.ssh/known_hosts) could not be opened, "
289 "check that the file exists and file permissions");
290 return -1;
292 case SSH_KNOWN_HOSTS_ERROR:
293 nbdkit_error ("known hosts error: %s", ssh_get_error (h->session));
294 return -1;
297 return 0;
300 /* Authenticate.
301 * See: http://api.libssh.org/master/libssh_tutor_authentication.html
303 static int
304 authenticate_pubkey (ssh_session session)
306 int rc;
308 rc = ssh_userauth_publickey_auto (session, NULL, NULL);
309 if (rc == SSH_AUTH_ERROR)
310 nbdkit_debug ("public key authentication failed: %s",
311 ssh_get_error (session));
313 return rc;
316 static int
317 authenticate_password (ssh_session session, const char *pass)
319 int rc;
321 rc = ssh_userauth_password (session, NULL, pass);
322 if (rc == SSH_AUTH_ERROR)
323 nbdkit_debug ("password authentication failed: %s",
324 ssh_get_error (session));
325 return rc;
328 static int
329 authenticate (struct ssh_handle *h)
331 int method, rc;
333 rc = ssh_userauth_none (h->session, NULL);
334 if (rc == SSH_AUTH_SUCCESS)
335 return 0;
336 if (rc == SSH_AUTH_ERROR)
337 return -1;
339 method = ssh_userauth_list (h->session, NULL);
340 nbdkit_debug ("authentication methods offered by the server [0x%x]: "
341 "%s%s%s%s%s%s%s",
342 method,
343 method & SSH_AUTH_METHOD_NONE ? " none" : "",
344 method & SSH_AUTH_METHOD_PASSWORD ? " password" : "",
345 method & SSH_AUTH_METHOD_PUBLICKEY ? " publickey" : "",
346 method & SSH_AUTH_METHOD_HOSTBASED ? " hostbased" : "",
347 method & SSH_AUTH_METHOD_INTERACTIVE
348 ? " keyboard-interactive" : "",
349 method & SSH_AUTH_METHOD_GSSAPI_MIC
350 ? " gssapi-with-mic" : "",
351 method & ~0x3f
352 ? " (and other unknown methods)" : "");
354 if (method & SSH_AUTH_METHOD_PUBLICKEY) {
355 rc = authenticate_pubkey (h->session);
356 if (rc == SSH_AUTH_SUCCESS) return 0;
359 if (password != NULL && (method & SSH_AUTH_METHOD_PASSWORD)) {
360 rc = authenticate_password (h->session, password);
361 if (rc == SSH_AUTH_SUCCESS) return 0;
364 nbdkit_error ("all possible authentication methods failed");
365 return -1;
368 /* This function opens or creates the remote file (depending on
369 * create=false|true). Parallel connections might call this function
370 * at the same time, and so we must hold a lock to ensure that the
371 * file is created at most once.
373 static pthread_mutex_t create_lock = PTHREAD_MUTEX_INITIALIZER;
375 static sftp_file
376 open_or_create_path (ssh_session session, sftp_session sftp, int readonly)
378 ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&create_lock);
379 int access_type;
380 int r;
381 sftp_file file;
383 access_type = readonly ? O_RDONLY : O_RDWR;
384 if (create) access_type |= O_CREAT | O_TRUNC;
386 file = sftp_open (sftp, path, access_type, S_IRWXU);
387 if (!file) {
388 nbdkit_error ("cannot %s file for %s: %s",
389 create ? "create" : "open",
390 readonly ? "reading" : "writing",
391 ssh_get_error (session));
392 return NULL;
395 if (create) {
396 /* There's no sftp_truncate call. However OpenSSH lets you call
397 * SSH_FXP_SETSTAT + SSH_FILEXFER_ATTR_SIZE which invokes
398 * truncate(2) on the server. Libssh doesn't provide a binding
399 * for SSH_FXP_FSETSTAT so we have to pass the session + path.
401 struct sftp_attributes_struct attrs = {
402 .flags = SSH_FILEXFER_ATTR_SIZE |
403 SSH_FILEXFER_ATTR_PERMISSIONS,
404 .size = create_size,
405 .permissions = create_mode,
408 r = sftp_setstat (sftp, path, &attrs);
409 if (r != SSH_OK) {
410 nbdkit_error ("setstat failed: %s", ssh_get_error (session));
412 /* Best-effort attempt to delete the remote file on failure. */
413 r = sftp_unlink (sftp, path);
414 if (r != SSH_OK)
415 nbdkit_debug ("unlink failed: %s", ssh_get_error (session));
417 return NULL;
421 /* On the next connection, don't create or truncate the file. */
422 create = false;
424 return file;
427 /* Create the per-connection handle. */
428 static void *
429 ssh_open (int readonly)
431 struct ssh_handle *h;
432 const int set = 1;
433 size_t i;
434 int r;
436 h = calloc (1, sizeof *h);
437 if (h == NULL) {
438 nbdkit_error ("calloc: %m");
439 return NULL;
442 /* Set up the SSH session. */
443 h->session = ssh_new ();
444 if (!h->session) {
445 nbdkit_error ("failed to initialize libssh session");
446 goto err;
449 if (ssh_debug_log > 0) {
450 ssh_options_set (h->session, SSH_OPTIONS_LOG_VERBOSITY, &ssh_debug_log);
451 /* Even though this is setting a "global", we must call it every
452 * time we set the session otherwise messages go to stderr.
454 ssh_set_log_callback (log_callback);
457 /* Disable Nagle's algorithm which is recommended by the libssh
458 * developers to improve performance of sftp. Ignore any error if
459 * we fail to set this.
461 ssh_options_set (h->session, SSH_OPTIONS_NODELAY, &set);
463 r = ssh_options_set (h->session, SSH_OPTIONS_HOST, host);
464 if (r != SSH_OK) {
465 nbdkit_error ("failed to set host in libssh session: %s: %s",
466 host, ssh_get_error (h->session));
467 goto err;
469 if (port != NULL) {
470 r = ssh_options_set (h->session, SSH_OPTIONS_PORT_STR, port);
471 if (r != SSH_OK) {
472 nbdkit_error ("failed to set port in libssh session: %s: %s",
473 port, ssh_get_error (h->session));
474 goto err;
477 if (user != NULL) {
478 r = ssh_options_set (h->session, SSH_OPTIONS_USER, user);
479 if (r != SSH_OK) {
480 nbdkit_error ("failed to set user in libssh session: %s: %s",
481 user, ssh_get_error (h->session));
482 goto err;
485 if (known_hosts != NULL) {
486 r = ssh_options_set (h->session, SSH_OPTIONS_KNOWNHOSTS, known_hosts);
487 if (r != SSH_OK) {
488 nbdkit_error ("failed to set known_hosts in libssh session: %s: %s",
489 known_hosts, ssh_get_error (h->session));
490 goto err;
492 /* XXX This is still going to read the global file, and there
493 * seems to be no way to disable that. However it doesn't matter
494 * as this file is rarely present.
497 for (i = 0; i < identities.len; ++i) {
498 r = ssh_options_set (h->session,
499 SSH_OPTIONS_ADD_IDENTITY, identities.ptr[i]);
500 if (r != SSH_OK) {
501 nbdkit_error ("failed to add identity in libssh session: %s: %s",
502 identities.ptr[i], ssh_get_error (h->session));
503 goto err;
506 if (timeout > 0) {
507 long arg = timeout;
508 r = ssh_options_set (h->session, SSH_OPTIONS_TIMEOUT, &arg);
509 if (r != SSH_OK) {
510 nbdkit_error ("failed to set timeout in libssh session: %" PRIu32 ": %s",
511 timeout, ssh_get_error (h->session));
512 goto err;
516 if (compression) {
517 r = ssh_options_set (h->session, SSH_OPTIONS_COMPRESSION, "yes");
518 if (r != SSH_OK) {
519 nbdkit_error ("failed to enable compression in libssh session: %s",
520 ssh_get_error (h->session));
521 goto err;
525 /* Read SSH config or alternative file. Must happen last so that
526 * the hostname has been set already.
528 if (config == NULL) {
529 /* NULL means parse the default files, which are ~/.ssh/config and
530 * /etc/ssh/ssh_config. If either are missing then they are
531 * ignored.
533 r = ssh_options_parse_config (h->session, NULL);
534 if (r != SSH_OK) {
535 nbdkit_error ("failed to parse local SSH configuration: %s",
536 ssh_get_error (h->session));
537 goto err;
540 else if (strcmp (config, "") != 0) {
541 /* User has specified a single file. This function ignores the
542 * case where the file is missing - should we check this? XXX
544 r = ssh_options_parse_config (h->session, config);
545 if (r != SSH_OK) {
546 nbdkit_error ("failed to parse SSH configuration: %s: %s",
547 config, ssh_get_error (h->session));
548 goto err;
552 /* Connect. */
553 r = ssh_connect (h->session);
554 if (r != SSH_OK) {
555 nbdkit_error ("failed to connect to remote host: %s: %s",
556 host, ssh_get_error (h->session));
557 goto err;
560 /* Verify the remote host. */
561 if (verify_remote_host && do_verify_remote_host (h) == -1)
562 goto err;
564 /* Authenticate. */
565 if (authenticate (h) == -1)
566 goto err;
568 /* Open the SFTP connection. */
569 h->sftp = sftp_new (h->session);
570 if (!h->sftp) {
571 nbdkit_error ("failed to allocate sftp session: %s",
572 ssh_get_error (h->session));
573 goto err;
575 r = sftp_init (h->sftp);
576 if (r != SSH_OK) {
577 nbdkit_error ("failed to initialize sftp session: %s",
578 ssh_get_error (h->session));
579 goto err;
582 /* Open or create the remote file. */
583 h->file = open_or_create_path (h->session, h->sftp, readonly);
584 if (!h->file)
585 goto err;
587 nbdkit_debug ("opened libssh handle");
589 return h;
591 err:
592 if (h->file)
593 sftp_close (h->file);
594 if (h->sftp)
595 sftp_free (h->sftp);
596 if (h->session) {
597 ssh_disconnect (h->session);
598 ssh_free (h->session);
600 free (h);
601 return NULL;
604 /* Free up the per-connection handle. */
605 static void
606 ssh_close (void *handle)
608 struct ssh_handle *h = handle;
609 int r;
611 r = sftp_close (h->file);
612 if (r != SSH_OK)
613 nbdkit_error ("cannot close file: %s", ssh_get_error (h->session));
615 sftp_free (h->sftp);
616 ssh_disconnect (h->session);
617 ssh_free (h->session);
618 free (h);
621 /* Get the file size. */
622 static int64_t
623 ssh_get_size (void *handle)
625 struct ssh_handle *h = handle;
626 sftp_attributes attrs;
627 int64_t r;
629 attrs = sftp_fstat (h->file);
630 r = attrs->size;
631 sftp_attributes_free (attrs);
633 return r;
636 /* Read data from the remote server. */
637 static int
638 ssh_pread (void *handle, void *buf, uint32_t count, uint64_t offset)
640 struct ssh_handle *h = handle;
641 int r;
642 ssize_t rs;
644 r = sftp_seek64 (h->file, offset);
645 if (r != SSH_OK) {
646 nbdkit_error ("seek64 failed: %s", ssh_get_error (h->session));
647 return -1;
650 while (count > 0) {
651 rs = sftp_read (h->file, buf, count);
652 if (rs < 0) {
653 nbdkit_error ("read failed: %s (%zd)", ssh_get_error (h->session), rs);
654 return -1;
656 buf += rs;
657 count -= rs;
660 return 0;
663 /* Write data to the remote server. */
664 static int
665 ssh_pwrite (void *handle, const void *buf, uint32_t count, uint64_t offset)
667 struct ssh_handle *h = handle;
668 int r;
669 ssize_t rs;
671 r = sftp_seek64 (h->file, offset);
672 if (r != SSH_OK) {
673 nbdkit_error ("seek64 failed: %s", ssh_get_error (h->session));
674 return -1;
677 while (count > 0) {
678 /* Openssh has a maximum packet size of 256K, so any write
679 * requests larger than this will fail in a peculiar way. (This
680 * limit doesn't seem to include the SFTP protocol overhead).
681 * Therefore if the count is larger than 128K, reduce the size of
682 * the request. I don't know whether 256K is a limit that applies
683 * to all servers.
685 rs = sftp_write (h->file, buf, MIN (count, 128*1024));
686 if (rs < 0) {
687 nbdkit_error ("write failed: %s (%zd)", ssh_get_error (h->session), rs);
688 return -1;
690 buf += rs;
691 count -= rs;
694 return 0;
697 static int
698 ssh_can_flush (void *handle)
700 struct ssh_handle *h = handle;
702 /* I added this extension to openssh 6.5 (April 2013). It may not
703 * be available in other SSH servers.
705 return sftp_extension_supported (h->sftp, "fsync@openssh.com", "1");
708 static int
709 ssh_can_multi_conn (void *handle)
711 struct ssh_handle *h = handle;
713 /* After examining the OpenSSH implementation of sftp-server we
714 * concluded that its write/flush behaviour is safe for advertising
715 * multi-conn. Other servers may not be safe. Use the
716 * fsync@openssh.com feature as a proxy.
718 return sftp_extension_supported (h->sftp, "fsync@openssh.com", "1");
721 static int
722 ssh_flush (void *handle)
724 struct ssh_handle *h = handle;
725 int r;
727 again:
728 r = sftp_fsync (h->file);
729 if (r == SSH_AGAIN)
730 goto again;
731 else if (r != SSH_OK) {
732 nbdkit_error ("fsync failed: %s", ssh_get_error (h->session));
733 return -1;
736 return 0;
739 static struct nbdkit_plugin plugin = {
740 .name = "ssh",
741 .version = PACKAGE_VERSION,
742 .unload = ssh_unload,
743 .config = ssh_config,
744 .config_complete = ssh_config_complete,
745 .config_help = ssh_config_help,
746 .magic_config_key = "path",
747 .open = ssh_open,
748 .close = ssh_close,
749 .get_size = ssh_get_size,
750 .pread = ssh_pread,
751 .pwrite = ssh_pwrite,
752 .can_flush = ssh_can_flush,
753 .flush = ssh_flush,
754 .can_multi_conn = ssh_can_multi_conn,
757 NBDKIT_REGISTER_PLUGIN(plugin)