/*
 * LICENCE[[
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1/CeCILL 2.O
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is kelis.fr code.
 *
 * The Initial Developer of the Original Code is
 * david.riveron@kelis.fr
 *
 * Portions created by the Initial Developer are Copyright (C) 2012
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 * samuel.monsarrat@kelis.fr
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * or the CeCILL Licence Version 2.0 (http://www.cecill.info/licences.en.html),
 * in which case the provisions of the GPL, the LGPL or the CeCILL are applicable
 * instead of those above. If you wish to allow use of your version of this file
 * only under the terms of either the GPL, the LGPL or the CeCILL, and not to allow
 * others to use your version of this file under the terms of the MPL, indicate
 * your decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL, the LGPL or the CeCILL. If you do not
 * delete the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL, the LGPL or the CeCILL.
 * ]]LICENCE
 */

/* ==== Global functions and variables =========================================================== */
var fs = require('fs'), http = require('http'), https = require('https'), httpProxy = require('http-proxy');

/*
 * Retourne true si on est root (sous Linux)
 */
function isRoot() { return isWin() ? false : process.getuid() == 0; }
/*
 * Retourne true si on est sous windows
 */
function isWin() { return process.platform == 'win32'; }


/*
 * Retourne la syntaxe de proxy.js
 */
function usage() {
	console.info('Usage: node proxy.js --config <config file> [  --port <port> ] [ --user <user> ] [ --key <key file> --cert <cert file> ] [ --accessLogPath <log file> ]');
}

/*
 * Lectures des paramètres de la ligne de commande
 */
function parseArguments () {
	var vArgs = {};
	try {
		for (var vParamsIndex=0; vParamsIndex<process.argv.length && process.argv[vParamsIndex].indexOf('--') != 0; vParamsIndex++);
		for (var i=vParamsIndex; i<process.argv.length; i+=2) {
			var vParamName = process.argv[i].substring(2);
			vArgs[vParamName] = process.argv[i+1];
		}
	} catch (e) {
		usage();
		process.exit(1);
	}
	if (!vArgs.config) {
		usage();
		process.exit(1);
	}
	return vArgs;
}

/*
 * Construction et contrôle des paramètres : les paramètres passés en ligne de commande sont prioritaires
 */
function parseParams (pArgs, pConfig) {
	var vParams = pConfig.param || {};
	
	vParams.port = pArgs.port || vParams.port || 8123;
	vParams.user = pArgs.user || vParams.user || null;
	vParams.key = pArgs.key || vParams.key || null;
	vParams.cert = pArgs.cert || vParams.cert || null;
	vParams.accessLogPath = pArgs.accessLogPath || vParams.accessLogPath || null;

	if (!isWin() && vParams.port < 1024 && !isRoot()) {
		console.error("Binding to ports less than 1024 requires root privileges.")
		usage();
		process.exit(1);
	}
	else if (isRoot() && !vParams.user) {
		console.error("Please provide a non-privileged user to bind as.")
		usage();
		process.exit(1);
	}
	return vParams;
}

/*
 * Lectures du fichier de configuration
 */
function parseConfig(pConfigFilePath) {
	try {
		var vConfigFile = fs.readFileSync(pConfigFilePath);
		var vOptions = JSON.parse(vConfigFile.toString());
		return vOptions;
	} catch (e) {
		console.error('An error occured while reading the configuration file : ' + e);
		process.exit(1);
	}
}

/*
 * Fonctions pour la journalisation des message
 * Pour le format, voir http://en.wikipedia.org/wiki/Common_Log_Format
 * 
 */ 
function logRequest (pRequest, pResponse) {
	if (!vLogStream) return;
	
	// Attention : accès à l'API privé de ServerResponse
	var vContentLengthMatch = pResponse._header && pResponse._header.match(/^content-length: (.+)$/m);
	var vContentLength = vContentLengthMatch ? vContentLengthMatch[1] : '-';
	
	var vLogLine = pRequest.connection.remoteAddress + ' ' + '- - [' + logRequest.formatDate(new Date()) + '] ' +
	'"' + pRequest.method + ' ' + pRequest.url + ' HTTP/' + pRequest.httpVersion + '" ' +
	pResponse.statusCode + ' ' + vContentLength + "\n"

	vLogStream.write(vLogLine);
}
logRequest.pad = function (pString) {
	var vResult = String(pString);
	if (vResult.length < 2) vResult = "0" + vResult;
	return vResult;
}
logRequest.monthNames = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
logRequest.formatDate = function (pDate) {
	var vTzO = pDate.getTimezoneOffset();
	var vTz = (vTzO < 0 ? '-' : '+') + logRequest.pad(Math.floor(Math.abs(vTzO) / 60)) + logRequest.pad((Math.abs(vTzO) % 60));
	return logRequest.pad(pDate.getDate()) + '/' + logRequest.monthNames[pDate.getMonth()] + '/' + pDate.getFullYear()
		+ ':' + logRequest.pad(pDate.getHours()) + ':' + logRequest.pad(pDate.getMinutes()) + ':' + logRequest.pad(pDate.getSeconds()) + ' ' + vTz; 
}

function openAccessLog() {
	if (vLogStream && vLogStream.end) vLogStream.end();
	if (vParams.accessLogPath) {
		vLogStream = fs.createWriteStream(vParams.accessLogPath, { flags: 'a' });
	}
}

function addForwardedHost(req) {
	if (req.headers['x-forwarded-port']) req.headers['x-forwarded-host'] += ',' + req.headers['host'];
	else req.headers['x-forwarded-host'] = req.headers['host'];
}

function dateFormat(pDate, pFormat, pUtc) {
	pUtc = pUtc ? 'getUTC' : 'get';
	return pFormat.replace (/%[YmdHMS]/g, function(m) {
		switch(m) {
			case '%Y': return pDate[pUtc + 'FullYear'] (); // no leading zeros required
			case '%m': m = 1 + pDate[pUtc + 'Month'] (); break;
			case '%d': m = pDate[pUtc + 'Date'] (); break;
			case '%H': m = pDate[pUtc + 'Hours'] (); break;
			case '%M': m = pDate[pUtc + 'Minutes'] (); break;
			case '%S': m = pDate[pUtc + 'Seconds'] (); break;
			default: return m.slice (1); // unknown code, remove %
		}
		// add leading zero if required
		return ('0' + m).slice (-2);
	});
}

function logInfo(pStr) {
	console.info(dateFormat(new Date(), "%Y-%m-%d %H:%M:%S") + " - " + pStr)
}
function logError(pStr) {
	console.error(dateFormat(new Date(), "%Y-%m-%d %H:%M:%S") + " - " + pStr)
}


/* ==== Main =========================================================== */
// ---- Init --------------------------------------------------------------
logInfo('Proxy server starting.');
process.on('SIGINT', function() {process.exit(0)});
process.on('SIGTERM', function() {process.exit(0)});
process.on('SIGHUP', function() {
	logInfo('Proxy server received hangup signal.');
	openAccessLog();
});
process.on('exit', function() {logInfo('Proxy server stopped.')});

var vArgs = parseArguments();
var vConfig = parseConfig(vArgs.config);
var vParams = parseParams(vArgs, vConfig);

var vLogStream = null;
openAccessLog();

// ---- Setting up proxy server ------------------------------------------
var vProxy = new httpProxy.RoutingProxy(vConfig);
var vHttpsOptions = null;
var vHttpServer = null;
if (vParams.key && vParams.cert) {
	try {
		vHttpsOptions = {
			key: fs.readFileSync(vParams.key).toString(),
			cert: fs.readFileSync(vParams.cert).toString()
		};
	} catch (e) {
		logError('An error occured while reading the key or cert file : ' + e);
		process.exit(1);
	}
	vHttpServer = https.createServer(vHttpsOptions);
} else {
	vHttpServer = http.createServer(vHttpsOptions);
}

vHttpServer.on('request', function(pRequest, pResponse) {
	pResponse.on('finish', function () {
		logRequest(pRequest, pResponse);
	});
	addForwardedHost(pRequest);
	vProxy.proxyRequest(pRequest, pResponse);
});

vHttpServer.on('upgrade', function(pRequest, pSocket, pHead) {
	addForwardedHost(pRequest);
	vProxy.proxyWebSocketRequest(pRequest, pSocket, pHead);
});

// ---- Start proxy server ---------------------------------------
try{
	vHttpServer.listen(vParams.port);
	if(!isWin() && vParams.user) {
		process.setuid(vParams.user);
	}
} catch(e) {
	logError("Error: [%s] Call: [%s]", e.message, e.syscall);
	process.exit(1);
}
