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

Как вы могли заметить, у меня так и не удается писать сюда о чём-нибудь интересном. Связано это с тем, что, в последнее время, в моей «кодерской» жизни особо ничего не происходит, ибо я занят ничегонеделучёбой.

Тем не менее, на днях я вспомнил один из своих проектов, код которого представлял из себя один большой костыль.

Встречайте, Remote Web Control — программа для удаленного управления компьютера с операционной системой Debian/Ubuntu посредством веб-интерфейса.

Логотип Remote Web Control
Логотип RWC

Описание проекта

Данный проект, на самом деле, был написан в качестве курсовой работы мною на первом курсе.

Вообще, стоит отметить, обычно я выбираю для «курсачей» именно такие темы, которые бы, в другом случае, я, скорее всего, забросил бы. То есть что-то масштабное и интересное.

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

После запуска, пользователь видит IP адрес и порт и, используя эти данные, может управлять компьютером с любого современного браузера.

Хочу сразу заметить, что в качестве ОС для сервера может выступать только Debian (и, скорее всего, Ubuntu). На остальных UNIX-системах не тестилось, а под Windows точно не заработает.

Функционал

  • Авторизация в интерфейсе (по идее, должно было быть разграничение прав, ибо соответствующие столбцы в базе данных есть, но я это недореализовал)
  • Эмуляция терминала — всё, что вводит в него пользователь, отправляется на исполнение управляемому компьютеру. Да, можно ввести rm -rf и всё удал
  • Загрузка файлов — указываешь ссылку для скачивания, а также куда скачать. Всё остальное делает curl на удаленном компьютере.
  • Скриншот — делается скриншот экрана системы и отображается в браузере.
  • Стрим экрана — киллер-фича. Выводится потоковое видео текущего состояния системы и можно даже кликать по нему и печатать в него. Всё отправится в систему. По сути, TeamViewer, только более багнутый и с плохой картинкой.

Технические данные

Так как это был курсовой проект, то я был несколько ограничен в выборе технологий. Основной задачей было написать программу под Linux, используя язык Си (именно его, а не C++).

Но, как вы понимаете, одного Си для такой системы было бы мало. В итоге, список используемых технологий получился следующим:

Веб-сервер — Си + сторонняя библиотека uthash для хэш-массивов

Веб-интерфейс — HTML/CSS/JS ( + Bootstrap и JSTerminal)

Связка интерфейс-сервер — JS (AJAX)

Трансляция видео-потока — shell-скрипт для запуска ffmpeg, JS и xdotool на сервере

Количество строк кода:

  • C – около тысячи строк кода
  • JavaScript – около 300 строк кода
  • shell – около 15 строк кода

Принцип работы

Вот тут и начинается самое интересное.

Сделать веб-сервер, отдающий HTML, оказалось просто. В интернете довольно много примеров кода, которые, используя только стандартные библиотеки, позволяют «слушать» определенный порт и отдавать запрошенный файл.

Однако, этого недостаточно. Потребовалось еще как-то обрабатывать GET-запросы. Как известно, они передаются прямо в URL. Например, в запросе:

example.com/index.html?user=test&password=123 

user и password будут являться GET-параметрами с соответствующими им значениями (test и 123).

Самый очевидный способ их обрабатывать — это разделять запрос сначала по '?':

 1 => example.com/index.html
 2 => user=test&password=123 

Затем всё, что после этого знака, разделять по '&':

1 => user=test
2 => password=123 

И, в заключительном шаге, формировать, например, хеш-массив, разделяя каждый элемент полученного выше результата по '=':

'user' => 'test'
'password' => '123' 

Больше я промучился с реализацией «сплита» по двум разделителям сразу. Для этого была написана (не без использования интернета) обертка для функций strstr и strtok_r. Потом из этого всего формировался хэш-массив.

Затем, в зависимости от ключа action в нём, выполнялась соответствующая команда (движение мыши, запуск потока и т.п.).

А костыли?

Они в каждой строчке кода

Выполнение команд

Самый главный, пожалуй, костыль — файл execute.html. Именно к нему клиент выполняет все AJAX-запросы (с GET параметрами). И именно в него веб-сервер записывает результаты выполнения команд (в особенности, результаты того, что вводишь в эмуляторе терминала), а клиент, снова через AJAX, берет из него эти результаты и выводит в интерфейс.

Например, авторизация:

if (Login(SLogin->value, SPass->value) != 0)
{
 printf(«Пользователь %s успешно авторизировался\n», SLogin->value);
WriteTextToFile(«webQ/execute.html», «login_success», «w»);
}

И выполнение команд, введенных в терминал, и возвращение результата:

snprintf(buf, sizeof buf, «%s 2>&1», UrlDecode(command));
 
// в файле execute.html хранится всё то, что выводит программа после выполнения команды
// веб-интерфейс, посредством javascript, а также ajax, получает контент этого файла и выводит в
// симулированный терминал на веб-странице
WriteTextToFile(«webQ/execute.html», "", «w»);
 
fp = popen(buf, «r»);
if (fp == NULL)
{ ... }
while (fgets(path, sizeof(path) — 1, fp) != NULL)
{
WriteTextToFile(«webQ/execute.html», path, «a»);
}

Приводить пример кода на JS не буду, ибо там всё очевидно.

Видео-поток

Трансляция видео и управление компьютером через него тоже оказалось не совсем тривиальной задачей.  Сразу же возникала проблема несоответствия координат клика мышки на клиенте и сервере. Связано это было, очевидно, с тем, что размер передаваемой картинки в разы меньше исходной.

В следствии этого, нужно было их пересчитывать. Для этого, сначала клиент передавал следующие команды (опять же, всё посредством AJAX): echo $(xdpyinfo | grep dimensions | awk '{print $2}' | cut -d 'x' -f1)
echo $(xdpyinfo | grep dimensions | awk '{print $2}' | cut -d 'x' -f2)

Потом результаты (а это ширина и высота экрана удаленного компьютера) записывались в соответствующие переменные и вычислялся коэффициент, на который нужно умножать координаты. Затем, с помощью нехитрой формулы, вычислялись итоговые X и Y:

OFFSET_WIDTH = REMOTE_WIDTH / MAX_WIDTH;
OFFSET_HEIGHT = REMOTE_HEIGHT / MAX_HEIGHT;
var x = Math.floor((e.pageX —  $(this).offset().left) * OFFSET_WIDTH);
var y = Math.floor((e.pageY —  $(this).offset().top) * OFFSET_HEIGHT);

И на последней стадии данные координаты, а также тип клика (ПКМ, ЛКМ и т.п.) передавались на сервер, а он, в свою очередь, выполнял на системе команду xdotool.

// отправляем утилите xdotool команду для передвижения мыши
snprintf(buf, sizeof buf, «xdotool mousemove %s %s», x, y);
system(buf);

Где-нибудь еще?

Да, были еще некоторые, связанные, например, со скачиванием файла, но они не такие интересные.

Баги

Их было множество, но, увы, почти всё забыл уже. Тем не менее, попробую некоторые перечислить.

  • Сервер иногда не перезапускался, так как порт не всегда освобождался корректно. Для этого приходилось менять на другой, либо ждать какое-то время, пока ОС сама его освободит
  • Передача клавиатурных нажатий в видео-потоке не работает с комбинациями клавиш, а также передает всё большими буквами (стало лень исправлять)
  • Хоть эмулятор терминала и назывался терминалом, он таковым не является, ибо он не сохраняет состояние. То есть, перемещаться посредством cd не выйдет
  • Некоторые замудренные команды, использующие определенные символы, не выполнялись, ибо сервер некорректно обрабатывал эти символы
  • Скриншот системы показывался, но на нём не были скрыты некоторые окна. Видимо, связано с особенностью работы утилиты import
  • Авторизация была чисто условной. Никто не мешает отправлять запросы к файлу execute.html сразу, без интерфейса

Скриншоты

Эмулятор терминала
Эмулятор терминала
Трансляция видео
Трансляция видео
Загрузка файлов
Загрузка файлов
Скриншот системы
Скриншот системы

Видео

Итог

В общем, это был очень классный опыт. До сих пор помню те бессонные ночи, когда я, пытаясь реализовать ту или иную функцию, ругался на низкоуровневость Си.

Не смотря на то, что практическая польза от данного проекта мала (слишком много багов и, думаю, уже есть конкуренты на этом рынке), я доволен результатом. Возможно, если бы я, как сначала собирался, переписал это под более подходящую технологию (даже под тот же C++), то может и получилось бы этому проекту выстрелить, но, увы, я оказался слишком ленив для этого.