1 | 'use strict' |
||
2 | |||
3 | import path from 'path' |
||
4 | import restify from 'restify' |
||
5 | import logger from 'winston' |
||
6 | import * as errors from 'restify-errors' |
||
7 | import chalk from 'chalk' |
||
8 | import config from '../config' |
||
9 | import router from './router' |
||
10 | // import favicon from 'serve-favicon' |
||
11 | import compression from 'compression' |
||
12 | import helmet from 'helmet' |
||
13 | import validator from 'restify-joi-middleware' |
||
14 | import passport from './helpers/passport' |
||
15 | import restifyRender from 'restify-render-middleware' |
||
16 | import { pug } from 'consolidate' |
||
17 | import acceptLanguage from 'accept-language' |
||
18 | import i18n, { languages } from './helpers/i18n' |
||
19 | import socketio from 'socket.io' |
||
20 | import channels from './channels' |
||
21 | import corsMiddleware from 'restify-cors-middleware' |
||
22 | |||
23 | // set supported languages |
||
24 | acceptLanguage.languages(languages) |
||
25 | |||
26 | const server = restify.createServer({ |
||
27 | name: 'Restify Devise', |
||
28 | version: '1.0.0', |
||
29 | // https://github.com/restify/node-restify/issues/1219#issuecomment-328499227 |
||
30 | // https://github.com/makeomatic/restify-formatter-jsonapi/issues/2#issuecomment-307285452 |
||
31 | ignoreTrailingSlash: true |
||
32 | }) |
||
33 | |||
34 | // set websocket app |
||
35 | const io = socketio.listen(server.server) |
||
36 | channels(io) |
||
37 | |||
38 | server.use(restify.plugins.acceptParser(server.acceptable)) |
||
39 | server.use(restify.plugins.queryParser()) |
||
40 | server.use(restify.plugins.bodyParser()) |
||
41 | server.use(restify.plugins.fullResponse()) |
||
42 | server.use(restify.plugins.gzipResponse()) |
||
43 | |||
44 | // CORS middleware |
||
45 | const cors = corsMiddleware({ |
||
46 | preflightMaxAge: 5, |
||
47 | allowHeaders: ['Authorization'] |
||
48 | }) |
||
49 | |||
50 | server.pre(cors.preflight) |
||
51 | server.use(cors.actual) |
||
52 | |||
53 | // https://www.npmjs.com/package/compression#filter-1 |
||
54 | server.use(compression({ |
||
55 | filter (request, response) { |
||
56 | if (request.headers['x-no-compression']) { |
||
57 | logger.warn('server', 'X-No-Compresseion') |
||
58 | // don't compress responses with this request header |
||
59 | return false |
||
60 | } |
||
61 | |||
62 | // fallback to standard filter function |
||
63 | return compression.filter(request, response) |
||
64 | } |
||
65 | })) |
||
66 | |||
67 | // https://www.npmjs.com/package/helmet#how-it-works |
||
68 | server.use(helmet()) |
||
69 | server.use(helmet.noCache()) |
||
70 | server.use(helmet.referrerPolicy()) |
||
71 | |||
72 | // joi validation middleware for restify |
||
73 | server.use(validator({ |
||
74 | joiOptions: { |
||
75 | allowUnknown: false |
||
76 | }, |
||
77 | errorTransformer: (validationInput, joiError) => { |
||
78 | const { type, context } = joiError.details[0] |
||
79 | const retError = new errors.BadRequestError() |
||
80 | retError.body.message = { |
||
81 | warn: `'${i18n.t(context.key)}' ${i18n.t(type, context)}`, |
||
82 | context: joiError.details[0].context |
||
83 | } |
||
84 | return retError |
||
85 | } |
||
86 | })) |
||
87 | |||
88 | // i18n middleware for restify |
||
89 | server.use(i18n.handler()) |
||
90 | |||
91 | // add res.render() |
||
92 | server.use(restifyRender({ |
||
93 | engine: pug, |
||
94 | dir: path.join(__dirname, 'views') |
||
95 | })) |
||
96 | |||
97 | server.pre((request, response, next) => { |
||
98 | logger.info('[server]', request.method, request.url) |
||
99 | next() |
||
100 | }) |
||
101 | |||
102 | server.pre((req, res, next) => { |
||
103 | const headerValue = req.headers['accept-language'] |
||
104 | |||
105 | if (headerValue) { |
||
106 | i18n.changeLanguage(acceptLanguage.get(headerValue)) |
||
107 | } |
||
108 | |||
109 | next() |
||
110 | }) |
||
111 | |||
112 | passport.initialize(server) |
||
113 | |||
114 | // asset routing added |
||
115 | server.get('/assets/*', restify.plugins.serveStatic({ |
||
116 | directory: __dirname |
||
117 | // default: 'style.css' |
||
118 | })) |
||
119 | |||
120 | // set routes app |
||
121 | router(server) |
||
122 | |||
123 | // TODO |
||
124 | // http://restify.com/docs/plugins-api/#auditlogger |
||
125 | // https://github.com/trentm/node-bunyan-winston/blob/master/restify-winston.js#L18-L80 |
||
126 | |||
127 | // Restify servers emit all the events from the node http.Server and has several other events you want to listen on. |
||
128 | // http://nodejs.org/docs/latest/api/http.html#http_class_http_server |
||
129 | |||
130 | // When a client request is sent for a URL that does not exist, restify will emit this event. |
||
131 | // Note that restify checks for listeners on this event, and if there are none, responds with a default 404 handler. |
||
132 | // It is expected that if you listen for this event, you respond to the client. |
||
133 | |||
134 | server.on('NotFound', (request, response, error, next) => { |
||
135 | const url = (request.isSecure()) |
||
136 | ? 'https' |
||
137 | : 'http' + '://' + request.headers.host + request.url |
||
138 | |||
139 | logger.warn('[server]', `Route ${chalk.cyan(url)} not found`) |
||
140 | return next(new errors.NotFoundError()) |
||
141 | }) |
||
142 | |||
143 | // When a client request is sent for a URL that does exist, but you have not registered a route for that HTTP verb, |
||
144 | // restify will emit this event. Note that restify checks for listeners on this event, and if there are none, |
||
145 | // responds with a default 405 handler. It is expected that if you listen for this event, you respond to the client. |
||
146 | // server.on('MethodNotAllowed', (request, response, next) => {}) |
||
147 | |||
148 | // When a client request is sent for a route that exists, but does not match the version(s) on those routes, |
||
149 | // restify will emit this event. Note that restify checks for listeners on this event, and if there are none, |
||
150 | // responds with a default 400 handler. It is expected that if you listen for this event, you respond to the client. |
||
151 | // server.on('VersionNotAllowed', (request, response, next) => {}) |
||
152 | |||
153 | // When a client request is sent for a route that exist, but has a content-type mismatch, |
||
154 | // restify will emit this event. Note that restify checks for listeners on this event, and if there are none, |
||
155 | // responds with a default 415 handler. It is expected that if you listen for this event, you respond to the client. |
||
156 | // server.on('UnsupportedMediaType', (request, response, next) => {}) |
||
157 | |||
158 | // Emitted after a route has finished all the handlers you registered. |
||
159 | // You can use this to write audit logs, etc. The route parameter will be the Route object that ran. |
||
160 | // server.on('after', (request, response, route, error) => {}) |
||
161 | |||
162 | // Emitted when some handler throws an uncaughtException somewhere in the chain. |
||
163 | // The default behavior is to just call res.send(error), and let the built-ins in restify handle transforming, |
||
164 | // but you can override to whatever you want here. |
||
165 | server.on('uncaughtException', (request, response, route, error) => { |
||
166 | logger.error('[server]', error.stack) |
||
167 | response.send(error) |
||
168 | }) |
||
169 | |||
170 | // error handler |
||
171 | server.on('error', (error) => { |
||
172 | onError(error) |
||
173 | }) |
||
174 | |||
175 | const port = normalizePort(config.server.port) |
||
176 | |||
177 | /** |
||
178 | * Normalize a port into a number, string, or false. |
||
179 | */ |
||
180 | function normalizePort (val) { |
||
181 | const port = parseInt(val, 10) |
||
182 | |||
183 | if (isNaN(port)) { |
||
184 | // named pipe |
||
185 | return val |
||
186 | } |
||
187 | |||
188 | if (port >= 0) { |
||
189 | // port number |
||
190 | return port |
||
191 | } |
||
192 | |||
193 | return false |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * Event listener for HTTP server "error" event. |
||
198 | */ |
||
199 | function onError (err) { |
||
200 | if (err.syscall !== 'listen') { |
||
201 | throw err |
||
202 | } |
||
203 | |||
204 | const bind = typeof port === 'string' |
||
205 | ? 'Pipe ' + port |
||
206 | : 'Port ' + port |
||
207 | |||
208 | // handle specific listen errors with friendly messages |
||
209 | /* eslint-disable no-unreachable */ |
||
210 | switch (err.code) { |
||
0 ignored issues
–
show
Coding Style
introduced
by
![]() |
|||
211 | case 'EACCES': |
||
212 | err.message = `${bind} requires elevated privileges` |
||
213 | logger.error('[server]', err.message) |
||
214 | break |
||
215 | |||
216 | // lsof -i tcp:8088 |
||
217 | // kill -9 <PID> |
||
218 | case 'EADDRINUSE': |
||
219 | err.message = `${bind} is already in use` |
||
220 | logger.error('[server]', err.message) |
||
221 | break |
||
222 | } |
||
223 | throw err |
||
224 | } |
||
225 | |||
226 | module.exports = server |
||
227 |