#!/usr/bin/php -q
<?php

function usage()
{
	$script = basename($_SERVER['argv'][0]);
	echo $script . "\n";
	echo "COMMANDES :\n";
	echo "  --probe-dir              Teste la connexion et la lecture du dossier distant (via cURL/libssh2)\n";
	echo "  --upload-test            Teste l'upload d'un fichier de diagnostic (via cURL/libssh2)\n";
	echo "  --system-probe           Teste le handshake et le SFTP via le binaire ssh/sftp systeme (OpenSSH)\n";
	echo "\nPARAMETRES :\n";
	echo "  [--host=<string>]        Host SFTP (defaut: config locale)\n";
	echo "  [--port=<int>]           Port SFTP (defaut: config locale)\n";
	echo "  [--username=<string>]    Login SFTP (defaut: config locale)\n";
	echo "  [--password=<string>]    Mot de passe SFTP (defaut: config locale)\n";
	echo "  [--remote-dir=<string>]  Dossier distant (defaut: config locale)\n";
	echo "  [--timeout=<int>]        Timeout en secondes (defaut: config locale)\n";
	echo "  [--auth=<string>]        auto|password|keyboard\n";
	echo "  [--local-file=<string>]  Fichier local a envoyer en mode upload-test\n";
	echo "  [--remote-file=<string>] Nom du fichier distant en mode upload-test\n";
	echo "  [--keep-remote-file]     Ne supprime pas le fichier de test distant\n";
	echo "\nEXEMPLES :\n";
	echo "  php5.6 cli/cliDiagSftpCurl.php --probe-dir\n";
	echo "  php5.6 cli/cliDiagSftpCurl.php --probe-dir --port=22 --auth=auto\n";
	echo "  php5.6 cli/cliDiagSftpCurl.php --upload-test --remote-file=diag_test.txt\n";
	echo "  php5.6 cli/cliDiagSftpCurl.php --system-probe\n";
	exit(1);
}

function loadDefaultConfig()
{
	$config = array(
		'host' => '',
		'port' => 22,
		'username' => '',
		'password' => '',
		'remoteDir' => '/',
		'timeout' => 30,
		'auth' => 'auto',
	);

	$localConfigPath = dirname(__DIR__) . '/config/autoload/local.php';
	if (!is_file($localConfigPath)) {
		return $config;
	}

	$localConfig = include $localConfigPath;
	if (!is_array($localConfig)) {
		return $config;
	}

	if (!isset($localConfig['application_cron']['actions']['transfertDossierWLVersPMS_BMH__spec_Tyha']['options']['sftp'])) {
		return $config;
	}

	$sftpConfig = $localConfig['application_cron']['actions']['transfertDossierWLVersPMS_BMH__spec_Tyha']['options']['sftp'];
	if (!is_array($sftpConfig)) {
		return $config;
	}

	if (isset($sftpConfig['host'])) {
		$config['host'] = trim((string) $sftpConfig['host']);
	}
	if (isset($sftpConfig['port'])) {
		$config['port'] = (int) $sftpConfig['port'];
	}
	if (isset($sftpConfig['username'])) {
		$config['username'] = trim((string) $sftpConfig['username']);
	}
	if (isset($sftpConfig['password'])) {
		$config['password'] = (string) $sftpConfig['password'];
	}
	if (isset($sftpConfig['remoteDir'])) {
		$config['remoteDir'] = trim((string) $sftpConfig['remoteDir']);
	}
	if (isset($sftpConfig['timeout'])) {
		$config['timeout'] = (int) $sftpConfig['timeout'];
	}

	return $config;
}

function normalizeRemotePath($remotePath)
{
	$remotePath = trim((string) $remotePath);
	if ($remotePath === '' || $remotePath === '/') {
		return '/';
	}

	return '/' . trim($remotePath, '/');
}

function buildSftpUrl($host, $remotePath)
{
	return 'sftp://' . $host . normalizeRemotePath($remotePath);
}

function resolveSshAuthTypes($authMode)
{
	if ($authMode === 'auto' && defined('CURLSSH_AUTH_ANY')) {
		return CURLSSH_AUTH_ANY;
	}

	$authTypes = 0;
	if (($authMode === 'auto' || $authMode === 'password') && defined('CURLSSH_AUTH_PASSWORD')) {
		$authTypes |= CURLSSH_AUTH_PASSWORD;
	}
	if (($authMode === 'auto' || $authMode === 'keyboard') && defined('CURLSSH_AUTH_KEYBOARD')) {
		$authTypes |= CURLSSH_AUTH_KEYBOARD;
	}

	return $authTypes > 0 ? $authTypes : null;
}

function buildCurlBaseOptions($config)
{
	$options = array(
		CURLOPT_PORT => (int) $config['port'],
		CURLOPT_USERPWD => $config['username'] . ':' . $config['password'],
		CURLOPT_RETURNTRANSFER => true,
		CURLOPT_CONNECTTIMEOUT => max(5, (int) $config['timeout']),
		CURLOPT_TIMEOUT => max(10, (int) $config['timeout']),
		CURLOPT_VERBOSE => true,
	);

	if (defined('CURLPROTO_SFTP')) {
		$options[CURLOPT_PROTOCOLS] = CURLPROTO_SFTP;
	}

	$sshAuthTypes = resolveSshAuthTypes($config['auth']);
	if ($sshAuthTypes !== null) {
		$options[CURLOPT_SSH_AUTH_TYPES] = $sshAuthTypes;
	}

	return $options;
}

function executeCurl($config, $extraOptions)
{
	$stderr = fopen('php://temp', 'w+');
	if ($stderr === false) {
		throw new Exception('Impossible d ouvrir le buffer STDERR temporaire.');
	}

	$curlHandle = curl_init();
	if ($curlHandle === false) {
		fclose($stderr);
		throw new Exception('Impossible d initialiser cURL.');
	}

	$options = buildCurlBaseOptions($config);
	foreach ($extraOptions as $optionKey => $optionValue) {
		$options[$optionKey] = $optionValue;
	}
	$options[CURLOPT_STDERR] = $stderr;

	curl_setopt_array($curlHandle, $options);
	$result = curl_exec($curlHandle);
	$info = curl_getinfo($curlHandle);
	$errorNo = curl_errno($curlHandle);
	$errorMessage = curl_error($curlHandle);

	rewind($stderr);
	$verboseOutput = stream_get_contents($stderr);
	fclose($stderr);
	curl_close($curlHandle);

	return array(
		'result' => $result,
		'info' => $info,
		'errorNo' => $errorNo,
		'errorMessage' => $errorMessage,
		'verbose' => $verboseOutput,
	);
}

function printHeader($label)
{
	echo "######################################################################################################\n";
	echo '## ' . $label . "\n";
	echo "######################################################################################################\n";
}

function printEnvironment()
{
	$curlVersion = curl_version();
	echo 'php=' . PHP_VERSION . "\n";
	echo 'curl_version=' . $curlVersion['version'] . "\n";
	echo 'ssl_version=' . $curlVersion['ssl_version'] . "\n";
	echo 'protocols=' . implode(',', $curlVersion['protocols']) . "\n";
	echo 'curlssh_auth_any=' . (defined('CURLSSH_AUTH_ANY') ? '1' : '0') . "\n";
	echo 'curlssh_auth_password=' . (defined('CURLSSH_AUTH_PASSWORD') ? '1' : '0') . "\n";
	echo 'curlssh_auth_keyboard=' . (defined('CURLSSH_AUTH_KEYBOARD') ? '1' : '0') . "\n";
	echo "\n";
}

function maskConfig($config)
{
	$displayConfig = $config;
	$displayConfig['password'] = $config['password'] !== '' ? str_repeat('*', min(strlen($config['password']), 8)) : '';
	return $displayConfig;
}

function printExecution($label, $execution)
{
	echo '## ' . $label . "\n";
	echo 'curl_errno=' . $execution['errorNo'] . "\n";
	echo 'curl_error=' . $execution['errorMessage'] . "\n";
	echo 'primary_ip=' . (isset($execution['info']['primary_ip']) ? $execution['info']['primary_ip'] : '') . "\n";
	echo 'primary_port=' . (isset($execution['info']['primary_port']) ? $execution['info']['primary_port'] : '') . "\n";
	echo 'connect_time=' . (isset($execution['info']['connect_time']) ? $execution['info']['connect_time'] : '') . "\n";
	echo 'appconnect_time=' . (isset($execution['info']['appconnect_time']) ? $execution['info']['appconnect_time'] : '') . "\n";
	echo 'total_time=' . (isset($execution['info']['total_time']) ? $execution['info']['total_time'] : '') . "\n";
	echo 'size_upload=' . (isset($execution['info']['size_upload']) ? $execution['info']['size_upload'] : '') . "\n";
	echo 'result_snippet=' . substr((string) $execution['result'], 0, 500) . "\n";
	echo "verbose_output_start\n";
	echo $execution['verbose'];
	echo "verbose_output_end\n\n";
}

function probeDir($config)
{
	$remoteDir = rtrim(normalizeRemotePath($config['remoteDir']), '/') . '/';
	$execution = executeCurl($config, array(
		CURLOPT_URL => buildSftpUrl($config['host'], $remoteDir),
		CURLOPT_DIRLISTONLY => true,
	));
	printExecution('probe-dir ' . $remoteDir, $execution);
	return $execution;
}

function uploadTest($config, $localFile, $remoteFile, $keepRemoteFile)
{
	$localFileWasCreated = false;
	if ($localFile === '') {
		$localFile = tempnam(sys_get_temp_dir(), 'sftp_diag_');
		if ($localFile === false) {
			throw new Exception('Impossible de creer le fichier temporaire local.');
		}
		if (file_put_contents($localFile, 'diag=' . date('c') . "\n") === false) {
			throw new Exception('Impossible d ecrire le fichier temporaire local.');
		}
		$localFileWasCreated = true;
	}

	if (!is_file($localFile)) {
		throw new Exception('Fichier local introuvable : ' . $localFile);
	}

	if ($remoteFile === '') {
		$remoteFile = 'diag_sftp_' . date('Ymd_His') . '.txt';
	}

	$remotePath = normalizeRemotePath($config['remoteDir']);
	if ($remotePath === '/') {
		$remotePath = '/' . ltrim($remoteFile, '/');
	} else {
		$remotePath = rtrim($remotePath, '/') . '/' . ltrim($remoteFile, '/');
	}

	$fileHandle = fopen($localFile, 'r');
	if ($fileHandle === false) {
		throw new Exception('Impossible d ouvrir le fichier local : ' . $localFile);
	}

	$execution = executeCurl($config, array(
		CURLOPT_URL => buildSftpUrl($config['host'], $remotePath),
		CURLOPT_UPLOAD => true,
		CURLOPT_INFILE => $fileHandle,
		CURLOPT_INFILESIZE => filesize($localFile),
	));
	fclose($fileHandle);

	printExecution('upload-test ' . $remotePath, $execution);

	if ($execution['errorNo'] === 0 && !$keepRemoteFile) {
		$deleteExecution = executeCurl($config, array(
			CURLOPT_URL => buildSftpUrl($config['host'], '/'),
			CURLOPT_QUOTE => array('rm ' . $remotePath),
		));
		printExecution('delete-test-file ' . $remotePath, $deleteExecution);
	}

	if ($localFileWasCreated && is_file($localFile)) {
		@unlink($localFile);
	}

	return $execution;
}

function findBinary($name)
{
	$descriptors = array(
		1 => array('pipe', 'w'),
		2 => array('pipe', 'w'),
	);
	$process = proc_open('command -v ' . escapeshellarg($name) . ' 2>/dev/null', $descriptors, $pipes);
	if (!is_resource($process)) {
		return '';
	}
	$path = trim((string) stream_get_contents($pipes[1]));
	fclose($pipes[1]);
	fclose($pipes[2]);
	proc_close($process);

	return $path;
}

function runProcess(array $command, $stdinData, $timeoutSeconds, array $extraEnv = array())
{
	$commandLine = '';
	foreach ($command as $part) {
		$commandLine .= ($commandLine === '' ? '' : ' ') . escapeshellarg($part);
	}

	$descriptors = array(
		0 => array('pipe', 'r'),
		1 => array('pipe', 'w'),
		2 => array('pipe', 'w'),
	);

	$env = null;
	if (!empty($extraEnv)) {
		$env = is_array($_ENV) ? $_ENV : array();
		if (empty($env)) {
			foreach (array('PATH', 'HOME', 'USER', 'LANG') as $key) {
				$value = getenv($key);
				if ($value !== false) {
					$env[$key] = $value;
				}
			}
		}
		foreach ($extraEnv as $key => $value) {
			$env[$key] = $value;
		}
	}

	$process = proc_open($commandLine, $descriptors, $pipes, null, $env);
	if (!is_resource($process)) {
		return array('exitCode' => -1, 'stdout' => '', 'stderr' => 'proc_open a echoue', 'timedOut' => false);
	}

	if ($stdinData !== null && $stdinData !== '') {
		fwrite($pipes[0], $stdinData);
	}
	fclose($pipes[0]);

	stream_set_blocking($pipes[1], false);
	stream_set_blocking($pipes[2], false);

	$stdout = '';
	$stderr = '';
	$timedOut = false;
	$exitCode = -1;
	$deadline = time() + max(1, (int) $timeoutSeconds);

	while (true) {
		$status = proc_get_status($process);
		$stdout .= stream_get_contents($pipes[1]);
		$stderr .= stream_get_contents($pipes[2]);

		if (!$status['running']) {
			$exitCode = (int) $status['exitcode'];
			break;
		}
		if (time() >= $deadline) {
			$timedOut = true;
			proc_terminate($process, 9);
			break;
		}
		usleep(100000);
	}

	$stdout .= stream_get_contents($pipes[1]);
	$stderr .= stream_get_contents($pipes[2]);
	fclose($pipes[1]);
	fclose($pipes[2]);
	proc_close($process);

	return array(
		'exitCode' => $exitCode,
		'stdout' => $stdout,
		'stderr' => $stderr,
		'timedOut' => $timedOut,
	);
}

function systemProbe($config)
{
	echo "## binaires systeme\n";
	$sshBinary = findBinary('ssh');
	$sftpBinary = findBinary('sftp');
	$sshpassBinary = findBinary('sshpass');
	echo 'ssh=' . ($sshBinary !== '' ? $sshBinary : 'ABSENT') . "\n";
	echo 'sftp=' . ($sftpBinary !== '' ? $sftpBinary : 'ABSENT') . "\n";
	echo 'sshpass=' . ($sshpassBinary !== '' ? $sshpassBinary : 'ABSENT') . "\n";
	if ($sshBinary !== '') {
		$versionResult = runProcess(array($sshBinary, '-V'), null, 10);
		echo 'ssh_version=' . trim($versionResult['stderr'] . $versionResult['stdout']) . "\n";
	}
	echo "\n";

	if ($sshBinary === '') {
		echo "## handshake : IMPOSSIBLE (binaire ssh absent)\n\n";
		return 1;
	}

	echo "## handshake SSH (ssh -vv, sans mot de passe : on observe uniquement la negociation)\n";
	$timeout = max(5, (int) $config['timeout']);
	$handshakeCommand = array(
		$sshBinary,
		'-vv',
		'-p', (string) (int) $config['port'],
		'-o', 'BatchMode=yes',
		'-o', 'StrictHostKeyChecking=no',
		'-o', 'UserKnownHostsFile=/dev/null',
		'-o', 'PreferredAuthentications=none',
		'-o', 'ConnectTimeout=' . $timeout,
		$config['username'] . '@' . $config['host'],
		'exit',
	);
	$handshake = runProcess($handshakeCommand, null, $timeout + 5);
	$stderr = $handshake['stderr'];

	$negotiated = array();
	foreach (explode("\n", $stderr) as $line) {
		if (stripos($line, 'kex: algorithm:') !== false
			|| stripos($line, 'kex: host key algorithm:') !== false
			|| stripos($line, 'kex: server->client cipher:') !== false
			|| stripos($line, 'kex: client->server cipher:') !== false
			|| stripos($line, 'Server host key:') !== false) {
			$negotiated[] = trim(preg_replace('/^debug\d*:\s*/i', '', trim($line)));
		}
	}

	$handshakeOk = (stripos($stderr, 'Authentications that can continue') !== false
		|| stripos($stderr, 'Permission denied') !== false
		|| stripos($stderr, 'Next authentication method') !== false
		|| stripos($stderr, 'Authenticated to') !== false);
	$handshakeFailed = (stripos($stderr, 'Unable to negotiate') !== false
		|| stripos($stderr, 'no matching') !== false);

	echo 'handshake_ok=' . ($handshakeOk && !$handshakeFailed ? 'OUI' : 'NON') . "\n";
	if ($handshakeFailed) {
		echo "handshake_failed_negotiation=OUI (algorithmes incompatibles cote client/serveur)\n";
	}
	echo "algos_negocies:\n";
	if (!empty($negotiated)) {
		foreach ($negotiated as $negotiatedLine) {
			echo '  ' . $negotiatedLine . "\n";
		}
	} else {
		echo "  (aucune ligne kex capturee)\n";
	}
	echo "ssh_stderr_tail:\n";
	$stderrLines = array_values(array_filter(explode("\n", $stderr), 'strlen'));
	$tail = array_slice($stderrLines, -25);
	foreach ($tail as $tailLine) {
		echo '  ' . $tailLine . "\n";
	}
	echo "\n";

	if ($handshakeOk && !$handshakeFailed) {
		echo ">>> CONCLUSION : le binaire ssh systeme NEGOCIE le handshake avec le serveur,\n";
		echo "    la ou curl/libssh2 (errno 2 'Failure establishing ssh session') echoue.\n";
		echo "    => libssh2 derriere curl est trop ancienne ; basculer le transfert sur le binaire sftp systeme.\n\n";
	}

	if ($sftpBinary === '') {
		echo "## test sftp reel : IMPOSSIBLE (binaire sftp absent)\n\n";
		return 0;
	}
	if ($config['password'] === '') {
		echo "## test sftp reel : IGNORE (aucun mot de passe ; passer --password=... ou completer local.php)\n\n";
		return 0;
	}
	if ($sshpassBinary === '') {
		echo "## test sftp reel : sshpass ABSENT -> auth par mot de passe non automatisable en CLI.\n";
		echo "   (Pour le transfert applicatif : installer sshpass, OU passer a une cle SSH, OU phpseclib.)\n\n";
		return 0;
	}

	echo "## test sftp reel via sshpass + sftp systeme (ls du dossier distant)\n";
	$remoteDir = rtrim(normalizeRemotePath($config['remoteDir']), '/') . '/';
	$batch = 'ls ' . $remoteDir . "\nbye\n";
	$sftpCommand = array(
		$sshpassBinary, '-e',
		$sftpBinary,
		'-P', (string) (int) $config['port'],
		'-o', 'BatchMode=no',
		'-o', 'StrictHostKeyChecking=no',
		'-o', 'UserKnownHostsFile=/dev/null',
		'-o', 'ConnectTimeout=' . $timeout,
		'-b', '-',
		$config['username'] . '@' . $config['host'],
	);
	$sftp = runProcess($sftpCommand, $batch, $timeout + 10, array('SSHPASS' => $config['password']));
	echo 'sftp_exit_code=' . $sftp['exitCode'] . ($sftp['timedOut'] ? ' (TIMEOUT)' : '') . "\n";
	echo "sftp_stdout:\n" . rtrim($sftp['stdout']) . "\n";
	echo "sftp_stderr:\n" . rtrim($sftp['stderr']) . "\n";
	echo 'sftp_result=' . ($sftp['exitCode'] === 0 ? 'SUCCES (transfert via binaire systeme viable)' : 'ECHEC') . "\n\n";

	return $sftp['exitCode'] === 0 ? 0 : 4;
}

$config = loadDefaultConfig();
$mode = '';
$localFile = '';
$remoteFile = '';
$keepRemoteFile = false;

for ($i = 1; $i < $_SERVER['argc']; $i++) {
	$arg = $_SERVER['argv'][$i];

	if ($arg === '--probe-dir') {
		$mode = 'probe-dir';
		continue;
	}
	if ($arg === '--upload-test') {
		$mode = 'upload-test';
		continue;
	}
	if ($arg === '--system-probe') {
		$mode = 'system-probe';
		continue;
	}
	if ($arg === '--keep-remote-file') {
		$keepRemoteFile = true;
		continue;
	}
	if (preg_match('@^--host=(.*)$@', $arg, $regs)) {
		$config['host'] = trim($regs[1]);
		continue;
	}
	if (preg_match('@^--port=(.*)$@', $arg, $regs)) {
		$config['port'] = (int) $regs[1];
		continue;
	}
	if (preg_match('@^--username=(.*)$@', $arg, $regs)) {
		$config['username'] = trim($regs[1]);
		continue;
	}
	if (preg_match('@^--password=(.*)$@', $arg, $regs)) {
		$config['password'] = (string) $regs[1];
		continue;
	}
	if (preg_match('@^--remote-dir=(.*)$@', $arg, $regs)) {
		$config['remoteDir'] = trim($regs[1]);
		continue;
	}
	if (preg_match('@^--timeout=(.*)$@', $arg, $regs)) {
		$config['timeout'] = (int) $regs[1];
		continue;
	}
	if (preg_match('@^--auth=(.*)$@', $arg, $regs)) {
		$config['auth'] = trim($regs[1]);
		continue;
	}
	if (preg_match('@^--local-file=(.*)$@', $arg, $regs)) {
		$localFile = trim($regs[1]);
		continue;
	}
	if (preg_match('@^--remote-file=(.*)$@', $arg, $regs)) {
		$remoteFile = trim($regs[1]);
		continue;
	}

	echo 'Parametre inconnu : ' . $arg . "\n";
	usage();
}

if ($mode === '') {
	usage();
}

$config['remoteDir'] = normalizeRemotePath($config['remoteDir']);
if (!in_array($config['auth'], array('auto', 'password', 'keyboard'))) {
	echo 'Mode auth invalide : ' . $config['auth'] . "\n";
	usage();
}

printHeader('Diagnostic SFTP cURL');
printEnvironment();
echo 'config=' . json_encode(maskConfig($config), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n\n";

try {
	if ($mode === 'probe-dir') {
		$execution = probeDir($config);
		exit($execution['errorNo'] === 0 ? 0 : 2);
	}

	if ($mode === 'upload-test') {
		$execution = uploadTest($config, $localFile, $remoteFile, $keepRemoteFile);
		exit($execution['errorNo'] === 0 ? 0 : 3);
	}

	if ($mode === 'system-probe') {
		exit(systemProbe($config));
	}
} catch (Exception $ex) {
	echo 'EXCEPTION=' . $ex->getMessage() . "\n";
	exit(99);
}

exit(0);