using dodohold.core; using Microsoft.AspNetCore.Mvc; using molilian.core; using System.Net; using System.Text.Json; namespace molilian.api.Controllers { [ApiController] [MyAuthorize("admin")] [Route("api/[controller]/[action]")] public class ProxyController : ControllerBase { private const string ProxyCheckRequestUrl = "http://cip.cc"; private const string ProxyCheckUserAgent = "curl/8.7.1"; readonly IAuthorizationProvider provider = new AdminProvider(); protected IHttpContextAccessor _accessor; public ProxyController(IHttpContextAccessor accessor) { _accessor = accessor; } [HttpGet] public ActionResult get() { return new APIResult(new { data = BuildProxyNodeListResult(1, 20, true, string.Empty, string.Empty) }); } [HttpPost] public ActionResult list([FromBody] JsonElement form) { int page = form.Read("current", 1); int size = form.Read("pageSize", 20); bool getTotal = form.Read("getTotal", true); string keyword = form.Read("keyword", string.Empty); string status = form.Read("status", string.Empty); return new APIResult(new { data = BuildProxyNodeListResult(page, size, getTotal, keyword, status) }); } private static dynamic BuildProxyNodeListResult( int page, int size, bool getTotal, string keyword, string status ) { page = page <= 0 ? 1 : page; size = size <= 0 ? 20 : size; string filter = string.Empty; string likeKeyword = string.Empty; IEnumerable matchedNodeNames = Array.Empty(); bool? statusValue = null; if (!string.IsNullOrWhiteSpace(status)) { status = status.Trim().ToLowerInvariant(); if (status == "enabled") { statusValue = true; filter += " AND status=@statusValue"; } else if (status == "disabled") { statusValue = false; filter += " AND status=@statusValue"; } } if (!string.IsNullOrWhiteSpace(keyword)) { likeKeyword = $"%{keyword.Trim()}%"; matchedNodeNames = FindMatchedNodeNames(likeKeyword).ToArray(); filter += " AND (name LIKE @likeKeyword OR nodeName LIKE @likeKeyword OR description LIKE @likeKeyword OR server LIKE @likeKeyword OR end_point LIKE @likeKeyword OR type LIKE @likeKeyword"; if (matchedNodeNames.Any()) { filter += " OR name IN @matchedNodeNames OR nodeName IN @matchedNodeNames"; } filter += ")"; } filter = filter.StringTrimStart(" AND "); var result = new DBContext.Table("proxy_nodes") .Where(filter, new { statusValue, likeKeyword, matchedNodeNames }) .Page(size, page) .Order("status DESC, name ASC") .PageList(getTotal); List tkAccounts = LoadProxyNodeAccountSources("tk_pool"); List jdAccounts = LoadProxyNodeAccountSources("jd_pool"); List pddAccounts = LoadProxyNodeAccountSources("pdd_pool"); foreach (var node in result.List) { var accounts = BuildUsageAccounts(node, "tk", tkAccounts) .Concat(BuildUsageAccounts(node, "jd", jdAccounts)) .Concat(BuildUsageAccounts(node, "pdd", pddAccounts)) .OrderByDescending(item => item.status) .ThenBy(item => item.platform) .ThenBy(item => item.name) .ToList(); node.accountCount = accounts.Count; node.onlineAccountCount = accounts.Count(item => item.status); node.offlineAccountCount = accounts.Count(item => !item.status); node.accounts = accounts; } return result; } [HttpPost] public async Task update([FromBody] JsonElement form) { var clientIp = _accessor.HttpContext.GetUserIp(); var token = provider.Get(_accessor.HttpContext); int id = form.Read("id", 0); string name = form.Read("name", string.Empty); string val = form.Read("val", string.Empty); if (id == 0) { return new APIResult(new { data = new { success = false, msg = "更新失败,请检查输入参数" } }); } int result; switch (name) { case "status": bool bVal = "true".Equals(val, StringComparison.OrdinalIgnoreCase) || "1".Equals(val); result = new DBContext.Table("proxy_nodes") .Add(name, bVal) .Add("last_time", DateTime.Now) .Where("id=@id", new { id }) .Update(); break; default: return new APIResult(new { data = new { success = false, msg = "更新失败,未授权操作" } }); } bool success = result > 0; if (success) { OperationLogCore.LogOperation(token.AccessKey, clientIp, $"proxy_nodes:{id}:{name}", string.Empty, val); ProxyNodesCore.Refresh(); await EndPointCore.NotifyReload(true); } return new APIResult(new { data = new { success, msg = success ? "更新成功" : "更新失败,请检查输入信息" } }); } [HttpGet] public async Task check(int id, string target = "internal") { var proxyNode = new DBContext.Table("proxy_nodes").Get("id=@id", new { id }); if (proxyNode == null) { return new APIResult(new { data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" } }); } target = string.IsNullOrWhiteSpace(target) ? "internal" : target.Trim().ToLowerInvariant(); if (target is not ("internal" or "external")) { return new APIResult(new { data = new { success = false, msg = "target 仅支持 internal 或 external" } }); } string externalProxyAddress = BuildExternalProxyAddress(proxyNode.id); var data = new ProxyNodeCheckResponseDTO { success = false, msg = "未执行检测", }; if (target == "internal") { var internalResult = await CheckProxyAsync( proxyNode.server, proxyNode.username, proxyNode.password ); data.success = internalResult.success; data.msg = internalResult.success ? "内网代理检测成功" : "内网代理检测失败"; data.check = new ProxyCheckChannelDTO { address = proxyNode.server, success = internalResult.success, result = internalResult.result, }; } else { var externalResult = await CheckProxyAsync( BuildProxyUri(externalProxyAddress, proxyNode.type), proxyNode.username, proxyNode.password ); data.success = externalResult.success; data.msg = externalResult.success ? "外网代理检测成功" : "外网代理检测失败"; data.check = new ProxyCheckChannelDTO { address = externalProxyAddress, success = externalResult.success, result = externalResult.result, }; } return new APIResult(new { data }); } [HttpGet] public async Task ChangePublicIpByName(string nodeName) { var proxyNode = new DBContext.Table("proxy_nodes") .Get("(nodeName=@nodeName OR name=@nodeName)", new { nodeName }); if (proxyNode == null) { return new APIResult(new { data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" } }); } int aliyunId = proxyNode.aliyun_id; string proxyServer = proxyNode.server; var account = new DBContext.Table("aliyun_pool").Get(aliyunId); if (account == null) { return new APIResult(new { data = new { success = false, msg = "没有匹配的 aliyun_pool 记录" } }); } var uri = new Uri(proxyServer); string privateIp = uri.Host; AliyunCore core = new(account); var (success, oldIp, newIp) = await core.ChangePublicIpAsync(privateIp); return new APIResult(new { data = new { success, msg = success ? $"更换公网 IP 成功:{oldIp} -> {newIp}" : "更换公网 IP 失败" } }); } [HttpGet] public async Task ChangePublicIp(int id) { id = id >= 20000 ? id - 20000 : id; var proxyNode = new DBContext.Table("proxy_nodes").Get(id); if (proxyNode == null) { return new APIResult(new { data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" } }); } int aliyunId = proxyNode.aliyun_id; string proxyServer = proxyNode.server; var account = new DBContext.Table("aliyun_pool").Get(aliyunId); if (account == null) { return new APIResult(new { data = new { success = false, msg = "没有匹配的 aliyun_pool 记录" } }); } var uri = new Uri(proxyServer); string privateIp = uri.Host; AliyunCore core = new(account); var (success, oldIp, newIp) = await core.ChangePublicIpAsync(privateIp); return new APIResult(new { data = new { success, msg = success ? $"更换公网 IP 成功:{oldIp} -> {newIp}" : "更换公网 IP 失败" } }); } private static IEnumerable BuildUsageAccounts( ProxyNodeListItemDTO node, string platform, IEnumerable accounts ) { var nodeNames = GetProxyNodeMatchNames(node).ToArray(); if (!nodeNames.Any()) { return Enumerable.Empty(); } return accounts .Where(account => MatchNodeName(account.nodeName, nodeNames)) .Select(account => new ProxyNodeUsageAccountDTO { id = account.id, platform = platform, name = account.name, company = account.company, nodeName = account.nodeName, status = account.status, }); } private static bool MatchNodeName( string accountNodes, IEnumerable nodeNames ) { var nodeNameSet = nodeNames .Where(item => !string.IsNullOrWhiteSpace(item)) .Select(item => item.Trim()) .ToHashSet(StringComparer.OrdinalIgnoreCase); if (nodeNameSet.Count == 0) { return false; } return SplitNodeNames(accountNodes) .Any(item => nodeNameSet.Contains(item)); } private static IEnumerable GetProxyNodeMatchNames(ProxyNodeListItemDTO node) { return new[] { node.nodeName, node.name } .Where(item => !string.IsNullOrWhiteSpace(item)) .Select(item => item.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase); } private static IEnumerable FindMatchedNodeNames(string keyword) { if (string.IsNullOrWhiteSpace(keyword)) { return Array.Empty(); } const string accountFilter = "name LIKE @keyword OR company LIKE @keyword OR nodename LIKE @keyword"; List allNodes = new(); allNodes.AddRange(LoadProxyNodeNameSources("tk_pool", accountFilter, keyword)); allNodes.AddRange(LoadProxyNodeNameSources("jd_pool", accountFilter, keyword)); allNodes.AddRange(LoadProxyNodeNameSources("pdd_pool", accountFilter, keyword)); return allNodes .SelectMany(item => SplitNodeNames(item.nodeName)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } private static List LoadProxyNodeAccountSources(string tableName) { return new DBContext.Table(tableName) .Fields("id, name, company, status, nodename AS nodeName") .Select()? .ToList() ?? new List(); } private static List LoadProxyNodeNameSources( string tableName, string filter, string keyword ) { return new DBContext.Table(tableName) .Fields("nodename AS nodeName") .Where(filter, new { keyword }) .Select()? .ToList() ?? new List(); } private static IEnumerable SplitNodeNames(string accountNodes) { if (string.IsNullOrWhiteSpace(accountNodes)) { return Array.Empty(); } return accountNodes .Replace(',', ',') .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } private static string BuildExternalProxyAddress(int id) { return $"bjapi.molilian.com:20{id:D3}"; } private static string BuildProxyUri(string address, string type) { if (string.IsNullOrWhiteSpace(address)) { return string.Empty; } address = address.Trim(); if (address.Contains("://", StringComparison.Ordinal) && Uri.TryCreate(address, UriKind.Absolute, out var absoluteUri)) { return absoluteUri.AbsoluteUri; } string scheme = string.IsNullOrWhiteSpace(type) ? "http" : type.Trim().ToLowerInvariant(); if (scheme is not ("http" or "https" or "socks4" or "socks4a" or "socks5")) { scheme = "http"; } return $"{scheme}://{address}"; } private static async Task<(bool success, string result)> CheckProxyAsync( string proxyAddress, string username, string password ) { try { string proxyUri = BuildProxyUri(proxyAddress, "http"); WebClientUtility client = new() { Timeout = TimeSpan.FromSeconds(3), UserAgent = ProxyCheckUserAgent, }; if (string.IsNullOrWhiteSpace(proxyUri)) { return (false, "代理地址为空"); } client.Proxy = new WebProxy { Address = new Uri(proxyUri), Credentials = string.IsNullOrWhiteSpace(username) && string.IsNullOrWhiteSpace(password) ? null : new NetworkCredential(username, password) }; var response = await client.RequestAsync(ProxyCheckRequestUrl); string body = response.Body()?.Trim() ?? string.Empty; if (response.Successed) { return (true, string.IsNullOrWhiteSpace(body) ? "代理可用,但目标站点无返回内容" : body); } if (response.ResponseException != null) { return (false, response.ResponseException.GetBaseException().Message); } if (response.ResponseMessage != null) { string message = $"HTTP {(int)response.ResponseMessage.StatusCode}"; if (!string.IsNullOrWhiteSpace(body)) { message += $" - {body}"; } return (false, message); } if (!string.IsNullOrWhiteSpace(body)) { return (false, body); } return (false, "请求失败,但未返回异常或响应内容"); } catch (TaskCanceledException) { return (false, "请求超时(3s)"); } catch (Exception ex) { return (false, ex.GetBaseException().Message); } } } public class ProxyNodeAccountSourceDTO { public int id { get; set; } public string name { get; set; } = string.Empty; public string company { get; set; } = string.Empty; public bool status { get; set; } public string nodeName { get; set; } = string.Empty; } public class ProxyNodeNameSourceDTO { public string nodeName { get; set; } = string.Empty; } public class ProxyNodeUsageAccountDTO { public int id { get; set; } public string platform { get; set; } = string.Empty; public string name { get; set; } = string.Empty; public string company { get; set; } = string.Empty; public string nodeName { get; set; } = string.Empty; public bool status { get; set; } } public class ProxyNodeListItemDTO { public int id { get; set; } public bool status { get; set; } public string name { get; set; } = string.Empty; public string nodeName { get; set; } = string.Empty; public string description { get; set; } = string.Empty; public string end_point { get; set; } = string.Empty; public string type { get; set; } = string.Empty; public string server { get; set; } = string.Empty; public string username { get; set; } = string.Empty; public DateTime create_time { get; set; } public DateTime last_time { get; set; } public int accountCount { get; set; } public int onlineAccountCount { get; set; } public int offlineAccountCount { get; set; } public List accounts { get; set; } = new(); } public class ProxyCheckChannelDTO { public string address { get; set; } = string.Empty; public bool success { get; set; } public string result { get; set; } = string.Empty; } public class ProxyNodeCheckResponseDTO { public bool success { get; set; } public string msg { get; set; } = string.Empty; public ProxyCheckChannelDTO? check { get; set; } } }