|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace ipinfo\ipinfo; |
| 4 | + |
| 5 | +require_once __DIR__ . "/Const.php"; |
| 6 | + |
| 7 | +use Exception; |
| 8 | +use ipinfo\ipinfo\cache\DefaultCache; |
| 9 | +use GuzzleHttp\Client; |
| 10 | +use Symfony\Component\HttpFoundation\IpUtils; |
| 11 | + |
| 12 | +/** |
| 13 | + * Exposes the IPinfo Plus API to client code. |
| 14 | + */ |
| 15 | +class IPinfoPlus |
| 16 | +{ |
| 17 | + const API_URL = "https://api.ipinfo.io/lookup"; |
| 18 | + const COUNTRY_FLAG_URL = "https://cdn.ipinfo.io/static/images/countries-flags/"; |
| 19 | + const STATUS_CODE_QUOTA_EXCEEDED = 429; |
| 20 | + const REQUEST_TIMEOUT_DEFAULT = 2; // seconds |
| 21 | + |
| 22 | + const CACHE_MAXSIZE = 4096; |
| 23 | + const CACHE_TTL = 86400; // 24 hours as seconds |
| 24 | + const CACHE_KEY_VSN = "1"; // update when cache vals change for same key. |
| 25 | + |
| 26 | + const COUNTRIES_DEFAULT = COUNTRIES; |
| 27 | + const EU_COUNTRIES_DEFAULT = EU; |
| 28 | + const COUNTRIES_FLAGS_DEFAULT = FLAGS; |
| 29 | + const COUNTRIES_CURRENCIES_DEFAULT = CURRENCIES; |
| 30 | + const CONTINENTS_DEFAULT = CONTINENTS; |
| 31 | + |
| 32 | + public $access_token; |
| 33 | + public $settings; |
| 34 | + public $cache; |
| 35 | + public $countries; |
| 36 | + public $eu_countries; |
| 37 | + public $countries_flags; |
| 38 | + public $countries_currencies; |
| 39 | + public $continents; |
| 40 | + protected $http_client; |
| 41 | + |
| 42 | + public function __construct($access_token = null, $settings = []) |
| 43 | + { |
| 44 | + $this->access_token = $access_token; |
| 45 | + $this->settings = $settings; |
| 46 | + |
| 47 | + /* |
| 48 | + Support a timeout first-class, then a `guzzle_opts` key that can |
| 49 | + override anything. |
| 50 | + */ |
| 51 | + $guzzle_opts = [ |
| 52 | + "http_errors" => false, |
| 53 | + "headers" => $this->buildHeaders(), |
| 54 | + "timeout" => $settings["timeout"] ?? self::REQUEST_TIMEOUT_DEFAULT, |
| 55 | + ]; |
| 56 | + if (isset($settings["guzzle_opts"])) { |
| 57 | + $guzzle_opts = array_merge($guzzle_opts, $settings["guzzle_opts"]); |
| 58 | + } |
| 59 | + $this->http_client = new Client($guzzle_opts); |
| 60 | + |
| 61 | + $this->countries = $settings["countries"] ?? self::COUNTRIES_DEFAULT; |
| 62 | + $this->countries_flags = |
| 63 | + $settings["countries_flags"] ?? self::COUNTRIES_FLAGS_DEFAULT; |
| 64 | + $this->countries_currencies = |
| 65 | + $settings["countries_currencies"] ?? |
| 66 | + self::COUNTRIES_CURRENCIES_DEFAULT; |
| 67 | + $this->eu_countries = |
| 68 | + $settings["eu_countries"] ?? self::EU_COUNTRIES_DEFAULT; |
| 69 | + $this->continents = $settings["continents"] ?? self::CONTINENTS_DEFAULT; |
| 70 | + |
| 71 | + if ( |
| 72 | + !array_key_exists("cache_disabled", $this->settings) || |
| 73 | + $this->settings["cache_disabled"] == false |
| 74 | + ) { |
| 75 | + if (array_key_exists("cache", $settings)) { |
| 76 | + $this->cache = $settings["cache"]; |
| 77 | + } else { |
| 78 | + $maxsize = $settings["cache_maxsize"] ?? self::CACHE_MAXSIZE; |
| 79 | + $ttl = $settings["cache_ttl"] ?? self::CACHE_TTL; |
| 80 | + $this->cache = new DefaultCache($maxsize, $ttl); |
| 81 | + } |
| 82 | + } else { |
| 83 | + $this->cache = null; |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + /** |
| 88 | + * Get formatted details for an IP address. |
| 89 | + * @param string|null $ip_address IP address to look up. |
| 90 | + * @return DetailsPlus Formatted IPinfo data. |
| 91 | + * @throws IPinfoException |
| 92 | + */ |
| 93 | + public function getDetails($ip_address = null) |
| 94 | + { |
| 95 | + $response_details = $this->getRequestDetails((string) $ip_address); |
| 96 | + return $this->formatDetailsObject($response_details); |
| 97 | + } |
| 98 | + |
| 99 | + public function formatDetailsObject($details = []) |
| 100 | + { |
| 101 | + // Enrich geo object if present |
| 102 | + if (isset($details["geo"]) && isset($details["geo"]["country_code"])) { |
| 103 | + $country_code = $details["geo"]["country_code"]; |
| 104 | + $details["geo"]["country_name"] = $this->countries[$country_code] ?? null; |
| 105 | + $details["geo"]["is_eu"] = in_array($country_code, $this->eu_countries); |
| 106 | + $details["geo"]["country_flag"] = |
| 107 | + $this->countries_flags[$country_code] ?? null; |
| 108 | + $details["geo"]["country_flag_url"] = |
| 109 | + self::COUNTRY_FLAG_URL . $country_code . ".svg"; |
| 110 | + $details["geo"]["country_currency"] = |
| 111 | + $this->countries_currencies[$country_code] ?? null; |
| 112 | + $details["geo"]["continent_info"] = |
| 113 | + $this->continents[$country_code] ?? null; |
| 114 | + } |
| 115 | + |
| 116 | + // Enrich abuse object with country_name if present |
| 117 | + if (isset($details["abuse"]) && isset($details["abuse"]["country"])) { |
| 118 | + $country_code = $details["abuse"]["country"]; |
| 119 | + $details["abuse"]["country_name"] = $this->countries[$country_code] ?? null; |
| 120 | + } |
| 121 | + |
| 122 | + return new DetailsPlus($details); |
| 123 | + } |
| 124 | + |
| 125 | + /** |
| 126 | + * Get details for a specific IP address. |
| 127 | + * @param string $ip_address IP address to query API for. |
| 128 | + * @return array IP response data. |
| 129 | + * @throws IPinfoException |
| 130 | + */ |
| 131 | + public function getRequestDetails(string $ip_address) |
| 132 | + { |
| 133 | + if ( |
| 134 | + // Avoid checking if bogon if the user provided no IP or explicitly |
| 135 | + // set it to "me" to get its IP info |
| 136 | + $ip_address && |
| 137 | + $ip_address != "me" && |
| 138 | + $this->isBogon($ip_address) |
| 139 | + ) { |
| 140 | + return [ |
| 141 | + "ip" => $ip_address, |
| 142 | + "bogon" => true, |
| 143 | + ]; |
| 144 | + } |
| 145 | + |
| 146 | + if ($this->cache != null) { |
| 147 | + $cachedRes = $this->cache->get($this->cacheKey($ip_address)); |
| 148 | + if ($cachedRes != null) { |
| 149 | + return $cachedRes; |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + $url = self::API_URL; |
| 154 | + if ($ip_address) { |
| 155 | + $url .= "/$ip_address"; |
| 156 | + } else { |
| 157 | + $url .= "/me"; |
| 158 | + } |
| 159 | + |
| 160 | + try { |
| 161 | + $response = $this->http_client->request("GET", $url); |
| 162 | + } catch (GuzzleException $e) { |
| 163 | + throw new IPinfoException($e->getMessage()); |
| 164 | + } catch (Exception $e) { |
| 165 | + throw new IPinfoException($e->getMessage()); |
| 166 | + } |
| 167 | + |
| 168 | + if ($response->getStatusCode() == self::STATUS_CODE_QUOTA_EXCEEDED) { |
| 169 | + throw new IPinfoException("IPinfo request quota exceeded."); |
| 170 | + } elseif ($response->getStatusCode() >= 400) { |
| 171 | + throw new IPinfoException( |
| 172 | + "Exception: " . |
| 173 | + json_encode([ |
| 174 | + "status" => $response->getStatusCode(), |
| 175 | + "reason" => $response->getReasonPhrase(), |
| 176 | + ]) |
| 177 | + ); |
| 178 | + } |
| 179 | + |
| 180 | + $raw_details = json_decode($response->getBody(), true); |
| 181 | + |
| 182 | + if ($this->cache != null) { |
| 183 | + $this->cache->set($this->cacheKey($ip_address), $raw_details); |
| 184 | + } |
| 185 | + |
| 186 | + return $raw_details; |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * Build headers for API request. |
| 191 | + * @return array Headers for API request. |
| 192 | + */ |
| 193 | + private function buildHeaders() |
| 194 | + { |
| 195 | + $headers = [ |
| 196 | + "user-agent" => "IPinfoClient/PHP/3.2.0", |
| 197 | + "accept" => "application/json", |
| 198 | + "content-type" => "application/json", |
| 199 | + ]; |
| 200 | + |
| 201 | + if ($this->access_token) { |
| 202 | + $headers["authorization"] = "Bearer {$this->access_token}"; |
| 203 | + } |
| 204 | + |
| 205 | + return $headers; |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * Check if IP address is bogon. |
| 210 | + * @param string $ip_address IP address. |
| 211 | + * @return bool true if bogon, else false. |
| 212 | + */ |
| 213 | + private function isBogon(string $ip_address) |
| 214 | + { |
| 215 | + return IpUtils::checkIp($ip_address, [ |
| 216 | + "0.0.0.0/8", |
| 217 | + "10.0.0.0/8", |
| 218 | + "100.64.0.0/10", |
| 219 | + "127.0.0.0/8", |
| 220 | + "169.254.0.0/16", |
| 221 | + "172.16.0.0/12", |
| 222 | + "192.0.0.0/24", |
| 223 | + "192.0.2.0/24", |
| 224 | + "192.168.0.0/16", |
| 225 | + "198.18.0.0/15", |
| 226 | + "198.51.100.0/24", |
| 227 | + "203.0.113.0/24", |
| 228 | + "224.0.0.0/4", |
| 229 | + "240.0.0.0/4", |
| 230 | + "255.255.255.255/32", |
| 231 | + "::/128", |
| 232 | + "::1/128", |
| 233 | + "::ffff:0:0/96", |
| 234 | + "::/96", |
| 235 | + "100::/64", |
| 236 | + "2001:10::/28", |
| 237 | + "2001:db8::/32", |
| 238 | + "fc00::/7", |
| 239 | + "fe80::/10", |
| 240 | + "fec0::/10", |
| 241 | + "ff00::/8", |
| 242 | + "2002::/24", |
| 243 | + "2002:a00::/24", |
| 244 | + "2002:7f00::/24", |
| 245 | + "2002:a9fe::/32", |
| 246 | + "2002:ac10::/28", |
| 247 | + "2002:c000::/40", |
| 248 | + "2002:c000:200::/40", |
| 249 | + "2002:c0a8::/32", |
| 250 | + "2002:c612::/31", |
| 251 | + "2002:c633:6400::/40", |
| 252 | + "2002:cb00:7100::/40", |
| 253 | + "2002:e000::/20", |
| 254 | + "2002:f000::/20", |
| 255 | + "2002:ffff:ffff::/48", |
| 256 | + "2001::/40", |
| 257 | + "2001:0:a00::/40", |
| 258 | + "2001:0:7f00::/40", |
| 259 | + "2001:0:a9fe::/48", |
| 260 | + "2001:0:ac10::/44", |
| 261 | + "2001:0:c000::/56", |
| 262 | + "2001:0:c000:200::/56", |
| 263 | + "2001:0:c0a8::/48", |
| 264 | + "2001:0:c612::/47", |
| 265 | + "2001:0:c633:6400::/56", |
| 266 | + "2001:0:cb00:7100::/56", |
| 267 | + "2001:0:e000::/36", |
| 268 | + "2001:0:f000::/36", |
| 269 | + "2001:0:ffff:ffff::/64", |
| 270 | + ]); |
| 271 | + } |
| 272 | + |
| 273 | + /** |
| 274 | + * Generate cache key for an IP address. |
| 275 | + * @param string $ip IP address. |
| 276 | + * @return string Cache key. |
| 277 | + */ |
| 278 | + private function cacheKey(string $ip) |
| 279 | + { |
| 280 | + return "plus_" . self::CACHE_KEY_VSN . "_" . $ip; |
| 281 | + } |
| 282 | +} |
0 commit comments