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
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
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"
57 #include "const-string-vector.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 */;
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
88 log_callback (int priority
, const char *function
, const char *message
, void *vp
)
90 const char *levels
[] =
91 { "none", "warning", "protocol", "packet", "function" };
94 if (priority
>= 0 && priority
< ARRAY_SIZE (levels
))
95 level
= levels
[priority
];
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
);
108 free (identities
.ptr
);
112 /* Called for each key=value passed on the command line. */
114 ssh_config (const char *key
, const char *value
)
118 if (strcmp (key
, "host") == 0)
121 else if (strcmp (key
, "path") == 0)
124 else if (strcmp (key
, "port") == 0)
127 else if (strcmp (key
, "user") == 0)
130 else if (strcmp (key
, "password") == 0) {
132 if (nbdkit_read_password (value
, &password
) == -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");
150 else if (strcmp (key
, "verify-remote-host") == 0) {
151 r
= nbdkit_parse_bool (value
);
154 verify_remote_host
= r
;
157 else if (strcmp (key
, "timeout") == 0) {
158 if (nbdkit_parse_uint32_t ("timeout", value
, &timeout
) == -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");
170 else if (strcmp (key
, "compression") == 0) {
171 r
= nbdkit_parse_bool (value
);
176 else if (strcmp (key
, "create") == 0) {
177 r
= nbdkit_parse_bool (value
);
182 else if (strcmp (key
, "create-size") == 0) {
183 create_size
= nbdkit_parse_size (value
);
184 if (create_size
== -1)
187 else if (strcmp (key
, "create-mode") == 0) {
188 r
= nbdkit_parse_unsigned (key
, value
, &create_mode
);
191 /* OpenSSH checks this too. */
192 if (create_mode
> 0777) {
193 nbdkit_error ("create-mode must be <= 0777");
199 nbdkit_error ("unknown parameter '%s'", key
);
206 /* The host and path parameters are mandatory. */
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");
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");
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. */
254 /* Verify the remote host.
255 * See: http://api.libssh.org/master/libssh_tutor_guided_tour.html
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
);
264 case SSH_KNOWN_HOSTS_OK
:
268 case SSH_KNOWN_HOSTS_CHANGED
:
269 nbdkit_error ("host key for server changed");
272 case SSH_KNOWN_HOSTS_OTHER
:
273 nbdkit_error ("host key for server was not found "
274 "but another type of key exists");
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");
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");
292 case SSH_KNOWN_HOSTS_ERROR
:
293 nbdkit_error ("known hosts error: %s", ssh_get_error (h
->session
));
301 * See: http://api.libssh.org/master/libssh_tutor_authentication.html
304 authenticate_pubkey (ssh_session session
)
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
));
317 authenticate_password (ssh_session session
, const char *pass
)
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
));
329 authenticate (struct ssh_handle
*h
)
333 rc
= ssh_userauth_none (h
->session
, NULL
);
334 if (rc
== SSH_AUTH_SUCCESS
)
336 if (rc
== SSH_AUTH_ERROR
)
339 method
= ssh_userauth_list (h
->session
, NULL
);
340 nbdkit_debug ("authentication methods offered by the server [0x%x]: "
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" : "",
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");
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
;
376 open_or_create_path (ssh_session session
, sftp_session sftp
, int readonly
)
378 ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&create_lock
);
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
);
388 nbdkit_error ("cannot %s file for %s: %s",
389 create
? "create" : "open",
390 readonly
? "reading" : "writing",
391 ssh_get_error (session
));
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
,
405 .permissions
= create_mode
,
408 r
= sftp_setstat (sftp
, path
, &attrs
);
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
);
415 nbdkit_debug ("unlink failed: %s", ssh_get_error (session
));
421 /* On the next connection, don't create or truncate the file. */
427 /* Create the per-connection handle. */
429 ssh_open (int readonly
)
431 struct ssh_handle
*h
;
436 h
= calloc (1, sizeof *h
);
438 nbdkit_error ("calloc: %m");
442 /* Set up the SSH session. */
443 h
->session
= ssh_new ();
445 nbdkit_error ("failed to initialize libssh session");
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
);
465 nbdkit_error ("failed to set host in libssh session: %s: %s",
466 host
, ssh_get_error (h
->session
));
470 r
= ssh_options_set (h
->session
, SSH_OPTIONS_PORT_STR
, port
);
472 nbdkit_error ("failed to set port in libssh session: %s: %s",
473 port
, ssh_get_error (h
->session
));
478 r
= ssh_options_set (h
->session
, SSH_OPTIONS_USER
, user
);
480 nbdkit_error ("failed to set user in libssh session: %s: %s",
481 user
, ssh_get_error (h
->session
));
485 if (known_hosts
!= NULL
) {
486 r
= ssh_options_set (h
->session
, SSH_OPTIONS_KNOWNHOSTS
, known_hosts
);
488 nbdkit_error ("failed to set known_hosts in libssh session: %s: %s",
489 known_hosts
, ssh_get_error (h
->session
));
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
]);
501 nbdkit_error ("failed to add identity in libssh session: %s: %s",
502 identities
.ptr
[i
], ssh_get_error (h
->session
));
508 r
= ssh_options_set (h
->session
, SSH_OPTIONS_TIMEOUT
, &arg
);
510 nbdkit_error ("failed to set timeout in libssh session: %" PRIu32
": %s",
511 timeout
, ssh_get_error (h
->session
));
517 r
= ssh_options_set (h
->session
, SSH_OPTIONS_COMPRESSION
, "yes");
519 nbdkit_error ("failed to enable compression in libssh session: %s",
520 ssh_get_error (h
->session
));
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
533 r
= ssh_options_parse_config (h
->session
, NULL
);
535 nbdkit_error ("failed to parse local SSH configuration: %s",
536 ssh_get_error (h
->session
));
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
);
546 nbdkit_error ("failed to parse SSH configuration: %s: %s",
547 config
, ssh_get_error (h
->session
));
553 r
= ssh_connect (h
->session
);
555 nbdkit_error ("failed to connect to remote host: %s: %s",
556 host
, ssh_get_error (h
->session
));
560 /* Verify the remote host. */
561 if (verify_remote_host
&& do_verify_remote_host (h
) == -1)
565 if (authenticate (h
) == -1)
568 /* Open the SFTP connection. */
569 h
->sftp
= sftp_new (h
->session
);
571 nbdkit_error ("failed to allocate sftp session: %s",
572 ssh_get_error (h
->session
));
575 r
= sftp_init (h
->sftp
);
577 nbdkit_error ("failed to initialize sftp session: %s",
578 ssh_get_error (h
->session
));
582 /* Open or create the remote file. */
583 h
->file
= open_or_create_path (h
->session
, h
->sftp
, readonly
);
587 nbdkit_debug ("opened libssh handle");
593 sftp_close (h
->file
);
597 ssh_disconnect (h
->session
);
598 ssh_free (h
->session
);
604 /* Free up the per-connection handle. */
606 ssh_close (void *handle
)
608 struct ssh_handle
*h
= handle
;
611 r
= sftp_close (h
->file
);
613 nbdkit_error ("cannot close file: %s", ssh_get_error (h
->session
));
616 ssh_disconnect (h
->session
);
617 ssh_free (h
->session
);
621 /* Get the file size. */
623 ssh_get_size (void *handle
)
625 struct ssh_handle
*h
= handle
;
626 sftp_attributes attrs
;
629 attrs
= sftp_fstat (h
->file
);
631 sftp_attributes_free (attrs
);
636 /* Read data from the remote server. */
638 ssh_pread (void *handle
, void *buf
, uint32_t count
, uint64_t offset
)
640 struct ssh_handle
*h
= handle
;
644 r
= sftp_seek64 (h
->file
, offset
);
646 nbdkit_error ("seek64 failed: %s", ssh_get_error (h
->session
));
651 rs
= sftp_read (h
->file
, buf
, count
);
653 nbdkit_error ("read failed: %s (%zd)", ssh_get_error (h
->session
), rs
);
663 /* Write data to the remote server. */
665 ssh_pwrite (void *handle
, const void *buf
, uint32_t count
, uint64_t offset
)
667 struct ssh_handle
*h
= handle
;
671 r
= sftp_seek64 (h
->file
, offset
);
673 nbdkit_error ("seek64 failed: %s", ssh_get_error (h
->session
));
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
685 rs
= sftp_write (h
->file
, buf
, MIN (count
, 128*1024));
687 nbdkit_error ("write failed: %s (%zd)", ssh_get_error (h
->session
), rs
);
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");
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");
722 ssh_flush (void *handle
)
724 struct ssh_handle
*h
= handle
;
728 r
= sftp_fsync (h
->file
);
731 else if (r
!= SSH_OK
) {
732 nbdkit_error ("fsync failed: %s", ssh_get_error (h
->session
));
739 static struct nbdkit_plugin plugin
= {
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",
749 .get_size
= ssh_get_size
,
751 .pwrite
= ssh_pwrite
,
752 .can_flush
= ssh_can_flush
,
754 .can_multi_conn
= ssh_can_multi_conn
,
757 NBDKIT_REGISTER_PLUGIN(plugin
)