diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..17a28c1
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,35 @@
+name: Tests
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php: ["8.1", "8.2", "8.3", "8.4"]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ coverage: pcov
+
+ - name: Validate composer.json
+ run: composer validate --strict
+
+ - name: Install dependencies
+ run: composer install --no-interaction --prefer-dist --optimize-autoloader
+
+ - name: Run tests
+ run: composer test
diff --git a/README.md b/README.md
index da0a297..8914e92 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,11 @@
# [
](https://ipinfo.io/) IPinfo Laravel Client Library
This is the official Laravel client library for the [IPinfo.io](https://ipinfo.io) IP address API, allowing you to look up your own IP address, or get any of the following details for an IP:
- - [IP geolocation](https://ipinfo.io/ip-geolocation-api) (city, region, country, postal code, latitude, and longitude)
- - [ASN details](https://ipinfo.io/asn-api) (ISP or network operator, associated domain name, and type, such as business, hosting, or company)
- - [Company information](https://ipinfo.io/ip-company-api) (the name and domain of the business that uses the IP address)
- - [Carrier details](https://ipinfo.io/ip-carrier-api) (the name of the mobile carrier and MNC and MCC for that carrier if the IP is used exclusively for mobile traffic)
+
+- [IP geolocation](https://ipinfo.io/ip-geolocation-api) (city, region, country, postal code, latitude, and longitude)
+- [ASN details](https://ipinfo.io/asn-api) (ISP or network operator, associated domain name, and type, such as business, hosting, or company)
+- [Company information](https://ipinfo.io/ip-company-api) (the name and domain of the business that uses the IP address)
+- [Carrier details](https://ipinfo.io/ip-carrier-api) (the name of the mobile carrier and MNC and MCC for that carrier if the IP is used exclusively for mobile traffic)
Check all the data we have for your IP address [here](https://ipinfo.io/what-is-my-ip).
@@ -14,7 +15,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).
-⚠️ 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
@@ -122,8 +123,8 @@ In-memory caching of `Details` data is provided by default via Laravel's file-ba
Default cache TTL and maximum size can be changed by setting values in the `$settings` argument array.
-* Default maximum cache size: 4096 (multiples of 2 are recommended to increase efficiency)
-* Default TTL: 24 hours (in minutes)
+- Default maximum cache size: 4096 (multiples of 2 are recommended to increase efficiency)
+- Default TTL: 24 hours (in minutes)
```php
'ipinfo' => [
@@ -223,10 +224,10 @@ By default, `ipinfolaravel` filters out requests that have `bot` or `spider` in
],
```
-To set your own filtering rules, *thereby replacing the default filter*, you can set `ipinfo.config` to your own, custom callable function which satisfies the following rules:
+To set your own filtering rules, _thereby replacing the default filter_, you can set `ipinfo.config` to your own, custom callable function which satisfies the following rules:
-* Accepts one request.
-* Returns *True to filter out, False to allow lookup*
+- Accepts one request.
+- Returns _True to filter out, False to allow lookup_
To use your own filter function:
@@ -259,6 +260,7 @@ object will be equal to `null`.
### Trying test application with Laravel Sail
Install Laravel Sail with:
+
```bash
cd testapp
@@ -277,6 +279,7 @@ echo "IPINFO_TOKEN=" > app/.env
```
Visit http://0.0.0.0:80. You should see a message similar to:
+
```
Hello world!
@@ -289,6 +292,21 @@ To run tests within `testapp` while Sail is up:
./vendor/bin/sail phpunit
```
+## 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, but it has the same configurations options.
+
+Add the following to the `Kernel::middleware` property:
+
+```php
+protected $middleware = [
+ ...
+ \ipinfo\ipinfolaravel\lite\ipinfolitelaravel::class,
+];
+```
+
### Other Libraries
There are official IPinfo client libraries available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. There are also many third-party libraries and integrations available for our API.
diff --git a/composer.json b/composer.json
index bd5fb91..df5f548 100644
--- a/composer.json
+++ b/composer.json
@@ -13,19 +13,20 @@
"homepage": "https://github.com/ipinfo/ipinfolaravel",
"keywords": ["Laravel", "ipinfolaravel"],
"require": {
- "illuminate/support": ">=7",
- "ipinfo/ipinfo": "^3.1.0"
+ "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0",
+ "ipinfo/ipinfo": "^3.2.0"
},
"require-dev": {
- "phpunit/phpunit": ">=8",
+ "phpunit/phpunit": "^9.6",
"mockery/mockery": "^1.4.2",
- "orchestra/testbench": ">=6.4.0",
+ "orchestra/testbench": "^8.36",
"sempro/phpunit-pretty-print": "^1.3.0",
"squizlabs/php_codesniffer": "^3.5.8"
},
"autoload": {
"psr-4": {
- "ipinfo\\ipinfolaravel\\": "src/"
+ "ipinfo\\ipinfolaravel\\": "src/",
+ "ipinfo\\ipinfolaravel\\lite\\": "src/"
}
},
"autoload-dev": {
@@ -35,7 +36,8 @@
},
"scripts": {
"check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src config",
- "fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src config"
+ "fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src config",
+ "test": "phpunit --colors=always --coverage-html=coverage"
},
"extra": {
"laravel": {
diff --git a/config/ipinfolitelaravel.php b/config/ipinfolitelaravel.php
new file mode 100644
index 0000000..87b2d3e
--- /dev/null
+++ b/config/ipinfolitelaravel.php
@@ -0,0 +1,5 @@
+merge(['ipinfo' => $details]);
+ $request->attributes->set('ipinfo', $details);
return $next($request);
}
diff --git a/src/lite/ipinfolitelaravel.php b/src/lite/ipinfolitelaravel.php
new file mode 100644
index 0000000..b6266a3
--- /dev/null
+++ b/src/lite/ipinfolitelaravel.php
@@ -0,0 +1,132 @@
+configure();
+
+ if ($this->filter && call_user_func($this->filter, $request)) {
+ $details = null;
+ } else {
+ try {
+ $details = $this->ipinfo->getDetails(
+ $this->ip_selector->getIP($request),
+ );
+ } catch (\Exception $e) {
+ $details = null;
+
+ // users can't catch this exception with their own wrapper
+ // middleware unfortunately, so we catch it for them. but for
+ // backwards-compatibility, we throw the exception again unless
+ // they've told us not to.
+ if ($this->no_except != true) {
+ throw $e;
+ }
+ }
+ }
+
+ $request->attributes->set("ipinfo", $details);
+
+ return $next($request);
+ }
+
+ /**
+ * Determine settings based on user-defined configs or use defaults.
+ */
+ public function configure()
+ {
+ $this->access_token = config("services.ipinfo.access_token", null);
+ $this->filter = config("services.ipinfo.filter", [
+ $this,
+ "defaultFilter",
+ ]);
+ $this->no_except = config("services.ipinfo.no_except", false);
+ $this->ip_selector = config(
+ "services.ipinfo.ip_selector",
+ new DefaultIPSelector(),
+ );
+
+ if (
+ $custom_countries = config("services.ipinfo.countries_file", null)
+ ) {
+ $this->settings["countries_file"] = $custom_countries;
+ }
+
+ if ($custom_cache = config("services.ipinfo.cache", null)) {
+ $this->settings["cache"] = $custom_cache;
+ } else {
+ $maxsize = config(
+ "services.ipinfo.cache_maxsize",
+ self::CACHE_MAXSIZE,
+ );
+ $ttl = config("services.ipinfo.cache_ttl", self::CACHE_TTL);
+ $this->settings["cache"] = new DefaultCache($maxsize, $ttl);
+ }
+
+ $this->ipinfo = new IPinfoLiteClient(
+ $this->access_token,
+ $this->settings,
+ );
+ }
+
+ /**
+ * Should IP lookup be skipped.
+ * @param Request $request Request object.
+ * @return bool Whether or not to filter out.
+ */
+ public function defaultFilter($request)
+ {
+ $user_agent = $request->header("user-agent");
+ if ($user_agent) {
+ $lower_user_agent = strtolower($user_agent);
+
+ $is_spider = strpos($lower_user_agent, "spider") !== false;
+ $is_bot = strpos($lower_user_agent, "bot") !== false;
+
+ return $is_spider || $is_bot;
+ }
+
+ return false;
+ }
+}
diff --git a/src/lite/ipinfolitelaravelServiceProvider.php b/src/lite/ipinfolitelaravelServiceProvider.php
new file mode 100644
index 0000000..4c270f3
--- /dev/null
+++ b/src/lite/ipinfolitelaravelServiceProvider.php
@@ -0,0 +1,64 @@
+app->runningInConsole()) {
+ $this->bootForConsole();
+ }
+ }
+
+ /**
+ * Register any package services.
+ * @return void
+ */
+ public function register()
+ {
+ $this->mergeConfigFrom(
+ __DIR__ . "/../../config/ipinfolitelaravel.php",
+ "ipinfolitelaravel",
+ );
+
+ // Register the service the package provides.
+ $this->app->singleton(
+ "ipinfolitelaravel",
+ fn($app) => new ipinfolitelaravel(),
+ );
+ }
+
+ /**
+ * Get the services provided by the provider.
+ * @return array
+ */
+ public function provides()
+ {
+ return ["ipinfolitelaravel"];
+ }
+
+ /**
+ * Console-specific booting.
+ * @return void
+ */
+ protected function bootForConsole()
+ {
+ // Publishing the configuration file.
+ $this->publishes(
+ [
+ __DIR__ . "/../../config/ipinfolitelaravel.php" => config_path(
+ "ipinfolitelaravel.php",
+ ),
+ ],
+ "ipinfolitelaravel.config",
+ );
+ }
+}
diff --git a/tests/IpinfolaravelTest.php b/tests/IpinfolaravelTest.php
new file mode 100644
index 0000000..8e647cc
--- /dev/null
+++ b/tests/IpinfolaravelTest.php
@@ -0,0 +1,151 @@
+getMockBuilder(ipinfolaravel::class)
+ ->onlyMethods(["configure"])
+ ->getMock();
+ $mw->method("configure")->willReturn(null);
+ $mw->ipinfo = $client;
+ $mw->ip_selector = $selector;
+ $mw->filter = $filter;
+ $mw->no_except = $noExcept;
+ return $mw;
+ }
+
+ public function test_handle_merges_details_on_success()
+ {
+ // mock IPinfoClient
+ $details = (object) ["city" => "TestCity", "ip" => "1.2.3.4"];
+ $client = $this->createMock(IPinfoClient::class);
+ $client
+ ->expects($this->once())
+ ->method("getDetails")
+ ->with("1.2.3.4")
+ ->willReturn($details);
+
+ // mock IP selector
+ $selector = $this->createMock(IPHandlerInterface::class);
+ $selector->method("getIP")->willReturn("1.2.3.4");
+
+ $mw = $this->makeMiddlewareWithMocks($client, $selector);
+
+ $request = Request::create("/foo", "GET");
+ $captured = null;
+ $next = function ($req) use (&$captured) {
+ $captured = $req->get("ipinfo");
+ return new Response("OK", 200);
+ };
+
+ $resp = $mw->handle($request, $next);
+
+ $this->assertSame($details, $captured);
+ $this->assertEquals(200, $resp->getStatusCode());
+ $this->assertEquals("OK", $resp->getContent());
+ }
+
+ public function test_handle_skips_lookup_when_filter_returns_true()
+ {
+ $client = $this->createMock(IPinfoClient::class);
+ $client->expects($this->never())->method("getDetails");
+
+ $selector = $this->createMock(IPHandlerInterface::class);
+ $selector->expects($this->never())->method("getIP");
+
+ $filter = fn($req) => true;
+
+ $mw = $this->makeMiddlewareWithMocks($client, $selector, $filter);
+
+ $request = Request::create("/foo", "GET");
+ $request->headers->set("user-agent", "my-bot");
+
+ $captured = "unset";
+ $next = function ($req) use (&$captured) {
+ $captured = $req->get("ipinfo");
+ return new Response();
+ };
+
+ $mw->handle($request, $next);
+ $this->assertNull($captured);
+ }
+
+ public function test_handle_throws_if_client_throws_and_no_except_false()
+ {
+ $this->expectException(\Exception::class);
+
+ $client = $this->createMock(IPinfoClient::class);
+ $client
+ ->method("getDetails")
+ ->willThrowException(new \Exception("fail"));
+
+ $selector = $this->createMock(IPHandlerInterface::class);
+ $selector->method("getIP")->willReturn("1.2.3.4");
+
+ $mw = $this->makeMiddlewareWithMocks($client, $selector, null, false);
+
+ $mw->handle(Request::create("/", "GET"), function () {});
+ }
+
+ public function test_handle_swallows_if_client_throws_and_no_except_true()
+ {
+ $client = $this->createMock(IPinfoClient::class);
+ $client
+ ->method("getDetails")
+ ->willThrowException(new \Exception("fail"));
+
+ $selector = $this->createMock(IPHandlerInterface::class);
+ $selector->method("getIP")->willReturn("1.2.3.4");
+
+ $mw = $this->makeMiddlewareWithMocks($client, $selector, null, true);
+
+ $captured = "unset";
+ $next = function ($req) use (&$captured) {
+ $captured = $req->get("ipinfo");
+ return new Response();
+ };
+
+ $mw->handle(Request::create("/", "GET"), $next);
+ $this->assertNull($captured);
+ }
+
+ public function test_defaultFilter_detects_bots_and_spiders()
+ {
+ $mw = new ipinfolaravel();
+
+ $r1 = Request::create("/", "GET");
+ $r1->headers->set("user-agent", "GoogleBot");
+ $this->assertTrue($mw->defaultFilter($r1));
+
+ $r2 = Request::create("/", "GET");
+ $r2->headers->set("user-agent", "SomeSpider/1.0");
+ $this->assertTrue($mw->defaultFilter($r2));
+
+ $r3 = Request::create("/", "GET");
+ $r3->headers->set("user-agent", "Mozilla/5.0");
+ $this->assertFalse($mw->defaultFilter($r3));
+
+ $r4 = Request::create("/", "GET");
+ $this->assertFalse($mw->defaultFilter($r4));
+ }
+}
diff --git a/tests/IpinfolitelaravelTest.php b/tests/IpinfolitelaravelTest.php
new file mode 100644
index 0000000..ac12f65
--- /dev/null
+++ b/tests/IpinfolitelaravelTest.php
@@ -0,0 +1,145 @@
+getMockBuilder(ipinfolitelaravel::class)
+ ->onlyMethods(["configure"])
+ ->getMock();
+ $mw->method("configure")->willReturn(null);
+ $mw->ipinfo = $client;
+ $mw->ip_selector = $selector;
+ $mw->filter = $filter;
+ $mw->no_except = $noExcept;
+ return $mw;
+ }
+
+ public function test_handle_merges_details_on_success()
+ {
+ $details = (object) ["country" => "US", "ip" => "8.8.8.8"];
+ $client = $this->createMock(IPinfoLiteClient::class);
+ $client
+ ->expects($this->once())
+ ->method("getDetails")
+ ->with("8.8.8.8")
+ ->willReturn($details);
+
+ $selector = $this->createMock(IPHandlerInterface::class);
+ $selector->method("getIP")->willReturn("8.8.8.8");
+
+ $mw = $this->makeMiddleware($client, $selector);
+
+ $request = Request::create("/foo", "GET");
+ $next = function ($req) use (&$out) {
+ $out = $req->get("ipinfo");
+ return new Response("OK", 200);
+ };
+
+ $resp = $mw->handle($request, $next);
+
+ $this->assertSame($details, $out);
+ $this->assertEquals(200, $resp->getStatusCode());
+ $this->assertEquals("OK", $resp->getContent());
+ }
+
+ public function test_handle_skips_lookup_when_filter_true()
+ {
+ $client = $this->createMock(IPinfoLiteClient::class);
+ $client->expects($this->never())->method("getDetails");
+
+ $selector = $this->createMock(IPHandlerInterface::class);
+ $selector->expects($this->never())->method("getIP");
+
+ $filter = fn($req) => true;
+ $mw = $this->makeMiddleware($client, $selector, $filter);
+
+ $request = Request::create("/", "GET");
+ $request->headers->set("user-agent", "GoogleBot");
+
+ $next = function ($req) use (&$out) {
+ $out = $req->get("ipinfo");
+ return new Response();
+ };
+
+ $mw->handle($request, $next);
+ $this->assertNull($out);
+ }
+
+ public function test_handle_throws_if_client_throws_and_no_except_false()
+ {
+ $this->expectException(\Exception::class);
+
+ $client = $this->createMock(IPinfoLiteClient::class);
+ $client
+ ->method("getDetails")
+ ->willThrowException(new \Exception("boom"));
+
+ $selector = $this->createMock(IPHandlerInterface::class);
+ $selector->method("getIP")->willReturn("1.1.1.1");
+
+ $mw = $this->makeMiddleware($client, $selector, null, false);
+ $mw->handle(Request::create("/", "GET"), fn($r) => new Response());
+ }
+
+ public function test_handle_swallows_if_client_throws_and_no_except_true()
+ {
+ $client = $this->createMock(IPinfoLiteClient::class);
+ $client
+ ->method("getDetails")
+ ->willThrowException(new \Exception("boom"));
+
+ $selector = $this->createMock(IPHandlerInterface::class);
+ $selector->method("getIP")->willReturn("1.1.1.1");
+
+ $mw = $this->makeMiddleware($client, $selector, null, true);
+
+ $next = function ($req) use (&$out) {
+ $out = $req->get("ipinfo");
+ return new Response();
+ };
+
+ $mw->handle(Request::create("/", "GET"), $next);
+ $this->assertNull($out);
+ }
+
+ public function test_defaultFilter_detects_bots_and_spiders()
+ {
+ $mw = new ipinfolitelaravel();
+
+ $r1 = Request::create("/", "GET");
+ $r1->headers->set("user-agent", "MySpider");
+ $this->assertTrue($mw->defaultFilter($r1));
+
+ $r2 = Request::create("/", "GET");
+ $r2->headers->set("user-agent", "someBOT/2.0");
+ $this->assertTrue($mw->defaultFilter($r2));
+
+ $r3 = Request::create("/", "GET");
+ $r3->headers->set("user-agent", "normal");
+ $this->assertFalse($mw->defaultFilter($r3));
+
+ $r4 = Request::create("/", "GET");
+ $this->assertFalse($mw->defaultFilter($r4));
+ }
+}