-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmonitor.php
More file actions
451 lines (383 loc) · 16.6 KB
/
monitor.php
File metadata and controls
451 lines (383 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
<?php
/**
* 网络监控核心类
*/
require_once 'config.php';
require_once 'database.php';
require_once 'logger.php';
class NetworkMonitor {
protected Database $db;
protected Logger $logger;
public function __construct(?Database $db = null, ?Logger $logger = null) {
$this->db = $db ?? new Database();
$this->logger = $logger ?? new Logger();
}
/**
* 获取数据库实例
*/
public function getDatabase(): Database {
return $this->db;
}
/**
* 获取日志实例
*/
public function getLogger(): Logger {
return $this->logger;
}
/**
* 检查单个代理
* @param array $proxy 代理信息
* @return array 检查结果
*/
public function checkProxy(array $proxy): array {
return $this->executeProxyCheck($proxy, TIMEOUT, TIMEOUT, '逐个检查');
}
/**
* 快速检查单个代理(用于批量检查,更短的超时时间)
* @param array $proxy 代理信息
* @param bool $enableRetry 是否启用失败重试(默认启用)
* @return array 检查结果
*/
public function checkProxyFast(array $proxy, bool $enableRetry = true): array {
return $this->executeProxyCheck($proxy, 3, 2, '快速检查', $enableRetry);
}
/**
* 执行代理检查的核心逻辑
* @param array $proxy 代理信息
* @param int $timeout 请求超时时间
* @param int $connectTimeout 连接超时时间
* @param string $logPrefix 日志前缀
* @param bool $enableRetry 是否启用失败重试
* @param int $retryCount 当前重试次数(内部使用)
* @return array 检查结果
*/
private function executeProxyCheck(array $proxy, int $timeout, int $connectTimeout, string $logPrefix, bool $enableRetry = false, int $retryCount = 0): array {
$status = 'offline';
$errorMessage = null;
$responseTime = 0;
$ch = null;
try {
$ch = curl_init();
// 检查curl_init是否成功
if ($ch === false) {
throw new Exception('curl_init() 失败');
}
// 基本curl设置
curl_setopt_array($ch, [
CURLOPT_URL => TEST_URL,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $connectTimeout,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_USERAGENT => 'NetWatch Monitor/1.0'
]);
// 设置代理
if ($proxy['type'] === 'socks5') {
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
} else {
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
}
$proxyUrl = $proxy['ip'] . ':' . $proxy['port'];
curl_setopt($ch, CURLOPT_PROXY, $proxyUrl);
// 如果有认证信息
if (!empty($proxy['username']) && !empty($proxy['password'])) {
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxy['username'] . ':' . $proxy['password']);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
// 获取代理服务器到目标网站的纯网络响应时间
// CURLINFO_STARTTRANSFER_TIME: 从请求开始到接收到第一个字节的时间
// 这个时间反映的是通过代理到达目标服务器的网络延迟
$transferTime = curl_getinfo($ch, CURLINFO_STARTTRANSFER_TIME);
// 获取连接建立时间(包括通过代理建立连接的时间)
$connectTime = curl_getinfo($ch, CURLINFO_CONNECT_TIME);
// 代理响应时间 = 数据传输时间 - 连接建立时间
// 这样得到的是纯粹的网络响应时间,不包括连接建立的开销
$responseTime = max(0, ($transferTime - $connectTime)) * 1000; // 转换为毫秒
// 如果计算结果异常,使用传输时间作为备选
if ($responseTime <= 0 || $transferTime <= 0) {
$responseTime = $transferTime * 1000;
}
curl_close($ch);
$ch = null; // 标记已关闭
// cURL error 18: transfer closed with outstanding read data remaining
$isPartialFileError = ($curlErrno === 18);
if ($response !== false && $httpCode === 200) {
$status = 'online';
$this->logger->info("代理 {$proxy['ip']}:{$proxy['port']} {$logPrefix}成功,网络响应时间: {$responseTime}ms");
} else if ($isPartialFileError && $httpCode === 200) {
// 这种情况是代理本身是通的,但与目标网站数据传输不完整,也视为在线
$status = 'online';
$errorMessage = "Partial transfer: " . $curlError;
$this->logger->info("代理 {$proxy['ip']}:{$proxy['port']} {$logPrefix}成功 (但传输不完整): {$curlError},网络响应时间: {$responseTime}ms");
} else {
$errorMessage = $curlError ?: "HTTP Code: $httpCode";
// 对于失败的请求,如果有传输时间数据则使用,否则设为0
if ($transferTime > 0) {
$responseTime = $transferTime * 1000;
} else {
$responseTime = 0;
}
// 如果启用了重试且这是第一次检测失败,进行第二次检测
if ($enableRetry && $retryCount === 0) {
$this->logger->info("代理 {$proxy['ip']}:{$proxy['port']} {$logPrefix}失败,进行第二次检测...");
// 短暂延迟后重试
usleep(defined('PROXY_RETRY_DELAY_US') ? PROXY_RETRY_DELAY_US : 200000); // 0.2秒延迟
return $this->executeProxyCheck($proxy, $timeout, $connectTimeout, $logPrefix, $enableRetry, 1);
}
$retryInfo = $retryCount > 0 ? "(第二次检测)" : "";
$errorMessageForLog = $errorMessage ?? '';
$this->logger->warning("代理 {$proxy['ip']}:{$proxy['port']} {$logPrefix}失败{$retryInfo}: {$errorMessageForLog},尝试时间: {$responseTime}ms");
}
} catch (Exception $e) {
// 确保在异常情况下也关闭curl句柄
if ($ch !== null) {
curl_close($ch);
$ch = null;
}
$errorMessage = $e->getMessage();
$responseTime = 0; // 异常情况下设为0
// 如果启用了重试且这是第一次检测异常,进行第二次检测
if ($enableRetry && $retryCount === 0) {
$this->logger->info("代理 {$proxy['ip']}:{$proxy['port']} {$logPrefix}异常,进行第二次检测...");
usleep(defined('PROXY_RETRY_DELAY_US') ? PROXY_RETRY_DELAY_US : 200000); // 0.2秒延迟
return $this->executeProxyCheck($proxy, $timeout, $connectTimeout, $logPrefix, $enableRetry, 1);
}
$retryInfo = $retryCount > 0 ? "(第二次检测)" : "";
$this->logger->error("代理 {$proxy['ip']}:{$proxy['port']} {$logPrefix}异常{$retryInfo}: $errorMessage");
}
// 更新数据库
$this->db->updateProxyStatus($proxy['id'], $status, $responseTime, $errorMessage ?? null);
return [
'status' => $status,
'response_time' => $responseTime,
'error_message' => $errorMessage ?? null
];
}
/**
* 检查所有代理
*/
public function checkAllProxies(): array {
$proxies = $this->db->getAllProxies();
$results = [];
$this->logger->info("开始检查 " . count($proxies) . " 个代理");
foreach ($proxies as $proxy) {
$result = $this->checkProxy($proxy);
// 过滤敏感信息后再返回
$filteredProxy = $this->filterSensitiveData($proxy);
$results[] = array_merge($filteredProxy, $result);
// 避免过于频繁的请求
usleep(defined('PROXY_REQUEST_THROTTLE_US') ? PROXY_REQUEST_THROTTLE_US : 10000); // 0.01秒延迟
}
$this->logger->info("代理检查完成");
return $results;
}
/**
* 批量导入代理
*/
public function importProxies(array $proxyList, string $importMode = 'skip'): array {
$imported = 0;
$skipped = 0;
$errors = [];
foreach ($proxyList as $proxyData) {
try {
// 如果是跳过模式,先检查是否已存在
if ($importMode === 'skip' && $this->db->proxyExists($proxyData['ip'], $proxyData['port'])) {
$skipped++;
continue;
}
if ($this->db->addProxy(
$proxyData['ip'],
$proxyData['port'],
$proxyData['type'],
$proxyData['username'] ?? null,
$proxyData['password'] ?? null
)) {
$imported++;
} else {
$errors[] = "导入失败: {$proxyData['ip']}:{$proxyData['port']}";
}
} catch (Exception $e) {
$errors[] = "导入异常: {$proxyData['ip']}:{$proxyData['port']} - " . $e->getMessage();
}
}
$this->logger->info("导入完成: 成功 $imported 个,跳过 $skipped 个,失败 " . count($errors) . " 个");
return [
'imported' => $imported,
'skipped' => $skipped,
'errors' => $errors
];
}
/**
* 从文件导入代理
* 格式: ip:port:type:username:password (每行一个)
*/
public function importFromFile(string $filename): array {
if (!file_exists($filename)) {
throw new Exception("文件不存在: $filename");
}
$lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$proxyList = [];
foreach ($lines as $lineNum => $line) {
$line = trim($line);
if (empty($line) || $line[0] === '#') {
continue; // 跳过空行和注释
}
$parts = explode(':', $line);
if (count($parts) < 3) {
$this->logger->warning("第 " . ($lineNum + 1) . " 行格式错误: $line");
continue;
}
$ip = trim($parts[0]);
$port = (int)trim($parts[1]);
$type = strtolower(trim($parts[2]));
// 校验IP地址合法性
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
$this->logger->warning("第 " . ($lineNum + 1) . " 行IP地址无效: $ip");
continue;
}
// 校验端口范围
if ($port < 1 || $port > 65535) {
$this->logger->warning("第 " . ($lineNum + 1) . " 行端口超出范围: $port");
continue;
}
// 校验代理类型白名单
$allowedTypes = ['http', 'https', 'socks5', 'socks4'];
if (!in_array($type, $allowedTypes, true)) {
$this->logger->warning("第 " . ($lineNum + 1) . " 行代理类型无效: $type (允许: " . implode(', ', $allowedTypes) . ")");
continue;
}
$proxyList[] = [
'ip' => $ip,
'port' => $port,
'type' => $type,
'username' => isset($parts[3]) ? trim($parts[3]) : null,
'password' => isset($parts[4]) ? trim($parts[4]) : null
];
}
return $this->importProxies($proxyList, 'add');
}
/**
* 获取统计信息
*/
public function getStats(): array {
return $this->db->getProxyStats();
}
/**
* 获取最近的日志
*/
public function getRecentLogs(int $limit = 100): array {
return $this->db->getRecentLogs($limit);
}
/**
* 获取所有代理(内部使用,包含敏感信息)
*/
public function getAllProxies(): array {
return $this->db->getAllProxies();
}
/**
* 获取所有代理(安全版本,不包含敏感信息)
*/
public function getAllProxiesSafe(): array {
$proxies = $this->db->getAllProxies();
return array_map([$this, 'filterSensitiveData'], $proxies);
}
/**
* 获取分页代理列表(安全版本)
*/
public function getProxiesPaginatedSafe(int $page = 1, int $perPage = 200): array {
$proxies = $this->db->getProxiesPaginated($page, $perPage);
return array_map([$this, 'filterSensitiveData'], $proxies);
}
/**
* 根据ID获取代理(内部使用,包含敏感信息)
* @param int $id 代理ID
* @return array|null 代理信息或null
*/
public function getProxyById(int $id): ?array {
return $this->db->getProxyById($id);
}
/**
* 获取故障代理
*/
public function getFailedProxies(): array {
return $this->db->getFailedProxies();
}
/**
* 添加警报
*/
public function addAlert(int $proxyId, string $alertType, string $message): bool {
return $this->db->addAlert($proxyId, $alertType, $message);
}
/**
* 清理旧日志
*/
public function cleanupOldLogs(int $days = 30): int {
return $this->db->cleanupOldLogs($days);
}
/**
* 添加代理
*/
public function addProxy(string $ip, int $port, string $type, ?string $username = null, ?string $password = null): bool {
return $this->db->addProxy($ip, $port, $type, $username, $password);
}
/**
* 获取代理总数
*/
public function getProxyCount(): int {
return $this->db->getProxyCount();
}
/**
* 过滤代理敏感信息(用户名和密码)
*/
public function filterSensitiveData(array $proxy): array {
$filtered = $proxy;
// 移除敏感信息
unset($filtered['username']);
unset($filtered['password']);
return $filtered;
}
/**
* 分批检查代理
*/
public function checkProxyBatch(int $offset = 0, int $limit = 20): array {
$proxies = $this->db->getProxiesBatch($offset, $limit);
$results = [];
$this->logger->info("开始分批检查代理: offset=$offset, limit=$limit, 实际获取 " . count($proxies) . " 个代理");
foreach ($proxies as $proxy) {
$result = $this->checkProxyFast($proxy);
// 过滤敏感信息后再返回
$filteredProxy = $this->filterSensitiveData($proxy);
$results[] = array_merge($filteredProxy, $result);
// 减少延迟时间,提高批量检查速度
usleep(defined('PROXY_REQUEST_THROTTLE_US') ? PROXY_REQUEST_THROTTLE_US : 10000); // 0.01秒延迟,更快的批量检查
}
$this->logger->info("分批检查完成: 检查了 " . count($results) . " 个代理");
return $results;
}
/**
* 搜索代理(安全版本)
* @param string $searchTerm 搜索词
* @param int $page 页码
* @param int $perPage 每页数量
* @param string $statusFilter 状态筛选
* @return array 搜索结果
*/
public function searchProxiesSafe(string $searchTerm, int $page = 1, int $perPage = 200, string $statusFilter = ''): array {
$proxies = $this->db->searchProxies($searchTerm, $page, $perPage, $statusFilter);
return array_map([$this, 'filterSensitiveData'], $proxies);
}
/**
* 获取搜索结果总数
* @param string $searchTerm 搜索词
* @param string $statusFilter 状态筛选
* @return int 搜索结果总数
*/
public function getSearchCount(string $searchTerm, string $statusFilter = ''): int {
return $this->db->getSearchCount($searchTerm, $statusFilter);
}
}