Files
2026-06-16 04:58:56 +08:00

438 lines
20 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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
. '// --- Cookie 域(可选,留空则按 HTTP_HOST 自动推断)---' . PHP_EOL
. '// define(\'CLOAK_COOKIE_DOMAIN\', \'.example.com\');' . PHP_EOL
. PHP_EOL
. '// --- GeoIP 本地国家库(MaxMind GeoLite2 Country---' . PHP_EOL
. 'define(\'CLOAK_GEOIP_ENABLED\', \'ON\');' . PHP_EOL
. 'define(\'CLOAK_GEOIP_DB_PATH\', __DIR__ . \'/storage/runtime/c7f2a9e1/cc_idx.dat\');' . PHP_EOL
. 'define(\'CLOAK_GEOIP_CACHE_TTL\', 2592000);' . 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>