feat: 目前完成了订阅功能和 Xray-Core 的更新管理.

This commit is contained in:
2025-01-07 09:50:02 +08:00
parent 78c7db692a
commit 99cb0a03a5
22 changed files with 1153 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
using System.Buffers.Text;
using System.Net;
using System.Text;
using Proxd_man.Config.Outbound;
using Proxd_man.Config.Stream;
namespace Proxd_man.ShareLink;
/// <summary>
/// Shadowsocks 分享链接解析器.
///
/// 遵循了 Shadowsocks 的 <a href="https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme"> SIP002 URI Scheme 规范</a>.
/// </summary>
public class ShadowSocksLinkParser : IShareLinkParser
{
public bool CanParse(string shareLink)
{
return shareLink.StartsWith("ss://", StringComparison.CurrentCultureIgnoreCase);
}
public BaseOutboundSetting Parse(string shareLink)
{
var content = shareLink[5..];
if (content.Length == 0)
{
throw new InvalidShareLinkException("Shadowsocks 分享链接内容为空.");
}
// 按照顺序, 先解析 userInfo.
var userInfoEndAt = content.IndexOf('@');
if (userInfoEndAt == -1)
{
throw new InvalidShareLinkException("Shadowsocks 分享链接中未包含用户信息.");
}
var userInfo = content[..userInfoEndAt];
string userInfoContent;
if (userInfo.Contains(':'))
{
userInfoContent = userInfo.Trim();
}
else
{
if (!Base64.IsValid(userInfo))
{
throw new InvalidShareLinkException("Shadowsocks 分享链接中的用户信息不是有效的 Base64 字符串.");
}
var userInfoBytes = Convert.FromBase64String(userInfo);
userInfoContent = Encoding.UTF8.GetString(userInfoBytes);
}
var userInfoParts = userInfoContent.Split(':', 2);
var method = userInfoParts[0];
var password = userInfoParts[1];
// 开始解析 userInfo 后面的内容.
content = content[(userInfoEndAt + 1)..];
var pathSeparatorAt = content.IndexOf('/');
var fragmentSeparatorAt = content.IndexOf('#');
int hostEndAt;
// 首先检查一下合法性, 遵循这些规则:
// 如果有 pathSeparator, 则检查是否有 "?", 且是否位于 pathSeparator 之后, 否则抛出异常.
// 其次, fragmentSeparator 只能出现在 pathSeparator 之后.
if (pathSeparatorAt != -1)
{
if (content.Contains('?') && content.IndexOf('?') < pathSeparatorAt)
{
throw new InvalidShareLinkException("Shadowsocks 分享链接中的 path 部分不合法.");
}
if (fragmentSeparatorAt != -1 && fragmentSeparatorAt < pathSeparatorAt)
{
throw new InvalidShareLinkException("Shadowsocks 分享链接中的 tag 部分不合法.");
}
hostEndAt = pathSeparatorAt;
} else if (fragmentSeparatorAt != -1)
{
hostEndAt = fragmentSeparatorAt;
}
else
{
hostEndAt = content.Length;
}
if (content[pathSeparatorAt..].Contains("?plugin="))
{
throw new InvalidShareLinkException("Shadowsocks 分享链接暂时不支持 plugin 参数.");
}
var name = fragmentSeparatorAt != -1 ? null :
// 需要 Url 解码
WebUtility.UrlDecode(content[(fragmentSeparatorAt + 1)..].Trim());
var hostContent = content[..hostEndAt];
var hostPortParts = hostContent.Split(':');
switch (hostPortParts.Length)
{
case > 2:
throw new InvalidShareLinkException("Shadowsocks 分享链接中的 host 部分不合法.");
case < 2:
throw new InvalidShareLinkException("Shadowsocks 分享链接中的 host 部分不包含端口.");
}
var host = hostPortParts[0];
if (!int.TryParse(hostPortParts[1], out var port))
{
throw new InvalidShareLinkException("Shadowsocks 分享链接中的端口不是一个数字.");
}
if (port is < 0 or > 65535)
{
throw new InvalidShareLinkException("Shadowsocks 分享链接中的端口超出了端口范围.");
}
return new ShadowSocksOutboundSetting
{
Protocol = OutboundProtocolType.Shadowsocks,
Name = name ?? $"{host}:{port}",
StreamSetting = new RawStreamSetting(),
Address = host,
Port = port,
Method = method,
Password = password
};
}
}
/// <summary>
/// Shadowsocks 分享链接解析器.
///
/// 这个版本使用 Uri 进行解析, 而不是手搓解析过程.
///
/// 遵循了 Shadowsocks 的 <a href="https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme"> SIP002 URI Scheme 规范</a>.
/// </summary>
public class ShadowSocksLinkParserV2 : IShareLinkParser
{
public bool CanParse(string shareLink)
{
return shareLink.StartsWith("ss://", StringComparison.CurrentCultureIgnoreCase);
}
public BaseOutboundSetting Parse(string shareLink)
{
var shareLinkUri = new Uri(shareLink);
var userInfoContent = shareLinkUri.UserInfo;
if (!userInfoContent.Contains(':'))
{
if (!Base64.IsValid(userInfoContent))
{
throw new InvalidShareLinkException("Shadowsocks 分享链接中的用户信息不是有效的 Base64 字符串.");
}
var userInfoBytes = Convert.FromBase64String(userInfoContent);
userInfoContent = Encoding.UTF8.GetString(userInfoBytes);
}
var userInfoParts = userInfoContent.Split(':');
if (userInfoParts.Length != 2)
{
throw new InvalidShareLinkException("Shadowsocks 分享链接中的 userInfo 不合法.");
}
var method = WebUtility.UrlDecode(userInfoParts[0]);
var password = WebUtility.UrlDecode(userInfoParts[1]);
var host = shareLinkUri.Host;
var port = (ushort) shareLinkUri.Port;
return new ShadowSocksOutboundSetting
{
Protocol = OutboundProtocolType.Shadowsocks,
Name = string.IsNullOrEmpty(shareLinkUri.Fragment) ? $"{host}:{port}" : WebUtility.UrlDecode(shareLinkUri.Fragment.Trim()),
StreamSetting = new RawStreamSetting(),
Address = host,
Port = port,
Method = method,
Password = password
};
}
}
public class VMessLinkParser : IShareLinkParser
{
public bool CanParse(string shareLink)
{
return shareLink.StartsWith("vmess://", StringComparison.CurrentCultureIgnoreCase);
}
public BaseOutboundSetting Parse(string shareLink)
{
throw new NotImplementedException();
}
}
+47
View File
@@ -0,0 +1,47 @@
using Proxd_man.Config.Outbound;
namespace Proxd_man.ShareLink;
public class ShareLinkParser
{
private static readonly Lazy<List<IShareLinkParser>> parser = new(() => [
new ShadowSocksLinkParserV2()
]);
public static BaseOutboundSetting Parse(string shareLink)
{
foreach (var p in parser.Value.Where(p => p.CanParse(shareLink)))
{
return p.Parse(shareLink);
}
throw new InvalidShareLinkException("不支持的分享链接: " + shareLink);
}
}
public interface IShareLinkParser
{
/// <summary>
/// 检查该分享链接是否可以被该实现解析.
///
/// 该方法只需要检查是否可以解析, 并不需要检查是否正确.
/// </summary>
/// <param name="shareLink">要尝试解析的出站配置分享链接.</param>
/// <returns>如果可以被实现解析, 则返回 true.</returns>
bool CanParse(string shareLink);
/// <summary>
/// 将分享链接解析为出站配置.
/// </summary>
/// <param name="shareLink">要解析的分享链接.</param>
/// <returns>返回分享链接解析得出的出站配置.</returns>
/// <exception cref="InvalidShareLinkException">当分享链接错误导致无法解析时抛出异常.</exception>
BaseOutboundSetting Parse(string shareLink);
}
public class InvalidShareLinkException(string message, Exception? cause = null) : Exception(message, cause);