/ lib / http-server / http-server.js
http-server.js
  1  /*!
  2   * lib/http-server/http-server.js
  3   * Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved.
  4   */
  5  
  6  
  7  // eslint-disable-next-line import/no-unresolved
  8  import { App } from '@tinyhttp/app'
  9  import sirv from 'sirv'
 10  import helmet from 'helmet'
 11  import nocache from 'nocache'
 12  
 13  import Logger from '../logger.js'
 14  import errors from '../errors.js'
 15  import network from '../bitcoin/network.js'
 16  import keysFile from '../../keys/index.js'
 17  
 18  const keys = keysFile[network.key]
 19  
 20  /**
 21   * @typedef {import('@tinyhttp/app').Request} Request
 22   * @typedef {import('@tinyhttp/app').Response} Response
 23   * @typedef {import('@tinyhttp/app').NextFunction} NextFunction
 24   */
 25  
 26  /**
 27   * HTTP server
 28   */
 29  class HttpServer {
 30  
 31      /**
 32       * Constructor
 33       * @param {number} port - port used by the http server
 34       * @param {string} host - host exposing the http server
 35       */
 36      constructor(port, host) {
 37          // Initialize server host and port
 38          this.host = host ?? '127.0.0.1'
 39          this.port = port
 40  
 41          // Listening server instance
 42          this.server = null
 43  
 44          // Initialize the tiny-http app
 45          this.app = new App({
 46              noMatchHandler: (req, res) => {
 47                  Logger.error(null, `HttpServer : 404 - Not found: ${req.method} - ${req.hostname}${req.path}`)
 48                  HttpServer.sendError(res, { status: '404 - Not found' }, 404)
 49              },
 50              // Error handler
 51              onError: (error, req, res) => {
 52                  // Detect if this is auth error
 53                  if (Object.values(errors.auth).includes(error)) {
 54                      HttpServer.sendError(res, error, 401)
 55                  } else {
 56                      Logger.error(error.stack || error, 'HttpServer : general error')
 57                      const returnValue = { status: 'Server error' }
 58                      HttpServer.sendError(res, returnValue, 500)
 59                  }
 60              }
 61          })
 62  
 63          // Middlewares for json responses and requests logging
 64          this.app.use(HttpServer.requestLogger)
 65          this.app.use(HttpServer.setCrossOrigin)
 66          this.app.use(helmet(HttpServer.HELMET_POLICY))
 67          this.app.use(nocache())
 68  
 69          this.app.use('/static', sirv('../static'))
 70  
 71          this.app.use((req, res, next) => {
 72              res.append('X-Dojo-Version', keys.dojoVersion)
 73              next()
 74          })
 75  
 76          this.app.use(HttpServer.setJSONResponse)
 77          this.app.use(HttpServer.setConnection)
 78      }
 79  
 80  
 81      /**
 82       * Start the http server
 83       * @returns {Server} returns the listening server instance
 84       */
 85      start() {
 86          // Start a http server
 87          this.server = this.app.listen(this.port, () => {
 88              Logger.info(`HttpServer : Listening on ${this.host}:${this.port}`)
 89          }, this.host)
 90  
 91          this.server.timeout = 600 * 1000
 92          // @see https://github.com/nodejs/node/issues/13391
 93          this.server.keepAliveTimeout = 0
 94  
 95          return this.server
 96      }
 97  
 98      /**
 99       * Stop the http server
100       */
101      stop() {
102          if (this.server == null) return
103          this.server.close()
104      }
105  
106      /**
107       * Return a http response without data
108       * @param {Response} res - http response object
109       */
110      static sendOk(res) {
111          const returnValue = { status: 'ok' }
112          res.status(200).json(returnValue)
113      }
114  
115      /**
116       * Return a http response without status
117       * @param {Response} res - http response object
118       * @param {object} data
119       */
120      static sendOkDataOnly(res, data) {
121          res.status(200).json(data)
122      }
123  
124      /**
125       * Return a http response with status and data
126       * @param {Response} res - http response object
127       * @param {object | number | string} data - data object
128       */
129      static sendOkData(res, data) {
130          const returnValue = {
131              status: 'ok',
132              data: data
133          }
134          res.status(200).json(returnValue)
135      }
136  
137      /**
138       * Return a http response with raw data
139       * @param {Response} res - http response object
140       * @param {object} data - data object
141       */
142      static sendRawData(res, data) {
143          res.status(200).send(data)
144      }
145  
146      /**
147       * Return an error response
148       * @param {Response} res - http response object
149       * @param {string | Error} data - data object
150       * @param {number} [errorCode=400] - HTTP status code
151       */
152      static sendError(res, data, errorCode) {
153          if (errorCode == null)
154              errorCode = 400
155  
156          if (data instanceof Error) {
157              Logger.error(data, 'API: Unhandled error')
158              data = errors.generic.GEN
159          }
160  
161          const returnValue = {
162              status: 'error',
163              error: data
164          }
165  
166          res.status(errorCode).json(returnValue)
167      }
168  
169      /*
170     * A middleware returning an authorization error response
171     * @param {Response} res - http response object
172     * @param {string} err - error
173     */
174      static sendAuthError(res, error) {
175          if (error) {
176              HttpServer.sendError(res, error, 401)
177          }
178      }
179  
180      /**
181       * Express middleware returnsing a json response
182       * @param {Request} req - http request object
183       * @param {Response} res - http response object
184       * @param {NextFunction} next - next middleware
185       */
186      static setJSONResponse(req, res, next) {
187          res.set('Content-Type', 'application/json')
188          next()
189      }
190  
191      /**
192       * Express middleware adding cors header
193       * @param {Request} req - http request object
194       * @param {Response} res - http response object
195       * @param {NextFunction} next - next middleware
196       */
197      static setCrossOrigin(req, res, next) {
198          res.set('Access-Control-Allow-Origin', '*')
199          res.set('Access-Control-Allow-Methods', 'GET,HEAD,POST,DELETE')
200          res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization,Accept')
201          next()
202      }
203  
204      /**
205       * Express middleware adding connection header
206       * @param {Response} req - http request object
207       * @param {Response} res - http response object
208       * @param {NextFunction} next - next middleware
209       */
210      static setConnection(req, res, next) {
211          res.set('Connection', 'close')
212          next()
213      }
214  
215      /**
216       * Express middleware logging url and methods called
217       * @param {Request} req - http request object
218       * @param {Response} res - http response object
219       * @param {NextFunction} next - next middleware
220       */
221      static requestLogger(req, res, next) {
222          const method = req.method
223          const url = req.originalUrl
224  
225          res.on('finish', () => {
226              Logger.info(`HttpServer : ${res.statusCode} - ${method} ${url}`)
227          })
228          next()
229      }
230  
231  }
232  
233  /**
234   * Helmet Policy
235   */
236  HttpServer.HELMET_POLICY = {
237      contentSecurityPolicy: {
238          useDefaults: false,
239          directives: {
240              'default-src': ['\'self\'', 'data:'],
241              'base-uri': ['\'self\''],
242              'font-src': ['\'self\'', 'https:', 'data:'],
243              'frame-ancestors': ['\'self\''],
244              'img-src': ['\'self\'', 'data:'],
245              'object-src': ['\'none\''],
246              'script-src': ['\'self\'', '\'unsafe-inline\''],
247              'style-src': ['\'self\'', 'https:', '\'unsafe-inline\''],
248              'media-src': ['\'self\'', 'data:'],
249          },
250      },
251      dnsPrefetchControl: true,
252      frameguard: true,
253      hidePoweredBy: true,
254      ieNoOpen: true,
255      noSniff: true,
256      referrerPolicy: true,
257      xssFilter: true
258  }
259  
260  
261  export default HttpServer