feat: audit log table + HTTPS setting + Nginx .well-known rewrite fallback

This commit is contained in:
2026-05-08 22:30:10 +00:00
parent eb2496953c
commit f7679c8f86
+96 -126
View File
@@ -10,14 +10,13 @@ class Installer {
self::createTables(); self::createTables();
self::setDefaults(); self::setDefaults();
self::createWellKnownFiles(); self::createWellKnownFiles();
self::registerCron(); self::addRewriteRules();
self::registerRewriteRules();
flush_rewrite_rules(); flush_rewrite_rules();
} }
public static function deactivate(): void { public static function deactivate(): void {
self::removeWellKnownFiles(); self::removeWellKnownFiles();
self::deregisterCron(); self::removeHtaccessRules();
flush_rewrite_rules(); flush_rewrite_rules();
} }
@@ -27,8 +26,9 @@ class Installer {
public static function createTables(): void { public static function createTables(): void {
global $wpdb; global $wpdb;
$charset = $wpdb->get_charset_collate(); $charset = $wpdb->get_charset_collate();
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$keysSql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_keys ( dbDelta("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_keys (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(120) NOT NULL, name VARCHAR(120) NOT NULL,
api_key VARCHAR(64) NOT NULL, api_key VARCHAR(64) NOT NULL,
@@ -38,34 +38,22 @@ class Installer {
is_active TINYINT(1) NOT NULL DEFAULT 1, is_active TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (id), PRIMARY KEY (id),
UNIQUE KEY api_key (api_key) UNIQUE KEY api_key (api_key)
) $charset;"; ) $charset;");
$revokedSql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_revoked ( dbDelta("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
jti VARCHAR(32) NOT NULL,
expires_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY jti (jti),
KEY expires_at (expires_at)
) $charset;";
$logSql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}asktam_mcp_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
key_id BIGINT UNSIGNED NOT NULL, key_id BIGINT UNSIGNED NOT NULL,
tool VARCHAR(100) NOT NULL, key_name VARCHAR(120) NOT NULL DEFAULT '',
tool VARCHAR(120) NOT NULL DEFAULT '',
method VARCHAR(60) NOT NULL DEFAULT '',
ip VARCHAR(45) NOT NULL DEFAULT '', ip VARCHAR(45) NOT NULL DEFAULT '',
status TINYINT(1) NOT NULL DEFAULT 1, status TINYINT(1) NOT NULL DEFAULT 1,
duration_ms SMALLINT UNSIGNED NOT NULL DEFAULT 0, duration SMALLINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY key_id (key_id), KEY key_id (key_id),
KEY created_at (created_at) KEY created_at (created_at)
) $charset;"; ) $charset;");
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($keysSql);
dbDelta($revokedSql);
dbDelta($logSql);
update_option('asktam_mcp_db_version', ASKTAM_MCP_VERSION); update_option('asktam_mcp_db_version', ASKTAM_MCP_VERSION);
} }
@@ -78,90 +66,46 @@ class Installer {
'allow_raw_sql' => false, 'allow_raw_sql' => false,
'wc_enabled' => class_exists('WooCommerce'), 'wc_enabled' => class_exists('WooCommerce'),
'require_https' => true, 'require_https' => true,
]); ], false);
} }
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Cron — prune expired revocations + old logs daily // Audit log helper — called from Server.php after every tool call
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private static function registerCron(): void { public static function log(array $keyRow, string $tool, string $method, bool $success, int $durationMs): void {
if (!wp_next_scheduled('asktam_mcp_daily_cleanup')) { $settings = get_option('asktam_mcp_settings', []);
wp_schedule_event(time(), 'daily', 'asktam_mcp_daily_cleanup'); if (empty($settings['log_requests'])) return;
}
add_action('asktam_mcp_daily_cleanup', [self::class, 'runCleanup']);
}
private static function deregisterCron(): void {
wp_clear_scheduled_hook('asktam_mcp_daily_cleanup');
}
public static function runCleanup(): void {
TokenManager::pruneRevocations();
global $wpdb; global $wpdb;
// Keep 90 days of logs $wpdb->insert($wpdb->prefix . 'asktam_mcp_log', [
'key_id' => $keyRow['id'],
'key_name' => $keyRow['name'],
'tool' => $tool,
'method' => $method,
'ip' => self::getClientIp(),
'status' => $success ? 1 : 0,
'duration' => min($durationMs, 32767),
'created_at'=> current_time('mysql'),
]);
// Prune logs older than 90 days weekly
if (rand(1, 100) === 1) {
$wpdb->query("DELETE FROM {$wpdb->prefix}asktam_mcp_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)"); $wpdb->query("DELETE FROM {$wpdb->prefix}asktam_mcp_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)");
} }
// -------------------------------------------------------------------------
// Nginx-compatible rewrite rules via WP rewrite API
// Works on both Apache (.htaccess) and Nginx (via WP's own rewrite)
// -------------------------------------------------------------------------
public static function registerRewriteRules(): void {
// These rules let WP handle /.well-known/ requests on Nginx
add_rewrite_rule(
'^\.well-known/oauth-authorization-server$',
'index.php?asktam_mcp_wellknown=oauth-authorization-server',
'top'
);
add_rewrite_rule(
'^\.well-known/oauth-protected-resource',
'index.php?asktam_mcp_wellknown=oauth-protected-resource',
'top'
);
// Register query var
add_filter('query_vars', function (array $vars): array {
$vars[] = 'asktam_mcp_wellknown';
return $vars;
});
// Handle the request
add_action('template_redirect', function (): void {
$which = get_query_var('asktam_mcp_wellknown');
if (!$which) return;
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: no-cache');
$base = rest_url('asktam-mcp/v1');
if ($which === 'oauth-authorization-server') {
echo wp_json_encode([
'issuer' => get_site_url(),
'authorization_endpoint' => $base . '/oauth/authorize',
'token_endpoint' => $base . '/oauth/token',
'token_endpoint_auth_methods_supported' => ['client_secret_post', 'client_secret_basic', 'none'],
'grant_types_supported' => ['authorization_code', 'client_credentials'],
'response_types_supported' => ['code'],
'code_challenge_methods_supported' => ['S256', 'plain'],
'scopes_supported' => ['read', 'write', 'admin', 'woocommerce'],
], JSON_UNESCAPED_SLASHES);
} elseif ($which === 'oauth-protected-resource') {
echo wp_json_encode([
'resource' => rest_url('asktam-mcp/v1/sse'),
'authorization_servers' => [get_site_url()],
'bearer_methods_supported' => ['header'],
'scopes_supported' => ['read', 'write', 'admin', 'woocommerce'],
], JSON_UNESCAPED_SLASHES);
} }
exit;
}); private static function getClientIp(): string {
foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $key) {
if (!empty($_SERVER[$key])) {
return sanitize_text_field(explode(',', $_SERVER[$key])[0]);
}
}
return '';
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// .well-known physical files (Apache) — still created as fallback // .well-known files — dynamic, no hardcoded URLs
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
public static function createWellKnownFiles(): void { public static function createWellKnownFiles(): void {
$dir = ABSPATH . '.well-known'; $dir = ABSPATH . '.well-known';
@@ -170,9 +114,13 @@ class Installer {
$bootstrap = <<<'PHP' $bootstrap = <<<'PHP'
<?php <?php
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
$wpLoad = dirname(dirname(__FILE__)) . '/wp-load.php'; $dir = dirname(dirname(__FILE__));
$wpLoad = $dir . '/wp-load.php';
if (!file_exists($wpLoad)) { http_response_code(500); exit('WordPress not found'); } if (!file_exists($wpLoad)) { http_response_code(500); exit('WordPress not found'); }
if (!isset($_SERVER['HTTP_HOST'])) $_SERVER['HTTP_HOST'] = 'localhost'; if (!isset($_SERVER['HTTP_HOST'])) {
// Read siteurl from wp-config without loading WP
$_SERVER['HTTP_HOST'] = 'localhost';
}
if (!isset($_SERVER['REQUEST_URI'])) $_SERVER['REQUEST_URI'] = '/'; if (!isset($_SERVER['REQUEST_URI'])) $_SERVER['REQUEST_URI'] = '/';
require $wpLoad; require $wpLoad;
} }
@@ -188,11 +136,11 @@ echo json_encode([
'issuer' => get_site_url(), 'issuer' => get_site_url(),
'authorization_endpoint' => $base . '/oauth/authorize', 'authorization_endpoint' => $base . '/oauth/authorize',
'token_endpoint' => $base . '/oauth/token', 'token_endpoint' => $base . '/oauth/token',
'token_endpoint_auth_methods_supported' => ['client_secret_post','client_secret_basic','none'], 'token_endpoint_auth_methods_supported' => ['client_secret_post', 'client_secret_basic', 'none'],
'grant_types_supported' => ['authorization_code','client_credentials'], 'grant_types_supported' => ['authorization_code', 'client_credentials'],
'response_types_supported' => ['code'], 'response_types_supported' => ['code'],
'code_challenge_methods_supported' => ['S256','plain'], 'code_challenge_methods_supported' => ['S256', 'plain'],
'scopes_supported' => ['read','write','admin','woocommerce'], 'scopes_supported' => ['read', 'write', 'admin', 'woocommerce'],
], JSON_UNESCAPED_SLASHES); ], JSON_UNESCAPED_SLASHES);
PHP; PHP;
@@ -205,23 +153,18 @@ echo json_encode([
'resource' => rest_url('asktam-mcp/v1/sse'), 'resource' => rest_url('asktam-mcp/v1/sse'),
'authorization_servers' => [get_site_url()], 'authorization_servers' => [get_site_url()],
'bearer_methods_supported' => ['header'], 'bearer_methods_supported' => ['header'],
'scopes_supported' => ['read','write','admin','woocommerce'], 'scopes_supported' => ['read', 'write', 'admin', 'woocommerce'],
], JSON_UNESCAPED_SLASHES); ], JSON_UNESCAPED_SLASHES);
PHP; PHP;
$htaccess = <<<'HTACCESS' $htaccess = "<Files \"oauth-authorization-server\">\n ForceType application/x-httpd-php\n</Files>\n<Files \"oauth-protected-resource\">\n ForceType application/x-httpd-php\n</Files>\n";
<Files "oauth-authorization-server">
ForceType application/x-httpd-php
</Files>
<Files "oauth-protected-resource">
ForceType application/x-httpd-php
</Files>
HTACCESS;
file_put_contents($dir . '/oauth-authorization-server', $authServer); file_put_contents($dir . '/oauth-authorization-server', $authServer);
file_put_contents($dir . '/oauth-protected-resource', $protectedResource); file_put_contents($dir . '/oauth-protected-resource', $protectedResource);
file_put_contents($dir . '/.htaccess', $htaccess); file_put_contents($dir . '/.htaccess', $htaccess);
self::addHtaccessRules();
// Nginx fallback — register WP rewrite rules for .well-known paths
self::addRewriteRules();
} }
public static function removeWellKnownFiles(): void { public static function removeWellKnownFiles(): void {
@@ -232,32 +175,59 @@ HTACCESS;
if (file_exists($path)) @unlink($path); if (file_exists($path)) @unlink($path);
} }
if (file_exists($dir) && count(scandir($dir)) === 2) @rmdir($dir); if (file_exists($dir) && count(scandir($dir)) === 2) @rmdir($dir);
self::removeHtaccessRules(); }
// -------------------------------------------------------------------------
// Rewrite rules — Apache .htaccess + WP rewrite (Nginx fallback)
// -------------------------------------------------------------------------
public static function addRewriteRules(): void {
// WP rewrite rules handle Nginx (no .htaccess)
add_rewrite_rule('^\.well-known/oauth-authorization-server$', 'index.php?asktam_mcp_wellknown=oauth-authorization-server', 'top');
add_rewrite_rule('^\.well-known/oauth-protected-resource', 'index.php?asktam_mcp_wellknown=oauth-protected-resource', 'top');
add_filter('query_vars', function($vars) {
$vars[] = 'asktam_mcp_wellknown';
return $vars;
});
add_action('template_redirect', function() {
$which = get_query_var('asktam_mcp_wellknown');
if (!$which) return;
$file = ABSPATH . '.well-known/' . basename($which);
if (file_exists($file)) {
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
include $file;
exit;
}
});
self::addHtaccessRules();
} }
private static function addHtaccessRules(): void { private static function addHtaccessRules(): void {
$file = ABSPATH . '.htaccess'; $htaccessFile = ABSPATH . '.htaccess';
if (!file_exists($file) || !is_writable($file)) return; if (!file_exists($htaccessFile) || !is_writable($htaccessFile)) return;
$content = file_get_contents($file);
$content = file_get_contents($htaccessFile);
if (str_contains($content, '# BEGIN AskTam MCP')) return; if (str_contains($content, '# BEGIN AskTam MCP')) return;
$rules = "\n# BEGIN AskTam MCP\n<IfModule mod_rewrite.c>\nRewriteEngine On\n" $rules = "\n# BEGIN AskTam MCP\n<IfModule mod_rewrite.c>\nRewriteEngine On\nRewriteRule ^.well-known/oauth-authorization-server$ .well-known/oauth-authorization-server [L,T=application/x-httpd-php]\nRewriteRule ^.well-known/oauth-protected-resource.*$ .well-known/oauth-protected-resource [L,T=application/x-httpd-php]\n</IfModule>\n# END AskTam MCP\n\n";
. "RewriteRule ^.well-known/oauth-authorization-server$ .well-known/oauth-authorization-server [L,T=application/x-httpd-php]\n"
. "RewriteRule ^.well-known/oauth-protected-resource.*$ .well-known/oauth-protected-resource [L,T=application/x-httpd-php]\n"
. "</IfModule>\n# END AskTam MCP\n\n";
$content = str_contains($content, '# BEGIN WordPress') $wpMarker = '# BEGIN WordPress';
? str_replace('# BEGIN WordPress', $rules . '# BEGIN WordPress', $content) $content = str_contains($content, $wpMarker)
? str_replace($wpMarker, $rules . $wpMarker, $content)
: $rules . $content; : $rules . $content;
file_put_contents($file, $content); file_put_contents($htaccessFile, $content);
} }
private static function removeHtaccessRules(): void { public static function removeHtaccessRules(): void {
$file = ABSPATH . '.htaccess'; $htaccessFile = ABSPATH . '.htaccess';
if (!file_exists($file)) return; if (!file_exists($htaccessFile)) return;
$content = file_get_contents($file); $content = file_get_contents($htaccessFile);
$content = preg_replace('/\n# BEGIN AskTam MCP.*?# END AskTam MCP\n/s', '', $content); $content = preg_replace('/\n# BEGIN AskTam MCP.*?# END AskTam MCP\n/s', '', $content);
file_put_contents($file, $content); file_put_contents($htaccessFile, $content);
} }
} }