CommonHelper.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. using AngleSharp;
  2. using AngleSharp.Css.Dom;
  3. using AngleSharp.Dom;
  4. using AutoMapper;
  5. using Hangfire;
  6. using IP2Region;
  7. using Masuit.MyBlogs.Core.Common.Mails;
  8. using Masuit.MyBlogs.Core.Infrastructure;
  9. using Masuit.Tools;
  10. using Masuit.Tools.Media;
  11. using MaxMind.GeoIP2;
  12. using MaxMind.GeoIP2.Exceptions;
  13. using MaxMind.GeoIP2.Model;
  14. using MaxMind.GeoIP2.Responses;
  15. using Polly;
  16. using System.Collections.Concurrent;
  17. using System.Drawing;
  18. using System.Net;
  19. using System.Net.Sockets;
  20. using System.Text;
  21. using Masuit.Tools.Systems;
  22. using TimeZoneConverter;
  23. namespace Masuit.MyBlogs.Core.Common
  24. {
  25. /// <summary>
  26. /// 公共类库
  27. /// </summary>
  28. public static class CommonHelper
  29. {
  30. private static readonly FileSystemWatcher FileSystemWatcher = new(AppContext.BaseDirectory + "App_Data", "*.txt")
  31. {
  32. IncludeSubdirectories = true,
  33. EnableRaisingEvents = true,
  34. NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size
  35. };
  36. static CommonHelper()
  37. {
  38. Init();
  39. FileSystemWatcher.Changed += (_, _) => Init();
  40. }
  41. private static void Init()
  42. {
  43. string ReadFile(string s)
  44. {
  45. return Policy<string>.Handle<IOException>().WaitAndRetry(5, i => TimeSpan.FromSeconds(i)).Execute(() =>
  46. {
  47. using var fs = File.Open(s, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
  48. using var sr = new StreamReader(fs, Encoding.UTF8);
  49. return sr.ReadToEnd();
  50. });
  51. }
  52. BanRegex = ReadFile(Path.Combine(AppContext.BaseDirectory + "App_Data", "ban.txt"));
  53. ModRegex = ReadFile(Path.Combine(AppContext.BaseDirectory + "App_Data", "mod.txt"));
  54. DenyIP = ReadFile(Path.Combine(AppContext.BaseDirectory + "App_Data", "denyip.txt"));
  55. var lines = File.Open(Path.Combine(AppContext.BaseDirectory + "App_Data", "DenyIPRange.txt"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite).ReadAllLines(Encoding.UTF8);
  56. DenyIPRange = new Dictionary<string, string>();
  57. foreach (var line in lines)
  58. {
  59. try
  60. {
  61. var strs = line.Split(' ');
  62. DenyIPRange[strs[0]] = strs[1];
  63. }
  64. catch (IndexOutOfRangeException)
  65. {
  66. }
  67. }
  68. IPWhiteList = ReadFile(Path.Combine(AppContext.BaseDirectory + "App_Data", "whitelist.txt")).Split(',', ',').ToList();
  69. }
  70. /// <summary>
  71. /// 敏感词
  72. /// </summary>
  73. public static string BanRegex { get; set; }
  74. /// <summary>
  75. /// 审核词
  76. /// </summary>
  77. public static string ModRegex { get; set; }
  78. /// <summary>
  79. /// 全局禁止IP
  80. /// </summary>
  81. public static string DenyIP { get; set; }
  82. /// <summary>
  83. /// ip白名单
  84. /// </summary>
  85. public static List<string> IPWhiteList { get; set; }
  86. /// <summary>
  87. /// 每IP错误的次数统计
  88. /// </summary>
  89. public static ConcurrentDictionary<string, int> IPErrorTimes { get; set; } = new();
  90. /// <summary>
  91. /// 系统设定
  92. /// </summary>
  93. public static ConcurrentDictionary<string, string> SystemSettings { get; set; } = new();
  94. /// <summary>
  95. /// IP黑名单地址段
  96. /// </summary>
  97. public static Dictionary<string, string> DenyIPRange { get; set; }
  98. /// <summary>
  99. /// 判断IP地址是否被黑名单
  100. /// </summary>
  101. /// <param name="ip"></param>
  102. /// <returns></returns>
  103. public static bool IsDenyIpAddress(this string ip)
  104. {
  105. if (IPWhiteList.Contains(ip))
  106. {
  107. return false;
  108. }
  109. return DenyIP.Contains(ip) || DenyIPRange.AsParallel().Any(kv => kv.Key.StartsWith(ip.Split('.')[0]) && ip.IpAddressInRange(kv.Key, kv.Value));
  110. }
  111. private static readonly DbSearcher IPSearcher = new(Path.Combine(AppContext.BaseDirectory + "App_Data", "ip2region.db"));
  112. public static readonly DatabaseReader MaxmindReader = new(Path.Combine(AppContext.BaseDirectory + "App_Data", "GeoLite2-City.mmdb"));
  113. private static readonly DatabaseReader MaxmindAsnReader = new(Path.Combine(AppContext.BaseDirectory + "App_Data", "GeoLite2-ASN.mmdb"));
  114. /// <summary>
  115. /// 是否是代理ip
  116. /// </summary>
  117. /// <param name="ip"></param>
  118. /// <param name="cancellationToken"></param>
  119. /// <returns></returns>
  120. public static async Task<bool> IsProxy(this IPAddress ip, CancellationToken cancellationToken = default)
  121. {
  122. var httpClient = Startup.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient();
  123. httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36 Edg/92.0.902.62");
  124. return await httpClient.GetStringAsync("https://ipinfo.io/" + ip, cancellationToken).ContinueWith(t =>
  125. {
  126. if (t.IsCompletedSuccessfully)
  127. {
  128. var ctx = BrowsingContext.New(Configuration.Default);
  129. var doc = ctx.OpenAsync(res => res.Content(t.Result)).Result;
  130. var isAnycast = doc.DocumentElement.QuerySelectorAll(".title").Where(e => e.TextContent.Contains("Anycast")).Select(e => e.Parent).Any(n => n.TextContent.Contains("True"));
  131. var isproxy = doc.DocumentElement.QuerySelectorAll("#block-privacy img").Any(e => e.OuterHtml.Contains("right"));
  132. return isAnycast || isproxy;
  133. }
  134. return false;
  135. });
  136. }
  137. public static AsnResponse GetIPAsn(this IPAddress ip)
  138. {
  139. if (ip.IsPrivateIP())
  140. {
  141. return new AsnResponse();
  142. }
  143. return Policy<AsnResponse>.Handle<AddressNotFoundException>().Fallback(new AsnResponse()).Execute(() => MaxmindAsnReader.Asn(ip));
  144. }
  145. private static CityResponse GetCityResp(IPAddress ip)
  146. {
  147. return Policy<CityResponse>.Handle<AddressNotFoundException>().Fallback(new CityResponse()).Execute(() => MaxmindReader.City(ip));
  148. }
  149. public static IPLocation GetIPLocation(this string ips)
  150. {
  151. return GetIPLocation(IPAddress.Parse(ips));
  152. }
  153. public static IPLocation GetIPLocation(this IPAddress ip)
  154. {
  155. if (ip.IsPrivateIP())
  156. {
  157. return new IPLocation("内网", null, null, "内网IP", null);
  158. }
  159. var city = GetCityResp(ip);
  160. var asn = GetIPAsn(ip);
  161. var countryName = city.Country.Names.GetValueOrDefault("zh-CN") ?? city.Country.Name;
  162. var cityName = city.City.Names.GetValueOrDefault("zh-CN") ?? city.City.Name;
  163. switch (ip.AddressFamily)
  164. {
  165. case AddressFamily.InterNetworkV6 when ip.IsIPv4MappedToIPv6:
  166. ip = ip.MapToIPv4();
  167. goto case AddressFamily.InterNetwork;
  168. case AddressFamily.InterNetwork:
  169. var parts = IPSearcher.MemorySearch(ip.ToString())?.Region.Split('|');
  170. if (parts != null)
  171. {
  172. var network = parts[^1] == "0" ? asn.AutonomousSystemOrganization : parts[^1] + "/" + asn.AutonomousSystemOrganization;
  173. parts[0] = parts[0] != "0" ? parts[0] : countryName;
  174. parts[3] = parts[3] != "0" ? parts[3] : cityName;
  175. return new IPLocation(parts[0], parts[2], parts[3], network?.Trim('/'), asn.AutonomousSystemNumber)
  176. {
  177. Address2 = countryName + cityName,
  178. Coodinate = city.Location
  179. };
  180. }
  181. goto default;
  182. default:
  183. return new IPLocation(countryName, null, cityName, asn.AutonomousSystemOrganization, asn.AutonomousSystemNumber)
  184. {
  185. Coodinate = city.Location
  186. };
  187. }
  188. }
  189. /// <summary>
  190. /// 获取ip所在时区
  191. /// </summary>
  192. /// <param name="ip"></param>
  193. /// <returns></returns>
  194. public static string GetClientTimeZone(this IPAddress ip)
  195. {
  196. if (ip.IsPrivateIP())
  197. {
  198. return "Asia/Shanghai";
  199. }
  200. return GetCityResp(ip).Location.TimeZone ?? "Asia/Shanghai";
  201. }
  202. /// <summary>
  203. /// 类型映射
  204. /// </summary>
  205. /// <typeparam name="T"></typeparam>
  206. /// <param name="source"></param>
  207. /// <returns></returns>
  208. public static T Mapper<T>(this object source) where T : class => Startup.ServiceProvider.GetRequiredService<IMapper>().Map<T>(source);
  209. /// <summary>
  210. /// 发送邮件
  211. /// </summary>
  212. /// <param name="title">标题</param>
  213. /// <param name="content">内容</param>
  214. /// <param name="tos">收件人</param>
  215. /// <param name="clientip"></param>
  216. [AutomaticRetry(Attempts = 1, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
  217. public static void SendMail(string title, string content, string tos, string clientip)
  218. {
  219. Startup.ServiceProvider.GetRequiredService<IMailSender>().Send(title, content, tos);
  220. RedisHelper.SAdd($"Email:{DateTime.Now:yyyyMMdd}", new { title, content, tos, time = DateTime.Now, clientip });
  221. RedisHelper.Expire($"Email:{DateTime.Now:yyyyMMdd}", 86400);
  222. }
  223. /// <summary>
  224. /// 清理html的img标签的除src之外的其他属性
  225. /// </summary>
  226. /// <param name="html"></param>
  227. /// <returns></returns>
  228. public static async Task<string> ClearImgAttributes(this string html)
  229. {
  230. var context = BrowsingContext.New(Configuration.Default);
  231. var doc = await context.OpenAsync(req => req.Content(html));
  232. var nodes = doc.DocumentElement.GetElementsByTagName("img");
  233. var allows = new[] { "src", "data-original", "width", "style", "class" };
  234. foreach (var node in nodes)
  235. {
  236. for (var i = 0; i < node.Attributes.Length; i++)
  237. {
  238. if (allows.Contains(node.Attributes[i].Name))
  239. {
  240. continue;
  241. }
  242. node.RemoveAttribute(node.Attributes[i].Name);
  243. }
  244. }
  245. return doc.Body.InnerHtml;
  246. }
  247. /// <summary>
  248. /// 将html的img标签懒加载
  249. /// </summary>
  250. /// <param name="html"></param>
  251. /// <param name="title"></param>
  252. /// <returns></returns>
  253. public static async Task<string> ReplaceImgAttribute(this string html, string title)
  254. {
  255. var context = BrowsingContext.New(Configuration.Default);
  256. var doc = await context.OpenAsync(req => req.Content(html));
  257. var nodes = doc.DocumentElement.GetElementsByTagName("img");
  258. foreach (var node in nodes)
  259. {
  260. if (node.HasAttribute("src"))
  261. {
  262. string src = node.Attributes["src"].Value;
  263. node.RemoveAttribute("src");
  264. node.SetAttribute("data-src", src);
  265. node.SetAttribute("decoding", "async");
  266. node.SetAttribute("class", node.Attributes["class"]?.Value + " lazyload");
  267. node.SetAttribute("loading", "lazy");
  268. node.SetAttribute("alt", SystemSettings["Title"]);
  269. node.SetAttribute("title", title);
  270. }
  271. }
  272. var elements = doc.DocumentElement.QuerySelectorAll("p,br");
  273. var els = elements.OrderByRandom().Take(Math.Max(elements.Length / 5, 3)).ToList();
  274. var href = "https://" + SystemSettings["Domain"].Split('|').OrderByRandom().FirstOrDefault();
  275. foreach (var el in els)
  276. {
  277. var a = doc.CreateElement("a");
  278. a.SetAttribute("href", href);
  279. a.SetAttribute("target", "_blank");
  280. a.SetAttribute("title", SystemSettings["Title"]);
  281. a.SetStyle("position: absolute;color: transparent;z-index: -1");
  282. a.TextContent = SystemSettings["Title"] + SystemSettings["Domain"];
  283. el.InsertAfter(a);
  284. var a2 = doc.CreateElement("a");
  285. a2.SetAttribute("href", "/craw/" + SnowFlake.NewId);
  286. a2.SetStyle("position: absolute;color: transparent;z-index: -1");
  287. a2.TextContent = title;
  288. a.InsertAfter(a2);
  289. }
  290. return doc.Body.InnerHtml;
  291. }
  292. /// <summary>
  293. /// 获取文章摘要
  294. /// </summary>
  295. /// <param name="html"></param>
  296. /// <param name="length">截取长度</param>
  297. /// <param name="min">摘要最少字数</param>
  298. /// <returns></returns>
  299. public static Task<string> GetSummary(this string html, int length = 150, int min = 10)
  300. {
  301. var context = BrowsingContext.New(Configuration.Default);
  302. return context.OpenAsync(req => req.Content(html)).ContinueWith(t =>
  303. {
  304. var summary = t.Result.DocumentElement.GetElementsByTagName("p").FirstOrDefault(n => n.TextContent.Length > min)?.TextContent ?? "没有摘要";
  305. return summary.Length > length ? summary[..length] + "..." : summary;
  306. });
  307. }
  308. public static string TrimQuery(this string path)
  309. {
  310. return path.Split('&').Where(s => s.Split('=', StringSplitOptions.RemoveEmptyEntries).Length == 2).Join("&");
  311. }
  312. /// <summary>
  313. /// 添加水印
  314. /// </summary>
  315. /// <param name="stream"></param>
  316. /// <returns></returns>
  317. public static Stream AddWatermark(this Stream stream)
  318. {
  319. if (!string.IsNullOrEmpty(SystemSettings.GetOrAdd("Watermark", string.Empty)))
  320. {
  321. try
  322. {
  323. var watermarker = new ImageWatermarker(stream)
  324. {
  325. SkipWatermarkForSmallImages = true,
  326. SmallImagePixelsThreshold = 90000
  327. };
  328. return watermarker.AddWatermark(SystemSettings["Watermark"], Color.LightGray, WatermarkPosition.BottomRight, 30);
  329. }
  330. catch
  331. {
  332. //
  333. }
  334. }
  335. return stream;
  336. }
  337. /// <summary>
  338. /// 转换时区
  339. /// </summary>
  340. /// <param name="time">UTC时间</param>
  341. /// <param name="zone">时区id</param>
  342. /// <returns></returns>
  343. public static DateTime ToTimeZone(this in DateTime time, string zone)
  344. {
  345. return TimeZoneInfo.ConvertTime(time, TZConvert.GetTimeZoneInfo(zone));
  346. }
  347. /// <summary>
  348. /// 转换时区
  349. /// </summary>
  350. /// <param name="time">UTC时间</param>
  351. /// <param name="zone">时区id</param>
  352. /// <param name="format">时间格式字符串</param>
  353. /// <returns></returns>
  354. public static string ToTimeZoneF(this in DateTime time, string zone, string format = "yyyy-MM-dd HH:mm:ss")
  355. {
  356. return ToTimeZone(time, zone).ToString(format);
  357. }
  358. /// <summary>
  359. /// 随机排序
  360. /// </summary>
  361. /// <typeparam name="T"></typeparam>
  362. /// <param name="source"></param>
  363. /// <returns></returns>
  364. public static IOrderedQueryable<T> OrderByRandom<T>(this IQueryable<T> source)
  365. {
  366. return source.OrderBy(_ => DataContext.Random());
  367. }
  368. }
  369. public class IPLocation
  370. {
  371. public IPLocation(string country, string province, string city, string isp, long? asn)
  372. {
  373. Country = country?.Trim('0');
  374. Province = province?.Trim('0');
  375. City = city?.Trim('0');
  376. ISP = isp;
  377. ASN = asn;
  378. }
  379. public string Country { get; set; }
  380. public string Province { get; set; }
  381. public string City { get; set; }
  382. public string ISP { get; set; }
  383. public long? ASN { get; set; }
  384. public string Address => new[] { Country, Province, City }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("");
  385. public string Address2 { get; set; }
  386. public string Network => ASN.HasValue ? ISP + "(AS" + ASN + ")" : ISP;
  387. public Location Coodinate { get; set; }
  388. public override string ToString()
  389. {
  390. string address = Address;
  391. string network = Network;
  392. if (string.IsNullOrWhiteSpace(address))
  393. {
  394. address = "未知地区";
  395. }
  396. if (string.IsNullOrWhiteSpace(network))
  397. {
  398. network = "未知网络";
  399. }
  400. return new[] { address, Address2, network }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
  401. }
  402. public static implicit operator string(IPLocation entry)
  403. {
  404. return entry.ToString();
  405. }
  406. public void Deconstruct(out string location, out string network, out string info)
  407. {
  408. location = new[] { Address, Address2 }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
  409. network = Network;
  410. info = ToString();
  411. }
  412. public bool Contains(string s)
  413. {
  414. return ToString().Contains(s, StringComparison.CurrentCultureIgnoreCase);
  415. }
  416. }
  417. }