diff --git a/TShockAPI/ConfigFile.cs b/TShockAPI/ConfigFile.cs index 428f1d8e..3faf935f 100644 --- a/TShockAPI/ConfigFile.cs +++ b/TShockAPI/ConfigFile.cs @@ -184,6 +184,9 @@ namespace TShockAPI [Description("This is kick players who have custom items in their inventory (via a mod)")] public bool KickCustomItems = false; + [Description("This will announce a player's location on join")] + public bool EnableGeoIP = false; + public static ConfigFile Read(string path) { if (!File.Exists(path)) diff --git a/TShockAPI/GeoIPCountry.cs b/TShockAPI/GeoIPCountry.cs new file mode 100644 index 00000000..a5db53fd --- /dev/null +++ b/TShockAPI/GeoIPCountry.cs @@ -0,0 +1,259 @@ +/* GeoIPCountry.cs + * + * Copyright (C) 2008 MaxMind, Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +using System; +using System.IO; +using System.Net; + +// This code is based on MaxMind's original C# code, which was ported from Java. +// This version is very simplified and does not support a majority of features for speed. + +namespace MaxMind +{ + /// + /// Allows for looking up a country based on an IP address. See www.maxmind.com for more details. + /// + /// + /// static void Main(string[] args) + /// { + /// using(GeoIPCountry geo = new GeoIPCountry("GeoIP.dat")) + /// { + /// try + /// { + /// Console.WriteLine("Country code of IP address 67.15.94.80: " + geo.GetCountryCode("67.15.94.80")); + /// } + /// catch(Exception ex) + /// { + /// Console.WriteLine(ex.ToString()); + /// } + /// } + /// } + /// + public sealed class GeoIPCountry : IDisposable + { + Stream _geodata; + bool _close; + + // hard coded position of where country data starts in the data file. + const long COUNTRY_BEGIN = 16776960; + + static readonly string[] CountryCodes = { + "--","AP","EU","AD","AE","AF","AG","AI","AL","AM","AN","AO","AQ","AR","AS", + "AT","AU","AW","AZ","BA","BB","BD","BE","BF","BG","BH","BI","BJ","BM","BN", + "BO","BR","BS","BT","BV","BW","BY","BZ","CA","CC","CD","CF","CG","CH","CI", + "CK","CL","CM","CN","CO","CR","CU","CV","CX","CY","CZ","DE","DJ","DK","DM", + "DO","DZ","EC","EE","EG","EH","ER","ES","ET","FI","FJ","FK","FM","FO","FR", + "FX","GA","GB","GD","GE","GF","GH","GI","GL","GM","GN","GP","GQ","GR","GS", + "GT","GU","GW","GY","HK","HM","HN","HR","HT","HU","ID","IE","IL","IN","IO", + "IQ","IR","IS","IT","JM","JO","JP","KE","KG","KH","KI","KM","KN","KP","KR", + "KW","KY","KZ","LA","LB","LC","LI","LK","LR","LS","LT","LU","LV","LY","MA", + "MC","MD","MG","MH","MK","ML","MM","MN","MO","MP","MQ","MR","MS","MT","MU", + "MV","MW","MX","MY","MZ","NA","NC","NE","NF","NG","NI","NL","NO","NP","NR", + "NU","NZ","OM","PA","PE","PF","PG","PH","PK","PL","PM","PN","PR","PS","PT", + "PW","PY","QA","RE","RO","RU","RW","SA","SB","SC","SD","SE","SG","SH","SI", + "SJ","SK","SL","SM","SN","SO","SR","ST","SV","SY","SZ","TC","TD","TF","TG", + "TH","TJ","TK","TM","TN","TO","TL","TR","TT","TV","TW","TZ","UA","UG","UM", + "US","UY","UZ","VA","VC","VE","VG","VI","VN","VU","WF","WS","YE","YT","RS", + "ZA","ZM","ME","ZW","A1","A2","O1","AX","GG","IM","JE","BL","MF" + }; + + static readonly string[] CountryNames = { + "N/A","Asia/Pacific Region","Europe","Andorra","United Arab Emirates","Afghanistan", + "Antigua and Barbuda","Anguilla","Albania","Armenia","Netherlands Antilles","Angola", + "Antarctica","Argentina","American Samoa","Austria","Australia","Aruba","Azerbaijan", + "Bosnia and Herzegovina","Barbados","Bangladesh","Belgium","Burkina Faso","Bulgaria", + "Bahrain","Burundi","Benin","Bermuda","Brunei Darussalam","Bolivia","Brazil","Bahamas", + "Bhutan","Bouvet Island","Botswana","Belarus","Belize","Canada","Cocos (Keeling) Islands", + "Congo, The Democratic Republic of the","Central African Republic","Congo","Switzerland", + "Cote D'Ivoire","Cook Islands","Chile","Cameroon","China","Colombia","Costa Rica","Cuba", + "Cape Verde","Christmas Island","Cyprus","Czech Republic","Germany","Djibouti","Denmark", + "Dominica","Dominican Republic","Algeria","Ecuador","Estonia","Egypt","Western Sahara", + "Eritrea","Spain","Ethiopia","Finland","Fiji","Falkland Islands (Malvinas)", + "Micronesia, Federated States of","Faroe Islands","France","France, Metropolitan","Gabon", + "United Kingdom","Grenada","Georgia","French Guiana","Ghana","Gibraltar","Greenland", + "Gambia","Guinea","Guadeloupe","Equatorial Guinea","Greece", + "South Georgia and the South Sandwich Islands","Guatemala","Guam","Guinea-Bissau","Guyana", + "Hong Kong","Heard Island and McDonald Islands","Honduras","Croatia","Haiti","Hungary", + "Indonesia","Ireland","Israel","India","British Indian Ocean Territory","Iraq", + "Iran, Islamic Republic of","Iceland","Italy","Jamaica","Jordan","Japan","Kenya", + "Kyrgyzstan","Cambodia","Kiribati","Comoros","Saint Kitts and Nevis", + "Korea, Democratic People's Republic of","Korea, Republic of","Kuwait","Cayman Islands", + "Kazakstan","Lao People's Democratic Republic","Lebanon","Saint Lucia","Liechtenstein", + "Sri Lanka","Liberia","Lesotho","Lithuania","Luxembourg","Latvia","Libyan Arab Jamahiriya", + "Morocco","Monaco","Moldova, Republic of","Madagascar","Marshall Islands","Macedonia", + "Mali","Myanmar","Mongolia","Macau","Northern Mariana Islands","Martinique","Mauritania", + "Montserrat","Malta","Mauritius","Maldives","Malawi","Mexico","Malaysia","Mozambique", + "Namibia","New Caledonia","Niger","Norfolk Island","Nigeria","Nicaragua","Netherlands", + "Norway","Nepal","Nauru","Niue","New Zealand","Oman","Panama","Peru","French Polynesia", + "Papua New Guinea","Philippines","Pakistan","Poland","Saint Pierre and Miquelon", + "Pitcairn Islands","Puerto Rico","Palestinian Territory","Portugal","Palau","Paraguay", + "Qatar","Reunion","Romania","Russian Federation","Rwanda","Saudi Arabia", + "Solomon Islands","Seychelles","Sudan","Sweden","Singapore","Saint Helena","Slovenia", + "Svalbard and Jan Mayen","Slovakia","Sierra Leone","San Marino","Senegal","Somalia", + "Suriname","Sao Tome and Principe","El Salvador","Syrian Arab Republic","Swaziland", + "Turks and Caicos Islands","Chad","French Southern Territories","Togo","Thailand", + "Tajikistan","Tokelau","Turkmenistan","Tunisia","Tonga","Timor-Leste","Turkey", + "Trinidad and Tobago","Tuvalu","Taiwan","Tanzania, United Republic of","Ukraine","Uganda", + "United States Minor Outlying Islands","United States","Uruguay","Uzbekistan", + "Holy See (Vatican City State)","Saint Vincent and the Grenadines","Venezuela", + "Virgin Islands, British","Virgin Islands, U.S.","Vietnam","Vanuatu","Wallis and Futuna", + "Samoa","Yemen","Mayotte","Serbia","South Africa","Zambia","Montenegro","Zimbabwe", + "Anonymous Proxy","Satellite Provider","Other","Aland Islands","Guernsey","Isle of Man", + "Jersey","Saint Barthelemy","Saint Martin" + }; + + // + // Constructor + // + + /// + /// Initialises a new instance of this class. + /// + /// An already open stream pointing to the contents of a GeoIP.dat file. + /// The stream is not closed when this class is disposed. Be sure to clean up afterwards! + public GeoIPCountry(Stream datafile) + { + _geodata = datafile; + _close = false; + } + + /// + /// Initialises a new instance of this class, using an on-disk database. + /// + /// Path to database file. + /// The file will be closed when this class is disposed. + public GeoIPCountry(string filename) + { + FileStream fs = new FileStream(filename, FileMode.Open); + _geodata = (Stream)fs; + _close = true; + } + + /// + /// Retrieves a two-letter code, defined by MaxMind, which details the country the specified IP address is located. + /// + /// IP address to query. + /// A two-letter code string. Throws exceptions on failure. + /// The IP address must be IPv4. + public string GetCountryCode(IPAddress ip) + { + return CountryCodes[FindIndex(ip)]; + } + + /// + /// Retrieves a two-letter code, defined by MaxMind, which details the country the specified IP address is located. Does not throw exceptions on failure. + /// + /// IP address to query. + /// Two-letter country code or null on failure. + public string TryGetCountryCode(IPAddress ip) + { + try + { + return CountryCodes[FindIndex(ip)]; + } + catch (Exception) + { + return null; + } + } + + /// + /// Gets the English name of a country, by a country code. + /// + /// Country code to look up, returned by GetCountryCode or TryGetCountryCode. + /// English name of the country, or null on failure. + public static string GetCountryNameByCode(string countrycode) + { + int index = Array.IndexOf(CountryCodes, countrycode); + return index == -1 ? null : CountryNames[index]; + } + + int FindIndex(IPAddress ip) + { + return (int)FindCountryCode(0, AddressToLong(ip), 31); + } + + // Converts an IPv4 address into a long, for reading from geo database + long AddressToLong(IPAddress ip) + { + if (ip.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) + throw new InvalidOperationException("IP address is not IPv4"); + + long num = 0; + byte[] bytes = ip.GetAddressBytes(); + for (int i = 0; i < 4; ++i) + { + long y = bytes[i]; + if (y < 0) + y += 256; + num += y << ((3 - i) * 8); + } + + return num; + } + + // Traverses the GeoIP binary data looking for a country code based + // on the IP address mask + long FindCountryCode(long offset, long ipnum, int depth) + { + byte[] buffer = new byte[6]; // 2 * MAX_RECORD_LENGTH + long[] x = new long[2]; + if (depth < 0) + throw new IOException("Cannot seek GeoIP database"); + + _geodata.Seek(6 * offset, SeekOrigin.Begin); + _geodata.Read(buffer, 0, 6); + + for (int i = 0; i < 2; i++) + { + x[i] = 0; + for (int j = 0; j < 3; j++) + { + int y = buffer[i * 3 + j]; + if (y < 0) + y += 256; + x[i] += (y << (j * 8)); + } + } + + if ((ipnum & (1 << depth)) > 0) + { + if (x[1] >= COUNTRY_BEGIN) + return x[1] - COUNTRY_BEGIN; + return FindCountryCode(x[1], ipnum, depth - 1); + } + else + { + if (x[0] >= COUNTRY_BEGIN) + return x[0] - COUNTRY_BEGIN; + return FindCountryCode(x[0], ipnum, depth - 1); + } + } + + public void Dispose() + { + if (_close && _geodata != null) + { + _geodata.Close(); + _geodata = null; + } + } + } +} + diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index ce01f940..ac1e71cb 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -60,6 +60,7 @@ namespace TShockAPI public bool RequestedSection = false; public DateTime LastDeath { get; set; } public bool ForceSpawn = false; + public string Country = "??"; public bool RealPlayer { diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index 7d0a034c..cb5b20e1 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -65,6 +65,7 @@ namespace TShockAPI public static IDbConnection DB; public static bool OverridePort; public static PacketBufferer PacketBuffer; + public static MaxMind.GeoIPCountry Geo; /// /// Called after TShock is initialized. Useful for plugins that needs hooks before tshock but also depend on tshock being loaded. @@ -170,6 +171,8 @@ namespace TShockAPI Regions = new RegionManager(DB); Itembans = new ItemManager(DB); RememberedPos = new RemeberedPosManager(DB); + if (Config.EnableGeoIP) + Geo = new MaxMind.GeoIPCountry(Path.Combine(SavePath, "GeoIP.dat")); Log.ConsoleInfo(string.Format("TShock Version {0} ({1}) now running.", Version, VersionCodename)); @@ -620,7 +623,15 @@ namespace TShockAPI NetMessage.SendData((int)PacketTypes.TimeSet, -1, -1, "", 0, 0, Main.sunModY, Main.moonModY); NetMessage.syncPlayers(); - Log.Info(string.Format("{0} ({1}) from '{2}' group joined.", player.Name, player.IP, player.Group.Name)); + if (Config.EnableGeoIP) + { + var code = Geo.TryGetCountryCode(IPAddress.Parse(player.IP)); + player.Country = code == null ? "N/A" : MaxMind.GeoIPCountry.GetCountryNameByCode(code); + Log.Info(string.Format("{0} ({1}) from '{2}' group from '{3}' joined.", player.Name, player.IP, player.Group.Name, player.Country)); + Tools.Broadcast(player.Name + " is from the " + player.Country, Color.Yellow); + } + else + Log.Info(string.Format("{0} ({1}) from '{2}' group joined.", player.Name, player.IP, player.Group.Name)); Tools.ShowFileToUser(player, "motd.txt"); if (HackedHealth(player)) diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index 16c5480d..86c48adf 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -103,6 +103,7 @@ +