9247281688
Тайный ключ SELF_RENDER_KEY уходил заголовком X-Render-Key на любой адрес, какой окажется в настройке. Проверялось ровно одно: адрес не пуст и ключ не пуст. Отсюда две молчаливые беды: опечатка в SELF_RENDER_ENDPOINT увела бы ключ чужому человеку навсегда и без признаков, а http:// увёз бы его открытым текстом. Дверь повторяет проверенное устройство Р123 от голосового робота: своя причина на пустой адрес и на пустой ключ, список разрешённых машин с пустым списком как «закрыто всё», обязательное https кроме своей машины, а «своя машина» — два независимых ответа: окружение из положительного списка И двоичная проверка петли 127.0.0.0/8, ::1, localhost по RFC 6761. Закрытая дверь = мягкая деградация: наружу не ходим, отдаём пусто, причина в журнал уровнем warning, потому что на боевом всё ниже не доезжает. Тумблера «можно без шифра» нет намеренно: он и был бы той самой опечаткой. Дыр было ДВЕ, а не одна. Тот же ключ и тот же SELF_RENDER_ENDPOINT уходят из RenderServiceYandexListSource — лента Яндекс.Карт. Починка только названного файла оставила бы дыру открытой, поэтому дверь общая на обе. Боевой поиск не сломается: умолчание списка НЕ пустое и содержит замеренный боевой адрес виртуалки 51-250-1-97.sslip.io — пустое умолчание остановило бы поиск в момент выката. Набор тот же, что владелец уже принял для OBZVON_ALLOWED_HOSTS: это одна и та же машина. Прогнаны все формы адреса, какие могут стоять в боевой настройке, в окружении production. Проверок 37, сторожа показаны красными пятью отдельными сломами, возврат доказан слепками. Larastan 0, Pint чисто. Не закрыто и ждёт слова владельца: ProxyMarketProbe шлёт тот же ключ на другую настройку без двери; боевое значение SELF_RENDER_ENDPOINT не замерено, на боевой не ходил. Отчёт: docs/superpowers/priyomka/stroyka-6/otchyot-pomoshchnika-r126-dver-rendera-2026-08-07.md
85 lines
3.5 KiB
PHP
85 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Autopodbor\Agent\Fetch\BatchPageFetcher;
|
|
use App\Services\Autopodbor\Agent\Fetch\PageFetcher;
|
|
use App\Services\Autopodbor\Agent\Fetch\SelfRenderClient;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
// SelfRenderClient — HTTP-клиент к рендер-сервису на виртуалке (замена xf4). Как XfetchClient: и
|
|
// PageFetcher (html), и BatchPageFetcher (htmlBatch). Для 2ГИС просит initialState, для Яндекса — HTML.
|
|
// Без ключа/адреса — молча пусто (деградация, не падение).
|
|
|
|
const EP = 'https://render.local/render';
|
|
|
|
function client(array $over = []): SelfRenderClient
|
|
{
|
|
return new SelfRenderClient(
|
|
endpoint: $over['endpoint'] ?? EP,
|
|
key: $over['key'] ?? 'secret',
|
|
concurrency: $over['concurrency'] ?? 4,
|
|
retries: $over['retries'] ?? 2,
|
|
// 🔴 Р126: список разрешённых машин пуст по умолчанию (закрыто всё), поэтому проверка
|
|
// обязана назвать, куда ей можно, — ровно как это делает боевая настройка.
|
|
razreshyonnyeHosty: $over['razreshyonnyeHosty'] ?? ['render.local'],
|
|
);
|
|
}
|
|
|
|
it('реализует оба интерфейса', function () {
|
|
expect(client())->toBeInstanceOf(PageFetcher::class)
|
|
->and(client())->toBeInstanceOf(BatchPageFetcher::class);
|
|
});
|
|
|
|
it('html: POST на endpoint, тип initialState для 2ГИС, отдаёт data', function () {
|
|
Http::fake([EP => Http::response(['data' => '{"state":1}'], 200)]);
|
|
|
|
$out = client()->html('https://2gis.ru/krasnoyarsk/search/ломбард');
|
|
|
|
expect($out)->toBe('{"state":1}');
|
|
Http::assertSent(fn ($r) => $r->url() === EP
|
|
&& $r['url'] === 'https://2gis.ru/krasnoyarsk/search/ломбард'
|
|
&& $r['type'] === 'initialState'
|
|
&& $r->hasHeader('X-Render-Key', 'secret'));
|
|
});
|
|
|
|
it('html: тип html для Яндекса и прочего', function () {
|
|
Http::fake([EP => Http::response(['data' => '<html>ya</html>'], 200)]);
|
|
|
|
expect(client()->html('https://yandex.ru/maps/org/1'))->toBe('<html>ya</html>');
|
|
Http::assertSent(fn ($r) => $r['type'] === 'html');
|
|
});
|
|
|
|
it('без ключа или адреса → пусто, запрос не шлётся (деградация)', function () {
|
|
Http::fake([EP => Http::response(['data' => 'x'], 200)]);
|
|
|
|
expect(client(['key' => ''])->html('https://2gis.ru/x'))->toBe('')
|
|
->and(client(['endpoint' => ''])->html('https://2gis.ru/x'))->toBe('');
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
it('htmlBatch: карта url => data', function () {
|
|
Http::fake([EP => function ($request) {
|
|
return Http::response(['data' => 'R:'.$request['url']], 200);
|
|
}]);
|
|
|
|
$urls = ['https://2gis.ru/a', 'https://2gis.ru/b'];
|
|
expect(client()->htmlBatch($urls))->toBe([
|
|
'https://2gis.ru/a' => 'R:https://2gis.ru/a',
|
|
'https://2gis.ru/b' => 'R:https://2gis.ru/b',
|
|
]);
|
|
});
|
|
|
|
it('htmlBatch: пустой ключ → все пусто, без запросов', function () {
|
|
Http::fake([EP => Http::response(['data' => 'x'], 200)]);
|
|
|
|
expect(client(['key' => ''])->htmlBatch(['https://2gis.ru/a']))->toBe(['https://2gis.ru/a' => '']);
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
it('html: неуспешный ответ → пусто', function () {
|
|
Http::fake([EP => Http::response('', 500)]);
|
|
|
|
expect(client()->html('https://2gis.ru/x'))->toBe('');
|
|
});
|