Apache MINA で任意のssh応答を返却させる

## 2019/9/30 追記

ソースコードをgithubに公開しました。
https://github.com/kaneuchi-0202/apache_mina_test

公開に合わせて記事内では要所のみ載せるようにしました。

# はじめに

## 導入

システムの試験、動作確認用のダミーサーバを作ることになりました。
ssh通信を行うシステムなので、sshサーバを立てることになり色々探した結果、
[Apache MINA](https://mina.apache.org/)がサーバサイドのライブラリとして良さそうだったので、使おうと思いました。

**まあ情報がない**。。。

公式のドキュメントもさらっとしてて、ネット上で記事を書いてる人も少ない。。
ので、ソース見ながら悪戦苦闘した記録だけ残しておきます。

## 環境

||バージョン|
|---|--:|
|OS|Windows 10 pro|
|java|jdk1.8.0_161|
|Apache MINA|2.3.0|
|gradle|2.2|

## MINAのインストール

build.gradleに以下を追記します。

``` build.gradle
dependencies {
	// SSH
    compile group: 'org.apache.sshd', name: 'sshd-core', version: '2.3.0'
}
```

# 実装

そこそこ色んなことを試したのでパターン別に紹介します。
SSHサーバ起動後は雑に無限ループさせてますが、特定のコマンドを受信したときに終了するようにとかするといいと思います。

## 通常のLinuxサーバのようにログインしてコマンド実行させたい場合

``` java:ShellLoginServerMain.java
public static void main(String[] args) {

		int port = 22;

		SshServer sshd = SshServer.setUpDefaultServer();

		sshd.setPort(port);
		sshd.setPasswordAuthenticator(new MyPasswordAuthenticator());
		sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());

		// Windows用
		sshd.setShellFactory(new ProcessShellFactory(new String[] { "C:\\WINDOWS\\system32\\cmd.exe" }));
		// Linux用
		//		      sshd.setShellFactory(new ProcessShellFactory(new String[] { "/bin/sh", "-i", "-l" }));

		try {
			sshd.start();
			System.out.println("sshサーバ起動。ポート：" + port);

			// sshd.start()だけではプログラムが終了してしまうため無限ループ
			while (true)
				;
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				sshd.stop();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * パスワード認証用クラス。
	 */
	static class MyPasswordAuthenticator implements PasswordAuthenticator {

		@Override
		public boolean authenticate(String username, String password, ServerSession session)
				throws PasswordChangeRequiredException, AsyncAuthException {

			// ユーザID：user、パスワード：passwordのみ認証成功とする
			return "user".equals(username) && "password".equals(password);
		}
	}
```

ログインシェルは実行環境のOSに合わせて、コメントアウトする行を変更します。
アプリケーションを起動すればssh接続ができます。簡単ですね。
`sshpass -p password ssh user@IPアドレス`

```
[root@localhost ~]# ssh user@192.168.1.1
Password authentication
Password:
Microsoft Windows [Version 10.0.17134.885]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\workspace\apache_mina\apache_mina_test>dir
2019/09/30  09:35    <DIR>          .
2019/09/30  09:35    <DIR>          ..
2019/09/30  09:35               454 .classpath
2019/09/30  09:35    <DIR>          .gradle
2019/09/30  09:35               640 .project
2019/09/30  09:35    <DIR>          .settings
2019/09/30  09:42    <DIR>          bin
2019/09/30  09:39             1,235 build.gradle
2019/09/30  09:35    <DIR>          gradle
2019/09/30  09:35             5,299 gradlew
2019/09/30  09:35             2,260 gradlew.bat
2019/09/30  09:35               589 settings.gradle
2019/09/30  09:35    <DIR>          src
C:\workspace\free\apache_mina\apache_mina_test>
```

※環境によっては文字化けするかもしれません

## ssh通信で電文の送受信を行う場合

### シェル形式

1コマンド1応答形式の通信であればCommandクラスを複数作成することで対応できます。コマンドに対してダミーの情報を返却させたい場合などですね。

``` java:ShellServerMain.java
public static void main(String[] args) {

	int port = 22;
	SshServer sshd = SshServer.setUpDefaultServer();
	sshd.setPort(port);
	sshd.setPasswordAuthenticator(new MyPasswordAuthenticator());
	sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
	sshd.setCommandFactory(new MyCommandFactory());

   ～省略～
}
```
``` java:MyCommandFactory.java
@Override
public Command createCommand(ChannelSession channel, String command) throws IOException {

	System.out.println("受信コマンド：" + command);

	// 受信コマンドに応じたCommandクラスを返却する
	if (command.equals("hoge")) {
		return new CommandA(command, ThreadUtils.newCachedThreadPool("hoge-thread"));
	} else {
		return new CommandB(command, ThreadUtils.newCachedThreadPool("fuga-thread"));
	}
}

/**
 * コマンドAクラス。
 */
class CommandA extends AbstractCommandSupport {

	protected CommandA(String command, CloseableExecutorService executorService) {
		super(command, executorService);
	}

	@Override
	public void run() {
		try {
			// コマンドA用のレスポンスを返却
			this.out.write("hoge\r\n".getBytes());
			this.out.flush();

			// リザルトコード0で処理を終了
			this.onExit(0);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

/**
 * コマンドBクラス。
 */
class CommandB extends AbstractCommandSupport {

	protected CommandB(String command, CloseableExecutorService executorService) {
		super(command, executorService);
	}

	@Override
	public void run() {
		try {
			// コマンドB用のレスポンスを返却
			this.out.write("fuga\r\n".getBytes());
			this.out.write("fuga\r\n".getBytes());
			this.out.flush();

			// リザルトコード0で処理を終了
			this.onExit(0);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
```

SSHサーバに`setCommandFactory()`で自作のCommandFactoryインスタンスを設定しておきます。リクエストが来た場合は`MyCommandFactory.createCommand()`が呼び出されるので、自作のコマンドを呼び分けています。
コマンド内では適当な応答を返しているだけです。コマンド内で最後に`this.onExit(0);`を入れておかないと、終了と判断されずいつまでもコンソールが返ってこなくなります。

例）

```
[root@localhost ~]# ssh user@192.168.1.1 hoge
Password authentication
Password:
hoge
[root@localhost ~]# ssh user@192.168.1.1 fuga
Password authentication
Password:
fuga
fuga
[root@localhost ~]#
```

### 対話形式

今回のシステムが対象としている機器が、一度つないだSSH接続を対話形式で使い続ける動きだったので、再現するために1コマンド内で受信電文を見て応答を変化させる方式をとりました。


``` java:MyCommandFactory.java
@Override
public Command createCommand(ChannelSession channel, String command) throws IOException {

	System.out.println("受信コマンド：" + command);
	return new MyCommand(command, ThreadUtils.newCachedThreadPool("cmd-thread"));
}

/**
 * 受信した電文に応じてレスポンスを返却するCommandクラス。
 */
class MyCommand extends AbstractCommandSupport {

	protected MyCommand(String command, CloseableExecutorService executorService) {
		super(command, executorService);
	}

	@Override
	public void run() {
		try {
			while (true) {
				// 無限ループで受信を待機
				byte[] buff = new byte[1024];
				if (this.in.read(buff) == -1) {

					// 終端に達した場合は終了
					System.out.println("EOF");
					break;
				}
				String recv = new String(buff);
				System.out.println("受信文字列：" + recv);

				// 受信文字列に応じたレスポンスを返却
				String msg = "[response]";
				if (recv.contains("hoge")) {
					msg += "hoge\r\n";
				} else if(recv.contains("fuga")){
					msg += "fuga\r\nfuga\r\n";
				} else if (recv.contains("quit")) {
					break;
				} else {
					msg += "unknown command receive.\r\n";
				}
				this.out.write(msg.getBytes());
				this.out.flush();
			}

			// リザルトコード0で処理を終了
			this.onExit(0);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
```

InputStreamの読込を待機して、受信した文字列に応じた応答を返却するようにしてます。

```
[root@localhost ~]# ssh user@192.168.1.1 hoge
Password authentication
Password:
hoge
[response]hoge
fuga
[response]fuga
fuga
aaa
[response]unknown command receive.
testes
[response]unknown command receive.
quit
[root@localhost ~]#
```

# おわりに

参考にできる記事が少なさ過ぎて苦労しましたが、どうにかやりたいことはできました。
それっぽいところのソースを追えば、なんやかんや実装できるっていういい経験でしたね。
CommandFactoryに`ScpCommandFactory`を指定すれば、SCPにも対応できますが自作のCommandFactoryと同時に指定できないのでうまいこと共存する方法はあるんですかねえ。

[参考]無理矢理自作コマンドとSCPを両立させたコード
https://github.com/kaneuchi-0202/apache_mina_test/tree/master/apache_mina_test/src/main/java/apache_mina_test/any_response/scp