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