ProxyController.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. using dodohold.core;
  2. using Microsoft.AspNetCore.Mvc;
  3. using molilian.core;
  4. using System.Net;
  5. using System.Text.Json;
  6. namespace molilian.api.Controllers
  7. {
  8. [ApiController]
  9. [MyAuthorize("admin")]
  10. [Route("api/[controller]/[action]")]
  11. public class ProxyController : ControllerBase
  12. {
  13. private const string ProxyCheckRequestUrl = "http://cip.cc";
  14. private const string ProxyCheckUserAgent = "curl/8.7.1";
  15. readonly IAuthorizationProvider provider = new AdminProvider();
  16. protected IHttpContextAccessor _accessor;
  17. public ProxyController(IHttpContextAccessor accessor)
  18. {
  19. _accessor = accessor;
  20. }
  21. [HttpGet]
  22. public ActionResult get()
  23. {
  24. return new APIResult(new
  25. {
  26. data = BuildProxyNodeListResult(1, 20, true, string.Empty, string.Empty)
  27. });
  28. }
  29. [HttpPost]
  30. public ActionResult list([FromBody] JsonElement form)
  31. {
  32. int page = form.Read("current", 1);
  33. int size = form.Read("pageSize", 20);
  34. bool getTotal = form.Read("getTotal", true);
  35. string keyword = form.Read("keyword", string.Empty);
  36. string status = form.Read("status", string.Empty);
  37. return new APIResult(new
  38. {
  39. data = BuildProxyNodeListResult(page, size, getTotal, keyword, status)
  40. });
  41. }
  42. private static dynamic BuildProxyNodeListResult(
  43. int page,
  44. int size,
  45. bool getTotal,
  46. string keyword,
  47. string status
  48. )
  49. {
  50. page = page <= 0 ? 1 : page;
  51. size = size <= 0 ? 20 : size;
  52. string filter = string.Empty;
  53. string likeKeyword = string.Empty;
  54. IEnumerable<string> matchedNodeNames = Array.Empty<string>();
  55. bool? statusValue = null;
  56. if (!string.IsNullOrWhiteSpace(status))
  57. {
  58. status = status.Trim().ToLowerInvariant();
  59. if (status == "enabled")
  60. {
  61. statusValue = true;
  62. filter += " AND status=@statusValue";
  63. }
  64. else if (status == "disabled")
  65. {
  66. statusValue = false;
  67. filter += " AND status=@statusValue";
  68. }
  69. }
  70. if (!string.IsNullOrWhiteSpace(keyword))
  71. {
  72. likeKeyword = $"%{keyword.Trim()}%";
  73. matchedNodeNames = FindMatchedNodeNames(likeKeyword).ToArray();
  74. 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";
  75. if (matchedNodeNames.Any())
  76. {
  77. filter += " OR name IN @matchedNodeNames OR nodeName IN @matchedNodeNames";
  78. }
  79. filter += ")";
  80. }
  81. filter = filter.StringTrimStart(" AND ");
  82. var result = new DBContext.Table("proxy_nodes")
  83. .Where(filter, new { statusValue, likeKeyword, matchedNodeNames })
  84. .Page(size, page)
  85. .Order("status DESC, name ASC")
  86. .PageList<ProxyNodeListItemDTO>(getTotal);
  87. List<ProxyNodeAccountSourceDTO> tkAccounts = LoadProxyNodeAccountSources("tk_pool");
  88. List<ProxyNodeAccountSourceDTO> jdAccounts = LoadProxyNodeAccountSources("jd_pool");
  89. List<ProxyNodeAccountSourceDTO> pddAccounts = LoadProxyNodeAccountSources("pdd_pool");
  90. foreach (var node in result.List)
  91. {
  92. var accounts = BuildUsageAccounts(node, "tk", tkAccounts)
  93. .Concat(BuildUsageAccounts(node, "jd", jdAccounts))
  94. .Concat(BuildUsageAccounts(node, "pdd", pddAccounts))
  95. .OrderByDescending(item => item.status)
  96. .ThenBy(item => item.platform)
  97. .ThenBy(item => item.name)
  98. .ToList();
  99. node.accountCount = accounts.Count;
  100. node.onlineAccountCount = accounts.Count(item => item.status);
  101. node.offlineAccountCount = accounts.Count(item => !item.status);
  102. node.accounts = accounts;
  103. }
  104. return result;
  105. }
  106. [HttpPost]
  107. public async Task<ActionResult> update([FromBody] JsonElement form)
  108. {
  109. var clientIp = _accessor.HttpContext.GetUserIp();
  110. var token = provider.Get(_accessor.HttpContext);
  111. int id = form.Read<int>("id", 0);
  112. string name = form.Read<string>("name", string.Empty);
  113. string val = form.Read<string>("val", string.Empty);
  114. if (id == 0)
  115. {
  116. return new APIResult(new
  117. {
  118. data = new { success = false, msg = "更新失败,请检查输入参数" }
  119. });
  120. }
  121. int result;
  122. switch (name)
  123. {
  124. case "status":
  125. bool bVal = "true".Equals(val, StringComparison.OrdinalIgnoreCase) || "1".Equals(val);
  126. result = new DBContext.Table("proxy_nodes")
  127. .Add(name, bVal)
  128. .Add("last_time", DateTime.Now)
  129. .Where("id=@id", new { id })
  130. .Update();
  131. break;
  132. default:
  133. return new APIResult(new
  134. {
  135. data = new { success = false, msg = "更新失败,未授权操作" }
  136. });
  137. }
  138. bool success = result > 0;
  139. if (success)
  140. {
  141. OperationLogCore.LogOperation(token.AccessKey, clientIp, $"proxy_nodes:{id}:{name}", string.Empty, val);
  142. ProxyNodesCore.Refresh();
  143. await EndPointCore.NotifyReload(true);
  144. }
  145. return new APIResult(new
  146. {
  147. data = new { success, msg = success ? "更新成功" : "更新失败,请检查输入信息" }
  148. });
  149. }
  150. [HttpGet]
  151. public async Task<ActionResult> check(int id, string target = "internal")
  152. {
  153. var proxyNode = new DBContext.Table("proxy_nodes").Get<ProxyNodesDTO>("id=@id", new { id });
  154. if (proxyNode == null)
  155. {
  156. return new APIResult(new
  157. {
  158. data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" }
  159. });
  160. }
  161. target = string.IsNullOrWhiteSpace(target) ? "internal" : target.Trim().ToLowerInvariant();
  162. if (target is not ("internal" or "external"))
  163. {
  164. return new APIResult(new
  165. {
  166. data = new { success = false, msg = "target 仅支持 internal 或 external" }
  167. });
  168. }
  169. string externalProxyAddress = BuildExternalProxyAddress(proxyNode.id);
  170. var data = new ProxyNodeCheckResponseDTO
  171. {
  172. success = false,
  173. msg = "未执行检测",
  174. };
  175. if (target == "internal")
  176. {
  177. var internalResult = await CheckProxyAsync(
  178. proxyNode.server,
  179. proxyNode.username,
  180. proxyNode.password
  181. );
  182. data.success = internalResult.success;
  183. data.msg = internalResult.success ? "内网代理检测成功" : "内网代理检测失败";
  184. data.check = new ProxyCheckChannelDTO
  185. {
  186. address = proxyNode.server,
  187. success = internalResult.success,
  188. result = internalResult.result,
  189. };
  190. }
  191. else
  192. {
  193. var externalResult = await CheckProxyAsync(
  194. BuildProxyUri(externalProxyAddress, proxyNode.type),
  195. proxyNode.username,
  196. proxyNode.password
  197. );
  198. data.success = externalResult.success;
  199. data.msg = externalResult.success ? "外网代理检测成功" : "外网代理检测失败";
  200. data.check = new ProxyCheckChannelDTO
  201. {
  202. address = externalProxyAddress,
  203. success = externalResult.success,
  204. result = externalResult.result,
  205. };
  206. }
  207. return new APIResult(new
  208. {
  209. data
  210. });
  211. }
  212. [HttpGet]
  213. public async Task<ActionResult> ChangePublicIpByName(string nodeName)
  214. {
  215. var proxyNode = new DBContext.Table("proxy_nodes")
  216. .Get<dynamic>("(nodeName=@nodeName OR name=@nodeName)", new { nodeName });
  217. if (proxyNode == null)
  218. {
  219. return new APIResult(new
  220. {
  221. data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" }
  222. });
  223. }
  224. int aliyunId = proxyNode.aliyun_id;
  225. string proxyServer = proxyNode.server;
  226. var account = new DBContext.Table("aliyun_pool").Get<AliyunPoolDTO>(aliyunId);
  227. if (account == null)
  228. {
  229. return new APIResult(new
  230. {
  231. data = new { success = false, msg = "没有匹配的 aliyun_pool 记录" }
  232. });
  233. }
  234. var uri = new Uri(proxyServer);
  235. string privateIp = uri.Host;
  236. AliyunCore core = new(account);
  237. var (success, oldIp, newIp) = await core.ChangePublicIpAsync(privateIp);
  238. return new APIResult(new
  239. {
  240. data = new
  241. {
  242. success,
  243. msg = success ? $"更换公网 IP 成功:{oldIp} -> {newIp}" : "更换公网 IP 失败"
  244. }
  245. });
  246. }
  247. [HttpGet]
  248. public async Task<ActionResult> ChangePublicIp(int id)
  249. {
  250. id = id >= 20000 ? id - 20000 : id;
  251. var proxyNode = new DBContext.Table("proxy_nodes").Get<dynamic>(id);
  252. if (proxyNode == null)
  253. {
  254. return new APIResult(new
  255. {
  256. data = new { success = false, msg = "没有匹配的 proxy_nodes 记录" }
  257. });
  258. }
  259. int aliyunId = proxyNode.aliyun_id;
  260. string proxyServer = proxyNode.server;
  261. var account = new DBContext.Table("aliyun_pool").Get<AliyunPoolDTO>(aliyunId);
  262. if (account == null)
  263. {
  264. return new APIResult(new
  265. {
  266. data = new { success = false, msg = "没有匹配的 aliyun_pool 记录" }
  267. });
  268. }
  269. var uri = new Uri(proxyServer);
  270. string privateIp = uri.Host;
  271. AliyunCore core = new(account);
  272. var (success, oldIp, newIp) = await core.ChangePublicIpAsync(privateIp);
  273. return new APIResult(new
  274. {
  275. data = new
  276. {
  277. success,
  278. msg = success ? $"更换公网 IP 成功:{oldIp} -> {newIp}" : "更换公网 IP 失败"
  279. }
  280. });
  281. }
  282. private static IEnumerable<ProxyNodeUsageAccountDTO> BuildUsageAccounts(
  283. ProxyNodeListItemDTO node,
  284. string platform,
  285. IEnumerable<ProxyNodeAccountSourceDTO> accounts
  286. )
  287. {
  288. var nodeNames = GetProxyNodeMatchNames(node).ToArray();
  289. if (!nodeNames.Any())
  290. {
  291. return Enumerable.Empty<ProxyNodeUsageAccountDTO>();
  292. }
  293. return accounts
  294. .Where(account => MatchNodeName(account.nodeName, nodeNames))
  295. .Select(account => new ProxyNodeUsageAccountDTO
  296. {
  297. id = account.id,
  298. platform = platform,
  299. name = account.name,
  300. company = account.company,
  301. nodeName = account.nodeName,
  302. status = account.status,
  303. });
  304. }
  305. private static bool MatchNodeName(
  306. string accountNodes,
  307. IEnumerable<string> nodeNames
  308. )
  309. {
  310. var nodeNameSet = nodeNames
  311. .Where(item => !string.IsNullOrWhiteSpace(item))
  312. .Select(item => item.Trim())
  313. .ToHashSet(StringComparer.OrdinalIgnoreCase);
  314. if (nodeNameSet.Count == 0)
  315. {
  316. return false;
  317. }
  318. return SplitNodeNames(accountNodes)
  319. .Any(item => nodeNameSet.Contains(item));
  320. }
  321. private static IEnumerable<string> GetProxyNodeMatchNames(ProxyNodeListItemDTO node)
  322. {
  323. return new[] { node.nodeName, node.name }
  324. .Where(item => !string.IsNullOrWhiteSpace(item))
  325. .Select(item => item.Trim())
  326. .Distinct(StringComparer.OrdinalIgnoreCase);
  327. }
  328. private static IEnumerable<string> FindMatchedNodeNames(string keyword)
  329. {
  330. if (string.IsNullOrWhiteSpace(keyword))
  331. {
  332. return Array.Empty<string>();
  333. }
  334. const string accountFilter = "name LIKE @keyword OR company LIKE @keyword OR nodename LIKE @keyword";
  335. List<ProxyNodeNameSourceDTO> allNodes = new();
  336. allNodes.AddRange(LoadProxyNodeNameSources("tk_pool", accountFilter, keyword));
  337. allNodes.AddRange(LoadProxyNodeNameSources("jd_pool", accountFilter, keyword));
  338. allNodes.AddRange(LoadProxyNodeNameSources("pdd_pool", accountFilter, keyword));
  339. return allNodes
  340. .SelectMany(item => SplitNodeNames(item.nodeName))
  341. .Distinct(StringComparer.OrdinalIgnoreCase)
  342. .ToList();
  343. }
  344. private static List<ProxyNodeAccountSourceDTO> LoadProxyNodeAccountSources(string tableName)
  345. {
  346. return new DBContext.Table(tableName)
  347. .Fields("id, name, company, status, nodename AS nodeName")
  348. .Select<ProxyNodeAccountSourceDTO>()?
  349. .ToList() ?? new List<ProxyNodeAccountSourceDTO>();
  350. }
  351. private static List<ProxyNodeNameSourceDTO> LoadProxyNodeNameSources(
  352. string tableName,
  353. string filter,
  354. string keyword
  355. )
  356. {
  357. return new DBContext.Table(tableName)
  358. .Fields("nodename AS nodeName")
  359. .Where(filter, new { keyword })
  360. .Select<ProxyNodeNameSourceDTO>()?
  361. .ToList() ?? new List<ProxyNodeNameSourceDTO>();
  362. }
  363. private static IEnumerable<string> SplitNodeNames(string accountNodes)
  364. {
  365. if (string.IsNullOrWhiteSpace(accountNodes))
  366. {
  367. return Array.Empty<string>();
  368. }
  369. return accountNodes
  370. .Replace(',', ',')
  371. .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
  372. }
  373. private static string BuildExternalProxyAddress(int id)
  374. {
  375. return $"bjapi.molilian.com:20{id:D3}";
  376. }
  377. private static string BuildProxyUri(string address, string type)
  378. {
  379. if (string.IsNullOrWhiteSpace(address))
  380. {
  381. return string.Empty;
  382. }
  383. address = address.Trim();
  384. if (address.Contains("://", StringComparison.Ordinal) &&
  385. Uri.TryCreate(address, UriKind.Absolute, out var absoluteUri))
  386. {
  387. return absoluteUri.AbsoluteUri;
  388. }
  389. string scheme = string.IsNullOrWhiteSpace(type) ? "http" : type.Trim().ToLowerInvariant();
  390. if (scheme is not ("http" or "https" or "socks4" or "socks4a" or "socks5"))
  391. {
  392. scheme = "http";
  393. }
  394. return $"{scheme}://{address}";
  395. }
  396. private static async Task<(bool success, string result)> CheckProxyAsync(
  397. string proxyAddress,
  398. string username,
  399. string password
  400. )
  401. {
  402. try
  403. {
  404. string proxyUri = BuildProxyUri(proxyAddress, "http");
  405. WebClientUtility client = new()
  406. {
  407. Timeout = TimeSpan.FromSeconds(3),
  408. UserAgent = ProxyCheckUserAgent,
  409. };
  410. if (string.IsNullOrWhiteSpace(proxyUri))
  411. {
  412. return (false, "代理地址为空");
  413. }
  414. client.Proxy = new WebProxy
  415. {
  416. Address = new Uri(proxyUri),
  417. Credentials = string.IsNullOrWhiteSpace(username) && string.IsNullOrWhiteSpace(password)
  418. ? null
  419. : new NetworkCredential(username, password)
  420. };
  421. var response = await client.RequestAsync(ProxyCheckRequestUrl);
  422. string body = response.Body()?.Trim() ?? string.Empty;
  423. if (response.Successed)
  424. {
  425. return (true, string.IsNullOrWhiteSpace(body) ? "代理可用,但目标站点无返回内容" : body);
  426. }
  427. if (response.ResponseException != null)
  428. {
  429. return (false, response.ResponseException.GetBaseException().Message);
  430. }
  431. if (response.ResponseMessage != null)
  432. {
  433. string message = $"HTTP {(int)response.ResponseMessage.StatusCode}";
  434. if (!string.IsNullOrWhiteSpace(body))
  435. {
  436. message += $" - {body}";
  437. }
  438. return (false, message);
  439. }
  440. if (!string.IsNullOrWhiteSpace(body))
  441. {
  442. return (false, body);
  443. }
  444. return (false, "请求失败,但未返回异常或响应内容");
  445. }
  446. catch (TaskCanceledException)
  447. {
  448. return (false, "请求超时(3s)");
  449. }
  450. catch (Exception ex)
  451. {
  452. return (false, ex.GetBaseException().Message);
  453. }
  454. }
  455. }
  456. public class ProxyNodeAccountSourceDTO
  457. {
  458. public int id { get; set; }
  459. public string name { get; set; } = string.Empty;
  460. public string company { get; set; } = string.Empty;
  461. public bool status { get; set; }
  462. public string nodeName { get; set; } = string.Empty;
  463. }
  464. public class ProxyNodeNameSourceDTO
  465. {
  466. public string nodeName { get; set; } = string.Empty;
  467. }
  468. public class ProxyNodeUsageAccountDTO
  469. {
  470. public int id { get; set; }
  471. public string platform { get; set; } = string.Empty;
  472. public string name { get; set; } = string.Empty;
  473. public string company { get; set; } = string.Empty;
  474. public string nodeName { get; set; } = string.Empty;
  475. public bool status { get; set; }
  476. }
  477. public class ProxyNodeListItemDTO
  478. {
  479. public int id { get; set; }
  480. public bool status { get; set; }
  481. public string name { get; set; } = string.Empty;
  482. public string nodeName { get; set; } = string.Empty;
  483. public string description { get; set; } = string.Empty;
  484. public string end_point { get; set; } = string.Empty;
  485. public string type { get; set; } = string.Empty;
  486. public string server { get; set; } = string.Empty;
  487. public string username { get; set; } = string.Empty;
  488. public DateTime create_time { get; set; }
  489. public DateTime last_time { get; set; }
  490. public int accountCount { get; set; }
  491. public int onlineAccountCount { get; set; }
  492. public int offlineAccountCount { get; set; }
  493. public List<ProxyNodeUsageAccountDTO> accounts { get; set; } = new();
  494. }
  495. public class ProxyCheckChannelDTO
  496. {
  497. public string address { get; set; } = string.Empty;
  498. public bool success { get; set; }
  499. public string result { get; set; } = string.Empty;
  500. }
  501. public class ProxyNodeCheckResponseDTO
  502. {
  503. public bool success { get; set; }
  504. public string msg { get; set; } = string.Empty;
  505. public ProxyCheckChannelDTO? check { get; set; }
  506. }
  507. }