懒得勤快 7 mesi fa
parent
commit
e55134db75

BIN
src/Masuit.MyBlogs.Core/App_Data/qqwry.dat


+ 11 - 24
src/Masuit.MyBlogs.Core/Common/CommonHelper.cs

@@ -121,6 +121,7 @@ namespace Masuit.MyBlogs.Core.Common
             return DenyIP.Contains(ip) || DenyIPRange.AsParallel().Any(kv => kv.Key.StartsWith(ip.Split('.')[0]) && ip.IpAddressInRange(kv.Key, kv.Value));
         }
 
+        private static readonly QQWrySearcher IPSearcher = new(Path.Combine(AppContext.BaseDirectory + "App_Data", "qqwry.dat"));
         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"));
 
@@ -169,11 +170,15 @@ namespace Masuit.MyBlogs.Core.Common
             {
                 ip = ip.MapToIPv4();
             }
+
+            var qqwry = IPSearcher.GetIpLocation(ip);
             return new IPLocation(countryName, cityName, asn.AutonomousSystemOrganization, asn.AutonomousSystemNumber)
             {
                 Coodinate = city.Location,
                 Continent = continent,
-                State = city.MostSpecificSubdivision.Names.GetValueOrDefault("zh-CN") ?? city.MostSpecificSubdivision.Name ?? Areas.FirstOrDefault(a => a.CountryCode == city.Country.IsoCode && (a.City == cityName || a.City_EN == city.City.Name))?.State
+                State = city.MostSpecificSubdivision.Names.GetValueOrDefault("zh-CN") ?? city.MostSpecificSubdivision.Name ?? Areas.FirstOrDefault(a => a.CountryCode == city.Country.IsoCode && (a.City == cityName || a.City_EN == city.City.Name))?.State,
+                Address2 = qqwry.City,
+                Network2 = qqwry.Network
             };
         }
 
@@ -389,7 +394,10 @@ namespace Masuit.MyBlogs.Core.Common
 
         public string Address => new[] { Continent, Country, State, City }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("");
 
+        public string Address2 { get; set; }
+
         public string Network => ASN.HasValue ? ISP + "(AS" + ASN + ")" : ISP;
+        public string Network2 { get; set; }
 
         public Location Coodinate { get; set; }
 
@@ -407,7 +415,7 @@ namespace Masuit.MyBlogs.Core.Common
                 network = "未知网络";
             }
 
-            return new[] { address, network }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
+            return new[] { address, Address2, network, Network2 }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
         }
 
         public static implicit operator string(IPLocation entry)
@@ -417,7 +425,7 @@ namespace Masuit.MyBlogs.Core.Common
 
         public void Deconstruct(out string location, out string network, out string info)
         {
-            location = Address;
+            location = new[] { Address, Address2 }.Where(s => !string.IsNullOrEmpty(s)).Distinct().Join("|");
             network = Network;
             info = ToString();
         }
@@ -431,26 +439,5 @@ namespace Masuit.MyBlogs.Core.Common
         {
             return ToString().Contains(s);
         }
-
-        public record CountryCity(string Continent, string Country, string State, string City)
-        {
-            public void Deconstruct(out string continent, out string country, out string state, out string city)
-            {
-                country = Country;
-                city = City;
-                continent = Continent;
-                state = State;
-            }
-
-            public static implicit operator string(CountryCity entry)
-            {
-                return entry.ToString();
-            }
-
-            public override string ToString()
-            {
-                return Continent + Country + State + City;
-            }
-        }
     }
 }

+ 323 - 0
src/Masuit.MyBlogs.Core/Common/QQWrySearcher.cs

@@ -0,0 +1,323 @@
+using System.Net;
+using System.Text;
+
+namespace Masuit.MyBlogs.Core.Common;
+
+/// <summary>
+/// QQWryIpSearch 请作为单例使用 数据库缓存在内存
+/// </summary>
+public sealed class QQWrySearcher : IDisposable
+{
+    private readonly SemaphoreSlim _initLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
+
+    private object _versionLock = new object();
+
+    private static readonly Encoding Gb2312Encoding;
+
+    /// <summary>
+    /// 数据库 缓存
+    /// </summary>
+    private byte[] _qqwryDbBytes;
+
+    /// <summary>
+    /// Ip索引 缓存
+    /// </summary>
+    private long[] _ipIndexCache;
+
+    /// <summary>
+    /// 起始定位
+    /// </summary>
+    private long _startPosition;
+
+    /// <summary>
+    /// 是否初始化
+    /// </summary>
+    private bool? _init;
+
+    private readonly string _dbPath;
+
+    private int? _ipCount;
+
+    private string _version;
+
+    /// <summary>
+    /// 记录总数
+    /// </summary>
+    public int IpCount
+    {
+        get
+        {
+            _ipCount ??= _ipIndexCache.Length;
+            return _ipCount.Value;
+        }
+    }
+
+    /// <summary>
+    /// 版本信息
+    /// </summary>
+    public string Version
+    {
+        get
+        {
+            if (!string.IsNullOrWhiteSpace(_version))
+            {
+                return _version;
+            }
+            lock (_versionLock)
+            {
+                if (!string.IsNullOrWhiteSpace(_version))
+                {
+                    return _version;
+                }
+                _version = GetIpLocation(IPAddress.Parse("255.255.255.255")).Network;
+                return _version;
+            }
+        }
+    }
+
+    static QQWrySearcher()
+    {
+        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+        Gb2312Encoding = Encoding.GetEncoding("gb2312");
+    }
+
+    public QQWrySearcher(string dbPath)
+    {
+        _dbPath = dbPath;
+        Init();
+    }
+
+    /// <summary>
+    /// 初始化
+    /// </summary>
+    /// <returns></returns>
+    public bool Init(bool getNewDb = false)
+    {
+        if (_init != null && !getNewDb)
+        {
+            return _init.Value;
+        }
+        _initLock.Wait();
+        try
+        {
+            if (_init != null && !getNewDb)
+            {
+                return _init.Value;
+            }
+
+            _qqwryDbBytes = File.ReadAllBytes(_dbPath);
+            _ipIndexCache = BlockToArray(ReadIpBlock(_qqwryDbBytes, out _startPosition));
+            _ipCount = null;
+            _version = null;
+            _init = true;
+        }
+        finally
+        {
+            _initLock.Release();
+        }
+
+        if (_qqwryDbBytes == null)
+        {
+            throw new InvalidOperationException("无法打开IP数据库" + _dbPath + "!");
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///  获取指定IP所在地理位置
+    /// </summary>
+    /// <param name="ip">要查询的IP地址</param>
+    /// <returns></returns>
+    public (string City, string Network) GetIpLocation(IPAddress ip)
+    {
+        if (ip.IsPrivateIP())
+        {
+            return ("内网", "内网");
+        }
+        var ipnum = IpToLong(ip);
+        return ReadLocation(ipnum, _startPosition, _ipIndexCache, _qqwryDbBytes);
+    }
+
+    /// <inheritdoc />
+    /// <summary>
+    /// 释放
+    /// </summary>
+    public void Dispose()
+    {
+        _initLock?.Dispose();
+        _versionLock = null;
+        _qqwryDbBytes = null;
+        _qqwryDbBytes = null;
+        _ipIndexCache = null;
+        _init = null;
+    }
+
+    ///<summary>
+    /// 将字符串形式的IP转换位long
+    ///</summary>
+    ///<param name="ip"></param>
+    ///<returns></returns>
+    private static long IpToLong(IPAddress ip)
+    {
+        var bytes = ip.GetAddressBytes();
+        var ipBytes = new byte[8];
+        for (var i = 0; i < 4; i++)
+        {
+            ipBytes[i] = bytes[3 - i];
+        }
+
+        return BitConverter.ToInt64(ipBytes);
+    }
+
+    ///<summary>
+    /// 将索引区字节块中的起始IP转换成Long数组
+    ///</summary>
+    ///<param name="ipBlock"></param>
+    private static long[] BlockToArray(byte[] ipBlock)
+    {
+        var ipArray = new long[ipBlock.Length / 7];
+        var ipIndex = 0;
+        var temp = new byte[8];
+        for (var i = 0; i < ipBlock.Length; i += 7)
+        {
+            Array.Copy(ipBlock, i, temp, 0, 4);
+            ipArray[ipIndex] = BitConverter.ToInt64(temp, 0);
+            ipIndex++;
+        }
+        return ipArray;
+    }
+
+    /// <summary>
+    ///  从IP数组中搜索指定IP并返回其索引
+    /// </summary>
+    /// <param name="ip"></param>
+    /// <param name="ipArray">IP数组</param>
+    /// <param name="start">指定搜索的起始位置</param>
+    /// <param name="end">指定搜索的结束位置</param>
+    /// <returns></returns>
+    private static int SearchIp(long ip, long[] ipArray, int start, int end)
+    {
+        while (true)
+        {
+            //计算中间索引
+            var middle = (start + end) / 2;
+            if (middle == start)
+            {
+                return middle;
+            }
+
+            if (ip < ipArray[middle])
+            {
+                end = middle;
+            }
+            else
+            {
+                start = middle;
+            }
+        }
+    }
+
+    ///<summary>
+    /// 读取IP文件中索引区块
+    ///</summary>
+    ///<returns></returns>
+    private static byte[] ReadIpBlock(byte[] bytes, out long startPosition)
+    {
+        long offset = 0;
+        startPosition = ReadLongX(bytes, offset, 4);
+        offset += 4;
+        var endPosition = ReadLongX(bytes, offset, 4);
+        offset = startPosition;
+        var count = (endPosition - startPosition) / 7 + 1;//总记录数
+        var ipBlock = new byte[count * 7];
+        for (var i = 0; i < ipBlock.Length; i++)
+        {
+            ipBlock[i] = bytes[offset + i];
+        }
+
+        return ipBlock;
+    }
+
+    /// <summary>
+    ///  从IP文件中读取指定字节并转换位long
+    /// </summary>
+    /// <param name="bytes"></param>
+    /// <param name="offset"></param>
+    /// <param name="bytesCount">需要转换的字节数,主意不要超过8字节</param>
+    /// <returns></returns>
+    private static long ReadLongX(byte[] bytes, long offset, int bytesCount)
+    {
+        var cBytes = new byte[8];
+        for (var i = 0; i < bytesCount; i++)
+        {
+            cBytes[i] = bytes[offset + i];
+        }
+
+        return BitConverter.ToInt64(cBytes, 0);
+    }
+
+    /// <summary>
+    ///  从IP文件中读取字符串
+    /// </summary>
+    /// <param name="bytes"></param>
+    /// <param name="flag">转向标志</param>
+    /// <param name="offset"></param>
+    /// <returns></returns>
+    private static string ReadString(byte[] bytes, int flag, ref long offset)
+    {
+        if (flag == 1 || flag == 2)//转向标志
+        {
+            offset = ReadLongX(bytes, offset, 3);
+        }
+        else
+        {
+            offset -= 1;
+        }
+
+        var list = new List<byte>();
+        var b = bytes[offset];
+        offset += 1;
+        while (b > 0)
+        {
+            list.Add(b);
+            b = bytes[offset];
+            offset += 1;
+        }
+
+        return Gb2312Encoding.GetString(list.ToArray());
+    }
+
+    private static (string City, string Network) ReadLocation(long ip, long startPosition, long[] ipIndex, byte[] qqwryDbBytes)
+    {
+        long offset = SearchIp(ip, ipIndex, 0, ipIndex.Length) * 7 + 4;
+
+        //偏移
+        var arrayOffset = startPosition + offset;
+        //跳过结束IP
+        arrayOffset = ReadLongX(qqwryDbBytes, arrayOffset, 3) + 4;
+        //读取标志
+        var flag = qqwryDbBytes[arrayOffset];
+        arrayOffset += 1;
+        //表示国家和地区被转向
+        if (flag == 1)
+        {
+            arrayOffset = ReadLongX(qqwryDbBytes, arrayOffset, 3);
+            //再读标志
+            flag = qqwryDbBytes[arrayOffset];
+            arrayOffset += 1;
+        }
+        var countryOffset = arrayOffset;
+        var city = ReadString(qqwryDbBytes, flag, ref arrayOffset);
+
+        if (flag == 2)
+        {
+            arrayOffset = countryOffset + 3;
+        }
+
+        flag = qqwryDbBytes[arrayOffset];
+        arrayOffset += 1;
+        var network = ReadString(qqwryDbBytes, flag, ref arrayOffset);
+        return (city, network);
+    }
+}

+ 3 - 1
src/Masuit.MyBlogs.Core/Controllers/ToolsController.cs

@@ -56,14 +56,16 @@ public sealed class ToolsController : BaseController
         {
             Location = loc.Coodinate,
             Address = loc.Address,
+            Address2 = loc.Address2,
             Network = new NetworkInfo
             {
                 Asn = asn.AutonomousSystemNumber,
                 Router = asn.Network + "",
                 Organization = loc.ISP
             },
+            Network2 = loc.Network2,
             TimeZone = loc.Coodinate.TimeZone + $"  UTC{TZConvert.GetTimeZoneInfo(loc.Coodinate.TimeZone ?? "Asia/Shanghai").BaseUtcOffset.Hours:+#;-#;0}",
-            IsProxy = loc.Network.Contains(new[] { "cloud", "Compute", "Serv", "Tech", "Solution", "Host", "云", "Datacenter", "Data Center", "Business", "ASN" }) || domain.Length > 1 || await IsProxy(ipAddress, cts.Token),
+            IsProxy = loc.Network.Contains(["cloud", "Compute", "Serv", "Tech", "Solution", "Host", "云", "Datacenter", "Data Center", "Business", "ASN"]) || domain.Length > 1 || await IsProxy(ipAddress, cts.Token),
             Domain = domain
         };
         if (Request.Method.Equals(HttpMethods.Get) || (Request.Headers[HeaderNames.Accept] + "").StartsWith(ContentType.Json))

+ 1 - 1
src/Masuit.MyBlogs.Core/Masuit.MyBlogs.Core.csproj

@@ -45,7 +45,7 @@
         <PackageReference Include="CHTCHSConv" Version="1.0.0" />
         <PackageReference Include="CLRStats" Version="1.0.0" />
         <PackageReference Include="Dispose.Scope.AspNetCore" Version="0.0.3" />
-        <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="5.0.0" />
+        <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="5.2.1" />
         <PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.3" />
         <PackageReference Include="FreeRedis" Version="1.3.6" />
         <PackageReference Include="Hangfire" Version="1.8.18" />

+ 2 - 0
src/Masuit.MyBlogs.Core/Models/ViewModel/IpInfo.cs

@@ -6,8 +6,10 @@ namespace Masuit.MyBlogs.Core.Models.ViewModel;
 public class IpInfo
 {
     public string Address { get; set; }
+    public string Address2 { get; set; }
     public Location Location { get; set; }
     public NetworkInfo Network { get; set; }
+    public string Network2 { get; set; }
     public bool IsProxy { get; set; }
     public string TimeZone { get; set; }
     public string Domain { get; set; }

+ 3 - 1
src/Masuit.MyBlogs.Core/Program.cs

@@ -2,10 +2,12 @@
 using Masuit.MyBlogs.Core;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
 using System.Diagnostics;
+using System.Net;
 using AngleSharp.Text;
 
 AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
-
+QQWrySearcher IPSearcher = new(Path.Combine(AppContext.BaseDirectory + "App_Data", "qqwry.dat"));
+var (city, network) = IPSearcher.GetIpLocation(IPAddress.Parse("2409:891f:6b40:11e8:25ed:834e:394b:793a"));
 try
 {
     if (Environment.OSVersion.Platform is not (PlatformID.MacOSX or PlatformID.Unix))

+ 9 - 1
src/Masuit.MyBlogs.Core/Views/Tools/GetIPInfo.cshtml

@@ -45,14 +45,22 @@
             <td>IP机房信息:</td>
             <td>@Model.Network.Organization</td>
         </tr>
+        <tr>
+            <td>IP机房信息2:</td>
+            <td>@Model.Network2</td>
+        </tr>
         <tr>
             <td>ASN:</td>
             <td>@Model.Network.Asn</td>
         </tr>
         <tr>
-            <td>参考地理位置:</td>
+            <td>参考地理位置1:</td>
             <td>@Model.Address</td>
         </tr>
+        <tr>
+            <td>参考地理位置2:</td>
+            <td>@Model.Address2</td>
+        </tr>
         <tr>
             <td>时区:</td>
             <td>@Model.TimeZone</td>