Files
CLOAK/install.php
T

430 lines
20 KiB
PHP
Raw Normal View History

2026-06-14 14:00:24 +08:00
<?php
/**
* install.php — IPCLOAKAPI 一键安装程序
*
* 功能:
* 1. 首次部署时展示配置表单(MySQL / Redis / 登录密码 / API 秘钥)
* 2. 提交后校验 MySQL 连接,创建数据表,写入 cong.php,写入 .install.lock 锁文件
* 3. 锁文件存在时拒绝重新安装(防止覆盖生产配置)
*/
define('LOCK_FILE', __DIR__ . '/.install.lock');
define('CONG_FILE', __DIR__ . '/cong.php');
define('CONG_BACKUP', __DIR__ . '/cong.php.bak');
require_once __DIR__ . '/lib/CloakPhpCompat.php';
$phpCompatReport = CloakPhpCompat::buildReport();
$errors = [];
$success = false;
$isLocked = file_exists(LOCK_FILE);
$schemaMsg = '';
// ── 重置安装锁(仅允许凭旧密码强制重置,用于迁移场景)──────────────────
if (!empty($_POST['action']) && $_POST['action'] === 'unlock') {
if (file_exists(CONG_FILE)) {
@include_once(CONG_FILE);
}
$oldPwd = defined('LOGIN_PASSWORD') ? LOGIN_PASSWORD : '';
if (!empty($_POST['unlock_password']) && $_POST['unlock_password'] === $oldPwd) {
@unlink(LOCK_FILE);
$isLocked = false;
} else {
$errors[] = '旧密码不正确,无法重置安装锁。';
}
}
// ── 表单提交处理 ────────────────────────────────────────────────────────
if (!$isLocked && !empty($_POST['action']) && $_POST['action'] === 'install') {
if (!$phpCompatReport['ok']) {
$errors = array_merge($errors, $phpCompatReport['errors']);
}
// 读取表单值
$db_host = trim($_POST['db_host'] ?? '127.0.0.1') ?: '127.0.0.1';
$db_name = trim($_POST['db_name'] ?? '');
$db_username = trim($_POST['db_username'] ?? '');
$db_password = $_POST['db_password'] ?? '';
$redis_enabled= (isset($_POST['redis_enabled']) && $_POST['redis_enabled'] === 'ON') ? 'ON' : 'OFF';
$redis_host = trim($_POST['redis_host'] ?? '127.0.0.1') ?: '127.0.0.1';
$redis_port = max(1, min(65535, (int)($_POST['redis_port'] ?? 6379)));
$redis_timeout= trim($_POST['redis_timeout']?? '0.5') ?: '0.5';
$redis_pwd = $_POST['redis_password'] ?? '';
$login_pwd = $_POST['login_password'] ?? '';
$check_key = trim($_POST['check_key'] ?? '');
$server_ip = trim($_POST['server_ip'] ?? '');
$cf_token = trim($_POST['cloudflare_api_token'] ?? '');
$cf_account = trim($_POST['cloudflare_account_id'] ?? '');
// 基础校验
if (empty($db_name)) $errors[] = '数据库名不能为空。';
if (empty($db_username)) $errors[] = '数据库用户名不能为空。';
if (empty($login_pwd)) $errors[] = '登录密码不能为空。';
// MySQL 连接测试并创建数据表
if (empty($errors)) {
$conn = @new mysqli($db_host, $db_username, $db_password, $db_name);
if ($conn->connect_error) {
$errors[] = 'MySQL 连接失败:' . $conn->connect_error;
} else {
require_once __DIR__ . '/lib/InstallSchema.php';
$schemaResult = InstallSchema::install($conn);
if (!$schemaResult['ok']) {
$errors[] = '数据库建表失败:' . ($schemaResult['error'] ?? '未知错误');
} else {
$schemaMsg = '已创建/确认数据表:' . implode('、', $schemaResult['tables']);
}
$conn->close();
}
}
// Redis 连接测试(可跳过)
$redisOk = null;
$redisMsg = '';
if (empty($errors) && $redis_enabled === 'ON') {
if (extension_loaded('redis')) {
try {
$r = new Redis();
if (!@$r->connect($redis_host, $redis_port, 0.5)) {
$redisMsg = 'Redis 连接失败,已跳过(安装仍可继续)。';
$redisOk = false;
} else {
if ($redis_pwd !== '') $r->auth($redis_pwd);
$r->ping();
$redisOk = true;
}
} catch (Throwable $e) {
$redisMsg = 'Redis 连接异常:' . $e->getMessage() . '(已跳过)';
$redisOk = false;
}
} else {
$redisMsg = 'phpredis 扩展未安装,Redis 功能不可用(已跳过)。';
$redisOk = false;
}
}
// 写入 cong.php
if (empty($errors)) {
// 备份旧文件
if (file_exists(CONG_FILE)) {
@copy(CONG_FILE, CONG_BACKUP);
}
$cong_content = '<?php' . PHP_EOL
. PHP_EOL
. '// error_reporting(E_ALL);' . PHP_EOL
. '// ini_set(\'display_errors\', 1);' . PHP_EOL
. PHP_EOL
. 'define(\'LOGIN_PASSWORD\', ' . var_export($login_pwd, true) . ');' . PHP_EOL
. PHP_EOL
. 'define(\'DB_USERNAME\', ' . var_export($db_username, true) . ');' . PHP_EOL
. 'define(\'DB_NAME\', ' . var_export($db_name, true) . ');' . PHP_EOL
. 'define(\'DB_PASSWORD\', ' . var_export($db_password, true) . ');' . PHP_EOL
. PHP_EOL
. '// --- API 鉴权秘钥(全局通用,对接 tiktokba.com cloak 服务)---' . PHP_EOL
. 'define(\'CHECK_KEY\', ' . var_export($check_key, true) . ');' . PHP_EOL
. PHP_EOL
. '// --- Redis 通用配置(可选)---' . PHP_EOL
. 'define(\'REDIS_ENABLED\', ' . var_export($redis_enabled, true) . ');' . PHP_EOL
. 'define(\'REDIS_HOST\', ' . var_export($redis_host, true) . ');' . PHP_EOL
. 'define(\'REDIS_PORT\', ' . (int)$redis_port . ');' . PHP_EOL
. 'define(\'REDIS_TIMEOUT\', ' . (float)$redis_timeout . ');' . PHP_EOL
. 'define(\'REDIS_PASSWORD\', ' . var_export($redis_pwd, true) . ');' . PHP_EOL
. PHP_EOL
. '// --- 访客日志异步写入(ON=队列落库,OFF=同步 INSERT---' . PHP_EOL
. 'define(\'VISITOR_LOG_ASYNC\', \'ON\');' . PHP_EOL
. PHP_EOL
. '// --- Cloudflare(可选;留空则域名仅本地登记,不走 API)---' . PHP_EOL
. 'define(\'SERVER_IP\', ' . var_export($server_ip, true) . ');' . PHP_EOL
. 'define(\'CLOUDFLARE_API_TOKEN\', ' . var_export($cf_token, true) . ');' . PHP_EOL
. 'define(\'CLOUDFLARE_ACCOUNT_ID\', ' . var_export($cf_account, true) . ');' . PHP_EOL;
if (file_put_contents(CONG_FILE, $cong_content) === false) {
$errors[] = '写入 cong.php 失败,请检查目录写入权限。';
} else {
// 写入锁文件
file_put_contents(LOCK_FILE, date('Y-m-d H:i:s') . ' installed');
$success = true;
}
}
}
// ── 工具函数 ─────────────────────────────────────────────────────────────
function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function post($k, $d = '') { return h($_POST[$k] ?? $d); }
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IPCLOAKAPI 安装程序</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f0f2f5;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:40px 16px}
.logo{text-align:center;margin-bottom:32px}
.logo h1{font-size:1.7rem;color:#16baaa;font-weight:700}
.logo p{color:#666;font-size:.9rem;margin-top:6px}
.card{background:#fff;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,.1);width:100%;max-width:600px;overflow:hidden}
.card-header{background:linear-gradient(135deg,#16baaa,#0e8d81);color:#fff;padding:18px 24px}
.card-header h2{font-size:1.1rem;font-weight:600;margin:0}
.card-body{padding:24px}
.section-title{font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#1565c0;margin:20px 0 10px;padding-bottom:6px;border-bottom:2px solid #e3f2fd}
.section-title:first-child{margin-top:0}
.form-group{margin-bottom:14px}
.form-group label{display:block;font-size:.85rem;color:#444;margin-bottom:4px;font-weight:500}
.form-group input[type=text],.form-group input[type=password],.form-group input[type=number]{width:100%;padding:8px 12px;border:1px solid #ced4da;border-radius:5px;font-size:.9rem;transition:border .2s}
.form-group input:focus{outline:none;border-color:#16baaa;box-shadow:0 0 0 3px rgba(22,186,170,.15)}
.form-group small{color:#888;font-size:.78rem;margin-top:3px;display:block}
.toggle-row{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.toggle-row label{font-size:.85rem;color:#444;font-weight:500;white-space:nowrap}
.toggle-switch{position:relative;width:46px;height:24px;flex-shrink:0}
.toggle-switch input{opacity:0;width:0;height:0}
.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#ccc;border-radius:24px;transition:.3s}
.slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.3s}
input:checked+.slider{background:#16baaa}
input:checked+.slider:before{transform:translateX(22px)}
.btn{display:inline-block;padding:10px 28px;border-radius:6px;font-size:.95rem;font-weight:600;cursor:pointer;border:none;text-decoration:none;transition:opacity .2s}
.btn:hover{opacity:.88}
.btn-primary{background:#16baaa;color:#fff;width:100%;margin-top:8px}
.btn-secondary{background:#6c757d;color:#fff}
.btn-sm{padding:6px 14px;font-size:.82rem}
.alert{padding:12px 16px;border-radius:6px;margin-bottom:16px;font-size:.88rem}
.alert-danger{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb}
.alert-success{background:#d4edda;color:#155724;border:1px solid #c3e6cb}
.alert-warning{background:#fff3cd;color:#856404;border:1px solid #ffeeba}
.alert-info{background:#d1ecf1;color:#0c5460;border:1px solid #bee5eb}
.divider{border:none;border-top:1px solid #e9ecef;margin:20px 0}
.locked-notice{text-align:center;padding:24px}
.locked-notice .icon{font-size:3rem;margin-bottom:12px}
.locked-notice h3{color:#444;margin-bottom:8px}
.locked-notice p{color:#666;font-size:.88rem;margin-bottom:16px}
details{border:1px solid #dee2e6;border-radius:6px;margin-top:16px}
details summary{padding:10px 14px;cursor:pointer;font-size:.85rem;color:#666;user-select:none}
details summary:hover{background:#f8f9fa}
details .details-body{padding:16px}
.step-indicator{display:flex;gap:0;margin-bottom:28px}
.step{flex:1;text-align:center;padding:8px 4px;font-size:.78rem;color:#aaa;border-bottom:2px solid #dee2e6;font-weight:500}
.step.active{color:#16baaa;border-bottom-color:#16baaa;font-weight:600}
.step.done{color:#28a745;border-bottom-color:#28a745}
</style>
</head>
<body>
<div class="logo">
<h1>IPCLOAKAPI</h1>
<p>一键安装程序 · 首次部署配置向导</p>
</div>
<div class="card">
<div class="card-header">
<h2>环境初始化配置</h2>
</div>
<div class="card-body">
<?php
$phpOk = $phpCompatReport['ok'];
$phpAlertClass = $phpOk ? 'alert-info' : 'alert-danger';
?>
<div class="alert <?php echo $phpAlertClass; ?>">
<strong>PHP 运行环境:</strong>
<?php echo htmlspecialchars(PHP_VERSION); ?>
<?php if ($phpOk): ?>
— 满足 Cloaka 要求(>= <?php echo CloakPhpCompat::MIN_PHP_VERSION; ?>
<?php else: ?>
— <strong>不满足最低要求</strong>,请升级至 PHP <?php echo CloakPhpCompat::MIN_PHP_VERSION; ?>+
<ul style="margin:8px 0 0 18px;padding:0;">
<?php foreach ($phpCompatReport['errors'] as $pe): ?>
<li><?php echo htmlspecialchars($pe); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if (!empty($phpCompatReport['warnings'])): ?>
<div style="margin-top:8px;font-size:.85rem;">
<?php foreach ($phpCompatReport['warnings'] as $pw): ?>
<div>⚠ <?php echo htmlspecialchars($pw); ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if ($isLocked && empty($errors)): ?>
<!-- ── 已安装状态 ── -->
<div class="locked-notice">
<div class="icon">🔒</div>
<h3>系统已安装</h3>
<p>安装锁文件存在,安装程序已被禁用,以防止覆盖生产配置。<br>如需重新配置,请输入当前登录密码以解锁。</p>
<a href="dashboard.php" class="btn btn-primary" style="width:auto;display:inline-block;margin-bottom:16px;">进入管理后台</a>
</div>
<details>
<summary>⚙️ 强制重新配置(危险操作,需验证旧密码)</summary>
<div class="details-body">
<div class="alert alert-warning">此操作将覆盖现有 cong.php,当前配置会备份到 cong.php.bak。</div>
<form method="post">
<input type="hidden" name="action" value="unlock">
<div class="form-group">
<label>当前登录密码</label>
<input type="password" name="unlock_password" placeholder="输入当前后台密码以解锁">
</div>
<button type="submit" class="btn btn-secondary btn-sm">验证并解锁</button>
</form>
</div>
</details>
<?php elseif ($success): ?>
<!-- ── 安装成功 ── -->
<div class="alert alert-success">
<strong>安装成功!</strong> cong.php 已写入,安装锁已创建。
</div>
<?php if (!empty($schemaMsg)): ?>
<div class="alert alert-info"><?php echo h($schemaMsg); ?></div>
<?php endif; ?>
<?php if (!empty($redisMsg)): ?>
<div class="alert alert-warning"><?php echo h($redisMsg); ?></div>
<?php endif; ?>
<p style="color:#555;font-size:.9rem;margin-bottom:20px;">系统已完成初始化配置,请立即进入后台开始使用。</p>
<a href="dashboard.php" class="btn btn-primary">进入管理后台 →</a>
<?php else: ?>
<!-- ── 安装表单 ── -->
<?php if (!empty($errors)): ?>
<div class="alert alert-danger">
<?php foreach ($errors as $e): ?><div>✗ <?php echo h($e); ?></div><?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($redisMsg) && empty($errors)): ?>
<div class="alert alert-warning"><?php echo h($redisMsg); ?></div>
<?php endif; ?>
<form method="post" id="installForm">
<input type="hidden" name="action" value="install">
<!-- MySQL 配置 -->
<div class="section-title">MySQL 数据库</div>
<div class="form-group">
<label>数据库主机</label>
<input type="text" name="db_host" value="<?php echo post('db_host','127.0.0.1'); ?>" placeholder="127.0.0.1">
<small>通常为 127.0.0.1 或 localhost</small>
</div>
<div class="form-group">
<label>数据库名 <span style="color:#dc3545">*</span></label>
<input type="text" name="db_name" value="<?php echo post('db_name'); ?>" placeholder="如:cloaka_db" required>
<small>安装时将自动创建 ip_groups、ip_list、visitor_logs 数据表</small>
</div>
<div class="form-group">
<label>数据库用户名 <span style="color:#dc3545">*</span></label>
<input type="text" name="db_username" value="<?php echo post('db_username'); ?>" placeholder="如:cloaka_user" required>
</div>
<div class="form-group">
<label>数据库密码</label>
<input type="password" name="db_password" value="" placeholder="数据库用户密码" autocomplete="new-password">
</div>
<hr class="divider">
<!-- Redis 配置 -->
<div class="section-title">Redis 缓存(可选)</div>
<div class="toggle-row">
<label>启用 Redis</label>
<label class="toggle-switch">
<input type="checkbox" id="redisToggle" name="redis_enabled" value="ON" <?php echo (($_POST['redis_enabled'] ?? 'ON') === 'ON') ? 'checked' : ''; ?>>
<span class="slider"></span>
</label>
<small style="color:#888">phpredis 扩展<?php echo extension_loaded('redis') ? ' <span style="color:#28a745">✓ 已安装</span>' : ' <span style="color:#dc3545">✗ 未安装</span>'; ?></small>
</div>
<div id="redisFields">
<div class="form-group">
<label>Redis 主机</label>
<input type="text" name="redis_host" value="<?php echo post('redis_host','127.0.0.1'); ?>" placeholder="127.0.0.1">
</div>
<div class="form-group">
<label>Redis 端口</label>
<input type="number" name="redis_port" value="<?php echo post('redis_port','6379'); ?>" min="1" max="65535" style="max-width:120px;">
</div>
<div class="form-group">
<label>连接超时(秒)</label>
<input type="text" name="redis_timeout" value="<?php echo post('redis_timeout','0.5'); ?>" style="max-width:100px;">
</div>
<div class="form-group">
<label>Redis 密码(无密码留空)</label>
<input type="password" name="redis_password" value="" placeholder="无密码留空" autocomplete="new-password">
</div>
</div>
<hr class="divider">
<!-- 系统安全 -->
<div class="section-title">系统安全</div>
<div class="form-group">
<label>后台登录密码 <span style="color:#dc3545">*</span></label>
<input type="password" name="login_password" value="" placeholder="请设置强密码" required autocomplete="new-password">
<small>用于登录 dashboard.php 后台管理页面</small>
</div>
<div class="form-group">
<label>API 秘钥(CHECK_KEY</label>
<input type="text" name="check_key" value="<?php echo post('check_key'); ?>" placeholder="ipcloakapi.com | cloak 服务的 API 秘钥">
<small>由服务商提供,可在安装后于 cong.php 中修改</small>
</div>
<hr class="divider">
<!-- Cloudflare(可选) -->
<div class="section-title">Cloudflare 域名自动化(可选)</div>
<div class="form-group">
<label>服务器公网 IPSERVER_IP</label>
<input type="text" name="server_ip" value="<?php echo post('server_ip'); ?>" placeholder="例如 203.0.113.10">
<small>NS 生效后用于创建 proxied 的 A 记录;可安装后在 cong.php 填写</small>
</div>
<div class="form-group">
<label>Cloudflare API Token</label>
<input type="password" name="cloudflare_api_token" value="" placeholder="需 Zone Edit、DNS Edit、SSL 权限" autocomplete="new-password">
<small>在 Cloudflare 控制台 → My Profile → API Tokens 创建</small>
</div>
<div class="form-group">
<label>Cloudflare Account ID</label>
<input type="text" name="cloudflare_account_id" value="<?php echo post('cloudflare_account_id'); ?>" placeholder="控制台右侧 Account ID">
<small>三项均留空时,域名功能保持本地登记(兼容宝塔手动建站)</small>
</div>
<hr class="divider">
<button type="submit" class="btn btn-primary" id="submitBtn">开始安装</button>
</form>
<?php endif; ?>
</div><!-- /card-body -->
</div><!-- /card -->
<p style="color:#aaa;font-size:.78rem;margin-top:20px;text-align:center;">
安装完成后请将 install.php 删除或设置服务器禁止访问,以保障安全。
</p>
<script>
(function() {
var toggle = document.getElementById('redisToggle');
var fields = document.getElementById('redisFields');
if (!toggle || !fields) return;
function updateFields() {
fields.style.display = toggle.checked ? '' : 'none';
}
toggle.addEventListener('change', updateFields);
updateFields();
var form = document.getElementById('installForm');
var btn = document.getElementById('submitBtn');
if (form && btn) {
form.addEventListener('submit', function() {
btn.disabled = true;
btn.textContent = '安装中...';
});
}
})();
</script>
</body>
</html>