- auth / Mac app rebuild
This commit is contained in:
@@ -9,6 +9,8 @@ $repo = $app->repository();
|
||||
$comments = $app->comments();
|
||||
$config = $app->config();
|
||||
|
||||
tcms_load_env(BLOG_ROOT . '/.env');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') {
|
||||
@@ -25,7 +27,9 @@ try {
|
||||
'name' => 'Ty Clifford Content Management System API',
|
||||
'site' => $config->get('site', []),
|
||||
'authenticated' => tcms_is_authenticated($config),
|
||||
'token_configured' => tcms_configured_token($config) !== '',
|
||||
'auth_required' => true,
|
||||
'credentials_configured' => tcms_credentials_configured(),
|
||||
'token_configured' => tcms_configured_token() !== '',
|
||||
'capabilities' => [
|
||||
'content' => ['list', 'get', 'save', 'delete'],
|
||||
'media' => ['list', 'upload'],
|
||||
@@ -34,7 +38,7 @@ try {
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$config->get('api.enabled', true)) {
|
||||
if (!tcms_api_enabled($config)) {
|
||||
tcms_error('The API is disabled for this TCMS site.', 403);
|
||||
}
|
||||
tcms_require_auth($config);
|
||||
@@ -67,6 +71,39 @@ function tcms_payload(): array
|
||||
return $_POST;
|
||||
}
|
||||
|
||||
function tcms_load_env(string $path): void
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (file($path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$key, $value] = explode('=', $line, 2);
|
||||
$key = trim($key);
|
||||
$value = trim($value);
|
||||
if ($key === '' || !preg_match('/^[A-Z0-9_]+$/', $key)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(str_starts_with($value, '"') && str_ends_with($value, '"')) ||
|
||||
(str_starts_with($value, "'") && str_ends_with($value, "'"))
|
||||
) {
|
||||
$value = substr($value, 1, -1);
|
||||
}
|
||||
|
||||
if (getenv($key) === false) {
|
||||
putenv($key . '=' . $value);
|
||||
}
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] ??= $value;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
function tcms_json(array $data, int $status = 200): never
|
||||
{
|
||||
@@ -83,26 +120,79 @@ function tcms_error(string $message, int $status = 400, array $extra = []): neve
|
||||
|
||||
function tcms_require_auth($config): void
|
||||
{
|
||||
if (!tcms_credentials_configured()) {
|
||||
tcms_error('API credentials are not configured. Create a site .env file with TCMS_API_USERNAME and TCMS_API_PASSWORD, TCMS_API_PASSWORD_HASH, or TCMS_API_TOKEN.', 503);
|
||||
}
|
||||
if (!tcms_is_authenticated($config)) {
|
||||
tcms_error('API authentication failed. Send the configured token as a Bearer token or X-TCMS-Token header.', 401);
|
||||
tcms_error('API authentication failed. Use HTTP Basic credentials from .env or send TCMS_API_TOKEN as a Bearer token or X-TCMS-Token header.', 401);
|
||||
}
|
||||
}
|
||||
|
||||
function tcms_is_authenticated($config): bool
|
||||
{
|
||||
$configured = tcms_configured_token($config);
|
||||
if ($configured === '') {
|
||||
return (bool) $config->get('api.allow_local_without_token', true) && tcms_is_local_request();
|
||||
if (!tcms_credentials_configured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$provided = tcms_request_token();
|
||||
return $provided !== '' && hash_equals($configured, $provided);
|
||||
$configuredToken = tcms_configured_token();
|
||||
$providedToken = tcms_request_token();
|
||||
if ($configuredToken !== '' && $providedToken !== '' && hash_equals($configuredToken, $providedToken)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$basic = tcms_request_basic_credentials();
|
||||
if ($basic === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$username, $password] = $basic;
|
||||
return tcms_username_matches($username) && tcms_password_matches($password);
|
||||
}
|
||||
|
||||
function tcms_configured_token($config): string
|
||||
function tcms_api_enabled($config): bool
|
||||
{
|
||||
$envToken = trim((string) getenv('TCMS_API_TOKEN'));
|
||||
return $envToken !== '' ? $envToken : trim((string) $config->get('api.token', ''));
|
||||
$envValue = tcms_env('TCMS_API_ENABLED');
|
||||
if ($envValue !== '') {
|
||||
return filter_var($envValue, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? true;
|
||||
}
|
||||
|
||||
return (bool) $config->get('api.enabled', true);
|
||||
}
|
||||
|
||||
function tcms_credentials_configured(): bool
|
||||
{
|
||||
return tcms_configured_token() !== ''
|
||||
|| (tcms_configured_username() !== '' && (tcms_configured_password() !== '' || tcms_configured_password_hash() !== ''));
|
||||
}
|
||||
|
||||
function tcms_configured_token(): string
|
||||
{
|
||||
return tcms_env('TCMS_API_TOKEN');
|
||||
}
|
||||
|
||||
function tcms_configured_username(): string
|
||||
{
|
||||
return tcms_env('TCMS_API_USERNAME');
|
||||
}
|
||||
|
||||
function tcms_configured_password(): string
|
||||
{
|
||||
return tcms_env('TCMS_API_PASSWORD');
|
||||
}
|
||||
|
||||
function tcms_configured_password_hash(): string
|
||||
{
|
||||
return tcms_env('TCMS_API_PASSWORD_HASH');
|
||||
}
|
||||
|
||||
function tcms_env(string $key): string
|
||||
{
|
||||
$value = getenv($key);
|
||||
if ($value === false) {
|
||||
$value = $_ENV[$key] ?? $_SERVER[$key] ?? '';
|
||||
}
|
||||
|
||||
return trim((string) $value);
|
||||
}
|
||||
|
||||
function tcms_request_token(): string
|
||||
@@ -116,10 +206,43 @@ function tcms_request_token(): string
|
||||
return trim((string) ($_SERVER['HTTP_X_TCMS_TOKEN'] ?? $headers['X-TCMS-Token'] ?? $headers['x-tcms-token'] ?? ''));
|
||||
}
|
||||
|
||||
function tcms_is_local_request(): bool
|
||||
/** @return array{0: string, 1: string}|null */
|
||||
function tcms_request_basic_credentials(): ?array
|
||||
{
|
||||
$remote = (string) ($_SERVER['REMOTE_ADDR'] ?? '');
|
||||
return in_array($remote, ['127.0.0.1', '::1', 'localhost', ''], true);
|
||||
if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
|
||||
return [(string) $_SERVER['PHP_AUTH_USER'], (string) $_SERVER['PHP_AUTH_PW']];
|
||||
}
|
||||
|
||||
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
||||
$auth = (string) ($_SERVER['HTTP_AUTHORIZATION'] ?? $headers['Authorization'] ?? $headers['authorization'] ?? '');
|
||||
if (!preg_match('/^Basic\s+(.+)$/i', $auth, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = base64_decode(trim($matches[1]), true);
|
||||
if (!is_string($decoded) || !str_contains($decoded, ':')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$username, $password] = explode(':', $decoded, 2);
|
||||
return [$username, $password];
|
||||
}
|
||||
|
||||
function tcms_username_matches(string $username): bool
|
||||
{
|
||||
$configured = tcms_configured_username();
|
||||
return $configured !== '' && hash_equals($configured, $username);
|
||||
}
|
||||
|
||||
function tcms_password_matches(string $password): bool
|
||||
{
|
||||
$hash = tcms_configured_password_hash();
|
||||
if ($hash !== '') {
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
$configured = tcms_configured_password();
|
||||
return $configured !== '' && hash_equals($configured, $password);
|
||||
}
|
||||
|
||||
function tcms_content_list(ContentRepository $repo): never
|
||||
|
||||
Reference in New Issue
Block a user