1 #region Copyright & License Information
2 /*
3  * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
4  * This file is part of OpenRA, which is free software. It is made
5  * available to you under the terms of the GNU General Public License
6  * as published by the Free Software Foundation, either version 3 of
7  * the License, or (at your option) any later version. For more
8  * information, see COPYING.
9  */
10 #endregion
11 
12 using System;
13 using System.IO;
14 using System.Linq;
15 using System.Net;
16 using System.Net.Sockets;
17 using System.Numerics;
18 using ICSharpCode.SharpZipLib.Zip;
19 
20 namespace OpenRA.Network
21 {
22 	public class GeoIP
23 	{
24 		class IP2LocationReader
25 		{
26 			public readonly DateTime Date;
27 			readonly Stream stream;
28 			readonly uint columnCount;
29 			readonly uint v4Count;
30 			readonly uint v4Offset;
31 			readonly uint v6Count;
32 			readonly uint v6Offset;
33 
IP2LocationReader(Stream source)34 			public IP2LocationReader(Stream source)
35 			{
36 				// Copy stream data for reuse
37 				stream = new MemoryStream();
38 				source.CopyTo(stream);
39 				stream.Position = 0;
40 
41 				if (stream.ReadUInt8() != 1)
42 					throw new InvalidDataException("Only IP2Location type 1 databases are supported.");
43 
44 				columnCount = stream.ReadUInt8();
45 				var year = stream.ReadUInt8();
46 				var month = stream.ReadUInt8();
47 				var day = stream.ReadUInt8();
48 				Date = new DateTime(2000 + year, month, day);
49 
50 				v4Count = stream.ReadUInt32();
51 				v4Offset = stream.ReadUInt32();
52 				v6Count = stream.ReadUInt32();
53 				v6Offset = stream.ReadUInt32();
54 			}
55 
AddressForIndex(long index, bool isIPv6)56 			BigInteger AddressForIndex(long index, bool isIPv6)
57 			{
58 				var start = isIPv6 ? v6Offset : v4Offset;
59 				var offset = isIPv6 ? 12 : 0;
60 				stream.Seek(start + index * (4 * columnCount + offset) - 1, SeekOrigin.Begin);
61 				return new BigInteger(stream.ReadBytes(isIPv6 ? 16 : 4).Append((byte)0).ToArray());
62 			}
63 
CountryForIndex(long index, bool isIPv6)64 			string CountryForIndex(long index, bool isIPv6)
65 			{
66 				// Read file offset for country entry
67 				var start = isIPv6 ? v6Offset : v4Offset;
68 				var offset = isIPv6 ? 12 : 0;
69 				stream.Seek(start + index * (4 * columnCount + offset) + offset + 3, SeekOrigin.Begin);
70 				var countryOffset = stream.ReadUInt32();
71 
72 				// Read length-prefixed country name
73 				stream.Seek(countryOffset + 3, SeekOrigin.Begin);
74 				var length = stream.ReadUInt8();
75 
76 				// "-" is used to represent an unknown country in the database
77 				var country = stream.ReadASCII(length);
78 				return country != "-" ? country : null;
79 			}
80 
LookupCountry(IPAddress ip)81 			public string LookupCountry(IPAddress ip)
82 			{
83 				var isIPv6 = ip.AddressFamily == AddressFamily.InterNetworkV6;
84 				if (!isIPv6 && ip.AddressFamily != AddressFamily.InterNetwork)
85 					return null;
86 
87 				// Locate IP using a binary search
88 				// The IP2Location python parser has an additional
89 				// optimization that can jump directly to the row, but this adds
90 				// extra complexity that isn't obviously needed for our limited database size
91 				long low = 0;
92 				long high = isIPv6 ? v6Count : v4Count;
93 
94 				// Append an empty byte to force the data to be treated as unsigned
95 				var ipNumber = new BigInteger(ip.GetAddressBytes().Reverse().Append((byte)0).ToArray());
96 				while (low <= high)
97 				{
98 					var mid = (low + high) / 2;
99 					var min = AddressForIndex(mid, isIPv6);
100 					var max = AddressForIndex(mid + 1, isIPv6);
101 					if (min <= ipNumber && ipNumber < max)
102 						return CountryForIndex(mid, isIPv6);
103 
104 					if (ipNumber < min)
105 						high = mid - 1;
106 					else
107 						low = mid + 1;
108 				}
109 
110 				return null;
111 			}
112 		}
113 
114 		static IP2LocationReader database;
115 
Initialize(string databasePath = R)116 		public static void Initialize(string databasePath = "IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP")
117 		{
118 			if (!File.Exists(databasePath))
119 				return;
120 
121 			try
122 			{
123 				using (var z = new ZipFile(databasePath))
124 				{
125 					var entry = z.FindEntry("IP2LOCATION-LITE-DB1.IPV6.BIN", false);
126 					database = new IP2LocationReader(z.GetInputStream(entry));
127 				}
128 			}
129 			catch (Exception e)
130 			{
131 				Log.Write("geoip", "DatabaseReader failed: {0}", e);
132 			}
133 		}
134 
LookupCountry(IPAddress ip)135 		public static string LookupCountry(IPAddress ip)
136 		{
137 			if (database != null)
138 			{
139 				try
140 				{
141 					return database.LookupCountry(ip);
142 				}
143 				catch (Exception e)
144 				{
145 					Log.Write("geoip", "LookupCountry failed: {0}", e);
146 				}
147 			}
148 
149 			return null;
150 		}
151 	}
152 }
153