От PHP к Zig: как я избавился от костылей в биллинг-системе

от автора

Привет, Хабр! Я full-cycle разработчик, работаю с Rust/Zig/Go и увлекаюсь APL как особым способом мышления о коде. На самом деле я пишу на большом количестве языков, но сегодня не об этом. Хочу рассказать об опыте миграции критической бизнес-логики с PHP на Zig и интеграции её в бинарник.

Контекст проекта

Мой установщик дистрибутива, изначально установка была архаичной: распакуй, запусти скрипт и т.д. Здесь всё автоматизировано

Мой установщик дистрибутива, изначально установка была архаичной: распакуй, запусти скрипт и т.д. Здесь всё автоматизировано

По работе мне досталась биллинг-панель для управления VPN-аккаунтами на базе OpenConnect (ocserv). Изначально система была реализована на PHP и представляла собой типичный legacy-код:

  • Отсутствие внятной архитектуры (фактически — организованный хаос)

  • Множество разрозненных скриптов

  • Проблемы с производительностью и стабильностью

Примерная схема исходной ситуации, стрелками обозначены потоки данных

Примерная схема исходной ситуации, стрелками обозначены потоки данных

Первый этап модернизации: переход на Go

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

  1. Вынес критичные компоненты в отдельные сервисы на Go:

    • Проверка срока действия подписок

    • Учет трафика пользователей

    • Фоновые задачи обслуживания

  2. Что это дало:

    • 10-кратный рост производительности ключевых операций

    • Снижение потребления памяти в 5-7 раз

    • Возможность работы в режиме 24/7 без «проседаний»

Новая конструкция: баннеры встроены в ocserv, подсобная логика разнесена по програмам на go — биллинг панель отвечает только за UI, php сведён к минимуму

Новая конструкция: баннеры встроены в ocserv, подсобная логика разнесена по програмам на go — биллинг панель отвечает только за UI, php сведён к минимуму

Сейчас об этом говорить не будем, но если вам интересна тема превращения тыквы в карету — дайте знать в комментариях, могу описать весь процесс трансформации в деталях и по шагам.

Проблема баннерных сообщений

Ограничение ocserv: поддерживает только одно статическое сообщение для всех пользователей. В реальности же требовалась:

  • Персонализация уведомлений

  • Разные сообщения для разных групп пользователей

  • Динамический контент (например, информация об остатке дней подписки)

Решение: гибкая система баннеров

Исходное PHP-решение

Работало через костыли:

  • Требует php-fpm и было связанно с PDO в биллинг панели

  • Нужно следить за правами на файлы и сокет

  • Переодически подвисало

  • Могло не сработать из-за плохой погоды за окном

Я бы сказал, что это хорошее решение для стадии прототипа — когда нужно проверить логику, но не в продакшене.

Новый подход на Zig

Я реализовал:

  1. Статическая библиотека (на Zig):

    • Интеграция напрямую с базой данных

    • Прямая интеграция в ocserv

  2. Преимущества нового решения:

    • Время отклика: 3-5 мс вместо 50-100 мс

    • Никаких лишних файлов

    • Никаких сбоев авторизации из-за баннера

    • Нагрузка на сервер снизилась на 70% (здесь конечно с учётом и переноса логики на go)

    • Количество инцидентов уменьшилось на 100%

Конечно, такую простую логику можно было написать и на C, но это только первый шаг в оптимизации дистрибутива системы. Дальше планируется переписать решение в виде плагина, а так же перенести другие нагруженные части системы в модули ocserv. Например:

  • Проверку срока действия подписок

  • Учёт трафика пользователей и их сессий

  • И т.д.

Исходный код компонента и его логика

Сейчас вы поймёте, почему это критическая бизнес-логика:

  • Во первых уведомления сами по себе несут исключительно важную информацию, вроде: —Ваша подписка истекает через 2 дня и 12 часов

  • Во вторых, в оригинальном решении малейший сбой в скрипте запроса (или банально неправильные права на файл или сокет) и пользователь не сможет подключиться — нужно ли уточнять, что это урон для бизнеса?

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

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

Исходный php-код модуля:

Скрытый текст
<?php  if (!true) die("-------- Test banner --------");  $username = $_SERVER["USERNAME"]; $site = '/var/www/html/panel'; // Подключение функций chdir($site); require_once('include/functions.php');  // Глобальное сообщение (может быть задано или оставлено пустым) $orig_gl_message = $db->query("SELECT * FROM {{table}} LIMIT 1;","global_message","assoc"); if (isset($orig_gl_message) && $orig_gl_message['status'] == true)  $GLOBAL_MESSAGE = $orig_gl_message['message_text'];  // Определение групп пользователей.  $GROUPS = getGroupsWithUsers();  // Сообщения для групп $GROUP_MESSAGES = getGroupMessages();  // Индивидуальные сообщения для пользователей $INDIVIDUAL_MESSAGES = getIndividualMessages();  // Функция для получения сообщения по группе function getGroupMessage($username, $groups, $groupMessages) {     foreach ($groups as $group => $users) {         if (in_array($username, $users)) {             return $groupMessages[$group]; // Возвращаем сообщение для группы         }     }     return null; // Если пользователь не принадлежит ни к одной группе }  // Функция для получения индивидуального сообщения function getIndividualMessage($username, $individualMessages) {     return $individualMessages[$username] ?? null; // Возвращаем индивидуальное сообщение, если оно есть }  // Получаем сообщение для текущего пользователя $groupMessage = getGroupMessage($username, $GROUPS, $GROUP_MESSAGES); $individualMessage = getIndividualMessage($username, $INDIVIDUAL_MESSAGES);   $banner = "";  // Безопасное получение данных пользователя $_username = addcslashes($username, "\\'"); $return = $db->query("SELECT * FROM {{table}} WHERE login='{$_username}';", "users", "assoc");  // 1й приоритет: Глобальное сообщение if (!empty($GLOBAL_MESSAGE)) {     $banner = $GLOBAL_MESSAGE; }  // 2й приоритет: Сообщение для группы elseif ($groupMessage) {     $banner = $groupMessage; }  // 3й приоритет: Индивидуальное сообщение elseif ($individualMessage) {     $banner = $individualMessage; }  // 4й приоритет: Срок ключа сообщение elseif (isset($return["expire"]) && is_string($return["expire"])) { $expire = @json_decode($return["expire"], true); if(is_array($expire)) { $n = 60*60*24; $expire_timestamp = $expire["end"]; // Сохраняем timestamp окончания $expire_start_timestamp = $expire["start"]; // Сохраняем timestamp начала         $expire_seconds = $expire_timestamp - time(); //$days = ($expire_seconds-$expire_seconds%$n)/$n; // Не полный день считается как полный (n + 1) $days = floor($expire_seconds / $n); // Округляем вниз до целого числа дней $hours = floor(($expire_seconds % $n) / (60 * 60)); // Вычисляем часы из остатка $start_date = date('d.m.Y', $expire_start_timestamp); // Получаем дату начала $today = date('d-m-Y'); // Получаем текущую дату $expire_date = date('d.m.Y', $expire_timestamp); // Получаем дату окончания if ($days >= 30 && $days < 31 ) { $messages = [ "Поздравляем! Вы выбрали лучшее. Ваш новый тариф уже активен с {$start_date} года до {$expire_date} года. Наслаждайтесь безопасным интернетом!", "Sizi gutlaýarys! Iň gowy tarifi saýladyňyz. Täze tarifiňiz {$start_date} -- {$expire_date} senesi işjeňdir. Howpsuz we çalt internetden lezzet alyň!"     ]; $banner .= $messages[array_rand($messages)];  } elseif ($days >= 6 && $days < 7) { $messages = [ "Уведомляем вас, что ваш тарифный план активен в период с {$start_date} по {$expire_date}. Желаем хорошего пользования!", "Size {$start_date} -- {$expire_date} seneleri aralygynda tarif planynyzyň işjeňdigi barada habar berýäris. Ulanyşyňyz rahat bolsun!"      ]; $banner .= $messages[array_rand($messages)];            } elseif ($days >= 1 && $days < 2) { $messages = [ "Здравствуйте! Срок действия вашего VPN-ключа истекает {$expire_date}года. Осталось дней: {$days} и часов: {$hours}.", "Hormatly agzamyz! Sizin VPN açaryňyzyň möhleti {$expire_date} senesinde gutarýar. Möhletine {$days} gün we {$hours} sagat galdy."     ]; $banner .= $messages[array_rand($messages)];                }elseif($days >= 0 && $days < 1) { $messages = [         "Внимание! Осталось часов: {$hours} до истечения срока действия вашего VPN-ключа. Период действия: с {$start_date}г по {$expire_date}г.",         "Üns beriň! VPN açaryňyzyň möhletiniň gutarmagyna {$hours} sagat galdy. Möhleti: {$start_date}-den {$expire_date}-e çenli."         ];     $banner .= $messages[array_rand($messages)]; }elseif($days >= -3 && $days <= 0) { $messages = [         "К сожалению, в настоящий момент ваш аккаунт заблокирован из-за нулевого баланса.",         "Bagyşlaň, häzirki wagtda balansyňyz nol bolany üçin hasabyňyz bloklanyldy. "         ];     $banner .= $messages[array_rand($messages)]; } } }  echo($banner); exit(0);

Как это было реализованно в плане интеграции php в ocserv

В папку с исходными кодами ocserv, добавляется C-хидер fpm-client.h:

Скрытый текст
#include <error.h> #include <errno.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h>  #include <fastcgi.h>  int fpm_client(char *fpm_socket_file_path, char *result, char *username) { int ret, len;  int unix_stream_socket; unix_stream_socket = socket(AF_UNIX, SOCK_STREAM, 0); if(unix_stream_socket < 0) { error(0, errno, "Can't create unix socket stream"); return(-1); }  struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; len = strlen(fpm_socket_file_path); memcpy(addr.sun_path, fpm_socket_file_path, len+1);  ret = connect(unix_stream_socket, (const struct sockaddr *) &addr, sizeof(addr)); if(ret < 0){ error(0, errno, "Can't connect to unix socket stream"); return(-1); }   FCGI_Header header; header.version = FCGI_VERSION_1; header.requestIdB1 = 0; header.requestIdB0 = 1; header.paddingLength = 0;  // FCGI_BEGIN_REQUEST  header.type = FCGI_BEGIN_REQUEST;  FCGI_BeginRequestBody begin_request_body; begin_request_body.roleB1 = 0; begin_request_body.roleB0 = FCGI_RESPONDER; begin_request_body.flags = 0;  len = sizeof(begin_request_body); header.contentLengthB1 = 0; header.contentLengthB0 = len;  ret = write(unix_stream_socket, &header, sizeof(header)); if(ret < 0) { error(0, errno, "Can't write `begin request header`"); return(-1); }  ret = write(unix_stream_socket, &begin_request_body, len); if(ret < 0) { error(0, errno, "Can't write `begin request body`"); return(-1); }  // FCGI_PARAMS  header.type = FCGI_PARAMS;  char *params[] = { "SCRIPT_FILENAME", "/etc/ocserv/banner.php", "REQUEST_METHOD", "GET", "USERNAME", "user" }; params[5] = username;  int i, c, nl, vl; char buffer[65536]; c = sizeof(params)/sizeof(params[0]); len = 0; for(i = 0; i < c; i += 2) { nl = strlen(params[i]); vl = strlen(params[i+1]);  memcpy(buffer+len, &nl, 1); len += 1;  memcpy(buffer+len, &vl, 1); len += 1;  memcpy(buffer+len, params[i], nl); len += nl;  memcpy(buffer+len, params[i+1], vl); len += vl; }  header.contentLengthB1 = (len >>8) & 0xff; header.contentLengthB0 = len & 0xff;  ret = write(unix_stream_socket, &header, sizeof(header)); if(ret < 0) { error(0, errno, "Can't write `params request header`"); return(-1); }  ret = write(unix_stream_socket, buffer, len); if(ret < 0) { error(0, errno, "Can't write `params request body`"); return(-1); }  header.contentLengthB1 = 0; header.contentLengthB0 = 0;  ret = write(unix_stream_socket, &header, sizeof(header)); if(ret < 0) { error(0, errno, "Can't write `empty params request header`"); return(-1); }  // FCGI_STDOUT  ret = read(unix_stream_socket, &header, 8); if(ret < 0) { error(0, errno, "Can't read stdout response header"); return(-1); }  len = 0; len = header.contentLengthB1 << 8; len = len | header.contentLengthB0;  memset(buffer, 0, 65536);  ret = read(unix_stream_socket, buffer, len); if(ret < 0) { error(0, errno, "Can't read stdout response body"); return(-1); }  //write(1, buffer, len);  char *offset; offset = strstr(buffer, "\r\n\r\n"); offset += 4;  //printf("\n%s\n", offset);  len = len-(offset-buffer); memcpy(result, offset, len);  ret = close(unix_stream_socket); if(ret < 0) { error(0, errno, "Can't close unix socket stream"); return(-1); }  return(0); } 

Далее вносятся правки в файл worker-auth.c:

Скрытый текст

Накладывается следующий патч (здесь для версии ocserv 0.12.6, в других версия положение может отличаться):

46a47,48 > #include "fpm_client.h" >  995a998,1021 >   char *fpm_socket_file_path = "/var/run/php/php-fpm.sock\0";    char banner[256], username[32];  memset(banner, 0, 256);  memset(username, 0, 32);    int len;  len = strlen(ws->username);  memcpy(username, ws->username, len);    ret = fpm_client(fpm_socket_file_path, banner, username);  /*  int fd = open("/tmp/ocserv.log", O_WRONLY);  char str[256];  sprintf(str, "\nret = %d, banner = %s", ret, banner);  len = strlen(str);  write(fd, str, len);  close(fd);  */    len = strlen(banner);  memcpy(WSCONFIG(ws)->banner, banner, len+1); > 

Этот патч вкатывается в функцию int post_common_handler(worker_st ws, unsigned http_ver, const char imsg)

Перед фрагментом

if (WSCONFIG(ws)->banner) { size =     snprintf(msg, sizeof(msg), "<banner>%s</banner>",      WSCONFIG(ws)->banner); if (size <= 0) goto fail; /* snprintf() returns not a very useful value, so we need to recalculate */ size = strlen(msg); } else { msg[0] = 0; size = 0; }

Дальше остаётся только собрать ocserv, на выходе получаем ocserv и ocserv-worker которые будут связаны с php-fpm для вызова скрипта баннера.

Сейчас мы подходим к основному нарративу статьи: как вызвать Zig-код из C. В гугле полно статей о том, как вызвать C из Zig, чего не скажешь об обратном процессе. Это и побудило меня написать статью.

Новое решение на Zig: логика и исходный код

Так же здесь я добавил дополнительную функцию: проверка на соответствие IP адерса назначеному прокси-серверу.

Подключение для некоторых клиентов разрешается только через прокси-прослойку, и в случае если клиент подключится не через свой прокси или в обход него — его нужно отключить и вывести баннер, о том что такой способ подключения для него запрещён. Отключение было реализовано в status-daemon и успешно работало, а вот баннер не выводился, потому что изначальная версия не передавала в скрипт информацию об IP адресе подключения.

В новом решении я добавил этот баннер, а контроль за отключением оставил как есть — зачем трогать то, что и так хорошо работает?

Само решение делалось в 3 итерации, соответственно в сборочном файле вышло 2 конфигурации сборки:

  • Сборка обычного бинарника для тестирования логики

  • Превращения бинарника в статическую библиотеку

  • Написание простеньких тестов

Для работы с PostgreSQL выбрал pg.zig, нативный драйвер.

var pool = try pg.Pool.init(allocator, .{     .size = 5,     .connect = .{.host = config.host, .port = config.port},     .auth = .{         .username = config.username,         .password = config.password,         .database = config.database,     }, });

На Хабре нет подсветки Zig, пришлось поставить C++ чтобы хоть как-то подсветить синтаксис.

На всякий случай сделал возможность настройки через toml-файл, по большему счёту я использовал это в процессе тестирования.

host = "" port = 5432 username = "" password = "" database = "" pool_size = 5 timeout_ms = 10000

Поскольку toml-конфигурация у меня крайне примитивная, ограничился самописным парсером.

const Config = struct {     host: []const u8 = "localhost",     port: u16 = 5432,     // ... };  fn parseToml(allocator: std.mem.Allocator, data: []const u8) !Config {     // Простой ручной парсинг }

Логика баннеров: прямой порт с PHP, но без динамической типизации.

fn getBanner(allocator: std.mem.Allocator, pool: *Pool, login: []const u8) !?Banner {     // 1. Проверка глобальных сообщений     // 2. Поиск в группах     // 3. Персональные уведомления     // 4. Проверка срока действия }

Ключевые моменты в переходе с PHP

Работа с памятью

PHP:

// Всё просто — выделили и забыли $message = loadMessageFromDB();

Zig:

// Явное управление памятью const message = try allocator.dupe(u8, db_message); defer allocator.free(message);

Использовал стратегию:

  • Арена-аллокатор для краткосрочных объектов

  • Общий пул для долгоживущих структур

  • Детальные проверки в тестах

Интеграция с PostgreSQL

Вместо привычного PDO:

var result = try pool.query(     \\SELECT * FROM messages WHERE active = true , .{}); defer result.deinit();  while (try result.next()) |row| {     const id = row.get(i32, 0);     // ... }

Реализовал:

  • Пуллинг соединений

  • Батчинг запросов

  • Zero-copy парсинг результатов

Обработка ошибок

const maybe_message = getBannerMessage(allocator, pool, login) catch |err| {         std.log.err("Failed to get banner message: {}", .{err});         return false; };

На каждый случай свой обработчик, ошибки возвращаются в ocserv и попадают в его лог.

Исходный код модуля

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

main.zig
const std = @import("std"); const pg = @import("pg");  // Конфигурация подключения к БД const Config = struct {     host: []const u8 = "127.0.0.1",     port: u16 = 5432,     username: []const u8 = "postgres",     password: []const u8 = "postgres",     database: []const u8 = "postgres",     pool_size: u16 = 5,     timeout_ms: u32 = 10_000, };  // Приоритеты баннерных сообщений const MessagePriority = enum {     global,     group,     individual,     expire, };  // Структура баннерного сообщения const BannerMessage = struct {     text: []const u8, // Текст сообщения (выделяется в куче)     priority: MessagePriority, // Приоритет сообщения };  // Данные о сроке действия const ExpireData = struct {     start_timestamp: i64, // Временная метка начала     end_timestamp: i64, // Временная метка окончания     start_date_str: []const u8, // Форматированная дата начала (выделяется в куче)     end_date_str: []const u8, // Форматированная дата окончания (выделяется в куче) };  /// Парсинг TOML конфига (примитивная реализация) /// Выделяет память под строковые значения конфига /// Возвращает ошибку при некорректных числовых значениях fn parseToml(allocator: std.mem.Allocator, data: []const u8) !Config {     var config = Config{};     var lines = std.mem.tokenizeSequence(u8, data, "\n");      while (lines.next()) |line| {         const trimmed = std.mem.trim(u8, line, " \t");         // Пропускаем пустые строки и комментарии         if (trimmed.len == 0 or trimmed[0] == '#') continue;          if (std.mem.indexOf(u8, trimmed, "=")) |eq_pos| {             const key = std.mem.trim(u8, trimmed[0..eq_pos], " \t\"'");             const value = std.mem.trim(u8, trimmed[eq_pos + 1 ..], " \t\"'");              // Обрабатываем каждое поле конфига             if (std.mem.eql(u8, key, "host")) {                 // Выделяем память под строку хоста                 config.host = try allocator.dupe(u8, value);             } else if (std.mem.eql(u8, key, "port")) {                 // Парсим числовое значение порта                 config.port = try std.fmt.parseInt(u16, value, 10);             } else if (std.mem.eql(u8, key, "username")) {                 // Выделяем память под имя пользователя                 config.username = try allocator.dupe(u8, value);             } else if (std.mem.eql(u8, key, "password")) {                 // Выделяем память под пароль                 config.password = try allocator.dupe(u8, value);             } else if (std.mem.eql(u8, key, "database")) {                 // Выделяем память под имя БД                 config.database = try allocator.dupe(u8, value);             } else if (std.mem.eql(u8, key, "pool_size")) {                 // Парсим размер пула соединений                 config.pool_size = try std.fmt.parseInt(u16, value, 10);             } else if (std.mem.eql(u8, key, "timeout_ms")) {                 // Парсим таймаут                 config.timeout_ms = try std.fmt.parseInt(u32, value, 10);             }         }     }      return config; }  /// Получение баннерного сообщения для пользователя /// Выделяет память под текст сообщения и временные данные /// Важно: вызывающая сторона должна освобождать память text в возвращаемом BannerMessage fn getBannerMessage(     allocator: std.mem.Allocator,     pool: *pg.Pool,     login: []const u8, ) !?BannerMessage {     // 1. Проверяем глобальное сообщение     {         // Выполняем запрос к БД (память управляется пулом)         var global_result = try pool.query(             \\SELECT message_text FROM global_message WHERE status = true LIMIT 1         , .{});         defer global_result.deinit(); // Гарантированное освобождение ресурсов запроса          if (try global_result.next()) |row| {             const message = row.get([]const u8, 0);             // Выделяем память под копию сообщения             return BannerMessage{                 .text = try allocator.dupe(u8, message),                 .priority = MessagePriority.global,             };         }     }      // 2. Получаем группы и сообщения     // Выделяем память под структуру групп (нужно освобождать)     var groups = try getGroupsWithUsers(allocator, pool);     defer {         // Ручное освобождение сложной структуры данных         var it = groups.iterator();         while (it.next()) |entry| {             // Освобождаем каждого пользователя в группе             for (entry.value_ptr.items) |user| {                 allocator.free(user);             }             entry.value_ptr.deinit(); // Освобождаем ArrayList             allocator.free(entry.key_ptr.*); // Освобождаем название группы         }         groups.deinit(); // Освобождаем хеш-мап     }      // Получаем сообщения для групп (нужно освобождать)     var group_messages = try getGroupMessages(allocator, pool);     defer {         // Освобождаем хеш-мап с сообщениями групп         var it = group_messages.iterator();         while (it.next()) |entry| {             allocator.free(entry.key_ptr.*); // Название группы             allocator.free(entry.value_ptr.*); // Текст сообщения         }         group_messages.deinit();     }      // Проверяем сообщение группы     if (findGroupForUser(login, &groups)) |group_name| {         if (group_messages.get(group_name)) |message| {             // Выделяем память под копию сообщения             return BannerMessage{                 .text = try allocator.dupe(u8, message),                 .priority = MessagePriority.group,             };         }     }      // 3. Проверяем индивидуальные сообщения (нужно освобождать)     var individual_messages = try getIndividualMessages(allocator, pool);     defer {         // Освобождаем хеш-мап с индивидуальными сообщениями         var it = individual_messages.iterator();         while (it.next()) |entry| {             allocator.free(entry.key_ptr.*); // Логин пользователя             allocator.free(entry.value_ptr.*); // Текст сообщения         }         individual_messages.deinit();     }      if (individual_messages.get(login)) |message| {         // Выделяем память под копию сообщения         return BannerMessage{             .text = try allocator.dupe(u8, message),             .priority = MessagePriority.individual,         };     }      // 4. Проверяем срок действия ключа     {         var user_result = try pool.query(             \\SELECT expire FROM users WHERE login = $1 LIMIT 1         , .{login});         defer user_result.deinit(); // Гарантированное освобождение          if (try user_result.next()) |row| {             const expire_json = row.get(?[]const u8, 0);             if (expire_json) |json| {                 const now = std.time.timestamp();                 // Парсим JSON (выделяет память под строки дат)                 const expire = try parseExpireJson(allocator, json);                 defer {                     // Освобождаем временные строки дат                     allocator.free(expire.start_date_str);                     allocator.free(expire.end_date_str);                 }                  // Рассчитываем оставшееся время                 const seconds_left = expire.end_timestamp - now;                 const days_left = @divTrunc(seconds_left, 86400);                 const hours_left = @divTrunc(@mod(seconds_left, 86400), 3600);                  // Формируем сообщения в зависимости от оставшегося времени                 if (days_left >= 30 and days_left < 31) {                     const msg1 = try std.fmt.allocPrint(allocator, "Поздравляем! Вы выбрали лучшее. Ваш новый тариф уже активен с {s} года до {s} года. Наслаждайтесь безопасным интернетом!", .{ expire.start_date_str, expire.end_date_str });                     defer allocator.free(msg1);                      const msg2 = try std.fmt.allocPrint(allocator, "Sizi gutlaýarys! Iň gowy tarifi saýladyňyz. Täze tarifiňiz {s} -- {s} senesi işjeňdir. Howpsuz we çalt internetden lezzet alyň!", .{ expire.start_date_str, expire.end_date_str });                     defer allocator.free(msg2);                      const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;                     return BannerMessage{                         .text = try allocator.dupe(u8, chosen),                         .priority = MessagePriority.expire,                     };                 } else if (days_left >= 6 and days_left < 7) {                     const msg1 = try std.fmt.allocPrint(allocator, "Уведомляем вас, что ваш тарифный план активен в период с {s} по {s}. Желаем хорошего пользования!", .{ expire.start_date_str, expire.end_date_str });                     defer allocator.free(msg1);                      const msg2 = try std.fmt.allocPrint(allocator, "Size {s} -- {s} seneleri aralygynda tarif planynyzyň işjeňdigi barada habar berýäris. Ulanyşyňyz rahat bolsun!", .{ expire.start_date_str, expire.end_date_str });                     defer allocator.free(msg2);                      const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;                     return BannerMessage{                         .text = try allocator.dupe(u8, chosen),                         .priority = MessagePriority.expire,                     };                 } else if (days_left >= 1 and days_left < 2) {                     const msg1 = try std.fmt.allocPrint(allocator, "Здравствуйте! Срок действия вашего VPN-ключа истекает {s} года. Осталось дней: {} и часов: {}.", .{ expire.end_date_str, days_left, hours_left });                     defer allocator.free(msg1);                      const msg2 = try std.fmt.allocPrint(allocator, "Hormatly agzamyz! Sizin VPN açaryňyzyň möhleti {s} senesinde gutarýar. Möhletine {} gün we {} sagat galdy.", .{ expire.end_date_str, days_left, hours_left });                     defer allocator.free(msg2);                      const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;                     return BannerMessage{                         .text = try allocator.dupe(u8, chosen),                         .priority = MessagePriority.expire,                     };                 } else if (days_left >= 0 and days_left < 1) {                     const msg1 = try std.fmt.allocPrint(allocator, "Внимание! Осталось часов: {} до истечения срока действия вашего VPN-ключа. Период действия: с {s}г по {s}г.", .{ hours_left, expire.start_date_str, expire.end_date_str });                     defer allocator.free(msg1);                      const msg2 = try std.fmt.allocPrint(allocator, "Üns beriň! VPN açaryňyzyň möhletiniň gutarmagyna {} sagat galdy. Möhleti: {s}-den {s}-e çenli.", .{ hours_left, expire.start_date_str, expire.end_date_str });                     defer allocator.free(msg2);                      const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;                     return BannerMessage{                         .text = try allocator.dupe(u8, chosen),                         .priority = MessagePriority.expire,                     };                 } else if (days_left >= -3 and days_left < 0) {                     const msg1: []const u8 = "К сожалению, в настоящий момент ваш аккаунт заблокирован из-за нулевого баланса.";                      const msg2: []const u8 = "Bagyşlaň, häzirki wagtda balansyňyz nol bolany üçin hasabyňyz bloklanyldy.";                      const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;                     return BannerMessage{                         .text = try allocator.dupe(u8, chosen),                         .priority = MessagePriority.expire,                     };                 }             }         }     }      return null; // Сообщение не найдено }  /// Парсинг JSON с датами истечения /// Выделяет память под строки дат fn parseExpireJson(allocator: std.mem.Allocator, json_str: []const u8) !ExpireData {     // Парсим JSON (использует временную память из аллокатора)     const parsed = try std.json.parseFromSlice(struct {         start: i64,         end: i64,     }, allocator, json_str, .{});     defer parsed.deinit(); // Освобождаем ресурсы парсера      const start_timestamp = parsed.value.start;     const end_timestamp = parsed.value.end;      // Форматируем даты (выделяем память под строки)     const start_date_str = try formatTimestamp(allocator, start_timestamp);     const end_date_str = try formatTimestamp(allocator, end_timestamp);      return ExpireData{         .start_timestamp = start_timestamp,         .end_timestamp = end_timestamp,         .start_date_str = start_date_str,         .end_date_str = end_date_str,     }; }  /// Форматирование временной метки в строку /// Выделяет память под результирующую строку fn formatTimestamp(allocator: std.mem.Allocator, timestamp: i64) ![]const u8 {     const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(timestamp) };     const epoch_day = epoch_seconds.getEpochDay();     const year_day = epoch_day.calculateYearDay();     const month_day = year_day.calculateMonthDay();      var buffer: [32]u8 = undefined;     const formatted = try std.fmt.bufPrint(&buffer, "{d:0>2}.{d:0>2}.{d}", .{         month_day.day_index + 1,         @intFromEnum(month_day.month) + 1,         year_day.year,     });      // Возвращаем копию строки в куче     return allocator.dupe(u8, formatted); }  /// Поиск группы для пользователя /// Не выделяет новую память, только ищет по существующим данным fn findGroupForUser(login: []const u8, groups: *std.StringArrayHashMap(std.ArrayList([]const u8))) ?[]const u8 {     var it = groups.iterator();     while (it.next()) |entry| {         for (entry.value_ptr.items) |user| {             if (std.mem.eql(u8, user, login)) {                 return entry.key_ptr.*; // Возвращаем существующую строку             }         }     }     return null; }  /// Получение групп с пользователями /// Выделяет память под структуры данных и строки fn getGroupsWithUsers(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap(std.ArrayList([]const u8)) {     var groups = std.StringArrayHashMap(std.ArrayList([]const u8)).init(allocator);      var result = try pool.query(         \\SELECT g.name AS group_name, u.login AS user_login         \\FROM groups g         \\JOIN group_users gu ON g.id = gu.group_id         \\JOIN users u ON gu.user_id = u.id         \\WHERE g.message_status = true         \\ORDER BY g.name, u.login     , .{});     defer result.deinit(); // Гарантированное освобождение      while (try result.next()) |row| {         const group_name = row.get([]const u8, 0);         const user_login = row.get([]const u8, 1);          // Выделяем память под копии строк         const owned_group_name = try allocator.dupe(u8, group_name);         const owned_user_login = try allocator.dupe(u8, user_login);          if (groups.getPtr(owned_group_name)) |users_list| {             // Добавляем пользователя в существующую группу             try users_list.append(owned_user_login);         } else {             // Создаем новую группу с пользователем             var users = std.ArrayList([]const u8).init(allocator);             try users.append(owned_user_login);             try groups.put(owned_group_name, users);         }     }      return groups; }  /// Получение сообщений для групп /// Выделяет память под структуры данных и строки fn getGroupMessages(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap([]const u8) {     var messages = std.StringArrayHashMap([]const u8).init(allocator);      var result = try pool.query(         \\SELECT g.name AS group_name, gm.message_text AS message_text         \\FROM group_messages gm         \\JOIN groups g ON gm.group_id = g.id         \\WHERE g.message_status = true         \\ORDER BY g.name     , .{});     defer result.deinit();      while (try result.next()) |row| {         const group_name = row.get([]const u8, 0);         const message_text = row.get([]const u8, 1);          // Выделяем память под копии строк         const owned_group_name = try allocator.dupe(u8, group_name);         const owned_message = try allocator.dupe(u8, message_text);          try messages.put(owned_group_name, owned_message);     }      return messages; }  /// Получение индивидуальных сообщений /// Выделяет память под структуры данных и строки fn getIndividualMessages(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap([]const u8) {     var messages = std.StringArrayHashMap([]const u8).init(allocator);      var result = try pool.query(         \\SELECT u.login AS user_login, um.message_text AS message_text         \\FROM user_messages um         \\JOIN users u ON um.user_id = u.id         \\WHERE u.accept_messages = true         \\ORDER BY u.login     , .{});     defer result.deinit();      while (try result.next()) |row| {         const user_login = row.get([]const u8, 0);         const message_text = row.get([]const u8, 1);          // Выделяем память под копии строк         const owned_login = try allocator.dupe(u8, user_login);         const owned_message = try allocator.dupe(u8, message_text);          try messages.put(owned_login, owned_message);     }      return messages; }  pub fn main() !void {     // Инициализация аллокатора (освобождается при выходе)     var gpa = std.heap.GeneralPurposeAllocator(.{}){};     defer _ = gpa.deinit(); // Проверка утечек при выходе     const allocator = gpa.allocator();      // Чтение аргументов командной строки (нужно освобождать)     const args = try std.process.argsAlloc(allocator);     defer std.process.argsFree(allocator, args);      if (args.len < 2) {         std.log.err("Usage: {s} <login> [config_path]", .{args[0]});         return error.MissingArguments;     }     const login = args[1];     const config_path = if (args.len > 2) args[2] else "config.toml";      // Чтение конфига (выделяет память под содержимое файла)     const config_file = try std.fs.cwd().readFileAlloc(allocator, config_path, 1 << 20);     defer allocator.free(config_file); // Освобождаем после использования      // Парсинг конфига (выделяет память под строки)     const config = try parseToml(allocator, config_file);     defer {         // Освобождаем все строки конфига         allocator.free(config.host);         allocator.free(config.username);         allocator.free(config.password);         allocator.free(config.database);     }      // Инициализация пула соединений (нужно освобождать)     var pool = try pg.Pool.init(allocator, .{         .size = config.pool_size,         .connect = .{             .port = config.port,             .host = config.host,         },         .auth = .{             .username = config.username,             .database = config.database,             .password = config.password,             .timeout = config.timeout_ms,         },     });     defer pool.deinit(); // Закрываем все соединения при выходе      // Проверка существования пользователя     var user_result = try pool.query(         \\SELECT active, to_rm, proxy_server_id          \\FROM users          \\WHERE login = $1         \\LIMIT 1     , .{login});     defer user_result.deinit(); // Гарантированное освобождение      if (try user_result.next()) |row| {         const active = row.get(bool, 0);         const to_rm = row.get(bool, 1);         const proxy_server_id = row.get(?i32, 2);          std.log.info("User found: active={}, to_rm={}", .{ active, to_rm });          // Проверка прокси-сервера         if (proxy_server_id) |proxy_id| {             std.log.info("User has proxy server with id={}", .{proxy_id});              var proxy_result = try pool.query(                 \\SELECT host(ip) as ip_text                  \\FROM proxy_servers                  \\WHERE id = $1 and strong = true                 \\LIMIT 1             , .{proxy_id});             defer proxy_result.deinit();              if (try proxy_result.next()) |proxy_row| {                 const ip = proxy_row.get([]const u8, 0);                 std.log.info("Proxy server IP: {s}", .{ip});             } else {                 std.log.warn("Proxy server with id={} not found", .{proxy_id});             }         } else {             std.log.info("User has no proxy server", .{});         }          // Получаем баннерное сообщение (нужно освобождать!)         if (try getBannerMessage(allocator, pool, login)) |banner| {             defer allocator.free(banner.text); // Освобождаем текст сообщения             std.log.info("Banner message (priority: {}): {s}", .{ banner.priority, banner.text });         } else {             std.log.info("No banner message for user", .{});         }     } else {         std.log.err("User with login '{s}' not found", .{login});     } } 

Здесь мы получаем программу которая принимает 2 аргумента: логин пользователя и путь к файлу конфигурации, выводит его баннер.

Так же мы проверяем, назначен ли пользователю прокси-сервер с режимом «strong» (если назначен, то подключения с других адресов для него недопустимы).

Полный код статической библиотеки с комментариями:

banner_check.zig
// Импорт стандартной библиотеки и модуля PostgreSQL const std = @import("std"); const pg = @import("pg");  // Конфигурация подключения к PostgreSQL const PgConfig = struct {     host: []const u8 = "localhost",  // Хост БД (выделяется динамически)     port: u16 = 5432,               // Порт БД     username: []const u8 = "postgres", // Логин (выделяется динамически)     password: []const u8 = "postgres", // Пароль (выделяется динамически)     database: []const u8 = "ocserv",   // Имя БД (выделяется динамически) };  // Приоритеты баннерных сообщений pub const MessagePriority = enum {     global,      // Глобальное сообщение (высший приоритет)     group,       // Групповое сообщение     individual,  // Индивидуальное сообщение     expire,      // Сообщение об истечении срока (низший приоритет) };  // Структура баннерного сообщения pub const BannerMessage = struct {     text: []const u8,         // Текст сообщения (выделяется динамически)     priority: MessagePriority, // Приоритет сообщения };  // Данные о сроке действия pub const ExpireData = struct {     start_timestamp: i64,     // Время начала (timestamp)     end_timestamp: i64,       // Время окончания (timestamp)     start_date_str: []const u8, // Форматированная дата начала (выделяется динамически)     end_date_str: []const u8,  // Форматированная дата окончания (выделяется динамически) };  // C-интерфейс для проверки прокси-сервера пользователя // Возвращает true, если подключение разрешено, false - если запрещено pub export fn check_user_proxy(     username: [*:0]const u8, // Входной параметр: логин пользователя (null-terminated строка)     remote_addr: [*:0]const u8, // Входной параметр: IP-адрес клиента (null-terminated строка)     proxy_ip_out: [*:0]u8,    // Выходной буфер для IP прокси-сервера (минимум 256 байт) ) callconv(.C) bool {     // Инициализируем GeneralPurposeAllocator для управления памятью     var gpa = std.heap.GeneralPurposeAllocator(.{}){};     // Гарантируем проверку утечек памяти при выходе из функции     defer _ = gpa.deinit();     const allocator = gpa.allocator();      // Инициализируем выходной буфер нулями     @memset(proxy_ip_out[0..256], 0);      // Логируем входные параметры     std.log.debug("Checking proxy for user: {s}, IP: {s}", .{ username, remote_addr });      // 1. Читаем конфигурационный файл     const config_file = std.fs.cwd().readFileAlloc(allocator, "/etc/ocserv/banner.toml", 1 << 20) catch |err| {         std.log.err("Failed to read config: {}", .{err});         return true; // При ошибке разрешаем подключение     };     // Гарантируем освобождение памяти файла конфигурации     defer allocator.free(config_file);      // Парсим TOML конфиг     const pg_config = parse_toml(allocator, config_file) catch |err| {         std.log.err("Invalid TOML config: {}", .{err});         return true; // При ошибке разрешаем подключение     };     // Гарантируем освобождение строк конфигурации     defer {         allocator.free(pg_config.host);         allocator.free(pg_config.username);         allocator.free(pg_config.password);         allocator.free(pg_config.database);     }      // 2. Инициализируем пул соединений с PostgreSQL     var pool = pg.Pool.init(allocator, .{         .size = 3, // Максимальное количество соединений в пуле         .connect = .{             .port = pg_config.port,             .host = pg_config.host,         },         .auth = .{             .username = pg_config.username,             .database = pg_config.database,             .password = pg_config.password,             .timeout = 5000, // Таймаут подключения 5 секунд         },     }) catch |err| {         std.log.err("DB connection failed: {}", .{err});         return true; // При ошибке разрешаем подключение     };     // Гарантируем закрытие пула соединений при выходе     defer pool.deinit();      // 3. Проверяем наличие пользователя в базе     var user_result = pool.query(         \\SELECT proxy_server_id FROM users WHERE login = $1 LIMIT 1     , .{std.mem.span(username)}) catch |err| {         std.log.err("User query failed: {}", .{err});         return true;     };     // Гарантируем освобождение ресурсов запроса     defer user_result.deinit();      // Получаем первую строку результата     const user_row = user_result.next() catch |err| {         std.log.err("User query iteration failed: {}", .{err});         return true;     } orelse {         std.log.err("User not found: {s}", .{std.mem.span(username)});         return true;     };      // Получаем ID прокси-сервера пользователя (может быть null)     const proxy_id: ?i32 = user_row.get(?i32, 0);     if (proxy_id) |id| {         std.log.debug("User proxy_id: {}", .{id});     } else {         std.log.debug("User has no proxy_id", .{});     }      // 4. Если прокси не назначен - разрешаем подключение     if (proxy_id == null) return true;      // 5. Получаем IP прокси-сервера из базы данных     var proxy_ip_result = pool.query(         \\SELECT host(ip) FROM proxy_servers WHERE id = $1 AND strong = true LIMIT 1     , .{proxy_id}) catch |err| {         std.log.err("Proxy query failed: {}", .{err});         return true;     };     // Гарантируем освобождение ресурсов запроса     defer proxy_ip_result.deinit();      // Получаем первую строку результата     const proxy_ip_row = proxy_ip_result.next() catch |err| {         std.log.err("Proxy IP query iteration failed: {}", .{err});         return true;     } orelse {         std.log.err("Proxy not found or not strong: id={}", .{proxy_id.?});         return true;     };      // Получаем IP-адрес прокси     const proxy_ip = proxy_ip_row.get([]const u8, 0);     if (proxy_ip.len == 0) {         std.log.err("Empty proxy IP for proxy_id={}", .{proxy_id.?});         return true;     }      std.log.debug("Found proxy IP: {s}", .{proxy_ip});      // Копируем proxy IP в выходной буфер (не более 255 символов + null-terminator)     const copy_len = @min(proxy_ip.len, 255);     @memcpy(proxy_ip_out[0..copy_len], proxy_ip[0..copy_len]);     proxy_ip_out[copy_len] = 0; // Добавляем null-terminator     std.log.debug("Copied proxy IP to output: '{s}'", .{proxy_ip_out[0..copy_len]});      // 6. Сравниваем IP-адреса клиента и прокси     const client_ip = std.mem.span(remote_addr);     std.log.debug("Comparing client IP: {s} with proxy IP: {s}", .{ client_ip, proxy_ip });      // Парсим IP-адреса для сравнения     const proxy_addr = std.net.Address.parseIp(proxy_ip, 0) catch |err| {         std.log.err("Invalid proxy IP '{s}': {}", .{ proxy_ip, err });         return true;     };     const client_addr = std.net.Address.parseIp(client_ip, 0) catch |err| {         std.log.err("Invalid client IP '{s}': {}", .{ client_ip, err });         return true;     };      // Сравниваем бинарные представления адресов     const equal = std.mem.eql(u8, std.mem.asBytes(&proxy_addr.in.sa.addr), std.mem.asBytes(&client_addr.in.sa.addr));     std.log.debug("IP comparison result: {}", .{equal});     return equal; }  // C-интерфейс для получения баннера пользователя pub export fn get_banner(     username: [*:0]const u8, // Входной параметр: имя пользователя     banner_out: [*:0]u8,     // Выходной буфер для баннера     banner_len: usize,       // Размер выходного буфера ) callconv(.C) bool {       // Возвращает true если баннер получен     // Инициализируем аллокатор     var gpa = std.heap.GeneralPurposeAllocator(.{}){};     defer _ = gpa.deinit();     const allocator = gpa.allocator();      // Инициализируем выходной буфер нулями     @memset(banner_out[0..banner_len], 0);      std.log.debug("Getting banner for user: {s}", .{username});      // 1. Читаем конфигурационный файл     const config_file = std.fs.cwd().readFileAlloc(allocator, "/etc/ocserv/banner.toml", 1 << 20) catch |err| {         std.log.err("Failed to read config: {}", .{err});         return false;     };     // Гарантируем освобождение памяти файла конфигурации     defer allocator.free(config_file);      // Парсим TOML конфиг     const pg_config = parse_toml(allocator, config_file) catch |err| {         std.log.err("Invalid TOML config: {}", .{err});         return false;     };     // Гарантируем освобождение строк конфигурации     defer {         allocator.free(pg_config.host);         allocator.free(pg_config.username);         allocator.free(pg_config.password);         allocator.free(pg_config.database);     }      // 2. Инициализируем пул соединений с PostgreSQL     var pool = pg.Pool.init(allocator, .{         .size = 3,         .connect = .{             .port = pg_config.port,             .host = pg_config.host,         },         .auth = .{             .username = pg_config.username,             .database = pg_config.database,             .password = pg_config.password,             .timeout = 5000,         },     }) catch |err| {         std.log.err("DB connection failed: {}", .{err});         return false;     };     // Гарантируем закрытие пула соединений при выходе     defer pool.deinit();      // 3. Получаем баннерное сообщение для пользователя     const login = std.mem.span(username);     const maybe_message = getBannerMessage(allocator, pool, login) catch |err| {         std.log.err("Failed to get banner message: {}", .{err});         return false;     };      // Если сообщение найдено     if (maybe_message) |message| {         // Гарантируем освобождение текста сообщения (кроме expire, который использует статические строки)         defer {             if (message.priority != .expire) {                 allocator.free(message.text);             }         }          // Копируем сообщение в выходной буфер         const copy_len = @min(message.text.len, banner_len - 1);         @memcpy(banner_out[0..copy_len], message.text[0..copy_len]);         banner_out[copy_len] = 0; // Добавляем null-terminator         std.log.debug("Copied banner message: '{s}'", .{banner_out[0..copy_len]});         return true;     }      return false; }  // Получение баннерного сообщения для пользователя fn getBannerMessage(     allocator: std.mem.Allocator, // Аллокатор для временных данных     pool: *pg.Pool,              // Пул соединений с БД     login: []const u8,           // Логин пользователя ) !?BannerMessage {     // 1. Проверяем глобальное сообщение (наивысший приоритет)     {         var global_result = try pool.query(             \\SELECT message_text FROM global_message WHERE status = true LIMIT 1         , .{});         defer global_result.deinit(); // Гарантируем освобождение ресурсов запроса          if (try global_result.next()) |row| {             const message = row.get([]const u8, 0);             // Создаем копию сообщения для возврата             return BannerMessage{                 .text = try allocator.dupe(u8, message),                 .priority = MessagePriority.global,             };         }     }      // 2. Получаем группы пользователей и сообщения групп     var groups = try getGroupsWithUsers(allocator, pool);     // Гарантируем освобождение памяти групп     defer {         var it = groups.iterator();         while (it.next()) |entry| {             // Освобождаем имена пользователей в группе             for (entry.value_ptr.items) |user| {                 allocator.free(user);             }             entry.value_ptr.deinit(); // Освобождаем список пользователей             allocator.free(entry.key_ptr.*); // Освобождаем имя группы         }         groups.deinit(); // Освобождаем хэш-мап групп     }      // Получаем сообщения групп     var group_messages = try getGroupMessages(allocator, pool);     // Гарантируем освобождение памяти сообщений групп     defer {         var it = group_messages.iterator();         while (it.next()) |entry| {             allocator.free(entry.key_ptr.*); // Освобождаем имя группы             allocator.free(entry.value_ptr.*); // Освобождаем текст сообщения         }         group_messages.deinit();     }      // Проверяем сообщение группы пользователя     if (findGroupForUser(login, &groups)) |group_name| {         if (group_messages.get(group_name)) |message| {             // Создаем копию сообщения для возврата             return BannerMessage{                 .text = try allocator.dupe(u8, message),                 .priority = MessagePriority.group,             };         }     }      // 3. Проверяем индивидуальные сообщения     var individual_messages = try getIndividualMessages(allocator, pool);     // Гарантируем освобождение памяти индивидуальных сообщений     defer {         var it = individual_messages.iterator();         while (it.next()) |entry| {             allocator.free(entry.key_ptr.*); // Освобождаем логин пользователя             allocator.free(entry.value_ptr.*); // Освобождаем текст сообщения         }         individual_messages.deinit();     }      // Если есть индивидуальное сообщение для пользователя     if (individual_messages.get(login)) |message| {         // Создаем копию сообщения для возврата         return BannerMessage{             .text = try allocator.dupe(u8, message),             .priority = MessagePriority.individual,         };     }      // 4. Проверяем срок действия ключа (низший приоритет)     {         var user_result = try pool.query(             \\SELECT expire FROM users WHERE login = $1 LIMIT 1         , .{login});         defer user_result.deinit(); // Гарантируем освобождение ресурсов запроса          if (try user_result.next()) |row| {             const expire_json = row.get(?[]const u8, 0);             if (expire_json) |json| {                 // Парсим JSON с датами                 const expire = try parseExpireJson(allocator, json);                 // Гарантируем освобождение форматированных дат                 defer {                     allocator.free(expire.start_date_str);                     allocator.free(expire.end_date_str);                 }                  // Вычисляем оставшееся время                 const now = std.time.timestamp();                 const seconds_left = expire.end_timestamp - now;                 const days_left = @divTrunc(seconds_left, 86400);                 const hours_left = @divTrunc(@mod(seconds_left, 86400), 3600);                  // Формируем сообщение в зависимости от оставшегося времени                 if (days_left >= 30 and days_left < 31) {                     // Сообщение о новом тарифе (русский и туркменский варианты)                     const msg1 = try std.fmt.allocPrint(allocator,                          "Поздравляем! Вы выбрали лучшее. Ваш новый тариф уже активен с {s} года до {s} года. Наслаждайтесь безопасным интернетом!",                          .{ expire.start_date_str, expire.end_date_str });                     defer allocator.free(msg1);                      const msg2 = try std.fmt.allocPrint(allocator,                          "Sizi gutlaýarys! Iň gowy tarifi saýladyňyz. Täze tarifiňiz {s} -- {s} senesi işjeňdir. Howpsuz we çalt internetden lezzet alyň!",                          .{ expire.start_date_str, expire.end_date_str });                     defer allocator.free(msg2);                      // Случайный выбор языка сообщения                     const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;                     return BannerMessage{                         .text = try allocator.dupe(u8, chosen),                         .priority = MessagePriority.expire,                     };                 }                  // ... (аналогичные блоки для других временных интервалов)                 else if (days_left >= -3 and days_left < 0) {                     // Сообщение о заблокированном аккаунте                     const msg1: []const u8 = "К сожалению, в настоящий момент ваш аккаунт заблокирован из-за нулевого баланса.";                     const msg2: []const u8 = "Bagyşlaň, häzirki wagtda balansyňyz nol bolany üçin hasabyňyz bloklanyldy.";                      const chosen = if (std.crypto.random.int(u1) == 0) msg1 else msg2;                     return BannerMessage{                         .text = try allocator.dupe(u8, chosen),                         .priority = MessagePriority.expire,                     };                 }             }         }     }      return null; // Сообщение не найдено }  // Вспомогательные функции  // Парсинг JSON с датами истечения срока fn parseExpireJson(allocator: std.mem.Allocator, json_str: []const u8) !ExpireData {     const parsed = try std.json.parseFromSlice(struct {         start: i64,         end: i64,     }, allocator, json_str, .{});     defer parsed.deinit(); // Гарантируем освобождение ресурсов парсера      const start_timestamp = parsed.value.start;     const end_timestamp = parsed.value.end;      // Форматируем даты в строки     const start_date_str = try formatTimestamp(allocator, start_timestamp);     const end_date_str = try formatTimestamp(allocator, end_timestamp);      return ExpireData{         .start_timestamp = start_timestamp,         .end_timestamp = end_timestamp,         .start_date_str = start_date_str,         .end_date_str = end_date_str,     }; }  // Форматирование временной метки в строку (DD.MM.YYYY) fn formatTimestamp(allocator: std.mem.Allocator, timestamp: i64) ![]const u8 {     // Конвертируем timestamp в компоненты даты     const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(timestamp) };     const epoch_day = epoch_seconds.getEpochDay();     const year_day = epoch_day.calculateYearDay();     const month_day = year_day.calculateMonthDay();      // Форматируем в буфер     var buffer: [32]u8 = undefined;     const formatted = try std.fmt.bufPrint(&buffer, "{d:0>2}.{d:0>2}.{d}", .{         month_day.day_index + 1, // День (1-based)         @intFromEnum(month_day.month) + 1, // Месяц (1-based)         year_day.year, // Год     });      // Возвращаем копию строки     return allocator.dupe(u8, formatted); }  // Поиск группы для пользователя fn findGroupForUser(login: []const u8, groups: *std.StringArrayHashMap(std.ArrayList([]const u8))) ?[]const u8 {     var it = groups.iterator();     while (it.next()) |entry| {         for (entry.value_ptr.items) |user| {             if (std.mem.eql(u8, user, login)) {                 return entry.key_ptr.*; // Возвращаем имя группы             }         }     }     return null; // Группа не найдена }  // Получение списка групп с пользователями fn getGroupsWithUsers(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap(std.ArrayList([]const u8)) {     var groups = std.StringArrayHashMap(std.ArrayList([]const u8)).init(allocator);      var result = try pool.query(         \\SELECT g.name AS group_name, u.login AS user_login         \\FROM groups g         \\JOIN group_users gu ON g.id = gu.group_id         \\JOIN users u ON gu.user_id = u.id         \\WHERE g.message_status = true         \\ORDER BY g.name, u.login     , .{});     defer result.deinit(); // Гарантируем освобождение ресурсов запроса      // Обрабатываем каждую строку результата     while (try result.next()) |row| {         const group_name = row.get([]const u8, 0);         const user_login = row.get([]const u8, 1);          // Создаем копии строк для хранения в хэш-мапе         const owned_group_name = try allocator.dupe(u8, group_name);         const owned_user_login = try allocator.dupe(u8, user_login);          // Добавляем пользователя в соответствующую группу         if (groups.getPtr(owned_group_name)) |users_list| {             try users_list.append(owned_user_login);         } else {             var users = std.ArrayList([]const u8).init(allocator);             try users.append(owned_user_login);             try groups.put(owned_group_name, users);         }     }      return groups; }  // Получение сообщений групп fn getGroupMessages(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap([]const u8) {     var messages = std.StringArrayHashMap([]const u8).init(allocator);      var result = try pool.query(         \\SELECT g.name AS group_name, gm.message_text AS message_text         \\FROM group_messages gm         \\JOIN groups g ON gm.group_id = g.id         \\WHERE g.message_status = true         \\ORDER BY g.name     , .{});     defer result.deinit(); // Гарантируем освобождение ресурсов запроса      // Обрабатываем каждую строку результата     while (try result.next()) |row| {         const group_name = row.get([]const u8, 0);         const message_text = row.get([]const u8, 1);          // Создаем копии строк для хранения в хэш-мапе         const owned_group_name = try allocator.dupe(u8, group_name);         const owned_message = try allocator.dupe(u8, message_text);          try messages.put(owned_group_name, owned_message);     }      return messages; }  // Получение индивидуальных сообщений пользователей fn getIndividualMessages(allocator: std.mem.Allocator, pool: *pg.Pool) !std.StringArrayHashMap([]const u8) {     var messages = std.StringArrayHashMap([]const u8).init(allocator);      var result = try pool.query(         \\SELECT u.login AS user_login, um.message_text AS message_text         \\FROM user_messages um         \\JOIN users u ON um.user_id = u.id         \\WHERE u.accept_messages = true         \\ORDER BY u.login     , .{});     defer result.deinit(); // Гарантируем освобождение ресурсов запроса      // Обрабатываем каждую строку результата     while (try result.next()) |row| {         const user_login = row.get([]const u8, 0);         const message_text = row.get([]const u8, 1);          // Создаем копии строк для хранения в хэш-мапе         const owned_login = try allocator.dupe(u8, user_login);         const owned_message = try allocator.dupe(u8, message_text);          try messages.put(owned_login, owned_message);     }      return messages; }  // Парсинг TOML конфига (упрощенная реализация) fn parse_toml(allocator: std.mem.Allocator, data: []const u8) !PgConfig {     var config = PgConfig{};     var lines = std.mem.tokenizeSequence(u8, data, "\n");      // Обрабатываем каждую строку файла     while (lines.next()) |line| {         const trimmed = std.mem.trim(u8, line, " \t");         // Пропускаем пустые строки и комментарии         if (trimmed.len == 0 or trimmed[0] == '#') continue;          // Разбираем строки вида "ключ = значение"         if (std.mem.indexOf(u8, trimmed, "=")) |eq_pos| {             const key = std.mem.trim(u8, trimmed[0..eq_pos], " \t\"'");             const value = std.mem.trim(u8, trimmed[eq_pos + 1 ..], " \t\"'");              // Обрабатываем каждый ключ             if (std.mem.eql(u8, key, "host")) {                 config.host = try allocator.dupe(u8, value); // Выделяем память для хоста             } else if (std.mem.eql(u8, key, "port")) {                 config.port = try std.fmt.parseInt(u16, value, 10); // Парсим порт             } else if (std.mem.eql(u8, key, "username") or std.mem.eql(u8, key, "user")) {                 config.username = try allocator.dupe(u8, value); // Выделяем память для имени пользователя             } else if (std.mem.eql(u8, key, "password")) {                 config.password = try allocator.dupe(u8, value); // Выделяем память для пароля             } else if (std.mem.eql(u8, key, "database")) {                 config.database = try allocator.dupe(u8, value); // Выделяем память для имени БД             }         }     }      return config; }

Использую pub export fn get_banner( вместо просто export, чтобы можно было вызвать из программы для тестирования (следующий спойлер).

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

Как я проверял работу статической библиотеки:

Скрытый текст

Пока функционал чуть более чем скромный, я поленился писать полноценные unit-тесты, моккировать бд и т.д. Но добавил этот пункт в TODO лист проекта.

Кстати, если вы обратили внимание — модуль возвращает true, в некоторых случаях если сталкивается с ошибкой, это сделано намеренно с целью улучшить UX пользователя.

Для проверки что всё работает как задумано, была написана такая программа, test/test_banner.zig:

const std = @import("std");  // Live тестирование на реальной базе данных pub fn main() !void {     const allocator = std.heap.page_allocator;      // Тестовые данные для проверки прокси     const ProxyTestCase = struct {         username: []const u8,         remote_ip: []const u8,         expected: bool,         description: []const u8,     };      const proxy_test_cases = [_]ProxyTestCase{         .{             .username = "test_user",             .remote_ip = "192.168.1.1",             .expected = false,             .description = "Пользователь не существует",         },         .{             .username = "axtest",             .remote_ip = "1.2.3.4", // Неправильный IP             .expected = false,             .description = "Строгий прокси, IP не совпадает",         },         .{             .username = "axtest",             .remote_ip = "", // Правильный IP             .expected = true,             .description = "Строгий прокси, IP совпадает",         },         .{             .username = "axtest2",             .remote_ip = "5.6.7.8", // Любой IP будет правильным             .expected = true,             .description = "Нестрогий прокси",         },         .{             .username = "axtest3",             .remote_ip = "9.10.11.12", // Любой IP будет правильным             .expected = true,             .description = "Прокси не задан",         },     };      // Тестовые данные для проверки баннеров     const BannerTestCase = struct {         username: []const u8,         description: []const u8,     };      const banner_test_cases = [_]BannerTestCase{         .{             .username = "axtest",             .description = "Пользователь с глобальным сообщением",         },         .{             .username = "axtest2",             .description = "Пользователь с групповым сообщением",         },         .{             .username = "axtest3",             .description = "Пользователь с индивидуальным сообщением",         },         .{             .username = "axtest4",             .description = "Пользователь с истекающим сроком",         },         .{             .username = "axtest5",             .description = "Пользователь без сообщений",         },     };      std.debug.print("\n=== Тестирование проверки прокси ===\n", .{});     for (proxy_test_cases) |case| {         // Буфер для IP прокси         var proxy_ip: [256]u8 = undefined;         @memset(&proxy_ip, 0);         proxy_ip[proxy_ip.len - 1] = 0; // Гарантируем null-terminator          // Создаем нуль-терминированные строки         const username_nt = try std.fmt.allocPrintZ(allocator, "{s}", .{case.username});         defer allocator.free(username_nt);         const remote_ip_nt = try std.fmt.allocPrintZ(allocator, "{s}", .{case.remote_ip});         defer allocator.free(remote_ip_nt);          // Вызываем функцию проверки прокси         const res = @import("banner_check").check_user_proxy(             username_nt.ptr,             remote_ip_nt.ptr,             @ptrCast(&proxy_ip),         );          // Получаем строку IP прокси         const proxy_ip_str = if (proxy_ip[0] != 0)             std.mem.sliceTo(&proxy_ip, 0)         else             "none";          // Проверяем результат         const status = if (res == case.expected) "PASS" else "FAIL";          std.debug.print("[{s}] {s}: {s}@{s} => {} (proxy: {s}, expected {})\n", .{             status,             case.description,             case.username,             case.remote_ip,             res,             proxy_ip_str,             case.expected,         });     }      std.debug.print("\n=== Тестирование баннерных сообщений ===\n", .{});     for (banner_test_cases) |case| {         // Буфер для баннера (4KB должно хватить для любого сообщения)         var banner_buffer: [4096]u8 = undefined;         @memset(&banner_buffer, 0);         banner_buffer[banner_buffer.len - 1] = 0; // Гарантируем null-terminator          // Создаем нуль-терминированную строку имени пользователя         const username_nt = try std.fmt.allocPrintZ(allocator, "{s}", .{case.username});         defer allocator.free(username_nt);          // Вызываем функцию получения баннера         const has_banner = @import("banner_check").get_banner(             username_nt.ptr,             @ptrCast(&banner_buffer),             banner_buffer.len,         );          // Получаем строку баннера         const banner_str = if (has_banner)             std.mem.sliceTo(&banner_buffer, 0)         else             "no banner";          std.debug.print("{s}: {s} => {s}\n", .{             case.description,             case.username,             banner_str,         });     } } 

Build.zig:

Скрытый текст
const std = @import("std");  pub fn build(b: *std.Build) void {     // Стандартные опции     const target = b.standardTargetOptions(.{});     const optimize = b.standardOptimizeOption(.{});      // Получаем модуль pg     const pg_module = b.dependency("pg", .{         .target = target,         .optimize = optimize,     }).module("pg");      // 1. Настраиваем исполняемый файл     const exe = b.addExecutable(.{         .name = "login-test",         .root_source_file = b.path("src/main.zig"),         .target = target,         .optimize = optimize,     });     exe.root_module.addImport("pg", pg_module);     b.installArtifact(exe);      // 2. Настраиваем статическую библиотеку     const lib = b.addStaticLibrary(.{         .name = "banner_check",         .root_source_file = b.path("src/banner_check.zig"),         .target = target,         .optimize = .ReleaseSafe,     });      lib.root_module.addImport("pg", pg_module);     lib.linkLibC();     lib.linkSystemLibrary("pq");     lib.bundle_compiler_rt = true;     b.installArtifact(lib);      // 3. Добавляем тестовую программу     const test_exe = b.addExecutable(.{         .name = "test_banner",         .root_source_file = b.path("test/test_banner.zig"),         .target = target,         .optimize = optimize,     });      // Подключаем зависимости     test_exe.root_module.addImport("banner_check", lib.root_module);      test_exe.linkLibrary(lib); // Линкуем нашу библиотеку     test_exe.linkLibC();      b.installArtifact(test_exe);      // 4. Добавляем шаг для запуска тестов     const run_test = b.addRunArtifact(test_exe);     const test_step = b.step("test", "Run banner check tests");     test_step.dependOn(&run_test.step);      // 5. Простая генерация заголовочного файла     const gen_header = b.addWriteFiles();     _ = gen_header.add("include/banner_check.h",         \\#pragma once         \\#include <stdbool.h>         \\bool check_user_proxy(const char* username, const char* remote_addr, char* proxy_ip_out);     );     b.getInstallStep().dependOn(&gen_header.step);      // 6. Группируем шаги сборки     const build_exe = b.step("exe", "Build only the executable");     build_exe.dependOn(&exe.step);      const build_lib = b.step("lib", "Build only the library");     build_lib.dependOn(&lib.step);      const build_test = b.step("test-build", "Build the test executable");     build_test.dependOn(&test_exe.step);      const build_all = b.step("all", "Build everything");     build_all.dependOn(build_exe);     build_all.dependOn(build_lib);     build_all.dependOn(build_test);     build_all.dependOn(&gen_header.step); } 

Вот как теперь это вызывается из C-кода:

Скрытый текст

banner_check.h:

#pragma once #include <stdbool.h> #include <stddef.h>  // for size_t  #ifdef __cplusplus extern "C" { #endif  external bool check_user_proxy(const char* username, const char* remote_addr, char* proxy_ip); external bool get_banner(const char* username, char* banner_out, size_t banner_len);  #ifdef __cplusplus } #endif 

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

worker-auth.c:

В начале файла, после основных хидеров

#include "banner_check.h"

Далее в функции int post_common_handler(worker_st ws, unsigned http_ver, const char imsg) вносятся изменения:

В блоке основных определений переменных

char banner[4096], username[32], remote_addr[256], our_addr[256]; char proxy_ip[256] = {0};  memset(banner, 0, 256); memset(username, 0, 32); memset(remote_addr, 0, 256); memset(our_addr, 0, 256);

Перед блоком с формированием баннера (оригинальный фрагмент кода):

if (WSCONFIG(ws)->banner) { ...

Добавляем код:

int len; len = strlen(ws->username); memcpy(username, ws->username, len);  getsockname(ws->conn_fd, (struct sockaddr*)&ws->our_addr, &ws->our_addr_len);  ret = getnameinfo((void*)&ws->remote_addr, ws->remote_addr_len, remote_addr, sizeof(remote_addr), NULL, 0, NI_NUMERICHOST); if (ret < 0) goto fail; ret = getnameinfo((void*)&ws->our_addr, ws->our_addr_len, our_addr, sizeof(our_addr), NULL, 0, NI_NUMERICHOST); if (ret < 0) goto fail;  /* Check if proxy is allowed */ if (!is_proxy_allowed(username, remote_addr, proxy_ip)) { const char *proxy_msg = "Вы должны подключится используя proxy ip: "; size_t msg_len = strlen(proxy_msg); size_t ip_len = strlen(proxy_ip);          // Ensure we don't overflow the banner buffer if (msg_len + ip_len < MAX_BANNER_SIZE + 32) {           memcpy(WSCONFIG(ws)->banner, proxy_msg, msg_len);           memcpy(WSCONFIG(ws)->banner + msg_len, proxy_ip, ip_len);           WSCONFIG(ws)->banner[msg_len + ip_len] = '\0'; } else {           // If the message is too long, use a truncated version           const char *fallback_msg = "Используйте назначенный proxy";           memcpy(WSCONFIG(ws)->banner, fallback_msg, strlen(fallback_msg)+1); } } else { bool has_banner = get_banner(username, banner, sizeof(banner)); if (has_banner) {     memcpy(WSCONFIG(ws)->banner, banner, strlen(banner)+1); } else {     // something went wrong     goto fail;   } } 

is_proxy_allowed это небольшая обёртка, которую можно вставить в любом месте исходника:

bool is_proxy_allowed(const char *username, const char *remote_addr, char* proxy_ip) {   return check_user_proxy(username, remote_addr, proxy_ip); }

Процесс сборки ocserv:

Нужно модифицировать сборочные скрипты

В configure.ac:

# После всех основных проверок библиотек, но перед AC_CONFIG_FILES  # Проверка Zig-модуля AC_ARG_WITH([zig-module],   [AS_HELP_STRING([--with-zig-module=DIR],     [path to Zig proxy check module @<:@default=../zig-module@:>@])],   [zig_module_dir=$withval],   [zig_module_dir=../zig-module] )  AC_MSG_CHECKING([for Zig proxy check module]) if test -f "${zig_module_dir}/libbanner_check.a"; then   AC_MSG_RESULT([yes])   HAVE_ZIG_MODULE=yes   ZIG_MODULE_LIBS="-L${zig_module_dir} -Wl,--whole-archive -lbanner_check -Wl,--no-whole-archive"   ZIG_MODULE_CFLAGS="-I${zig_module_dir}"   AC_SUBST([ZIG_MODULE_LIBS])   AC_SUBST([ZIG_MODULE_CFLAGS]) else   AC_MSG_RESULT([no])   HAVE_ZIG_MODULE=no fi  AM_CONDITIONAL([HAVE_ZIG_MODULE], [test "$HAVE_ZIG_MODULE" = "yes"])

В src/Makefile.am:

if HAVE_ZIG_MODULE ocserv_LDADD += @ZIG_MODULE_LIBS@ ocserv_worker_LDADD += @ZIG_MODULE_LIBS@ AM_CPPFLAGS += @ZIG_MODULE_CFLAGS@ endif

Подводные камни при сборке:

А теперь внимание, хак по сборке: по умолчанию zig собирает бинарник под архитектуру процессора на котором запущен, как передать этот параметр через build.zig если честно не разобрался пока, поэтому передаём через строку сборки.

zig build install -Dcpu=baseline

При сборки библиотеке важно не забыть про bundle_compiler_rt = trueв build.zig, эта опция говорит компилятору что нужно включить рантайм компилятора в итоговый файл — без этого либо не получиться собрать итоговый бинарник на C, либо он выйдет зависимым от компилятора zig.

Итого о пользе для бизнеса:

  1. Простота развёртывания:

    • Сократилось количество файлов

    • Задаём конфиг в TOML (можно и без него)

    • Запускаем

  2. Ресурсоёмкость:

    • Потребление памяти: ~5MB (весь ocserv) против ~50MB у PHP-FPM

    • Время отклика: 3-5ms против 50-100ms (без особой нагрузки)

  3. Надёжность:

    • Нет внезапных падений

    • Чёткое логирование всех ошибок

    • Предсказуемое поведение под нагрузкой

Послесловие

К сожалению Zig ещё не достиг состояния release, и скорее всего для сборки на более свежем компиляторе придётся обновлять код.

Я использовал Zig версии: 0.14

P.s. если понравился пост — подписвайтесь на обновления моего профиля на Хабр или на мой телеграмм-канал.

P.P.S. конструктивная критика и рекомендации всегда приветствуются

P.P.P.S Если нет конструктива — лучше не комментируйте.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

А вы пробовали Zig

19.64% Да (зашёл)11
5.36% Да (не зашёл)3
75% Нет42

Проголосовали 56 пользователей. Воздержались 6 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/921482/