Skip to content

Commit

Permalink
- changed geo-ip client to use https://ip-api.com/docs/api:batch
Browse files Browse the repository at this point in the history
  • Loading branch information
PredatH0r committed Jan 28, 2024
1 parent 15c9923 commit 0dc8bb4
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 96 deletions.
154 changes: 67 additions & 87 deletions ServerBrowser/GeoIpClient.cs
Expand Up @@ -11,11 +11,12 @@

namespace ServerBrowser
{
/// <summary>
/// Geo-IP client using the free https://ip-api.com/docs/api:batch service
/// </summary>
class GeoIpClient
{
internal const int ThreadCount = 7;
private const string DefaultServiceUrlFormat = "http://api.ipapi.com/{0}?access_key=9c5fc4375488ed26aa2f26b613324f4a&language=en&output=json";
private DateTime usageExceeded = DateTime.MinValue;
private const string DefaultServiceUrlFormat = "http://ip-api.com/batch?fields=223&lang=en";

/// <summary>
/// the cache holds either a GeoInfo object, or a multicast callback delegate waiting for a GeoInfo object
Expand All @@ -30,75 +31,92 @@ public GeoIpClient(string cacheFile)
{
this.cacheFile = cacheFile;
this.ServiceUrlFormat = DefaultServiceUrlFormat;
for (int i=0; i<ThreadCount; i++)
ThreadPool.QueueUserWorkItem(context => this.ProcessLoop());
ThreadPool.QueueUserWorkItem(context => this.ProcessLoop());
}

#region ProcessLoop()
private void ProcessLoop()
{
using (var client = new XWebClient(5000))
{
int sleepMillis = 1000;
while (true)
{
var ip = this.queue.Take();
if (ip == null)
Thread.Sleep(sleepMillis);
var count = Math.Max(1, Math.Min(100, this.queue.Count));
var ips = new IPAddress[count];
for (int i = 0; i < count; i++)
ips[i] = this.queue.Take();
if (ips[ips.Length - 1] == null)
break;

bool err = true;
var ipInt = Ip4Utils.ToInt(ip);
var req = new StringBuilder(count * 18);
req.Append("[");
foreach (var ip in ips)
req.Append('"').Append(ip).Append("\",");
req[req.Length - 1] = ']';

Dictionary<uint,GeoInfo> geoInfos = null;
try
{
var url = string.Format(this.ServiceUrlFormat, ip);
var result = client.DownloadString(url);
if (result != null)
{
object o;
Action<GeoInfo> callbacks;
lock (cache)
callbacks = cache.TryGetValue(ipInt, out o) ? o as Action<GeoInfo> : null;
var geoInfo = this.HandleResult(ipInt, result);
if (callbacks != null && geoInfo != null)
ThreadPool.QueueUserWorkItem(ctx => callbacks(geoInfo));
err = false;
}
var result = client.UploadString(ServiceUrlFormat, req.ToString());
var rateLimit = client.ResponseHeaders["X-Rl"];
sleepMillis = rateLimit == "0" ? (int.TryParse(client.ResponseHeaders["X-Ttl"], out var sec) ? sec * 1000: 4000) : 0;
geoInfos = this.HandleResult(ips, result);
}
catch
{
// ignore
}

if (err)
{
lock (this.cache)
this.cache.Remove(ipInt);
foreach (var ip in ips)
{
var ipInt = Ip4Utils.ToInt(ip);
Action<GeoInfo> callbacks = null;
GeoInfo geoInfo = null;
lock (cache)
{
bool isSet = cache.TryGetValue(ipInt, out var o);
if (geoInfos == null || !geoInfos.TryGetValue(ipInt, out geoInfo))
{
//this.cache.Remove(ipInt);
}
else
{
callbacks = o as Action<GeoInfo>;
if (geoInfo != null || !isSet)
cache[ipInt] = geoInfo;
}
}

if (callbacks != null && geoInfo != null)
{
//ThreadPool.QueueUserWorkItem(ctx => callbacks(geoInfo));
callbacks(geoInfo);
}
}
}
}
}
#endregion

#region HandleResult()
private GeoInfo HandleResult(uint ip, string result)
private Dictionary<uint, GeoInfo> HandleResult(IList<IPAddress> ips, string result)
{
var ser = new DataContractJsonSerializer(typeof(NekudoGeopIpFullResponse));
var info = (NekudoGeopIpFullResponse)ser.ReadObject(new MemoryStream(Encoding.UTF8.GetBytes(result)));
var ser = new DataContractJsonSerializer(typeof(IpApiResponse[]));
var infoArray = (IpApiResponse[])ser.ReadObject(new MemoryStream(Encoding.UTF8.GetBytes(result)));

if (!info.success)
var ok = ips.Count == infoArray.Length;
var data = new Dictionary<uint, GeoInfo>();
for (int i = 0; i < ips.Count; i++)
{
if (info.error?.code == 104)
this.usageExceeded = DateTime.UtcNow.Date;
lock (cache)
this.cache.Remove(ip);
return null;
var ipInt = Ip4Utils.ToInt(ips[i]);
var info = infoArray[i];
var geoInfo = ok ? new GeoInfo(info.countryCode, info.country, info.region, info.regionName, info.city, info.lat, info.lon) : null;
data[ipInt] = geoInfo;
}

var geoInfo = new GeoInfo(info.country_code, info.country_name, info.region_code, info.region_name, info.city, info.latitude, info.longitude);
lock (cache)
{
cache[ip] = geoInfo;
}
return geoInfo;
return data;
}
#endregion

Expand All @@ -122,9 +140,6 @@ public void Lookup(IPAddress ip, Action<GeoInfo> callback)

if (geoInfo == null)
{
if (this.usageExceeded == DateTime.UtcNow.Date)
return;

if (cached == null)
{
cache.Add(ipInt, callback);
Expand Down Expand Up @@ -272,51 +287,16 @@ public override string ToString()
}
#endregion

#if false
private class NekudoGeopIpShortResponse
{
public class Country
{
public string name;
public string code;
}

public class Location
{
public decimal latitude;
public decimal longitude;
public string time_zone;
}

public string city;
public Country country;
public Location location;
public string ip;
}
#endif

#region class NekudoGeopIpFullResponse
public class NekudoGeopIpFullResponse
#region class IpApiResponse
public class IpApiResponse
{
public class NekudoGeoIpError
{
public int code;
public string type;
public string info;
}

public bool success;
public NekudoGeoIpError error;

public string ip;
public string country_code;
public string country_name;
public string region_code;
public string region_name;
public string country;
public string countryCode;
public string region;
public string regionName;
public string city;
public decimal latitude;
public decimal longitude;
public decimal lat;
public decimal lon;
}
#endregion

}
4 changes: 2 additions & 2 deletions ServerBrowser/Program.cs
Expand Up @@ -41,8 +41,8 @@ public static void Init(string fontName, float fontSize, string skinName)
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);

ThreadPool.SetMinThreads(50 + GeoIpClient.ThreadCount + 5, 100);
ThreadPool.SetMaxThreads(50 + GeoIpClient.ThreadCount + 5, 100);
ThreadPool.SetMinThreads(50 + 1 + 5, 100);
ThreadPool.SetMaxThreads(50 + 1 + 5, 100);

AppearanceObject.DefaultFont = new Font(fontName, fontSize); // must not create a Font instance before initializing GDI+
UserLookAndFeel.Default.SkinName = skinName;
Expand Down
14 changes: 8 additions & 6 deletions ServerBrowser/ServerBrowserForm.cs
Expand Up @@ -33,7 +33,7 @@ namespace ServerBrowser
{
public partial class ServerBrowserForm : XtraForm
{
private const string Version = "2.65";
private const string Version = "2.66";
private const string DevExpressVersion = "v23.2";
private const string OldSteamWebApiText = "<Steam Web API>";
private const string CustomDetailColumnPrefix = "ServerInfo.";
Expand Down Expand Up @@ -703,13 +703,14 @@ protected virtual void UpdateViewModel()
#region CreateServerSource()
protected virtual IServerSource CreateServerSource(string addressAndPort)
{
var endpoint = Ip4Utils.ParseEndpoint(addressAndPort);
string steamWebApiKey = "";
if (endpoint.Port == 0)
IPEndPoint endpoint = null;
if (Regex.IsMatch(addressAndPort, @"^[\dA-Fa-f]{32}$"))
steamWebApiKey = addressAndPort;
else
{
if (Regex.IsMatch(addressAndPort, @"^[\dA-Fa-f]{32}$"))
steamWebApiKey = addressAndPort;
else
endpoint = Ip4Utils.ParseEndpoint(addressAndPort);
if (endpoint.Port == 0)
{
var result = XtraMessageBox.Show(this, "To use the \"<Steam Web API>\" as a Master Server, you need a Steam Web API Key (32 hex digits).\n" +
"Valve issues these keys free of charge to web site owners and developers, but not necessarily to users.\n" +
Expand All @@ -727,6 +728,7 @@ protected virtual IServerSource CreateServerSource(string addressAndPort)
return null;
}
}

return new MasterServerClient(endpoint, steamWebApiKey);
}
#endregion
Expand Down
2 changes: 1 addition & 1 deletion ServerBrowser/ServerSources/MasterServerClient.cs
Expand Up @@ -16,7 +16,7 @@ public MasterServerClient(IPEndPoint masterServerEndpoint, string steamWebApiKey

public void GetAddresses(Region region, IpFilter filter, int maxResults, MasterIpCallback callback)
{
var master = masterServerEndPoint.Port == 0 ? (MasterServer)new MasterServerWebApi(steamWebApiKey) : new MasterServerUdp(masterServerEndPoint);
var master = masterServerEndPoint?.Port == null ? (MasterServer)new MasterServerWebApi(steamWebApiKey) : new MasterServerUdp(masterServerEndPoint);
master.GetAddressesLimit = maxResults;
master.GetAddresses(region, callback, filter);
}
Expand Down
4 changes: 4 additions & 0 deletions ServerBrowser/XWebClient.cs
Expand Up @@ -50,6 +50,7 @@ public void Dispose()
#endregion

public WebHeaderCollection Headers { get; set; } = new WebHeaderCollection();
public WebHeaderCollection ResponseHeaders { get; private set; }

public int ResumeOffset { get; set; }

Expand Down Expand Up @@ -159,6 +160,8 @@ public byte[] DownloadData(Uri url)
var req = this.CreateWebRequest(url);
using (var res = req.GetResponse())
{
ResponseHeaders = res.Headers;

var mem = new MemoryStream((int)req.ContentLength);
using (var r = res.GetResponseStream())
{
Expand Down Expand Up @@ -220,6 +223,7 @@ public string UploadString(Uri url, string data)
using (var res = req.GetResponse())
using (var r = new StreamReader(res.GetResponseStream(), this.Encoding))
{
ResponseHeaders = res.Headers;
return r.ReadToEnd();
}
}
Expand Down

0 comments on commit 0dc8bb4

Please sign in to comment.