<?php
###############################################################################
/**
 * api-sl.php - Secure Layer entry point for server-side API access.
 *
 * A companion file to api.php, placed in the same installation root folder.
 * Do NOT modify the original api.php.
 *
 * Accepts an encrypted Bearer token in the Authorization header,
 * decrypts it to extract a session ID, and injects it before the
 * WMD loader starts the session. This avoids passing session IDs
 * as plain URL parameters (as in the basic docs example).
 * 
 * @author Anil Kumar <akumar@codepunch.com>
 * @link   https://codepunch.com
 *
 */
 
###############################################################################

// Anti-replay window for the Bearer token in seconds.
// The token is a single-use delivery mechanism for the session ID, not a long-lived
// credential. Keep this value tight - 2 seconds is sufficient for most use cases
// but allows for minor network latency.
//
// To override, add the following to config.php:
//   $api_sl_token_expiry = 5;  // your preferred value in seconds

###############################################################################
// WMD SED internal encryption key. This is a fixed constant in the WMD SED
// codebase, shared across all installations. It is not a secret and should
// not be changed here — it is effectively a magic number that must match
// WMD SED's own internal value to decrypt the etoken correctly.
//
// If WMD SED is ever updated, verify this value still matches:
//   WMD SED source -> authentication class -> private $FIRSTKEY
define('WMDSED_MAGIC_KEY', 'PbD6mEcik3Jq7Ed3wDukwAe+6FoYPqGfbvQa8LouISI=');

/**
 * Decrypts and verifies an encrypted Bearer token generated by WMD SED.
 *
 * WMD SED assembles the token at login as:
 *   salt ; session_id ; unix_timestamp ; server_ip
 *   [0]      [1]            [2]             [3]
 *
 * parts[1] and parts[2] are used. parts[3] (server IP) is carried in the
 * token but not validated here — it is informational only.
 *
 * It then encrypts this using:
 *   Encryption: AES-256-CBC with WMDSED_MAGIC_KEY
 *   Integrity:  HMAC-SHA3-512 of the ciphertext using the installation's
 *               unique public key ($db_config_publickey from config.php)
 *
 * The token is then returned as 'etoken' in the c=auth response and passed
 * back to this script as a Bearer token in the Authorization header.
 *
 * Security model:
 *   WMDSED_MAGIC_KEY is a public constant shared across all WMD SED
 *   installations. The actual security lies in the HMAC verification,
 *   which uses $db_config_publickey — a random key that is automatically
 *   generated by the WMD SED installation script and is unique to each
 *   installation. It is never shared, never published, and cannot be
 *   derived from any public information about WMD SED. A token generated
 *   by one WMD instance will fail HMAC verification on any other instance,
 *   making cross-installation replay attacks impossible.
 *
 * @param  string $input  Base64-encoded encrypted token (the etoken value)
 * @param  string $dcp    Base64-encoded installation public key from config.php
 * @return string|false   Decrypted semicolon-delimited payload, or false if invalid
 */

function token_decode($input, $dcp) {
	$first_key  = base64_decode(WMDSED_MAGIC_KEY);
	$second_key = base64_decode($dcp);
	$mix        = base64_decode($input);
	$method     = "aes-256-cbc";

	$iv_length = openssl_cipher_iv_length($method);

	// Unpack the three components from the raw token bytes
	$iv               = substr($mix, 0, $iv_length);
	$second_encrypted = substr($mix, $iv_length, 64);       // HMAC-SHA3-512 = 64 bytes
	$first_encrypted  = substr($mix, $iv_length + 64);      // AES ciphertext

	$data                 = openssl_decrypt($first_encrypted, $method, $first_key, OPENSSL_RAW_DATA, $iv);
	$second_encrypted_new = hash_hmac('sha3-512', $first_encrypted, $second_key, true);

	// Verify HMAC before returning the decrypted payload
	if($second_encrypted_new !== false && $second_encrypted !== false) {
		if(hash_equals($second_encrypted, $second_encrypted_new))
			return $data;
	}
	return false;
}

###############################################################################
/**
 * Extracts the Bearer token from the Authorization header of the incoming request.
 *
 * Tries three methods in order to handle different server environments:
 *   1. apache_request_headers() for Apache
 *   2. $_SERVER['HTTP_AUTHORIZATION'] for Nginx / PHP-FPM
 *   3. $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] for CGI and rewrite scenarios
 *
 * Expected header format:
 *   Authorization: Bearer <encrypted-token>
 *
 * Note: On Nginx, the Authorization header may need to be explicitly passed
 * through to PHP in the nginx config:
 *   fastcgi_param HTTP_AUTHORIZATION $http_authorization;
 *
 * @return string|null  The raw Bearer token string, or null if the header is
 *                      absent or does not contain a valid Bearer token.
 */

function getBearerToken() {
    $headers = null;

    // Apache
    if (function_exists('apache_request_headers')) {
        $requestHeaders = apache_request_headers();
        $requestHeaders = array_combine(
            array_map('ucwords', array_keys($requestHeaders)),
            array_values($requestHeaders)
        );
        if (isset($requestHeaders['Authorization'])) {
            $headers = trim($requestHeaders['Authorization']);
        }
    }

    // Nginx / PHP-FPM
    if ($headers === null && isset($_SERVER['HTTP_AUTHORIZATION'])) {
        $headers = trim($_SERVER['HTTP_AUTHORIZATION']);
    }

    // CGI / other setups
    if ($headers === null && isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
        $headers = trim($_SERVER['REDIRECT_HTTP_AUTHORIZATION']);
    }

    if (!empty($headers)) {
        if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
            return $matches[1];
        }
    }
    return null;
}


###############################################################################
/**
 * Authenticates an incoming API request using an encrypted Bearer token.
 *
 * This function implements the WMD SED session ID injection pattern, but instead
 * of accepting a plain session ID as a URL parameter (which would appear in server
 * logs), it accepts an AES-256-CBC encrypted and HMAC-SHA3-512 signed Bearer token
 * in the Authorization header.
 *
 * The token encodes a session ID and a timestamp. The 2-second expiry on the token
 * is intentional and should not be confused with the WMD session timeout. These are
 * two separate things:
 *
 *   - Token expiry (2 seconds): The Bearer token itself is a single-use delivery
 *     mechanism. It exists only to securely hand off the session ID to this script.
 *     Once the session ID is injected, the token's job is done. Keeping the window
 *     tight prevents replay attacks if a token is ever intercepted.
 *
 *   - WMD session timeout: Controlled by WMD SED's own session settings. Once the
 *     session ID is injected and the session is active, it lives as long as the WMD
 *     session does, independent of this token.
 *
 * The security of this mechanism relies entirely on $db_config_publickey,
 * which is auto-generated by the WMD SED installation script and is unique
 * to each installation. No two installations share the same key.
 *
 * If the token is missing or invalid, the function returns silently
 * without setting a session ID. The WMD loader will then start an unauthenticated
 * session, and WMD SED will handle the request as it normally would for any
 * unauthenticated call — returning an appropriate error response. There is no
 * need to explicitly block or exit on auth failure here.
 *
 * The only exception is an expired token, which returns a 401 immediately to
 * signal to the calling script that a fresh token is needed.
 */

function authenticate_token() {
	$etoken = getBearerToken();
	if($etoken != null) {
		include("config.php");
		$token = token_decode($etoken, $db_config_publickey);
		if($token !== false) {
			$parts = explode(';', $token);
			if(count($parts) == 4) {
				$token = $parts[1];
				if (preg_match('/^[a-zA-Z0-9_-]{1,256}$/', $token) > 0) {
					session_id($token);
					$time = $parts[2];
					// Token expiry window in seconds. Default is 2 seconds which is sufficient
					// for most use cases but allows for minor network latency.
					// To override, add $api_sl_token_expiry to config.php e.g:
					//   $api_sl_token_expiry = 5;
					if((time() - $time) > (isset($api_sl_token_expiry) ? (int)$api_sl_token_expiry : 2)) {
						session_start();
						session_destroy();
						http_response_code(401);
						exit(json_encode(['status' => 'notok', 'error' => 'Token expired']));
					}
				}
			}
		}
	}
}

###############################################################################

authenticate_token();

###############################################################################

require 'lib/php/loader.php';

###############################################################################

\CodePunch\UI\API::api();

###############################################################################
