Skip to content

Commit 9894e9a

Browse files
feat(core-manager): implement IP whitelisting (#3686)
1 parent fed54c9 commit 9894e9a

File tree

12 files changed

+206
-53
lines changed

12 files changed

+206
-53
lines changed

__tests__/unit/core-manager/server-boot-dispose.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import { Sandbox } from "@packages/core-test-framework";
88
import { Identifiers } from "@packages/core-manager/src/ioc";
99
import { Server } from "@packages/core-manager/src/server";
1010
import { ActionReader } from "@packages/core-manager/src/action-reader";
11+
import { PluginFactory } from "@packages/core-manager/src/plugins/plugin-factory";
12+
import { defaults } from "@packages/core-manager/src/defaults";
1113

1214
let sandbox: Sandbox;
1315
let server: Server;
16+
let pluginsConfiguration = defaults.plugins
1417

1518
let logger = {
1619
info: jest.fn(),
@@ -42,10 +45,16 @@ beforeEach(() => {
4245

4346
sandbox.app.bind(Identifiers.HTTP).to(Server).inSingletonScope();
4447
sandbox.app.bind(Identifiers.ActionReader).to(ActionReader).inSingletonScope();
48+
sandbox.app.bind(Identifiers.PluginFactory).to(PluginFactory).inSingletonScope();
4549
sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger);
50+
sandbox.app.bind(Container.Identifiers.PluginConfiguration).toConstantValue({
51+
get: jest.fn().mockReturnValue(pluginsConfiguration)
52+
});
53+
4654

4755
sandbox.app.terminate = jest.fn();
4856

57+
4958
server = sandbox.app.get<Server>(Identifiers.HTTP);
5059
});
5160

__tests__/unit/core-manager/server.test.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { Identifiers } from "@packages/core-manager/src/ioc";
77
import { Actions } from "@packages/core-manager/src/contracts";
88
import { Server } from "@packages/core-manager/src/server";
99
import { ActionReader } from "@packages/core-manager/src/action-reader";
10+
import { PluginFactory } from "@packages/core-manager/src/plugins/plugin-factory";
11+
import { defaults } from "@packages/core-manager/src/defaults";
1012
import { Assets } from "./__fixtures__";
1113

1214
let sandbox: Sandbox;
@@ -19,6 +21,8 @@ let logger = {
1921
error: jest.fn(),
2022
}
2123

24+
let pluginsConfiguration = defaults.plugins
25+
2226
beforeEach(() => {
2327
let dummyAction = new Assets.DummyAction();
2428
spyOnMethod = jest.spyOn(dummyAction, "method")
@@ -34,6 +38,10 @@ beforeEach(() => {
3438
sandbox.app.bind(Identifiers.HTTP).to(Server).inSingletonScope();
3539
sandbox.app.bind(Identifiers.ActionReader).toConstantValue(actionReader);
3640
sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger);
41+
sandbox.app.bind(Identifiers.PluginFactory).to(PluginFactory).inSingletonScope();
42+
sandbox.app.bind(Container.Identifiers.PluginConfiguration).toConstantValue({
43+
get: jest.fn().mockReturnValue(pluginsConfiguration)
44+
});
3745

3846
sandbox.app.terminate = jest.fn();
3947

@@ -45,17 +53,15 @@ afterEach(() => {
4553
})
4654

4755
describe("Server", () => {
48-
beforeEach(async () => {
49-
await server.initialize("serverName", {})
50-
await server.boot()
51-
})
52-
5356
afterEach(async () => {
5457
await server.dispose();
5558
})
5659

5760
describe("inject", () => {
5861
it("should be ok", async () => {
62+
await server.initialize("serverName", {})
63+
await server.boot()
64+
5965
const injectOptions = {
6066
method: "POST",
6167
url: "/",
@@ -77,6 +83,9 @@ describe("Server", () => {
7783
});
7884

7985
it("should return RCP error if called with invalid action params", async () => {
86+
await server.initialize("serverName", {})
87+
await server.boot()
88+
8089
const injectOptions = {
8190
method: "POST",
8291
url: "/",
@@ -99,6 +108,9 @@ describe("Server", () => {
99108
});
100109

101110
it("should return RCP error if error inside Action method", async () => {
111+
await server.initialize("serverName", {})
112+
await server.boot()
113+
102114
const injectOptions = {
103115
method: "POST",
104116
url: "/",
@@ -126,6 +138,9 @@ describe("Server", () => {
126138
});
127139

128140
it("should return RCP error if RPC schema is invalid", async () => {
141+
await server.initialize("serverName", {})
142+
await server.boot()
143+
129144
const injectOptions = {
130145
method: "POST",
131146
url: "/",
@@ -148,6 +163,9 @@ describe("Server", () => {
148163
});
149164

150165
it("should return RCP error if error in Validator", async () => {
166+
await server.initialize("serverName", {})
167+
await server.boot()
168+
151169
const injectOptions = {
152170
method: "POST",
153171
url: "/",
@@ -172,5 +190,38 @@ describe("Server", () => {
172190
expect(parsedResponse.statusCode).toBe(200)
173191
expect(parsedResponse.body.error.code).toBe(-32600)
174192
});
193+
194+
it("should return RCP error if whitelisted", async () => {
195+
pluginsConfiguration.whitelist = []
196+
197+
await server.initialize("serverName", {})
198+
await server.boot()
199+
200+
const injectOptions = {
201+
method: "POST",
202+
url: "/",
203+
payload: {
204+
jsonrpc: "2.0",
205+
id: "1",
206+
method: "dummy",
207+
params: { id: 123 },
208+
},
209+
headers: {
210+
"content-type": "application/vnd.api+json",
211+
},
212+
};
213+
214+
const response = await server.inject(injectOptions);
215+
const parsedResponse: Record<string, any> = { body: response.result, statusCode: response.statusCode };
216+
217+
expect(parsedResponse).toEqual({
218+
body: {
219+
jsonrpc: '2.0',
220+
error: { code: -32001, message: 'Unauthorized' },
221+
id: null
222+
},
223+
statusCode: 200
224+
})
225+
});
175226
})
176227
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * as Actions from "./action"
2+
export * as Plugins from "./plugins"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface RegisterPluginObject {
2+
plugin: any,
3+
options?: any
4+
}
5+
6+
export interface PluginFactory {
7+
preparePlugins(): Array<RegisterPluginObject>
8+
}

packages/core-manager/src/defaults.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ export const defaults = {
1515
cert: process.env.CORE_MONITOR_SSL_CERT,
1616
},
1717
},
18+
},
19+
plugins: {
20+
whitelist: ["*"]
1821
}
1922
};

packages/core-manager/src/ioc/identifiers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export const Identifiers = {
22
HTTP: Symbol.for("API<HTTP>"),
33
HTTPS: Symbol.for("API<HTTPS>"),
44
ActionReader: Symbol.for("Discover<Action>"),
5+
PluginFactory: Symbol.for("Factory<Plugin>"),
56
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./plugin-factory"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as rpc from "@hapist/json-rpc";
2+
import * as whitelist from "@hapist/whitelist";
3+
4+
import { Container, Providers } from "@arkecosystem/core-kernel";
5+
import { Validation } from "@arkecosystem/crypto";
6+
7+
import { Identifiers } from "../ioc";
8+
import { Plugins } from "../contracts";
9+
import { ActionReader } from "../action-reader";
10+
import { rpcResponseHandler } from "./rpc-response-handler";
11+
12+
@Container.injectable()
13+
export class PluginFactory implements Plugins.PluginFactory {
14+
@Container.inject(Container.Identifiers.PluginConfiguration)
15+
@Container.tagged("plugin", "@arkecosystem/core-manager")
16+
private readonly configuration!: Providers.PluginConfiguration;
17+
18+
@Container.inject(Identifiers.ActionReader)
19+
private readonly actionReader!: ActionReader;
20+
21+
public preparePlugins(): Array<Plugins.RegisterPluginObject> {
22+
let pluginConfig = this.configuration.get("plugins")
23+
24+
return [
25+
{
26+
plugin: rpcResponseHandler
27+
},
28+
{
29+
plugin: whitelist,
30+
options: {
31+
// @ts-ignore
32+
whitelist: pluginConfig.whitelist
33+
// whitelist: ["*"],
34+
// whitelist: [],
35+
},
36+
},
37+
{
38+
plugin: rpc,
39+
options: {
40+
methods: this.actionReader.discoverActions(),
41+
processor: {
42+
schema: {
43+
properties: {
44+
id: {
45+
type: ["number", "string"],
46+
},
47+
jsonrpc: {
48+
pattern: "2.0",
49+
type: "string",
50+
},
51+
method: {
52+
type: "string",
53+
},
54+
params: {
55+
type: "object",
56+
},
57+
},
58+
required: ["jsonrpc", "method", "id"],
59+
type: "object",
60+
},
61+
validate(data: object, schema: object) {
62+
try {
63+
const { error } = Validation.validator.validate(schema, data);
64+
return { value: data, error: error ? error : null };
65+
} catch (error) {
66+
return { value: null, error: error.stack };
67+
}
68+
},
69+
},
70+
},
71+
}
72+
]
73+
}
74+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Server as HapiServer } from "@hapi/hapi";
2+
3+
const getRpcResponseCode = (httpResponseCode: number) => {
4+
return -32001
5+
6+
// TODO: Implement after auth plugin
7+
// if (httpResponseCode === 401) {
8+
// return -32001
9+
// } if (httpResponseCode === 403) {
10+
// return -32001
11+
// }
12+
//
13+
// throw new Error("Unsupported status code")
14+
}
15+
16+
export const rpcResponseHandler = {
17+
name: "rcpResponseHandler",
18+
version: "0.1.0",
19+
register: (server: HapiServer) => {
20+
server.ext({
21+
type: "onPreResponse",
22+
method(request, h) {
23+
let response = request.response;
24+
if (!response.isBoom) {
25+
return h.continue;
26+
}
27+
28+
return h.response({
29+
jsonrpc: "2.0",
30+
error: {
31+
code: getRpcResponseCode(response.output.statusCode),
32+
message: response.output.payload.message,
33+
},
34+
id: null
35+
});
36+
}
37+
})
38+
}
39+
}

packages/core-manager/src/server.ts

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import { Server as HapiServer, ServerInjectOptions, ServerInjectResponse } from "@hapi/hapi";
2-
import * as rpc from "@hapist/json-rpc";
3-
42
import { readFileSync } from "fs";
53

64
import { Container, Contracts, Types } from "@arkecosystem/core-kernel";
7-
import { Validation } from "@arkecosystem/crypto";
85

96
import { Identifiers } from "./ioc";
10-
import { ActionReader } from "./action-reader";
7+
import { Plugins } from "./contracts";
118

129
@Container.injectable()
1310
export class Server {
@@ -17,53 +14,19 @@ export class Server {
1714
@Container.inject(Container.Identifiers.LogService)
1815
private readonly logger!: Contracts.Kernel.Logger;
1916

20-
@Container.inject(Identifiers.ActionReader)
21-
private readonly actionReader!: ActionReader;
17+
@Container.inject(Identifiers.PluginFactory)
18+
private readonly pluginFactory!: Plugins.PluginFactory;
2219

23-
private server: HapiServer;
20+
private server!: HapiServer;
2421

2522
private name!: string;
2623

27-
public async initialize(name: string, optionsServer: Types.JsonObject): Promise<void> {
24+
public async initialize(name: string, serverOptions: Types.JsonObject): Promise<void> {
2825
this.name = name;
29-
this.server = new HapiServer(this.getServerOptions(optionsServer));
30-
this.server.app.app = this.app;
31-
32-
await this.server.register({
33-
plugin: rpc,
34-
options: {
35-
methods: this.actionReader.discoverActions(),
36-
processor: {
37-
schema: {
38-
properties: {
39-
id: {
40-
type: ["number", "string"],
41-
},
42-
jsonrpc: {
43-
pattern: "2.0",
44-
type: "string",
45-
},
46-
method: {
47-
type: "string",
48-
},
49-
params: {
50-
type: "object",
51-
},
52-
},
53-
required: ["jsonrpc", "method", "id"],
54-
type: "object",
55-
},
56-
validate(data: object, schema: object) {
57-
try {
58-
const { error } = Validation.validator.validate(schema, data);
59-
return { value: data, error: error ? error : null };
60-
} catch (error) {
61-
return { value: null, error: error.stack };
62-
}
63-
},
64-
},
65-
},
66-
});
26+
this.server = new HapiServer(this.getServerOptions(serverOptions));
27+
// this.server.app.app = this.app;
28+
29+
await this.server.register(this.pluginFactory.preparePlugins());
6730
}
6831

6932
public async inject(options: string | ServerInjectOptions): Promise<ServerInjectResponse> {

0 commit comments

Comments
 (0)