/* $Header$ */

// node.js remote debug server relay
// Copyright (C) Omnis Software Ltd 2018

/* Changes
Date			Edit			Fault				Description
21-Sep-21	rmm11169							Improved remote debugger nodejs server error reporting.
24-Sep-19	rmm10254							Added run script support to headless Linux executable.
24-Jan-19	rmm9924								Improved remote debug server error reporting e.g. EADDRINUSE if multiple copies of Omnis are trying to use the same remote debug port.
28-Jun-18	rmm_rd								Studio 9.0 remote debugger
	*/
'use strict';

// Omnis port parameter - The debuggee Omnis is listening on this port ready for us to connect
const omnisPort = +process.argv[2];
if (isNaN(omnisPort) || omnisPort <= 0 || omnisPort > 65535)
{
	console.log("Invalid Omnis port number parameter:" + omnisPort);
	process.exit(-1);
}
process.noDeprecation = true;

// Required modules
const fs = require('fs');
const path = require('path');

// Configuration directory parameter
const configurationDirectory = process.argv[3];
if (!configurationDirectory || !fs.existsSync(configurationDirectory) || !fs.lstatSync(configurationDirectory).isDirectory()) // rmm9924: added isDirectory test
{
	console.log("Configuration directory parameter is not a pathname to an existing directory:" + configurationDirectory);
	process.exit(-2);
}

// Start rmm9924
// Error file - used to return error information to Omnis.  Omnis deletes an existing error file before starting node.js
var errorFilePath = configurationDirectory + "/remote_debug_server_error.txt";

// Catch any unexpected exception
process.on('uncaughtException', function (err) {
	// Write error file
	fs.writeFileSync(errorFilePath, err.toString());
	process.exit(-3);
});

// Start rmm11169: Moved additional dependencies to after setting up process uncaught exception handler
const https = require('https');
const WebSocket = require('ws');
const net = require('net');
const auth = require('basic-auth');
var pbkdf2 = require('pbkdf2')
// End rmm11169

// Change while(0) to while(1) to get a delay of 20 seconds - this gives the JS debugger time to attach so you can debug the rest of the initialisation code
var startDate = new Date();
while (0) {
	var diff = Math.abs(new Date() - startDate);
	if (diff > 20000)
		break;
}
// End rmm9924

// Load configuration
global.omnisRemoteDebugConfig = JSON.parse(fs.readFileSync(configurationDirectory + "/remote_debug_server_config.json" , 'utf8'));	// Store in global, so we can easily view the config when debugging this script file
var config = global.omnisRemoteDebugConfig;
// Start rmm10254: Allow remote debug port to be overridden by command line
if (process.argv.length >= 5)
{
	var overridePort = process.argv[4]*1;
	if (overridePort > 0)
		config.debugPort = overridePort;
}
// End rmm10254
if (config.ca)
{
	// Configured certificate authorities - usually only needed when using a self-signed certificate
	if (Array.isArray(config.ca)) {
		for (var i = 0; i < config.ca.length; ++i) {
			config.ca[i] = fs.readFileSync(configurationDirectory + "/" + config.ca[i]);
		}
	} else {
		if (config.ca.length > 0)
			config.ca = fs.readFileSync(configurationDirectory + "/" + config.ca);
	}
}

// Web socket server connection
var wsConnection;
// Connection to Omnis debuggee (the Omnis process that started node.js to run as a web socket server for the debugger)
var debuggeeSocket;
var buffer = Buffer.alloc(0);
function openSocket() {
	debuggeeSocket = net.connect(omnisPort, "127.0.0.1");
	debuggeeSocket.setKeepAlive(true);
	debuggeeSocket.on('connect', function () {

	});
	debuggeeSocket.on('error', function (err) {
		// If we get a connection refused error, the Omnis server has died
		if ("ECONNREFUSED" == err.code) {
			process.kill(process.pid);
			return;
		}
		// Discard partial received message data
		buffer = Buffer.alloc(0);
		// Kill socket
		debuggeeSocket.destroy();
		// Re-open socket after 100 millisecond retry delay
		setTimeout(openSocket, 100);
	});
	debuggeeSocket.on('close', function () {
		// Discard partial received message data
		buffer = Buffer.alloc(0);
		// Kill socket
		debuggeeSocket.destroy();
		// Re-open socket after 100 millisecond retry delay
		setTimeout(openSocket, 100);
	});
	debuggeeSocket.on('data', function (data) {
		// data is a Buffer, as we do not use a character encoding for the socket
		// Need to split the stream of data into individual messages
		// Each message is prefixed with ascii length value terminated by +
		buffer = Buffer.concat([buffer, data], buffer.length + data.length);
		while (1) {
			var i = buffer.indexOf('+');
			if (i != -1) {
				var len = +buffer.toString('utf8', 0, i);
				var msgSpace = len + i + 1;
				if (buffer.length >= msgSpace) {
					try {
						wsConnection.send(buffer.slice(i + 1, msgSpace));
					} 
					catch (e) {
						// Ignore exception - connection has probably closed
					}
					if (buffer.length > msgSpace) {
						var bufferCopy = Buffer.alloc(buffer.length - msgSpace);
						buffer.copy(bufferCopy, 0, msgSpace, buffer.length);
						buffer = bufferCopy;
					}
					else
						buffer = Buffer.alloc(0);
				}
				else
					break;
			} else
				break;
		}
	});
	debuggeeSocket.webSocketClosed = function () {
		// Called when the web socket closes - send a message to the core to make sure the core is aware that the debug session
		// needs to be closed
		// Until we need to extend the debug protocol any further, just send an empty JSON object
		debuggeeSocket.write("2+{}");
	}
}

// Web Socket server - receives messages from the debugger client (the user interface to the remote debugger) and forwards them
// to the debuggee.  Also takes messages received from the debuggee and forwards them to the debugger client.
const server = https.createServer({
	pfx: fs.readFileSync(configurationDirectory + "/" + config.serverPfx),
	ca: config.ca,
	passphrase: config.pfxPassPhrase,
	requestCert: config.requestCert,
	rejectUnauthorized: config.rejectUnauthorized
});

const wss = new WebSocket.Server({
	server: server,
	verifyClient: function (info) {
		if (config.userName && config.userName.length) {
			// Basic authentication is required
			var credentials = auth(info.req);
			if (!credentials)
				return false;
			if (config.userName == credentials.name) {
				// Compare PBKDF2 hash for the supplied password with the hashed password
				try {
					var configHash = config.hashedPassword;
					var buf = Buffer.from(configHash, 'base64');
					// Extract the parameters that were used to generate the configHash password
					var extractedIterations = buf.readUInt32BE(0);
					var extractedSaltLen = buf.readUInt32BE(4);
					var extractedKeyLen = buf.readUInt32BE(8 + extractedSaltLen);
					var extractedSalt = Buffer.alloc(extractedSaltLen);
					buf.copy(extractedSalt, 0, 8, 8 + extractedSaltLen);

					// Generate a new key from the supplied credentials password, using the extracted parameters
					var newKey = pbkdf2.pbkdf2Sync(credentials.pass, extractedSalt, extractedIterations, extractedKeyLen, 'sha512');
					// Build the hash for credentials.pass
					var newBuf = new Buffer(newKey.length + extractedSalt.length + 12);
					newBuf.writeUInt32BE(extractedIterations, 0);
					newBuf.writeUInt32BE(extractedSalt.length, 4);
					extractedSalt.copy(newBuf, 8);
					newBuf.writeUInt32BE(newKey.length, 8 + extractedSalt.length);
					newKey.copy(newBuf, 12 + extractedSalt.length);
					var newHash = newBuf.toString('base64');
					// If the new hash matches the config hash, the password is good
					if (newHash == configHash) {
						return true;
					}
				}
				catch (e) {
				}
			}
			return false;
		}
		return true;
	}
});

wss.on('connection', function connection(ws, request) {
		if (wsConnection) {
			// We only allow a single client of the debugger, so reject this connection silently
			ws.close(1000, "A remote debug client is already connected");
			return;
		}
		wsConnection = ws;
    ws.on('message', function incoming(message) {
    	// Received message - forward to Omnis socket - each message is prefixed with its length, send as an ascii value terminated by +
    	debuggeeSocket.write(message.length + "+");
    	debuggeeSocket.write(message);
    });
    ws.on('close', function close(code, reason) {
    	if (wsConnection) {
    		debuggeeSocket.webSocketClosed();
    		wsConnection = null;
    	}
    });
    ws.on('error', function error(error) {
    	if (wsConnection) {
    		wsConnection = null;
	    	ws.close(1001, "An error occurred: " + error.toString());
    		debuggeeSocket.webSocketClosed();
    	}
    });
});

// Start rmm9924
server.on('error', (err) => {
	// Write error file
	fs.writeFileSync(errorFilePath, err.toString());
	process.exit(-4);
});

var client = new net.Socket();
client.connect(config.debugPort, '127.0.0.1', function () {
	// Port is already in use, so this is an error
	// Used to detect issues when running on Windows, since SO_REUSEADDR behaves differently on Windows to macOS and Linux
	fs.writeFileSync(errorFilePath, "Port " + config.debugPort + " is being used by another process");
	process.exit(-5);
});

client.on('error', function () {
	// Almost certainly connection refused.
	// We can start the server
	server.listen(config.debugPort);
	// And open the connection to Omnis
	openSocket();
});
// End rmm9924
// End of file
