XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

LumaTunnel

【WinUI】LumaTunnel 是一个面向个人多设备的 Windows 代理系统。客户端在本机提供 HTTP/HTTPS CONNECT 与 SOCKS5 TCP 代理,并通过一个受信任 TLS 证书保护的 WSS 会话,将多个 TCP 流复用到自建 Windows Server 节点。

公开
关注 0 Fork 0 Star 0
UTF-8
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.");
    }
}