REST API сервер на Bash с использованием сокетов и Apache

Всем привет! Ранее рассказывал о том, как создать REST API и Web-сервер на PowerShell для Windows, а также упоминал, что подобный сервер будет работать и в системе Linux, благодаря кроссплатформенной версии PowerShell Core. Пример такой реализации вы можете найти в репозитории dotNET-Systemd-API на GitHub, с помощью данного подхода возможно настроить управление службами Linuxуправляемые системой systemd (используя в системе команды systemctl) на удаленной машине через любой REST-клиент, например, curl. Из явных преимуществ, такой сервер поддерживает обработку нескольких одновременных подключений и видов авторизации благодаря встроенным методам класса .NET HttpListener, где по мимо прочего, используя его в системе Linux возможно комбинировать сразу два языка, а если точнее, все встроенные утилиты операционной системы, такие как grep, sed, awk и т.п. в паре с PowerShell. Безусловно, для подобных целей лучше используются специализированные серверные фреймворки или библиотеки, такие как Flask или Django в Python, но меня не покидала идея реализации похожего сервера, где описание логики будет производиться на языке одного только Bash. Используя любой инструмент, который дает возможность сетевого взаимодействия между сервером и клиентом может послужить отправной точкой в решение поставленной задачи. Из очевидных минусов, придется не только описать обработку входящих HTTP-запросов и соответствующих на них ответов, а так же придумать логику этой обработки, например, проверку авторотационных данных передаваемых в заголовке запроса. Приведу примеры, с помощью которых можно создать такой сервер используя сетевые сокеты netcat, socat и ncat, а также веб-сервера Apache с использованием встроенных модулей.

Netcat

Практически в каждом дистрибутиве Linux присутствует встроенная утилита netcat (или nc), которая позволяет устанавливать TCP/IP и UDP соединения, а также может использоваться для чтения и записи данных через сетевые сокеты, прослушивания портов, отправки файлов и многого другого. Создать и запустить сокет в режиме прослушивания (listen) можно одной командой:

nc -l -w 1 -p 8081

Отправляем запрос подключения на удаленном клиенте, используя curl:

curl http://192.168.3.101:8081/api

Или, используя PowerShell:

Invoke-RestMethod http://192.168.3.101:8081/api

На стороне сервера netcat получаем запросы с следующим содержимым:

GET /api HTTP/1.1
Host: 192.168.3.101:8081
User-Agent: curl/8.4.0
Accept: */*

GET /api HTTP/1.1
Host: 192.168.3.101:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.19045; ru-RU) PowerShell/7.3.7

Первая строка содержит метод запроса GET (используется клиентом по умолчанию, чаще применяется для получения информации, если на стороне сервера не декларируется иначе), конечную точку /api и протокол HTTP версии 1.1. Вторая, адрес сервера, где запущен netcat. Третья строка содержит наименование и версию агента, который совершил подключение. Этих данных достаточно, что бы реализовать базовую обработку запроса, например так:

port=8081
while true; do
  # Принимаем запрос и читаем первую строку с методом и конечной точкой
  request_in=$(nc -l -w 1 -p $port)
  request=$(echo "$request_in" | head -n 1)
  # Забираем метод и конечную точку из запроса клиента
  method=$(echo "$request" | awk '{print $1}')
  endpoint=$(echo "$request" | awk '{print $2}')
  # Проверяем, что запрошенный метод поддерживается и конечная точка подлежит обработке
  if [[ $method == "GET" ]]; then
    if [[ $endpoint == "/api/disk" ]]; then
      # Получаем данные состояни дисков из утилиты операционной системы в формате JSON
      response=$(lsblk -e7 -f --json)
        header="HTTP/1.1 200 OK\nContent-Type: application/json\n\n"
      else
        response="404 Not Found: Endpoint unavailable"
        header="HTTP/1.1 404 Not Found\n\n"
      fi
  else
    response="405 Method Not Allowed: Only supports GET requests"
    header="HTTP/1.1 405 Method Not Allowed\n\n"
  fi
  # Формируем вывод и отправляем ответ клиенту
  echo -e "$header$response" | nc -l -N -p $port -w 1
done

Запускаем код прямо в консоли или из файла (например, server.sh), после чего отправляем запрос от клиента к серверу на конечную точкой /api/disk и ожидаем ответ в формате json, вывод которого в PowerShell будет обработан как объект по умолчанию.

Invoke-RestMethod http://192.168.3.101:8081
$(Invoke-RestMethod http://192.168.3.101:8081/api/disk).blockdevices
$(Invoke-RestMethod http://192.168.3.101:8081/api/disk).blockdevices.children

Содержимое ответа на стороне клиента, отправленный на сервер netcat

Содержимое ответа на стороне клиента, отправленный на сервер netcat

На стороне сервера читаем запрос целиком, вытаскиваем из него нужные данные предоставленные клиентом, проверяем их на наличие доступных методов и конечных точек, на основании полученной информации формируем и отправляем ответ. По мимо этих данных, мы также можем передавать дополнительные параметры в заголовке или теле запроса.

Авторизация

Данные, включая заголовки запроса, полученные на сервере netcat от разных клиентов

Данные, включая заголовки запроса, полученные на сервере netcat от разных клиентов

На скриншоте выше можно заметить строку Authorization: Basic cmVzdDphcGk=, это заголовок запроса, в котором содержится тип авторизации (Basic) и сами авторизационные данные зашифрованные в формате Base64 — это стандарт кодирования двоичных данных при помощи 64 символов ASCII. Используя PowerShell или curl есть как минимум два способа, с помощью которых можно передать эти данные. Напрямую через заголовок запроса, предварительно сконвертировав логин и пароль разделенные символом двоеточия (:), или использовать соответствующий параметр:

$EncodingCred = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("${user}:${pass}"))
$Headers = @{"Authorization" = "Basic ${EncodingCred}"}
Invoke-RestMethod -Headers $Headers -Uri http://192.168.3.101:8081/api/service/cron

curl http://192.168.3.101:8081/api/service/cron -u rest:api

Что бы обработать данные на сервере, нужно считать соответствующий заголовок переданный от клиента, после чего в дополнительном условие сопоставить полученные данные с теми, которые заданы на самом сервер (предварительно также закодированные в формат Base64). Если полученные данные равны, пропускаем по остальным условиям, если нет, возвращаем соответствующую ошибку.

port=8081
# Добавляем параметр проверки, что авторизация должна или не должна использоваться
auth="true"
# Задаем логин и пароль
user="rest"
pass="api"
while true; do
  request_in=$(nc -l -w 1 -p $port)
  request=$(echo "$request_in" | head -n 1)
  method=$(echo "$request" | awk '{print $1}')
  endpoint=$(echo "$request" | awk '{print $2}')
  # Проверяем, что авторизация используется
  if [[ $auth == "true" ]]; then
    # Формируем авторизационные данные на сервере в формате Base64
    cred_server=$(echo -n "$user:$pass" | base64 | tr -d '[:space:]')
    # Забираем авторизационные данные клиента
    cred_client=$(echo "$request_in" | grep "Authorization: Basic" | awk '{print $3}' | tr -d '[:space:]')
    # Проверяем, что данные клиента и сервера равны
    if [[ $cred_server == $cred_client ]]; then
      auth_status="true"
    else
      auth_status="false"
     fi
  fi
  if [[ $auth == "false" ]] || [[ $auth == "true" ]] && [[ $auth_status == "true" ]]; then
    if [[ $method == "GET" ]]; then
      if [[ $endpoint == "/api/disk" ]]; then
        response=$(lsblk -e7 -f --json)
          header="HTTP/1.1 200 OK\nContent-Type: application/json\n\n"
        else
          response="404 Not Found: Endpoint unavailable"
          header="HTTP/1.1 404 Not Found\n\n"
        fi
    else
      response="405 Method Not Allowed: Only supports GET requests"
      header="HTTP/1.1 405 Method Not Allowed\n\n"
    fi
    echo -e "$header$response" | nc -l -N -p $port -w 1
  # Если авторизация используется и данные невалидные, отвечает ошибкой
  else
    echo -e "HTTP/1.1 401 Unauthorized\n\n401 Unauthorized" | nc -l -N -p $port -w 1
  fi
done

Точно таким же образом возможно настроить фильтрацию по ip-адресу или подсети клиента. Пример обработки нескольких конечных точек для остановки и запуска службы (unit systemd), а так же чтения и обработки заголовков запроса можно найти в полной версии скрипта, который опубликован на GitHub. При использовании метода GET через PowerShell версии Core проблем не возникает, сервер netcat обрабатывает каждый запрос корректно, но если использовать метод POST или клиент curl, то при каждом новом обращении к серверу происходит разрыв соединения, и при последующем повторным обращении возвращается ответ на предыдущий запрос. Для решения этой проблемы, может помочь увеличение времени ожидания ответа на стороне клиента, например так: curl --connect-timeout 10 --max-time 30 http://192.168.3.101:8081/api/disk и/или сервера: nc -l -w 10 -p $port, а также использование фоновых потоков. Это не является стабильным решением, иногда ответ мог вернуться сразу, но чаще в ходе экспериментов данная проблема сохранялась.

Разрыв соединения при каждом первом запросе клиента к серверу netcat

Разрыв соединения при каждом первом запросе клиента к серверу netcat

Socat и Ncat

Так как у большинства современных дистрибутивов Linux по соображениям безопасности в утилите netcat отсутствует опция (-e), которая позволяла бы напрямую передавать исполняемый файл для обработки входящих соединений, это подтолкнуло меня на использование других инструментов. С помощью socat можно настроить TCP-сервер так, чтобы он слушал входящие соединения на указанном порту и для каждого отдельного установленного соединения запускал указанный скрипт в новом процессе (используя параметр fork), тем самым позволяя обрабатывать одновременно несколько запросов от разных клиентов. Процесс socat перенаправляет предоставленные HTTP-клиентом данные на входе (stdin) скрипту, которые могут быть прочитаны с помощью команды read с сохранением содержимого в переменные. Вот простой пример (предварительно создаем файл со скриптом touch /tmp/socat-api-server.sh и открываем его на редактирование):

#!/usr/bin/bash
# Получаем запрос от клиента и читаем содержиое запроса из переменных окружения:
read -r REQUEST_METHOD REQUEST_URI REQUEST_HTTP_VERSION
# Проверяем, что метод запроса GET
if [ "$REQUEST_METHOD" = "GET" ]; then
   # Отправляем ответ клиенту с содержимым endpoint
   echo -e "HTTP/1.1 200 OK\nContent-Type: text/plain\n\n"
   echo -e "uri: $REQUEST_URI\n"
else
   echo -e "HTTP/1.1 200 OK\nContent-Type: text/plain\n\n"
   echo -e "method $REQUEST_METHOD not supported\n"
fi

Устанавливаем инструмент в системе (в примере для Ubuntu):

sudo apt install socat

Предоставляем скрипту права на выполнение, и запускаем сервер одной командой:

sudo chmod +x /tmp/socat-api-server.sh
socat TCP4-LISTEN:8081,fork EXEC:/tmp/socat-api-server.sh

Отправка запросов на сервер socat, который возвращает в ответе конечную точку

Отправка запросов на сервер socat, который возвращает в ответе конечную точку

Можно добиться точно такого же результата, используя синтаксис текущего скрипта с инструментом ncat (часть пакета Nmap):

sudo apt install ncat
ncat -l -p 8081 --keep-open --exec /tmp/socat-api-server.sh

Такой подход работает уже стабильно с использованием различных методов, а его конфигурация становится куда проще. Но, так как socat и ncat не поставляются в составе дистрибутива, то их придется устанавливать как зависимость, в таком случае целесообразнее воспользоваться полноценным серверным решением, например, Apache.

Apache

В отличии от предыдущих вариантов, Apache это полноценный веб-сервер, который содержит широкий набор встроенных модулей, в частности CGI (Common Gateway Interface), который позволяет использовать любые исполняемые файлы и скрипты, в том числе написанные на языке командной оболочки Bash. Устанавливаем сам сервер (для удобства, в данном примере дальнейшая настройка производится из под пользователя с правами root) и jqlang, который пригодится для обработки JSON в дальнейшем:

apt install apache2 jq

Меняем порт по умолчанию на 8443 (для примера, можно указать любой другой) в конфигурационном файле /etc/apache2/ports.conf одной командой:

cat /etc/apache2/ports.conf | sed -r "s/^Listen.+/Listen 8443/" > /etc/apache2/ports.conf

Активируем встроенный модуль базовой HTTP-аутентификации (Base64) и создаем пользователя (в примере, rest с паролем api):

a2enmod auth_basic
htpasswd -b -c /etc/apache2/.htpasswd rest api

Создаем файл Bash-скрипта по пути /var/www/api/api.sh и предоставляем ему права на выполнение:

mkdir /var/www/api && touch /var/www/api/api.sh && chmod +x /var/www/api/api.sh

Открываем файла с помощью любого редактора (например, nano /var/www/api/api.sh) и вставляем следующее содержимое:

#!/bin/bash
# Формируем базовый HTTP-ответ
echo "Content-type: application/json"
echo
# Читаем содержимое Body запроса из стандартного ввода (stdin):
read -n $CONTENT_LENGTH POST_DATA
# Читаем содержимое встроенных переменных
request=$(echo {\"method\": \"$REQUEST_METHOD\", \"url\": \"$REQUEST_URI\"})
client=$(echo {\"address\": \"$REMOTE_ADDR\", \"port\": \"$REMOTE_PORT\", \"agent\": \"$HTTP_USER_AGENT\", \"type_auth\": \"$AUTH_TYPE\", \"user\": \"$REMOTE_USER\"})
server=$(echo {\"address\": \"$SERVER_NAME\", \"port\": \"$SERVER_PORT\", \"version\": \"$SERVER_SOFTWARE\", \"protocol\": \"$SERVER_PROTOCOL\"})
# Читаем содержимое тела запроса и переданых заголовков:
content=$(echo {\"type\": \"$CONTENT_TYPE\", \"length\": \"$CONTENT_LENGTH\", \"body\": \"$POST_DATA\", \"status\": \"$HTTP_STATUS\"})
response=$(echo {\"request\": [$request], \"client\": [$client], \"server\": [$server], \"content\": [$content]})
echo $response | jq .

Теперь необходимо создать и настроить VirtualHost для нового сайта, который будет отвечать за обработку запросов нашего будущего API-сервера:


    DocumentRoot /var/www/html
    # Связать endpoint (включая все дочернии в пути) с исполняемым файлом
    ScriptAlias /api /var/www/api/api.sh
    # Все опции, вложенные внутрь секции Directory, применяются к указанной директории
    
        # Разрешить выполнение CGI-скриптов
        Options +ExecCGI
        # Обрабатывать все файлы с расширение sh как CGI-скрипт
        AddHandler cgi-script .sh
        AllowOverride None
        Require all granted
    
    # Добавить авторизацию для endpoint
    
        AuthType Basic
        AuthName "Restricted Area"
        AuthUserFile /etc/apache2/.htpasswd
        Require valid-user
        SetHandler cgi-script
        Options +ExecCGI
    
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

Активируем модуль для работы cgi-скриптов и созданный VirtualHost, после чего запускаем сервер:

a2enmod cgi
a2ensite api.confs
systemctl restart apache2

Если все сделано правильно, то можем отправить свой первый запрос (с указанием данных для авторизации) на сервер:

curl http://192.168.3.101:8443/api -u rest:api

{
  "request": [
    {
      "method": "GET",
      "url": "/api"
    }
  ],
  "client": [
    {
      "address": "192.168.3.100",
      "port": "25757",
      "agent": "curl/8.4.0",
      "type_auth": "Basic",
      "user": "rest"
    }
  ],
  "server": [
    {
      "address": "192.168.3.101",
      "port": "8443",
      "version": "Apache/2.4.52 (Ubuntu)",
      "protocol": "HTTP/1.1"
    }
  ],
  "content": [
    {
      "type": "",
      "length": "",
      "body": "",
      "status": ""
    }
  ]
}

В выводе ответа мы получаем содержимое параметров нашего запроса, а именно метод, конечную точку, данные клиента и сервера. Доступ к этим данным мы получаем через переменные окружения, которые создаются и заполняются сервером Apache на основе информации, содержащейся в HTTP-запросе. Для пример, передадим заголовок Status с содержимым text и произвольный текст (test) в теле запроса, что бы прочитать их содержимое на сервере:

curl -s http://192.168.3.101:8443/api -u rest:api -H "Status: text" --data "test" | jq .content[]

{
  "type": "application/x-www-form-urlencoded",
  "length": "5",
  "body": "test",
  "status": "text"
}

Вот список всех переменных, которые могут быть полезны:

REQUEST_METHOD — метод HTTP-запроса (GET, POST, HEAD и т.д.)
REQUEST_URI — оригинальный URI запроса
QUERY_STRING — строка запроса URL
CONTENT_TYPE — тип содержимого запроса в заголовке клиента (например, application/json)
CONTENT_LENGTH — длина тела запроса в байтах (чаще, для POST-запросов)
read -n $CONTENT_LENGTH POST_DATA — прочитать содержимое Body из стандартного ввода (stdin).
HTTP_STATUS — читаем содержимое переданного заголовка (например,»Status: text"), которое определяется заранее и регламентируется в дальнейшем
HTTP_USER_AGENT — название агента клиента из заголовка (например, curl/8.4.0)
REMOTE_ADDR — адрес клиента
REMOTE_PORT — порт клиента
SERVER_NAME — адрес сервера
SERVER_PORT — порт сервера
SCRIPT_NAME — путь и имя CGI-скрипта
SERVER_SOFTWARE — имя и версия сервера
SERVER_PROTOCOL — версия протокола HTTP (например, HTTP/1.1)
HTTPS — если установлено, то запрос был сделан с использованием HTTPS
AUTH_TYPE — тип аутентификации, если он был предоставлен
REMOTE_USER — имя пользователя, если была использована аутентификация
DOCUMENT_ROOT — корневой каталог веб-сервера

Имея доступ к содержимому перечисленных переменных и используя возможности Bash, можно сконфигурировать простой, но при этом полноценный REST API сервер. Приведу пример обработки нескольких конечных точек для управления службами Linux:

#!/bin/bash
# Функция для получения списка всех служб, а также статус автозапуска и времени работы для каждой службы отдельно
function list-units {
    service_name=$1
    service_list=$(systemctl list-units --all --type=service --plain --no-legend --no-pager --output=json | jq --arg service_name "$service_name" '
        .[] | select(.unit | test($service_name))
    ')
    for unit in $(echo $service_list | jq -r .unit); do
        uptime=$(systemctl status $unit 2>/dev/null | grep -P "Active:.+;" | sed -r "s/.+; | ago//g")
        startup=$(systemctl status $unit 2>/dev/null } | grep -oP "enabled|disabled" | head -n 1)
        echo $service_list | jq --arg unit "$unit" --arg uptime "$uptime" --arg startup "$startup" '
            . | select (.unit == $unit) + {uptime: $uptime, startup: $startup}
        '
    done
}
# Первое условие для проверки запрошенного метода и конечной точки
if [ "$REQUEST_METHOD" == "GET" -a "$REQUEST_URI" == "/api/uptime" ]; then
    # Возвращаем время работы в формате обычного текста
    echo "Content-type: text/plain"
    echo
    uptime -p | sed "s/up //" | awk -F "," '{print $1,$2,$3}'
# Формируем ответ на вторую конечную точку
elif [ "$REQUEST_METHOD" == "GET" -a "$REQUEST_URI" == "/api/disk" ]; then
    # Декларируем, что тип ответа сервера будет в формате json
    echo "Content-type: application/json"
    echo
    # Получаем информацию о блочных устройствах из утилиты lsblk в формате json
    lsblk -e7 -f --json | jq .
# Проверяем наименование клиента, если браузер (Chrome), возвращаем ответ в формате HTML
elif [ "$REQUEST_METHOD" == "GET" ] && [ "$REQUEST_URI" == "/api/service" ] && echo "$HTTP_USER_AGENT" | grep -q "Chrome"; then
    # Получаем список всех служб в формате json
    response=$(systemctl list-units --all --type=service --plain --no-legend --no-pager --output=json)
    # Формируем ответ в формате HTML
    echo "Content-type: text/html"
    echo
    echo ""
    echo ""
    echo "Service list"
    echo ""
    echo ""
    echo ""
    echo ""
    echo ""
    echo ""
    echo ""
    echo ""
    echo ""
    echo ""
    # Забираем содержимое массива json в формате текста и оборачиваем все получаемые значения в тэги HTML таблицы
    echo "$response" | jq -r '.[] | ""'
    echo "
UnitLoadActiveSubDescription
\(.unit)\(.load)\(.active)\(.sub)\(.description)
" echo " " echo "" # В остальных случаях возвращает ответ в формате json elif [ "$REQUEST_METHOD" == "GET" -a "$REQUEST_URI" == "/api/service" ]; then response=$(systemctl list-units --all --type=service --plain --no-legend --no-pager --output=json) echo "Content-type: application/json" echo echo $response | jq . # Если в родительском пути uri /api/service/ содержится текст, забираем его elif [ "$REQUEST_METHOD" == "GET" ] && echo "$REQUEST_URI" | grep -qP "/api/service/.+"; then echo "Content-type: application/json" echo service_name=$(echo $REQUEST_URI | sed -r "s/.+\///g") # Проверяем, что службы с переданным наименованием существуют service_list=$(list-units "$service_name") # Если список служб пуст, то возвращаем ответ с ошибкой. if [ $(echo $service_list | wc -w) -eq 0 ]; then echo Service $service_name not found else echo $service_list | jq . fi # Обрабатывает POST запросы для остановки или запуска службы elif [ "$REQUEST_METHOD" == "POST" ] && echo "$REQUEST_URI" | grep -qP "/api/service/.+"; then echo "Content-type: application/json" echo service_name=$(echo $REQUEST_URI | sed -r "s/.+\///g") service_list=$(list-units "$service_name") # Проверяем, что была передана только одна служба if [ $(echo $service_list | jq .unit | wc -l) -ne 1 ]; then echo "Bad request. Only one service can be transferred." else # Проверяем заголовок статуса на одно из трех доступных значений if [ "$HTTP_STATUS" = "stop" ] || [ "$HTTP_STATUS" = "start" ] || [ "$HTTP_STATUS" = "restart" ]; then sudo systemctl $HTTP_STATUS $(echo $service_list | jq -r .unit) list-units "$service_name" else echo "Bad request. You need to pass the service Status in the request header: stop, start or restart." fi fi # Если ничего не попадо под условия, возвращаем ответ с указанием причины elif [ "$REQUEST_METHOD" != "GET" ]; then echo "Content-type: text/plain" echo echo "Method not supported" else echo "Content-type: text/plain" echo echo "Endpoint $REQUEST_URI not found" fi

Перезагрузка сервер не требуется, все изменения в файле api.sh действующего сайта применяются сразу же после сохранения файла. Теперь необходимо пользователю (чаще всего это www-data) из под которого запускается сервер Apache предоставить права sudo к командам для управления службами, сделать это можно одной командой:

echo "www-data ALL=(ALL) NOPASSWD: /bin/systemctl start *, /bin/systemctl stop *, /bin/systemctl restart *" >> /etc/sudoers

Теперь проверяем вывод с помощью curl и PowerShell:

# Проверяем статус службы cron
curl -s http://192.168.3.101:8443/api/service/cron -u rest:api
# Останавливаем службу
curl -s -X POST http://192.168.3.101:8443/api/service/cron -u rest:api -H "Status: stop"
# Запускаем службу
curl -s -X POST http://192.168.3.101:8443/api/service/cron -u rest:api -H "Status: start"

# Заполняем заголовок запроса авторизации для PowerShell
$user = "rest"
$pass = "api"
$EncodingCred = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("${user}:${pass}"))
$Headers = @{"Authorization" = "Basic ${EncodingCred}"}
Invoke-RestMethod -Headers $Headers -Uri http://192.168.3.101:8443/api/service/cron

Получение ответов от сервера Apache

Получение ответов от сервера Apache

Как видно из примера, с использованием инструмента jq мы можем достаточно просто обрабатывать полученные данные и отвечать клиенту в формате JSON (как это делают большинство подобных серверов), так и в формате HTML (например, но основании наименования клиента Chrome), добавив соответствующие тэги:

Список всех юнитов в формате HTML

Список всех юнитов в формате HTML

Исходный скрипт также опубликован на GitHub, вы можете наполнить его десятками конечных точек используя дополнительные условия для управления или предоставления информации из операционный системы с помощью Bash для любого REST-клиента, в том числе работающего в системе Windows. Также, если добавить немного JavaScript, совместно с Bash возможно создать простой Web-сервер, как это реализовано в проекте WinAPI на PowerShell. На мой взгляд, такой подход будет больше полезен системным администраторам которые хорошо ориентируются в командной оболочке и хотели бы применить свои знания для создания своего сервера с возможностью удаленного управления через протокол HTTP.

© Habrahabr.ru