xref: /web-php/include/ip-to-country.inc (revision 25d2dc1c)
1<?php
2
3/*
4
5 This script uses the local copy of the ip-to-country.com
6 database available on all php.net mirror sites to find
7 out what country the user is in. This data is stored in
8 a cookie for future usage, and another check is only done,
9 if the cookie is expired.
10
11 We have an index to speed up the search. The first line of
12 the index file contains the step value used to create the
13 index. The IP should be divided by this number, when an
14 appropriate index entry is searched for. Then CSV formatted
15 lines are placed in the file with the division results and
16 starting indexes (pointing to the data file).
17
18 The data file has a fixed record size format to ease
19 fseek()ing. The format of the database is:
20
21  n records representing IP range and country relations
22  the format of a record is:
23
24   - starting IP number [10 bytes, padded with zeros from left]
25   - ending IP number   [10 bytes, padded with zeros from left]
26   - ISO country code   [3 bytes / letters]
27   - newline            [1 byte: \n]
28
29 IP numbers can be created with i2c_ip2num().
30
31 Some things to consider:
32
33  - Now 'NA' is stored in the cookie (no country detected).
34    This is partly for safety reasons, not to bog down the
35    server with country detection in case the data files are
36    not there, or they are bogus, or the user has a non listed
37    IP.
38*/
39
40// Start the detection
41i2c_go();
42
43// Try to find out the country of the user and
44// set a cookie in the browser (in case there is no
45// cookie already storing this information)
46function i2c_go()
47{
48    global $COUNTRY;
49
50    // Get the real IP of the user
51    $ipnum = i2c_realip();
52
53    // User already has a country detected with this IP stored
54    if (!empty($_COOKIE['COUNTRY']) && strpos($_COOKIE['COUNTRY'], ',') !== false) {
55       list($COUNTRY, $storedip) = explode(",", $_COOKIE['COUNTRY']);
56       if ($storedip == $ipnum) { return true; }
57    }
58
59    // Convert the IP number to some useable form for searching
60    $ipn = (float) sprintf("%u", ip2long($ipnum));
61
62    // Find the index to start search from
63    $idx = i2c_search_in_index($ipn);
64
65    // If we were unable to find any helpful entry
66    // in the index do not search, as it would take
67    // a very long time. It is an error, if we have
68    // not found anything in the index
69    if ($idx !== false) {
70        $country = i2c_search_in_db($ipn, $idx);
71    } else {
72        $country = 'NA';
73    }
74
75    // Set global variable for further usage
76    $COUNTRY = $country;
77
78    // Set the country in a cookie for a week
79    return mirror_setcookie("COUNTRY", "$country,$ipnum", 60 * 60 * 24 * 7);
80
81}
82
83// Find nearest index entry for IP number
84function i2c_search_in_index($ip)
85{
86    // Indexed part and record number to jump to
87    $idxpart = 0; $recnum = 0;
88
89    // Open the index file for reading
90    $dbidx = fopen(
91        __DIR__ . "/../backend/ip-to-country.idx",
92        "r",
93    );
94    if (!$dbidx) { return false; }
95
96    // Read in granularity from index file and
97    // convert current IP to something useful
98    $granularity = (int) fgets($dbidx, 64);
99    if (!$granularity) {
100        // The file is empty (demo file)
101        return false;
102    }
103    $ip_chunk = (int) ($ip / $granularity);
104
105    // Loop till we can read the file
106    while (!feof($dbidx)) {
107
108        // Get CSV data from index file
109        $data = fgetcsv($dbidx, 100);
110
111        // Compare current index part with our IP
112        if ($ip_chunk >= $idxpart && $ip_chunk < (int) $data[0]) {
113            return [$recnum, (int) $data[1]];
114        }
115
116        // Store for next compare
117        $idxpart = (int) $data[0];
118        $recnum = (int) $data[1];
119    }
120
121    // Return record number found
122    return [$recnum, -1];
123}
124
125// Find the country searching from record $idx
126// $ip should be an IP number and not an IP address
127function i2c_search_in_db($ip, $idx)
128{
129    // Default range and country
130    $range_start = 0; $range_end = 0;
131    $country = "NA";
132
133    // Open DB for reading
134    $ipdb = fopen(
135        $_SERVER['DOCUMENT_ROOT'] . "/backend/ip-to-country.db",
136        "r",
137    );
138
139    // Return with "NA" in case of we cannot open the db
140    if (!$ipdb) { return $country; }
141
142    // Jump to record $idx
143    fseek($ipdb, ($idx[0] ? (($idx[0] - 1) * 24) : 0));
144
145    // Read records until we hit the end of the file,
146    // or we find the range where this IP is, or we
147    // reach the next indexed part [where the IP should
148    // not be found, so there is no point in searching further]
149    while (!feof($ipdb) && !($range_start <= $ip && $range_end >= $ip)) {
150
151        // We had run out of the indexed region,
152        // where we expected to find the IP
153        if ($idx[1] != -1 && $idx[0] > $idx[1]) {
154            $country = "NA"; break;
155        }
156
157        // Try to read record
158        $record = fread($ipdb, 24);
159
160        // Unable to read the record => error
161        if (strlen($record) != 24) { $country = "NA"; break; }
162
163        // Split the record to it's parts
164        $range_start = (float) substr($record, 0, 10);
165        $range_end = (float) substr($record, 10, 10);
166        $country = substr($record, 20, 3);
167
168        // Getting closer to the end of the indexed region
169        $idx[0] += 1;
170    }
171
172    // Close datafile
173    fclose($ipdb);
174
175    // Return with the country found
176    return $country;
177}
178
179// Check if the current country is valid
180function i2c_valid_country()
181{
182    global $COUNTRY, $COUNTRIES;
183    return (!empty($COUNTRY) && $COUNTRY != "NA" && isset($COUNTRIES[$COUNTRY]));
184}
185
186// Returns the real IP address of the user
187function i2c_realip()
188{
189    // No IP found (will be overwritten by for
190    // if any IP is found behind a firewall)
191    $ip = false;
192
193    // If HTTP_CLIENT_IP is set, then give it priority
194    if (!empty($_SERVER["HTTP_CLIENT_IP"])) {
195        $ip = $_SERVER["HTTP_CLIENT_IP"];
196    }
197
198    // User is behind a proxy and check that we discard RFC1918 IP addresses
199    // if they are behind a proxy then only figure out which IP belongs to the
200    // user.  Might not need any more hackin if there is a squid reverse proxy
201    // infront of apache.
202    if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
203
204        // Put the IP's into an array which we shall work with shortly.
205        $ips = explode(", ", $_SERVER['HTTP_X_FORWARDED_FOR']);
206        if ($ip) { array_unshift($ips, $ip); $ip = false; }
207
208        for ($i = 0; $i < count($ips); $i++) {
209            // Skip RFC 1918 IP's 10.0.0.0/8, 172.16.0.0/12 and
210            // 192.168.0.0/16
211            // Also skip RFC 6598 IP's
212            if (!preg_match('/^(?:10|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])|172\.(?:1[6-9]|2\d|3[01])|192\.168)\./', $ips[$i]) && ip2long($ips[$i])) {
213                $ip = $ips[$i];
214                break;
215            }
216        }
217    }
218
219    // Return with the found IP or the remote address
220    return $ip ?: $_SERVER['REMOTE_ADDR'];
221}
222