25 мая 2013 г.

Программное подключение по ssh при помощи jsch

Используются: Maven 3, IntelliJ Idea 12 Ultimate, JSch 0.1.50, SSH.

Рассмотрим программное взаимодействие с удалённым сервером по SSH. Для этого нам пригодится библиотека JSch. Для удобства создадим maven-проект в Idea (File - New Module - Maven Module). Назовём проект ssh-example. В качестве архетипа проекта можно использовать архетип по умолчанию (т.е. базовый пустой проект). В pom.xml добавим последнюю версию библиотеки:
<dependencies>
<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.50</version>
</dependency>
</dependencies>
Давайте создадим класс ru.blogspot.developer.remarks.SshManager. В него добавим статические поля, чтобы в любой момент эти параметры легко было найти и изменить.
private static final int SSH_PORT = 22;
private static final int CONNECTION_TIMEOUT = 10000;
private static final int BUFFER_SIZE = 1024;
SSH_PORT - это порт для SSH подключения. На любом сервере обычно это 22-ой порт. Однако может быть и другой. CONNECTION_TIMEOUT - время, в течение которого мы будем ожидать ответа от удалённого сервера. Указывается в миллисекундах. В данном случае у нас установлено 10 секунд. BUFFER_SIZE - размер буфера для приёма ответа от сервера.
Следующие три параметра нужны для доступа к удалённом хосту. Разумеется, у вас они будут другими.
private static final String HOSTNAME = "192.168.1.2";
private static final String USERNAME = "admin";
private static final String PASSWORD = "123456";
Далее статический метод main, создающий экземпляр нашего класса и вызывающий подключение к хосту. Затем мы выводим полученные строки на экран.
public static void main(String[] args) {
SshManager manager = new SshManager();
List<String> lines = manager.connectAndExecuteListCommand(HOSTNAME, USERNAME, PASSWORD);
System.out.println(lines);
}
Основной метод, в котором реализуется вся логика работы нашего приложения:
public List<String> connectAndExecuteListCommand(String host, String username, String password) {
List<String> lines = new ArrayList<String>();
try {
    String command = "ls -1\n";
    Session session = initSession(host, username, password);
    Channel channel = initChannel(command, session);
    InputStream in = channel.getInputStream();
    channel.connect();
    String dataFromChannel = getDataFromChannel(channel, in);
    lines.addAll(Arrays.asList(dataFromChannel.split("\n")));
    channel.disconnect();
    session.disconnect();
} catch (Exception e) {
    System.out.println(e);
}
return lines;
}
В качестве примера мы подключимся к хосту и выполним там команду ls -l (вывести все файлы и папки в текущей директории в один столбец). Результат выполнения мы получим в виде набора строк, по одному элементу в каждой строке. Разумеется, вы можете выполнить абсолютно любую консольную команду, какую вы захотите.

Далее идут вспомогательные методы. Метод создания сессии:
private Session initSession(String host, String username, String password) throws JSchException {
JSch jsch = new JSch();
Session session = jsch.getSession(username, host, SSH_PORT);
session.setPassword(password);
UserInfo userInfo = new MyUserInfo();
session.setUserInfo(userInfo);
session.setConfig("StrictHostKeyChecking", "no");
session.connect(CONNECTION_TIMEOUT);
return session;
}
Здесь нам потребуется создать вспомогательный класс MyUserInfo, реализующий интерфейс UserInfo.

public class MyUserInfo implements UserInfo {

    private String password;

    public void showMessage(String message) {
        System.out.println(message);
    }

    public boolean promptYesNo(String message) {
        System.out.println(message);
        return true;
    }

    @Override
    public String getPassphrase() {
        return null;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public boolean promptPassphrase(String arg0) {
        System.out.println(arg0);
        return true;
    }

    @Override
    public boolean promptPassword(String arg0) {
        System.out.println(arg0);
        this.password = arg0;
        return true;
    }
}

По сути это заранее определённый набор ответов пользователя на запросы системы. Например, при запросе пароля будет автоматически введён пароль, который здесь указан. При любом вопросе с вариантами ответа "Да/Нет" всегда будет подставляться ответ "Да". Такой класс позволяет полностью автоматизировать взаимодействие по SSH.

Вернёмся к нашему основному классу. Следующий вспомогательный метод:
private Channel initChannel(String commands, Session session) throws JSchException {
Channel channel = session.openChannel("exec");
ChannelExec channelExec = (ChannelExec) channel;
channelExec.setCommand(commands);
channelExec.setInputStream(null);
channelExec.setErrStream(System.err);
return channel;
}
Здесь создаётся канал для выполнения команд, о чём свидетельствует параметр "exec". Устанавливаем строку с командами, которые мы хотим выполнить. Если вы хотите выполнить сразу несколько команд, просто разделите их символом перевода строки, как и в обычной командной оболочке. Входной поток устанавливаем в null (мы его не будем использовать). Поток ошибок устанавливаем, соответственно, в System.err.

Метод для получения данных из канала (т.е. ответа от хоста).
private String getDataFromChannel(Channel channel, InputStream in)
    throws IOException {
StringBuilder result = new StringBuilder();
byte[] tmp = new byte[BUFFER_SIZE];
while (true) {
    while (in.available() > 0) {
        int i = in.read(tmp, 0, BUFFER_SIZE);
        if (i < 0) {
            break;
        }
        result.append(new String(tmp, 0, i));
    }
    if (channel.isClosed()) {
        int exitStatus = channel.getExitStatus();
        System.out.println("exit-status: " + exitStatus);
        break;
    }
    trySleep(1000);
}
return result.toString();
}
Здесь мы считываем данные в буфер указанного нами размера до тех пор, пока данные продолжают поступать. После закрытия канала получаем код статуса. Если всё прошло успешно, статус равен нулю. Пока канал не закрыт, ожидаем получения новых данных с интервалом в одну секунду.

Для этого используется ещё один простой метод:
private void trySleep(int sleepTimeInMilliseconds) {
try {
    Thread.sleep(sleepTimeInMilliseconds);
} catch (Exception e) {
}
}
Всё, наш основной класс готов. Осталась одна маленькая деталь.

Нам нужно создать автономный исполняемый (runnable jar) файл, чтобы его легко можно было запускать прямо из командной строки вне какой-либо среды исполнения. Как это сделать, описано здесь.

Теперь выполните в консоли команду java -jar target/ssh-example-1.0-SNAPSHOT-executable.jar, находясь при этом в директории вашего проекта. Если всё было сделано правильно, вы получите список файлов и каталогов в домашней директории на удалённом хосте.

12 комментариев:

  1. не могу войти в режим суперпользователя - standard in must be a tty ... на РНР такая же фигня

    ОтветитьУдалить
  2. А каким образом вы пытаетесь войти в этот режим?

    ОтветитьУдалить
  3. >>Для удобства создадим maven-проект в Idea
    Как это в блокноте сделать?

    ОтветитьУдалить
    Ответы
    1. Достаточно создать следующую структуру каталогов с pom-файлом:
      ├── pom.xml
      ├── src
      │   ├── main
      │   │   ├── java
      │   │   └── resources
      │   └── test
      │   └── java

      Java-классы следует поместить src/main/java.

      Удалить
  4. Пробую выключить машину консольной командой sudo poweroff, но сервер на той стороне запрашивает повторно ввести пароль, из-за чего ошибка "sudo: no tty present and no askpass program specified". Как вариант, могу сделать пользователя привилегированным, чтобы пароль не запрашивался, но это не подходит... В коде вроде предусмотрено, чтобы пароль автоматически вводился((( Возможно как-нибудь пофиксить??? :(

    ОтветитьУдалить
    Ответы
    1. Я думаю, не очень правильно выключать удалённый сервер через ssh - вы же связь с ним можете потерять.

      Удалить
    2. В этом вся суть. Необходимо гасить стойку с серверами в соседнем помещении в конце рабочего дня. Здесь вроде отличное решение для автоматизации данного процесса. Но вот проблему с повторным вводом пароля не могу решить(((

      Удалить
  5. Решил проблему. Ответ вот здесь: http://www.jcraft.com/jsch/examples/Sudo.java.html
    Вдруг кому пригодится
    // man sudo
    // -S The -S (stdin) option causes sudo to read the password from the
    // standard input instead of the terminal device.
    // -p The -p (prompt) option allows you to override the default
    // password prompt and use a custom one.
    ((ChannelExec)channel).setCommand("sudo -S -p '' "+command);

    ОтветитьУдалить
  6. Ребята, а как обойти:
    Kerberos username
    Kerberos password

    Может кто-то сталкивался.. У автора, видимо, такого не было.

    ОтветитьУдалить
  7. Нашел решение, если кто-то тоже воткнется:

    session.setConfig(
    "PreferredAuthentications",
    "publickey,keyboard-interactive,password");

    ОтветитьУдалить
  8. Друзья, а почему мне пустой list возвращается в итоге?..

    exit-status: 0
    []

    "session.setConfig" не влияет, а в остальном код как у автора

    ОтветитьУдалить
  9. В Винде, при формировании вывода, синтаксис будет таким:

    lines.addAll(Arrays.asList(dataFromChannel.split("\r\n")));

    иначе все лепить в одну строку будет.
    Ваш КЭП

    ОтветитьУдалить