288 lines
10 KiB
C#
288 lines
10 KiB
C#
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<RemoteDesktopController> _logger;
|
|
|
|
public RemoteDesktopController(
|
|
IGuacamoleService guacamoleService,
|
|
AppDbContext context,
|
|
ILogger<RemoteDesktopController> logger)
|
|
{
|
|
_guacamoleService = guacamoleService;
|
|
_context = context;
|
|
_logger = logger;
|
|
}
|
|
|
|
[HttpPost("generate-token/{deviceId}")]
|
|
public async Task<ActionResult<GenerateTokenResponse>> GenerateToken(
|
|
long deviceId,
|
|
[FromBody] GenerateTokenRequest request)
|
|
{
|
|
var device = await _context.AmtDevices.FindAsync(deviceId);
|
|
if (device == null)
|
|
return NotFound(new { error = "设备不存在" });
|
|
|
|
WindowsCredential? credential = request.CredentialId.HasValue
|
|
? await _context.WindowsCredentials.FindAsync(request.CredentialId.Value)
|
|
: await _context.WindowsCredentials.FirstOrDefaultAsync(c => c.IsDefault);
|
|
|
|
if (credential == null)
|
|
return BadRequest(new { error = "请先配置 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}";
|
|
_logger.LogInformation("Generated remote access token for device {Ip}, expires at {ExpiresAt}",
|
|
device.IpAddress, expiresAt);
|
|
|
|
return Ok(new GenerateTokenResponse
|
|
{
|
|
Success = true,
|
|
Token = token,
|
|
AccessUrl = $"{baseUrl}/remote/{token}",
|
|
ExpiresAt = expiresAt,
|
|
MaxUseCount = accessToken.MaxUseCount,
|
|
DeviceIp = device.IpAddress
|
|
});
|
|
}
|
|
|
|
[HttpGet("connect-by-token/{token}")]
|
|
public async Task<ActionResult<RemoteDesktopResponse>> 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 NotFound(new { error = "无效的访问链接" });
|
|
|
|
if (!accessToken.IsValid())
|
|
return BadRequest(new { error = "访问链接已过期或已达到使用次数上限" });
|
|
|
|
if (accessToken.Device == null || accessToken.WindowsCredential == null)
|
|
return BadRequest(new { error = "设备或凭据信息不完整" });
|
|
|
|
accessToken.UseCount++;
|
|
accessToken.UsedAt = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
|
|
var guacToken = await _guacamoleService.GetAuthTokenAsync();
|
|
if (string.IsNullOrEmpty(guacToken))
|
|
return StatusCode(503, new { error = "无法连接到 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 StatusCode(500, new { error = "创建远程连接失败" });
|
|
|
|
var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId);
|
|
|
|
return Ok(new RemoteDesktopResponse
|
|
{
|
|
Success = true,
|
|
ConnectionUrl = connectionUrl,
|
|
ConnectionId = connectionId,
|
|
Token = guacToken,
|
|
DeviceIp = accessToken.Device.IpAddress
|
|
});
|
|
}
|
|
|
|
[HttpGet("validate-token/{token}")]
|
|
public async Task<ActionResult<ValidateTokenResponse>> ValidateToken(string token)
|
|
{
|
|
var accessToken = await _context.RemoteAccessTokens
|
|
.Include(t => t.Device)
|
|
.FirstOrDefaultAsync(t => t.Token == token);
|
|
|
|
if (accessToken == null)
|
|
return Ok(new ValidateTokenResponse { Valid = false, Error = "无效的访问链接" });
|
|
|
|
if (!accessToken.IsValid())
|
|
return Ok(new ValidateTokenResponse { Valid = false, Error = "访问链接已过期或已达到使用次数上限" });
|
|
|
|
return Ok(new ValidateTokenResponse
|
|
{
|
|
Valid = true,
|
|
DeviceIp = accessToken.Device?.IpAddress,
|
|
ExpiresAt = accessToken.ExpiresAt,
|
|
RemainingUses = accessToken.MaxUseCount > 0 ? accessToken.MaxUseCount - accessToken.UseCount : -1
|
|
});
|
|
}
|
|
|
|
[HttpGet("list-tokens/{deviceId}")]
|
|
public async Task<ActionResult<IEnumerable<TokenInfoDto>>> 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(tokens);
|
|
}
|
|
|
|
[HttpDelete("revoke-token/{tokenId}")]
|
|
public async Task<ActionResult> RevokeToken(long tokenId)
|
|
{
|
|
var token = await _context.RemoteAccessTokens.FindAsync(tokenId);
|
|
if (token == null)
|
|
return NotFound(new { error = "Token 不存在" });
|
|
|
|
_context.RemoteAccessTokens.Remove(token);
|
|
await _context.SaveChangesAsync();
|
|
return Ok(new { success = true });
|
|
}
|
|
|
|
[HttpPost("cleanup-tokens")]
|
|
public async Task<ActionResult> CleanupExpiredTokens()
|
|
{
|
|
var count = await _context.RemoteAccessTokens
|
|
.Where(t => t.ExpiresAt < DateTime.UtcNow)
|
|
.ExecuteDeleteAsync();
|
|
return Ok(new { success = true, deletedCount = count });
|
|
}
|
|
|
|
[HttpPost("connect/{deviceId}")]
|
|
public async Task<ActionResult<RemoteDesktopResponse>> Connect(long deviceId, [FromBody] RdpCredentials credentials)
|
|
{
|
|
var device = await _context.AmtDevices.FindAsync(deviceId);
|
|
if (device == null)
|
|
return NotFound(new { error = "设备不存在" });
|
|
|
|
var guacToken = await _guacamoleService.GetAuthTokenAsync();
|
|
if (string.IsNullOrEmpty(guacToken))
|
|
return StatusCode(503, new { error = "无法连接到 Guacamole 服务" });
|
|
|
|
var connectionName = $"AMT-{device.IpAddress}";
|
|
var connectionId = await _guacamoleService.CreateOrGetConnectionAsync(
|
|
guacToken, connectionName, device.IpAddress, credentials.Username, credentials.Password);
|
|
|
|
if (string.IsNullOrEmpty(connectionId))
|
|
return StatusCode(500, new { error = "创建远程连接失败" });
|
|
|
|
var connectionUrl = await _guacamoleService.GetConnectionUrlAsync(guacToken, connectionId);
|
|
|
|
return Ok(new RemoteDesktopResponse
|
|
{
|
|
Success = true,
|
|
ConnectionUrl = connectionUrl,
|
|
ConnectionId = connectionId,
|
|
Token = guacToken
|
|
});
|
|
}
|
|
|
|
[HttpGet("test")]
|
|
public async Task<ActionResult> TestConnection()
|
|
{
|
|
var token = await _guacamoleService.GetAuthTokenAsync();
|
|
if (string.IsNullOrEmpty(token))
|
|
return StatusCode(503, new { success = false, error = "无法连接到 Guacamole 服务" });
|
|
return Ok(new { 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; }
|
|
}
|
|
|
|
#endregion
|