feat: audit log table + HTTPS setting + Nginx .well-known rewrite fallback
This commit is contained in:
+91
-121
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -209,19 +157,14 @@ echo json_encode([
|
|||||||
], 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user