From b3e158fce512af35e4f7e0d621faa7adc0a4f3ad Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 27 Feb 2026 10:54:41 +0100 Subject: [PATCH] copy: use scp(1) for scp:// and sftp:// transfers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit curl's SCP/SFTP backend (libssh2) does not support interactive password prompting — it fails with an authentication error unless credentials are supplied non-interactively. Replace the curl path for SSH URIs with native scp(1), which inherits the TTY and prompts the user for a password as expected. Both scp:// and sftp:// URIs are handled by parsing the URI into a [user@]host:path argument. Port is extracted if present and passed via -P. If no user is embedded in the URI, the -u flag value (remote_user) is used instead. Host-key verification is skipped with -o StrictHostKeyChecking=no, the same trade-off already made by the test framework. When the destination URI ends with '/', a meaningful remote filename is derived from the source: the datastore's on-disk filename (e.g., startup-config.cfg), the datastore short-name for in-memory stores, or the source file's own basename for plain file copies. Signed-off-by: Joachim Wiberg --- src/bin/copy.c | 204 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 190 insertions(+), 14 deletions(-) diff --git a/src/bin/copy.c b/src/bin/copy.c index 150af2ca..763a6aba 100644 --- a/src/bin/copy.c +++ b/src/bin/copy.c @@ -163,6 +163,76 @@ static bool is_uri(const char *str) return strstr(str, "://") != NULL; } +static bool is_ssh_uri(const char *uri) +{ + return !strncmp(uri, "scp://", 6) || !strncmp(uri, "sftp://", 7); +} + +/* + * Parse scp:// or sftp:// URI into an scp(1)-compatible remote string + * of the form [user@]host:path, and optionally extract the port. + * + * URI format: scheme://[user@]host[:port]/path + * + * Returns a heap-allocated string the caller must free, and sets *portp + * to a heap-allocated port string (or NULL) the caller must also free. + * Returns NULL on parse failure. + */ +static char *ssh_uri_to_remote(const char *uri, char **portp) +{ + const char *p, *slash, *path; + char auth[256], *auth_at, *auth_colon, *host_start; + char *user, *remote = NULL, *port = NULL; + size_t auth_len; + + *portp = NULL; + + if (!strncmp(uri, "scp://", 6)) + p = uri + 6; + else if (!strncmp(uri, "sftp://", 7)) + p = uri + 7; + else + return NULL; + + slash = strchr(p, '/'); + if (!slash) + slash = p + strlen(p); + + auth_len = (size_t)(slash - p); + if (auth_len >= sizeof(auth)) + return NULL; + memcpy(auth, p, auth_len); + auth[auth_len] = '\0'; + + auth_at = strchr(auth, '@'); + if (auth_at) { + *auth_at = '\0'; + user = auth; + host_start = auth_at + 1; + } else { + user = (char *)remote_user; /* may be NULL */ + host_start = auth; + } + + auth_colon = strchr(host_start, ':'); + if (auth_colon) { + *auth_colon = '\0'; + port = strdup(auth_colon + 1); + if (!port) + return NULL; + } + + path = *slash ? slash : "/"; + + if (user) + (void)asprintf(&remote, "%s@%s:%s", user, host_start, path); + else + (void)asprintf(&remote, "%s:%s", host_start, path); + + *portp = port; + return remote; +} + static bool is_stdout(const char *path) { if (!path) @@ -394,29 +464,37 @@ static int subprocess(char * const *argv) static int curl(char *op, const char *path, const char *uri) { - char *argv[] = { - "curl", "-L", op, NULL, NULL, NULL, NULL, NULL, - }; - int err = 1; + char *argv[10] = { "curl", "-L", NULL }; + int err = 1, i = 2; + int path_i, uri_i, user_i = 0; - argv[3] = strdup(path); - argv[4] = strdup(uri); - if (!(argv[3] && argv[4])) + argv[i++] = op; + + path_i = i; + argv[i] = strdup(path); + if (!argv[i++]) + goto out; + + uri_i = i; + argv[i] = strdup(uri); + if (!argv[i++]) goto out; if (remote_user) { - argv[5] = strdup("-u"); - argv[6] = strdup(remote_user); - if (!(argv[5] && argv[6])) + argv[i++] = "-u"; + user_i = i; + argv[i] = strdup(remote_user); + if (!argv[i++]) goto out; } + err = subprocess(argv); out: - free(argv[6]); - free(argv[5]); - free(argv[4]); - free(argv[3]); + free(argv[path_i]); + free(argv[uri_i]); + if (user_i) + free(argv[user_i]); return err; } @@ -445,6 +523,72 @@ static int curl_download(const char *uri, const char *dstpath) return 0; } +static int scp_upload(const char *srcpath, const char *uri) +{ + char *argv[12] = { "scp", "-o", "StrictHostKeyChecking=no", NULL }; + int i = 3, err = 1; + char *remote = NULL, *port = NULL, *src_dup = NULL; + + remote = ssh_uri_to_remote(uri, &port); + if (!remote) { + warnx("failed to parse URI: %s", uri); + goto out; + } + + if (port) { + argv[i++] = "-P"; + argv[i++] = port; + } + + src_dup = strdup(srcpath); + if (!src_dup) + goto out; + argv[i++] = src_dup; + argv[i++] = remote; + + err = subprocess(argv); + if (err) + warnx("upload to %s failed", uri); +out: + free(src_dup); + free(port); + free(remote); + return err; +} + +static int scp_download(const char *uri, const char *dstpath) +{ + char *argv[12] = { "scp", "-o", "StrictHostKeyChecking=no", NULL }; + int i = 3, err = 1; + char *remote = NULL, *port = NULL, *dst_dup = NULL; + + remote = ssh_uri_to_remote(uri, &port); + if (!remote) { + warnx("failed to parse URI: %s", uri); + goto out; + } + + if (port) { + argv[i++] = "-P"; + argv[i++] = port; + } + + argv[i++] = remote; + dst_dup = strdup(dstpath); + if (!dst_dup) + goto out; + argv[i++] = dst_dup; + + err = subprocess(argv); + if (err) + warnx("download of %s failed", uri); +out: + free(dst_dup); + free(port); + free(remote); + return err; +} + static int cat(const char *srcpath) { char *argv[] = { "cat", NULL, NULL }; @@ -492,6 +636,8 @@ static int put(const char *srcpath, const char *dst, err = sysrepo_import(ds, srcpath); else if (is_stdout(dst)) err = cat(srcpath); + else if (is_ssh_uri(dst)) + err = scp_upload(srcpath, dst); else if (is_uri(dst)) err = curl_upload(srcpath, dst); @@ -513,6 +659,8 @@ static int get(const char *src, const struct infix_ds *ds, const char *path) if (ds) err = sysrepo_export(ds, path); + else if (is_ssh_uri(src)) + err = scp_download(src, path); else if (is_uri(src)) err = curl_download(src, path); @@ -581,6 +729,7 @@ static int copy(const char *src, const char *dst) { const struct infix_ds *srcds = NULL, *dstds = NULL; char *srcpath = NULL, *dstpath = NULL; + char *dst_uri = NULL; bool rmsrc = false; mode_t oldmask; int err = 1; @@ -597,6 +746,32 @@ static int copy(const char *src, const char *dst) if (err) goto err; + /* + * When uploading to an SSH URI ending in '/', scp(1) would use the + * basename of the local (temp) file as the remote name. Append a + * meaningful name instead: the datastore's on-disk filename, or the + * source file's own basename. + */ + if (is_ssh_uri(dst) && dst[strlen(dst) - 1] == '/') { + const char *bn, *slash; + + if (srcds && srcds->path) { + slash = strrchr(srcds->path, '/'); + bn = slash ? slash + 1 : srcds->path; + } else if (srcds) { + bn = srcds->name; + } else { + slash = strrchr(srcpath, '/'); + bn = slash ? slash + 1 : srcpath; + } + + if (asprintf(&dst_uri, "%s%s", dst, bn) < 0) { + err = 1; + goto err; + } + dst = dst_uri; + } + err = resolve_dst(&dst, &dstds, &dstpath); if (err) goto err; @@ -614,6 +789,7 @@ err: if (rmsrc) rmtmp(srcpath); + free(dst_uri); if (dstpath) free(dstpath); free(srcpath);