fix: real connection validation for Central API settings
- Add SmartHoneypotAPIClient::test_connection():
1. GET /api/v1/health — verifies URL is reachable
2. POST /api/v1/submit with empty blocks — verifies token:
400 = auth passed (payload rejected as expected)
403 = wrong or missing token
- Store connection_ok (null/true/false), last_verified, last_error in settings
- 'Active' dot now has 3 states: grey=untested, green=verified, red=failed
- Error message displayed inline when connection fails
- 'Test Connection' button appears as soon as URL is set
- Saving with changed URL or token resets connection status to untested
- Save action preserves verified status when URL/token unchanged
This commit is contained in:
@@ -160,14 +160,63 @@ class SmartHoneypotAPIClient {
|
|||||||
|
|
||||||
public static function defaults(): array {
|
public static function defaults(): array {
|
||||||
return [
|
return [
|
||||||
'enabled' => false,
|
'enabled' => false,
|
||||||
'api_url' => '',
|
'api_url' => '',
|
||||||
'api_token' => '',
|
'api_token' => '',
|
||||||
'last_sync' => 0,
|
'last_sync' => 0,
|
||||||
'sent_total' => 0,
|
'sent_total' => 0,
|
||||||
|
'connection_ok' => null, // null=untested, true=ok, false=failed
|
||||||
|
'last_verified' => 0,
|
||||||
|
'last_error' => '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests reachability (health endpoint) and auth (submit with bad payload).
|
||||||
|
* Returns ['ok' => bool, 'message' => string]
|
||||||
|
*/
|
||||||
|
public static function test_connection(): array {
|
||||||
|
$s = self::settings();
|
||||||
|
|
||||||
|
if (empty($s['api_url'])) {
|
||||||
|
return ['ok' => false, 'message' => 'No API URL configured.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = trailingslashit(esc_url_raw($s['api_url']));
|
||||||
|
$headers = ['Content-Type' => 'application/json'];
|
||||||
|
if (!empty($s['api_token'])) {
|
||||||
|
$headers['Authorization'] = 'Bearer ' . $s['api_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1 — reachability
|
||||||
|
$health = wp_remote_get($base . 'api/v1/health', ['timeout' => 8]);
|
||||||
|
if (is_wp_error($health)) {
|
||||||
|
return ['ok' => false, 'message' => 'Cannot reach API: ' . $health->get_error_message()];
|
||||||
|
}
|
||||||
|
$code = wp_remote_retrieve_response_code($health);
|
||||||
|
if ($code !== 200) {
|
||||||
|
return ['ok' => false, 'message' => "API returned HTTP {$code}. Verify the URL is correct."];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2 — token validation: POST with an intentionally invalid payload.
|
||||||
|
// Auth passes → 400 (bad payload). Wrong/missing token → 403.
|
||||||
|
$auth = wp_remote_post($base . 'api/v1/submit', [
|
||||||
|
'timeout' => 8,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => wp_json_encode(['site_hash' => 'connectivity_test', 'blocks' => []]),
|
||||||
|
]);
|
||||||
|
if (is_wp_error($auth)) {
|
||||||
|
return ['ok' => false, 'message' => 'Token check request failed: ' . $auth->get_error_message()];
|
||||||
|
}
|
||||||
|
$auth_code = wp_remote_retrieve_response_code($auth);
|
||||||
|
if ($auth_code === 403) {
|
||||||
|
return ['ok' => false, 'message' => 'API reachable but token rejected (HTTP 403). Check the token matches API_TOKEN in Docker.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 400 = auth passed, payload correctly rejected — exactly what we expect
|
||||||
|
return ['ok' => true, 'message' => 'Connection verified. API is reachable and token is accepted.'];
|
||||||
|
}
|
||||||
|
|
||||||
public static function settings(): array {
|
public static function settings(): array {
|
||||||
return wp_parse_args(get_option(self::OPT_SETTINGS, []), self::defaults());
|
return wp_parse_args(get_option(self::OPT_SETTINGS, []), self::defaults());
|
||||||
}
|
}
|
||||||
@@ -311,19 +360,40 @@ class SmartHoneypotAdmin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($_POST['hp_action'] === 'save_api_settings') {
|
if ($_POST['hp_action'] === 'save_api_settings') {
|
||||||
$current = SmartHoneypotAPIClient::settings();
|
$current = SmartHoneypotAPIClient::settings();
|
||||||
|
$new_url = esc_url_raw(trim($_POST['hp_api_url'] ?? ''));
|
||||||
|
$new_token = sanitize_text_field($_POST['hp_api_token'] ?? '');
|
||||||
|
$url_changed = $new_url !== $current['api_url'];
|
||||||
|
$tok_changed = $new_token !== $current['api_token'];
|
||||||
|
|
||||||
$new = [
|
$new = [
|
||||||
'enabled' => !empty($_POST['hp_api_enabled']),
|
'enabled' => !empty($_POST['hp_api_enabled']),
|
||||||
'api_url' => esc_url_raw(trim($_POST['hp_api_url'] ?? '')),
|
'api_url' => $new_url,
|
||||||
'api_token' => sanitize_text_field($_POST['hp_api_token'] ?? ''),
|
'api_token' => $new_token,
|
||||||
'last_sync' => $current['last_sync'],
|
'last_sync' => $current['last_sync'],
|
||||||
'sent_total' => $current['sent_total'],
|
'sent_total' => $current['sent_total'],
|
||||||
|
// Reset verification if URL or token changed
|
||||||
|
'connection_ok' => ($url_changed || $tok_changed) ? null : $current['connection_ok'],
|
||||||
|
'last_verified' => ($url_changed || $tok_changed) ? 0 : $current['last_verified'],
|
||||||
|
'last_error' => ($url_changed || $tok_changed) ? '' : $current['last_error'],
|
||||||
];
|
];
|
||||||
update_option(SmartHoneypotAPIClient::OPT_SETTINGS, $new);
|
update_option(SmartHoneypotAPIClient::OPT_SETTINGS, $new);
|
||||||
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'saved' => 1], admin_url('admin.php')));
|
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'saved' => 1], admin_url('admin.php')));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($_POST['hp_action'] === 'test_connection') {
|
||||||
|
$result = SmartHoneypotAPIClient::test_connection();
|
||||||
|
$s = SmartHoneypotAPIClient::settings();
|
||||||
|
$s['connection_ok'] = $result['ok'];
|
||||||
|
$s['last_verified'] = time();
|
||||||
|
$s['last_error'] = $result['ok'] ? '' : $result['message'];
|
||||||
|
update_option(SmartHoneypotAPIClient::OPT_SETTINGS, $s);
|
||||||
|
set_transient('hp_conn_result', $result, 60);
|
||||||
|
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'tested' => 1], admin_url('admin.php')));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($_POST['hp_action'] === 'flush_queue') {
|
if ($_POST['hp_action'] === 'flush_queue') {
|
||||||
SmartHoneypotAPIClient::flush();
|
SmartHoneypotAPIClient::flush();
|
||||||
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'flushed' => 1], admin_url('admin.php')));
|
wp_redirect(add_query_arg(['page' => self::MENU_SLUG, 'tab' => 'settings', 'flushed' => 1], admin_url('admin.php')));
|
||||||
@@ -350,6 +420,13 @@ class SmartHoneypotAdmin {
|
|||||||
<?php if (!empty($_GET['flushed'])): ?>
|
<?php if (!empty($_GET['flushed'])): ?>
|
||||||
<div class="notice notice-success is-dismissible"><p>Queue flushed to central API.</p></div>
|
<div class="notice notice-success is-dismissible"><p>Queue flushed to central API.</p></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($_GET['tested'])):
|
||||||
|
$res = get_transient('hp_conn_result');
|
||||||
|
if ($res):
|
||||||
|
$cls = $res['ok'] ? 'notice-success' : 'notice-error'; ?>
|
||||||
|
<div class="notice <?= $cls ?> is-dismissible"><p><?= esc_html($res['message']) ?></p></div>
|
||||||
|
<?php endif;
|
||||||
|
endif; ?>
|
||||||
|
|
||||||
<nav class="nav-tab-wrapper hp-tabs">
|
<nav class="nav-tab-wrapper hp-tabs">
|
||||||
<a href="<?= esc_url(admin_url('admin.php?page=' . self::MENU_SLUG . '&tab=logs')) ?>"
|
<a href="<?= esc_url(admin_url('admin.php?page=' . self::MENU_SLUG . '&tab=logs')) ?>"
|
||||||
@@ -489,8 +566,8 @@ class SmartHoneypotAdmin {
|
|||||||
<div style="max-width:700px;margin-top:20px">
|
<div style="max-width:700px;margin-top:20px">
|
||||||
<h2>Central API Settings</h2>
|
<h2>Central API Settings</h2>
|
||||||
<p style="color:#646970;margin-bottom:16px">
|
<p style="color:#646970;margin-bottom:16px">
|
||||||
Submit blocked attempts anonymously to a central dashboard for aggregate threat intelligence.
|
Submit blocked attempts to a central dashboard for aggregate threat intelligence.
|
||||||
Only anonymised data is sent: masked IPs (first 2 octets only), form type, block reason, and UA family. No site URL, no full IPs.
|
Data sent: full IP address, form type, block reason, UA family, and timestamp. No site URL or usernames are ever sent.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@@ -533,30 +610,63 @@ class SmartHoneypotAdmin {
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<h3>Submission Status</h3>
|
<h3>Connection Status</h3>
|
||||||
|
<?php
|
||||||
|
// Resolve dot state: null=untested, true=verified, false=failed
|
||||||
|
$conn = $s['connection_ok'];
|
||||||
|
if ($conn === true) {
|
||||||
|
$dot_class = 'dot-on';
|
||||||
|
$dot_label = 'Verified — connected and token accepted';
|
||||||
|
$dot_style = '';
|
||||||
|
} elseif ($conn === false) {
|
||||||
|
$dot_class = 'dot-fail';
|
||||||
|
$dot_label = 'Connection failed';
|
||||||
|
$dot_style = 'background:#b32d2e;';
|
||||||
|
} else {
|
||||||
|
$dot_class = 'dot-off';
|
||||||
|
$dot_label = 'Not tested yet';
|
||||||
|
$dot_style = '';
|
||||||
|
}
|
||||||
|
?>
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status</th>
|
<th>Connection</th>
|
||||||
<td>
|
<td>
|
||||||
<span class="hp-api-status">
|
<span class="hp-api-status">
|
||||||
<span class="dot <?= $s['enabled'] && $s['api_url'] ? 'dot-on' : 'dot-off' ?>"></span>
|
<span class="dot <?= $dot_class ?>" style="<?= $dot_style ?>"></span>
|
||||||
<?= $s['enabled'] && $s['api_url'] ? 'Active' : 'Inactive' ?>
|
<?= esc_html($dot_label) ?>
|
||||||
</span>
|
</span>
|
||||||
|
<?php if ($conn === false && $s['last_error']): ?>
|
||||||
|
<p class="description" style="color:#b32d2e;margin-top:4px"><?= esc_html($s['last_error']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<?php if ($s['last_verified']): ?>
|
||||||
|
<tr><th>Last Verified</th><td><?= esc_html(date('Y-m-d H:i:s', $s['last_verified'])) ?></td></tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<tr><th>Enabled</th><td><?= $s['enabled'] ? 'Yes' : 'No' ?></td></tr>
|
||||||
<tr><th>Last Sync</th><td><?= $s['last_sync'] ? esc_html(date('Y-m-d H:i:s', $s['last_sync'])) : 'Never' ?></td></tr>
|
<tr><th>Last Sync</th><td><?= $s['last_sync'] ? esc_html(date('Y-m-d H:i:s', $s['last_sync'])) : 'Never' ?></td></tr>
|
||||||
<tr><th>Total Sent</th><td><?= number_format((int)$s['sent_total']) ?> blocks</td></tr>
|
<tr><th>Total Sent</th><td><?= number_format((int) $s['sent_total']) ?> blocks</td></tr>
|
||||||
<tr><th>Queue Size</th><td><?= number_format($queue_size) ?> pending blocks</td></tr>
|
<tr><th>Queue Size</th><td><?= number_format($queue_size) ?> pending</td></tr>
|
||||||
<tr><th>Next Auto-Flush</th><td><?= $next_run ? esc_html(date('Y-m-d H:i:s', $next_run)) . ' (every 5 min)' : 'Not scheduled' ?></td></tr>
|
<tr><th>Next Auto-Flush</th><td><?= $next_run ? esc_html(date('Y-m-d H:i:s', $next_run)) . ' (every 5 min)' : 'Not scheduled' ?></td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<?php if ($queue_size > 0 && $s['enabled'] && $s['api_url']): ?>
|
<div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;align-items:center">
|
||||||
<form method="post" style="margin-top:12px">
|
<?php if ($s['api_url']): ?>
|
||||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
<form method="post">
|
||||||
<input type="hidden" name="hp_action" value="flush_queue">
|
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||||
<button type="submit" class="button button-secondary">Flush Queue Now (<?= $queue_size ?> pending)</button>
|
<input type="hidden" name="hp_action" value="test_connection">
|
||||||
</form>
|
<button type="submit" class="button button-primary">Test Connection</button>
|
||||||
<?php endif; ?>
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($queue_size > 0 && $s['enabled'] && $s['api_url']): ?>
|
||||||
|
<form method="post">
|
||||||
|
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||||
|
<input type="hidden" name="hp_action" value="flush_queue">
|
||||||
|
<button type="submit" class="button button-secondary">Flush Queue Now (<?= $queue_size ?> pending)</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user