Skip to content

Commit 750e553

Browse files
committed
extracted GeoIpApiClientBase class from IpDashApiDotComGeoIpClient
implemented IpInfoDotIo online API client (ipinfo.io) implemented a dummy NoGeoIpClient as a fallback when there are errors with the selected client MaxMindGeoIP2Client displays info message when the local database file is missing
1 parent dcae59d commit 750e553

12 files changed

+912
-687
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Globalization;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Net;
8+
using System.Text;
9+
using System.Threading;
10+
11+
namespace ServerBrowser;
12+
13+
/// <summary>
14+
/// A base class for clients which use an online API to lookup geo-ip info and store it in a local cache.
15+
/// </summary>
16+
public abstract class GeoIpApiClientBase : IGeoIpClient
17+
{
18+
private readonly Dictionary<uint, object> cache = new Dictionary<uint, object>();
19+
20+
private readonly BlockingCollection<IPAddress> queue = new ();
21+
private string cacheFile;
22+
23+
protected abstract int GetMaxIpsPerRequest();
24+
25+
public GeoIpApiClientBase()
26+
{
27+
var baseDir = Path.GetDirectoryName(this.GetType().Assembly.Location) ?? ".";
28+
this.cacheFile = Path.Combine(baseDir, "locations.txt");
29+
MoveConfigFilesFromOldLocation();
30+
31+
ThreadPool.QueueUserWorkItem(_ => this.ProcessLoop());
32+
}
33+
34+
#region MoveConfigFilesFromOldLocation()
35+
private void MoveConfigFilesFromOldLocation()
36+
{
37+
if (File.Exists(this.cacheFile))
38+
return;
39+
40+
try
41+
{
42+
var geoCache = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "locations.txt");
43+
if (File.Exists(geoCache))
44+
File.Move(geoCache, this.cacheFile);
45+
}
46+
catch
47+
{
48+
// ignore
49+
}
50+
}
51+
#endregion
52+
53+
#region ProcessLoop()
54+
private void ProcessLoop()
55+
{
56+
using var client = new XWebClient(5000);
57+
int sleepMillis = 1000;
58+
while (true)
59+
{
60+
Thread.Sleep(sleepMillis);
61+
var count = Math.Max(1, Math.Min(GetMaxIpsPerRequest(), this.queue.Count));
62+
var ips = new IPAddress[count];
63+
for (int i = 0; i < count; i++)
64+
ips[i] = this.queue.Take();
65+
if (ips[ips.Length - 1] == null)
66+
break;
67+
68+
Dictionary<uint,GeoInfo> geoInfos = null;
69+
try
70+
{
71+
geoInfos = this.RequestGeoInfo(client, ips, ref sleepMillis);
72+
}
73+
catch
74+
{
75+
// ignore
76+
}
77+
78+
foreach (var ip in ips)
79+
{
80+
var ipInt = Ip4Utils.ToInt(ip);
81+
Action<GeoInfo> callbacks = null;
82+
GeoInfo geoInfo = null;
83+
lock (cache)
84+
{
85+
bool isSet = cache.TryGetValue(ipInt, out var o);
86+
if (geoInfos == null || !geoInfos.TryGetValue(ipInt, out geoInfo))
87+
{
88+
//this.cache.Remove(ipInt);
89+
}
90+
else
91+
{
92+
callbacks = o as Action<GeoInfo>;
93+
if (geoInfo != null || !isSet)
94+
cache[ipInt] = geoInfo;
95+
}
96+
}
97+
98+
if (callbacks != null && geoInfo != null)
99+
{
100+
//ThreadPool.QueueUserWorkItem(ctx => callbacks(geoInfo));
101+
callbacks(geoInfo);
102+
}
103+
}
104+
}
105+
}
106+
#endregion
107+
108+
protected abstract Dictionary<uint, GeoInfo> RequestGeoInfo(XWebClient client,IPAddress[] ips, ref int sleepMillis);
109+
110+
#region Lookup()
111+
public void Lookup(IPAddress ip, Action<GeoInfo> callback)
112+
{
113+
uint ipInt = Ip4Utils.ToInt(ip);
114+
GeoInfo geoInfo = null;
115+
lock (cache)
116+
{
117+
if (cache.TryGetValue(ipInt, out var cached))
118+
geoInfo = cached as GeoInfo;
119+
120+
if (geoInfo == null)
121+
{
122+
if (cached == null)
123+
{
124+
cache.Add(ipInt, callback);
125+
this.queue.Add(ip);
126+
}
127+
else
128+
{
129+
var callbacks = (Action<GeoInfo>)cached;
130+
callbacks += callback;
131+
cache[ipInt] = callbacks;
132+
}
133+
return;
134+
}
135+
}
136+
callback(geoInfo);
137+
}
138+
#endregion
139+
140+
#region CancelPendingRequests()
141+
public void CancelPendingRequests()
142+
{
143+
while (this.queue.TryTake(out _))
144+
{
145+
}
146+
147+
lock (cache)
148+
{
149+
var keys = cache.Keys.ToList();
150+
foreach (var key in keys)
151+
{
152+
if (cache[key] is not GeoInfo)
153+
cache.Remove(key);
154+
}
155+
}
156+
}
157+
#endregion
158+
159+
#region LoadCache()
160+
public bool LoadCache()
161+
{
162+
if (!File.Exists(this.cacheFile))
163+
return true;
164+
lock (cache)
165+
{
166+
foreach (var line in File.ReadAllLines(this.cacheFile))
167+
{
168+
try
169+
{
170+
var parts = line.Split(new[] { '=' }, 2);
171+
uint ipInt = 0;
172+
var octets = parts[0].Split('.');
173+
foreach (var octet in octets)
174+
ipInt = (ipInt << 8) + uint.Parse(octet);
175+
var loc = parts[1].Split('|');
176+
var countryCode = loc[0];
177+
var latitude = decimal.Parse(loc[5], NumberFormatInfo.InvariantInfo);
178+
var longitude = decimal.Parse(loc[6], NumberFormatInfo.InvariantInfo);
179+
if (countryCode != "")
180+
{
181+
var geoInfo = new GeoInfo(countryCode, loc[1], loc[2], loc[3], loc[4], latitude, longitude);
182+
cache[ipInt] = geoInfo;
183+
}
184+
}
185+
catch
186+
{
187+
// ignore
188+
}
189+
}
190+
191+
// override wrong geo-IP information (MS Azure IPs list Washington even for NL/EU servers)
192+
cache[Ip4Utils.ToInt(104, 40, 134, 97)] = new GeoInfo("NL", "Netherlands", null, null, null, 0, 0);
193+
cache[Ip4Utils.ToInt(104, 40, 213, 215)] = new GeoInfo("NL", "Netherlands", null, null, null, 0, 0);
194+
195+
// Vultr also spreads their IPs everywhere
196+
cache[Ip4Utils.ToInt(45, 32, 153, 115)] = new GeoInfo("DE", "Germany", null, null, null, 0, 0); // listed as NL, but is DE
197+
cache[Ip4Utils.ToInt(45, 32, 205, 149)] = new GeoInfo("US", "United States", "TX", null, null, 0, 0); // listed as NL, but is TX
198+
199+
// i3d.net
200+
cache[Ip4Utils.ToInt(185, 179, 200, 69)] = new GeoInfo("ZA", "South Africa", null, null, null, 0, 0); // listed as NL, but is ZA
201+
}
202+
203+
return true;
204+
}
205+
#endregion
206+
207+
#region SaveCache()
208+
public void SaveCache()
209+
{
210+
try
211+
{
212+
var sb = new StringBuilder();
213+
lock (this.cache)
214+
{
215+
foreach (var entry in this.cache)
216+
{
217+
var ip = entry.Key;
218+
var info = entry.Value as GeoInfo;
219+
if (info == null) continue;
220+
sb.AppendFormat("{0}.{1}.{2}.{3}={4}|{5}|{6}|{7}|{8}|{9}|{10}\n",
221+
ip >> 24, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF,
222+
info.Iso2, info.Country, info.State, info.Region, info.City, info.Latitude, info.Longitude);
223+
}
224+
}
225+
226+
File.WriteAllText(this.cacheFile, sb.ToString());
227+
}
228+
catch
229+
{
230+
// ignore
231+
}
232+
}
233+
#endregion
234+
235+
#region Dispose()
236+
public virtual void Dispose()
237+
{
238+
}
239+
#endregion
240+
}
241+
242+
#region class GeoInfo
243+
public class GeoInfo
244+
{
245+
public string Iso2 { get; }
246+
public string Country { get; }
247+
public string State { get; }
248+
public string Region { get; }
249+
public string City { get; }
250+
public decimal Longitude { get; }
251+
public decimal Latitude { get; }
252+
253+
public GeoInfo(string iso2, string country, string state, string region, string city, decimal latitude, decimal longitude)
254+
{
255+
this.Iso2 = iso2;
256+
this.Country = country;
257+
this.State = state;
258+
this.Region = region;
259+
this.City = city;
260+
this.Longitude = longitude;
261+
this.Latitude = latitude;
262+
}
263+
264+
public override string ToString()
265+
{
266+
StringBuilder sb = new StringBuilder();
267+
sb.Append(Country);
268+
if (!string.IsNullOrEmpty(State))
269+
sb.Append(", ").Append(State);
270+
if (!string.IsNullOrEmpty(Region))
271+
sb.Append(", ").Append(Region);
272+
if (!string.IsNullOrEmpty(City))
273+
sb.Append(", ").Append(City);
274+
return sb.ToString();
275+
}
276+
277+
}
278+
#endregion

ServerBrowser/GeoIp/IGeoIpClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ namespace ServerBrowser
55
{
66
internal interface IGeoIpClient : IDisposable
77
{
8+
bool LoadCache();
89
void Lookup(IPAddress ip, Action<GeoInfo> callback);
910
void CancelPendingRequests();
10-
void LoadCache();
1111
void SaveCache();
1212
}
1313
}

0 commit comments

Comments
 (0)