using AmtScanner.Api.Data; using AmtScanner.Api.Models; using AmtScanner.Api.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Security.Cryptography; namespace AmtScanner.Api.Controllers; [ApiController] [Route("api/[controller]")] public class RemoteDesktopController : ControllerBase { private readonly IGuacamoleService _guacamoleService; private readonly AppDbContext _context; private readonly ILogger _logger; public RemoteDesktopController( IGuacamoleService guacamoleService, AppDbContext context, ILogger logger) { _guacamoleService = guacamoleService; _context = context; _logger = logger; } /// /// 生成远程访问 Token(管理员使用) /// [HttpPost("generate-token/{deviceId}")] public async Task>> GenerateToken( long deviceId, [FromBody] GenerateTokenRequest request) { var device = await _context.AmtDevices.FindAsync(deviceId); if (device == null) return Ok(ApiResponse.Fail(404, "设备不存在")); WindowsCredential? credential = null; if (request.CredentialId.HasValue) { credential = await _context.WindowsCredentials.FindAsync(request.CredentialId.Value); } else { credential = await _context.WindowsCredentials.FirstOrDefaultAsync(c => c.IsDefault); } if (credential == null) return Ok(ApiResponse.Fail(400, "请先配置 Windows 凭据")); var token = GenerateRandomToken(); var expiresAt = DateTime.UtcNow.AddMinutes(request.ExpiresInMinutes ?? 30); var accessToken = new RemoteAccessToken { Token = token, DeviceId = deviceId, WindowsCredentialId = credential.Id, ExpiresAt = expiresAt, MaxUseCount = request.MaxUseCount ?? 1, Note = request.Note }; _context.RemoteAccessTokens.Add(accessToken); await _context.SaveChangesAsync(); var baseUrl = $"{Request.Scheme}://{Request.Host}"; var accessUrl = $"{baseUrl}/remote/{token}"; _logger.LogInformation("Generated remote access token for device {Ip}, expires at {ExpiresAt}", device.IpAddress, expiresAt); return Ok(ApiResponse.Success(new GenerateTokenResponse { Success = true, Token = token, AccessUrl = accessUrl, ExpiresAt = expiresAt, MaxUseCount = accessToken.MaxUseCount, DeviceIp = device.IpAddress }, "Token 生成成功")); } /// /// 通过 Token 连接远程桌面 /// [HttpGet("connect-by-token/{token}")] public async Task>> ConnectByToken(string token) { var accessToken = await _context.RemoteAccessTokens .Include(t => t.Device) .Include(t => t.WindowsCredential) .FirstOrDefaultAsync(t => t.Token == token); if (accessToken == null) return Ok(ApiResponse.Fail(404, "无效的访问链接")); if (!accessToken.IsValid()) return Ok(ApiResponse.Fail(400, "访问链接已过期或已达到使用次数上限")); if (accessToken.Device == null || accessToken.WindowsCredential == null) return Ok(ApiResponse.Fail(400, "设备或凭据信息不完整")); accessToken.UseCount++; accessToken.UsedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); var guacToken = await _guacamoleService.GetAuthTokenAsync(); if (string.IsNullOrEmpty(guacToken)) return Ok(ApiResponse.Fail(503, "无法连接到 Guacamole 服务")); var connectionName = $"AMT-{accessToken.Device.IpAddress}"; var connectionId = await _guacamoleService.CreateOrGetConnectionAsync( guacToken, connectionName, accessToken.Device.IpAddress, accessToken.WindowsCredential.Username, accessToken.WindowsCredential.Password); if (string.IsNullOrEmpty(connectionId)) return Ok(ApiResponse.Fail(500, "创建远程连接失败")); var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId); return Ok(ApiResponse.Success(new RemoteDesktopResponse { Success = true, ConnectionUrl = connectionUrl, ConnectionId = connectionId, Token = guacToken, DeviceIp = accessToken.Device.IpAddress })); } /// /// 验证 Token 是否有效 /// [HttpGet("validate-token/{token}")] public async Task>> ValidateToken(string token) { var accessToken = await _context.RemoteAccessTokens .Include(t => t.Device) .FirstOrDefaultAsync(t => t.Token == token); if (accessToken == null) return Ok(ApiResponse.Success(new ValidateTokenResponse { Valid = false, Error = "无效的访问链接" })); if (!accessToken.IsValid()) return Ok(ApiResponse.Success(new ValidateTokenResponse { Valid = false, Error = "访问链接已过期或已达到使用次数上限" })); return Ok(ApiResponse.Success(new ValidateTokenResponse { Valid = true, DeviceIp = accessToken.Device?.IpAddress, ExpiresAt = accessToken.ExpiresAt, RemainingUses = accessToken.MaxUseCount > 0 ? accessToken.MaxUseCount - accessToken.UseCount : -1 })); } /// /// 获取设备的所有有效 Token /// [HttpGet("list-tokens/{deviceId}")] public async Task>>> GetDeviceTokens(long deviceId) { var tokens = await _context.RemoteAccessTokens .Where(t => t.DeviceId == deviceId && t.ExpiresAt > DateTime.UtcNow) .OrderByDescending(t => t.CreatedAt) .Select(t => new TokenInfoDto { Id = t.Id, Token = t.Token, CreatedAt = t.CreatedAt, ExpiresAt = t.ExpiresAt, MaxUseCount = t.MaxUseCount, UseCount = t.UseCount, Note = t.Note }) .ToListAsync(); return Ok(ApiResponse>.Success(tokens)); } /// /// 撤销 Token /// [HttpDelete("revoke-token/{tokenId}")] public async Task>> RevokeToken(long tokenId) { var token = await _context.RemoteAccessTokens.FindAsync(tokenId); if (token == null) return Ok(ApiResponse.Fail(404, "Token 不存在")); _context.RemoteAccessTokens.Remove(token); await _context.SaveChangesAsync(); return Ok(ApiResponse.Success(null, "Token 已撤销")); } /// /// 清理过期 Token /// [HttpPost("cleanup-tokens")] public async Task>> CleanupExpiredTokens() { var count = await _context.RemoteAccessTokens .Where(t => t.ExpiresAt < DateTime.UtcNow) .ExecuteDeleteAsync(); return Ok(ApiResponse.Success(new CleanupTokensResponse { DeletedCount = count }, "已清理 " + count + " 个过期 Token")); } /// /// 直接连接(需要输入凭据) /// [HttpPost("connect/{deviceId}")] public async Task>> Connect(long deviceId, [FromBody] RdpCredentials credentials) { var device = await _context.AmtDevices.FindAsync(deviceId); if (device == null) return Ok(ApiResponse.Fail(404, "设备不存在")); var guacToken = await _guacamoleService.GetAuthTokenAsync(); if (string.IsNullOrEmpty(guacToken)) return Ok(ApiResponse.Fail(503, "无法连接到 Guacamole 服务")); var connectionName = $"AMT-{device.IpAddress}"; var connectionId = await _guacamoleService.CreateOrGetConnectionAsync( guacToken, connectionName, device.IpAddress, credentials.Username, credentials.Password); if (string.IsNullOrEmpty(connectionId)) return Ok(ApiResponse.Fail(500, "创建远程连接失败")); var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId); return Ok(ApiResponse.Success(new RemoteDesktopResponse { Success = true, ConnectionUrl = connectionUrl, ConnectionId = connectionId, Token = guacToken })); } /// /// 测试 Guacamole 连接 /// [HttpGet("test")] public async Task>> TestConnection() { var token = await _guacamoleService.GetAuthTokenAsync(); if (string.IsNullOrEmpty(token)) return Ok(ApiResponse.Fail(503, "无法连接到 Guacamole 服务")); return Ok(ApiResponse.Success(new TestConnectionResponse { Success = true, Message = "Guacamole 服务连接正常" })); } private static string GenerateRandomToken() { var bytes = new byte[24]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(bytes); return Convert.ToBase64String(bytes).Replace("+", "-").Replace("/", "_").Replace("=", ""); } } #region Request/Response Models public class GenerateTokenRequest { public long? CredentialId { get; set; } public int? ExpiresInMinutes { get; set; } = 30; public int? MaxUseCount { get; set; } = 1; public string? Note { get; set; } } public class GenerateTokenResponse { public bool Success { get; set; } public string Token { get; set; } = string.Empty; public string AccessUrl { get; set; } = string.Empty; public DateTime ExpiresAt { get; set; } public int MaxUseCount { get; set; } public string? DeviceIp { get; set; } public string? Error { get; set; } } public class ValidateTokenResponse { public bool Valid { get; set; } public string? DeviceIp { get; set; } public DateTime? ExpiresAt { get; set; } public int RemainingUses { get; set; } public string? Error { get; set; } } public class TokenInfoDto { public long Id { get; set; } public string Token { get; set; } = string.Empty; public DateTime CreatedAt { get; set; } public DateTime ExpiresAt { get; set; } public int MaxUseCount { get; set; } public int UseCount { get; set; } public string? Note { get; set; } } public class RdpCredentials { public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; } public class RemoteDesktopResponse { public bool Success { get; set; } public string? ConnectionUrl { get; set; } public string? ConnectionId { get; set; } public string? Token { get; set; } public string? DeviceIp { get; set; } public string? Error { get; set; } } public class CleanupTokensResponse { public int DeletedCount { get; set; } } public class TestConnectionResponse { public bool Success { get; set; } public string Message { get; set; } = string.Empty; } #endregion