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 /* All compatible methods were tried and none worked. Come up with
365 * an actionable diagnostic message if we recognise the problem.
367 if (!(method
& SSH_AUTH_METHOD_PUBLICKEY
) && password
== NULL
) {
368 nbdkit_error ("the server does not offer public key authentication; "
369 "try using the password=... parameter");
372 if ((method
& SSH_AUTH_METHOD_PASSWORD
) && password
!= NULL
) {
373 nbdkit_error ("password authentication failed, "
374 "is the username and password correct?");
377 if (!(method
& SSH_AUTH_METHOD_PASSWORD
) && password
!= NULL
) {
378 nbdkit_error ("the server does not offer password authentication "
379 "but you tried to use a password; if you have root access "
380 "to the server, try editing 'sshd_config' and setting "
381 "'PasswordAuthentication yes'; otherwise try setting up "
382 "public key authentication");
386 nbdkit_error ("all possible authentication methods failed");
390 /* This function opens or creates the remote file (depending on
391 * create=false|true). Parallel connections might call this function
392 * at the same time, and so we must hold a lock to ensure that the
393 * file is created at most once.
395 static pthread_mutex_t create_lock
= PTHREAD_MUTEX_INITIALIZER
;
398 open_or_create_path (ssh_session session
, sftp_session sftp
, int readonly
)
400 ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&create_lock
);
405 access_type
= readonly
? O_RDONLY
: O_RDWR
;
406 if (create
) access_type
|= O_CREAT
| O_TRUNC
;
408 file
= sftp_open (sftp
, path
, access_type
, S_IRWXU
);
410 nbdkit_error ("cannot %s file for %s: %s",
411 create
? "create" : "open",
412 readonly
? "reading" : "writing",
413 ssh_get_error (session
));
418 /* There's no sftp_truncate call. However OpenSSH lets you call
419 * SSH_FXP_SETSTAT + SSH_FILEXFER_ATTR_SIZE which invokes
420 * truncate(2) on the server. Libssh doesn't provide a binding
421 * for SSH_FXP_FSETSTAT so we have to pass the session + path.
423 struct sftp_attributes_struct attrs
= {
424 .flags
= SSH_FILEXFER_ATTR_SIZE
|
425 SSH_FILEXFER_ATTR_PERMISSIONS
,
427 .permissions
= create_mode
,
430 r
= sftp_setstat (sftp
, path
, &attrs
);
432 nbdkit_error ("setstat failed: %s", ssh_get_error (session
));
434 /* Best-effort attempt to delete the remote file on failure. */
435 r
= sftp_unlink (sftp
, path
);
437 nbdkit_debug ("unlink failed: %s", ssh_get_error (session
));
443 /* On the next connection, don't create or truncate the file. */
449 /* Create the per-connection handle. */
451 ssh_open (int readonly
)
453 struct ssh_handle
*h
;
458 h
= calloc (1, sizeof *h
);
460 nbdkit_error ("calloc: %m");
464 /* Set up the SSH session. */
465 h
->session
= ssh_new ();
467 nbdkit_error ("failed to initialize libssh session");
471 if (ssh_debug_log
> 0) {
472 ssh_options_set (h
->session
, SSH_OPTIONS_LOG_VERBOSITY
, &ssh_debug_log
);
473 /* Even though this is setting a "global", we must call it every
474 * time we set the session otherwise messages go to stderr.
476 ssh_set_log_callback (log_callback
);
479 /* Disable Nagle's algorithm which is recommended by the libssh
480 * developers to improve performance of sftp. Ignore any error if
481 * we fail to set this.
483 ssh_options_set (h
->session
, SSH_OPTIONS_NODELAY
, &set
);
485 r
= ssh_options_set (h
->session
, SSH_OPTIONS_HOST
, host
);
487 nbdkit_error ("failed to set host in libssh session: %s: %s",
488 host
, ssh_get_error (h
->session
));
492 r
= ssh_options_set (h
->session
, SSH_OPTIONS_PORT_STR
, port
);
494 nbdkit_error ("failed to set port in libssh session: %s: %s",
495 port
, ssh_get_error (h
->session
));
500 r
= ssh_options_set (h
->session
, SSH_OPTIONS_USER
, user
);
502 nbdkit_error ("failed to set user in libssh session: %s: %s",
503 user
, ssh_get_error (h
->session
));
507 if (known_hosts
!= NULL
) {
508 r
= ssh_options_set (h
->session
, SSH_OPTIONS_KNOWNHOSTS
, known_hosts
);
510 nbdkit_error ("failed to set known_hosts in libssh session: %s: %s",
511 known_hosts
, ssh_get_error (h
->session
));
514 /* XXX This is still going to read the global file, and there
515 * seems to be no way to disable that. However it doesn't matter
516 * as this file is rarely present.
519 for (i
= 0; i
< identities
.len
; ++i
) {
520 r
= ssh_options_set (h
->session
,
521 SSH_OPTIONS_ADD_IDENTITY
, identities
.ptr
[i
]);
523 nbdkit_error ("failed to add identity in libssh session: %s: %s",
524 identities
.ptr
[i
], ssh_get_error (h
->session
));
530 r
= ssh_options_set (h
->session
, SSH_OPTIONS_TIMEOUT
, &arg
);
532 nbdkit_error ("failed to set timeout in libssh session: %" PRIu32
": %s",
533 timeout
, ssh_get_error (h
->session
));
539 r
= ssh_options_set (h
->session
, SSH_OPTIONS_COMPRESSION
, "yes");
541 nbdkit_error ("failed to enable compression in libssh session: %s",
542 ssh_get_error (h
->session
));
547 /* Read SSH config or alternative file. Must happen last so that
548 * the hostname has been set already.
550 if (config
== NULL
) {
551 /* NULL means parse the default files, which are ~/.ssh/config and
552 * /etc/ssh/ssh_config. If either are missing then they are
555 r
= ssh_options_parse_config (h
->session
, NULL
);
557 nbdkit_error ("failed to parse local SSH configuration: %s",
558 ssh_get_error (h
->session
));
562 else if (strcmp (config
, "") != 0) {
563 /* User has specified a single file. This function ignores the
564 * case where the file is missing - should we check this? XXX
566 r
= ssh_options_parse_config (h
->session
, config
);
568 nbdkit_error ("failed to parse SSH configuration: %s: %s",
569 config
, ssh_get_error (h
->session
));
575 r
= ssh_connect (h
->session
);
577 nbdkit_error ("failed to connect to remote host: %s: %s",
578 host
, ssh_get_error (h
->session
));
582 /* Verify the remote host. */
583 if (verify_remote_host
&& do_verify_remote_host (h
) == -1)
587 if (authenticate (h
) == -1)
590 /* Open the SFTP connection. */
591 h
->sftp
= sftp_new (h
->session
);
593 nbdkit_error ("failed to allocate sftp session: %s",
594 ssh_get_error (h
->session
));
597 r
= sftp_init (h
->sftp
);
599 nbdkit_error ("failed to initialize sftp session: %s",
600 ssh_get_error (h
->session
));
604 /* Open or create the remote file. */
605 h
->file
= open_or_create_path (h
->session
, h
->sftp
, readonly
);
609 nbdkit_debug ("opened libssh handle");
615 sftp_close (h
->file
);
619 ssh_disconnect (h
->session
);
620 ssh_free (h
->session
);
626 /* Free up the per-connection handle. */
628 ssh_close (void *handle
)
630 struct ssh_handle
*h
= handle
;
633 r
= sftp_close (h
->file
);
635 nbdkit_error ("cannot close file: %s", ssh_get_error (h
->session
));
638 ssh_disconnect (h
->session
);
639 ssh_free (h
->session
);
643 /* Get the file size. */
645 ssh_get_size (void *handle
)
647 struct ssh_handle
*h
= handle
;
648 sftp_attributes attrs
;
651 attrs
= sftp_fstat (h
->file
);
653 sftp_attributes_free (attrs
);
658 /* Read data from the remote server. */
660 ssh_pread (void *handle
, void *buf
, uint32_t count
, uint64_t offset
)
662 struct ssh_handle
*h
= handle
;
666 r
= sftp_seek64 (h
->file
, offset
);
668 nbdkit_error ("seek64 failed: %s", ssh_get_error (h
->session
));
673 rs
= sftp_read (h
->file
, buf
, count
);
675 nbdkit_error ("read failed: %s (%zd)", ssh_get_error (h
->session
), rs
);
685 /* Write data to the remote server. */
687 ssh_pwrite (void *handle
, const void *buf
, uint32_t count
, uint64_t offset
)
689 struct ssh_handle
*h
= handle
;
693 r
= sftp_seek64 (h
->file
, offset
);
695 nbdkit_error ("seek64 failed: %s", ssh_get_error (h
->session
));
700 /* Openssh has a maximum packet size of 256K, so any write
701 * requests larger than this will fail in a peculiar way. (This
702 * limit doesn't seem to include the SFTP protocol overhead).
703 * Therefore if the count is larger than 128K, reduce the size of
704 * the request. I don't know whether 256K is a limit that applies
707 rs
= sftp_write (h
->file
, buf
, MIN (count
, 128*1024));
709 nbdkit_error ("write failed: %s (%zd)", ssh_get_error (h
->session
), rs
);
720 ssh_can_flush (void *handle
)
722 struct ssh_handle
*h
= handle
;
724 /* I added this extension to openssh 6.5 (April 2013). It may not
725 * be available in other SSH servers.
727 return sftp_extension_supported (h
->sftp
, "fsync@openssh.com", "1");
731 ssh_can_multi_conn (void *handle
)
733 struct ssh_handle
*h
= handle
;
735 /* After examining the OpenSSH implementation of sftp-server we
736 * concluded that its write/flush behaviour is safe for advertising
737 * multi-conn. Other servers may not be safe. Use the
738 * fsync@openssh.com feature as a proxy.
740 return sftp_extension_supported (h
->sftp
, "fsync@openssh.com", "1");
744 ssh_flush (void *handle
)
746 struct ssh_handle
*h
= handle
;
750 r
= sftp_fsync (h
->file
);
753 else if (r
!= SSH_OK
) {
754 nbdkit_error ("fsync failed: %s", ssh_get_error (h
->session
));
761 static struct nbdkit_plugin plugin
= {
763 .version
= PACKAGE_VERSION
,
764 .unload
= ssh_unload
,
765 .config
= ssh_config
,
766 .config_complete
= ssh_config_complete
,
767 .config_help
= ssh_config_help
,
768 .magic_config_key
= "path",
771 .get_size
= ssh_get_size
,
773 .pwrite
= ssh_pwrite
,
774 .can_flush
= ssh_can_flush
,
776 .can_multi_conn
= ssh_can_multi_conn
,
779 NBDKIT_REGISTER_PLUGIN (plugin
)