package org.eclipse.team.internal.ccvs.ssh; | |
/* | |
* (c) Copyright IBM Corp. 2000, 2001. | |
* All Rights Reserved. | |
*/ | |
/** | |
* An SSH 1.5 client.. | |
*/ | |
import java.io.BufferedInputStream; | |
import java.io.BufferedOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.InterruptedIOException; | |
import java.io.OutputStream; | |
import java.net.Socket; | |
import org.eclipse.team.internal.ccvs.core.connection.CVSAuthenticationException; | |
public class Client { | |
// client identification string | |
private static final String clientId = "SSH-1.5-Java 1.2.2\n"; | |
// server identification string | |
private static String serverId = null; | |
// maximum outgoing packet size | |
private static final int MAX_CLIENT_PACKET_SIZE = 1024; | |
// packet types | |
private static final int SSH_MSG_DISCONNECT = 1; | |
private static final int SSH_SMSG_PUBLIC_KEY = 2; | |
private static final int SSH_CMSG_SESSION_KEY = 3; | |
private static final int SSH_CMSG_USER = 4; | |
private static final int SSH_CMSG_AUTH_PASSWORD = 9; | |
private static final int SSH_CMSG_REQUEST_PTY = 10; | |
private static final int SSH_CMSG_EXEC_SHELL = 12; | |
private static final int SSH_CMSG_EXEC_CMD = 13; | |
private static final int SSH_SMSG_SUCCESS = 14; | |
private static final int SSH_SMSG_FAILURE = 15; | |
private static final int SSH_CMSG_STDIN_DATA = 16; | |
private static final int SSH_SMSG_STDOUT_DATA = 17; | |
private static final int SSH_SMSG_STDERR_DATA = 18; | |
private static final int SSH_SMSG_EXITSTATUS = 20; | |
private static final int SSH_CMSG_EXIT_CONFIRMATION = 33; | |
private static final int SSH_MSG_DEBUG = 36; | |
// cipher names | |
private static String[] cipherNames = { "None", "IDEA", "DES", "3DES", "TSS", "RC4", "Blowfish" }; | |
// cipher types | |
private static int SSH_CIPHER_NONE = 0; | |
private static int SSH_CIPHER_IDEA = 1; | |
private static int SSH_CIPHER_DES = 2; | |
private static int SSH_CIPHER_3DES = 3; | |
private static int SSH_CIPHER_TSS = 4; | |
private static int SSH_CIPHER_RC4 = 5; | |
private static int SSH_CIPHER_BLOWFISH = 6; | |
// preferred cipher types | |
private int[] preferredCipherTypes = { SSH_CIPHER_BLOWFISH }; | |
// authentication methods | |
private final int SSH_AUTH_RHOSTS = 1; | |
private final int SSH_AUTH_RSA = 2; | |
private final int SSH_AUTH_PASSWORD = 3; | |
private final int SSH_AUTH_RHOSTS_RSA = 4; | |
private String host; | |
private int port; | |
private String username; | |
private String password; | |
private String command; | |
private Socket socket; | |
private InputStream socketIn; | |
private OutputStream socketOut; | |
private InputStream is; | |
private OutputStream os; | |
private boolean connected = false; | |
private int timeout = -1; | |
private Cipher cipher = null; | |
private class StandardInputStream extends InputStream { | |
private ServerPacket packet = null; | |
private InputStream buffer = null; | |
private int buflen = 0; | |
private boolean atEnd = false; | |
private boolean closed = false; | |
private int exitStatus = 0; | |
public int available() throws IOException { | |
if (closed) { | |
throw new IOException(SSHPlugin.getResourceString("closed")); | |
} | |
int available = buffer == null ? 0 : buffer.available(); | |
if (available == 0 && socketIn.available() > 0) { | |
fill(); | |
available = buffer.available(); | |
} | |
return available; | |
} | |
public void close() throws IOException { | |
if (!closed) { | |
closed = true; | |
if (packet != null) { | |
packet.close(false); | |
buffer = null; | |
packet = null; | |
buflen = 0; | |
} | |
} | |
} | |
public int read() throws IOException { | |
if (closed) { | |
throw new IOException(SSHPlugin.getResourceString("closed")); | |
} | |
if (atEnd) { | |
return -1; | |
} | |
if (buffer == null || buffer.available() == 0) { | |
fill(); | |
} | |
return buffer.read(); | |
} | |
public int read(byte b[], int off, int len) throws IOException { | |
if (closed) { | |
throw new IOException(SSHPlugin.getResourceString("closed")); | |
} | |
if (atEnd) { | |
return -1; | |
} | |
if (buffer == null || buffer.available() == 0) { | |
fill(); | |
} | |
return buffer.read(b, off, len); | |
} | |
private void fill() throws IOException { | |
if (buffer != null) { | |
buffer.close(); | |
} | |
packet = new ServerPacket(socketIn, cipher); | |
int packetType = packet.getType(); | |
switch (packetType) { | |
case SSH_SMSG_STDOUT_DATA : | |
case SSH_SMSG_STDERR_DATA : | |
case SSH_MSG_DEBUG : | |
buffer = packet.getInputStream(); | |
buflen = Misc.readInt(buffer); | |
break; | |
case SSH_SMSG_EXITSTATUS : | |
buffer = null; | |
buflen = 0; | |
atEnd = true; | |
InputStream pis = packet.getInputStream(); | |
exitStatus = Misc.readInt(pis); | |
pis.close(); | |
send(SSH_CMSG_EXIT_CONFIRMATION, null); | |
break; | |
default : | |
throw new IOException(SSHPlugin.getResourceString("Client.packetType", new Object[] {new Integer(packetType)} )); | |
} | |
} | |
} | |
private class StandardOutputStream extends OutputStream { | |
private int MAX_BUFFER_SIZE = MAX_CLIENT_PACKET_SIZE; | |
private byte[] buffer = new byte[MAX_BUFFER_SIZE]; | |
private int bufpos = 0; | |
private boolean closed = false; | |
public void close() throws IOException { | |
if (!closed) { | |
try { | |
flush(); | |
} finally { | |
closed = true; | |
} | |
} | |
} | |
public void flush() throws IOException { | |
if (closed) { | |
throw new IOException(SSHPlugin.getResourceString("closed")); | |
} | |
if (bufpos > 0) { | |
send(SSH_CMSG_STDIN_DATA, buffer, 0, bufpos); | |
bufpos = 0; | |
} | |
} | |
public void write(int b) throws IOException { | |
if (closed) { | |
throw new IOException(SSHPlugin.getResourceString("closed")); | |
} | |
buffer[bufpos++] = (byte) b; | |
if (bufpos == MAX_BUFFER_SIZE) { | |
flush(); | |
} | |
} | |
public void write(byte b[], int off, int len) throws IOException { | |
if (closed) { | |
throw new IOException(SSHPlugin.getResourceString("closed")); | |
} | |
int bytesWritten = 0; | |
int totalBytesWritten = 0; | |
if (bufpos > 0) { | |
bytesWritten = Math.min(MAX_BUFFER_SIZE - bufpos, len); | |
System.arraycopy(b, off, buffer, bufpos, bytesWritten); | |
bufpos += bytesWritten; | |
totalBytesWritten += bytesWritten; | |
if (bufpos == MAX_BUFFER_SIZE) { | |
flush(); | |
} | |
} | |
while (len - totalBytesWritten >= MAX_BUFFER_SIZE) { | |
send(SSH_CMSG_STDIN_DATA, b, off + totalBytesWritten, MAX_BUFFER_SIZE); | |
totalBytesWritten += MAX_BUFFER_SIZE; | |
} | |
if (totalBytesWritten < len) { | |
bytesWritten = len - totalBytesWritten; | |
System.arraycopy(b, off + totalBytesWritten, buffer, 0, bytesWritten); | |
bufpos += bytesWritten; | |
} | |
} | |
} | |
public Client(InputStream socketIn, OutputStream socketOut, String username, String password) { | |
this.socketIn = socketIn; | |
this.socketOut = socketOut; | |
this.username = username; | |
this.password = password; | |
} | |
public Client(InputStream socketIn, OutputStream socketOut, String username, String password, String command) { | |
this(socketIn, socketOut, username, password); | |
this.command = command; | |
} | |
public Client(String host, int port, String username, String password) { | |
this.host = host; | |
this.port = port; | |
this.username = username; | |
this.password = password; | |
} | |
public Client(String host, int port, String username, String password, String command) { | |
this(host, port, username, password); | |
this.command = command; | |
} | |
public Client(String host, int port, String username, String password, String command, int timeout) { | |
this(host, port, username, password, command); | |
this.timeout = timeout; | |
} | |
/** | |
* Close all streams and sockets. | |
*/ | |
private void cleanup() throws IOException { | |
try { | |
if (is != null) | |
is.close(); | |
} finally { | |
try { | |
if (os != null) | |
os.close(); | |
} finally { | |
try { | |
if (socketIn != null) | |
socketIn.close(); | |
} finally { | |
try { | |
if (socketOut != null) | |
socketOut.close(); | |
} finally { | |
try { | |
if (socket != null) | |
socket.close(); | |
} finally { | |
socket = null; | |
} | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Connect to the remote server. If an exception is thrown, the caller | |
* can asssume that all streams and sockets are closed. | |
*/ | |
public void connect() throws IOException, CVSAuthenticationException { | |
// If we're already connected, just ignore the invokation | |
if (connected) | |
return; | |
// Otherwise, set up the connection | |
try { | |
// Create the socket (the socket should always be null here) | |
if (socket == null) { | |
try { | |
socket = new Socket(host, port); | |
} catch (InterruptedIOException e) { | |
// If we get this exception, chances are the host is not responding | |
throw new InterruptedIOException(SSHPlugin.getResourceString("Client.socket", new Object[] {host})); | |
} | |
if (timeout >= 0) { | |
socket.setSoTimeout(timeout * 1000); | |
} | |
socketIn = new BufferedInputStream(socket.getInputStream()); | |
socketOut = new BufferedOutputStream(socket.getOutputStream()); | |
} | |
// read the ssh server id. The socket creation may of failed if the | |
// server cannot accept our connection request. We don't expect the | |
// socket to be closed at this point. | |
StringBuffer buf = new StringBuffer(); | |
int c; | |
while ((c = socketIn.read()) != '\n') { | |
if (c == -1) | |
throw new IOException(SSHPlugin.getResourceString("Client.socketClosed")); | |
buf.append((char) c); | |
} | |
serverId = buf.toString(); | |
// send our id. | |
socketOut.write(clientId.getBytes()); | |
socketOut.flush(); | |
login(); | |
// start a shell and enter interactive session or start by | |
// executing the given command. | |
if( command == null ) { | |
startShell(); | |
} else { | |
executeCommand(); | |
} | |
is = new StandardInputStream(); | |
os = new StandardOutputStream(); | |
connected = true; | |
} catch (IOException e) { | |
// If an exception occurs while connected, make sure we disconnect before passing the exception on | |
try { | |
cleanup(); | |
} finally { | |
throw e; | |
} | |
} | |
} | |
/** | |
* Terminate the connection to the server. | |
*/ | |
public void disconnect() throws IOException { | |
if (connected) { | |
connected = false; | |
send(SSH_MSG_DISCONNECT, null); | |
cleanup(); | |
} | |
} | |
public InputStream getInputStream() throws IOException { | |
if (!connected) { | |
throw new IOException(SSHPlugin.getResourceString("Client.notConnected")); | |
} | |
return is; | |
} | |
public OutputStream getOutputStream() throws IOException { | |
if (!connected) { | |
throw new IOException(SSHPlugin.getResourceString("Client.notConnected")); | |
} | |
return os; | |
} | |
private void startShell() throws IOException { | |
ServerPacket packet = null; | |
int packetType; | |
send_SSH_CMSG_REQUEST_PTY(); | |
try { | |
packet = new ServerPacket(socketIn, cipher); | |
packetType = packet.getType(); | |
if (packetType != SSH_SMSG_SUCCESS) { | |
throw new IOException(SSHPlugin.getResourceString("Client.packetType", new Object[] {new Integer(packetType)} )); | |
} | |
} finally { | |
if (packet != null) { | |
packet.close(true /*perform crc check*/); | |
} | |
} | |
send(SSH_CMSG_EXEC_SHELL, null); | |
} | |
private void executeCommand() throws IOException { | |
send(SSH_CMSG_EXEC_CMD, command); | |
} | |
private void login() throws IOException, CVSAuthenticationException { | |
ServerPacket packet = null; | |
int packetType; | |
try { | |
packet = new ServerPacket(socketIn, cipher); | |
packetType = packet.getType(); | |
if (packetType != SSH_SMSG_PUBLIC_KEY) { | |
throw new IOException(SSHPlugin.getResourceString("Client.packetType", new Object[] {new Integer(packetType)} )); | |
} | |
receive_SSH_SMSG_PUBLIC_KEY(packet); | |
} finally { | |
if (packet != null) { | |
packet.close(true); | |
} | |
} | |
try { | |
packet = new ServerPacket(socketIn, cipher); | |
packetType = packet.getType(); | |
if (packetType != SSH_SMSG_SUCCESS) { | |
throw new IOException(SSHPlugin.getResourceString("Client.packetType", new Object[] {new Integer(packetType)} )); | |
} | |
} finally { | |
if (packet != null) { | |
packet.close(true); | |
} | |
} | |
send(SSH_CMSG_USER, username); | |
try { | |
packet = new ServerPacket(socketIn, cipher); | |
packetType = packet.getType(); | |
if (packetType != SSH_SMSG_FAILURE) { | |
throw new IOException(SSHPlugin.getResourceString("Client.packetType", new Object[] {new Integer(packetType)} )); | |
} | |
} finally { | |
if (packet != null) { | |
packet.close(true); | |
} | |
} | |
send(SSH_CMSG_AUTH_PASSWORD, password); | |
try { | |
packet = new ServerPacket(socketIn, cipher); | |
packetType = packet.getType(); | |
if (packetType == SSH_SMSG_FAILURE) { | |
throw new CVSAuthenticationException(SSHPlugin.getResourceString("Client.authenticationFailed")); | |
} | |
if (packetType != SSH_SMSG_SUCCESS) { | |
throw new IOException(SSHPlugin.getResourceString("Client.packetType", new Object[] {new Integer(packetType)} )); | |
} | |
} finally { | |
if (packet != null) { | |
packet.close(true); | |
} | |
} | |
} | |
private void receive_SSH_SMSG_PUBLIC_KEY(ServerPacket packet) throws IOException { | |
InputStream pis = packet.getInputStream(); | |
byte[] anti_spoofing_cookie = new byte[8]; | |
Misc.readFully(pis, anti_spoofing_cookie); | |
byte[] server_key_bits = new byte[4]; | |
Misc.readFully(pis, server_key_bits); | |
byte[] server_key_public_exponent = Misc.readMpInt(pis); | |
byte[] server_key_public_modulus = Misc.readMpInt(pis); | |
byte[] host_key_bits = new byte[4]; | |
Misc.readFully(pis, host_key_bits); | |
byte[] host_key_public_exponent = Misc.readMpInt(pis); | |
byte[] host_key_public_modulus = Misc.readMpInt(pis); | |
byte[] protocol_flags = new byte[4]; | |
Misc.readFully(pis, protocol_flags); | |
byte[] supported_ciphers_mask = new byte[4]; | |
Misc.readFully(pis, supported_ciphers_mask); | |
byte[] supported_authentications_mask = new byte[4]; | |
Misc.readFully(pis, supported_authentications_mask); | |
pis.close(); | |
send_SSH_CMSG_SESSION_KEY(anti_spoofing_cookie, server_key_public_modulus, host_key_public_modulus, supported_ciphers_mask, server_key_public_exponent, host_key_public_exponent); | |
} | |
private void send(int packetType, String s) throws IOException { | |
byte[] data = s == null ? new byte[0] : s.getBytes("UTF-8"); | |
send(packetType, data, 0, data.length); | |
} | |
private void send(int packetType, byte[] data, int off, int len) throws IOException { | |
data = data == null ? null : Misc.lengthEncode(data, off, len); | |
ClientPacket packet = new ClientPacket(packetType, data, cipher); | |
socketOut.write(packet.getBytes()); | |
socketOut.flush(); | |
} | |
private void send_SSH_CMSG_REQUEST_PTY() throws IOException { | |
byte packet_type = SSH_CMSG_REQUEST_PTY; | |
byte[] termType = Misc.lengthEncode("dumb".getBytes(), 0, 4); | |
byte[] row = {0, 0, 0, 0}; | |
byte[] col = {0, 0, 0, 0}; | |
byte[] XPixels = {0, 0, 0, 0}; | |
byte[] YPixels = {0, 0, 0, 0}; | |
byte[] terminalModes = {0}; | |
byte[] data = new byte[termType.length + row.length + col.length + XPixels.length + YPixels.length + terminalModes.length]; | |
int offset = 0; | |
System.arraycopy(termType, 0, data, offset, termType.length); | |
offset += termType.length; | |
System.arraycopy(row, 0, data, offset, row.length); | |
offset += row.length; | |
System.arraycopy(col, 0, data, offset, col.length); | |
offset += col.length; | |
System.arraycopy(XPixels, 0, data, offset, XPixels.length); | |
offset += XPixels.length; | |
System.arraycopy(YPixels, 0, data, offset, YPixels.length); | |
offset += YPixels.length; | |
System.arraycopy(terminalModes, 0, data, offset, terminalModes.length); | |
ClientPacket packet = new ClientPacket(packet_type, data, cipher); | |
socketOut.write(packet.getBytes()); | |
socketOut.flush(); | |
} | |
private void send_SSH_CMSG_SESSION_KEY(byte[] anti_spoofing_cookie, byte[] server_key_public_modulus, byte[] host_key_public_modulus, byte[] supported_ciphers_mask, byte[] server_key_public_exponent, byte[] host_key_public_exponent) throws IOException { | |
byte packet_type = SSH_CMSG_SESSION_KEY; | |
// session_id | |
byte[] session_id = new byte[host_key_public_modulus.length + server_key_public_modulus.length + anti_spoofing_cookie.length]; | |
int offset = 0; | |
System.arraycopy(host_key_public_modulus, 0, session_id, offset, host_key_public_modulus.length); | |
offset += host_key_public_modulus.length; | |
System.arraycopy(server_key_public_modulus, 0, session_id, offset, server_key_public_modulus.length); | |
offset += server_key_public_modulus.length; | |
System.arraycopy(anti_spoofing_cookie, 0, session_id, offset, anti_spoofing_cookie.length); | |
session_id = Misc.md5(session_id); | |
// cipher_type | |
byte cipher_type = 0; | |
boolean foundSupportedCipher = false; | |
for (int i = 0; i < preferredCipherTypes.length && !foundSupportedCipher; ++i) { | |
cipher_type = (byte) preferredCipherTypes[i]; | |
foundSupportedCipher = (supported_ciphers_mask[3] & (byte) (1 << cipher_type)) != 0; | |
} | |
if (!foundSupportedCipher) { | |
throw new IOException(SSHPlugin.getResourceString("Client.cipher")); | |
} | |
// session_key | |
byte[] session_key = new byte[32]; | |
byte[] session_key_xored = new byte[32]; | |
byte[] session_key_encrypted = null; | |
Misc.random(session_key, 0, session_key.length, true); | |
System.arraycopy(session_key, 0, session_key_xored, 0, session_key.length); | |
Misc.xor(session_key_xored, 0, session_id, 0, session_key_xored, 0, session_id.length); | |
byte[] result = Misc.encryptRSAPkcs1(session_key_xored, server_key_public_exponent, server_key_public_modulus); | |
result = Misc.encryptRSAPkcs1(result, host_key_public_exponent, host_key_public_modulus); | |
session_key_encrypted = new byte[result.length + 2]; | |
session_key_encrypted[1] = (byte) ((8 * result.length) & 0xff); | |
session_key_encrypted[0] = (byte) (((8 * result.length) >> 8) & 0xff); | |
for (int i = 0; i < result.length; i++) { | |
session_key_encrypted[i + 2] = result[i]; | |
} | |
// protocol_flags | |
byte[] protocol_flags = {0, 0, 0, 0}; | |
// data | |
byte[] data = new byte[1 + anti_spoofing_cookie.length + session_key_encrypted.length + protocol_flags.length]; | |
offset = 0; | |
data[offset++] = (byte) cipher_type; | |
System.arraycopy(anti_spoofing_cookie, 0, data, offset, anti_spoofing_cookie.length); | |
offset += anti_spoofing_cookie.length; | |
System.arraycopy(session_key_encrypted, 0, data, offset, session_key_encrypted.length); | |
offset += session_key_encrypted.length; | |
System.arraycopy(protocol_flags, 0, data, offset, protocol_flags.length); | |
// cipher | |
cipher = Cipher.getInstance(cipherNames[cipher_type]); | |
cipher.setKey(session_key); | |
// packet | |
ClientPacket packet = new ClientPacket(packet_type, data, null); | |
socketOut.write(packet.getBytes()); | |
socketOut.flush(); | |
} | |
} |