@keplr-wallet/background 0.10.23 → 0.10.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/build/index.d.ts +1 -0
  2. package/build/index.js +9 -0
  3. package/build/index.js.map +1 -1
  4. package/build/phishing-list/constants.d.ts +1 -0
  5. package/build/phishing-list/constants.js +5 -0
  6. package/build/phishing-list/constants.js.map +1 -0
  7. package/build/phishing-list/handler.d.ts +3 -0
  8. package/build/phishing-list/handler.js +19 -0
  9. package/build/phishing-list/handler.js.map +1 -0
  10. package/build/phishing-list/index.d.ts +2 -0
  11. package/build/phishing-list/index.js +15 -0
  12. package/build/phishing-list/index.js.map +1 -0
  13. package/build/phishing-list/init.d.ts +3 -0
  14. package/build/phishing-list/init.js +12 -0
  15. package/build/phishing-list/init.js.map +1 -0
  16. package/build/phishing-list/internal.d.ts +2 -0
  17. package/build/phishing-list/internal.js +15 -0
  18. package/build/phishing-list/internal.js.map +1 -0
  19. package/build/phishing-list/messages.d.ts +9 -0
  20. package/build/phishing-list/messages.js +30 -0
  21. package/build/phishing-list/messages.js.map +1 -0
  22. package/build/phishing-list/messages.spec.d.ts +1 -0
  23. package/build/phishing-list/messages.spec.js +102 -0
  24. package/build/phishing-list/messages.spec.js.map +1 -0
  25. package/build/phishing-list/service.d.ts +22 -0
  26. package/build/phishing-list/service.js +86 -0
  27. package/build/phishing-list/service.js.map +1 -0
  28. package/build/phishing-list/service.spec.d.ts +1 -0
  29. package/build/phishing-list/service.spec.js +302 -0
  30. package/build/phishing-list/service.spec.js.map +1 -0
  31. package/build/phishing-list/utils.d.ts +1 -0
  32. package/build/phishing-list/utils.js +27 -0
  33. package/build/phishing-list/utils.js.map +1 -0
  34. package/build/phishing-list/utils.spec.d.ts +1 -0
  35. package/build/phishing-list/utils.spec.js +142 -0
  36. package/build/phishing-list/utils.spec.js.map +1 -0
  37. package/package.json +11 -11
  38. package/src/index.ts +11 -0
  39. package/src/phishing-list/constants.ts +1 -0
  40. package/src/phishing-list/handler.ts +27 -0
  41. package/src/phishing-list/index.ts +2 -0
  42. package/src/phishing-list/init.ts +11 -0
  43. package/src/phishing-list/internal.ts +2 -0
  44. package/src/phishing-list/messages.spec.ts +104 -0
  45. package/src/phishing-list/messages.ts +32 -0
  46. package/src/phishing-list/service.spec.ts +367 -0
  47. package/src/phishing-list/service.ts +88 -0
  48. package/src/phishing-list/utils.spec.ts +144 -0
  49. package/src/phishing-list/utils.ts +27 -0
@@ -0,0 +1,104 @@
1
+ import { CheckURLIsPhishingMsg } from "./messages";
2
+
3
+ describe("Test phishing list service messages", () => {
4
+ test("Test CheckURLIsPhishingMsg", () => {
5
+ const tests: {
6
+ url: string;
7
+ invalid?: boolean;
8
+ }[] = [
9
+ {
10
+ url: "",
11
+ invalid: true,
12
+ },
13
+ {
14
+ url: ".",
15
+ invalid: true,
16
+ },
17
+ {
18
+ url: "..",
19
+ invalid: true,
20
+ },
21
+ {
22
+ url: ".test.",
23
+ invalid: true,
24
+ },
25
+ {
26
+ url: "..test",
27
+ invalid: true,
28
+ },
29
+ {
30
+ url: "test.com.",
31
+ invalid: true,
32
+ },
33
+ {
34
+ url: "test.com..",
35
+ invalid: true,
36
+ },
37
+ {
38
+ url: "asd.test.com.",
39
+ invalid: true,
40
+ },
41
+ {
42
+ url: "asd..test.com.",
43
+ invalid: true,
44
+ },
45
+ {
46
+ url: "http://",
47
+ invalid: true,
48
+ },
49
+ {
50
+ url: "https://.",
51
+ invalid: true,
52
+ },
53
+ {
54
+ url: "https://..",
55
+ invalid: true,
56
+ },
57
+ {
58
+ url: "https://.test.",
59
+ invalid: true,
60
+ },
61
+ {
62
+ url: "https://..test",
63
+ invalid: true,
64
+ },
65
+ {
66
+ url: "https://test.com.",
67
+ },
68
+ {
69
+ url: "https://test.com..",
70
+ },
71
+ {
72
+ url: "https://.test.com.",
73
+ },
74
+ {
75
+ url: "https://..test.com..",
76
+ },
77
+ {
78
+ url: "https://test..com",
79
+ },
80
+ {
81
+ url: "https://..test..com..",
82
+ },
83
+ {
84
+ url: "https://asd.test.com.",
85
+ },
86
+ {
87
+ url: "https://asd..test.com.",
88
+ },
89
+ ];
90
+
91
+ for (const test of tests) {
92
+ const msg = new CheckURLIsPhishingMsg();
93
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
94
+ // @ts-ignore
95
+ msg.origin = test.url;
96
+
97
+ if ("invalid" in test) {
98
+ expect(() => msg.validateBasic()).toThrow();
99
+ } else {
100
+ expect(() => msg.validateBasic()).not.toThrow();
101
+ }
102
+ }
103
+ });
104
+ });
@@ -0,0 +1,32 @@
1
+ import { Message } from "@keplr-wallet/router";
2
+ import { ROUTE } from "./constants";
3
+ import { parseDomainUntilSecondLevel } from "./utils";
4
+
5
+ export class CheckURLIsPhishingMsg extends Message<boolean> {
6
+ public static type() {
7
+ return "check-url-is-phishing";
8
+ }
9
+
10
+ constructor() {
11
+ super();
12
+ }
13
+
14
+ validateBasic(): void {
15
+ const url = new URL(this.origin);
16
+
17
+ // Will throw an error if url has not second level domain.
18
+ parseDomainUntilSecondLevel(url.origin);
19
+ }
20
+
21
+ approveExternal(): boolean {
22
+ return true;
23
+ }
24
+
25
+ route(): string {
26
+ return ROUTE;
27
+ }
28
+
29
+ type(): string {
30
+ return CheckURLIsPhishingMsg.type();
31
+ }
32
+ }
@@ -0,0 +1,367 @@
1
+ import { PhishingListService } from "./service";
2
+ import Http from "http";
3
+
4
+ const phishings = [
5
+ "keplr-vvallet-app.online",
6
+ "xn--kplr-vva.com",
7
+ "keplr-vvallet.tech",
8
+ "app-keplr-vvallet.online",
9
+ "keplr-vvallet-app.space",
10
+ "keplr-vvallet-app.host",
11
+ "app-keplr-vvallet.tech",
12
+ "app-keplr-vvallet.host",
13
+ ];
14
+
15
+ const createMockServer = () => {
16
+ let queryCount = 0;
17
+
18
+ const server = Http.createServer((req, resp) => {
19
+ queryCount++;
20
+
21
+ // Most desired case
22
+ if (req.url === "/list1") {
23
+ resp.writeHead(200);
24
+ resp.end(phishings.join("\n"));
25
+ return;
26
+ }
27
+
28
+ // If some strange cases exist
29
+ if (req.url === "/list2") {
30
+ resp.writeHead(200);
31
+ let str = "";
32
+ for (let i = 0; i < phishings.length; i++) {
33
+ let phishing = phishings[i];
34
+
35
+ switch (i) {
36
+ case 0:
37
+ phishing = phishing + "\n";
38
+ break;
39
+ case 1:
40
+ // Windows line break
41
+ phishing = phishing + "\r\n";
42
+ break;
43
+ case 2:
44
+ phishing = phishing + ";";
45
+ break;
46
+ case 3:
47
+ phishing = phishing + ",";
48
+ break;
49
+ case 4:
50
+ phishing = phishing + " ; ";
51
+ break;
52
+ case 5:
53
+ phishing = phishing + " , ";
54
+ break;
55
+ case 6:
56
+ phishing = "third-domain." + phishing;
57
+ break;
58
+ default:
59
+ phishing = ` ${phishing} `;
60
+ }
61
+
62
+ str += phishing;
63
+ }
64
+ resp.end(str);
65
+ return;
66
+ }
67
+
68
+ // Even if an invalid endpoint exists, only that endpoint should be ignored and proceeded.
69
+ // When second fetch, add other domain to test fetching interval.
70
+ if (req.url === "/list3") {
71
+ resp.writeHead(200);
72
+ let str = "";
73
+ for (let i = 0; i < phishings.length; i++) {
74
+ let phishing = phishings[i];
75
+
76
+ switch (i) {
77
+ case 0:
78
+ phishing = phishing + "\n";
79
+ break;
80
+ case 1:
81
+ phishing = phishing + ".\n";
82
+ break;
83
+ case 2:
84
+ phishing = "." + phishing + "\r\n";
85
+ break;
86
+ case 3:
87
+ phishing = "invalid;.." + phishing + "..;";
88
+ break;
89
+ case 4:
90
+ phishing = phishing + "/,";
91
+ break;
92
+ case 5:
93
+ phishing = phishing + "?test\n";
94
+ break;
95
+ case 6:
96
+ phishing = "third-domain." + phishing + "\n";
97
+ break;
98
+ default:
99
+ phishing = ` ${phishing} `;
100
+ }
101
+
102
+ str += phishing;
103
+ }
104
+ str += ";invalid";
105
+
106
+ if (queryCount === 2) {
107
+ str += ";added.domain";
108
+ }
109
+
110
+ resp.end(str);
111
+ return;
112
+ }
113
+
114
+ if (req.url === "/test-retry") {
115
+ if (queryCount % 3 === 0) {
116
+ resp.writeHead(400);
117
+ resp.end();
118
+ return;
119
+ }
120
+
121
+ resp.writeHead(200);
122
+ resp.end(phishings.slice(0, queryCount).join("\n"));
123
+ return;
124
+ }
125
+
126
+ throw new Error("invalid url");
127
+ });
128
+
129
+ server.listen();
130
+
131
+ const address = server.address();
132
+ if (!address || typeof address === "string") {
133
+ throw new Error("Failed to get address for server");
134
+ }
135
+ const port = address.port;
136
+
137
+ return {
138
+ port,
139
+ closeServer: () => {
140
+ server.close();
141
+ },
142
+ getQueryCount: () => {
143
+ return queryCount;
144
+ },
145
+ };
146
+ };
147
+
148
+ describe("Test phishing list service", () => {
149
+ let eachService: PhishingListService | undefined;
150
+ let port: number = -1;
151
+ let closeServer: (() => void) | undefined;
152
+ let getQueryCount: () => number;
153
+
154
+ beforeEach(() => {
155
+ const server = createMockServer();
156
+ port = server.port;
157
+ closeServer = server.closeServer;
158
+ getQueryCount = server.getQueryCount;
159
+ });
160
+
161
+ afterEach(() => {
162
+ if (eachService) {
163
+ eachService.stop();
164
+ eachService = undefined;
165
+ }
166
+
167
+ if (closeServer) {
168
+ closeServer();
169
+ closeServer = undefined;
170
+ }
171
+ });
172
+
173
+ const waitServiceInit = (service: PhishingListService) => {
174
+ return new Promise<void>((resolve) => {
175
+ if (service.hasInited) {
176
+ resolve();
177
+ return;
178
+ }
179
+
180
+ const intervalId = setInterval(() => {
181
+ if (service.hasInited) {
182
+ resolve();
183
+ clearInterval(intervalId);
184
+ }
185
+ }, 10);
186
+ });
187
+ };
188
+
189
+ const testCheckURLIsPhishing = (service: PhishingListService) => {
190
+ for (const phishing of phishings) {
191
+ expect(service.checkURLIsPhishing(`http://${phishing}`)).toBe(true);
192
+ expect(service.checkURLIsPhishing(`https://${phishing}`)).toBe(true);
193
+ expect(service.checkURLIsPhishing(`https://test.${phishing}`)).toBe(true);
194
+ expect(service.checkURLIsPhishing(`https://test.${phishing}.`)).toBe(
195
+ true
196
+ );
197
+ expect(service.checkURLIsPhishing(`https://test.${phishing}./`)).toBe(
198
+ true
199
+ );
200
+ expect(service.checkURLIsPhishing(`https://test.${phishing}..`)).toBe(
201
+ true
202
+ );
203
+ expect(service.checkURLIsPhishing(`https://test.${phishing}../`)).toBe(
204
+ true
205
+ );
206
+ expect(
207
+ service.checkURLIsPhishing(`https://test.${phishing}..?test`)
208
+ ).toBe(true);
209
+ expect(
210
+ service.checkURLIsPhishing(`https://test.${phishing}..?test=1`)
211
+ ).toBe(true);
212
+ expect(service.checkURLIsPhishing(`https://.test.test.${phishing}`)).toBe(
213
+ true
214
+ );
215
+ expect(
216
+ service.checkURLIsPhishing(`https://..test.test.${phishing}`)
217
+ ).toBe(true);
218
+ expect(
219
+ service.checkURLIsPhishing(`https://..test..test...${phishing}`)
220
+ ).toBe(true);
221
+ }
222
+ };
223
+
224
+ test("Test basic PhishingListService with valid cases", async () => {
225
+ const service = new PhishingListService({
226
+ blockListUrl: `http://127.0.0.1:${port}/list1`,
227
+ fetchingIntervalMs: 3600,
228
+ retryIntervalMs: 3600,
229
+ });
230
+ eachService = service;
231
+
232
+ service.init();
233
+
234
+ await waitServiceInit(service);
235
+
236
+ testCheckURLIsPhishing(service);
237
+ });
238
+
239
+ test("Test basic PhishingListService with strange separators", async () => {
240
+ const service = new PhishingListService({
241
+ blockListUrl: `http://127.0.0.1:${port}/list2`,
242
+ fetchingIntervalMs: 3600,
243
+ retryIntervalMs: 3600,
244
+ });
245
+ eachService = service;
246
+
247
+ service.init();
248
+
249
+ await waitServiceInit(service);
250
+
251
+ testCheckURLIsPhishing(service);
252
+ });
253
+
254
+ test("Test basic PhishingListService with strange cases", async () => {
255
+ const service = new PhishingListService({
256
+ blockListUrl: `http://127.0.0.1:${port}/list3`,
257
+ fetchingIntervalMs: 3600,
258
+ retryIntervalMs: 3600,
259
+ });
260
+ eachService = service;
261
+
262
+ service.init();
263
+
264
+ await waitServiceInit(service);
265
+
266
+ testCheckURLIsPhishing(service);
267
+ });
268
+
269
+ test("Test basic PhishingListService fetching interval", async () => {
270
+ const service = new PhishingListService({
271
+ blockListUrl: `http://127.0.0.1:${port}/list3`,
272
+ fetchingIntervalMs: 200,
273
+ retryIntervalMs: 3600,
274
+ });
275
+ eachService = service;
276
+
277
+ service.init();
278
+
279
+ await waitServiceInit(service);
280
+
281
+ testCheckURLIsPhishing(service);
282
+ expect(service.checkURLIsPhishing("https://added.domain")).toBe(false);
283
+
284
+ expect(getQueryCount()).toBe(1);
285
+
286
+ // Wait re-fetching
287
+ await new Promise((resolve) => setTimeout(resolve, 210));
288
+
289
+ testCheckURLIsPhishing(service);
290
+ // See the implementation of /list3
291
+ expect(service.checkURLIsPhishing("https://added.domain")).toBe(true);
292
+
293
+ expect(getQueryCount()).toBe(2);
294
+
295
+ // Wait re-fetching
296
+ await new Promise((resolve) => setTimeout(resolve, 210));
297
+
298
+ testCheckURLIsPhishing(service);
299
+ // See the implementation of /list3
300
+ expect(service.checkURLIsPhishing("https://added.domain")).toBe(false);
301
+
302
+ expect(getQueryCount()).toBe(3);
303
+ });
304
+
305
+ test("Test basic PhishingListService fetching interval with retry interval", async () => {
306
+ const service = new PhishingListService({
307
+ blockListUrl: `http://127.0.0.1:${port}/test-retry`,
308
+ fetchingIntervalMs: 200,
309
+ retryIntervalMs: 100,
310
+ });
311
+ eachService = service;
312
+
313
+ service.init();
314
+
315
+ const testPhishingUntil = (count: number) => {
316
+ const tests = phishings.slice(0, count);
317
+ for (const test of tests) {
318
+ expect(service.checkURLIsPhishing("https://" + test)).toBe(true);
319
+ }
320
+ if (phishings.length > count) {
321
+ const test = phishings[count];
322
+ expect(service.checkURLIsPhishing("https://" + test)).toBe(false);
323
+ }
324
+ };
325
+
326
+ await waitServiceInit(service);
327
+
328
+ testPhishingUntil(1);
329
+ expect(getQueryCount()).toBe(1);
330
+
331
+ // Wait re-fetching
332
+ await new Promise((resolve) => setTimeout(resolve, 210));
333
+
334
+ // See the implementation of /test-retry
335
+ testPhishingUntil(2);
336
+ expect(getQueryCount()).toBe(2);
337
+
338
+ // Wait re-fetching
339
+ await new Promise((resolve) => setTimeout(resolve, 210));
340
+
341
+ // See the implementation of /test-retry
342
+ // In this case, the fetching should be failed.
343
+ // So, there is no update on phishing list.
344
+ testPhishingUntil(2);
345
+ expect(getQueryCount()).toBe(3);
346
+
347
+ // Wait retry for failed query
348
+ await new Promise((resolve) => setTimeout(resolve, 110));
349
+
350
+ // See the implementation of /test-retry
351
+ testPhishingUntil(4);
352
+ expect(getQueryCount()).toBe(4);
353
+
354
+ // Not yet re-fetching
355
+ await new Promise((resolve) => setTimeout(resolve, 110));
356
+
357
+ // See the implementation of /test-retry
358
+ testPhishingUntil(4);
359
+ expect(getQueryCount()).toBe(4);
360
+
361
+ // Now re-fetching
362
+ await new Promise((resolve) => setTimeout(resolve, 110));
363
+ // See the implementation of /test-retry
364
+ testPhishingUntil(5);
365
+ expect(getQueryCount()).toBe(5);
366
+ });
367
+ });
@@ -0,0 +1,88 @@
1
+ import Axios from "axios";
2
+ import { parseDomainUntilSecondLevel } from "./utils";
3
+
4
+ export class PhishingListService {
5
+ protected map: Map<string, boolean> = new Map();
6
+
7
+ protected _hasInited: boolean = false;
8
+ protected _hasStopped: boolean = false;
9
+ protected timeoutId?: NodeJS.Timeout;
10
+
11
+ constructor(
12
+ public readonly opts: {
13
+ readonly blockListUrl: string;
14
+ readonly fetchingIntervalMs: number;
15
+ readonly retryIntervalMs: number;
16
+ }
17
+ ) {}
18
+
19
+ get hasInited(): boolean {
20
+ return this._hasInited;
21
+ }
22
+
23
+ init() {
24
+ this.startFetchPhishingList();
25
+ }
26
+
27
+ stop() {
28
+ if (this.timeoutId != null) {
29
+ clearTimeout(this.timeoutId);
30
+ this.timeoutId = undefined;
31
+ }
32
+ this._hasStopped = true;
33
+ }
34
+
35
+ async startFetchPhishingList() {
36
+ if (this.timeoutId != null) {
37
+ clearTimeout(this.timeoutId);
38
+ this.timeoutId = undefined;
39
+ }
40
+
41
+ if (this._hasStopped) {
42
+ return;
43
+ }
44
+
45
+ let failed = false;
46
+ try {
47
+ const res = await Axios.get<string>(this.opts.blockListUrl);
48
+
49
+ const domains = res.data
50
+ .split(/(\r?\n)|,|;|\s|\t/)
51
+ .filter((str) => str != null)
52
+ .map((str) => {
53
+ return str.trim();
54
+ })
55
+ .filter((str) => str.length > 0);
56
+
57
+ const map = new Map<string, boolean>();
58
+
59
+ for (const domain of domains) {
60
+ try {
61
+ map.set(parseDomainUntilSecondLevel(domain), true);
62
+ } catch (e) {
63
+ console.log(e);
64
+ }
65
+ }
66
+
67
+ this._hasInited = true;
68
+ this.map = map;
69
+ } catch (e) {
70
+ failed = true;
71
+ console.log(e);
72
+ }
73
+
74
+ if (!this._hasStopped) {
75
+ this.timeoutId = setTimeout(
76
+ () => {
77
+ this.startFetchPhishingList();
78
+ },
79
+ failed ? this.opts.retryIntervalMs : this.opts.fetchingIntervalMs
80
+ );
81
+ }
82
+ }
83
+
84
+ checkURLIsPhishing(url: string): boolean {
85
+ const parsed = new URL(url);
86
+ return this.map.get(parseDomainUntilSecondLevel(parsed.origin)) === true;
87
+ }
88
+ }