You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2021/03/05 09:26:14 UTC

[mina-sshd] 04/07: [SSHD-1133] Added capability to specify a custom charset for handling the SCP protocol textual commands and responses

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 da23f559add671399c218c1525852c683d07ea5e
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Feb 26 11:19:00 2021 +0200

    [SSHD-1133] Added capability to specify a custom charset for handling the SCP protocol textual commands and responses
---
 CHANGES.md                                         |  3 +-
 docs/scp.md                                        | 15 ++++++++-
 .../org/apache/sshd/scp/ScpModuleProperties.java   | 14 ++++++++
 .../scp/client/ScpRemote2RemoteTransferHelper.java | 25 +++++++++-----
 .../java/org/apache/sshd/scp/common/ScpHelper.java | 38 +++++++++++++++-------
 .../apache/sshd/scp/common/helpers/ScpAckInfo.java | 28 +++++++++-------
 .../apache/sshd/scp/common/helpers/ScpIoUtils.java | 27 ++++++++-------
 .../org/apache/sshd/scp/server/ScpCommand.java     |  3 +-
 .../java/org/apache/sshd/scp/server/ScpShell.java  |  2 +-
 .../java/org/apache/sshd/scp/client/ScpTest.java   | 30 ++++++++---------
 10 files changed, 121 insertions(+), 64 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index df0eb3e..131c6d0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -46,4 +46,5 @@
 * [SSHD-1125](https://issues.apache.org/jira/browse/SSHD-1125) Added mechanism to throttle pending write requests in BufferedIoOutputStream
 * [SSHD-1127](https://issues.apache.org/jira/browse/SSHD-1127) Added capability to register a custom receiver for SFTP STDERR channel raw or stream data
 * [SSHD-1133](https://issues.apache.org/jira/browse/SSHD-1133) Added capability to specify a custom charset for parsing incoming commands to the `ScpShell`
-* [SSHD-1133](https://issues.apache.org/jira/browse/SSHD-1133) Added capability to specify a custom charset for returning environment variables related data from the `ScpShell`
\ No newline at end of file
+* [SSHD-1133](https://issues.apache.org/jira/browse/SSHD-1133) Added capability to specify a custom charset for returning environment variables related data from the `ScpShell`
+* [SSHD-1133](https://issues.apache.org/jira/browse/SSHD-1133) Added capability to specify a custom charset for handling the SCP protocol textual commands and responses
\ No newline at end of file
diff --git a/docs/scp.md b/docs/scp.md
index 96274b8..d04ecd7 100644
--- a/docs/scp.md
+++ b/docs/scp.md
@@ -38,6 +38,19 @@ try (ClientSession session = client.connect(user, host, port)
 
 ```
 
+### General text encoding/decoding
+
+The basic SCP protocol is text-based and therefore subject to character encoding/decoding of the data being exchanged. By default, the exchange is supposed to use the UTF-8 encoding which is the default/standard one for SSH. However, there are clients/servers "in the wild" that do not conform to this convention. For this purpose, it is possible to define a different  character encoding via the `SCP_INCOMING/OUTGOING_ENCODING` properties - e.g.:
+
+```java
+SshServer sshd = ...setup server...
+// Can also use the character name string rather than the object instance itself
+ScpModuleProperties.SCP_INCOMING_ENCODING.set(sshd, Charset.forName("US-ASCII"));
+ScpModuleProperties.SCP_OUTGOING_ENCODING.set(sshd, Charset.forName("US-ASCII"));
+```
+
+**Caveat emptor:** the code does not enforce "symmetry" of the chosen character sets - in other words, users can either by design or error cause different encoding to be used for the incoming commands vs. the outgoing responses. It is important to bear in mind that if the text to be encoded/decoded contains characters that cannot be safely handled by the chosen encoder/decoder than the result might not be correctly parsed/understood by the peer.
+
 ## Client-side SCP
 
 In order to obtain an `ScpClient` one needs to use an `ScpClientCreator`:
@@ -202,7 +215,7 @@ ScpModuleProperties.SHELL_NAME_ENCODING_CHARSET.set(sshd, Charset.forName("US-AS
 ScpModuleProperties.SHELL_NAME_DECODING_CHARSET.set(sshd, Charset.forName("US-ASCII"));
 ```
 
-**Caveat emptor:** that the code does not enforce "symmetry" of the chosen character sets - in other words, user can either by design or error cause different encoding to be used for the incoming commands vs. the outgoing responses. It is important to bear in mind that if the text to be encoded/decoded contains characters that cannot be  safely handled by the chosen encoder/decoder than the result might not be correctly parsed/understood by the peer.
+**Caveat emptor:** the code does not enforce "symmetry" of the chosen character sets - in other words, users can either by design or error cause different encoding to be used for the incoming commands vs. the outgoing responses. It is important to bear in mind that if the text to be encoded/decoded contains characters that cannot be safely handled by the chosen encoder/decoder than the result might not be correctly parsed/understood by the peer.
 
 A similar behavior is controlled via `SHELL_ENVVARS_ENCODING_CHARSET` which controls responses from the `ScpShell` regarding environment variables - the main difference being that the default is US-ASCII rather than UTF-8.
 
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/ScpModuleProperties.java b/sshd-scp/src/main/java/org/apache/sshd/scp/ScpModuleProperties.java
index 9f19e02..8c3a2e9 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/ScpModuleProperties.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/ScpModuleProperties.java
@@ -47,6 +47,20 @@ public final class ScpModuleProperties {
             = Property.duration("scp-exec-channel-exit-status-timeout", Duration.ofSeconds(5));
 
     /**
+     * Used to indicate the {@link Charset} (or its name) for decoding incoming commands/responses sent by the peer
+     * (either client or server).
+     */
+    public static final Property<Charset> SCP_INCOMING_ENCODING
+            = Property.charset("scp-incoming-encoding-charset", StandardCharsets.UTF_8);
+
+    /**
+     * Used to indicate the {@link Charset} (or its name) for encoding outgoing commands/responses sent to the peer
+     * (either client or server).
+     */
+    public static final Property<Charset> SCP_OUTGOING_ENCODING
+            = Property.charset("scp-outgoing-encoding-charset", StandardCharsets.UTF_8);
+
+    /**
      * Whether to synchronize written file data with underlying file-system
      */
     public static final Property<Boolean> PROP_AUTO_SYNC_FILE_ON_WRITE
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 387ebbd..fc2a036 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
@@ -24,6 +24,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.StreamCorruptedException;
+import java.nio.charset.Charset;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -36,6 +37,7 @@ import org.apache.sshd.common.util.SelectorUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.LimitInputStream;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.scp.ScpModuleProperties;
 import org.apache.sshd.scp.client.ScpClient.Option;
 import org.apache.sshd.scp.common.helpers.AbstractScpCommandDetails;
 import org.apache.sshd.scp.common.helpers.ScpAckInfo;
@@ -54,6 +56,8 @@ import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails;
  */
 public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
     protected final ScpRemote2RemoteTransferListener listener;
+    protected final Charset csIn;
+    protected final Charset csOut;
 
     private final ClientSession sourceSession;
     private final ClientSession destSession;
@@ -70,7 +74,10 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
     public ScpRemote2RemoteTransferHelper(ClientSession sourceSession, ClientSession destSession,
                                           ScpRemote2RemoteTransferListener listener) {
         this.sourceSession = Objects.requireNonNull(sourceSession, "No source session provided");
+        this.csIn = ScpModuleProperties.SCP_INCOMING_ENCODING.getRequired(sourceSession);
         this.destSession = Objects.requireNonNull(destSession, "No destination session provided");
+        this.csOut = ScpModuleProperties.SCP_OUTGOING_ENCODING.getRequired(destSession);
+
         this.listener = listener;
     }
 
@@ -199,7 +206,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
             throw new IllegalArgumentException("Invalid file transfer request: " + header);
         }
 
-        ScpIoUtils.writeLine(dstOut, header);
+        ScpIoUtils.writeLine(dstOut, csOut, header);
         ScpAckInfo ackInfo = transferStatusCode(header, dstIn, srcOut);
         ackInfo.validateCommandStatusCode("[DST] " + header, "handleFileTransferRequest");
 
@@ -272,7 +279,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
             throw new IllegalArgumentException("Invalid file transfer request: " + header);
         }
 
-        ScpIoUtils.writeLine(dstOut, header);
+        ScpIoUtils.writeLine(dstOut, csOut, header);
         ScpAckInfo ackInfo = transferStatusCode(header, dstIn, srcOut);
         ackInfo.validateCommandStatusCode("[DST@" + depth + "] " + header, "handleDirectoryTransferRequest");
 
@@ -339,7 +346,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
                     }
 
                     case ScpDirEndCommandDetails.COMMAND_NAME: {
-                        ScpIoUtils.writeLine(dstOut, header);
+                        ScpIoUtils.writeLine(dstOut, csOut, header);
                         ackInfo = transferStatusCode(header, dstIn, srcOut);
                         ackInfo.validateCommandStatusCode("[DST@" + depth + "] " + header, "handleDirectoryTransferRequest");
 
@@ -377,7 +384,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
 
         long xferCount;
         try (InputStream inputStream = new LimitInputStream(srcIn, length)) {
-            ScpAckInfo.sendOk(srcOut); // ready to receive the data from source
+            ScpAckInfo.sendOk(srcOut, csOut); // ready to receive the data from source
             xferCount = IoUtils.copy(inputStream, dstOut);
             dstOut.flush(); // make sure all data sent to destination
         }
@@ -392,7 +399,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
         ackInfo.validateCommandStatusCode("[SRC-EOF] " + header, "transferSimpleFile");
 
         // wait for destination to signal data received
-        ackInfo = ScpAckInfo.readAck(dstIn, false);
+        ackInfo = ScpAckInfo.readAck(dstIn, csIn, false);
         ackInfo.validateCommandStatusCode("[DST-EOF] " + header, "transferSimpleFile");
         return xferCount;
     }
@@ -402,7 +409,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
             String destination, InputStream dstIn, OutputStream dstOut,
             String header)
             throws IOException {
-        ScpIoUtils.writeLine(dstOut, header);
+        ScpIoUtils.writeLine(dstOut, csOut, header);
         ScpAckInfo ackInfo = transferStatusCode(header, dstIn, srcOut);
         ackInfo.validateCommandStatusCode("[DST] " + header, "transferTimestampCommand");
 
@@ -414,11 +421,11 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
     }
 
     protected ScpAckInfo transferStatusCode(Object logHint, InputStream in, OutputStream out) throws IOException {
-        ScpAckInfo ackInfo = ScpAckInfo.readAck(in, false);
+        ScpAckInfo ackInfo = ScpAckInfo.readAck(in, csIn, false);
         if (log.isDebugEnabled()) {
             log.debug("transferStatusCode({})[{}] {}", this, logHint, ackInfo);
         }
-        ackInfo.send(out);
+        ackInfo.send(out, csOut);
         return ackInfo;
     }
 
@@ -436,7 +443,7 @@ public class ScpRemote2RemoteTransferHelper extends AbstractLoggingBean {
             return new ScpAckInfo(c);
         }
 
-        String line = ScpIoUtils.readLine(in, false);
+        String line = ScpIoUtils.readLine(in, csIn, false);
         if ((c == ScpAckInfo.WARNING) || (c == ScpAckInfo.ERROR)) {
             if (log.isDebugEnabled()) {
                 log.debug("receiveNextCmd({})[{}] - ACK={}", this, logHint, new ScpAckInfo(c, line));
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 a22bdcb..6114066 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
@@ -24,6 +24,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.StreamCorruptedException;
+import java.nio.charset.Charset;
 import java.nio.file.DirectoryStream;
 import java.nio.file.FileSystem;
 import java.nio.file.InvalidPathException;
@@ -45,6 +46,7 @@ import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.LimitInputStream;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.scp.ScpModuleProperties;
 import org.apache.sshd.scp.common.ScpTransferEventListener.FileOperation;
 import org.apache.sshd.scp.common.helpers.DefaultScpFileOpener;
 import org.apache.sshd.scp.common.helpers.ScpAckInfo;
@@ -79,18 +81,30 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
     public static final int MIN_SEND_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
 
     protected final InputStream in;
+    protected final Charset csIn;
     protected final OutputStream out;
+    protected final Charset csOut;
     protected final FileSystem fileSystem;
     protected final ScpFileOpener opener;
     protected final ScpTransferEventListener listener;
 
     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(session, in, ScpModuleProperties.SCP_INCOMING_ENCODING.getRequired(session),
+             out, ScpModuleProperties.SCP_OUTGOING_ENCODING.getRequired(session),
+             fileSystem, opener, eventListener);
+    }
+
+    public ScpHelper(Session session,
+                     InputStream in, Charset csIn, OutputStream out, Charset csOut,
+                     FileSystem fileSystem, ScpFileOpener opener, ScpTransferEventListener eventListener) {
         this.sessionInstance = Objects.requireNonNull(session, "No session");
         this.in = Objects.requireNonNull(in, "No input stream");
+        this.csIn = Objects.requireNonNull(csIn, "No input charset");
         this.out = Objects.requireNonNull(out, "No output stream");
+        this.csOut = Objects.requireNonNull(csOut, "No output charset");
         this.fileSystem = fileSystem;
         this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
         this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
@@ -182,7 +196,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
                 case -1:
                     return;
                 case ScpReceiveDirCommandDetails.COMMAND_NAME:
-                    line = ScpIoUtils.readLine(in);
+                    line = ScpIoUtils.readLine(in, csIn);
                     line = Character.toString((char) c) + line;
                     isDir = true;
                     if (debugEnabled) {
@@ -190,14 +204,14 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
                     }
                     break;
                 case ScpReceiveFileCommandDetails.COMMAND_NAME:
-                    line = ScpIoUtils.readLine(in);
+                    line = ScpIoUtils.readLine(in, csIn);
                     line = Character.toString((char) c) + line;
                     if (debugEnabled) {
                         log.debug("receive({}) - Received 'C' header: {}", this, line);
                     }
                     break;
                 case ScpTimestampCommandDetails.COMMAND_NAME:
-                    line = ScpIoUtils.readLine(in);
+                    line = ScpIoUtils.readLine(in, csIn);
                     line = Character.toString((char) c) + line;
                     if (debugEnabled) {
                         log.debug("receive({}) - Received 'T' header: {}", this, line);
@@ -206,7 +220,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
                     sendOk();
                     continue;
                 case ScpDirEndCommandDetails.COMMAND_NAME:
-                    line = ScpIoUtils.readLine(in);
+                    line = ScpIoUtils.readLine(in, csIn);
                     line = Character.toString((char) c) + line;
                     if (debugEnabled) {
                         log.debug("receive({}) - Received 'E' header: {}", this, line);
@@ -238,7 +252,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
         }
 
         if ((c == ScpAckInfo.WARNING) || (c == ScpAckInfo.ERROR)) {
-            String line = ScpIoUtils.readLine(in, true);
+            String line = ScpIoUtils.readLine(in, csIn, true);
             if (log.isDebugEnabled()) {
                 log.debug("receiveNextCmd - ACK={}", new ScpAckInfo(c, line));
             }
@@ -388,7 +402,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
     }
 
     public String readLine(boolean canEof) throws IOException {
-        return ScpIoUtils.readLine(in, canEof);
+        return ScpIoUtils.readLine(in, csIn, canEof);
     }
 
     public void send(Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
@@ -538,7 +552,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
 
         ScpTimestampCommandDetails time = resolver.getTimestamp();
         if (preserve && (time != null)) {
-            ScpAckInfo ackInfo = ScpIoUtils.sendAcknowledgedCommand(time, in, out);
+            ScpAckInfo ackInfo = ScpIoUtils.sendAcknowledgedCommand(time, in, csIn, out, csOut);
             String cmd = time.toHeader();
             if (debugEnabled) {
                 log.debug("sendStream({})[{}] command='{}' ACK={}", this, resolver, cmd, ackInfo);
@@ -711,7 +725,7 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
     }
 
     protected ScpAckInfo sendAcknowledgedCommand(String cmd) throws IOException {
-        return ScpIoUtils.sendAcknowledgedCommand(cmd, in, out);
+        return ScpIoUtils.sendAcknowledgedCommand(cmd, in, csIn, out, csOut);
     }
 
     public void sendOk() throws IOException {
@@ -727,11 +741,11 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
     }
 
     protected void sendResponseMessage(int level, String message) throws IOException {
-        ScpAckInfo.sendAck(out, level, message);
+        ScpAckInfo.sendAck(out, csOut, level, message);
     }
 
     public ScpAckInfo readAck(boolean canEof) throws IOException {
-        return ScpAckInfo.readAck(in, canEof);
+        return ScpAckInfo.readAck(in, csIn, canEof);
     }
 
     @Override
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
index b89e9db..9b0b986 100644
--- 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
@@ -23,6 +23,7 @@ import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.charset.Charset;
 
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -59,8 +60,8 @@ public class ScpAckInfo {
         return line;
     }
 
-    public <O extends OutputStream> O send(O out) throws IOException {
-        return sendAck(out, getStatusCode(), getLine());
+    public <O extends OutputStream> O send(O out, Charset cs) throws IOException {
+        return sendAck(out, cs, getStatusCode(), getLine());
     }
 
     public void validateCommandStatusCode(String command, Object location) throws IOException {
@@ -83,7 +84,7 @@ public class ScpAckInfo {
         }
     }
 
-    public static ScpAckInfo readAck(InputStream in, boolean canEof) throws IOException {
+    public static ScpAckInfo readAck(InputStream in, Charset cs, boolean canEof) throws IOException {
         int statusCode = in.read();
         if (statusCode == -1) {
             if (canEof) {
@@ -96,7 +97,7 @@ public class ScpAckInfo {
             return new ScpAckInfo(statusCode);  // OK status has no extra data
         }
 
-        String line = ScpIoUtils.readLine(in);
+        String line = ScpIoUtils.readLine(in, cs);
         return new ScpAckInfo(statusCode, line);
     }
 
@@ -104,24 +105,27 @@ public class ScpAckInfo {
      * Sends {@link #OK} ACK code
      *
      * @param  out         The target {@link OutputStream}
+     * @param  cs          The {@link Charset} to use to write the textual data
      * @throws IOException If failed to send the ACK code
      */
-    public static void sendOk(OutputStream out) throws IOException {
-        sendAck(out, OK, null /* ignored */);
+    public static void sendOk(OutputStream out, Charset cs) throws IOException {
+        sendAck(out, cs, 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 sendWarning(O out, Charset cs, String message) throws IOException {
+        return sendAck(out, cs, 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 sendError(O out, Charset cs, String message) throws IOException {
+        return sendAck(out, cs, ScpAckInfo.ERROR, (message == null) ? "" : message);
     }
 
-    public static <O extends OutputStream> O sendAck(O out, int level, String message) throws IOException {
+    public static <O extends OutputStream> O sendAck(
+            O out, Charset cs, int level, String message)
+            throws IOException {
         out.write(level);
         if (level != OK) {
-            ScpIoUtils.writeLine(out, message); // this also flushes
+            ScpIoUtils.writeLine(out, cs, message); // this also flushes
         } else {
             out.flush();
         }
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 4ad78f2..6407287 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
@@ -24,7 +24,7 @@ import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
+import java.nio.charset.Charset;
 import java.time.Duration;
 import java.util.Collection;
 import java.util.Collections;
@@ -52,16 +52,16 @@ public final class ScpIoUtils {
         throw new UnsupportedOperationException("No instance");
     }
 
-    public static String readLine(InputStream in) throws IOException {
-        return readLine(in, false);
+    public static String readLine(InputStream in, Charset cs) throws IOException {
+        return readLine(in, cs, false);
     }
 
-    public static String readLine(InputStream in, boolean canEof) throws IOException {
+    public static String readLine(InputStream in, Charset cs, 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());
+                    return baos.toString(cs.name());
                 } else if (c == -1) {
                     if (!canEof) {
                         throw new EOFException("EOF while await end of line");
@@ -74,22 +74,25 @@ public final class ScpIoUtils {
         }
     }
 
-    public static void writeLine(OutputStream out, String cmd) throws IOException {
+    public static void writeLine(OutputStream out, Charset cs, String cmd) throws IOException {
         if (cmd != null) {
-            out.write(cmd.getBytes(StandardCharsets.UTF_8));
+            out.write(cmd.getBytes(cs));
         }
         out.write('\n');
         out.flush();
     }
 
-    public static ScpAckInfo sendAcknowledgedCommand(AbstractScpCommandDetails cmd, InputStream in, OutputStream out)
+    public static ScpAckInfo sendAcknowledgedCommand(
+            AbstractScpCommandDetails cmd, InputStream in, Charset csIn, OutputStream out, Charset csOut)
             throws IOException {
-        return sendAcknowledgedCommand(cmd.toHeader(), in, out);
+        return sendAcknowledgedCommand(cmd.toHeader(), in, csIn, out, csOut);
     }
 
-    public static ScpAckInfo sendAcknowledgedCommand(String cmd, InputStream in, OutputStream out) throws IOException {
-        writeLine(out, cmd);
-        return ScpAckInfo.readAck(in, false);
+    public static ScpAckInfo sendAcknowledgedCommand(
+            String cmd, InputStream in, Charset csIn, OutputStream out, Charset csOut)
+            throws IOException {
+        writeLine(out, csOut, cmd);
+        return ScpAckInfo.readAck(in, csIn, false);
     }
 
     public static String getExitStatusName(Integer exitStatus) {
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 a3a588a..7b25b68 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
@@ -19,6 +19,7 @@
 package org.apache.sshd.scp.server;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.Objects;
 
@@ -220,7 +221,7 @@ public class ScpCommand extends AbstractFileSystemCommand implements ServerChann
             log.debug("writeCommandResponseMessage({}) command='{}', exit-status={}: {}",
                     getServerSession(), command, exitValue, exitMessage);
         }
-        ScpAckInfo.sendAck(getOutputStream(), exitValue, exitMessage);
+        ScpAckInfo.sendAck(getOutputStream(), StandardCharsets.UTF_8, 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 df4d433..232a4b0 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
@@ -499,7 +499,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
                 exitValue = ScpAckInfo.ERROR;
             }
             String exitMessage = GenericUtils.trimToEmpty(e.getMessage());
-            ScpAckInfo.sendAck(getOutputStream(), exitValue, exitMessage);
+            ScpAckInfo.sendAck(getOutputStream(), StandardCharsets.UTF_8, 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 6f6c095..7f4f2e4 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
@@ -947,7 +947,7 @@ public class ScpTest extends AbstractScpTestSupport {
             os.write(0);
             os.flush();
 
-            String header = ScpIoUtils.readLine(is, false);
+            String header = ScpIoUtils.readLine(is, StandardCharsets.UTF_8, false);
             String expHeader
                     = ScpReceiveFileCommandDetails.COMMAND_NAME + ScpReceiveFileCommandDetails.DEFAULT_FILE_OCTAL_PERMISSIONS
                       + " " + Files.size(target) + " " + fileName;
@@ -979,33 +979,33 @@ public class ScpTest extends AbstractScpTestSupport {
 
         try (OutputStream os = c.getOutputStream();
              InputStream is = c.getInputStream()) {
-            ScpAckInfo.sendOk(os);
+            ScpAckInfo.sendOk(os, StandardCharsets.UTF_8);
 
-            String header = ScpIoUtils.readLine(is, false);
+            String header = ScpIoUtils.readLine(is, StandardCharsets.UTF_8, false);
             String expPrefix = ScpReceiveDirCommandDetails.COMMAND_NAME
                                + ScpReceiveDirCommandDetails.DEFAULT_DIR_OCTAL_PERMISSIONS + " 0 ";
             assertTrue("Bad header prefix for " + path + ": " + header, header.startsWith(expPrefix));
-            ScpAckInfo.sendOk(os);
+            ScpAckInfo.sendOk(os, StandardCharsets.UTF_8);
 
-            header = ScpIoUtils.readLine(is, false);
+            header = ScpIoUtils.readLine(is, StandardCharsets.UTF_8, false);
             String fileName = Objects.toString(target.getFileName(), null);
             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)));
-            ScpAckInfo.sendOk(os);
+            ScpAckInfo.sendOk(os, StandardCharsets.UTF_8);
 
             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);
 
-            ScpAckInfo.sendOk(os);
+            ScpAckInfo.sendOk(os, StandardCharsets.UTF_8);
 
-            header = ScpIoUtils.readLine(is, false);
+            header = ScpIoUtils.readLine(is, StandardCharsets.UTF_8, false);
             assertEquals("Mismatched end value for " + path, "E", header);
-            ScpAckInfo.sendOk(os);
+            ScpAckInfo.sendOk(os, StandardCharsets.UTF_8);
 
             return new String(buffer, StandardCharsets.UTF_8);
         } finally {
@@ -1022,7 +1022,7 @@ public class ScpTest extends AbstractScpTestSupport {
         try (OutputStream os = c.getOutputStream();
              InputStream is = c.getInputStream()) {
 
-            ScpAckInfo.sendOk(os);
+            ScpAckInfo.sendOk(os, StandardCharsets.UTF_8);
             assertEquals("Mismatched response for command: " + command, ScpAckInfo.ERROR, is.read());
         } finally {
             c.disconnect();
@@ -1051,7 +1051,7 @@ public class ScpTest extends AbstractScpTestSupport {
             os.flush();
             assertAckReceived(is, "Sent data (length=" + data.length() + ") for " + path + "[" + name + "]");
 
-            ScpAckInfo.sendOk(os);
+            ScpAckInfo.sendOk(os, StandardCharsets.UTF_8);
 
             Thread.sleep(100);
         } finally {
@@ -1060,7 +1060,7 @@ public class ScpTest extends AbstractScpTestSupport {
     }
 
     protected void assertAckReceived(OutputStream os, InputStream is, String command) throws IOException {
-        ScpIoUtils.writeLine(os, command);
+        ScpIoUtils.writeLine(os, StandardCharsets.UTF_8, command);
         assertAckReceived(is, command);
     }
 
@@ -1080,7 +1080,7 @@ public class ScpTest extends AbstractScpTestSupport {
             assertAckReceived(is, command);
 
             command = "C7777 " + data.length() + " " + name;
-            ScpIoUtils.writeLine(os, command);
+            ScpIoUtils.writeLine(os, StandardCharsets.UTF_8, command);
             assertEquals("Mismatched response for command=" + command, ScpAckInfo.ERROR, is.read());
         } finally {
             c.disconnect();
@@ -1105,8 +1105,8 @@ public class ScpTest extends AbstractScpTestSupport {
             os.flush();
             assertAckReceived(is, "Send data of " + path);
 
-            ScpAckInfo.sendOk(os);
-            ScpIoUtils.writeLine(os, ScpDirEndCommandDetails.HEADER);
+            ScpAckInfo.sendOk(os, StandardCharsets.UTF_8);
+            ScpIoUtils.writeLine(os, StandardCharsets.UTF_8, ScpDirEndCommandDetails.HEADER);
             assertAckReceived(is, "Signal end of " + path);
         } finally {
             c.disconnect();