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 a2afd6b6cb8e3a79a9a5a67ce16b569c4f132bb1 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri Aug 14 12:08:36 2020 +0300 [SSHD-1005] Restructured some SCP code to enable easier re-use --- docs/scp.md | 17 +- docs/sftp.md | 4 + .../apache/sshd/scp/client/AbstractScpClient.java | 85 +--- .../apache/sshd/scp/client/DefaultScpClient.java | 4 + .../org/apache/sshd/scp/common/ScpException.java | 4 +- .../java/org/apache/sshd/scp/common/ScpHelper.java | 461 +++++---------------- .../org/apache/sshd/scp/common/ScpTimestamp.java | 41 +- .../common/helpers/AbstractScpCommandDetails.java | 40 ++ .../scp/common/helpers/CommandStatusHandler.java | 41 ++ .../common/helpers/ScpDirEndCommandDetails.java | 39 ++ .../apache/sshd/scp/common/helpers/ScpIoUtils.java | 423 +++++++++++++++++++ .../helpers/ScpPathCommandDetailsSupport.java | 178 ++++++++ .../helpers/ScpReceiveDirCommandDetails.java | 44 ++ .../helpers/ScpReceiveFileCommandDetails.java | 45 ++ .../org/apache/sshd/scp/server/ScpCommand.java | 11 +- .../java/org/apache/sshd/scp/server/ScpShell.java | 9 +- .../java/org/apache/sshd/scp/client/ScpTest.java | 45 +- 17 files changed, 1009 insertions(+), 482 deletions(-) diff --git a/docs/scp.md b/docs/scp.md index c9bff2a..a91a09d 100644 --- a/docs/scp.md +++ b/docs/scp.md @@ -1,4 +1,4 @@ -## SCP +# SCP Both client-side and server-side SCP are supported. Starting from version 2.0, the SCP related code is located in the `sshd-scp` module, so you need to add this additional dependency to your maven project: @@ -13,7 +13,7 @@ to add this additional dependency to your maven project: ``` -### `ScpTransferEventListener` +## `ScpTransferEventListener` Callback to inform about SCP related events. `ScpTransferEventListener`(s) can be registered on *both* client and server side: @@ -38,7 +38,7 @@ Callback to inform about SCP related events. `ScpTransferEventListener`(s) can b } ``` -### Client-side SCP +## Client-side SCP In order to obtain an `ScpClient` one needs to use an `ScpClientCreator`: @@ -66,7 +66,7 @@ ScpClient client2 = creator.createScpClient(session, new SomeOtherListener()); ``` -#### ScpFileOpener(s) +## ScpFileOpener(s) As part of the `ScpClientCreator`, the SCP module also uses a `ScpFileOpener` instance in order to access the local files. The default implementation simply opens an [InputStream](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html) @@ -126,7 +126,7 @@ different sensitivity via `DirectoryScanner#setCaseSensitive` call (or executes * `Windows` - case insensitive * `Unix` - case sensitive -### Server-side SCP +## Server-side SCP Setting up SCP support on the server side is straightforward - simply initialize a `ScpCommandFactory` and set it as the **primary** command factory. If support for commands other than SCP is also required then provide @@ -147,7 +147,7 @@ The `ScpCommandFactory` allows users to attach an `ScpFileOpener` and/or `ScpTra monitoring and intervention on the accessed local files. Furthermore, the factory can also be configured with a custom executor service for executing the requested copy commands as well as controlling the internal buffer sizes used to copy files. -### The SCP "shell" +## The SCP "shell" Some SCP clients (e.g. [WinSCP](https://winscp.net/)) open a shell connection even if configured to use pure SCP in order to retrieve information about the remote server's files and potentially navigate through them. In other words, SCP is only used as the **transfer** protocol, but @@ -171,3 +171,8 @@ is likely to require. For this purpose, the `ScpCommandFactory` also implements ``` **Note:** a similar result can be achieved if activating SSHD from the command line by specifying `-o ShellFactory=scp` + +## References + +* [How the SCP protocol works](https://chuacw.ath.cx/development/b/chuacw/archive/2019/02/04/how-the-scp-protocol-works.aspx) +* [scp.c](https://github.com/cloudsigma/illumos-omnios/blob/master/usr/src/cmd/ssh/scp/scp.c) diff --git a/docs/sftp.md b/docs/sftp.md index bba6d01..7d1cf72 100644 --- a/docs/sftp.md +++ b/docs/sftp.md @@ -514,3 +514,7 @@ for sending and receiving the newly added extension. See how other extensions are implemented and follow their example +## References + +* [SFTP drafts for the various versions](https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/) + diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/AbstractScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/AbstractScpClient.java index 54418cf..1930ef8 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/AbstractScpClient.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/AbstractScpClient.java @@ -24,16 +24,12 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; -import java.time.Duration; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.EnumSet; -import java.util.Set; import org.apache.sshd.client.channel.ChannelExec; import org.apache.sshd.client.channel.ClientChannel; -import org.apache.sshd.client.channel.ClientChannelEvent; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.SshException; @@ -44,17 +40,14 @@ import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.common.util.logging.AbstractLoggingBean; import org.apache.sshd.core.CoreModuleProperties; -import org.apache.sshd.scp.ScpModuleProperties; import org.apache.sshd.scp.common.ScpException; import org.apache.sshd.scp.common.ScpHelper; +import org.apache.sshd.scp.common.helpers.ScpIoUtils; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public abstract class AbstractScpClient extends AbstractLoggingBean implements ScpClient { - public static final Set<ClientChannelEvent> COMMAND_WAIT_EVENTS - = Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.CLOSED)); - protected AbstractScpClient() { super(); } @@ -180,26 +173,8 @@ public abstract class AbstractScpClient extends AbstractLoggingBean implements S * @see #handleCommandExitStatus(String, Integer) */ protected void handleCommandExitStatus(String cmd, ClientChannel channel) throws IOException { - // give a chance for the exit status to be received - Duration timeout = ScpModuleProperties.SCP_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT.getRequired(channel); - if (GenericUtils.isNegativeOrNull(timeout)) { - handleCommandExitStatus(cmd, (Integer) null); - return; - } - - long waitStart = System.nanoTime(); - Collection<ClientChannelEvent> events = channel.waitFor(COMMAND_WAIT_EVENTS, timeout); - long waitEnd = System.nanoTime(); - if (log.isDebugEnabled()) { - log.debug("handleCommandExitStatus({}) cmd='{}', waited={} nanos, events={}", - getClientSession(), cmd, waitEnd - waitStart, events); - } - - /* - * There are sometimes race conditions in the order in which channels are closed and exit-status sent by the - * remote peer (if at all), thus there is no guarantee that we will have an exit status here - */ - handleCommandExitStatus(cmd, channel.getExitStatus()); + ScpIoUtils.handleCommandExitStatus( + getClientSession(), cmd, channel, (session, command, status) -> handleCommandExitStatus(command, status), log); } /** @@ -208,30 +183,10 @@ public abstract class AbstractScpClient extends AbstractLoggingBean implements S * * @param cmd The attempted remote copy command * @param exitStatus The exit status - if {@code null} then no status was reported - * @throws IOException If failed the command + * @throws IOException If received non-OK exit status */ protected void handleCommandExitStatus(String cmd, Integer exitStatus) throws IOException { - if (log.isDebugEnabled()) { - log.debug("handleCommandExitStatus({}) cmd='{}', exit-status={}", - getClientSession(), cmd, ScpHelper.getExitStatusName(exitStatus)); - } - - if (exitStatus == null) { - return; - } - - int statusCode = exitStatus; - switch (statusCode) { - case ScpHelper.OK: // do nothing - break; - case ScpHelper.WARNING: - log.warn("handleCommandExitStatus({}) cmd='{}' may have terminated with some problems", getClientSession(), - cmd); - break; - default: - throw new ScpException( - "Failed to run command='" + cmd + "': " + ScpHelper.getExitStatusName(exitStatus), exitStatus); - } + ScpIoUtils.handleCommandExitStatus(getClientSession(), cmd, exitStatus, log); } protected Collection<Option> addTargetIsDirectory(Collection<Option> options) { @@ -245,35 +200,7 @@ public abstract class AbstractScpClient extends AbstractLoggingBean implements S } protected ChannelExec openCommandChannel(ClientSession session, String cmd) throws IOException { - Duration waitTimeout = ScpModuleProperties.SCP_EXEC_CHANNEL_OPEN_TIMEOUT.getRequired(session); - ChannelExec channel = session.createExecChannel(cmd); - - long startTime = System.nanoTime(); - try { - channel.open().verify(waitTimeout); - long endTime = System.nanoTime(); - long nanosWait = endTime - startTime; - if (log.isTraceEnabled()) { - log.trace("openCommandChannel(" + session + ")[" + cmd + "]" - + " completed after " + nanosWait - + " nanos out of " + waitTimeout.toNanos()); - } - - return channel; - } catch (IOException | RuntimeException e) { - long endTime = System.nanoTime(); - long nanosWait = endTime - startTime; - if (log.isTraceEnabled()) { - log.trace("openCommandChannel(" + session + ")[" + cmd + "]" - + " failed (" + e.getClass().getSimpleName() + ")" - + " to complete after " + nanosWait - + " nanos out of " + waitTimeout.toNanos() - + ": " + e.getMessage()); - } - - channel.close(false); - throw e; - } + return ScpIoUtils.openCommandChannel(session, cmd, log); } @FunctionalInterface diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java index 4ebf567..471ef2d 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java @@ -50,6 +50,10 @@ public class DefaultScpClient extends AbstractScpClient { protected final ScpTransferEventListener listener; private final ClientSession clientSession; + public DefaultScpClient(ClientSession clientSession) { + this(clientSession, DefaultScpFileOpener.INSTANCE, ScpTransferEventListener.EMPTY); + } + public DefaultScpClient( ClientSession clientSession, ScpFileOpener fileOpener, ScpTransferEventListener eventListener) { this.clientSession = Objects.requireNonNull(clientSession, "No client session"); diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpException.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpException.java index fb4f91c..ab1fada 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpException.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpException.java @@ -22,6 +22,8 @@ package org.apache.sshd.scp.common; import java.io.IOException; import java.util.Objects; +import org.apache.sshd.scp.common.helpers.ScpIoUtils; + /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ @@ -34,7 +36,7 @@ public class ScpException extends IOException { } public ScpException(Integer exitStatus) { - this("Exit status=" + ScpHelper.getExitStatusName(Objects.requireNonNull(exitStatus, "No exit status")), exitStatus); + this("Exit status=" + ScpIoUtils.getExitStatusName(Objects.requireNonNull(exitStatus, "No exit status")), exitStatus); } public ScpException(String message, Integer exitStatus) { 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 6ef5dbf..53c3f94 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,14 +18,11 @@ */ package org.apache.sshd.scp.common; -import java.io.ByteArrayOutputStream; -import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StreamCorruptedException; -import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.InvalidPathException; @@ -39,7 +36,6 @@ import java.util.Collection; import java.util.EnumSet; import java.util.Objects; import java.util.Set; -import java.util.concurrent.TimeUnit; import org.apache.sshd.common.file.util.MockPath; import org.apache.sshd.common.session.Session; @@ -50,21 +46,21 @@ 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.ScpDirEndCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpIoUtils; +import org.apache.sshd.scp.common.helpers.ScpPathCommandDetailsSupport; +import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ -@SuppressWarnings("PMD.AvoidUsingOctalValues") public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Session> { /** * Command prefix used to identify SCP commands */ public static final String SCP_COMMAND_PREFIX = "scp"; - public static final int OK = 0; - public static final int WARNING = 1; - public static final int ERROR = 2; - /** * Default size (in bytes) of send / receive buffer size */ @@ -79,19 +75,6 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess public static final int MIN_RECEIVE_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE; public static final int MIN_SEND_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE; - public static final int S_IRUSR = 0000400; - public static final int S_IWUSR = 0000200; - public static final int S_IXUSR = 0000100; - public static final int S_IRGRP = 0000040; - public static final int S_IWGRP = 0000020; - public static final int S_IXGRP = 0000010; - public static final int S_IROTH = 0000004; - public static final int S_IWOTH = 0000002; - public static final int S_IXOTH = 0000001; - - public static final String DEFAULT_DIR_OCTAL_PERMISSIONS = "0755"; - public static final String DEFAULT_FILE_OCTAL_PERMISSIONS = "0644"; - protected final InputStream in; protected final OutputStream out; protected final FileSystem fileSystem; @@ -100,8 +83,8 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess private final Session sessionInstance; - public ScpHelper(Session session, InputStream in, OutputStream out, - FileSystem fileSystem, ScpFileOpener opener, ScpTransferEventListener eventListener) { + public ScpHelper(Session session, InputStream in, OutputStream out, FileSystem fileSystem, ScpFileOpener opener, + ScpTransferEventListener eventListener) { this.sessionInstance = Objects.requireNonNull(session, "No session"); this.in = Objects.requireNonNull(in, "No input stream"); this.out = Objects.requireNonNull(out, "No output stream"); @@ -124,9 +107,11 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess Path path = new MockPath(line); receiveStream(line, new ScpTargetStreamResolver() { @Override - @SuppressWarnings("synthetic-access") // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=537593 + @SuppressWarnings("synthetic-access") // see + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=537593 public OutputStream resolveTargetStream( - Session session, String name, long length, Set<PosixFilePermission> perms, OpenOption... options) + Session session, String name, long length, + Set<PosixFilePermission> perms, OpenOption... options) throws IOException { if (log.isDebugEnabled()) { log.debug("resolveTargetStream({}) name={}, perms={}, len={} - started local stream download", @@ -141,13 +126,15 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } @Override - @SuppressWarnings("synthetic-access") // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=537593 + @SuppressWarnings("synthetic-access") // see + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=537593 public void postProcessReceivedData( - String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) + String name, boolean preserve, Set<PosixFilePermission> perms, + ScpTimestamp time) throws IOException { if (log.isDebugEnabled()) { - log.debug("postProcessReceivedData({}) name={}, perms={}, preserve={} time={}", - ScpHelper.this, name, perms, preserve, time); + log.debug("postProcessReceivedData({}) name={}, perms={}, preserve={} time={}", ScpHelper.this, + name, perms, preserve, time); } } @@ -159,14 +146,10 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess }); } - public void receive( - Path local, boolean recursive, boolean shouldBeDir, boolean preserve, int bufferSize) + public void receive(Path local, boolean recursive, boolean shouldBeDir, boolean preserve, int bufferSize) throws IOException { - Path localPath = Objects.requireNonNull(local, "No local path") - .normalize() - .toAbsolutePath(); - Path path = opener.resolveIncomingReceiveLocation( - getSession(), localPath, recursive, shouldBeDir, preserve); + Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath(); + Path path = opener.resolveIncomingReceiveLocation(getSession(), localPath, recursive, shouldBeDir, preserve); receive((session, line, isDir, time) -> { if (recursive && isDir) { receiveDir(line, path, time, preserve, bufferSize); @@ -177,85 +160,28 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } protected void receive(ScpReceiveLineHandler handler) throws IOException { - ack(); - ScpTimestamp time = null; - for (Session session = getSession();;) { - String line; - boolean isDir = false; - int c = readAck(true); - switch (c) { - case -1: - return; - case 'D': - line = readLine(); - line = Character.toString((char) c) + line; - isDir = true; - if (log.isDebugEnabled()) { - log.debug("receive({}) - Received 'D' header: {}", this, line); - } - break; - case 'C': - line = readLine(); - line = Character.toString((char) c) + line; - if (log.isDebugEnabled()) { - log.debug("receive({}) - Received 'C' header: {}", this, line); - } - break; - case 'T': - line = readLine(); - line = Character.toString((char) c) + line; - if (log.isDebugEnabled()) { - log.debug("receive({}) - Received 'T' header: {}", this, line); - } - time = ScpTimestamp.parseTime(line); - ack(); - continue; - case 'E': - line = readLine(); - line = Character.toString((char) c) + line; - if (log.isDebugEnabled()) { - log.debug("receive({}) - Received 'E' header: {}", this, line); - } - ack(); - return; - default: - // a real ack that has been acted upon already - continue; - } - - try { - handler.process(session, line, isDir, time); - } finally { - time = null; - } - } + ScpIoUtils.receive(getSession(), in, out, log, this, handler); } - public void receiveDir( - String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) + public void receiveDir(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException { - Path path = Objects.requireNonNull(local, "No local path") - .normalize() - .toAbsolutePath(); + Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath(); boolean debugEnabled = log.isDebugEnabled(); if (debugEnabled) { - log.debug("receiveDir({})[{}] Receiving directory {} - preserve={}, time={}, buffer-size={}", - this, header, path, preserve, time, bufferSize); - } - if (!header.startsWith("D")) { - throw new IOException("Expected a 'D; message but got '" + header + "'"); + log.debug("receiveDir({})[{}] Receiving directory {} - preserve={}, time={}, buffer-size={}", this, header, + path, preserve, time, bufferSize); } - Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5)); - int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6))); - String name = header.substring(header.indexOf(' ', 6) + 1); + ScpReceiveDirCommandDetails details = new ScpReceiveDirCommandDetails(header); + String name = details.getName(); + long length = details.getLength(); if (length != 0) { throw new IOException("Expected 0 length for directory=" + name + " but got " + length); } Session session = getSession(); - Path file = opener.resolveIncomingFilePath( - session, path, name, preserve, perms, time); + Set<PosixFilePermission> perms = details.getPermissions(); + Path file = opener.resolveIncomingFilePath(session, path, name, preserve, perms, time); ack(); @@ -269,16 +195,17 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess log.debug("receiveDir({})[{}] Received header: {}", this, file, header); } - if (header.startsWith("C")) { + char cmdChar = header.charAt(0); + if (cmdChar == ScpReceiveFileCommandDetails.COMMAND_NAME) { receiveFile(header, file, time, preserve, bufferSize); time = null; - } else if (header.startsWith("D")) { + } else if (cmdChar == ScpReceiveDirCommandDetails.COMMAND_NAME) { receiveDir(header, file, time, preserve, bufferSize); time = null; - } else if (header.equals("E")) { + } else if (cmdChar == ScpDirEndCommandDetails.COMMAND_NAME) { ack(); break; - } else if (header.startsWith("T")) { + } else if (cmdChar == ScpTimestamp.COMMAND_NAME) { time = ScpTimestamp.parseTime(header); ack(); } else { @@ -292,15 +219,12 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess listener.endFolderEvent(session, FileOperation.RECEIVE, path, perms, null); } - public void receiveFile( - String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) + public void receiveFile(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException { - Path path = Objects.requireNonNull(local, "No local path") - .normalize() - .toAbsolutePath(); + Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath(); if (log.isDebugEnabled()) { - log.debug("receiveFile({})[{}] Receiving file {} - preserve={}, time={}, buffer-size={}", - this, header, path, preserve, time, bufferSize); + log.debug("receiveFile({})[{}] Receiving file {} - preserve={}, time={}, buffer-size={}", this, header, + path, preserve, time, bufferSize); } ScpTargetStreamResolver targetStreamResolver = opener.createScpTargetStreamResolver(getSession(), path); @@ -308,32 +232,29 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } public void receiveStream( - String header, ScpTargetStreamResolver resolver, ScpTimestamp time, boolean preserve, int bufferSize) + String header, ScpTargetStreamResolver resolver, ScpTimestamp time, boolean preserve, + int bufferSize) throws IOException { - if (!header.startsWith("C")) { - throw new IOException("receiveStream(" + resolver + ") Expected a C message but got '" + header + "'"); - } - if (bufferSize < MIN_RECEIVE_BUFFER_SIZE) { throw new IOException( - "receiveStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_RECEIVE_BUFFER_SIZE - + ")"); + "receiveStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + + MIN_RECEIVE_BUFFER_SIZE + ")"); } - Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5)); - long length = Long.parseLong(header.substring(6, header.indexOf(' ', 6))); - String name = header.substring(header.indexOf(' ', 6) + 1); + ScpReceiveFileCommandDetails details = new ScpReceiveFileCommandDetails(header); + long length = details.getLength(); if (length < 0L) { // TODO consider throwing an exception... log.warn("receiveStream({})[{}] bad length in header: {}", this, resolver, header); } - // if file size is less than buffer size allocate only expected file size + // if file size is less than buffer size allocate only expected file + // size int bufSize; boolean debugEnabled = log.isDebugEnabled(); if (length == 0L) { if (debugEnabled) { - log.debug("receiveStream({})[{}] zero file size (perhaps special file) using copy buffer size={}", - this, resolver, MIN_RECEIVE_BUFFER_SIZE); + log.debug("receiveStream({})[{}] zero file size (perhaps special file) using copy buffer size={}", this, + resolver, MIN_RECEIVE_BUFFER_SIZE); } bufSize = MIN_RECEIVE_BUFFER_SIZE; } else { @@ -341,15 +262,17 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } if (bufSize < 0) { // TODO consider throwing an exception - log.warn("receiveStream({})[{}] bad buffer size ({}) using default ({})", - this, resolver, bufSize, MIN_RECEIVE_BUFFER_SIZE); + log.warn("receiveStream({})[{}] bad buffer size ({}) using default ({})", this, resolver, bufSize, + MIN_RECEIVE_BUFFER_SIZE); bufSize = MIN_RECEIVE_BUFFER_SIZE; } Session session = getSession(); + String name = details.getName(); + Set<PosixFilePermission> perms = details.getPermissions(); try (InputStream is = new LimitInputStream(this.in, length); - OutputStream os = resolver.resolveTargetStream( - session, name, length, perms, IoUtils.EMPTY_OPEN_OPTIONS)) { + OutputStream os = resolver.resolveTargetStream(session, name, length, perms, + IoUtils.EMPTY_OPEN_OPTIONS)) { ack(); Path file = resolver.getEventListenerFilePath(); @@ -380,26 +303,10 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } public String readLine(boolean canEof) throws IOException { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Byte.MAX_VALUE)) { - for (;;) { - int c = in.read(); - if (c == '\n') { - return baos.toString(StandardCharsets.UTF_8.name()); - } else if (c == -1) { - if (!canEof) { - throw new EOFException("EOF while await end of line"); - } - return null; - } else { - baos.write(c); - } - } - } + return ScpIoUtils.readLine(in, canEof); } - public void send( - Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) - throws IOException { + public void send(Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException { int readyCode = readAck(false); boolean debugEnabled = log.isDebugEnabled(); if (debugEnabled) { @@ -519,12 +426,13 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess public void sendStream(ScpSourceStreamResolver resolver, boolean preserve, int bufferSize) throws IOException { if (bufferSize < MIN_SEND_BUFFER_SIZE) { throw new IOException( - "sendStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_SEND_BUFFER_SIZE - + ")"); + "sendStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + + MIN_SEND_BUFFER_SIZE + ")"); } long fileSize = resolver.getSize(); - // if file size is less than buffer size allocate only expected file size + // if file size is less than buffer size allocate only expected file + // size int bufSize; boolean debugEnabled = log.isDebugEnabled(); if (fileSize <= 0L) { @@ -538,46 +446,36 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } if (bufSize < 0) { // TODO consider throwing an exception - log.warn("sendStream({})[{}] bad buffer size ({}) using default ({})", - this, resolver, bufSize, MIN_SEND_BUFFER_SIZE); + log.warn("sendStream({})[{}] bad buffer size ({}) using default ({})", this, resolver, bufSize, + MIN_SEND_BUFFER_SIZE); bufSize = MIN_SEND_BUFFER_SIZE; } ScpTimestamp time = resolver.getTimestamp(); if (preserve && (time != null)) { - String cmd = "T" + TimeUnit.MILLISECONDS.toSeconds(time.getLastModifiedTime()) - + " " + "0" + " " + TimeUnit.MILLISECONDS.toSeconds(time.getLastAccessTime()) - + " " + "0"; - if (debugEnabled) { - log.debug("sendStream({})[{}] send timestamp={} command: {}", this, resolver, time, cmd); - } - out.write(cmd.getBytes(StandardCharsets.UTF_8)); - out.write('\n'); - out.flush(); - - int readyCode = readAck(false); + int readyCode = ScpIoUtils.sendTimeCommand(in, out, time, log, this); + String cmd = time.toHeader(); if (debugEnabled) { log.debug("sendStream({})[{}] command='{}' ready code={}", this, resolver, cmd, readyCode); } + validateAckReplyCode(cmd, resolver, readyCode, false); } Set<PosixFilePermission> perms = EnumSet.copyOf(resolver.getPermissions()); - String octalPerms - = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_FILE_OCTAL_PERMISSIONS : getOctalPermissions(perms); + String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) + ? ScpReceiveFileCommandDetails.DEFAULT_FILE_OCTAL_PERMISSIONS + : ScpPathCommandDetailsSupport.getOctalPermissions(perms); String fileName = resolver.getFileName(); - String cmd = "C" + octalPerms + " " + fileSize + " " + fileName; + String cmd = ScpReceiveFileCommandDetails.COMMAND_NAME + octalPerms + " " + fileSize + " " + fileName; if (debugEnabled) { log.debug("sendStream({})[{}] send 'C' command: {}", this, resolver, cmd); } - out.write(cmd.getBytes(StandardCharsets.UTF_8)); - out.write('\n'); - out.flush(); - int readyCode = readAck(false); + int readyCode = sendAcknowledgedCommand(cmd); if (debugEnabled) { - log.debug("sendStream({})[{}] command='{}' ready code={}", - this, resolver, cmd.substring(0, cmd.length() - 1), readyCode); + log.debug("sendStream({})[{}] command='{}' ready code={}", this, resolver, + cmd.substring(0, cmd.length() - 1), readyCode); } validateAckReplyCode(cmd, resolver, readyCode, false); @@ -608,34 +506,22 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess validateCommandStatusCode(command, location, readyCode, eofAllowed); } - protected void validateAckReplyCode(String command, Object location, int replyCode, boolean eofAllowed) throws IOException { + protected void validateAckReplyCode(String command, Object location, int replyCode, boolean eofAllowed) + throws IOException { validateCommandStatusCode(command, location, replyCode, eofAllowed); } protected 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); - } + ScpIoUtils.validateCommandStatusCode(command, location, statusCode, eofAllowed); } public void sendDir(Path local, boolean preserve, int bufferSize) throws IOException { Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath(); boolean debugEnabled = log.isDebugEnabled(); if (debugEnabled) { - log.debug("sendDir({}) Sending directory {} - preserve={}, buffer-size={}", - this, path, preserve, bufferSize); + log.debug("sendDir({}) Sending directory {} - preserve={}, buffer-size={}", this, path, preserve, + bufferSize); } LinkOption[] options = IoUtils.getLinkOptions(true); @@ -644,19 +530,14 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess BasicFileAttributes basic = opener.getLocalBasicFileAttributes(session, path, options); FileTime lastModified = basic.lastModifiedTime(); FileTime lastAccess = basic.lastAccessTime(); - String cmd = "T" + lastModified.to(TimeUnit.SECONDS) + " " - + "0" + " " + lastAccess.to(TimeUnit.SECONDS) + " " - + "0"; + ScpTimestamp time = new ScpTimestamp(lastModified, lastAccess); + String cmd = time.toHeader(); if (debugEnabled) { - log.debug("sendDir({})[{}] send last-modified={}, last-access={} command: {}", - this, path, lastModified, lastAccess, cmd); + log.debug("sendDir({})[{}] send last-modified={}, last-access={} command: {}", this, path, lastModified, + lastAccess, cmd); } - out.write(cmd.getBytes(StandardCharsets.UTF_8)); - out.write('\n'); - out.flush(); - - int readyCode = readAck(false); + int readyCode = sendAcknowledgedCommand(cmd); if (debugEnabled) { if (debugEnabled) { log.debug("sendDir({})[{}] command='{}' ready code={}", this, path, cmd, readyCode); @@ -666,20 +547,18 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess } Set<PosixFilePermission> perms = opener.getLocalFilePermissions(session, path, options); - String octalPerms - = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_DIR_OCTAL_PERMISSIONS : getOctalPermissions(perms); - String cmd = "D" + octalPerms + " " + "0" + " " + Objects.toString(path.getFileName(), null); + String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) + ? ScpReceiveDirCommandDetails.DEFAULT_DIR_OCTAL_PERMISSIONS + : ScpPathCommandDetailsSupport.getOctalPermissions(perms); + String cmd = ScpReceiveDirCommandDetails.COMMAND_NAME + octalPerms + " " + "0" + " " + + Objects.toString(path.getFileName(), null); if (debugEnabled) { log.debug("sendDir({})[{}] send 'D' command: {}", this, path, cmd); } - out.write(cmd.getBytes(StandardCharsets.UTF_8)); - out.write('\n'); - out.flush(); - int readyCode = readAck(false); + int readyCode = sendAcknowledgedCommand(cmd); if (debugEnabled) { - log.debug("sendDir({})[{}] command='{}' ready code={}", - this, path, cmd.substring(0, cmd.length() - 1), readyCode); + log.debug("sendDir({})[{}] command='{}' ready code={}", this, path, cmd, readyCode); } validateAckReplyCode(cmd, path, readyCode, false); @@ -705,178 +584,36 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess if (debugEnabled) { log.debug("sendDir({})[{}] send 'E' command", this, path); } - out.write("E\n".getBytes(StandardCharsets.UTF_8)); - out.flush(); - readyCode = readAck(false); + readyCode = sendAcknowledgedCommand(ScpDirEndCommandDetails.HEADER); if (debugEnabled) { log.debug("sendDir({})[{}] 'E' command reply code=", this, path, readyCode); } - validateAckReplyCode("E", path, readyCode, false); - } - - public static String getOctalPermissions(Collection<PosixFilePermission> perms) { - int pf = 0; - - for (PosixFilePermission p : perms) { - switch (p) { - case OWNER_READ: - pf |= S_IRUSR; - break; - case OWNER_WRITE: - pf |= S_IWUSR; - break; - case OWNER_EXECUTE: - pf |= S_IXUSR; - break; - case GROUP_READ: - pf |= S_IRGRP; - break; - case GROUP_WRITE: - pf |= S_IWGRP; - break; - case GROUP_EXECUTE: - pf |= S_IXGRP; - break; - case OTHERS_READ: - pf |= S_IROTH; - break; - case OTHERS_WRITE: - pf |= S_IWOTH; - break; - case OTHERS_EXECUTE: - pf |= S_IXOTH; - break; - default: // ignored - } - } - - return String.format("%04o", pf); + validateAckReplyCode(ScpDirEndCommandDetails.HEADER, path, readyCode, false); } - public static Set<PosixFilePermission> parseOctalPermissions(String str) { - int perms = Integer.parseInt(str, 8); - Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class); - if ((perms & S_IRUSR) != 0) { - p.add(PosixFilePermission.OWNER_READ); - } - if ((perms & S_IWUSR) != 0) { - p.add(PosixFilePermission.OWNER_WRITE); - } - if ((perms & S_IXUSR) != 0) { - p.add(PosixFilePermission.OWNER_EXECUTE); - } - if ((perms & S_IRGRP) != 0) { - p.add(PosixFilePermission.GROUP_READ); - } - if ((perms & S_IWGRP) != 0) { - p.add(PosixFilePermission.GROUP_WRITE); - } - if ((perms & S_IXGRP) != 0) { - p.add(PosixFilePermission.GROUP_EXECUTE); - } - if ((perms & S_IROTH) != 0) { - p.add(PosixFilePermission.OTHERS_READ); - } - if ((perms & S_IWOTH) != 0) { - p.add(PosixFilePermission.OTHERS_WRITE); - } - if ((perms & S_IXOTH) != 0) { - p.add(PosixFilePermission.OTHERS_EXECUTE); - } - - return p; + protected int sendAcknowledgedCommand(String cmd) throws IOException { + return ScpIoUtils.sendAcknowledgedCommand(cmd, in, out, log); } protected void sendWarning(String message) throws IOException { - sendResponseMessage(WARNING, message); + sendResponseMessage(ScpIoUtils.WARNING, message); } protected void sendError(String message) throws IOException { - sendResponseMessage(ERROR, message); + sendResponseMessage(ScpIoUtils.ERROR, message); } protected void sendResponseMessage(int level, String message) throws IOException { - sendResponseMessage(out, level, message); - } - - 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); - out.write(message.getBytes(StandardCharsets.UTF_8)); - out.write('\n'); - out.flush(); - return out; - } - - public static String getExitStatusName(Integer exitStatus) { - if (exitStatus == null) { - return "null"; - } - - switch (exitStatus) { - case OK: - return "OK"; - case WARNING: - return "WARNING"; - case ERROR: - return "ERROR"; - default: - return exitStatus.toString(); - } + ScpIoUtils.sendResponseMessage(out, level, message); } public void ack() throws IOException { - out.write(0); - out.flush(); + ScpIoUtils.ack(out); } public int readAck(boolean canEof) throws IOException { - int c = in.read(); - switch (c) { - case -1: - if (log.isDebugEnabled()) { - log.debug("readAck({})[EOF={}] received EOF", this, canEof); - } - if (!canEof) { - throw new EOFException("readAck - EOF before ACK"); - } - break; - case OK: - if (log.isDebugEnabled()) { - log.debug("readAck({})[EOF={}] read OK", this, canEof); - } - break; - case WARNING: { - if (log.isDebugEnabled()) { - log.debug("readAck({})[EOF={}] read warning message", this, canEof); - } - - String line = readLine(); - log.warn("readAck({})[EOF={}] - Received warning: {}", this, canEof, line); - break; - } - case ERROR: { - if (log.isDebugEnabled()) { - log.debug("readAck({})[EOF={}] read error message", this, canEof); - } - String line = readLine(); - if (log.isDebugEnabled()) { - log.debug("readAck({})[EOF={}] received error: {}", this, canEof, line); - } - throw new ScpException("Received nack: " + line, c); - } - default: - break; - } - return c; + return ScpIoUtils.readAck(in, canEof, log, this); } @Override diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTimestamp.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTimestamp.java index 9c4372c..23fcb5d 100644 --- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTimestamp.java +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTimestamp.java @@ -19,21 +19,43 @@ package org.apache.sshd.scp.common; +import java.nio.file.attribute.FileTime; import java.util.Date; import java.util.concurrent.TimeUnit; import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.scp.common.helpers.AbstractScpCommandDetails; /** * Represents an SCP timestamp definition - * + * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ -public class ScpTimestamp { +public class ScpTimestamp extends AbstractScpCommandDetails { + public static final char COMMAND_NAME = 'T'; + private final long lastModifiedTime; private final long lastAccessTime; + public ScpTimestamp(String header) { + super(COMMAND_NAME); + + if (header.charAt(0) != COMMAND_NAME) { + throw new IllegalArgumentException("Expected a '" + COMMAND_NAME + "' but got '" + header + "'"); + } + + String[] numbers = GenericUtils.split(header.substring(1), ' '); + lastModifiedTime = TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[0])); + lastAccessTime = TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[2])); + } + + public ScpTimestamp(FileTime modTime, FileTime accTime) { + this(modTime.to(TimeUnit.MILLISECONDS), accTime.to(TimeUnit.MILLISECONDS)); + } + public ScpTimestamp(long modTime, long accTime) { + super(COMMAND_NAME); + lastModifiedTime = modTime; lastAccessTime = accTime; } @@ -47,6 +69,12 @@ public class ScpTimestamp { } @Override + public String toHeader() { + return Character.toString(getCommand()) + TimeUnit.MILLISECONDS.toSeconds(getLastModifiedTime()) + + " 0 " + TimeUnit.MILLISECONDS.toSeconds(getLastAccessTime()) + "0"; + } + + @Override public String toString() { return "modified=" + new Date(lastModifiedTime) + ";accessed=" + new Date(lastAccessTime); @@ -55,16 +83,13 @@ public class ScpTimestamp { /** * @param line The time specification - format: * {@code T<mtime-sec> <mtime-micros> <atime-sec> <atime-micros>} where specified - * times are in seconds since UTC + * times are in seconds since UTC - ignored if {@code null} * @return The {@link ScpTimestamp} value with the timestamps converted to <U>milliseconds</U> - * @throws NumberFormatException if bad numerical values - <B>Note:</B> does not check if 1st character is 'T'. + * @throws NumberFormatException if bad numerical values - <B>Note:</B> validates that 1st character is 'T'. * @see <A HREF="https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works">How the * SCP protocol works</A> */ public static ScpTimestamp parseTime(String line) throws NumberFormatException { - String[] numbers = GenericUtils.split(line.substring(1), ' '); - return new ScpTimestamp( - TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[0])), - TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[2]))); + return GenericUtils.isEmpty(line) ? null : new ScpTimestamp(line); } } diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/AbstractScpCommandDetails.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/AbstractScpCommandDetails.java new file mode 100644 index 0000000..b484ebe --- /dev/null +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/AbstractScpCommandDetails.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public abstract class AbstractScpCommandDetails { + protected final char command; + + protected AbstractScpCommandDetails(char command) { + this.command = command; + } + + public char getCommand() { + return command; + } + + /** + * @return The equivalent SCP command header represented by these details + */ + public abstract String toHeader(); +} diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/CommandStatusHandler.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/CommandStatusHandler.java new file mode 100644 index 0000000..a7a88e8 --- /dev/null +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/CommandStatusHandler.java @@ -0,0 +1,41 @@ +/* + * 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.IOException; + +import org.apache.sshd.client.session.ClientSession; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface CommandStatusHandler { + /** + * Invoked by the various <code>upload/download</code> methods after having successfully completed the remote copy + * command and (optionally) having received an exit status from the remote server + * + * @param session The associated {@link ClientSession} + * @param cmd The attempted remote copy command + * @param exitStatus The exit status - if {@code null} then no status was reported + * @throws IOException If failed the command + */ + void handleCommandExitStatus(ClientSession session, String cmd, Integer exitStatus) throws IOException; +} diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java new file mode 100644 index 0000000..b1a638f --- /dev/null +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpDirEndCommandDetails.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class ScpDirEndCommandDetails extends AbstractScpCommandDetails { + public static final char COMMAND_NAME = 'E'; + public static final String HEADER = "E"; + + public static final ScpDirEndCommandDetails INSTANCE = new ScpDirEndCommandDetails(); + + public ScpDirEndCommandDetails() { + super(COMMAND_NAME); + } + + @Override + public String toHeader() { + return HEADER; + } +} 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 new file mode 100644 index 0000000..b8e271c --- /dev/null +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpIoUtils.java @@ -0,0 +1,423 @@ +/* + * 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.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.sshd.client.channel.ChannelExec; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.scp.ScpModuleProperties; +import org.apache.sshd.scp.common.ScpException; +import org.apache.sshd.scp.common.ScpReceiveLineHandler; +import org.apache.sshd.scp.common.ScpTimestamp; +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)); + + private ScpIoUtils() { + throw new UnsupportedOperationException("No instance"); + } + + public static String readLine(InputStream in) throws IOException { + return readLine(in, false); + } + + public static String readLine(InputStream in, boolean canEof) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Byte.MAX_VALUE)) { + for (;;) { + int c = in.read(); + if (c == '\n') { + return baos.toString(StandardCharsets.UTF_8.name()); + } else if (c == -1) { + if (!canEof) { + throw new EOFException("EOF while await end of line"); + } + return null; + } else { + baos.write(c); + } + } + } + } + + public static void writeLine(OutputStream out, String cmd) throws IOException { + 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 ScpTimestamp} 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, ScpTimestamp time, Logger log, Object logHint) + 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; + } + + public static int sendAcknowledgedCommand( + String cmd, InputStream in, OutputStream out, Logger log) + 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(); + } + + /** + * Reads command line(s) and invokes the handler until EOF or and "E" command is received + * + * @param session The associated {@link Session} + * @param in The {@link InputStream} to read from + * @param out The {@link OutputStream} to write ACKs to + * @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 + * @param handler The {@link ScpReceiveLineHandler} to invoke when a command has been read + * @throws IOException If failed to read/write + */ + public static void receive( + Session session, InputStream in, OutputStream out, Logger log, Object logHint, ScpReceiveLineHandler handler) + throws IOException { + ack(out); + + boolean debugEnabled = (log != null) && log.isDebugEnabled(); + for (ScpTimestamp time = null;;) { + String line; + boolean isDir = false; + int c = readAck(in, true, log, logHint); + switch (c) { + case -1: + return; + case ScpReceiveDirCommandDetails.COMMAND_NAME: + line = readLine(in); + line = Character.toString((char) c) + line; + isDir = true; + if (debugEnabled) { + log.debug("receive({}) - Received 'D' header: {}", logHint, line); + } + break; + case ScpReceiveFileCommandDetails.COMMAND_NAME: + line = readLine(in); + line = Character.toString((char) c) + line; + if (debugEnabled) { + log.debug("receive({}) - Received 'C' header: {}", logHint, line); + } + break; + case ScpTimestamp.COMMAND_NAME: + line = readLine(in); + line = Character.toString((char) c) + line; + if (debugEnabled) { + log.debug("receive({}) - Received 'T' header: {}", logHint, line); + } + time = ScpTimestamp.parseTime(line); + ack(out); + continue; + case ScpDirEndCommandDetails.COMMAND_NAME: + line = readLine(in); + line = Character.toString((char) c) + line; + if (debugEnabled) { + log.debug("receive({}) - Received 'E' header: {}", logHint, line); + } + ack(out); + return; + default: + // a real ack that has been acted upon already + continue; + } + + try { + handler.process(session, line, isDir, time); + } finally { + time = null; + } + } + } + + 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); + } + } + + public static String getExitStatusName(Integer exitStatus) { + if (exitStatus == null) { + return "null"; + } + + switch (exitStatus) { + case OK: + return "OK"; + case WARNING: + return "WARNING"; + case ERROR: + return "ERROR"; + default: + return exitStatus.toString(); + } + } + + public static ChannelExec openCommandChannel(ClientSession session, String cmd, Logger log) throws IOException { + Duration waitTimeout = ScpModuleProperties.SCP_EXEC_CHANNEL_OPEN_TIMEOUT.getRequired(session); + ChannelExec channel = session.createExecChannel(cmd); + + long startTime = System.nanoTime(); + try { + channel.open().verify(waitTimeout); + long endTime = System.nanoTime(); + long nanosWait = endTime - startTime; + if ((log != null) && log.isTraceEnabled()) { + log.trace("openCommandChannel(" + session + ")[" + cmd + "]" + + " completed after " + nanosWait + + " nanos out of " + waitTimeout.toNanos()); + } + + return channel; + } catch (IOException | RuntimeException e) { + long endTime = System.nanoTime(); + long nanosWait = endTime - startTime; + if ((log != null) && log.isTraceEnabled()) { + log.trace("openCommandChannel(" + session + ")[" + cmd + "]" + + " failed (" + e.getClass().getSimpleName() + ")" + + " to complete after " + nanosWait + + " nanos out of " + waitTimeout.toNanos() + + ": " + e.getMessage()); + } + + channel.close(false); + throw e; + } + } + + /** + * Invoked by the various <code>upload/download</code> methods after having successfully completed the remote copy + * command and (optionally) having received an exit status from the remote server. If no exit status received within + * {@link CoreModuleProperties#CHANNEL_CLOSE_TIMEOUT} the no further action is taken. Otherwise, the exit status is + * examined to ensure it is either OK or WARNING - if not, an {@link ScpException} is thrown + * + * @param session The associated {@link ClientSession} + * @param cmd The attempted remote copy command + * @param channel The {@link ClientChannel} through which the command was sent - <B>Note:</B> then channel may + * be in the process of being closed + * @param handler The {@link CommandStatusHandler} to invoke once the exit status is received. if {@code null} + * then {@link #handleCommandExitStatus(ClientSession, String, Integer, Logger)} is called + * @param log An optional {@link Logger} to use for issuing log messages - ignored if {@code null} + * @throws IOException If failed the command + */ + public static void handleCommandExitStatus( + ClientSession session, String cmd, ClientChannel channel, CommandStatusHandler handler, Logger log) + throws IOException { + // give a chance for the exit status to be received + Duration timeout = ScpModuleProperties.SCP_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT.getRequired(channel); + if (GenericUtils.isNegativeOrNull(timeout)) { + if (handler == null) { + handleCommandExitStatus(session, cmd, null, log); + } else { + handler.handleCommandExitStatus(session, cmd, (Integer) null); + } + return; + } + + long waitStart = System.nanoTime(); + Collection<ClientChannelEvent> events = channel.waitFor(COMMAND_WAIT_EVENTS, timeout); + long waitEnd = System.nanoTime(); + if ((log != null) && log.isDebugEnabled()) { + log.debug("handleCommandExitStatus({}) cmd='{}', waited={} nanos, events={}", + session, cmd, waitEnd - waitStart, events); + } + + /* + * There are sometimes race conditions in the order in which channels are closed and exit-status sent by the + * remote peer (if at all), thus there is no guarantee that we will have an exit status here + */ + Integer exitStatus = channel.getExitStatus(); + if (handler == null) { + handleCommandExitStatus(session, cmd, exitStatus, log); + } else { + handler.handleCommandExitStatus(session, cmd, exitStatus); + } + } + + /** + * Invoked by the various <code>upload/download</code> methods after having successfully completed the remote copy + * command and (optionally) having received an exit status from the remote server + * + * @param session The associated {@link ClientSession} + * @param cmd The attempted remote copy command + * @param exitStatus The exit status - if {@code null} then no status was reported + * @param log An optional {@link Logger} to use for issuing log messages - ignored if {@code null} + * @throws IOException If got a an error exit status + */ + public static void handleCommandExitStatus( + ClientSession session, String cmd, Integer exitStatus, Logger log) + throws IOException { + if ((log != null) && log.isDebugEnabled()) { + log.debug("handleCommandExitStatus({}) cmd='{}', exit-status={}", + session, cmd, ScpIoUtils.getExitStatusName(exitStatus)); + } + + if (exitStatus == null) { + return; + } + + int statusCode = exitStatus; + switch (statusCode) { + case OK: // do nothing + break; + case WARNING: + if (log != null) { + log.warn("handleCommandExitStatus({}) cmd='{}' may have terminated with some problems", session, cmd); + } + break; + default: + throw new ScpException( + "Failed to run command='" + cmd + "': " + ScpIoUtils.getExitStatusName(exitStatus), exitStatus); + } + } + +} diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpPathCommandDetailsSupport.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpPathCommandDetailsSupport.java new file mode 100644 index 0000000..5197656 --- /dev/null +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpPathCommandDetailsSupport.java @@ -0,0 +1,178 @@ +/* + * 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.nio.file.attribute.PosixFilePermission; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Set; + +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@SuppressWarnings("PMD.AvoidUsingOctalValues") +public abstract class ScpPathCommandDetailsSupport extends AbstractScpCommandDetails implements NamedResource { + // File permissions masks + public static final int S_IRUSR = 0000400; + public static final int S_IWUSR = 0000200; + public static final int S_IXUSR = 0000100; + public static final int S_IRGRP = 0000040; + public static final int S_IWGRP = 0000020; + public static final int S_IXGRP = 0000010; + public static final int S_IROTH = 0000004; + public static final int S_IWOTH = 0000002; + public static final int S_IXOTH = 0000001; + + private Set<PosixFilePermission> permissions; + private long length; + private String name; + + protected ScpPathCommandDetailsSupport(char command) { + super(command); + } + + protected ScpPathCommandDetailsSupport(char command, String header) { + super(command); + + ValidateUtils.checkNotNullAndNotEmpty(header, "No header provided"); + if (header.charAt(0) != command) { + throw new IllegalArgumentException("Expected a '" + command + "' message but got '" + header + "'"); + } + + permissions = parseOctalPermissions(header.substring(1, 5)); + length = Long.parseLong(header.substring(6, header.indexOf(' ', 6))); + name = header.substring(header.indexOf(' ', 6) + 1); + } + + public Set<PosixFilePermission> getPermissions() { + return permissions; + } + + public void setPermissions(Set<PosixFilePermission> permissions) { + this.permissions = permissions; + } + + public long getLength() { + return length; + } + + public void setLength(long length) { + this.length = length; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toHeader() { + return getCommand() + getOctalPermissions(getPermissions()) + " " + getLength() + " " + getName(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "[name=" + getName() + + ", len=" + getLength() + + ", perms=" + getPermissions() + + "]"; + } + + public static String getOctalPermissions(Collection<PosixFilePermission> perms) { + int pf = 0; + + for (PosixFilePermission p : perms) { + switch (p) { + case OWNER_READ: + pf |= S_IRUSR; + break; + case OWNER_WRITE: + pf |= S_IWUSR; + break; + case OWNER_EXECUTE: + pf |= S_IXUSR; + break; + case GROUP_READ: + pf |= S_IRGRP; + break; + case GROUP_WRITE: + pf |= S_IWGRP; + break; + case GROUP_EXECUTE: + pf |= S_IXGRP; + break; + case OTHERS_READ: + pf |= S_IROTH; + break; + case OTHERS_WRITE: + pf |= S_IWOTH; + break; + case OTHERS_EXECUTE: + pf |= S_IXOTH; + break; + default: // ignored + } + } + + return String.format("%04o", pf); + } + + public static Set<PosixFilePermission> parseOctalPermissions(String str) { + int perms = Integer.parseInt(str, 8); + Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class); + if ((perms & S_IRUSR) != 0) { + p.add(PosixFilePermission.OWNER_READ); + } + if ((perms & S_IWUSR) != 0) { + p.add(PosixFilePermission.OWNER_WRITE); + } + if ((perms & S_IXUSR) != 0) { + p.add(PosixFilePermission.OWNER_EXECUTE); + } + if ((perms & S_IRGRP) != 0) { + p.add(PosixFilePermission.GROUP_READ); + } + if ((perms & S_IWGRP) != 0) { + p.add(PosixFilePermission.GROUP_WRITE); + } + if ((perms & S_IXGRP) != 0) { + p.add(PosixFilePermission.GROUP_EXECUTE); + } + if ((perms & S_IROTH) != 0) { + p.add(PosixFilePermission.OTHERS_READ); + } + if ((perms & S_IWOTH) != 0) { + p.add(PosixFilePermission.OTHERS_WRITE); + } + if ((perms & S_IXOTH) != 0) { + p.add(PosixFilePermission.OTHERS_EXECUTE); + } + + return p; + } +} diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveDirCommandDetails.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveDirCommandDetails.java new file mode 100644 index 0000000..f9f67ee --- /dev/null +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveDirCommandDetails.java @@ -0,0 +1,44 @@ +/* + * 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 org.apache.sshd.common.util.GenericUtils; + +/** + * Holds the details of a "Dmmmm <length> <directory>" command - e.g., "D0755 0 dirname" + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class ScpReceiveDirCommandDetails extends ScpPathCommandDetailsSupport { + public static final String DEFAULT_DIR_OCTAL_PERMISSIONS = "0755"; + public static final char COMMAND_NAME = 'D'; + + public ScpReceiveDirCommandDetails() { + super(COMMAND_NAME); + } + + public ScpReceiveDirCommandDetails(String header) { + super(COMMAND_NAME, header); + } + + public static ScpReceiveDirCommandDetails parse(String header) { + return GenericUtils.isEmpty(header) ? null : new ScpReceiveDirCommandDetails(header); + } +} diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveFileCommandDetails.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveFileCommandDetails.java new file mode 100644 index 0000000..7524707 --- /dev/null +++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpReceiveFileCommandDetails.java @@ -0,0 +1,45 @@ +/* + * 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 org.apache.sshd.common.util.GenericUtils; + +/** + * Holds the details of a "Cmmmm <length> <filename>" command - e.g., "C0644 299 file1.txt" + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class ScpReceiveFileCommandDetails extends ScpPathCommandDetailsSupport { + public static final String DEFAULT_FILE_OCTAL_PERMISSIONS = "0644"; + + public static final char COMMAND_NAME = 'C'; + + public ScpReceiveFileCommandDetails() { + super(COMMAND_NAME); + } + + public ScpReceiveFileCommandDetails(String header) { + super(COMMAND_NAME, header); + } + + public static ScpReceiveFileCommandDetails parse(String header) { + return GenericUtils.isEmpty(header) ? null : new ScpReceiveFileCommandDetails(header); + } +} 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 030b62a..fcaf577 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,6 +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.server.Environment; import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.channel.ChannelSession; @@ -156,7 +157,7 @@ public class ScpCommand extends AbstractFileSystemCommand { @Override public void run() { - int exitValue = ScpHelper.OK; + int exitValue = ScpIoUtils.OK; String exitMessage = null; ServerSession session = getServerSession(); String command = getCommand(); @@ -177,13 +178,13 @@ public class ScpCommand extends AbstractFileSystemCommand { if (e instanceof ScpException) { statusCode = ((ScpException) e).getExitStatus(); } - exitValue = (statusCode == null) ? ScpHelper.ERROR : statusCode; + exitValue = (statusCode == null) ? ScpIoUtils.ERROR : statusCode; // this is an exception so status cannot be OK/WARNING - if ((exitValue == ScpHelper.OK) || (exitValue == ScpHelper.WARNING)) { + if ((exitValue == ScpIoUtils.OK) || (exitValue == ScpIoUtils.WARNING)) { if (debugEnabled) { log.debug("run({})[{}] normalize status code={}", session, command, exitValue); } - exitValue = ScpHelper.ERROR; + exitValue = ScpIoUtils.ERROR; } exitMessage = GenericUtils.trimToEmpty(e.getMessage()); writeCommandResponseMessage(command, exitValue, exitMessage); @@ -213,7 +214,7 @@ public class ScpCommand extends AbstractFileSystemCommand { log.debug("writeCommandResponseMessage({}) command='{}', exit-status={}: {}", getServerSession(), command, exitValue, exitMessage); } - ScpHelper.sendResponseMessage(getOutputStream(), exitValue, exitMessage); + ScpIoUtils.sendResponseMessage(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 faa9339..3cde5f1 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,6 +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.server.Environment; import org.apache.sshd.server.channel.ChannelSession; import org.apache.sshd.server.command.AbstractFileSystemCommand; @@ -475,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) ? ScpHelper.ERROR : statusCode; + int exitValue = (statusCode == null) ? ScpIoUtils.ERROR : statusCode; // this is an exception so status cannot be OK/WARNING - if ((exitValue == ScpHelper.OK) || (exitValue == ScpHelper.WARNING)) { - exitValue = ScpHelper.ERROR; + if ((exitValue == ScpIoUtils.OK) || (exitValue == ScpIoUtils.WARNING)) { + exitValue = ScpIoUtils.ERROR; } String exitMessage = GenericUtils.trimToEmpty(e.getMessage()); - ScpHelper.sendResponseMessage(getOutputStream(), exitValue, exitMessage); + ScpIoUtils.sendResponseMessage(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 5e0ec88..788c0fa 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 @@ -40,12 +40,6 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import ch.ethz.ssh2.Connection; -import ch.ethz.ssh2.ConnectionInfo; -import ch.ethz.ssh2.SCPClient; -import com.jcraft.jsch.ChannelExec; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.Factory; @@ -66,6 +60,11 @@ 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.ScpDirEndCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpIoUtils; +import org.apache.sshd.scp.common.helpers.ScpPathCommandDetailsSupport; +import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails; +import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails; import org.apache.sshd.scp.server.ScpCommand; import org.apache.sshd.scp.server.ScpCommandFactory; import org.apache.sshd.server.SshServer; @@ -84,6 +83,14 @@ import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; +import com.jcraft.jsch.ChannelExec; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; + +import ch.ethz.ssh2.Connection; +import ch.ethz.ssh2.ConnectionInfo; +import ch.ethz.ssh2.SCPClient; + /** * Test for SCP support. * @@ -922,7 +929,7 @@ public class ScpTest extends BaseTestSupport { @Override protected void onExit(int exitValue, String exitMessage) { outputDebugMessage("onExit(%s) status=%d", this, exitValue); - super.onExit((exitValue == ScpHelper.OK) ? testExitValue : exitValue, exitMessage); + super.onExit((exitValue == ScpIoUtils.OK) ? testExitValue : exitValue, exitMessage); } } @@ -1086,7 +1093,7 @@ public class ScpTest extends BaseTestSupport { String remotePath = CommonTestSupportUtils.resolveRelativeRemotePath(parentPath, remoteDir); String fileName = "file.txt"; Path remoteFile = remoteDir.resolve(fileName); - String mode = ScpHelper.getOctalPermissions(EnumSet.of( + String mode = ScpPathCommandDetailsSupport.getOctalPermissions(EnumSet.of( PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); @@ -1138,7 +1145,9 @@ public class ScpTest extends BaseTestSupport { os.flush(); String header = readLine(is); - String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + Files.size(target) + " " + fileName; + String expHeader + = ScpReceiveFileCommandDetails.COMMAND_NAME + ScpReceiveFileCommandDetails.DEFAULT_FILE_OCTAL_PERMISSIONS + + " " + Files.size(target) + " " + fileName; assertEquals("Mismatched header for " + path, expHeader, header); String lenValue = header.substring(6, header.indexOf(' ', 6)); @@ -1171,14 +1180,17 @@ public class ScpTest extends BaseTestSupport { os.flush(); String header = readLine(is); - String expPrefix = "D" + ScpHelper.DEFAULT_DIR_OCTAL_PERMISSIONS + " 0 "; + String expPrefix = ScpReceiveDirCommandDetails.COMMAND_NAME + + ScpReceiveDirCommandDetails.DEFAULT_DIR_OCTAL_PERMISSIONS + " 0 "; assertTrue("Bad header prefix for " + path + ": " + header, header.startsWith(expPrefix)); os.write(0); os.flush(); header = readLine(is); String fileName = Objects.toString(target.getFileName(), null); - String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + Files.size(target) + " " + fileName; + String expHeader + = ScpReceiveFileCommandDetails.COMMAND_NAME + ScpReceiveFileCommandDetails.DEFAULT_FILE_OCTAL_PERMISSIONS + + " " + Files.size(target) + " " + fileName; assertEquals("Mismatched dir header for " + path, expHeader, header); int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6))); os.write(0); @@ -1233,9 +1245,10 @@ public class ScpTest extends BaseTestSupport { Path parent = target.getParent(); Collection<PosixFilePermission> perms = IoUtils.getPermissions(parent); - String octalPerms = ScpHelper.getOctalPermissions(perms); + String octalPerms = ScpPathCommandDetailsSupport.getOctalPermissions(perms); String name = Objects.toString(target.getFileName(), null); - assertAckReceived(os, is, "C" + octalPerms + " " + data.length() + " " + name); + assertAckReceived(os, is, + ScpReceiveFileCommandDetails.COMMAND_NAME + octalPerms + " " + data.length() + " " + name); os.write(data.getBytes(StandardCharsets.UTF_8)); os.flush(); @@ -1298,11 +1311,9 @@ public class ScpTest extends BaseTestSupport { os.flush(); assertAckReceived(is, "Send data of " + path); - os.write(0); - os.flush(); + ScpIoUtils.ack(os); + ScpIoUtils.writeLine(os, ScpDirEndCommandDetails.HEADER); - os.write("E\n".getBytes(StandardCharsets.UTF_8)); - os.flush(); assertAckReceived(is, "Signal end of " + path); } finally { c.disconnect();