/*
* 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);
}
}