105 lines
3.6 KiB
C#
105 lines
3.6 KiB
C#
using System.Globalization;
|
||
using System.Net;
|
||
|
||
namespace MoShiro.Protocol;
|
||
|
||
/// <summary>
|
||
/// 单个 Agent 任务的目标:<c>http</c> 是 <c>http(s)://主机[:端口][/路径]</c>,其余 Kind 是 <c>主机[:端口]</c>(IP 或域名,IPv6 带端口时用方括号)。
|
||
/// Backend 只检查格式,不做 DNS 解析;解析在 Agent 上进行,每个解析出的地址都要在 <c>allowed_target_cidrs</c> 里。
|
||
/// </summary>
|
||
public static class TargetHost
|
||
{
|
||
public const string HttpKind = "http_request";
|
||
|
||
/// <summary>解析目标;<paramref name="port"/> 为 0 表示没有写端口,<paramref name="url"/> 只在 http(s) URL 时有值。</summary>
|
||
public static bool TryParse(string? target, out string host, out int port, out Uri? url)
|
||
{
|
||
host = "";
|
||
port = 0;
|
||
url = null;
|
||
if (string.IsNullOrWhiteSpace(target) || target.Length > 2048 || target.Any(char.IsWhiteSpace))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (target.Contains("://", StringComparison.Ordinal))
|
||
{
|
||
if (!Uri.TryCreate(target, UriKind.Absolute, out Uri? uri) || uri.Scheme is not ("http" or "https") || uri.UserInfo.Length > 0 || !ValidHost(uri.IdnHost))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
url = uri;
|
||
host = uri.IdnHost.Trim('[', ']');
|
||
port = uri.IsDefaultPort ? 0 : uri.Port;
|
||
return true;
|
||
}
|
||
|
||
// IPAddress.TryParse 会把「[v6]:端口」的端口丢掉,带方括号的先按端点解析
|
||
if (!target.StartsWith('[') && IPAddress.TryParse(target, out IPAddress? ip))
|
||
{
|
||
host = ip.ToString();
|
||
return true;
|
||
}
|
||
|
||
if (IPEndPoint.TryParse(target, out IPEndPoint? endpoint))
|
||
{
|
||
if (endpoint.Port is 0)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
host = endpoint.Address.ToString();
|
||
port = endpoint.Port;
|
||
return true;
|
||
}
|
||
|
||
int colon = target.LastIndexOf(':');
|
||
string name = colon < 0 ? target : target[..colon];
|
||
if (colon >= 0 && (!int.TryParse(target.AsSpan(colon + 1), NumberStyles.None, CultureInfo.InvariantCulture, out port) || port is < 1 or > 65535))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!ValidHost(name))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
host = name.ToLowerInvariant();
|
||
return true;
|
||
}
|
||
|
||
/// <summary>按 Kind 检查并规范化:<c>http</c> 必须是 http(s) URL,其余 Kind 必须是主机[:端口];不合格返回 null。</summary>
|
||
public static string? Normalize(string kindId, string? target)
|
||
{
|
||
if (!TryParse(target, out string host, out int port, out Uri? url) || (url is not null) != (kindId == HttpKind))
|
||
{
|
||
return null;
|
||
}
|
||
|
||
if (url is not null)
|
||
{
|
||
return url.AbsoluteUri;
|
||
}
|
||
|
||
string shown = IPAddress.TryParse(host, out IPAddress? ip) && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 && port != 0 ? "[" + host + "]" : host;
|
||
return port == 0 ? host : shown + ":" + port.ToString(CultureInfo.InvariantCulture);
|
||
}
|
||
|
||
private static bool ValidHost(string name)
|
||
{
|
||
if (IPAddress.TryParse(name.Trim('[', ']'), out _))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (name.Length is 0 or > 253 || name[^1] == '-' || name[0] is '-' or '.')
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return name.TrimEnd('.').Split('.').All(label => label.Length is > 0 and <= 63 && label.All(c => char.IsAsciiLetterOrDigit(c) || c == '-'));
|
||
}
|
||
}
|