Due to the upload requirements of implementing a message protocol over sftp 
where truncated
messages aren't easily detected by the receiving system, the extension is 
required to avoid
data corruption. In particular, the operation permits us to upload to a scratch 
name to be
ignored by the pickup process, and then rename to a name that will pick up, 
while atomically
erroring if the pickup name already exists.

In particular, the only safe upload sequence is:

rm working.@tmp@
put working.@tmp@
do {
   ln working.@tmp@ serial.txt
   increment serial
} while (ln reported file exists)

The ln operation is [email protected]. We cannot substitute a ren operation 
here because
ren is allowed to clobber an existing file. Even if your code checks for this, 
there is beneath
it a race condition because the rename() system call doesn't check, and we must 
be race condition
free.

Anyway, I didn't think there were any more sftp providers left standing that 
didn't implement
[email protected]; turns out I was wrong. I whipped up a quick patch to the 
hosting provider
involved to demonstrate how easy this is to implement. The patch probably 
works, but I have no
place to try it. I'm fairly confident because I just implemented a client side 
version a few
months ago.
--- 
sshd-core_src_main_java_org_apache_sshd_server_subsystem_sftp_SftpSubsystem.orig.java
       Thu Feb 25 11:35:59 2016
+++ 
sshd-core_src_main_java_org_apache_sshd_server_subsystem_sftp_SftpSubsystem.java
    Thu Feb 25 12:02:38 2016
@@ -214,6 +214,7 @@
             Collections.unmodifiableList(
                     Arrays.asList(
                             new OpenSSHExtension(FsyncExtensionParser.NAME, 
"1")
+                            new OpenSSHExtension(HardLinkExtensionParser.NAME, 
"1")
                     ));
 
     public static final List<String> DEFAULT_OPEN_SSH_EXTENSIONS_NAMES =
@@ -599,6 +600,9 @@
             case SftpConstants.EXT_SPACE_AVAILABLE:
                 doSpaceAvailable(buffer, id);
                 break;
+            case HardLinkExtensionParser.NAME:
+                doHardLink(buffer, id);
+                break;
             default:
                 if (log.isDebugEnabled()) {
                     log.debug("executeExtendedCommand({}) received unsupported 
SSH_FXP_EXTENDED({})", getServerSession(), extension);
@@ -606,6 +610,32 @@
                 sendStatus(BufferUtils.clear(buffer), id, 
SftpConstants.SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(" + extension + 
") is unsupported or not implemented");
                 break;
         }
+    }
+
+    protected void doHardLink(Buffer buffer, int id) throws IOException {
+        String srcFile = buffer.getString();
+        String dstFile = buffer.getString();
+
+        try {
+            doHardLink(id, srcFile, dstFile);
+        } catch (IOException | RuntimeException e) {
+            sendStatus(BufferUtils.clear(buffer), id, e);
+            return;
+        }
+
+        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
+    }
+
+    protected void doHardLink(int id, String srcFile, String dstFile) throws 
IOException {
+        if (log.isDebugEnabled()) {
+            log.debug("doHardLink({})[id={}] SSH_FXP_EXTENDED[{}] (src={}, 
dst={})",
+                      getServerSession(), id, HardLinkExtensionParser.NAME;,
+                      srcFile, dstFile);
+        }
+
+        Path src = resolveFile(srcFile);
+        Path dst = resolveFile(dstFile);
+        Files.createLink(src, dst);
     }
 
     protected void doSpaceAvailable(Buffer buffer, int id) throws IOException {

Reply via email to