Files
2026-10-21 16:44:52 +00:00

44 lines
3.0 KiB
PHP

<?php
/**
* Minimaler Ersatz für Googles Token- und FCM-Schnittstelle (Router für "php -S"), nur für Tests.
* POST /token -> prüft das JWT (RS256, öffentlicher Schlüssel aus FCM_PUBKEY_FILE) und liefert ein Zugriffstoken
* POST /v1/projects/<projekt>/messages:send -> prüft das Zugriffstoken und protokolliert die Nachricht in FCM_LOG (eine JSON-Zeile je Aufruf)
* Enthält ein Geräte-Token das Wort "gone", antwortet der Server wie Google bei einem abgemeldeten Gerät (404 NOT_FOUND).
*/
$log = (string) getenv('FCM_LOG');
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$raw = (string) file_get_contents('php://input');
$write = function (array $row) use ($log) { file_put_contents($log, json_encode($row, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND | LOCK_EX); };
$b64d = fn(string $s) => base64_decode(strtr($s, '-_', '+/'));
if ($path === '/token' && $_SERVER['REQUEST_METHOD'] === 'POST') {
parse_str($raw, $form);
$parts = explode('.', (string) ($form['assertion'] ?? ''));
$ok = false;
$claims = [];
if (count($parts) === 3 && ($form['grant_type'] ?? '') === 'urn:ietf:params:oauth:grant-type:jwt-bearer') {
$pub = openssl_pkey_get_public((string) file_get_contents((string) getenv('FCM_PUBKEY_FILE')));
$ok = $pub && openssl_verify($parts[0] . '.' . $parts[1], $b64d($parts[2]), $pub, OPENSSL_ALGO_SHA256) === 1;
$claims = json_decode($b64d($parts[1]), true) ?: [];
$ok = $ok && ($claims['scope'] ?? '') === 'https://www.googleapis.com/auth/firebase.messaging' && ($claims['exp'] ?? 0) > time();
}
$write(['kind' => 'token', 'ok' => $ok, 'iss' => $claims['iss'] ?? null]);
header('Content-Type: application/json');
if (!$ok) { http_response_code(400); echo json_encode(['error' => 'invalid_grant']); exit; }
echo json_encode(['access_token' => 'tok-' . bin2hex(random_bytes(6)), 'expires_in' => 3600, 'token_type' => 'Bearer']);
exit;
}
if (preg_match('#^/v1/projects/([^/]+)/messages:send$#', (string) $path, $m) && $_SERVER['REQUEST_METHOD'] === 'POST') {
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$msg = json_decode($raw, true)['message'] ?? [];
header('Content-Type: application/json');
if (!str_starts_with($auth, 'Bearer tok-')) { $write(['kind' => 'send', 'ok' => false, 'reason' => 'auth']); http_response_code(401); echo json_encode(['error' => ['status' => 'UNAUTHENTICATED']]); exit; }
$token = (string) ($msg['token'] ?? '');
if (str_contains($token, 'gone')) { $write(['kind' => 'send', 'ok' => false, 'token' => $token, 'reason' => 'gone']); http_response_code(404); echo json_encode(['error' => ['status' => 'NOT_FOUND', 'message' => 'Requested entity was not found.']]); exit; }
$write(['kind' => 'send', 'ok' => true, 'project' => $m[1], 'token' => $token, 'data' => $msg['data'] ?? [], 'priority' => $msg['android']['priority'] ?? '', 'hasNotificationField' => isset($msg['notification'])]);
echo json_encode(['name' => 'projects/' . $m[1] . '/messages/' . bin2hex(random_bytes(4))]);
exit;
}
http_response_code(404);