jenkins-bot has submitted this change and it was merged.

Change subject: Update to service-template-node v0.2.2
......................................................................


Update to service-template-node v0.2.2

- Logging: log only whitelisted headers for privacy reasons
- Routes: fully-asynchronous route loading
- Bump dependencies' versions

Bug: T111709
Change-Id: I72c09303e09b40ab56a442ad8e80f6f7453d2c99
---
M .gitignore
M app.js
M config.dev.yaml
M lib/util.js
M package.json
M test/utils/headers.js
M test/utils/server.js
7 files changed, 135 insertions(+), 71 deletions(-)

Approvals:
  BearND: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/.gitignore b/.gitignore
index e4e5956..5792c40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
 Dockerfile
+.idea/
 coverage
 config.yaml
 node_modules
 npm-debug.log
 .DS_Store
-/.idea/
diff --git a/app.js b/app.js
index 434e77b..707a90b 100644
--- a/app.js
+++ b/app.js
@@ -52,15 +52,28 @@
         }
     }
 
+    // set up header whitelisting for logging
+    if(!app.conf.log_header_whitelist) {
+        app.conf.log_header_whitelist = [
+                'cache-control', 'content-type', 'content-length', 'if-match',
+                'user-agent', 'x-request-id'
+        ];
+    }
+    app.conf.log_header_whitelist = new RegExp('^(?:' + 
app.conf.log_header_whitelist.map(function(item) {
+        return item.trim();
+    }).join('|') + ')$', 'i');
+
     // set up the spec
     if(!app.conf.spec) {
         app.conf.spec = __dirname + '/spec.yaml';
     }
-    try {
-        app.conf.spec = yaml.safeLoad(fs.readFileSync(app.conf.spec));
-    } catch(e) {
-        app.logger.log('warn/spec', 'Could not load the spec: ' + e);
-        app.conf.spec = {};
+    if(app.conf.spec.constructor !== Object) {
+        try {
+            app.conf.spec = yaml.safeLoad(fs.readFileSync(app.conf.spec));
+        } catch(e) {
+            app.logger.log('warn/spec', 'Could not load the spec: ' + e);
+            app.conf.spec = {};
+        }
     }
     if(!app.conf.spec.swagger) {
         app.conf.spec.swagger = '2.0';
@@ -80,15 +93,19 @@
     // set the CORS and CSP headers
     app.all('*', function(req, res, next) {
         if(app.conf.cors !== false) {
-            res.header('Access-Control-Allow-Origin', app.conf.cors);
-            res.header('Access-Control-Allow-Headers', 'Accept, 
X-Requested-With, Content-Type');
+            res.header('access-control-allow-origin', app.conf.cors);
+            res.header('access-control-allow-headers', 'accept, 
x-requested-with, content-type');
+            res.header('access-control-expose-headers', 'etag');
         }
-        res.header('X-XSS-Protection', '1; mode=block');
-        res.header('X-Content-Type-Options', 'nosniff');
-        res.header('X-Frame-Options', 'SAMEORIGIN');
-        res.header('Content-Security-Policy', app.conf.csp);
-        res.header('X-Content-Security-Policy', app.conf.csp);
-        res.header('X-WebKit-CSP', app.conf.csp);
+        res.header('x-xss-protection', '1; mode=block');
+        res.header('x-content-type-options', 'nosniff');
+        res.header('x-frame-options', 'SAMEORIGIN');
+        res.header('content-security-policy', app.conf.csp);
+        res.header('x-content-security-policy', app.conf.csp);
+        res.header('x-webkit-csp', app.conf.csp);
+
+        sUtil.initAndLogRequest(req, app);
+
         next();
     });
 
@@ -118,29 +135,34 @@
 function loadRoutes (app) {
 
     // get the list of files in routes/
-    return fs.readdirAsync(__dirname + '/routes')
-    .map(function (fname) {
-        // ... and then load each route
-        // but only if it's a js file
-        if(!/\.js$/.test(fname)) {
-            return;
-        }
-        // import the route file
-        var route = require(__dirname + '/routes/' + fname);
-        route = route(app);
-        // check that the route exports the object we need
-        if(route.constructor !== Object || !route.path || !route.router || 
!(route.api_version || route.skip_domain)) {
-            throw new TypeError('routes/' + fname + ' does not export the 
correct object!');
-        }
-        // wrap the route handlers with Promise.try() blocks
-        sUtil.wrapRouteHandlers(route.router, app);
-        // determine the path prefix
-        var prefix = '';
-        if(!route.skip_domain) {
-            prefix = '/:domain/v' + route.api_version;
-        }
-        // all good, use that route
-        app.use(prefix + route.path, route.router);
+    return fs.readdirAsync(__dirname + '/routes').map(function(fname) {
+        return BBPromise.try(function () {
+            // ... and then load each route
+            // but only if it's a js file
+            if(!/\.js$/.test(fname)) {
+                return undefined;
+            }
+            // import the route file
+            var route = require(__dirname + '/routes/' + fname);
+            return route(app);
+        }).then(function (route) {
+            if(route === undefined) {
+                return undefined;
+            }
+            // check that the route exports the object we need
+            if (route.constructor !== Object || !route.path || !route.router 
|| !(route.api_version || route.skip_domain)) {
+                throw new TypeError('routes/' + fname + ' does not export the 
correct object!');
+            }
+            // wrap the route handlers with Promise.try() blocks
+            sUtil.wrapRouteHandlers(route.router);
+            // determine the path prefix
+            var prefix = '';
+            if(!route.skip_domain) {
+                prefix = '/:domain/v' + route.api_version;
+            }
+            // all good, use that route
+            app.use(prefix + route.path, route.router);
+        });
     }).then(function () {
         // catch errors
         sUtil.setErrorHandler(app);
@@ -149,6 +171,7 @@
     });
 
 }
+
 
 /**
  * Creates and start the service's web server
@@ -160,8 +183,9 @@
     // return a promise which creates an HTTP server,
     // attaches the app to it, and starts accepting
     // incoming client requests
+    var server;
     return new BBPromise(function (resolve) {
-        http.createServer(app).listen(
+        server = http.createServer(app).listen(
             app.conf.port,
             app.conf.interface,
             resolve
@@ -169,10 +193,12 @@
     }).then(function () {
         app.logger.log('info',
             'Worker ' + process.pid + ' listening on ' + app.conf.interface + 
':' + app.conf.port);
+        return server;
     });
 
 }
 
+
 /**
  * The service's entry point. It takes over the configuration
  * options and the logger- and metrics-reporting objects from
diff --git a/config.dev.yaml b/config.dev.yaml
index ef07a32..0d21504 100644
--- a/config.dev.yaml
+++ b/config.dev.yaml
@@ -47,6 +47,16 @@
       # no_proxy_list:
       #   - domain1.com
       #   - domain2.org
+      # the list of incoming request headers that can be logged; if left empty,
+      # the following headers are allowed: cache-control, content-length,
+      # content-type, if-match, user-agent, x-request-id
+      # log_header_whitelist:
+      #   - cache-control
+      #   - content-length
+      #   - content-type
+      #   - if-match
+      #   - user-agent
+      #   - x-request-id
       restbase_uri: https://restbase.wikimedia.org
       # whether to print extra debug info
       debug: true
diff --git a/lib/util.js b/lib/util.js
index b729ab8..18c0886 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -45,14 +45,15 @@
 /**
  * Generates an object suitable for logging out of a request object
  *
- * @param {Request} the request
+ * @param {Request} req          the request
+ * @param {RegExp}  whitelistRE  the RegExp used to filter headers
  * @return {Object} an object containing the key components of the request
  */
-function reqForLog(req) {
+function reqForLog(req, whitelistRE) {
 
-    return {
+    var ret = {
         url: req.originalUrl,
-        headers: req.headers,
+        headers: {},
         method: req.method,
         params: req.params,
         query: req.query,
@@ -61,13 +62,23 @@
         remotePort: req.connection.remotePort
     };
 
+    if(req.headers && whitelistRE) {
+        Object.keys(req.headers).forEach(function(hdr) {
+            if(whitelistRE.test(hdr)) {
+                ret.headers[hdr] = req.headers[hdr];
+            }
+        });
+    }
+
+    return ret;
+
 }
 
 
 /**
  * Serialises an error object in a form suitable for logging
  *
- * @param {Error} the error to serialise
+ * @param {Error} err error to serialise
  * @return {Object} the serialised version of the error
  */
 function errForLog(err) {
@@ -93,27 +104,21 @@
 }
 
 
-
 /**
  * Wraps all of the given router's handler functions with
  * promised try blocks so as to allow catching all errors,
  * regardless of whether a handler returns/uses promises
  * or not.
  *
- * @param {Router} the router object
- * @param {Application} the application object
+ * @param {Router} router object
  */
-function wrapRouteHandlers(router, app) {
+function wrapRouteHandlers(router) {
 
     router.stack.forEach(function(routerLayer) {
         routerLayer.route.stack.forEach(function(layer) {
             var origHandler = layer.handle;
             layer.handle = function(req, res, next) {
                 BBPromise.try(function() {
-                    req.headers = req.headers || {};
-                    req.headers['x-request-id'] = req.headers['x-request-id'] 
|| generateRequestId();
-                    req.logger = app.logger.child({request_id: 
req.headers['x-request-id']});
-                    req.logger.log('trace/req', {req: reqForLog(req), msg: 
'incoming request'});
                     return origHandler(req, res, next);
                 })
                 .catch(next);
@@ -209,7 +214,7 @@
 /**
  * Creates a new router with some default options.
  *
- * @param {Object} opts additional options to pass to express.Router()
+ * @param {Object=} opts additional options to pass to express.Router()
  * @return {Router} a new router object
  */
 function createRouter(opts) {
@@ -229,8 +234,23 @@
 }
 
 
+/**
+ * Adds logger to the request and logs it
+ *
+ * @param {*} req request object
+ * @param {Application} app application object
+ */
+function initAndLogRequest(req, app) {
+    req.headers = req.headers || {};
+    req.headers['x-request-id'] = req.headers['x-request-id'] || 
generateRequestId();
+    req.logger = app.logger.child({request_id: req.headers['x-request-id']});
+    req.logger.log('trace/req', {req: reqForLog(req, 
app.conf.log_header_whitelist), msg: 'incoming request'});
+}
+
+
 module.exports = {
     HTTPError: HTTPError,
+    initAndLogRequest: initAndLogRequest,
     wrapRouteHandlers: wrapRouteHandlers,
     setErrorHandler: setErrorHandler,
     router: createRouter
diff --git a/package.json b/package.json
index 7f746e3..6816883 100644
--- a/package.json
+++ b/package.json
@@ -30,30 +30,31 @@
     "Marko Obrovac <[email protected]>",
     "Gabriel Wicke <[email protected]>"
   ],
-  "license": "Apache2",
+  "license": "Apache-2.0",
   "bugs": {
     "url": "https://phabricator.wikimedia.org/tag/mobile_content_service/";
   },
   "homepage": "https://www.mediawiki.org/wiki/RESTBase_services_for_apps";,
   "dependencies": {
     "bluebird": "~2.8.2",
-    "body-parser": "^1.13.2",
-    "bunyan": "^1.4.0",
+    "body-parser": "^1.14.1",
+    "bunyan": "^1.5.1",
     "cassandra-uuid": "^0.0.2",
-    "compression": "^1.5.1",
-    "domino": "^1.0.18",
-    "express": "^4.13.1",
-    "js-yaml": "^3.3.1",
+    "compression": "^1.6.0",
+    "domino": "^1.0.19",
+    "express": "^4.13.3",
+    "js-yaml": "^3.4.3",
     "preq": "^0.4.4",
-    "service-runner": "^0.2.1",
-    "underscore": "~1.8.3"
+    "service-runner": "^0.2.11",
+    "underscore": "^1.8.3"
   },
   "devDependencies": {
-    "istanbul": "^0.3.17",
-    "mocha": "^2.2.5",
+    "extend": "^3.0.0",
+    "istanbul": "^0.3.22",
+    "mocha": "^2.3.3",
     "mocha-jshint": "^2.2.3",
-    "mocha-lcov-reporter": "^0.0.2",
-    "swagger-router": "^0.1.1"
+    "mocha-lcov-reporter": "^1.0.0",
+    "swagger-router": "^0.2.0"
   },
   "deploy": {
     "target": "debian",
diff --git a/test/utils/headers.js b/test/utils/headers.js
index 38508a6..5d3cc08 100644
--- a/test/utils/headers.js
+++ b/test/utils/headers.js
@@ -13,7 +13,7 @@
             assert.contentType(res, expContentType);
             assert.deepEqual(!!res.headers.etag, true, 'No ETag header 
present');
             assert.deepEqual(res.headers['access-control-allow-origin'], '*');
-            assert.deepEqual(res.headers['access-control-allow-headers'], 
'Accept, X-Requested-With, Content-Type');
+            assert.deepEqual(res.headers['access-control-allow-headers'], 
'accept, x-requested-with, content-type');
             assert.deepEqual(res.headers['content-security-policy'],
                 "default-src 'self'; object-src 'none'; media-src *; img-src 
*; style-src *; frame-ancestors 'self'");
             assert.deepEqual(res.headers['x-content-security-policy'],
diff --git a/test/utils/server.js b/test/utils/server.js
index 9ae0e28..cb56ea1 100644
--- a/test/utils/server.js
+++ b/test/utils/server.js
@@ -8,9 +8,10 @@
 var BBPromise = require('bluebird');
 var ServiceRunner = require('service-runner');
 var logStream = require('./logStream');
-var fs        = require('fs');
-var assert    = require('./assert');
-var yaml      = require('js-yaml');
+var fs = require('fs');
+var assert = require('./assert');
+var yaml = require('js-yaml');
+var extend = require('extend');
 
 
 // set up the configuration
@@ -19,7 +20,8 @@
 };
 // build the API endpoint URI by supposing the actual service
 // is the last one in the 'services' list in the config file
-var myService = config.conf.services[config.conf.services.length - 1];
+var myServiceIdx = config.conf.services.length - 1;
+var myService = config.conf.services[myServiceIdx];
 config.uri = 'http://localhost:' + myService.conf.port + '/';
 // no forking, run just one process when testing
 config.conf.num_workers = 0;
@@ -29,8 +31,10 @@
     level: 'trace',
     stream: logStream()
 };
+// make a deep copy of it for later reference
+var origConfig = extend(true, {}, config);
 
-var stop    = function () {};
+var stop = function () {};
 var options = null;
 var runner = new ServiceRunner();
 
@@ -43,6 +47,9 @@
         console.log('server options changed; restarting');
         stop();
         options = _options;
+        // set up the config
+        config = extend(true, {}, origConfig);
+        extend(true, config.conf.services[myServiceIdx].conf, options);
         return runner.run(config.conf)
         .then(function(servers) {
             var server = servers[0];

-- 
To view, visit https://gerrit.wikimedia.org/r/247279
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I72c09303e09b40ab56a442ad8e80f6f7453d2c99
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/services/mobileapps
Gerrit-Branch: master
Gerrit-Owner: Mobrovac <[email protected]>
Gerrit-Reviewer: BearND <[email protected]>
Gerrit-Reviewer: Bgerstle <[email protected]>
Gerrit-Reviewer: Dbrant <[email protected]>
Gerrit-Reviewer: Fjalapeno <[email protected]>
Gerrit-Reviewer: GWicke <[email protected]>
Gerrit-Reviewer: Mholloway <[email protected]>
Gerrit-Reviewer: Mhurd <[email protected]>
Gerrit-Reviewer: Mobrovac <[email protected]>
Gerrit-Reviewer: Niedzielski <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to