This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 9fe278dc34e167f649df5a92f150bc5a0e0d63f4 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Thu Aug 20 15:11:57 2020 +0300 [SSHD-1056] Added 3-way CLI option to SCP command --- CHANGES.md | 3 +- docs/cli.md | 24 +- .../sshd/cli/client/CliClientModuleProperties.java | 9 +- .../org/apache/sshd/cli/client/ScpCommandMain.java | 397 ++++++++++++++++----- .../sshd/client/session/ClientUserAuthService.java | 3 +- .../java/org/apache/sshd/scp/client/ScpClient.java | 56 ++- .../scp/client/ScpRemote2RemoteTransferHelper.java | 99 +++-- .../java/org/apache/sshd/scp/common/ScpHelper.java | 156 +++++--- .../org/apache/sshd/scp/common/ScpLocation.java | 141 ++++++-- .../apache/sshd/scp/common/helpers/ScpAckInfo.java | 130 +++++++ .../apache/sshd/scp/common/helpers/ScpIoUtils.java | 145 +------- .../org/apache/sshd/scp/server/ScpCommand.java | 12 +- .../java/org/apache/sshd/scp/server/ScpShell.java | 10 +- .../java/org/apache/sshd/scp/client/ScpTest.java | 23 +- .../sshd/scp/common/ScpLocationParsingTest.java | 92 +++++ 15 files changed, 891 insertions(+), 409 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e13a19e..1a8973e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,7 @@ or `-key-file` command line option. * [SSHD-1030](https://issues.apache.org/jira/browse/SSHD-1030) Added a NoneFileSystemFactory implementation * [SSHD-1042](https://issues.apache.org/jira/browse/SSHD-1042) Added more callbacks to SftpEventListener * [SSHD-1040](https://issues.apache.org/jira/browse/SSHD-1040) Make server key available after KEX completed. +* [SSHD-1060](https://issues.apache.org/jira/browse/SSHD-1060) Do not store logger level in fields. ## Behavioral changes and enhancements @@ -41,5 +42,5 @@ or `-key-file` command line option. * [SSHD-1047](https://issues.apache.org/jira/browse/SSHD-1047) Support for SSH jumps. * [SSHD-1048](https://issues.apache.org/jira/browse/SSHD-1048) Wrap instead of rethrow IOException in Future. * [SSHD-1050](https://issues.apache.org/jira/browse/SSHD-1050) Fixed race condition in AuthFuture if exception caught before authentication started. +* [SSHD-1056](https://issues.apache.org/jira/browse/SSHD-1005) Added support for SCP remote-to-remote directory transfer - including '-3' option of SCP command CLI. * [SSHD-1058](https://issues.apache.org/jira/browse/SSHD-1058) Improve exception logging strategy. -* [SSHD-1060](https://issues.apache.org/jira/browse/SSHD-1060) Do not store logger level in fields. diff --git a/docs/cli.md b/docs/cli.md index bc57426..f232bb7 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -97,7 +97,26 @@ to **disable** an option one must use `-o PtyMode=WHATEVER=0`. ### `ScpCommandMain` -Reminiscent of the [scp(1)](https://linux.die.net/man/1/scp) CLI client. +Reminiscent of the [scp(1)](https://man7.org/linux/man-pages/man1/scp.1.html) CLI client - including support for "3-way" copy +(a.k.a. remote-to-remote) option: + +``` +scp -p -r -3 user1@server1:source user2@server2:destination +``` + +In this context, it is worth mentioning that the CLI also supports URI locations having the format `scp://[user@]host[:port][/path]` + +``` +# If port is omitted then 22 is assumed +scp -p scp://user1@server1:2222/source/file /home/user2/destination + +# Note: same effect can be achieved with -P option + +scp -p -P 2222 user1@server1:source/file /home/user2/destination + +# the URI is better suited for remote-to-remote transfers +scp -p -r -3 scp://user1@server1:2222/source scp://user2@server2:3333/destination +``` ### `SshServerMain` @@ -122,3 +141,6 @@ provided when subsystems are auto-detected and/or filtered. * **Shell** - unless otherwise instructed, the default SSH server uses an internal shell (see `InteractiveProcessShellFactory`). The shell can be overridden or disabled by specifying a `-o ShellFactory=XXX` option where the value can either be `none` to specify that no shell is to be used, or the fully-qualified name of a class that implements the `ShellFactory` interface. The implementation must be public and have a public no-args constructor for instantiating it. + +**Note:** A special value of `scp` can be used to use the built-in `ScpShell` instead of the interactive one (reminder: the SCP "shell" is a limited shell that provides +a good enough functionality for *WinScp*). diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/CliClientModuleProperties.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/CliClientModuleProperties.java index 1287784..2af0c44 100644 --- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/CliClientModuleProperties.java +++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/CliClientModuleProperties.java @@ -28,19 +28,18 @@ import org.apache.sshd.common.Property; */ public final class CliClientModuleProperties { /** - * Key used to retrieve the value of the timeout after which it will abort the connection if the connection - * has not been established - in milliseconds. + * Key used to retrieve the value of the timeout after which it will abort the connection if the connection has not + * been established - in milliseconds. */ public static final Property<Duration> CONECT_TIMEOUT - = Property.duration("cli-connect-timeout", Duration.ofMinutes(2)); + = Property.duration("cli-connect-timeout", Duration.ofMinutes(2)); /** * Key used to retrieve the value of the timeout after which it will close the connection if the other side has not * been authenticated - in milliseconds. */ public static final Property<Duration> AUTH_TIMEOUT - = Property.duration("cli-auth-timeout", Duration.ofMinutes(2)); - + = Property.duration("cli-auth-timeout", Duration.ofMinutes(2)); private CliClientModuleProperties() { throw new UnsupportedOperationException("No instance"); diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java index a23eff5..cfaf2d8 100644 --- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java +++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java @@ -20,21 +20,29 @@ package org.apache.sshd.cli.client; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; +import java.security.KeyPair; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; import org.apache.sshd.cli.CliSupport; +import org.apache.sshd.client.ClientFactoryManager; +import org.apache.sshd.client.auth.AuthenticationIdentitiesProvider; +import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.io.NoCloseInputStream; @@ -42,12 +50,16 @@ import org.apache.sshd.common.util.threads.ThreadUtils; import org.apache.sshd.scp.client.ScpClient; import org.apache.sshd.scp.client.ScpClient.Option; import org.apache.sshd.scp.client.ScpClientCreator; +import org.apache.sshd.scp.client.ScpRemote2RemoteTransferHelper; +import org.apache.sshd.scp.client.ScpRemote2RemoteTransferListener; import org.apache.sshd.scp.common.ScpLocation; import org.apache.sshd.scp.common.ScpTransferEventListener; +import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; /** - * TODO Add javadoc - * + * @see <A HREF="https://man7.org/linux/man-pages/man1/scp.1.html">SCP(1) - manual page</A> * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public class ScpCommandMain extends SshClientCliSupport { @@ -56,11 +68,16 @@ public class ScpCommandMain extends SshClientCliSupport { */ public static final String SCP_PORT_OPTION = "-P"; + /** + * Copies between two remote hosts are transferred through the local host + */ + public static final String SCP_REMOTE_TO_REMOTE_OPTION = "-3"; + public ScpCommandMain() { super(); // in case someone wants to extend it } - ////////////////////////////////////////////////////////////////////////// + /* -------------------------------------------------------------------------------- */ public static String[] normalizeCommandArguments(PrintStream stdout, PrintStream stderr, String... args) { int numArgs = GenericUtils.length(args); @@ -70,6 +87,7 @@ public class ScpCommandMain extends SshClientCliSupport { List<String> effective = new ArrayList<>(numArgs); boolean error = false; + boolean threeWay = false; for (int index = 0; (index < numArgs) && (!error); index++) { String argName = args[index]; // handled by 'setupClientSession' @@ -86,6 +104,9 @@ public class ScpCommandMain extends SshClientCliSupport { || "-q".equals(argName) || "-C".equals(argName) || "-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName)) { effective.add(argName); + } else if (SCP_REMOTE_TO_REMOTE_OPTION.equals(argName)) { + threeWay = true; + effective.add(argName); } else if (argName.charAt(0) == '-') { error = showError(stderr, "Unknown option: " + argName); break; @@ -103,15 +124,22 @@ public class ScpCommandMain extends SshClientCliSupport { break; } - if (source.isLocal() == target.isLocal()) { - error = showError(stderr, "Both targets are either remote or local"); - break; - } + if (threeWay) { + if (source.isLocal() || target.isLocal()) { + error = showError(stderr, "Both targets must be remote for the 3-way copy option"); + break; + } + + adjustRemoteTargetArguments(source, source, target, effective); + } else { + if (source.isLocal() == target.isLocal()) { + error = showError(stderr, "Both targets are either remote or local"); + break; + } - ScpLocation remote = source.isLocal() ? target : source; - effective.add(remote.resolveUsername() + "@" + remote.getHost()); - effective.add(source.toString()); - effective.add(target.toString()); + ScpLocation remote = source.isLocal() ? target : source; + adjustRemoteTargetArguments(remote, source, target, effective); + } break; } } @@ -123,6 +151,23 @@ public class ScpCommandMain extends SshClientCliSupport { return effective.toArray(new String[effective.size()]); } + /* -------------------------------------------------------------------------------- */ + + private static void adjustRemoteTargetArguments( + ScpLocation remote, ScpLocation source, ScpLocation target, Collection<String> effective) { + int port = remote.resolvePort(); + if (port != SshConstants.DEFAULT_PORT) { + effective.add(SCP_PORT_OPTION); + effective.add(Integer.toString(port)); + } + + effective.add(remote.resolveUsername() + "@" + remote.getHost()); + effective.add(source.toString()); + effective.add(target.toString()); + } + + /* -------------------------------------------------------------------------------- */ + public static ScpClientCreator resolveScpClientCreator(PrintStream stderr, String... args) { String className = null; for (int index = 0, numArgs = GenericUtils.length(args); index < numArgs; index++) { @@ -159,6 +204,233 @@ public class ScpCommandMain extends SshClientCliSupport { } } + /* -------------------------------------------------------------------------------- */ + + public static Set<Option> parseCopyOptions(String[] args) { + if (GenericUtils.isEmpty(args)) { + return Collections.emptySet(); + } + + Set<Option> options = EnumSet.noneOf(Option.class); + for (String argName : args) { + if ("-r".equals(argName)) { + options.add(Option.TargetIsDirectory); + options.add(Option.Recursive); + } else if ("-p".equals(argName)) { + options.add(Option.PreserveAttributes); + } + } + + return options; + } + + /* -------------------------------------------------------------------------------- */ + + public static void showUsageMessage(PrintStream stderr) { + stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i identity] [-io nio2|mina|netty]" + + " [" + SCP_REMOTE_TO_REMOTE_OPTION + "]" + + " [" + Option.Recursive.getOptionValue() + "]" + + " [" + Option.PreserveAttributes.getOptionValue() + "]" + + " [-v[v][v]] [-E logoutput] [-q] [-o option=value] [-o creator=class name]" + + " [-c cipherlist] [-m maclist] [-J proxyJump] [-w password] [-C] <source> <target>"); + stderr.println(); + stderr.println("Where <source> or <target> are either 'user@host:file' or a local file path"); + stderr.println("NOTE: exactly ONE of the source or target must be remote and the other one local"); + stderr.println(" or both remote if " + SCP_REMOTE_TO_REMOTE_OPTION + " specified"); + } + + /* -------------------------------------------------------------------------------- */ + + @SuppressWarnings("checkstyle:ParameterNumber") + public static void xferLocalToRemote( + BufferedReader stdin, PrintStream stdout, PrintStream stderr, String[] args, + ScpLocation source, ScpLocation target, Collection<Option> options, + OutputStream logStream, Level level, boolean quiet) + throws Exception { + ScpClientCreator creator = resolveScpClientCreator(stderr, args); + ClientSession session = ((logStream == null) || (creator == null) || GenericUtils.isEmpty(args)) + ? null : setupClientSession(SCP_PORT_OPTION, stdin, level, stdout, stderr, args); + if (session == null) { + showUsageMessage(stderr); + System.exit(-1); + return; // not that we really need it... + } + + try { + if (!quiet) { + creator.setScpTransferEventListener(new ScpTransferEventListener() { + @Override + public void startFolderEvent( + Session session, FileOperation op, Path file, Set<PosixFilePermission> perms) { + logEvent("startFolderEvent", session, op, file, -1L, perms, null); + } + + @Override + public void endFolderEvent( + Session session, FileOperation op, Path file, Set<PosixFilePermission> perms, + Throwable thrown) { + logEvent("endFolderEvent", session, op, file, -1L, perms, thrown); + } + + @Override + public void startFileEvent( + Session session, FileOperation op, Path file, long length, Set<PosixFilePermission> perms) { + logEvent("startFileEvent", session, op, file, length, perms, null); + } + + @Override + public void endFileEvent( + Session session, FileOperation op, Path file, long length, Set<PosixFilePermission> perms, + Throwable thrown) { + logEvent("endFileEvent", session, op, file, length, perms, thrown); + } + + private void logEvent( + String name, Session session, FileOperation op, Path file, long length, + Collection<PosixFilePermission> perms, Throwable thrown) { + PrintStream ps = (thrown == null) ? stdout : stderr; + ps.append(" ").append(name) + .append('[').append(session.toString()).append(']') + .append('[').append(op.name()).append(']') + .append(' ').append(file.toString()); + if (length > 0L) { + ps.append(' ').append("length=").append(Long.toString(length)); + } + ps.append(' ').append(String.valueOf(perms)); + + if (thrown != null) { + ps.append(" - ").append(thrown.getClass().getSimpleName()).append(": ") + .append(thrown.getMessage()); + } + ps.println(); + } + }); + } + + ScpClient client = creator.createScpClient(session); + if (source.isLocal()) { + client.upload(source.getPath(), target.getPath(), options); + } else { + client.download(source.getPath(), target.getPath(), options); + } + } finally { + session.close(); + } + + } + + /* -------------------------------------------------------------------------------- */ + + @SuppressWarnings("checkstyle:ParameterNumber") + public static void xferRemoteToRemote( + BufferedReader stdin, PrintStream stdout, PrintStream stderr, String[] args, + ScpLocation source, ScpLocation target, Collection<Option> options, + OutputStream logStream, Level level, boolean quiet) + throws Exception { + ClientSession srcSession = ((logStream == null) || GenericUtils.isEmpty(args)) + ? null : setupClientSession(SCP_PORT_OPTION, stdin, level, stdout, stderr, args); + if (srcSession == null) { + showUsageMessage(stderr); + System.exit(-1); + return; // not that we really need it... + } + + try { + ClientFactoryManager manager = srcSession.getFactoryManager(); + // TODO see if there is a way to specify a different port or proxy jump for the target + HostConfigEntry entry = resolveHost( + manager, target.resolveUsername(), target.getHost(), target.resolvePort(), null); + // TODO use a configurable wait time + ClientSession dstSession = manager.connect(entry, null, null) + .verify(CliClientModuleProperties.CONECT_TIMEOUT.getRequired(srcSession)) + .getSession(); + try { + // TODO see if there is a way to specify different password/key for target + // copy non-default identities from source session + AuthenticationIdentitiesProvider provider = srcSession.getRegisteredIdentities(); + Iterable<?> ids = (provider == null) ? null : provider.loadIdentities(); + Iterator<?> iter = (ids == null) ? null : ids.iterator(); + while ((iter != null) && iter.hasNext()) { + Object v = iter.next(); + if (v instanceof String) { + dstSession.addPasswordIdentity((String) v); + } else if (v instanceof KeyPair) { + dstSession.addPublicKeyIdentity((KeyPair) v); + } else { + throw new UnsupportedOperationException("Unsupported source identity: " + v); + } + } + + dstSession.auth().verify(CliClientModuleProperties.AUTH_TIMEOUT.getRequired(dstSession)); + + ScpRemote2RemoteTransferListener listener = quiet ? null : new ScpRemote2RemoteTransferListener() { + @Override + public void startDirectFileTransfer( + ClientSession srcSession, String source, + ClientSession dstSession, String destination, + ScpTimestampCommandDetails timestamp, ScpReceiveFileCommandDetails details) + throws IOException { + logEvent("FILE-START: ", source, destination, null); + } + + @Override + public void startDirectDirectoryTransfer( + ClientSession srcSession, String source, + ClientSession dstSession, String destination, + ScpTimestampCommandDetails timestamp, ScpReceiveDirCommandDetails details) + throws IOException { + logEvent("DIR-START: ", source, destination, null); + } + + @Override + public void endDirectFileTransfer( + ClientSession srcSession, String source, + ClientSession dstSession, String destination, + ScpTimestampCommandDetails timestamp, ScpReceiveFileCommandDetails details, + long xferSize, Throwable thrown) + throws IOException { + logEvent("FILE-END: ", source, destination, thrown); + } + + @Override + public void endDirectDirectoryTransfer( + ClientSession srcSession, String source, + ClientSession dstSession, String destination, + ScpTimestampCommandDetails timestamp, ScpReceiveDirCommandDetails details, + Throwable thrown) + throws IOException { + logEvent("DIR-END: ", source, destination, thrown); + } + + private void logEvent(String event, String src, String dst, Throwable thrown) { + PrintStream ps = (thrown == null) ? stdout : stderr; + ps.append(" ").append(event) + .append(' ').append(src).append(" ==> ").append(dst); + if (thrown != null) { + ps.append(" - ").append(thrown.getClass().getSimpleName()).append(": ") + .append(thrown.getMessage()); + } + ps.println(); + } + }; + ScpRemote2RemoteTransferHelper helper = new ScpRemote2RemoteTransferHelper(srcSession, dstSession, listener); + boolean preserveAttributes = GenericUtils.isNotEmpty(options) && options.contains(Option.PreserveAttributes); + if (GenericUtils.isNotEmpty(options) + && (options.contains(Option.Recursive) || options.contains(Option.TargetIsDirectory))) { + helper.transferDirectory(source.getPath(), target.getPath(), preserveAttributes); + } else { + helper.transferFile(source.getPath(), target.getPath(), preserveAttributes); + } + } finally { + dstSession.close(); + } + } finally { + srcSession.close(); + } + } + + ////////////////////////////////////////////////////////////////////////// + public static void main(String[] args) throws Exception { PrintStream stdout = System.out; PrintStream stderr = System.err; @@ -178,95 +450,26 @@ public class ScpCommandMain extends SshClientCliSupport { } } - ScpClientCreator creator = resolveScpClientCreator(stderr, args); - ClientSession session = ((logStream == null) || (creator == null) || GenericUtils.isEmpty(args)) - ? null : setupClientSession(SCP_PORT_OPTION, stdin, level, stdout, stderr, args); - if (session == null) { - stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i identity] [-io nio2|mina|netty]" - + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value] [-o creator=class name]" - + " [-c cipherlist] [-m maclist] [-J proxyJump] [-w password] [-C] <source> <target>"); - stderr.println(); - stderr.println("Where <source> or <target> are either 'user@host:file' or a local file path"); - stderr.println("NOTE: exactly ONE of the source or target must be remote and the other one local"); - System.exit(-1); - return; // not that we really need it... - } - - try { - // see the way normalizeCommandArguments works... - Collection<Option> options = EnumSet.noneOf(Option.class); - boolean quiet = false; - for (int index = 0; index < numArgs; index++) { - String argName = args[index]; - if ("-r".equals(argName)) { - options.add(Option.Recursive); - } else if ("-p".equals(argName)) { - options.add(Option.PreserveAttributes); - } else if ("-q".equals(argName)) { - quiet = true; - } - } - - if (!quiet) { - creator.setScpTransferEventListener(new ScpTransferEventListener() { - @Override - public void startFolderEvent( - Session session, FileOperation op, Path file, Set<PosixFilePermission> perms) { - logEvent("startFolderEvent", session, op, file, -1L, perms, null); - } - - @Override - public void endFolderEvent( - Session session, FileOperation op, Path file, Set<PosixFilePermission> perms, - Throwable thrown) { - logEvent("endFolderEvent", session, op, file, -1L, perms, thrown); - } - - @Override - public void startFileEvent( - Session session, FileOperation op, Path file, long length, Set<PosixFilePermission> perms) { - logEvent("startFileEvent", session, op, file, length, perms, null); - } - - @Override - public void endFileEvent( - Session session, FileOperation op, Path file, long length, Set<PosixFilePermission> perms, - Throwable thrown) { - logEvent("endFileEvent", session, op, file, length, perms, thrown); - } + // see the way normalizeCommandArguments works... + ScpLocation source = (numArgs >= 2) ? new ScpLocation(args[numArgs - 2]) : null; + ScpLocation target = (numArgs >= 2) ? new ScpLocation(args[numArgs - 1]) : null; - private void logEvent( - String name, Session session, FileOperation op, Path file, long length, - Collection<PosixFilePermission> perms, Throwable thrown) { - PrintStream ps = (thrown == null) ? stdout : stderr; - ps.append(" ").append(name) - .append('[').append(session.toString()).append(']') - .append('[').append(op.name()).append(']') - .append(' ').append(file.toString()); - if (length > 0L) { - ps.append(' ').append("length=").append(Long.toString(length)); - } - ps.append(' ').append(String.valueOf(perms)); - - if (thrown != null) { - ps.append(" - ").append(thrown.getClass().getSimpleName()).append(": ") - .append(thrown.getMessage()); - } - ps.println(); - } - }); + Collection<Option> options = parseCopyOptions(args); + boolean quiet = false; + boolean threeWay = false; + for (int index = 0; index < numArgs; index++) { + String argName = args[index]; + if ("-q".equals(argName)) { + quiet = true; + } else if (SCP_REMOTE_TO_REMOTE_OPTION.equals(argName)) { + threeWay = true; } + } - ScpClient client = creator.createScpClient(session); - ScpLocation source = new ScpLocation(args[numArgs - 2]); - ScpLocation target = new ScpLocation(args[numArgs - 1]); - if (source.isLocal()) { - client.upload(source.getPath(), target.getPath(), options); - } else { - client.download(source.getPath(), target.getPath(), options); - } - } finally { - session.close(); + if (threeWay) { + xferRemoteToRemote(stdin, stdout, stderr, args, source, target, options, logStream, level, quiet); + } else { + xferLocalToRemote(stdin, stdout, stderr, args, source, target, options, logStream, level, quiet); } } finally { if ((logStream != stdout) && (logStream != stderr)) { diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java index e328eec..52410b7 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientUserAuthService.java @@ -279,9 +279,8 @@ public class ClientUserAuthService extends AbstractCloseable implements Service, protected void tryNext(int cmd) throws Exception { ClientSession session = getClientSession(); - boolean debugEnabled = log.isDebugEnabled(); // Loop until we find something to try - while (true) { + for (boolean debugEnabled = log.isDebugEnabled();; debugEnabled = log.isDebugEnabled()) { if (userAuth == null) { if (debugEnabled) { log.debug("tryNext({}) starting authentication mechanisms: client={}, server={}", diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpClient.java index ef781bc..c262ad6 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpClient.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpClient.java @@ -40,9 +40,23 @@ import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails; */ public interface ScpClient extends SessionHolder<ClientSession>, ClientSessionHolder { enum Option { - Recursive, - PreserveAttributes, - TargetIsDirectory + Recursive("-r"), + PreserveAttributes("-p"), + TargetIsDirectory("-d"), + ; + + private final String optionValue; + + Option(String optionValue) { + this.optionValue = optionValue; + } + + /** + * @return The option value to use in the issued SCP command + */ + public String getOptionValue() { + return optionValue; + } } @Override @@ -136,15 +150,9 @@ public interface ScpClient extends SessionHolder<ClientSession>, ClientSessionHo static String createSendCommand(String remote, Collection<Option> options) { StringBuilder sb = new StringBuilder(remote.length() + Long.SIZE).append(ScpHelper.SCP_COMMAND_PREFIX); - if (options.contains(Option.Recursive)) { - sb.append(" -r"); - } - if (options.contains(Option.TargetIsDirectory)) { - sb.append(" -d"); - } - if (options.contains(Option.PreserveAttributes)) { - sb.append(" -p"); - } + appendCommandOption(sb, options, Option.TargetIsDirectory); + appendCommandOption(sb, options, Option.Recursive); + appendCommandOption(sb, options, Option.PreserveAttributes); sb.append(" -t").append(" --").append(' ').append(remote); return sb.toString(); @@ -153,14 +161,26 @@ public interface ScpClient extends SessionHolder<ClientSession>, ClientSessionHo static String createReceiveCommand(String remote, Collection<Option> options) { ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified"); StringBuilder sb = new StringBuilder(remote.length() + Long.SIZE).append(ScpHelper.SCP_COMMAND_PREFIX); - if (options.contains(Option.Recursive)) { - sb.append(" -r"); - } - if (options.contains(Option.PreserveAttributes)) { - sb.append(" -p"); - } + appendCommandOption(sb, options, Option.Recursive); + appendCommandOption(sb, options, Option.PreserveAttributes); sb.append(" -f").append(" --").append(' ').append(remote); return sb.toString(); } + + /** + * Appends the specified option command value if appears in provided options collection + * + * @param sb The {@link StringBuilder} target + * @param options The command options - ignored if {@code null} + * @param opt The required option + * @return The updated builder + */ + static StringBuilder appendCommandOption(StringBuilder sb, Collection<Option> options, Option opt) { + if (GenericUtils.isNotEmpty(options) && options.contains(opt)) { + sb.append(' ').append(opt.getOptionValue()); + } + + return sb; + } } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java index 07a8ae2..387ebbd 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/ScpRemote2RemoteTransferHelper.java @@ -38,6 +38,7 @@ import org.apache.sshd.common.util.io.LimitInputStream; import org.apache.sshd.common.util.logging.AbstractLoggingBean; import org.apache.sshd.scp.client.ScpClient.Option; import org.apache.sshd.scp.common.helpers.AbstractScpCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpAckInfo; import org.apache.sshd.scp.common.helpers.ScpDirEndCommandDetails; import org.apache.sshd.scp.common.helpers.ScpIoUtils; import org.apache.sshd.scp.common.helpers.ScpPathCommandDetailsSupport; @@ -143,8 +144,8 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { ChannelExec dstChannel = ScpIoUtils.openCommandChannel(dstSession, dstCmd, log); try (InputStream dstIn = dstChannel.getInvertedOut(); OutputStream dstOut = dstChannel.getInvertedIn()) { - int statusCode = transferStatusCode("XFER-CMD", dstIn, srcOut); - ScpIoUtils.validateCommandStatusCode("XFER-CMD", "executeTransfer", statusCode, false); + ScpAckInfo ackInfo = transferStatusCode("XFER-CMD", dstIn, srcOut); + ackInfo.validateCommandStatusCode("XFER-CMD", "executeTransfer"); if (srcOptions.contains(Option.TargetIsDirectory) || dstOptions.contains(Option.TargetIsDirectory)) { redirectDirectoryTransfer(source, srcIn, srcOut, destination, dstIn, dstOut, 0); @@ -163,8 +164,13 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { String source, InputStream srcIn, OutputStream srcOut, String destination, InputStream dstIn, OutputStream dstOut) throws IOException { + Object data = receiveNextCmd("redirectFileTransfer", srcIn); + if (data instanceof ScpAckInfo) { + throw new StreamCorruptedException("Unexpected ACK instead of header: " + data); + } + boolean debugEnabled = log.isDebugEnabled(); - String header = ScpIoUtils.readLine(srcIn, false); + String header = (String) data; if (debugEnabled) { log.debug("redirectFileTransfer({}) {} => {}: header={}", this, source, destination, header); } @@ -194,8 +200,8 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { } ScpIoUtils.writeLine(dstOut, header); - int statusCode = transferStatusCode(header, dstIn, srcOut); - ScpIoUtils.validateCommandStatusCode("[DST] " + header, "handleFileTransferRequest", statusCode, false); + ScpAckInfo ackInfo = transferStatusCode(header, dstIn, srcOut); + ackInfo.validateCommandStatusCode("[DST] " + header, "handleFileTransferRequest"); ScpReceiveFileCommandDetails fileDetails = new ScpReceiveFileCommandDetails(header); signalReceivedCommand(fileDetails); @@ -228,8 +234,13 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { String destination, InputStream dstIn, OutputStream dstOut, int depth) throws IOException { + Object data = receiveNextCmd("redirectDirectoryTransfer", srcIn); + if (data instanceof ScpAckInfo) { + throw new StreamCorruptedException("Unexpected ACK instead of header: " + data); + } + + String header = (String) data; boolean debugEnabled = log.isDebugEnabled(); - String header = ScpIoUtils.readLine(srcIn, false); if (debugEnabled) { log.debug("redirectDirectoryTransfer({})[depth={}] {} => {}: header={}", this, depth, source, destination, header); @@ -262,9 +273,8 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { } ScpIoUtils.writeLine(dstOut, header); - int statusCode = transferStatusCode(header, dstIn, srcOut); - ScpIoUtils.validateCommandStatusCode("[DST@" + depth + "] " + header, "handleDirectoryTransferRequest", statusCode, - false); + ScpAckInfo ackInfo = transferStatusCode(header, dstIn, srcOut); + ackInfo.validateCommandStatusCode("[DST@" + depth + "] " + header, "handleDirectoryTransferRequest"); ScpReceiveDirCommandDetails dirDetails = new ScpReceiveDirCommandDetails(header); signalReceivedCommand(dirDetails); @@ -284,7 +294,12 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { for (boolean debugEnabled = log.isDebugEnabled(), dirEndSignal = false; !dirEndSignal; debugEnabled = log.isDebugEnabled()) { - header = ScpIoUtils.readLine(srcIn, false); + Object data = receiveNextCmd("handleDirectoryTransferRequest", srcIn); + if (data instanceof ScpAckInfo) { + throw new StreamCorruptedException("Unexpected ACK instead of header: " + data); + } + + header = (String) data; if (debugEnabled) { log.debug("handleDirectoryTransferRequest({})[depth={}] {} => {}: header={}", this, depth, source, destination, header); @@ -325,9 +340,8 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { case ScpDirEndCommandDetails.COMMAND_NAME: { ScpIoUtils.writeLine(dstOut, header); - statusCode = transferStatusCode(header, dstIn, srcOut); - ScpIoUtils.validateCommandStatusCode("[DST@" + depth + "] " + header, "handleDirectoryTransferRequest", - statusCode, false); + ackInfo = transferStatusCode(header, dstIn, srcOut); + ackInfo.validateCommandStatusCode("[DST@" + depth + "] " + header, "handleDirectoryTransferRequest"); ScpDirEndCommandDetails details = ScpDirEndCommandDetails.parse(header); signalReceivedCommand(details); @@ -363,7 +377,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { long xferCount; try (InputStream inputStream = new LimitInputStream(srcIn, length)) { - ScpIoUtils.ack(srcOut); // ready to receive the data from source + ScpAckInfo.sendOk(srcOut); // ready to receive the data from source xferCount = IoUtils.copy(inputStream, dstOut); dstOut.flush(); // make sure all data sent to destination } @@ -374,12 +388,12 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { } // wait for source to signal data finished and pass it along - int statusCode = transferStatusCode("SRC-EOF", srcIn, dstOut); - ScpIoUtils.validateCommandStatusCode("[SRC-EOF] " + header, "transferSimpleFile", statusCode, false); + ScpAckInfo ackInfo = transferStatusCode("SRC-EOF", srcIn, dstOut); + ackInfo.validateCommandStatusCode("[SRC-EOF] " + header, "transferSimpleFile"); // wait for destination to signal data received - statusCode = ScpIoUtils.readAck(dstIn, false, log, "DST-EOF"); - ScpIoUtils.validateCommandStatusCode("[DST-EOF] " + header, "transferSimpleFile", statusCode, false); + ackInfo = ScpAckInfo.readAck(dstIn, false); + ackInfo.validateCommandStatusCode("[DST-EOF] " + header, "transferSimpleFile"); return xferCount; } @@ -389,35 +403,48 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean { String header) throws IOException { ScpIoUtils.writeLine(dstOut, header); - int statusCode = transferStatusCode(header, dstIn, srcOut); - ScpIoUtils.validateCommandStatusCode("[DST] " + header, "transferTimestampCommand", statusCode, false); + ScpAckInfo ackInfo = transferStatusCode(header, dstIn, srcOut); + ackInfo.validateCommandStatusCode("[DST] " + header, "transferTimestampCommand"); - header = ScpIoUtils.readLine(srcIn, false); - return header; + Object data = receiveNextCmd("transferTimestampCommand", srcIn); + if (data instanceof ScpAckInfo) { + throw new StreamCorruptedException("Unexpected ACK instead of header: " + data); + } + return (String) data; } - protected int transferStatusCode(Object logHint, InputStream in, OutputStream out) throws IOException { - int statusCode = in.read(); - if (statusCode == -1) { - throw new EOFException("readAck(" + logHint + ") - EOF before ACK"); + protected ScpAckInfo transferStatusCode(Object logHint, InputStream in, OutputStream out) throws IOException { + ScpAckInfo ackInfo = ScpAckInfo.readAck(in, false); + if (log.isDebugEnabled()) { + log.debug("transferStatusCode({})[{}] {}", this, logHint, ackInfo); + } + ackInfo.send(out); + return ackInfo; + } + + // NOTE: we rely on the fact that an SCP command does not start with an ACK code + protected Object receiveNextCmd(Object logHint, InputStream in) throws IOException { + int c = in.read(); + if (c == -1) { + throw new EOFException(logHint + " - premature EOF while waiting for next command"); } - if (statusCode != ScpIoUtils.OK) { - String line = ScpIoUtils.readLine(in); + if (c == ScpAckInfo.OK) { if (log.isDebugEnabled()) { - log.debug("transferStatusCode({})[{}] status={}, line='{}'", this, logHint, statusCode, line); + log.debug("receiveNextCmd({})[{}] - ACK={}", this, logHint, c); } - out.write(statusCode); - ScpIoUtils.writeLine(out, line); - } else { + return new ScpAckInfo(c); + } + + String line = ScpIoUtils.readLine(in, false); + if ((c == ScpAckInfo.WARNING) || (c == ScpAckInfo.ERROR)) { if (log.isDebugEnabled()) { - log.debug("transferStatusCode({})[{}] status={}", this, logHint, statusCode); + log.debug("receiveNextCmd({})[{}] - ACK={}", this, logHint, new ScpAckInfo(c, line)); } - out.write(statusCode); - out.flush(); + return new ScpAckInfo(c, line); } - return statusCode; + return Character.toString((char) c) + line; } // Useful "hook" for implementors diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java index 1a68d86..5db850c 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java @@ -18,6 +18,7 @@ */ package org.apache.sshd.scp.common; +import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -46,6 +47,7 @@ import org.apache.sshd.common.util.io.LimitInputStream; import org.apache.sshd.common.util.logging.AbstractLoggingBean; import org.apache.sshd.scp.common.ScpTransferEventListener.FileOperation; import org.apache.sshd.scp.common.helpers.DefaultScpFileOpener; +import org.apache.sshd.scp.common.helpers.ScpAckInfo; import org.apache.sshd.scp.common.helpers.ScpDirEndCommandDetails; import org.apache.sshd.scp.common.helpers.ScpIoUtils; import org.apache.sshd.scp.common.helpers.ScpPathCommandDetailsSupport; @@ -167,14 +169,15 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess * @throws IOException If failed to read/write */ protected void receive(ScpReceiveLineHandler handler) throws IOException { - ack(); + sendOk(); boolean debugEnabled = log.isDebugEnabled(); Session session = getSession(); for (ScpTimestampCommandDetails time = null;; debugEnabled = log.isDebugEnabled()) { String line; boolean isDir = false; - int c = readAck(true); + + int c = receiveNextCmd(); switch (c) { case -1: return; @@ -200,7 +203,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess log.debug("receive({}) - Received 'T' header: {}", this, line); } time = ScpTimestampCommandDetails.parse(line); - ack(); + sendOk(); continue; case ScpDirEndCommandDetails.COMMAND_NAME: line = ScpIoUtils.readLine(in); @@ -208,7 +211,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess if (debugEnabled) { log.debug("receive({}) - Received 'E' header: {}", this, line); } - ack(); + sendOk(); return; default: // a real ack that has been acted upon already @@ -223,6 +226,27 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } } + // NOTE: we rely on the fact that an SCP command does not start with an ACK code + protected int receiveNextCmd() throws IOException { + int c = in.read(); + if (c == -1) { + return c; + } + + if (c == ScpAckInfo.OK) { + return c; + } + + if ((c == ScpAckInfo.WARNING) || (c == ScpAckInfo.ERROR)) { + String line = ScpIoUtils.readLine(in, true); + if (log.isDebugEnabled()) { + log.debug("receiveNextCmd - ACK={}", new ScpAckInfo(c, line)); + } + } + + return c; + } + public void receiveDir(String header, Path local, ScpTimestampCommandDetails time, boolean preserve, int bufferSize) throws IOException { Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath(); @@ -243,7 +267,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess Set<PosixFilePermission> perms = details.getPermissions(); Path file = opener.resolveIncomingFilePath(session, path, name, preserve, perms, time); - ack(); + sendOk(); time = null; @@ -263,11 +287,11 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess receiveDir(header, file, time, preserve, bufferSize); time = null; } else if (cmdChar == ScpDirEndCommandDetails.COMMAND_NAME) { - ack(); + sendOk(); break; } else if (cmdChar == ScpTimestampCommandDetails.COMMAND_NAME) { time = ScpTimestampCommandDetails.parse(header); - ack(); + sendOk(); } else { throw new IOException("Unexpected message: '" + header + "'"); } @@ -333,7 +357,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess try (InputStream is = new LimitInputStream(this.in, length); OutputStream os = resolver.resolveTargetStream(session, name, length, perms, IoUtils.EMPTY_OPEN_OPTIONS)) { - ack(); + sendOk(); Path file = resolver.getEventListenerFilePath(); listener.startFileEvent(session, FileOperation.RECEIVE, file, length, perms); @@ -349,13 +373,13 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess resolver.postProcessReceivedData(name, preserve, perms, time); - ack(); + sendOk(); - int replyCode = readAck(false); + ScpAckInfo ackInfo = readAck(false); if (debugEnabled) { - log.debug("receiveStream({})[{}] ack reply code={}", this, resolver, replyCode); + log.debug("receiveStream({})[{}] ACK={}", this, resolver, ackInfo); } - validateAckReplyCode("receiveStream", resolver, replyCode, false); + validateAckReplyCode("receiveStream", resolver, ackInfo); } public String readLine() throws IOException { @@ -367,12 +391,12 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } public void send(Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException { - int readyCode = readAck(false); + ScpAckInfo ackInfo = readAck(false); boolean debugEnabled = log.isDebugEnabled(); if (debugEnabled) { - log.debug("send({}) ready code={}", paths, readyCode); + log.debug("send({}) ACK={}", paths, ackInfo); } - validateOperationReadyCode("send", "Paths", readyCode, false); + validateOperationReadyCode("send", "Paths", ackInfo); LinkOption[] options = IoUtils.getLinkOptions(true); for (String pattern : paths) { @@ -420,11 +444,11 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess public void sendPaths(Collection<? extends Path> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException { - int readyCode = readAck(false); + ScpAckInfo ackInfo = readAck(false); if (log.isDebugEnabled()) { - log.debug("sendPaths({}) ready code={}", paths, readyCode); + log.debug("sendPaths({}) ACK={}", paths, ackInfo); } - validateOperationReadyCode("sendPaths", "Paths", readyCode, false); + validateOperationReadyCode("sendPaths", "Paths", ackInfo); LinkOption[] options = IoUtils.getLinkOptions(true); for (Path file : paths) { @@ -513,13 +537,13 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess ScpTimestampCommandDetails time = resolver.getTimestamp(); if (preserve && (time != null)) { - int readyCode = ScpIoUtils.sendTimeCommand(in, out, time, log, this); + ScpAckInfo ackInfo = ScpIoUtils.sendAcknowledgedCommand(time, in, out); String cmd = time.toHeader(); if (debugEnabled) { - log.debug("sendStream({})[{}] command='{}' ready code={}", this, resolver, cmd, readyCode); + log.debug("sendStream({})[{}] command='{}' ACK={}", this, resolver, cmd, ackInfo); } - validateAckReplyCode(cmd, resolver, readyCode, false); + validateAckReplyCode(cmd, resolver, ackInfo); } Set<PosixFilePermission> perms = EnumSet.copyOf(resolver.getPermissions()); @@ -532,12 +556,12 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess log.debug("sendStream({})[{}] send 'C' command: {}", this, resolver, cmd); } - int readyCode = sendAcknowledgedCommand(cmd); + ScpAckInfo ackInfo = sendAcknowledgedCommand(cmd); if (debugEnabled) { - log.debug("sendStream({})[{}] command='{}' ready code={}", this, resolver, - cmd.substring(0, cmd.length() - 1), readyCode); + log.debug("sendStream({})[{}] command='{}' ACK={}", this, resolver, + cmd.substring(0, cmd.length() - 1), ackInfo); } - validateAckReplyCode(cmd, resolver, readyCode, false); + validateAckReplyCode(cmd, resolver, ackInfo); Session session = getSession(); try (InputStream in = resolver.resolveSourceStream(session, fileSize, perms, IoUtils.EMPTY_OPEN_OPTIONS)) { @@ -552,28 +576,50 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess listener.endFileEvent(session, FileOperation.SEND, path, fileSize, perms, null); resolver.closeSourceStream(session, fileSize, perms, in); } - ack(); + sendOk(); - readyCode = readAck(false); + ackInfo = readAck(false); if (debugEnabled) { - log.debug("sendStream({})[{}] command='{}' reply code={}", this, resolver, cmd, readyCode); + log.debug("sendStream({})[{}] command='{}' ACK={}", this, resolver, cmd, ackInfo); } - validateAckReplyCode("sendStream", resolver, readyCode, false); + validateAckReplyCode("sendStream", resolver, ackInfo); } - protected void validateOperationReadyCode(String command, Object location, int readyCode, boolean eofAllowed) + protected void validateOperationReadyCode(String command, Object location, ScpAckInfo ackInfo) throws IOException { - validateCommandStatusCode(command, location, readyCode, eofAllowed); + validateCommandStatusCode(command, location, ackInfo, false); } - protected void validateAckReplyCode(String command, Object location, int replyCode, boolean eofAllowed) + protected void validateAckReplyCode(String command, Object location, ScpAckInfo ackInfo) throws IOException { - validateCommandStatusCode(command, location, replyCode, eofAllowed); + validateCommandStatusCode(command, location, ackInfo, false); } - protected void validateCommandStatusCode(String command, Object location, int statusCode, boolean eofAllowed) + protected void validateCommandStatusCode(String command, Object location, ScpAckInfo ackInfo, boolean eofAllowed) throws IOException { - ScpIoUtils.validateCommandStatusCode(command, location, statusCode, eofAllowed); + if (ackInfo == null) { + if (eofAllowed) { + return; + } + + log.error("validateCommandStatusCode({})[{}] unexpected EOF while waiting on ACK for command={}", + this, location, command); + throw new EOFException("EOF while waiting on ACK for command=" + command + " at " + location); + } + + int statusCode = ackInfo.getStatusCode(); + switch (statusCode) { + case ScpAckInfo.OK: + break; + case ScpAckInfo.WARNING: + log.warn("validateCommandStatusCode({})[{}] advisory ACK={} for command={}", + this, location, ackInfo, command); + break; + default: + log.error("validateCommandStatusCode({})[{}] bad ACK={} for command={}", + this, location, ackInfo, command); + ackInfo.validateCommandStatusCode(command, location); // this actually throws an SCPException + } } public void sendDir(Path local, boolean preserve, int bufferSize) throws IOException { @@ -597,13 +643,13 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess lastAccess, cmd); } - int readyCode = sendAcknowledgedCommand(cmd); + ScpAckInfo ackInfo = sendAcknowledgedCommand(cmd); if (debugEnabled) { if (debugEnabled) { - log.debug("sendDir({})[{}] command='{}' ready code={}", this, path, cmd, readyCode); + log.debug("sendDir({})[{}] command='{}' ACK={}", this, path, cmd, ackInfo); } } - validateAckReplyCode(cmd, path, readyCode, false); + validateAckReplyCode(cmd, path, ackInfo); } Set<PosixFilePermission> perms = opener.getLocalFilePermissions(session, path, options); @@ -616,11 +662,11 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess log.debug("sendDir({})[{}] send 'D' command: {}", this, path, cmd); } - int readyCode = sendAcknowledgedCommand(cmd); + ScpAckInfo ackInfo = sendAcknowledgedCommand(cmd); if (debugEnabled) { - log.debug("sendDir({})[{}] command='{}' ready code={}", this, path, cmd, readyCode); + log.debug("sendDir({})[{}] command='{}' ACK={}", this, path, cmd, ackInfo); } - validateAckReplyCode(cmd, path, readyCode, false); + validateAckReplyCode(cmd, path, ackInfo); try (DirectoryStream<Path> children = opener.getLocalFolderChildren(session, path)) { listener.startFolderEvent(session, FileOperation.SEND, path, perms); @@ -645,35 +691,35 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess log.debug("sendDir({})[{}] send 'E' command", this, path); } - readyCode = sendAcknowledgedCommand(ScpDirEndCommandDetails.HEADER); + ackInfo = sendAcknowledgedCommand(ScpDirEndCommandDetails.HEADER); if (debugEnabled) { - log.debug("sendDir({})[{}] 'E' command reply code=", this, path, readyCode); + log.debug("sendDir({})[{}] 'E' command ACK={}", this, path, ackInfo); } - validateAckReplyCode(ScpDirEndCommandDetails.HEADER, path, readyCode, false); + validateAckReplyCode(ScpDirEndCommandDetails.HEADER, path, ackInfo); } - protected int sendAcknowledgedCommand(String cmd) throws IOException { - return ScpIoUtils.sendAcknowledgedCommand(cmd, in, out, log); + protected ScpAckInfo sendAcknowledgedCommand(String cmd) throws IOException { + return ScpIoUtils.sendAcknowledgedCommand(cmd, in, out); + } + + public void sendOk() throws IOException { + sendResponseMessage(ScpAckInfo.OK, null /* ignored */); } protected void sendWarning(String message) throws IOException { - sendResponseMessage(ScpIoUtils.WARNING, message); + sendResponseMessage(ScpAckInfo.WARNING, message); } protected void sendError(String message) throws IOException { - sendResponseMessage(ScpIoUtils.ERROR, message); + sendResponseMessage(ScpAckInfo.ERROR, message); } protected void sendResponseMessage(int level, String message) throws IOException { - ScpIoUtils.sendResponseMessage(out, level, message); - } - - public void ack() throws IOException { - ScpIoUtils.ack(out); + ScpAckInfo.sendAck(out, level, message); } - public int readAck(boolean canEof) throws IOException { - return ScpIoUtils.readAck(in, canEof, log, this); + public ScpAckInfo readAck(boolean canEof) throws IOException { + return ScpAckInfo.readAck(in, canEof); } @Override diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpLocation.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpLocation.java index 9e0a04d..4605bbc 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpLocation.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpLocation.java @@ -22,18 +22,21 @@ package org.apache.sshd.scp.common; import java.io.Serializable; import java.util.Objects; +import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.auth.MutableUserHolder; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.OsUtils; import org.apache.sshd.common.util.ValidateUtils; /** - * Represents a local or remote SCP location in the format {@code user@host:path} for a remote path and a simple path - * for a local one. If user is omitted for a remote path then current user is used. - * + * Represents a local or remote SCP location in the format "user@host:path" or + * "scp://[user@]host[:port][/path]" for a remote path and a simple path for a local one. If user is omitted + * for a remote path then current user is used. + * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { + public static final String SCHEME = "scp://"; public static final char HOST_PART_SEPARATOR = ':'; public static final char USERNAME_PART_SEPARATOR = '@'; @@ -42,6 +45,7 @@ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { private String host; private String username; private String path; + private int port; public ScpLocation() { this(null); @@ -56,6 +60,17 @@ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { update(locSpec, this); } + public ScpLocation(String username, String host, String path) { + this(username, host, 0, path); + } + + public ScpLocation(String username, String host, int port, String path) { + this.username = username; + this.host = host; + this.port = port; + this.path = path; + } + public String getHost() { return host; } @@ -68,6 +83,19 @@ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { return GenericUtils.isEmpty(getHost()); } + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public int resolvePort() { + int portValue = getPort(); + return (portValue <= 0) ? SshConstants.DEFAULT_PORT : portValue; + } + @Override public String getUsername() { return username; @@ -104,7 +132,10 @@ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { @Override public int hashCode() { - return Objects.hash(getHost(), resolveUsername(), OsUtils.getComparablePath(getPath())); + return isLocal() + ? Objects.hashCode(OsUtils.getComparablePath(getPath())) + : Objects.hash(getHost(), resolveUsername(), OsUtils.getComparablePath(getPath())) + + 31 * Integer.hashCode(resolvePort()); } @Override @@ -120,7 +151,8 @@ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { } ScpLocation other = (ScpLocation) obj; - if (this.isLocal() != other.isLocal()) { + boolean thisLocal = this.isLocal(); + if (thisLocal != other.isLocal()) { return false; } @@ -130,13 +162,14 @@ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { return false; } - if (isLocal()) { + if (thisLocal) { return true; } // we know other is also remote or we would not have reached this point return Objects.equals(resolveUsername(), other.resolveUsername()) - && Objects.equals(getHost(), other.getHost()); + && Objects.equals(getHost(), other.getHost()) + && (resolvePort() == other.resolvePort()); } @Override @@ -155,15 +188,25 @@ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { return p; } - return resolveUsername() - + Character.toString(USERNAME_PART_SEPARATOR) - + getHost() - + Character.toString(HOST_PART_SEPARATOR) - + p; + int portValue = resolvePort(); + String userValue = resolveUsername(); + StringBuilder sb = new StringBuilder(); + if (portValue != SshConstants.DEFAULT_PORT) { + sb.append(SCHEME); + } + sb.append(userValue).append(USERNAME_PART_SEPARATOR).append(getHost()); + sb.append(HOST_PART_SEPARATOR); + if (portValue != SshConstants.DEFAULT_PORT) { + sb.append(portValue); + } + sb.append(p); + + return sb.toString(); } /** - * Parses a local or remote SCP location in the format {@code user@host:path} + * Parses a local or remote SCP location in the format "user@host:path" or + * "scp://[user@]host[:port][/path]" * * @param locSpec The location specification - ignored if {@code null}/empty * @return The {@link ScpLocation} or {@code null} if no specification provider @@ -175,52 +218,72 @@ public class ScpLocation implements MutableUserHolder, Serializable, Cloneable { } /** - * Parses a local or remote SCP location in the format {@code user@host:path} + * Parses a local or remote SCP location in the format "user@host:path" or + * "scp://[user@]host[:port][/path]" * * @param <L> Type of {@link ScpLocation} being updated - * @param locSpec The location specification - ignored if {@code null}/empty + * @param spec The location specification - ignored if {@code null}/empty * @param location The {@link ScpLocation} to update - never {@code null} * @return The updated location (unless no specification) * @throws IllegalArgumentException if invalid specification */ - public static <L extends ScpLocation> L update(String locSpec, L location) { + public static <L extends ScpLocation> L update(String spec, L location) { Objects.requireNonNull(location, "No location to update"); - if (GenericUtils.isEmpty(locSpec)) { + if (GenericUtils.isEmpty(spec)) { return location; } location.setHost(null); location.setUsername(null); - - int pos = locSpec.indexOf(HOST_PART_SEPARATOR); - if (pos < 0) { // assume a local path - location.setPath(locSpec); - return location; - } - - /* - * NOTE !!! in such a case there may be confusion with a host named 'a', but there is a limit to how smart we - * can be... - */ - if ((pos == 1) && OsUtils.isWin32()) { - char drive = locSpec.charAt(0); - if (((drive >= 'a') && (drive <= 'z')) || ((drive >= 'A') && (drive <= 'Z'))) { - location.setPath(locSpec); + location.setPort(0); + + String login; + if (spec.startsWith(SCHEME)) { + int pos = spec.indexOf('/', SCHEME.length()); + ValidateUtils.checkTrue(pos > 0, "Invalid remote specification (missing path specification): %s", spec); + + login = spec.substring(SCHEME.length(), pos); + location.setPath(spec.substring(pos)); + + pos = login.indexOf(HOST_PART_SEPARATOR); + ValidateUtils.checkTrue(pos != 0, "Invalid remote specification (malformed port specification): %s", spec); + if (pos > 0) { + ValidateUtils.checkTrue(pos < (login.length() - 1), "Invalid remote specification (no port specification): %s", + spec); + location.setPort(Integer.parseInt(login.substring(pos + 1))); + login = login.substring(0, pos); + } + } else { + int pos = spec.indexOf(HOST_PART_SEPARATOR); + if (pos < 0) { // assume a local path + location.setPath(spec); return location; } - } - String login = locSpec.substring(0, pos); - ValidateUtils.checkTrue(pos < (locSpec.length() - 1), "Invalid remote specification (missing path): %s", locSpec); - location.setPath(locSpec.substring(pos + 1)); + /* + * NOTE !!! in such a case there may be confusion with a host named 'a', but there is a limit to how smart we + * can be... + */ + if ((pos == 1) && OsUtils.isWin32()) { + char drive = spec.charAt(0); + if (((drive >= 'a') && (drive <= 'z')) || ((drive >= 'A') && (drive <= 'Z'))) { + location.setPath(spec); + return location; + } + } + + login = spec.substring(0, pos); + ValidateUtils.checkTrue(pos < (spec.length() - 1), "Invalid remote specification (missing path): %s", spec); + location.setPath(spec.substring(pos + 1)); + } - pos = login.indexOf(USERNAME_PART_SEPARATOR); - ValidateUtils.checkTrue(pos != 0, "Invalid remote specification (missing username): %s", locSpec); + int pos = login.indexOf(USERNAME_PART_SEPARATOR); + ValidateUtils.checkTrue(pos != 0, "Invalid remote specification (missing username): %s", spec); if (pos < 0) { location.setHost(login); } else { location.setUsername(login.substring(0, pos)); - ValidateUtils.checkTrue(pos < (login.length() - 1), "Invalid remote specification (missing host): %s", locSpec); + ValidateUtils.checkTrue(pos < (login.length() - 1), "Invalid remote specification (missing host): %s", spec); location.setHost(login.substring(pos + 1)); } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpAckInfo.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpAckInfo.java new file mode 100644 index 0000000..b89e9db --- /dev/null +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpAckInfo.java @@ -0,0 +1,130 @@ +/* + * 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.scp.common.helpers; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.scp.common.ScpException; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class ScpAckInfo { + // ACK status codes + public static final int OK = 0; + public static final int WARNING = 1; + public static final int ERROR = 2; + + private final int statusCode; + private final String line; + + public ScpAckInfo(int statusCode) { + this(statusCode, null); + } + + public ScpAckInfo(int statusCode, String line) { + ValidateUtils.checkTrue(statusCode >= 0, "Invalid status code: %d", statusCode); + + this.statusCode = statusCode; + this.line = line; + } + + public int getStatusCode() { + return statusCode; + } + + public String getLine() { + return line; + } + + public <O extends OutputStream> O send(O out) throws IOException { + return sendAck(out, getStatusCode(), getLine()); + } + + public void validateCommandStatusCode(String command, Object location) throws IOException { + int code = getStatusCode(); + if ((code != OK) && (code != WARNING)) { + throw new ScpException( + "Bad reply code (" + code + ") for command='" + command + "' at " + location + ": " + getLine(), code); + } + } + + @Override + public String toString() { + int code = getStatusCode(); + String l = getLine(); + // OK code has no line + if ((code == OK) || GenericUtils.isEmpty(l)) { + return Integer.toString(code); + } else { + return code + ": " + l; + } + } + + public static ScpAckInfo readAck(InputStream in, boolean canEof) throws IOException { + int statusCode = in.read(); + if (statusCode == -1) { + if (canEof) { + return null; + } + throw new EOFException("readAck - EOF before ACK"); + } + + if (statusCode == OK) { + return new ScpAckInfo(statusCode); // OK status has no extra data + } + + String line = ScpIoUtils.readLine(in); + return new ScpAckInfo(statusCode, line); + } + + /** + * Sends {@link #OK} ACK code + * + * @param out The target {@link OutputStream} + * @throws IOException If failed to send the ACK code + */ + public static void sendOk(OutputStream out) throws IOException { + sendAck(out, OK, null /* ignored */); + } + + public static <O extends OutputStream> O sendWarning(O out, String message) throws IOException { + return sendAck(out, ScpAckInfo.WARNING, (message == null) ? "" : message); + } + + public static <O extends OutputStream> O sendError(O out, String message) throws IOException { + return sendAck(out, ScpAckInfo.ERROR, (message == null) ? "" : message); + } + + public static <O extends OutputStream> O sendAck(O out, int level, String message) throws IOException { + out.write(level); + if (level != OK) { + ScpIoUtils.writeLine(out, message); // this also flushes + } else { + out.flush(); + } + return out; + } +} diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java index 775a502..4ad78f2 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java @@ -45,11 +45,6 @@ import org.slf4j.Logger; * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public final class ScpIoUtils { - // ACK status codes - public static final int OK = 0; - public static final int WARNING = 1; - public static final int ERROR = 2; - public static final Set<ClientChannelEvent> COMMAND_WAIT_EVENTS = Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.CLOSED)); @@ -80,137 +75,21 @@ public final class ScpIoUtils { } public static void writeLine(OutputStream out, String cmd) throws IOException { - out.write(cmd.getBytes(StandardCharsets.UTF_8)); + if (cmd != null) { + out.write(cmd.getBytes(StandardCharsets.UTF_8)); + } out.write('\n'); out.flush(); } - /** - * Sends the "T..." command and waits for ACK - * - * @param in The {@link InputStream} to read from - * @param out The target {@link OutputStream} - * @param time The {@link ScpTimestampCommandDetails} value to send - * @param log An optional {@link Logger} to use for issuing log messages - ignored if {@code null} - * @param logHint An optional hint to be used in the logged messages to identifier the caller's context - * @return The read ACK value - * @throws IOException If failed to complete the read/write cyle - */ - public static int sendTimeCommand( - InputStream in, OutputStream out, ScpTimestampCommandDetails time, Logger log, Object logHint) + public static ScpAckInfo sendAcknowledgedCommand(AbstractScpCommandDetails cmd, InputStream in, OutputStream out) throws IOException { - String cmd = time.toHeader(); - if ((log != null) && log.isDebugEnabled()) { - log.debug("sendTimeCommand({}) send timestamp={} command: {}", logHint, time, cmd); - } - writeLine(out, cmd); - - return readAck(in, false, log, logHint); - } - - /** - * Reads a single ACK from the input - * - * @param in The {@link InputStream} to read from - * @param canEof If {@code true} then OK if EOF is received before full ACK received - * @param log An optional {@link Logger} to use for issuing log messages - ignored if {@code null} - * @param logHint An optional hint to be used in the logged messages to identifier the caller's context - * @return The read ACK value - * @throws IOException If failed to complete the read - */ - public static int readAck(InputStream in, boolean canEof, Logger log, Object logHint) throws IOException { - int c = in.read(); - boolean debugEnabled = (log != null) && log.isDebugEnabled(); - switch (c) { - case -1: - if (debugEnabled) { - log.debug("readAck({})[EOF={}] received EOF", logHint, canEof); - } - if (!canEof) { - throw new EOFException("readAck - EOF before ACK"); - } - break; - case OK: - if (debugEnabled) { - log.debug("readAck({})[EOF={}] read OK", logHint, canEof); - } - break; - case WARNING: { - if (debugEnabled) { - log.debug("readAck({})[EOF={}] read warning message", logHint, canEof); - } - - String line = readLine(in); - if (log != null) { - log.warn("readAck({})[EOF={}] - Received warning: {}", logHint, canEof, line); - } - break; - } - case ERROR: { - if (debugEnabled) { - log.debug("readAck({})[EOF={}] read error message", logHint, canEof); - } - String line = readLine(in); - if (debugEnabled) { - log.debug("readAck({})[EOF={}] received error: {}", logHint, canEof, line); - } - throw new ScpException("Received nack: " + line, c); - } - default: - break; - } - - return c; + return sendAcknowledgedCommand(cmd.toHeader(), in, out); } - public static int sendAcknowledgedCommand( - String cmd, InputStream in, OutputStream out, Logger log) - throws IOException { + public static ScpAckInfo sendAcknowledgedCommand(String cmd, InputStream in, OutputStream out) throws IOException { writeLine(out, cmd); - return readAck(in, false, log, cmd); - } - - /** - * Sends {@link #OK} ACK code - * - * @param out The target {@link OutputStream} - * @throws IOException If failed to send the ACK code - */ - public static void ack(OutputStream out) throws IOException { - out.write(OK); - out.flush(); - } - - public static <O extends OutputStream> O sendWarning(O out, String message) throws IOException { - return sendResponseMessage(out, WARNING, message); - } - - public static <O extends OutputStream> O sendError(O out, String message) throws IOException { - return sendResponseMessage(out, ERROR, message); - } - - public static <O extends OutputStream> O sendResponseMessage(O out, int level, String message) throws IOException { - out.write(level); - writeLine(out, message); - return out; - } - - public static void validateCommandStatusCode(String command, Object location, int statusCode, boolean eofAllowed) - throws IOException { - switch (statusCode) { - case -1: - if (!eofAllowed) { - throw new EOFException("Unexpected EOF for command='" + command + "' on " + location); - } - break; - case OK: - break; - case WARNING: - break; - default: - throw new ScpException( - "Bad reply code (" + statusCode + ") for command='" + command + "' on " + location, statusCode); - } + return ScpAckInfo.readAck(in, false); } public static String getExitStatusName(Integer exitStatus) { @@ -219,11 +98,11 @@ public final class ScpIoUtils { } switch (exitStatus) { - case OK: + case ScpAckInfo.OK: return "OK"; - case WARNING: + case ScpAckInfo.WARNING: return "WARNING"; - case ERROR: + case ScpAckInfo.ERROR: return "ERROR"; default: return exitStatus.toString(); @@ -335,9 +214,9 @@ public final class ScpIoUtils { int statusCode = exitStatus; switch (statusCode) { - case OK: // do nothing + case ScpAckInfo.OK: // do nothing break; - case WARNING: + case ScpAckInfo.WARNING: if (log != null) { log.warn("handleCommandExitStatus({}) cmd='{}' may have terminated with some problems", session, cmd); } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommand.java b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommand.java index 074c8fd..329940c 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommand.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommand.java @@ -29,7 +29,7 @@ import org.apache.sshd.scp.common.ScpFileOpener; import org.apache.sshd.scp.common.ScpHelper; import org.apache.sshd.scp.common.ScpTransferEventListener; import org.apache.sshd.scp.common.helpers.DefaultScpFileOpener; -import org.apache.sshd.scp.common.helpers.ScpIoUtils; +import org.apache.sshd.scp.common.helpers.ScpAckInfo; import org.apache.sshd.server.Environment; import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.channel.ChannelSession; @@ -157,7 +157,7 @@ public class ScpCommand extends AbstractFileSystemCommand { @Override public void run() { - int exitValue = ScpIoUtils.OK; + int exitValue = ScpAckInfo.OK; String exitMessage = null; ServerSession session = getServerSession(); String command = getCommand(); @@ -178,13 +178,13 @@ public class ScpCommand extends AbstractFileSystemCommand { if (e instanceof ScpException) { statusCode = ((ScpException) e).getExitStatus(); } - exitValue = (statusCode == null) ? ScpIoUtils.ERROR : statusCode; + exitValue = (statusCode == null) ? ScpAckInfo.ERROR : statusCode; // this is an exception so status cannot be OK/WARNING - if ((exitValue == ScpIoUtils.OK) || (exitValue == ScpIoUtils.WARNING)) { + if ((exitValue == ScpAckInfo.OK) || (exitValue == ScpAckInfo.WARNING)) { if (debugEnabled) { log.debug("run({})[{}] normalize status code={}", session, command, exitValue); } - exitValue = ScpIoUtils.ERROR; + exitValue = ScpAckInfo.ERROR; } exitMessage = GenericUtils.trimToEmpty(e.getMessage()); writeCommandResponseMessage(command, exitValue, exitMessage); @@ -208,7 +208,7 @@ public class ScpCommand extends AbstractFileSystemCommand { log.debug("writeCommandResponseMessage({}) command='{}', exit-status={}: {}", getServerSession(), command, exitValue, exitMessage); } - ScpIoUtils.sendResponseMessage(getOutputStream(), exitValue, exitMessage); + ScpAckInfo.sendAck(getOutputStream(), exitValue, exitMessage); } @Override 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 3cde5f1..dedde72 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 @@ -61,7 +61,7 @@ import org.apache.sshd.scp.common.ScpFileOpener; import org.apache.sshd.scp.common.ScpHelper; import org.apache.sshd.scp.common.ScpTransferEventListener; import org.apache.sshd.scp.common.helpers.DefaultScpFileOpener; -import org.apache.sshd.scp.common.helpers.ScpIoUtils; +import org.apache.sshd.scp.common.helpers.ScpAckInfo; import org.apache.sshd.server.Environment; import org.apache.sshd.server.channel.ChannelSession; import org.apache.sshd.server.command.AbstractFileSystemCommand; @@ -476,13 +476,13 @@ public class ScpShell extends AbstractFileSystemCommand { variables.put(STATUS, 0); } catch (IOException e) { Integer statusCode = e instanceof ScpException ? ((ScpException) e).getExitStatus() : null; - int exitValue = (statusCode == null) ? ScpIoUtils.ERROR : statusCode; + int exitValue = (statusCode == null) ? ScpAckInfo.ERROR : statusCode; // this is an exception so status cannot be OK/WARNING - if ((exitValue == ScpIoUtils.OK) || (exitValue == ScpIoUtils.WARNING)) { - exitValue = ScpIoUtils.ERROR; + if ((exitValue == ScpAckInfo.OK) || (exitValue == ScpAckInfo.WARNING)) { + exitValue = ScpAckInfo.ERROR; } String exitMessage = GenericUtils.trimToEmpty(e.getMessage()); - ScpIoUtils.sendResponseMessage(getOutputStream(), exitValue, exitMessage); + ScpAckInfo.sendAck(getOutputStream(), exitValue, exitMessage); variables.put(STATUS, exitValue); } } diff --git a/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpTest.java b/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpTest.java index 0d5583b..b7bef34 100644 --- a/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpTest.java +++ b/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpTest.java @@ -62,6 +62,7 @@ import org.apache.sshd.scp.common.ScpFileOpener; import org.apache.sshd.scp.common.ScpHelper; import org.apache.sshd.scp.common.ScpTransferEventListener; import org.apache.sshd.scp.common.helpers.DefaultScpFileOpener; +import org.apache.sshd.scp.common.helpers.ScpAckInfo; import org.apache.sshd.scp.common.helpers.ScpDirEndCommandDetails; import org.apache.sshd.scp.common.helpers.ScpIoUtils; import org.apache.sshd.scp.common.helpers.ScpPathCommandDetailsSupport; @@ -820,7 +821,7 @@ public class ScpTest extends AbstractScpTestSupport { @Override protected void onExit(int exitValue, String exitMessage) { outputDebugMessage("onExit(%s) status=%d", this, exitValue); - super.onExit((exitValue == ScpIoUtils.OK) ? testExitValue : exitValue, exitMessage); + super.onExit((exitValue == ScpAckInfo.OK) ? testExitValue : exitValue, exitMessage); } } @@ -1067,13 +1068,13 @@ public class ScpTest extends AbstractScpTestSupport { try (OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream()) { - ScpIoUtils.ack(os); + ScpAckInfo.sendOk(os); String header = ScpIoUtils.readLine(is, false); String expPrefix = ScpReceiveDirCommandDetails.COMMAND_NAME + ScpReceiveDirCommandDetails.DEFAULT_DIR_OCTAL_PERMISSIONS + " 0 "; assertTrue("Bad header prefix for " + path + ": " + header, header.startsWith(expPrefix)); - ScpIoUtils.ack(os); + ScpAckInfo.sendOk(os); header = ScpIoUtils.readLine(is, false); String fileName = Objects.toString(target.getFileName(), null); @@ -1082,18 +1083,18 @@ public class ScpTest extends AbstractScpTestSupport { + " " + Files.size(target) + " " + fileName; assertEquals("Mismatched dir header for " + path, expHeader, header); int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6))); - ScpIoUtils.ack(os); + ScpAckInfo.sendOk(os); byte[] buffer = new byte[length]; length = is.read(buffer, 0, buffer.length); assertEquals("Mismatched read buffer size for " + path, length, buffer.length); assertAckReceived(is, "Read date of " + path); - ScpIoUtils.ack(os); + ScpAckInfo.sendOk(os); header = ScpIoUtils.readLine(is, false); assertEquals("Mismatched end value for " + path, "E", header); - ScpIoUtils.ack(os); + ScpAckInfo.sendOk(os); return new String(buffer, StandardCharsets.UTF_8); } finally { @@ -1110,8 +1111,8 @@ public class ScpTest extends AbstractScpTestSupport { try (OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream()) { - ScpIoUtils.ack(os); - assertEquals("Mismatched response for command: " + command, ScpIoUtils.ERROR, is.read()); + ScpAckInfo.sendOk(os); + assertEquals("Mismatched response for command: " + command, ScpAckInfo.ERROR, is.read()); } finally { c.disconnect(); } @@ -1139,7 +1140,7 @@ public class ScpTest extends AbstractScpTestSupport { os.flush(); assertAckReceived(is, "Sent data (length=" + data.length() + ") for " + path + "[" + name + "]"); - ScpIoUtils.ack(os); + ScpAckInfo.sendOk(os); Thread.sleep(100); } finally { @@ -1169,7 +1170,7 @@ public class ScpTest extends AbstractScpTestSupport { command = "C7777 " + data.length() + " " + name; ScpIoUtils.writeLine(os, command); - assertEquals("Mismatched response for command=" + command, ScpIoUtils.ERROR, is.read()); + assertEquals("Mismatched response for command=" + command, ScpAckInfo.ERROR, is.read()); } finally { c.disconnect(); } @@ -1193,7 +1194,7 @@ public class ScpTest extends AbstractScpTestSupport { os.flush(); assertAckReceived(is, "Send data of " + path); - ScpIoUtils.ack(os); + ScpAckInfo.sendOk(os); ScpIoUtils.writeLine(os, ScpDirEndCommandDetails.HEADER); assertAckReceived(is, "Signal end of " + path); } finally { diff --git a/sshd-scp/src/test/java/org/apache/sshd/scp/common/ScpLocationParsingTest.java b/sshd-scp/src/test/java/org/apache/sshd/scp/common/ScpLocationParsingTest.java new file mode 100644 index 0000000..9aa52ca --- /dev/null +++ b/sshd-scp/src/test/java/org/apache/sshd/scp/common/ScpLocationParsingTest.java @@ -0,0 +1,92 @@ +/* + * 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.scp.common; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory; +import org.apache.sshd.util.test.JUnitTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.Assume; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.Parameterized.UseParametersRunnerFactory; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests +@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class) +@Category({ NoIoTestCase.class }) +public class ScpLocationParsingTest extends JUnitTestSupport { + private final String value; + private final ScpLocation location; + + public ScpLocationParsingTest(String value, ScpLocation location) { + this.value = value; + this.location = location; + } + + @Parameters(name = "value={0}") + public static List<Object[]> parameters() { + return new ArrayList<Object[]>() { + // not serializing it + private static final long serialVersionUID = 1L; + + { + addTestCase(null, null); + addTestCase("", null); + addTestCase("/local/path/value", new ScpLocation(null, null, "/local/path/value")); + addTestCase("user@host:/remote/path/value", new ScpLocation("user", "host", "/remote/path/value")); + addTestCase("scp://user@host/remote/path/value", new ScpLocation("user", "host", "/remote/path/value")); + addTestCase("scp://user@host:22/remote/path/value", new ScpLocation("user", "host", "/remote/path/value")); + addTestCase("scp://user@host:2222/remote/path/value", + new ScpLocation("user", "host", 2222, "/remote/path/value")); + } + + private void addTestCase(String value, ScpLocation expected) { + add(new Object[] { value, expected }); + } + }; + } + + @Test + public void testLocationParsing() { + ScpLocation actual = ScpLocation.parse(value); + assertEquals(location, actual); + } + + @Test + public void testLocationToString() { + Assume.assumeTrue("No expected value to compate", location != null); + Assume.assumeTrue("Default port being used", + location.isLocal() || (location.resolvePort() != SshConstants.DEFAULT_PORT)); + String spec = location.toString(); + assertEquals(value, spec); + } +}