Skip to content

Commit 5242317

Browse files
authored
Merge pull request #99 from ipinfo/silvano/eng-283-add-lite-api-support-to-ipinfonode
Add `IPinfoLiteWrapper` to support Lite API
2 parents f9859e1 + e8b62c5 commit 5242317

File tree

6 files changed

+277
-7
lines changed

6 files changed

+277
-7
lines changed

.github/workflows/test.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
node-version: ${{ matrix.version }}
1818
- run: npm ci
1919
- run: npm test
20+
env:
21+
IPINFO_TOKEN: ${{ secrets.IPINFO_TOKEN }}
2022
- run: npm run build
2123
- run: cd test-app && npm install && node index.js
2224
env:

README.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ You'll need an IPinfo API access token, which you can get by signing up for a fr
1717

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

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

2222
### Installation
2323

@@ -57,7 +57,7 @@ const ipinfo = await ipinfoWrapper.lookupIp("1.1.1.1");
5757
5858
<details><summary>Standalone example</summary>
5959

60-
1. Create `ipinfo.js` with the following code, then replace `MY_TOKEN` with
60+
1. Create `ipinfo.js` with the following code, then replace `MY_TOKEN` with
6161
[your token](https://ipinfo.io/account/token).
6262

6363
```typescript
@@ -79,7 +79,7 @@ node ipinfo.js
7979
// ...
8080
```
8181
82-
3. Run `ipinfo.js` with an IP to lookup, like `2.2.2.2` `8.8.8.8` or
82+
3. Run `ipinfo.js` with an IP to lookup, like `2.2.2.2` `8.8.8.8` or
8383
[your IP](https://ipinfo.io/what-is-my-ip).
8484
8585
```shell
@@ -95,7 +95,7 @@ node ipinfo.js 2.2.2.2
9595
9696
Each `lookup` method will throw an error when the lookup does not complete
9797
successfully. A program that performs a lookup should catch errors unless it is
98-
desirable for the error to bubble up. For example, if your program is performing
98+
desirable for the error to bubble up. For example, if your program is performing
9999
a lookup to find the country code of an IP you can return "N/A" when catching an
100100
error.
101101
@@ -106,6 +106,23 @@ const countryCode = ipinfoWrapper
106106
.catch((error) => "N/A");
107107
```
108108
109+
### Lite API
110+
111+
The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required.
112+
113+
The returned details are slightly different from the Core API.
114+
115+
```typescript
116+
import IPinfoLiteWrapper from "node-ipinfo";
117+
118+
const ipinfoWrapper = new IPinfoLiteWrapper("MY_TOKEN");
119+
const ipinfo = await ipinfoWrapper.lookupIp("8.8.8.8");
120+
console.log(ipinfo.countryCode)
121+
// US
122+
console.log(ipinfo.country)
123+
// United States
124+
```
125+
109126
### Caching
110127
111128
This library uses an LRU cache (deletes the least-recently-used items).
@@ -224,7 +241,7 @@ const { IPinfoWrapper } = require("node-ipinfo");
224241
225242
const ipinfoWrapper = new IPinfoWrapper("MY_TOKEN");
226243
227-
const ips = ["1.1.1.1", "8.8.8.8", "1.2.3.4"];
244+
const ips = ["1.1.1.1", "8.8.8.8", "1.2.3.4"];
228245
ipinfoWrapper.getMap(ips).then(response => {
229246
console.log(response);
230247
});
@@ -239,7 +256,7 @@ const { IPinfoWrapper } = require("node-ipinfo");
239256
240257
const ipinfoWrapper = new IPinfoWrapper("MY_TOKEN");
241258
242-
const ips = ["1.1.1.1", "8.8.8.8", "1.2.3.4/country"];
259+
const ips = ["1.1.1.1", "8.8.8.8", "1.2.3.4/country"];
243260
244261
ipinfoWrapper
245262
.getBatch(ips)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as dotenv from "dotenv";
2+
import { IPBogon, IPinfoLite } from "../src/common";
3+
import IPinfoLiteWrapper from "../src/ipinfoLiteWrapper";
4+
5+
const testIfTokenIsSet = process.env.IPINFO_TOKEN ? test : test.skip;
6+
7+
beforeAll(() => {
8+
dotenv.config();
9+
});
10+
11+
describe("IPinfoLiteWrapper", () => {
12+
testIfTokenIsSet("lookupIp", async () => {
13+
const ipinfoWrapper = new IPinfoLiteWrapper(process.env.IPINFO_TOKEN!);
14+
15+
// test multiple times for cache.
16+
for (let i = 0; i < 5; i++) {
17+
const data = (await ipinfoWrapper.lookupIp(
18+
"8.8.8.8"
19+
)) as IPinfoLite;
20+
expect(data.ip).toEqual("8.8.8.8");
21+
expect(data.asn).toEqual("AS15169");
22+
expect(data.asName).toEqual("Google LLC");
23+
expect(data.asDomain).toEqual("google.com");
24+
expect(data.countryCode).toEqual("US");
25+
expect(data.country).toEqual("United States");
26+
expect(data.continentCode).toEqual("NA");
27+
expect(data.continent).toEqual("North America");
28+
expect(data.isEU).toEqual(false);
29+
}
30+
});
31+
32+
testIfTokenIsSet("isBogon", async () => {
33+
const ipinfoWrapper = new IPinfoLiteWrapper(process.env.IPINFO_TOKEN!);
34+
35+
const data = (await ipinfoWrapper.lookupIp("198.51.100.1")) as IPBogon;
36+
expect(data.ip).toEqual("198.51.100.1");
37+
expect(data.bogon).toEqual(true);
38+
});
39+
40+
test("Error is thrown for invalid token", async () => {
41+
const ipinfo = new IPinfoLiteWrapper("invalid-token");
42+
await expect(ipinfo.lookupIp("1.2.3.4")).rejects.toThrow();
43+
});
44+
45+
test("Error is thrown when response cannot be parsed as JSON", async () => {
46+
const baseUrlWithUnparseableResponse = "https://ipinfo.io/developers#";
47+
48+
const ipinfo = new IPinfoLiteWrapper(
49+
"token",
50+
undefined,
51+
undefined,
52+
undefined,
53+
baseUrlWithUnparseableResponse
54+
);
55+
56+
await expect(ipinfo.lookupIp("1.2.3.4")).rejects.toThrow();
57+
58+
const result = await ipinfo
59+
.lookupIp("1.2.3.4")
60+
.then((_) => "parseable")
61+
.catch((_) => "unparseable");
62+
63+
expect(result).toEqual("unparseable");
64+
});
65+
});

src/common.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const HOST: string = "ipinfo.io";
2+
export const HOST_LITE: string = "api.ipinfo.io/lite";
23

34
// cache version
45
export const CACHE_VSN: string = "1";
@@ -98,6 +99,23 @@ export interface IPinfo {
9899
domains: Domains;
99100
}
100101

102+
export interface IPBogon {
103+
ip: string;
104+
bogon: boolean;
105+
}
106+
107+
export interface IPinfoLite {
108+
ip: string;
109+
asn: string;
110+
asName: string;
111+
asDomain: string;
112+
countryCode: string;
113+
country: string;
114+
continentCode: string;
115+
continent: string;
116+
isEU: boolean;
117+
}
118+
101119
export interface Prefix {
102120
netblock: string;
103121
id: string;

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import IPinfoWrapper from "./ipinfoWrapper";
2+
import IPinfoLiteWrapper from "./ipinfoLiteWrapper";
23
import Cache from "./cache/cache";
34
import LruCache from "./cache/lruCache";
45
import ApiLimitError from "./errors/apiLimitError";
56

67
export { Options } from "lru-cache";
78

8-
export { Cache, LruCache, IPinfoWrapper, ApiLimitError };
9+
export { Cache, LruCache, IPinfoWrapper, IPinfoLiteWrapper, ApiLimitError };
910
export {
1011
Asn,
1112
Company,

src/ipinfoLiteWrapper.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import fetch from "node-fetch";
2+
import type { RequestInit, Response } from "node-fetch";
3+
import { defaultEuCountries } from "../config/utils";
4+
import Cache from "./cache/cache";
5+
import LruCache from "./cache/lruCache";
6+
import ApiLimitError from "./errors/apiLimitError";
7+
import { isInSubnet } from "subnet-check";
8+
import {
9+
REQUEST_TIMEOUT_DEFAULT,
10+
CACHE_VSN,
11+
HOST_LITE,
12+
BOGON_NETWORKS,
13+
IPinfoLite,
14+
IPBogon
15+
} from "./common";
16+
import VERSION from "./version";
17+
18+
const clientUserAgent = `IPinfoClient/nodejs/${VERSION}`;
19+
20+
export default class IPinfoLiteWrapper {
21+
private token: string;
22+
private baseUrl: string;
23+
private euCountries: Array<string>;
24+
private cache: Cache;
25+
private timeout: number;
26+
27+
/**
28+
* Creates IPinfoWrapper object to communicate with the [IPinfo](https://ipinfo.io/) API.
29+
*
30+
* @param token Token string provided by IPinfo for registered user.
31+
* @param cache An implementation of IPCache interface. If it is not provided
32+
* then LruCache is used as default.
33+
* @param timeout Timeout in milliseconds that controls the timeout of requests.
34+
* It defaults to 5000 i.e. 5 seconds. A timeout of 0 disables the timeout feature.
35+
* @param i18nData Internationalization data for customizing countries-related information.
36+
* @param i18nData.countries Custom countries data. If not provided, default countries data will be used.
37+
* @param i18nData.countriesFlags Custom countries flags data. If not provided, default countries flags data will be used.
38+
* @param i18nData.countriesCurrencies Custom countries currencies data. If not provided, default countries currencies data will be used.
39+
* @param i18nData.continents Custom continents data. If not provided, default continents data will be used.
40+
* @param i18nData.euCountries Custom EU countries data. If not provided or an empty array, default EU countries data will be used.
41+
*/
42+
constructor(
43+
token: string,
44+
cache?: Cache,
45+
timeout?: number,
46+
i18nData?: {
47+
euCountries?: Array<string>;
48+
},
49+
baseUrl?: string
50+
) {
51+
this.token = token;
52+
this.euCountries =
53+
i18nData?.euCountries && i18nData?.euCountries.length !== 0
54+
? i18nData.euCountries
55+
: defaultEuCountries;
56+
this.cache = cache ? cache : new LruCache();
57+
this.timeout =
58+
timeout === null || timeout === undefined
59+
? REQUEST_TIMEOUT_DEFAULT
60+
: timeout;
61+
this.baseUrl = baseUrl || `https://${HOST_LITE}`;
62+
}
63+
64+
public static cacheKey(k: string) {
65+
return `${k}:${CACHE_VSN}`;
66+
}
67+
68+
public async fetchApi(
69+
path: string,
70+
init: RequestInit = {}
71+
): Promise<Response> {
72+
const headers = {
73+
Accept: "application/json",
74+
Authorization: `Bearer ${this.token}`,
75+
"Content-Type": "application/json",
76+
"User-Agent": clientUserAgent
77+
};
78+
79+
const request = Object.assign(
80+
{
81+
timeout: this.timeout,
82+
method: "GET",
83+
compress: false
84+
},
85+
init,
86+
{ headers: Object.assign(headers, init.headers) }
87+
);
88+
89+
const url = [this.baseUrl, path].join(
90+
!this.baseUrl.endsWith("/") && !path.startsWith("/") ? "/" : ""
91+
);
92+
93+
return fetch(url, request).then((response: Response) => {
94+
if (response.status === 429) {
95+
throw new ApiLimitError();
96+
}
97+
98+
if (response.status >= 400) {
99+
throw new Error(
100+
`Received an error from the IPinfo API ` +
101+
`(using authorization ${headers["Authorization"]}) ` +
102+
`${response.status} ${response.statusText} ${response.url}`
103+
);
104+
}
105+
106+
return response;
107+
});
108+
}
109+
110+
/**
111+
* Lookup IP information using the IP.
112+
*
113+
* @param ip IP address against which the location information is required.
114+
* @return Response containing location information.
115+
*/
116+
public async lookupIp(
117+
ip: string | undefined = undefined
118+
): Promise<IPinfoLite | IPBogon> {
119+
if (ip && this.isBogon(ip)) {
120+
return {
121+
ip,
122+
bogon: true
123+
};
124+
}
125+
126+
if (!ip) {
127+
ip = "me";
128+
}
129+
130+
const data = await this.cache.get(IPinfoLiteWrapper.cacheKey(ip));
131+
132+
if (data) {
133+
return data;
134+
}
135+
136+
return this.fetchApi(ip).then(async (response) => {
137+
const data = await response.json();
138+
139+
const ipinfo = {
140+
ip: data.ip,
141+
asn: data.asn,
142+
asName: data.as_name,
143+
asDomain: data.as_domain,
144+
countryCode: data.country_code,
145+
country: data.country,
146+
continentCode: data.continent_code,
147+
continent: data.continent,
148+
isEU: this.euCountries.includes(data.country_code)
149+
};
150+
151+
this.cache.set(IPinfoLiteWrapper.cacheKey(ip), ipinfo);
152+
153+
return ipinfo;
154+
});
155+
}
156+
157+
private isBogon(ip: string): boolean {
158+
if (ip != "") {
159+
for (var network of BOGON_NETWORKS) {
160+
if (isInSubnet(ip, network)) {
161+
return true;
162+
}
163+
}
164+
}
165+
return false;
166+
}
167+
}

0 commit comments

Comments
 (0)