Application: "Freeradius v3", "Nginx", "Inetd", "Bash", "EAP Testing".
Задача: создание пользовательского инструмента с web-интерфейсом, посредством которого можно выявлять доступные способы аутентификации через сервер "Freeradius", в данном случае обслуживающий сервис "Eduroam".
В качестве терминатора интерфейса воспользуемся web-сервером "Nginx". Фоновые процедуры возложим на спарку "Inetd + Bash", написав простейший обработчик запросов. Тестирование аутентификации как таковой потребует применения специализированной утилиты "EAP Testing".
В итоге web-интерфейс, состоящий из двух последовательных частей, будет выглядеть примерно так:
Пример web-интерфейса тестирования доступных методов аутентификации.
Исходим от того, что конфигурация сервера аутентификации "Freeradius" соответствует приведённой в располагающейся рядом инструкции.
Установка утилиты "EAP Testing".
В репозиториях основных дистрибутивов утилита "eapol_test" отсутствует, так что придётся её установить вручную.
Предварительно установим необходимые для сборки системные библиотеки и утилиты:
# aptitude install --without-recommends git libssl-dev devscripts pkg-config libnl-3-dev libnl-genl-3-dev
Загружаем исходный код утилиты, собираем её и размещаем в подходящем месте файловой системы:
# cd /usr/src
# git clone --depth 1 --no-single-branch https://github.com/FreeRADIUS/freeradius-server.git
# cd freeradius-server/scripts/travis/ && ./eapol_test-build.sh
# cp ./eapol_test/eapol_test /usr/local/bin/
# git clone --depth 1 --no-single-branch https://github.com/FreeRADIUS/freeradius-server.git
# cd freeradius-server/scripts/travis/ && ./eapol_test-build.sh
# cp ./eapol_test/eapol_test /usr/local/bin/
Настройка точки подключения к "Freeradius".
Чтобы утилита "EAP Testing" могла обращаться к "Freeradius" необходимо добавить в конфигурацию такового описание точки подключения в роли клиента (вроде WiFi-AP), с той лишь разницей, что разрешено оно будет только через "локальную сетевую петлю":
# vi /etc/freeradius/3.0/clients.conf
....
# Точка входа для web-клиента тестирования поддерживаемых методов аутентификации
client web_eapol_test {
ipaddr = 127.0.0.1
secret = strongPassword
require_message_authenticator = yes
nas_type = other
virtual_server = outer-example-eduroam
}
....
# Точка входа для web-клиента тестирования поддерживаемых методов аутентификации
client web_eapol_test {
ipaddr = 127.0.0.1
secret = strongPassword
require_message_authenticator = yes
nas_type = other
virtual_server = outer-example-eduroam
}
....
Проверяем корректность конфигурации и применяем её, перезагружая сервис:
# freeradius -C -X && systemctl restart freeradius
Установка и преднастройка "Inetd".
Вначале для запуска bash-скрипта, посредством которого осуществляется тестирование аутентификации, была применена обёртка "fcgiwrap", но в процессе наладки функционала выяснилось, что она в принципе не поддерживает порционный обмен данными между web-сервером и клиентом - а мне хотелось не заставлять пользователя ждать, пока пройдут все тесты, информируя его о прохождении таковых поэтапно. Для этого наилучшим образом подходил запуск скрипта через супер-сервис "inetd" и выдача ответов клиенту посредством механизма "chunked transfer encoding".
Устанавливаем простейший "inetd", игнорируя его наследников "xinetd" и "rlinetd" - их функциональность для решения задачи не требуется:
# aptitude install openbsd-inetd
Анонсируем используемый "inetd" сетевой порт в перечне известных несущей операционной системе:
# vi /etc/services
....
# Local services
eg-auth-test 8889/tcp # Eduroam Gateway Auth Test
# Local services
eg-auth-test 8889/tcp # Eduroam Gateway Auth Test
Регистрируем inetd-сервис, принимающий сетевые подключения (только на "локальной петле") и передающие их указанному bash-скрипту (его наличие обязательно, так что лучше его создать заранее, хоть бы и пустым):
# vi /etc/inetd.conf
....
#:OTHER: Other services
127.0.0.1:eg-auth-test stream tcp nowait www-data /var/www/cgi-bin/auth-test/index.cgi
#:OTHER: Other services
127.0.0.1:eg-auth-test stream tcp nowait www-data /var/www/cgi-bin/auth-test/index.cgi
# /etc/init.d/openbsd-inetd restart
Проверяем состояние сервиса и прослушиваемого сетевого порта:
# /etc/init.d/openbsd-inetd status
# netstat -apn | grep -i inetd
# netstat -apn | grep -i inetd
Установка и настройка web-сервера "Nginx".
Устанавливаем терминирующий интерфейс web-сервер:
# aptitude install nginx-light
Заранее заготовим директорию для SSL-сертификатов - без шифрования сеанса связи работать с паролями пользователь недопустимо, разумеется:
# mkdir -p /etc/ssl/nginx && chown -R root:root /etc/ssl/nginx
# openssl dhparam -out /etc/ssl/nginx/dhparam.2048.pem 2048
# openssl dhparam -out /etc/ssl/nginx/dhparam.2048.pem 2048
Небольшая преднастройка сервиса:
# vi /etc/nginx/nginx.conf
user www-data www-data;
worker_processes auto;
....
http {
....
# Велим Nginx не выдавать сведения о номере своей версии
server_tokens off;
....
worker_processes auto;
....
http {
....
# Велим Nginx не выдавать сведения о номере своей версии
server_tokens off;
....
Описываем конфигурацию web-сайта, посредством взаимодействия с которым будет осуществляться тестирование:
# vi /etc/nginx/sites-available/eduroam.example.net.conf
# Во избежание возможного DDoS ограничиваем число конкурентных соединений к сервису
upstream eduroam-auth-test {
server 127.0.0.1:8889 max_conns=24;
}
....
# Налаживаем перенаправление всех запросов в HTTPS
server {
listen 80;
server_name eduroam.example.net;
location / {
rewrite ^ https://eduroam.example.net$request_uri permanent;
}
}
# Описание web-сайта как такового, с правилами проксирования запросов
server {
listen 443 ssl http2;
server_name eduroam.example.net;
....
# Явно указываем обслуживать здесь только SSL/TLS подключения
ssl on;
# Описываем параметры установления соединений SSL/TLS
ssl_dhparam /etc/ssl/nginx/dhparam.2048.pem;
ssl_certificate /etc/ssl/nginx/eduroam.example.net.crt;
ssl_certificate_key /etc/ssl/nginx/eduroam.example.net.key.decrypt;
ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Обрабатываем обращения без явного указания имени скрипта
location ~ ^/auth-test {
# Указываем корневую директорию ресурса
root /var/www/cgi-bin;
index index.cgi;
# Отлавливаем все обращения к скриптам и проксируем их в Inetd-Bash
location ~ ^/auth-test/.*\.cgi$ {
# Не сжимаем обращения к скриптам
gzip off;
# Задаём параметры проксирования трафика
proxy_pass http://eduroam-auth-test;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Отключаем по возможности буферизацию, чтобы порционные данные пролетали немедленно
proxy_request_buffering off;
proxy_buffering off;
# Переключаемся на проксирование посредством современного HTTP/1.1
# (это требуется для поддержки "Transfer-Encoding: chunked")
proxy_http_version 1.1;
}
}
}
upstream eduroam-auth-test {
server 127.0.0.1:8889 max_conns=24;
}
....
# Налаживаем перенаправление всех запросов в HTTPS
server {
listen 80;
server_name eduroam.example.net;
location / {
rewrite ^ https://eduroam.example.net$request_uri permanent;
}
}
# Описание web-сайта как такового, с правилами проксирования запросов
server {
listen 443 ssl http2;
server_name eduroam.example.net;
....
# Явно указываем обслуживать здесь только SSL/TLS подключения
ssl on;
# Описываем параметры установления соединений SSL/TLS
ssl_dhparam /etc/ssl/nginx/dhparam.2048.pem;
ssl_certificate /etc/ssl/nginx/eduroam.example.net.crt;
ssl_certificate_key /etc/ssl/nginx/eduroam.example.net.key.decrypt;
ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Обрабатываем обращения без явного указания имени скрипта
location ~ ^/auth-test {
# Указываем корневую директорию ресурса
root /var/www/cgi-bin;
index index.cgi;
# Отлавливаем все обращения к скриптам и проксируем их в Inetd-Bash
location ~ ^/auth-test/.*\.cgi$ {
# Не сжимаем обращения к скриптам
gzip off;
# Задаём параметры проксирования трафика
proxy_pass http://eduroam-auth-test;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Отключаем по возможности буферизацию, чтобы порционные данные пролетали немедленно
proxy_request_buffering off;
proxy_buffering off;
# Переключаемся на проксирование посредством современного HTTP/1.1
# (это требуется для поддержки "Transfer-Encoding: chunked")
proxy_http_version 1.1;
}
}
}
Проверяем корректность конфигурации и применяем таковую:
# nginx -t && /etc/init.d/nginx reload
Учитывая то, что в конфигурации "Nginx" выше мы ограничили количество конкурентных запросов к сервису проверки аутентификации определённым числом, при превышении лимита пользователи будут получать ответ сервера "502 Bad Gateway". Изящнее было бы в таком случае отдавать специальную страничку с информацией о временной недоступности сервиса.
Пишем фоновый сервис тестирования методов аутентификации.
Выше мы установили всё необходимое программное обеспечение и осталось самое "простое" - написать bash-скрипт, принимающий данные пользователя через web-интерфейс, вызывающий утилиту "eapol_test", аккумулирующий ответы таковой и отдающий результаты пользователю в оформленном виде.
Заготавливаем место для скрипта (он будет вызываться не через CGI, но не будем множить директории):
# mkdir -p /var/www/cgi-bin/auth-test
# chown www-data:www-data /var/www/cgi-bin/auth-test && chmod o-rwx /var/www/cgi-bin/auth-test
# chown www-data:www-data /var/www/cgi-bin/auth-test && chmod o-rwx /var/www/cgi-bin/auth-test
Пишем bash-скрипт, функционал которого прокомментирован внутри:
# vi /var/www/cgi-bin/auth-test/index.cgi && chmod +x /var/www/cgi-bin/auth-test/index.cgi && chown www-data:www-data /var/www/cgi-bin/auth-test/index.cgi && chmod o-rwx /var/www/cgi-bin/auth-test/index.cgi
#!/bin/bash
# Формируем массив с параметрами разных методов аутентификации
#
EAP="TTLS"
PHASE2="auth=MSCHAPV2"
DESC="EAP-TTLS / MSCHAPv2"
#
EAP[1]="TTLS"
PHASE2[1]="autheap=MSCHAPV2"
DESC[1]="EAP-TTLS / EAP-MSCHAPv2"
#
EAP[2]="TTLS"
PHASE2[2]="autheap=GTC"
DESC[2]="EAP-TTLS / EAP-GTC"
#
EAP[3]="TTLS"
PHASE2[3]="auth=PAP"
DESC[3]="EAP-TTLS / PAP"
#
EAP[4]="PEAP"
PHASE2[4]="auth=MSCHAPV2"
DESC[4]="EAP-PEAP / MSCHAPv2"
#
EAP[5]="PEAP"
PHASE2[5]="autheap=MSCHAPV2"
DESC[5]="EAP-PEAP / EAP-MSCHAPv2"
#
EAP[6]="PEAP"
PHASE2[6]="autheap=GTC"
DESC[6]="EAP-PEAP / EAP-GTC"
#
#EAP[7]="TTLS"
#PHASE2[7]="autheap=MD5"
#DESC[7]="EAP-TTLS / (unsafe) EAP-MD5"
#
#EAP[8]="PEAP"
#PHASE2[8]="autheap=MD5"
#DESC[8]="EAP-PEAP / (unsafe) EAP-MD5"
#
#EAP[9]=MSCHAPv2
#DESC[9]="(unsafe) EAP-MSCHAPv2"
#
#EAP[10]=MD5
#DESC[10]="(unsafe) EAP-MD5"
# --- #
# Объявляем простую bash-функцию конвертирования экранированной URL-style строки в чистый текст
urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; }
# Первым делом читаем прикреплённые к запросу заголовки
while read -t 0.01 HEADER ; do
# Для простоты детектирования пустых строк отрезаем у таковых управляющие символы "переноса строки (carriage return)"
HEADER="${HEADER//$'\r'}"
# Прерываем чтение на первой пустой строке (разделителе блока заголовков и тела запроса)
[ -z "${HEADER}" ] && break
# Выясняем длину прикреплённого POST-запроса
[ ! -z "$(echo ${HEADER} | grep -i 'content-length')" ] && { POST_LENGTH=$(echo ${HEADER} | awk -F ':' '{print $2}' | tr -d '[:space:]'); }
done
# Вторым проходом читаем прикреплённые к запросу POST-данные
# (ожидается Plain-Text)
[ ! -z "${POST_LENGTH}" ] && read -N ${POST_LENGTH} -t 0.01 IN_POST_STRING
# Вычленяем значения возможно объявленных ожидаемых переменных
for I in $(urldecode $IN_POST_STRING | tr '&' '\n') ; do
VAR_PAIR=( $(echo ${I} | tr '=' '\n') )
[[ -z "${USER_NAME}" && "${VAR_PAIR[0]}" == "user_name" ]] && USER_NAME="${VAR_PAIR[1]}"
[[ -z "${USER_PASSWORD}" && "${VAR_PAIR[0]}" == "user_password" ]] && USER_PASSWORD="${VAR_PAIR[1]}"
done
# --- #
# Первый обязательный заголовок, включающий режим работы по современому протоколу HTTP/1.1
# (это требуется для поддержки "Transfer-Encoding: chunked")
echo "HTTP/1.1 200 OK"
# Специальным заголовком указываем вышестоящему прокси не буферизировать данные
echo "X-Accel-Buffering: no"
# Указываем тип выдаваемых данных (это важно)
# (именно для "text/html" браузеры дорисовывают страницу с получением каждой порции данных)
echo "Content-type: text/html"
# Включаем режим порционной передачи данных и явного закрытия соединения по завершению работы
echo "Transfer-Encoding: chunked"
echo "Connection: close"
# Выдаём обязательный разделяющий перенос строки между заголовками и последующим потоком данных
echo ""
# --- #
# Формируем первый выдаваемый блок данных
BLOCK="<!DOCTYPE HTML>\n\
<html>\n\
<head>\n\
<meta charset=\"utf-8\">\n\
<font face=\"sans-serif\">\n\
<title>Eduroam authentication testing</title>\n\
<style>\n\
a {text-decoration: none;}\n\
a:visited {color: blue;}\n\
.pole {border: #C0C0C0 1px solid; text-align: left; padding: 4pt;}\n\
.button {border: #C0C0C0 1px solid; padding: 4pt;}\n\
table {text-align: left; border-collapse: collapse; margin: 0pt; padding: 0pt;}\n\
tr {border: none; margin: 0pt; padding: 0pt;}\n\
th {background-color: #F5F5F5; border: #C0C0C0 1px solid; text-align: center; font-size: 90%; margin: 0pt; padding-left: 6pt; padding-top: 4pt; padding-right: 6pt; padding-bottom: 4pt;}\n\
td {border: #C0C0C0 1px solid; text-align: left; vertical-align: top; margin: 0pt; padding-left: 6pt; padding-top: 4pt; padding-right: 6pt; padding-bottom: 4pt;}\n\
</style>\n\
</head>\n\
<body>\n\
<h2 style=\"color: #333333;\">Eduroam authentication testing (via network Example)</h2>\n\
\n"
# Вычисляем длину строки блока данных в байтах и переводим число в требуемый для "chunked transfer encoding" шестнадцатеричный формат
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
# Отдаём клиенту первый блок данных, предваряя его указанием длины
# (важно разделять параметры длины и блоки данных только и только последовательностями "\r\n")
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# --- #
if [[ -z "${USER_NAME}" || -z "${USER_PASSWORD}" ]] ; then
# В случае отсутствия логина и пароля на входе выдаём запрос на ввод таковых
BLOCK=${BLOCK}"<h3 style=\"color: #333333;\">Verification of authentication methods for the following username and password:</h3>\n"
BLOCK=${BLOCK}"<form action=\"/auth-test/index.cgi\" method=\"post\" enctype=\"application/x-www-form-urlencoded\">"
BLOCK=${BLOCK}"<table style='font-size: 12pt'>\n"
BLOCK=${BLOCK}"<tr>\n"
BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt; vertical-align: middle;\">Username:</td>\n"
BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt;\"><input class=\"pole\" type=\"text\" name=\"user_name\" size=\"24\" spellcheck=\"off\" autocapitalize=\"off\" autocorrect=\"off\" placeholder=\"username@domain.ltd\" tabindex=\"1\" required /></td>\n"
BLOCK=${BLOCK}"<td rowspan=\"2\" style=\"border: none; text-align: center; vertical-align: middle;\"><input class=\"button\" type=\"submit\" value=\"Check!\" tabindex=\"3\" /></td>\n"
BLOCK=${BLOCK}"</tr>\n"
BLOCK=${BLOCK}"<tr>\n"
BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt; vertical-align: middle;\">Password:</td>\n"
BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt;\"><input class=\"pole\" type=\"password\" name=\"user_password\" size=\"24\" autocomplete=\"off\" spellcheck=\"off\" autocapitalize=\"off\" autocorrect=\"off\" placeholder=\"xxxxxxxxxxxx\" tabindex=\"2\" required /></td>\n"
BLOCK=${BLOCK}"</tr>\n"
BLOCK=${BLOCK}"</table>\n"
BLOCK=${BLOCK}"</form>\n"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
else
# Профилактически создаём директорию для временных файлов
mkdir -p /var/tmp/eg-auth-test
# Зачищаем возможно осиротевшие при внезапном прерывании процедуры временные файлы
find /var/tmp/eg-auth-test -type f -mmin +3 -print0 | xargs --null --no-run-if-empty rm --force
# Выводим заготовку таблицы результатов
BLOCK=${BLOCK}"( <a href=\"/auth-test/\">new check</a> )<br />\n"
BLOCK=${BLOCK}"<h3 style=\"color: #333333;\">Validation results of available for \"${USER_NAME}\" authentication methods (${#EAP[@]} options):</h3>\n"
BLOCK=${BLOCK}"<table style='font-size: 12pt'>\n"
BLOCK=${BLOCK}"<tr>\n"
BLOCK=${BLOCK}"<th>#</th><th>EAP method / Auth type</th><th>Result</th>\n"
BLOCK=${BLOCK}"</tr>\n"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# Перебираем все вышеперечисленные варианты методов аутентификации
for I in "${!EAP[@]}" ; do
# Создаём временный файл
TMPF=$(mktemp --tmpdir=/var/tmp/eg-auth-test)
# Формируем временный конфигурационный файл
cat << EOF > "${TMPF}"
network={
key_mgmt=WPA-EAP
eap=${EAP[$I]}
phase2="${PHASE2[$I]}"
identity="${USER_NAME}"
password="${USER_PASSWORD}"
}
EOF
# Рисуем строку таблицы для этапа тестирования
BLOCK=${BLOCK}"<tr>\n"
BLOCK=${BLOCK}"<td>$(echo $(( ${I} + 1 )))</td>"
BLOCK=${BLOCK}"<td>$(echo ${DESC[$I]})</td>"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# Осуществляем тестирование аутентификации и вставляем результаты в таблицу
eapol_test -c ${TMPF} -a 127.0.0.1 -p 1812 -s strongPassword -r0 -t10 > /dev/null 2>&1
if [ "${?}" -eq "0" ] ; then
BLOCK=${BLOCK}"<td style=\"color: green;\">SUCCESS</td>"
else
BLOCK=${BLOCK}"<td style=\"color: red;\">FAILURE</td>"
fi
BLOCK=${BLOCK}"</tr>\n"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# Зачищаем среду тестирования для следующей итерации
unset EAP[$I]
unset PHASE2[$I]
unset DESC[$I]
rm -f "${TMPF}"
done
unset I
# После исполнения всех тестов закрываем таблицу
BLOCK=${BLOCK}"</td>\n"
BLOCK=${BLOCK}"</tr>\n"
BLOCK=${BLOCK}"</table>\n"
BLOCK=${BLOCK}"<br /><span style=\"font-size: 90%; color: #808080;\">Checking time: "$(date +'%Y-%m-%d %H:%M')"</span>\n"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
fi
# Завершаем отрисовку HTML-страницы
BLOCK=${BLOCK}"</body>\n</html>"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# Следуя требованиям протокола завершаем сеанс передачи блоком данных нулевой длинны
echo -en "0\r\n"
echo -en "\r\n"
exit ${?}
# Формируем массив с параметрами разных методов аутентификации
#
EAP="TTLS"
PHASE2="auth=MSCHAPV2"
DESC="EAP-TTLS / MSCHAPv2"
#
EAP[1]="TTLS"
PHASE2[1]="autheap=MSCHAPV2"
DESC[1]="EAP-TTLS / EAP-MSCHAPv2"
#
EAP[2]="TTLS"
PHASE2[2]="autheap=GTC"
DESC[2]="EAP-TTLS / EAP-GTC"
#
EAP[3]="TTLS"
PHASE2[3]="auth=PAP"
DESC[3]="EAP-TTLS / PAP"
#
EAP[4]="PEAP"
PHASE2[4]="auth=MSCHAPV2"
DESC[4]="EAP-PEAP / MSCHAPv2"
#
EAP[5]="PEAP"
PHASE2[5]="autheap=MSCHAPV2"
DESC[5]="EAP-PEAP / EAP-MSCHAPv2"
#
EAP[6]="PEAP"
PHASE2[6]="autheap=GTC"
DESC[6]="EAP-PEAP / EAP-GTC"
#
#EAP[7]="TTLS"
#PHASE2[7]="autheap=MD5"
#DESC[7]="EAP-TTLS / (unsafe) EAP-MD5"
#
#EAP[8]="PEAP"
#PHASE2[8]="autheap=MD5"
#DESC[8]="EAP-PEAP / (unsafe) EAP-MD5"
#
#EAP[9]=MSCHAPv2
#DESC[9]="(unsafe) EAP-MSCHAPv2"
#
#EAP[10]=MD5
#DESC[10]="(unsafe) EAP-MD5"
# --- #
# Объявляем простую bash-функцию конвертирования экранированной URL-style строки в чистый текст
urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; }
# Первым делом читаем прикреплённые к запросу заголовки
while read -t 0.01 HEADER ; do
# Для простоты детектирования пустых строк отрезаем у таковых управляющие символы "переноса строки (carriage return)"
HEADER="${HEADER//$'\r'}"
# Прерываем чтение на первой пустой строке (разделителе блока заголовков и тела запроса)
[ -z "${HEADER}" ] && break
# Выясняем длину прикреплённого POST-запроса
[ ! -z "$(echo ${HEADER} | grep -i 'content-length')" ] && { POST_LENGTH=$(echo ${HEADER} | awk -F ':' '{print $2}' | tr -d '[:space:]'); }
done
# Вторым проходом читаем прикреплённые к запросу POST-данные
# (ожидается Plain-Text)
[ ! -z "${POST_LENGTH}" ] && read -N ${POST_LENGTH} -t 0.01 IN_POST_STRING
# Вычленяем значения возможно объявленных ожидаемых переменных
for I in $(urldecode $IN_POST_STRING | tr '&' '\n') ; do
VAR_PAIR=( $(echo ${I} | tr '=' '\n') )
[[ -z "${USER_NAME}" && "${VAR_PAIR[0]}" == "user_name" ]] && USER_NAME="${VAR_PAIR[1]}"
[[ -z "${USER_PASSWORD}" && "${VAR_PAIR[0]}" == "user_password" ]] && USER_PASSWORD="${VAR_PAIR[1]}"
done
# --- #
# Первый обязательный заголовок, включающий режим работы по современому протоколу HTTP/1.1
# (это требуется для поддержки "Transfer-Encoding: chunked")
echo "HTTP/1.1 200 OK"
# Специальным заголовком указываем вышестоящему прокси не буферизировать данные
echo "X-Accel-Buffering: no"
# Указываем тип выдаваемых данных (это важно)
# (именно для "text/html" браузеры дорисовывают страницу с получением каждой порции данных)
echo "Content-type: text/html"
# Включаем режим порционной передачи данных и явного закрытия соединения по завершению работы
echo "Transfer-Encoding: chunked"
echo "Connection: close"
# Выдаём обязательный разделяющий перенос строки между заголовками и последующим потоком данных
echo ""
# --- #
# Формируем первый выдаваемый блок данных
BLOCK="<!DOCTYPE HTML>\n\
<html>\n\
<head>\n\
<meta charset=\"utf-8\">\n\
<font face=\"sans-serif\">\n\
<title>Eduroam authentication testing</title>\n\
<style>\n\
a {text-decoration: none;}\n\
a:visited {color: blue;}\n\
.pole {border: #C0C0C0 1px solid; text-align: left; padding: 4pt;}\n\
.button {border: #C0C0C0 1px solid; padding: 4pt;}\n\
table {text-align: left; border-collapse: collapse; margin: 0pt; padding: 0pt;}\n\
tr {border: none; margin: 0pt; padding: 0pt;}\n\
th {background-color: #F5F5F5; border: #C0C0C0 1px solid; text-align: center; font-size: 90%; margin: 0pt; padding-left: 6pt; padding-top: 4pt; padding-right: 6pt; padding-bottom: 4pt;}\n\
td {border: #C0C0C0 1px solid; text-align: left; vertical-align: top; margin: 0pt; padding-left: 6pt; padding-top: 4pt; padding-right: 6pt; padding-bottom: 4pt;}\n\
</style>\n\
</head>\n\
<body>\n\
<h2 style=\"color: #333333;\">Eduroam authentication testing (via network Example)</h2>\n\
\n"
# Вычисляем длину строки блока данных в байтах и переводим число в требуемый для "chunked transfer encoding" шестнадцатеричный формат
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
# Отдаём клиенту первый блок данных, предваряя его указанием длины
# (важно разделять параметры длины и блоки данных только и только последовательностями "\r\n")
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# --- #
if [[ -z "${USER_NAME}" || -z "${USER_PASSWORD}" ]] ; then
# В случае отсутствия логина и пароля на входе выдаём запрос на ввод таковых
BLOCK=${BLOCK}"<h3 style=\"color: #333333;\">Verification of authentication methods for the following username and password:</h3>\n"
BLOCK=${BLOCK}"<form action=\"/auth-test/index.cgi\" method=\"post\" enctype=\"application/x-www-form-urlencoded\">"
BLOCK=${BLOCK}"<table style='font-size: 12pt'>\n"
BLOCK=${BLOCK}"<tr>\n"
BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt; vertical-align: middle;\">Username:</td>\n"
BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt;\"><input class=\"pole\" type=\"text\" name=\"user_name\" size=\"24\" spellcheck=\"off\" autocapitalize=\"off\" autocorrect=\"off\" placeholder=\"username@domain.ltd\" tabindex=\"1\" required /></td>\n"
BLOCK=${BLOCK}"<td rowspan=\"2\" style=\"border: none; text-align: center; vertical-align: middle;\"><input class=\"button\" type=\"submit\" value=\"Check!\" tabindex=\"3\" /></td>\n"
BLOCK=${BLOCK}"</tr>\n"
BLOCK=${BLOCK}"<tr>\n"
BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt; vertical-align: middle;\">Password:</td>\n"
BLOCK=${BLOCK}"<td style=\"border: none; padding: 3pt;\"><input class=\"pole\" type=\"password\" name=\"user_password\" size=\"24\" autocomplete=\"off\" spellcheck=\"off\" autocapitalize=\"off\" autocorrect=\"off\" placeholder=\"xxxxxxxxxxxx\" tabindex=\"2\" required /></td>\n"
BLOCK=${BLOCK}"</tr>\n"
BLOCK=${BLOCK}"</table>\n"
BLOCK=${BLOCK}"</form>\n"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
else
# Профилактически создаём директорию для временных файлов
mkdir -p /var/tmp/eg-auth-test
# Зачищаем возможно осиротевшие при внезапном прерывании процедуры временные файлы
find /var/tmp/eg-auth-test -type f -mmin +3 -print0 | xargs --null --no-run-if-empty rm --force
# Выводим заготовку таблицы результатов
BLOCK=${BLOCK}"( <a href=\"/auth-test/\">new check</a> )<br />\n"
BLOCK=${BLOCK}"<h3 style=\"color: #333333;\">Validation results of available for \"${USER_NAME}\" authentication methods (${#EAP[@]} options):</h3>\n"
BLOCK=${BLOCK}"<table style='font-size: 12pt'>\n"
BLOCK=${BLOCK}"<tr>\n"
BLOCK=${BLOCK}"<th>#</th><th>EAP method / Auth type</th><th>Result</th>\n"
BLOCK=${BLOCK}"</tr>\n"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# Перебираем все вышеперечисленные варианты методов аутентификации
for I in "${!EAP[@]}" ; do
# Создаём временный файл
TMPF=$(mktemp --tmpdir=/var/tmp/eg-auth-test)
# Формируем временный конфигурационный файл
cat << EOF > "${TMPF}"
network={
key_mgmt=WPA-EAP
eap=${EAP[$I]}
phase2="${PHASE2[$I]}"
identity="${USER_NAME}"
password="${USER_PASSWORD}"
}
EOF
# Рисуем строку таблицы для этапа тестирования
BLOCK=${BLOCK}"<tr>\n"
BLOCK=${BLOCK}"<td>$(echo $(( ${I} + 1 )))</td>"
BLOCK=${BLOCK}"<td>$(echo ${DESC[$I]})</td>"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# Осуществляем тестирование аутентификации и вставляем результаты в таблицу
eapol_test -c ${TMPF} -a 127.0.0.1 -p 1812 -s strongPassword -r0 -t10 > /dev/null 2>&1
if [ "${?}" -eq "0" ] ; then
BLOCK=${BLOCK}"<td style=\"color: green;\">SUCCESS</td>"
else
BLOCK=${BLOCK}"<td style=\"color: red;\">FAILURE</td>"
fi
BLOCK=${BLOCK}"</tr>\n"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# Зачищаем среду тестирования для следующей итерации
unset EAP[$I]
unset PHASE2[$I]
unset DESC[$I]
rm -f "${TMPF}"
done
unset I
# После исполнения всех тестов закрываем таблицу
BLOCK=${BLOCK}"</td>\n"
BLOCK=${BLOCK}"</tr>\n"
BLOCK=${BLOCK}"</table>\n"
BLOCK=${BLOCK}"<br /><span style=\"font-size: 90%; color: #808080;\">Checking time: "$(date +'%Y-%m-%d %H:%M')"</span>\n"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
fi
# Завершаем отрисовку HTML-страницы
BLOCK=${BLOCK}"</body>\n</html>"
printf -v LENGTH "%x" $(echo -en "${BLOCK}" | wc -c) >/dev/null 2>&1
echo -en "${LENGTH}\r\n"
echo -en "${BLOCK}\r\n" 2>/dev/null
unset BLOCK
# Следуя требованиям протокола завершаем сеанс передачи блоком данных нулевой длинны
echo -en "0\r\n"
echo -en "\r\n"
exit ${?}
Пояснения к особенностям реализации.
Несмотря на простоту решения без проблем не обошлось. К сожалению, как я ни бился, примерно в половине случаев связь с "inetd/xinetd" обрывалась, если подключение устанавливалось извне, с выдачей следующего сообщения:
....
* Recv failure: Connection reset by peer
* stopped the pause stream!
* Closing connection 0
curl: (56) Recv failure: Connection reset by peer
* Recv failure: Connection reset by peer
* stopped the pause stream!
* Closing connection 0
curl: (56) Recv failure: Connection reset by peer
Наблюдение за самим приложением "inetd/xinetd" посредством утилиты "strace" не показало никакой разницы между поведением сервиса при успешном завершении обработки запроса или его сбое - это явно не на уровне приложения случается.
Наблюдение посредством "tcpdump" и "wireshark" выявили неожиданные непредсказуемые посылки несущей web-сервис операционной системой сигнала RST без видимой на то причины. Причину понять не удалось.
Единственная рабочая схема получилась с запуском "inetd/xinetd" на "локальной сетевой петле", обращения к которой шли через фронтальный прокси, в роли которого я использовал "Nginx". В этой конфигурации схема работала безупречно - тестирование и практика не выявили сбоев.
Способы отладки сервиса.
На этапе наладки функциональности bash-скрипта обращаться к спарке "Inetd + Bash" проще без лишних посредников, напрямую в порт "Inetd", с "локальной петли":
$ curl -N -vvv --request POST -H "Content-Type: application/x-www-form-urlencoded" --data "user_name=username@example.net&user_password=userPassword" 127.0.0.1:8889
Чтобы яснее представить, что посылается в сетевой порт, можно запустить "netcat" и отправить в него POST-запрос:
$ nc -l 8888
$ curl --request POST -H "Content-Type: application/x-www-form-urlencoded" --data "qwerty=12345&asd=7890" 127.0.0.1:8888
$ curl --request POST -H "Content-Type: application/x-www-form-urlencoded" --data "qwerty=12345&asd=7890" 127.0.0.1:8888
В результате хорошо видна структура передаваемых данных, с разделением на заголовки и тело POST-запроса:
POST / HTTP/1.1
Host: 127.0.0.1:8888
User-Agent: curl/7.58.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Content-Length: 21
qwerty=12345&asd=7890
Host: 127.0.0.1:8888
User-Agent: curl/7.58.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Content-Length: 21
qwerty=12345&asd=7890