Repository: mina-sshd Updated Branches: refs/heads/master 54b56b844 -> c9524a24a
[SSHD-538] Handle correctly paths with double-slashes in them Fixed subtle bug that was related to normalization issues Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/c9524a24 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/c9524a24 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/c9524a24 Branch: refs/heads/master Commit: c9524a24a651b7b9af996e4673d0a541bc5106bb Parents: 54b56b8 Author: Lyor Goldstein <[email protected]> Authored: Sun Jul 26 15:40:46 2015 +0300 Committer: Lyor Goldstein <[email protected]> Committed: Sun Jul 26 15:40:46 2015 +0300 ---------------------------------------------------------------------- .../apache/sshd/common/util/SelectorUtils.java | 90 +++++++++++++++- .../server/subsystem/sftp/SftpSubsystem.java | 6 +- .../sshd/client/subsystem/sftp/SftpTest.java | 33 ++++++ .../sshd/common/util/SelectorUtilsTest.java | 102 +++++++++++++++++++ 4 files changed, 226 insertions(+), 5 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9524a24/sshd-core/src/main/java/org/apache/sshd/common/util/SelectorUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/SelectorUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/SelectorUtils.java index f38a75b..00af8b2 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/util/SelectorUtils.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/util/SelectorUtils.java @@ -533,7 +533,6 @@ public final class SelectorUtils { return ret; } - /** * Normalizes the path by removing '.', '..' and double separators (e.g. '//') * @@ -594,6 +593,95 @@ public final class SelectorUtils { } /** + * Applies the "slashification" rules as specified in + * <A HREF="http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266">Single Unix Specification version 3, section 3.266</A> + * and <A HREF="http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11">section 4.11 - Pathname resolution</A> + * @param path The original path - ignored if {@code null}/empty or does + * not contain any slashes + * @param sepChar The "slash" character + * @return The effective path - may be same as input if no changes required + */ + public static String applySlashifyRules(String path, char sepChar) { + if (GenericUtils.isEmpty(path)) { + return path; + } + + int curPos = path.indexOf(sepChar); + if (curPos < 0) { + return path; // no slashes to handle + } + + int lastPos = 0; + StringBuilder sb = null; + while (curPos < path.length()) { + curPos++; // skip the 1st '/' + + /* + * As per Single Unix Specification version 3, section 3.266: + * + * Multiple successive slashes are considered to be the + * same as one slash + */ + int nextPos = curPos; + while ((nextPos < path.length()) && (path.charAt(nextPos) == sepChar)) { + nextPos++; + } + + /* + * At this stage, nextPos is the first non-slash character after a + * possibly 'seqLen' sequence of consecutive slashes. + */ + int seqLen = nextPos - curPos; + if (seqLen > 0) { + if (sb == null) { + sb = new StringBuilder(path.length() - seqLen); + } + + if (lastPos < curPos) { + String clrText = path.substring(lastPos, curPos); + sb.append(clrText); + } + + lastPos = nextPos; + } + + if (nextPos >= path.length()) { + break; // no more data + } + + curPos = path.indexOf(sepChar, nextPos); + if (curPos < nextPos) { + break; // no more slashes + } + } + + // check if any leftovers for the modified path + if (sb != null) { + if (lastPos < path.length()) { + String clrText = path.substring(lastPos); + sb.append(clrText); + } + + path = sb.toString(); + } + + /* + * At this point we know for sure that 'path' contains only SINGLE + * slashes. According to section 4.11 - Pathname resolution + * + * A pathname that contains at least one non-slash character + * and that ends with one or more trailing slashes shall be + * resolved as if a single dot character ( '.' ) were appended + * to the pathname. + */ + if ((path.length() > 1) && (path.charAt(path.length() - 1) == sepChar)) { + return path + "."; + } else { + return path; + } + } + + /** * Converts a possibly '/' separated path to a local path. <B>Note:</B> * takes special care of Windows drive paths - e.g., {@code C:} * by converting them to "C:\" http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9524a24/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java index fa63181..103c5db 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java @@ -2927,15 +2927,13 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna protected Path resolveFile(String remotePath) throws IOException, InvalidPathException { // In case double slashes and other patterns are used - String path = SelectorUtils.normalizePath(remotePath, "/"); - String localPath = SelectorUtils.translateToLocalPath(path); - + String path = SelectorUtils.applySlashifyRules(remotePath, '/'); // In case we are running on Windows + String localPath = SelectorUtils.translateToLocalPath(path); Path p = defaultDir.resolve(localPath); if (log.isTraceEnabled()) { log.trace("resolveFile({}) {}", remotePath, p); } - return p; } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9524a24/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java index 82e5578..e7af9ee 100644 --- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java @@ -31,8 +31,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.EnumSet; import java.util.Iterator; @@ -49,7 +51,9 @@ import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensi import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension; import org.apache.sshd.common.Factory; import org.apache.sshd.common.FactoryManager; +import org.apache.sshd.common.file.FileSystemFactory; import org.apache.sshd.common.random.Random; +import org.apache.sshd.common.session.Session; import org.apache.sshd.common.subsystem.sftp.SftpConstants; import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils; import org.apache.sshd.common.subsystem.sftp.extensions.Supported2Parser.Supported2; @@ -109,6 +113,35 @@ public class SftpTest extends AbstractSftpClientTestSupport { Thread.sleep(5 * 60000); } + @Test // see extra fix for SSHD-538 + public void testNavigateBeyondRootFolder() throws Exception { + Path rootLocation = Paths.get(OsUtils.isUNIX() ? "/" : "C:\\"); + final FileSystem fsRoot = rootLocation.getFileSystem(); + sshd.setFileSystemFactory(new FileSystemFactory() { + @Override + public FileSystem createFileSystem(Session session) throws IOException { + return fsRoot; + } + }); + + try (SshClient client = SshClient.setUpDefaultClient()) { + client.start(); + + try (ClientSession session = client.connect(getCurrentTestName(), "localhost", port).verify(7L, TimeUnit.SECONDS).getSession()) { + session.addPasswordIdentity(getCurrentTestName()); + session.auth().verify(5L, TimeUnit.SECONDS); + + try (SftpClient sftp = session.createSftpClient()) { + String rootDir = sftp.canonicalPath("/"); + String upDir = sftp.canonicalPath(rootDir + "/.."); + assertEquals("Mismatched root dir parent", rootDir, upDir); + } + } finally { + client.stop(); + } + } + } + @Test public void testNormalizeRemoteRootValues() throws Exception { try (SshClient client = SshClient.setUpDefaultClient()) { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9524a24/sshd-core/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java b/sshd-core/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java new file mode 100644 index 0000000..82d6367 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.common.util; + +import java.util.Random; + +import org.apache.sshd.util.BaseTestSupport; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class SelectorUtilsTest extends BaseTestSupport { + public SelectorUtilsTest() { + super(); + } + + @Test + public void testApplySlashifyRules() { + for (String expected : new String[] { + null, "", getCurrentTestName(), getClass().getSimpleName() + "/" + getCurrentTestName(), + "/" + getClass().getSimpleName(), "/" + getClass().getSimpleName() + "/" + getCurrentTestName() + }) { + String actual = SelectorUtils.applySlashifyRules(expected, '/'); + assertSame("Mismatched results for '" + expected + "'", expected, actual); + } + + String[] comps = { getClass().getSimpleName(), getCurrentTestName() }; + Random rnd = new Random(System.nanoTime()); + StringBuilder sb = new StringBuilder(Byte.MAX_VALUE); + for (int index = 0; index < Long.SIZE; index++) { + if (sb.length() > 0) { + sb.setLength(0); // start from scratch + } + + boolean prepend = rnd.nextBoolean(); + if (prepend) { + slashify(sb, rnd); + } + + sb.append(comps[0]); + for (int j = 1; j < comps.length; j++) { + slashify(sb, rnd); + sb.append(comps[j]); + } + + boolean append = rnd.nextBoolean(); + if (append) { + slashify(sb, rnd); + } + + String path = sb.toString(); + sb.setLength(0); + if (prepend) { + sb.append('/'); + } + + sb.append(comps[0]); + for (int j = 1; j < comps.length; j++) { + sb.append('/').append(comps[j]); + } + + if (append) { + sb.append('/').append('.'); + } + + String expected = sb.toString(); + String actual = SelectorUtils.applySlashifyRules(path, '/'); + assertEquals("Mismatched results for path=" + path, expected, actual); + } + } + + + private static int slashify(StringBuilder sb, Random rnd) { + int slashes = 1 /* at least one slash */ + rnd.nextInt(Byte.SIZE); + for (int k = 0; k < slashes; k++) { + sb.append('/'); + } + + return slashes; + } + +}
