better-staridc-MNBT
1<?php
2if(!defined('IN_CRONLITE'))exit();
3
4function mnbt_node_json($success, $msg, $data = []) {
5 return json_encode([
6 'success' => (bool)$success,
7 'code' => $success ? 200 : 400,
8 'msg' => $msg,
9 'data' => $data,
10 ], JSON_UNESCAPED_UNICODE);
11}
12
13function mnbt_node_exit($success, $msg, $data = []) {
14 exit(mnbt_node_json($success, $msg, $data));
15}
16
17function mnbt_node_random_id($prefix) {
18 return $prefix . '_' . bin2hex(random_bytes(12));
19}
20
21function mnbt_node_query_ignore_duplicate_column($DB, $sql) {
22 try {
23 return $DB->query($sql);
24 } catch (mysqli_sql_exception $e) {
25 if ((int)$e->getCode() === 1060 || stripos($e->getMessage(), 'Duplicate column name') !== false) {
26 return false;
27 }
28 throw $e;
29 }
30}
31
32function mnbt_node_ensure_tables($DB) {
33 // 数据表结构统一在 install.sql 中创建,此函数保留用于运行时的兼容性检查
34 // 实际表结构在系统安装时由 install.sql 创建,升级时通过增量 SQL 维护
35}
36
37function mnbt_node_platform_secret($conf) {
38 if (is_array($conf) && !empty($conf['node_api_key'])) return (string)$conf['node_api_key'];
39 if (is_array($conf) && !empty($conf['api'])) return (string)$conf['api'];
40 return defined('SYS_KEY') ? SYS_KEY : 'MNBT';
41}
42
43function mnbt_node_default_base_url() {
44 $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
45 $host = $_SERVER['HTTP_HOST'] ?? '';
46 if ($host === '') return '';
47 $script = str_replace('\\', '/', $_SERVER['SCRIPT_NAME'] ?? '');
48 $dir = trim(str_replace('\\', '/', dirname($script)), '/');
49 if ($dir === 'admin') {
50 $dir = '';
51 } elseif (substr($dir, -6) === '/admin') {
52 $dir = substr($dir, 0, -6);
53 }
54 return rtrim($scheme . '://' . $host . ($dir === '' ? '' : '/' . $dir), '/');
55}
56
57function mnbt_node_admin_config($conf, $node, $mnbtUrl = '', $intervalSeconds = 10) {
58 $intervalSeconds = max(5, (int)$intervalSeconds);
59 $mnbtUrl = rtrim((string)$mnbtUrl, '/');
60 if ($mnbtUrl === '') $mnbtUrl = mnbt_node_default_base_url();
61 return [
62 'mnbt_url' => $mnbtUrl,
63 'platform_secret' => mnbt_node_platform_secret($conf),
64 'node_id' => (string)($node['node_id'] ?? ''),
65 'node_secret' => (string)($node['node_secret'] ?? ''),
66 'node_name' => (string)($node['node_name'] ?? ''),
67 'version' => '0.1.0',
68 'interval_seconds' => $intervalSeconds,
69 'capabilities' => [
70 'heartbeat',
71 'task',
72 'report',
73 'forbidden_scan',
74 ],
75 ];
76}
77
78function mnbt_node_effective_status($node, $now = null, $offlineAfterSeconds = 60) {
79 if (($node['enabled'] ?? 'true') !== 'true') return 'disabled';
80 if ($now === null) $now = time();
81 $lastHeartbeat = trim((string)($node['last_heartbeat'] ?? ''));
82 if ($lastHeartbeat === '') return 'offline';
83 $heartbeatTime = strtotime($lastHeartbeat);
84 if (!$heartbeatTime) return 'offline';
85 return ((int)$now - (int)$heartbeatTime) <= (int)$offlineAfterSeconds ? 'online' : 'offline';
86}
87
88function mnbt_node_body_hash($body) {
89 return hash('sha256', (string)$body);
90}
91
92function mnbt_node_signing_key($platformSecret, $nodeSecret) {
93 return hash_hmac('sha256', (string)$nodeSecret, (string)$platformSecret);
94}
95
96function mnbt_node_canonical_request($method, $path, $body, $timestamp, $nonce) {
97 return strtoupper((string)$method) . "\n" .
98 (string)$path . "\n" .
99 mnbt_node_body_hash($body) . "\n" .
100 (string)$timestamp . "\n" .
101 (string)$nonce;
102}
103
104function mnbt_node_signature($method, $path, $body, $platformSecret, $nodeSecret, $timestamp, $nonce) {
105 return hash_hmac(
106 'sha256',
107 mnbt_node_canonical_request($method, $path, $body, $timestamp, $nonce),
108 mnbt_node_signing_key($platformSecret, $nodeSecret)
109 );
110}
111
112function mnbt_node_build_headers($nodeId, $method, $path, $body, $platformSecret, $nodeSecret, $timestamp = null, $nonce = null) {
113 if ($timestamp === null) $timestamp = time();
114 if ($nonce === null) $nonce = bin2hex(random_bytes(12));
115 return [
116 'X-MNBT-Node' => $nodeId,
117 'X-MNBT-Time' => (string)$timestamp,
118 'X-MNBT-Nonce' => $nonce,
119 'X-MNBT-Sign' => mnbt_node_signature($method, $path, $body, $platformSecret, $nodeSecret, $timestamp, $nonce),
120 ];
121}
122
123function mnbt_node_header_value($headers, $name) {
124 foreach ($headers as $key => $value) {
125 if (strtolower((string)$key) === strtolower($name)) return is_array($value) ? reset($value) : $value;
126 }
127 return '';
128}
129
130function mnbt_node_verify_signature($headers, $method, $path, $body, $platformSecret, $nodeSecret, $now = null) {
131 $timestamp = (int)mnbt_node_header_value($headers, 'X-MNBT-Time');
132 $nonce = (string)mnbt_node_header_value($headers, 'X-MNBT-Nonce');
133 $sign = (string)mnbt_node_header_value($headers, 'X-MNBT-Sign');
134 if ($timestamp <= 0 || $nonce === '' || $sign === '') return false;
135 if ($now === null) $now = time();
136 if (abs((int)$now - $timestamp) > 300) return false;
137 $expected = mnbt_node_signature($method, $path, $body, $platformSecret, $nodeSecret, $timestamp, $nonce);
138 return hash_equals($expected, $sign);
139}
140
141function mnbt_node_request_path() {
142 $uri = $_SERVER['REQUEST_URI'] ?? '/api/node.php';
143 $path = parse_url($uri, PHP_URL_PATH) ?: '/api/node.php';
144 $query = $_SERVER['QUERY_STRING'] ?? '';
145 return $query === '' ? $path : $path . '?' . $query;
146}
147
148function mnbt_node_get_headers() {
149 if (function_exists('getallheaders')) return getallheaders();
150 $headers = [];
151 foreach ($_SERVER as $key => $value) {
152 if (substr($key, 0, 5) === 'HTTP_') {
153 $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
154 $headers[$name] = $value;
155 }
156 }
157 return $headers;
158}
159
160function mnbt_node_normalize_json($value) {
161 if (is_array($value)) return $value;
162 if (!is_string($value) || trim($value) === '') return [];
163 $data = json_decode($value, true);
164 return is_array($data) ? $data : [];
165}
166
167function mnbt_node_clip($value, $maxLength) {
168 $value = trim((string)$value);
169 if (function_exists('mb_substr')) return mb_substr($value, 0, $maxLength, 'UTF-8');
170 return substr($value, 0, $maxLength);
171}
172
173function mnbt_node_match_target($match) {
174 $type = $match['type'] ?? 'file';
175 if ($type === 'database') {
176 $table = $match['table'] ?? '';
177 $field = $match['field'] ?? '';
178 $recordId = isset($match['record_id']) ? '#' . $match['record_id'] : '';
179 return trim($table . '.' . $field . $recordId, '.');
180 }
181 if ($type === 'log') return (string)($match['path'] ?? $match['url'] ?? '');
182 return (string)($match['path'] ?? '');
183}
184
185function mnbt_node_normalize_forbidden_report($payload) {
186 $payload = mnbt_node_normalize_json($payload);
187 $summary = isset($payload['summary']) && is_array($payload['summary']) ? $payload['summary'] : [];
188 $matches = isset($payload['matches']) && is_array($payload['matches']) ? $payload['matches'] : [];
189 $normalizedMatches = [];
190 foreach ($matches as $match) {
191 if (!is_array($match)) continue;
192 $normalizedMatches[] = [
193 'site' => mnbt_node_clip($match['site'] ?? ($payload['site'] ?? ''), 250),
194 'type' => mnbt_node_clip($match['type'] ?? 'file', 30),
195 'target' => mnbt_node_clip(mnbt_node_match_target($match), 1000),
196 'line' => max(0, (int)($match['line'] ?? 0)),
197 'keyword' => mnbt_node_clip($match['keyword'] ?? '', 250),
198 'excerpt' => mnbt_node_clip($match['excerpt'] ?? '', 200),
199 ];
200 }
201 if (!isset($summary['matches'])) $summary['matches'] = count($normalizedMatches);
202 if (!isset($summary['scanned_files'])) $summary['scanned_files'] = 0;
203 if (!isset($summary['scanned_rows'])) $summary['scanned_rows'] = 0;
204 return [
205 'site' => mnbt_node_clip($payload['site'] ?? '', 250),
206 'summary' => $summary,
207 'matches' => $normalizedMatches,
208 ];
209}
210
211function mnbt_node_cleanup_nonces($DB) {
212 $cutoff = date('Y-m-d H:i:s', time() - 600);
213 $DB->query_prepare("DELETE FROM `MN_node_nonce` WHERE `created_at` < ?", [$cutoff]);
214}
215
216function mnbt_node_nonce_used($DB, $nodeId, $nonce) {
217 mnbt_node_cleanup_nonces($DB);
218 $exists = $DB->get_row_prepare("SELECT id FROM `MN_node_nonce` WHERE `node_id`=? AND `nonce`=? LIMIT 1", [$nodeId, $nonce]);
219 if ($exists) return true;
220 return !$DB->query_prepare("INSERT INTO `MN_node_nonce` (`node_id`,`nonce`,`created_at`) VALUES (?,?,?)", [$nodeId, $nonce, date('Y-m-d H:i:s')]);
221}
222
223function mnbt_node_authenticate($DB, $conf, $body) {
224 mnbt_node_ensure_tables($DB);
225 $headers = mnbt_node_get_headers();
226 $nodeId = (string)mnbt_node_header_value($headers, 'X-MNBT-Node');
227 $nonce = (string)mnbt_node_header_value($headers, 'X-MNBT-Nonce');
228 if ($nodeId === '') return [false, null, '缺少节点ID'];
229 $node = $DB->get_row_prepare("SELECT * FROM `MN_node` WHERE `node_id`=? LIMIT 1", [$nodeId]);
230 if (!$node || ($node['enabled'] ?? 'true') !== 'true') return [false, null, '节点不存在或已禁用'];
231 if (!mnbt_node_verify_signature($headers, $_SERVER['REQUEST_METHOD'] ?? 'POST', mnbt_node_request_path(), $body, mnbt_node_platform_secret($conf), $node['node_secret'])) {
232 return [false, null, '节点签名校验失败'];
233 }
234 if (mnbt_node_nonce_used($DB, $nodeId, $nonce)) return [false, null, '重复请求'];
235 return [true, $node, 'ok'];
236}
237
238function mnbt_node_upsert_heartbeat($DB, $node, $payload) {
239 $capabilities = isset($payload['capabilities']) ? json_encode($payload['capabilities'], JSON_UNESCAPED_UNICODE) : ($node['capabilities'] ?? '[]');
240 $version = (string)($payload['version'] ?? ($node['version'] ?? ''));
241 $nodeName = (string)($payload['node_name'] ?? ($node['node_name'] ?? ''));
242 $ip = $_SERVER['REMOTE_ADDR'] ?? '';
243 $now = date('Y-m-d H:i:s');
244 $DB->query_prepare(
245 "UPDATE `MN_node` SET `node_name`=?, `status`='online', `ip`=?, `version`=?, `capabilities`=?, `last_heartbeat`=?, `updated_at`=? WHERE `node_id`=?",
246 [$nodeName, $ip, $version, $capabilities, $now, $now, $node['node_id']]
247 );
248}
249
250function mnbt_node_pull_task($DB, $nodeId) {
251 $task = $DB->get_row_prepare("SELECT * FROM `MN_node_task` WHERE `node_id`=? AND `status`='pending' ORDER BY id ASC LIMIT 1", [$nodeId]);
252 if (!$task) return null;
253 $now = date('Y-m-d H:i:s');
254 $DB->query_prepare("UPDATE `MN_node_task` SET `status`='running', `pulled_at`=?, `updated_at`=? WHERE `task_id`=?", [$now, $now, $task['task_id']]);
255 return [
256 'task_id' => $task['task_id'],
257 'action' => $task['action'],
258 'payload' => mnbt_node_normalize_json($task['payload']),
259 ];
260}
261
262function mnbt_node_store_forbidden_report($DB, $nodeId, $taskId, $payload, $status = 'success') {
263 $report = mnbt_node_normalize_forbidden_report($payload);
264 $summary = $report['summary'];
265 $now = date('Y-m-d H:i:s');
266 $DB->query_prepare("DELETE FROM `MN_forbidden_scan` WHERE `task_id`=?", [$taskId]);
267 $DB->query_prepare("DELETE FROM `MN_forbidden_match` WHERE `task_id`=?", [$taskId]);
268 $DB->query_prepare(
269 "INSERT INTO `MN_forbidden_scan` (`task_id`,`node_id`,`site`,`status`,`scanned_files`,`scanned_rows`,`matches_count`,`summary`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?,?,?,?,?)",
270 [
271 $taskId,
272 $nodeId,
273 $report['site'],
274 $status,
275 (int)($summary['scanned_files'] ?? 0),
276 (int)($summary['scanned_rows'] ?? 0),
277 (int)($summary['matches'] ?? count($report['matches'])),
278 json_encode($summary, JSON_UNESCAPED_UNICODE),
279 $now,
280 $now,
281 ]
282 );
283 foreach ($report['matches'] as $match) {
284 $DB->query_prepare(
285 "INSERT INTO `MN_forbidden_match` (`task_id`,`node_id`,`site`,`match_type`,`target`,`line_no`,`keyword`,`excerpt`,`created_at`) VALUES (?,?,?,?,?,?,?,?,?)",
286 [$taskId, $nodeId, $match['site'], $match['type'], $match['target'], $match['line'], $match['keyword'], $match['excerpt'], $now]
287 );
288 }
289}
290
291function mnbt_node_report_result($DB, $nodeId, $payload) {
292 $taskId = (string)($payload['task_id'] ?? '');
293 if ($taskId === '') return [false, '缺少任务ID'];
294 $task = $DB->get_row_prepare("SELECT * FROM `MN_node_task` WHERE `task_id`=? AND `node_id`=? LIMIT 1", [$taskId, $nodeId]);
295 if (!$task) return [false, '任务不存在'];
296 $status = (($payload['status'] ?? 'success') === 'failed') ? 'failed' : 'success';
297 $result = $payload['result'] ?? [];
298 $error = (string)($payload['error'] ?? '');
299 if (($task['action'] ?? '') === 'forbidden_scan' && $status === 'success') {
300 mnbt_node_store_forbidden_report($DB, $nodeId, $taskId, $result, $status);
301 }
302 $now = date('Y-m-d H:i:s');
303 $DB->query_prepare(
304 "UPDATE `MN_node_task` SET `status`=?, `result`=?, `error`=?, `finished_at`=?, `updated_at`=? WHERE `task_id`=?",
305 [$status, json_encode($result, JSON_UNESCAPED_UNICODE), $error, $now, $now, $taskId]
306 );
307 return [true, '结果已接收'];
308}
309
310function mnbt_node_register($DB, $btId, $nodeName = '', $nodeId = '', $nodeSecret = '') {
311 mnbt_node_ensure_tables($DB);
312 if ($nodeId === '') $nodeId = mnbt_node_random_id('node');
313 if ($nodeSecret === '') $nodeSecret = bin2hex(random_bytes(32));
314 $now = date('Y-m-d H:i:s');
315 $exists = $DB->get_row_prepare("SELECT id FROM `MN_node` WHERE `node_id`=? LIMIT 1", [$nodeId]);
316 if ($exists) {
317 $DB->query_prepare(
318 "UPDATE `MN_node` SET `bt_id`=?, `node_name`=?, `node_secret`=?, `enabled`='true', `updated_at`=? WHERE `node_id`=?",
319 [(int)$btId, $nodeName, $nodeSecret, $now, $nodeId]
320 );
321 } else {
322 $DB->query_prepare(
323 "INSERT INTO `MN_node` (`bt_id`,`node_id`,`node_name`,`node_secret`,`status`,`enabled`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?,?,?)",
324 [(int)$btId, $nodeId, $nodeName, $nodeSecret, 'offline', 'true', $now, $now]
325 );
326 }
327 return [
328 'node_id' => $nodeId,
329 'node_secret' => $nodeSecret,
330 ];
331}
332
333function mnbt_node_create_task($DB, $nodeId, $action, $payload = []) {
334 mnbt_node_ensure_tables($DB);
335 $taskId = mnbt_node_random_id('task');
336 $now = date('Y-m-d H:i:s');
337 $DB->query_prepare(
338 "INSERT INTO `MN_node_task` (`task_id`,`node_id`,`action`,`payload`,`status`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?,?)",
339 [$taskId, $nodeId, $action, json_encode($payload, JSON_UNESCAPED_UNICODE), 'pending', $now, $now]
340 );
341 return $taskId;
342}