Skip to content

Commit bc1e85a

Browse files
committed
PhishingController implementation
1 parent e981734 commit bc1e85a

File tree

5 files changed

+194
-1
lines changed

5 files changed

+194
-1
lines changed

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@types/node": "^10.1.4",
5252
"@types/sinon": "^4.3.3",
5353
"eth-json-rpc-infura": "^3.1.2",
54+
"eth-phishing-detect": "^1.1.13",
5455
"eth-query": "^2.1.2",
5556
"husky": "^0.14.3",
5657
"isomorphic-fetch": "^2.2.1",

src/BaseController.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { EventEmitter } from 'events';
22
import { ChildControllerContext } from './ComposableController';
33

4-
type Listener<T> = (state: T) => void;
4+
/**
5+
* State change callbacks
6+
*/
7+
export type Listener<T> = (state: T) => void;
58

69
/**
710
* @type BaseConfig

src/PhishingController.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import 'isomorphic-fetch';
2+
import { stub } from 'sinon';
3+
import PhishingController from './PhishingController';
4+
5+
describe('NetworkStatusController', () => {
6+
it('should set default state', () => {
7+
const controller = new PhishingController();
8+
expect(controller.state.phishing).toHaveProperty('blacklist');
9+
expect(controller.state.phishing).toHaveProperty('fuzzylist');
10+
expect(controller.state.phishing).toHaveProperty('version');
11+
expect(controller.state.phishing).toHaveProperty('whitelist');
12+
});
13+
14+
it('should set default config', () => {
15+
const controller = new PhishingController();
16+
expect(controller.config).toEqual({ interval: 180000 });
17+
});
18+
19+
it('should poll on correct interval', () => {
20+
const mock = stub(global, 'setInterval');
21+
/* tslint:disable-next-line:no-unused-expression */
22+
new PhishingController(undefined, { interval: 1337 });
23+
expect(mock.getCall(0).args[1]).toBe(1337);
24+
mock.restore();
25+
});
26+
27+
it('should update lists on interval', () => {
28+
return new Promise((resolve) => {
29+
const controller = new PhishingController(undefined, { interval: 10 });
30+
const mock = stub(controller, 'updatePhishingLists');
31+
setTimeout(() => {
32+
expect(mock.called).toBe(true);
33+
mock.restore();
34+
resolve();
35+
}, 20);
36+
});
37+
});
38+
39+
it('should update lists', async () => {
40+
const controller = new PhishingController();
41+
controller.update({}, true);
42+
await controller.updatePhishingLists();
43+
expect(controller.state.phishing).toHaveProperty('blacklist');
44+
expect(controller.state.phishing).toHaveProperty('fuzzylist');
45+
expect(controller.state.phishing).toHaveProperty('version');
46+
expect(controller.state.phishing).toHaveProperty('whitelist');
47+
});
48+
49+
it('should not update infura rate if disabled', async () => {
50+
const controller = new PhishingController(undefined, { disabled: true });
51+
controller.update({}, true);
52+
await controller.updatePhishingLists();
53+
expect(controller.state.phishing).toBe(undefined);
54+
});
55+
56+
it('should clear previous interval', () => {
57+
const mock = stub(global, 'clearInterval');
58+
const controller = new PhishingController(undefined, { interval: 1337 });
59+
controller.interval = 1338;
60+
expect(mock.called).toBe(true);
61+
mock.restore();
62+
});
63+
64+
it('should verify approved domain', () => {
65+
const controller = new PhishingController();
66+
expect(controller.test('metamask.io')).toBe(false);
67+
});
68+
});

src/PhishingController.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import BaseController, { BaseConfig, BaseState } from './BaseController';
2+
3+
const DEFAULT_PHISHING_RESPONSE = require('eth-phishing-detect/src/config.json');
4+
const PhishingDetector = require('eth-phishing-detect/src/detector');
5+
6+
/**
7+
* @type EthPhishingResponse
8+
*
9+
* Configuration response from the eth-phishing-detect package
10+
* consisting of approved and unapproved website origins
11+
*
12+
* @property blacklist - List of unapproved origins
13+
* @property fuzzylist - List of fuzzy-matched unapproved origins
14+
* @property tolerance - Fuzzy match tolerance level
15+
* @property version - Versin number of this configuration
16+
* @property whitelist - List of approved origins
17+
*/
18+
export interface EthPhishingResponse {
19+
blacklist: string[];
20+
fuzzylist: string[];
21+
tolerance: number;
22+
version: number;
23+
whitelist: string[];
24+
}
25+
26+
/**
27+
* @type PhishingConfig
28+
*
29+
* Phishing controller configuration
30+
*
31+
* @property interval - Polling interval used to fetch new block / approve lists
32+
*/
33+
export interface PhishingConfig extends BaseConfig {
34+
interval: number;
35+
}
36+
37+
/**
38+
* @type PhishingState
39+
*
40+
* Phishing controller state
41+
*
42+
* @property phishing - eth-phishing-detect configuration
43+
*/
44+
export interface PhishingState extends BaseState {
45+
phishing: EthPhishingResponse;
46+
}
47+
48+
/**
49+
* Controller that passively polls on a set interval for approved and unapproved website origins
50+
*/
51+
export class PhishingController extends BaseController<PhishingState, PhishingConfig> {
52+
private detector: any;
53+
private handle?: NodeJS.Timer;
54+
55+
/**
56+
* Creates a PhishingController instance
57+
*
58+
* @param state - Initial state to set on this controller
59+
* @param config - Initial options used to configure this controller
60+
*/
61+
constructor(state?: Partial<PhishingState>, config?: Partial<PhishingConfig>) {
62+
super(state, config);
63+
this.defaultConfig = { interval: 180000 };
64+
this.defaultState = { phishing: DEFAULT_PHISHING_RESPONSE };
65+
this.detector = new PhishingDetector(this.defaultState.phishing);
66+
this.initialize();
67+
}
68+
69+
/**
70+
* Sets a new polling interval
71+
*
72+
* @param interval - Polling interval used to fetch new exchange rates
73+
*/
74+
set interval(interval: number) {
75+
this.handle && clearInterval(this.handle);
76+
this.updatePhishingLists();
77+
this.handle = setInterval(() => {
78+
this.updatePhishingLists();
79+
}, interval);
80+
}
81+
82+
/**
83+
* Determines if a given origin is unapproved
84+
*
85+
* @param origin - Domain origin of a website
86+
* @returns - True if the origin is an unapproved origin
87+
*/
88+
test(origin: string) {
89+
return this.detector.check(origin).result;
90+
}
91+
92+
/**
93+
* Updates lists of approved and unapproved website origins
94+
*/
95+
async updatePhishingLists() {
96+
let phishing;
97+
if (this.disabled) {
98+
return;
99+
}
100+
try {
101+
const response = await fetch('https://api.infura.io/v2/blacklist');
102+
const json = await response.json();
103+
phishing = json && json.whitelist ? json : /* istanbul ignore next */ null;
104+
} catch (error) {
105+
/* tslint:disable-next-line:no-empty */
106+
}
107+
this.detector = new PhishingDetector(phishing);
108+
phishing && this.update({ phishing });
109+
}
110+
}
111+
112+
export default PhishingController;

0 commit comments

Comments
 (0)