Skip to content

Commit 5a42512

Browse files
committed
Add support for Lite API
1 parent f9859e1 commit 5a42512

File tree

3 files changed

+250
-0
lines changed

3 files changed

+250
-0
lines changed
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/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)