UNPKG

9.54 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const Ajv = require("ajv");
4const lodash_1 = require("lodash");
5const accounts_1 = require("./accounts");
6const config_1 = require("./config");
7const middleware_manager_1 = require("./middleware-manager");
8const routing_table_1 = require("./routing-table");
9const route_broadcaster_1 = require("./route-broadcaster");
10const stats_1 = require("./stats");
11const rate_backend_1 = require("./rate-backend");
12const utils_1 = require("../routing/utils");
13const http_1 = require("http");
14const invalid_json_body_error_1 = require("../errors/invalid-json-body-error");
15const log_1 = require("../common/log");
16const Prometheus = require("prom-client");
17const log = log_1.create('admin-api');
18const ajv = new Ajv();
19const validateBalanceUpdate = ajv.compile(require('../schemas/BalanceUpdate.json'));
20class 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}
232exports.default = AdminApi;
233//# sourceMappingURL=admin-api.js.map
\No newline at end of file