-/* $OpenBSD: sftp-client.c,v 1.82 2008/04/18 12:32:11 djm Exp $ */
+/* $OpenBSD: sftp-client.c,v 1.90 2009/10/11 10:41:26 dtucker Exp $ */
/*
* Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
*
#endif
#include <sys/uio.h>
+#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
/* 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
+
struct sftp_conn {
int fd_in;
int fd_out;
u_int exts;
};
+static char *
+get_handle(int fd, u_int expected_id, u_int *len, const char *errfmt, ...)
+ __attribute__((format(printf, 4, 5)));
+
static void
send_msg(int fd, Buffer *m)
{
}
static char *
-get_handle(int fd, u_int expected_id, u_int *len)
+get_handle(int fd, u_int expected_id, u_int *len, const char *errfmt, ...)
{
Buffer msg;
u_int type, id;
- char *handle;
+ char *handle, errmsg[256];
+ va_list args;
+ int status;
+
+ va_start(args, errfmt);
+ if (errfmt != NULL)
+ vsnprintf(errmsg, sizeof(errmsg), errfmt, args);
+ va_end(args);
buffer_init(&msg);
get_msg(fd, &msg);
id = buffer_get_int(&msg);
if (id != expected_id)
- fatal("ID mismatch (%u != %u)", id, expected_id);
+ fatal("%s: ID mismatch (%u != %u)",
+ errfmt == NULL ? __func__ : errmsg, id, expected_id);
if (type == SSH2_FXP_STATUS) {
- int status = buffer_get_int(&msg);
-
- error("Couldn't get handle: %s", fx2txt(status));
+ status = buffer_get_int(&msg);
+ if (errfmt != NULL)
+ error("%s: %s", errmsg, fx2txt(status));
buffer_free(&msg);
return(NULL);
} else if (type != SSH2_FXP_HANDLE)
- fatal("Expected SSH2_FXP_HANDLE(%u) packet, got %u",
- SSH2_FXP_HANDLE, type);
+ fatal("%s: Expected SSH2_FXP_HANDLE(%u) packet, got %u",
+ errfmt == NULL ? __func__ : errmsg, SSH2_FXP_HANDLE, type);
handle = buffer_get_string(&msg, len);
buffer_free(&msg);
return(a);
}
-#ifdef USE_STATVFS
static int
-get_decode_statvfs(int fd, struct statvfs *st, u_int expected_id, int quiet)
+get_decode_statvfs(int fd, struct sftp_statvfs *st, u_int expected_id,
+ int quiet)
{
Buffer msg;
u_int type, id, flag;
}
bzero(st, sizeof(*st));
- st->f_bsize = buffer_get_int(&msg);
- st->f_frsize = buffer_get_int(&msg);
+ st->f_bsize = buffer_get_int64(&msg);
+ st->f_frsize = buffer_get_int64(&msg);
st->f_blocks = buffer_get_int64(&msg);
st->f_bfree = buffer_get_int64(&msg);
st->f_bavail = buffer_get_int64(&msg);
st->f_files = buffer_get_int64(&msg);
st->f_ffree = buffer_get_int64(&msg);
st->f_favail = buffer_get_int64(&msg);
- st->f_fsid = buffer_get_int(&msg);
- flag = buffer_get_int(&msg);
- st->f_namemax = buffer_get_int(&msg);
+ st->f_fsid = buffer_get_int64(&msg);
+ flag = buffer_get_int64(&msg);
+ st->f_namemax = buffer_get_int64(&msg);
st->f_flag = (flag & SSH2_FXE_STATVFS_ST_RDONLY) ? ST_RDONLY : 0;
st->f_flag |= (flag & SSH2_FXE_STATVFS_ST_NOSUID) ? ST_NOSUID : 0;
return 0;
}
-#endif
struct sftp_conn *
do_init(int fd_in, int fd_out, u_int transfer_buflen, u_int num_requests)
while (buffer_len(&msg) > 0) {
char *name = buffer_get_string(&msg, NULL);
char *value = buffer_get_string(&msg, NULL);
+ int known = 0;
- debug2("Init extension: \"%s\"", name);
if (strcmp(name, "posix-rename@openssh.com") == 0 &&
- strcmp(value, "1") == 0)
+ strcmp(value, "1") == 0) {
exts |= SFTP_EXT_POSIX_RENAME;
- if (strcmp(name, "statvfs@openssh.com") == 0 &&
- strcmp(value, "1") == 0)
+ known = 1;
+ } else if (strcmp(name, "statvfs@openssh.com") == 0 &&
+ strcmp(value, "2") == 0) {
exts |= SFTP_EXT_STATVFS;
- if (strcmp(name, "fstatvfs@openssh.com") == 0 &&
- strcmp(value, "1") == 0)
+ known = 1;
+ } if (strcmp(name, "fstatvfs@openssh.com") == 0 &&
+ strcmp(value, "2") == 0) {
exts |= SFTP_EXT_FSTATVFS;
+ known = 1;
+ }
+ if (known) {
+ debug2("Server supports extension \"%s\" revision %s",
+ name, value);
+ } else {
+ debug2("Unrecognised server extension \"%s\"", name);
+ }
xfree(name);
xfree(value);
}
buffer_clear(&msg);
- handle = get_handle(conn->fd_in, id, &handle_len);
+ handle = get_handle(conn->fd_in, id, &handle_len,
+ "remote readdir(\"%s\")", path);
if (handle == NULL)
return(-1);
if (printflag)
printf("%s\n", longname);
+ /*
+ * Directory entries should never contain '/'
+ * These can be used to attack recursive ops
+ * (e.g. send '../../../../etc/passwd')
+ */
+ if (strchr(filename, '/') != NULL) {
+ error("Server sent suspect path \"%s\" "
+ "during readdir of \"%s\"", filename, path);
+ goto next;
+ }
+
if (dir) {
*dir = xrealloc(*dir, ents + 2, sizeof(**dir));
(*dir)[ents] = xmalloc(sizeof(***dir));
memcpy(&(*dir)[ents]->a, a, sizeof(*a));
(*dir)[++ents] = NULL;
}
-
+ next:
xfree(filename);
xfree(longname);
}
}
int
-do_mkdir(struct sftp_conn *conn, char *path, Attrib *a)
+do_mkdir(struct sftp_conn *conn, char *path, Attrib *a, int printflag)
{
u_int status, id;
strlen(path), a);
status = get_status(conn->fd_in, id);
- if (status != SSH2_FX_OK)
+ if (status != SSH2_FX_OK && printflag)
error("Couldn't create directory: %s", fx2txt(status));
return(status);
}
#endif
-#ifdef USE_STATVFS
int
-do_statvfs(struct sftp_conn *conn, const char *path, struct statvfs *st,
+do_statvfs(struct sftp_conn *conn, const char *path, struct sftp_statvfs *st,
int quiet)
{
Buffer msg;
return get_decode_statvfs(conn->fd_in, st, id, quiet);
}
-#endif
#ifdef notyet
int
do_fstatvfs(struct sftp_conn *conn, const char *handle, u_int handle_len,
- struct statvfs *st, int quiet)
+ struct sftp_statvfs *st, int quiet)
{
Buffer msg;
u_int id;
int
do_download(struct sftp_conn *conn, char *remote_path, char *local_path,
- int pflag)
+ Attrib *a, int pflag)
{
- Attrib junk, *a;
+ Attrib junk;
Buffer msg;
char *handle;
int local_fd, status = 0, write_error;
TAILQ_INIT(&requests);
- a = do_stat(conn, remote_path, 0);
- if (a == NULL)
- return(-1);
+ if (a == NULL && (a = do_stat(conn, remote_path, 0)) == NULL)
+ return -1;
- /* XXX: should we preserve set[ug]id? */
+ /* 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
send_msg(conn->fd_out, &msg);
debug3("Sent message SSH2_FXP_OPEN I:%u P:%s", id, remote_path);
- handle = get_handle(conn->fd_in, id, &handle_len);
+ handle = get_handle(conn->fd_in, id, &handle_len,
+ "remote open(\"%s\")", remote_path);
if (handle == NULL) {
buffer_free(&msg);
return(-1);
return(status);
}
+static int
+download_dir_internal(struct sftp_conn *conn, char *src, char *dst,
+ Attrib *dirattrib, int pflag, int printflag, int depth)
+{
+ int i, ret = 0;
+ SFTP_DIRENT **dir_entries;
+ char *filename, *new_src, *new_dst;
+ mode_t mode = 0777;
+
+ if (depth >= MAX_DIR_DEPTH) {
+ error("Maximum directory depth exceeded: %d levels", depth);
+ return -1;
+ }
+
+ if (dirattrib == NULL &&
+ (dirattrib = do_stat(conn, src, 1)) == NULL) {
+ error("Unable to stat remote directory \"%s\"", src);
+ return -1;
+ }
+ if (!S_ISDIR(dirattrib->perm)) {
+ error("\"%s\" is not a directory", src);
+ return -1;
+ }
+ if (printflag)
+ printf("Retrieving %s\n", src);
+
+ if (dirattrib->flags & SSH2_FILEXFER_ATTR_PERMISSIONS)
+ mode = dirattrib->perm & 01777;
+ else {
+ debug("Server did not send permissions for "
+ "directory \"%s\"", dst);
+ }
+
+ if (mkdir(dst, mode) == -1 && errno != EEXIST) {
+ error("mkdir %s: %s", dst, strerror(errno));
+ return -1;
+ }
+
+ if (do_readdir(conn, src, &dir_entries) == -1) {
+ error("%s: Failed to get directory contents", src);
+ return -1;
+ }
+
+ for (i = 0; dir_entries[i] != NULL && !interrupted; i++) {
+ 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,
+ &(dir_entries[i]->a), pflag, printflag,
+ depth + 1) == -1)
+ ret = -1;
+ } else if (S_ISREG(dir_entries[i]->a.perm) ) {
+ if (do_download(conn, new_src, new_dst,
+ &(dir_entries[i]->a), pflag) == -1) {
+ error("Download of file %s to %s failed",
+ new_src, new_dst);
+ ret = -1;
+ }
+ } else
+ logit("%s: not a regular file\n", new_src);
+
+ xfree(new_dst);
+ xfree(new_src);
+ }
+
+ if (pflag) {
+ 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("Can't set times on \"%s\": %s",
+ dst, strerror(errno));
+ } else
+ debug("Server did not send times for directory "
+ "\"%s\"", dst);
+ }
+
+ free_sftp_dirents(dir_entries);
+
+ return ret;
+}
+
+int
+download_dir(struct sftp_conn *conn, char *src, char *dst,
+ Attrib *dirattrib, int pflag, int printflag)
+{
+ char *src_canon;
+ int ret;
+
+ if ((src_canon = do_realpath(conn, src)) == NULL) {
+ error("Unable to canonicalise path \"%s\"", src);
+ return -1;
+ }
+
+ ret = download_dir_internal(conn, src_canon, dst,
+ dirattrib, pflag, printflag, 0);
+ xfree(src_canon);
+ return ret;
+}
+
int
do_upload(struct sftp_conn *conn, char *local_path, char *remote_path,
int pflag)
buffer_clear(&msg);
- handle = get_handle(conn->fd_in, id, &handle_len);
+ handle = get_handle(conn->fd_in, id, &handle_len,
+ "remote open(\"%s\")", remote_path);
if (handle == NULL) {
close(local_fd);
buffer_free(&msg);
len = 0;
else do
len = read(local_fd, data, conn->transfer_buflen);
- while ((len == -1) && (errno == EINTR || errno == EAGAIN));
+ while ((len == -1) &&
+ (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK));
if (len == -1)
fatal("Couldn't read from \"%s\": %s", local_path,
return status;
}
+
+static int
+upload_dir_internal(struct sftp_conn *conn, char *src, char *dst,
+ int pflag, int printflag, int depth)
+{
+ int ret = 0, status;
+ DIR *dirp;
+ struct dirent *dp;
+ char *filename, *new_src, *new_dst;
+ struct stat sb;
+ Attrib a;
+
+ if (depth >= MAX_DIR_DEPTH) {
+ error("Maximum directory depth exceeded: %d levels", depth);
+ return -1;
+ }
+
+ if (stat(src, &sb) == -1) {
+ error("Couldn't stat directory \"%s\": %s",
+ src, strerror(errno));
+ return -1;
+ }
+ if (!S_ISDIR(sb.st_mode)) {
+ error("\"%s\" is not a directory", src);
+ return -1;
+ }
+ if (printflag)
+ printf("Entering %s\n", src);
+
+ attrib_clear(&a);
+ stat_to_attrib(&sb, &a);
+ a.flags &= ~SSH2_FILEXFER_ATTR_SIZE;
+ a.flags &= ~SSH2_FILEXFER_ATTR_UIDGID;
+ a.perm &= 01777;
+ if (!pflag)
+ a.flags &= ~SSH2_FILEXFER_ATTR_ACMODTIME;
+
+ status = do_mkdir(conn, dst, &a, 0);
+ /*
+ * we lack a portable status for errno EEXIST,
+ * so if we get a SSH2_FX_FAILURE back we must check
+ * if it was created successfully.
+ */
+ if (status != SSH2_FX_OK) {
+ if (status != SSH2_FX_FAILURE)
+ return -1;
+ if (do_stat(conn, dst, 0) == NULL)
+ return -1;
+ }
+
+ if ((dirp = opendir(src)) == NULL) {
+ error("Failed to open dir \"%s\": %s", src, strerror(errno));
+ return -1;
+ }
+
+ while (((dp = readdir(dirp)) != NULL) && !interrupted) {
+ if (dp->d_ino == 0)
+ continue;
+ filename = dp->d_name;
+ new_dst = path_append(dst, filename);
+ new_src = path_append(src, filename);
+
+ if (lstat(new_src, &sb) == -1) {
+ logit("%s: lstat failed: %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,
+ pflag, depth + 1, printflag) == -1)
+ ret = -1;
+ } else if (S_ISREG(sb.st_mode)) {
+ if (do_upload(conn, new_src, new_dst, pflag) == -1) {
+ error("Uploading of file %s to %s failed!",
+ new_src, new_dst);
+ ret = -1;
+ }
+ } else
+ logit("%s: not a regular file\n", filename);
+ xfree(new_dst);
+ xfree(new_src);
+ }
+
+ do_setstat(conn, dst, &a);
+
+ (void) closedir(dirp);
+ return ret;
+}
+
+int
+upload_dir(struct sftp_conn *conn, char *src, char *dst, int printflag,
+ int pflag)
+{
+ char *dst_canon;
+ int ret;
+
+ if ((dst_canon = do_realpath(conn, dst)) == NULL) {
+ error("Unable to canonicalise path \"%s\"", dst);
+ return -1;
+ }
+
+ ret = upload_dir_internal(conn, src, dst_canon, pflag, printflag, 0);
+ xfree(dst_canon);
+ return ret;
+}
+
+char *
+path_append(char *p1, 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);
+}
+