<?php
/*
 * SSO handshake endpoint — STANDALONE. Receives POST from WHMCS server,
 * validates HMAC + IP, mints a one-time token to disk, echoes JSON {url}.
 *
 * Does NOT bootstrap filegator. We don't need any of its services for
 * the handshake (no DB, no session, no router). Standalone keeps the
 * response deterministically JSON — filegator's bootstrap has multiple
 * `die('Folder not writable: ...')` early-aborts that would otherwise
 * leak as non-JSON HTTP 200 bodies.
 *
 * The token is consumed later when the customer's browser hits
 * /_fm/?_sso=<token> — at THAT point PuqHostingAuth::user() runs inside
 * the normal filegator boot and sets up the session.
 */

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');

// Load just our configuration to get the HMAC secret + IP allowlist.
// We don't need filegator's services for the handshake itself.
$configFile = __DIR__ . '/configuration.php';
if (!is_file($configFile)) {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'msg' => 'configuration.php missing']);
    exit;
}

// configuration.php expects these constants — they're set by dist/index.php
// in the normal flow. Provide minimal stubs so the file loads without errors.
if (!defined('APP_ENV')) define('APP_ENV', 'production');
if (!defined('APP_PUBLIC_PATH')) define('APP_PUBLIC_PATH', '');
if (!defined('APP_PUBLIC_DIR')) define('APP_PUBLIC_DIR', __DIR__ . '/dist');
if (!defined('APP_VERSION')) define('APP_VERSION', '0');

$cfg = require $configFile;
$authCfg = (array) ($cfg['services']['Filegator\\Services\\Auth\\AuthInterface']['config'] ?? []);
$hmacSecret = (string) ($authCfg['hmac_secret'] ?? '');
$allowedIps = (array)  ($authCfg['allowed_ips'] ?? []);
$tokenDir   = (string) ($authCfg['token_dir']   ?? (__DIR__ . '/private/sso_sessions'));

if ($hmacSecret === '') {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'msg' => 'hmac_secret not configured']);
    exit;
}

// --- IP allowlist ---
$sourceIp = $_SERVER['REMOTE_ADDR'] ?? '';
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $list = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
    $sourceIp = $list[0] ?? $sourceIp;
}
if (!empty($allowedIps) && !in_array($sourceIp, $allowedIps, true)) {
    http_response_code(403);
    echo json_encode(['status' => 'error', 'msg' => 'IP not allowed: ' . $sourceIp]);
    exit;
}

// --- POST fields ---
$login    = (string) ($_POST['login']    ?? '');
$pass     = (string) ($_POST['pass']     ?? '');
$domain   = (string) ($_POST['domain']   ?? '');
$exp      = (int)    ($_POST['exp']      ?? 0);
$nonce    = (string) ($_POST['nonce']    ?? '');
$sig      = (string) ($_POST['sig']      ?? '');
$backUrl  = (string) ($_POST['back_url'] ?? '');  // WHMCS client-area URL, optional

if ($login === '' || $pass === '' || $domain === '' || $exp <= 0 || $nonce === '' || $sig === '') {
    http_response_code(400);
    echo json_encode(['status' => 'error', 'msg' => 'missing fields']);
    exit;
}
if ($exp < time()) {
    http_response_code(400);
    echo json_encode(['status' => 'error', 'msg' => 'request expired']);
    exit;
}

$canonical = $login . '|' . $pass . '|' . $domain . '|' . $exp . '|' . $nonce;
$expected  = hash_hmac('sha256', $canonical, $hmacSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(403);
    echo json_encode(['status' => 'error', 'msg' => 'invalid signature']);
    exit;
}

// --- Mint single-use token ---
if (!is_dir($tokenDir)) {
    @mkdir($tokenDir, 0750, true);
}
if (!is_writable($tokenDir)) {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'msg' => 'token dir not writable: ' . $tokenDir]);
    exit;
}

$token = bin2hex(random_bytes(32));
$file = $tokenDir . '/' . $token . '.json';
// Sanity-clamp back_url — only http/https, max 2 KB. Prevents javascript:
// injection if WHMCS-side ever lets a customer override it.
$cleanBack = '';
if ($backUrl !== '' && strlen($backUrl) <= 2048
    && preg_match('#^https?://[^\s<>"]+$#i', $backUrl)) {
    $cleanBack = $backUrl;
}
$payload = [
    'login'    => $login,
    'pass'     => $pass,
    'domain'   => $domain,
    'back_url' => $cleanBack,
    'expires'  => time() + 60,
];
if (file_put_contents($file, json_encode($payload), LOCK_EX) === false) {
    http_response_code(500);
    echo json_encode(['status' => 'error', 'msg' => 'token persist failed']);
    exit;
}
@chmod($file, 0640);

// --- Garbage-collect old tokens (1-in-10 probability) ---
if (random_int(0, 9) === 0) {
    $cutoff = time() - 300;
    foreach (glob($tokenDir . '/*.json') ?: [] as $f) {
        if (@filemtime($f) < $cutoff) @unlink($f);
    }
}

// --- Build activation URL ---
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) $scheme = $_SERVER['HTTP_X_FORWARDED_PROTO'];
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
// /_fm/sso.php → /_fm/
$base = $_SERVER['REQUEST_URI'] ?? '/sso.php';
$base = preg_replace('#/sso(?:\.php)?(?:\?.*)?$#', '/', $base);
if ($base === '') $base = '/';
if (substr($base, -1) !== '/') $base .= '/';

$url = $scheme . '://' . $host . $base . '?_sso=' . urlencode($token);

echo json_encode(['status' => 'success', 'url' => $url]);
