Александр Фефелов
В процессе работы над одним 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 нам понадобятся:
n дистрибутив TelnetD, включающий исходные тексты
(файл telnetd-1.0.zip) и документацию (файл telnetd-1.0_docs.zip);
n инструментарий Apache Ant. (В задачи данной
статьи не входит описание работы с Ant. Будем предполагать, что Ant уже
установлен и настроен.)
Развернем дистрибутив и скомпилируем TelnetD,
выполнив в каталоге установки команду:
ant jar
При компиляции будут выданы предупреждения об
использовании устаревшего (deprecated) метода java.lang. Thread.stop(), которые
могут быть безболезненно проигнорированы. В результате компиляции в каталоге build
будет создан файл telnetd.jar. (Обратите внимание на его малый размер. В моем
случае – всего 86060 байт.)
Теперь скопируем все файлы из каталога src\net\ wimpi\telnetd\resources
в каталог 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.
Ссылки:
1. http://www.faqs.org/rfcs/rfc854.html
– спецификация протокола Telnet.
2. http://telnetd.sourceforge.net –
домашняя страница TelnetD.
3. http://ant.apache.org – домашняя страница Jakarta
Ant.
4. http://www.pvv.ntnu.no/~asgaut/crypto/thesis/thesis.html
– вариант модификации протокола Telnet с целью повышения безопасности.
5. http://www.ietf.org/html.charters/secsh-charter.html
– материалы рабочей группы IETF Secure Shell по стандартизации протокола SSH.