Init
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Список систем, которым разрешено обращаться в модуль, и правила для каждой.
|
||||
*
|
||||
* <p>Читается из внешнего файла, чтобы настройки менялись без пересборки. Файл
|
||||
* перечитывается сам, когда меняется время его изменения; проверка выполняется
|
||||
* не чаще раза в секунду, чтобы не ходить в файловую систему на каждом запросе.
|
||||
* Файла нет — работают настройки по умолчанию, и сервис поднимается без него.
|
||||
*/
|
||||
@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<String> types, List<String> requireCompanion) {
|
||||
}
|
||||
|
||||
private final Path file;
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
private volatile Map<String, SystemPolicy> 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<String, SystemPolicy> 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<String, SystemPolicy> 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<String, SystemEntry> entries = mapper.readValue(Files.readAllBytes(file),
|
||||
mapper.getTypeFactory().constructMapType(TreeMap.class, String.class, SystemEntry.class));
|
||||
Map<String, SystemPolicy> 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<String> types = entry.types() == null ? base.types() : new HashSet<>(entry.types());
|
||||
Set<String> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user