/*
* 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.session;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.IoFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.future.WriteFuture;
import org.apache.mina.core.session.IoSession;
import org.apache.sshd.common.Channel;
import org.apache.sshd.common.Cipher;
import org.apache.sshd.common.Compression;
import org.apache.sshd.common.Digest;
import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.KeyExchange;
import org.apache.sshd.common.Mac;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.Random;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.future.CloseFuture;
import org.apache.sshd.common.future.DefaultCloseFuture;
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.BufferUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The AbstractSession handles all the basic SSH protocol such as key exchange, authentication,
* encoding and decoding. Both server side and client side sessions should inherit from this
* abstract class. Some basic packet processing methods are defined but the actual call to these
* methods should be done from the {@link #handleMessage(org.apache.sshd.common.util.Buffer)}
* method, which is dependant on the state and side of this session.
*
* TODO: if there is any very big packet, decoderBuffer and uncompressBuffer will get quite big
* and they won't be resized down at any time. Though the packet size is really limited
* by the channel max packet size
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public abstract class AbstractSession {
/**
* Name of the property where this session is stored in the attributes of the
* underlying MINA session. See {@link #getSession(IoSession, boolean)}
* and {@link #attachSession(IoSession, AbstractSession)}.
*/
public static final String SESSION = "com.google.code.sshd.session";
/** Our logger */
protected final Logger log = LoggerFactory.getLogger(getClass());
/** The factory manager used to retrieve factories of Ciphers, Macs and other objects */
protected final FactoryManager factoryManager;
/** The underlying MINA session */
protected final IoSession ioSession;
/** The pseudo random generator */
protected final Random random;
/** Lock object for this session state */
protected final Object lock = new Object();
/**
* A future that will be set 'closed' when the connection is closed.
*/
protected final CloseFuture closeFuture = new DefaultCloseFuture(lock);
/** The session is being closed */
protected volatile boolean closing;
/** Boolean indicating if this session has been authenticated or not */
protected boolean authed;
/** Map of channels keyed by the identifier */
protected final Map<Integer, Channel> channels = new ConcurrentHashMap<Integer, Channel>();
/** Next channel identifier */
protected int nextChannelId;
//
// Key exchange support
//
protected byte[] sessionId;
protected String serverVersion;
protected String clientVersion;
protected String[] serverProposal;
protected String[] clientProposal;
protected String[] negociated;
protected byte[] I_C; // the payload of the client's SSH_MSG_KEXINIT
protected byte[] I_S; // the payload of the factoryManager's SSH_MSG_KEXINIT
protected KeyExchange kex;
//
// SSH packets encoding / decoding support
//
protected Cipher outCipher;
protected Cipher inCipher;
protected int outCipherSize = 8;
protected int inCipherSize = 8;
protected Mac outMac;
protected Mac inMac;
protected byte[] inMacResult;
protected Compression outCompression;
protected Compression inCompression;
protected long seqi;
protected long seqo;
protected Buffer decoderBuffer = new Buffer();
protected Buffer uncompressBuffer;
protected int decoderState;
protected int decoderLength;
protected final Object encodeLock = new Object();
protected final Object decodeLock = new Object();
protected final Map<AttributeKey<?>, Object> attributes = new ConcurrentHashMap<AttributeKey<?>, Object>();
/**
* Create a new session.
*
* @param factoryManager the factory manager
* @param ioSession the underlying MINA session
*/
public AbstractSession(FactoryManager factoryManager, IoSession ioSession) {
this.factoryManager = factoryManager;
this.ioSession = ioSession;
this.random = factoryManager.getRandomFactory().create();
}
/**
* Retrieve the session from the MINA session.
* If the session has not been attached, an IllegalStateException
* will be thrown
*
* @param ioSession the MINA session
* @return the session attached to the MINA session
*/
public static final AbstractSession getSession(IoSession ioSession) {
return getSession(ioSession, false);
}
/**
* Retrieve the session from the MINA session.
* If the session has not been attached and allowNull is <code>false</code>,
* an IllegalStateException will be thrown, else a <code>null</code> will
* be returned
*
* @param ioSession the MINA session
* @param allowNull if <code>true</code>, a <code>null</code> value may be
* returned if no session is attached
* @return the session attached to the MINA session or <code>null</code>
*/
public static final AbstractSession getSession(IoSession ioSession, boolean allowNull) {
AbstractSession session = (AbstractSession) ioSession.getAttribute(SESSION);
if (!allowNull && session == null) {
throw new IllegalStateException("No session available");
}
return session;
}
/**
* Attach a session to the MINA session
*
* @param ioSession the MINA session
* @param session the session to attach
*/
public static final void attachSession(IoSession ioSession, AbstractSession session) {
ioSession.setAttribute(SESSION, session);
}
/**
* Retrieve the factory manager
*
* @return the factory manager for this session
*/
public FactoryManager getFactoryManager() {
return factoryManager;
}
/**
* Main input point for the MINA framework.
*
* This method will be called each time new data is received on
* the socket and will append it to the input buffer before
* calling the {@link #decode()} method.
*
* @param buffer the new buffer received
* @throws Exception if an error occurs while decoding or handling the data
*/
public void messageReceived(IoBuffer buffer) throws Exception {
synchronized (decodeLock) {
decoderBuffer.putBuffer(buffer);
// One of those property will be set by the constructor and the other
// one should be set by the readIdentification method
if (clientVersion == null || serverVersion == null) {
if (readIdentification(decoderBuffer)) {
decoderBuffer.compact();
} else {
return;
}
}
decode();
}
}
/**
* Abstract method for processing incoming decoded packets.
* The given buffer will hold the decoded packet, starting from
* the command byte at the read position.
* Packets must be processed within this call or be copied because
* the given buffer is meant to be changed and updated when this
* method returns.
*
* @param buffer the buffer containing the packet
* @throws Exception if an exeption occurs while handling this packet.
*/
protected abstract void handleMessage(Buffer buffer) throws Exception;
/**
* Handle any exceptions that occured on this session.
* The session will be closed and a disconnect packet will be
* sent before if the given exception is an
* {@link org.apache.sshd.common.SshException}.
*
* @param t the exception to process
* @throws IOException
*/
public void exceptionCaught(Throwable t) {
log.warn("Exception caught", t);
try {
if (t instanceof SshException) {
int code = ((SshException) t).getDisconnectCode();
if (code > 0) {
disconnect(code, t.getMessage());
return;
}
}
} catch (Throwable t2) {
// Ignore
}
close(false);
}
/**
* Close this session.
* This method will close all channels, then close the underlying MINA session.
* The call will not block until the mina session is actually closed.
*/
public CloseFuture close(final boolean immediately) {
class IoSessionCloser implements IoFutureListener {
public void operationComplete(IoFuture future) {
synchronized (lock) {
log.debug("IoSession closed");
closeFuture.setClosed();
lock.notifyAll();
}
}
};
synchronized (lock) {
if (!closing) {
try {
closing = true;
log.info("Closing session");
Channel[] channelToClose = channels.values().toArray(new Channel[0]);
if (channelToClose.length > 0) {
final AtomicInteger latch = new AtomicInteger(channelToClose.length);
for (Channel channel : channelToClose) {
log.debug("Closing channel {}", channel.getId());
channel.close(immediately).addListener(new SshFutureListener() {
public void operationComplete(SshFuture sshFuture) {
if (latch.decrementAndGet() == 0) {
log.debug("Closing IoSession");
ioSession.close(true).addListener(new IoSessionCloser());
}
}
});
}
} else {
log.debug("Closing IoSession");
ioSession.close(immediately).addListener(new IoSessionCloser());
}
} catch (Throwable t) {
log.warn("Error closing session", t);
}
}
return closeFuture;
}
}
/**
* Encode and send the given buffer.
* The buffer has to have 5 bytes free at the beginning to allow the encoding to take place.
* Also, the write position of the buffer has to be set to the position of the last byte to write.
*
* @param buffer the buffer to encode and send
* @return a future that can be used to check when the packet has actually been sent
* @throws java.io.IOException if an error occured when encoding sending the packet
*/
public WriteFuture writePacket(Buffer buffer) throws IOException {
// Synchronize all write requests as needed by the encoding algorithm
// and also queue the write request in this synchronized block to ensure
// packets are sent in the correct order
synchronized (encodeLock) {
encode(buffer);
IoBuffer bb = IoBuffer.wrap(buffer.array(), buffer.rpos(), buffer.available());
return ioSession.write(bb);
}
}
/**
* Create a new buffer for the specified SSH packet and reserve the needed space
* (5 bytes) for the packet header.
*
* @param cmd the SSH command
* @return a new buffer ready for write
*/
public Buffer createBuffer(SshConstants.Message cmd) {
Buffer buffer = new Buffer();
buffer.rpos(5);
buffer.wpos(5);
buffer.putByte(cmd.toByte());
return buffer;
}
/**
* Encode a buffer into the SSH protocol.
* This method need to be called into a synchronized block around encodeLock
*
* @param buffer the buffer to encode
* @throws IOException if an exception occurs during the encoding process
*/
private void encode(Buffer buffer) throws IOException {
try {
// Check that the packet has some free space for the header
if (buffer.rpos() < 5) {
log.warn("Performance cost: when sending a packet, ensure that "
+ "5 bytes are available in front of the buffer");
Buffer nb = new Buffer();
nb.wpos(5);
nb.putBuffer(buffer);
buffer = nb;
}
// Grab the length of the packet (excluding the 5 header bytes)
int len = buffer.available();
int off = buffer.rpos() - 5;
// Debug log the packet
if (log.isTraceEnabled()) {
log.trace("Sending packet #{}: {}", seqo, buffer.printHex());
}
// Compress the packet if needed
if (outCompression != null && (authed || !outCompression.isDelayed())) {
outCompression.compress(buffer);
len = buffer.available();
}
// Compute padding length
int bsize = outCipherSize;
int oldLen = len;
len += 5;
int pad = (-len) & (bsize - 1);
if (pad < bsize) {
pad += bsize;
}
len = len + pad - 4;
// Write 5 header bytes
buffer.wpos(off);
buffer.putInt(len);
buffer.putByte((byte) pad);
// Fill padding
buffer.wpos(off + oldLen + 5 + pad);
random.fill(buffer.array(), buffer.wpos() - pad, pad);
// Compute mac
if (outMac != null) {
int macSize = outMac.getBlockSize();
int l = buffer.wpos();
buffer.wpos(l + macSize);
outMac.updateUInt(seqo);
outMac.update(buffer.array(), off, l);
outMac.doFinal(buffer.array(), l);
}
// Encrypt packet, excluding mac
if (outCipher != null) {
outCipher.update(buffer.array(), off, len + 4);
}
// Increment packet id
seqo = (seqo + 1) & 0xffffffffL;
// Make buffer ready to be read
buffer.rpos(off);
} catch (SshException e) {
throw e;
} catch (Exception e) {
throw new SshException(e);
}
}
/**
* Decode the incoming buffer and handle packets as needed.
*
* @throws Exception
*/
protected void decode() throws Exception {
// Decoding loop
for (;;) {
// Wait for beginning of packet
if (decoderState == 0) {
// The read position should always be 0 at this point because we have compacted this buffer
assert decoderBuffer.rpos() == 0;
// If we have received enough bytes, start processing those
if (decoderBuffer.available() > inCipherSize) {
// Decrypt the first bytes
if (inCipher != null) {
inCipher.update(decoderBuffer.array(), 0, inCipherSize);
}
// Read packet length
decoderLength = decoderBuffer.getInt();
// Check packet length validity
if (decoderLength < 5 || decoderLength > (256 * 1024)) {
log.info("Error decoding packet (invalid length) {}", decoderBuffer.printHex());
throw new SshException(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR,
"Invalid packet length: " + decoderLength);
}
// Ok, that's good, we can go to the next step
decoderState = 1;
} else {
// need more data
break;
}
// We have received the beinning of the packet
} else if (decoderState == 1) {
// The read position should always be 4 at this point
assert decoderBuffer.rpos() == 4;
int macSize = inMac != null ? inMac.getBlockSize() : 0;
// Check if the packet has been fully received
if (decoderBuffer.available() >= decoderLength + macSize) {
byte[] data = decoderBuffer.array();
// Decrypt the remaining of the packet
if (inCipher != null){
inCipher.update(data, inCipherSize, decoderLength + 4 - inCipherSize);
}
// Check the mac of the packet
if (inMac != null) {
// Update mac with packet id
inMac.updateUInt(seqi);
// Update mac with packet data
inMac.update(data, 0, decoderLength + 4);
// Compute mac result
inMac.doFinal(inMacResult, 0);
// Check the computed result with the received mac (just after the packet data)
if (!BufferUtils.equals(inMacResult, 0, data, decoderLength + 4, macSize)) {
throw new SshException(SshConstants.SSH2_DISCONNECT_MAC_ERROR,
"MAC Error");
}
}
// Increment incoming packet sequence number
seqi = (seqi + 1) & 0xffffffffL;
// Get padding
byte pad = decoderBuffer.getByte();
Buffer buf;
int wpos = decoderBuffer.wpos();
// Decompress if needed
if (inCompression != null && (authed || !inCompression.isDelayed())) {
if (uncompressBuffer == null) {
uncompressBuffer = new Buffer();
} else {
uncompressBuffer.clear();
}
decoderBuffer.wpos(decoderBuffer.rpos() + decoderLength - 1 - pad);
inCompression.uncompress(decoderBuffer, uncompressBuffer);
buf = uncompressBuffer;
} else {
decoderBuffer.wpos(decoderLength + 4 - pad);
buf = decoderBuffer;
}
if (log.isTraceEnabled()) {
log.trace("Received packet #{}: {}", seqi, buf.printHex());
}
// Process decoded packet
handleMessage(buf);
// Set ready to handle next packet
decoderBuffer.rpos(decoderLength + 4 + macSize);
decoderBuffer.wpos(wpos);
decoderBuffer.compact();
decoderState = 0;
} else {
// need more data
break;
}
}
}
}
/**
* Send our identification.
*
* @param ident our identification to send
*/
protected void sendIdentification(String ident) {
IoBuffer buffer = IoBuffer.allocate(32);
buffer.setAutoExpand(true);
buffer.put((ident + "\r\n").getBytes());
buffer.flip();
ioSession.write(buffer);
}
/**
* Read the other side identification.
* This method is specific to the client or server side, but both should call
* {@link #doReadIdentification(org.apache.sshd.common.util.Buffer)} and
* store the result in the needed property.
*
* @param buffer the buffer containing the remote identification
* @return <code>true</code> if the identification has been fully read or
* <code>false</code> if more data is needed
* @throws IOException if an error occurs such as a bad protocol version
*/
protected abstract boolean readIdentification(Buffer buffer) throws IOException;
/**
* Read the remote identification from this buffer.
* If more data is needed, the buffer will be reset to its original state
* and a <code>null</code> value will be returned. Else the identification
* string will be returned and the data read will be consumed from the buffer.
*
* @param buffer the buffer containing the identification string
* @return the remote identification or <code>null</code> if more data is needed
*/
protected String doReadIdentification(Buffer buffer) {
byte[] data = new byte[256];
for (;;) {
int rpos = buffer.rpos();
int pos = 0;
boolean needLf = false;
for (;;) {
if (buffer.available() == 0) {
// Need more data, so undo reading and return null
buffer.rpos(rpos);
return null;
}
byte b = buffer.getByte();
if (b == '\r') {
needLf = true;
continue;
}
if (b == '\n') {
break;
}
if (needLf) {
throw new IllegalStateException("Incorrect identification: bad line ending");
}
if (pos >= data.length) {
throw new IllegalStateException("Incorrect identification: line too long");
}
data[pos++] = b;
}
String str = new String(data, 0, pos);
if (str.startsWith("SSH-")) {
return str;
}
if (buffer.rpos() > 16 * 1024) {
throw new IllegalStateException("Incorrect identification: too many header lines");
}
}
}
/**
* Create our proposal for SSH negociation
*
* @param hostKeyTypes the list of supported host key types
* @return an array of 10 strings holding this proposal
*/
protected String[] createProposal(String hostKeyTypes) {
return new String[] {
NamedFactory.Utils.getNames(factoryManager.getKeyExchangeFactories()),
hostKeyTypes,
NamedFactory.Utils.getNames(factoryManager.getCipherFactories()),
NamedFactory.Utils.getNames(factoryManager.getCipherFactories()),
NamedFactory.Utils.getNames(factoryManager.getMacFactories()),
NamedFactory.Utils.getNames(factoryManager.getMacFactories()),
NamedFactory.Utils.getNames(factoryManager.getCompressionFactories()),
NamedFactory.Utils.getNames(factoryManager.getCompressionFactories()),
"",
""
};
}
/**
* Send the key exchange initialization packet.
* This packet contains random data along with our proposal.
*
* @param proposal our proposal for key exchange negociation
* @return the sent packet which must be kept for later use
* @throws IOException if an error occured sending the packet
*/
protected byte[] sendKexInit(String[] proposal) throws IOException {
Buffer buffer = createBuffer(SshConstants.Message.SSH_MSG_KEXINIT);
int p = buffer.wpos();
buffer.wpos(p + 16);
random.fill(buffer.array(), p, 16);
for (String s : proposal) {
buffer.putString(s);
}
buffer.putByte((byte) 0);
buffer.putInt(0);
byte[] data = buffer.getCompactData();
writePacket(buffer);
return data;
}
/**
* Receive the remote key exchange init message.
* The packet data is returned for later use.
*
* @param buffer the buffer containing the key exchange init packet
* @param proposal the remote proposal to fill
* @return the packet data
*/
protected byte[] receiveKexInit(Buffer buffer, String[] proposal) {
// Recreate the packet payload which will be needed at a later time
byte[] d = buffer.array();
byte[] data = new byte[buffer.available() + 1];
data[0] = SshConstants.Message.SSH_MSG_KEXINIT.toByte();
System.arraycopy(d, buffer.rpos(), data, 1, data.length - 1);
// Skip 16 bytes of random data
buffer.rpos(buffer.rpos() + 16);
// Read proposal
for (int i = 0; i < proposal.length; i++) {
proposal[i] = buffer.getString();
}
// Skip 5 bytes
buffer.getByte();
buffer.getInt();
// Return data
return data;
}
/**
* Send a message to put new keys into use.
*
* @throws IOException if an error occurs sending the message
*/
protected void sendNewKeys() throws IOException {
log.info("Send SSH_MSG_NEWKEYS");
Buffer buffer = createBuffer(SshConstants.Message.SSH_MSG_NEWKEYS);
writePacket(buffer);
}
/**
* Put new keys into use.
* This method will intialize the ciphers, digests, macs and compression
* according to the negociated server and client proposals.
*
* @param isServer boolean indicating if this session is on the server or the client side
* @throws Exception if an error occurs
*/
protected void receiveNewKeys(boolean isServer) throws Exception {
byte[] IVc2s;
byte[] IVs2c;
byte[] Ec2s;
byte[] Es2c;
byte[] MACc2s;
byte[] MACs2c;
byte[] K = kex.getK();
byte[] H = kex.getH();
Digest hash = kex.getHash();
Cipher s2ccipher;
Cipher c2scipher;
Mac s2cmac;
Mac c2smac;
Compression s2ccomp;
Compression c2scomp;
if (sessionId == null) {
sessionId = new byte[H.length];
System.arraycopy(H, 0, sessionId, 0, H.length);
}
Buffer buffer = new Buffer();
buffer.putMPInt(K);
buffer.putRawBytes(H);
buffer.putByte((byte) 0x41);
buffer.putRawBytes(sessionId);
int pos = buffer.available();
byte[] buf = buffer.array();
hash.update(buf, 0, pos);
IVc2s = hash.digest();
int j = pos - sessionId.length - 1;
buf[j]++;
hash.update(buf, 0, pos);
IVs2c = hash.digest();
buf[j]++;
hash.update(buf, 0, pos);
Ec2s = hash.digest();
buf[j]++;
hash.update(buf, 0, pos);
Es2c = hash.digest();
buf[j]++;
hash.update(buf, 0, pos);
MACc2s = hash.digest();
buf[j]++;
hash.update(buf, 0, pos);
MACs2c = hash.digest();
s2ccipher = NamedFactory.Utils.create(factoryManager.getCipherFactories(), negociated[SshConstants.PROPOSAL_ENC_ALGS_STOC]);
Es2c = resizeKey(Es2c, s2ccipher.getBlockSize(), hash, K, H);
s2ccipher.init(isServer ? Cipher.Mode.Encrypt : Cipher.Mode.Decrypt, Es2c, IVs2c);
s2cmac = NamedFactory.Utils.create(factoryManager.getMacFactories(), negociated[SshConstants.PROPOSAL_MAC_ALGS_STOC]);
s2cmac.init(MACs2c);
c2scipher = NamedFactory.Utils.create(factoryManager.getCipherFactories(), negociated[SshConstants.PROPOSAL_ENC_ALGS_CTOS]);
Ec2s = resizeKey(Ec2s, c2scipher.getBlockSize(), hash, K, H);
c2scipher.init(isServer ? Cipher.Mode.Decrypt : Cipher.Mode.Encrypt, Ec2s, IVc2s);
c2smac = NamedFactory.Utils.create(factoryManager.getMacFactories(), negociated[SshConstants.PROPOSAL_MAC_ALGS_CTOS]);
c2smac.init(MACc2s);
s2ccomp = NamedFactory.Utils.create(factoryManager.getCompressionFactories(), negociated[SshConstants.PROPOSAL_COMP_ALGS_STOC]);
c2scomp = NamedFactory.Utils.create(factoryManager.getCompressionFactories(), negociated[SshConstants.PROPOSAL_COMP_ALGS_CTOS]);
if (isServer) {
outCipher = s2ccipher;
outMac = s2cmac;
outCompression = s2ccomp;
inCipher = c2scipher;
inMac = c2smac;
inCompression = c2scomp;
} else {
outCipher = c2scipher;
outMac = c2smac;
outCompression = c2scomp;
inCipher = s2ccipher;
inMac = s2cmac;
inCompression = s2ccomp;
}
outCipherSize = outCipher.getIVSize();
if (outCompression != null) {
outCompression.init(Compression.Type.Deflater, -1);
}
inCipherSize = inCipher.getIVSize();
inMacResult = new byte[inMac.getBlockSize()];
if (inCompression != null) {
inCompression.init(Compression.Type.Inflater, -1);
}
}
/**
* Private method used while putting new keys into use that will resize the key used to
* initialize the cipher to the needed length.
*
* @param E the key to resize
* @param blockSize the cipher block size
* @param hash the hash algorithm
* @param K the key exchange K parameter
* @param H the key exchange H parameter
* @return the resize key
* @throws Exception if a problem occur while resizing the key
*/
private byte[] resizeKey(byte[] E, int blockSize, Digest hash, byte[] K, byte[] H) throws Exception {
while (blockSize > E.length) {
Buffer buffer = new Buffer();
buffer.putMPInt(K);
buffer.putRawBytes(H);
buffer.putRawBytes(E);
hash.update(buffer.array(), 0, buffer.available());
byte[] foo = hash.digest();
byte[] bar = new byte[E.length + foo.length];
System.arraycopy(E, 0, bar, 0, E.length);
System.arraycopy(foo, 0, bar, E.length, foo.length);
E = bar;
}
return E;
}
/**
* Send a disconnect packet with the given reason and message.
* Once the packet has been sent, the session will be closed
* asynchronously.
*
* @param reason the reason code for this disconnect
* @param msg the text message
* @throws IOException if an error occured sending the packet
*/
public void disconnect(int reason, String msg) throws IOException {
Buffer buffer = createBuffer(SshConstants.Message.SSH_MSG_DISCONNECT);
buffer.putInt(reason);
buffer.putString(msg);
buffer.putString("");
WriteFuture f = writePacket(buffer);
f.addListener(new IoFutureListener() {
public void operationComplete(IoFuture future) {
close(false);
}
});
}
/**
* Send an unimplemented packet. This packet should contain the
* sequence id of the usupported packet: this number is assumed to
* be the last packet received.
*
* @throws IOException if an error occured sending the packet
*/
protected void notImplemented() throws IOException {
Buffer buffer = createBuffer(SshConstants.Message.SSH_MSG_UNIMPLEMENTED);
buffer.putInt(seqi - 1);
writePacket(buffer);
}
/**
* Compute the negociated proposals by merging the client and
* server proposal. The negocatiated proposal will be stored in
* the {@link #negociated} property.
*/
protected void negociate() {
String[] guess = new String[SshConstants.PROPOSAL_MAX];
for (int i = 0; i < SshConstants.PROPOSAL_MAX; i++) {
String[] c = clientProposal[i].split(",");
String[] s = serverProposal[i].split(",");
for (String ci : c) {
for (String si : s) {
if (ci.equals(si)) {
guess[i] = ci;
break;
}
}
if (guess[i] != null) {
break;
}
}
if (guess[i] == null && i != SshConstants.PROPOSAL_LANG_CTOS && i != SshConstants.PROPOSAL_LANG_STOC) {
throw new IllegalStateException("Unable to negociate");
}
}
negociated = guess;
}
/**
* Process incoming data on a channel
*
* @param buffer the buffer containing the data
* @throws Exception if an error occurs
*/
protected void channelData(Buffer buffer) throws Exception {
Channel channel = getChannel(buffer);
channel.handleData(buffer);
}
/**
* Process incoming extended data on a channel
*
* @param buffer the buffer containing the data
* @throws Exception if an error occurs
*/
protected void channelExtendedData(Buffer buffer) throws Exception {
Channel channel = getChannel(buffer);
channel.handleExtendedData(buffer);
}
/**
* Process a window adjust packet on a channel
*
* @param buffer the buffer containing the window adjustement parameters
* @throws Exception if an error occurs
*/
protected void channelWindowAdjust(Buffer buffer) throws Exception {
try {
Channel channel = getChannel(buffer);
channel.handleWindowAdjust(buffer);
} catch (SshException e) {
log.info(e.getMessage());
}
}
/**
* Process end of file on a channel
*
* @param buffer the buffer containing the packet
* @throws Exception if an error occurs
*/
protected void channelEof(Buffer buffer) throws Exception {
Channel channel = getChannel(buffer);
channel.handleEof();
}
/**
* Close a channel due to a close packet received
*
* @param buffer the buffer containing the packet
* @throws Exception if an error occurs
*/
protected void channelClose(Buffer buffer) throws Exception {
Channel channel = getChannel(buffer);
channel.handleClose();
channelForget(channel);
}
/**
*
* @param channel
*/
public void channelForget(Channel channel) {
channels.remove(channel.getId());
}
/**
* Service a request on a channel
*
* @param buffer the buffer containing the request
* @throws Exception if an error occurs
*/
protected void channelRequest(Buffer buffer) throws IOException {
Channel channel = getChannel(buffer);
channel.handleRequest(buffer);
}
/**
* Process a failure on a channel
*
* @param buffer the buffer containing the packet
* @throws Exception if an error occurs
*/
protected void channelFailure(Buffer buffer) throws Exception {
Channel channel = getChannel(buffer);
channel.handleFailure();
}
/**
* Retrieve the channel designated by the given packet
*
* @param buffer the incoming packet
* @return the target channel
* @throws IOException if the channel does not exists
*/
protected Channel getChannel(Buffer buffer) throws IOException {
int recipient = buffer.getInt();
Channel channel = channels.get(recipient);
if (channel == null) {
buffer.rpos(buffer.rpos() - 5);
SshConstants.Message cmd = buffer.getCommand();
throw new SshException("Received " + cmd + " on unknown channel " + recipient);
}
return channel;
}
/**
* Retrieve a configuration property as an integer
*
* @param name the name of the property
* @param defaultValue the default value
* @return the value of the configuration property or the default value if not found
*/
public int getIntProperty(String name, int defaultValue) {
try {
String v = factoryManager.getProperties().get(name);
if (v != null) {
return Integer.parseInt(v);
}
} catch (Exception e) {
// Ignore
}
return defaultValue;
}
/**
* Returns the value of the user-defined attribute of this session.
*
* @param key the key of the attribute; must not be null.
* @return <tt>null</tt> if there is no attribute with the specified key
*/
@SuppressWarnings("unchecked")
public <T> T getAttribute(AttributeKey<T> key) {
return (T)attributes.get(key);
}
/**
* Sets a user-defined attribute.
*
* @param key the key of the attribute; must not be null.
* @param value the value of the attribute; must not be null.
* @return The old value of the attribute. <tt>null</tt> if it is new.
*/
@SuppressWarnings("unchecked")
public <T, E extends T> T setAttribute(AttributeKey<T> key, E value) {
return (T)attributes.put(key, value);
}
}