Навеяло комментариями: у людей стойкая ассоциация между 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
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH
# обязательный заголовок
echo "Content-Type: application/json"
echo ""
# к параметру добавляем "x" чтобы избежать пустых строк
if [ "x$QUERY_STRING" = "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$QUERY_STRING" = "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$QUERY_STRING" = "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$QUERY_STRING" = "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
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH
echo "Content-Type: text/html"
echo ""
pid=
if [ "x$QUERY_STRING" = "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$QUERY_STRING" = "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$QUERY_STRING" = "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.
lexore
vipw, /etc/rc.local, Apache... повеяло чем-то теплым и ламповым.
Попробуйте дернуть файл https://ваш_сайт/.i2pd