add a sftp client "cp" command that supports server-side copying
authordjm <djm@openbsd.org>
Thu, 31 Mar 2022 03:07:03 +0000 (03:07 +0000)
committerdjm <djm@openbsd.org>
Thu, 31 Mar 2022 03:07:03 +0000 (03:07 +0000)
of files. Useful for this task and for testing the copy-data
extension. Patch from Mike Frysinger; ok dtucker@

usr.bin/ssh/sftp-client.c
usr.bin/ssh/sftp-client.h
usr.bin/ssh/sftp.1
usr.bin/ssh/sftp.c

index 0666a9f..403a2a8 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp-client.c,v 1.161 2022/01/17 21:41:04 djm Exp $ */
+/* $OpenBSD: sftp-client.c,v 1.162 2022/03/31 03:07:03 djm Exp $ */
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
  *
@@ -83,6 +83,7 @@ struct sftp_conn {
 #define SFTP_EXT_LSETSTAT      0x00000020
 #define SFTP_EXT_LIMITS                0x00000040
 #define SFTP_EXT_PATH_EXPAND   0x00000080
+#define SFTP_EXT_COPY_DATA     0x00000100
        u_int exts;
        u_int64_t limit_kbps;
        struct bwlimit bwlimit_in, bwlimit_out;
@@ -514,6 +515,10 @@ do_init(int fd_in, int fd_out, u_int transfer_buflen, u_int num_requests,
                    strcmp((char *)value, "1") == 0) {
                        ret->exts |= SFTP_EXT_PATH_EXPAND;
                        known = 1;
+               } else if (strcmp(name, "copy-data") == 0 &&
+                   strcmp((char *)value, "1") == 0) {
+                       ret->exts |= SFTP_EXT_COPY_DATA;
+                       known = 1;
                }
                if (known) {
                        debug2("Server supports extension \"%s\" revision %s",
@@ -1058,6 +1063,121 @@ do_expand_path(struct sftp_conn *conn, const char *path)
        return do_realpath_expand(conn, path, 1);
 }
 
+int
+do_copy(struct sftp_conn *conn, const char *oldpath, const char *newpath)
+{
+       Attrib junk, *a;
+       struct sshbuf *msg;
+       u_char *old_handle, *new_handle;
+       u_int mode, status, id;
+       size_t old_handle_len, new_handle_len;
+       int r;
+
+       /* Return if the extension is not supported */
+       if ((conn->exts & SFTP_EXT_COPY_DATA) == 0) {
+               error("Server does not support copy-data extension");
+               return -1;
+       }
+
+       /* Make sure the file exists, and we can copy its perms */
+       if ((a = do_stat(conn, oldpath, 0)) == NULL)
+               return -1;
+
+       /* Do not preserve set[ug]id here, as we do not preserve ownership */
+       if (a->flags & SSH2_FILEXFER_ATTR_PERMISSIONS) {
+               mode = a->perm & 0777;
+
+               if (!S_ISREG(a->perm)) {
+                       error("Cannot copy non-regular file: %s", oldpath);
+                       return -1;
+               }
+       } else {
+               /* NB: The user's umask will apply to this */
+               mode = 0666;
+       }
+
+       /* Set up the new perms for the new file */
+       attrib_clear(a);
+       a->perm = mode;
+       a->flags |= SSH2_FILEXFER_ATTR_PERMISSIONS;
+
+       if ((msg = sshbuf_new()) == NULL)
+               fatal("%s: sshbuf_new failed", __func__);
+
+       attrib_clear(&junk); /* Send empty attributes */
+
+       /* Open the old file for reading */
+       id = conn->msg_id++;
+       if ((r = sshbuf_put_u8(msg, SSH2_FXP_OPEN)) != 0 ||
+           (r = sshbuf_put_u32(msg, id)) != 0 ||
+           (r = sshbuf_put_cstring(msg, oldpath)) != 0 ||
+           (r = sshbuf_put_u32(msg, SSH2_FXF_READ)) != 0 ||
+           (r = encode_attrib(msg, &junk)) != 0)
+               fatal("%s: buffer error: %s", __func__, ssh_err(r));
+       send_msg(conn, msg);
+       debug3("Sent message SSH2_FXP_OPEN I:%u P:%s", id, oldpath);
+
+       sshbuf_reset(msg);
+
+       old_handle = get_handle(conn, id, &old_handle_len,
+           "remote open(\"%s\")", oldpath);
+       if (old_handle == NULL) {
+               sshbuf_free(msg);
+               return -1;
+       }
+
+       /* Open the new file for writing */
+       id = conn->msg_id++;
+       if ((r = sshbuf_put_u8(msg, SSH2_FXP_OPEN)) != 0 ||
+           (r = sshbuf_put_u32(msg, id)) != 0 ||
+           (r = sshbuf_put_cstring(msg, newpath)) != 0 ||
+           (r = sshbuf_put_u32(msg, SSH2_FXF_WRITE|SSH2_FXF_CREAT|
+           SSH2_FXF_TRUNC)) != 0 ||
+           (r = encode_attrib(msg, a)) != 0)
+               fatal("%s: buffer error: %s", __func__, ssh_err(r));
+       send_msg(conn, msg);
+       debug3("Sent message SSH2_FXP_OPEN I:%u P:%s", id, newpath);
+
+       sshbuf_reset(msg);
+
+       new_handle = get_handle(conn, id, &new_handle_len,
+           "remote open(\"%s\")", newpath);
+       if (new_handle == NULL) {
+               sshbuf_free(msg);
+               free(old_handle);
+               return -1;
+       }
+
+       /* Copy the file data */
+       id = conn->msg_id++;
+       if ((r = sshbuf_put_u8(msg, SSH2_FXP_EXTENDED)) != 0 ||
+           (r = sshbuf_put_u32(msg, id)) != 0 ||
+           (r = sshbuf_put_cstring(msg, "copy-data")) != 0 ||
+           (r = sshbuf_put_string(msg, old_handle, old_handle_len)) != 0 ||
+           (r = sshbuf_put_u64(msg, 0)) != 0 ||
+           (r = sshbuf_put_u64(msg, 0)) != 0 ||
+           (r = sshbuf_put_string(msg, new_handle, new_handle_len)) != 0 ||
+           (r = sshbuf_put_u64(msg, 0)) != 0)
+               fatal("%s: buffer error: %s", __func__, ssh_err(r));
+       send_msg(conn, msg);
+       debug3("Sent message copy-data \"%s\" 0 0 -> \"%s\" 0",
+              oldpath, newpath);
+
+       status = get_status(conn, id);
+       if (status != SSH2_FX_OK)
+               error("Couldn't copy file \"%s\" to \"%s\": %s", oldpath,
+                   newpath, fx2txt(status));
+
+       /* Clean up everything */
+       sshbuf_free(msg);
+       do_close(conn, old_handle, old_handle_len);
+       do_close(conn, new_handle, new_handle_len);
+       free(old_handle);
+       free(new_handle);
+
+       return status == SSH2_FX_OK ? 0 : -1;
+}
+
 int
 do_rename(struct sftp_conn *conn, const char *oldpath, const char *newpath,
     int force_legacy)
index 0d5356c..6c62821 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp-client.h,v 1.35 2022/01/01 01:55:30 jsg Exp $ */
+/* $OpenBSD: sftp-client.h,v 1.36 2022/03/31 03:07:03 djm Exp $ */
 
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
@@ -119,6 +119,9 @@ int do_statvfs(struct sftp_conn *, const char *, struct sftp_statvfs *, int);
 /* Rename 'oldpath' to 'newpath' */
 int do_rename(struct sftp_conn *, const char *, const char *, int);
 
+/* Copy 'oldpath' to 'newpath' */
+int do_copy(struct sftp_conn *, const char *, const char *);
+
 /* Link 'oldpath' to 'newpath' */
 int do_hardlink(struct sftp_conn *, const char *, const char *);
 
index 7eebeea..766adce 100644 (file)
@@ -1,4 +1,4 @@
-.\" $OpenBSD: sftp.1,v 1.138 2021/07/02 05:11:21 dtucker Exp $
+.\" $OpenBSD: sftp.1,v 1.139 2022/03/31 03:07:03 djm Exp $
 .\"
 .\" Copyright (c) 2001 Damien Miller.  All rights reserved.
 .\"
@@ -22,7 +22,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd $Mdocdate: July 2 2021 $
+.Dd $Mdocdate: March 31 2022 $
 .Dt SFTP 1
 .Os
 .Sh NAME
@@ -144,7 +144,7 @@ will abort if any of the following
 commands fail:
 .Ic get , put , reget , reput , rename , ln ,
 .Ic rm , mkdir , chdir , ls ,
-.Ic lchdir , chmod , chown ,
+.Ic lchdir , copy , cp , chmod , chown ,
 .Ic chgrp , lpwd , df , symlink ,
 and
 .Ic lmkdir .
@@ -400,6 +400,18 @@ If the
 flag is specified, then symlinks will not be followed.
 Note that this is only supported by servers that implement
 the "lsetstat@openssh.com" extension.
+.It Ic copy Ar oldpath Ar newpath
+Copy remote file from
+.Ar oldpath
+to
+.Ar newpath .
+.Pp
+Note that this is only supported by servers that implement the "copy-data"
+extension.
+.It Ic cp Ar oldpath Ar newpath
+Alias to
+.Ic copy
+command.
 .It Xo Ic df
 .Op Fl hi
 .Op Ar path
index bc4ba5e..ad66df9 100644 (file)
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp.c,v 1.213 2022/03/18 02:50:21 djm Exp $ */
+/* $OpenBSD: sftp.c,v 1.214 2022/03/31 03:07:03 djm Exp $ */
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
  *
@@ -116,6 +116,7 @@ enum sftp_command {
        I_CHGRP,
        I_CHMOD,
        I_CHOWN,
+       I_COPY,
        I_DF,
        I_GET,
        I_HELP,
@@ -159,6 +160,8 @@ static const struct CMD cmds[] = {
        { "chgrp",      I_CHGRP,        REMOTE  },
        { "chmod",      I_CHMOD,        REMOTE  },
        { "chown",      I_CHOWN,        REMOTE  },
+       { "copy",       I_COPY,         REMOTE  },
+       { "cp",         I_COPY,         REMOTE  },
        { "df",         I_DF,           REMOTE  },
        { "dir",        I_LS,           REMOTE  },
        { "exit",       I_QUIT,         NOARGS  },
@@ -265,6 +268,8 @@ help(void)
            "chgrp [-h] grp path                Change group of file 'path' to 'grp'\n"
            "chmod [-h] mode path               Change permissions of file 'path' to 'mode'\n"
            "chown [-h] own path                Change owner of file 'path' to 'own'\n"
+           "copy oldpath newpath               Copy remote file\n"
+           "cp oldpath newpath                 Copy remote file\n"
            "df [-hi] [path]                    Display statistics for current directory or\n"
            "                                   filesystem containing 'path'\n"
            "exit                               Quit sftp\n"
@@ -1342,6 +1347,10 @@ parse_args(const char **cpp, int *ignore_errors, int *disable_echo, int *aflag,
                if ((optidx = parse_link_flags(cmd, argv, argc, sflag)) == -1)
                        return -1;
                goto parse_two_paths;
+       case I_COPY:
+               if ((optidx = parse_no_flags(cmd, argv, argc)) == -1)
+                       return -1;
+               goto parse_two_paths;
        case I_RENAME:
                if ((optidx = parse_rename_flags(cmd, argv, argc, lflag)) == -1)
                        return -1;
@@ -1509,6 +1518,11 @@ parse_dispatch_command(struct sftp_conn *conn, const char *cmd, char **pwd,
                err = process_put(conn, path1, path2, *pwd, pflag,
                    rflag, aflag, fflag);
                break;
+       case I_COPY:
+               path1 = make_absolute(path1, *pwd);
+               path2 = make_absolute(path2, *pwd);
+               err = do_copy(conn, path1, path2);
+               break;
        case I_RENAME:
                path1 = make_absolute(path1, *pwd);
                path2 = make_absolute(path2, *pwd);