Глава 3: Открытие удаленного терминала


Сайт создан в системе uCoz
Вернуться к содержанию

 

Открытие удаленного терминала

Мы уже упоминали, что отдельное соединение по SSH может быть разделено между несколькими "channels" (прим.перевод. т.е. каналами), которые могут использоваться для различных целей.

В этой главе рассказывается о том, как открывать один из таких каналов и использовать для удаленного запуска интерпретатора команд (прим.перевод. в обиходе консоли или терминала).

Открытие и закрытие канала

Функция ssh_channel_new() создает канал и возвращает переменную типа ssh_channel.

После создания канала открывается сессия SSH путем вызова функции ssh_channel_open_session().

Когда надобность в канале отпадет, необходимо отправить EOF (прим.перевод. end-of-file - символ конца файла) путем вызова функции sh_channel_close(). Сам же канал следует удалять функцией ssh_channel_free().

Ниже приводится пример операции открытия и закрытия канала

int shell_session(ssh_session session)
{
ssh_channel channel;
int rc;
channel = ssh_channel_new(session);
if (channel == NULL)
return SSH_ERROR;
rc = ssh_channel_open_session(channel);
if (rc != SSH_OK)
{
ssh_channel_free(channel);
return rc;
}
...
ssh_channel_close(channel);
ssh_channel_send_eof(channel);
ssh_channel_free(channel);
return SSH_OK;
}

Интерактивная и не интерактивная сессия

К интерактивной относится сессия, если используется командный интерпретатор в котором пользователь вводит одну команду за другой. В то же время, к не интерактивной приравнивается запуск команд в фоновом режиме без управляющего терминала.

Если планируется использовать интерактивную сессию, необходимо создать псеводотерминал на удаленной стороне, который обычно называют сокращенно "pty" от "pseudo-teletype"(прим.перевод. псевдо-телетайп). При этом удаленные процессы не видят разницы при работе с текст-ориентированным терминалом.

Если необходимо, запрашиваем "pty" при помощи функции ssh_channel_request_pty(), а размерность псевдотерминала устанавливаем (в строках и столбцах) функцией ssh_channel_change_pty_size().

Вне зависимости от выбранного типа сессии (интерактивной или не интерактивной), следующим шагом запрашиваем командную оболочку при помощи функции ssh_channel_request_shell().

int interactive_shell_session(ssh_channel channel)
{
int rc;
rc = ssh_channel_request_pty(channel);
if (rc != SSH_OK) return rc;
rc = ssh_channel_change_pty_size(channel, 80, 24);
if (rc != SSH_OK) return rc;
rc = ssh_channel_request_shell(channel);
if (rc != SSH_OK) return rc;
...
return rc;
}

Отображение передаваемых данных удаленным компьютером

Обычно программе необходимо принимать все данные "отображаемые" pty, потому что существует необходимость в анализе, регистрации или отображение этих данных.

ssh_channel_read() и ssh_channel_read_nonblocking() являются простым способом для чтения данных из канала.

Пример ниже показывает как ждать данных от удаленной стороны с помощью ssh_channel_read():

int interactive_shell_session(ssh_channel channel)
{
int rc;
char buffer[256];
int nbytes;
 
int stdout_fd = fileno(sdout);
 
rc = ssh_channel_request_pty(channel);
if (rc != SSH_OK) return rc;
rc = ssh_channel_change_pty_size(channel, 80, 24);
if (rc != SSH_OK) return rc;
rc = ssh_channel_request_shell(channel);
if (rc != SSH_OK) return rc;
while (ssh_channel_is_open(channel) &&
!ssh_channel_is_eof(channel))
{
nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
if (nbytes < 0)
return SSH_ERROR;
if (nbytes > 0)
write(stdout_fd, buffer, nbytes);
}
return rc;
}

В отличии от ssh_channel_read(), ssh_channel_read_nonblocking() никогда не ждет прихода данных от удаленного процесса и возвращается немедленно.

Если ssh_channel_read_nonblocking() используется в цикле, необходимо использовать вместе с ней usleep(3). Иначе, использующий её процесс будет занимать все процессорное время (прим.перевод. в обиходе зацикливание), а компьютер, на котором он запущен, встанет.

Передача пользовательского ввода удаленному компьютеру

Ввод пользователя передается на удаленную сторону с помощью функции ssh_channel_write().

Следующий пример показывает как комбинируется неблокирующее чтение канала SSH и ожидание ввода символов с клавиатуры. После локального ввода производится отправка на удаленный компьютер:

/* Под Linux, данная функция определяется всякий раз, когда требуется определять нажата клавиша
или нет. Под Windows,является стандартной и не требует определения
*/
int kbhit()
{
struct timeval tv = { 0L, 0L };
fd_set fds;
FD_ZERO(&fds);
FD_SET(0, &fds);
return select(1, &fds, NULL, NULL, &tv);
}
/* Достаточно простой эмулятор терминала:
- печать данных принимаемых от удал. компа
- отправка клав. ввода на удал. комп
*/
int interactive_shell_session(ssh_channel channel)
{
/* Инициализация терминала и сессии пропущены */
...
char buffer[256];
int nbytes, nwritten;
while (ssh_channel_is_open(channel) &&
!ssh_channel_is_eof(channel))
{
nbytes = ssh_channel_read_nonblocking(channel, buffer, sizeof(buffer), 0);
if (nbytes < 0) return SSH_ERROR;
if (nbytes > 0)
{
nwritten = write(1, buffer, nbytes);
if (nwritten != nbytes) return SSH_ERROR;
}
if (!kbhit())
{
usleep(50000L); // 0.05 second
continue;
}
nbytes = read(0, buffer, sizeof(buffer));
if (nbytes < 0) return SSH_ERROR;
if (nbytes > 0)
{
nwritten = ssh_channel_write(channel, buffer, nbytes);
if (nwritten != nbytes) return SSH_ERROR;
}
}
return rc;
}

Конечно, данная реализация является скудным эмулятором терминала, так как нажатие клавиш не должно выполняться локально, а на удаленной стороне. Так же пользовательский ввод не должен дожидаться отправки нажатия клавиши "Enter", а отправлять нажатие каждой клавиши. Это может быть достигнуто за счет установки локального терминала в "raw" режим с помощью функции cfmakeraw(3), которая является стандартной функцией Linux (прим.перевод. из состава библиотеки GNU C (GLIBC) или её варианта EGLIBC), а на других системах её нужно определять:

static void cfmakeraw(struct termios *termios_p)
{
termios_p->c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
termios_p->c_oflag &= ~OPOST;
termios_p->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
termios_p->c_cflag &= ~(CSIZE|PARENB);
termios_p->c_cflag |= CS8;
}

Если не используется локальный терминал, а нечто из окружение графического рабочего стола, решение данных проблем с "echo" будут иметь отличия.

Более продуманный способ получения удаленные данные

Предупреждение ssh_select() и ssh_channel_select() больше не ревалентны, так как в libssh предполагается улучшить быстродействие системы в части асинхронного обмена данными, в будущем этот подраздел будет удален. ***

Функции ssh_channel_read() и ssh_channel_read_nonblocking() являются простыми, но они не адаптированы к приему данных более чем из одного канала SSH или файлового дескриптора. Предыдущий пример показывал получение данных из стандартного ввода (клавиатуры) в тоже время как получение данных из канала SSH было сложным. Функции ssh_select() и ssh_channel_select() обеспечивают более элегантный способ ожидания прихода данных из нескольких источников ввода.

Функции ssh_select() и ssh_channel_select() напоминают стандартную UNIX-функцию select(2). Идея состоит в том, чтобы ждать некоего события: входных данных для чтения, исходящих данных для блокировки или исключающего случая. Обе эти функции "пассивно ждут", таким образом их можно безопасно использовать в цикле бесчисленное количество раз, так как они не будут сжирать процессорное время и проводить к зависанию компьютера. Поэтому эти функции довольно часто используются в пользовательских приложениях оперирующих циклами.

Ниже приводится пример функции, которая ожидает как прихода данных из SSH-канала, так и со стандартного ввода (клавиатуры):

int interactive_shell_session(ssh_session session, ssh_channel channel)
{
/* Иницализация сессии и терминала пропущены */
...
char buffer[256];
int nbytes, nwritten;
while (ssh_channel_is_open(channel) &&
!ssh_channel_is_eof(channel))
{
struct timeval timeout;
ssh_channel in_channels[2], out_channels[2];
fd_set fds;
int maxfd;
timeout.tv_sec = 30;
timeout.tv_usec = 0;
in_channels[0] = channel;
in_channels[1] = NULL;
FD_ZERO(&fds);
FD_SET(0, &fds);
FD_SET(ssh_get_fd(session), &fds);
maxfd = ssh_get_fd(session) + 1;
ssh_select(in_channels, out_channels, maxfd, &fds, &timeout);
if (out_channels[0] != NULL)
{
nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
if (nbytes < 0) return SSH_ERROR;
if (nbytes > 0)
{
nwritten = write(1, buffer, nbytes);
if (nwritten != nbytes) return SSH_ERROR;
}
}
if (FD_ISSET(0, &fds))
{
nbytes = read(0, buffer, sizeof(buffer));
if (nbytes < 0) return SSH_ERROR;
if (nbytes > 0)
{
nwritten = ssh_channel_write(channel, buffer, nbytes);
if (nbytes != nwritten) return SSH_ERROR;
}
}
}
return rc;
}

Использование графических приложений на удаленной стороне

Если запускаемое удаленно приложение является графическим, можно перенаправлять протокол X11 на локальный компьютер.

Для этого, первым что нужно сделать, дать право осуществлять соединения X11 с помощью ssh_channel_accept_x11(). Тогда можно создать туннель для протокола X11 с помощью функции ssh_channel_request_x11().

Следующий код производит инициализацию канала и открытие сессии интерпретатора команд, и параллельно обрабатывает X11-соединения:

int interactive_shell_session(ssh_channel channel)
{
int rc;
ssh_channel x11channel;
rc = ssh_channel_request_pty(channel);
if (rc != SSH_OK) return rc;
rc = ssh_channel_change_pty_size(channel, 80, 24);
if (rc != SSH_OK) return rc;
rc = ssh_channel_request_x11(channel, 0, NULL, NULL, 0);
if (rc != SSH_OK) return rc;
rc = ssh_channel_request_shell(channel);
if (rc != SSH_OK) return rc;
/* Чтение данных отправленных с удаленного компьютера */
...
}

Не забудьте установить переменную окружения $DISPLAY на удаленной стороне или не пытайтесь использовать X11-туннель удаленным приложением:

$ export DISPLAY=:0
$ xclock &

 

Вернуться к содержанию