- 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/ChangeLog b/ChangeLog
index 60eae3a..2fedecc 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -32,6 +32,11 @@
    - dtucker@cvs.openbsd.org 2009/08/16 23:29:26
      [sshd_config.5]
      Add PubkeyAuthentication to the list allowed in a Match block (bz #1577)
+   - 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@
 
 20091002
  - (djm) [Makefile.in] Mention readconf.o in ssh-keysign's make deps.
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);
+}
+
diff --git a/sftp-client.h b/sftp-client.h
index edb4679..1d08c40 100644
--- a/sftp-client.h
+++ b/sftp-client.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp-client.h,v 1.17 2008/06/08 20:15:29 dtucker Exp $ */
+/* $OpenBSD: sftp-client.h,v 1.18 2009/08/18 18:36:20 djm Exp $ */
 
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
@@ -68,7 +68,7 @@
 int do_rm(struct sftp_conn *, char *);
 
 /* Create directory 'path' */
-int do_mkdir(struct sftp_conn *, char *, Attrib *);
+int do_mkdir(struct sftp_conn *, char *, Attrib *, int);
 
 /* Remove directory 'path' */
 int do_rmdir(struct sftp_conn *, char *);
@@ -103,7 +103,13 @@
  * Download 'remote_path' to 'local_path'. Preserve permissions and times
  * if 'pflag' is set
  */
-int do_download(struct sftp_conn *, char *, char *, int);
+int do_download(struct sftp_conn *, char *, char *, Attrib *, int);
+
+/*
+ * Recursively download 'remote_directory' to 'local_directory'. Preserve 
+ * times if 'pflag' is set
+ */
+int download_dir(struct sftp_conn *, char *, char *, Attrib *, int, int);
 
 /*
  * Upload 'local_path' to 'remote_path'. Preserve permissions and times
@@ -111,4 +117,13 @@
  */
 int do_upload(struct sftp_conn *, char *, char *, int);
 
+/*
+ * Recursively upload 'local_directory' to 'remote_directory'. Preserve 
+ * times if 'pflag' is set
+ */
+int upload_dir(struct sftp_conn *, char *, char *, int, int);
+
+/* Concatenate paths, taking care of slashes. Caller must free result. */
+char *path_append(char *, char *);
+
 #endif
diff --git a/sftp.1 b/sftp.1
index fcd1d24..21dc5d8 100644
--- a/sftp.1
+++ b/sftp.1
@@ -1,4 +1,4 @@
-.\" $OpenBSD: sftp.1,v 1.73 2009/08/13 13:39:54 jmc Exp $
+.\" $OpenBSD: sftp.1,v 1.74 2009/08/18 18:36:20 djm Exp $
 .\"
 .\" Copyright (c) 2001 Damien Miller.  All rights reserved.
 .\"
@@ -22,7 +22,7 @@
 .\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 .\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 .\"
-.Dd $Mdocdate: August 13 2009 $
+.Dd $Mdocdate: August 18 2009 $
 .Dt SFTP 1
 .Os
 .Sh NAME
@@ -31,7 +31,7 @@
 .Sh SYNOPSIS
 .Nm sftp
 .Bk -words
-.Op Fl 1246Cqv
+.Op Fl 1246Cpqrv
 .Op Fl B Ar buffer_size
 .Op Fl b Ar batchfile
 .Op Fl c Ar cipher
@@ -223,6 +223,9 @@
 .El
 .It Fl P Ar port
 Specifies the port to connect to on the remote host.
+.It Fl p
+Preserves modification times, access times, and modes from the
+original files transferred.
 .It Fl q
 Quiet mode: disables the progress meter as well as warning and
 diagnostic messages from
@@ -232,6 +235,11 @@
 Increasing this may slightly improve file transfer speed
 but will increase memory usage.
 The default is 64 outstanding requests.
+.It Fl r
+Recursively copy entire directories when uploading and downloading.
+Note that
+.Nm
+does not follow symbolic links encountered in the tree traversal.
 .It Fl S Ar program
 Name of the
 .Ar program
@@ -322,7 +330,7 @@
 Quit
 .Nm sftp .
 .It Xo Ic get
-.Op Fl P
+.Op Fl Ppr
 .Ar remote-path
 .Op Ar local-path
 .Xc
@@ -341,10 +349,20 @@
 is specified, then
 .Ar local-path
 must specify a directory.
-If the
-.Fl P
+.Pp
+If ether the
+.Fl Ppr
+or
+.Fl p
 flag is specified, then full file permissions and access times are
 copied too.
+.Pp
+If the
+.Fl r
+flag is specified then directories will be copied recursively.
+Note that
+.Nm
+does not follow symbolic links when performing recursive transfers.
 .It Ic help
 Display help text.
 .It Ic lcd Ar path
@@ -440,10 +458,20 @@
 is specified, then
 .Ar remote-path
 must specify a directory.
-If the
+.Pp
+If ether the
 .Fl P
-flag is specified, then the file's full permission and access time are
+or
+.Fl p
+flag is specified, then full file permissions and access times are
 copied too.
+.Pp
+If the
+.Fl r
+flag is specified then directories will be copied recursively.
+Note that
+.Nm
+does not follow symbolic links when performing recursive transfers.
 .It Ic pwd
 Display remote working directory.
 .It Ic quit
diff --git a/sftp.c b/sftp.c
index 0123fd7..75b16b2 100644
--- a/sftp.c
+++ b/sftp.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp.c,v 1.110 2009/08/13 13:39:54 jmc Exp $ */
+/* $OpenBSD: sftp.c,v 1.111 2009/08/18 18:36:21 djm Exp $ */
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
  *
@@ -35,6 +35,9 @@
 #ifdef HAVE_PATHS_H
 # include <paths.h>
 #endif
+#ifdef HAVE_LIBGEN_H
+#include <libgen.h>
+#endif
 #ifdef USE_LIBEDIT
 #include <histedit.h>
 #else
@@ -83,6 +86,12 @@
 /* This is set to 0 if the progressmeter is not desired. */
 int showprogress = 1;
 
+/* When this option is set, we always recursively download/upload directories */
+int global_rflag = 0;
+
+/* When this option is set, the file transfers will always preserve times */
+int global_pflag = 0;
+
 /* SIGINT received during command processing */
 volatile sig_atomic_t interrupted = 0;
 
@@ -216,7 +225,7 @@
 	    "df [-hi] [path]                    Display statistics for current directory or\n"
 	    "                                   filesystem containing 'path'\n"
 	    "exit                               Quit sftp\n"
-	    "get [-P] remote-path [local-path]  Download file\n"
+	    "get [-Pr] remote-path [local-path] Download file\n"
 	    "help                               Display this help text\n"
 	    "lcd path                           Change local directory to 'path'\n"
 	    "lls [ls-options [path]]            Display local directory listing\n"
@@ -227,7 +236,7 @@
 	    "lumask umask                       Set local umask to 'umask'\n"
 	    "mkdir path                         Create remote directory\n"
 	    "progress                           Toggle display of progress meter\n"
-	    "put [-P] local-path [remote-path]  Upload file\n"
+	    "put [-Pr] local-path [remote-path] Upload file\n"
 	    "pwd                                Display remote working directory\n"
 	    "quit                               Quit sftp\n"
 	    "rename oldpath newpath             Rename remote file\n"
@@ -314,21 +323,6 @@
 }
 
 static 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);
-}
-
-static char *
 make_absolute(char *p, char *pwd)
 {
 	char *abs_str;
@@ -343,27 +337,8 @@
 }
 
 static int
-infer_path(const char *p, char **ifp)
-{
-	char *cp;
-
-	cp = strrchr(p, '/');
-	if (cp == NULL) {
-		*ifp = xstrdup(p);
-		return(0);
-	}
-
-	if (!cp[1]) {
-		error("Invalid path");
-		return(-1);
-	}
-
-	*ifp = xstrdup(cp + 1);
-	return(0);
-}
-
-static int
-parse_getput_flags(const char *cmd, char **argv, int argc, int *pflag)
+parse_getput_flags(const char *cmd, char **argv, int argc, int *pflag,
+    int *rflag)
 {
 	extern int opterr, optind, optopt, optreset;
 	int ch;
@@ -371,13 +346,17 @@
 	optind = optreset = 1;
 	opterr = 0;
 
-	*pflag = 0;
-	while ((ch = getopt(argc, argv, "Pp")) != -1) {
+	*rflag = *pflag = 0;
+	while ((ch = getopt(argc, argv, "PpRr")) != -1) {
 		switch (ch) {
 		case 'p':
 		case 'P':
 			*pflag = 1;
 			break;
+		case 'r':
+		case 'R':
+			*rflag = 1;
+			break;
 		default:
 			error("%s: Invalid flag -%c", cmd, optopt);
 			return -1;
@@ -489,62 +468,79 @@
 	return(S_ISDIR(a->perm));
 }
 
+/* Check whether path returned from glob(..., GLOB_MARK, ...) is a directory */
 static int
-process_get(struct sftp_conn *conn, char *src, char *dst, char *pwd, int pflag)
+pathname_is_dir(char *pathname)
+{
+	size_t l = strlen(pathname);
+
+	return l > 0 && pathname[l - 1] == '/';
+}
+
+static int
+process_get(struct sftp_conn *conn, char *src, char *dst, char *pwd,
+    int pflag, int rflag)
 {
 	char *abs_src = NULL;
 	char *abs_dst = NULL;
-	char *tmp;
 	glob_t g;
-	int err = 0;
-	int i;
+	char *filename, *tmp=NULL;
+	int i, err = 0;
 
 	abs_src = xstrdup(src);
 	abs_src = make_absolute(abs_src, pwd);
-
 	memset(&g, 0, sizeof(g));
+
 	debug3("Looking up %s", abs_src);
-	if (remote_glob(conn, abs_src, 0, NULL, &g)) {
+	if (remote_glob(conn, abs_src, GLOB_MARK, NULL, &g)) {
 		error("File \"%s\" not found.", abs_src);
 		err = -1;
 		goto out;
 	}
 
-	/* If multiple matches, dst must be a directory or unspecified */
-	if (g.gl_matchc > 1 && dst && !is_dir(dst)) {
-		error("Multiple files match, but \"%s\" is not a directory",
-		    dst);
+	/*
+	 * If multiple matches then dst must be a directory or
+	 * unspecified.
+	 */
+	if (g.gl_matchc > 1 && dst != NULL && !is_dir(dst)) {
+		error("Multiple source paths, but destination "
+		    "\"%s\" is not a directory", dst);
 		err = -1;
 		goto out;
 	}
 
 	for (i = 0; g.gl_pathv[i] && !interrupted; i++) {
-		if (infer_path(g.gl_pathv[i], &tmp)) {
+		tmp = xstrdup(g.gl_pathv[i]);
+		if ((filename = basename(tmp)) == NULL) {
+			error("basename %s: %s", tmp, strerror(errno));
+			xfree(tmp);
 			err = -1;
 			goto out;
 		}
 
 		if (g.gl_matchc == 1 && dst) {
-			/* If directory specified, append filename */
-			xfree(tmp);
 			if (is_dir(dst)) {
-				if (infer_path(g.gl_pathv[0], &tmp)) {
-					err = 1;
-					goto out;
-				}
-				abs_dst = path_append(dst, tmp);
-				xfree(tmp);
-			} else
+				abs_dst = path_append(dst, filename);
+			} else {
 				abs_dst = xstrdup(dst);
+			}
 		} else if (dst) {
-			abs_dst = path_append(dst, tmp);
-			xfree(tmp);
-		} else
-			abs_dst = tmp;
+			abs_dst = path_append(dst, filename);
+		} else {
+			abs_dst = xstrdup(filename);
+		}
+		xfree(tmp);
 
 		printf("Fetching %s to %s\n", g.gl_pathv[i], abs_dst);
-		if (do_download(conn, g.gl_pathv[i], abs_dst, pflag) == -1)
-			err = -1;
+		if (pathname_is_dir(g.gl_pathv[i]) && (rflag || global_rflag)) {
+			if (download_dir(conn, g.gl_pathv[i], abs_dst, NULL, 
+			    pflag || global_pflag, 1) == -1)
+				err = -1;
+		} else {
+			if (do_download(conn, g.gl_pathv[i], abs_dst, NULL,
+			    pflag || global_pflag) == -1)
+				err = -1;
+		}
 		xfree(abs_dst);
 		abs_dst = NULL;
 	}
@@ -556,14 +552,15 @@
 }
 
 static int
-process_put(struct sftp_conn *conn, char *src, char *dst, char *pwd, int pflag)
+process_put(struct sftp_conn *conn, char *src, char *dst, char *pwd,
+    int pflag, int rflag)
 {
 	char *tmp_dst = NULL;
 	char *abs_dst = NULL;
-	char *tmp;
+	char *tmp = NULL, *filename = NULL;
 	glob_t g;
 	int err = 0;
-	int i;
+	int i, dst_is_dir = 1;
 	struct stat sb;
 
 	if (dst) {
@@ -573,16 +570,20 @@
 
 	memset(&g, 0, sizeof(g));
 	debug3("Looking up %s", src);
-	if (glob(src, GLOB_NOCHECK, NULL, &g)) {
+	if (glob(src, GLOB_NOCHECK | GLOB_MARK, NULL, &g)) {
 		error("File \"%s\" not found.", src);
 		err = -1;
 		goto out;
 	}
 
+	/* If we aren't fetching to pwd then stash this status for later */
+	if (tmp_dst != NULL)
+		dst_is_dir = remote_is_dir(conn, tmp_dst);
+
 	/* If multiple matches, dst may be directory or unspecified */
-	if (g.gl_matchc > 1 && tmp_dst && !remote_is_dir(conn, tmp_dst)) {
-		error("Multiple files match, but \"%s\" is not a directory",
-		    tmp_dst);
+	if (g.gl_matchc > 1 && tmp_dst && !dst_is_dir) {
+		error("Multiple paths match, but destination "
+		    "\"%s\" is not a directory", tmp_dst);
 		err = -1;
 		goto out;
 	}
@@ -593,38 +594,38 @@
 			error("stat %s: %s", g.gl_pathv[i], strerror(errno));
 			continue;
 		}
-
-		if (!S_ISREG(sb.st_mode)) {
-			error("skipping non-regular file %s",
-			    g.gl_pathv[i]);
-			continue;
-		}
-		if (infer_path(g.gl_pathv[i], &tmp)) {
+		
+		tmp = xstrdup(g.gl_pathv[i]);
+		if ((filename = basename(tmp)) == NULL) {
+			error("basename %s: %s", tmp, strerror(errno));
+			xfree(tmp);
 			err = -1;
 			goto out;
 		}
 
 		if (g.gl_matchc == 1 && tmp_dst) {
 			/* If directory specified, append filename */
-			if (remote_is_dir(conn, tmp_dst)) {
-				if (infer_path(g.gl_pathv[0], &tmp)) {
-					err = 1;
-					goto out;
-				}
-				abs_dst = path_append(tmp_dst, tmp);
-				xfree(tmp);
-			} else
+			if (dst_is_dir)
+				abs_dst = path_append(tmp_dst, filename);
+			else
 				abs_dst = xstrdup(tmp_dst);
-
 		} else if (tmp_dst) {
-			abs_dst = path_append(tmp_dst, tmp);
-			xfree(tmp);
-		} else
-			abs_dst = make_absolute(tmp, pwd);
+			abs_dst = path_append(tmp_dst, filename);
+		} else {
+			abs_dst = make_absolute(xstrdup(filename), pwd);
+		}
+		xfree(tmp);
 
 		printf("Uploading %s to %s\n", g.gl_pathv[i], abs_dst);
-		if (do_upload(conn, g.gl_pathv[i], abs_dst, pflag) == -1)
-			err = -1;
+		if (pathname_is_dir(g.gl_pathv[i]) && (rflag || global_rflag)) {
+			if (upload_dir(conn, g.gl_pathv[i], abs_dst,
+			    pflag || global_pflag, 1) == -1)
+				err = -1;
+		} else {
+			if (do_upload(conn, g.gl_pathv[i], abs_dst,
+			    pflag || global_pflag) == -1)
+				err = -1;
+		}
 	}
 
 out:
@@ -1065,7 +1066,7 @@
 }
 
 static int
-parse_args(const char **cpp, int *pflag, int *lflag, int *iflag, int *hflag,
+parse_args(const char **cpp, int *pflag, int *rflag, int *lflag, int *iflag, int *hflag,
     unsigned long *n_arg, char **path1, char **path2)
 {
 	const char *cmd, *cp = *cpp;
@@ -1109,13 +1110,13 @@
 	}
 
 	/* Get arguments and parse flags */
-	*lflag = *pflag = *hflag = *n_arg = 0;
+	*lflag = *pflag = *rflag = *hflag = *n_arg = 0;
 	*path1 = *path2 = NULL;
 	optidx = 1;
 	switch (cmdnum) {
 	case I_GET:
 	case I_PUT:
-		if ((optidx = parse_getput_flags(cmd, argv, argc, pflag)) == -1)
+		if ((optidx = parse_getput_flags(cmd, argv, argc, pflag, rflag)) == -1)
 			return -1;
 		/* Get first pathname (mandatory) */
 		if (argc - optidx < 1) {
@@ -1235,7 +1236,7 @@
     int err_abort)
 {
 	char *path1, *path2, *tmp;
-	int pflag = 0, lflag = 0, iflag = 0, hflag = 0, cmdnum, i;
+	int pflag = 0, rflag = 0, lflag = 0, iflag = 0, hflag = 0, cmdnum, i;
 	unsigned long n_arg = 0;
 	Attrib a, *aa;
 	char path_buf[MAXPATHLEN];
@@ -1243,7 +1244,7 @@
 	glob_t g;
 
 	path1 = path2 = NULL;
-	cmdnum = parse_args(&cmd, &pflag, &lflag, &iflag, &hflag, &n_arg,
+	cmdnum = parse_args(&cmd, &pflag, &rflag, &lflag, &iflag, &hflag, &n_arg,
 	    &path1, &path2);
 
 	if (iflag != 0)
@@ -1261,10 +1262,10 @@
 		err = -1;
 		break;
 	case I_GET:
-		err = process_get(conn, path1, path2, *pwd, pflag);
+		err = process_get(conn, path1, path2, *pwd, pflag, rflag);
 		break;
 	case I_PUT:
-		err = process_put(conn, path1, path2, *pwd, pflag);
+		err = process_put(conn, path1, path2, *pwd, pflag, rflag);
 		break;
 	case I_RENAME:
 		path1 = make_absolute(path1, *pwd);
@@ -1290,7 +1291,7 @@
 		attrib_clear(&a);
 		a.flags |= SSH2_FILEXFER_ATTR_PERMISSIONS;
 		a.perm = 0777;
-		err = do_mkdir(conn, path1, &a);
+		err = do_mkdir(conn, path1, &a, 1);
 		break;
 	case I_RMDIR:
 		path1 = make_absolute(path1, *pwd);
@@ -1668,7 +1669,7 @@
 	extern char *__progname;
 
 	fprintf(stderr,
-	    "usage: %s [-1246Cqv] [-B buffer_size] [-b batchfile] [-c cipher]\n"
+	    "usage: %s [-1246Cpqrv] [-B buffer_size] [-b batchfile] [-c cipher]\n"
 	    "          [-D sftp_server_path] [-F ssh_config] "
 	    "[-i identity_file]\n"
 	    "          [-o ssh_option] [-P port] [-R num_requests] "
@@ -1710,7 +1711,7 @@
 	infile = stdin;
 
 	while ((ch = getopt(argc, argv,
-	    "1246hqvCc:D:i:o:s:S:b:B:F:P:R:")) != -1) {
+	    "1246hqrvCc:D:i:o:s:S:b:B:F:P:R:")) != -1) {
 		switch (ch) {
 		/* Passed through to ssh(1) */
 		case '4':
@@ -1764,9 +1765,15 @@
 			batchmode = 1;
 			addargs(&args, "-obatchmode yes");
 			break;
+		case 'p':
+			global_pflag = 1;
+			break;
 		case 'D':
 			sftp_direct = optarg;
 			break;
+		case 'r':
+			global_rflag = 1;
+			break;
 		case 'R':
 			num_requests = strtol(optarg, &cp, 10);
 			if (num_requests == 0 || *cp != '\0')