- djm@cvs.openbsd.org 2009/08/18 18:36:21
     [sftp-client.h sftp.1 sftp-client.c sftp.c]
     recursive transfer support for get/put and on the commandline
     work mostly by carlosvsilvapt@gmail.com for the Google Summer of Code
     with some tweaks by me; "go for it" deraadt@
diff --git a/sftp-client.c b/sftp-client.c
index 14c172d..cc4a5b1 100644
--- a/sftp-client.c
+++ b/sftp-client.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp-client.c,v 1.88 2009/08/14 18:17:49 djm Exp $ */
+/* $OpenBSD: sftp-client.c,v 1.89 2009/08/18 18:36:20 djm Exp $ */
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
  *
@@ -36,6 +36,7 @@
 #endif
 #include <sys/uio.h>
 
+#include <dirent.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <signal.h>
@@ -61,6 +62,9 @@
 /* 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;
@@ -497,6 +501,17 @@
 			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));
@@ -505,7 +520,7 @@
 				memcpy(&(*dir)[ents]->a, a, sizeof(*a));
 				(*dir)[++ents] = NULL;
 			}
-
+ next:
 			xfree(filename);
 			xfree(longname);
 		}
@@ -560,7 +575,7 @@
 }
 
 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;
 
@@ -569,7 +584,7 @@
 	    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);
@@ -908,9 +923,9 @@
 
 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;
@@ -929,9 +944,8 @@
 
 	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;
 
 	/* Do not preserve set[ug]id here, as we do not preserve ownership */
 	if (a->flags & SSH2_FILEXFER_ATTR_PERMISSIONS)
@@ -1146,6 +1160,114 @@
 	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)
@@ -1328,3 +1450,123 @@
 
 	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 (S_ISDIR(DTTOIF(dp->d_type))) {
+			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(DTTOIF(dp->d_type)) ) {
+			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);
+}
+