package ru.pdguard.config; import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; import ru.pdguard.mask.MaskMode; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; /** * Список систем, которым разрешено обращаться в модуль, и правила для каждой. * *

Читается из внешнего файла, чтобы настройки менялись без пересборки. Файл * перечитывается сам, когда меняется время его изменения; проверка выполняется * не чаще раза в секунду, чтобы не ходить в файловую систему на каждом запросе. * Файла нет — работают настройки по умолчанию, и сервис поднимается без него. */ @ApplicationScoped public class SystemsConfig { private static final Logger LOG = Logger.getLogger(SystemsConfig.class); /** Имя политики, которая применяется к запросам без заголовка системы. */ public static final String DEFAULT_SYSTEM = "default"; private static final long RECHECK_MILLIS = 1000; /** Описание одной системы в файле настроек. */ @RegisterForReflection public record SystemEntry(Boolean enabled, Boolean demask, String maskMode, List types, List requireCompanion) { } private final Path file; private final ObjectMapper mapper; private volatile Map policies = Map.of(DEFAULT_SYSTEM, SystemPolicy.DEFAULT); private volatile long fileTimestamp; private volatile long lastCheck; @Inject public SystemsConfig(@ConfigProperty(name = "pdguard.systems-file", defaultValue = "config/systems.json") String path, ObjectMapper mapper) { this.file = Path.of(path); this.mapper = mapper; reload(); } /** Правила для системы; неизвестная система получает настройки по умолчанию. */ public SystemPolicy policyFor(String systemId) { refreshIfChanged(); Map current = policies; SystemPolicy policy = systemId == null ? null : current.get(systemId); if (policy != null) { return policy; } return current.getOrDefault(DEFAULT_SYSTEM, SystemPolicy.DEFAULT); } /** Известна ли система по имени. */ public boolean isKnown(String systemId) { refreshIfChanged(); return systemId != null && policies.containsKey(systemId); } /** Текущие настройки — для отдачи в административном интерфейсе. */ public Map current() { refreshIfChanged(); return new TreeMap<>(policies); } /** Перечитать файл настроек немедленно. */ public final synchronized void reload() { lastCheck = System.currentTimeMillis(); if (!Files.isReadable(file)) { LOG.infof("Файл настроек %s не найден, применяются настройки по умолчанию", file.toAbsolutePath()); policies = Map.of(DEFAULT_SYSTEM, SystemPolicy.DEFAULT); fileTimestamp = 0; return; } try { fileTimestamp = Files.getLastModifiedTime(file).toMillis(); Map entries = mapper.readValue(Files.readAllBytes(file), mapper.getTypeFactory().constructMapType(TreeMap.class, String.class, SystemEntry.class)); Map parsed = new TreeMap<>(); entries.forEach((name, entry) -> parsed.put(name, toPolicy(entry))); parsed.putIfAbsent(DEFAULT_SYSTEM, SystemPolicy.DEFAULT); policies = Map.copyOf(parsed); LOG.infof("Настройки систем перечитаны из %s: %s", file.toAbsolutePath(), parsed.keySet()); } catch (IOException | IllegalArgumentException e) { // Битый файл не должен ронять работающий сервис: остаются прежние настройки. LOG.errorf(e, "Не удалось прочитать %s, продолжаем с прежними настройками", file.toAbsolutePath()); } } private void refreshIfChanged() { long now = System.currentTimeMillis(); if (now - lastCheck < RECHECK_MILLIS) { return; } lastCheck = now; try { if (!Files.isReadable(file)) { return; } if (Files.getLastModifiedTime(file).toMillis() != fileTimestamp) { reload(); } } catch (IOException e) { LOG.debugf(e, "Не удалось проверить время изменения %s", file); } } private static SystemPolicy toPolicy(SystemEntry entry) { SystemPolicy base = SystemPolicy.DEFAULT; Set types = entry.types() == null ? base.types() : new HashSet<>(entry.types()); Set companions = entry.requireCompanion() == null ? base.requireCompanion() : new HashSet<>(entry.requireCompanion()); MaskMode mode = entry.maskMode() == null ? base.maskMode() : MaskMode.valueOf(entry.maskMode().toUpperCase(Locale.ROOT)); return new SystemPolicy( entry.enabled() == null || entry.enabled(), entry.demask() == null || entry.demask(), mode, types, companions); } }