Skip to content

Commit 6ef334f

Browse files
authored
Merge pull request #99 from ipinfo/silvano/eng-497-add-plus-bundle-support-in-ipinfophp-library
Add support for Plus bundle
2 parents da53d85 + d794c32 commit 6ef334f

File tree

3 files changed

+553
-0
lines changed

3 files changed

+553
-0
lines changed

src/DetailsPlus.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace ipinfo\ipinfo;
4+
5+
/**
6+
* Holds formatted data received from Plus API for a single IP address.
7+
*/
8+
class DetailsPlus
9+
{
10+
public $ip;
11+
public $hostname;
12+
public $geo;
13+
public $asn;
14+
public $mobile;
15+
public $anonymous;
16+
public $is_anonymous;
17+
public $is_anycast;
18+
public $is_hosting;
19+
public $is_mobile;
20+
public $is_satellite;
21+
public $abuse;
22+
public $company;
23+
public $privacy;
24+
public $domains;
25+
public $bogon;
26+
public $all;
27+
28+
public function __construct($raw_details)
29+
{
30+
foreach ($raw_details as $property => $value) {
31+
// Handle nested 'as' object - rename to 'asn' to avoid PHP keyword
32+
if ($property === 'as') {
33+
$this->asn = (object) $value;
34+
} elseif (in_array($property, ['geo', 'mobile', 'anonymous', 'abuse', 'company', 'privacy', 'domains'])) {
35+
$this->$property = (object) $value;
36+
} else {
37+
$this->$property = $value;
38+
}
39+
}
40+
$this->all = $raw_details;
41+
}
42+
43+
/**
44+
* Returns json string representation.
45+
*
46+
* @internal this class should implement Stringable explicitly when leaving support for PHP verision < 8.0
47+
*/
48+
public function __toString(): string
49+
{
50+
return json_encode($this);
51+
}
52+
}

src/IPinfoPlus.php

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
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

Comments
 (0)