You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by tw...@apache.org on 2025/03/15 18:18:02 UTC
(mina-sshd) 02/03: GH-677: Fix ScpShell
This is an automated email from the ASF dual-hosted git repository.
twolf pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 2a042e5af990516ab579d76a5b8ff8c0b1342c36
Author: Thomas Wolf <tw...@apache.org>
AuthorDate: Fri Mar 14 18:26:48 2025 +0100
GH-677: Fix ScpShell
Fix the handling of the home directory when a RootedFileSystem is used.
'currentDir' and 'homeDir' must be paths from the file system used.
WinSCP may, at least initially, send Windows-style paths; so ensure
that the path is converted.
When listing the root directory, don't include the ".." entry for the
parent directory.
Tested manually on Windows.
---
CHANGES.md | 1 +
.../java/org/apache/sshd/scp/server/ScpShell.java | 233 ++++++++++++++-------
2 files changed, 154 insertions(+), 80 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index ff9710dca..8e542e122 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -31,6 +31,7 @@
* [GH-650](https://github.com/apache/mina-sshd/issues/650) Use the correct key from a user certificate in server-side pubkey auth
* [GH-663](https://github.com/apache/mina-sshd/issues/663) Fix racy `IoSession` creation
* [GH-664](https://github.com/apache/mina-sshd/issues/664) Skip MAC negotiation if an AEAD cipher was negotiated
+* [GH-677](https://github.com/apache/mina-sshd/issues/677) Fix current directory handling in `ScpShell` for WinSCP
* [GH-678](https://github.com/apache/mina-sshd/issues/678) `ScpShell`: write month names in English for WinSCP
* [GH-690](https://github.com/apache/mina-sshd/issues/690) Handle append mode for buggy SFTP v3 servers
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpShell.java b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpShell.java
index c7f2a888b..b5f49548f 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpShell.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpShell.java
@@ -18,6 +18,7 @@
*/
package org.apache.sshd.scp.server;
+import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
@@ -52,6 +53,9 @@ import java.util.function.Predicate;
import java.util.stream.Stream;
import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory;
+import org.apache.sshd.common.file.root.RootedFileSystem;
+import org.apache.sshd.common.file.util.BaseFileSystem;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.io.IoUtils;
@@ -69,7 +73,7 @@ import org.apache.sshd.server.channel.ServerChannelSessionHolder;
import org.apache.sshd.server.command.AbstractFileSystemCommand;
/**
- * This commands SCP support for a ChannelSession.
+ * This command provides SCP support for a ChannelSession.
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
@@ -77,17 +81,28 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
public static final String STATUS = "status";
- /** The "PWD" environment variable */
+ /** The "PWD" environment variable */
public static final String ENV_PWD = "PWD";
- /** The "HOME" environment variable */
+ /** The "HOME" environment variable */
public static final String ENV_HOME = "HOME";
/**
- * Key for the language - format "en_US.UTF-8"
+ * Key for the language - format "en_US.UTF-8"
*/
public static final String ENV_LANG = "LANG";
+ private static final int LS_ALL = 1 << 0;
+ private static final int LS_DIR_PLAIN = 1 << 1;
+ private static final int LS_LONG = 1 << 2;
+ private static final int LS_FULL_TIME = 1 << 3;
+
+ private static final int SCP_D = 1 << 0;
+ private static final int SCP_F = 1 << 1;
+ private static final int SCP_P = 1 << 2;
+ private static final int SCP_R = 1 << 3;
+ private static final int SCP_T = 1 << 4;
+
protected final Map<String, Object> variables = new HashMap<>();
protected final Charset nameEncodingCharset;
protected final Charset envVarsEnodingCharset;
@@ -137,6 +152,36 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
public void setFileSystemFactory(FileSystemFactory factory, SessionContext session) throws IOException {
homeDir = factory.getUserHomeDir(session);
super.setFileSystemFactory(factory, session);
+ FileSystem fs = getFileSystem();
+ if (fs instanceof RootedFileSystem) {
+ Path fsLocalRoot = ((RootedFileSystem) fs).getRoot();
+ Path newHome = fs.getPath("/");
+ if (homeDir != null && homeDir.startsWith(fsLocalRoot)) {
+ homeDir = fsLocalRoot.relativize(homeDir);
+ int n = homeDir.getNameCount();
+ for (int i = 0; i < n; i++) {
+ Path p = homeDir.getName(i);
+ if (!p.toString().isEmpty()) {
+ newHome = newHome.resolve(p);
+ }
+ }
+ }
+ homeDir = newHome;
+ log.debug("Home dir in RootedFileSystem = {}", homeDir);
+ currentDir = homeDir;
+ } else if (fs instanceof BaseFileSystem<?>) {
+ homeDir = ((BaseFileSystem<?>) fs).getDefaultDir();
+ currentDir = homeDir;
+ } else if (factory instanceof NativeFileSystemFactory) {
+ // A native file system will allow the user to navigate anywhere. Not recommended.
+ if (homeDir == null) {
+ homeDir = new File(".").getCanonicalFile().toPath();
+ }
+ log.debug("Home dir in native FileSystem = {}", homeDir);
+ currentDir = homeDir;
+ } else {
+ throw new IOException("ScpShell filesystem must be native or a RootedFileSystem or BaseFileSystem");
+ }
}
protected void println(String cmd, Object x, OutputStream out, Charset cs) {
@@ -172,15 +217,9 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
boolean debugEnabled = log.isDebugEnabled();
ChannelSession channel = getServerChannelSession();
try {
- // TODO find some better alternative
- if (homeDir == null) {
- currentDir = opener.resolveLocalPath(channel.getSession(), fileSystem, ".");
- log.warn("run - no home dir - starting at {}", currentDir);
- } else {
- currentDir = homeDir;
- if (debugEnabled) {
- log.debug("run - starting at home dir={}", homeDir);
- }
+ currentDir = homeDir;
+ if (debugEnabled) {
+ log.debug("run - starting at home dir={}", homeDir);
}
prepareEnvironment(getEnvironment());
@@ -284,9 +323,18 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
unset(argv);
break;
case "unalias":
+ // Has no effect; we might also return status=0 (success)
variables.put(STATUS, 1);
break;
default:
+ // TODO: rm -r -f path to support deletions
+ // TODO: mv -f oldname newname to support renaming
+ // TODO: mkdir name to create a new directory
+ // TODO: ln -s target link if the file system supports links
+ // TODO: chmod
+ // TODO: cp -p -r -f for remote-only copy
+ // see https://github.com/winscp/winscp/blob/88b50c1/source/core/ScpFileSystem.cpp#L108
+ // There'd be more, like sha512sum for supporting the checksum tab of file properties
handleUnsupportedCommand(command, argv);
}
stdout.flush();
@@ -301,10 +349,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
Locale locale = Locale.getDefault();
String languageTag = locale.toLanguageTag();
env.put(ENV_LANG, languageTag.replace('-', '_') + "." + nameEncodingCharset.displayName());
-
- if (homeDir != null) {
- env.put(ENV_HOME, homeDir.toString());
- }
+ env.put(ENV_HOME, homeDir.toString());
updatePwdEnvVariable(currentDir);
}
@@ -415,11 +460,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
}
protected void scp(String command, String[] argv) throws Exception {
- boolean optR = false;
- boolean optT = false;
- boolean optF = false;
- boolean optD = false;
- boolean optP = false;
+ int options = 0;
boolean isOption = true;
String path = null;
for (int i = 1; i < argv.length; i++) {
@@ -439,26 +480,27 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
char optVal = argVal.charAt(1);
switch (optVal) {
case 'r':
- optR = true;
+ options |= SCP_R;
break;
case 't':
- optT = true;
+ options |= SCP_T;
break;
case 'f':
- optF = true;
+ options |= SCP_F;
break;
case 'd':
- optD = true;
+ options |= SCP_D;
break;
case 'p':
- optP = true;
+ options |= SCP_P;
break;
default:
signalError(argv[0], "scp: unsupported option: " + argVal);
return;
}
} else if (path == null) {
- path = argVal;
+ // WinSCP sends local paths, but let's be sure here.
+ path = toScpPath(argVal);
isOption = false;
} else {
signalError(argv[0], "scp: one and only one path argument expected");
@@ -466,27 +508,34 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
}
}
- if ((optT && optF) || (!optT && !optF)) {
+ int tf = options & (SCP_T | SCP_F);
+ if (tf != SCP_T && tf != SCP_F) {
signalError(argv[0], "scp: one and only one of -t and -f option expected");
return;
}
- doScp(command, path, optR, optT, optF, optD, optP);
+ doScp(command, path, options);
}
- protected void doScp(
- String command, String path, boolean optR, boolean optT, boolean optF, boolean optD, boolean optP)
- throws Exception {
+ protected void doScp(String command, String path, int options) throws Exception {
try {
ChannelSession channel = getServerChannelSession();
ScpHelper helper = new ScpHelper(
channel.getSession(), getInputStream(), getOutputStream(),
fileSystem, opener, listener);
Path localPath = currentDir.resolve(path);
- if (optT) {
- helper.receive(command, localPath, optR, optD, optP, receiveBufferSize);
+ if ((options & SCP_T) != 0) {
+ if (log.isDebugEnabled()) {
+ log.debug("doScp({}) receiving file in {} at {}", getServerChannelSession(), path, localPath);
+ }
+ helper.receive(command, localPath, (options & SCP_R) != 0, (options & SCP_D) != 0, (options & SCP_P) != 0,
+ receiveBufferSize);
} else {
- helper.send(Collections.singletonList(localPath.toString()), optR, optP, sendBufferSize);
+ if (log.isDebugEnabled()) {
+ log.debug("doScp({}) sending file {} from {}", getServerChannelSession(), path, localPath);
+ }
+ helper.send(Collections.singletonList(localPath.toString()), (options & SCP_R) != 0, (options & SCP_P) != 0,
+ sendBufferSize);
}
variables.put(STATUS, 0);
} catch (IOException e) {
@@ -556,6 +605,23 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
}
}
+ private String toScpPath(String winScpPath) {
+ // WinSCP may send windows paths like C:\foo\bar. Map this to a virtual path if needed.
+ String separator = fileSystem.getSeparator();
+ String scpPath = winScpPath.replace("\\", separator);
+ if (scpPath.equals(winScpPath)) {
+ // Assume it's OK
+ return scpPath;
+ }
+ int i = scpPath.indexOf(separator);
+ // TODO: UNC paths? Funny \? prefixes? Looks like WinSCP doesn't send those.
+ if (i == 2 && scpPath.charAt(1) == ':') {
+ // Strip drive letter
+ scpPath = scpPath.substring(2);
+ }
+ return scpPath;
+ }
+
protected void cd(String[] argv) throws Exception {
if (argv.length == 1) {
if (homeDir != null) {
@@ -582,7 +648,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
// TODO make sure not escaping the user's sandbox filesystem
Path cwd = currentDir;
- cwd = cwd.resolve(path).toAbsolutePath().normalize();
+ cwd = cwd.resolve(toScpPath(path)).toAbsolutePath().normalize();
if (!Files.exists(cwd)) {
signalError(argv[0], "no such file or directory: " + path, nameEncodingCharset);
} else if (!Files.isDirectory(cwd)) {
@@ -604,11 +670,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
}
protected void ls(String[] argv) throws Exception {
- // find options
- boolean optListAll = false;
- boolean optDirAsPlain = false;
- boolean optLong = false;
- boolean optFullTime = false;
+ int options = 0;
String path = null;
for (int k = 1; k < argv.length; k++) {
String argValue = argv[k];
@@ -618,7 +680,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
}
if (argValue.equals("--full-time")) {
- optFullTime = true;
+ options |= LS_FULL_TIME;
} else if (argValue.charAt(0) == '-') {
int argLen = argValue.length();
if (argLen == 1) {
@@ -631,13 +693,13 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
// TODO should we raise an error if option re-specified ?
switch (optValue) {
case 'a':
- optListAll = true;
+ options |= LS_ALL;
break;
case 'd':
- optDirAsPlain = true;
+ options |= LS_DIR_PLAIN;
break;
case 'l':
- optLong = true;
+ options |= LS_LONG;
break;
default:
signalError(argv[0], "unsupported option: -" + optValue);
@@ -645,48 +707,59 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
}
}
} else if (path == null) {
- path = argValue;
+ path = toScpPath(argValue);
} else {
signalError(argv[0], "unsupported option: " + argValue);
return;
}
}
- // TODO see what optDirAsPlain means
- doLs(argv[0], path, optListAll, optLong, optFullTime);
+ doLs(argv[0], path, options);
}
- protected void doLs(
- String cmd, String path, boolean optListAll, boolean optLong, boolean optFullTime)
- throws Exception {
- // list current directory content
- Predicate<Path> filter = p -> {
- String fileName = p.getFileName().toString();
- return optListAll || fileName.equals(".")
- || fileName.equals("..") || !fileName.startsWith(".");
- };
-
- // TODO make sure not listing above user's home directory
- Stream<Path> files = path != null
- ? Stream.of(currentDir.resolve(path))
- : Stream.concat(Stream.of(".", "..").map(currentDir::resolve), Files.list(currentDir));
- OutputStream stdout = getOutputStream();
- OutputStream stderr = getErrorStream();
- variables.put(STATUS, 0);
- files
- .filter(filter)
- .map(p -> new PathEntry(p, currentDir))
- .sorted()
- .forEach(p -> {
- try {
- String str = p.display(optLong, optFullTime);
- println(cmd, str, stdout, nameEncodingCharset);
- } catch (NoSuchFileException e) {
- println(cmd, cmd + ": " + p.path.toString() + ": no such file or directory", stderr,
- nameEncodingCharset);
- variables.put(STATUS, 1);
- }
- });
+ protected void doLs(String cmd, String path, int options) throws Exception {
+ boolean listDirectory = path == null;
+ Path toList = currentDir;
+ if (path != null) {
+ toList = currentDir.resolve(path);
+ listDirectory = ((options & LS_DIR_PLAIN) == 0) && Files.isDirectory(toList);
+ }
+ Path inDir = listDirectory ? toList : currentDir;
+ // Hide the .. entry if we're listing the root
+ Stream<String> dotDirs = Stream.empty();
+ if (listDirectory) {
+ dotDirs = toList.getNameCount() == 0 ? Stream.of(".") : Stream.of(".", "..");
+ }
+ Predicate<Path> filter;
+ if (!listDirectory || (options & LS_ALL) != 0) {
+ filter = p -> true;
+ } else {
+ filter = p -> {
+ String fileName = p.getFileName().toString();
+ return fileName.equals(".") || fileName.equals("..") || !fileName.startsWith(".");
+ };
+ }
+ try (Stream<Path> files = !listDirectory
+ ? Stream.of(toList)
+ : Stream.concat(dotDirs.map(toList::resolve), Files.list(toList))) {
+ OutputStream stdout = getOutputStream();
+ OutputStream stderr = getErrorStream();
+ variables.put(STATUS, 0);
+ files
+ .filter(filter)
+ .map(p -> new PathEntry(p, inDir))
+ .sorted()
+ .forEach(p -> {
+ try {
+ String str = p.display((options & LS_LONG) != 0, (options & LS_FULL_TIME) != 0);
+ println(cmd, str, stdout, nameEncodingCharset);
+ } catch (NoSuchFileException e) {
+ println(cmd, cmd + ": " + p.path.toString() + ": no such file or directory", stderr,
+ nameEncodingCharset);
+ variables.put(STATUS, 1);
+ }
+ });
+ }
}
protected static class PathEntry implements Comparable<PathEntry> {