242 lines
8.6 KiB
C#
242 lines
8.6 KiB
C#
using System.Text;
|
||
using System.Text.Json;
|
||
|
||
namespace AmtScanner.Api.Services;
|
||
|
||
public interface IGuacamoleService
|
||
{
|
||
Task<string?> GetAuthTokenAsync();
|
||
Task<string?> CreateOrGetConnectionAsync(string token, string connectionName, string hostname, string username, string password);
|
||
Task<string?> GetConnectionUrlAsync(string token, string connectionId);
|
||
}
|
||
|
||
public class GuacamoleService : IGuacamoleService
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ILogger<GuacamoleService> _logger;
|
||
private readonly IConfiguration _configuration;
|
||
|
||
private string GuacamoleBaseUrl => _configuration["Guacamole:BaseUrl"] ?? "http://localhost:8080/guacamole";
|
||
private string GuacamoleUsername => _configuration["Guacamole:Username"] ?? "guacadmin";
|
||
private string GuacamolePassword => _configuration["Guacamole:Password"] ?? "guacadmin";
|
||
|
||
public GuacamoleService(HttpClient httpClient, ILogger<GuacamoleService> logger, IConfiguration configuration)
|
||
{
|
||
_httpClient = httpClient;
|
||
_logger = logger;
|
||
_configuration = configuration;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取 Guacamole 认证 Token
|
||
/// </summary>
|
||
public async Task<string?> GetAuthTokenAsync()
|
||
{
|
||
try
|
||
{
|
||
var content = new FormUrlEncodedContent(new[]
|
||
{
|
||
new KeyValuePair<string, string>("username", GuacamoleUsername),
|
||
new KeyValuePair<string, string>("password", GuacamolePassword)
|
||
});
|
||
|
||
var response = await _httpClient.PostAsync($"{GuacamoleBaseUrl}/api/tokens", content);
|
||
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
_logger.LogError("Failed to get Guacamole token: {Status}", response.StatusCode);
|
||
return null;
|
||
}
|
||
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
using var doc = JsonDocument.Parse(json);
|
||
|
||
if (doc.RootElement.TryGetProperty("authToken", out var tokenElement))
|
||
{
|
||
return tokenElement.GetString();
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Error getting Guacamole token");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建或获取已存在的连接
|
||
/// </summary>
|
||
public async Task<string?> CreateOrGetConnectionAsync(string token, string connectionName, string hostname, string username, string password)
|
||
{
|
||
try
|
||
{
|
||
// 先查找是否已存在同名连接
|
||
var existingId = await FindConnectionByNameAsync(token, connectionName);
|
||
if (existingId != null)
|
||
{
|
||
// 更新连接参数
|
||
await UpdateConnectionAsync(token, existingId, connectionName, hostname, username, password);
|
||
return existingId;
|
||
}
|
||
|
||
// 创建新连接
|
||
var connectionData = new Dictionary<string, object>
|
||
{
|
||
["name"] = connectionName,
|
||
["parentIdentifier"] = "ROOT",
|
||
["protocol"] = "rdp",
|
||
["parameters"] = new Dictionary<string, string>
|
||
{
|
||
["hostname"] = hostname,
|
||
["port"] = "3389",
|
||
["username"] = username,
|
||
["password"] = password,
|
||
["security"] = "any",
|
||
["ignore-cert"] = "true",
|
||
["resize-method"] = "display-update",
|
||
["enable-wallpaper"] = "true",
|
||
["enable-theming"] = "true",
|
||
["enable-font-smoothing"] = "true",
|
||
["enable-full-window-drag"] = "true",
|
||
["enable-desktop-composition"] = "true",
|
||
["enable-menu-animations"] = "true",
|
||
["disable-bitmap-caching"] = "false",
|
||
["disable-offscreen-caching"] = "false",
|
||
["disable-glyph-caching"] = "false"
|
||
},
|
||
["attributes"] = new Dictionary<string, string>()
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(connectionData);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
|
||
var response = await _httpClient.PostAsync(
|
||
$"{GuacamoleBaseUrl}/api/session/data/mysql/connections?token={token}",
|
||
content);
|
||
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
var error = await response.Content.ReadAsStringAsync();
|
||
_logger.LogError("Failed to create connection: {Status} - {Error}", response.StatusCode, error);
|
||
return null;
|
||
}
|
||
|
||
var responseJson = await response.Content.ReadAsStringAsync();
|
||
using var doc = JsonDocument.Parse(responseJson);
|
||
|
||
if (doc.RootElement.TryGetProperty("identifier", out var idElement))
|
||
{
|
||
return idElement.GetString();
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Error creating Guacamole connection");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 查找已存在的连接
|
||
/// </summary>
|
||
private async Task<string?> FindConnectionByNameAsync(string token, string connectionName)
|
||
{
|
||
try
|
||
{
|
||
var response = await _httpClient.GetAsync(
|
||
$"{GuacamoleBaseUrl}/api/session/data/mysql/connections?token={token}");
|
||
|
||
if (!response.IsSuccessStatusCode)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var json = await response.Content.ReadAsStringAsync();
|
||
using var doc = JsonDocument.Parse(json);
|
||
|
||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||
{
|
||
if (prop.Value.TryGetProperty("name", out var nameElement) &&
|
||
nameElement.GetString() == connectionName)
|
||
{
|
||
return prop.Name;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Error finding connection by name");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新连接参数
|
||
/// </summary>
|
||
private async Task UpdateConnectionAsync(string token, string connectionId, string connectionName, string hostname, string username, string password)
|
||
{
|
||
try
|
||
{
|
||
var connectionData = new Dictionary<string, object>
|
||
{
|
||
["name"] = connectionName,
|
||
["parentIdentifier"] = "ROOT",
|
||
["protocol"] = "rdp",
|
||
["identifier"] = connectionId,
|
||
["parameters"] = new Dictionary<string, string>
|
||
{
|
||
["hostname"] = hostname,
|
||
["port"] = "3389",
|
||
["username"] = username,
|
||
["password"] = password,
|
||
["security"] = "any",
|
||
["ignore-cert"] = "true",
|
||
["resize-method"] = "display-update",
|
||
["enable-wallpaper"] = "true",
|
||
["enable-theming"] = "true",
|
||
["enable-font-smoothing"] = "true",
|
||
["enable-full-window-drag"] = "true",
|
||
["enable-desktop-composition"] = "true",
|
||
["enable-menu-animations"] = "true"
|
||
},
|
||
["attributes"] = new Dictionary<string, string>()
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(connectionData);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
|
||
var request = new HttpRequestMessage(HttpMethod.Put,
|
||
$"{GuacamoleBaseUrl}/api/session/data/mysql/connections/{connectionId}?token={token}")
|
||
{
|
||
Content = content
|
||
};
|
||
|
||
await _httpClient.SendAsync(request);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Error updating connection");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取连接 URL(用于 iframe 嵌入)
|
||
/// </summary>
|
||
public async Task<string?> GetConnectionUrlAsync(string token, string connectionId)
|
||
{
|
||
// Guacamole 客户端 URL 格式
|
||
// 连接标识符需要 Base64 编码: connectionId + "\0" + "c" + "\0" + "mysql"
|
||
var connectionIdentifier = $"{connectionId}\0c\0mysql";
|
||
var encodedId = Convert.ToBase64String(Encoding.UTF8.GetBytes(connectionIdentifier));
|
||
|
||
return $"{GuacamoleBaseUrl}/#/client/{encodedId}?token={token}";
|
||
}
|
||
}
|