/* * 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.client.channel; import java.io.IOException; import java.util.Collections; import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.Map; import org.apache.sshd.agent.SshAgentFactory; import org.apache.sshd.common.SshConstants; 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.OsUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; /** * <P>Serves as the base channel session for executing remote commands - including * a full shell. <B>Note:</B> all the configuration changes via the various * {@code setXXX} methods must be made <U>before</U> the channel is actually * open. If they are invoked afterwards then they have no effect (silently * ignored).</P> * <P>A typical code snippet would be:</P> * <PRE> * try (client = SshClient.setUpDefaultClient()) { * client.start(); * * try (ClientSession s = client.connect(getCurrentTestName(), "localhost", port).verify(7L, TimeUnit.SECONDS).getSession()) { * s.addPasswordIdentity(getCurrentTestName()); * s.auth().verify(5L, TimeUnit.SECONDS); * * try (ChannelExec shell = s.createExecChannel("my super duper command")) { * shell.setEnv("var1", "val1"); * shell.setEnv("var2", "val2"); * ...etc... * * shell.setPtyType(...); * shell.setPtyLines(...); * ...etc... * * shell.open().verify(5L, TimeUnit.SECONDS); * shell.waitFor(ClientChannel.CLOSED, TimeUnit.SECONDS.toMillis(17L)); // can use zero for infinite wait * * Integer status = shell.getExitStatus(); * if (status.intValue() != 0) { * ...error... * } * } * } finally { * client.stop(); * } * } * </PRE> * * @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 = GenericUtils.<PtyMode, Integer>mapBuilder() .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(); 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) { this.usePty = usePty; ptyType = System.getenv("TERM"); if (GenericUtils.isEmpty(ptyType)) { ptyType = "dummy"; } ptyModes.putAll(DEFAULT_PTY_MODES); } public void setupSensibleDefaultPty() { try { if (OsUtils.isUNIX()) { ptyModes = SttySupport.getUnixPtyModes(); ptyColumns = SttySupport.getTerminalWidth(); ptyLines = SttySupport.getTerminalHeight(); } else { ptyType = "windows"; } } catch (Throwable t) { // Ignore exceptions } } public boolean isAgentForwarding() { return agentForwarding; } public void setAgentForwarding(boolean agentForwarding) { this.agentForwarding = agentForwarding; } public boolean isUsePty() { return usePty; } public void setUsePty(boolean usePty) { this.usePty = usePty; } public String getPtyType() { return ptyType; } public void setPtyType(String ptyType) { this.ptyType = ptyType; } public int getPtyColumns() { return ptyColumns; } public void setPtyColumns(int ptyColumns) { this.ptyColumns = ptyColumns; } public int getPtyLines() { return ptyLines; } public void setPtyLines(int ptyLines) { this.ptyLines = ptyLines; } public int getPtyWidth() { return ptyWidth; } public void setPtyWidth(int ptyWidth) { this.ptyWidth = ptyWidth; } public int getPtyHeight() { return ptyHeight; } public void setPtyHeight(int ptyHeight) { this.ptyHeight = ptyHeight; } public Map<PtyMode, Integer> getPtyModes() { return ptyModes; } public void setPtyModes(Map<PtyMode, Integer> ptyModes) { this.ptyModes = (ptyModes == null) ? Collections.emptyMap() : ptyModes; } public void setEnv(String key, String value) { env.put(key, value); } public void sendWindowChange(int columns, int lines) throws IOException { sendWindowChange(columns, lines, ptyHeight, ptyWidth); } public void sendWindowChange(int columns, int lines, int height, int width) throws IOException { if (log.isDebugEnabled()) { log.debug("sendWindowChange({}) cols={}, lines={}, height={}, width={}", this, columns, lines, height, width); } ptyColumns = columns; ptyLines = lines; ptyHeight = height; ptyWidth = 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); writePacket(buffer); } protected void doOpenPty() throws IOException { Session session = getSession(); if (agentForwarding) { if (log.isDebugEnabled()) { log.debug("doOpenPty({}) Send agent forwarding request", this); } 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); buffer.putBoolean(false); // want-reply writePacket(buffer); } if (usePty) { if (log.isDebugEnabled()) { log.debug("doOpenPty({}) Send SSH_MSG_CHANNEL_REQUEST pty-req: type={}, cols={}, lines={}, height={}, width={}, modes={}", this, ptyType, ptyColumns, ptyLines, ptyHeight, ptyWidth, ptyModes); } 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()); }); modes.putByte(PtyMode.TTY_OP_END); buffer.putBytes(modes.getCompactData()); writePacket(buffer); } if (GenericUtils.size(env) > 0) { if (log.isDebugEnabled()) { log.debug("doOpenPty({}) Send SSH_MSG_CHANNEL_REQUEST env: {}", this, env); } // Cannot use forEach because of the IOException being thrown by writePacket for (Map.Entry<String, 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); buffer.putInt(getRecipient()); buffer.putString("env"); buffer.putBoolean(false); // want-reply buffer.putString(key); buffer.putString(value); writePacket(buffer); } } } }