Навеяло комментариями: у людей стойкая ассоциация между Perl как языком и CGI как технологией, использовавшейся в начале веб‑времен.
Всё логично: на тот момент Perl был одним из немногих распространенных скриптовых языков, и естественно что на нем делать CGI‑скрипты было удобнее, чем на shell, например.
Но это не означает что одно к другому было гвоздями прибито.
А вообще технология была по‑своему хорошая: установили вебсервер (как правило — Apache), настроили каталог, из которого запускаются скрипты — и запускай что хочешь.
Причем в отличии от более поздних сложных вебсистем — это был буквально запуск программы на сервере, точно так же, как вы бы запустили ее через терминал или из консоли. Все, что передавалось в программу через HTTP — она получала через STDIN, всё что она выводила в STDOUT — прилетало клиенту в браузер.
По сути обычная консольная неинтерактивная программа, чистый REST API — сервер всегда начинает выполнение программы с чистого листа, и программа работает с тем что ей передали.
Минус был в том, что на запуск программы всегда нужно какое‑то время, особенно если она написана на скриптовом языке, требующем обработки интерпретатором.
Для разовых запросов это не критично, но когда программа стартует хотя бы 0.5 секунды, а у вас идет 100 запросов в секунду — всё начинает немного тормозить.
Поэтому позже появилась технология FastCGI, когда программа «подвешивалась» в пред‑запущенном состоянии и ждала данных, а потом и вовсе перешли на встроенные сервера с многопоточностью.
А сейчас покажу, как можно использовать это в наши дни:
Как уже писал в предыдущей статье — настроил себе «универсальный блок доступа»: компьютер‑одноплатник с маршрутизирующим прокси‑сервером, который отделяет агнцев от козлищ, мух от котлет, а больных от здоровых.
Но, как и любая программа — все эти навороты могут потребовать перезапуска. К тому же домашний интернет сам по себе штука нестабильная, то дерево на провода упало, то экскаватор что‑то не то выкопал, пойди пойми почему вдруг перестали грузиться котики.
Надо понимать что произошло.
Конечно, всегда можно зайти по ssh, набрать в консоли несколько заклинаний и всё узнать, но можно же это облегчить?
При этом писать отдельную вебсистему, которая будет всё проверять — как‑то черезчур.
Итак, ТЗ:
-
имеются несколько программ, запускаемых в фоне
-
имеются методы проверки, работает ли программа или «зависла»
-
требуется сделать простенькую вебстраницу с текущим статусом и с возможностью перезапуска программ.
-
поскольку все это глубоко внутри локальной сети — авторизация не требуется, но и поломать кривыми ручонками никто ничего не должен.
Устанавливаем на этом сервере Apache (Nginx без плясок с бубном простой CGI не умеет):
apt install apache2
После установки там всё работает «из коробки», нужно только сделать симлинк на cgi-модуль, по умолчанию он отключен:
cd /etc/apache2/mods-enabled
ln -s ../mods-available/cgi.load .
/etc/init.d/apache2 restart
Готово. По умолчанию скрипты CGI должны лежать в /usr/lib/cgi-bin/, а через веб доступны как /cgi-bin/*.cgi
Разумеется можно поменять, особенно если устаревшее «cgi-bin» глаз режет — но сейчас мы его и не увидим. И разумеется, сразу в этом каталоге пусто.
Создаем первый файл, env.cgi:
#!/bin/sh echo "Content-Type: text/html"; echo "" echo "<pre>" env echo "</pre>" id
chmod 755 env.cgi
Заходим браузером на сервер:
http://xx.xx.xx.xx/cgi-bin/env.cgi
GATEWAY_INTERFACE=CGI/1.1 REMOTE_ADDR=XX.XX.XX.XX QUERY_STRING= HTTP_USER_AGENT=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 DOCUMENT_ROOT=/var/www/html REMOTE_PORT=40472 HTTP_UPGRADE_INSECURE_REQUESTS=1 HTTP_ACCEPT=text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 SERVER_SIGNATURE= Apache/2.4.62 (Debian) Server at 10.1.0.4 Port 80 .... uid=33(www-data) gid=33(www-data) groups=33(www-data)
Никаких сложных программ, новых или старых языков, фреймворков, ничего, простой shell-скрипт.
Тут интересна строка QUERY_STRING= и нижняя uid=33(www-data) gid=33(www-data) groups=33(www-data)
В QUERY_STRING= помещается строка, которая будет введена в адресе после имени скрипта:
http://XX.XX.XX.XX/cgi-bin/env.cgi?blablabla -> QUERY_STRING=blablabla
По-хорошему это должны быть urlencoded-параметры GET запроса, но по факту вообще не имеет значения что за строка там будет, лишь бы не нарушать соглашения по символам.
В данном случае туда можно отправлять единственную строку, указывающую на то, с какой именно программой должен работать наш скрипт.
А нижняя строка, результат команды id — показывает, что вебсервер работает под пользователем www-data.
Для проверки работоспособности интернета и различных прокси-вариантом можно использовать стандартный curl:
Запрос «напрямую» даст нам внешний IP, под которым нас видно в интернете:
curl http://v4v6.ipv6-test.com/api/myip.php ‑silent
Запрос через sock5-прокси даст на м внешний IP, под которым нас видно через прокси, а также проверит работоспособность самого прокси:
curl -x socks5h://127.0.0.1:1080 http://v4v6.ipv6-test.com/api/myip.php —silent
Остальные — по аналогии.
Можно сделать все проверки в одном скрипте и вернуть в JSON‑формате, но некоторые запросы могут отрабатывать долго, и скрипт будет ждать их все. Это не очень удобно, поэтому скрипт будет всё‑таки один, но что именно он проверяет — будет зависить от параметра запроса.
Таким образом, получаем скрипт check_one.cgi
#!/bin/sh q=$QUERY_STRING # все остальное удалим unset $(env | cut -d= -f1) PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH # обязательный заголовок echo "Content-Type: application/json" echo "" # к параметру добавляем "x" чтобы избежать пустых строк if [ "x$q" = "xxray" ] ; then # тут запрашиваем ответ сервера, нам дадут IP адрес INETIP=$( curl http://v4v6.ipv6-test.com/api/myip.php --silent ) PROXYIP=$( curl -x socks5h://127.0.0.1:1080 http://v4v6.ipv6-test.com/api/myip.php --silent ) echo "{\"extip\":\"${INETIP}\",\"proxyip\":\"${PROXYIP}\"}" elif [ "x$q" = "xi2p" ] ; then X=1 # тут и далее - важен сам факт ответа, поэтому только заголовки и код ошибки curl -x socks5h://127.0.0.1:4447 http://flibusta.i2p -I --silent -o /dev/null if [ $? -ne 0 ] ; then X=0 fi echo "{\"i2p\":${X}}"; elif [ "x$q" = "xnodpi" ] ; then X=1 curl -x socks5h://127.0.0.1:1081 https://jnn-pa.googleapis.com -I --silent -o /dev/null if [ $? -ne 0 ] ; then X=0 fi echo "{\"nodpi\":${X}}"; elif [ "x$q" = "xproxy" ] ; then X=1 curl -x socks5h://127.0.0.1:6007 http://v4v6.ipv6-test.com/api/myip.php -I --silent -o /dev/null if [ $? -ne 0 ] ; then X=0 fi echo "{\"proxy\":${X}}"; else # чтобы не забыть что можно указывать echo "{\"options\":[\"proxy\",\"xray\",\"i2p\",\"nodpi\"]}" fi
Теперь, указывая те или иные опции, получим ответ, работает тот или иной маршрут или нет.
Перезапуск сервисов можно сделать так:
Поскольку вебсервер работает под юзером www-data — прав перезапускать всё что хочется у него нет, и давать их ему мы не будем.
Вместо этого запустим сервисы конкретно под ним, и так, чтобы они перестартовывали сами.
Делаем типовые скрипты наподобие:
#!/bin/sh # exec > /dev/null exec 2>&1 cd /tmp while [ 1 ] ; do /usr/local/etc/xray/xray -c /usr/local/etc/xray/config.json done
Программа xray запускается не в фоне, поэтому если убить процесс, или он сам умрет — бесконечный цикл в скрипте запустит его снова. Таким образом страхуемся от нештатных падений, а в случае зависания достаточно просто убить зависший процесс, прав на это хватит.
Полностью аналогично — для других процессов.
Остается запустить процессы. Для этого удобно использовать скрипт /etc/rc.local (в данном случае он работает, если нет — ну в другое место добавить, в /etc/init.d например)
su www-data -s /bin/sh -c 'setsid /usr/local/bin/start_xray &'
Параметр -s /bin/sh нужен потому, что у пользователя www-data своего шелла нет (см. vipw)
Параметр -с запускает команду, в данном случае setsid, который запускает скрипт запуска (масло масляное на масле) демоном.
Остальные — по аналогии. После рестарта компьютера запускаются стартовые скрипты, которые запускают программы, которые теперь работают под юзером, и в случае их падения или убийства автоматически рестартуют.
Единственный нюанс с i2pd — он ищет конфиги в домашнем каталоге, в ~/.i2pd, а для данного юзера это каталог /var/www (снова см. vipw), именно там должны лежать настройки и там должны быть права доступа на запись в каталог настроек.
Остается написать скрипт-киллер:
#!/bin/sh q=$QUERY_STRING # все остальное удалим unset $(env | cut -d= -f1) PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH echo "Content-Type: text/html" echo "" pid= if [ "x$q" = "xxray" ] ; then pid=$(ps ax| grep -v grep | grep 'xray/xray' | awk '{ print $1 }') if [ -n "$pid" ] ; then echo $pid kill $pid sleep 3 fi elif [ "x$q" = "xi2p" ] ; then pid=$(ps ax| grep -v grep | grep '/i2pd' | awk '{ print $1 }') if [ -n "$pid" ] ; then echo $pid kill $pid sleep 3 fi elif [ "x$q" = "xnodpi" ] ; then pid=$(ps ax| grep -v grep | grep '/ciadpi' | awk '{ print $1 }') if [ -n "$pid" ] ; then echo $pid kill $pid sleep 3 fi else echo "{\"options\":[\"xray\",\"i2p\",\"nodpi\"]}" fi
ps ax дает список процессов, первый «grep» убирает из выдачи сам grep, второй «grep» ищет искомую программу, awk вытаскивает PID, kill его убивает, sleep для того, чтобы чуть подождать пока процесс умрет.
В общем практически всё, остается изобразить страничку:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>HTML gate</title> <style type="text/css"> body, html { margin: 0; padding: 0; font-family: Arial, sans-serif; height: 100%; } .placeholder { display:inline-block; width:10em; } .txt_error { color:#ff0000; font-weight:bold; } .txt_ok { color:#00ff00; font-weight:bold; } .hero { height: 100vh; background-image: url('bg-image.jpeg'); background-color: black; background-position: center; background-repeat: no-repeat; background-size:cover; color: white; box-sizing: border-box; } #status { padding:2em; } #buttons { display:flex; align-items: center; justify-content: center; gap: 5em; padding-top: 300px; } #buttons button, #check { padding: 15px 35px; font-size: 20px; border: solid 3px #ff8b00; background: #00003370; color: #ffc800; border-radius: 10px; cursor:pointer; } .disabled { color:#736e5f!important; border: solid 3px #736e5f!important; cursor:not-allowed!important; } </style> </head> <body> <div class="hero" id="head"> <div id="status"> <table> <tr> <td>External IP</td> <td><span id="extip" class="placeholder"><span class="loading">...</span></span></td> </tr> <tr> <td>Proxy IP</td> <td><span id="proxyip" class="placeholder"><span class="loading">...</span></span></td> </tr> <tr> <td>I2P network</td> <td><span id="i2p" class="placeholder"><span class="loading">...</span></span></td> </tr> <tr> <td>Proxy</td> <td><span id="proxy" class="placeholder"><span class="loading">...</span></span></td> </tr> <tr> <td>NoDPI</td> <td><span id="nodpi" class="placeholder"><span class="loading">...</span></span></td> </tr> </table> <button onclick="check()" id="check">Check</button> </div> <div id="buttons"> <button onclick="kill(this,'xray')">Restart Xray</button> <button onclick="kill(this,'i2p')">Restart i2p</button> <button onclick="kill(this,'nodpi')">Restart NoDPI</button> </div> </div> <script> function kill(ctl,id){ console.log(ctl, id); ctl.disabled=true; ctl.classList.add('disabled'); fetch('/cgi-bin/kill_one.cgi?'+id) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { window.setTimeout(check,10000); ctl.disabled=false; ctl.classList.remove('disabled'); check(); }) .catch(error => { console.error('Error fetching data:', error); ctl.disabled=false; ctl.classList.remove('disabled'); }); } function check(){ const urls = [ 'xray', 'i2p', 'proxy', 'nodpi' ]; const labels = document.querySelectorAll('.placeholder'); labels.forEach(item => { item.innerHTML = '<span class="loading">...</span>'; }); urls.forEach(url => { fetch('/cgi-bin/check_one.cgi?'+url) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { Object.entries(data).forEach(([key, value]) => { const element = document.getElementById(key); if (element) { if(value == 1){ element.classList.remove('txt_error'); element.classList.add('txt_ok'); element.textContent = 'OK'; }else if(value == 0){ element.classList.add('txt_error'); element.classList.remove('txt_ok'); element.textContent = 'ERROR'; }else{ element.classList.remove('txt_error'); element.classList.add('txt_ok'); element.textContent = value; } } }); }) .catch(error => { console.error('Error fetching data:', error); }); }); } check(); </script> </body> </html>
Чистое админство, shell и немного javascript — и получился вполне работающий веб-интерфейс.
А если в начинку не лезть — то и не видно, что это древний допотопный CGI.
ссылка на оригинал статьи https://habr.com/ru/articles/872596/
Добавить комментарий