АЛЕКСАНДР ФЕФЕЛОВ
Java: встраиваем сервер Telnet
В процессе работы над одним Java-проектом я столкнулся с необходимостью удаленного управления серверным приложением. Разрабатывать полновесный графический интерфейс пользователя (GUI) мне показалось нерациональным. Ведь если GUI создать на базе Swing или SWT, то придется устанавливать дополнительные программы (JRE и собственно GUI) на клиентских рабочих местах. А если GUI построить на основе JSP, то дополнительные программы (JSP-контейнер) придется устанавливать на сервере. В то же время проект был рассчитан на эксплуатацию квалифицированным персоналом, которому нет нужды до свистулек и погремушек графического интерфейса. Значит, вполне подходящим выбором могли быть доступные удаленно интерфейс командной строки (CLI) или текстовый интерфейс пользователя (TUI). Наиболее очевидный выбор для реализации удаленных CLI и TUI – это Telnet (RFC 854). Данная статья и посвящена встраиванию функционала сервера Telnet в Java-приложения.
TelnetD
Инструмент, способный помочь в решении поставленной задачи, был быстро найден. Это TelnetD – встраиваемый многопоточный сервер Telnet, реализованный на языке Java Дитером Вимбергером (Dieter Wimberger). TelnetD поставляется в виде исходных текстов по модифицированной лицензии BSD. На момент написания статьи была доступна версия 1.0.
Архитектура TelnetD
Центральным объектом TelnetD является сервер (Daemon по терминологии разработчика), который создает все другие необходимые для работы объекты – слушателей (Listeners), менеджер оболочек (ShellManager) и менеджер терминалов (TerminalManager).
Слушатели принимают и управляют сетевыми соединениями от клиентских программ.
Менеджер оболочек занимается созданием оболочек (интерпретаторов командной строки), которые воспринимают и исполняют команды пользователя и реагируют на события сетевого соединения (такие как бездействие, таймаут или разрыв).
Поддержка конкретных реализаций терминалов (таких как ANSI, VT100 или xterm), возложена на менеджер терминалов.
Все перечисленные компоненты TelnetD конфигурируются с помощью объектов java.util.Properties, а значит настройки можно хранить в обычных текстовых файлах.
Для встраивания TelnetD в программу нужно всего лишь запрограммировать собственный интерпретатор командной строки, реализующий необходимую логику взаимодействия с пользователем.
Начнем…
Для работы с TelnetD нам понадобятся:
- дистрибутив TelnetD, включающий исходные тексты (файл telnetd-1.0.zip) и документацию (файл telnetd-1.0_docs.zip);
- инструментарий Apache Ant. (В задачи данной статьи не входит описание работы с Ant. Будем предполагать, что Ant уже установлен и настроен.)
Развернем дистрибутив и скомпилируем TelnetD, выполнив в каталоге установки команду:
ant jar
При компиляции будут выданы предупреждения об использовании устаревшего (deprecated) метода java.lang. Thread.stop(), которые могут быть безболезненно проигнорированы. В результате компиляции в каталоге build будет создан файл telnetd.jar. (Обратите внимание на его малый размер. В моем случае – всего 86060 байт.)
Теперь скопируем все файлы из каталога src et wimpi elnetd esources в каталог build, в нем выполним команду:
java -classpath telnetd.jar net.wimpi.telnetd.TelnetD
которая запустит сервер TelnetD на выполнение в демонстрационном режиме, и попытаемся установить соединение с сервером с помощью команды:
telnet localhost 6666
Voila! Наш сервер работает.
Пример использования TelnetD
Как было сказано выше, для встраивания TelnetD в программу необходимо создать свой класс для интерпретатора командной строки. Этот класс должен реализовывать интерфейс Shell из пакета net.wimpi.telnetd.shell.
Следует отметить, что интерфейс Shell является расширением интерфейса ConnectionListener из пакета net. wimpi.telnetd.net. А это значит, что интерпретатор командной строки должен предоставлять методы, реагирующие на события соединения.
Мы создадим максимально простой интерпретатор, который будет ждать ввода пользователем какого-либо символа и выполнять соответствующее действие. Набор действий невелик – это выдача дополнительной информации о сеансе, завершение сеанса и останов сервера. Итак, код:
package telnetdtest;
import net.wimpi.telnetd.*;
import net.wimpi.telnetd.io.*;
import net.wimpi.telnetd.net.*;
import net.wimpi.telnetd.shell.*;
public class Test1Shell implements Shell {
// Создание экземпляра интерпретатора командной строки (оболочки). Этот метод, хотя и не описан в интерфейсе Shell, должен быть
// реализован, так как он используется менеджером оболочек для создания экземпляра конкретной оболочки
public static Shell createShell() {
return new Test1Shell();
}
// Собственно интерпретатор
public void run(Connection con) {
connection = con;
termIO = (TerminalIO) connection.getTerminalIO();
// Регистрируем этот объект в качестве обработчика событий соединения
this.connection.addConnectionListener(this);
// Выводим баннер, содержащий номер версии TelnetD
termIO.write("TelnetD testbed 1"
+ CRLF + "TelnetD version: "
+ TelnetD.getReference().getVersion() + CRLF);
termIO.flush();
while (true) {
// Выводим подсказку
termIO.write(CRLF
+ "Press [i] for information," + CRLF
+ "[x] for exit," + CRLF + "[s] for shutdown"
+ CRLF + CRLF);
termIO.flush();
// Ждем ввода от пользователя
int ch = termIO.read();
// Реагируем соответствующим образом
switch (ch) {
case 120: // x - завершение сеанса
termIO.write("Goobye!");
termIO.flush();
connection.close();
return;
case 115: // s - останов сервера
termIO.write("Requiescat in pace!");
termIO.flush();
TelnetD.getReference().setServing(false);
TelnetD.getReference().shutdown();
System.exit(0);
return;
case 105: // i - информация о сервере и сеансе
// Получаем данные о соединении
ConnectionData data =
connection.getConnectionData();
// Выводим данные на экран
termIO.write("Additional info:" + CRLF);
termIO.write("Connected from: "
+ data.getHostName()
+ " [" + data.getHostAddress() + ":"
+ data.getPort() + "]" + CRLF);
termIO.write("Guessed locale: "
+ data.getLocale() + CRLF);
termIO.write("Negotiated terminal type: "
+ data.getNegotiatedTerminalType() + CRLF);
termIO.write("Scrolling support: "
+ (termIO.getTerminal().supportsScrolling() ?
"yes" : "no") + CRLF);
termIO.write("Graphics rendition support: "
+ (termIO.getTerminal().supportsSGR() ?
"yes" : "no") + CRLF);
termIO.write("Negotiated columns: "
+ data.getTerminalColumns() + CRLF);
termIO.write("Negotiated rows: "
+ data.getTerminalRows() + CRLF);
termIO.write("Login shell: "
+ data.getLoginShell() + CRLF);
termIO.flush();
break;
}
}
}
// Реакция на событие соединения: таймаут
public void connectionTimedOut(ConnectionEvent ce) {
termIO.write("*** Connection timedout" + CRLF);
termIO.flush();
connection.close();
}
// Реакция на событие соединения: бездействие
public void connectionIdle(ConnectionEvent ce) {
termIO.write("*** Connection idle" + CRLF);
termIO.flush();
}
// Реакция на событие соединения: запрос на разрыв соединения (Ctrl+D)
public void connectionLogoutRequest(ConnectionEvent ce) {
termIO.write("*** Connection logout request" + CRLF);
termIO.flush();
}
// Реакция на событие соединения: разрыв соединения
public void connectionBroken(ConnectionEvent ce) {
termIO.write("*** Connection broken" + CRLF);
termIO.flush();
}
// Реакция на событие соединения: сигнал NVT BREAK
public void connectionSentBreak(ConnectionEvent ce) {
termIO.write("*** Connection break" + CRLF);
termIO.flush();
}
private Connection connection;
private TerminalIO termIO;
private final String CRLF = BasicTerminalIO.CRLF;
}
Конфигурационный файл для нашего примера описывает параметры ведения протоколов работы TelnetD, перечни известных терминалов, оболочек и слушателей с указанием реализующих их классов и параметров:
#=====================================================
# Параметры системных журналов
#=====================================================
syslog=on
syslog.media=terminal
syslog.stampformat=[yyyy-MM-dd hh:mm:ss z]
syslog.path=
#=====================================================
# Параметры отладочных журналов
#=====================================================
debuglog=off
debuglog.media=terminal
debuglog.stampformat=[yyyy-MM-dd hh:mm:ss z]
debuglog.path=
#=====================================================
# Терминалы
#=====================================================
# Известные терминалы
terminals=vt100,ansi,windoof,xterm
# Классы терминалов
term.vt100.class=net.wimpi.telnetd.io.terminal.vt100
term.ansi.class=net.wimpi.telnetd.io.terminal.ansi
term.windoof.class=net.wimpi.telnetd.io.terminal.Windoof
term.xterm.class=net.wimpi.telnetd.io.terminal.xterm
# Алиасы терминалов
term.vt100.aliases=default,vt100-am,vt102,dec-vt100
term.ansi.aliases=color-xterm,xterm-color,vt320,vt220,linux
term.windoof.aliases=
term.xterm.aliases=
#=====================================================
# Оболочки
#====================================================
# Известные оболочки
shells=myShell
#-------------------------------------------
# Параметры оболочки myShell
#-------------------------------------------
# Класс оболочки
shell.myShell.class=telnetdtest.Test1Shell
#=====================================================
# Слушатели
#=====================================================
# Известные слушатели
listeners=myListener
#-------------------------------------------
# Параметры слушателя myListener
#-------------------------------------------
# Порт, на котором слушатель ожидает соединения
myListener.port=7241
# Максимальное количество запросов на соединение (защита от флуда)
myListener.floodprotection=5
# Максимальное количество активных соединений
myListener.maxcon=10
# Максимальное количество соединений, ожидающих активизации
myListener.maxqueued=0
# Время бездействия соединения
myListener.time_to_warning=300000
# Время таймаута соединения
myListener.time_to_timedout=100000
# Периодичность проверки бездействия и таймаута
myListener.housekeepinginterval=1000
# Режим ввода
myListener.inputmode=character
# Имя оболочки, запускаемой слушателем
myListener.loginshell=myShell
# Класс фильтра соединений
myListener.connectionfilter=
Теперь создадим программу, которая запустит наш Telnet-сервер:
package telnetdtest;
import java.io.*;
import java.util.*;
import net.wimpi.telnetd.*;
public class Test1 {
public static void main(String[] args)
throws Exception {
System.out.println("TelnetD testbed 1");
// Загружаем настройки TelnetD из файла
Properties settings = new Properties();
settings.load(new FileInputStream("test1.properties"));
// Создаем сервер
TelnetD daemon = TelnetD.createTelnetD(settings);
// Разрешаем серверу работать
daemon.setServing(true);
}
}
Интерфейс пользователя
В предыдущем примере для организации общения с пользователем применялись возможности посимвольного ввода, которых явно недостаточно для реальных приложений.
Пакет net.wimpi.telnetd.io.toolkit предоставляет инструменты для организации более развитого диалога с пользователем. В число этих инструментов входят как активные (взаимодействующие с пользователем каким-либо образом), так и пассивные (только лишь отображающие что-либо) компоненты.
Активные компоненты представлены однострочным (класс Editfield) и многострочным (класс Editarea) редакторами, меню (класс Selection), флажком (класс Checkbox) и пейджером (класс Pager). Для работы с активным компонентом необходимо создать нужный объект, вызвать соответствующие методы для настройки параметров, вызвать метод run(), приводящий к активизации компонента, и, наконец, вызвать метод getValue() для получения результатов работы компонента.
Пассивные компоненты – это метка (класс Label), строки заголовка (класс Titlebar) и статуса (класс Statusbar). Для пассивного компонента после создания и настройки параметров необходимо вызывать метод draw(), что приведет к отображению компонента.
Посмотрим некоторые из этих компонентов в деле. Для этого изменим код метода run(…) в интерпретаторе из первого примера. Теперь он выглядит так:
public void run(Connection con) {
connection = con;
termIO = (TerminalIO) connection.getTerminalIO();
// Регистрируем этот объект в качестве обработчика событий соединения
this.connection.addConnectionListener(this);
while (true) {
// Очищаем экран
termIO.eraseScreen();
// Создаем и отображаем строки заголовка и статуса
Titlebar title = new Titlebar(termIO, "");
title.setTitleText("TelnetD testbed 2");
title.setForegroundColor(ColorHelper.YELLOW);
title.setBackgroundColor(ColorHelper.BLUE);
title.setAlignment(Titlebar.ALIGN_CENTER);
title.draw();
Statusbar status = new Statusbar(termIO, "");
status.setStatusText("TelnetD version: "
+ TelnetD.getReference().getVersion());
status.setForegroundColor(ColorHelper.YELLOW);
status.setBackgroundColor(ColorHelper.BLUE);
status.setAlignment(Statusbar.ALIGN_RIGHT);
status.draw();
// Выводим подсказку к меню
termIO.homeCursor();
termIO.moveCursor(BasicTerminalIO.DOWN, 5);
termIO.write(CRLF + "Use arrow keys to select "
+ "command, [Enter] or [Tab] to run command"
+ CRLF + "Command:" + CRLF);
termIO.flush();
// Создаем меню выбора и активизируем его
Selection menu = new Selection(termIO, "");
menu.addOption("Editfield demo");
menu.addOption("Editarea demo");
menu.addOption("Exit");
menu.addOption("Shutdown");
menu.run();
// Получаем выбор пользователя
int cmd = menu.getSelected();
// Реагируем соответствующим образом
switch (cmd) {
case 0: // демонстрация возможностей однострочного редактора
termIO.eraseScreen();
termIO.homeCursor();
termIO.write("Editfield demo" + CRLF
+ "--------------" + CRLF);
final int MAX_EDITFIELD_CHARS = 20;
// Выводим подсказку к редактору
termIO.write(CRLF + "Type any text (max "
+ MAX_EDITFIELD_CHARS + " chars), press "
+ "[Enter] or [Tab] to finish typing"
+ CRLF + "Text:" + CRLF);
termIO.flush();
// Создаем однострочный редактор
Editfield editfield = new Editfield(termIO,
"", MAX_EDITFIELD_CHARS);
// Ждем ввода от пользователя
editfield.run();
// Показываем пользователю результат
termIO.write(CRLF + "Your input: " + CRLF
+ editfield.getValue() + CRLF);
termIO.flush();
break;
case 1: // демонстрация возможностей многострочного редактора
termIO.eraseScreen();
termIO.homeCursor();
termIO.write("Editarea demo" + CRLF
+ "-------------" + CRLF);
final int MAX_EDITAREA_ROWS = 4;
// Выводим подсказку к редактору
termIO.write(CRLF + "Type any text (max "
+ MAX_EDITAREA_ROWS + " rows), press [Tab] "
+ "to finish typing" + CRLF + "Text:" + CRLF);
termIO.flush();
// Создаем многострочный редактор
Editarea editarea = new Editarea(termIO, "",
MAX_EDITAREA_ROWS, MAX_EDITAREA_ROWS);
// Ждем ввода от пользователя
editarea.run();
// Показываем пользователю результат
termIO.write(CRLF + "Your input: " + CRLF
+ editarea.getValue() + CRLF);
termIO.flush();
break;
case 2: // завершение сеанса
connection.close();
return;
case 3: // останов сервера
TelnetD.getReference().setServing(false);
TelnetD.getReference().shutdown();
System.exit(0);
return;
}
// Ждем, пока пользователь нажмет любую клавишу
termIO.write(CRLF + "Press any key");
termIO.flush();
termIO.read();
}
}
Заключение
Итак, используя TelnetD, можно с минимальными затратами обеспечить возможность удаленного управления Java-приложением по протоколу Telnet.
Здесь, однако, следует отметить, что протокол Telnet передает все данные «открытым текстом», а значит, использование его в публичных сетях небезопасно. В качестве альтернативы протоколу Telnet для применения в публичных сетях можно предложить нестандартные расширения протокола Telnet или протокол SSH.
Ссылки:
- http://www.faqs.org/rfcs/rfc854.html – спецификация протокола Telnet.
- http://telnetd.sourceforge.net – домашняя страница TelnetD.
- http://ant.apache.org – домашняя страница Jakarta Ant.
- http://www.pvv.ntnu.no/~asgaut/crypto/thesis/thesis.html – вариант модификации протокола Telnet с целью повышения безопасности.
- http://www.ietf.org/html.charters/secsh-charter.html – материалы рабочей группы IETF Secure Shell по стандартизации протокола SSH.
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|