显示禁闭内容
是否对公众显示此内容
561
5
引用原文希米密聊 : https://www.dalao.net/thread-60045.htm
- 为我的 ximi-im 聊天程序写了个单页安装程序,代码结构很简单,下面贴上;
- 原理很简单就是使用curl命令从我服务器上直接下载源代码到服务器,然后生成数据库配置文件;
- 对小白用户来说更省事,不需改动代码,但是对于虚拟主机可能不是很友好,很多虚拟机不支持curl扩展,
- 对于不支持curl扩展的依然是可以打包上传文件不影响安装;
预览:




代码:
- 这是我写的聊天程序,单页安装程序,是直接可以运行的,
- 代码供参考,更专业一点可以加上文件Hash验证;
- 如果需要改为适合自己程序,发给ai 提取其中样式结构即可;
<?php
// ========================================================
// 1. 安全拦截:防止重复安装
// ========================================================
if (file_exists(__DIR__ . '/setting.php')) {
die('<div style="padding:20px;background:#ffebee;color:#c62828;border-radius:5px;font-family:sans-serif;"><b>⛔ 安装程序已经运行过了!</b><br>如需重新安装,请删除目录内的 <code>setting.php</code> 以及原有数据库文件夹及文件,然后刷新此页面。</div>');
}
// ========================================================
// 2. 环境依赖检测 (严格区分必需与可选)
// ========================================================
$required_extensions = ['pdo', 'pdo_sqlite', 'json', 'mbstring'];
$optional_extensions = ['curl'];
$env_status = [];
$install_ok = true; // 默认允许安装
// 检查必需扩展
foreach ($required_extensions as $ext) {
$loaded = extension_loaded($ext);
$env_status[$ext] = ['name' => $ext, 'type' => 'required', 'loaded' => $loaded];
if (!$loaded) $install_ok = false;
}
// 检查可选扩展
foreach ($optional_extensions as $ext) {
$loaded = extension_loaded($ext);
$env_status[$ext] = ['name' => $ext, 'type' => 'optional', 'loaded' => $loaded];
}
$curl_enabled = extension_loaded('curl');
$file_list = ['db.php', 'admin.php', 'api.php', 'ximi.js', 'web.js', 'crypto-js.min.js', 'index.html', 'favicon.ico'];
// ========================================================
// 3. 异步修复 API 接口 (仅当 curl 开启时有效)
// ========================================================
if (isset($_GET['action']) && $_GET['action'] === 'repair_file') {
header('Content-Type: application/json');
if (!$curl_enabled) {
die(json_encode(['status' => 'error', 'msg' => '缺少CURL扩展']));
}
$file = $_POST['file'] ?? '';
if (!in_array($file, $file_list)) {
die(json_encode(['status' => 'error', 'msg' => '非法文件']));
}
$base_url = "https://www.ximi.me/usr/demo/xm-im/";
$save_to = __DIR__ . '/' . $file;
$tmp_zip = $save_to . '.zip';
$ch = curl_init($base_url . $file . ".zip");
$fp = fopen($tmp_zip, 'w+');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
$success = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
fclose($fp);
if ($success && file_exists($tmp_zip)) {
rename($tmp_zip, $save_to);
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error', 'msg' => $error ?: '下载失败']);
}
exit;
}
// ========================================================
// 4. 表单提交与配置文件生成
// ========================================================
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$install_ok) die("环境检测未通过,无法继续安装!");
function rand_str($len) {
return substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, $len);
}
$dir_name = trim($_POST['dir_name'] ?? '');
if (empty($dir_name)) $dir_name = rand_str(8);
$db_name = trim($_POST['db_name'] ?? '');
if (empty($db_name)) {
$db_name = rand_str(16) . '.db';
} else {
if (substr($db_name, -3) !== '.db') $db_name .= '.db';
}
$admin_db_name = 'admin_' . rand_str(16) . '.db';
$config_content = "<?php\n// 自动生成的配置文件\nreturn [\n"
. " 'db_dir' => '" . addslashes($dir_name) . "',\n"
. " 'db_name' => '" . addslashes($db_name) . "',\n"
. " 'admin_db_name' => '" . addslashes($admin_db_name) . "'\n"
. "];\n";
file_put_contents(__DIR__ . '/setting.php', $config_content);
// 优化后的安装成功页面,保持统一的毛玻璃 UI 风格
echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script src="https://cdn.tailwindcss.com"></script><title>安装成功 - ximi IM</title></head>
<body class="bg-gradient-to-br from-slate-100 to-gray-200 flex items-center justify-center min-h-screen p-4">
<div class="bg-white/70 backdrop-blur-xl border border-white/50 p-10 rounded-3xl shadow-2xl text-center max-w-md w-full">
<div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6 text-4xl shadow-inner">✅</div>
<h2 class="text-2xl text-slate-800 font-bold mb-4">安装成功就绪</h2>
<p class="text-slate-500 mb-8 text-sm">系统环境、数据库目录与配置文件已安全生成,即将带您前往管理后台。</p>
<div class="w-full bg-gray-200 rounded-full h-1.5 mb-2 overflow-hidden"><div class="bg-green-500 h-1.5 rounded-full animate-[progress_2s_ease-in-out_forwards]"></div></div>
<div class="text-xs text-gray-400">正在重定向...</div>
<style>@keyframes progress { 0% { width: 0%; } 100% { width: 100%; } }</style>
<script>setTimeout(function(){ window.location.href="admin.php"; }, 2000);</script>
</div></body></html>';
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<title>ximi IM - 部署向导</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; }
/* 毛玻璃基础特效 */
.glass-panel {
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
}
/* 交互式步骤切换动画 */
.step-content {
display: none;
opacity: 0;
transform: translateX(10px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.step-content.active {
display: block;
opacity: 1;
transform: translateX(0);
}
/* 极简终端风格展示 */
.term-text { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
</style>
</head>
<body class="bg-gradient-to-br from-slate-100 to-gray-300 min-h-screen flex items-center justify-center p-4 sm:p-8 relative overflow-hidden">
<div class="absolute top-[-10%] left-[-5%] w-96 h-96 bg-blue-300 rounded-full mix-blend-multiply filter blur-3xl opacity-40"></div>
<div class="absolute bottom-[-10%] right-[-5%] w-96 h-96 bg-emerald-300 rounded-full mix-blend-multiply filter blur-3xl opacity-40"></div>
<div class="glass-panel w-full max-w-5xl rounded-[2rem] shadow-2xl flex flex-col md:flex-row overflow-hidden relative z-10 min-h-[600px]">
<div class="w-full md:w-1/3 bg-slate-900/5 p-8 border-b md:border-b-0 md:border-r border-white/40 flex flex-col">
<div class="mb-12 mt-4">
<h1 class="text-3xl font-bold text-slate-800 tracking-tight">ximi IM</h1>
<p class="text-xs text-slate-500 mt-2 tracking-widest uppercase">Secure Setup Wizard</p>
</div>
<div class="space-y-8 relative">
<div class="flex items-start step-indicator" id="nav-step-1">
<div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors duration-300 bg-blue-600 text-white shadow-md z-10 relative nav-icon">1</div>
<div class="ml-4">
<h3 class="text-sm font-semibold text-slate-800">系统特性</h3>
<p class="text-xs text-slate-500 mt-1">了解核心架构机制</p>
</div>
</div>
<div class="flex items-start step-indicator" id="nav-step-2">
<div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors duration-300 bg-white/60 text-slate-400 z-10 relative nav-icon">2</div>
<div class="ml-4">
<h3 class="text-sm font-semibold text-slate-800">环境巡检</h3>
<p class="text-xs text-slate-500 mt-1">服务器依赖项验证</p>
</div>
</div>
<div class="flex items-start step-indicator" id="nav-step-3">
<div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors duration-300 bg-white/60 text-slate-400 z-10 relative nav-icon">3</div>
<div class="ml-4">
<h3 class="text-sm font-semibold text-slate-800">文件校验</h3>
<p class="text-xs text-slate-500 mt-1">完整性检测与自动化修复</p>
</div>
</div>
<div class="flex items-start step-indicator" id="nav-step-4">
<div class="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors duration-300 bg-white/60 text-slate-400 z-10 relative nav-icon">4</div>
<div class="ml-4">
<h3 class="text-sm font-semibold text-slate-800">核心配置</h3>
<p class="text-xs text-slate-500 mt-1">数据库与存储参数设置</p>
</div>
</div>
<div class="absolute left-4 top-4 bottom-8 w-px bg-slate-300/50 -z-0"></div>
</div>
<div class="mt-auto pt-10">
<p class="text-[10px] text-slate-400">Powered by PHP & SQLite</p>
</div>
</div>
<div class="w-full md:w-2/3 p-8 md:p-12 relative flex flex-col">
<div class="step-content active flex-1" id="step-1">
<h2 class="text-2xl font-bold text-slate-800 mb-6">欢迎使用</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8">
<div class="bg-white/50 border border-white p-5 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div class="text-xl mb-2">🔐</div>
<h3 class="font-semibold text-sm text-slate-700">端到端加密通信</h3>
<p class="text-xs text-slate-500 mt-2 leading-relaxed">服务端仅存储密文,彻底杜绝明文数据泄露及中间人监听风险。</p>
</div>
<div class="bg-white/50 border border-white p-5 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div class="text-xl mb-2">💣</div>
<h3 class="font-semibold text-sm text-slate-700">阅后即焚机制</h3>
<p class="text-xs text-slate-500 mt-2 leading-relaxed">高度敏感信息读取后立即在服务端及客户端实行物理销毁,不留痕迹。</p>
</div>
<div class="bg-white/50 border border-white p-5 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div class="text-xl mb-2">📦</div>
<h3 class="font-semibold text-sm text-slate-700">无缝轻量架构</h3>
<p class="text-xs text-slate-500 mt-2 leading-relaxed">基于原生 PHP 与 SQLite 构建,摆脱繁重的中间件,极简高效。</p>
</div>
<div class="bg-white/50 border border-white p-5 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div class="text-xl mb-2">🚀</div>
<h3 class="font-semibold text-sm text-slate-700">极简极速部署</h3>
<p class="text-xs text-slate-500 mt-2 leading-relaxed">无需繁琐命令,单文件上传即用,自动化向导完成所有初始化。</p>
</div>
</div>
<div class="bg-amber-50/80 border border-amber-100 text-amber-700 px-4 py-3 rounded-lg text-xs flex items-center">
<span class="mr-2">💡</span> 强烈建议在生产环境配置 Nginx HTTPS (SSL证书) 以激活完整的安全机制。
</div>
</div>
<div class="step-content flex-1" id="step-2">
<h2 class="text-2xl font-bold text-slate-800 mb-6">服务器环境巡检</h2>
<div class="bg-white/60 border border-white rounded-xl shadow-sm overflow-hidden mb-6">
<table class="w-full text-sm text-left">
<thead class="bg-slate-50/50 text-slate-500 text-xs uppercase border-b border-white">
<tr>
<th class="px-6 py-3 font-medium">扩展组件名称</th>
<th class="px-6 py-3 font-medium text-center">必要性</th>
<th class="px-6 py-3 font-medium text-right">状态检测</th>
</tr>
</thead>
<tbody class="divide-y divide-white/60">
<?php foreach ($env_status as $ext => $info): ?>
<tr class="hover:bg-white/40 transition-colors">
<td class="px-6 py-3 font-medium text-slate-700 term-text"><?=$info['name']?></td>
<td class="px-6 py-3 text-center text-xs">
<span class="px-2 py-1 rounded-md <?= $info['type'] == 'required' ? 'bg-slate-200 text-slate-700' : 'bg-slate-100 text-slate-500' ?>">
<?= $info['type'] == 'required' ? 'Required' : 'Optional' ?>
</span>
</td>
<td class="px-6 py-3 text-right">
<?php if ($info['loaded']): ?>
<span class="inline-flex items-center text-emerald-600 bg-emerald-50 px-2 py-1 rounded-md text-xs font-semibold"><span class="w-1.5 h-1.5 bg-emerald-500 rounded-full mr-1.5"></span>PASS</span>
<?php else: ?>
<span class="inline-flex items-center <?= $info['type'] == 'required' ? 'text-red-600 bg-red-50' : 'text-amber-600 bg-amber-50' ?> px-2 py-1 rounded-md text-xs font-semibold"><span class="w-1.5 h-1.5 <?= $info['type'] == 'required' ? 'bg-red-500' : 'bg-amber-500' ?> rounded-full mr-1.5"></span>FAIL</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<li class="" style="
margin-top: -9px;
padding-bottom: 0.5rem;
color: #88909a;
font-size: 14px;
margin-left: 22px;
">
curl为非必需扩展,仅用于自动修复文件缺失!
</li>
<?php if (!$install_ok): ?>
<div class="bg-red-50 border border-red-100 text-red-600 px-4 py-3 rounded-lg text-sm mb-4">
❌ 核心必需扩展缺失,请配置 php.ini 或在服务器面板(如 1Panel/Synology)中安装后重试。
</div>
<?php else: ?>
<div class="bg-emerald-50 border border-emerald-100 text-emerald-700 px-4 py-3 rounded-lg text-sm mb-4">
✅ 核心环境依赖已就绪,准许安装。
</div>
<?php endif; ?>
</div>
<div class="step-content flex-1" id="step-3">
<div class="flex justify-between items-end mb-6">
<div>
<h2 class="text-2xl font-bold text-slate-800">系统文件校验</h2>
<p class="text-xs text-slate-500 mt-1">检测程序主体文件完整性,支持自动从云端获取缺失项。</p>
</div>
<?php if ($curl_enabled): ?>
<button id="repair-all-btn" class="bg-blue-50 text-blue-600 hover:bg-blue-600 hover:text-white transition-colors px-3 py-1.5 rounded-lg text-xs font-medium shadow-sm">一键修复全部</button>
<?php endif; ?>
</div>
<div id="file-list" class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-6">
<?php foreach ($file_list as $file):
$exists = file_exists(__DIR__ . '/' . $file);
?>
<div class="bg-white/60 border border-white p-3 rounded-xl flex justify-between items-center shadow-sm" data-file="<?=$file?>">
<span class="text-sm font-medium text-slate-700 term-text"><?=$file?></span>
<div class="file-status text-xs">
<?php if ($exists): ?>
<span class="text-emerald-500 font-semibold bg-emerald-50 px-2 py-1 rounded">Normal</span>
<?php elseif ($curl_enabled): ?>
<button onclick="repairFile('<?=$file?>')" class="text-blue-500 hover:text-blue-700 font-medium bg-blue-50 px-2 py-1 rounded transition-colors">自动修复</button>
<?php else: ?>
<span class="text-slate-400 italic">缺失 (需手动上传)</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="step-content flex-1" id="step-4">
<h2 class="text-2xl font-bold text-slate-800 mb-2">安全配置</h2>
<p class="text-xs text-slate-500 mb-8">设定您的存储路径,保持留空将由算法自动生成高强度乱码命名以防止穷举探测。</p>
<form id="install-form" method="POST" class="space-y-5">
<div class="bg-white/50 border border-white p-6 rounded-2xl shadow-sm">
<div class="mb-5">
<label class="block text-sm font-medium text-slate-700 mb-1">物理数据存放目录 <span class="text-slate-400 text-xs font-normal">(db_dir)</span></label>
<input type="text" name="dir_name" placeholder="建议留空,自动生成随机目录" class="w-full bg-white/70 border border-slate-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all term-text">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">前端数据库文件名 <span class="text-slate-400 text-xs font-normal">(db_name)</span></label>
<input type="text" name="db_name" placeholder="建议留空,自动生成随机库名" class="w-full bg-white/70 border border-slate-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all term-text">
</div>
<div class="mt-4 pt-4 border-t border-slate-200/50 flex items-center justify-between">
<span class="text-xs text-slate-500">管理端独立数据库 (admin_db) 将强制执行随机生成。</span>
<span class="text-xs bg-slate-100 text-slate-500 px-2 py-1 rounded term-text">Auto</span>
</div>
</div>
</form>
</div>
<div class="mt-auto pt-6 border-t border-white/50 flex justify-between items-center">
<button id="btn-prev" class="px-5 py-2.5 rounded-xl text-sm font-medium text-slate-600 hover:bg-white/50 transition-colors hidden">
返回上一步
</button>
<div class="flex-1"></div> <button id="btn-next" class="px-8 py-2.5 rounded-xl text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 shadow-lg shadow-blue-500/30 transition-all">
下一步
</button>
<button id="btn-install" class="px-8 py-2.5 rounded-xl text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 shadow-lg shadow-emerald-500/30 transition-all hidden" onclick="document.getElementById('install-form').submit()">
执行系统安装
</button>
</div>
</div>
</div>
<script>
// --- 交互式步骤导航逻辑 ---
let currentStep = 1;
const totalSteps = 4;
const envPassed = <?= $install_ok ? 'true' : 'false' ?>;
function updateUI() {
// 内容区切换
document.querySelectorAll('.step-content').forEach(el => el.classList.remove('active'));
document.getElementById(`step-${currentStep}`).classList.add('active');
// 侧边栏状态更新
for (let i = 1; i <= totalSteps; i++) {
const icon = document.querySelector(`#nav-step-${i} .nav-icon`);
if (i < currentStep) {
icon.className = 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors duration-300 bg-emerald-500 text-white shadow-md z-10 relative nav-icon';
icon.innerHTML = '✓';
} else if (i === currentStep) {
icon.className = 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors duration-300 bg-blue-600 text-white shadow-md z-10 relative nav-icon';
icon.innerHTML = i;
} else {
icon.className = 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-colors duration-300 bg-white/60 text-slate-400 z-10 relative nav-icon';
icon.innerHTML = i;
}
}
// 按钮显隐逻辑
document.getElementById('btn-prev').style.display = currentStep > 1 ? 'block' : 'none';
if (currentStep === totalSteps) {
document.getElementById('btn-next').style.display = 'none';
document.getElementById('btn-install').style.display = 'block';
} else {
document.getElementById('btn-next').style.display = 'block';
document.getElementById('btn-install').style.display = 'none';
}
// 环境检测未通过,锁定第2步
if (currentStep === 2 && !envPassed) {
document.getElementById('btn-next').disabled = true;
document.getElementById('btn-next').classList.add('opacity-50', 'cursor-not-allowed');
document.getElementById('btn-next').innerHTML = '环境未就绪';
} else {
document.getElementById('btn-next').disabled = false;
document.getElementById('btn-next').classList.remove('opacity-50', 'cursor-not-allowed');
document.getElementById('btn-next').innerHTML = '下一步';
}
}
document.getElementById('btn-next').addEventListener('click', () => {
if (currentStep < totalSteps) { currentStep++; updateUI(); }
});
document.getElementById('btn-prev').addEventListener('click', () => {
if (currentStep > 1) { currentStep--; updateUI(); }
});
// --- 原有的异步文件修复逻辑无缝接入 ---
async function repairFile(filename) {
const el = document.querySelector(`[data-file="${filename}"] .file-status`);
el.innerHTML = '<span class="text-amber-500 bg-amber-50 px-2 py-1 rounded font-medium animate-pulse">Pulling...</span>';
const formData = new FormData();
formData.append('file', filename);
try {
const response = await fetch('?action=repair_file', { method: 'POST', body: formData });
const result = await response.json();
if (result.status === 'success') {
el.innerHTML = '<span class="text-emerald-500 font-semibold bg-emerald-50 px-2 py-1 rounded">Fixed</span>';
} else {
el.innerHTML = `<button class="text-red-500 hover:text-red-700 font-medium bg-red-50 px-2 py-1 rounded" onclick="repairFile('${filename}')">重试</button>`;
}
} catch (e) {
el.innerHTML = `<button class="text-red-500 hover:text-red-700 font-medium bg-red-50 px-2 py-1 rounded" onclick="repairFile('${filename}')">Error(重试)</button>`;
}
}
document.getElementById('repair-all-btn')?.addEventListener('click', async function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = '修复进行中...';
btn.classList.add('opacity-50', 'cursor-not-allowed');
const buttons = document.querySelectorAll('#file-list .file-status button');
for (const actionBtn of buttons) {
actionBtn.click();
await new Promise(r => setTimeout(r, 800));
}
setTimeout(() => {
btn.innerHTML = '执行完毕';
btn.classList.remove('text-blue-600', 'bg-blue-50');
btn.classList.add('text-emerald-600', 'bg-emerald-50');
}, 1000);
});
// 初始化界面
updateUI();
</script>
</body>
</html>
评论加载中...
-
1楼@六哥 ,你上次不是没有安装成功吗,用这个就可以直接安装了,文件我放在国内的博客服务器上了,不需要魔法访问了;
2楼我这是为了偷懒,没写数据库创建,因为首次运行后台会自动创建数据库,不需要那么麻烦
3楼
六哥
https://haoma.id/你给我看看咋回
是否使用的是用的最新版,安装的时候扩展都满足吗
4楼Parse error: syntax error, unexpected '=>' (T_DOUBLE_ARROW), expecting ')' in /www/wwwroot/haoma.id/api.php on line 358看了下报错信息与日志提示,报的是语法错误,
但是我本地检查没有问题,我自己本地服务器都能运行;
发给ai审查提示有错误的注释符合与大括号,我检查看不到,可能是幽灵符号,复制ai给的代码时候出现的,重新生成了一份,
你替换api.php试试看,还有记得给主目录755或是777权限;
api.php<?php ini_set('display_errors', 0); error_reporting(E_ALL); // 强制设置响应头为 JSON header('Content-Type: application/json; charset=utf-8'); date_default_timezone_set('Asia/Shanghai'); // 【新增】配置文件检测逻辑 $settingFile = __DIR__ . '/setting.php'; if (!file_exists($settingFile)) { // 返回一个特定的 JSON 状态码,告知前端系统尚未安装 header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'code' => 503, 'msg' => '系统未安装', 'redirect' => 'install.php' ]); exit; // 终止后续数据库连接等操作,防止报错 } // 如果存在,则继续载入配置 $config = require $settingFile; /* // 💡 【安全限制】:只允许您信任的域进行跨域访问 $allowed_origins = ['https://app.hhqq.net','http://192.168.1.10:555', 'http://www.ximi.me']; $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, $allowed_origins)) { header("Access-Control-Allow-Origin: $origin"); header("Access-Control-Allow-Credentials: true"); header("Access-Control-Allow-Methods: POST, GET, OPTIONS"); header("Access-Control-Allow-Headers: Content-Type, Authorization"); } if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); } */ // ======================================================== // 核心模块 1:全局黑名单拦截 (Req 7) // ======================================================== $blacklistFile = __DIR__ . '/blacklist.json'; $failedAttemptsFile = __DIR__ . '/failed_attempts.json'; if (!file_exists($blacklistFile)) file_put_contents($blacklistFile, json_encode([])); function get_client_ip() { return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } $current_ip = get_client_ip(); $blacklist = json_decode(file_get_contents($blacklistFile), true) ?? []; if (in_array($current_ip, $blacklist)) { header('HTTP/1.0 403 Forbidden'); exit(json_encode(['code' => 403, 'msg' => 'Access Denied: IP Banned'])); } function ban_ip($reason) { global $blacklistFile, $current_ip; $blacklist = json_decode(file_get_contents($blacklistFile), true) ?? []; if (!in_array($current_ip, $blacklist)) { $blacklist[] = $current_ip; file_put_contents($blacklistFile, json_encode($blacklist)); } error_log("[SECURITY ALERT] IP: $current_ip Banned. Reason: $reason"); echo json_encode(['code' => 403, 'msg' => '严重违规:您的请求触发了防御机制,IP 已被永久拦截。']); exit; } // ======================================================== // 核心模块 2:非法攻击计数与熔断 (Req 3) // ======================================================== function record_failed_attempt() { global $failedAttemptsFile, $current_ip; $attempts = file_exists($failedAttemptsFile) ? json_decode(file_get_contents($failedAttemptsFile), true) : []; $attempts[$current_ip] = ($attempts[$current_ip] ?? 0) + 1; file_put_contents($failedAttemptsFile, json_encode($attempts)); if ($attempts[$current_ip] >= 10) { ban_ip("非法字符尝试攻击超过10次"); } } function validate_input_or_fail($str, $regex) { if (!preg_match($regex, $str)) { record_failed_attempt(); echo json_encode(['code' => 400, 'msg' => '输入包含非法字符或长度不符,,请求被拦截, 至少4位用户名,6位密码']); exit; } return true; } // ======================================================== // 核心模块 3:数据库初始化与连接 (Req 1, 9, 10) // ======================================================== require_once __DIR__ . '/db.php'; // 需确保 db.php 中正确实例化了 $pdo // 请将此段代码放在 api.php 中 $pdo 连接定义之后(通常在 db.php 引入之后) function check_upload_enabled() { global $pdo; // 关键:声明使用全局的 $pdo 对象 try { $stmt = $pdo->query("SELECT value FROM settings WHERE key='upload_enabled'"); $res = $stmt->fetch(PDO::FETCH_ASSOC); if ($res && $res['value'] == '0') { // 返回 JSON 并立即停止执行 echo json_encode(['code' => 403, 'msg' => '系统全局文件上传通道已关闭!']); exit; } } catch (Exception $e) { // 如果表不存在,默认视为开启(防止报错) return; } } try { // Req 1 & 9: 自动创建新结构的用户表 $pdo->exec("CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_text TEXT NOT NULL, public_key TEXT, nickname TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_ip TEXT, stop_user INTEGER DEFAULT 0 )"); // Req 10: 保持 messages 表不变 $pdo->exec("CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, sender_id INTEGER NOT NULL, receiver_id INTEGER NOT NULL, msg_type TEXT NOT NULL, encrypt_iv TEXT NOT NULL, encrypted_aes_key TEXT NOT NULL, encrypted_content TEXT NOT NULL, is_downloaded INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )"); } catch (PDOException $e) { exit(json_encode(['code' => 500, 'msg' => '初始化数据表失败'])); } // ======================================================== // 核心模块 4:IP与账号状态鉴权体系 (Req 4, 7) // ======================================================== function verify_user_auth($pdo, $userId) { global $current_ip; if (!$userId) { exit(json_encode(['code' => 400, 'msg' => '鉴权失败:缺少UID'])); } $stmt = $pdo->prepare("SELECT last_ip, stop_user FROM users WHERE id = :id"); $stmt->execute([':id' => $userId]); $user = $stmt->fetch(); if (!$user) exit(json_encode(['code' => 404, 'msg' => '用户不存在'])); if ($user['stop_user'] == 1) exit(json_encode(['code' => 403, 'msg' => '账号已被封禁'])); if ($user['last_ip'] !== $current_ip) exit(json_encode(['code' => 403, 'msg' => '登录 IP 不匹配,拒绝执行越权操作'])); } // ======================================================== // 核心模块 5:底层物理防御沙盒 (Req 5, 6) // ======================================================== define('UPLOADS_DIR', __DIR__ . '/uploads'); if (!file_exists(UPLOADS_DIR)) mkdir(UPLOADS_DIR, 0755, true); function enforce_sandbox($pdo, $userId, $target_path) { global $current_ip; $base_dir = realpath(UPLOADS_DIR); $real_target = realpath($target_path); // 如果路径存在,并且其物理绝对路径不是以 uploads 目录开头 -> 绝对是越权/跳跃目录攻击 if ($real_target !== false && strpos($real_target, $base_dir) !== 0) { // 1. 禁用账号 if ($userId) { $pdo->prepare("UPDATE users SET stop_user = 1 WHERE id = ?")->execute([$userId]); } // 2. 记录错误日志 (ero.json) $logFile = __DIR__ . '/ero.json'; $logData = file_exists($logFile) ? json_decode(file_get_contents($logFile), true) : []; $logData[] = [ 'time' => date('Y-m-d H:i:s'), 'user_id' => $userId ?? 'unknown', 'ip' => $current_ip, 'attempted_path' => $target_path, 'resolved_path' => $real_target ]; file_put_contents($logFile, json_encode($logData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); // 3. 封禁IP并熔断 ban_ip("检测到致命的越权文件操作尝试"); } } // 删除文件夹工具(带沙盒防御) function delete_dir_safe($pdo, $userId, $dir) { enforce_sandbox($pdo, $userId, $dir); if (!is_dir($dir)) return; $files = array_diff(scandir($dir), ['.', '..']); foreach ($files as $file) { $path = $dir . DIRECTORY_SEPARATOR . $file; is_dir($path) ? delete_dir_safe($pdo, $userId, $path) : @unlink($path); } @rmdir($dir); } // ======================================================== // 路由处理入口 // ======================================================== $input = json_decode(file_get_contents('php://input'), true) ?? []; $action = $_GET['action'] ?? ''; // ------------------------------------------ // 1. 用户注册 (Req 1, 2, 3) // ------------------------------------------ if ($action === 'register') { $username = trim($input['username'] ?? ''); $password = trim($input['password'] ?? ''); $publicKey = trim($input['public_key'] ?? ''); $nickname = trim($input['nickname'] ?? ''); if (empty($nickname)) $nickname = $username; // Req 2 & 3: 严格正则验证 // 用户名:只允许英文、数字,4-20位 validate_input_or_fail($username, '/^[a-zA-Z0-9]{4,20}$/'); // 密码:允许大小写字母、数字及常见安全标点,不允许 < > ? 等 validate_input_or_fail($password, '/^[a-zA-Z0-9!@#\$%\^&\*\(\)_\+\-\=\[\]\{\}\|;\':",\.\/~` ]{6,64}$/'); // 昵称:中文、英文、数字、下划线,3-16位 validate_input_or_fail($nickname, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9_]{3,16}$/u'); try { $md5_password = md5($password); $stmt = $pdo->prepare("INSERT INTO users (username, password_text, public_key, nickname, last_ip, stop_user) VALUES (:username, :password, :public_key, :nickname, :last_ip, 0)"); $stmt->execute([ ':username' => $username, ':password' => $md5_password, ':public_key' => $publicKey, ':nickname' => $nickname, ':last_ip' => $current_ip ]); echo json_encode(['code' => 200, 'msg' => '注册成功']); } catch (PDOException $e) { echo json_encode(['code' => $e->getCode() == '23000' ? 400 : 500, 'msg' => $e->getCode() == '23000' ? '用户名已存在' : '注册失败']); } exit; } // ------------------------------------------ // 2. 用户登录 (Req 1, 7) // ------------------------------------------ if ($action === 'login') { $username = trim($input['username'] ?? ''); $password = trim($input['password'] ?? ''); // 防爆破:同样对登录输入进行合法性校验 validate_input_or_fail($username, '/^[a-zA-Z0-9]{4,20}$/'); $stmt = $pdo->prepare("SELECT id, username, password_text, public_key, nickname, stop_user FROM users WHERE username = :username"); $stmt->execute([':username' => $username]); $user = $stmt->fetch(); if ($user) { if ($user['stop_user'] == 1) { exit(json_encode(['code' => 403, 'msg' => '该账号因严重违规已被管理员或系统封禁'])); } if ($user['password_text'] === md5($password)) { // 更新最后登录IP $pdo->prepare("UPDATE users SET last_ip = :ip WHERE id = :id")->execute([':ip' => $current_ip, ':id' => $user['id']]); echo json_encode([ 'code' => 200, 'msg' => '登录成功', 'data' => ['id' => $user['id'], 'username' => $user['username'], 'public_key' => $user['public_key'], 'nickname' => $user['nickname']] ]); exit; } } // 登录失败增加错误记录 record_failed_attempt(); echo json_encode(['code' => 400, 'msg' => '用户名或密码错误']); exit; } // ------------------------------------------ // 3. 获取公钥 // ------------------------------------------ if ($action === 'get_public_key') { $userId = intval($_GET['user_id'] ?? 0); $stmt = $pdo->prepare("SELECT public_key FROM users WHERE id = :id"); $stmt->execute([':id' => $userId]); $user = $stmt->fetch(); if ($user) echo json_encode(['code' => 200, 'data' => ['public_key' => $user['public_key']]]); else echo json_encode(['code' => 404, 'msg' => '未找到该用户']); exit; } // ------------------------------------------ // 4. 发送消息/上传单文件 (Req 4) // ------------------------------------------ if ($action === 'send_message') { $inputData = (strpos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== false) ? $input : $_POST; $sender_id = intval($inputData['sender_id'] ?? 0); $receiver_id = intval($inputData['receiver_id'] ?? 0); $msg_type = trim($inputData['msg_type'] ?? 'text'); $encrypted_content = trim($inputData['encrypted_content'] ?? ''); verify_user_auth($pdo, $sender_id); if ($msg_type === 'file') { check_upload_enabled(); } if ($msg_type === 'file' && isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) { $originalName = basename($_FILES['file']['name']); if (!in_array(strtolower(pathinfo($originalName, PATHINFO_EXTENSION)), ['enc', 'json'])) { ban_ip("尝试上传非法格式后缀文件"); } $safeFileName = time() . '_' . uniqid() . '.enc'; $targetPath = UPLOADS_DIR . '/' . $safeFileName; if (move_uploaded_file($_FILES['file']['tmp_name'], $targetPath)) { $encrypted_content = base64_encode(urlencode($originalName) . '|uploads/' . $safeFileName); } else { exit(json_encode(['code' => 500, 'msg' => '文件保存失败'])); } } try { $stmt = $pdo->prepare("INSERT INTO messages (sender_id, receiver_id, msg_type, encrypt_iv, encrypted_aes_key, encrypted_content) VALUES (?, ?, ?, ?, ?, ?)"); $stmt->execute([$sender_id, $receiver_id, $msg_type, $inputData['encrypt_iv'] ?? '', $inputData['encrypted_aes_key'] ?? '', $encrypted_content]); echo json_encode(['code' => 200, 'msg' => '发送成功']); } catch (PDOException $e) { echo json_encode(['code' => 500, 'msg' => '写入失败']); } exit; } // ------------------------------------------ // 5. 拉取及列表路由 // ------------------------------------------ if ($action === 'list_users') { $stmt = $pdo->query("SELECT id, username, nickname FROM users ORDER BY id ASC"); echo json_encode(['code' => 200, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); exit; } if ($action === 'pull_messages') { $userId = intval($_GET['user_id'] ?? 0); verify_user_auth($pdo, $userId); $pdo->prepare("DELETE FROM messages WHERE receiver_id = ? AND is_downloaded = 1")->execute([$userId]); $stmt = $pdo->prepare("SELECT * FROM messages WHERE receiver_id = ? AND is_downloaded = 0"); $stmt->execute([$userId]); $messages = $stmt->fetchAll(PDO::FETCH_ASSOC); $textIds = array_column(array_filter($messages, fn($m) => $m['msg_type'] === 'text'), 'id'); if ($textIds) $pdo->exec("UPDATE messages SET is_downloaded = 1 WHERE id IN (" . implode(',', $textIds) . ")"); echo json_encode(['code' => 200, 'data' => $messages]); exit; } // ------------------------------------------ // 6. 清除列队及关联文件 (Req 4, 5, 6) // ------------------------------------------ if ($action === 'clear_server_queue') { $userId = intval($input['user_id'] ?? 0); verify_user_auth($pdo, $userId); $stmt = $pdo->prepare("SELECT encrypted_content FROM messages WHERE receiver_id = ? AND msg_type = 'file'"); $stmt->execute([$userId]); foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $info) { $parts = explode('|', base64_decode($info['encrypted_content'])); if (count($parts) >= 2) { $pathStr = trim($parts[1]); // 安全沙盒检测:如果是完整的文件夹(分片),或者是单个文件 if (preg_match('/^[a-f0-9]{32}$/i', $pathStr)) { $targetDir = UPLOADS_DIR . '/' . $pathStr; delete_dir_safe($pdo, $userId, $targetDir); } else if (strpos($pathStr, 'uploads/') === 0) { // 防止传入 uploads/../../index.php $targetFile = __DIR__ . '/' . $pathStr; enforce_sandbox($pdo, $userId, $targetFile); if (file_exists($targetFile)) @unlink($targetFile); } } } $pdo->prepare("DELETE FROM messages WHERE receiver_id = ?")->execute([$userId]); echo json_encode(['code' => 200, 'msg' => '队列及相关文件已安全清空']); exit; } // ------------------------------------------ // 7. 分片上传与清单管理 // ------------------------------------------ if ($action === 'upload_chunk') { $dirId = trim($_GET['id'] ?? ''); $chunkName = trim($_POST['chunk_name'] ?? ''); check_upload_enabled(); // 修正:不要传任何参数 // 强正则限制文件夹和文件名格式,从源头切断 ../ 的可能 if (!preg_match('/^[a-f0-9]{32}$/i', $dirId) || !preg_match('/^chunk_\d+\.enc$/', $chunkName)) { ban_ip("非法的分片上传格式参数"); } $uploadDir = UPLOADS_DIR . "/$dirId/"; if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true); if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) { $targetPath = $uploadDir . $chunkName; move_uploaded_file($_FILES['file']['tmp_name'], $targetPath); echo json_encode(['code' => 200]); } else { echo json_encode(['code' => 500]); } exit; } if ($action === 'save_manifest') { // 补上这行,这才是阻断非法上传的核心! check_upload_enabled(); $dirId = trim($_POST['id'] ?? ''); if (!preg_match('/^[a-f0-9]{32}$/i', $dirId)) exit; $uploadDir = UPLOADS_DIR . "/$dirId/"; if (!file_exists($uploadDir)) mkdir($uploadDir, 0755, true); file_put_contents($uploadDir . "manifest.json", $_POST['manifest'] ?? ''); echo json_encode(['code' => 200]); exit; } if ($action === 'get_manifest') { $dirId = $_GET['id'] ?? ''; if (!preg_match('/^[a-f0-9]{32}$/i', $dirId)) exit; $file = UPLOADS_DIR . "/$dirId/manifest.json"; if (file_exists($file)) echo file_get_contents($file); exit; } // ------------------------------------------ // 8. 物理文件删除 (Req 4, 5, 6) // ------------------------------------------ if ($action === 'delete_files') { // 【重要变更】:此接口现在强制要求传入操作者的 user_id 用于鉴权 $userId = intval($_GET['user_id'] ?? 0); verify_user_auth($pdo, $userId); $dirId = trim($_GET['id'] ?? ''); $messageId = intval($_GET['message_id'] ?? 0); // 双重保险:格式校验 + 沙盒检测 if (!preg_match('/^[a-f0-9]{32}$/i', $dirId)) { ban_ip("尝试使用非标准标识符删除文件"); } if (is_dir($targetDir)) { $files = array_diff(scandir($targetDir), ['.', '..']); foreach ($files as $file) { @unlink($targetDir . '/' . $file); } @rmdir($targetDir); } else if (file_exists($targetDir)) { @unlink($targetDir); } delete_dir_safe($pdo, $userId, $targetDir); if ($messageId > 0) { $pdo->prepare("DELETE FROM messages WHERE id = ?")->execute([$messageId]); } echo json_encode(['code' => 200]); exit; } // ======================================================== // 安全加固模块:双重加密身份安全验证体系 (RSA + AES) // ======================================================== // ======================================================== // 安全加固模块:双重加密身份安全验证体系 (RSA + AES) —— 兼容传参自适应版 // ======================================================== // 1. 预验证接口:生成 128 位随机字符 Token 并进行混合加密 if ($action === 'pre_verify') { if (session_status() === PHP_SESSION_NONE) { session_start(); } // 【双保险获取 UID】:优先从前端传参或 Session 中获取 $userId = intval($_GET['user_id'] ?? $_SESSION['user_id'] ?? 0); if (!$userId) { echo json_encode(['code' => 403, 'msg' => '鉴权失败:未提供有效的操作用户ID']); exit; } // 强行将有效的 UID 写入当前 Session,为接下来的敏感操作铺路 $_SESSION['user_id'] = $userId; // 从 users 表提取当前用户的公钥 $stmt = $pdo->prepare("SELECT public_key FROM users WHERE id = ?"); $stmt->execute([$userId]); $pubKey = $stmt->fetchColumn(); if (!$pubKey) { echo json_encode(['code' => 500, 'msg' => '您的账号未配置安全公钥,无法完成身份双重验证']); exit; } // 生成安全算法物料 $aesKey = bin2hex(random_bytes(8)); // 生成 16 位文本字符作为 AES-128 密钥 $token = bin2hex(random_bytes(64)); // 生成 128 位纯随机字符作为 Token $_SESSION['temp_token'] = $token; // 将原始明文 Token 安全存入服务端 Session 备查 // 使用用户的 RSA 公钥加密 AES 密钥 $encryptedAesKey = ''; $rsaSuccess = openssl_public_encrypt($aesKey, $encryptedAesKey, $pubKey, OPENSSL_PKCS1_PADDING); if (!$rsaSuccess) { echo json_encode(['code' => 500, 'msg' => '服务端内部异常:RSA 公钥加密失败']); exit; } // 使用 AES-128-ECB 模式加密 Token 字符 $encryptedToken = openssl_encrypt($token, "AES-128-ECB", $aesKey, 0); echo json_encode([ 'code' => 200, 'aes_key_enc' => base64_encode($encryptedAesKey), 'token_enc' => $encryptedToken ]); exit; } // 2. 核心敏感操作执行接口:解密比对成功后,才执行真实的修改或注销 if ($action === 'update_profile' || $action === 'delete_account') { if (session_status() === PHP_SESSION_NONE) { session_start(); } $input = json_decode(file_get_contents('php://input'), true) ?? []; // 强制阻断比对:检验前端通过私钥层层解密带回来的 Token 凭证 if (!isset($input['verify_token']) || !isset($_SESSION['temp_token']) || $input['verify_token'] !== $_SESSION['temp_token']) { echo json_encode(['code' => 403, 'msg' => '核心身份验证失败:令牌无效、篡改或已过期']); exit; } // 熔断机制:验证成功后立即彻底销毁 Session 令牌,防止重放攻击 unset($_SESSION['temp_token']); // 获取当前合法的操作人 ID $userId = intval($_SESSION['user_id'] ?? $input['user_id'] ?? 0); if (!$userId) { echo json_encode(['code' => 403, 'msg' => '鉴权失败:无法锁定操作人身份']); exit; } // --- 验证全通过,开始安全执行数据库事务 --- if ($action === 'update_profile') { $nickname = trim($input['nickname'] ?? ''); $password = trim($input['password'] ?? ''); if (empty($nickname) && empty($password)) { echo json_encode(['code' => 400, 'msg' => '未传入任何实质性修改内容']); exit; } // 业务分支 A:修改昵称 if (!empty($nickname)) { $stmt = $pdo->prepare("UPDATE users SET nickname = ? WHERE id = ?"); $stmt->execute([$nickname, $userId]); } // 业务分支 B:修改密码 (保持你项目原有的 md5 散列存储规范) if (!empty($password)) { $md5_password = md5($password); $stmt = $pdo->prepare("UPDATE users SET password_text = ? WHERE id = ?"); $stmt->execute([$md5_password, $userId]); } echo json_encode(['code' => 200, 'msg' => '账号隐私数据已安全同步更新']); } elseif ($action === 'delete_account') { // 业务分支 C:注销账号逻辑 $stmt = $pdo->prepare("DELETE FROM users WHERE id = ?"); $stmt->execute([$userId]); session_destroy(); // 彻底销毁当前登录会话 echo json_encode(['code' => 200, 'msg' => '您的账号及全部关联数据已被物理抹除']); } exit; } echo json_encode(['code' => 404, 'msg' => '无对应接口']);
域名市场域名载入中...
帖子
用户
Google
Bing
搜索历史
清空
猜你想搜
加载中...