Project background

FlexiManage is an open source application layer framework based on the SD-WAN platform by flexiWAN, an Israeli startup, including flexiManage Server framework, based on which a number of references and improvements have been made

The directory structure

  • api
  • billing
  • bin
  • broker
  • controllers
  • deviceLogic
  • logging
  • logs
  • migrations
  • models
  • notifications
  • periodic
  • public
  • routes
  • services
  • utils
  • websocket
  • authenticate.js
  • configs.js
  • expressserver.js
  • flexibilling.js
  • mongoConns.js
  • rateLimitStore.js
  • token.js

On the pit case

BFF erases HTTPS node module authentication

[Bug Description] SSL is not configured on the server and hardware for authentication. However, the NODE starts the HTTPS module to authenticate SSL by default. As a result, services cannot be started

[Bug Analysis] SSL authentication of the Node module

[Solution] The first layer BFF is used for transparent interface, which facilitates microservitization of subsequent service layers

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const express = require('express');
const request = require('request');
const app = express();
const bodyParser = require('body-parser');
const router = express.Router();

const SUCC_REG = /^2[0-9]{2}$/

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

const headers = {
            'authorization': "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZmEzYTY5OGZjNDI2ODEwODc3MDYzZDQiLCJ1c2VybmFtZSI6Im1jYWlkYW9Ac2luYS5jb20iLCJvcmciOiI1ZmFkZTkyZDljNGQ2MDQyOWRjN2RhNmMiLCJvcmdOYW1lIjoidHQiLCJhY2NvdW50IjoiNWZhM2E2OThmYzQyNjgxMDg3NzA2M2QzIiwiYWNjb3VudE5hbWUiOiJ0ZXN0IiwicGVybXMiOnsiam9icyI6MTUsImJpbGxpbmciOjMsImFjY291bnRzIjo3LCJvcmdhbml6YXRpb25zIjoxNSwiZGV2aWNlcyI6MTUsInRva2VucyI6MTUsImFwcGlkZW50aWZpY2F0aW9ucyI6MTUsIm1lbWJlcnMiOjE1LCJ0dW5uZWxzIjoxNSwiYWNjZXNzdG9rZW5zIjoxNSwibm90aWZpY2F0aW9ucyI6MTUsInBhdGhsYWJlbHMiOjE1LCJtbHBvbGljaWVzIjoxNX0sImlhdCI6MTYwODExMjcwMiwiZXhwIjoxNjA4NzE3NTAyfQ.LYFv1pBP1540gb-NRCCe4dvbQ0T9HSoZHMkD8xkMFLc",
            'Content-Type': 'application/json'
        },
        errMsg = {
            msg:'unexpected response'
        },
        baseUrl = 'https://10.100.37.101:3443';


// 获取所有设备接口
app.get('/api/devices',(req,res)=> {
    console.log(req.url)
    request({
        url: `${baseUrl}${req.url}`,
        method: 'GET',
        headers
    }, (err, response, body) => {
        console.log(response.statusCode)
        if(SUCC_REG.test(response.statusCode)) {
            res.send({code: 200,msg:JSON.parse(response.body)})
        } else {
            res.send(errMsg)
        }
    })
});

// 获取单个设备接口
app.get('/api/devices/:id',(req,res)=> {
    console.log(req.url)
    request({
        url: `${baseUrl}${req.url}`,
        method: 'GET',
        headers
    }, (err, response, body) => {
        console.log(response.statusCode)
        if(SUCC_REG.test(response.statusCode)) {
            res.send({code: 200,msg:JSON.parse(response.body)})
        } else {
            res.send(errMsg)
        }
    })
});

// 获取路由接口
app.get('/api/devices/:id/routes',(req,res)=> {
    console.log(req.url)
    request({
        url: `https://10.100.37.101:3443/api/devices/${req.params.id}/routes`,
        method: 'GET',
        headers
    }, (err, response, body) => {
        console.log(response.statusCode)
        if(SUCC_REG.test(response.statusCode)) {
            res.send({code: 200,msg:JSON.parse(response.body)})
        } else {
            res.send(errMsg)
        }
    })
});

// 启动单个设备
app.post('/api/devices/:id/apply/start',(req,res)=> {
    console.log(req.url);
    request({
        url: `${baseUrl}/api/devices/${req.params.id}/apply`,
        method: 'POST',
        headers,
        body: JSON.stringify({
            "method": "start"
        })
    }, (err, response, body) => {
        let r = JSON.parse(body)
        if(r.status == 'completed') {
            res.send({code: 200,msg:'start success'})
        } else {
            res.send({msg: 'start error'})
        }
    })
});

// 停止单个设备
app.post('/api/devices/:id/apply/stop',(req,res)=> {
    console.log(req.url)
    request({
        url: `${baseUrl}/api/devices/${req.params.id}/apply`,
        method: 'POST',
        headers,
        body: JSON.stringify({
            "method": "stop"
        })
    }, (err, response, body) => {
        let r = JSON.parse(body)
        if(r.status == 'completed') {
            res.send({code: 200,msg:'stop success'})
        } else {
            res.send({msg: 'stop error'})
        }
    })
});

// 同步单个设备
app.post('/api/devices/:id/apply',(req,res)=> {
    console.log(req.url)
    request.post({
        url: `${baseUrl}${req.url}`,
        headers,
        body: JSON.stringify({
            "method": "sync"
        })
    }, (err, response, body) => {
        let r = JSON.parse(body)
        if(r.status == 'completed') {
            res.send({code: 200,msg:'update success'})
        } else {
            res.send({msg: 'update error'})
        }
    })
});

// 删除单个设备
app.delete('/api/devices/:id',(req,res)=> {
    console.log(req.url)
    request({
        url: `${baseUrl}${req.url}`,
        method: 'DELETE',
        headers
    }, (err, response, body) => {
        console.log(response.statusCode)
        if(SUCC_REG.test(response.statusCode)) {
            res.send({code: 200,msg:JSON.parse(response.body)})
        } else {
            res.send(errMsg)
        }
    })
});

// 更新设备详情
app.put('/api/devices/:id',(req,res)=> {
    request({
        url: `${baseUrl}${req.url}`,
        method: 'PUT',
        headers,
        body: JSON.stringify(req.body)
    }, (err, response, body) => {
        console.log('put device', response.statusCode)
        if(SUCC_REG.test(response.statusCode)) {
            res.send({code: 200,msg:JSON.parse(response.body)})
        } else {
            res.send(errMsg)
            console.log('error device', response.statusCode, response.body)
        }
    })
});

// 删除隧道接口
app.post('/api/devices/apply/delTunnel',(req,res)=> {
    console.log('req.body', req.body)
    request.post({
        url: `${baseUrl}/api/devices/apply`,
        headers,
        body: JSON.stringify(req.body)
    }, (err, response, body) => {
        let r = JSON.parse(body)
        console.log(r)
        if(r.status == 'completed') {
            res.send({code: 200,msg:'删除隧道成功'})
        } else {
            res.send({msg: r.error})
        }
    })
});

// 建立隧道接口
app.post('/api/devices/apply/createTunnel',(req,res)=> {
    console.log(req.body)
    request.post({
        url: `${baseUrl}/api/devices/apply`,
        headers,
        body: JSON.stringify(req.body)
    }, (err, response, body) => {
        let r = JSON.parse(body)
        console.log(r)
        if(r.status == 'completed') {
            res.send({code: 200,msg:r.message})
        } else {
            res.send({msg: r.error})
        }
    })
});



// 获取所有隧道接口
app.get('/api/tunnels',(req,res)=> {
    console.log(req.url)
    request({
        url: `${baseUrl}${req.url}`,
        method: 'GET',
        headers
    }, (err, response, body) => {
        console.log(response.statusCode)
        if(SUCC_REG.test(response.statusCode)) {
            res.send({code: 200,msg:JSON.parse(response.body)})
        } else {
            res.send(errMsg)
        }
    })
});

app.listen(6000, '127.0.0.1', ()=>{
    console.log('app server');
});
Copy the code

The request body of the Express request interface is different

[Bug Description] The express instance has the same POST request, but the body body is different, so that it cannot be distinguished, thus overwriting the subsequent interface

[Bug analysis] The body body is not matched after the regular match is loaded during the routing process

[Solution] Distinguish routing interfaces by forwarding requests or adding routing modules

/ app/start a single device. The post ('/API/devices / : id/apply/start ', (the req, res) = > {the console. The log (the req. Url); request({ url: `${baseUrl}/api/devices/${req.params.id}/apply`, method: 'POST', headers, body: JSON.stringify({ "method": "start" }) }, (err, response, body) => { let r = JSON.parse(body) if(r.status == 'completed') { res.send({code: 200,msg:'start success'}) } else { res.send({msg: 'start error'}) } }) }); / app/stop a single device. The post ('/API/devices / : id/apply/stop ', (the req, res) = > {the console. The log (the req. Url) request ({url: `${baseUrl}/api/devices/${req.params.id}/apply`, method: 'POST', headers, body: JSON.stringify({ "method": "stop" }) }, (err, response, body) => { let r = JSON.parse(body) if(r.status == 'completed') { res.send({code: 200,msg:'stop success'}) } else { res.send({msg: 'stop error'}) } }) });Copy the code

The source code parsing

It is mainly a Node application with Express as the core, which encapsulates the base class of Express for instance, connects real-time data with Websocket, and consumes storage of redis output

expressserver

class ExpressServer {
  constructor (port, securePort, openApiYaml) {
    this.port = port;
    this.securePort = securePort;
    this.app = express();
    this.openApiPath = openApiYaml;
    this.schema = yamljs.load(openApiYaml);
    const restServerUrl = configs.get('restServerUrl');
    const servers = this.schema.servers.filter(server => server.url.includes(restServerUrl));
    if (servers.length === 0) {
      this.schema.servers.unshift({
        description: 'Local Server',
        url: restServerUrl + '/api'
      });
    }

    this.setupMiddleware = this.setupMiddleware.bind(this);
    this.addErrorHandler = this.addErrorHandler.bind(this);
    this.onError = this.onError.bind(this);
    this.onListening = this.onListening.bind(this);
    this.launch = this.launch.bind(this);
    this.close = this.close.bind(this);

    this.setupMiddleware();
  }

  setupMiddleware () {
    // this.setupAllowedMedia();
    this.app.use((req, res, next) => {
      console.log(`${req.method}: ${req.url}`);
      return next();
    });

    // Request logging middleware - must be defined before routers.
    this.app.use(reqLogger);
    this.app.set('trust proxy', true); // Needed to get the public IP if behind a proxy

    // Don't expose system internals in response headers
    this.app.disable('x-powered-by');

    // Use morgan request logger in development mode
    if (configs.get('environment') === 'development') this.app.use(morgan('dev'));

    // Start periodic device tasks
    deviceStatus.start();
    deviceQueues.start();
    deviceSwVersion.start();
    deviceSwUpgrade.start();
    notifyUsers.start();
    appRules.start();

    // Secure traffic only
    this.app.all('*', (req, res, next) => {
      // Allow Let's encrypt certbot to access its certificate dirctory
      if (!configs.get('shouldRedirectHttps') ||
          req.secure || req.url.startsWith('/.well-known/acme-challenge')) {
        return next();
      } else {
        return res.redirect(
          307, 'https://' + req.hostname + ':' + configs.get('redirectHttpsPort') + req.url
        );
      }
    });

    // Global rate limiter to protect against DoS attacks
    // Windows size of 5 minutes
    const inMemoryStore = new RateLimitStore(5 * 60 * 1000);
    const rateLimiter = rateLimit({
      store: inMemoryStore,
      max: +configs.get('userIpReqRateLimit'), // Rate limit for requests in 5 min per IP address
      message: 'Request rate limit exceeded',
      onLimitReached: (req, res, options) => {
        logger.error(
          'Request rate limit exceeded. blocking request', {
            params: { ip: req.ip },
            req: req
          });
      }
    });
    this.app.use(rateLimiter);

    // General settings here
    this.app.use(cors.cors);
    this.app.use(bodyParser.json());
    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: false }));
    this.app.use(cookieParser());

    // Routes allowed without authentication
    this.app.use(express.static(path.join(__dirname, configs.get('clientStaticDir'))));

    // Secure traffic only
    this.app.all('*', (req, res, next) => {
      // Allow Let's encrypt certbot to access its certificate dirctory
      if (!configs.get('shouldRedirectHttps') ||
          req.secure || req.url.startsWith('/.well-known/acme-challenge')) {
        return next();
      } else {
        return res.redirect(
          307, 'https://' + req.hostname + ':' + configs.get('redirectHttpsPort') + req.url
        );
      }
    });

    // no authentication
    this.app.use('/api/connect', require('./routes/connect'));
    this.app.use('/api/users', require('./routes/users'));

    // add API documentation
    this.app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(this.schema));

    // initialize passport and authentication
    this.app.use(passport.initialize());

    // Enable db admin only in development mode
    if (configs.get('environment') === 'development') {
      logger.warn('Warning: Enabling UI database access');
      this.app.use('/admindb', mongoExpress(mongoExpressConfig));
    }

    // Enable routes for non-authorized links
    this.app.use('/ok', express.static(path.join(__dirname, 'public', 'ok.html')));
    this.app.use('/spec', express.static(path.join(__dirname, 'api', 'openapi.yaml')));
    this.app.get('/hello', (req, res) => res.send('Hello World'));

    this.app.get('/api/version', (req, res) => res.json({ version }));

    this.app.use(cors.corsWithOptions);
    this.app.use(auth.verifyUserJWT);
    // this.app.use(auth.verifyPermission);

    try {
      // FIXME: temporary map the OLD routes
      // this.app.use('/api/devices', require('./routes/devices'));
      // this.app.use('/api/devicestats', require('./routes/deviceStats'));
      // this.app.use('/api/jobs', require('./routes/deviceQueue'));
      this.app.use('/api/portals', require('./routes/portals'));
    } catch (error) {
      logger.error('Error: Can\'t connect OLD routes');
    }

    // Intialize routes
    this.app.use('/api/admin', adminRouter);

    const validator = new OpenApiValidator({
      apiSpec: this.openApiPath,
      validateRequests: true,
      validateResponses: configs.get('validateOpenAPIResponse')
    });

    validator
      .install(this.app)
      .then(async () => {
        await this.app.use(openapiRouter());
        await this.launch();
        logger.info('Express server running');
      });
  }

  addErrorHandler () {
    // "catchall" handler, for any request that doesn't match one above, send back index.html file.
    this.app.get('*', (req, res, next) => {
      logger.info('Route not found', { req: req });
      res.sendFile(path.join(__dirname, configs.get('clientStaticDir'), 'index.html'));
    });

    // catch 404 and forward to error handler
    this.app.use(function (req, res, next) {
      next(createError(404));
    });

    // Request error logger - must be defined after all routers
    // Set log severity on the request to log errors only for 5xx status codes.
    this.app.use((err, req, res, next) => {
      req.logSeverity = err.status || 500;
      next(err);
    });
    this.app.use(errLogger);

    /**
     * suppressed eslint rule: The next variable is required here, even though it's not used.
     *
     ** */
    // eslint-disable-next-line no-unused-vars
    this.app.use((error, req, res, next) => {
      const errorResponse = error.error || error.message || error.errors || 'Unknown error';
      res.status(error.status || 500);
      res.type('json');
      res.json({ error: errorResponse });
    });
  }

  /**
   * Event listener for HTTP/HTTPS server "error" event.
   */
  onError (port) {
    return function (error) {
      if (error.syscall !== 'listen') {
        throw error;
      }

      const bind = 'Port ' + port;

      // handle specific listen errors with friendly messages
      /* eslint-disable no-unreachable */
      switch (error.code) {
        case 'EACCES':
          console.error(bind + ' requires elevated privileges');
          process.exit(1);
        case 'EADDRINUSE':
          console.error(bind + ' is already in use');
          process.exit(1);
        default:
          throw error;
      }
    };
  }

  /**
  * Event listener for HTTP server "listening" event.
  */
  onListening (server) {
    return function () {
      const addr = server.address();
      const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
      console.debug('Listening on ' + bind);
    };
  }

  async launch () {
    this.addErrorHandler();

    try {
      this.server = http.createServer(this.app);

      this.options = {
        key: fs.readFileSync(path.join(__dirname, 'bin', configs.get('httpsCertKey'))),
        cert: fs.readFileSync(path.join(__dirname, 'bin', configs.get('httpsCert')))
      };
      this.secureServer = https.createServer(this.options, this.app);

      // setup wss here
      this.wss = new WebSocket.Server({
        server: configs.get('shouldRedirectHttps') ? this.secureServer : this.server,
        verifyClient: connections.verifyDevice
      });

      connections.registerConnectCallback('broker', broker.deviceConnectionOpened);
      connections.registerCloseCallback('broker', broker.deviceConnectionClosed);
      connections.registerCloseCallback('deviceStatus', deviceStatus.deviceConnectionClosed);

      this.wss.on('connection', connections.createConnection);
      console.log('Websocket server running');

      this.server.listen(this.port, () => {
        console.log('HTTP server listening on port', { params: { port: this.port } });
      });
      this.server.on('error', this.onError(this.port));
      this.server.on('listening', this.onListening(this.server));

      this.secureServer.listen(this.securePort, () => {
        console.log('HTTPS server listening on port', { params: { port: this.securePort } });
      });
      this.secureServer.on('error', this.onError(this.securePort));
      this.secureServer.on('listening', this.onListening(this.secureServer));
    } catch (error) {
      console.log('Express server lunch error', { params: { message: error.message } });
    }
  }

  async close () {
    if (this.server !== undefined) {
      await this.server.close();
      console.log(`HTTP Server on port ${this.port} shut down`);
    }
    if (this.secureServer !== undefined) {
      await this.secureServer.close();
      console.log(`HTTPS Server on port ${this.securePort} shut down`);
    }
  }
}
Copy the code

It encapsulates an Express base class that contains middleware handling, error handling, and listener servers

conclusion

The extended application based on Express encapsulation mainly uses the middleware principle of Express, which can be similar to Nest. js. Its core is also an application based on Express encapsulation, but Nest. js is more isolated based on ng module idea, which is more like a Node version of Spring framework on the server side. However, this application is still like express node application, slightly redundant