diff --git a/.gitignore b/.gitignore index a98a8df..6458e55 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ share/ polls/ botanygarden/ robots.txt +signup/vendor/ diff --git a/blocks/.csfdeny b/blocks/.csfdeny new file mode 100644 index 0000000..fad06de --- /dev/null +++ b/blocks/.csfdeny @@ -0,0 +1,21 @@ +############################################################################### +# Copyright 2006-2018, Way to the Web Limited +# URL: http://www.configserver.com +# Email: sales@waytotheweb.com +############################################################################### +# The following IP addresses will be blocked in iptables +# One IP address per line +# CIDR addressing allowed with a quaded IP (e.g. 192.168.254.0/24) +# Only list IP addresses, not domain names (they will be ignored) +# +# Note: If you add the text "do not delete" to the comments of an entry then +# DENY_IP_LIMIT will ignore those entries and not remove them +# +# Advanced port+ip filtering allowed with the following format +# tcp/udp|in/out|s/d=port,port,...|s/d=ip +# +# See readme.txt for more information regarding advanced port filtering +# +94.134.107.75 # lfd: (sshd-session) sshd-session brute‑force 94.134.107.75 (DE/Germany/i5E866B4B.versanet.de): 1 in the last 3600 secs - Fri Sep 26 08:28:02 2025 +2001:9e8:65ff:f600:bcfe:932d:c0a0:6735 # lfd: (sshd-session) sshd-session brute‑force 2001:9e8:65ff:f600:bcfe:932d:c0a0:6735 (Unknown): 1 in the last 3600 secs - Fri Sep 26 08:52:42 2025 +2001:9e8:65d8:1900:8cbd:20f5:640a:c57b # lfd: (sshd-session) sshd-session brute‑force 2001:9e8:65d8:1900:8cbd:20f5:640a:c57b (Unknown): 1 in the last 3600 secs - Sun Sep 28 17:45:13 2025 diff --git a/blocks/.env b/blocks/.env new file mode 100644 index 0000000..e4559ec --- /dev/null +++ b/blocks/.env @@ -0,0 +1,6 @@ +APP_ENV=prod +DENY_FILE=.csfdeny +ALLOW_IPS=149.56.184.115 +TRUST_PROXY=0 +TRUSTED_PROXIES= +API_TOKEN= diff --git a/blocks/index.php b/blocks/index.php new file mode 100644 index 0000000..7695b05 --- /dev/null +++ b/blocks/index.php @@ -0,0 +1,266 @@ + 'prod', + 'DENY_FILE' => '/etc/csf/csf.deny', // or full path to ".csfdeny" + 'ALLOW_IPS' => '', // comma-separated allowlist + 'TRUST_PROXY' => '0', // "1" to respect X-Forwarded-For from trusted proxies + 'TRUSTED_PROXIES'=> '', // comma-separated IPs/CIDRs of proxies + 'API_TOKEN' => '', // optional shared token +]; + +if (($env['APP_ENV'] ?? 'prod') !== 'dev') { + ini_set('display_errors', '0'); + ini_set('log_errors', '1'); +} +//////////////////////////// +// Access control +//////////////////////////// +$clientIp = resolveClientIp((bool)intval($env['TRUST_PROXY']), $env['TRUSTED_PROXIES']); +if (!ipAllowed($clientIp, $env['ALLOW_IPS'])) { + http_response_code(403); + echo json_encode(['error' => 'forbidden', 'reason' => 'ip_not_allowed', 'client_ip' => $clientIp], JSON_UNESCAPED_SLASHES); + exit; +} +if (($env['API_TOKEN'] ?? '') !== '' && !hash_equals($env['API_TOKEN'], $_SERVER['HTTP_X_API_TOKEN'] ?? '')) { + http_response_code(403); + echo json_encode(['error' => 'forbidden', 'reason' => 'bad_token'], JSON_UNESCAPED_SLASHES); + exit; +} + +//////////////////////////// +// Read & parse deny file +//////////////////////////// +$file = $env['DENY_FILE']; +if (!is_readable($file)) { + http_response_code(500); + echo json_encode(['error' => 'unreadable_file', 'path' => $file], JSON_UNESCAPED_SLASHES); + exit; +} + +$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; +$entries = []; +foreach ($lines as $rawLine) { + $line = trim($rawLine); + if ($line === '' || str_starts_with($line, '#')) { + continue; + } + // Split off trailing comment after a space + '#' + $comment = null; + $hashPos = strpos($line, ' #'); + if ($hashPos !== false) { + $comment = trim(substr($line, $hashPos + 2)); + $line = trim(substr($line, 0, $hashPos)); + } + + $parsed = parseCsfLine($line); + if ($parsed !== null) { + $parsed['raw'] = $rawLine; + if ($comment !== null) { + $parsed['comment'] = $comment; + } + $entries[] = $parsed; + } +} + +//////////////////////////// +// Output +//////////////////////////// +echo json_encode([ + 'generated_at' => gmdate('c'), + 'source' => realpath($file) ?: $file, + 'count' => count($entries), + 'entries' => $entries, +], JSON_UNESCAPED_SLASHES); + +/* -------------------- helpers -------------------- */ + +function loadEnv(string $path): array +{ + $out = []; + if (!is_file($path) || !is_readable($path)) { + return $out; + } + foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $l) { + $l = trim($l); + if ($l === '' || str_starts_with($l, '#')) { continue; } + if (!str_contains($l, '=')) { continue; } + [$k, $v] = array_map('trim', explode('=', $l, 2)); + $v = trim($v, " \t\n\r\0\x0B\"'"); + $out[$k] = $v; + } + return $out; +} + +function resolveClientIp(bool $trustProxy, string $trustedProxiesCsv): string +{ + $remote = $_SERVER['REMOTE_ADDR'] ?? ''; + if (!$trustProxy) { + return $remote; + } + $proxyList = array_filter(array_map('trim', explode(',', $trustedProxiesCsv ?: ''))); + // If the REMOTE_ADDR is not a trusted proxy, ignore forwarded headers + if (!ipAllowed($remote, implode(',', $proxyList))) { + return $remote; + } + $xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''; + if ($xff === '') { + return $remote; + } + // pick the first IP in XFF chain + $first = trim(explode(',', $xff)[0]); + return filter_var($first, FILTER_VALIDATE_IP) ? $first : $remote; +} + +function ipAllowed(string $ip, string $allowCsv): bool +{ + if ($allowCsv === '') { + return false; + } + $allow = array_filter(array_map('trim', explode(',', $allowCsv))); + foreach ($allow as $cidrOrIp) { + if ($cidrOrIp === '') { continue; } + if (str_contains($cidrOrIp, '/')) { + if (ipInCidr($ip, $cidrOrIp)) { return true; } + } else { + if (hash_equals($cidrOrIp, $ip)) { return true; } + } + } + return false; +} + +function parseCsfLine(string $line): ?array +{ + // If it contains pipes, treat as advanced rule + if (str_contains($line, '|')) { + $parts = array_map('trim', explode('|', $line)); + if (count($parts) < 2) { + return null; + } + // proto(s) + $protoRaw = strtolower($parts[0]); + $protocols = array_filter(array_map('trim', explode('/', $protoRaw))); + // direction + $direction = strtolower($parts[1] ?? ''); + $srcIp = $dstIp = null; + $srcPorts = $dstPorts = []; + + for ($i = 2; $i < count($parts); $i++) { + $kv = explode('=', $parts[$i], 2); + if (count($kv) !== 2) { continue; } + [$key, $val] = [strtolower(trim($kv[0])), trim($kv[1])]; + if ($key === 's' || $key === 'src') { + // src may be IP/CIDR or port list + if (looksLikeIpOrCidr($val)) { + $srcIp = $val; + } else { + $srcPorts = parsePortList($val); + } + } elseif ($key === 'd' || $key === 'dst') { + if (looksLikeIpOrCidr($val)) { + $dstIp = $val; + } else { + $dstPorts = parsePortList($val); + } + } + } + return [ + 'type' => 'rule', + 'protocols' => $protocols ?: ['tcp'], + 'direction' => in_array($direction, ['in','out'], true) ? $direction : 'in', + 'ports' => ['source' => $srcPorts, 'dest' => $dstPorts], + 'source_ip' => $srcIp, + 'dest_ip' => $dstIp, + ]; + } + + // Plain IP or CIDR + if (looksLikeIpOrCidr($line)) { + // split ip/cidr + $ip = $line; + $cidr = null; + if (str_contains($line, '/')) { + [$ip, $mask] = explode('/', $line, 2); + $ip = trim($ip); + $cidr = (string)intval($mask); + } + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + return null; + } + return [ + 'type' => ($cidr !== null ? 'cidr' : 'ip'), + 'ip' => $ip, + 'cidr' => $cidr, + ]; + } + + return null; +} + +function looksLikeIpOrCidr(string $val): bool +{ + if (str_contains($val, '/')) { + [$ip, $mask] = explode('/', $val, 2); + return (bool)filter_var($ip, FILTER_VALIDATE_IP) && ctype_digit($mask); + } + return (bool)filter_var($val, FILTER_VALIDATE_IP); +} + +function parsePortList(string $val): array +{ + $out = []; + foreach (explode(',', $val) as $p) { + $p = trim($p); + if ($p === '') { continue; } + // keep ranges as raw strings (e.g., "1000:2000"), normalize digits + if (preg_match('/^\d+(:\d+)?$/', $p)) { + $out[] = $p; + } + } + return $out; +} + +function ipInCidr(string $ip, string $cidr): bool +{ + [$subnet, $maskBits] = explode('/', $cidr, 2); + $maskBits = (int)$maskBits; + $ipBin = @inet_pton($ip); + $subnetBin = @inet_pton($subnet); + if ($ipBin === false || $subnetBin === false) { + return false; + } + $len = strlen($ipBin); + if ($len !== strlen($subnetBin)) { + return false; // v4 vs v6 mismatch + } + $bytes = intdiv($maskBits, 8); + $remainder = $maskBits % 8; + + if ($bytes > 0 && substr($ipBin, 0, $bytes) !== substr($subnetBin, 0, $bytes)) { + return false; + } + if ($remainder === 0) { + return true; + } + $mask = chr(0xFF << (8 - $remainder) & 0xFF); + return (ord($ipBin[$bytes]) & ord($mask)) === (ord($subnetBin[$bytes]) & ord($mask)); +} diff --git a/signup/_probe.php b/signup/_probe.php new file mode 100644 index 0000000..d2afc07 --- /dev/null +++ b/signup/_probe.php @@ -0,0 +1,9 @@ +['pipe','r'],1=>['pipe','w'],2=>['pipe','w']]; +$p=proc_open($cmd,$ds,$pipes); +fwrite($pipes[0],"To: root@tilde.club\r\nSubject: proc_open test\r\n\r\nhi\r\n"); fclose($pipes[0]); +$stdout=stream_get_contents($pipes[1]); fclose($pipes[1]); +$stderr=stream_get_contents($pipes[2]); fclose($pipes[2]); +$rc=proc_close($p); +var_dump(['rc'=>$rc,'stderr'=>$stderr,'sendmail_path'=>ini_get('sendmail_path')]); diff --git a/signup/composer.json b/signup/composer.json new file mode 100644 index 0000000..337f31e --- /dev/null +++ b/signup/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "pear/net_dns2": "^1.5" + } +} diff --git a/signup/composer.lock b/signup/composer.lock new file mode 100644 index 0000000..1a97c8e --- /dev/null +++ b/signup/composer.lock @@ -0,0 +1,70 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "013e57ab3ff38d936fd23c522b9d5268", + "packages": [ + { + "name": "pear/net_dns2", + "version": "v1.5.5", + "source": { + "type": "git", + "url": "https://github.com/mikepultz/netdns2.git", + "reference": "ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikepultz/netdns2/zipball/ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9", + "reference": "ea39ef5a97d5c2b9893a8c35af7b5fd5b0e40bc9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-0": { + "Net_DNS2": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Mike Pultz", + "email": "mike@mikepultz.com", + "homepage": "https://mikepultz.com/", + "role": "lead" + } + ], + "description": "Native PHP DNS Resolver and Updater Library", + "homepage": "https://netdns2.com/", + "keywords": [ + "PEAR", + "dns", + "network" + ], + "support": { + "issues": "https://github.com/mikepultz/netdns2/issues", + "source": "https://github.com/mikepultz/netdns2" + }, + "time": "2025-05-17T20:56:28+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +}