410 lines
14 KiB
C#
410 lines
14 KiB
C#
using AmtScanner.Api.Data;
|
||
using AmtScanner.Api.Models;
|
||
using AmtScanner.Api.Services;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using System.Net.Sockets;
|
||
|
||
namespace AmtScanner.Api.Controllers;
|
||
|
||
[ApiController]
|
||
[Route("api/[controller]")]
|
||
public class DevicesController : ControllerBase
|
||
{
|
||
private readonly AppDbContext _context;
|
||
private readonly ILogger<DevicesController> _logger;
|
||
private readonly IAmtPowerService _powerService;
|
||
private readonly ICredentialService _credentialService;
|
||
|
||
public DevicesController(
|
||
AppDbContext context,
|
||
ILogger<DevicesController> logger,
|
||
IAmtPowerService powerService,
|
||
ICredentialService credentialService)
|
||
{
|
||
_context = context;
|
||
_logger = logger;
|
||
_powerService = powerService;
|
||
_credentialService = credentialService;
|
||
}
|
||
|
||
[HttpGet]
|
||
public async Task<ActionResult<ApiResponse<List<AmtDevice>>>> GetAllDevices()
|
||
{
|
||
var devices = await _context.AmtDevices.ToListAsync();
|
||
return Ok(ApiResponse<List<AmtDevice>>.Success(devices));
|
||
}
|
||
|
||
[HttpGet("{id}")]
|
||
public async Task<ActionResult<ApiResponse<AmtDevice>>> GetDevice(long id)
|
||
{
|
||
var device = await _context.AmtDevices.FindAsync(id);
|
||
|
||
if (device == null)
|
||
{
|
||
return Ok(ApiResponse<AmtDevice>.Fail(404, "设备不存在"));
|
||
}
|
||
|
||
return Ok(ApiResponse<AmtDevice>.Success(device));
|
||
}
|
||
|
||
[HttpDelete("{id}")]
|
||
public async Task<ActionResult<ApiResponse<object>>> DeleteDevice(long id)
|
||
{
|
||
var device = await _context.AmtDevices.FindAsync(id);
|
||
|
||
if (device == null)
|
||
{
|
||
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
|
||
}
|
||
|
||
_context.AmtDevices.Remove(device);
|
||
await _context.SaveChangesAsync();
|
||
|
||
return Ok(ApiResponse<object>.Success(null, "删除成功"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 手动添加设备
|
||
/// </summary>
|
||
[HttpPost]
|
||
public async Task<ActionResult<ApiResponse<AmtDevice>>> AddDevice([FromBody] AddDeviceRequest request)
|
||
{
|
||
// 验证 IP 地址格式
|
||
if (string.IsNullOrWhiteSpace(request.IpAddress))
|
||
{
|
||
return Ok(ApiResponse<AmtDevice>.Fail(400, "IP 地址不能为空"));
|
||
}
|
||
|
||
// 检查设备是否已存在
|
||
var existingDevice = await _context.AmtDevices.FirstOrDefaultAsync(d => d.IpAddress == request.IpAddress);
|
||
if (existingDevice != null)
|
||
{
|
||
return Ok(ApiResponse<AmtDevice>.Fail(400, $"设备 {request.IpAddress} 已存在"));
|
||
}
|
||
|
||
var device = new AmtDevice
|
||
{
|
||
IpAddress = request.IpAddress,
|
||
Hostname = request.Hostname,
|
||
Description = request.Description,
|
||
MajorVersion = 0,
|
||
MinorVersion = 0,
|
||
ProvisioningState = ProvisioningState.UNKNOWN,
|
||
AmtOnline = false,
|
||
OsOnline = false,
|
||
DiscoveredAt = DateTime.UtcNow,
|
||
LastSeenAt = DateTime.UtcNow
|
||
};
|
||
|
||
// 如果提供了 Windows 凭据,一并保存
|
||
if (!string.IsNullOrEmpty(request.WindowsUsername))
|
||
{
|
||
device.WindowsUsername = request.WindowsUsername;
|
||
if (!string.IsNullOrEmpty(request.WindowsPassword))
|
||
{
|
||
device.WindowsPassword = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.WindowsPassword));
|
||
}
|
||
}
|
||
|
||
_context.AmtDevices.Add(device);
|
||
await _context.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("Manually added device {Ip}", request.IpAddress);
|
||
|
||
return Ok(ApiResponse<AmtDevice>.Success(device, "设备添加成功"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新设备信息
|
||
/// </summary>
|
||
[HttpPut("{id}")]
|
||
public async Task<ActionResult<ApiResponse<AmtDevice>>> UpdateDevice(long id, [FromBody] UpdateDeviceRequest request)
|
||
{
|
||
var device = await _context.AmtDevices.FindAsync(id);
|
||
|
||
if (device == null)
|
||
{
|
||
return Ok(ApiResponse<AmtDevice>.Fail(404, "设备不存在"));
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(request.Hostname))
|
||
device.Hostname = request.Hostname;
|
||
if (!string.IsNullOrEmpty(request.Description))
|
||
device.Description = request.Description;
|
||
|
||
await _context.SaveChangesAsync();
|
||
|
||
return Ok(ApiResponse<AmtDevice>.Success(device, "更新成功"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置设备的 Windows 登录凭据
|
||
/// </summary>
|
||
[HttpPut("{id}/credentials")]
|
||
public async Task<ActionResult<ApiResponse<object>>> SetDeviceCredentials(long id, [FromBody] SetDeviceCredentialsRequest request)
|
||
{
|
||
var device = await _context.AmtDevices.FindAsync(id);
|
||
|
||
if (device == null)
|
||
{
|
||
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
|
||
}
|
||
|
||
device.WindowsUsername = request.Username;
|
||
// 简单加密存储密码(生产环境应使用更安全的加密方式)
|
||
device.WindowsPassword = string.IsNullOrEmpty(request.Password) ? null :
|
||
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.Password));
|
||
|
||
await _context.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("Updated Windows credentials for device {Id} ({Ip})", id, device.IpAddress);
|
||
|
||
return Ok(ApiResponse<object>.Success(null, "凭据设置成功"));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取设备的 Windows 凭据(仅返回用户名,不返回密码)
|
||
/// </summary>
|
||
[HttpGet("{id}/credentials")]
|
||
public async Task<ActionResult<ApiResponse<DeviceCredentialsDto>>> GetDeviceCredentials(long id)
|
||
{
|
||
var device = await _context.AmtDevices.FindAsync(id);
|
||
|
||
if (device == null)
|
||
{
|
||
return Ok(ApiResponse<DeviceCredentialsDto>.Fail(404, "设备不存在"));
|
||
}
|
||
|
||
return Ok(ApiResponse<DeviceCredentialsDto>.Success(new DeviceCredentialsDto
|
||
{
|
||
DeviceId = device.Id,
|
||
Username = device.WindowsUsername,
|
||
HasPassword = !string.IsNullOrEmpty(device.WindowsPassword)
|
||
}));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测所有设备的在线状态
|
||
/// </summary>
|
||
[HttpGet("status")]
|
||
public async Task<ActionResult<ApiResponse<List<DeviceStatusDto>>>> CheckAllDevicesStatus()
|
||
{
|
||
var devices = await _context.AmtDevices.ToListAsync();
|
||
var credentials = await _context.AmtCredentials.ToListAsync();
|
||
var statusList = new List<DeviceStatusDto>();
|
||
|
||
// 并行检测所有设备
|
||
var tasks = devices.Select(async device =>
|
||
{
|
||
var (amtOnline, openPorts) = await CheckAmtOnlineAsync(device.IpAddress);
|
||
var osOnline = false;
|
||
|
||
// 如果 AMT 在线,尝试查询电源状态来判断 OS 是否在线
|
||
if (amtOnline && openPorts.Count > 0)
|
||
{
|
||
osOnline = await CheckOsOnlineAsync(device.IpAddress, openPorts, credentials);
|
||
}
|
||
|
||
// 更新数据库中的在线状态
|
||
device.AmtOnline = amtOnline;
|
||
device.OsOnline = osOnline;
|
||
if (amtOnline)
|
||
{
|
||
device.LastSeenAt = DateTime.UtcNow;
|
||
}
|
||
|
||
return new DeviceStatusDto
|
||
{
|
||
Id = device.Id,
|
||
IpAddress = device.IpAddress,
|
||
AmtOnline = amtOnline,
|
||
OsOnline = osOnline
|
||
};
|
||
});
|
||
|
||
statusList = (await Task.WhenAll(tasks)).ToList();
|
||
|
||
// 保存更新
|
||
await _context.SaveChangesAsync();
|
||
|
||
return Ok(ApiResponse<List<DeviceStatusDto>>.Success(statusList));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测单个设备的在线状态
|
||
/// </summary>
|
||
[HttpGet("{id}/status")]
|
||
public async Task<ActionResult<ApiResponse<DeviceStatusDto>>> CheckDeviceStatus(long id)
|
||
{
|
||
var device = await _context.AmtDevices.FindAsync(id);
|
||
|
||
if (device == null)
|
||
{
|
||
return Ok(ApiResponse<DeviceStatusDto>.Fail(404, "设备不存在"));
|
||
}
|
||
|
||
var credentials = await _context.AmtCredentials.ToListAsync();
|
||
var (amtOnline, openPorts) = await CheckAmtOnlineAsync(device.IpAddress);
|
||
var osOnline = false;
|
||
|
||
// 如果 AMT 在线,尝试查询电源状态来判断 OS 是否在线
|
||
if (amtOnline && openPorts.Count > 0)
|
||
{
|
||
osOnline = await CheckOsOnlineAsync(device.IpAddress, openPorts, credentials);
|
||
}
|
||
|
||
// 更新数据库
|
||
device.AmtOnline = amtOnline;
|
||
device.OsOnline = osOnline;
|
||
if (amtOnline)
|
||
{
|
||
device.LastSeenAt = DateTime.UtcNow;
|
||
}
|
||
await _context.SaveChangesAsync();
|
||
|
||
return Ok(ApiResponse<DeviceStatusDto>.Success(new DeviceStatusDto
|
||
{
|
||
Id = device.Id,
|
||
IpAddress = device.IpAddress,
|
||
AmtOnline = amtOnline,
|
||
OsOnline = osOnline
|
||
}));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测 AMT 是否在线(通过尝试连接AMT端口)
|
||
/// </summary>
|
||
private async Task<(bool isOnline, List<int> openPorts)> CheckAmtOnlineAsync(string ipAddress)
|
||
{
|
||
int[] amtPorts = { 16992, 16993 };
|
||
var openPorts = new List<int>();
|
||
|
||
foreach (var port in amtPorts)
|
||
{
|
||
try
|
||
{
|
||
using var client = new TcpClient();
|
||
var connectTask = client.ConnectAsync(ipAddress, port);
|
||
|
||
// 设置超时时间为2秒(增加超时以应对网络延迟)
|
||
if (await Task.WhenAny(connectTask, Task.Delay(2000)) == connectTask)
|
||
{
|
||
if (client.Connected)
|
||
{
|
||
openPorts.Add(port);
|
||
_logger.LogInformation("Device {Ip} AMT port {Port} is open", ipAddress, port);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogInformation("Device {Ip} AMT port {Port} connection timeout", ipAddress, port);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogInformation("Failed to connect to {Ip}:{Port}: {Error}", ipAddress, port, ex.Message);
|
||
}
|
||
}
|
||
|
||
var isOnline = openPorts.Count > 0;
|
||
_logger.LogInformation("Device {Ip} AMT online: {Online}, open ports: [{Ports}]",
|
||
ipAddress, isOnline, string.Join(", ", openPorts));
|
||
return (isOnline, openPorts);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测操作系统是否在线(通过查询电源状态)
|
||
/// </summary>
|
||
private async Task<bool> CheckOsOnlineAsync(string ipAddress, List<int> openPorts, List<AmtCredential> credentials)
|
||
{
|
||
// 尝试使用所有凭据查询电源状态
|
||
foreach (var credential in credentials)
|
||
{
|
||
try
|
||
{
|
||
var decryptedPassword = _credentialService.DecryptPassword(credential.Password);
|
||
var powerState = await _powerService.GetPowerStateAsync(
|
||
ipAddress,
|
||
credential.Username,
|
||
decryptedPassword,
|
||
openPorts);
|
||
|
||
if (powerState.Success)
|
||
{
|
||
// PowerState = 2 表示开机(操作系统在运行)
|
||
var osOnline = powerState.PowerState == 2;
|
||
_logger.LogInformation("Device {Ip} OS online: {Online} (PowerState: {State} - {StateText})",
|
||
ipAddress, osOnline, powerState.PowerState, powerState.PowerStateText);
|
||
return osOnline;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning("Device {Ip} failed to get power state: {Error}",
|
||
ipAddress, powerState.Error);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning("Failed to get power state for {Ip} with credential {User}: {Error}",
|
||
ipAddress, credential.Username, ex.Message);
|
||
}
|
||
}
|
||
|
||
// 如果无法查询电源状态,返回 false
|
||
_logger.LogWarning("Device {Ip} OS online status unknown (no valid credentials or query failed)", ipAddress);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设备状态DTO
|
||
/// </summary>
|
||
public class DeviceStatusDto
|
||
{
|
||
public long Id { get; set; }
|
||
public string IpAddress { get; set; } = string.Empty;
|
||
public bool AmtOnline { get; set; }
|
||
public bool OsOnline { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新设备请求
|
||
/// </summary>
|
||
public class UpdateDeviceRequest
|
||
{
|
||
public string? Hostname { get; set; }
|
||
public string? Description { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置设备 Windows 凭据请求
|
||
/// </summary>
|
||
public class SetDeviceCredentialsRequest
|
||
{
|
||
public string? Username { get; set; }
|
||
public string? Password { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设备凭据DTO
|
||
/// </summary>
|
||
public class DeviceCredentialsDto
|
||
{
|
||
public long DeviceId { get; set; }
|
||
public string? Username { get; set; }
|
||
public bool HasPassword { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加设备请求
|
||
/// </summary>
|
||
public class AddDeviceRequest
|
||
{
|
||
public string IpAddress { get; set; } = string.Empty;
|
||
public string? Hostname { get; set; }
|
||
public string? Description { get; set; }
|
||
public string? WindowsUsername { get; set; }
|
||
public string? WindowsPassword { get; set; }
|
||
}
|