From 76250643ab7174ec6a52e7c63e835bca21cfd461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9?= Date: Mon, 29 Aug 2016 18:11:17 +0200 Subject: [PATCH] Add bearer option to the lib and cli --- README.md | 9 +++++++- package.json | 1 - src/cli.js | 3 +++ src/index.js | 60 ++++++++++++++++++++++++++++++++++++++++++++------ test/test.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 124 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 064ce46..a9dbc37 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ dafuq allows you to create an api that executes files on the os command line (vi * **commands**: Path where to look for commands to run on requests. * **shebang** (optional): If specified, this will be the command line interpreter to be used when running the file. If it is not specified we will check the file for execution permisions and run it by itself. Defaults to ''. * **debug** (optional): Show debug info. If true, `console.log` will be used as loggin function. If a function it will used as loggin function instead of the default . Defaults to `false`. +* **bearer** (optional): Add bearer token authorization method to the api. The acces token is provided as the value of this config. Defaults to '' ### Example @@ -44,6 +45,7 @@ app.use('/api.cmd/', dafuq({ commands: './commands', shebang: '/usr/bin/env node', // optional debug: true // optional + bearer: 'y67x81eg-21od-eewg-ciey-d52f6crtcrqv' })) app.listen(3000) ``` @@ -60,7 +62,12 @@ POST /hello ### CLI dafuq also allows to be used as cli: ``` -$ dafuq --commands="./commands" [--port=3000] [--shebang="/usr/bin/env node"] [--debug] +$ dafuq \ + --commands="./commands" \ + --port=8080 \ # Defaults to 3000 + --shebang="/usr/bin/env node" \ # Defaults to '' (direct terminal execution) + --bearer="y67x81eg-21od-eewg-ciey-d52f6crtcrqv" # API will require bearer access token + --debug ``` ## Considerations diff --git a/package.json b/package.json index d349254..735162e 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "babel-cli": "^6.14.0", "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-istanbul": "^2.0.0", - "babel-polyfill": "^6.13.0", "babel-preset-es2015": "^6.14.0", "babel-register": "^6.14.0", "core-js": "^2.4.1", diff --git a/src/cli.js b/src/cli.js index e7ba745..2f35e43 100755 --- a/src/cli.js +++ b/src/cli.js @@ -15,6 +15,9 @@ var argv = require('yargs') // shebang .describe('shebang', 'the interpreter to use when running the command files') .default('shebang', '') + // bearer + .describe('bearer', 'an access token that must be provided on the requests to the api') + .default('bearer', '') // port .describe('port', 'the port where to listen for api call') .alias('port', 'p') diff --git a/src/index.js b/src/index.js index dc581a7..fbf52fb 100644 --- a/src/index.js +++ b/src/index.js @@ -200,6 +200,38 @@ function execCommand(command, cb) { }) } +const HEADER_BEARER_REGEX = /^bearer (.*)$/i + +/** + * Returns a middleware that will reject the request with 401 if the + * bearer token at the request does not match the token provided. + * + * @param {String} token The token that the request must provide to continue + * @return {Function} Middleware function + * + * @see {@link https://www.npmjs.com/package/express-bearer-token} + * @see {@link https://tools.ietf.org/html/rfc6750} + */ +function accessMiddleware(token) { + return (req, res, next) => { + let bearer = null + if (req.body && req.body.access_token) + bearer = req.body.access_token + else if (req.query && req.query.access_token) + bearer = req.query.access_token + else if (req.headers && req.headers['authorization']) { + const match = HEADER_BEARER_REGEX.exec(req.headers['authorization']) + if (match) + bearer = match[1] + } + + if (bearer === token) + next() + else + res.status(401).send() + } +} + export default function dafuq(config) { // Allow constructor to be only the commands directory @@ -209,7 +241,8 @@ export default function dafuq(config) { // Assign default values const opts = Object.assign({ shebang: '', - debug: false + debug: false, + brearer: '' }, config) // Options validation @@ -222,6 +255,9 @@ export default function dafuq(config) { if (opts.shebang && (typeof opts.shebang !== 'string' || opts.shebang.length == 0)) throw new TypeError('shebang must be a non empty string') + if (opts.bearer && (typeof opts.bearer !== 'string' || opts.bearer.length == 0)) + throw new TypeError('bearer must be a non empty string') + if (opts.debug !== undefined) { if (opts.debug === true) opts.debug = IS_TEST ? (() => {}) : console.log @@ -261,7 +297,8 @@ export default function dafuq(config) { * put the result of its execution on response object in the property pointed * by moduleName. * - * @param {String} file + * @param {String} file + * @return {Function} Middleware function */ function executionMiddleware(file) { return (req, res, next) => { @@ -292,12 +329,21 @@ export default function dafuq(config) { const filePath = file.relative const url = '/' + path.dirname(file.relative) const method = path.basename(filePath, path.extname(filePath)).toLowerCase() - const middleware = executionMiddleware(file.absolute) + const middlewares = [] + + // If bearer is defined, add an access middleware + if (opts.bearer) + middlewares.push(accessMiddleware(opts.bearer)) + + // If the method is not any of the "get" methods add the multipart + // upload middleware + if (method !== 'get' && method !== 'head' && method !== 'options') + middlewares.push(upload.any()) + + middlewares.push(executionMiddleware(file.absolute)) + opts.debug(`Adding ${ method } ${ url }`) - if (method === 'get' || method === 'head' || method === 'options') - app[method](url, middleware) - else - app[method](url, upload.any(), middleware) + app[method](url, ...middlewares) }) // Fallback behaviour, send the result diff --git a/test/test.js b/test/test.js index 87d5ec7..98e603c 100644 --- a/test/test.js +++ b/test/test.js @@ -66,6 +66,21 @@ describe('Constructor', function() { }).should.throw(/shebang/); }) + it('should throw if bearer is not a valid string', function() { + build({ + path: './commands', + bearer: 1 + }).should.throw(/bearer/); + build({ + path: './commands', + bearer: {} + }).should.throw(/bearer/); + build({ + path: './commands', + bearer: [] + }).should.throw(/bearer/); + }) + it('should throw if debug is not a boolean nor a function', function() { build({ path: './commands', @@ -103,7 +118,7 @@ describe('Constructor', function() { }) describe('Invoking a file', () => { - describe('without shebang', () => { + describe('specifing path', () => { let app; before(function() { app = dafuq({ path: './commands' }); @@ -190,7 +205,50 @@ describe('Invoking a file', () => { .get('/no-exec') .expect(404) .end(done) - }) + }) + }) + + describe('specifing bearer', () => { + let app; + const token = 'klr5udmm-qc7g-2ndh-98v2-qjn5039avxqn' + + before(function() { + app = dafuq({ + path: './commands', + bearer: token + }); + }) + + it('should forbid access if no token on the request', (done) => { + request(app) + .get('/hello') + .expect(401) + .end(done) + }) + + it('should allow access if token on the query', (done) => { + request(app) + .get('/hello') + .query({ 'access_token': token }) + .expect(200) + .end(done) + }) + + it('should allow access if token on the body', (done) => { + request(app) + .post('/hello') + .send({ 'access_token': token }) + .expect(200) + .end(done) + }) + + it('should allow access if token on the header', (done) => { + request(app) + .get('/hello') + .set('Authorization', 'Bearer ' + token) + .expect(200) + .end(done) + }) }) describe('specifing shebang', () => {