using System.Globalization; using System.Net; namespace MoShiro.Protocol; /// /// 单个 Agent 任务的目标:http 是 http(s)://主机[:端口][/路径],其余 Kind 是 主机[:端口](IP 或域名,IPv6 带端口时用方括号)。 /// Backend 只检查格式,不做 DNS 解析;解析在 Agent 上进行,每个解析出的地址都要在 allowed_target_cidrs 里。 /// public static class TargetHost { public const string HttpKind = "http_request"; /// 解析目标; 为 0 表示没有写端口, 只在 http(s) URL 时有值。 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; } /// 按 Kind 检查并规范化:http 必须是 http(s) URL,其余 Kind 必须是主机[:端口];不合格返回 null。 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 == '-')); } }