diff --git a/.gitignore b/.gitignore index 2fa5ecdff..b5bdc5342 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ coverage build/Release node_modules .lock-wscript -temp \ No newline at end of file +temp +tags +auth.db diff --git a/bin.js b/bin.js index df8c74753..9270593e8 100755 --- a/bin.js +++ b/bin.js @@ -15,6 +15,8 @@ program .option('-t, --type [value]', 'http|https, http for default') .option('-p, --port [value]', 'proxy port, 8001 for default') .option('-w, --web [value]' , 'web GUI port, 8002 for default') + .option('-I, --no-webinterface', 'disable WebInterface') + .option('-P, --no-persistence', 'disable data persistence, will also disable WebInterface') .option('-f, --file [value]', 'save request data to a specified file, will use in-memory db if not specified') .option('-r, --rule [value]', 'path for rule file,') .option('-g, --root [value]', 'generate root CA') @@ -23,9 +25,16 @@ program .option('-s, --silent', 'do not print anything into terminal') .option('-c, --clear', 'clear all the tmp certificates') .option('-o, --global', 'set as global proxy for system') + .option('-A, --auth', 'enable proxy authorization') + .option('-a, --adduser [ ]...', 'add proxy user') + .option('-m, --moduser ', 'modify proxy user password') + .option('-d, --deluser [user]...', 'delete proxy user') + .option('-F, --authFile [value]', 'save proxy auth data to a specified file, if not specified use __dirname/auth.db') .option('install', '[alpha] install node modules') .parse(process.argv); +var authFile = program.authFile ? path.resolve(process.cwd(), program.authFile) : path.resolve(__dirname, 'auth.db'); + if(program.clear){ require("./lib/certMgr").clearCerts(function(){ console.log( color.green("all certs cleared") ); @@ -46,6 +55,58 @@ if(program.clear){ }); npm.registry.log.on("log", function (message) {}); }); +}else if (program.adduser || program.deluser || program.moduser){ + var db, Datastore = require('nedb'), + args = program.args, + users = []; + + try { + db = new Datastore({filename: authFile, autoload: true}); + db.ensureIndex({ fieldName: 'username', unique: true}); + db.persistence.setAutocompactionInterval(5001); + + console.log("proxy auth file : " + authFile); + } catch (e) { + console.log('create proxy auth database file failed, ' + e); + process.exit(-1); + } + + if (program.adduser) { + args.unshift(program.adduser); + + if (args.length % 2 !== 0) { + console.log('add user failed, every user must be set a password.'); + process.exit(-1); + } + + for (var i = 0; i < args.length; i += 2) { + users.push({username: args[i], password: args[i + 1]}); + } + + db.insert(users, function (err) { + err && console.log('create proxy user failed: ' + err.message); + }); + } else if (program.deluser) { + args.push(program.deluser); + + args.forEach(function (user) { + db.remove({username: user}, {}, function (err) { + err && console.log('delete user failed: ' + err.message); + }); + }); + } else { + if (args.length <= 0) { + console.log('user password must be set.'); + process.exit(-1); + } + + db.update({username: program.moduser}, {username: program.moduser, password: args[0]}, {upsert: true}, function (err) { + err && console.log('modify proxy user failed: ' + err.message); + }); + } + + // db.persistence.compactDatafile(); + db.persistence.stopAutocompaction(); }else{ var proxy = require("./proxy.js"); var ruleModule; @@ -91,9 +152,12 @@ if(program.clear){ throttle : program.throttle, webPort : program.web, rule : ruleModule, - disableWebInterface : false, + disableWebInterface : !program.persistence || !program.webinterface, + disablePersistence : !program.persistence, setAsGlobalProxy : program.global, interceptHttps : program.intercept, - silent : program.silent + silent : program.silent, + auth : program.auth, + authFile : authFile }); } diff --git a/lib/requestHandler.js b/lib/requestHandler.js index 04b0cda4c..361d66246 100644 --- a/lib/requestHandler.js +++ b/lib/requestHandler.js @@ -1,5 +1,5 @@ var http = require("http"), - https = require("https"), + https = require("https"), net = require("net"), fs = require("fs"), url = require("url"), @@ -49,26 +49,26 @@ function userRequestHandler(req,userRes){ req : req, startTime : new Date().getTime() }; - if(global.recorder){ - resourceInfoId = global.recorder.appendRecord(resourceInfo); - } + resourceInfoId = global.recorder ? global.recorder.appendRecord(resourceInfo) : -1; logUtil.printLog(color.green("\nreceived request to : " + host + path)); //get request body and route to local or remote async.series([ + function (callback) { + auth(callback, req, userRes, resourceInfo, resourceInfoId, protocol); + }, fetchReqData, routeReq ],function(){ //mark some ext info - if(req.anyproxy_map_local){ - global.recorder.updateExtInfo(resourceInfoId, {map : req.anyproxy_map_local}); - } + req.anyproxy_map_local && global.recorder && global.recorder.updateExtInfo(resourceInfoId, {map : req.anyproxy_map_local}); }); //get request body function fetchReqData(callback){ var postData = []; + req.on("data",function(chunk){ postData.push(chunk); }); @@ -297,7 +297,7 @@ function connectReqHandler(req, socket, head){ req : req, startTime : new Date().getTime() }; - resourceInfoId = global.recorder.appendRecord(resourceInfo); + resourceInfoId = global.recorder ? global.recorder.appendRecord(resourceInfo) : -1; var proxyPort, proxyHost, @@ -305,7 +305,9 @@ function connectReqHandler(req, socket, head){ httpsServerMgrInstance; async.series([ - + function (callback) { + auth(callback, req, socket, resourceInfo, resourceInfoId, 'http'); + }, //check if internal https server exists function(callback){ if(!shouldIntercept){ @@ -428,6 +430,64 @@ function setRules(newRule){ } } +// proxy authorization +function auth(cb, req, res, info, id, protocol) { + if (protocol === 'https' || !global.auth) { + return cb(); + } + + var index, header = req.headers["proxy-authorization"], + end = function (code) { + info.endTime = new Date().getTime(); + info.statusCode = code; + info.resHeader = {}; + info.resBody = ""; + info.length = 0; + + global.recorder && global.recorder.updateRecord(id, info); + + if (res.writeHead) { + res.removeHeader('Date'); + res.writeHead(code, {"Connection": "closed", "Content-Length": 0}); + } else { + res.write('HTTP/' + req.httpVersion + ' ' + code + ' ' + + (code === 401 ? 'Unauthorized' : 'Proxy Authentication Required') + + '\r\n\r\n', 'UTF-8'); + } + + res.end(); + return false; + }; + + if (!header) { + return end(407); + } + + if (!header.startsWith("Basic ")) { + return end(401); + } + + header = Buffer.from(header.split(" ", 2)[1], "base64"); + + if (header.length <= 0) { + return end(401); + } + + header = header.toString("ascii"); + index = header.indexOf(":"); + + if (index === -1) { + return end(401); + } + + global.auth.findOne({username: header.slice(0, index++), password: header.slice(index)}, function (err, doc) { + if (err || doc === null) { + return end(401); + } + cb(); + }); +} + function getRuleSummary(){ return userRule.summary(); } diff --git a/proxy.js b/proxy.js index 8f566d7f7..a3257164c 100644 --- a/proxy.js +++ b/proxy.js @@ -52,6 +52,8 @@ var requestHandler = util.freshRequire('./requestHandler'); //option.disableWebInterface //option.silent : false(default) //option.interceptHttps ,internal param for https +//option.auth : false(default) +//option.authFile : __dirname/auth.db(default) function proxyServer(option){ option = option || {}; @@ -63,13 +65,21 @@ function proxyServer(option){ proxyWebPort = option.webPort || DEFAULT_WEB_PORT, //port for web interface socketPort = option.socketPort || DEFAULT_WEBSOCKET_PORT, //port for websocket proxyConfigPort = option.webConfigPort || DEFAULT_CONFIG_PORT, //port to ui config server - disableWebInterface = !!option.disableWebInterface, + disableWebInterface = !!option.disablePersistence || !!option.disableWebInterface, + disablePersistence = !!option.disablePersistence, ifSilent = !!option.silent; if(ifSilent){ logUtil.setPrintStatus(false); } + if (option.auth) { + var Datastore = require('nedb'); + global.auth = new Datastore({filename: option.authFile, autoload: true}); + global.auth.persistence.setAutocompactionInterval(5001); + logUtil.printLog('proxy auth file loaded : ' + option.authFile); + } + // copy the rule to keep the original proxyRules independent proxyRules = Object.assign({}, proxyRules); @@ -110,16 +120,18 @@ function proxyServer(option){ //clear cache dir, prepare recorder function(callback){ util.clearCacheDir(function(){ - if(option.dbFile){ - global.recorder = new Recorder({filename: option.dbFile}); - }else{ - global.recorder = new Recorder(); + if (!option.disablePersistence) { + if(option.dbFile){ + global.recorder = new Recorder({filename: option.dbFile}); + }else{ + global.recorder = new Recorder(); + } } callback(); }); }, - //creat proxy server + //create proxy server function(callback){ if(proxyType == T_TYPE_HTTPS){ certMgr.getCertificate(proxyHost,function(err,keyContent,crtContent){ @@ -153,8 +165,10 @@ function proxyServer(option){ //start web socket service function(callback){ - self.ws = new wsServer({port : socketPort}); - callback(null); + if (!disableWebInterface) { + self.ws = new wsServer({port : socketPort}); + callback(null); + } }, //start web interface diff --git a/rule_sample/rule_remove_proxy_auth_header.js b/rule_sample/rule_remove_proxy_auth_header.js new file mode 100644 index 000000000..25f424da6 --- /dev/null +++ b/rule_sample/rule_remove_proxy_auth_header.js @@ -0,0 +1,11 @@ +//rule scheme : + +module.exports = { + replaceRequestOption : function(req,option){ + var newOption = option; + delete newOption.headers['proxy-authorization']; + delete newOption.headers['proxy-connection']; + + return newOption; + } +}; diff --git a/test/data/.gitignore b/test/data/.gitignore new file mode 100644 index 000000000..f9be8dfe0 --- /dev/null +++ b/test/data/.gitignore @@ -0,0 +1 @@ +!* diff --git a/test/data/auth.db b/test/data/auth.db new file mode 100644 index 000000000..a466bf732 --- /dev/null +++ b/test/data/auth.db @@ -0,0 +1,2 @@ +{"username":"chopin","password":"ngo","_id":"WkKABWbVTmecX1Ee"} +{"$$indexCreated":{"fieldName":"username","unique":true,"sparse":false}} diff --git a/test/proxy_auth_spec.js b/test/proxy_auth_spec.js new file mode 100644 index 000000000..f5410c98e --- /dev/null +++ b/test/proxy_auth_spec.js @@ -0,0 +1,100 @@ +/* +* test for proxy auth +* +*/ + +const ProxyServerUtil = require('./util/ProxyServerUtil.js'); +const { proxyGet, generateUrl } = require('./util/HttpUtil.js'); +const Server = require('./server/server.js'); +const { printLog } = require('./util/CommonUtil.js'); + +testAuth('http'); +testAuth('https'); + +function testAuth(protocol) { + describe('Proxy auth should be working in :' + protocol, () => { + let proxyServer ; + let serverInstance ; + + beforeAll((done) => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000000; + printLog('Start server for proxy_auth_spec'); + + global.auth = null; + serverInstance = new Server(); + + proxyServer = ProxyServerUtil.proxyServerWithProxyAuth(); + + setTimeout(function() { + done(); + }, 2000); + }); + + afterAll(() => { + serverInstance && serverInstance.close(); + proxyServer && proxyServer.close(); + global.auth = null; + printLog('Close server for proxy_auth_spec'); + + }); + + it('Should be return 407 if not set proxy auth', done => { + const url = generateUrl(protocol, '/test'); + proxyGet(url, {}) + .then(res => { + if (protocol === 'http') { + expect(res.statusCode).toEqual(407); + expect(res.body).toEqual(''); + done(); + } else { + console.log('error happened in proxy get for proxy auth without auth'); + done.fail('error happened when test proxy auth without auth'); + } + }, error => { + if (protocol === 'http') { + console.log('error happened in proxy get for proxy auth without auth: ', error); + done.fail('error happened when test proxy auth without auth'); + } else { + expect(error.message.slice(-3)).toEqual('407'); + done(); + } + }); + }); + + it('Should be return 401 if proxy auth failed', done => { + const url = generateUrl(protocol, '/test'); + proxyGet(url, {}, {'Proxy-Authorization': 'Basic Y2hvcGluOnBhc3M='}) + .then(res => { + if (protocol === 'http') { + expect(res.statusCode).toEqual(401); + expect(res.body).toEqual(''); + done(); + } else { + console.log('error happened in proxy get for proxy auth with error pass'); + done.fail('error happened when test proxy auth with error pass'); + } + }, error => { + if (protocol === 'http') { + console.log('error happened in proxy get for proxy auth with error pass: ', error); + done.fail('error happened when test proxy auth with error pass'); + } else { + expect(error.message.slice(-3)).toEqual('401'); + done(); + } + }); + }); + + it('Should be normal if proxy auth success', done => { + const url = generateUrl(protocol, '/test'); + proxyGet(url, {}, {'Proxy-Authorization': 'Basic Y2hvcGluOm5nbw=='}) + .then(res => { + expect(res.statusCode).toEqual(200); + expect(res.body).toEqual('something'); + done(); + }, error => { + console.log('error happened in proxy get for proxy auth: ', error); + done.fail('error happened when test proxy auth'); + }); + }); + }); +} diff --git a/test/util/ProxyServerUtil.js b/test/util/ProxyServerUtil.js index 0a333ec93..d118eed90 100644 --- a/test/util/ProxyServerUtil.js +++ b/test/util/ProxyServerUtil.js @@ -5,6 +5,7 @@ let proxy = require('../../proxy.js'); const util = require('../../lib/util.js'); +const path = require('path'); const DEFAULT_OPTIONS = { type: "http", @@ -58,8 +59,19 @@ function proxyServerWithoutHttpsIntercept (rule) { return new proxy.proxyServer(options); } +function proxyServerWithProxyAuth () { + proxy = util.freshRequire('../proxy.js'); + + const options = util.merge({}, DEFAULT_OPTIONS); + options.auth = true; + options.authFile = path.resolve(process.cwd(), 'test/data/auth.db'); + + return new proxy.proxyServer(options); +} + module.exports = { defaultProxyServer, proxyServerWithoutHttpsIntercept, - proxyServerWithRule -}; \ No newline at end of file + proxyServerWithRule, + proxyServerWithProxyAuth +};