1
import reduct = require('reduct')
2 2
import Ajv = require('ajv')
3 2
import { mapValues as pluck } from 'lodash'
4 2
import Accounts from './accounts'
5 2
import Config from './config'
6 2
import MiddlewareManager from './middleware-manager'
7
import BalanceMiddleware from '../middlewares/balance'
8
import AlertMiddleware from '../middlewares/alert'
9 2
import RoutingTable from './routing-table'
10 2
import RouteBroadcaster from './route-broadcaster'
11 2
import Stats from './stats'
12 2
import RateBackend from './rate-backend'
13 2
import { formatRoutingTableAsJson } from '../routing/utils'
14 2
import { Server, IncomingMessage, ServerResponse } from 'http'
15 2
import InvalidJsonBodyError from '../errors/invalid-json-body-error'
16
import { BalanceUpdate } from '../schemas/BalanceUpdate'
17 2
import { create as createLogger } from '../common/log'
18 2
import * as Prometheus from 'prom-client'
19 2
const log = createLogger('admin-api')
20 2
const ajv = new Ajv()
21 2
const validateBalanceUpdate = ajv.compile(require('../schemas/BalanceUpdate.json'))
22

23
interface Route {
24
  method: 'GET' | 'POST' | 'DELETE'
25
  match: string
26
  fn: (url: string | undefined, body: object) => Promise<object | string | void>
27
  responseType?: string
28
}
29

30 2
export default class AdminApi {
31
  private accounts: Accounts
32
  private config: Config
33
  private middlewareManager: MiddlewareManager
34
  private routingTable: RoutingTable
35
  private routeBroadcaster: RouteBroadcaster
36
  private rateBackend: RateBackend
37
  private stats: Stats
38

39
  private server?: Server
40
  private routes: Route[]
41

42
  constructor (deps: reduct.Injector) {
43 2
    this.accounts = deps(Accounts)
44 2
    this.config = deps(Config)
45 2
    this.middlewareManager = deps(MiddlewareManager)
46 2
    this.routingTable = deps(RoutingTable)
47 2
    this.routeBroadcaster = deps(RouteBroadcaster)
48 2
    this.rateBackend = deps(RateBackend)
49 2
    this.stats = deps(Stats)
50

51 2
    this.routes = [
52
      { method: 'GET', match: '/status$', fn: this.getStatus },
53
      { method: 'GET', match: '/routing$', fn: this.getRoutingStatus },
54
      { method: 'GET', match: '/accounts$', fn: this.getAccountStatus },
55
      { method: 'GET', match: '/accounts/', fn: this.getAccountAdminInfo },
56
      { method: 'POST', match: '/accounts/', fn: this.sendAccountAdminInfo },
57
      { method: 'GET', match: '/balance$', fn: this.getBalanceStatus },
58
      { method: 'POST', match: '/balance$', fn: this.postBalance },
59
      { method: 'GET', match: '/rates$', fn: this.getBackendStatus },
60
      { method: 'GET', match: '/stats$', fn: this.getStats },
61
      { method: 'GET', match: '/alerts$', fn: this.getAlerts },
62
      { method: 'DELETE', match: '/alerts/', fn: this.deleteAlert },
63
      { method: 'GET', match: '/metrics$', fn: this.getMetrics, responseType: Prometheus.register.contentType },
64
      { method: 'POST', match: '/addAccount$', fn: this.addAccount }
65
    ]
66
  }
67

68
  listen () {
69
    const {
70 2
      adminApi = false,
71 2
      adminApiHost = '127.0.0.1',
72 2
      adminApiPort = 7780
73 0
    } = this.config
74

75 0
    log.info('listen called')
76

77 2
    if (adminApi) {
78 0
      log.info('admin api listening. host=%s port=%s', adminApiHost, adminApiPort)
79 0
      this.server = new Server()
80 0
      this.server.listen(adminApiPort, adminApiHost)
81 0
      this.server.on('request', (req, res) => {
82 0
        this.handleRequest(req, res).catch((e) => {
83 0
          let err = e
84 2
          if (!e || typeof e !== 'object') {
85 0
            err = new Error('non-object thrown. error=' + e)
86
          }
87

88 2
          log.warn('error in admin api request handler. error=%s', err.stack ? err.stack : err)
89 2
          res.statusCode = e.httpErrorCode || 500
90 0
          res.setHeader('Content-Type', 'text/plain')
91 0
          res.end(String(err))
92
        })
93
      })
94
    }
95
  }
96

97
  private async handleRequest (req: IncomingMessage, res: ServerResponse) {
98 0
    req.setEncoding('utf8')
99 0
    let body = ''
100 0
    await new Promise((resolve, reject) => {
101 0
      req.on('data', data => body += data)
102 0
      req.once('end', resolve)
103 0
      req.once('error', reject)
104
    })
105

106 2
    const urlPrefix = (req.url || '').split('?')[0] + '$'
107 0
    const route = this.routes.find((route) =>
108 2
      route.method === req.method && urlPrefix.startsWith(route.match))
109 2
    if (!route) {
110 0
      res.statusCode = 404
111 0
      res.setHeader('Content-Type', 'text/plain')
112 0
      res.end('Not Found')
113 0
      return
114
    }
115

116 2
    const resBody = await route.fn.call(this, req.url, body && JSON.parse(body))
117 2
    if (resBody) {
118 0
      res.statusCode = 200
119 2
      if (route.responseType) {
120 0
        res.setHeader('Content-Type', route.responseType)
121 0
        res.end(resBody)
122
      } else {
123 0
        res.setHeader('Content-Type', 'application/json')
124 0
        res.end(JSON.stringify(resBody))
125
      }
126
    } else {
127 0
      res.statusCode = 204
128 0
      res.end()
129
    }
130
  }
131

132
  private async getStatus () {
133 2
    const balanceStatus = await this.getBalanceStatus()
134 2
    const accountStatus = await this.getAccountStatus()
135 2
    return {
136
      balances: pluck(balanceStatus['accounts'], 'balance'),
137
      connected: pluck(accountStatus['accounts'], 'connected'),
138
      localRoutingTable: formatRoutingTableAsJson(this.routingTable)
139
    }
140
  }
141

142
  private async getRoutingStatus () {
143 2
    return this.routeBroadcaster.getStatus()
144
  }
145

146
  private async getAccountStatus () {
147 2
    return this.accounts.getStatus()
148
  }
149

150
  private async getBalanceStatus () {
151 2
    const middleware = this.middlewareManager.getMiddleware('balance')
152 2
    if (!middleware) return {}
153 2
    const balanceMiddleware = middleware as BalanceMiddleware
154 2
    return balanceMiddleware.getStatus()
155
  }
156

157
  private async postBalance (url: string | undefined, _data: object) {
158 2
    try {
159 2
      validateBalanceUpdate(_data)
160
    } catch (err) {
161 2
      const firstError = (validateBalanceUpdate.errors &&
162
        validateBalanceUpdate.errors[0]) ||
163
        { message: 'unknown validation error', dataPath: '' }
164 2
      throw new InvalidJsonBodyError('invalid balance update: error=' + firstError.message + ' dataPath=' + firstError.dataPath, validateBalanceUpdate.errors || [])
165
    }
166

167 2
    const data = _data as BalanceUpdate
168 2
    const middleware = this.middlewareManager.getMiddleware('balance')
169 2
    if (!middleware) return
170 2
    const balanceMiddleware = middleware as BalanceMiddleware
171 2
    balanceMiddleware.modifyBalance(data.accountId, data.amountDiff)
172
  }
173

174
  private getBackendStatus (): Promise<{ [s: string]: any }> {
175 2
    return this.rateBackend.getStatus()
176
  }
177

178
  private async getStats () {
179 2
    return this.stats.getStatus()
180
  }
181

182
  private async getAlerts () {
183 2
    const middleware = this.middlewareManager.getMiddleware('alert')
184 2
    if (!middleware) return {}
185 2
    const alertMiddleware = middleware as AlertMiddleware
186 2
    return {
187
      alerts: alertMiddleware.getAlerts()
188
    }
189
  }
190

191
  private async deleteAlert (url: string | undefined) {
192 2
    const middleware = this.middlewareManager.getMiddleware('alert')
193 2
    if (!middleware) return {}
194 2
    const alertMiddleware = middleware as AlertMiddleware
195 2
    if (!url) throw new Error('no path on request')
196 2
    const match = /^\/alerts\/(\d+)$/.exec(url.split('?')[0])
197 2
    if (!match) throw new Error('invalid alert id')
198 2
    alertMiddleware.dismissAlert(+match[1])
199
  }
200

201
  private async getMetrics () {
202 0
    const promRegistry = Prometheus.register
203 0
    const ilpRegistry = this.stats.getRegistry()
204 0
    const mergedRegistry = Prometheus.Registry.merge([promRegistry, ilpRegistry])
205 0
    return mergedRegistry.metrics()
206
  }
207

208
  private _getPlugin (url: string | undefined) {
209 2
    if (!url) throw new Error('no path on request')
210 2
    const match = /^\/accounts\/([A-Za-z0-9_.\-~]+)$/.exec(url.split('?')[0])
211 2
    if (!match) throw new Error('invalid account.')
212 2
    const account = match[1]
213 2
    const plugin = this.accounts.getPlugin(account)
214 2
    if (!plugin) throw new Error('account does not exist. account=' + account)
215 2
    const info = this.accounts.getInfo(account)
216 2
    return {
217
      account,
218
      info,
219
      plugin
220
    }
221
  }
222

223
  private async getAccountAdminInfo (url: string | undefined) {
224 2
    if (!url) throw new Error('no path on request')
225 2
    const { account, info, plugin } = this._getPlugin(url)
226 2
    if (!plugin.getAdminInfo) throw new Error('plugin has no admin info. account=' + account)
227 2
    return {
228
      account,
229
      plugin: info.plugin,
230
      info: (await plugin.getAdminInfo())
231
    }
232
  }
233

234
  private async sendAccountAdminInfo (url: string | undefined, body?: object) {
235 2
    if (!url) throw new Error('no path on request')
236 2
    if (!body) throw new Error('no json body provided to set admin info.')
237 2
    const { account, info, plugin } = this._getPlugin(url)
238 2
    if (!plugin.sendAdminInfo) throw new Error('plugin does not support sending admin info. account=' + account)
239 2
    return {
240
      account,
241
      plugin: info.plugin,
242
      result: (await plugin.sendAdminInfo(body))
243
    }
244
  }
245

246
  private async addAccount (url: string | undefined, body?: any) {
247 2
    if (!url) throw new Error('no path on request')
248 2
    if (!body) throw new Error('no json body provided to make plugin.')
249

250 2
    const { id, options } = body
251 2
    this.accounts.add(id, options)
252 2
    const plugin = this.accounts.getPlugin(id)
253 2
    await this.middlewareManager.addPlugin(id, plugin)
254

255 2
    await plugin.connect({ timeout: Infinity })
256 2
    this.routeBroadcaster.track(id)
257 2
    this.routeBroadcaster.reloadLocalRoutes()
258 2
    return {
259
      plugin: id,
260
      connected: plugin.isConnected()
261
    }
262
  }
263
}

Read our documentation on viewing source code .

Loading