303 lines
9.8 KiB
C#
303 lines
9.8 KiB
C#
using System.Collections.Concurrent;
|
||
using System.Net;
|
||
using System.Net.Sockets;
|
||
using AmtScanner.Api.Data;
|
||
using AmtScanner.Api.Models;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace AmtScanner.Api.Services;
|
||
|
||
public interface IAmtScanService
|
||
{
|
||
Task<string> StartScanAsync(string networkSegment, string subnetMask);
|
||
ScanStatus? GetScanStatus(string taskId);
|
||
void CancelScan(string taskId);
|
||
Task<object> TestSingleIpAsync(string ip);
|
||
}
|
||
|
||
public class ScanStatus
|
||
{
|
||
public string TaskId { get; set; } = string.Empty;
|
||
public string Status { get; set; } = "idle"; // idle, running, completed, cancelled
|
||
public int TotalCount { get; set; }
|
||
private int _scannedCount;
|
||
private int _foundDevices;
|
||
|
||
public int ScannedCount
|
||
{
|
||
get => _scannedCount;
|
||
set => _scannedCount = value;
|
||
}
|
||
|
||
public int FoundDevices
|
||
{
|
||
get => _foundDevices;
|
||
set => _foundDevices = value;
|
||
}
|
||
|
||
public void IncrementScanned() => Interlocked.Increment(ref _scannedCount);
|
||
public void IncrementFound() => Interlocked.Increment(ref _foundDevices);
|
||
}
|
||
|
||
public class AmtScanService : IAmtScanService
|
||
{
|
||
private readonly IServiceScopeFactory _scopeFactory;
|
||
private readonly ILogger<AmtScanService> _logger;
|
||
private static readonly ConcurrentDictionary<string, ScanStatus> _scanTasks = new();
|
||
private static readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationTokens = new();
|
||
|
||
// AMT端口: 16992 (HTTP), 16993 (HTTPS)
|
||
private const int AMT_HTTP_PORT = 16992;
|
||
private const int AMT_HTTPS_PORT = 16993;
|
||
private const int TIMEOUT_MS = 1000; // TCP连接超时1秒足够
|
||
|
||
public AmtScanService(IServiceScopeFactory scopeFactory, ILogger<AmtScanService> logger)
|
||
{
|
||
_scopeFactory = scopeFactory;
|
||
_logger = logger;
|
||
}
|
||
|
||
public async Task<string> StartScanAsync(string networkSegment, string subnetMask)
|
||
{
|
||
var taskId = Guid.NewGuid().ToString("N");
|
||
var cts = new CancellationTokenSource();
|
||
|
||
var ipList = GenerateIpList(networkSegment, subnetMask);
|
||
|
||
var status = new ScanStatus
|
||
{
|
||
TaskId = taskId,
|
||
Status = "running",
|
||
TotalCount = ipList.Count,
|
||
ScannedCount = 0,
|
||
FoundDevices = 0
|
||
};
|
||
|
||
_scanTasks[taskId] = status;
|
||
_cancellationTokens[taskId] = cts;
|
||
|
||
// 后台执行扫描
|
||
_ = Task.Run(async () => await ExecuteScanAsync(taskId, ipList, cts.Token));
|
||
|
||
return taskId;
|
||
}
|
||
|
||
public ScanStatus? GetScanStatus(string taskId)
|
||
{
|
||
return _scanTasks.TryGetValue(taskId, out var status) ? status : null;
|
||
}
|
||
|
||
public void CancelScan(string taskId)
|
||
{
|
||
if (_cancellationTokens.TryGetValue(taskId, out var cts))
|
||
{
|
||
cts.Cancel();
|
||
if (_scanTasks.TryGetValue(taskId, out var status))
|
||
{
|
||
status.Status = "cancelled";
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task ExecuteScanAsync(string taskId, List<string> ipList, CancellationToken cancellationToken)
|
||
{
|
||
var status = _scanTasks[taskId];
|
||
var foundDevices = new ConcurrentBag<AmtPendingDevice_new>();
|
||
|
||
try
|
||
{
|
||
// 并行扫描,TCP端口扫描可以用更高并发
|
||
var options = new ParallelOptions
|
||
{
|
||
MaxDegreeOfParallelism = 100,
|
||
CancellationToken = cancellationToken
|
||
};
|
||
|
||
await Parallel.ForEachAsync(ipList, options, async (ip, ct) =>
|
||
{
|
||
try
|
||
{
|
||
var result = await ScanSingleIpAsync(ip, ct);
|
||
if (result != null)
|
||
{
|
||
foundDevices.Add(result);
|
||
status.IncrementFound();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug("扫描 {Ip} 失败: {Error}", ip, ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
status.IncrementScanned();
|
||
}
|
||
});
|
||
|
||
// 保存发现的设备到数据库
|
||
if (foundDevices.Any())
|
||
{
|
||
using var scope = _scopeFactory.CreateScope();
|
||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||
|
||
foreach (var device in foundDevices)
|
||
{
|
||
// 检查是否已存在
|
||
var existing = await db.AmtPendingDevices_new
|
||
.FirstOrDefaultAsync(d => d.IpAddress == device.IpAddress);
|
||
|
||
if (existing == null)
|
||
{
|
||
db.AmtPendingDevices_new.Add(device);
|
||
}
|
||
else
|
||
{
|
||
// 更新已存在的设备信息
|
||
existing.AmtVersion = device.AmtVersion;
|
||
existing.ProvisioningState = device.ProvisioningState;
|
||
existing.UpdatedAt = DateTime.UtcNow;
|
||
}
|
||
}
|
||
|
||
await db.SaveChangesAsync();
|
||
_logger.LogInformation("扫描完成,发现 {Count} 个AMT设备", foundDevices.Count);
|
||
}
|
||
|
||
status.Status = "completed";
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
status.Status = "cancelled";
|
||
_logger.LogInformation("扫描任务 {TaskId} 已取消", taskId);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
status.Status = "completed";
|
||
_logger.LogError(ex, "扫描任务 {TaskId} 出错", taskId);
|
||
}
|
||
finally
|
||
{
|
||
// 清理
|
||
_cancellationTokens.TryRemove(taskId, out _);
|
||
}
|
||
}
|
||
|
||
private async Task<AmtPendingDevice_new?> ScanSingleIpAsync(string ip, CancellationToken ct)
|
||
{
|
||
try
|
||
{
|
||
// 检测16992和16993端口
|
||
bool httpOpen = await IsPortOpenAsync(ip, AMT_HTTP_PORT, TIMEOUT_MS, ct);
|
||
bool httpsOpen = await IsPortOpenAsync(ip, AMT_HTTPS_PORT, TIMEOUT_MS, ct);
|
||
|
||
if (httpOpen || httpsOpen)
|
||
{
|
||
string portInfo = "";
|
||
if (httpOpen && httpsOpen) portInfo = "16992,16993";
|
||
else if (httpOpen) portInfo = "16992";
|
||
else portInfo = "16993";
|
||
|
||
_logger.LogInformation("发现AMT设备: {Ip}, 开放端口: {Ports}", ip, portInfo);
|
||
|
||
return new AmtPendingDevice_new
|
||
{
|
||
IpAddress = ip,
|
||
AmtVersion = "Unknown", // 需要通过WS-Man获取
|
||
ProvisioningState = httpsOpen ? "Post" : "Pre", // HTTPS开放通常表示已配置
|
||
Source = "scan",
|
||
CreatedAt = DateTime.UtcNow,
|
||
UpdatedAt = DateTime.UtcNow
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||
{
|
||
throw;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private static async Task<bool> IsPortOpenAsync(string ip, int port, int timeoutMs, CancellationToken ct)
|
||
{
|
||
try
|
||
{
|
||
using var client = new TcpClient();
|
||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||
cts.CancelAfter(timeoutMs);
|
||
|
||
await client.ConnectAsync(IPAddress.Parse(ip), port, cts.Token);
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static List<string> GenerateIpList(string networkSegment, string subnetMask)
|
||
{
|
||
var ipList = new List<string>();
|
||
|
||
var networkBytes = IPAddress.Parse(networkSegment).GetAddressBytes();
|
||
var maskBytes = IPAddress.Parse(subnetMask).GetAddressBytes();
|
||
|
||
// 计算网络地址和广播地址
|
||
var networkAddress = new byte[4];
|
||
var broadcastAddress = new byte[4];
|
||
|
||
for (int i = 0; i < 4; i++)
|
||
{
|
||
networkAddress[i] = (byte)(networkBytes[i] & maskBytes[i]);
|
||
broadcastAddress[i] = (byte)(networkBytes[i] | ~maskBytes[i]);
|
||
}
|
||
|
||
// 生成IP列表(排除网络地址和广播地址)
|
||
var startIp = BitConverter.ToUInt32(networkAddress.Reverse().ToArray(), 0) + 1;
|
||
var endIp = BitConverter.ToUInt32(broadcastAddress.Reverse().ToArray(), 0) - 1;
|
||
|
||
for (uint ip = startIp; ip <= endIp; ip++)
|
||
{
|
||
var bytes = BitConverter.GetBytes(ip).Reverse().ToArray();
|
||
ipList.Add(new IPAddress(bytes).ToString());
|
||
}
|
||
|
||
return ipList;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 测试单个IP的AMT端口,用于调试
|
||
/// </summary>
|
||
public async Task<object> TestSingleIpAsync(string ip)
|
||
{
|
||
try
|
||
{
|
||
bool httpOpen = await IsPortOpenAsync(ip, AMT_HTTP_PORT, 2000, CancellationToken.None);
|
||
bool httpsOpen = await IsPortOpenAsync(ip, AMT_HTTPS_PORT, 2000, CancellationToken.None);
|
||
|
||
return new
|
||
{
|
||
success = httpOpen || httpsOpen,
|
||
ip = ip,
|
||
port16992 = httpOpen,
|
||
port16993 = httpsOpen,
|
||
isAmtDevice = httpOpen || httpsOpen
|
||
};
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new
|
||
{
|
||
success = false,
|
||
ip = ip,
|
||
error = ex.Message,
|
||
errorType = ex.GetType().Name
|
||
};
|
||
}
|
||
}
|
||
}
|