<?php
/**
 * 跳板网关 - 主入口
 *
 * 访问流程：
 * 1. /t/{目标编号} -> 进入七层防御检查
 * 2. 通过检查 -> 展示 JS 挑战页面
 * 3. JS 挑战通过 -> verify.php 发放凭证
 * 4. 凭证跳转 -> redirect.php 执行真实跳转
 */

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/includes/database.php';
require_once __DIR__ . '/includes/ip_checker.php';
require_once __DIR__ . '/includes/ua_filter.php';
require_once __DIR__ . '/includes/rate_limiter.php';
require_once __DIR__ . '/includes/challenge.php';
require_once __DIR__ . '/includes/token.php';
require_once __DIR__ . '/includes/stats.php';

// 自动清理：每天执行一次，只清垃圾数据，不动访问日志（统计永久保留）
$cleanup_flag = DATA_DIR . '/.last_cleanup';
$last_cleanup = @file_get_contents($cleanup_flag);
if (!$last_cleanup || (time() - (int)$last_cleanup) > 86400) {
    @file_put_contents($cleanup_flag, time());
    register_shutdown_function(function () {
        cleanup_rate_limits();
        cleanup_tokens();
        $db = get_db();
        $db->exec('DELETE FROM ip_cache WHERE created_at < ' . (time() - IP_CACHE_TTL));
    });
}

// 解析请求路径
$request_uri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($request_uri, PHP_URL_PATH);
$path = rtrim($path, '/');

// 路由分发
if (preg_match('#^/t/([a-zA-Z0-9_-]+)$#', $path, $matches)) {
    handle_gateway($matches[1]);
} elseif ($path === '/verify') {
    handle_verify();
} elseif ($path === '/beacon') {
    handle_beacon();
} elseif ($path === '/redirect') {
    handle_redirect();
} elseif ($path === '/hp-assets/check') {
    handle_honeypot();
} else {
    show_404();
}

// ============================================
// 主入口处理
// ============================================
function handle_gateway(string $target_key): void {
    global $TARGET_URLS;

    // 目标不存在 -> 真 404
    if (!isset($TARGET_URLS[$target_key])) {
        show_404();
        return;
    }

    $ip = get_client_ip();
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';

    // 第 1 层：黑名单检查（蜜罐/频率限制封禁的 IP）
    if (is_blacklisted($ip)) {
        $bl_reason = get_blacklist_reason($ip);
        log_visit($ip, $target_key, 'blocked_blacklist', [], json_encode(['reason' => $bl_reason, 'progress' => '访问→黑名单✗'], JSON_UNESCAPED_UNICODE));
        show_404();
        return;
    }

    // 第 2 层：UA 黑名单
    if (is_bot_ua($ua)) {
        $matched = get_matched_ua_pattern($ua);
        log_visit($ip, $target_key, 'blocked_ua', [], json_encode(['matched' => $matched, 'ua_length' => strlen($ua), 'progress' => '访问→黑名单→UA✗'], JSON_UNESCAPED_UNICODE));
        show_404();
        return;
    }

    // 第 3 层：IP 检测（硬性条件：国家=JP + 连接类型 + 非VPN/代理/Tor/Relay/机房）
    $ip_info = check_ip($ip);
    $ip_check = is_ip_allowed($ip_info);
    if (!$ip_check['pass']) {
        $reason_map = [
            'country_not_jp'        => 'blocked_ip_country',
            'connection_type_blocked' => 'blocked_ip_type',
            'security_flags'        => 'blocked_ip_security',
            'api_error'             => 'blocked_ip',
        ];
        $action = $reason_map[$ip_check['reason']] ?? 'blocked_ip';
        $detail = [
            'country'  => $ip_info['country_code'] ?? '',
            'type'     => $ip_info['connection_type'] ?? '',
            'status'   => $ip_info['status'] ?? '',
            'ip_reason' => $ip_check['reason'],
            'flags'    => $ip_check['flags'] ?? '',
            'progress' => '访问→黑名单→UA→IP✗',
        ];
        log_visit($ip, $target_key, $action, $ip_info, json_encode($detail, JSON_UNESCAPED_UNICODE));
        show_404();
        return;
    }

    // 第 4 层：频率限制
    if (is_rate_limited($ip)) {
        log_visit($ip, $target_key, 'blocked_rate', $ip_info, json_encode(['window' => RATE_LIMIT_WINDOW . 's', 'max' => RATE_LIMIT_MAX, 'progress' => '访问→黑名单→UA→IP→频率✗'], JSON_UNESCAPED_UNICODE));
        show_404();
        return;
    }

    // 通过前四层 -> 展示 JS 挑战页面（第 5、6 层在客户端执行）
    $challenge = create_challenge($target_key, $ip);
    show_challenge_page($challenge, $target_key);
}

// ============================================
// JS 挑战验证端点
// ============================================
function handle_verify(): void {
    header('Content-Type: application/json');

    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        http_response_code(405);
        echo json_encode(['error' => 'method_not_allowed']);
        return;
    }

    $body = json_decode(file_get_contents('php://input'), true);
    if (!$body) {
        http_response_code(400);
        echo json_encode(['error' => 'invalid_request']);
        return;
    }

    $ip = get_client_ip();
    $target_key = $body['target_key'] ?? '';

    global $TARGET_URLS;
    if (!isset($TARGET_URLS[$target_key])) {
        http_response_code(400);
        echo json_encode(['error' => 'invalid_target']);
        return;
    }

    // PoW + 指纹 + 行为验证（返回详细结果）
    $client_steps = $body['steps'] ?? [];
    $challenge_result = verify_challenge($body, $ip);
    if (!$challenge_result['pass']) {
        $ip_info = check_ip($ip);
        $reason = $challenge_result['reason'];
        $step_progress = build_verify_progress($reason, $client_steps);
        $detail = $challenge_result['detail'];
        $detail['progress'] = $step_progress;
        $detail['steps'] = $client_steps;
        $detail_json = json_encode($detail, JSON_UNESCAPED_UNICODE);
        log_visit($ip, $target_key, $reason, $ip_info, $detail_json);
        http_response_code(403);
        echo json_encode(['error' => 'challenge_failed']);
        return;
    }

    // 软性条件检测：时区=Asia/Tokyo 或 浏览器语言含ja，至少满足一项
    $locale = $body['locale'] ?? [];
    $locale_result = verify_locale($locale);
    if (!$locale_result['pass']) {
        $ip_info = check_ip($ip);
        $detail = [
            'progress'  => '访问→黑名单→UA→IP→频率→页面→勾选→点击→计算→提交→签名→PoW→指纹→行为→地域✗',
            'timezone'  => $locale_result['timezone'],
            'language'  => $locale_result['language'],
            'languages' => $locale_result['languages'],
            'steps'     => $client_steps,
        ];
        log_visit($ip, $target_key, 'blocked_locale', $ip_info, json_encode($detail, JSON_UNESCAPED_UNICODE));
        http_response_code(403);
        echo json_encode(['error' => 'challenge_failed']);
        return;
    }

    // 全部通过 -> 发放一次性凭证
    $ip_info = check_ip($ip);
    $token = generate_token($target_key, $ip);
    $pass_progress = '访问→黑名单→UA→IP→频率→页面→勾选→点击→计算→提交→签名→PoW→指纹→行为→地域✓';
    $pass_detail = json_encode(['progress' => $pass_progress, 'steps' => $client_steps, 'locale' => $locale_result], JSON_UNESCAPED_UNICODE);
    log_visit($ip, $target_key, 'passed', $ip_info, $pass_detail);

    echo json_encode(['token' => $token]);
}

// ============================================
// 凭证跳转端点
// ============================================
function handle_redirect(): void {
    $token = $_GET['t'] ?? '';
    $ip = get_client_ip();

    if (empty($token)) {
        show_404();
        return;
    }

    $target_key = validate_and_consume_token($token, $ip);
    if ($target_key === null) {
        show_404();
        return;
    }

    global $TARGET_URLS;
    if (!isset($TARGET_URLS[$target_key])) {
        show_404();
        return;
    }

    // 执行跳转
    $url = $TARGET_URLS[$target_key];
    header('Location: ' . $url, true, 302);
    exit;
}

// ============================================
// Beacon 离开上报端点
// ============================================
function handle_beacon(): void {
    $body = json_decode(file_get_contents('php://input'), true);
    if (!$body) { http_response_code(204); exit; }

    $ip = get_client_ip();
    $target_key = $body['target_key'] ?? '';
    $last_step = $body['last_step'] ?? '页面';
    $duration = intval($body['duration'] ?? 0);
    $steps_data = $body['steps'] ?? [];

    $progress = build_progress_string($last_step, $steps_data, $duration);
    $ip_info = check_ip($ip);

    log_visit($ip, $target_key, 'left_page', $ip_info, json_encode([
        'last_step' => $last_step,
        'progress' => $progress,
        'duration' => $duration,
        'steps' => $steps_data
    ], JSON_UNESCAPED_UNICODE));

    http_response_code(204);
    exit;
}

function build_progress_string(string $last_step, array $steps, int $duration): string {
    $all_steps = ['访问','黑名单','UA','IP','频率','页面','勾选','点击','计算','提交','签名','PoW','指纹','行为','地域'];
    $step_map = ['页面' => 5, '勾选' => 6, '点击' => 7, '计算' => 8, '提交' => 9];

    $reached = $step_map[$last_step] ?? 5;
    $parts = [];
    for ($i = 0; $i < $reached; $i++) {
        $parts[] = $all_steps[$i];
    }
    $parts[] = $all_steps[$reached] . '✗';
    return implode('→', $parts);
}

function build_verify_progress(string $reason, array $steps): string {
    $all_steps = ['访问','黑名单','UA','IP','频率','页面','勾选','点击','计算','提交','签名','PoW','指纹','行为','地域'];
    $fail_map = [
        'blocked_signature' => 10,
        'blocked_pow' => 11,
        'blocked_fingerprint' => 12,
        'blocked_behavior' => 13,
        'blocked_locale' => 14,
    ];
    $fail_idx = $fail_map[$reason] ?? 13;
    $parts = [];
    for ($i = 0; $i < $fail_idx; $i++) {
        $parts[] = $all_steps[$i];
    }
    $parts[] = $all_steps[$fail_idx] . '✗';
    return implode('→', $parts);
}

// ============================================
// 蜜罐陷阱端点
// ============================================
function handle_honeypot(): void {
    $ip = get_client_ip();
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    add_to_blacklist($ip, 'honeypot');
    log_visit($ip, '', 'blocked_honeypot', [], json_encode(['trigger' => '/hp-assets/check', 'ua' => mb_substr($ua, 0, 100), 'progress' => '访问→蜜罐✗'], JSON_UNESCAPED_UNICODE));
    show_404();
}

// ============================================
// 真 404 伪装页
// ============================================
function show_404(): void {
    http_response_code(404);
    echo '<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>404 Not Found</title>
<style>body{font-family:Arial,sans-serif;text-align:center;padding:80px 20px;background:#fafafa;color:#555}
h1{font-size:72px;margin:0;color:#ddd}p{font-size:16px;margin:20px 0}</style></head>
<body><h1>404</h1><p>The requested resource was not found on this server.</p>
<p style="font-size:12px;color:#aaa">That\'s all we know.</p></body></html>';
    exit;
}

// ============================================
// JS 挑战页面
// ============================================
function show_challenge_page(array $challenge, string $target_key): void {
    $nonce_js     = htmlspecialchars($challenge['nonce'], ENT_QUOTES);
    $target_js    = htmlspecialchars($challenge['target_key'], ENT_QUOTES);
    $difficulty   = (int) $challenge['difficulty'];
    $timestamp_js = (int) $challenge['timestamp'];
    $sig_js       = htmlspecialchars($challenge['signature'], ENT_QUOTES);

    echo '<!DOCTYPE html>
<html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>セキュリティ確認</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Hiragino Kaku Gothic ProN",sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;background:#fafbfc}
.box{width:380px;background:#fff;border:2px solid #e1e4e8;border-radius:6px;overflow:hidden}
.header{padding:20px 24px;border-bottom:1px solid #e1e4e8;display:flex;align-items:center;gap:12px}
.header .dot{width:12px;height:12px;border-radius:50%;background:#2ea44f}
.header span{font-size:14px;font-weight:600;color:#24292e}
.content{padding:32px 24px}
p{font-size:13px;color:#586069;margin-bottom:24px;line-height:1.5}
.check-area{display:flex;align-items:center;gap:10px;padding:12px 16px;border:1px solid #e1e4e8;border-radius:6px;cursor:pointer;margin-bottom:20px;transition:border-color 0.2s}
.check-area:hover{border-color:#0366d6}
.check-area input{width:16px;height:16px;accent-color:#2ea44f}
.check-area label{font-size:14px;color:#24292e;cursor:pointer}
button{width:100%;padding:12px;border:1px solid rgba(27,31,35,0.15);border-radius:6px;background:#2ea44f;color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:background 0.2s}
button:hover:not(:disabled){background:#2c974b}
button:disabled{background:#94d3a2;border-color:#94d3a2;cursor:not-allowed}
.spinner{width:28px;height:28px;border:3px solid #e1e4e8;border-top-color:#2ea44f;border-radius:50%;animation:spin 0.8s linear infinite;margin:16px auto 8px;display:none}
@keyframes spin{to{transform:rotate(360deg)}}
.msg{font-size:12px;color:#586069;text-align:center;margin-top:12px;min-height:18px}
.msg.error{color:#cb2431}
.footer{padding:16px 24px;border-top:1px solid #e1e4e8;text-align:center}
.footer span{font-size:11px;color:#959da5}
.hidden-zone{position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;opacity:0}
</style></head><body>
<div class="box">
<div class="header"><div class="dot"></div><span>セキュリティ確認</span></div>
<div class="content">
<p>安全にページへアクセスするため、以下の確認にご協力ください。</p>
<div class="check-area" onclick="document.getElementById(\'humanCheck\').click()">
<input type="checkbox" id="humanCheck" onclick="onCheckboxClick(event)">
<label for="humanCheck" onclick="event.stopPropagation()">私はロボットではありません</label>
</div>
<button id="submitBtn" disabled onclick="startVerify(event)">確認して続行</button>
<div class="spinner" id="spinner"></div>
<p class="msg" id="status-msg"></p>
</div>
<div class="footer"><span>このチェックはセキュリティのために実施されます</span></div>
</div>
<!-- 蜜罐陷阱 -->
<div class="hidden-zone"><a href="/hp-assets/check">resource</a></div>
<script>
var cfg={nonce:"' . $nonce_js . '",targetKey:"' . $target_js . '",diff:' . $difficulty . ',ts:' . $timestamp_js . ',sig:"' . $sig_js . '"};

// ===== 14步时间线 =====
var T0=Date.now();
var steps={pageLoad:T0,checkbox:0,click:0,powDone:0,submit:0};
var submitted=false;

// ===== 行为数据收集 =====
var behavior={
    pageLoadTime:T0,checkboxTime:0,submitTime:0,
    mouseMoveCnt:0,touchCnt:0,touchRadiusSum:0,
    checkboxCoord:null,submitCoord:null,
    hadFocus:document.hasFocus(),hadVisibility:!document.hidden,
    maxTouchPoints:navigator.maxTouchPoints||0,
    hasTouch:"ontouchstart" in window,
    deviceMemory:navigator.deviceMemory||0,
    hardwareConcurrency:navigator.hardwareConcurrency||0,
    hasOrientation:typeof screen.orientation!=="undefined",
    colorDepth:screen.colorDepth||0
};

document.addEventListener("mousemove",function(){behavior.mouseMoveCnt++;},{passive:true});
document.addEventListener("touchstart",function(e){
    behavior.touchCnt++;
    if(e.touches&&e.touches[0]){var t=e.touches[0];behavior.touchRadiusSum+=(t.radiusX||0)+(t.radiusY||0);}
},{passive:true});
window.addEventListener("focus",function(){behavior.hadFocus=true;});
document.addEventListener("visibilitychange",function(){if(!document.hidden)behavior.hadVisibility=true;});

// ===== 离开时上报（sendBeacon）=====
function getLastStep(){
    if(steps.submit)return "提交";
    if(steps.powDone)return "计算";
    if(steps.click)return "点击";
    if(steps.checkbox)return "勾选";
    return "页面";
}
function sendLeaveBeacon(){
    if(submitted)return;
    var data={target_key:cfg.targetKey,last_step:getLastStep(),
        steps:{pageLoad:0,checkbox:steps.checkbox?steps.checkbox-T0:0,click:steps.click?steps.click-T0:0,powDone:steps.powDone?steps.powDone-T0:0},
        duration:Date.now()-T0};
    navigator.sendBeacon("/beacon",JSON.stringify(data));
    submitted=true;
}
window.addEventListener("pagehide",sendLeaveBeacon);

function toggleBtn(){
    var cb=document.getElementById("humanCheck");
    document.getElementById("submitBtn").disabled=!cb.checked;
    if(cb.checked&&!behavior.checkboxTime){behavior.checkboxTime=Date.now();steps.checkbox=Date.now();}
}

function onCheckboxClick(e){
    e.stopPropagation();
    toggleBtn();
    if(e.clientX!==undefined)behavior.checkboxCoord={x:e.clientX,y:e.clientY};
}

function startVerify(e){
    if(!document.getElementById("humanCheck").checked)return;
    behavior.submitTime=Date.now();
    steps.click=Date.now();
    if(e&&e.clientX!==undefined)behavior.submitCoord={x:e.clientX,y:e.clientY};
    document.getElementById("submitBtn").disabled=true;
    document.getElementById("submitBtn").textContent="確認中...";
    document.getElementById("spinner").style.display="block";
    document.getElementById("status-msg").textContent="確認処理中です。しばらくお待ちください...";
    document.getElementById("status-msg").className="msg";
    runPoW();
}

function collectFingerprint(){
    var fp={};
    try{fp.webdriver=!!navigator.webdriver}catch(e){fp.webdriver=null}
    try{fp.pluginCount=navigator.plugins?navigator.plugins.length:0}catch(e){fp.pluginCount=0}
    try{fp.languageCount=navigator.languages?navigator.languages.length:0}catch(e){fp.languageCount=0}
    try{fp.hasChrome=!!window.chrome}catch(e){fp.hasChrome=false}
    try{fp.notificationPermission=Notification.permission||"default"}catch(e){fp.notificationPermission="default"}
    try{fp.screenWidth=screen.width;fp.screenHeight=screen.height}catch(e){}
    try{
        var c=document.createElement("canvas");var ctx=c.getContext("2d");
        ctx.textBaseline="top";ctx.font="14px Arial";ctx.fillText("gateway-fp",2,2);
        fp.canvasHash=c.toDataURL().slice(-50);
    }catch(e){fp.canvasHash=""}
    try{
        var gl=document.createElement("canvas").getContext("webgl");
        var dbg=gl.getExtension("WEBGL_debug_renderer_info");
        fp.webglRenderer=dbg?gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL):"";
    }catch(e){fp.webglRenderer=""}
    try{
        fp.hasAutomation=!!(window.__nightmare||window._phantom||window.__selenium_unwrapped||
            window.__webdriver_evaluate||window.__driver_evaluate||
            document.__selenium_unwrapped||document.__webdriver_evaluate);
    }catch(e){fp.hasAutomation=false}
    return fp;
}

async function solveAsync(nonce,prefix){
    var encoder=new TextEncoder();
    for(var n=0;n<1e8;n++){
        var data=encoder.encode(nonce+n.toString());
        var hashBuf=await crypto.subtle.digest("SHA-256",data);
        var hashArr=Array.from(new Uint8Array(hashBuf));
        var hex=hashArr.map(function(b){return b.toString(16).padStart(2,"0")}).join("");
        if(hex.startsWith(prefix))return n.toString();
    }
    return null;
}

async function runPoW(){
    var fp=collectFingerprint();
    if(fp.hasAutomation||fp.webdriver===true){
        document.getElementById("spinner").style.display="none";
        document.getElementById("status-msg").textContent="アクセスが拒否されました。";
        document.getElementById("status-msg").className="msg error";
        return;
    }

    var prefix="";for(var i=0;i<cfg.diff;i++)prefix+="0";
    var answer=await solveAsync(cfg.nonce,prefix);
    steps.powDone=Date.now();
    if(!answer){
        document.getElementById("spinner").style.display="none";
        document.getElementById("status-msg").textContent="確認に失敗しました。ページを更新してください。";
        document.getElementById("status-msg").className="msg error";
        return;
    }

    steps.submit=Date.now();
    var tz="";try{tz=Intl.DateTimeFormat().resolvedOptions().timeZone||"";}catch(e){}
    var lang=navigator.language||navigator.userLanguage||"";
    var langs=navigator.languages?Array.from(navigator.languages):[];

    var payload={
        nonce:cfg.nonce,target_key:cfg.targetKey,answer:answer,
        timestamp:cfg.ts,signature:cfg.sig,fingerprint:fp,
        locale:{timezone:tz,language:lang,languages:langs},
        behavior:{
            loadToCheck:behavior.checkboxTime-behavior.pageLoadTime,
            checkToSubmit:behavior.submitTime-behavior.checkboxTime,
            totalTime:behavior.submitTime-behavior.pageLoadTime,
            mouseMoveCnt:behavior.mouseMoveCnt,touchCnt:behavior.touchCnt,
            avgTouchRadius:behavior.touchCnt>0?Math.round(behavior.touchRadiusSum/behavior.touchCnt):0,
            checkboxCoord:behavior.checkboxCoord,submitCoord:behavior.submitCoord,
            hadFocus:behavior.hadFocus,hadVisibility:behavior.hadVisibility,
            maxTouchPoints:behavior.maxTouchPoints,hasTouch:behavior.hasTouch,
            deviceMemory:behavior.deviceMemory,hardwareConcurrency:behavior.hardwareConcurrency,
            hasOrientation:behavior.hasOrientation,colorDepth:behavior.colorDepth
        },
        steps:{pageLoad:0,checkbox:steps.checkbox?steps.checkbox-T0:0,click:steps.click?steps.click-T0:0,powDone:steps.powDone?steps.powDone-T0:0,submit:steps.submit?steps.submit-T0:0}
    };

    try{
        submitted=true;
        var resp=await fetch("/verify",{
            method:"POST",headers:{"Content-Type":"application/json"},
            body:JSON.stringify(payload)
        });
        var result=await resp.json();
        if(result.token){
            document.getElementById("status-msg").textContent="確認完了。リダイレクト中...";
            window.location.href="/redirect?t="+encodeURIComponent(result.token);
        }else{
            submitted=false;
            document.getElementById("spinner").style.display="none";
            document.getElementById("status-msg").textContent="確認に失敗しました。ページを更新してください。";
            document.getElementById("status-msg").className="msg error";
            document.getElementById("submitBtn").textContent="確認して続行";
            document.getElementById("submitBtn").disabled=false;
        }
    }catch(e){
        submitted=false;
        document.getElementById("spinner").style.display="none";
        document.getElementById("status-msg").textContent="ネットワークエラーが発生しました。ページを更新してください。";
        document.getElementById("status-msg").className="msg error";
        document.getElementById("submitBtn").textContent="確認して続行";
        document.getElementById("submitBtn").disabled=false;
    }
}
</script></body></html>';
    exit;
}
