1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const Ajv = require("ajv");
|
4 | const lodash_1 = require("lodash");
|
5 | const accounts_1 = require("./accounts");
|
6 | const config_1 = require("./config");
|
7 | const middleware_manager_1 = require("./middleware-manager");
|
8 | const routing_table_1 = require("./routing-table");
|
9 | const route_broadcaster_1 = require("./route-broadcaster");
|
10 | const stats_1 = require("./stats");
|
11 | const rate_backend_1 = require("./rate-backend");
|
12 | const utils_1 = require("../routing/utils");
|
13 | const http_1 = require("http");
|
14 | const invalid_json_body_error_1 = require("../errors/invalid-json-body-error");
|
15 | const log_1 = require("../common/log");
|
16 | const Prometheus = require("prom-client");
|
17 | const log = log_1.create('admin-api');
|
18 | const ajv = new Ajv();
|
19 | const validateBalanceUpdate = ajv.compile(require('../schemas/BalanceUpdate.json'));
|
20 | class AdminApi {
|
21 | constructor(deps) {
|
22 | this.accounts = deps(accounts_1.default);
|
23 | this.config = deps(config_1.default);
|
24 | this.middlewareManager = deps(middleware_manager_1.default);
|
25 | this.routingTable = deps(routing_table_1.default);
|
26 | this.routeBroadcaster = deps(route_broadcaster_1.default);
|
27 | this.rateBackend = deps(rate_backend_1.default);
|
28 | this.stats = deps(stats_1.default);
|
29 | this.routes = [
|
30 | { method: 'GET', match: '/status$', fn: this.getStatus },
|
31 | { method: 'GET', match: '/routing$', fn: this.getRoutingStatus },
|
32 | { method: 'GET', match: '/accounts$', fn: this.getAccountStatus },
|
33 | { method: 'GET', match: '/accounts/', fn: this.getAccountAdminInfo },
|
34 | { method: 'POST', match: '/accounts/', fn: this.sendAccountAdminInfo },
|
35 | { method: 'GET', match: '/balance$', fn: this.getBalanceStatus },
|
36 | { method: 'POST', match: '/balance$', fn: this.postBalance },
|
37 | { method: 'GET', match: '/rates$', fn: this.getBackendStatus },
|
38 | { method: 'GET', match: '/stats$', fn: this.getStats },
|
39 | { method: 'GET', match: '/alerts$', fn: this.getAlerts },
|
40 | { method: 'DELETE', match: '/alerts/', fn: this.deleteAlert },
|
41 | { method: 'GET', match: '/metrics$', fn: this.getMetrics, responseType: Prometheus.register.contentType },
|
42 | { method: 'POST', match: '/addAccount$', fn: this.addAccount }
|
43 | ];
|
44 | }
|
45 | listen() {
|
46 | const { adminApi = false, adminApiHost = '127.0.0.1', adminApiPort = 7780 } = this.config;
|
47 | log.info('listen called');
|
48 | if (adminApi) {
|
49 | log.info('admin api listening. host=%s port=%s', adminApiHost, adminApiPort);
|
50 | this.server = new http_1.Server();
|
51 | this.server.listen(adminApiPort, adminApiHost);
|
52 | this.server.on('request', (req, res) => {
|
53 | this.handleRequest(req, res).catch((e) => {
|
54 | let err = e;
|
55 | if (!e || typeof e !== 'object') {
|
56 | err = new Error('non-object thrown. error=' + e);
|
57 | }
|
58 | log.warn('error in admin api request handler. error=%s', err.stack ? err.stack : err);
|
59 | res.statusCode = e.httpErrorCode || 500;
|
60 | res.setHeader('Content-Type', 'text/plain');
|
61 | res.end(String(err));
|
62 | });
|
63 | });
|
64 | }
|
65 | }
|
66 | async handleRequest(req, res) {
|
67 | req.setEncoding('utf8');
|
68 | let body = '';
|
69 | await new Promise((resolve, reject) => {
|
70 | req.on('data', data => body += data);
|
71 | req.once('end', resolve);
|
72 | req.once('error', reject);
|
73 | });
|
74 | const urlPrefix = (req.url || '').split('?')[0] + '$';
|
75 | const route = this.routes.find((route) => route.method === req.method && urlPrefix.startsWith(route.match));
|
76 | if (!route) {
|
77 | res.statusCode = 404;
|
78 | res.setHeader('Content-Type', 'text/plain');
|
79 | res.end('Not Found');
|
80 | return;
|
81 | }
|
82 | const resBody = await route.fn.call(this, req.url, body && JSON.parse(body));
|
83 | if (resBody) {
|
84 | res.statusCode = 200;
|
85 | if (route.responseType) {
|
86 | res.setHeader('Content-Type', route.responseType);
|
87 | res.end(resBody);
|
88 | }
|
89 | else {
|
90 | res.setHeader('Content-Type', 'application/json');
|
91 | res.end(JSON.stringify(resBody));
|
92 | }
|
93 | }
|
94 | else {
|
95 | res.statusCode = 204;
|
96 | res.end();
|
97 | }
|
98 | }
|
99 | async getStatus() {
|
100 | const balanceStatus = await this.getBalanceStatus();
|
101 | const accountStatus = await this.getAccountStatus();
|
102 | return {
|
103 | balances: lodash_1.mapValues(balanceStatus['accounts'], 'balance'),
|
104 | connected: lodash_1.mapValues(accountStatus['accounts'], 'connected'),
|
105 | localRoutingTable: utils_1.formatRoutingTableAsJson(this.routingTable)
|
106 | };
|
107 | }
|
108 | async getRoutingStatus() {
|
109 | return this.routeBroadcaster.getStatus();
|
110 | }
|
111 | async getAccountStatus() {
|
112 | return this.accounts.getStatus();
|
113 | }
|
114 | async getBalanceStatus() {
|
115 | const middleware = this.middlewareManager.getMiddleware('balance');
|
116 | if (!middleware)
|
117 | return {};
|
118 | const balanceMiddleware = middleware;
|
119 | return balanceMiddleware.getStatus();
|
120 | }
|
121 | async postBalance(url, _data) {
|
122 | try {
|
123 | validateBalanceUpdate(_data);
|
124 | }
|
125 | catch (err) {
|
126 | const firstError = (validateBalanceUpdate.errors &&
|
127 | validateBalanceUpdate.errors[0]) ||
|
128 | { message: 'unknown validation error', dataPath: '' };
|
129 | throw new invalid_json_body_error_1.default('invalid balance update: error=' + firstError.message + ' dataPath=' + firstError.dataPath, validateBalanceUpdate.errors || []);
|
130 | }
|
131 | const data = _data;
|
132 | const middleware = this.middlewareManager.getMiddleware('balance');
|
133 | if (!middleware)
|
134 | return;
|
135 | const balanceMiddleware = middleware;
|
136 | balanceMiddleware.modifyBalance(data.accountId, data.amountDiff);
|
137 | }
|
138 | getBackendStatus() {
|
139 | return this.rateBackend.getStatus();
|
140 | }
|
141 | async getStats() {
|
142 | return this.stats.getStatus();
|
143 | }
|
144 | async getAlerts() {
|
145 | const middleware = this.middlewareManager.getMiddleware('alert');
|
146 | if (!middleware)
|
147 | return {};
|
148 | const alertMiddleware = middleware;
|
149 | return {
|
150 | alerts: alertMiddleware.getAlerts()
|
151 | };
|
152 | }
|
153 | async deleteAlert(url) {
|
154 | const middleware = this.middlewareManager.getMiddleware('alert');
|
155 | if (!middleware)
|
156 | return {};
|
157 | const alertMiddleware = middleware;
|
158 | if (!url)
|
159 | throw new Error('no path on request');
|
160 | const match = /^\/alerts\/(\d+)$/.exec(url.split('?')[0]);
|
161 | if (!match)
|
162 | throw new Error('invalid alert id');
|
163 | alertMiddleware.dismissAlert(+match[1]);
|
164 | }
|
165 | async getMetrics() {
|
166 | const promRegistry = Prometheus.register;
|
167 | const ilpRegistry = this.stats.getRegistry();
|
168 | const mergedRegistry = Prometheus.Registry.merge([promRegistry, ilpRegistry]);
|
169 | return mergedRegistry.metrics();
|
170 | }
|
171 | _getPlugin(url) {
|
172 | if (!url)
|
173 | throw new Error('no path on request');
|
174 | const match = /^\/accounts\/([A-Za-z0-9_.\-~]+)$/.exec(url.split('?')[0]);
|
175 | if (!match)
|
176 | throw new Error('invalid account.');
|
177 | const account = match[1];
|
178 | const plugin = this.accounts.getPlugin(account);
|
179 | if (!plugin)
|
180 | throw new Error('account does not exist. account=' + account);
|
181 | const info = this.accounts.getInfo(account);
|
182 | return {
|
183 | account,
|
184 | info,
|
185 | plugin
|
186 | };
|
187 | }
|
188 | async getAccountAdminInfo(url) {
|
189 | if (!url)
|
190 | throw new Error('no path on request');
|
191 | const { account, info, plugin } = this._getPlugin(url);
|
192 | if (!plugin.getAdminInfo)
|
193 | throw new Error('plugin has no admin info. account=' + account);
|
194 | return {
|
195 | account,
|
196 | plugin: info.plugin,
|
197 | info: (await plugin.getAdminInfo())
|
198 | };
|
199 | }
|
200 | async sendAccountAdminInfo(url, body) {
|
201 | if (!url)
|
202 | throw new Error('no path on request');
|
203 | if (!body)
|
204 | throw new Error('no json body provided to set admin info.');
|
205 | const { account, info, plugin } = this._getPlugin(url);
|
206 | if (!plugin.sendAdminInfo)
|
207 | throw new Error('plugin does not support sending admin info. account=' + account);
|
208 | return {
|
209 | account,
|
210 | plugin: info.plugin,
|
211 | result: (await plugin.sendAdminInfo(body))
|
212 | };
|
213 | }
|
214 | async addAccount(url, body) {
|
215 | if (!url)
|
216 | throw new Error('no path on request');
|
217 | if (!body)
|
218 | throw new Error('no json body provided to make plugin.');
|
219 | const { id, options } = body;
|
220 | this.accounts.add(id, options);
|
221 | const plugin = this.accounts.getPlugin(id);
|
222 | await this.middlewareManager.addPlugin(id, plugin);
|
223 | await plugin.connect({ timeout: Infinity });
|
224 | this.routeBroadcaster.track(id);
|
225 | this.routeBroadcaster.reloadLocalRoutes();
|
226 | return {
|
227 | plugin: id,
|
228 | connected: plugin.isConnected()
|
229 | };
|
230 | }
|
231 | }
|
232 | exports.default = AdminApi;
|
233 |
|
\ | No newline at end of file |