Skip to content

Commit 545b3a5

Browse files
feat: add ipService to get current ip address
Add a new ipService to get the current ip address. As this information is critical for us, use three different ways to determine the ip address, so even if one of them fails, use the result from the next one. Use the `play.nativescript.org/api/whoami` endpoint to get data for the public ip address.
1 parent 0a7adfa commit 545b3a5

File tree

8 files changed

+212
-1
lines changed

8 files changed

+212
-1
lines changed

config/config.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"UPLOAD_PLAYGROUND_FILES_ENDPOINT": "https://play.nativescript.org/api/files",
88
"SHORTEN_URL_ENDPOINT": "https://play.nativescript.org/api/shortenurl?longUrl=%s",
99
"INSIGHTS_URL_ENDPOINT": "https://play.nativescript.org/api/insights",
10+
"WHOAMI_URL_ENDPOINT": "https://play.nativescript.org/api/whoami",
1011
"PREVIEW_APP_ENVIRONMENT": "live",
1112
"GA_TRACKING_ID": "UA-111455-51"
1213
}

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,4 @@ $injector.require("watchIgnoreListService", "./services/watch-ignore-list-servic
229229
$injector.requirePublicClass("initializeService", "./services/initialize-service");
230230

231231
$injector.require("npmConfigService", "./services/npm-config-service");
232+
$injector.require("ipService", "./services/ip-service");

lib/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class Configuration implements IConfiguration { // User specific config
99
UPLOAD_PLAYGROUND_FILES_ENDPOINT: string = null;
1010
SHORTEN_URL_ENDPOINT: string = null;
1111
INSIGHTS_URL_ENDPOINT: string = null;
12+
WHOAMI_URL_ENDPOINT: string = null;
1213
PREVIEW_APP_ENVIRONMENT: string = null;
1314
GA_TRACKING_ID: string = null;
1415
DISABLE_HOOKS: boolean = false;

lib/declarations.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ interface IConfiguration extends Config.IConfig {
432432
UPLOAD_PLAYGROUND_FILES_ENDPOINT: string;
433433
SHORTEN_URL_ENDPOINT: string;
434434
INSIGHTS_URL_ENDPOINT: string;
435+
WHOAMI_URL_ENDPOINT: string;
435436
PREVIEW_APP_ENVIRONMENT: string;
436437
GA_TRACKING_ID: string;
437438
}

lib/definitions/ip-service.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Describes the service used to get information for the current IP Address.
3+
*/
4+
interface IIPService {
5+
/**
6+
* Gives information about the current public IPv4 address.
7+
* @returns {Promise<string>} The IP address or null in case unable to find the current IP.
8+
*/
9+
getCurrentIPv4Address(): Promise<string>;
10+
}

lib/services/ip-service.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
export class IPService implements IIPService {
2+
private static GET_IP_TIMEOUT = 1000;
3+
constructor(private $config: IConfiguration,
4+
private $httpClient: Server.IHttpClient,
5+
private $logger: ILogger) { }
6+
7+
public async getCurrentIPv4Address(): Promise<string> {
8+
const ipAddress = await this.getIPAddressFromServiceReturningJSONWithIPProperty(this.$config.WHOAMI_URL_ENDPOINT) ||
9+
await this.getIPAddressFromServiceReturningJSONWithIPProperty("https://api.myip.com") ||
10+
await this.getIPAddressFromIpifyOrgAPI() ||
11+
null;
12+
13+
return ipAddress;
14+
}
15+
16+
private async getIPAddressFromServiceReturningJSONWithIPProperty(apiEndpoint: string): Promise<string> {
17+
let ipAddress: string = null;
18+
try {
19+
const response = await this.$httpClient.httpRequest({
20+
method: "GET",
21+
url: apiEndpoint,
22+
timeout: IPService.GET_IP_TIMEOUT
23+
});
24+
25+
this.$logger.trace(`${apiEndpoint} returns ${response.body}`);
26+
27+
const jsonData = JSON.parse(response.body);
28+
ipAddress = jsonData.ip;
29+
} catch (err) {
30+
this.$logger.trace(`Unable to get information about current IP Address from ${apiEndpoint} Error is:`, err);
31+
}
32+
33+
return ipAddress;
34+
}
35+
36+
private async getIPAddressFromIpifyOrgAPI(): Promise<string> {
37+
// https://www.ipify.org/
38+
const ipifyOrgAPIEndpoint = "https://api.ipify.org";
39+
let ipAddress: string = null;
40+
try {
41+
const response = await this.$httpClient.httpRequest({
42+
method: "GET",
43+
url: ipifyOrgAPIEndpoint,
44+
timeout: IPService.GET_IP_TIMEOUT
45+
});
46+
47+
this.$logger.trace(`${ipifyOrgAPIEndpoint} returns ${response.body}`);
48+
49+
ipAddress = (response.body || '').toString();
50+
} catch (err) {
51+
this.$logger.trace(`Unable to get information about current IP Address from ${ipifyOrgAPIEndpoint} Error is:`, err);
52+
}
53+
54+
return ipAddress;
55+
}
56+
}
57+
58+
$injector.register("ipService", IPService);

npm-shrinkwrap.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/services/ip-service.ts

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Yok } from "../../lib/common/yok";
2+
import { LoggerStub } from "../stubs";
3+
import { IPService } from "../../lib/services/ip-service";
4+
import { assert } from "chai";
5+
6+
describe("ipService", () => {
7+
const ip = "8.8.8.8";
8+
const whoamiDefaultEndpoint = "https://who.am.i/api/whoami";
9+
const errorMsgForDefaultEndpoint = `Unable to get data from ${whoamiDefaultEndpoint}`;
10+
const errMsgForMyipCom = "Unable to get data from myip.com";
11+
const errMsgForIpifyOrg = "Unable to get data from ipify.org";
12+
13+
const createTestInjector = (): IInjector => {
14+
const testInjector = new Yok();
15+
testInjector.register("httpClient", {
16+
httpRequest: async (options: any, proxySettings?: IProxySettings): Promise<Server.IResponse> => (<any>{})
17+
});
18+
19+
testInjector.register("logger", LoggerStub);
20+
testInjector.register("ipService", IPService);
21+
testInjector.register("config", {
22+
WHOAMI_URL_ENDPOINT: whoamiDefaultEndpoint
23+
});
24+
return testInjector;
25+
};
26+
27+
describe("getCurrentIPv4Address", () => {
28+
it("returns result from default service (play.nativescript.org) when it succeeds", async () => {
29+
const testInjector = createTestInjector();
30+
const httpClient = testInjector.resolve<Server.IHttpClient>("httpClient");
31+
const httpRequestPassedOptions: any[] = [];
32+
httpClient.httpRequest = async (options: any, proxySettings?: IProxySettings): Promise<Server.IResponse> => {
33+
httpRequestPassedOptions.push(options);
34+
return <any>{ body: JSON.stringify({ ip }) };
35+
};
36+
37+
const ipService = testInjector.resolve<IIPService>("ipService");
38+
const ipAddress = await ipService.getCurrentIPv4Address();
39+
40+
assert.equal(ipAddress, ip);
41+
assert.deepEqual(httpRequestPassedOptions, [{ method: "GET", url: whoamiDefaultEndpoint, timeout: 1000 }]);
42+
});
43+
44+
it("returns result from myip.com when the default endpoint fails", async () => {
45+
const testInjector = createTestInjector();
46+
const httpClient = testInjector.resolve<Server.IHttpClient>("httpClient");
47+
const httpRequestPassedOptions: any[] = [];
48+
httpClient.httpRequest = async (options: any, proxySettings?: IProxySettings): Promise<Server.IResponse> => {
49+
httpRequestPassedOptions.push(options);
50+
if (options.url === whoamiDefaultEndpoint) {
51+
throw new Error(errorMsgForDefaultEndpoint);
52+
}
53+
return <any>{ body: JSON.stringify({ ip }) };
54+
};
55+
56+
const ipService = testInjector.resolve<IIPService>("ipService");
57+
const ipAddress = await ipService.getCurrentIPv4Address();
58+
59+
assert.equal(ipAddress, ip);
60+
assert.deepEqual(httpRequestPassedOptions, [
61+
{ method: "GET", url: whoamiDefaultEndpoint, timeout: 1000 },
62+
{ method: "GET", url: "https://api.myip.com", timeout: 1000 }
63+
]);
64+
65+
const logger = testInjector.resolve<LoggerStub>("logger");
66+
assert.isTrue(logger.traceOutput.indexOf(errorMsgForDefaultEndpoint) !== -1, `Trace output\n'${logger.traceOutput}'\ndoes not contain expected message:\n${errorMsgForDefaultEndpoint}`);
67+
68+
});
69+
70+
it("returns result from ipify.com when it default endpoint and myip.comm both fail", async () => {
71+
const testInjector = createTestInjector();
72+
const httpClient = testInjector.resolve<Server.IHttpClient>("httpClient");
73+
const httpRequestPassedOptions: any[] = [];
74+
httpClient.httpRequest = async (options: any, proxySettings?: IProxySettings): Promise<Server.IResponse> => {
75+
httpRequestPassedOptions.push(options);
76+
if (options.url === whoamiDefaultEndpoint) {
77+
throw new Error(errorMsgForDefaultEndpoint);
78+
}
79+
80+
if (options.url === "https://api.myip.com") {
81+
throw new Error(errMsgForMyipCom);
82+
}
83+
84+
return <any>{ body: ip };
85+
};
86+
87+
const ipService = testInjector.resolve<IIPService>("ipService");
88+
const ipAddress = await ipService.getCurrentIPv4Address();
89+
90+
assert.equal(ipAddress, ip);
91+
assert.deepEqual(httpRequestPassedOptions, [
92+
{ method: "GET", url: whoamiDefaultEndpoint, timeout: 1000 },
93+
{ method: "GET", url: "https://api.myip.com", timeout: 1000 },
94+
{ method: "GET", url: "https://api.ipify.org", timeout: 1000 }
95+
]);
96+
97+
const logger = testInjector.resolve<LoggerStub>("logger");
98+
assert.isTrue(logger.traceOutput.indexOf(errorMsgForDefaultEndpoint) !== -1, `Trace output\n'${logger.traceOutput}'\ndoes not contain expected message:\n${errorMsgForDefaultEndpoint}`);
99+
assert.isTrue(logger.traceOutput.indexOf(errMsgForMyipCom) !== -1, `Trace output\n'${logger.traceOutput}'\ndoes not contain expected message:\n${errMsgForMyipCom}`);
100+
});
101+
102+
it("returns null when all endpoints fail", async () => {
103+
const testInjector = createTestInjector();
104+
const httpClient = testInjector.resolve<Server.IHttpClient>("httpClient");
105+
const httpRequestPassedOptions: any[] = [];
106+
httpClient.httpRequest = async (options: any, proxySettings?: IProxySettings): Promise<Server.IResponse> => {
107+
httpRequestPassedOptions.push(options);
108+
if (options.url === whoamiDefaultEndpoint) {
109+
throw new Error(errorMsgForDefaultEndpoint);
110+
}
111+
112+
if (options.url === "https://api.myip.com") {
113+
throw new Error(errMsgForMyipCom);
114+
}
115+
116+
if (options.url === "https://api.ipify.org") {
117+
throw new Error(errMsgForIpifyOrg);
118+
}
119+
120+
return <any>{ body: ip };
121+
};
122+
123+
const ipService = testInjector.resolve<IIPService>("ipService");
124+
const ipAddress = await ipService.getCurrentIPv4Address();
125+
126+
assert.isNull(ipAddress);
127+
assert.deepEqual(httpRequestPassedOptions, [
128+
{ method: "GET", url: whoamiDefaultEndpoint, timeout: 1000 },
129+
{ method: "GET", url: "https://api.myip.com", timeout: 1000 },
130+
{ method: "GET", url: "https://api.ipify.org", timeout: 1000 }
131+
]);
132+
133+
const logger = testInjector.resolve<LoggerStub>("logger");
134+
assert.isTrue(logger.traceOutput.indexOf(errorMsgForDefaultEndpoint) !== -1, `Trace output\n'${logger.traceOutput}'\ndoes not contain expected message:\n${errorMsgForDefaultEndpoint}`);
135+
assert.isTrue(logger.traceOutput.indexOf(errMsgForMyipCom) !== -1, `Trace output\n'${logger.traceOutput}'\ndoes not contain expected message:\n${errMsgForMyipCom}`);
136+
assert.isTrue(logger.traceOutput.indexOf(errMsgForIpifyOrg) !== -1, `Trace output\n'${logger.traceOutput}'\ndoes not contain expected message:\n${errMsgForMyipCom}`);
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)