diff --git a/config/user.example.yml b/config/user.example.yml new file mode 100644 index 0000000..a047b38 --- /dev/null +++ b/config/user.example.yml @@ -0,0 +1,4 @@ +- username: foo + password: bar +- username: user + password: pass \ No newline at end of file diff --git a/config/user.yml b/config/user.yml new file mode 100644 index 0000000..a047b38 --- /dev/null +++ b/config/user.yml @@ -0,0 +1,4 @@ +- username: foo + password: bar +- username: user + password: pass \ No newline at end of file diff --git a/lib/config.js b/lib/config.js index 504e1ef..0788a9e 100644 --- a/lib/config.js +++ b/lib/config.js @@ -278,14 +278,10 @@ var config = convict({ var configFilename = __dirname + '/../config/' + config.get('application.config') + '.yml'; -if (fs.existsSync(configFilename)) { - console.log('Using custom config', configFilename); - try { - var doc = yaml.safeLoad(fs.readFileSync(configFilename, 'utf8')); - config.load(doc); - } catch (e) { - console.error(e); - } +var appConfig = tryLoadYamlFile(configFilename); + +if (appConfig) { + config.load(appConfig); } try { @@ -296,4 +292,19 @@ try { process.exit(); } +config.user = tryLoadYamlFile(__dirname + '/../config/user.yml') + module.exports = config; + +function tryLoadYamlFile(configFilename) { + if (fs.existsSync(configFilename)) { + console.log('Using custom config', configFilename); + try { + return yaml.safeLoad(fs.readFileSync(configFilename, 'utf8')); + } catch (e) { + console.error(e); + } + } + + return false; +} diff --git a/lib/ffrk-proxy.js b/lib/ffrk-proxy.js index 1042076..1098128 100644 --- a/lib/ffrk-proxy.js +++ b/lib/ffrk-proxy.js @@ -24,6 +24,7 @@ function FFRKProxy(certStore) { cert: certStore.defaultCaCert, key: certStore.defaultCaKey, whitelist: this.whitelist, + user: config.user || false, }); this.proxy.use(this.middleware.bind(this)); diff --git a/node_modules/thin/lib/auth.js b/node_modules/thin/lib/auth.js new file mode 100644 index 0000000..5919d49 --- /dev/null +++ b/node_modules/thin/lib/auth.js @@ -0,0 +1,99 @@ +/*! + * basic-auth + * Copyright(c) 2013 TJ Holowaychuk + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015 Douglas Christopher Wilson + * Modified version of https://github.com/jshttp/basic-auth + * MIT Licensed + */ + +'use strict' + +/** + * RegExp for basic auth credentials + * + * credentials = auth-scheme 1*SP token68 + * auth-scheme = "Basic" ; case insensitive + * token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" + * @private + */ +var credentialsRegExp = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9\-\._~\+\/]+=*) *$/; + +/** + * RegExp for basic auth user/pass + * + * user-pass = userid ":" password + * userid = * + * password = *TEXT + * @private + */ +var userPassRegExp = /^([^:]*):(.*)$/; + +/** + * Parse the Authorization header field of a request. + * + * @param {object} req + * @return {object} with .username and .password + * @public + */ + +module.exports = function auth(req, mode) { + if (!req) { + throw new TypeError('argument req is required') + } + + var authHeader = 'authorization'; + + if (mode && mode.toLowerCase() == 'proxy') { + authHeader = 'proxy-authorization'; + } + + // Get header + var header = (req.req || req).headers[authHeader] + + // Parse header + var match = credentialsRegExp.exec(header || '') + + if (!match) { + return + } + + // Decode user pass + var userPass = userPassRegExp.exec(decodeBase64(match[1])) + + if (!userPass) { + return + } + + // Return credentials object + return new Credentials(userPass[1], userPass[2]) +} + +/** + * Decode base64 string. + * @private + */ + +function decodeBase64(str) { + return new Buffer(str, 'base64').toString() +} + +/** + * Object to represent user credentials. + * @private + */ + +function Credentials(username, password) { + this.username = username + this.password = password +} + +Credentials.prototype.checkAuthFromList = function(authList) { + for (var i = 0; i < authList.length; i++) { + if (authList[i].username === this.username && authList[i].password === this.password) { + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/node_modules/thin/lib/thin.js b/node_modules/thin/lib/thin.js index 3b71e49..92e4253 100644 --- a/node_modules/thin/lib/thin.js +++ b/node_modules/thin/lib/thin.js @@ -7,6 +7,7 @@ var request = require('request'); var url = require('url'); var EventEmitter = require('events').EventEmitter; var util = require('util'); +var auth = require('./auth'); var Logger = require('./logger'); @@ -21,14 +22,13 @@ function Mitm(opts) { this.opts.followRedirect = true; } - // socket path for system mitm https server + // Socket path for system mitm https server this.socketTemplate = os.tmpdir() + '/node-thin-{%s}.' + process.pid + '.sock'; if (process.platform === 'win32') { this.socketTemplate = '4505{%s}'; } - this.socket = this.socketTemplate.replace('{%s}', '0'); this.interceptors = []; @@ -43,9 +43,10 @@ Mitm.prototype.listen = function(port, host, cb) { var _this = this; if (process.platform !== 'win32') { - // make sure there's no previously created socket - if (fs.existsSync(this.socket)) + // Make sure there's no previously created socket + if (fs.existsSync(this.socket)) { fs.unlinkSync(this.socket); + } } var options = { @@ -54,38 +55,36 @@ Mitm.prototype.listen = function(port, host, cb) { cert: this.opts.cert || fs.readFileSync(__dirname + '/../cert/dummy.crt', 'utf8'), }; - // fake https server, MITM if you want + // Fake https server, MITM if you want this.httpsServer = https.createServer(options, this._handler.bind(this)).listen(this.socket); if (this.opts.whitelist && Object.keys(this.opts.whitelist).length > 0) { - Object.keys(this.opts.whitelist).forEach(function(element, index) { - if (_this.opts.whitelist[element].hasOwnProperty('cert')) { - var socket = _this.socketTemplate.replace('{%s}', (1+index)); - var options = { - SNICallback: _this.opts.SNICallback, - cert: fs.readFileSync(_this.opts.whitelist[element].cert, 'utf8'), - key: fs.readFileSync(_this.opts.whitelist[element].key, 'utf8'), - }; - - _this.httpsServers[element] = { - server: https.createServer(options, _this._handler.bind(_this)).listen(socket), - socket: socket, - } + Object.keys(this.opts.whitelist).forEach(function(element, index) { + if (_this.opts.whitelist[element].hasOwnProperty('cert')) { + var socket = _this.socketTemplate.replace('{%s}', (1 + index)); + var options = { + SNICallback: _this.opts.SNICallback, + cert: fs.readFileSync(_this.opts.whitelist[element].cert, 'utf8'), + key: fs.readFileSync(_this.opts.whitelist[element].key, 'utf8'), + }; + + _this.httpsServers[element] = { + server: https.createServer(options, _this._handler.bind(_this)).listen(socket), + socket: socket, } - }); + } + }); } - // start HTTP server with custom request handler callback function + // Start HTTP server with custom request handler callback function this.httpServer = http.createServer(this._handler.bind(this)).listen(port, host, function(err) { if (err) { _this.log.error('Cannot start proxy', err); } - // else - // this.log.info('Listening on %s:%s [%d]', this.opts.host, this.opts.port, process.pid); cb(err); }); - // add handler for HTTPS (which issues a CONNECT to the proxy) + // Add handler for HTTPS (which issues a CONNECT to the proxy) this.httpServer.addListener('connect', this._httpsHandler.bind(this)); }; @@ -93,13 +92,26 @@ Mitm.prototype.close = function(cb) { var _this = this; _this.httpServer.close(function(err) { - if (err) + if (err) { return cb(err); + } + _this.httpsServer.close(cb); }); }; Mitm.prototype._handler = function(req, res) { + var credentials = auth(req, 'proxy'); + + if (!!this.opts.user) { + if ((!credentials || !credentials.checkAuthFromList(this.opts.user))) { + res.statusCode = 407; + res.setHeader('Proxy-Authenticate', 'Basic realm="example"'); + res.end('Access denied'); + + return; + } + } var interceptors = this.interceptors.concat([this.direct.bind(this)]); var layer = 0; @@ -107,7 +119,10 @@ Mitm.prototype._handler = function(req, res) { (function runner() { var interceptor = interceptors[layer++]; interceptor(req, res, function(err) { - if (err) return res.end('Proxy error: ' + err.toString()); + if (err) { + return res.end('Proxy error: ' + err.toString()); + } + runner(); }); })(); @@ -123,7 +138,7 @@ Mitm.prototype._httpsHandler = function(request, socketRequest, bodyhead) { log.info(' = will connect to socket "%s"', this.socket); - // set up TCP connection + // Set up TCP connection var proxySocket = new net.Socket(); if (this.opts.whitelist && !this.opts.whitelist.hasOwnProperty(host)) { @@ -152,7 +167,7 @@ Mitm.prototype._httpsHandler = function(request, socketRequest, bodyhead) { proxySocket.write(bodyhead); - // tell the caller the connection was successfully established + // Tell the caller the connection was successfully established socketRequest.write('HTTP/' + httpVersion + ' 200 Connection established\r\n\r\n'); } @@ -232,9 +247,11 @@ Mitm.prototype.forward = function(req, res, cb) { // Set original headers except proxy system's headers var exclude = ['proxy-connection']; - for (var hname in req.headers) - if (!~exclude.indexOf(hname)) + for (var hname in req.headers) { + if (!~exclude.indexOf(hname)) { params.headers[hname] = req.headers[hname]; + } + } var buffer = ''; req.on('data', function(chunk) {