470 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<object>>>> GetAllDevices()
{
var devices = await _context.AmtDevices.ToListAsync();
// 解密 AMT 密码返回给前端
var result = devices.Select(d => new {
d.Id,
d.IpAddress,
d.Hostname,
d.SystemUuid,
d.MajorVersion,
d.MinorVersion,
d.ProvisioningState,
d.Description,
d.AmtOnline,
d.OsOnline,
d.WindowsUsername,
d.WindowsPassword,
d.AmtUsername,
AmtPasswordDecrypted = string.IsNullOrEmpty(d.AmtPassword) ? null :
System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(d.AmtPassword)),
d.DiscoveredAt,
d.LastSeenAt
}).ToList();
return Ok(ApiResponse<List<object>>.Success(result.Cast<object>().ToList()));
}
[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>
/// 设置设备的 AMT 登录凭据
/// </summary>
[HttpPut("{id}/amt-credentials")]
public async Task<ActionResult<ApiResponse<object>>> SetAmtCredentials(long id, [FromBody] SetAmtCredentialsRequest request)
{
var device = await _context.AmtDevices.FindAsync(id);
if (device == null)
{
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
}
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
{
return Ok(ApiResponse<object>.Fail(400, "用户名和密码不能为空"));
}
device.AmtUsername = request.Username;
// 简单加密存储密码(生产环境应使用更安全的加密方式)
device.AmtPassword = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.Password));
await _context.SaveChangesAsync();
_logger.LogInformation("Updated AMT credentials for device {Id} ({Ip})", id, device.IpAddress);
return Ok(ApiResponse<object>.Success(null, "AMT凭据设置成功"));
}
/// <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; }
}
/// <summary>
/// 设置设备 AMT 凭据请求
/// </summary>
public class SetAmtCredentialsRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}