Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ coverage
composer.lock
vendor
.phpunit.cache
.env
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ You'll need an IPinfo API access token, which you can get by signing up for a fr

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).

⚠️ 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.
The library also supports the Lite API, see the [Lite API section](#lite-api) for more info.

#### Installation

Expand Down Expand Up @@ -233,6 +233,21 @@ $details->all;
*/
```

### Lite API

The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required.

The returned details are slightly different from the Core API.

```php
$access_token = '123456789abc';
$client = new IPinfoLite($access_token);

$res = $client->getDetails("8.8.8.8")
$res->country_code // US
$res->country // United States
```

### Caching

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.
Expand Down
43 changes: 43 additions & 0 deletions src/DetailsLite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace ipinfo\ipinfo;

/**
* Holds formatted data received from Lite API for a single IP address.
*/
class DetailsLite
{
public $ip;
public $asn;
public $as_name;
public $as_domain;
public $country_code;
public $country;
public $continent_code;
public $continent;
public $country_name;
public $is_eu;
public $country_flag;
public $country_flag_url;
public $country_currency;
public $bogon;
public $all;

public function __construct($raw_details)
{
foreach ($raw_details as $property => $value) {
$this->$property = $value;
}
$this->all = $raw_details;
}

/**
* Returns json string representation.
*
* @internal this class should implement Stringable explicitly when leaving support for PHP verision < 8.0
*/
public function __toString(): string
{
return json_encode($this);
}
}
283 changes: 283 additions & 0 deletions src/IPinfoLite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
<?php

namespace ipinfo\ipinfo;

require_once __DIR__ . "/Const.php";

use Exception;
use ipinfo\ipinfo\cache\DefaultCache;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Promise;
use Symfony\Component\HttpFoundation\IpUtils;

/**
* Exposes the IPinfo library to client code.
*/
class IPinfoLite
{
const API_URL = "https://api.ipinfo.io/lite";
const COUNTRY_FLAG_URL = "https://cdn.ipinfo.io/static/images/countries-flags/";
const STATUS_CODE_QUOTA_EXCEEDED = 429;
const REQUEST_TIMEOUT_DEFAULT = 2; // seconds

const CACHE_MAXSIZE = 4096;
const CACHE_TTL = 86400; // 24 hours as seconds
const CACHE_KEY_VSN = "1"; // update when cache vals change for same key.

const COUNTRIES_DEFAULT = COUNTRIES;
const EU_COUNTRIES_DEFAULT = EU;
const COUNTRIES_FLAGS_DEFAULT = FLAGS;
const COUNTRIES_CURRENCIES_DEFAULT = CURRENCIES;
const CONTINENTS_DEFAULT = CONTINENTS;

const BATCH_MAX_SIZE = 1000;
const BATCH_TIMEOUT = 5; // seconds

public $access_token;
public $settings;
public $cache;
public $countries;
public $eu_countries;
public $countries_flags;
public $countries_currencies;
public $continents;
protected $http_client;

public function __construct($access_token = null, $settings = [])
{
$this->access_token = $access_token;
$this->settings = $settings;

/*
Support a timeout first-class, then a `guzzle_opts` key that can
override anything.
*/
$guzzle_opts = [
"http_errors" => false,
"headers" => $this->buildHeaders(),
"timeout" => $settings["timeout"] ?? self::REQUEST_TIMEOUT_DEFAULT,
];
if (isset($settings["guzzle_opts"])) {
$guzzle_opts = array_merge($guzzle_opts, $settings["guzzle_opts"]);
}
$this->http_client = new Client($guzzle_opts);

$this->countries = $settings["countries"] ?? self::COUNTRIES_DEFAULT;
$this->countries_flags =
$settings["countries_flags"] ?? self::COUNTRIES_FLAGS_DEFAULT;
$this->countries_currencies =
$settings["countries_currencies"] ??
self::COUNTRIES_CURRENCIES_DEFAULT;
$this->eu_countries =
$settings["eu_countries"] ?? self::EU_COUNTRIES_DEFAULT;
$this->continents = $settings["continents"] ?? self::CONTINENTS_DEFAULT;

if (
!array_key_exists("cache_disabled", $this->settings) ||
$this->settings["cache_disabled"] == false
) {
if (array_key_exists("cache", $settings)) {
$this->cache = $settings["cache"];
} else {
$maxsize = $settings["cache_maxsize"] ?? self::CACHE_MAXSIZE;
$ttl = $settings["cache_ttl"] ?? self::CACHE_TTL;
$this->cache = new DefaultCache($maxsize, $ttl);
}
} else {
$this->cache = null;
}
}

/**
* Get formatted details for an IP address.
* @param string|null $ip_address IP address to look up.
* @return Details Formatted IPinfo data.
* @throws IPinfoException
*/
public function getDetails($ip_address = null)
{
$response_details = $this->getRequestDetails((string) $ip_address);
return $this->formatDetailsObject($response_details);
}

public function formatDetailsObject($details = [])
{
$country_code = $details["country_code"] ?? null;
$details["country_name"] = $details["country"] ?? null;
$details["is_eu"] = in_array($country_code, $this->eu_countries);
$details["country_flag"] =
$this->countries_flags[$country_code] ?? null;
$details["country_flag_url"] =
self::COUNTRY_FLAG_URL . $country_code . ".svg";
$details["country_currency"] =
$this->countries_currencies[$country_code] ?? null;

return new DetailsLite($details);
}

/**
* Get details for a specific IP address.
* @param string $ip_address IP address to query API for.
* @return array IP response data.
* @throws IPinfoException
*/
public function getRequestDetails(string $ip_address)
{
if (
// Avoid checking if bogon if the user provided no IP or explicitly
// set it to "me" to get its IP info
$ip_address &&
$ip_address != "me" &&
$this->isBogon($ip_address)
) {
return [
"ip" => $ip_address,
"bogon" => true,
];
}

if ($this->cache != null) {
$cachedRes = $this->cache->get($this->cacheKey($ip_address));
if ($cachedRes != null) {
return $cachedRes;
}
}

$url = self::API_URL;
if ($ip_address) {
$url .= "/$ip_address";
} else {
$url .= "/me";
}

try {
$response = $this->http_client->request("GET", $url);
} catch (GuzzleException $e) {
throw new IPinfoException($e->getMessage());
} catch (Exception $e) {
throw new IPinfoException($e->getMessage());
}

if ($response->getStatusCode() == self::STATUS_CODE_QUOTA_EXCEEDED) {
throw new IPinfoException("IPinfo request quota exceeded.");
} elseif ($response->getStatusCode() >= 400) {
throw new IPinfoException(
"Exception: " .
json_encode([
"status" => $response->getStatusCode(),
"reason" => $response->getReasonPhrase(),
])
);
}

$raw_details = json_decode($response->getBody(), true);

if ($this->cache != null) {
$this->cache->set($this->cacheKey($ip_address), $raw_details);
}

return $raw_details;
}

/**
* Build headers for API request.
* @return array Headers for API request.
*/
private function buildHeaders()
{
$headers = [
"user-agent" => "IPinfoClient/PHP/3.1.4",
"accept" => "application/json",
"content-type" => "application/json",
];

if ($this->access_token) {
$headers["authorization"] = "Bearer {$this->access_token}";
}

return $headers;
}

/**
* Returns a versioned cache key given a user-input key.
* @param string $k key to transform into a versioned cache key.
* @return string the versioned cache key.
*/
private function cacheKey($k)
{
return sprintf("%s_v%s", $k, self::CACHE_KEY_VSN);
}

/**
* Check if an IP address is a bogon.
*
* @param string $ip The IP address to check
* @return bool True if the IP address is a bogon, false otherwise
*/
public function isBogon($ip)
{
// Check if the IP address is in the range
return IpUtils::checkIp($ip, $this->bogonNetworks);
}

// List of bogon CIDRs.
protected $bogonNetworks = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to do this now, but we should refactor this duplication with src/IPinfo.php

"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/4",
"240.0.0.0/4",
"255.255.255.255/32",
"::/128",
"::1/128",
"::ffff:0:0/96",
"::/96",
"100::/64",
"2001:10::/28",
"2001:db8::/32",
"fc00::/7",
"fe80::/10",
"fec0::/10",
"ff00::/8",
"2002::/24",
"2002:a00::/24",
"2002:7f00::/24",
"2002:a9fe::/32",
"2002:ac10::/28",
"2002:c000::/40",
"2002:c000:200::/40",
"2002:c0a8::/32",
"2002:c612::/31",
"2002:c633:6400::/40",
"2002:cb00:7100::/40",
"2002:e000::/20",
"2002:f000::/20",
"2002:ffff:ffff::/48",
"2001::/40",
"2001:0:a00::/40",
"2001:0:7f00::/40",
"2001:0:a9fe::/48",
"2001:0:ac10::/44",
"2001:0:c000::/56",
"2001:0:c000:200::/56",
"2001:0:c0a8::/48",
"2001:0:c612::/47",
"2001:0:c633:6400::/56",
"2001:0:cb00:7100::/56",
"2001:0:e000::/36",
"2001:0:f000::/36",
"2001:0:ffff:ffff::/64",
];
}
Loading
Loading