cncjs

Форк
0
/
app.js 
355 строк · 12.8 Кб
1
/* eslint callback-return: 0 */
2
import fs from 'fs';
3
import path from 'path';
4
import bodyParser from 'body-parser';
5
import compress from 'compression';
6
import cookieParser from 'cookie-parser';
7
import multiparty from 'connect-multiparty';
8
import connectRestreamer from 'connect-restreamer';
9
import engines from 'consolidate';
10
import errorhandler from 'errorhandler';
11
import express from 'express';
12
import expressJwt from 'express-jwt';
13
import session from 'express-session';
14
import 'hogan.js'; // required by consolidate
15
import i18next from 'i18next';
16
import i18nextBackend from 'i18next-node-fs-backend';
17
import jwt from 'jsonwebtoken';
18
import methodOverride from 'method-override';
19
import morgan from 'morgan';
20
import favicon from 'serve-favicon';
21
import serveStatic from 'serve-static';
22
import sessionFileStore from 'session-file-store';
23
import _get from 'lodash/get';
24
import _noop from 'lodash/noop';
25
import rimraf from 'rimraf';
26
import {
27
  LanguageDetector as i18nextLanguageDetector,
28
  handle as i18nextHandle
29
} from 'i18next-express-middleware';
30
import urljoin from './lib/urljoin';
31
import logger from './lib/logger';
32
import settings from './config/settings';
33
import * as api from './api';
34
import errclient from './lib/middleware/errclient';
35
import errlog from './lib/middleware/errlog';
36
import errnotfound from './lib/middleware/errnotfound';
37
import errserver from './lib/middleware/errserver';
38
import config from './services/configstore';
39
import {
40
  authorizeIPAddress,
41
  validateUser
42
} from './access-control';
43
import {
44
  ERR_FORBIDDEN
45
} from './constants';
46

47
const log = logger('app');
48

49
const renderPage = (view = 'index', cb = _noop) => (req, res, next) => {
50
  // Override IE's Compatibility View Settings
51
  // http://stackoverflow.com/questions/6156639/x-ua-compatible-is-set-to-ie-edge-but-it-still-doesnt-stop-compatibility-mode
52
  res.set({ 'X-UA-Compatible': 'IE=edge' });
53

54
  const locals = { ...cb(req, res) };
55
  res.render(view, locals);
56
};
57

58
const appMain = () => {
59
  const app = express();
60

61
  { // Settings
62
    if (process.env.NODE_ENV === 'development') {
63
      // Error handler - https://github.com/expressjs/errorhandler
64
      // Development error handler, providing stack traces and error message responses
65
      // for requests accepting text, html, or json.
66
      app.use(errorhandler());
67

68
      // a custom "verbose errors" setting which can be used in the templates via settings['verbose errors']
69
      app.enable('verbose errors'); // Enables verbose errors in development
70
      app.disable('view cache'); // Disables view template compilation caching in development
71
    } else {
72
      // a custom "verbose errors" setting which can be used in the templates via settings['verbose errors']
73
      app.disable('verbose errors'); // Disables verbose errors in production
74
      app.enable('view cache'); // Enables view template compilation caching in production
75
    }
76

77
    app.enable('trust proxy'); // Enables reverse proxy support, disabled by default
78
    app.enable('case sensitive routing'); // Enable case sensitivity, disabled by default, treating "/Foo" and "/foo" as the same
79
    app.disable('strict routing'); // Enable strict routing, by default "/foo" and "/foo/" are treated the same by the router
80
    app.disable('x-powered-by'); // Enables the X-Powered-By: Express HTTP header, enabled by default
81

82
    for (let i = 0; i < settings.view.engines.length; ++i) {
83
      const extension = settings.view.engines[i].extension;
84
      const template = settings.view.engines[i].template;
85
      app.engine(extension, engines[template]);
86
    }
87
    app.set('view engine', settings.view.defaultExtension); // The default engine extension to use when omitted
88
    app.set('views', [
89
      path.resolve(__dirname, '../app'),
90
      path.resolve(__dirname, 'views')
91
    ]); // The view directory path
92

93
    log.debug('app.settings: %j', app.settings);
94
  }
95

96
  // Setup i18n (i18next)
97
  i18next
98
    .use(i18nextBackend)
99
    .use(i18nextLanguageDetector)
100
    .init(settings.i18next);
101

102
  app.use(async (req, res, next) => {
103
    try {
104
      // IP Address Access Control
105
      const ipaddr = req.ip || req.connection.remoteAddress;
106
      await authorizeIPAddress(ipaddr);
107
    } catch (err) {
108
      log.warn(err);
109
      res.status(ERR_FORBIDDEN).end('Forbidden Access');
110
      return;
111
    }
112

113
    next();
114
  });
115

116
  // Removes the 'X-Powered-By' header in earlier versions of Express
117
  app.use((req, res, next) => {
118
    res.removeHeader('X-Powered-By');
119
    next();
120
  });
121

122
  // Middleware
123
  // https://github.com/senchalabs/connect
124

125
  try {
126
    // https://github.com/valery-barysok/session-file-store
127
    const path = settings.middleware.session.path; // Defaults to './cncjs-sessions'
128

129
    rimraf.sync(path);
130
    fs.mkdirSync(path);
131

132
    const FileStore = sessionFileStore(session);
133
    app.use(session({
134
      // https://github.com/expressjs/session#secret
135
      secret: settings.secret,
136

137
      // https://github.com/expressjs/session#resave
138
      resave: true,
139

140
      // https://github.com/expressjs/session#saveuninitialized
141
      saveUninitialized: true,
142

143
      store: new FileStore({
144
        path: path,
145
        logFn: (...args) => {
146
          log.debug.apply(log, args);
147
        }
148
      })
149
    }));
150
  } catch (err) {
151
    log.error(err);
152
  }
153

154
  app.use(favicon(path.join(_get(settings, 'assets.app.path', ''), 'favicon.ico')));
155
  app.use(cookieParser());
156

157
  // Connect's body parsing middleware. This only handles urlencoded and json bodies.
158
  // https://github.com/expressjs/body-parser
159
  app.use(bodyParser.json(settings.middleware['body-parser'].json));
160
  app.use(bodyParser.urlencoded(settings.middleware['body-parser'].urlencoded));
161

162
  // For multipart bodies, please use the following modules:
163
  // - [busboy](https://github.com/mscdex/busboy) and [connect-busboy](https://github.com/mscdex/connect-busboy)
164
  // - [multiparty](https://github.com/andrewrk/node-multiparty) and [connect-multiparty](https://github.com/andrewrk/connect-multiparty)
165
  app.use(multiparty(settings.middleware.multiparty));
166

167
  // https://github.com/dominictarr/connect-restreamer
168
  // connect's bodyParser has a problem when using it with a proxy.
169
  // It gobbles up all the body events, so that the proxy doesn't see anything!
170
  app.use(connectRestreamer());
171

172
  // https://github.com/expressjs/method-override
173
  app.use(methodOverride());
174
  if (settings.verbosity > 0) {
175
    // https://github.com/expressjs/morgan#use-custom-token-formats
176
    // Add an ID to all requests and displays it using the :id token
177
    morgan.token('id', (req, res) => {
178
      return req.session.id;
179
    });
180
    app.use(morgan(settings.middleware.morgan.format));
181
  }
182
  app.use(compress(settings.middleware.compression));
183

184
  Object.keys(settings.assets).forEach((name) => {
185
    const asset = settings.assets[name];
186

187
    log.debug('assets: name=%s, asset=%s', name, JSON.stringify(asset));
188
    if (!(asset.path)) {
189
      log.error('asset path is not defined');
190
      return;
191
    }
192

193
    asset.routes.forEach((assetRoute) => {
194
      const route = urljoin(settings.route || '/', assetRoute || '');
195
      log.debug('> route=%s', name, route);
196
      app.use(route, serveStatic(asset.path, {
197
        maxAge: asset.maxAge
198
      }));
199
    });
200
  });
201

202
  app.use(i18nextHandle(i18next, {}));
203

204
  { // Secure API Access
205
    app.use(urljoin(settings.route, 'api'), expressJwt({
206
      secret: config.get('secret'),
207
      credentialsRequired: true
208
    }));
209

210
    app.use(async (err, req, res, next) => {
211
      let bypass = !(err && (err.name === 'UnauthorizedError'));
212

213
      // Check whether the app is running in development mode
214
      bypass = bypass || (process.env.NODE_ENV === 'development');
215

216
      // Check whether the request path is not restricted
217
      const whitelist = [
218
        // Also see "src/app/api/index.js"
219
        urljoin(settings.route, 'api/signin')
220
      ];
221
      bypass = bypass || whitelist.some(path => {
222
        return req.path.indexOf(path) === 0;
223
      });
224

225
      if (!bypass) {
226
        // Check whether the provided credential is correct
227
        const token = _get(req, 'query.token') || _get(req, 'body.token');
228
        try {
229
          // User Validation
230
          const user = jwt.verify(token, settings.secret) || {};
231
          await validateUser(user);
232
          bypass = true;
233
        } catch (err) {
234
          log.warn(err);
235
        }
236
      }
237

238
      if (!bypass) {
239
        const ipaddr = req.ip || req.connection.remoteAddress;
240
        log.warn(`Forbidden: ipaddr=${ipaddr}, code="${err.code}", message="${err.message}"`);
241
        res.status(ERR_FORBIDDEN).end('Forbidden Access');
242
        return;
243
      }
244

245
      next();
246
    });
247
  }
248

249
  { // Register API routes with public access
250
    // Also see "src/app/app.js"
251
    app.post(urljoin(settings.route, 'api/signin'), api.users.signin);
252
  }
253

254
  { // Register API routes with authorized access
255
    // Version
256
    app.get(urljoin(settings.route, 'api/version/latest'), api.version.getLatestVersion);
257

258
    // State
259
    app.get(urljoin(settings.route, 'api/state'), api.state.get);
260
    app.post(urljoin(settings.route, 'api/state'), api.state.set);
261
    app.delete(urljoin(settings.route, 'api/state'), api.state.unset);
262

263
    // G-code
264
    app.get(urljoin(settings.route, 'api/gcode'), api.gcode.fetch);
265
    app.post(urljoin(settings.route, 'api/gcode'), api.gcode.upload);
266
    app.get(urljoin(settings.route, 'api/gcode/download'), api.gcode.download);
267
    app.post(urljoin(settings.route, 'api/gcode/download'), api.gcode.download); // Alias
268

269
    // Controllers
270
    app.get(urljoin(settings.route, 'api/controllers'), api.controllers.get);
271

272
    // Commands
273
    app.get(urljoin(settings.route, 'api/commands'), api.commands.fetch);
274
    app.post(urljoin(settings.route, 'api/commands'), api.commands.create);
275
    app.get(urljoin(settings.route, 'api/commands/:id'), api.commands.read);
276
    app.put(urljoin(settings.route, 'api/commands/:id'), api.commands.update);
277
    app.delete(urljoin(settings.route, 'api/commands/:id'), api.commands.__delete);
278
    app.post(urljoin(settings.route, 'api/commands/run/:id'), api.commands.run);
279

280
    // Events
281
    app.get(urljoin(settings.route, 'api/events'), api.events.fetch);
282
    app.post(urljoin(settings.route, 'api/events/'), api.events.create);
283
    app.get(urljoin(settings.route, 'api/events/:id'), api.events.read);
284
    app.put(urljoin(settings.route, 'api/events/:id'), api.events.update);
285
    app.delete(urljoin(settings.route, 'api/events/:id'), api.events.__delete);
286

287
    // Machines
288
    app.get(urljoin(settings.route, 'api/machines'), api.machines.fetch);
289
    app.post(urljoin(settings.route, 'api/machines'), api.machines.create);
290
    app.get(urljoin(settings.route, 'api/machines/:id'), api.machines.read);
291
    app.put(urljoin(settings.route, 'api/machines/:id'), api.machines.update);
292
    app.delete(urljoin(settings.route, 'api/machines/:id'), api.machines.__delete);
293

294
    // Macros
295
    app.get(urljoin(settings.route, 'api/macros'), api.macros.fetch);
296
    app.post(urljoin(settings.route, 'api/macros'), api.macros.create);
297
    app.get(urljoin(settings.route, 'api/macros/:id'), api.macros.read);
298
    app.put(urljoin(settings.route, 'api/macros/:id'), api.macros.update);
299
    app.delete(urljoin(settings.route, 'api/macros/:id'), api.macros.__delete);
300

301
    // MDI
302
    app.get(urljoin(settings.route, 'api/mdi'), api.mdi.fetch);
303
    app.post(urljoin(settings.route, 'api/mdi'), api.mdi.create);
304
    app.put(urljoin(settings.route, 'api/mdi'), api.mdi.bulkUpdate);
305
    app.get(urljoin(settings.route, 'api/mdi/:id'), api.mdi.read);
306
    app.put(urljoin(settings.route, 'api/mdi/:id'), api.mdi.update);
307
    app.delete(urljoin(settings.route, 'api/mdi/:id'), api.mdi.__delete);
308

309
    // Users
310
    app.get(urljoin(settings.route, 'api/users'), api.users.fetch);
311
    app.post(urljoin(settings.route, 'api/users/'), api.users.create);
312
    app.get(urljoin(settings.route, 'api/users/:id'), api.users.read);
313
    app.put(urljoin(settings.route, 'api/users/:id'), api.users.update);
314
    app.delete(urljoin(settings.route, 'api/users/:id'), api.users.__delete);
315

316
    // Watch
317
    app.get(urljoin(settings.route, 'api/watch/files'), api.watch.getFiles);
318
    app.post(urljoin(settings.route, 'api/watch/files'), api.watch.getFiles);
319
    app.get(urljoin(settings.route, 'api/watch/file'), api.watch.readFile);
320
    app.post(urljoin(settings.route, 'api/watch/file'), api.watch.readFile);
321
  }
322

323
  // page
324
  app.get(urljoin(settings.route, '/'), renderPage('index.hbs', (req, res) => {
325
    const webroot = _get(settings, 'assets.app.routes[0]', ''); // with trailing slash
326
    const lng = req.language;
327
    const t = req.t;
328

329
    return {
330
      webroot: webroot,
331
      lang: lng,
332
      title: `${t('title')} ${settings.version}`,
333
      loading: t('loading')
334
    };
335
  }));
336

337
  { // Error handling
338
    app.use(errlog());
339
    app.use(errclient({
340
      error: 'XHR error'
341
    }));
342
    app.use(errnotfound({
343
      view: path.join('common', '404.hogan'),
344
      error: 'Not found'
345
    }));
346
    app.use(errserver({
347
      view: path.join('common', '500.hogan'),
348
      error: 'Internal server error'
349
    }));
350
  }
351

352
  return app;
353
};
354

355
export default appMain;
356

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.