2010年12月16日

JavaでSSH

JavaでSSHしてみたいと思い、色々とライブラリを探してたのですが最終的にorion-ssh2に落ち着きました。
 
理由は、ライセンス形態がBSD(ライク)だったから。
 
もともとはGanymed SSH-2として始まり、途中trileadとなり、現在はorion-ssh2(build 214)としてプロジェクトが続いています。

ダウンロードしたorion-ssh2-214.jarにクラス・パスを通せば準備完了です。


 
次に示すサンプルコードではSSH接続後、サーバ側でuptimeコマンドを実行し、
その標準出力を表示しています(解説はコードの後で)。
 

public class SSHClient implements ConnectionMonitor {
  
 public static void main(String[] args) {
SSHClient ssh = new SSHClient();
ssh.getUpTime("localhost", "root", "password4root");
}

public void getUpTime(String host, String user, String password) {
String command = "uptime";
Connection conn = new Connection(host, 22);
conn.addConnectionMonitor(this);

try {
System.out.println("connecting to " + host + "...");
int connTimeoutInMlillis = 5000;
conn.connect(null, connTimeoutInMlillis, 0);
System.out.println("succeed: connect to " + host);
if ( conn.authenticateWithPassword(user, password) ) {
Session sess = conn.openSession();
System.out.println("succeed: authenticate with password.");
/*
* requestPTY(
* java.lang.String term,
* int term_width_characters,
* int term_height_characters,
* int term_width_pixels,
* int term_height_pixels,
* byte[] terminal_modes
* )
*/
sess.requestPTY("vt100", 0, 0, 0, 0, null);
sess.startShell();
// get stdout, in, error
OutputStream stdin = sess.getStdin();
InputStream stdout = sess.getStdout();
InputStream stderror = sess.getStderr();

// send command
stdin.write( command.getBytes() );
stdin.write('\n');
stdin.flush();

long timeoutInMillis = 2000;
int conditions = ChannelCondition.STDOUT_DATA |
ChannelCondition.STDERR_DATA |
ChannelCondition.CLOSED |
ChannelCondition.EOF;
byte[] buffer = new byte[1024];
int condition = sess.waitForCondition(conditions, timeoutInMillis); // conditions, timeoutInMillis

// read stdout, stderror
while ( condition != ChannelCondition.TIMEOUT ) {

if ( condition == ChannelCondition.STDOUT_DATA ) {
int len = stdout.read(buffer);
if ( len > 0 ) {
System.out.write(buffer, 0, len);
}
}

if ( condition == ChannelCondition.STDERR_DATA ) {
int len = stderror.read(buffer);
if ( len > 0 ) {
System.err.write(buffer, 0, len);
}
}

if ( condition == ChannelCondition.EOF ) {
// connection closed.
System.out.println("SSH connection has been closed(EOF).");
break;
}

if ( condition >= ChannelCondition.EXIT_STATUS ) {
/*
* unkown condition (50,58 is not defined in ChannelCondition, but appears!).
* treat the condition that is bigger than EXIT_STATUS(32) as connection closed.
*/
System.out.println("SSH connection has been closed("+condition+").");
break;
}
condition = sess.waitForCondition(conditions, timeoutInMillis);
}

// close session
sess.close();
// close connection
conn.close();
} else {
System.out.println("login failed.");
}
} catch (IOException e) {
e.printStackTrace();
}

}

@Override
public void connectionLost(Throwable arg0) {
System.out.println("lost connection!");
}
}


このコードを実行すると次のような出力になります。

connecting to localhost…
succeed: connect to localhost
succeed: authenticate with password.
uptime
Last login: Thu Dec 12 17:52:44 2010 from 192.168.x.x
[root@server ~]# uptime
 19:00:52 up 24 days,  2:36,  3 users,  load average: 0.00, 0.00, 0.00
[root@server ~]# lost connection!


最後に解説。

まず、conn.connect(null, connTimeoutInMlillis, 0);でコネクションを取得し、conn.authenticateWithPassword(user, password)で認証しています。

ちなみにこのコードではSSHサーバ側の設定によっては、

Password authentication fails, I get "Authentication method password not supported by the server at this stage".
 
というエラーが発生することがあります。

このような場合、FAQにあるとおり対話的な認証を行うかなサーバ側の設定(sshd_config)を変更(PasswordAuthentication yes)する必要があります。

認証に成功した場合、セッションをオープン(conn.openSession())し、
SSHサーバの標準入力・出力及び標準エラー出力を取得します。
コマンドを実行するには標準入力にコマンドのバイト配列を書き込みます。
 
stdin.write( command.getBytes() );
stdin.write('\n');
stdin.flush();

実行したら後はサーバのレスポンスを待機して読み込みます。
待機する受信(チャンネル)コンディションを定義し、sess.waitForCondition(conditions, timeoutInMillis);で待機します。

次にタイムアウトでないこと確認しつつ、whileループ内で各出力を読み込みます。全ての出力を読み込んだか否かは判断が出来ないため、whileループの最後で再度待ち受けます(sess.waitForCondition(conditions, timeoutInMillis);)。

高速化するならプロンプト([root@server ~]# )を正規表現を使って判断してそこで抜ける、という手もありますが、確実にプロンプトを判断できるとは限らないから僕は嫌なんですよね。。

ChannelCondition.EOF、ChannelCondition.EXIT_STATUSを受信すれば即座に終了しますが、それ以外ではタイムアウトが発生した場合にループを抜けます。

ですので、このサンプルコードでは、2秒以上時間を要するレスポンスは全てタイムアウトになってしまいます。

対話的なアプリケーションを作成するなら問題無いのですが、非対話的な場合は、
このレスポンス読み込みの終了の判断とタイムアウト時間の設定が難しいところです。

ちなみに、

sess.requestPTY("vt100", 0, 0, 0, 0, null);
 
をコメントアウトすると、サーバ側がプロンプトを戻さなくなり、
次のような実行結果になります。
 
 19:00:52 up 24 days,  2:36,  3 users,  load average: 0.00, 0.00, 0.00 


実用SSH 第2版―セキュアシェル徹底活用ガイド
Daniel J. Barrett Richard E. Silverman Robert G. Byrnes 
オライリー・ジャパン 
売り上げランキング: 361928

Posted by netbuffalo at 18:00│TrackBack(0)Java 


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/1911728