Skip to content

Commit 0bd9636

Browse files
authored
Merge pull request #93 from ipinfo/silvano/eng-289-add-lite-api-support-to-ipinfophp
Add `IPinfoLite` to support Lite API
2 parents 2daad17 + 8f7b6fd commit 0bd9636

File tree

6 files changed

+568
-31
lines changed

6 files changed

+568
-31
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ coverage
33
composer.lock
44
vendor
55
.phpunit.cache
6+
.env

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ You'll need an IPinfo API access token, which you can get by signing up for a fr
1414

1515
The free plan is limited to 50,000 requests per month, and doesn't include some of the data fields such as IP type and company data. To enable all the data fields and additional request volumes see [https://ipinfo.io/pricing](https://ipinfo.io/pricing?ref=lib-PHP).
1616

17-
⚠️ Note: This library does not currently support our newest free API https://ipinfo.io/lite. If you’d like to use IPinfo Lite, you can call the [endpoint directly](https://ipinfo.io/developers/lite-api) using your preferred HTTP client. Developers are also welcome to contribute support for Lite by submitting a pull request.
17+
The library also supports the Lite API, see the [Lite API section](#lite-api) for more info.
1818

1919
#### Installation
2020

@@ -233,6 +233,21 @@ $details->all;
233233
*/
234234
```
235235

236+
### Lite API
237+
238+
The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required.
239+
240+
The returned details are slightly different from the Core API.
241+
242+
```php
243+
$access_token = '123456789abc';
244+
$client = new IPinfoLite($access_token);
245+
246+
$res = $client->getDetails("8.8.8.8")
247+
$res->country_code // US
248+
$res->country // United States
249+
```
250+
236251
### Caching
237252

238253
In-memory caching of `Details` data is provided by default via the [symfony/cache](https://github.com/symfony/cache/) library. LRU (least recently used) cache-invalidation functionality has been added to the default TTL (time to live). This means that values will be cached for the specified duration; if the cache's max size is reached, cache values will be invalidated as necessary, starting with the oldest cached value.

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)