- djm@cvs.openbsd.org 2010/12/04 00:18:01
     [sftp-server.c sftp.1 sftp-client.h sftp.c PROTOCOL sftp-client.c]
     add a protocol extension to support a hard link operation. It is
     available through the "ln" command in the client. The old "ln"
     behaviour of creating a symlink is available using its "-s" option
     or through the preexisting "symlink" command; based on a patch from
     miklos AT szeredi.hu in bz#1555; ok markus@
diff --git a/ChangeLog b/ChangeLog
index a8aeacc..4e60f13 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -11,6 +11,13 @@
      [auth-rsa.c]
      move check for revoked keys to run earlier (in auth_rsa_key_allowed)
      bz#1829; patch from ldv AT altlinux.org; ok markus@
+   - djm@cvs.openbsd.org 2010/12/04 00:18:01
+     [sftp-server.c sftp.1 sftp-client.h sftp.c PROTOCOL sftp-client.c]
+     add a protocol extension to support a hard link operation. It is
+     available through the "ln" command in the client. The old "ln"
+     behaviour of creating a symlink is available using its "-s" option
+     or through the preexisting "symlink" command; based on a patch from
+     miklos AT szeredi.hu in bz#1555; ok markus@
 
 20101204
  - (djm) [openbsd-compat/bindresvport.c] Use arc4random_uniform(range)
diff --git a/PROTOCOL b/PROTOCOL
index 5d2a711..c281960 100644
--- a/PROTOCOL
+++ b/PROTOCOL
@@ -275,4 +275,20 @@
 Both the "statvfs@openssh.com" and "fstatvfs@openssh.com" extensions are
 advertised in the SSH_FXP_VERSION hello with version "2".
 
-$OpenBSD: PROTOCOL,v 1.16 2010/08/31 11:54:45 djm Exp $
+10. sftp: Extension request "hardlink@openssh.com"
+
+This request is for creating a hard link to a regular file. This
+request is implemented as a SSH_FXP_EXTENDED request with the
+following format:
+
+	uint32		id
+	string		"hardlink@openssh.com"
+	string		oldpath
+	string		newpath
+
+On receiving this request the server will perform the operation
+link(oldpath, newpath) and will respond with a SSH_FXP_STATUS message.
+This extension is advertised in the SSH_FXP_VERSION hello with version
+"1".
+
+$OpenBSD: PROTOCOL,v 1.17 2010/12/04 00:18:01 djm Exp $
diff --git a/sftp-client.c b/sftp-client.c
index 4e009ef..caa384b 100644
--- a/sftp-client.c
+++ b/sftp-client.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp-client.c,v 1.93 2010/09/22 22:58:51 djm Exp $ */
+/* $OpenBSD: sftp-client.c,v 1.94 2010/12/04 00:18:01 djm Exp $ */
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
  *
@@ -75,6 +75,7 @@
 #define SFTP_EXT_POSIX_RENAME	0x00000001
 #define SFTP_EXT_STATVFS	0x00000002
 #define SFTP_EXT_FSTATVFS	0x00000004
+#define SFTP_EXT_HARDLINK	0x00000008
 	u_int exts;
 	u_int64_t limit_kbps;
 	struct bwlimit bwlimit_in, bwlimit_out;
@@ -378,10 +379,14 @@
 		    strcmp(value, "2") == 0) {
 			ret->exts |= SFTP_EXT_STATVFS;
 			known = 1;
-		} if (strcmp(name, "fstatvfs@openssh.com") == 0 &&
+		} else if (strcmp(name, "fstatvfs@openssh.com") == 0 &&
 		    strcmp(value, "2") == 0) {
 			ret->exts |= SFTP_EXT_FSTATVFS;
 			known = 1;
+		} else if (strcmp(name, "hardlink@openssh.com") == 0 &&
+		    strcmp(value, "1") == 0) {
+			ret->exts |= SFTP_EXT_HARDLINK;
+			known = 1;
 		}
 		if (known) {
 			debug2("Server supports extension \"%s\" revision %s",
@@ -795,6 +800,39 @@
 }
 
 int
+do_hardlink(struct sftp_conn *conn, char *oldpath, char *newpath)
+{
+	Buffer msg;
+	u_int status, id;
+
+	buffer_init(&msg);
+
+	/* Send link request */
+	id = conn->msg_id++;
+	if ((conn->exts & SFTP_EXT_HARDLINK) == 0) {
+		error("Server does not support hardlink@openssh.com extension");
+		return -1;
+	}
+
+	buffer_put_char(&msg, SSH2_FXP_EXTENDED);
+	buffer_put_int(&msg, id);
+	buffer_put_cstring(&msg, "hardlink@openssh.com");
+	buffer_put_cstring(&msg, oldpath);
+	buffer_put_cstring(&msg, newpath);
+	send_msg(conn, &msg);
+	debug3("Sent message hardlink@openssh.com \"%s\" -> \"%s\"",
+	       oldpath, newpath);
+	buffer_free(&msg);
+
+	status = get_status(conn, id);
+	if (status != SSH2_FX_OK)
+		error("Couldn't link file \"%s\" to \"%s\": %s", oldpath,
+		    newpath, fx2txt(status));
+
+	return(status);
+}
+
+int
 do_symlink(struct sftp_conn *conn, char *oldpath, char *newpath)
 {
 	Buffer msg;
diff --git a/sftp-client.h b/sftp-client.h
index 145fc38..aef54ef 100644
--- a/sftp-client.h
+++ b/sftp-client.h
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp-client.h,v 1.19 2010/09/22 22:58:51 djm Exp $ */
+/* $OpenBSD: sftp-client.h,v 1.20 2010/12/04 00:18:01 djm Exp $ */
 
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
@@ -94,6 +94,9 @@
 /* Rename 'oldpath' to 'newpath' */
 int do_rename(struct sftp_conn *, char *, char *);
 
+/* Link 'oldpath' to 'newpath' */
+int do_hardlink(struct sftp_conn *, char *, char *);
+
 /* Rename 'oldpath' to 'newpath' */
 int do_symlink(struct sftp_conn *, char *, char *);
 
diff --git a/sftp-server.c b/sftp-server.c
index 47edcd0..b268d08 100644
--- a/sftp-server.c
+++ b/sftp-server.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp-server.c,v 1.92 2010/11/04 02:45:34 djm Exp $ */
+/* $OpenBSD: sftp-server.c,v 1.93 2010/12/04 00:18:01 djm Exp $ */
 /*
  * Copyright (c) 2000-2004 Markus Friedl.  All rights reserved.
  *
@@ -535,6 +535,9 @@
 	/* fstatvfs extension */
 	buffer_put_cstring(&msg, "fstatvfs@openssh.com");
 	buffer_put_cstring(&msg, "2"); /* version */
+	/* hardlink extension */
+	buffer_put_cstring(&msg, "hardlink@openssh.com");
+	buffer_put_cstring(&msg, "1"); /* version */
 	send_msg(&msg);
 	buffer_free(&msg);
 }
@@ -1223,6 +1226,27 @@
 }
 
 static void
+process_extended_hardlink(u_int32_t id)
+{
+	char *oldpath, *newpath;
+	int ret, status;
+
+	oldpath = get_string(NULL);
+	newpath = get_string(NULL);
+	debug3("request %u: hardlink", id);
+	logit("hardlink old \"%s\" new \"%s\"", oldpath, newpath);
+	if (readonly)
+		status = SSH2_FX_PERMISSION_DENIED;
+	else {
+		ret = link(oldpath, newpath);
+		status = (ret == -1) ? errno_to_portable(errno) : SSH2_FX_OK;
+	}
+	send_status(id, status);
+	xfree(oldpath);
+	xfree(newpath);
+}
+
+static void
 process_extended(void)
 {
 	u_int32_t id;
@@ -1236,6 +1260,8 @@
 		process_extended_statvfs(id);
 	else if (strcmp(request, "fstatvfs@openssh.com") == 0)
 		process_extended_fstatvfs(id);
+	else if (strcmp(request, "hardlink@openssh.com") == 0)
+		process_extended_hardlink(id);
 	else
 		send_status(id, SSH2_FX_OP_UNSUPPORTED);	/* MUST */
 	xfree(request);
diff --git a/sftp.1 b/sftp.1
index 3bb0c06..89b5d35 100644
--- a/sftp.1
+++ b/sftp.1
@@ -1,4 +1,4 @@
-.\" $OpenBSD: sftp.1,v 1.87 2010/11/18 15:01:00 jmc Exp $
+.\" $OpenBSD: sftp.1,v 1.88 2010/12/04 00:18:01 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: November 18 2010 $
+.Dd $Mdocdate: December 4 2010 $
 .Dt SFTP 1
 .Os
 .Sh NAME
@@ -128,7 +128,7 @@
 .Ic get , put , rename , ln ,
 .Ic rm , mkdir , chdir , ls ,
 .Ic lchdir , chmod , chown ,
-.Ic chgrp , lpwd , df ,
+.Ic chgrp , lpwd , df , symlink ,
 and
 .Ic lmkdir .
 Termination on error can be suppressed on a command by command basis by
@@ -392,11 +392,19 @@
 .It Ic lmkdir Ar path
 Create local directory specified by
 .Ar path .
-.It Ic ln Ar oldpath Ar newpath
-Create a symbolic link from
+.It Xo Ic ln
+.Op Fl s
+.Ar oldpath
+.Ar newpath
+.Xc
+Create a link from
 .Ar oldpath
 to
 .Ar newpath .
+If the
+.Fl s
+flag is specified the created link is a symbolic link, otherwise it is
+a hard link.
 .It Ic lpwd
 Print local working directory.
 .It Xo Ic ls
diff --git a/sftp.c b/sftp.c
index d605505..ab667f5 100644
--- a/sftp.c
+++ b/sftp.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: sftp.c,v 1.131 2010/10/23 22:06:12 sthen Exp $ */
+/* $OpenBSD: sftp.c,v 1.132 2010/12/04 00:18:01 djm Exp $ */
 /*
  * Copyright (c) 2001-2004 Damien Miller <djm@openbsd.org>
  *
@@ -132,6 +132,7 @@
 #define I_GET		5
 #define I_HELP		6
 #define I_LCHDIR	7
+#define I_LINK		25
 #define I_LLS		8
 #define I_LMKDIR	9
 #define I_LPWD		10
@@ -176,7 +177,7 @@
 	{ "lchdir",	I_LCHDIR,	LOCAL	},
 	{ "lls",	I_LLS,		LOCAL	},
 	{ "lmkdir",	I_LMKDIR,	LOCAL	},
-	{ "ln",		I_SYMLINK,	REMOTE	},
+	{ "ln",		I_LINK,		REMOTE	},
 	{ "lpwd",	I_LPWD,		LOCAL	},
 	{ "ls",		I_LS,		REMOTE	},
 	{ "lumask",	I_LUMASK,	NOARGS	},
@@ -240,7 +241,7 @@
 	    "lcd path                           Change local directory to 'path'\n"
 	    "lls [ls-options [path]]            Display local directory listing\n"
 	    "lmkdir path                        Create local directory\n"
-	    "ln oldpath newpath                 Symlink remote file\n"
+	    "ln [-s] oldpath newpath            Link remote file (-s for symlink)\n"
 	    "lpwd                               Print local working directory\n"
 	    "ls [-1afhlnrSt] [path]             Display remote directory listing\n"
 	    "lumask umask                       Set local umask to 'umask'\n"
@@ -377,6 +378,30 @@
 }
 
 static int
+parse_link_flags(const char *cmd, char **argv, int argc, int *sflag)
+{
+	extern int opterr, optind, optopt, optreset;
+	int ch;
+
+	optind = optreset = 1;
+	opterr = 0;
+
+	*sflag = 0;
+	while ((ch = getopt(argc, argv, "s")) != -1) {
+		switch (ch) {
+		case 's':
+			*sflag = 1;
+			break;
+		default:
+			error("%s: Invalid flag -%c", cmd, optopt);
+			return -1;
+		}
+	}
+
+	return optind;
+}
+
+static int
 parse_ls_flags(char **argv, int argc, int *lflag)
 {
 	extern int opterr, optind, optopt, optreset;
@@ -1088,7 +1113,7 @@
 
 static int
 parse_args(const char **cpp, int *pflag, int *rflag, int *lflag, int *iflag,
-    int *hflag, unsigned long *n_arg, char **path1, char **path2)
+    int *hflag, int *sflag, unsigned long *n_arg, char **path1, char **path2)
 {
 	const char *cmd, *cp = *cpp;
 	char *cp2, **argv;
@@ -1138,7 +1163,8 @@
 	switch (cmdnum) {
 	case I_GET:
 	case I_PUT:
-		if ((optidx = parse_getput_flags(cmd, argv, argc, pflag, rflag)) == -1)
+		if ((optidx = parse_getput_flags(cmd, argv, argc,
+		    pflag, rflag)) == -1)
 			return -1;
 		/* Get first pathname (mandatory) */
 		if (argc - optidx < 1) {
@@ -1154,8 +1180,11 @@
 			undo_glob_escape(*path2);
 		}
 		break;
-	case I_RENAME:
+	case I_LINK:
+		if ((optidx = parse_link_flags(cmd, argv, argc, sflag)) == -1)
+			return -1;
 	case I_SYMLINK:
+	case I_RENAME:
 		if (argc - optidx < 2) {
 			error("You must specify two paths after a %s "
 			    "command.", cmd);
@@ -1258,7 +1287,8 @@
     int err_abort)
 {
 	char *path1, *path2, *tmp;
-	int pflag = 0, rflag = 0, lflag = 0, iflag = 0, hflag = 0, cmdnum, i;
+	int pflag = 0, rflag = 0, lflag = 0, iflag = 0, hflag = 0, sflag = 0;
+	int cmdnum, i;
 	unsigned long n_arg = 0;
 	Attrib a, *aa;
 	char path_buf[MAXPATHLEN];
@@ -1266,8 +1296,8 @@
 	glob_t g;
 
 	path1 = path2 = NULL;
-	cmdnum = parse_args(&cmd, &pflag, &rflag, &lflag, &iflag, &hflag, &n_arg,
-	    &path1, &path2);
+	cmdnum = parse_args(&cmd, &pflag, &rflag, &lflag, &iflag, &hflag,
+	    &sflag, &n_arg, &path1, &path2);
 
 	if (iflag != 0)
 		err_abort = 0;
@@ -1295,8 +1325,11 @@
 		err = do_rename(conn, path1, path2);
 		break;
 	case I_SYMLINK:
+		sflag = 1;
+	case I_LINK:
+		path1 = make_absolute(path1, *pwd);
 		path2 = make_absolute(path2, *pwd);
-		err = do_symlink(conn, path1, path2);
+		err = (sflag ? do_symlink : do_hardlink)(conn, path1, path2);
 		break;
 	case I_RM:
 		path1 = make_absolute(path1, *pwd);