Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 4.3.0

- Add support for IPinfo Residential Proxy API

## 4.2.0

- Add support for IPinfo Core API
Expand Down
61 changes: 61 additions & 0 deletions __tests__/ipinfoResProxyWrapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as dotenv from "dotenv";
import { IPBogon, IPinfoResProxy } from "../src/common";
import IPinfoResProxyWrapper from "../src/ipinfoResProxyWrapper";

const testIfTokenIsSet = process.env.IPINFO_TOKEN ? test : test.skip;

beforeAll(() => {
dotenv.config();
});

describe("IPinfoResProxyWrapper", () => {
testIfTokenIsSet("lookupIp", async () => {
const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!);

for (let i = 0; i < 5; i++) {
const data = (await wrapper.lookupIp(
"139.5.0.122"
)) as IPinfoResProxy;

expect(data.ip).toEqual("139.5.0.122");
expect(data.service).toBeDefined();
expect(typeof data.service).toBe("string");
expect(data.last_seen).toBeDefined();
expect(typeof data.last_seen).toBe("string");
expect(data.percent_days_seen).toBeDefined();
expect(typeof data.percent_days_seen).toBe("number");
}
});

testIfTokenIsSet("isBogon", async () => {
const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!);

const data = (await wrapper.lookupIp("198.51.100.1")) as IPBogon;
expect(data.ip).toEqual("198.51.100.1");
expect(data.bogon).toEqual(true);
});

test("Error is thrown for invalid token", async () => {
const wrapper = new IPinfoResProxyWrapper("invalid-token");
await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow();
});

test("Error is thrown when response cannot be parsed as JSON", async () => {
const baseUrlWithUnparseableResponse = "https://ipinfo.io/developers#";
const wrapper = new IPinfoResProxyWrapper(
"token",
undefined,
undefined,
baseUrlWithUnparseableResponse
);

await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow();

const result = await wrapper
.lookupIp("1.2.3.4")
.then((_) => "parseable")
.catch((_) => "unparseable");

expect(result).toEqual("unparseable");
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-ipinfo",
"version": "4.2.0",
"version": "4.3.0",
"description": "Official Node client library for IPinfo",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
Expand Down
8 changes: 8 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const HOST: string = "ipinfo.io";
export const HOST_LITE: string = "api.ipinfo.io/lite";
export const HOST_CORE: string = "api.ipinfo.io/lookup";
export const HOST_PLUS: string = "api.ipinfo.io/lookup";
export const HOST_RES_PROXY: string = "ipinfo.io/resproxy";

// cache version
export const CACHE_VSN: string = "1";
Expand Down Expand Up @@ -202,6 +203,13 @@ export interface IPinfoPlus {
is_satellite: boolean;
}

export interface IPinfoResProxy {
ip: string;
service: string;
last_seen: string;
percent_days_seen: number;
}

export interface Prefix {
netblock: string;
id: string;
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import IPinfoWrapper from "./ipinfoWrapper";
import IPinfoLiteWrapper from "./ipinfoLiteWrapper";
import IPinfoCoreWrapper from "./ipinfoCoreWrapper";
import IPinfoPlusWrapper from "./ipinfoPlusWrapper";
import IPinfoResProxyWrapper from "./ipinfoResProxyWrapper";
import Cache from "./cache/cache";
import LruCache from "./cache/lruCache";
import ApiLimitError from "./errors/apiLimitError";
Expand All @@ -15,6 +16,7 @@ export {
IPinfoLiteWrapper,
IPinfoCoreWrapper,
IPinfoPlusWrapper,
IPinfoResProxyWrapper,
ApiLimitError
};
export {
Expand All @@ -27,6 +29,7 @@ export {
IPinfo,
IPinfoCore,
IPinfoPlus,
IPinfoResProxy,
Prefix,
Prefixes6,
AsnResponse,
Expand Down
128 changes: 128 additions & 0 deletions src/ipinfoResProxyWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { RequestInit, Response } from "node-fetch";
import Cache from "./cache/cache";
import LruCache from "./cache/lruCache";
import ApiLimitError from "./errors/apiLimitError";
import { isInSubnet } from "subnet-check";
import {
REQUEST_TIMEOUT_DEFAULT,
CACHE_VSN,
HOST_RES_PROXY,
BOGON_NETWORKS,
IPinfoResProxy,
IPBogon
} from "./common";
import VERSION from "./version";

const clientUserAgent = `IPinfoClient/nodejs/${VERSION}`;

export default class IPinfoResProxyWrapper {
private token: string;
private baseUrl: string;
private cache: Cache;
private timeout: number;

/**
* Creates IPinfoResProxyWrapper object to communicate with the IPinfo Res Proxy API.
*
* @param token Token string provided by IPinfo for the registered user.
* @param cache An implementation of IPCache interface, or LruCache if not specified.
* @param timeout Request timeout in milliseconds, or 5000ms if not specified. 0 disables the timeout.
* @param baseUrl The base url to use for api requests, or "ipinfo.io" if not specified.
*/
constructor(
token: string,
cache?: Cache,
timeout?: number,
baseUrl?: string
) {
this.token = token;
this.cache = cache || new LruCache();
this.timeout =
timeout === null || timeout === undefined
? REQUEST_TIMEOUT_DEFAULT
: timeout;
this.baseUrl = baseUrl || `https://${HOST_RES_PROXY}`;
}

public static cacheKey(k: string): string {
return `${k}:${CACHE_VSN}`;
}

public async fetchApi(
path: string,
init: RequestInit = {}
): Promise<Response> {
const headers = {
Accept: "application/json",
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/json",
"User-Agent": clientUserAgent
};

const request = Object.assign(
{
timeout: this.timeout,
method: "GET",
compress: false
},
init,
{ headers: Object.assign(headers, init.headers) }
);

const url = [this.baseUrl, path].join(
!this.baseUrl.endsWith("/") && !path.startsWith("/") ? "/" : ""
);

return fetch(url, request).then((response: Response) => {
if (response.status === 429) {
throw new ApiLimitError();
}

if (response.status >= 400) {
throw new Error(
`Received an error from the IPinfo API ` +
`(using authorization ${headers["Authorization"]}) ` +
`${response.status} ${response.statusText} ${response.url}`
);
}

return response;
});
}

public async lookupIp(
ip: string | undefined = undefined
): Promise<IPinfoResProxy | IPBogon> {
if (ip && this.isBogon(ip)) {
return { ip, bogon: true };
}

if (!ip) {
ip = "me";
}

const data = await this.cache.get(IPinfoResProxyWrapper.cacheKey(ip));

if (data) {
return data;
}

return this.fetchApi(ip).then(async (response) => {
const ipinfo = (await response.json()) as IPinfoResProxy;
this.cache.set(IPinfoResProxyWrapper.cacheKey(ip), ipinfo);

return ipinfo;
});
}

private isBogon(ip: string): boolean {
if (ip != "") {
for (let network of BOGON_NETWORKS) {
if (isInSubnet(ip, network)) {
return true;
}
}
}
return false;
}
}
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const VERSION = "4.2.0";
const VERSION = "4.3.0";

export default VERSION;