mirror of
https://github.com/rekryt/iplist.git
synced 2026-08-30 05:47:26 +00:00
New output formats: - `format=geosite` — v2ray/xray geosite.dat, built natively by GeositeDatWriter (domain-list-community rejects 406 of 409 portal names, so an external generator was not an option); `domaintype=suffix|full|keyword|regex`. - `format=singbox` — sing-box rule-set source JSON, served inline; it becomes an attachment only with `?filesave=1`. - `format=srs` — the same rule-set compiled to binary by the sing-box binary (SINGBOX_PATH); without the binary the format returns a plain-text error. - `format=geoip` — same contract, now built in PHP by GeoipDatWriter. `SYS_GEOIP_NATIVE=false` keeps the v2fly binary path for one release. Infrastructure: - src/Infrastructure/Codec: ProtobufWriter, GeoipDatWriter, GeositeDatWriter, SingboxRuleSetBuilder, EntryPayload, DatEncoder. Writers are pure functions — no I/O, no Amp, no singletons — so the same class runs inline and inside an amphp/parallel worker. Above SYS_ENCODE_WORKER_THRESHOLD encoding goes to a worker, below it yields to the event loop every 32 lists. - TempWorkspace: one temp dir per request under storage/tmp, removed in a finally, plus a sweeper for dirs orphaned by SIGKILL (SYS_TMP_TTL, SYS_TMP_SWEEP_INTERVAL). Replaces the leaking geoip/input + geoip/output. - ProcessRunner: the single place that starts external binaries — command as an array, both pipes buffered concurrently (not reading them deadlocked a chatty child), exit code and stderr surfaced in the error body. Fixes: - wildcard domain collapse no longer emits a bare "." for scraped junk like `DNSdumpster..`; such entries are dropped by SiteFactory::normalizeDomains on ingestion, and a matching guard sits in Site::getDomains. - geoip.dat was served as text/plain; both engines now use application/octet-stream. - HttpClient in AsyncTest is shared across the run: per-test clients kept their keep-alive pools alive and starved the Windows StreamSelectDriver by the end of a full run. UI, docs, build: - Form.vue: new formats, domaintype and rule-set version selects, wildcard on by default, filesave hidden for attachment-only formats; public/ rebuilt. - IndexTemplate.php (no-JS form on /index) caught up with the frontend. - README.md, README.en.md, CLAUDE.md, ROADMAP.md, .env.example. - Dockerfile: sing-box from the release tarball (-glibc), v2fly/geoip built on golang:1.25 with a pinned tag; the apt Go 1.19 stage was the broken build. https://github.com/itdoginfo/podkop/issues/418
121 lines
4.8 KiB
PHP
121 lines
4.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace OpenCCK\App\Controller;
|
||
|
||
use OpenCCK\AsyncTest;
|
||
use OpenCCK\Infrastructure\Storage\TempWorkspace;
|
||
|
||
/**
|
||
* Инвариант: после запроса, который шеллит внешний бинарник, в storage/tmp не
|
||
* остаётся ничего — ни при успехе, ни при провале генерации.
|
||
*
|
||
* Считаем не общее число записей, а только каталоги с префиксом `geoip-`:
|
||
* тест не должен зависеть ни от чужих воркспейсов, ни от подметальщика,
|
||
* который висит на таймере в Server::startTempSweeper().
|
||
*
|
||
* Проверки состояния диска — обычными is_dir/scandir, а не `Amp\File`:
|
||
* без ext-uv/ext-eio `Amp\File` уходит в пул процессов-воркеров, и поднятый
|
||
* ради ассертов пул потом мешает остальным тестам в этом же процессе.
|
||
*/
|
||
final class TempCleanupTest extends AsyncTest {
|
||
private ?string $previousEngine = null;
|
||
|
||
/**
|
||
* Временные файлы создаёт только путь через внешнюю утилиту, поэтому здесь
|
||
* он включается принудительно: по умолчанию geoip.dat собирается нативно и
|
||
* на диск ничего не пишет (см. ROADMAP §7).
|
||
*/
|
||
protected function setUp(): void {
|
||
parent::setUp();
|
||
$this->previousEngine = $_ENV['SYS_GEOIP_NATIVE'] ?? null;
|
||
$_ENV['SYS_GEOIP_NATIVE'] = 'false';
|
||
}
|
||
|
||
protected function tearDown(): void {
|
||
if ($this->previousEngine === null) {
|
||
unset($_ENV['SYS_GEOIP_NATIVE']);
|
||
} else {
|
||
$_ENV['SYS_GEOIP_NATIVE'] = $this->previousEngine;
|
||
}
|
||
|
||
parent::tearDown();
|
||
}
|
||
|
||
/**
|
||
* @return array<int, string>
|
||
*/
|
||
private function workspaces(string $prefix): array {
|
||
$base = TempWorkspace::basePath();
|
||
if (!is_dir($base)) {
|
||
return [];
|
||
}
|
||
clearstatcache();
|
||
$entries = scandir($base) ?: [];
|
||
|
||
return array_values(array_filter($entries, fn(string $e) => str_starts_with($e, $prefix . '-')));
|
||
}
|
||
|
||
public function testGeoipRequestLeavesNoWorkspace(): void {
|
||
// Бинарника в фикстурах нет, поэтому запрос заведомо падает на
|
||
// Process::start — именно этот путь и должен убирать за собой.
|
||
$body = $this->body($this->get('/', ['format' => 'geoip', 'data' => 'cidr4']));
|
||
|
||
self::assertStringStartsWith('# Error:', $body);
|
||
self::assertSame([], $this->workspaces('geoip'));
|
||
}
|
||
|
||
public function testGeoipRequestWithMissingBinaryDirLeavesNoWorkspace(): void {
|
||
$previous = $_ENV['GEOIP_PATH'] ?? null;
|
||
$_ENV['GEOIP_PATH'] = PATH_ROOT . '/definitely-missing-bin/';
|
||
|
||
try {
|
||
$body = $this->body($this->get('/', ['format' => 'geoip', 'data' => 'cidr4']));
|
||
|
||
self::assertStringStartsWith('# Error:', $body);
|
||
self::assertSame([], $this->workspaces('geoip'));
|
||
} finally {
|
||
if ($previous === null) {
|
||
unset($_ENV['GEOIP_PATH']);
|
||
} else {
|
||
$_ENV['GEOIP_PATH'] = $previous;
|
||
}
|
||
}
|
||
}
|
||
|
||
public function testInvalidDataRequestCreatesNoWorkspace(): void {
|
||
$this->body($this->get('/', ['format' => 'geoip', 'data' => 'domains']));
|
||
|
||
self::assertSame([], $this->workspaces('geoip'));
|
||
}
|
||
|
||
public function testDestroyIsRecursiveAndIdempotent(): void {
|
||
$workspace = TempWorkspace::create('test-nested');
|
||
$workspace->write('input/deep/payload.txt', 'data');
|
||
|
||
self::assertFileExists($workspace->path('input/deep/payload.txt'));
|
||
|
||
$workspace->destroy();
|
||
$workspace->destroy();
|
||
|
||
self::assertDirectoryDoesNotExist($workspace->path());
|
||
}
|
||
|
||
public function testSweepKeepsFreshWorkspacesAndRemovesExpiredOnes(): void {
|
||
$workspace = TempWorkspace::create('test-sweep');
|
||
$workspace->write('payload.txt', 'data');
|
||
|
||
try {
|
||
// TTL заведомо больше возраста только что созданного каталога
|
||
TempWorkspace::sweep(3600);
|
||
self::assertDirectoryExists($workspace->path(), 'свежий воркспейс не должен подметаться');
|
||
|
||
// TTL = 0 — просрочено всё, включая только что созданное
|
||
self::assertGreaterThanOrEqual(1, TempWorkspace::sweep(0));
|
||
self::assertDirectoryDoesNotExist($workspace->path());
|
||
} finally {
|
||
$workspace->destroy();
|
||
}
|
||
}
|
||
}
|