我的项目背景

flexiManage是以色列一家初创公司flexiWAN开源的基于SD-WAN平台的应用层的框架,包含flexiManage服务端框架,基于此服务端框架进行了一些借鉴和改良

目录构造

  • 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

踩坑案例

BFF抹掉https的node模块验证

[bug形容] 做验证应用服务端及硬件侧未配置ssl,而node启动https模块会默认验证ssl,导致无奈启动服务

[bug剖析] node模块的ssl验证

[解决方案] 起一层bff用于透传接口,后续不便将后续服务层进行微服务化等解决

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');});

express申请接口申请体不同无奈匹配

[bug形容] express实例中同样post申请,只是body体不同而导致无奈辨别,从而笼罩后续接口

[bug剖析] express的中间件原理,在加载路由过程正则匹配后不会匹配body体

[解决方案] 辨别路由接口,通过request转发或加上路由模块辨别

// 启动单个设施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'})        }    })});

源码解析

次要是以express为外围的node利用,封装了express的基类进行实例,配合websocket进行实时数据的连贯,redis的输入生产存储

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`);    }  }}

封装了一个express的基类,次要蕴含中间件的解决、错误处理、监听server

总结

基于express封装的扩大利用,次要利用的是express的中间件原理,能够同类类比nest.js,其外围也是基于express封装的利用,但nest.js基于ng的模块思维做的隔离性更好,更像是服务端的一种node版的spring框架,而本利用的确还是像express的node利用,略显冗余