├── .gitignore ├── bin └── bridgit.js ├── src ├── config │ └── defaults.json ├── constants │ └── index.js ├── utils │ ├── index.js │ ├── startServer.js │ ├── mergeConfiguration.js │ └── logger.js ├── index.js ├── commands │ ├── hawk.js │ └── config.js └── middlewares │ └── hawk.js ├── test ├── unit │ ├── index.spec.js │ ├── utils │ │ ├── index.spec.js │ │ ├── logger.spec.js │ │ └── mergeConfiguration.spec.js │ └── commands │ │ ├── hawk.spec.js │ │ └── config.spec.js └── helpers │ └── index.js ├── .travis.yml ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug* 4 | coverage 5 | tmp.* -------------------------------------------------------------------------------- /bin/bridgit.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const bridgit = require('../src/index'); 4 | 5 | bridgit(); -------------------------------------------------------------------------------- /src/config/defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "origin": "http://127.0.0.1:8000", 3 | "prefix": "", 4 | "algorithm": "sha256", 5 | "encryptPayload": true 6 | } 7 | -------------------------------------------------------------------------------- /test/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | require('assert'); 2 | 3 | const bridgit = require('../../src'); 4 | 5 | describe('index', () => { 6 | it('exposes a function', () => { 7 | expect(typeof bridgit).toBe('function'); 8 | }); 9 | }); -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const os = require('os'); 3 | 4 | module.exports = { 5 | configFilePath: path.join(os.homedir(), '.bridgit.json'), 6 | hawkOptionKeys: ['id', 'key', 'algorithm', 'encryptPayload'], 7 | configKeys: ['id', 'key', 'origin', 'port', 'algorithm', 'prefix', 'encryptPayload'] 8 | } -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | exports.requireSrc = (srcPath) => { 4 | return require('../../src/' + srcPath); 5 | } 6 | 7 | exports.resolveSrc = (srcPath) => { 8 | return path.resolve('src/' + srcPath); 9 | } 10 | 11 | exports.sleep = (delay=200) => new Promise((resolve) => setTimeout(resolve, delay)); -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearEmpty, 3 | }; 4 | 5 | function clearEmpty(obj) { 6 | if (typeof obj !== 'object') throw new Error('argument expected to be an object!'); 7 | let o = Object.assign({}, obj); 8 | let val; 9 | for (let key in o) { 10 | val = o[key]; 11 | if (val === null || val === undefined) { 12 | delete o[key]; 13 | } 14 | } 15 | return o; 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/startServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Koa = require('koa'); 4 | const bodyParser = require('koa-bodyparser'); 5 | const logger = require('./logger'); 6 | 7 | module.exports = function start(module, config) { 8 | let app = new Koa(); 9 | 10 | app.use(bodyParser()); 11 | let middleware = require(`../middlewares/${module}`); 12 | app.use(middleware(config)); 13 | 14 | let port = config.port || 3000; 15 | app.listen(port); 16 | logger.colorful(`> ${module.toUpperCase()} proxy server start listening on http://127.0.0.1:${port}`); 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/utils/index.spec.js: -------------------------------------------------------------------------------- 1 | require('assert'); 2 | const {requireSrc} = require('../../helpers'); 3 | 4 | const {clearEmpty} = requireSrc('utils'); 5 | 6 | describe('util index, clearEmpty', () => { 7 | it('throws when argument is not object', () => { 8 | expect(() => { 9 | clearEmpty('foo'); 10 | }).toThrow(); 11 | }); 12 | 13 | it('returns a new object', () => { 14 | let src = {}; 15 | let ret = clearEmpty(src); 16 | expect(ret).not.toBe(src); 17 | }); 18 | 19 | it('remove properties whose value is null or undefined', () => { 20 | let src = { 21 | name: 'foo', 22 | age: null, 23 | sex: undefined 24 | }; 25 | let ret = clearEmpty(src); 26 | expect(ret).toEqual({name: 'foo'}); 27 | }); 28 | }); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '7' 4 | deploy: 5 | provider: npm 6 | email: jingkai.zhao@foxmail.com 7 | api_key: 8 | secure: rT0/T2eUMyxdD2KbUt3kDfUs9Q7aqtuhPD+T5oyOJUtaPkPXLUv0ykKPfKw9k6jGpPuvc/Go00rpewaovbrVNCbaq8IrWUQnyB0dGMfz3dOCVYoQCyJH9Z9re9FmQ6PPtwxtk/8SvLKdLhnsEXjBdLmP7UXx5lCGtJkSn0XAG75QHQu0Nd7FPo0eTnHhJKqVSw7tzWpLLZqPnN6ByIUmgeNsPnSb3sp2bxFS8KaggT6RI9EEp3fPUMKcIVDrumX2Aqf9mfgJ8A5IcTKx0J9vC/a9pc9uSMyNZp8JuHsgftarze7CA/LkleL8HmtUX/QSKAnMpvy1T3bzSKRj8nIcelXJLcudJyxpQj8NGg0n21exJTPYxK+iNEcEo9seOvxUVLuX51pe4Bov/vxsS98X47/dkRjGBYFhOFLEsPwgr9XxvDklgqsOPKy1x6o7A2eq5A6/NYHzGXWRC9+cWzGfmuM7XpLUfEFjrN1U+Fv2sfkDv981+r3Z6lPDjrmQigjqDf6chtuxoDlMTMRuGX2j8AF/BKBK9iq8yYW8UfY2/Mia2F0hRh6YEHF7H78GpRLlZ7jSi2LgXVPIFif4zPYwtxnxzUUMtazW0RMWvSgZz8zi7VQQkPxYV4rvZIxbpHUZ0oydDBZaPUbhpY7ye91JYi8/jnOoWoysmCHedWxJkYU= 9 | on: 10 | repo: jkzing/bridgit 11 | tags: true 12 | branch: master 13 | -------------------------------------------------------------------------------- /test/unit/utils/logger.spec.js: -------------------------------------------------------------------------------- 1 | require('assert'); 2 | jest.mock('chalk', () => { 3 | return { 4 | yellow: jest.fn(), 5 | cyan: jest.fn(), 6 | } 7 | }); 8 | 9 | const chalk = require('chalk'); 10 | const {requireSrc} = require('../../helpers'); 11 | 12 | const {colorful} = requireSrc('utils/logger'); 13 | 14 | describe('util logger', () => { 15 | beforeAll(() => { 16 | 17 | }); 18 | 19 | afterAll(() => { 20 | jest.unmock('chalk'); 21 | }); 22 | 23 | test('colorful log yellow in color with 1 argument', () => { 24 | colorful('foo'); 25 | expect(chalk.yellow.mock.calls.length).toBe(1) 26 | }); 27 | 28 | test('colorful log with right color and right timestamp', () => { 29 | colorful('foo', 'cyan', true); 30 | expect(chalk.cyan.mock.calls.length).toBe(1); 31 | expect(chalk.cyan.mock.calls[0][0]).toMatch(/\[.+\] -/); 32 | }) 33 | }); -------------------------------------------------------------------------------- /src/utils/mergeConfiguration.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const os = require('os'); 4 | const configFilePath = require('../constants').configFilePath; 5 | const {clearEmpty} = require('./index'); 6 | const logger = require('./logger'); 7 | 8 | const defaultsPath = require.resolve('../config/defaults.json'); 9 | 10 | function validateArgs(args) { 11 | return args.every(arg => typeof arg === 'object'); 12 | } 13 | 14 | /** 15 | * merge config objects 16 | * no need for deep merge for now 17 | */ 18 | module.exports = function merge(...args) { 19 | if (!validateArgs(args)) { 20 | throw new Error('Some of merge arguments are not object'); 21 | } 22 | let storedConf, defaultsConf; 23 | try { 24 | let configData = fs.readFileSync(configFilePath, {encoding: 'utf-8'}); 25 | let defaultsData = fs.readFileSync(defaultsPath, {encoding: 'utf-8'}); 26 | storedConf = JSON.parse(configData); 27 | defaultsConf = JSON.parse(defaultsData); 28 | } catch (e) {throw e} 29 | defaultsConf = defaultsConf || {}; 30 | // ignore properties that are empty (null) 31 | storedConf = clearEmpty(storedConf || {}); 32 | args = args.map(arg => clearEmpty(arg)); 33 | return _.merge(defaultsConf, storedConf, ...args); 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const log = console.log; 3 | 4 | function getTimestamp() { 5 | return new Date().toISOString().replace('T', ' '); 6 | } 7 | 8 | function colorful(msg, color='yellow', timestamp=false) { 9 | if (!chalk[color]) { 10 | throw new Error('chalk does not have this color'); 11 | } 12 | 13 | if (timestamp) { 14 | msg = `[${getTimestamp()}] - ${msg}`; 15 | } 16 | log(chalk[color](msg)); 17 | } 18 | 19 | function info(msg, timestamp=false) { 20 | /* istanbul ignore next */ 21 | colorful(msg, 'cyan', timestamp); 22 | } 23 | 24 | function warn(msg, timestamp=false) { 25 | /* istanbul ignore next */ 26 | colorful(msg, 'grey', timestamp); 27 | } 28 | 29 | function error(msg, timestamp=false) { 30 | /* istanbul ignore next */ 31 | colorful(msg, 'red', timestamp); 32 | } 33 | 34 | function success(msg, timestamp=false) { 35 | /* istanbul ignore next */ 36 | colorful(msg, 'green', timestamp); 37 | } 38 | 39 | function config(conf={}) { 40 | console.table( 41 | Object 42 | .entries(conf) 43 | .map(([k, v]) => ({ 44 | key: k, 45 | value: v, 46 | })) 47 | ); 48 | } 49 | 50 | module.exports = { 51 | info, 52 | warn, 53 | error, 54 | success, 55 | config, 56 | colorful, 57 | } 58 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('console.table'); 4 | const bridgit = require('commander'); 5 | const hawkCommand = require('./commands/hawk'); 6 | const configCommand = require('./commands/config'); 7 | const pkg = require('../package.json'); 8 | 9 | /** 10 | * possibly refactor solution: 11 | * spawn each command here as a separete program 12 | * when it's get complicated 13 | * https://github.com/tj/commander.js/issues/1 14 | */ 15 | 16 | bridgit 17 | .version('v' + pkg.version); 18 | 19 | bridgit 20 | .command('hawk') 21 | .description('start hawk authorization proxy server') 22 | .option('-i, --id [id]', 'The hawk credential id') 23 | .option('-k, --key [key]', 'The hawk credential key') 24 | .option('-o, --origin [origin]', 'The proxy origin server host') 25 | .option('-p, --port [port]', 'Which port should proxy server start on') 26 | .option('-a, --algorithm [algorithm]', 'Which algorithm should hawk use to encrypt') 27 | .option('-P, --prefix [prefix]', 'Prefix string that should be added to request header') 28 | .option('-E, --encrypt-payload', 'Should hawk encrypt request body') 29 | .option('-c, --config [config]', 'With a specified config file path') 30 | .action(hawkCommand); 31 | 32 | bridgit 33 | .command('config [key] [value]') 34 | .description('configuration operations, use --help to checkout') 35 | .action(configCommand); 36 | 37 | 38 | module.exports = function() { 39 | bridgit.parse(process.argv); 40 | } 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bridgit", 3 | "version": "1.2.2", 4 | "description": "A authorization proxy server", 5 | "main": "bin/bridgit.js", 6 | "scripts": { 7 | "start": "node bin/bridgit.js", 8 | "test:dev": "jest --coverage", 9 | "test": "jest --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 10 | }, 11 | "files": [ 12 | "src", 13 | "bin/brdgit.js" 14 | ], 15 | "bin": { 16 | "bridgit": "bin/bridgit.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/jkzing/bridgit.git" 21 | }, 22 | "author": "JingkaiZhao", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/jkzing/bridgit/issues" 26 | }, 27 | "homepage": "https://github.com/jkzing/bridgit#readme", 28 | "dependencies": { 29 | "axios": "^0.16.2", 30 | "chalk": "^1.1.3", 31 | "commander": "^2.9.0", 32 | "console.table": "^0.8.0", 33 | "coveralls": "^2.13.1", 34 | "hawk": "^3.x", 35 | "jest": "^20.0.4", 36 | "jest-cli": "^20.0.4", 37 | "koa": "^2.3.0", 38 | "koa-bodyparser": "^4.2.0", 39 | "lodash": "^4.17.2" 40 | }, 41 | "jest": { 42 | "testEnvironment": "node", 43 | "coverageReporters": [ 44 | "lcov" 45 | ], 46 | "coveragePathIgnorePatterns": [ 47 | "dist/", 48 | "node_modules/", 49 | ".node/", 50 | "test/" 51 | ] 52 | }, 53 | "engines": { 54 | "node": ">= 6.0.0", 55 | "npm": ">= 3.0.0" 56 | }, 57 | "devDependencies": {} 58 | } 59 | -------------------------------------------------------------------------------- /test/unit/utils/mergeConfiguration.spec.js: -------------------------------------------------------------------------------- 1 | require('assert'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const {requireSrc} = require('../../helpers'); 5 | 6 | const merge = requireSrc('utils/mergeConfiguration'); 7 | const {configFilePath, configKeys} = requireSrc('constants'); 8 | 9 | 10 | describe('util mergeConfiguration', () => { 11 | beforeEach(() => { 12 | try { 13 | fs.unlinkSync(configFilePath); 14 | } catch(e) {} 15 | }); 16 | 17 | it('should merge defaults and configurations with arguments', () => { 18 | fs.writeFileSync( 19 | configFilePath, 20 | JSON.stringify({origin: 'https://www.google.com'}), 21 | {encoding: 'utf-8'} 22 | ); 23 | 24 | let res = merge({id: 'foo'}, {key: 'bar'}); 25 | expect(res).toEqual({ 26 | id: 'foo', 27 | key: 'bar', 28 | origin: 'https://www.google.com', 29 | prefix: '', 30 | algorithm: 'sha256', 31 | encryptPayload: true 32 | }); 33 | }); 34 | 35 | it('should throw when argument is not object', () => { 36 | expect(() => { 37 | merge('foo'); 38 | }).toThrow(); 39 | }); 40 | 41 | it('should omit null in configuration and arguments', () => { 42 | fs.writeFileSync( 43 | configFilePath, 44 | JSON.stringify({origin: null}), 45 | {encoding: 'utf-8'} 46 | ); 47 | 48 | let res = merge({id: null}, {key: null}); 49 | expect(res).toEqual({ 50 | origin: 'http://127.0.0.1:8000', 51 | prefix: '', 52 | algorithm: 'sha256', 53 | encryptPayload: true 54 | }); 55 | }) 56 | }) -------------------------------------------------------------------------------- /src/commands/hawk.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | const start = require('../utils/startServer'); 6 | const merge = require('../utils/mergeConfiguration'); 7 | const logger = require('../utils/logger'); 8 | const {hawkOptionKeys, configKeys} = require('../constants'); 9 | 10 | const invalidJsonReg = /JSON/; 11 | 12 | module.exports = function action(options) { 13 | if (typeof options !== 'object') { 14 | throw new Error('Wrong command arguments provided.'); 15 | } 16 | 17 | let configurations = []; 18 | 19 | let configFile = options.config; 20 | if (configFile) { 21 | try { 22 | let filePath = path.resolve(configFile); 23 | let configFileData = fs.readFileSync(filePath, {encoding: 'utf-8'}); 24 | configFile = JSON.parse(configFileData); 25 | } catch (e) { 26 | if (e.code === 'ENOENT') { 27 | logger.error(`Can not file config file at ${options.config}.`); 28 | return; 29 | } else if (e.message && invalidJsonReg.test(e.message)) { 30 | logger.error(e.message); 31 | return; 32 | } else { 33 | logger.warn( 34 | 'Unknown error happened when parsing config file, ' + 35 | `config file ${options.config} will be omitted.` 36 | ); 37 | } 38 | } 39 | } 40 | 41 | if (typeof configFile === 'object') { 42 | configurations.push(configFile); 43 | } 44 | 45 | // pick up configuration relavent options 46 | configurations.push(_.pick(options, configKeys)); 47 | 48 | let config = merge.apply(null, configurations); 49 | 50 | const [req, opts] = [ 51 | _.omit(config, hawkOptionKeys), 52 | _.pick(config, hawkOptionKeys) 53 | ]; 54 | 55 | req.options = opts; 56 | 57 | start('hawk', req); 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/config.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const fs = require('fs'); 3 | const logger = require('../utils/logger'); 4 | 5 | const {configFilePath, configKeys} = require('../constants'); 6 | 7 | let commands = { 8 | get(key, value, options) { 9 | // value should be ommited 10 | let config 11 | try { 12 | let configData = fs.readFileSync(configFilePath, {encoding: 'utf-8'}); 13 | config = JSON.parse(configData); 14 | } catch(e) { 15 | logger.error('No configuration file found, or configuration file is not valid JSON.'); 16 | return; 17 | } 18 | if (key) { 19 | logger.config({ 20 | [key]: config[key], 21 | }); 22 | } else { 23 | logger.config(config); 24 | } 25 | }, 26 | set(key, value, options) { 27 | /* istanbul ignore if */ 28 | if (!key) return; 29 | let config; 30 | try { 31 | let configData = fs.readFileSync(configFilePath, {encoding: 'utf-8'}); 32 | config = JSON.parse(configData); 33 | } catch(e) { 34 | config = {} 35 | } 36 | config = _.merge( 37 | config, 38 | {[key]: value} 39 | ); 40 | fs.writeFileSync(configFilePath, JSON.stringify(config), { 41 | encoding: 'utf-8', 42 | }); 43 | }, 44 | new(filePath, omit, options) { 45 | let data = _.zipObject(configKeys, Array(configKeys.length).fill(null)); 46 | /* istanbul ignore if */ 47 | if (!/.json$/.test(filePath)) { 48 | filePath = filePath + '.json'; 49 | } 50 | fs.writeFile(filePath, JSON.stringify(data, null, 4), { 51 | encoding: 'utf-8', 52 | }, (err) => { 53 | if (err) { 54 | logger.error(err.message); 55 | return; 56 | } 57 | logger.success(`${filePath} created successfully.`); 58 | }); 59 | } 60 | } 61 | 62 | 63 | module.exports = function action(cmd, key, value, options) { 64 | if (!commands.hasOwnProperty(cmd)) { 65 | logger.error(`Command argument should be one of ${Object.keys(commands).join(', ')}.`) 66 | return; 67 | } 68 | 69 | commands[cmd](key, value, options); 70 | } 71 | -------------------------------------------------------------------------------- /test/unit/commands/hawk.spec.js: -------------------------------------------------------------------------------- 1 | require('assert'); 2 | 3 | const path = require('path'); 4 | const os = require('os'); 5 | const fs = require('fs'); 6 | const {requireSrc} = require('../../helpers'); 7 | 8 | const hawkAction = requireSrc('commands/hawk'); 9 | const {configFilePath} = requireSrc('constants'); 10 | 11 | let options, middleware; 12 | jest.mock('../../../src/utils/startServer', () => { 13 | return jest.fn((m, o) => { 14 | middleware = m; 15 | options = o; 16 | }); 17 | }); 18 | 19 | const MOCK_OPTIONS = { 20 | id: 'foo', 21 | key: 'bar', 22 | origin: 'http://foo.com', 23 | port: 1000, 24 | algorithm: 'aes128', 25 | prefix: 'session-', 26 | encryptPayload: false 27 | }; 28 | 29 | const MOCK_EXPECTION = { 30 | options: { 31 | id: 'foo', 32 | key: 'bar', 33 | algorithm: 'aes128', 34 | encryptPayload: false 35 | }, 36 | origin: 'http://foo.com', 37 | port: 1000, 38 | prefix: 'session-' 39 | }; 40 | 41 | describe('hawk command', () => { 42 | beforeEach(() => { 43 | options = null; 44 | middleware = null; 45 | try { 46 | fs.writeFileSync( 47 | configFilePath, 48 | JSON.stringify({}), 49 | {encoding: 'utf-8'} 50 | ); 51 | } catch(e) {} 52 | }); 53 | 54 | afterAll(() => { 55 | jest.unmock('../../../src/utils/startServer'); 56 | }); 57 | 58 | it('should parse options correctly', () => { 59 | hawkAction({}) 60 | // equals default options 61 | expect(options).toEqual({ 62 | options: { 63 | algorithm: 'sha256', 64 | encryptPayload: true 65 | }, 66 | origin: 'http://127.0.0.1:8000', 67 | prefix: '', 68 | }); 69 | }); 70 | 71 | it('should merge options properly', () => { 72 | hawkAction(MOCK_OPTIONS); 73 | expect(options).toEqual(MOCK_EXPECTION); 74 | }); 75 | 76 | it('should load config file properly', () => { 77 | let filePath = 'tmp.bridgit.json'; 78 | fs.writeFileSync(path.resolve(filePath), JSON.stringify(MOCK_OPTIONS), { 79 | encoding: 'utf-8', 80 | }); 81 | hawkAction({ 82 | config: filePath 83 | }); 84 | expect(options).toEqual(MOCK_EXPECTION); 85 | }); 86 | 87 | it('throws when option is not an object', () => { 88 | expect(() => { 89 | hawkAction('foo'); 90 | }).toThrow(); 91 | }); 92 | }); -------------------------------------------------------------------------------- /test/unit/commands/config.spec.js: -------------------------------------------------------------------------------- 1 | require('assert'); 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const {requireSrc, resolveSrc, sleep} = require('../../helpers'); 6 | 7 | const configAction = requireSrc('commands/config'); 8 | const logger = requireSrc('utils/logger'); 9 | const {configFilePath, configKeys} = requireSrc('constants'); 10 | 11 | 12 | describe('config command', () => { 13 | let error, warn, config, success; 14 | beforeAll(() => { 15 | error = jest.spyOn(logger, 'error'); 16 | warn = jest.spyOn(logger, 'warn'); 17 | config = jest.spyOn(logger, 'config'); 18 | success = jest.spyOn(logger, 'success'); 19 | }); 20 | 21 | beforeEach(() => { 22 | try { 23 | fs.unlinkSync(configFilePath); 24 | } catch(e) {} 25 | }); 26 | 27 | afterEach(() => { 28 | error.mockReset(); 29 | warn.mockReset(); 30 | config.mockReset(); 31 | success.mockReset(); 32 | }); 33 | 34 | it('should warn when sub-command is not valid', () => { 35 | configAction('foo'); 36 | expect(error).toHaveBeenCalled(); 37 | }); 38 | 39 | it('should print specified key/value with ', () => { 40 | fs.writeFileSync( 41 | configFilePath, 42 | JSON.stringify({id: 'foo', key: 'bar'}), 43 | { 44 | encoding: 'utf-8', 45 | } 46 | ); 47 | configAction('get', 'id', null); 48 | expect(config).toHaveBeenCalledWith({id: 'foo'}); 49 | 50 | 51 | configAction('get', null, null); 52 | expect(config).toHaveBeenCalledWith({id: 'foo', key: 'bar'}); 53 | }); 54 | 55 | it('should save key/value in config file with ', () => { 56 | let foo = 'foo' + Date.now(); 57 | configAction('set', 'id', foo); 58 | let config = fs.readFileSync(configFilePath, {encoding: 'utf-8'}); 59 | config = JSON.parse(config); 60 | expect(config.id).toBe(foo); 61 | }); 62 | 63 | it('should generate an sample config file with ', async () => { 64 | configAction('new', 'tmp.new'); 65 | // write file here is async function, so... 66 | await sleep(200); 67 | let config = fs.readFileSync(path.resolve('tmp.new.json'), {encoding: 'utf-8'}); 68 | config = JSON.parse(config); 69 | expect(Object.keys(config)).toEqual(configKeys); 70 | expect(success).toHaveBeenCalled(); 71 | }); 72 | 73 | it('should warn when using with config file can not be loaded', () => { 74 | configAction('get', 'id', null); 75 | expect(error).toHaveBeenCalled(); 76 | }); 77 | 78 | it('should create new config file with when it can not be found', () => { 79 | configAction('set', 'id', 'foo'); 80 | let config = fs.readFileSync(configFilePath, {encoding: 'utf-8'}); 81 | expect(config).toEqual(JSON.stringify({id: 'foo'})) 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/middlewares/hawk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * koa middleware for hawk auth 3 | */ 4 | 5 | const hawk = require('hawk'); 6 | const axios = require('axios'); 7 | const logger = require('../utils/logger'); 8 | 9 | function createHawkHeader(request, origin, options={}) { 10 | let contentType, payload = ''; 11 | let urlWithoutQuery = request.url.split('?')[0]; 12 | const url = origin + urlWithoutQuery; 13 | const method = request.method; 14 | 15 | if (request.hasOwnProperty('body')) { 16 | if (Object.keys(request.body).length) payload = JSON.stringify(request.body); 17 | } 18 | 19 | if (payload && request.headers.hasOwnProperty('content-type')) { 20 | contentType = request.headers['content-type']; 21 | } else { 22 | contentType = 'text/plain'; 23 | } 24 | 25 | let authOptions = { 26 | credentials: { 27 | id: `${options.id}`, 28 | key: options.key, 29 | algorithm: options.algorithm 30 | }, 31 | contentType, 32 | }; 33 | 34 | if (options.encryptPayload) { 35 | authOptions.payload = payload; 36 | } 37 | 38 | let artifact = hawk.client.header(url, method, authOptions); 39 | 40 | return artifact.err ? undefined : artifact.field; 41 | } 42 | 43 | module.exports = function hawkMiddleWare(config) { 44 | const endpoint = axios.create({ 45 | baseURL: config.origin, 46 | timeout: 3600 * 1000 47 | }); 48 | return async function (ctx, next) { 49 | let req = ctx.request; 50 | let requestUrl = req.url; 51 | 52 | let authorization = createHawkHeader(req, config.origin, config.options); 53 | 54 | let options = { 55 | method: req.method, 56 | url: config.origin + requestUrl, 57 | headers: { 58 | 'Authorization': `${config.prefix || ''}${authorization}` 59 | } 60 | } 61 | 62 | // set content type if it has one 63 | const contentType = req.headers['content-type']; 64 | if (contentType) { 65 | options.headers['content-type'] = contentType; 66 | } else { 67 | options.headers['content-type'] = 'text/plain'; 68 | } 69 | 70 | if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') { 71 | options.data = req.body; 72 | } 73 | 74 | logger.info(`Sending ${req.method} request to ${options.url}.`, true); 75 | let response 76 | try { 77 | response = await endpoint(options); 78 | } catch (e) { 79 | response = e.response 80 | } 81 | 82 | if (Math.floor(response.status / 100) === 2) { 83 | logger.success('Request success, sending back response.', true); 84 | } else { 85 | logger.error(`Request failed with status ${response.status} ${response.statusText}.`, true); 86 | } 87 | 88 | let responseBody; 89 | try { 90 | responseBody = JSON.parse(response.data); 91 | } catch (e) { 92 | responseBody = response.data; 93 | } 94 | 95 | ctx.status = response.status; 96 | ctx.body = responseBody; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bridgit 2 | 3 | [![npm version](https://badge.fury.io/js/bridgit.svg)](https://badge.fury.io/js/bridgit) 4 | [![Build Status](https://travis-ci.org/jkzing/bridgit.svg?branch=master)](https://travis-ci.org/jkzing/bridgit) 5 | [![Coverage Status](https://coveralls.io/repos/github/jkzing/bridgit/badge.svg?branch=master)](https://coveralls.io/github/jkzing/bridgit?branch=master) 6 | 7 | bridgit is a proxy server intend to forward http request to a server with authentication. 8 | 9 | Support different authentication protocol. (hawk for now) 10 | 11 | # Installation 12 | 13 | ``` 14 | npm install -g bridgit 15 | ``` 16 | 17 | # Commands 18 | 19 | ## hawk 20 | 21 | Simply use follow command to start the proxy server for hawk authentication. 22 | 23 | ``` bash 24 | bridgit hawk 25 | ``` 26 | 27 | Initially, the proxy server would intercept request from http://127.0.0.1:3000, 28 | encrypt the request with hawk, 29 | add the authentication artifact in request header as `Authorization`, 30 | and foward it to the same uri at http://127.0.0.1:8000. 31 | 32 | So you can call your RESTful API at http://127.0.0.1:3000/your_api_uri now. 33 | 34 | There are several options you can use to customize the proxy server: 35 | 36 | ``` bash 37 | bridgit hawk 38 | [-o, --origin=] # origin to forward 39 | [-p, --port=] # server port for bridgit to listen on 40 | [-P, --prefix=] # auth header prefix 41 | [-i, --id=] # hawk credentials id 42 | [-k, --key=] # hawk crendentials key 43 | [-a, --algorithm=] # hawk algorithm 44 | [-E, --encrypt-payload=] # should include payload when encrypt 45 | [-c, --config=] # With a specified config file path 46 | ``` 47 | You can also use `bridgit hawk --help` to view available options. 48 | 49 | Here are some usage examples: 50 | 51 | ### Use with options 52 | ``` bash 53 | bridgit hawk -i your_id -k your_key -o http://www.google.com 54 | ``` 55 | Will start hawk server with `your_id` and `your_key`, then proxy request to `http://www.google.com`. 56 | 57 | ### Use with config file 58 | ``` bash 59 | bridgit hawk -c ~/config.json 60 | ``` 61 | Will load ~/config.json as your configuration, and keep global config as defaults. 62 | 63 | > NOTE: the config file you are using is considered in JSON format, please ensure that. 64 | 65 | 66 | ## config 67 | 68 | > From 1.1.0, default configuration file will be generated under your $HOME directory, named .bridgit.json. 69 | 70 | ### set/get global configurations 71 | 72 | `bridgit config set ` 73 | or 74 | `bridgit config get ` 75 | 76 | Here `key` can be any support option in proxy server command (like hawk). 77 | 78 | ``` bash 79 | bridgit config set id your_id # store your_id as id in config file 80 | bridgit config set port 4000 # store 4000 as port in config file 81 | bridgit config get port # print current port config 82 | bridgit config get # print all key-values in config file 83 | ``` 84 | 85 | > NOTE: You should only use fullname for options to set config, shortland name will not take effect. 86 | 87 | 88 | PS: The options' priority is higher than config file. For example: 89 | 90 | ``` bash 91 | bridgit config set id id_config 92 | bridgit hawk --id=id_option 93 | ``` 94 | Will result in proxy server using `id_options` as hawk id. 95 | 96 | ### generate an empty config file 97 | ``` bash 98 | bridgit config new ~/your_config.json 99 | ``` 100 | Will create a new empty config file @~/your_config.json. 101 | 102 | # Todo 103 | 104 | * Test cases coverage 105 | * Support OAuth2 106 | --------------------------------------------------------------------------------