Навеяло комментариями: у людей стойкая ассоциация между 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.

Комментарии (1)


  1. lexore
    11.01.2025 02:44

    vipw, /etc/rc.local, Apache... повеяло чем-то теплым и ламповым.

    Единственный нюанс с i2pd - он ищет конфиги в домашнем каталоге, в ~/.i2pd, а для данного юзера это каталог /var/www

    Попробуйте дернуть файл https://ваш_сайт/.i2pd