288 lines
9.3 KiB
PHP
288 lines
9.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
final class Settings
|
|
{
|
|
public const BASE_DIR = '/usr/local/hitme_plugins/mx_autologin';
|
|
public const PLUGIN_DIR = '/usr/local/directadmin/plugins/mx_autologin';
|
|
public const SECRET_DIR = self::BASE_DIR . '/secrets';
|
|
public const LOG_DIR = self::BASE_DIR . '/logs';
|
|
public const STATE_DIR = self::BASE_DIR . '/state';
|
|
public const AUDIT_LOG = self::LOG_DIR . '/audit.log';
|
|
public const EXTERNAL_SETTINGS = self::PLUGIN_DIR . '/plugin-settings.conf';
|
|
|
|
/** @var array<string,string> */
|
|
private array $values;
|
|
|
|
/**
|
|
* @param array<string,string> $values
|
|
*/
|
|
private function __construct(array $values)
|
|
{
|
|
$this->values = $values;
|
|
$this->validate();
|
|
}
|
|
|
|
public static function load(string $path): self
|
|
{
|
|
$values = self::defaults();
|
|
if (is_file($path)) {
|
|
$lines = file($path, FILE_IGNORE_NEW_LINES);
|
|
if ($lines !== false) {
|
|
foreach ($lines as $line) {
|
|
$line = trim($line);
|
|
if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
|
|
continue;
|
|
}
|
|
[$key, $value] = explode('=', $line, 2);
|
|
$values[trim($key)] = trim($value);
|
|
}
|
|
}
|
|
}
|
|
return new self($values);
|
|
}
|
|
|
|
/**
|
|
* @param array<string,string> $values
|
|
*/
|
|
public static function fromArray(array $values): self
|
|
{
|
|
return new self(array_merge(self::defaults(), $values));
|
|
}
|
|
|
|
/**
|
|
* @return array<string,string>
|
|
*/
|
|
public static function defaults(): array
|
|
{
|
|
return [
|
|
'DEFAULT_ENABLE' => 'true',
|
|
'MAIL_LOGIN_METHOD' => 'login_url',
|
|
'MX_LOGIN_MODE' => '1',
|
|
'MAIL_LOGIN_TARGET_NAME' => 'mx1',
|
|
'MAIL_LOGIN_TARGET_BASE_URL' => 'https://mx1.mojserwer.pl:2222',
|
|
'MAIL_LOGIN_REDIRECT_URL' => '/',
|
|
'LOGIN_KEY_TTL' => '14400',
|
|
'USER_IP_BOUND' => '1',
|
|
'IP_MASK' => '1',
|
|
'MAIL_LOGIN_SOURCE_SERVER_NAME' => '',
|
|
'MAIL_LOGIN_CLIENT_IP_MODE' => 'direct',
|
|
'MAIL_LOGIN_TRUSTED_PROXIES' => '',
|
|
'MAIL_LOGIN_REMOTE_TRANSPORT' => 'ssh',
|
|
'MAIL_LOGIN_SSH_HOST' => 'mx1.mojserwer.pl',
|
|
'MAIL_LOGIN_SSH_PORT' => '22',
|
|
'MAIL_LOGIN_SSH_USER' => 'root',
|
|
'MAIL_LOGIN_SSH_IDENTITY' => self::SECRET_DIR . '/mx1_ed25519',
|
|
'MAIL_LOGIN_SSH_KNOWN_HOSTS' => self::SECRET_DIR . '/known_hosts',
|
|
'MAIL_LOGIN_CONNECT_TIMEOUT_SECONDS' => '5',
|
|
'MAIL_LOGIN_RATE_LIMIT_PER_USER_PER_MINUTE' => '5',
|
|
'MAIL_LOGIN_RATE_LIMIT_PER_IP_PER_MINUTE' => '20',
|
|
'MAIL_LOGIN_ALLOW_PASSWORD_FALLBACK' => 'false',
|
|
];
|
|
}
|
|
|
|
public static function normalizeHost(string $host): string
|
|
{
|
|
$host = trim($host);
|
|
$host = preg_replace('/^https?:\/\//i', '', $host) ?? $host;
|
|
$host = preg_replace('/\/.*$/', '', $host) ?? $host;
|
|
if (str_starts_with($host, '[')) {
|
|
$end = strpos($host, ']');
|
|
if ($end !== false) {
|
|
return strtolower(substr($host, 1, $end - 1));
|
|
}
|
|
}
|
|
$host = preg_replace('/:[0-9]+$/', '', $host) ?? $host;
|
|
return strtolower(trim($host, ". \t\n\r\0\x0B"));
|
|
}
|
|
|
|
public function getString(string $key, string $default = ''): string
|
|
{
|
|
return $this->values[$key] ?? $default;
|
|
}
|
|
|
|
public function targetBaseUrl(): string
|
|
{
|
|
return rtrim($this->getString('MAIL_LOGIN_TARGET_BASE_URL'), '/');
|
|
}
|
|
|
|
public function defaultEnable(): bool
|
|
{
|
|
return in_array(strtolower($this->getString('DEFAULT_ENABLE', 'true')), ['1', 'true', 'yes', 'on'], true);
|
|
}
|
|
|
|
public function method(): string
|
|
{
|
|
return $this->getString('MAIL_LOGIN_METHOD', 'login_url');
|
|
}
|
|
|
|
public function mxLoginMode(): int
|
|
{
|
|
return (int)$this->getString('MX_LOGIN_MODE', '1');
|
|
}
|
|
|
|
public function redirectUrl(): string
|
|
{
|
|
return $this->getString('MAIL_LOGIN_REDIRECT_URL', '/');
|
|
}
|
|
|
|
public function loginKeyTtl(): int
|
|
{
|
|
return (int)$this->getString('LOGIN_KEY_TTL', '14400');
|
|
}
|
|
|
|
public function userIpBound(): bool
|
|
{
|
|
return $this->getString('USER_IP_BOUND', '1') === '1';
|
|
}
|
|
|
|
public function ipMask(): bool
|
|
{
|
|
return $this->getString('IP_MASK', '1') === '1';
|
|
}
|
|
|
|
public function auditClientIp(string $clientIp): string
|
|
{
|
|
return $this->ipMask() ? 'MASKED' : $clientIp;
|
|
}
|
|
|
|
public function sourceServerName(): string
|
|
{
|
|
return $this->sanitizeServerName($this->getString('MAIL_LOGIN_SOURCE_SERVER_NAME', ''));
|
|
}
|
|
|
|
public function clientIpMode(): string
|
|
{
|
|
return $this->getString('MAIL_LOGIN_CLIENT_IP_MODE', 'direct');
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function trustedProxies(): array
|
|
{
|
|
return $this->csvNetworks('MAIL_LOGIN_TRUSTED_PROXIES');
|
|
}
|
|
|
|
public function sshHost(): string
|
|
{
|
|
return self::normalizeHost($this->getString('MAIL_LOGIN_SSH_HOST'));
|
|
}
|
|
|
|
public function sshPort(): int
|
|
{
|
|
return (int)$this->getString('MAIL_LOGIN_SSH_PORT', '22');
|
|
}
|
|
|
|
public function sshUser(): string
|
|
{
|
|
return $this->getString('MAIL_LOGIN_SSH_USER', 'root');
|
|
}
|
|
|
|
public function sshIdentity(): string
|
|
{
|
|
return $this->getString('MAIL_LOGIN_SSH_IDENTITY');
|
|
}
|
|
|
|
public function sshKnownHosts(): string
|
|
{
|
|
return $this->getString('MAIL_LOGIN_SSH_KNOWN_HOSTS');
|
|
}
|
|
|
|
public function connectTimeout(): int
|
|
{
|
|
return (int)$this->getString('MAIL_LOGIN_CONNECT_TIMEOUT_SECONDS', '5');
|
|
}
|
|
|
|
public function rateLimitPerUserPerMinute(): int
|
|
{
|
|
return (int)$this->getString('MAIL_LOGIN_RATE_LIMIT_PER_USER_PER_MINUTE', '5');
|
|
}
|
|
|
|
public function rateLimitPerIpPerMinute(): int
|
|
{
|
|
return (int)$this->getString('MAIL_LOGIN_RATE_LIMIT_PER_IP_PER_MINUTE', '20');
|
|
}
|
|
|
|
public function sanitizeServerName(string $name): string
|
|
{
|
|
$name = strtolower(trim($name));
|
|
$name = preg_replace('/[^a-z0-9-]+/', '-', $name) ?? '';
|
|
$name = trim($name, '-');
|
|
if ($name === '') {
|
|
return '';
|
|
}
|
|
return substr($name, 0, 32);
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private function csvNetworks(string $key): array
|
|
{
|
|
$raw = $this->getString($key, '');
|
|
if ($raw === '') {
|
|
return [];
|
|
}
|
|
$out = [];
|
|
foreach (explode(',', $raw) as $item) {
|
|
$item = trim($item);
|
|
if ($item === '') {
|
|
continue;
|
|
}
|
|
if (str_contains($item, '/')) {
|
|
[$ip, $prefix] = explode('/', $item, 2);
|
|
if (filter_var($ip, FILTER_VALIDATE_IP) && preg_match('/^[0-9]+$/', $prefix)) {
|
|
$out[] = $ip . '/' . $prefix;
|
|
}
|
|
} else {
|
|
$host = self::normalizeHost($item);
|
|
if ($host !== '') {
|
|
$out[] = $host;
|
|
}
|
|
}
|
|
}
|
|
return array_values(array_unique($out));
|
|
}
|
|
|
|
private function validate(): void
|
|
{
|
|
$ttlRaw = $this->getString('LOGIN_KEY_TTL', '');
|
|
if (!preg_match('/^[0-9]+$/', $ttlRaw)) {
|
|
throw new InvalidArgumentException('LOGIN_KEY_TTL must be an integer');
|
|
}
|
|
$ttl = (int)$ttlRaw;
|
|
if ($ttl !== 0 && ($ttl < 5 || $ttl > 86400)) {
|
|
throw new InvalidArgumentException('LOGIN_KEY_TTL must be 0 or between 5 and 86400 seconds');
|
|
}
|
|
if (!in_array($this->getString('USER_IP_BOUND', ''), ['0', '1'], true)) {
|
|
throw new InvalidArgumentException('USER_IP_BOUND must be 0 or 1');
|
|
}
|
|
if (!in_array($this->getString('IP_MASK', ''), ['0', '1'], true)) {
|
|
throw new InvalidArgumentException('IP_MASK must be 0 or 1');
|
|
}
|
|
if (!in_array(strtolower($this->getString('DEFAULT_ENABLE', 'true')), ['0', '1', 'true', 'false', 'yes', 'no', 'on', 'off'], true)) {
|
|
throw new InvalidArgumentException('DEFAULT_ENABLE must be a boolean value');
|
|
}
|
|
if ($this->method() !== 'login_url') {
|
|
throw new InvalidArgumentException('MAIL_LOGIN_METHOD must be login_url');
|
|
}
|
|
if (!in_array($this->getString('MX_LOGIN_MODE', '1'), ['1', '2'], true)) {
|
|
throw new InvalidArgumentException('MX_LOGIN_MODE must be 1 or 2');
|
|
}
|
|
if (!in_array($this->clientIpMode(), ['direct', 'trusted_proxy'], true)) {
|
|
throw new InvalidArgumentException('MAIL_LOGIN_CLIENT_IP_MODE must be direct or trusted_proxy');
|
|
}
|
|
$base = parse_url($this->targetBaseUrl());
|
|
if (!is_array($base) || ($base['scheme'] ?? '') !== 'https' || empty($base['host'])) {
|
|
throw new InvalidArgumentException('MAIL_LOGIN_TARGET_BASE_URL must be an https URL');
|
|
}
|
|
if ($this->sshPort() < 1 || $this->sshPort() > 65535) {
|
|
throw new InvalidArgumentException('MAIL_LOGIN_SSH_PORT is invalid');
|
|
}
|
|
if ($this->connectTimeout() < 1 || $this->connectTimeout() > 30) {
|
|
throw new InvalidArgumentException('MAIL_LOGIN_CONNECT_TIMEOUT_SECONDS is invalid');
|
|
}
|
|
}
|
|
}
|