prüft das JWT (RS256, öffentlicher Schlüssel aus FCM_PUBKEY_FILE) und liefert ein Zugriffstoken * POST /v1/projects//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);