LumaTunnel
【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。
关注
0
Fork
0
Star
0
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text.Json;
using LumaTunnel.Shared.Models;
using LumaTunnel.Shared.Protocol;
namespace LumaTunnel.Client.Core.Nodes;
public sealed class NodeApiClient : IDisposable
{
private readonly HttpClient _client;
public NodeApiClient(TimeSpan? timeout = null)
{
_client = new HttpClient(new SocketsHttpHandler
{
UseProxy = false,
AutomaticDecompression = System.Net.DecompressionMethods.All,
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
})
{
Timeout = timeout ?? TimeSpan.FromSeconds(10)
};
}
public async Task<(NodeHealthDto Health, TimeSpan Latency)> GetHealthAsync(Uri nodeUri, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
using var response = await _client.GetAsync(BuildEndpoint(nodeUri, "v1/health"), cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var health = await DeserializeAsync<NodeHealthDto>(response, cancellationToken).ConfigureAwait(false);
return (health, stopwatch.Elapsed);
}
public async Task<DeviceEnrollmentResponse> EnrollAsync(Uri nodeUri, DeviceEnrollmentRequest request, CancellationToken cancellationToken = default)
{
using var response = await _client.PostAsJsonAsync(BuildEndpoint(nodeUri, "v1/device/enroll"), request, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await DeserializeAsync<DeviceEnrollmentResponse>(response, cancellationToken).ConfigureAwait(false);
}
public async Task<NodeStatusDto> GetStatusAsync(Uri nodeUri, NodeStatusRequest request, CancellationToken cancellationToken = default)
{
using var response = await _client.PostAsJsonAsync(BuildEndpoint(nodeUri, "v1/node/status"), request, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await DeserializeAsync<NodeStatusDto>(response, cancellationToken).ConfigureAwait(false);
}
public void Dispose()
{
_client.Dispose();
GC.SuppressFinalize(this);
}
private static Uri BuildEndpoint(Uri nodeUri, string route)
{
var builder = new UriBuilder(nodeUri);
var path = builder.Path.TrimEnd('/');
builder.Path = path.EndsWith("/api", StringComparison.OrdinalIgnoreCase)
? $"{path}/{route}"
: $"{path}/api/{route}";
builder.Query = string.Empty;
builder.Fragment = string.Empty;
return builder.Uri;
}
private static async Task<T> DeserializeAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken) where T : class
{
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<T>(content, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false)
?? throw new InvalidDataException($"The node returned an empty {typeof(T).Name} response.");
}
}
using System.Diagnostics;
using System.Net.Http.Json;
using System.Text.Json;
using LumaTunnel.Shared.Models;
using LumaTunnel.Shared.Protocol;
namespace LumaTunnel.Client.Core.Nodes;
public sealed class NodeApiClient : IDisposable
{
private readonly HttpClient _client;
public NodeApiClient(TimeSpan? timeout = null)
{
_client = new HttpClient(new SocketsHttpHandler
{
UseProxy = false,
AutomaticDecompression = System.Net.DecompressionMethods.All,
PooledConnectionLifetime = TimeSpan.FromMinutes(5)
})
{
Timeout = timeout ?? TimeSpan.FromSeconds(10)
};
}
public async Task<(NodeHealthDto Health, TimeSpan Latency)> GetHealthAsync(Uri nodeUri, CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
using var response = await _client.GetAsync(BuildEndpoint(nodeUri, "v1/health"), cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var health = await DeserializeAsync<NodeHealthDto>(response, cancellationToken).ConfigureAwait(false);
return (health, stopwatch.Elapsed);
}
public async Task<DeviceEnrollmentResponse> EnrollAsync(Uri nodeUri, DeviceEnrollmentRequest request, CancellationToken cancellationToken = default)
{
using var response = await _client.PostAsJsonAsync(BuildEndpoint(nodeUri, "v1/device/enroll"), request, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await DeserializeAsync<DeviceEnrollmentResponse>(response, cancellationToken).ConfigureAwait(false);
}
public async Task<NodeStatusDto> GetStatusAsync(Uri nodeUri, NodeStatusRequest request, CancellationToken cancellationToken = default)
{
using var response = await _client.PostAsJsonAsync(BuildEndpoint(nodeUri, "v1/node/status"), request, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await DeserializeAsync<NodeStatusDto>(response, cancellationToken).ConfigureAwait(false);
}
public void Dispose()
{
_client.Dispose();
GC.SuppressFinalize(this);
}
private static Uri BuildEndpoint(Uri nodeUri, string route)
{
var builder = new UriBuilder(nodeUri);
var path = builder.Path.TrimEnd('/');
builder.Path = path.EndsWith("/api", StringComparison.OrdinalIgnoreCase)
? $"{path}/{route}"
: $"{path}/api/{route}";
builder.Query = string.Empty;
builder.Fragment = string.Empty;
return builder.Uri;
}
private static async Task<T> DeserializeAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken) where T : class
{
await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<T>(content, TunnelMessageSerializer.Options, cancellationToken).ConfigureAwait(false)
?? throw new InvalidDataException($"The node returned an empty {typeof(T).Name} response.");
}
}