実践JavaでSSH接続

# はじめに

Javaでリモート接続してコマンド実行したり、リモート環境とのファイル転送をしたい用途があったので、ライブラリを使って試してみました。


# 使用したライブラリ

`JSch`

http://www.jcraft.com/jsch/

https://mvnrepository.com/artifact/com.jcraft/jsch

# 基本的な使い方

リモート接続して下記を実施する方法を紹介します。

- コマンド実行
- ファイル転送


## 接続方法

共通の接続方法についてです。
sshのコマンドとしては下記と同等です。
```shell
ssh -p 22 root@192.168.1.1
```



```java
String user = "root";
String host = "192.168.1.1";
String port = "22";

JSch jsch = new JSch();
Session session = jsch.getSession(user, host, port);
session.setConfig("StrictHostKeyChecking", "no");
session.setPassword(password);
session.connect();
```

上記は`known_hosts` ファイルを使用しない方法であり、指定する場合は
`session.setConfig("StrictHostKeyChecking", "no");`を使用せず下記を使用してください。
``` java
jsch.setKnownHosts(".ssh/known_hosts");
```


## コマンド実行

リモート接続してコマンド実行する。

sshのコマンドとしては下記と同等です。
```shell
ssh -p 22 root@192.168.1.1 ls
```

```java
String command = "ls";

ChannelExec channel = (ChannelExec) session.openChannel("exec");
channel.setCommand(command);
channel.connect();
```

## ファイルアップロード

リモート先にファイルをアップロードする。

scpのコマンドとしては下記と同等です。
```shell
scp -p 22 test.txt root@192.168.1.1:/tmp/
```

```java
String fileName = "test.txt";
String dir = "/tmp/";
FileInputStream fileStream = new FileInputStream(fileName);

ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");
channel.connect();
channel.cd(dir);
channel.put(fileStream, fileName);
```

## ファイルダウンロード

リモート先のファイルをダウンロードする。

scpのコマンドとしては下記と同等です。
```shell
scp -p 22 root@192.168.1.1:/tmp/test.txt ./tmp/
```

```java
String remoteFilePath = "/tmp/test.txt";
String localPath = "tmp/";

ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");
channel.connect();
channel.get(remoteFilePath, localPath);
```


# 実践編

## 実装


### 設定情報

```java
public class RemoteConfig {

    protected String host;
    protected int port;
    protected String user;
    protected String password;

    public RemoteConfig() {
        this.host = "192.168.1.1";
        this.port = 22;
        this.user = "root";
        this.password = "hoge";
    }

    public String getHost() { return host; }
    public int getPort() { return port; }
    public String getUser() { return user; }
    public String getPassword() { return password; }
}
```

### 基底クラス

```java
abstract public class RemoteBee implements Closeable {

    protected Session session = null;

    public RemoteBee(RemoteConfig connectionSetting) {
        connectSession(new JSch(), connectionSetting);
    }

    /**
     * 接続を開始.
     *
     * @param connectionSetting 接続設定
     */
    protected void connectSession(JSch jsch, RemoteConfig connectionSetting) {
        try {
            this.session = jsch.getSession(
                    connectionSetting.getUser(),
                    connectionSetting.getHost(),
                    connectionSetting.getPort()
            );
            this.session.setConfig("StrictHostKeyChecking", "no");
            this.session.setPassword(connectionSetting.getPassword());
            this.session.connect();
        } catch (JSchException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public RemoteData execute(RemoteData remoteData) {
        try {
            openChannel();
            return exec(remoteData);
        } finally {
            closeChannel();
        }
    }

    abstract protected RemoteData exec(RemoteData remoteData);

    /**
     * 接続.
     */
    abstract protected void openChannel();

    /**
     * 接続を閉じる
     */
    protected void closeChannel() {
        getChannel().disconnect();
    }

    abstract protected Channel getChannel();

    /**
     * 接続を閉じる.
     */
    @Override
    public void close() {
        session.disconnect();
    }
}
```

## Sshコマンド実行

```java
public class ShellRemoteData extends RemoteData {
    protected String command;
    protected List<String> resultMessage = new ArrayList<>();
    protected List<String> errorMessage = new ArrayList<>();;
    protected Boolean isSuccess = false;

    public ShellRemoteData(String command) {
        this.command = command;
    }

    public void addResultMessage(String message) {
        this.resultMessage.add(message);
    }

    public void addErrorMessage(String message) {
        this.errorMessage.add(message);
    }

    public void setSuccess(Boolean success) {
        isSuccess = success;
    }

    public String getCommand() {
        return command;
    }

    public List<String> getResultMessage() {
        return resultMessage;
    }

    public List<String> getErrorMessage() {
        return errorMessage;
    }

    public Boolean getSuccess() {
        return isSuccess;
    }
}
```

```java
public class ShellRemote extends RemoteBee {

    protected ChannelExec channel = null;

    public ShellRemote(RemoteConfig connectionSetting) {
        super(connectionSetting);
    }

    @Override
    protected void openChannel() {
        try {
            this.channel = (ChannelExec) this.session.openChannel("exec");
        } catch (JSchException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    @Override
    protected ShellRemoteData exec(RemoteData remoteData) {
        ShellRemoteData shellRemoteData = (ShellRemoteData) remoteData;
        try {
            this.channel.setCommand(shellRemoteData.getCommand());
            this.channel.connect();

            try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(channel.getErrStream()));
                 BufferedReader echoReader = new BufferedReader(new InputStreamReader(channel.getInputStream()))
            ) {
                String line;
                // 実行結果
                while ((line = echoReader.readLine()) != null) {
                    shellRemoteData.addResultMessage(line);
                }
                // 実行時のエラー結果
                while ((line = errorReader.readLine()) != null) {
                    shellRemoteData.addErrorMessage(line);
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            // コマンドの戻り値を取得する
            int returnCode = channel.getExitStatus();
            if (returnCode == 0) {
                System.out.println("====== COMMAND SUCCESS ======");
            } else {
                System.out.println("====== COMMAND ERROR ======");
            }
            shellRemoteData.setSuccess(returnCode == 0);

        } catch (JSchException e) {
            throw new RuntimeException(e);
        }
        return shellRemoteData;
    }

    @Override
    protected ChannelExec getChannel() {
        return channel;
    }

}
```

## ファイルの転送

```java
public class ScpRemoteData extends RemoteData {
    protected String localPath;
    protected String fileName;
    protected String remotePath;

    public ScpRemoteData(String localPath, String fileName, String remotePath) {
        this.localPath = localPath;
        this.fileName = fileName;
        this.remotePath = remotePath;
    }

    public FileInputStream getFile() throws FileNotFoundException {
        return new FileInputStream(localPath + fileName);
    }
    
    public String getRemoteFile() {
        return remotePath + fileName;
    }

    public String getLocalPath() { return localPath; }
    public String getFileName() { return fileName; }
    public String getRemotePath() { return remotePath; }
}
```

#### アップロードする
```java
public class UploadRemote extends RemoteBee {

    protected ChannelSftp channel = null;

    public UploadRemote(RemoteConfig connectionSetting) {
        super(connectionSetting);
    }

    @Override
    protected void openChannel() {
        try {
            this.channel = (ChannelSftp) this.session.openChannel("sftp");
        } catch (JSchException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    @Override
    protected ChannelSftp getChannel() {
        return channel;
    }

    @Override
    protected ScpRemoteData exec(RemoteData remoteData) {
        ScpRemoteData scpRemoteData = (ScpRemoteData) remoteData;
        try {
            this.channel.connect();
            this.channel.cd(scpRemoteData.getRemotePath());
            this.channel.put(scpRemoteData.getFile(), scpRemoteData.getFileName());
        } catch (SftpException | JSchException | FileNotFoundException e) {
            throw new RuntimeException(e);
        }
        return scpRemoteData;
    }
}
```
#### ダウンロードする
```java
public class DownloadRemote extends RemoteBee {

    protected ChannelSftp channel = null;

    public DownloadRemote(RemoteConfig connectionSetting) {
        super(connectionSetting);
    }

    @Override
    protected void openChannel() {
        try {
            this.channel = (ChannelSftp) this.session.openChannel("sftp");
        } catch (JSchException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    @Override
    protected ChannelSftp getChannel() {
        return channel;
    }

    @Override
    protected ScpRemoteData exec(RemoteData remoteData) {
        ScpRemoteData scpRemoteData = (ScpRemoteData) remoteData;
        try {
            this.channel.connect();
            this.channel.get(scpRemoteData.getRemotePath(), scpRemoteData.getLocalPath());
        } catch (SftpException | JSchException e) {
            throw new RuntimeException(e);
        }
        return scpRemoteData;
    }
}
```

## 使い方

```java
public class Main {
    public static void main(String[] args) {
        RemoteConfig remoteConfig = new RemoteConfig();

        try (UploadRemote uploadRemote = new UploadRemote(remoteConfig)) {
            // ダンプするファイルをUPする
            uploadRemote.execute(new ScpRemoteData("tmp/", "testBefore.dump" ,"/tmp/"));
        }

        try (ShellRemote shellRemote = new ShellRemote(remoteConfig)) {
            // テスト前の DB をリストアする
            shellRemote.execute(new ShellRemoteData("createdb testdb"));
            shellRemote.execute(new ShellRemoteData("psql -U postgres testdb < testBefore.dump"));
            // テストを実施する
            shellRemote.execute(new ShellRemoteData("psql -U postgres -c \"update db SET column_value = 1;\""));
            // テスト後の DB をダンプする
            shellRemote.execute(new ShellRemoteData("pg_dump testdb > /tmp/testAfter.dump"));
        }

        try (DownloadRemote downloadRemote = new DownloadRemote(remoteConfig)) {
            // ダンプしたファイルをDLする
            downloadRemote.execute(new ScpRemoteData("tmp/", "testAfter.dump" ,"/tmp/"));
        }
    }
}
```



# 感想

今回JavaからSsh接続したかった目的は、WebアプリのE2Eテストでアプリの状態をテスト前に戻したいという用途でした。
E2Eテストは、JavaのSeleniumで作られたものがあったのですが、アプリが動作する環境をテスト前に戻すために、
シェルコマンド(`.sh`ファイル)が別に実装されていたため、シェルコマンドをJavaとは別に実行しないといけないなど冗長さがありました。
全部Javaでできないかなーということで今回のライブラリを試してみました。

Javaからsshコマンドを実行したいことは多くはないですが、覚えておくと便利だなって思いました。

# 備考

Javaは久しぶりだったのできれいに作れた気がしないので、細かな実装部分に関してはご容赦ください。
作りがおかしいところがあればご指摘お願いします。


# 参考

https://qiita.com/tool-taro/items/ab68353970ee792163c6

https://qiita.com/tool-taro/items/a3f016a58e3e32675858

https://qiita.com/hushukang/items/0ce4d5552f0484348b24