[Серверная оптимизация, API, DevOps] Haproxy — программирование и конфигурирование средствами Lua

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
06-Дек-2020 00:30

Сервер Haproxy имеет встроенные средства для выполнения скриптов Lua.
Язык программирования Lua для расширения возможностей различных серверов используется очень широко. Например, на Lua можно программировать для серверов Redis, Nginx (nginx-extras, openresty), Envoy. Это вполне закономерно, так как язык программирования Lua как раз и был разработан для удобства встраивания в приложения в качестве скриптового языка.
В этом сообщении я рассмотрю варианты использования Lua для расширения возможностей Haproxy.
Согласно документации, скрипты Lua на сервере Haproxy могут выполняться в шести контекстах:
— body context (контекст времени загрузки конфигурации сервера Haproxy, когда выполняются скрипты, заданные директивой lua-load);
— init context (контекст функций, которые вызываются сразу после загрузки конфигурации, и зарегистрированы системной функции core.register_init(function);
— task context (контекст функций, выполняемых по расписанию и зарегистрированных системной функцией core.register_task(function));
— action context (контекст функций, зарегистрированных системной функцией сore.register_action(function));
— sample-fetch context (контекст функций, зарегистрированных системной функцией сore.register_fetches(function));
— converter context (контекст функций, зарегистрированных системной функцией сore.register_converters(function)).
Фактически есть еще один контекст выполнения, который не указан в документации:
— service context (контекст функций, зарегистрированных системной функцией сore.register_service(function));
Начнем с самой простой конфигурации сервера Haproxy. Конфигурация состоит из двух секций frontend — то есть то, к чему обращается клиент с запросом, и backend — то, куда проксируется запрос клиента через сервер Haproxy:
frontend jwt
        mode http
        bind *:80
        use_backend backend_app
backend backend_app
        mode http
        server app1 app:3000

Теперь все запросы, приходящие на порт 80 Haproxy будут перенаправлены на порт 3000 сервера app.
Services
Services — это функции, определенные в скриптах Lua, которые формируют ответ без обращения к бэкенду. Эти функции регистрируются вызовом системной функции сore.register_service(function)).
Определим простейший Service в файле guarde.lua:
function _M.hello_world(applet)
  applet:set_status(200)
  local response = string.format([[<html><body>Hello World!</body></html>]], message);
  applet:add_header("content-type", "text/html");
  applet:add_header("content-length", string.len(response))
  applet:start_response()
  applet:send(response)
end

И зарегистрируем ее как Service в файле register.lua:
package.path = package.path  .. "./?.lua;/usr/local/etc/haproxy/?.lua"
local guard = require("guard")
core.register_service("hello-world", "http", guard.hello_world);

Параметр «http» является триггером, который допускает использование Service только в контексте http запроса (mode http).
Дополним конфигурацию сервера Haproxy:
global
        lua-load /usr/local/etc/haproxy/register.lua
frontend jwt
        mode http
        bind *:80
        use_backend backend_app
        http-request use-service lua.hello-world   if { path /hello_world }
backend backend_app
        mode http
        server app1 app:3000

Теперь, обратившись к серверу Haproxy с запросом /hello_world, клиент получит не ответ с проксируемого сервера, а ответ сервиса lua.hello-world.
В качестве параметра функции передается контекст запроса в параметре applet. Нет возможности передать дополнительные параметры файле конфигурации.
Actions
Actions — действия, выполняемые после получения запроса от клиента или после получения ответа от проксируемого сервера. Actions могут выполнять асинхронные операции (например запросы к базе данных) и не имеют возвращаемого значения. С сервером Actions общаются путем установки переменных контекста запроса. Контекст запроса предается в качестве параметра при вызове Action. Традиционно имя этого параметра txn. Создадим Action, который будет проверять наличие авторизации Bearer в запросе:
function _M.validate_token_action(txn)
  local auth_header = core.tokenize(txn.sf:hdr("Authorization"), " ")
  if auth_header[1] ~= "Bearer" or not auth_header[2] then
    return txn:set_var("txn.not_authorized", true);
  end
  local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
  if not claim then
    return txn:set_var("txn.not_authorized", true);
  end
  if claim.exp < os.time() then
    return txn:set_var("txn.authentication_timeout", true);
  end
  txn:set_var("txn.jwt_authorized", true);
end

Зарегистрируем этот Action:
core.register_action("validate-token", { "http-req" }, guard.validate_token_action);

Параметр { «http-req» } является триггером, который позволяет использовать этот Action только в контексте http и только на этапе запроса клиента (и запрещает использовать на этапе ответа проксируемого сервера).
В конфигурации Haproxy, Action регистрируется в секции http-request:
frontend jwt
        mode http
        bind *:80
        http-request use-service lua.hello-world   if { path /hello_world }
        http-request lua.validate-token                 if { path -m beg /api/ }

На основании значения переменных, установленных в Action, формируются ACL (Access Control Lists) — ключевой элемент в конфигурациях Haproxy:
acl jwt_authorized  var(txn.jwt_authorized) -m bool
        use_backend app if jwt_authorized { path -m beg /api/ }

Полный листинг конфигурации сервера Haproxy для Action validate-token:
global
        lua-load /usr/local/etc/haproxy/register.lua
frontend jwt
        mode http
        bind *:80
        http-request use-service lua.hello-world   if { path /hello_world }
        http-request lua.validate-token            if { path -m beg /api }
        acl bad_request            var(txn.bad_request)               -m bool
        acl not_authorized         var(txn.not_authorized)            -m bool
        acl authentication_timeout var(txn.authentication_timeout)    -m bool
        acl too_many_request       var(txn.too_many_request)          -m bool
        acl jwt_authorized         var(txn.jwt_authorized)            -m bool
        http-request deny deny_status 400 if bad_request { path -m beg /api/ }
        http-request deny deny_status 401 if !jwt_authorized { path -m beg /api/ } || not_authorized { path -m beg /api/ }
        http-request return status 419 content-type text/html string "Authentication Timeout" if authentication_timeout { path -m beg /api/ }
        http-request deny deny_status 429 if too_many_request { path -m beg /api/  }
        http-request deny deny_status 429 if too_many_request { path -m beg /auth/  }
        use_backend app if { path /hello }
        use_backend app if { path /auth/login }
        use_backend app if jwt_authorized { path -m beg /api/ }
backend app
        mode http
        server app1 app:3000

Fetches
Fetches — это значения которые вычисляются в процессе запроса. Они могут быть только синхронными, и принимают параметры, заданные в конфигурации Haproxy. Например, та же самая проверка авторизации может быть выполнена как Fetch:
function _M.validate_token_fetch(txn)
  local auth_header = core.tokenize(txn.sf:hdr("Authorization"), " ")
  if auth_header[1] ~= "Bearer" or not auth_header[2] then
    return "not_authorized";
  end
  local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
  if not claim then
    return "not_authorized";
  end
  if claim.exp < os.time() then
    return "authentication_timeout";
  end
  return "jwt_authorized:" .. claim.jti;
end
core.register_fetches("validate-token", _M.validate_token_fetch);

Установка ACL по значениям из Fetches задается так:
http-request set-var(txn.validate_token) lua.validate-token()
        acl bad_request var(txn.validate_token) == "bad_request" -m bool
        acl not_authorized var(txn.validate_token) == "not_authorized" -m bool
        acl authentication_timeout var(txn.validate_token) == "authentication_timeout" -m bool
        acl too_many_request var(txn.validate_token) == "too_many_request" -m bool
        acl jwt_authorized var(txn.validate_token) -m beg "jwt_authorized"

Converters
Converters в качестве параметра принимают строку и возвращают значение. Converters, также как и Fetches, могут быть только синхронными и принимают параметры, задаваемые в конфигурации Haproxy. В конфигурации Haproxy Converters отделяются от значения, к которому они применяются, запятой.
Соаздадим Converter, который будет заголовку Authorization преобразовывать в строку:
function _M.validate_token_converter(auth_header_string)
  local auth_header = core.tokenize(auth_header_string, " ")
  if auth_header[1] ~= "Bearer" or not auth_header[2] then
    return "not_authorized";
  end
  local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
  if not claim then
    return "not_authorized";
  end
  if claim.exp < os.time() then
    return "authentication_timeout";
  end
  return "jwt_authorized";
end
core.register_converters("validate-token-converter",  _M.validate_token_converter);

В файле конфигурации использование конвертера задается следующим образом:
http-request set-var(txn.validate_token) hdr(authorization),lua.validate-token-converter

К значениею заголовка Authorization, который извлекается системным Fetch hdr() применяется Converter lua.validate-token-converter.
Stick Table
Stick Table — это хранилище пар ключ-значение, которые оптимизировано для учета количества запросов в единицу времени, и служат, прежде всего, для защиты серверов от атак DDoS или брутфорса (напрмер перебора паролей или выкачки запросами REST больших объемов данных). В паре с такими средствами как Fetches и Converters, эти таблицы могут подсчитывать количество запросов, например, с определенным сессионным cookie или jti, не давая тем самым использовать одну авторизацию для организации распределенной атаки с сотен тысяч устройств. К положительным сторонам Stick Table относится скорость работы и простота конфигурирования. К отрицательным — ограниченное количество регистров для учета значений (всего восемь регистров), потребление памяти, потеря данных после перегрузки сервера Haproxy. Рассмотрим как задаются правила в Stick Table:
stick-table  type string  size 100k  expire 30s store http_req_rate(10s)
        http-request track-sc1 lua.validate-token()
        http-request deny deny_status 429 if { sc_http_req_rate(1) gt 3 }

Строка 1. Создается таблица. В качестве ключа используется значение типа строка. Максимальный размер таблицы 100k. Срок хранения ключа 30 секунд. В качестве значения будут накапливаться количество запросов за последние 10 секунд с одинаковым значением ключа типа строка.
Строка 2. Задается значение ключа, полученного из Fetch lua.validate-token() и регистр 1, в котором будут накапливаться значения (track-sc1)
Строка 3. Если количество запросов с ключом, заданными в строке 2, накопленных в регистре с номером 1 (sc_http_req_rate(1)) превышает 3 — сервер отдает ответ со статусом 429.
Код использованный в данном сообщении доступен в репозитарии. В частности, там есть файл docker-compose.yml, который поможет поднять необходимую для работы среду.
apapacy@gmail.com
5 декабря 2020 года.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_servernaja_optimizatsija (Серверная оптимизация), #_api, #_devops, #_haproxy, #_lua, #_jwt, #_servernaja_optimizatsija (
Серверная оптимизация
)
, #_api, #_devops
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 20-Май 22:39
Часовой пояс: UTC + 5