Привет, Хабр! Я full-cycle разработчик, работаю с Rust/Zig/Go и увлекаюсь APL как особым способом мышления о коде. На самом деле я пишу на большом количестве языков, но сегодня не об этом. Хочу рассказать об опыте миграции критической бизнес-логики с PHP на Zig и интеграции её в бинарник.
Контекст проекта
По работе мне досталась биллинг-панель для управления VPN-аккаунтами на базе OpenConnect (ocserv). Изначально система была реализована на PHP и представляла собой типичный legacy-код:
-
Отсутствие внятной архитектуры (фактически — организованный хаос)
-
Множество разрозненных скриптов
-
Проблемы с производительностью и стабильностью
Первый этап модернизации: переход на Go
В рамках ограниченного бюджета я провел частичный рефакторинг:
-
Вынес критичные компоненты в отдельные сервисы на Go:
-
Проверка срока действия подписок
-
Учет трафика пользователей
-
Фоновые задачи обслуживания
-
-
Что это дало:
-
10-кратный рост производительности ключевых операций
-
Снижение потребления памяти в 5-7 раз
-
Возможность работы в режиме 24/7 без «проседаний»
-
Сейчас об этом говорить не будем, но если вам интересна тема превращения тыквы в карету — дайте знать в комментариях, могу описать весь процесс трансформации в деталях и по шагам.
Проблема баннерных сообщений
Ограничение ocserv: поддерживает только одно статическое сообщение для всех пользователей. В реальности же требовалась:
-
Персонализация уведомлений
-
Разные сообщения для разных групп пользователей
-
Динамический контент (например, информация об остатке дней подписки)
Решение: гибкая система баннеров
Исходное PHP-решение
Работало через костыли:
-
Требует php-fpm и было связанно с PDO в биллинг панели
-
Нужно следить за правами на файлы и сокет
-
Переодически подвисало
-
Могло не сработать из-за плохой погоды за окном
Я бы сказал, что это хорошее решение для стадии прототипа — когда нужно проверить логику, но не в продакшене.
Новый подход на Zig
Я реализовал:
-
Статическая библиотека (на Zig):
-
Интеграция напрямую с базой данных
-
Прямая интеграция в ocserv
-
-
Преимущества нового решения:
-
Время отклика: 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.
Итого о пользе для бизнеса:
-
Простота развёртывания:
-
Сократилось количество файлов
-
Задаём конфиг в TOML (можно и без него)
-
Запускаем
-
-
Ресурсоёмкость:
-
Потребление памяти: ~5MB (весь ocserv) против ~50MB у PHP-FPM
-
Время отклика: 3-5ms против 50-100ms (без особой нагрузки)
-
-
Надёжность:
-
Нет внезапных падений
-
Чёткое логирование всех ошибок
-
Предсказуемое поведение под нагрузкой
-
Послесловие
К сожалению Zig ещё не достиг состояния release, и скорее всего для сборки на более свежем компиляторе придётся обновлять код.
Я использовал Zig версии: 0.14
P.s. если понравился пост — подписвайтесь на обновления моего профиля на Хабр или на мой телеграмм-канал.
P.P.S. конструктивная критика и рекомендации всегда приветствуются
P.P.P.S Если нет конструктива — лучше не комментируйте.
ссылка на оригинал статьи https://habr.com/ru/articles/921482/
Добавить комментарий