Otkrivanje promjena tehnološkog sklopa klijenta: upozorenja u stvarnom vremenu za PHP 8.3
Javni tehnološki stack klijenta može se neprimjetno promijeniti: migracija CMS-a, zamjena CDN-a, novi analitički proizvod ili nadogradnja okvira koja utječe na vaše sljedeće postavljanje. Ručna provjera tehnoloških izvješća nije skalabilna, ali mali planirani PHP program može uspostaviti pouzdanu početnu vrijednost i upozoriti vas samo kada se nešto značajno promijeni.
Ovaj vodič izrađuje taj program u izvornom PHP-u 8.3. Poziva API za otkrivanje tehnologija web-mjesta, provjerava odgovor na granici aplikacije, pohranjuje jednu snimku po web-mjestu i proizvodi obavijesti prilagođene cronu. Pouzdanost i dokazi ostaju dostupni za istragu, dok uobičajene promjene dokaza ne stvaraju bučna upozorenja.
Dobijte pristup i kopirajte token ograničen na uslugu
- Registrirajte se na https://ai.mihajlo.mk/register ili upotrijebite https://ai.mihajlo.mk/login ako već imate račun.
- Otvorite stranicu usluge Website Technology Detector.
- Odaberite dostupni plan Free, Plus ili Pro i dovršite njegovu aktivaciju.
- Otvorite službenu dokumentaciju usluge.
- Pronađite ploču Service token i kopirajte token ograničen na tu uslugu.
Ova usluga zahtijeva autentikaciju. Prihvaća Bearer token, zaglavlje X-API-Token ili parametar upita token. Projekt koristi Bearer token kako vjerodajnica ne bi završila u URL-ovima, zapisnicima pristupa proxyja i povijesti preglednika.
Ponovno generiranje tokena usluge opoziva prethodno aktivni token. Rotaciju tretirajte kao promjenu postavljanja: ažurirajte tajnu svugdje gdje se monitor izvodi, provjerite novi token i tek tada smatrajte rotaciju dovršenom.
Potvrdite krajnju točku prije pisanja PHP-a
Točan zahtjev je POST https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies, s JSON tijelom koje sadrži url. Pošaljite jedan minimalni zahtjev iz pouzdanog terminala:
export WEBSITE_TECH_TOKEN='YOUR_SERVICE_TOKEN'
curl --fail-with-body \
--connect-timeout 3 \
--max-time 15 \
-X POST \
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies' \
-H "Authorization: Bearer ${WEBSITE_TECH_TOKEN}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{"url":"https://client.example"}'
Nikada ne spremajte u repozitorij tu povijest ljuske ni stvarni token. Za lokalni razvoj stavite rezervirana mjesta u .env.local i isključite datoteku iz Gita:
# .env.local
WEBSITE_TECH_TOKEN=YOUR_SERVICE_TOKEN
MONITORED_URLS=https://client.example,https://shop.client.example
TECH_STATE_DIR=/var/lib/stack-monitor
# .gitignore
.env.local
.phpunit.cache/
vendor/
Izvorni PHP ne učitava datoteke okruženja automatski. Tijekom lokalne provjere izvezite ovu datoteku kroz ljusku pomoću set -a; . ./.env.local; set +a. U produkciji umetnite iste varijable kroz raspoređivač, mehanizam tajni spremnika ili upravitelj usluga.
Odaberite malu arhitekturu svjesnu kvarova
Planirana naredba bolje odgovara od web-rute. Otkrivanje je pozadinski posao, nijedan korisnik ne bi trebao čekati na njega, a cron već pruža raspoređivanje i isporuku e-pošte na uobičajenim Linux hostovima.
Put podataka namjerno je kratak:
- Namjenski API klijent šalje autentificirani zahtjev s ograničenim vremenskim čekanjima i ponovnim pokušajima.
- Mapator provjerava otkrivanja, pouzdanost, dokaze, verzije i informacije o preusmjeravanju.
- Naredba uspoređuje stabilne identitete tehnologija, verzije i preusmjeravanja sa spremljenom snimkom.
- Atomsko ažuriranje stanja postaje sljedeća početna vrijednost.
- Naredba ispisuje samo promjene ili neuspjehe, dopuštajući cronu da obavijesti razvojnog programera.
Prvo uspješno pokretanje stvara početnu vrijednost bez podizanja upozorenja. Pouzdanost i dokazi spremaju se radi dijagnostike, ali su isključeni iz otkrivanja promjena jer male varijacije dokaza inače mogu proizvesti ponavljajuće obavijesti.
Stvorite projekt izvornog PHP-a
Projekt zahtijeva PHP 8.3, proširenja cURL i JSON, Composer te PHPUnit za automatizirane testove.
{
"name": "example/client-stack-monitor",
"require": {
"php": "^8.3",
"ext-curl": "*",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.0"
},
"autoload": {
"classmap": ["src/"]
},
"scripts": {
"test": "phpunit tests"
}
}
client-stack-monitor/
├── bin/
│ └── check-stack.php
├── src/
│ ├── Transport.php
│ ├── CurlTransport.php
│ ├── TechnologyDetectorClient.php
│ └── DetectionSnapshot.php
├── tests/
│ └── TechnologyDetectorClientTest.php
├── composer.json
└── .env.local
Pokrenite composer install nakon stvaranja datoteka.
Izgradite HTTP granicu koja se može umetnuti
Apstrakcija prijenosa drži pojedinosti cURL-a izvan domene i omogućuje testiranje ponovnih pokušaja bez mrežnog pristupa.
<?php
// src/Transport.php
declare(strict_types=1);
namespace App;
interface Transport
{
public function post(
string $url,
array $headers,
string $body,
int $connectTimeout,
int $responseTimeout
): HttpResponse;
}
final readonly class HttpResponse
{
public function __construct(
public int $status,
public string $body,
public array $headers = []
) {}
}
final class TransportFailure extends \RuntimeException {}
final class ApiFailure extends \RuntimeException
{
public function __construct(
public readonly string $kind,
string $message,
public readonly ?int $status = null
) {
parent::__construct($message);
}
}
<?php
// src/CurlTransport.php
declare(strict_types=1);
namespace App;
final class CurlTransport implements Transport
{
public function post(
string $url,
array $headers,
string $body,
int $connectTimeout,
int $responseTimeout
): HttpResponse {
$handle = curl_init($url);
if ($handle === false) {
throw new TransportFailure('Unable to initialize cURL.');
}
$responseHeaders = [];
curl_setopt_array($handle, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => $connectTimeout,
CURLOPT_TIMEOUT => $responseTimeout,
CURLOPT_HEADERFUNCTION => static function ($curl, string $line) use (&$responseHeaders): int {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
]);
$bodyResult = curl_exec($handle);
if ($bodyResult === false) {
$message = curl_error($handle);
curl_close($handle);
throw new TransportFailure($message ?: 'Network request failed.');
}
$status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
return new HttpResponse($status, $bodyResult, $responseHeaders);
}
}
Dodajte ograničene ponovne pokušaje i strukturirane neuspjehe
Prolazne mrežne pogreške, ograničenja stope i odabrani neuspjesi poslužitelja dobivaju najviše tri pokušaja. Neuspjesi autentikacije i provjere vraćaju se odmah: ponovni pokušaj s nevažećim tokenom ili tijelom troši kvotu i odgađa dijagnostiku.
<?php
// src/TechnologyDetectorClient.php
declare(strict_types=1);
namespace App;
final class TechnologyDetectorClient
{
private const ENDPOINT =
'https://ai.mihajlo.mk/api/website-technology-detector/v1/detect-technologies';
public function __construct(
private readonly Transport $transport,
private readonly string $token,
private readonly \Closure $sleep
) {
if ($token === '') {
throw new \InvalidArgumentException('WEBSITE_TECH_TOKEN is required.');
}
}
public function detect(string $url): array
{
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new \InvalidArgumentException('A valid absolute URL is required.');
}
$body = json_encode(['url' => $url], JSON_THROW_ON_ERROR);
$retryable = [429, 500, 502, 503, 504];
for ($attempt = 1; $attempt <= 3; $attempt++) {
try {
$response = $this->transport->post(
self::ENDPOINT,
[
'Authorization: Bearer ' . $this->token,
'Accept: application/json',
'Content-Type: application/json',
],
$body,
3,
15
);
} catch (TransportFailure $error) {
if ($attempt === 3) {
throw new ApiFailure('network', $error->getMessage());
}
($this->sleep)(200_000 * $attempt);
continue;
}
if ($response->status >= 200 && $response->status < 300) {
try {
$decoded = json_decode($response->body, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $error) {
throw new ApiFailure('protocol', 'API returned invalid JSON.');
}
if (!is_array($decoded)) {
throw new ApiFailure('protocol', 'API response must be a JSON object.');
}
return $decoded;
}
if (in_array($response->status, $retryable, true) && $attempt < 3) {
$retryAfter = $response->headers['retry-after'] ?? null;
$seconds = ctype_digit((string) $retryAfter)
? min(2, (int) $retryAfter)
: 0;
($this->sleep)(
$seconds > 0 ? $seconds * 1_000_000 : 200_000 * $attempt
);
continue;
}
$kind = match ($response->status) {
400, 422 => 'validation',
401, 403 => 'authentication',
429 => 'rate_limit',
default => $response->status >= 500 ? 'service' : 'http',
};
throw new ApiFailure(
$kind,
"Technology API failed with HTTP {$response->status}.",
$response->status
);
}
throw new ApiFailure('internal', 'Retry loop ended unexpectedly.');
}
}
Mapirajte odgovor na granici
Vanjski JSON ne smije neprovjeren dospjeti u logiku usporedbe. Mapator u nastavku odbacuje nedostajuće identitete i pogrešno tipiziranu pouzdanost, dokaze, verzije ili podatke o preusmjeravanju. Čuva korisna dijagnostička polja, a istodobno stvara stabilan prikaz za usporedbu.
<?php
// src/DetectionSnapshot.php
declare(strict_types=1);
namespace App;
final readonly class DetectionSnapshot
{
public function __construct(
public array $detections,
public array $redirects
) {}
public static function fromApi(array $payload): self
{
$items = $payload['detections'] ?? null;
$redirects = $payload['redirects'] ?? [];
if (!is_array($items) || !is_array($redirects)) {
throw new ApiFailure(
'protocol',
'Response detections and redirects must be arrays.'
);
}
$mapped = [];
foreach ($items as $item) {
if (!is_array($item)) {
throw new ApiFailure('protocol', 'Each detection must be an object.');
}
$name = $item['name'] ?? null;
$confidence = $item['confidence'] ?? null;
$evidence = $item['evidence'] ?? [];
$versions = $item['versions'] ?? [];
if (!is_string($name) || trim($name) === '') {
throw new ApiFailure('protocol', 'Detection name is missing.');
}
if ($confidence !== null && !is_numeric($confidence)) {
throw new ApiFailure('protocol', 'Confidence must be numeric.');
}
if (!is_array($evidence) || !is_array($versions)) {
throw new ApiFailure('protocol', 'Evidence and versions must be arrays.');
}
$versions = array_values(array_filter(
$versions,
static fn ($value): bool => is_string($value) && $value !== ''
));
sort($versions, SORT_STRING);
$mapped[strtolower(trim($name))] = [
'name' => trim($name),
'confidence' => $confidence === null ? null : (float) $confidence,
'evidence' => $evidence,
'versions' => $versions,
];
}
ksort($mapped, SORT_STRING);
return new self($mapped, array_values($redirects));
}
public function comparisonView(): array
{
return [
'technologies' => array_map(
static fn (array $item): array => [
'name' => $item['name'],
'versions' => $item['versions'],
],
$this->detections
),
'redirects' => $this->redirects,
];
}
public function toArray(): array
{
return [
'detections' => $this->detections,
'redirects' => $this->redirects,
'comparison' => $this->comparisonView(),
];
}
}
Održavajte ovaj mapator usklađenim sa shemom odgovora prikazanom u službenoj dokumentaciji. Ako se usluga nekompatibilno promijeni, neuspjeh protokola sigurniji je od tihog bilježenja praznog stacka i izvještavanja da je svaka tehnologija nestala.
Usporedite snimke i pošaljite obavijesti
Naredba provjerava konfiguraciju, serijalizira istodobna pokretanja zaključavanjem datoteke, zapisuje stanje atomskim preimenovanjem i bilježi operativne događaje putem sysloga. Ispisuje samo djelotvorne promjene i neuspjehe.
<?php
// bin/check-stack.php
declare(strict_types=1);
use App\CurlTransport;
use App\DetectionSnapshot;
use App\TechnologyDetectorClient;
require dirname(__DIR__) . '/vendor/autoload.php';
openlog('stack-monitor', LOG_PID, LOG_USER);
$token = getenv('WEBSITE_TECH_TOKEN') ?: '';
$stateDir = getenv('TECH_STATE_DIR') ?: '';
$urls = array_values(array_filter(array_map(
'trim',
explode(',', getenv('MONITORED_URLS') ?: '')
)));
if ($token === '' || $stateDir === '' || $urls === []) {
fwrite(STDOUT, "Stack monitor configuration is incomplete.\n");
exit(2);
}
if (!is_dir($stateDir) || !is_writable($stateDir)) {
fwrite(STDOUT, "State directory is missing or not writable.\n");
exit(2);
}
$client = new TechnologyDetectorClient(
new CurlTransport(),
$token,
static fn (int $microseconds) => usleep($microseconds)
);
$exit = 0;
foreach ($urls as $url) {
$id = hash('sha256', $url);
$stateFile = $stateDir . '/' . $id . '.json';
$lock = fopen($stateDir . '/' . $id . '.lock', 'c');
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
syslog(LOG_WARNING, "Skipped concurrent check url_hash={$id}");
continue;
}
try {
$snapshot = DetectionSnapshot::fromApi($client->detect($url));
$current = $snapshot->toArray();
$previous = null;
if (is_file($stateFile)) {
$previous = json_decode(
(string) file_get_contents($stateFile),
true,
512,
JSON_THROW_ON_ERROR
);
}
$temporary = $stateFile . '.' . getmypid() . '.tmp';
file_put_contents(
$temporary,
json_encode($current, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
LOCK_EX
);
rename($temporary, $stateFile);
if ($previous === null) {
syslog(LOG_INFO, "Baseline created url_hash={$id}");
} elseif (($previous['comparison'] ?? null) !== $current['comparison']) {
echo "Public technology stack changed: {$url}\n";
echo json_encode([
'before' => $previous['comparison'] ?? null,
'after' => $current['comparison'],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n";
$exit = max($exit, 10);
} else {
syslog(LOG_INFO, "No stack change url_hash={$id}");
}
} catch (Throwable $error) {
syslog(LOG_ERR, "Check failed url_hash={$id} type=" . $error::class);
echo "Technology check failed for {$url}: {$error->getMessage()}\n";
$exit = 20;
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
}
exit($exit);
Naredba ažurira snimku nakon uspješne usporedbe, tako da jedna promjena proizvede jednu obavijest umjesto ponavljajućih upozorenja. Neuspjeli zahtjev nikada ne prepisuje posljednje pouzdano stanje.
Testirajte bez pozivanja aktivne usluge
Deterministički lažni prijenos provjerava najvažniji put neuspjeha: odgovor s ograničenom stopom ponovno se pokušava, dok stvarni kod klijenta i dalje obrađuje token i tijelo zahtjeva.
<?php
// tests/TechnologyDetectorClientTest.php
declare(strict_types=1);
use App\HttpResponse;
use App\TechnologyDetectorClient;
use App\Transport;
use PHPUnit\Framework\TestCase;
final class TechnologyDetectorClientTest extends TestCase
{
public function testRetriesRateLimitAndReturnsDecodedPayload(): void
{
$transport = new class implements Transport {
public int $calls = 0;
public function post(
string $url,
array $headers,
string $body,
int $connectTimeout,
int $responseTimeout
): HttpResponse {
$this->calls++;
if ($this->calls === 1) {
return new HttpResponse(429, '{}', ['retry-after' => '1']);
}
self::assertSame(['url' => 'https://client.example'], json_decode($body, true));
self::assertContains('Authorization: Bearer test-token', $headers);
self::assertSame(3, $connectTimeout);
self::assertSame(15, $responseTimeout);
return new HttpResponse(200, json_encode([
'detections' => [[
'name' => 'Example CMS',
'confidence' => 95,
'evidence' => ['header'],
'versions' => ['8.3'],
]],
'redirects' => [],
], JSON_THROW_ON_ERROR));
}
};
$delays = [];
$client = new TechnologyDetectorClient(
$transport,
'test-token',
static function (int $delay) use (&$delays): void {
$delays[] = $delay;
}
);
$result = $client->detect('https://client.example');
self::assertSame(2, $transport->calls);
self::assertSame([1_000_000], $delays);
self::assertSame('Example CMS', $result['detections'][0]['name']);
}
}
Pokrenite composer test. Dodajte popratne testove za neposredni neuspjeh 401, iscrpljene ponovne pokušaje 503, nevažeći JSON, neispravna otkrivanja, početnu vrijednost pri prvom pokretanju i promijenjenu verziju. Ti slučajevi štite ponašanje koje će najvjerojatnije proizvesti propuštena ili obmanjujuća upozorenja.
Sigurno postavite i rasporedite monitor
Stvorite direktorij stanja izvan web-korijena i ograničite ga na račun operativnog sustava koji pokreće posao. Nemojte bilježiti token, zaglavlja autorizacije, potpune API odgovore ni tijela odgovora iz neuspjeha. Naredba bilježi hash URL-a u syslog jer čak i javno ime hosta klijenta može biti osjetljiva operativna informacija.
Unos u cronu može učitati datoteku okruženja čitljivu samo rootu i poslati izlaz naredbe e-poštom:
[email protected]
17 * * * * set -a; . /etc/client-stack-monitor.env; set +a; cd /opt/client-stack-monitor && /usr/bin/php bin/check-stack.php
Odaberite učestalost koja odgovara kvoti aktiviranog plana. Izlazni kod 0 znači uspjeh ili stvaranje početne vrijednosti, 10 znači otkrivenu promjenu, a 20 znači da je barem jedna provjera neuspjela. Cron e-pošta pruža obavijest, dok se ti izlazni kodovi također čisto integriraju s raspoređivačem ili sustavom nadzora.
Uobičajeni produkcijski neuspjesi
- 401 ili 403: provjerite umetnuti token i je li ponovno generiran. Klijent namjerno ne pokušava ponovno neuspjehe autentikacije.
- 400 ili 422: pregledajte konfigurirani URL i ugovor zahtjeva. Neuspjesi provjere nisu prolazni.
- 429: smanjite učestalost raspoređivanja ili pregledajte aktivni plan. Ponovni pokušaji su ograničeni i poštuju kratki numerički
Retry-After. - Ponovljeni 5xx ili vremenska čekanja: zadržite posljednju početnu vrijednost, upozorite na neuspjeh i pustite da sljedeće planirano pokretanje pokuša ponovno.
- Neočekivani neuspjeh protokola: usporedite službenu dokumentaciju odgovora s
DetectionSnapshot::fromApi(); nikada nemojte oslabiti provjeru samo da biste potisnuli pogrešku. - Nema cron e-pošte: potvrdite da host ima ispravan prijenos pošte ili usmjerite izlaze koji nisu nula i standardni izlaz kroz nadzornu infrastrukturu koju poslužitelj već koristi.
Kontrolni popis za završnu provjeru
- Plan usluge je aktivan, a token ograničen na uslugu umeće se iz okruženja.
- Minimalni POST zahtjev uspijeva prema točnoj krajnjoj točki za otkrivanje.
composer testprolazi bez mrežnog pristupa.- Prvo pokretanje naredbe stvara stanje, ali ne šalje upozorenje o promjeni.
- Kontrolirana izmjena snimke proizvodi izlazni kod
10i vidljive podatke prije i poslije. - Nevažeći token proizvodi jedan neuspjeh autentikacije bez ponovnih pokušaja ili gubitka stanja.
- Direktorij stanja i datoteka okruženja čitljivi su samo računu za postavljanje.
- Syslog prima događaje uspjeha i neuspjeha s hashiranim URL-om, bez vjerodajnica.
- Učestalost raspoređivača ostaje unutar ograničenja odabranog plana.
Pouzdan monitor stacka manje se odnosi na slanje čestih zahtjeva, a više na očuvanje vjerodostojnog stanja. Provjeravanjem vanjskih podataka, razlikovanjem trajnih neuspjeha od prolaznih, potiskivanjem šuma dokaza i odbijanjem prepisivanja dobre početne vrijednosti nakon pogreške, ova mala PHP usluga pretvara promjenu javnog web-mjesta u koristan inženjerski signal. Sljedeći put kada klijent tiho zamijeni okvir, CMS ili put preusmjeravanja, otkriće stiže kao djelotvorno upozorenje umjesto neugodnog iznenađenja.