Skip to content

Commit d2c734b

Browse files
committed
Add support for Lite API
1 parent e13d7f0 commit d2c734b

File tree

3 files changed

+525
-0
lines changed

3 files changed

+525
-0
lines changed

src/DetailsLite.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace ipinfo\ipinfo;
4+
5+
/**
6+
* Holds formatted data received from Lite API for a single IP address.
7+
*/
8+
class DetailsLite
9+
{
10+
public $ip;
11+
public $asn;
12+
public $as_name;
13+
public $as_domain;
14+
public $country_code;
15+
public $country;
16+
public $continent_code;
17+
public $continent;
18+
public $country_name;
19+
public $is_eu;
20+
public $country_flag;
21+
public $country_flag_url;
22+
public $country_currency;
23+
public $bogon;
24+
public $all;
25+
26+
public function __construct($raw_details)
27+
{
28+
foreach ($raw_details as $property => $value) {
29+
$this->$property = $value;
30+
}
31+
$this->all = $raw_details;
32+
}
33+
34+
/**
35+
* Returns json string representation.
36+
*
37+
* @internal this class should implement Stringable explicitly when leaving support for PHP verision < 8.0
38+
*/
39+
public function __toString(): string
40+
{
41+
return json_encode($this);
42+
}
43+
}

src/IPinfoLite.php

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

0 commit comments

Comments
 (0)