diff options
Diffstat (limited to 'sftp-client.c')
-rw-r--r-- | sftp-client.c | 2947 |
1 files changed, 2947 insertions, 0 deletions
diff --git a/sftp-client.c b/sftp-client.c new file mode 100644 index 0000000..e014648 --- /dev/null +++ b/sftp-client.c @@ -0,0 +1,2947 @@ +/* $OpenBSD: sftp-client.c,v 1.168 2023/01/11 05:39:38 djm Exp $ */ +/* + * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* XXX: memleaks */ +/* XXX: signed vs unsigned */ +/* XXX: remove all logging, only return status codes */ +/* XXX: copy between two remote sites */ + +#include "includes.h" + +#include <sys/types.h> +#ifdef HAVE_SYS_STATVFS_H +#include <sys/statvfs.h> +#endif +#include "openbsd-compat/sys-queue.h" +#ifdef HAVE_SYS_STAT_H +# include <sys/stat.h> +#endif +#ifdef HAVE_SYS_TIME_H +# include <sys/time.h> +#endif +#include <sys/uio.h> + +#include <dirent.h> +#include <errno.h> +#ifdef HAVE_POLL_H +#include <poll.h> +#else +# ifdef HAVE_SYS_POLL_H +# include <sys/poll.h> +# endif +#endif +#include <fcntl.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "xmalloc.h" +#include "ssherr.h" +#include "sshbuf.h" +#include "log.h" +#include "atomicio.h" +#include "progressmeter.h" +#include "misc.h" +#include "utf8.h" + +#include "sftp.h" +#include "sftp-common.h" +#include "sftp-client.h" + +extern volatile sig_atomic_t interrupted; +extern int showprogress; + +/* Default size of buffer for up/download (fix sftp.1 scp.1 if changed) */ +#define DEFAULT_COPY_BUFLEN 32768 + +/* Default number of concurrent xfer requests (fix sftp.1 scp.1 if changed) */ +#define DEFAULT_NUM_REQUESTS 64 + +/* Minimum amount of data to read at a time */ +#define MIN_READ_SIZE 512 + +/* Maximum depth to descend in directory trees */ +#define MAX_DIR_DEPTH 64 + +/* Directory separator characters */ +#ifdef HAVE_CYGWIN +# define SFTP_DIRECTORY_CHARS "/\\" +#else /* HAVE_CYGWIN */ +# define SFTP_DIRECTORY_CHARS "/" +#endif /* HAVE_CYGWIN */ + +struct sftp_conn { + int fd_in; + int fd_out; + u_int download_buflen; + u_int upload_buflen; + u_int num_requests; + u_int version; + u_int msg_id; +#define SFTP_EXT_POSIX_RENAME 0x00000001 +#define SFTP_EXT_STATVFS 0x00000002 +#define SFTP_EXT_FSTATVFS 0x00000004 +#define SFTP_EXT_HARDLINK 0x00000008 +#define SFTP_EXT_FSYNC 0x00000010 +#define SFTP_EXT_LSETSTAT 0x00000020 +#define SFTP_EXT_LIMITS 0x00000040 +#define SFTP_EXT_PATH_EXPAND 0x00000080 +#define SFTP_EXT_COPY_DATA 0x00000100 +#define SFTP_EXT_GETUSERSGROUPS_BY_ID 0x00000200 + u_int exts; + u_int64_t limit_kbps; + struct bwlimit bwlimit_in, bwlimit_out; +}; + +/* Tracks in-progress requests during file transfers */ +struct request { + u_int id; + size_t len; + u_int64_t offset; + TAILQ_ENTRY(request) tq; +}; +TAILQ_HEAD(requests, request); + +static u_char * +get_handle(struct sftp_conn *conn, u_int expected_id, size_t *len, + const char *errfmt, ...) __attribute__((format(printf, 4, 5))); + +static struct request * +request_enqueue(struct requests *requests, u_int id, size_t len, + uint64_t offset) +{ + struct request *req; + + req = xcalloc(1, sizeof(*req)); + req->id = id; + req->len = len; + req->offset = offset; + TAILQ_INSERT_TAIL(requests, req, tq); + return req; +} + +static struct request * +request_find(struct requests *requests, u_int id) +{ + struct request *req; + + for (req = TAILQ_FIRST(requests); + req != NULL && req->id != id; + req = TAILQ_NEXT(req, tq)) + ; + return req; +} + +/* ARGSUSED */ +static int +sftpio(void *_bwlimit, size_t amount) +{ + struct bwlimit *bwlimit = (struct bwlimit *)_bwlimit; + + refresh_progress_meter(0); + if (bwlimit != NULL) + bandwidth_limit(bwlimit, amount); + return 0; +} + +static void +send_msg(struct sftp_conn *conn, struct sshbuf *m) +{ + u_char mlen[4]; + struct iovec iov[2]; + + if (sshbuf_len(m) > SFTP_MAX_MSG_LENGTH) + fatal("Outbound message too long %zu", sshbuf_len(m)); + + /* Send length first */ + put_u32(mlen, sshbuf_len(m)); + iov[0].iov_base = mlen; + iov[0].iov_len = sizeof(mlen); + iov[1].iov_base = (u_char *)sshbuf_ptr(m); + iov[1].iov_len = sshbuf_len(m); + + if (atomiciov6(writev, conn->fd_out, iov, 2, sftpio, + conn->limit_kbps > 0 ? &conn->bwlimit_out : NULL) != + sshbuf_len(m) + sizeof(mlen)) + fatal("Couldn't send packet: %s", strerror(errno)); + + sshbuf_reset(m); +} + +static void +get_msg_extended(struct sftp_conn *conn, struct sshbuf *m, int initial) +{ + u_int msg_len; + u_char *p; + int r; + + sshbuf_reset(m); + if ((r = sshbuf_reserve(m, 4, &p)) != 0) + fatal_fr(r, "reserve"); + if (atomicio6(read, conn->fd_in, p, 4, sftpio, + conn->limit_kbps > 0 ? &conn->bwlimit_in : NULL) != 4) { + if (errno == EPIPE || errno == ECONNRESET) + fatal("Connection closed"); + else + fatal("Couldn't read packet: %s", strerror(errno)); + } + + if ((r = sshbuf_get_u32(m, &msg_len)) != 0) + fatal_fr(r, "sshbuf_get_u32"); + if (msg_len > SFTP_MAX_MSG_LENGTH) { + do_log2(initial ? SYSLOG_LEVEL_ERROR : SYSLOG_LEVEL_FATAL, + "Received message too long %u", msg_len); + fatal("Ensure the remote shell produces no output " + "for non-interactive sessions."); + } + + if ((r = sshbuf_reserve(m, msg_len, &p)) != 0) + fatal_fr(r, "reserve"); + if (atomicio6(read, conn->fd_in, p, msg_len, sftpio, + conn->limit_kbps > 0 ? &conn->bwlimit_in : NULL) + != msg_len) { + if (errno == EPIPE) + fatal("Connection closed"); + else + fatal("Read packet: %s", strerror(errno)); + } +} + +static void +get_msg(struct sftp_conn *conn, struct sshbuf *m) +{ + get_msg_extended(conn, m, 0); +} + +static void +send_string_request(struct sftp_conn *conn, u_int id, u_int code, const char *s, + u_int len) +{ + struct sshbuf *msg; + int r; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + if ((r = sshbuf_put_u8(msg, code)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_string(msg, s, len)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + debug3("Sent message fd %d T:%u I:%u", conn->fd_out, code, id); + sshbuf_free(msg); +} + +static void +send_string_attrs_request(struct sftp_conn *conn, u_int id, u_int code, + const void *s, u_int len, Attrib *a) +{ + struct sshbuf *msg; + int r; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + if ((r = sshbuf_put_u8(msg, code)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_string(msg, s, len)) != 0 || + (r = encode_attrib(msg, a)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + debug3("Sent message fd %d T:%u I:%u F:0x%04x M:%05o", + conn->fd_out, code, id, a->flags, a->perm); + sshbuf_free(msg); +} + +static u_int +get_status(struct sftp_conn *conn, u_int expected_id) +{ + struct sshbuf *msg; + u_char type; + u_int id, status; + int r; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + get_msg(conn, msg); + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "compose"); + + if (id != expected_id) + fatal("ID mismatch (%u != %u)", id, expected_id); + if (type != SSH2_FXP_STATUS) + fatal("Expected SSH2_FXP_STATUS(%u) packet, got %u", + SSH2_FXP_STATUS, type); + + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse"); + sshbuf_free(msg); + + debug3("SSH2_FXP_STATUS %u", status); + + return status; +} + +static u_char * +get_handle(struct sftp_conn *conn, u_int expected_id, size_t *len, + const char *errfmt, ...) +{ + struct sshbuf *msg; + u_int id, status; + u_char type; + u_char *handle; + char errmsg[256]; + va_list args; + int r; + + va_start(args, errfmt); + if (errfmt != NULL) + vsnprintf(errmsg, sizeof(errmsg), errfmt, args); + va_end(args); + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + get_msg(conn, msg); + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + + if (id != expected_id) + fatal("%s: ID mismatch (%u != %u)", + errfmt == NULL ? __func__ : errmsg, id, expected_id); + if (type == SSH2_FXP_STATUS) { + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse status"); + if (errfmt != NULL) + error("%s: %s", errmsg, fx2txt(status)); + sshbuf_free(msg); + return(NULL); + } else if (type != SSH2_FXP_HANDLE) + fatal("%s: Expected SSH2_FXP_HANDLE(%u) packet, got %u", + errfmt == NULL ? __func__ : errmsg, SSH2_FXP_HANDLE, type); + + if ((r = sshbuf_get_string(msg, &handle, len)) != 0) + fatal_fr(r, "parse handle"); + sshbuf_free(msg); + + return handle; +} + +/* XXX returning &static is error-prone. Refactor to fill *Attrib argument */ +static Attrib * +get_decode_stat(struct sftp_conn *conn, u_int expected_id, int quiet) +{ + struct sshbuf *msg; + u_int id; + u_char type; + int r; + static Attrib a; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + get_msg(conn, msg); + + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + + if (id != expected_id) + fatal("ID mismatch (%u != %u)", id, expected_id); + if (type == SSH2_FXP_STATUS) { + u_int status; + + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse status"); + if (quiet) + debug("stat remote: %s", fx2txt(status)); + else + error("stat remote: %s", fx2txt(status)); + sshbuf_free(msg); + return(NULL); + } else if (type != SSH2_FXP_ATTRS) { + fatal("Expected SSH2_FXP_ATTRS(%u) packet, got %u", + SSH2_FXP_ATTRS, type); + } + if ((r = decode_attrib(msg, &a)) != 0) { + error_fr(r, "decode_attrib"); + sshbuf_free(msg); + return NULL; + } + debug3("Received stat reply T:%u I:%u F:0x%04x M:%05o", + type, id, a.flags, a.perm); + sshbuf_free(msg); + + return &a; +} + +static int +get_decode_statvfs(struct sftp_conn *conn, struct sftp_statvfs *st, + u_int expected_id, int quiet) +{ + struct sshbuf *msg; + u_char type; + u_int id; + u_int64_t flag; + int r; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + get_msg(conn, msg); + + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + + debug3("Received statvfs reply T:%u I:%u", type, id); + if (id != expected_id) + fatal("ID mismatch (%u != %u)", id, expected_id); + if (type == SSH2_FXP_STATUS) { + u_int status; + + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse status"); + if (quiet) + debug("remote statvfs: %s", fx2txt(status)); + else + error("remote statvfs: %s", fx2txt(status)); + sshbuf_free(msg); + return -1; + } else if (type != SSH2_FXP_EXTENDED_REPLY) { + fatal("Expected SSH2_FXP_EXTENDED_REPLY(%u) packet, got %u", + SSH2_FXP_EXTENDED_REPLY, type); + } + + memset(st, 0, sizeof(*st)); + if ((r = sshbuf_get_u64(msg, &st->f_bsize)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_frsize)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_blocks)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_bfree)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_bavail)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_files)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_ffree)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_favail)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_fsid)) != 0 || + (r = sshbuf_get_u64(msg, &flag)) != 0 || + (r = sshbuf_get_u64(msg, &st->f_namemax)) != 0) + fatal_fr(r, "parse statvfs"); + + st->f_flag = (flag & SSH2_FXE_STATVFS_ST_RDONLY) ? ST_RDONLY : 0; + st->f_flag |= (flag & SSH2_FXE_STATVFS_ST_NOSUID) ? ST_NOSUID : 0; + + sshbuf_free(msg); + + return 0; +} + +struct sftp_conn * +do_init(int fd_in, int fd_out, u_int transfer_buflen, u_int num_requests, + u_int64_t limit_kbps) +{ + u_char type; + struct sshbuf *msg; + struct sftp_conn *ret; + int r; + + ret = xcalloc(1, sizeof(*ret)); + ret->msg_id = 1; + ret->fd_in = fd_in; + ret->fd_out = fd_out; + ret->download_buflen = ret->upload_buflen = + transfer_buflen ? transfer_buflen : DEFAULT_COPY_BUFLEN; + ret->num_requests = + num_requests ? num_requests : DEFAULT_NUM_REQUESTS; + ret->exts = 0; + ret->limit_kbps = 0; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_INIT)) != 0 || + (r = sshbuf_put_u32(msg, SSH2_FILEXFER_VERSION)) != 0) + fatal_fr(r, "parse"); + + send_msg(ret, msg); + + get_msg_extended(ret, msg, 1); + + /* Expecting a VERSION reply */ + if ((r = sshbuf_get_u8(msg, &type)) != 0) + fatal_fr(r, "parse type"); + if (type != SSH2_FXP_VERSION) { + error("Invalid packet back from SSH2_FXP_INIT (type %u)", + type); + sshbuf_free(msg); + free(ret); + return(NULL); + } + if ((r = sshbuf_get_u32(msg, &ret->version)) != 0) + fatal_fr(r, "parse version"); + + debug2("Remote version: %u", ret->version); + + /* Check for extensions */ + while (sshbuf_len(msg) > 0) { + char *name; + u_char *value; + size_t vlen; + int known = 0; + + if ((r = sshbuf_get_cstring(msg, &name, NULL)) != 0 || + (r = sshbuf_get_string(msg, &value, &vlen)) != 0) + fatal_fr(r, "parse extension"); + if (strcmp(name, "posix-rename@openssh.com") == 0 && + strcmp((char *)value, "1") == 0) { + ret->exts |= SFTP_EXT_POSIX_RENAME; + known = 1; + } else if (strcmp(name, "statvfs@openssh.com") == 0 && + strcmp((char *)value, "2") == 0) { + ret->exts |= SFTP_EXT_STATVFS; + known = 1; + } else if (strcmp(name, "fstatvfs@openssh.com") == 0 && + strcmp((char *)value, "2") == 0) { + ret->exts |= SFTP_EXT_FSTATVFS; + known = 1; + } else if (strcmp(name, "hardlink@openssh.com") == 0 && + strcmp((char *)value, "1") == 0) { + ret->exts |= SFTP_EXT_HARDLINK; + known = 1; + } else if (strcmp(name, "fsync@openssh.com") == 0 && + strcmp((char *)value, "1") == 0) { + ret->exts |= SFTP_EXT_FSYNC; + known = 1; + } else if (strcmp(name, "lsetstat@openssh.com") == 0 && + strcmp((char *)value, "1") == 0) { + ret->exts |= SFTP_EXT_LSETSTAT; + known = 1; + } else if (strcmp(name, "limits@openssh.com") == 0 && + strcmp((char *)value, "1") == 0) { + ret->exts |= SFTP_EXT_LIMITS; + known = 1; + } else if (strcmp(name, "expand-path@openssh.com") == 0 && + 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; + } else if (strcmp(name, + "users-groups-by-id@openssh.com") == 0 && + strcmp((char *)value, "1") == 0) { + ret->exts |= SFTP_EXT_GETUSERSGROUPS_BY_ID; + known = 1; + } + if (known) { + debug2("Server supports extension \"%s\" revision %s", + name, value); + } else { + debug2("Unrecognised server extension \"%s\"", name); + } + free(name); + free(value); + } + + sshbuf_free(msg); + + /* Query the server for its limits */ + if (ret->exts & SFTP_EXT_LIMITS) { + struct sftp_limits limits; + if (do_limits(ret, &limits) != 0) + fatal_f("limits failed"); + + /* If the caller did not specify, find a good value */ + if (transfer_buflen == 0) { + ret->download_buflen = MINIMUM(limits.read_length, + SFTP_MAX_MSG_LENGTH - 1024); + ret->upload_buflen = MINIMUM(limits.write_length, + SFTP_MAX_MSG_LENGTH - 1024); + ret->download_buflen = MAXIMUM(ret->download_buflen, 64); + ret->upload_buflen = MAXIMUM(ret->upload_buflen, 64); + debug3("server upload/download buffer sizes " + "%llu / %llu; using %u / %u", + (unsigned long long)limits.write_length, + (unsigned long long)limits.read_length, + ret->upload_buflen, ret->download_buflen); + } + + /* Use the server limit to scale down our value only */ + if (num_requests == 0 && limits.open_handles) { + ret->num_requests = + MINIMUM(DEFAULT_NUM_REQUESTS, limits.open_handles); + if (ret->num_requests == 0) + ret->num_requests = 1; + debug3("server handle limit %llu; using %u", + (unsigned long long)limits.open_handles, + ret->num_requests); + } + } + + /* Some filexfer v.0 servers don't support large packets */ + if (ret->version == 0) { + ret->download_buflen = MINIMUM(ret->download_buflen, 20480); + ret->upload_buflen = MINIMUM(ret->upload_buflen, 20480); + } + + ret->limit_kbps = limit_kbps; + if (ret->limit_kbps > 0) { + bandwidth_limit_init(&ret->bwlimit_in, ret->limit_kbps, + ret->download_buflen); + bandwidth_limit_init(&ret->bwlimit_out, ret->limit_kbps, + ret->upload_buflen); + } + + return ret; +} + +u_int +sftp_proto_version(struct sftp_conn *conn) +{ + return conn->version; +} + +int +do_limits(struct sftp_conn *conn, struct sftp_limits *limits) +{ + u_int id, msg_id; + u_char type; + struct sshbuf *msg; + int r; + + if ((conn->exts & SFTP_EXT_LIMITS) == 0) { + error("Server does not support limits@openssh.com extension"); + return -1; + } + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + 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, "limits@openssh.com")) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + debug3("Sent message limits@openssh.com I:%u", id); + + get_msg(conn, msg); + + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &msg_id)) != 0) + fatal_fr(r, "parse"); + + debug3("Received limits reply T:%u I:%u", type, msg_id); + if (id != msg_id) + fatal("ID mismatch (%u != %u)", msg_id, id); + if (type != SSH2_FXP_EXTENDED_REPLY) { + debug_f("expected SSH2_FXP_EXTENDED_REPLY(%u) packet, got %u", + SSH2_FXP_EXTENDED_REPLY, type); + /* Disable the limits extension */ + conn->exts &= ~SFTP_EXT_LIMITS; + sshbuf_free(msg); + return 0; + } + + memset(limits, 0, sizeof(*limits)); + if ((r = sshbuf_get_u64(msg, &limits->packet_length)) != 0 || + (r = sshbuf_get_u64(msg, &limits->read_length)) != 0 || + (r = sshbuf_get_u64(msg, &limits->write_length)) != 0 || + (r = sshbuf_get_u64(msg, &limits->open_handles)) != 0) + fatal_fr(r, "parse limits"); + + sshbuf_free(msg); + + return 0; +} + +int +do_close(struct sftp_conn *conn, const u_char *handle, u_int handle_len) +{ + u_int id, status; + struct sshbuf *msg; + int r; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + id = conn->msg_id++; + if ((r = sshbuf_put_u8(msg, SSH2_FXP_CLOSE)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_string(msg, handle, handle_len)) != 0) + fatal_fr(r, "parse"); + send_msg(conn, msg); + debug3("Sent message SSH2_FXP_CLOSE I:%u", id); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("close remote: %s", fx2txt(status)); + + sshbuf_free(msg); + + return status == SSH2_FX_OK ? 0 : -1; +} + + +static int +do_lsreaddir(struct sftp_conn *conn, const char *path, int print_flag, + SFTP_DIRENT ***dir) +{ + struct sshbuf *msg; + u_int count, id, i, expected_id, ents = 0; + size_t handle_len; + u_char type, *handle; + int status = SSH2_FX_FAILURE; + int r; + + if (dir) + *dir = NULL; + + id = conn->msg_id++; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_OPENDIR)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_cstring(msg, path)) != 0) + fatal_fr(r, "compose OPENDIR"); + send_msg(conn, msg); + + handle = get_handle(conn, id, &handle_len, + "remote readdir(\"%s\")", path); + if (handle == NULL) { + sshbuf_free(msg); + return -1; + } + + if (dir) { + ents = 0; + *dir = xcalloc(1, sizeof(**dir)); + (*dir)[0] = NULL; + } + + for (; !interrupted;) { + id = expected_id = conn->msg_id++; + + debug3("Sending SSH2_FXP_READDIR I:%u", id); + + sshbuf_reset(msg); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_READDIR)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_string(msg, handle, handle_len)) != 0) + fatal_fr(r, "compose READDIR"); + send_msg(conn, msg); + + sshbuf_reset(msg); + + get_msg(conn, msg); + + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + + debug3("Received reply T:%u I:%u", type, id); + + if (id != expected_id) + fatal("ID mismatch (%u != %u)", id, expected_id); + + if (type == SSH2_FXP_STATUS) { + u_int rstatus; + + if ((r = sshbuf_get_u32(msg, &rstatus)) != 0) + fatal_fr(r, "parse status"); + debug3("Received SSH2_FXP_STATUS %d", rstatus); + if (rstatus == SSH2_FX_EOF) + break; + error("Couldn't read directory: %s", fx2txt(rstatus)); + goto out; + } else if (type != SSH2_FXP_NAME) + fatal("Expected SSH2_FXP_NAME(%u) packet, got %u", + SSH2_FXP_NAME, type); + + if ((r = sshbuf_get_u32(msg, &count)) != 0) + fatal_fr(r, "parse count"); + if (count > SSHBUF_SIZE_MAX) + fatal_f("nonsensical number of entries"); + if (count == 0) + break; + debug3("Received %d SSH2_FXP_NAME responses", count); + for (i = 0; i < count; i++) { + char *filename, *longname; + Attrib a; + + if ((r = sshbuf_get_cstring(msg, &filename, + NULL)) != 0 || + (r = sshbuf_get_cstring(msg, &longname, + NULL)) != 0) + fatal_fr(r, "parse filenames"); + if ((r = decode_attrib(msg, &a)) != 0) { + error_fr(r, "couldn't decode attrib"); + free(filename); + free(longname); + goto out; + } + + if (print_flag) + mprintf("%s\n", longname); + + /* + * Directory entries should never contain '/' + * These can be used to attack recursive ops + * (e.g. send '../../../../etc/passwd') + */ + if (strpbrk(filename, SFTP_DIRECTORY_CHARS) != NULL) { + error("Server sent suspect path \"%s\" " + "during readdir of \"%s\"", filename, path); + } else if (dir) { + *dir = xreallocarray(*dir, ents + 2, sizeof(**dir)); + (*dir)[ents] = xcalloc(1, sizeof(***dir)); + (*dir)[ents]->filename = xstrdup(filename); + (*dir)[ents]->longname = xstrdup(longname); + memcpy(&(*dir)[ents]->a, &a, sizeof(a)); + (*dir)[++ents] = NULL; + } + free(filename); + free(longname); + } + } + status = 0; + + out: + sshbuf_free(msg); + do_close(conn, handle, handle_len); + free(handle); + + if (status != 0 && dir != NULL) { + /* Don't return results on error */ + free_sftp_dirents(*dir); + *dir = NULL; + } else if (interrupted && dir != NULL && *dir != NULL) { + /* Don't return partial matches on interrupt */ + free_sftp_dirents(*dir); + *dir = xcalloc(1, sizeof(**dir)); + **dir = NULL; + } + + return status == SSH2_FX_OK ? 0 : -1; +} + +int +do_readdir(struct sftp_conn *conn, const char *path, SFTP_DIRENT ***dir) +{ + return(do_lsreaddir(conn, path, 0, dir)); +} + +void free_sftp_dirents(SFTP_DIRENT **s) +{ + int i; + + if (s == NULL) + return; + for (i = 0; s[i]; i++) { + free(s[i]->filename); + free(s[i]->longname); + free(s[i]); + } + free(s); +} + +int +do_rm(struct sftp_conn *conn, const char *path) +{ + u_int status, id; + + debug2("Sending SSH2_FXP_REMOVE \"%s\"", path); + + id = conn->msg_id++; + send_string_request(conn, id, SSH2_FXP_REMOVE, path, strlen(path)); + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote delete %s: %s", path, fx2txt(status)); + return status == SSH2_FX_OK ? 0 : -1; +} + +int +do_mkdir(struct sftp_conn *conn, const char *path, Attrib *a, int print_flag) +{ + u_int status, id; + + debug2("Sending SSH2_FXP_MKDIR \"%s\"", path); + + id = conn->msg_id++; + send_string_attrs_request(conn, id, SSH2_FXP_MKDIR, path, + strlen(path), a); + + status = get_status(conn, id); + if (status != SSH2_FX_OK && print_flag) + error("remote mkdir \"%s\": %s", path, fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +int +do_rmdir(struct sftp_conn *conn, const char *path) +{ + u_int status, id; + + debug2("Sending SSH2_FXP_RMDIR \"%s\"", path); + + id = conn->msg_id++; + send_string_request(conn, id, SSH2_FXP_RMDIR, path, + strlen(path)); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote rmdir \"%s\": %s", path, fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +Attrib * +do_stat(struct sftp_conn *conn, const char *path, int quiet) +{ + u_int id; + + debug2("Sending SSH2_FXP_STAT \"%s\"", path); + + id = conn->msg_id++; + + send_string_request(conn, id, + conn->version == 0 ? SSH2_FXP_STAT_VERSION_0 : SSH2_FXP_STAT, + path, strlen(path)); + + return(get_decode_stat(conn, id, quiet)); +} + +Attrib * +do_lstat(struct sftp_conn *conn, const char *path, int quiet) +{ + u_int id; + + if (conn->version == 0) { + if (quiet) + debug("Server version does not support lstat operation"); + else + logit("Server version does not support lstat operation"); + return(do_stat(conn, path, quiet)); + } + + id = conn->msg_id++; + send_string_request(conn, id, SSH2_FXP_LSTAT, path, + strlen(path)); + + return(get_decode_stat(conn, id, quiet)); +} + +#ifdef notyet +Attrib * +do_fstat(struct sftp_conn *conn, const u_char *handle, u_int handle_len, + int quiet) +{ + u_int id; + + debug2("Sending SSH2_FXP_FSTAT \"%s\""); + + id = conn->msg_id++; + send_string_request(conn, id, SSH2_FXP_FSTAT, handle, + handle_len); + + return(get_decode_stat(conn, id, quiet)); +} +#endif + +int +do_setstat(struct sftp_conn *conn, const char *path, Attrib *a) +{ + u_int status, id; + + debug2("Sending SSH2_FXP_SETSTAT \"%s\"", path); + + id = conn->msg_id++; + send_string_attrs_request(conn, id, SSH2_FXP_SETSTAT, path, + strlen(path), a); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote setstat \"%s\": %s", path, fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +int +do_fsetstat(struct sftp_conn *conn, const u_char *handle, u_int handle_len, + Attrib *a) +{ + u_int status, id; + + debug2("Sending SSH2_FXP_FSETSTAT"); + + id = conn->msg_id++; + send_string_attrs_request(conn, id, SSH2_FXP_FSETSTAT, handle, + handle_len, a); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote fsetstat: %s", fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +/* Implements both the realpath and expand-path operations */ +static char * +do_realpath_expand(struct sftp_conn *conn, const char *path, int expand) +{ + struct sshbuf *msg; + u_int expected_id, count, id; + char *filename, *longname; + Attrib a; + u_char type; + int r; + const char *what = "SSH2_FXP_REALPATH"; + + if (expand) + what = "expand-path@openssh.com"; + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + expected_id = id = conn->msg_id++; + if (expand) { + debug2("Sending SSH2_FXP_EXTENDED(expand-path@openssh.com) " + "\"%s\"", path); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_EXTENDED)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_cstring(msg, + "expand-path@openssh.com")) != 0 || + (r = sshbuf_put_cstring(msg, path)) != 0) + fatal_fr(r, "compose %s", what); + send_msg(conn, msg); + } else { + debug2("Sending SSH2_FXP_REALPATH \"%s\"", path); + send_string_request(conn, id, SSH2_FXP_REALPATH, + path, strlen(path)); + } + get_msg(conn, msg); + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + + if (id != expected_id) + fatal("ID mismatch (%u != %u)", id, expected_id); + + if (type == SSH2_FXP_STATUS) { + u_int status; + char *errmsg; + + if ((r = sshbuf_get_u32(msg, &status)) != 0 || + (r = sshbuf_get_cstring(msg, &errmsg, NULL)) != 0) + fatal_fr(r, "parse status"); + error("%s %s: %s", expand ? "expand" : "realpath", + path, *errmsg == '\0' ? fx2txt(status) : errmsg); + free(errmsg); + sshbuf_free(msg); + return NULL; + } else if (type != SSH2_FXP_NAME) + fatal("Expected SSH2_FXP_NAME(%u) packet, got %u", + SSH2_FXP_NAME, type); + + if ((r = sshbuf_get_u32(msg, &count)) != 0) + fatal_fr(r, "parse count"); + if (count != 1) + fatal("Got multiple names (%d) from %s", count, what); + + if ((r = sshbuf_get_cstring(msg, &filename, NULL)) != 0 || + (r = sshbuf_get_cstring(msg, &longname, NULL)) != 0 || + (r = decode_attrib(msg, &a)) != 0) + fatal_fr(r, "parse filename/attrib"); + + debug3("%s %s -> %s", what, path, filename); + + free(longname); + + sshbuf_free(msg); + + return(filename); +} + +char * +do_realpath(struct sftp_conn *conn, const char *path) +{ + return do_realpath_expand(conn, path, 0); +} + +int +can_expand_path(struct sftp_conn *conn) +{ + return (conn->exts & SFTP_EXT_PATH_EXPAND) != 0; +} + +char * +do_expand_path(struct sftp_conn *conn, const char *path) +{ + if (!can_expand_path(conn)) { + debug3_f("no server support, fallback to realpath"); + return do_realpath_expand(conn, path, 0); + } + 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) +{ + struct sshbuf *msg; + u_int status, id; + int r, use_ext = (conn->exts & SFTP_EXT_POSIX_RENAME) && !force_legacy; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + /* Send rename request */ + id = conn->msg_id++; + if (use_ext) { + debug2("Sending SSH2_FXP_EXTENDED(posix-rename@openssh.com) " + "\"%s\" to \"%s\"", oldpath, newpath); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_EXTENDED)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_cstring(msg, + "posix-rename@openssh.com")) != 0) + fatal_fr(r, "compose posix-rename"); + } else { + debug2("Sending SSH2_FXP_RENAME \"%s\" to \"%s\"", + oldpath, newpath); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_RENAME)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0) + fatal_fr(r, "compose rename"); + } + if ((r = sshbuf_put_cstring(msg, oldpath)) != 0 || + (r = sshbuf_put_cstring(msg, newpath)) != 0) + fatal_fr(r, "compose paths"); + send_msg(conn, msg); + debug3("Sent message %s \"%s\" -> \"%s\"", + use_ext ? "posix-rename@openssh.com" : + "SSH2_FXP_RENAME", oldpath, newpath); + sshbuf_free(msg); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote rename \"%s\" to \"%s\": %s", oldpath, + newpath, fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +int +do_hardlink(struct sftp_conn *conn, const char *oldpath, const char *newpath) +{ + struct sshbuf *msg; + u_int status, id; + int r; + + if ((conn->exts & SFTP_EXT_HARDLINK) == 0) { + error("Server does not support hardlink@openssh.com extension"); + return -1; + } + debug2("Sending SSH2_FXP_EXTENDED(hardlink@openssh.com) " + "\"%s\" to \"%s\"", oldpath, newpath); + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + /* Send link request */ + 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, "hardlink@openssh.com")) != 0 || + (r = sshbuf_put_cstring(msg, oldpath)) != 0 || + (r = sshbuf_put_cstring(msg, newpath)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + debug3("Sent message hardlink@openssh.com \"%s\" -> \"%s\"", + oldpath, newpath); + sshbuf_free(msg); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote link \"%s\" to \"%s\": %s", oldpath, + newpath, fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +int +do_symlink(struct sftp_conn *conn, const char *oldpath, const char *newpath) +{ + struct sshbuf *msg; + u_int status, id; + int r; + + if (conn->version < 3) { + error("This server does not support the symlink operation"); + return(SSH2_FX_OP_UNSUPPORTED); + } + debug2("Sending SSH2_FXP_SYMLINK \"%s\" to \"%s\"", oldpath, newpath); + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + /* Send symlink request */ + id = conn->msg_id++; + if ((r = sshbuf_put_u8(msg, SSH2_FXP_SYMLINK)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_cstring(msg, oldpath)) != 0 || + (r = sshbuf_put_cstring(msg, newpath)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + debug3("Sent message SSH2_FXP_SYMLINK \"%s\" -> \"%s\"", oldpath, + newpath); + sshbuf_free(msg); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote symlink file \"%s\" to \"%s\": %s", oldpath, + newpath, fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +int +do_fsync(struct sftp_conn *conn, u_char *handle, u_int handle_len) +{ + struct sshbuf *msg; + u_int status, id; + int r; + + /* Silently return if the extension is not supported */ + if ((conn->exts & SFTP_EXT_FSYNC) == 0) + return -1; + debug2("Sending SSH2_FXP_EXTENDED(fsync@openssh.com)"); + + /* Send fsync request */ + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + 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, "fsync@openssh.com")) != 0 || + (r = sshbuf_put_string(msg, handle, handle_len)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + debug3("Sent message fsync@openssh.com I:%u", id); + sshbuf_free(msg); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote fsync: %s", fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +#ifdef notyet +char * +do_readlink(struct sftp_conn *conn, const char *path) +{ + struct sshbuf *msg; + u_int expected_id, count, id; + char *filename, *longname; + Attrib a; + u_char type; + int r; + + debug2("Sending SSH2_FXP_READLINK \"%s\"", path); + + expected_id = id = conn->msg_id++; + send_string_request(conn, id, SSH2_FXP_READLINK, path, strlen(path)); + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + get_msg(conn, msg); + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + + if (id != expected_id) + fatal("ID mismatch (%u != %u)", id, expected_id); + + if (type == SSH2_FXP_STATUS) { + u_int status; + + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse status"); + error("Couldn't readlink: %s", fx2txt(status)); + sshbuf_free(msg); + return(NULL); + } else if (type != SSH2_FXP_NAME) + fatal("Expected SSH2_FXP_NAME(%u) packet, got %u", + SSH2_FXP_NAME, type); + + if ((r = sshbuf_get_u32(msg, &count)) != 0) + fatal_fr(r, "parse count"); + if (count != 1) + fatal("Got multiple names (%d) from SSH_FXP_READLINK", count); + + if ((r = sshbuf_get_cstring(msg, &filename, NULL)) != 0 || + (r = sshbuf_get_cstring(msg, &longname, NULL)) != 0 || + (r = decode_attrib(msg, &a)) != 0) + fatal_fr(r, "parse filenames/attrib"); + + debug3("SSH_FXP_READLINK %s -> %s", path, filename); + + free(longname); + + sshbuf_free(msg); + + return filename; +} +#endif + +int +do_statvfs(struct sftp_conn *conn, const char *path, struct sftp_statvfs *st, + int quiet) +{ + struct sshbuf *msg; + u_int id; + int r; + + if ((conn->exts & SFTP_EXT_STATVFS) == 0) { + error("Server does not support statvfs@openssh.com extension"); + return -1; + } + + debug2("Sending SSH2_FXP_EXTENDED(statvfs@openssh.com) \"%s\"", path); + + id = conn->msg_id++; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_EXTENDED)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_cstring(msg, "statvfs@openssh.com")) != 0 || + (r = sshbuf_put_cstring(msg, path)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + sshbuf_free(msg); + + return get_decode_statvfs(conn, st, id, quiet); +} + +#ifdef notyet +int +do_fstatvfs(struct sftp_conn *conn, const u_char *handle, u_int handle_len, + struct sftp_statvfs *st, int quiet) +{ + struct sshbuf *msg; + u_int id; + + if ((conn->exts & SFTP_EXT_FSTATVFS) == 0) { + error("Server does not support fstatvfs@openssh.com extension"); + return -1; + } + + debug2("Sending SSH2_FXP_EXTENDED(fstatvfs@openssh.com)"); + + id = conn->msg_id++; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_EXTENDED)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_cstring(msg, "fstatvfs@openssh.com")) != 0 || + (r = sshbuf_put_string(msg, handle, handle_len)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + sshbuf_free(msg); + + return get_decode_statvfs(conn, st, id, quiet); +} +#endif + +int +do_lsetstat(struct sftp_conn *conn, const char *path, Attrib *a) +{ + struct sshbuf *msg; + u_int status, id; + int r; + + if ((conn->exts & SFTP_EXT_LSETSTAT) == 0) { + error("Server does not support lsetstat@openssh.com extension"); + return -1; + } + + debug2("Sending SSH2_FXP_EXTENDED(lsetstat@openssh.com) \"%s\"", path); + + id = conn->msg_id++; + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_EXTENDED)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_cstring(msg, "lsetstat@openssh.com")) != 0 || + (r = sshbuf_put_cstring(msg, path)) != 0 || + (r = encode_attrib(msg, a)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + sshbuf_free(msg); + + status = get_status(conn, id); + if (status != SSH2_FX_OK) + error("remote lsetstat \"%s\": %s", path, fx2txt(status)); + + return status == SSH2_FX_OK ? 0 : -1; +} + +static void +send_read_request(struct sftp_conn *conn, u_int id, u_int64_t offset, + u_int len, const u_char *handle, u_int handle_len) +{ + struct sshbuf *msg; + int r; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_READ)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_string(msg, handle, handle_len)) != 0 || + (r = sshbuf_put_u64(msg, offset)) != 0 || + (r = sshbuf_put_u32(msg, len)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + sshbuf_free(msg); +} + +static int +send_open(struct sftp_conn *conn, const char *path, const char *tag, + u_int openmode, Attrib *a, u_char **handlep, size_t *handle_lenp) +{ + Attrib junk; + u_char *handle; + size_t handle_len; + struct sshbuf *msg; + int r; + u_int id; + + debug2("Sending SSH2_FXP_OPEN \"%s\"", path); + + *handlep = NULL; + *handle_lenp = 0; + + if (a == NULL) { + attrib_clear(&junk); /* Send empty attributes */ + a = &junk; + } + /* Send open request */ + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + 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, path)) != 0 || + (r = sshbuf_put_u32(msg, openmode)) != 0 || + (r = encode_attrib(msg, a)) != 0) + fatal_fr(r, "compose %s open", tag); + send_msg(conn, msg); + sshbuf_free(msg); + debug3("Sent %s message SSH2_FXP_OPEN I:%u P:%s M:0x%04x", + tag, id, path, openmode); + if ((handle = get_handle(conn, id, &handle_len, + "%s open \"%s\"", tag, path)) == NULL) + return -1; + /* success */ + *handlep = handle; + *handle_lenp = handle_len; + return 0; +} + +static const char * +progress_meter_path(const char *path) +{ + const char *progresspath; + + if ((progresspath = strrchr(path, '/')) == NULL) + return path; + progresspath++; + if (*progresspath == '\0') + return path; + return progresspath; +} + +int +do_download(struct sftp_conn *conn, const char *remote_path, + const char *local_path, Attrib *a, int preserve_flag, int resume_flag, + int fsync_flag, int inplace_flag) +{ + struct sshbuf *msg; + u_char *handle; + int local_fd = -1, write_error; + int read_error, write_errno, lmodified = 0, reordered = 0, r; + u_int64_t offset = 0, size, highwater; + u_int mode, id, buflen, num_req, max_req, status = SSH2_FX_OK; + off_t progress_counter; + size_t handle_len; + struct stat st; + struct requests requests; + struct request *req; + u_char type; + + debug2_f("download remote \"%s\" to local \"%s\"", + remote_path, local_path); + + TAILQ_INIT(&requests); + + if (a == NULL && (a = do_stat(conn, remote_path, 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; + else + mode = 0666; + + if ((a->flags & SSH2_FILEXFER_ATTR_PERMISSIONS) && + (!S_ISREG(a->perm))) { + error("download %s: not a regular file", remote_path); + return(-1); + } + + if (a->flags & SSH2_FILEXFER_ATTR_SIZE) + size = a->size; + else + size = 0; + + buflen = conn->download_buflen; + + /* Send open request */ + if (send_open(conn, remote_path, "remote", SSH2_FXF_READ, NULL, + &handle, &handle_len) != 0) + return -1; + + local_fd = open(local_path, O_WRONLY | O_CREAT | + ((resume_flag || inplace_flag) ? 0 : O_TRUNC), mode | S_IWUSR); + if (local_fd == -1) { + error("open local \"%s\": %s", local_path, strerror(errno)); + goto fail; + } + offset = highwater = 0; + if (resume_flag) { + if (fstat(local_fd, &st) == -1) { + error("stat local \"%s\": %s", + local_path, strerror(errno)); + goto fail; + } + if (st.st_size < 0) { + error("\"%s\" has negative size", local_path); + goto fail; + } + if ((u_int64_t)st.st_size > size) { + error("Unable to resume download of \"%s\": " + "local file is larger than remote", local_path); + fail: + do_close(conn, handle, handle_len); + free(handle); + if (local_fd != -1) + close(local_fd); + return -1; + } + offset = highwater = st.st_size; + } + + /* Read from remote and write to local */ + write_error = read_error = write_errno = num_req = 0; + max_req = 1; + progress_counter = offset; + + if (showprogress && size != 0) { + start_progress_meter(progress_meter_path(remote_path), + size, &progress_counter); + } + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + while (num_req > 0 || max_req > 0) { + u_char *data; + size_t len; + + /* + * Simulate EOF on interrupt: stop sending new requests and + * allow outstanding requests to drain gracefully + */ + if (interrupted) { + if (num_req == 0) /* If we haven't started yet... */ + break; + max_req = 0; + } + + /* Send some more requests */ + while (num_req < max_req) { + debug3("Request range %llu -> %llu (%d/%d)", + (unsigned long long)offset, + (unsigned long long)offset + buflen - 1, + num_req, max_req); + req = request_enqueue(&requests, conn->msg_id++, + buflen, offset); + offset += buflen; + num_req++; + send_read_request(conn, req->id, req->offset, + req->len, handle, handle_len); + } + + sshbuf_reset(msg); + get_msg(conn, msg); + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + debug3("Received reply T:%u I:%u R:%d", type, id, max_req); + + /* Find the request in our queue */ + if ((req = request_find(&requests, id)) == NULL) + fatal("Unexpected reply %u", id); + + switch (type) { + case SSH2_FXP_STATUS: + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse status"); + if (status != SSH2_FX_EOF) + read_error = 1; + max_req = 0; + TAILQ_REMOVE(&requests, req, tq); + free(req); + num_req--; + break; + case SSH2_FXP_DATA: + if ((r = sshbuf_get_string(msg, &data, &len)) != 0) + fatal_fr(r, "parse data"); + debug3("Received data %llu -> %llu", + (unsigned long long)req->offset, + (unsigned long long)req->offset + len - 1); + if (len > req->len) + fatal("Received more data than asked for " + "%zu > %zu", len, req->len); + lmodified = 1; + if ((lseek(local_fd, req->offset, SEEK_SET) == -1 || + atomicio(vwrite, local_fd, data, len) != len) && + !write_error) { + write_errno = errno; + write_error = 1; + max_req = 0; + } + else if (!reordered && req->offset <= highwater) + highwater = req->offset + len; + else if (!reordered && req->offset > highwater) + reordered = 1; + progress_counter += len; + free(data); + + if (len == req->len) { + TAILQ_REMOVE(&requests, req, tq); + free(req); + num_req--; + } else { + /* Resend the request for the missing data */ + debug3("Short data block, re-requesting " + "%llu -> %llu (%2d)", + (unsigned long long)req->offset + len, + (unsigned long long)req->offset + + req->len - 1, num_req); + req->id = conn->msg_id++; + req->len -= len; + req->offset += len; + send_read_request(conn, req->id, + req->offset, req->len, handle, handle_len); + /* Reduce the request size */ + if (len < buflen) + buflen = MAXIMUM(MIN_READ_SIZE, len); + } + if (max_req > 0) { /* max_req = 0 iff EOF received */ + if (size > 0 && offset > size) { + /* Only one request at a time + * after the expected EOF */ + debug3("Finish at %llu (%2d)", + (unsigned long long)offset, + num_req); + max_req = 1; + } else if (max_req < conn->num_requests) { + ++max_req; + } + } + break; + default: + fatal("Expected SSH2_FXP_DATA(%u) packet, got %u", + SSH2_FXP_DATA, type); + } + } + + if (showprogress && size) + stop_progress_meter(); + + /* Sanity check */ + if (TAILQ_FIRST(&requests) != NULL) + fatal("Transfer complete, but requests still in queue"); + /* + * Truncate at highest contiguous point to avoid holes on interrupt, + * or unconditionally if writing in place. + */ + if (inplace_flag || read_error || write_error || interrupted) { + if (reordered && resume_flag) { + error("Unable to resume download of \"%s\": " + "server reordered requests", local_path); + } + debug("truncating at %llu", (unsigned long long)highwater); + if (ftruncate(local_fd, highwater) == -1) + error("local ftruncate \"%s\": %s", local_path, + strerror(errno)); + } + if (read_error) { + error("read remote \"%s\" : %s", remote_path, fx2txt(status)); + status = -1; + do_close(conn, handle, handle_len); + } else if (write_error) { + error("write local \"%s\": %s", local_path, + strerror(write_errno)); + status = SSH2_FX_FAILURE; + do_close(conn, handle, handle_len); + } else { + if (do_close(conn, handle, handle_len) != 0 || interrupted) + status = SSH2_FX_FAILURE; + else + status = SSH2_FX_OK; + /* Override umask and utimes if asked */ +#ifdef HAVE_FCHMOD + if (preserve_flag && fchmod(local_fd, mode) == -1) +#else + if (preserve_flag && chmod(local_path, mode) == -1) +#endif /* HAVE_FCHMOD */ + error("local chmod \"%s\": %s", local_path, + strerror(errno)); + if (preserve_flag && + (a->flags & SSH2_FILEXFER_ATTR_ACMODTIME)) { + struct timeval tv[2]; + tv[0].tv_sec = a->atime; + tv[1].tv_sec = a->mtime; + tv[0].tv_usec = tv[1].tv_usec = 0; + if (utimes(local_path, tv) == -1) + error("local set times \"%s\": %s", + local_path, strerror(errno)); + } + if (resume_flag && !lmodified) + logit("File \"%s\" was not modified", local_path); + else if (fsync_flag) { + debug("syncing \"%s\"", local_path); + if (fsync(local_fd) == -1) + error("local sync \"%s\": %s", + local_path, strerror(errno)); + } + } + close(local_fd); + sshbuf_free(msg); + free(handle); + + return status == SSH2_FX_OK ? 0 : -1; +} + +static int +download_dir_internal(struct sftp_conn *conn, const char *src, const char *dst, + int depth, Attrib *dirattrib, int preserve_flag, int print_flag, + int resume_flag, int fsync_flag, int follow_link_flag, int inplace_flag) +{ + int i, ret = 0; + SFTP_DIRENT **dir_entries; + char *filename, *new_src = NULL, *new_dst = NULL; + mode_t mode = 0777, tmpmode = mode; + + if (depth >= MAX_DIR_DEPTH) { + error("Maximum directory depth exceeded: %d levels", depth); + return -1; + } + + debug2_f("download dir remote \"%s\" to local \"%s\"", src, dst); + + if (dirattrib == NULL && + (dirattrib = do_stat(conn, src, 1)) == NULL) { + error("stat remote \"%s\" directory failed", src); + return -1; + } + if (!S_ISDIR(dirattrib->perm)) { + error("\"%s\" is not a directory", src); + return -1; + } + if (print_flag && print_flag != SFTP_PROGRESS_ONLY) + mprintf("Retrieving %s\n", src); + + if (dirattrib->flags & SSH2_FILEXFER_ATTR_PERMISSIONS) { + mode = dirattrib->perm & 01777; + tmpmode = mode | (S_IWUSR|S_IXUSR); + } else { + debug("download remote \"%s\": server " + "did not send permissions", dst); + } + + if (mkdir(dst, tmpmode) == -1 && errno != EEXIST) { + error("mkdir %s: %s", dst, strerror(errno)); + return -1; + } + + if (do_readdir(conn, src, &dir_entries) == -1) { + error("remote readdir \"%s\" failed", src); + return -1; + } + + for (i = 0; dir_entries[i] != NULL && !interrupted; i++) { + free(new_dst); + free(new_src); + + filename = dir_entries[i]->filename; + new_dst = path_append(dst, filename); + new_src = path_append(src, filename); + + if (S_ISDIR(dir_entries[i]->a.perm)) { + if (strcmp(filename, ".") == 0 || + strcmp(filename, "..") == 0) + continue; + if (download_dir_internal(conn, new_src, new_dst, + depth + 1, &(dir_entries[i]->a), preserve_flag, + print_flag, resume_flag, + fsync_flag, follow_link_flag, inplace_flag) == -1) + ret = -1; + } else if (S_ISREG(dir_entries[i]->a.perm) || + (follow_link_flag && S_ISLNK(dir_entries[i]->a.perm))) { + /* + * If this is a symlink then don't send the link's + * Attrib. do_download() will do a FXP_STAT operation + * and get the link target's attributes. + */ + if (do_download(conn, new_src, new_dst, + S_ISLNK(dir_entries[i]->a.perm) ? NULL : + &(dir_entries[i]->a), + preserve_flag, resume_flag, fsync_flag, + inplace_flag) == -1) { + error("Download of file %s to %s failed", + new_src, new_dst); + ret = -1; + } + } else + logit("download \"%s\": not a regular file", new_src); + + } + free(new_dst); + free(new_src); + + if (preserve_flag) { + if (dirattrib->flags & SSH2_FILEXFER_ATTR_ACMODTIME) { + struct timeval tv[2]; + tv[0].tv_sec = dirattrib->atime; + tv[1].tv_sec = dirattrib->mtime; + tv[0].tv_usec = tv[1].tv_usec = 0; + if (utimes(dst, tv) == -1) + error("local set times on \"%s\": %s", + dst, strerror(errno)); + } else + debug("Server did not send times for directory " + "\"%s\"", dst); + } + + if (mode != tmpmode && chmod(dst, mode) == -1) + error("local chmod directory \"%s\": %s", dst, + strerror(errno)); + + free_sftp_dirents(dir_entries); + + return ret; +} + +int +download_dir(struct sftp_conn *conn, const char *src, const char *dst, + Attrib *dirattrib, int preserve_flag, int print_flag, int resume_flag, + int fsync_flag, int follow_link_flag, int inplace_flag) +{ + char *src_canon; + int ret; + + if ((src_canon = do_realpath(conn, src)) == NULL) { + error("download \"%s\": path canonicalization failed", src); + return -1; + } + + ret = download_dir_internal(conn, src_canon, dst, 0, + dirattrib, preserve_flag, print_flag, resume_flag, fsync_flag, + follow_link_flag, inplace_flag); + free(src_canon); + return ret; +} + +int +do_upload(struct sftp_conn *conn, const char *local_path, + const char *remote_path, int preserve_flag, int resume, + int fsync_flag, int inplace_flag) +{ + int r, local_fd; + u_int openmode, id, status = SSH2_FX_OK, reordered = 0; + off_t offset, progress_counter; + u_char type, *handle, *data; + struct sshbuf *msg; + struct stat sb; + Attrib a, t, *c = NULL; + u_int32_t startid, ackid; + u_int64_t highwater = 0; + struct request *ack = NULL; + struct requests acks; + size_t handle_len; + + debug2_f("upload local \"%s\" to remote \"%s\"", + local_path, remote_path); + + TAILQ_INIT(&acks); + + if ((local_fd = open(local_path, O_RDONLY)) == -1) { + error("open local \"%s\": %s", local_path, strerror(errno)); + return(-1); + } + if (fstat(local_fd, &sb) == -1) { + error("fstat local \"%s\": %s", local_path, strerror(errno)); + close(local_fd); + return(-1); + } + if (!S_ISREG(sb.st_mode)) { + error("local \"%s\" is not a regular file", local_path); + close(local_fd); + return(-1); + } + stat_to_attrib(&sb, &a); + + a.flags &= ~SSH2_FILEXFER_ATTR_SIZE; + a.flags &= ~SSH2_FILEXFER_ATTR_UIDGID; + a.perm &= 0777; + if (!preserve_flag) + a.flags &= ~SSH2_FILEXFER_ATTR_ACMODTIME; + + if (resume) { + /* Get remote file size if it exists */ + if ((c = do_stat(conn, remote_path, 0)) == NULL) { + close(local_fd); + return -1; + } + + if ((off_t)c->size >= sb.st_size) { + error("resume \"%s\": destination file " + "same size or larger", local_path); + close(local_fd); + return -1; + } + + if (lseek(local_fd, (off_t)c->size, SEEK_SET) == -1) { + close(local_fd); + return -1; + } + } + + openmode = SSH2_FXF_WRITE|SSH2_FXF_CREAT; + if (resume) + openmode |= SSH2_FXF_APPEND; + else if (!inplace_flag) + openmode |= SSH2_FXF_TRUNC; + + /* Send open request */ + if (send_open(conn, remote_path, "dest", openmode, &a, + &handle, &handle_len) != 0) { + close(local_fd); + return -1; + } + + id = conn->msg_id; + startid = ackid = id + 1; + data = xmalloc(conn->upload_buflen); + + /* Read from local and write to remote */ + offset = progress_counter = (resume ? c->size : 0); + if (showprogress) { + start_progress_meter(progress_meter_path(local_path), + sb.st_size, &progress_counter); + } + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + for (;;) { + int len; + + /* + * Can't use atomicio here because it returns 0 on EOF, + * thus losing the last block of the file. + * Simulate an EOF on interrupt, allowing ACKs from the + * server to drain. + */ + if (interrupted || status != SSH2_FX_OK) + len = 0; + else do + len = read(local_fd, data, conn->upload_buflen); + while ((len == -1) && + (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)); + + if (len == -1) { + fatal("read local \"%s\": %s", + local_path, strerror(errno)); + } else if (len != 0) { + ack = request_enqueue(&acks, ++id, len, offset); + sshbuf_reset(msg); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_WRITE)) != 0 || + (r = sshbuf_put_u32(msg, ack->id)) != 0 || + (r = sshbuf_put_string(msg, handle, + handle_len)) != 0 || + (r = sshbuf_put_u64(msg, offset)) != 0 || + (r = sshbuf_put_string(msg, data, len)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + debug3("Sent message SSH2_FXP_WRITE I:%u O:%llu S:%u", + id, (unsigned long long)offset, len); + } else if (TAILQ_FIRST(&acks) == NULL) + break; + + if (ack == NULL) + fatal("Unexpected ACK %u", id); + + if (id == startid || len == 0 || + id - ackid >= conn->num_requests) { + u_int rid; + + sshbuf_reset(msg); + get_msg(conn, msg); + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &rid)) != 0) + fatal_fr(r, "parse"); + + if (type != SSH2_FXP_STATUS) + fatal("Expected SSH2_FXP_STATUS(%d) packet, " + "got %d", SSH2_FXP_STATUS, type); + + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse status"); + debug3("SSH2_FXP_STATUS %u", status); + + /* Find the request in our queue */ + if ((ack = request_find(&acks, rid)) == NULL) + fatal("Can't find request for ID %u", rid); + TAILQ_REMOVE(&acks, ack, tq); + debug3("In write loop, ack for %u %zu bytes at %lld", + ack->id, ack->len, (unsigned long long)ack->offset); + ++ackid; + progress_counter += ack->len; + if (!reordered && ack->offset <= highwater) + highwater = ack->offset + ack->len; + else if (!reordered && ack->offset > highwater) { + debug3_f("server reordered ACKs"); + reordered = 1; + } + free(ack); + } + offset += len; + if (offset < 0) + fatal_f("offset < 0"); + } + sshbuf_free(msg); + + if (showprogress) + stop_progress_meter(); + free(data); + + if (status != SSH2_FX_OK) { + error("write remote \"%s\": %s", remote_path, fx2txt(status)); + status = SSH2_FX_FAILURE; + } + + if (inplace_flag || (resume && (status != SSH2_FX_OK || interrupted))) { + debug("truncating at %llu", (unsigned long long)highwater); + attrib_clear(&t); + t.flags = SSH2_FILEXFER_ATTR_SIZE; + t.size = highwater; + do_fsetstat(conn, handle, handle_len, &t); + } + + if (close(local_fd) == -1) { + error("close local \"%s\": %s", local_path, strerror(errno)); + status = SSH2_FX_FAILURE; + } + + /* Override umask and utimes if asked */ + if (preserve_flag) + do_fsetstat(conn, handle, handle_len, &a); + + if (fsync_flag) + (void)do_fsync(conn, handle, handle_len); + + if (do_close(conn, handle, handle_len) != 0) + status = SSH2_FX_FAILURE; + + free(handle); + + return status == SSH2_FX_OK ? 0 : -1; +} + +static int +upload_dir_internal(struct sftp_conn *conn, const char *src, const char *dst, + int depth, int preserve_flag, int print_flag, int resume, int fsync_flag, + int follow_link_flag, int inplace_flag) +{ + int ret = 0; + DIR *dirp; + struct dirent *dp; + char *filename, *new_src = NULL, *new_dst = NULL; + struct stat sb; + Attrib a, *dirattrib; + u_int32_t saved_perm; + + debug2_f("upload local dir \"%s\" to remote \"%s\"", src, dst); + + if (depth >= MAX_DIR_DEPTH) { + error("Maximum directory depth exceeded: %d levels", depth); + return -1; + } + + if (stat(src, &sb) == -1) { + error("stat local \"%s\": %s", src, strerror(errno)); + return -1; + } + if (!S_ISDIR(sb.st_mode)) { + error("\"%s\" is not a directory", src); + return -1; + } + if (print_flag && print_flag != SFTP_PROGRESS_ONLY) + mprintf("Entering %s\n", src); + + stat_to_attrib(&sb, &a); + a.flags &= ~SSH2_FILEXFER_ATTR_SIZE; + a.flags &= ~SSH2_FILEXFER_ATTR_UIDGID; + a.perm &= 01777; + if (!preserve_flag) + a.flags &= ~SSH2_FILEXFER_ATTR_ACMODTIME; + + /* + * sftp lacks a portable status value to match errno EEXIST, + * so if we get a failure back then we must check whether + * the path already existed and is a directory. Ensure we can + * write to the directory we create for the duration of the transfer. + */ + saved_perm = a.perm; + a.perm |= (S_IWUSR|S_IXUSR); + if (do_mkdir(conn, dst, &a, 0) != 0) { + if ((dirattrib = do_stat(conn, dst, 0)) == NULL) + return -1; + if (!S_ISDIR(dirattrib->perm)) { + error("\"%s\" exists but is not a directory", dst); + return -1; + } + } + a.perm = saved_perm; + + if ((dirp = opendir(src)) == NULL) { + error("local opendir \"%s\": %s", src, strerror(errno)); + return -1; + } + + while (((dp = readdir(dirp)) != NULL) && !interrupted) { + if (dp->d_ino == 0) + continue; + free(new_dst); + free(new_src); + filename = dp->d_name; + new_dst = path_append(dst, filename); + new_src = path_append(src, filename); + + if (lstat(new_src, &sb) == -1) { + logit("local lstat \"%s\": %s", filename, + strerror(errno)); + ret = -1; + } else if (S_ISDIR(sb.st_mode)) { + if (strcmp(filename, ".") == 0 || + strcmp(filename, "..") == 0) + continue; + + if (upload_dir_internal(conn, new_src, new_dst, + depth + 1, preserve_flag, print_flag, resume, + fsync_flag, follow_link_flag, inplace_flag) == -1) + ret = -1; + } else if (S_ISREG(sb.st_mode) || + (follow_link_flag && S_ISLNK(sb.st_mode))) { + if (do_upload(conn, new_src, new_dst, + preserve_flag, resume, fsync_flag, + inplace_flag) == -1) { + error("upload \"%s\" to \"%s\" failed", + new_src, new_dst); + ret = -1; + } + } else + logit("%s: not a regular file", filename); + } + free(new_dst); + free(new_src); + + do_setstat(conn, dst, &a); + + (void) closedir(dirp); + return ret; +} + +int +upload_dir(struct sftp_conn *conn, const char *src, const char *dst, + int preserve_flag, int print_flag, int resume, int fsync_flag, + int follow_link_flag, int inplace_flag) +{ + char *dst_canon; + int ret; + + if ((dst_canon = do_realpath(conn, dst)) == NULL) { + error("upload \"%s\": path canonicalization failed", dst); + return -1; + } + + ret = upload_dir_internal(conn, src, dst_canon, 0, preserve_flag, + print_flag, resume, fsync_flag, follow_link_flag, inplace_flag); + + free(dst_canon); + return ret; +} + +static void +handle_dest_replies(struct sftp_conn *to, const char *to_path, int synchronous, + u_int *nreqsp, u_int *write_errorp) +{ + struct sshbuf *msg; + u_char type; + u_int id, status; + int r; + struct pollfd pfd; + + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + + /* Try to eat replies from the upload side */ + while (*nreqsp > 0) { + debug3_f("%u outstanding replies", *nreqsp); + if (!synchronous) { + /* Bail out if no data is ready to be read */ + pfd.fd = to->fd_in; + pfd.events = POLLIN; + if ((r = poll(&pfd, 1, 0)) == -1) { + if (errno == EINTR) + break; + fatal_f("poll: %s", strerror(errno)); + } else if (r == 0) + break; /* fd not ready */ + } + sshbuf_reset(msg); + get_msg(to, msg); + + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "dest parse"); + debug3("Received dest reply T:%u I:%u R:%u", type, id, *nreqsp); + if (type != SSH2_FXP_STATUS) { + fatal_f("Expected SSH2_FXP_STATUS(%d) packet, got %d", + SSH2_FXP_STATUS, type); + } + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse dest status"); + debug3("dest SSH2_FXP_STATUS %u", status); + if (status != SSH2_FX_OK) { + /* record first error */ + if (*write_errorp == 0) + *write_errorp = status; + } + /* + * XXX this doesn't do full reply matching like do_upload and + * so cannot gracefully truncate terminated uploads at a + * high-water mark. ATM the only caller of this function (scp) + * doesn't support transfer resumption, so this doesn't matter + * a whole lot. + * + * To be safe, do_crossload truncates the destination file to + * zero length on upload failure, since we can't trust the + * server not to have reordered replies that could have + * inserted holes where none existed in the source file. + * + * XXX we could get a more accutate progress bar if we updated + * the counter based on the reply from the destination... + */ + (*nreqsp)--; + } + debug3_f("done: %u outstanding replies", *nreqsp); + sshbuf_free(msg); +} + +int +do_crossload(struct sftp_conn *from, struct sftp_conn *to, + const char *from_path, const char *to_path, + Attrib *a, int preserve_flag) +{ + struct sshbuf *msg; + int write_error, read_error, r; + u_int64_t offset = 0, size; + u_int id, buflen, num_req, max_req, status = SSH2_FX_OK; + u_int num_upload_req; + off_t progress_counter; + u_char *from_handle, *to_handle; + size_t from_handle_len, to_handle_len; + struct requests requests; + struct request *req; + u_char type; + + debug2_f("crossload src \"%s\" to dst \"%s\"", from_path, to_path); + + TAILQ_INIT(&requests); + + if (a == NULL && (a = do_stat(from, from_path, 0)) == NULL) + return -1; + + if ((a->flags & SSH2_FILEXFER_ATTR_PERMISSIONS) && + (!S_ISREG(a->perm))) { + error("download \"%s\": not a regular file", from_path); + return(-1); + } + if (a->flags & SSH2_FILEXFER_ATTR_SIZE) + size = a->size; + else + size = 0; + + buflen = from->download_buflen; + if (buflen > to->upload_buflen) + buflen = to->upload_buflen; + + /* Send open request to read side */ + if (send_open(from, from_path, "origin", SSH2_FXF_READ, NULL, + &from_handle, &from_handle_len) != 0) + return -1; + + /* Send open request to write side */ + a->flags &= ~SSH2_FILEXFER_ATTR_SIZE; + a->flags &= ~SSH2_FILEXFER_ATTR_UIDGID; + a->perm &= 0777; + if (!preserve_flag) + a->flags &= ~SSH2_FILEXFER_ATTR_ACMODTIME; + if (send_open(to, to_path, "dest", + SSH2_FXF_WRITE|SSH2_FXF_CREAT|SSH2_FXF_TRUNC, a, + &to_handle, &to_handle_len) != 0) { + do_close(from, from_handle, from_handle_len); + return -1; + } + + /* Read from remote "from" and write to remote "to" */ + offset = 0; + write_error = read_error = num_req = num_upload_req = 0; + max_req = 1; + progress_counter = 0; + + if (showprogress && size != 0) { + start_progress_meter(progress_meter_path(from_path), + size, &progress_counter); + } + if ((msg = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + while (num_req > 0 || max_req > 0) { + u_char *data; + size_t len; + + /* + * Simulate EOF on interrupt: stop sending new requests and + * allow outstanding requests to drain gracefully + */ + if (interrupted) { + if (num_req == 0) /* If we haven't started yet... */ + break; + max_req = 0; + } + + /* Send some more requests */ + while (num_req < max_req) { + debug3("Request range %llu -> %llu (%d/%d)", + (unsigned long long)offset, + (unsigned long long)offset + buflen - 1, + num_req, max_req); + req = request_enqueue(&requests, from->msg_id++, + buflen, offset); + offset += buflen; + num_req++; + send_read_request(from, req->id, req->offset, + req->len, from_handle, from_handle_len); + } + + /* Try to eat replies from the upload side (nonblocking) */ + handle_dest_replies(to, to_path, 0, + &num_upload_req, &write_error); + + sshbuf_reset(msg); + get_msg(from, msg); + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + debug3("Received origin reply T:%u I:%u R:%d", + type, id, max_req); + + /* Find the request in our queue */ + if ((req = request_find(&requests, id)) == NULL) + fatal("Unexpected reply %u", id); + + switch (type) { + case SSH2_FXP_STATUS: + if ((r = sshbuf_get_u32(msg, &status)) != 0) + fatal_fr(r, "parse status"); + if (status != SSH2_FX_EOF) + read_error = 1; + max_req = 0; + TAILQ_REMOVE(&requests, req, tq); + free(req); + num_req--; + break; + case SSH2_FXP_DATA: + if ((r = sshbuf_get_string(msg, &data, &len)) != 0) + fatal_fr(r, "parse data"); + debug3("Received data %llu -> %llu", + (unsigned long long)req->offset, + (unsigned long long)req->offset + len - 1); + if (len > req->len) + fatal("Received more data than asked for " + "%zu > %zu", len, req->len); + + /* Write this chunk out to the destination */ + sshbuf_reset(msg); + if ((r = sshbuf_put_u8(msg, SSH2_FXP_WRITE)) != 0 || + (r = sshbuf_put_u32(msg, to->msg_id++)) != 0 || + (r = sshbuf_put_string(msg, to_handle, + to_handle_len)) != 0 || + (r = sshbuf_put_u64(msg, req->offset)) != 0 || + (r = sshbuf_put_string(msg, data, len)) != 0) + fatal_fr(r, "compose write"); + send_msg(to, msg); + debug3("Sent message SSH2_FXP_WRITE I:%u O:%llu S:%zu", + id, (unsigned long long)offset, len); + num_upload_req++; + progress_counter += len; + free(data); + + if (len == req->len) { + TAILQ_REMOVE(&requests, req, tq); + free(req); + num_req--; + } else { + /* Resend the request for the missing data */ + debug3("Short data block, re-requesting " + "%llu -> %llu (%2d)", + (unsigned long long)req->offset + len, + (unsigned long long)req->offset + + req->len - 1, num_req); + req->id = from->msg_id++; + req->len -= len; + req->offset += len; + send_read_request(from, req->id, + req->offset, req->len, + from_handle, from_handle_len); + /* Reduce the request size */ + if (len < buflen) + buflen = MAXIMUM(MIN_READ_SIZE, len); + } + if (max_req > 0) { /* max_req = 0 iff EOF received */ + if (size > 0 && offset > size) { + /* Only one request at a time + * after the expected EOF */ + debug3("Finish at %llu (%2d)", + (unsigned long long)offset, + num_req); + max_req = 1; + } else if (max_req < from->num_requests) { + ++max_req; + } + } + break; + default: + fatal("Expected SSH2_FXP_DATA(%u) packet, got %u", + SSH2_FXP_DATA, type); + } + } + + if (showprogress && size) + stop_progress_meter(); + + /* Drain replies from the server (blocking) */ + debug3_f("waiting for %u replies from destination", num_upload_req); + handle_dest_replies(to, to_path, 1, &num_upload_req, &write_error); + + /* Sanity check */ + if (TAILQ_FIRST(&requests) != NULL) + fatal("Transfer complete, but requests still in queue"); + /* Truncate at 0 length on interrupt or error to avoid holes at dest */ + if (read_error || write_error || interrupted) { + debug("truncating \"%s\" at 0", to_path); + do_close(to, to_handle, to_handle_len); + free(to_handle); + if (send_open(to, to_path, "dest", + SSH2_FXF_WRITE|SSH2_FXF_CREAT|SSH2_FXF_TRUNC, a, + &to_handle, &to_handle_len) != 0) { + error("dest truncate \"%s\" failed", to_path); + to_handle = NULL; + } + } + if (read_error) { + error("read origin \"%s\": %s", from_path, fx2txt(status)); + status = -1; + do_close(from, from_handle, from_handle_len); + if (to_handle != NULL) + do_close(to, to_handle, to_handle_len); + } else if (write_error) { + error("write dest \"%s\": %s", to_path, fx2txt(write_error)); + status = SSH2_FX_FAILURE; + do_close(from, from_handle, from_handle_len); + if (to_handle != NULL) + do_close(to, to_handle, to_handle_len); + } else { + if (do_close(from, from_handle, from_handle_len) != 0 || + interrupted) + status = -1; + else + status = SSH2_FX_OK; + if (to_handle != NULL) { + /* Need to resend utimes after write */ + if (preserve_flag) + do_fsetstat(to, to_handle, to_handle_len, a); + do_close(to, to_handle, to_handle_len); + } + } + sshbuf_free(msg); + free(from_handle); + free(to_handle); + + return status == SSH2_FX_OK ? 0 : -1; +} + +static int +crossload_dir_internal(struct sftp_conn *from, struct sftp_conn *to, + const char *from_path, const char *to_path, + int depth, Attrib *dirattrib, int preserve_flag, int print_flag, + int follow_link_flag) +{ + int i, ret = 0; + SFTP_DIRENT **dir_entries; + char *filename, *new_from_path = NULL, *new_to_path = NULL; + mode_t mode = 0777; + Attrib curdir; + + debug2_f("crossload dir src \"%s\" to dst \"%s\"", from_path, to_path); + + if (depth >= MAX_DIR_DEPTH) { + error("Maximum directory depth exceeded: %d levels", depth); + return -1; + } + + if (dirattrib == NULL && + (dirattrib = do_stat(from, from_path, 1)) == NULL) { + error("stat remote \"%s\" failed", from_path); + return -1; + } + if (!S_ISDIR(dirattrib->perm)) { + error("\"%s\" is not a directory", from_path); + return -1; + } + if (print_flag && print_flag != SFTP_PROGRESS_ONLY) + mprintf("Retrieving %s\n", from_path); + + curdir = *dirattrib; /* dirattrib will be clobbered */ + curdir.flags &= ~SSH2_FILEXFER_ATTR_SIZE; + curdir.flags &= ~SSH2_FILEXFER_ATTR_UIDGID; + if ((curdir.flags & SSH2_FILEXFER_ATTR_PERMISSIONS) == 0) { + debug("Origin did not send permissions for " + "directory \"%s\"", to_path); + curdir.perm = S_IWUSR|S_IXUSR; + curdir.flags |= SSH2_FILEXFER_ATTR_PERMISSIONS; + } + /* We need to be able to write to the directory while we transfer it */ + mode = curdir.perm & 01777; + curdir.perm = mode | (S_IWUSR|S_IXUSR); + + /* + * sftp lacks a portable status value to match errno EEXIST, + * so if we get a failure back then we must check whether + * the path already existed and is a directory. Ensure we can + * write to the directory we create for the duration of the transfer. + */ + if (do_mkdir(to, to_path, &curdir, 0) != 0) { + if ((dirattrib = do_stat(to, to_path, 0)) == NULL) + return -1; + if (!S_ISDIR(dirattrib->perm)) { + error("\"%s\" exists but is not a directory", to_path); + return -1; + } + } + curdir.perm = mode; + + if (do_readdir(from, from_path, &dir_entries) == -1) { + error("origin readdir \"%s\" failed", from_path); + return -1; + } + + for (i = 0; dir_entries[i] != NULL && !interrupted; i++) { + free(new_from_path); + free(new_to_path); + + filename = dir_entries[i]->filename; + new_from_path = path_append(from_path, filename); + new_to_path = path_append(to_path, filename); + + if (S_ISDIR(dir_entries[i]->a.perm)) { + if (strcmp(filename, ".") == 0 || + strcmp(filename, "..") == 0) + continue; + if (crossload_dir_internal(from, to, + new_from_path, new_to_path, + depth + 1, &(dir_entries[i]->a), preserve_flag, + print_flag, follow_link_flag) == -1) + ret = -1; + } else if (S_ISREG(dir_entries[i]->a.perm) || + (follow_link_flag && S_ISLNK(dir_entries[i]->a.perm))) { + /* + * If this is a symlink then don't send the link's + * Attrib. do_download() will do a FXP_STAT operation + * and get the link target's attributes. + */ + if (do_crossload(from, to, new_from_path, new_to_path, + S_ISLNK(dir_entries[i]->a.perm) ? NULL : + &(dir_entries[i]->a), preserve_flag) == -1) { + error("crossload \"%s\" to \"%s\" failed", + new_from_path, new_to_path); + ret = -1; + } + } else { + logit("origin \"%s\": not a regular file", + new_from_path); + } + } + free(new_to_path); + free(new_from_path); + + do_setstat(to, to_path, &curdir); + + free_sftp_dirents(dir_entries); + + return ret; +} + +int +crossload_dir(struct sftp_conn *from, struct sftp_conn *to, + const char *from_path, const char *to_path, + Attrib *dirattrib, int preserve_flag, int print_flag, int follow_link_flag) +{ + char *from_path_canon; + int ret; + + if ((from_path_canon = do_realpath(from, from_path)) == NULL) { + error("crossload \"%s\": path canonicalization failed", + from_path); + return -1; + } + + ret = crossload_dir_internal(from, to, from_path_canon, to_path, 0, + dirattrib, preserve_flag, print_flag, follow_link_flag); + free(from_path_canon); + return ret; +} + +int +can_get_users_groups_by_id(struct sftp_conn *conn) +{ + return (conn->exts & SFTP_EXT_GETUSERSGROUPS_BY_ID) != 0; +} + +int +do_get_users_groups_by_id(struct sftp_conn *conn, + const u_int *uids, u_int nuids, + const u_int *gids, u_int ngids, + char ***usernamesp, char ***groupnamesp) +{ + struct sshbuf *msg, *uidbuf, *gidbuf; + u_int i, expected_id, id; + char *name, **usernames = NULL, **groupnames = NULL; + u_char type; + int r; + + *usernamesp = *groupnamesp = NULL; + if (!can_get_users_groups_by_id(conn)) + return SSH_ERR_FEATURE_UNSUPPORTED; + + if ((msg = sshbuf_new()) == NULL || + (uidbuf = sshbuf_new()) == NULL || + (gidbuf = sshbuf_new()) == NULL) + fatal_f("sshbuf_new failed"); + expected_id = id = conn->msg_id++; + debug2("Sending SSH2_FXP_EXTENDED(users-groups-by-id@openssh.com)"); + for (i = 0; i < nuids; i++) { + if ((r = sshbuf_put_u32(uidbuf, uids[i])) != 0) + fatal_fr(r, "compose uids"); + } + for (i = 0; i < ngids; i++) { + if ((r = sshbuf_put_u32(gidbuf, gids[i])) != 0) + fatal_fr(r, "compose gids"); + } + if ((r = sshbuf_put_u8(msg, SSH2_FXP_EXTENDED)) != 0 || + (r = sshbuf_put_u32(msg, id)) != 0 || + (r = sshbuf_put_cstring(msg, + "users-groups-by-id@openssh.com")) != 0 || + (r = sshbuf_put_stringb(msg, uidbuf)) != 0 || + (r = sshbuf_put_stringb(msg, gidbuf)) != 0) + fatal_fr(r, "compose"); + send_msg(conn, msg); + get_msg(conn, msg); + if ((r = sshbuf_get_u8(msg, &type)) != 0 || + (r = sshbuf_get_u32(msg, &id)) != 0) + fatal_fr(r, "parse"); + if (id != expected_id) + fatal("ID mismatch (%u != %u)", id, expected_id); + if (type == SSH2_FXP_STATUS) { + u_int status; + char *errmsg; + + if ((r = sshbuf_get_u32(msg, &status)) != 0 || + (r = sshbuf_get_cstring(msg, &errmsg, NULL)) != 0) + fatal_fr(r, "parse status"); + error("users-groups-by-id %s", + *errmsg == '\0' ? fx2txt(status) : errmsg); + free(errmsg); + sshbuf_free(msg); + sshbuf_free(uidbuf); + sshbuf_free(gidbuf); + return -1; + } else if (type != SSH2_FXP_EXTENDED_REPLY) + fatal("Expected SSH2_FXP_EXTENDED_REPLY(%u) packet, got %u", + SSH2_FXP_EXTENDED_REPLY, type); + + /* reuse */ + sshbuf_free(uidbuf); + sshbuf_free(gidbuf); + uidbuf = gidbuf = NULL; + if ((r = sshbuf_froms(msg, &uidbuf)) != 0 || + (r = sshbuf_froms(msg, &gidbuf)) != 0) + fatal_fr(r, "parse response"); + if (nuids > 0) { + usernames = xcalloc(nuids, sizeof(*usernames)); + for (i = 0; i < nuids; i++) { + if ((r = sshbuf_get_cstring(uidbuf, &name, NULL)) != 0) + fatal_fr(r, "parse user name"); + /* Handle unresolved names */ + if (*name == '\0') { + free(name); + name = NULL; + } + usernames[i] = name; + } + } + if (ngids > 0) { + groupnames = xcalloc(ngids, sizeof(*groupnames)); + for (i = 0; i < ngids; i++) { + if ((r = sshbuf_get_cstring(gidbuf, &name, NULL)) != 0) + fatal_fr(r, "parse user name"); + /* Handle unresolved names */ + if (*name == '\0') { + free(name); + name = NULL; + } + groupnames[i] = name; + } + } + if (sshbuf_len(uidbuf) != 0) + fatal_f("unexpected extra username data"); + if (sshbuf_len(gidbuf) != 0) + fatal_f("unexpected extra groupname data"); + sshbuf_free(uidbuf); + sshbuf_free(gidbuf); + sshbuf_free(msg); + /* success */ + *usernamesp = usernames; + *groupnamesp = groupnames; + return 0; +} + +char * +path_append(const char *p1, const char *p2) +{ + char *ret; + size_t len = strlen(p1) + strlen(p2) + 2; + + ret = xmalloc(len); + strlcpy(ret, p1, len); + if (p1[0] != '\0' && p1[strlen(p1) - 1] != '/') + strlcat(ret, "/", len); + strlcat(ret, p2, len); + + return(ret); +} + +char * +make_absolute(char *p, const char *pwd) +{ + char *abs_str; + + /* Derelativise */ + if (p && !path_absolute(p)) { + abs_str = path_append(pwd, p); + free(p); + return(abs_str); + } else + return(p); +} + +int +remote_is_dir(struct sftp_conn *conn, const char *path) +{ + Attrib *a; + + /* XXX: report errors? */ + if ((a = do_stat(conn, path, 1)) == NULL) + return(0); + if (!(a->flags & SSH2_FILEXFER_ATTR_PERMISSIONS)) + return(0); + return(S_ISDIR(a->perm)); +} + + +int +local_is_dir(const char *path) +{ + struct stat sb; + + /* XXX: report errors? */ + if (stat(path, &sb) == -1) + return(0); + + return(S_ISDIR(sb.st_mode)); +} + +/* Check whether path returned from glob(..., GLOB_MARK, ...) is a directory */ +int +globpath_is_dir(const char *pathname) +{ + size_t l = strlen(pathname); + + return l > 0 && pathname[l - 1] == '/'; +} + |