/* * 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.server.channel; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.channel.ChannelOutputStream; import org.apache.sshd.common.channel.ChannelPipedInputStream; import org.apache.sshd.common.channel.ChannelPipedOutputStream; import org.apache.sshd.common.future.CloseFuture; import org.apache.sshd.common.future.SshFuture; import org.apache.sshd.common.future.SshFutureListener; import org.apache.sshd.common.util.Buffer; import org.apache.sshd.common.util.IoUtils; import org.apache.sshd.common.util.LfToCrLfFilterOutputStream; import org.apache.sshd.common.util.LoggingFilterOutputStream; import org.apache.sshd.server.CommandFactory; import org.apache.sshd.server.ServerChannel; import org.apache.sshd.server.ShellFactory; import org.apache.sshd.server.Signals; import org.apache.sshd.server.ShellFactory.Environment; import org.apache.sshd.server.ShellFactory.SignalListener; import org.apache.sshd.server.session.ServerSession; /** * TODO Add javadoc * * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ public class ChannelSession extends AbstractServerChannel { public static class Factory implements NamedFactory<ServerChannel> { public String getName() { return "session"; } public ServerChannel create() { return new ChannelSession(); } } protected static class StandardEnvironment implements Environment { private final Map<Integer, List<SignalListener>> qualifiedListeners; private final List<SignalListener> listeners; private final Map<String,String> env; public StandardEnvironment() { qualifiedListeners = new ConcurrentHashMap<Integer, List<SignalListener>>(3); listeners = createSignalListenerList(); env = new ConcurrentHashMap<String, String>(); } protected CopyOnWriteArrayList<SignalListener> createSignalListenerList() { return new CopyOnWriteArrayList<SignalListener>(); } public void addSignalListener(int signal, SignalListener listener) { if (listener == null) { throw new IllegalArgumentException("listener may not be null"); } getSignalListenersList(signal, true).add(listener); } public void addSignalListener(SignalListener listener) { if (listener == null) { throw new IllegalArgumentException("listener may not be null"); } getSignalListenersList().add(listener); } public Map<String, String> getEnv() { return env; } public void removeSignalListener(int signal, SignalListener listener) { if (listener == null) { throw new IllegalArgumentException("listener may not be null"); } final List<SignalListener> ls = getSignalListenersList(signal, false); if (ls != null) { ls.remove(listener); } } public void removeSignalListener(SignalListener listener) { if (listener == null) { throw new IllegalArgumentException("listener may not be null"); } getSignalListenersList().remove(listener); } public void signal(int signal) { final List<SignalListener> qls = getSignalListenersList(signal, false); final List<SignalListener> ls = getSignalListenersList(); if (qls != null) { for(SignalListener l : qls) { l.signal(signal); } } for(SignalListener l : ls) { l.signal(signal); } } /** * adds a variable to the environment. This method is called <code>set</code> * according to the name of the appropriate posix command <code>set</code> * @param key environment variable name * @param value environment variable value */ public void set(String key, String value) { // TODO: listening for property changes would be nice too. getEnv().put(key, value); } protected List<SignalListener> getSignalListenersList(int signal, boolean create) { List<SignalListener> ls = qualifiedListeners.get(signal); if (ls == null && create) { synchronized (qualifiedListeners) { ls = createSignalListenerList(); qualifiedListeners.put(signal, ls); } } // may be null in case create=false return ls; } protected List<SignalListener> getSignalListenersList() { return listeners; } } protected static enum PtyMode { // Chars VINTR(1), VQUIT(2), VERASE(3), VKILL(4), VEOF(5), VEOL(6), VEOL2(7), VSTART(8), VSTOP(9), VSUSP(10), VDSUSP(11), VREPRINT(12), VWERASE(13), VLNEXT(14), VFLUSH(15), VSWTCH(16), VSTATUS(17), VDISCARD(18), // I flags IGNPAR(30), PARMRK(31), INPCK(32), ISTRIP(33), INCLR(34), IGNCR(35), ICRNL(36), IUCLC(37), IXON(38), IXANY(39), IXOFF(40), IMAXBEL(41), // L flags ISIG(50), ICANON(51), XCASE(52), ECHO(53), ECHOE(54), ECHOK(55), ECHONL(56), NOFLSH(57), TOSTOP(58), IEXTEN(59), ECHOCTL(60), ECHOKE(61), PENDIN(62), // O flags OPOST(70), OLCUC(71), ONLCR(72), OCRNL(73), ONOCR(74), ONLRET(75), // C flags CS7(90), CS8(91), PARENB(92), PARODD(93), // Speeed TTY_OP_ISPEED(128), TTY_OP_OSPEED(129); private int v; private PtyMode(int v) { this.v = v; } public int toInt() { return v; } static Map<Integer,PtyMode> commands; static { commands = new HashMap<Integer, PtyMode>(); for (PtyMode c : PtyMode.values()) { commands.put(c.toInt(), c); } } public static PtyMode fromInt(int b) { return commands.get(0x00FF & (b + 256)); } } protected static class PtyModeValue { public final PtyMode mode; public final int value; public PtyModeValue(PtyMode mode, int value) { this.mode = mode; this.value = value; } public String toString() { return mode + "(" + mode.toInt() + ") =" + value; } } protected String type; protected PtyModeValue[] ptyModes; protected InputStream in; protected OutputStream out; protected OutputStream err; protected ShellFactory.Shell shell; protected OutputStream shellIn; protected InputStream shellOut; protected InputStream shellErr; protected StandardEnvironment env = new StandardEnvironment(); public ChannelSession() { } public CloseFuture close(boolean immediately) { return super.close(immediately).addListener(new SshFutureListener() { public void operationComplete(SshFuture sshFuture) { if (shell != null) { shell.destroy(); shell = null; } remoteWindow.notifyClosed(); IoUtils.closeQuietly(in, out, err, shellIn, shellOut, shellErr); } }); } @Override public void handleEof() throws IOException { super.handleEof(); shellIn.close(); } public void handleRequest(Buffer buffer) throws IOException { log.info("Received SSH_MSG_CHANNEL_REQUEST on channel {}", id); String type = buffer.getString(); log.info("Received channel request: {}", type); if (!handleRequest(type, buffer)) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_FAILURE); buffer.putInt(recipient); session.writePacket(buffer); } } protected void doWriteData(byte[] data, int off, int len) throws IOException { shellIn.write(data, off, len); shellIn.flush(); } protected void doWriteExtendedData(byte[] data, int off, int len) throws IOException { throw new UnsupportedOperationException("Server channel does not support extended data"); } protected boolean handleRequest(String type, Buffer buffer) throws IOException { if ("env".equals(type)) { return handleEnv(buffer); } if ("pty-req".equals(type)) { return handlePtyReq(buffer); } if ("window-change".equals(type)) { return handleWindowChange(buffer); } if ("signal".equals(type)) { return handleSignal(buffer); } if ("shell".equals(type)) { if (checkType(type)) { return handleShell(buffer); } else { return false; } } if ("exec".equals(type)) { if (checkType(type)) { return handleExec(buffer); } else { return false; } } if ("subsystem".equals(type)) { if (checkType(type)) { return handleSubsystem(buffer); } else { return false; } } if ("auth-agent-req@openssh.com".equals(type)) { return handleAgentForwarding(buffer); } if ("x11-req".equals(type)) { return handleX11Forwarding(buffer); } return false; } /** * Only one of "shell", "exec" or "subsystem" command * is permitted for a given channel. * * @param type * @return */ private boolean checkType(String type) { if (this.type == null) { this.type = type; return true; } else { return false; } } protected boolean handleEnv(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); String name = buffer.getString(); String value = buffer.getString(); addEnvVariable(name, value); log.debug("env for channel {}: {} = {}", new Object[] { id, name, value }); if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } return true; } protected boolean handlePtyReq(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); String term = buffer.getString(); int tColumns = buffer.getInt(); int tRows = buffer.getInt(); int tWidth = buffer.getInt(); int tHeight = buffer.getInt(); byte[] modes = buffer.getBytes(); List<PtyModeValue> modeList = new ArrayList<PtyModeValue>(); for (int i = 0; i < modes.length && modes[i] != 0;) { PtyMode mode = PtyMode.fromInt(modes[i++]); int val = ((modes[i++] << 24) & 0xff000000) | ((modes[i++] << 16) & 0x00ff0000) | ((modes[i++] << 8) & 0x0000ff00) | ((modes[i++] ) & 0x000000ff); PtyModeValue m = new PtyModeValue(mode, val); modeList.add(m); } ptyModes = modeList.toArray(new PtyModeValue[0]); if (log.isDebugEnabled()) { StringBuffer strModes = new StringBuffer(); for (PtyModeValue m : ptyModes) { if (strModes.length() > 0) { strModes.append(", "); } strModes.append(m); } log.debug("pty for channel {}: term={}, size=({} - {}), pixels=({}, {}), modes=[{}]", new Object[] { id, term, tColumns, tRows, tWidth, tHeight, strModes.toString() }); } addEnvVariable("TERM", term); addEnvVariable("COLUMNS", Integer.toString(tColumns)); addEnvVariable("LINES", Integer.toString(tRows)); // TODO: handle pty request correctly if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } return true; } protected boolean handleWindowChange(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); int tColumns = buffer.getInt(); int tRows = buffer.getInt(); int tWidth = buffer.getInt(); int tHeight = buffer.getInt(); log.debug("window-change for channel {}: ({} - {}), ({}, {})", new Object[] { id, tColumns, tRows, tWidth, tHeight }); // TODO: handle window-change request correctly final StandardEnvironment e = getEnvironment(); e.set("COLUMNS", Integer.toString(tColumns)); e.set("LINES", Integer.toString(tRows)); e.signal(Signals.SIGWINCH); if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } return true; } protected boolean handleSignal(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); String name = buffer.getString(); log.debug("Signal received on channel {}: {}", id, name); // TODO: handle signal request correctly if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } return true; } protected boolean handleShell(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); if (((ServerSession) session).getServerFactoryManager().getShellFactory() == null) { return false; } addEnvVariable("USER", ((ServerSession) session).getUsername()); shell = ((ServerSession) session).getServerFactoryManager().getShellFactory().createShell(); // If the shell wants to be aware of the session, let's do that if (shell instanceof ShellFactory.SessionAware) { ((ShellFactory.SessionAware) shell).setSession((ServerSession) session); } out = new ChannelOutputStream(this, remoteWindow, log, SshConstants.Message.SSH_MSG_CHANNEL_DATA); err = new ChannelOutputStream(this, remoteWindow, log, SshConstants.Message.SSH_MSG_CHANNEL_EXTENDED_DATA); // Wrap in logging filters out = new LoggingFilterOutputStream(out, "OUT:", log); err = new LoggingFilterOutputStream(err, "ERR:", log); if (getPtyModeValue(PtyMode.ONLCR) != 0) { out = new LfToCrLfFilterOutputStream(out); err = new LfToCrLfFilterOutputStream(err); } in = new ChannelPipedInputStream(localWindow); shellIn = new ChannelPipedOutputStream((ChannelPipedInputStream) in); shell.setInputStream(in); shell.setOutputStream(out); shell.setErrorStream(err); shell.setExitCallback(new ShellFactory.ExitCallback() { public void onExit(int exitValue) { try { closeShell(exitValue); } catch (IOException e) { log.info("Error closing shell", e); } } }); if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } shell.start(getEnvironment()); shellIn = new LoggingFilterOutputStream(shellIn, "IN: ", log); return true; } protected int getPtyModeValue(PtyMode mode) { if (ptyModes != null) { for (PtyModeValue m : ptyModes) { if (m.mode == mode) { return m.value; } } } return 0; } protected boolean handleExec(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); String commandLine = buffer.getString(); if (((ServerSession) session).getServerFactoryManager().getCommandFactory() == null) { return false; } CommandFactory.Command command = ((ServerSession) session).getServerFactoryManager().getCommandFactory().createCommand(commandLine); // If the command wants to be aware of the session, let's do that if (command instanceof CommandFactory.SessionAware) { ((CommandFactory.SessionAware) command).setSession((ServerSession) session); } // Set streams and exit callback out = new ChannelOutputStream(this, remoteWindow, log, SshConstants.Message.SSH_MSG_CHANNEL_DATA); err = new ChannelOutputStream(this, remoteWindow, log, SshConstants.Message.SSH_MSG_CHANNEL_EXTENDED_DATA); // Wrap in logging filters out = new LoggingFilterOutputStream(out, "OUT:", log); err = new LoggingFilterOutputStream(err, "ERR:", log); in = new ChannelPipedInputStream(localWindow); shellIn = new ChannelPipedOutputStream((ChannelPipedInputStream) in); command.setInputStream(in); command.setOutputStream(out); command.setErrorStream(err); command.setExitCallback(new CommandFactory.ExitCallback() { public void onExit(int exitValue) { try { closeShell(exitValue); } catch (IOException e) { log.info("Error closing shell", e); } } }); if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } // Launch command command.start(); return true; } protected boolean handleSubsystem(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); // TODO: start subsystem if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } return true; } protected boolean handleAgentForwarding(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); // TODO: start agent forwarding if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } return true; } protected boolean handleX11Forwarding(Buffer buffer) throws IOException { boolean wantReply = buffer.getBoolean(); // TODO: start x11 forwarding if (wantReply) { buffer = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_SUCCESS); buffer.putInt(recipient); session.writePacket(buffer); } return true; } protected void addEnvVariable(String name, String value) { getEnvironment().set(name, value); } protected StandardEnvironment getEnvironment() { return env; } protected void closeShell(int exitValue) throws IOException { sendEof(); sendExitStatus(exitValue); // TODO: We should wait for all streams to be consumed before closing the channel close(false); } }