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 2019/02/21 18:50:48 UTC

[mina-sshd] 03/05: [SSHD-897] Exposed API to provide PTY and/or environment options when opening a client command or shell channel

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 0393ebe048f2e7bd56dd0ae6c29b40dca65ab755
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Wed Feb 20 10:22:22 2019 +0200

    [SSHD-897] Exposed API to provide PTY and/or environment options when opening a client command or shell channel
---
 CHANGES.md                                         |   7 +
 .../common/channel/PtyChannelConfiguration.java    | 111 +++++++++++++
 .../channel/PtyChannelConfigurationHolder.java     |  60 +++++++
 .../channel/PtyChannelConfigurationMutator.java    |  82 ++++++++++
 .../apache/sshd/common/channel/SttySupport.java    |   2 +-
 .../apache/sshd/client/channel/ChannelExec.java    |   6 +-
 .../apache/sshd/client/channel/ChannelShell.java   |   6 +-
 .../client/channel/PtyCapableChannelSession.java   | 181 ++++++++++++---------
 .../sshd/client/session/AbstractClientSession.java |  15 +-
 .../apache/sshd/client/session/ClientSession.java  |  67 ++++++--
 .../java/org/apache/sshd/client/ClientTest.java    |   2 +-
 11 files changed, 436 insertions(+), 103 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 01570c0..9e82712 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -20,6 +20,13 @@ current sesssion - client/server proposals and what has been negotiated.
 
 * The `SignalListener` accepts a `Channel` argument indicating the channel instance through which the signal was received.
 
+* When creating a client shell or command channel one can provide optional PTY and/or environment values in order
+to override the internal default ones.
+
+    * In this context, the `PtyCapableChannelSession#setEnv` method has been modified to accept ANY object.
+    When the environment values are sent to the server, the object's `toString()` will be used. Furthermore,
+    if one provides a `null` value, the previous registered value (if any) is **removed**.
+
 ## Minor code helpers
 
 * The `Session` object provides a `isServerSession` method that can be used to distinguish between
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfiguration.java b/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfiguration.java
new file mode 100644
index 0000000..141012c
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfiguration.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.channel;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class PtyChannelConfiguration implements PtyChannelConfigurationMutator {
+    private String ptyType;
+    private int ptyColumns = DEFAULT_COLUMNS_COUNT;
+    private int ptyLines = DEFAULT_ROWS_COUNT;
+    private int ptyWidth = DEFAULT_WIDTH;
+    private int ptyHeight = DEFAULT_HEIGHT;
+    private Map<PtyMode, Integer> ptyModes = new EnumMap<>(PtyMode.class);
+
+    public PtyChannelConfiguration() {
+        ptyModes.putAll(DEFAULT_PTY_MODES);
+    }
+
+    @Override
+    public String getPtyType() {
+        return ptyType;
+    }
+
+    @Override
+    public void setPtyType(String ptyType) {
+        this.ptyType = ptyType;
+    }
+
+    @Override
+    public int getPtyColumns() {
+        return ptyColumns;
+    }
+
+    @Override
+    public void setPtyColumns(int ptyColumns) {
+        this.ptyColumns = ptyColumns;
+    }
+
+    @Override
+    public int getPtyLines() {
+        return ptyLines;
+    }
+
+    @Override
+    public void setPtyLines(int ptyLines) {
+        this.ptyLines = ptyLines;
+    }
+
+    @Override
+    public int getPtyWidth() {
+        return ptyWidth;
+    }
+
+    @Override
+    public void setPtyWidth(int ptyWidth) {
+        this.ptyWidth = ptyWidth;
+    }
+
+    @Override
+    public int getPtyHeight() {
+        return ptyHeight;
+    }
+
+    @Override
+    public void setPtyHeight(int ptyHeight) {
+        this.ptyHeight = ptyHeight;
+    }
+
+    @Override
+    public Map<PtyMode, Integer> getPtyModes() {
+        return ptyModes;
+    }
+
+    @Override
+    public void setPtyModes(Map<PtyMode, Integer> ptyModes) {
+        this.ptyModes = ptyModes;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()
+            + "[type=" + getPtyType()
+            + ", lines=" + getPtyLines()
+            + ", columns=" + getPtyColumns()
+            + ", height=" + getPtyHeight()
+            + ", width=" + getPtyWidth()
+            + ", modes=" + getPtyModes()
+            + "]";
+    }
+}
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfigurationHolder.java b/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfigurationHolder.java
new file mode 100644
index 0000000..6ba629b
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfigurationHolder.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.channel;
+
+import java.util.Map;
+
+import org.apache.sshd.common.util.MapEntryUtils.EnumMapBuilder;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface PtyChannelConfigurationHolder {
+    int DEFAULT_COLUMNS_COUNT = 80;
+    int DEFAULT_ROWS_COUNT = 24;
+    int DEFAULT_WIDTH = 640;
+    int DEFAULT_HEIGHT = 480;
+
+    String DUMMY_PTY_TYPE = "dummy";
+    String WINDOWS_PTY_TYPE = "windows";
+
+    Map<PtyMode, Integer> DEFAULT_PTY_MODES =
+        EnumMapBuilder.<PtyMode, Integer>builder(PtyMode.class)
+            .put(PtyMode.ISIG, 1)
+            .put(PtyMode.ICANON, 1)
+            .put(PtyMode.ECHO, 1)
+            .put(PtyMode.ECHOE, 1)
+            .put(PtyMode.ECHOK, 1)
+            .put(PtyMode.ECHONL, 0)
+            .put(PtyMode.NOFLSH, 0)
+            .immutable();
+
+    String getPtyType();
+
+    int getPtyColumns();
+
+    int getPtyLines();
+
+    int getPtyWidth();
+
+    int getPtyHeight();
+
+    Map<PtyMode, Integer> getPtyModes();
+}
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfigurationMutator.java b/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfigurationMutator.java
new file mode 100644
index 0000000..bd3cfc9
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/channel/PtyChannelConfigurationMutator.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.channel;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.sshd.common.util.OsUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface PtyChannelConfigurationMutator extends PtyChannelConfigurationHolder {
+    void setPtyType(String ptyType);
+
+    void setPtyColumns(int ptyColumns);
+
+    void setPtyLines(int ptyLines);
+
+    void setPtyWidth(int ptyWidth);
+
+    void setPtyHeight(int ptyHeight);
+
+    void setPtyModes(Map<PtyMode, Integer> ptyModes);
+
+    static <M extends PtyChannelConfigurationMutator> M copyConfiguration(PtyChannelConfigurationHolder src, M dst) {
+        if ((src == null) || (dst == null)) {
+            return dst;
+        }
+
+        dst.setPtyColumns(src.getPtyColumns());
+        dst.setPtyHeight(src.getPtyHeight());
+        dst.setPtyLines(src.getPtyLines());
+        dst.setPtyModes(src.getPtyModes());
+        dst.setPtyType(src.getPtyType());
+        dst.setPtyWidth(src.getPtyWidth());
+        return dst;
+    }
+
+    /**
+     * Uses O/S detection to initialize some default PTY related values
+     *
+     * @param <M> Generic {@link PtyChannelConfigurationMutator} instance
+     * @param mutator The mutator to update - ignored if {@code null}
+     * @return The updated mutator
+     * @throws IOException If failed to access some O/S related configuration
+     * @throws InterruptedException If interrupted during access of O/S related configuration
+     */
+    static <M extends PtyChannelConfigurationMutator> M setupSensitiveDefaultPtyConfiguration(M mutator)
+            throws IOException, InterruptedException {
+        if (mutator == null) {
+            return null;
+        }
+
+        if (OsUtils.isUNIX()) {
+            mutator.setPtyModes(SttySupport.getUnixPtyModes());
+            mutator.setPtyColumns(SttySupport.getTerminalWidth());
+            mutator.setPtyLines(SttySupport.getTerminalHeight());
+        } else {
+            mutator.setPtyType(WINDOWS_PTY_TYPE);
+        }
+
+        return mutator;
+    }
+}
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/channel/SttySupport.java b/sshd-common/src/main/java/org/apache/sshd/common/channel/SttySupport.java
index bd02674..d996dee 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/channel/SttySupport.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/channel/SttySupport.java
@@ -67,7 +67,7 @@ public final class SttySupport {
             if (str.charAt(0) == 'v') {
                 str = str.substring(1);
                 int v = findChar(stty, str);
-                if (v < 0 && "reprint".equals(str)) {
+                if ((v < 0) && "reprint".equals(str)) {
                     v = findChar(stty, "rprnt");
                 }
                 if (v >= 0) {
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelExec.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelExec.java
index b5dfa14..4363c0d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelExec.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelExec.java
@@ -20,9 +20,11 @@ package org.apache.sshd.client.channel;
 
 import java.io.IOException;
 import java.util.Date;
+import java.util.Map;
 
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.channel.PtyChannelConfigurationHolder;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
@@ -42,8 +44,8 @@ public class ChannelExec extends PtyCapableChannelSession {
 
     private final String command;
 
-    public ChannelExec(String command) {
-        super(false);
+    public ChannelExec(String command, PtyChannelConfigurationHolder configHolder, Map<String, ?> env) {
+        super(false, configHolder, env);
         this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "Command may not be null/empty");
     }
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelShell.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelShell.java
index 32000b8..bf3940c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelShell.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelShell.java
@@ -20,9 +20,11 @@ package org.apache.sshd.client.channel;
 
 import java.io.IOException;
 import java.util.Date;
+import java.util.Map;
 
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.channel.PtyChannelConfigurationHolder;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.buffer.Buffer;
 
@@ -39,8 +41,8 @@ public class ChannelShell extends PtyCapableChannelSession {
     public static final String REQUEST_SHELL_REPLY = "channel-shell-want-reply";
     public static final boolean DEFAULT_REQUEST_SHELL_REPLY = false;
 
-    public ChannelShell() {
-        super(true);
+    public ChannelShell(PtyChannelConfigurationHolder configHolder, Map<String, ?> env) {
+        super(true, configHolder, env);
     }
 
     @Override
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java
index e62c44d..c42eae3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java
@@ -20,18 +20,19 @@ package org.apache.sshd.client.channel;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.EnumMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Objects;
 
 import org.apache.sshd.agent.SshAgentFactory;
 import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.channel.PtyChannelConfiguration;
+import org.apache.sshd.common.channel.PtyChannelConfigurationHolder;
+import org.apache.sshd.common.channel.PtyChannelConfigurationMutator;
 import org.apache.sshd.common.channel.PtyMode;
-import org.apache.sshd.common.channel.SttySupport;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.MapEntryUtils.EnumMapBuilder;
-import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 
@@ -75,51 +76,41 @@ import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class PtyCapableChannelSession extends ChannelSession {
-    public static final int DEFAULT_COLUMNS_COUNT = 80;
-    public static final int DEFAULT_ROWS_COUNT = 24;
-    public static final int DEFAULT_WIDTH = 640;
-    public static final int DEFAULT_HEIGHT = 480;
-    public static final Map<PtyMode, Integer> DEFAULT_PTY_MODES =
-        EnumMapBuilder.<PtyMode, Integer>builder(PtyMode.class)
-            .put(PtyMode.ISIG, 1)
-            .put(PtyMode.ICANON, 1)
-            .put(PtyMode.ECHO, 1)
-            .put(PtyMode.ECHOE, 1)
-            .put(PtyMode.ECHOK, 1)
-            .put(PtyMode.ECHONL, 0)
-            .put(PtyMode.NOFLSH, 0)
-            .immutable();
-
+public class PtyCapableChannelSession extends ChannelSession implements PtyChannelConfigurationMutator {
     private boolean agentForwarding;
     private boolean usePty;
-    private String ptyType;
-    private int ptyColumns = DEFAULT_COLUMNS_COUNT;
-    private int ptyLines = DEFAULT_ROWS_COUNT;
-    private int ptyWidth = DEFAULT_WIDTH;
-    private int ptyHeight = DEFAULT_HEIGHT;
-    private Map<PtyMode, Integer> ptyModes = new EnumMap<>(PtyMode.class);
-    private final Map<String, String> env = new LinkedHashMap<>();
-
-    public PtyCapableChannelSession(boolean usePty) {
+    private final Map<String, Object> env = new LinkedHashMap<>();
+    private final PtyChannelConfiguration config;
+
+    public PtyCapableChannelSession(boolean usePty, PtyChannelConfigurationHolder configHolder, Map<String, ?> env) {
         this.usePty = usePty;
+        this.config = PtyChannelConfigurationMutator.copyConfiguration(
+            configHolder, new PtyChannelConfiguration());
+        this.config.setPtyType(resolvePtyType(this.config));
+        if (GenericUtils.isNotEmpty(env)) {
+            for (Map.Entry<String, ?> ee : env.entrySet()) {
+                setEnv(ee.getKey(), ee.getValue());
+            }
+        }
+    }
+
+    protected String resolvePtyType(PtyChannelConfigurationHolder configHolder) {
+        String ptyType = configHolder.getPtyType();
+        if (GenericUtils.isNotEmpty(ptyType)) {
+            return ptyType;
+        }
+
         ptyType = System.getenv("TERM");
-        if (GenericUtils.isEmpty(ptyType)) {
-            ptyType = "dummy";
+        if (GenericUtils.isNotEmpty(ptyType)) {
+            return ptyType;
         }
 
-        ptyModes.putAll(DEFAULT_PTY_MODES);
+        return DUMMY_PTY_TYPE;
     }
 
     public void setupSensibleDefaultPty() {
         try {
-            if (OsUtils.isUNIX()) {
-                ptyModes = SttySupport.getUnixPtyModes();
-                ptyColumns = SttySupport.getTerminalWidth();
-                ptyLines = SttySupport.getTerminalHeight();
-            } else {
-                ptyType = "windows";
-            }
+            PtyChannelConfigurationMutator.setupSensitiveDefaultPtyConfiguration(this);
         } catch (Throwable t) {
             if (log.isDebugEnabled()) {
                 log.debug("setupSensibleDefaultPty({}) Failed ({}) to setup: {}",
@@ -147,60 +138,84 @@ public class PtyCapableChannelSession extends ChannelSession {
         this.usePty = usePty;
     }
 
+    @Override
     public String getPtyType() {
-        return ptyType;
+        return config.getPtyType();
     }
 
+    @Override
     public void setPtyType(String ptyType) {
-        this.ptyType = ptyType;
+        config.setPtyType(ptyType);
     }
 
+    @Override
     public int getPtyColumns() {
-        return ptyColumns;
+        return config.getPtyColumns();
     }
 
+    @Override
     public void setPtyColumns(int ptyColumns) {
-        this.ptyColumns = ptyColumns;
+        config.setPtyColumns(ptyColumns);
     }
 
+    @Override
     public int getPtyLines() {
-        return ptyLines;
+        return config.getPtyLines();
     }
 
+    @Override
     public void setPtyLines(int ptyLines) {
-        this.ptyLines = ptyLines;
+        config.setPtyLines(ptyLines);
     }
 
+    @Override
     public int getPtyWidth() {
-        return ptyWidth;
+        return config.getPtyWidth();
     }
 
+    @Override
     public void setPtyWidth(int ptyWidth) {
-        this.ptyWidth = ptyWidth;
+        config.setPtyWidth(ptyWidth);
     }
 
+    @Override
     public int getPtyHeight() {
-        return ptyHeight;
+        return config.getPtyHeight();
     }
 
+    @Override
     public void setPtyHeight(int ptyHeight) {
-        this.ptyHeight = ptyHeight;
+        config.setPtyHeight(ptyHeight);
     }
 
+    @Override
     public Map<PtyMode, Integer> getPtyModes() {
-        return ptyModes;
+        return config.getPtyModes();
     }
 
+    @Override
     public void setPtyModes(Map<PtyMode, Integer> ptyModes) {
-        this.ptyModes = (ptyModes == null) ? Collections.emptyMap() : ptyModes;
+        config.setPtyModes((ptyModes == null) ? Collections.emptyMap() : ptyModes);
     }
 
-    public void setEnv(String key, String value) {
-        env.put(key, value);
+    /**
+     * @param key The (never {@code null}) key (Note: may be empty...)
+     * @param value The value to set - if {@code null} then the pre-existing
+     * value for the key (if any) is <U>removed</U>.
+     * @return The replaced/removed previous value - {@code null} if no previous
+     * value set for the key.
+     */
+    public Object setEnv(String key, Object value) {
+        ValidateUtils.checkNotNull(key, "No key provided");
+        if (value == null) {
+            return env.remove(key);
+        } else {
+            return env.put(key, value);
+        }
     }
 
     public void sendWindowChange(int columns, int lines) throws IOException {
-        sendWindowChange(columns, lines, ptyHeight, ptyWidth);
+        sendWindowChange(columns, lines, getPtyHeight(), getPtyWidth());
     }
 
     public void sendWindowChange(int columns, int lines, int height, int width) throws IOException {
@@ -209,20 +224,20 @@ public class PtyCapableChannelSession extends ChannelSession {
                       this, columns, lines, height, width);
         }
 
-        ptyColumns = columns;
-        ptyLines = lines;
-        ptyHeight = height;
-        ptyWidth = width;
+        setPtyColumns(columns);
+        setPtyLines(lines);
+        setPtyHeight(height);
+        setPtyWidth(width);
 
         Session session = getSession();
         Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_CHANNEL_REQUEST, Long.SIZE);
         buffer.putInt(getRecipient());
         buffer.putString("window-change");
         buffer.putBoolean(false);   // want-reply
-        buffer.putInt(ptyColumns);
-        buffer.putInt(ptyLines);
-        buffer.putInt(ptyHeight);
-        buffer.putInt(ptyWidth);
+        buffer.putInt(getPtyColumns());
+        buffer.putInt(getPtyLines());
+        buffer.putInt(getPtyHeight());
+        buffer.putInt(getPtyWidth());
         writePacket(buffer);
     }
 
@@ -234,7 +249,8 @@ public class PtyCapableChannelSession extends ChannelSession {
                 log.debug("doOpenPty({}) Send agent forwarding request", this);
             }
 
-            String channelType = session.getStringProperty(SshAgentFactory.PROXY_AUTH_CHANNEL_TYPE, SshAgentFactory.DEFAULT_PROXY_AUTH_CHANNEL_TYPE);
+            String channelType = session.getStringProperty(
+                SshAgentFactory.PROXY_AUTH_CHANNEL_TYPE, SshAgentFactory.DEFAULT_PROXY_AUTH_CHANNEL_TYPE);
             Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_CHANNEL_REQUEST, Long.SIZE);
             buffer.putInt(getRecipient());
             buffer.putString(channelType);
@@ -244,25 +260,28 @@ public class PtyCapableChannelSession extends ChannelSession {
 
         if (usePty) {
             if (debugEnabled) {
-                log.debug("doOpenPty({}) Send SSH_MSG_CHANNEL_REQUEST pty-req: type={}, cols={}, lines={}, height={}, width={}, modes={}",
-                          this, ptyType, ptyColumns, ptyLines, ptyHeight, ptyWidth, ptyModes);
+                log.debug("doOpenPty({}) Send SSH_MSG_CHANNEL_REQUEST pty-req: {}", this, config);
             }
 
             Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_CHANNEL_REQUEST, Byte.MAX_VALUE);
             buffer.putInt(getRecipient());
             buffer.putString("pty-req");
             buffer.putBoolean(false);   // want-reply
-            buffer.putString(ptyType);
-            buffer.putInt(ptyColumns);
-            buffer.putInt(ptyLines);
-            buffer.putInt(ptyHeight);
-            buffer.putInt(ptyWidth);
-
-            Buffer modes = new ByteArrayBuffer(GenericUtils.size(ptyModes) * (1 + Integer.BYTES) + Long.SIZE, false);
-            ptyModes.forEach((mode, value) -> {
-                modes.putByte((byte) mode.toInt());
-                modes.putInt(value.longValue());
-            });
+            buffer.putString(getPtyType());
+            buffer.putInt(getPtyColumns());
+            buffer.putInt(getPtyLines());
+            buffer.putInt(getPtyHeight());
+            buffer.putInt(getPtyWidth());
+
+            Map<PtyMode, Integer> ptyModes = getPtyModes();
+            int numModes = GenericUtils.size(ptyModes);
+            Buffer modes = new ByteArrayBuffer(numModes * (1 + Integer.BYTES) + Long.SIZE, false);
+            if (numModes > 0) {
+                ptyModes.forEach((mode, value) -> {
+                    modes.putByte((byte) mode.toInt());
+                    modes.putInt(value.longValue());
+                });
+            }
             modes.putByte(PtyMode.TTY_OP_END);
             buffer.putBytes(modes.getCompactData());
             writePacket(buffer);
@@ -274,15 +293,17 @@ public class PtyCapableChannelSession extends ChannelSession {
             }
 
             // Cannot use forEach because of the IOException being thrown by writePacket
-            for (Map.Entry<String, String> entry : env.entrySet()) {
+            for (Map.Entry<String, ?> entry : env.entrySet()) {
                 String key = entry.getKey();
-                String value = entry.getValue();
-                Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_CHANNEL_REQUEST, key.length() + value.length() + Integer.SIZE);
+                Object value = entry.getValue();
+                String str = Objects.toString(value);
+                Buffer buffer = session.createBuffer(
+                    SshConstants.SSH_MSG_CHANNEL_REQUEST, key.length() + GenericUtils.length(str) + Integer.SIZE);
                 buffer.putInt(getRecipient());
                 buffer.putString("env");
                 buffer.putBoolean(false);   // want-reply
                 buffer.putString(key);
-                buffer.putString(value);
+                buffer.putString(str);
                 writePacket(buffer);
             }
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index e197077..20610c4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -48,6 +48,7 @@ import org.apache.sshd.common.RuntimeSshException;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.channel.PtyChannelConfigurationHolder;
 import org.apache.sshd.common.cipher.BuiltinCiphers;
 import org.apache.sshd.common.cipher.CipherNone;
 import org.apache.sshd.common.config.keys.KeyUtils;
@@ -283,12 +284,14 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
-    public ChannelExec createExecChannel(String command) throws IOException {
-        ChannelExec channel = new ChannelExec(command);
+    public ChannelExec createExecChannel(
+            String command, PtyChannelConfigurationHolder ptyConfig, Map<String, ?> env)
+                throws IOException {
+        ChannelExec channel = new ChannelExec(command, ptyConfig, env);
         ConnectionService service = getConnectionService();
         int id = service.registerChannel(channel);
         if (log.isDebugEnabled()) {
-            log.debug("createExecChannel({})[{}] created id={}", this, command, id);
+            log.debug("createExecChannel({})[{}] created id={} - PTY={}", this, command, id, ptyConfig);
         }
         return channel;
     }
@@ -388,16 +391,16 @@ public abstract class AbstractClientSession extends AbstractSession implements C
     }
 
     @Override
-    public ChannelShell createShellChannel() throws IOException {
+    public ChannelShell createShellChannel(PtyChannelConfigurationHolder ptyConfig, Map<String, ?> env) throws IOException {
         if ((inCipher instanceof CipherNone) || (outCipher instanceof CipherNone)) {
             throw new IllegalStateException("Interactive channels are not supported with none cipher");
         }
 
-        ChannelShell channel = new ChannelShell();
+        ChannelShell channel = new ChannelShell(ptyConfig, env);
         ConnectionService service = getConnectionService();
         int id = service.registerChannel(channel);
         if (log.isDebugEnabled()) {
-            log.debug("createShellChannel({}) created id={}", this, id);
+            log.debug("createShellChannel({}) created id={} - PTY={}", this, id, ptyConfig);
         }
         return channel;
     }
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index b134ba0..950227c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -48,6 +48,7 @@ import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker;
 import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
 import org.apache.sshd.common.AttributeRepository;
+import org.apache.sshd.common.channel.PtyChannelConfigurationHolder;
 import org.apache.sshd.common.forward.PortForwardingManager;
 import org.apache.sshd.common.future.KeyExchangeFuture;
 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
@@ -148,21 +149,54 @@ public interface ClientSession
     ClientChannel createChannel(String type, String subType) throws IOException;
 
     /**
-     * Create a channel to start a shell.
+     * Create a channel to start a shell using default PTY settings and environment.
      *
      * @return The created {@link ChannelShell}
      * @throws IOException If failed to create the requested channel
      */
-    ChannelShell createShellChannel() throws IOException;
+    default ChannelShell createShellChannel() throws IOException {
+        return createShellChannel(null, Collections.emptyMap());
+    }
+
+    /**
+     * Create a channel to start a shell using specific PTY settings and/or environment.
+     *
+     * @param ptyConfig The PTY configuration to use - if {@code null} then
+     * internal defaults are used
+     * @param env Extra environment configuration to be transmitted to the server - ignored
+     * if {@code null}/empty.
+     * @return The created {@link ChannelShell}
+     * @throws IOException If failed to create the requested channel
+     */
+    ChannelShell createShellChannel(
+        PtyChannelConfigurationHolder ptyConfig, Map<String, ?> env)
+            throws IOException;
+
+    /**
+     * Create a channel to execute a command using default PTY settings and environment.
+     *
+     * @param command The command to execute
+     * @return The created {@link ChannelExec}
+     * @throws IOException If failed to create the requested channel
+     */
+    default ChannelExec createExecChannel(String command) throws IOException {
+        return createExecChannel(command, null, Collections.emptyMap());
+    }
 
     /**
-     * Create a channel to execute a command.
+     * Create a channel to execute a command using specific PTY settings and/or environment.
      *
      * @param command The command to execute
+     * @param ptyConfig The PTY configuration to use - if {@code null} then
+     * internal defaults are used
+     * @param env Extra environment configuration to be transmitted to the server - ignored
+     * if {@code null}/empty.
      * @return The created {@link ChannelExec}
      * @throws IOException If failed to create the requested channel
      */
-    ChannelExec createExecChannel(String command) throws IOException;
+    ChannelExec createExecChannel(
+        String command, PtyChannelConfigurationHolder ptyConfig, Map<String, ?> env)
+            throws IOException;
 
     /**
      * Execute a command that requires no input and returns its output
@@ -236,7 +270,9 @@ public interface ClientSession
      * @throws IOException If failed to execute the command or got a non-zero exit status
      * @see ClientChannel#validateCommandExitStatusCode(String, Integer) validateCommandExitStatusCode
      */
-    default void executeRemoteCommand(String command, OutputStream stdout, OutputStream stderr, Charset charset) throws IOException {
+    default void executeRemoteCommand(
+            String command, OutputStream stdout, OutputStream stderr, Charset charset)
+                throws IOException {
         if (charset == null) {
             charset = StandardCharsets.US_ASCII;
         }
@@ -276,7 +312,9 @@ public interface ClientSession
      * @return The created {@link ChannelDirectTcpip}
      * @throws IOException If failed to create the requested channel
      */
-    ChannelDirectTcpip createDirectTcpipChannel(SshdSocketAddress local, SshdSocketAddress remote) throws IOException;
+    ChannelDirectTcpip createDirectTcpipChannel(
+        SshdSocketAddress local, SshdSocketAddress remote)
+            throws IOException;
 
     /**
      * Starts a local port forwarding and returns a tracker that stops the
@@ -290,8 +328,11 @@ public interface ClientSession
      * @throws IOException If failed to set up the requested forwarding
      * @see #startLocalPortForwarding(SshdSocketAddress, SshdSocketAddress)
      */
-    default ExplicitPortForwardingTracker createLocalPortForwardingTracker(SshdSocketAddress local, SshdSocketAddress remote) throws IOException {
-        return new ExplicitPortForwardingTracker(this, true, local, remote, startLocalPortForwarding(local, remote));
+    default ExplicitPortForwardingTracker createLocalPortForwardingTracker(
+            SshdSocketAddress local, SshdSocketAddress remote)
+                throws IOException {
+        return new ExplicitPortForwardingTracker(
+            this, true, local, remote, startLocalPortForwarding(local, remote));
     }
 
     /**
@@ -306,8 +347,11 @@ public interface ClientSession
      * @throws IOException If failed to set up the requested forwarding
      * @see #startRemotePortForwarding(SshdSocketAddress, SshdSocketAddress)
      */
-    default ExplicitPortForwardingTracker createRemotePortForwardingTracker(SshdSocketAddress remote, SshdSocketAddress local) throws IOException {
-        return new ExplicitPortForwardingTracker(this, false, local, remote, startRemotePortForwarding(remote, local));
+    default ExplicitPortForwardingTracker createRemotePortForwardingTracker(
+            SshdSocketAddress remote, SshdSocketAddress local)
+                throws IOException {
+        return new ExplicitPortForwardingTracker(
+            this, false, local, remote, startRemotePortForwarding(remote, local));
     }
 
     /**
@@ -397,6 +441,7 @@ public interface ClientSession
     static Iterator<String> passwordIteratorOf(ClientSession session) {
         return (session == null)
             ? Collections.<String>emptyIterator()
-            : PasswordIdentityProvider.iteratorOf(session.getRegisteredIdentities(), session.getPasswordIdentityProvider());
+            : PasswordIdentityProvider.iteratorOf(
+                session.getRegisteredIdentities(), session.getPasswordIdentityProvider());
     }
 }
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
index 11ef70e..8ee4e9a 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
@@ -805,7 +805,7 @@ public class ClientTest extends BaseTestSupport {
         client.start();
 
         try (ClientSession session = createTestClientSession();
-             ChannelShell channel = new ChannelShell();
+             ChannelShell channel = new ChannelShell(null, Collections.emptyMap());
              ByteArrayOutputStream sent = new ByteArrayOutputStream();
              ByteArrayOutputStream out = new ByteArrayOutputStream();
              ByteArrayOutputStream err = new ByteArrayOutputStream()) {