using AngleSharp; using AngleSharp.Css.Dom; using AngleSharp.Dom; using AutoMapper; using Hangfire; using IP2Region; using Masuit.MyBlogs.Core.Common.Mails; using Masuit.MyBlogs.Core.Infrastructure; using Masuit.Tools; using Masuit.Tools.Media; using MaxMind.GeoIP2; using MaxMind.GeoIP2.Exceptions; using MaxMind.GeoIP2.Model; using MaxMind.GeoIP2.Responses; using Polly; using System.Collections.Concurrent; using System.Drawing; using System.Net; using System.Net.Sockets; using System.Text; using Masuit.Tools.Systems; using TimeZoneConverter; namespace Masuit.MyBlogs.Core.Common { /// /// 公共类库 /// public static class CommonHelper { private static readonly FileSystemWatcher FileSystemWatcher = new(AppContext.BaseDirectory + "App_Data", "*.txt") { IncludeSubdirectories = true, EnableRaisingEvents = true, NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size }; static CommonHelper() { Init(); FileSystemWatcher.Changed += (_, _) => Init(); } private static void Init() { string ReadFile(string s) { return Policy.Handle().WaitAndRetry(5, i => TimeSpan.FromSeconds(i)).Execute(() => { using var fs = File.Open(s, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var sr = new StreamReader(fs, Encoding.UTF8); return sr.ReadToEnd(); }); } BanRegex = ReadFile(Path.Combine(AppContext.BaseDirectory + "App_Data", "ban.txt")); ModRegex = ReadFile(Path.Combine(AppContext.BaseDirectory + "App_Data", "mod.txt")); DenyIP = ReadFile(Path.Combine(AppContext.BaseDirectory + "App_Data", "denyip.txt")); var lines = File.Open(Path.Combine(AppContext.BaseDirectory + "App_Data", "DenyIPRange.txt"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite).ReadAllLines(Encoding.UTF8); DenyIPRange = new Dictionary(); foreach (var line in lines) { try { var strs = line.Split(' '); DenyIPRange[strs[0]] = strs[1]; } catch (IndexOutOfRangeException) { } } IPWhiteList = ReadFile(Path.Combine(AppContext.BaseDirectory + "App_Data", "whitelist.txt")).Split(',', ',').ToList(); } /// /// 敏感词 /// public static string BanRegex { get; set; } /// /// 审核词 /// public static string ModRegex { get; set; } /// /// 全局禁止IP /// public static string DenyIP { get; set; } /// /// ip白名单 /// public static List IPWhiteList { get; set; } /// /// 每IP错误的次数统计 /// public static ConcurrentDictionary IPErrorTimes { get; set; } = new(); /// /// 系统设定 /// public static ConcurrentDictionary SystemSettings { get; set; } = new(); /// /// IP黑名单地址段 /// public static Dictionary DenyIPRange { get; set; } /// /// 判断IP地址是否被黑名单 /// /// /// public static bool IsDenyIpAddress(this string ip) { if (IPWhiteList.Contains(ip)) { return false; } return DenyIP.Contains(ip) || DenyIPRange.AsParallel().Any(kv => kv.Key.StartsWith(ip.Split('.')[0]) && ip.IpAddressInRange(kv.Key, kv.Value)); } private static readonly DbSearcher IPSearcher = new(Path.Combine(AppContext.BaseDirectory + "App_Data", "ip2region.db")); public static readonly DatabaseReader MaxmindReader = new(Path.Combine(AppContext.BaseDirectory + "App_Data", "GeoLite2-City.mmdb")); private static readonly DatabaseReader MaxmindAsnReader = new(Path.Combine(AppContext.BaseDirectory + "App_Data", "GeoLite2-ASN.mmdb")); /// /// 是否是代理ip /// /// /// /// public static async Task IsProxy(this IPAddress ip, CancellationToken cancellationToken = default) { var httpClient = Startup.ServiceProvider.GetRequiredService().CreateClient(); 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"); return await httpClient.GetStringAsync("https://ipinfo.io/" + ip, cancellationToken).ContinueWith(t => { if (t.IsCompletedSuccessfully) { var ctx = BrowsingContext.New(Configuration.Default); var doc = ctx.OpenAsync(res => res.Content(t.Result)).Result; var isAnycast = doc.DocumentElement.QuerySelectorAll(".title").Where(e => e.TextContent.Contains("Anycast")).Select(e => e.Parent).Any(n => n.TextContent.Contains("True")); var isproxy = doc.DocumentElement.QuerySelectorAll("#block-privacy img").Any(e => e.OuterHtml.Contains("right")); return isAnycast || isproxy; } return false; }); } public static AsnResponse GetIPAsn(this IPAddress ip) { if (ip.IsPrivateIP()) { return new AsnResponse(); } return Policy.Handle().Fallback(new AsnResponse()).Execute(() => MaxmindAsnReader.Asn(ip)); } private static CityResponse GetCityResp(IPAddress ip) { return Policy.Handle().Fallback(new CityResponse()).Execute(() => MaxmindReader.City(ip)); } public static IPLocation GetIPLocation(this string ips) { return GetIPLocation(IPAddress.Parse(ips)); } public static IPLocation GetIPLocation(this IPAddress ip) { if (ip.IsPrivateIP()) { return new IPLocation("内网", null, null, "内网IP", null); } var city = GetCityResp(ip); var asn = GetIPAsn(ip); var countryName = city.Country.Names.GetValueOrDefault("zh-CN") ?? city.Country.Name; var cityName = city.City.Names.GetValueOrDefault("zh-CN") ?? city.City.Name; switch (ip.AddressFamily) { case AddressFamily.InterNetworkV6 when ip.IsIPv4MappedToIPv6: ip = ip.MapToIPv4(); goto case AddressFamily.InterNetwork; case AddressFamily.InterNetwork: var parts = IPSearcher.MemorySearch(ip.ToString())?.Region.Split('|'); if (parts != null) { var network = parts[^1] == "0" ? asn.AutonomousSystemOrganization : parts[^1] + "/" + asn.AutonomousSystemOrganization; parts[0] = parts[0] != "0" ? parts[0] : countryName; parts[3] = parts[3] != "0" ? parts[3] : cityName; return new IPLocation(parts[0], parts[2], parts[3], network?.Trim('/'), asn.AutonomousSystemNumber) { Address2 = countryName + cityName, Coodinate = city.Location }; } goto default; default: return new IPLocation(countryName, null, cityName, asn.AutonomousSystemOrganization, asn.AutonomousSystemNumber) { Coodinate = city.Location }; } } /// /// 获取ip所在时区 /// /// /// public static string GetClientTimeZone(this IPAddress ip) { if (ip.IsPrivateIP()) { return "Asia/Shanghai"; } return GetCityResp(ip).Location.TimeZone ?? "Asia/Shanghai"; } /// /// 类型映射 /// /// /// /// public static T Mapper(this object source) where T : class => Startup.ServiceProvider.GetRequiredService().Map(source); /// /// 发送邮件 /// /// 标题 /// 内容 /// 收件人 /// [AutomaticRetry(Attempts = 1, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public static void SendMail(string title, string content, string tos, string clientip) { Startup.ServiceProvider.GetRequiredService().Send(title, content, tos); RedisHelper.SAdd($"Email:{DateTime.Now:yyyyMMdd}", new { title, content, tos, time = DateTime.Now, clientip }); RedisHelper.Expire($"Email:{DateTime.Now:yyyyMMdd}", 86400); } /// /// 清理html的img标签的除src之外的其他属性 /// /// /// public static async Task ClearImgAttributes(this string html) { var context = BrowsingContext.New(Configuration.Default); var doc = await context.OpenAsync(req => req.Content(html)); var nodes = doc.DocumentElement.GetElementsByTagName("img"); var allows = new[] { "src", "data-original", "width", "style", "class" }; foreach (var node in nodes) { for (var i = 0; i < node.Attributes.Length; i++) { if (allows.Contains(node.Attributes[i].Name)) { continue; } node.RemoveAttribute(node.Attributes[i].Name); } } return doc.Body.InnerHtml; } /// /// 将html的img标签懒加载 /// /// /// /// public static async Task ReplaceImgAttribute(this string html, string title) { var context = BrowsingContext.New(Configuration.Default); var doc = await context.OpenAsync(req => req.Content(html)); var nodes = doc.DocumentElement.GetElementsByTagName("img"); foreach (var node in nodes) { if (node.HasAttribute("src")) { string src = node.Attributes["src"].Value; node.RemoveAttribute("src"); node.SetAttribute("data-src", src); node.SetAttribute("decoding", "async"); node.SetAttribute("class", node.Attributes["class"]?.Value + " lazyload"); node.SetAttribute("loading", "lazy"); node.SetAttribute("alt", SystemSettings["Title"]); node.SetAttribute("title", title); } } var elements = doc.DocumentElement.QuerySelectorAll("p,br"); var els = elements.OrderByRandom().Take(Math.Max(elements.Length / 5, 3)).ToList(); var href = "https://" + SystemSettings["Domain"].Split('|').OrderByRandom().FirstOrDefault(); foreach (var el in els) { var a = doc.CreateElement("a"); a.SetAttribute("href", href); a.SetAttribute("target", "_blank"); a.SetAttribute("title", SystemSettings["Title"]); a.SetStyle("position: absolute;color: transparent;z-index: -1"); a.TextContent = SystemSettings["Title"] + SystemSettings["Domain"]; el.InsertAfter(a); var a2 = doc.CreateElement("a"); a2.SetAttribute("href", "/craw/" + SnowFlake.NewId); a2.SetStyle("position: absolute;color: transparent;z-index: -1"); a2.TextContent = title; a.InsertAfter(a2); } return doc.Body.InnerHtml; } /// /// 获取文章摘要 /// /// /// 截取长度 /// 摘要最少字数 /// public static Task GetSummary(this string html, int length = 150, int min = 10) { var context = BrowsingContext.New(Configuration.Default); return context.OpenAsync(req => req.Content(html)).ContinueWith(t => { var summary = t.Result.DocumentElement.GetElementsByTagName("p").FirstOrDefault(n => n.TextContent.Length > min)?.TextContent ?? "没有摘要"; return summary.Length > length ? summary[..length] + "..." : summary; }); } public static string TrimQuery(this string path) { return path.Split('&').Where(s => s.Split('=', StringSplitOptions.RemoveEmptyEntries).Length == 2).Join("&"); } /// /// 添加水印 /// /// /// public static Stream AddWatermark(this Stream stream) { if (!string.IsNullOrEmpty(SystemSettings.GetOrAdd("Watermark", string.Empty))) { try { var watermarker = new ImageWatermarker(stream) { SkipWatermarkForSmallImages = true, SmallImagePixelsThreshold = 90000 }; return watermarker.AddWatermark(SystemSettings["Watermark"], Color.LightGray, WatermarkPosition.BottomRight, 30); } catch { // } } return stream; } /// /// 转换时区 /// /// UTC时间 /// 时区id /// public static DateTime ToTimeZone(this in DateTime time, string zone) { return TimeZoneInfo.ConvertTime(time, TZConvert.GetTimeZoneInfo(zone)); } /// /// 转换时区 /// /// UTC时间 /// 时区id /// 时间格式字符串 /// public static string ToTimeZoneF(this in DateTime time, string zone, string format = "yyyy-MM-dd HH:mm:ss") { return ToTimeZone(time, zone).ToString(format); } /// /// 随机排序 /// /// /// /// public static IOrderedQueryable OrderByRandom(this IQueryable source) { return source.OrderBy(_ => DataContext.Random()); } } public class IPLocation { public IPLocation(string country, string province, string city, string isp, long? asn) { Country = country?.Trim('0'); Province = province?.Trim('0'); City = city?.Trim('0'); ISP = isp; ASN = asn; } public string Country { get; set; } public string Province { get; set; } public string City { get; set; } public string ISP { get; set; } public long? ASN { get; set; } public string Address => new[] { Country, Province, City }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join(""); public string Address2 { get; set; } public string Network => ASN.HasValue ? ISP + "(AS" + ASN + ")" : ISP; public Location Coodinate { get; set; } public override string ToString() { string address = Address; string network = Network; if (string.IsNullOrWhiteSpace(address)) { address = "未知地区"; } if (string.IsNullOrWhiteSpace(network)) { network = "未知网络"; } return new[] { address, Address2, network }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|"); } public static implicit operator string(IPLocation entry) { return entry.ToString(); } public void Deconstruct(out string location, out string network, out string info) { location = new[] { Address, Address2 }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|"); network = Network; info = ToString(); } public bool Contains(string s) { return ToString().Contains(s, StringComparison.CurrentCultureIgnoreCase); } } }