<?php

# Copyright (c) 2009-2015 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#
#   * Redistributions in binary form must reproduce the above
#     copyright notice, this list of conditions and the following
#     disclaimer in the documentation and/or other materials provided
#     with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

define('S_OK', 'OK');
define('S_BAD_OTP', 'BAD_OTP');
define('S_REPLAYED_OTP', 'REPLAYED_OTP');
define('S_DELAYED_OTP', 'DELAYED_OTP');
define('S_BAD_SIGNATURE', 'BAD_SIGNATURE');
define('S_MISSING_PARAMETER', 'MISSING_PARAMETER');
define('S_NO_SUCH_CLIENT', 'NO_SUCH_CLIENT');
define('S_OPERATION_NOT_ALLOWED', 'OPERATION_NOT_ALLOWED');
define('S_BACKEND_ERROR', 'BACKEND_ERROR');
define('S_NOT_ENOUGH_ANSWERS', 'NOT_ENOUGH_ANSWERS');
define('S_REPLAYED_REQUEST', 'REPLAYED_REQUEST');


define('TS_SEC', 1/8);
define('TS_REL_TOLERANCE', 0.3);
define('TS_ABS_TOLERANCE', 20);

define('TOKEN_LEN', 32);
define('OTP_MAX_LEN', 48); // TOKEN_LEN plus public identity of 0..16

function logdie ($logger, $str)
{
	$logger->log(LOG_INFO, $str);
	die($str . "\n");
}

function getHttpVal ($key, $default, $a)
{
	if (array_key_exists($key, $a))
	{
		$val = $a[$key];
	}
	else
	{
		$val = $default;
	}

	$val = trim($val);
	$val = str_replace('\\', '', $val);

	return $val;
}

// Sign a http query string in the array of key-value pairs
// return b64 encoded hmac hash
function sign($a, $apiKey, $logger)
{
	ksort($a);

	$qs = http_build_query($a);
	$qs = urldecode($qs);
	$qs = utf8_encode($qs);

	// base64 encoded binary digest
	$hmac = hash_hmac('sha1', $qs, $apiKey, TRUE);
	$hmac = base64_encode($hmac);

	$logger->log(LOG_DEBUG, "SIGN: $qs H=$hmac");

	return $hmac;
}

function curl_settings($logger, $ident, $ch, $url, $timeout, $opts)
{
	$logger->log(LOG_DEBUG, "$ident adding URL : $url");

	curl_setopt($ch, CURLOPT_URL, $url);
	curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
	curl_setopt($ch, CURLOPT_USERAGENT, 'YK-VAL');
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
	curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);

	if (is_array($opts) === FALSE)
	{
		$logger->log(LOG_WARN, $ident . 'curl options must be an array');
		return;
	}

	foreach ($opts as $key => $val)
		if (curl_setopt($ch, $key, $val) === FALSE)
			$logger->log(LOG_WARN, "$ident failed to set " . curl_opt_name($key));
}

// returns the string name of a curl constant,
//	or "curl option" if constant not found.
// e.g.
//  curl_opt_name(CURLOPT_URL) returns "CURLOPT_URL"
//  curl_opt_name(CURLOPT_BLABLA) returns "curl option"
function curl_opt_name($opt)
{
	$consts = get_defined_constants(true);
	$consts = $consts['curl'];

	$name = array_search($opt, $consts, TRUE);

	// array_search may return either on failure...
	if ($name === FALSE || $name === NULL)
		return 'curl option';

	return $name;
}

// This function takes a list of URLs.  It will return the content of
// the first successfully retrieved URL, whose content matches ^OK.
// The request are sent asynchronously.  Some of the URLs can fail
// with unknown host, connection errors, or network timeout, but as
// long as one of the URLs given work, data will be returned.  If all
// URLs fail, data from some URL that did not match parameter $match
// (defaults to ^OK) is returned, or if all URLs failed, false.
function retrieveURLasync($ident, $urls, $logger, $ans_req=1, $match="^OK", $returl=False, $timeout=10, $curlopts)
{
	$mh = curl_multi_init();
	$ch = array();

	foreach ($urls as $url)
	{
		$handle = curl_init();
		curl_settings($logger, $ident, $handle, $url, $timeout, $curlopts);
		curl_multi_add_handle($mh, $handle);
		$ch[$handle] = $handle;
	}

	$ans_arr = array();

	do
	{
		while (curl_multi_exec($mh, $active) == CURLM_CALL_MULTI_PERFORM);

		while ($info = curl_multi_info_read($mh))
		{
			$logger->log(LOG_DEBUG, "$ident curl multi info : ", $info);

			if ($info['result'] == CURLE_OK)
			{
				$str = curl_multi_getcontent($info['handle']);

				$logger->log(LOG_DEBUG, "$ident curl multi content : $str");

				if (preg_match("/$match/", $str))
				{
					$logger->log(LOG_DEBUG, "$ident response matches $match");
					$error = curl_error($info['handle']);
					$errno = curl_errno($info['handle']);
					$cinfo = curl_getinfo($info['handle']);
					$logger->log(LOG_INFO, "$ident errno/error: $errno/$error", $cinfo);

					if ($returl)
						$ans_arr[] = "url=" . $cinfo['url'] . "\n" . $str;
					else
						$ans_arr[] = $str;
				}

				if (count($ans_arr) >= $ans_req)
				{
					foreach ($ch as $h)
					{
						curl_multi_remove_handle($mh, $h);
						curl_close($h);
					}
					curl_multi_close($mh);

					return $ans_arr;
				}

				curl_multi_remove_handle($mh, $info['handle']);
				curl_close($info['handle']);
				unset($ch[$info['handle']]);
			}

			curl_multi_select($mh);
		}
	}
	while($active);

	foreach ($ch as $h)
	{
		curl_multi_remove_handle($mh, $h);
		curl_close($h);
	}
	curl_multi_close($mh);

	if (count($ans_arr) > 0)
		return $ans_arr;

	return false;
}

function KSMdecryptOTP($urls, $logger, $curlopts)
{
	$response = retrieveURLasync('YK-KSM', $urls, $logger, $ans_req=1, $match='^OK', $returl=False, $timeout=10, $curlopts);

	if ($response === FALSE)
		return false;

	$response = array_shift($response);

	$logger->log(LOG_DEBUG, "YK-KSM response: $response");

	$ret = array();

	if (sscanf($response,
		'OK counter=%04x low=%04x high=%02x use=%02x',
		$ret['session_counter'],
		$ret['low'],
		$ret['high'],
		$ret['session_use']) !== 4)
	{
		return false;
	}

	return $ret;
}

function sendResp($status, $logger, $apiKey = '', $extra = null)
{
	if ($logger->request !== NULL)
		$logger->request->set('status', $status);

	$a['status'] = $status;

	// 2008-11-21T06:11:55Z0711
	$t = substr(microtime(false), 2, 3);
	$t = gmdate('Y-m-d\TH:i:s\Z0') . $t;

	$a['t'] = $t;

	if ($extra)
		foreach ($extra as $param => $value)
			$a[$param] = $value;

	$h = sign($a, $apiKey, $logger);

	$str = "";
	$str .= "h=" . $h . "\r\n";
	$str .= "t=" . $a['t'] . "\r\n";

	if ($extra)
		foreach ($extra as $param => $value)
			$str .= $param . "=" . $value . "\r\n";

	$str .= "status=" . $a['status'] . "\r\n";
	$str .= "\r\n";

	$logger->log(LOG_INFO, "Response: " . $str . " (at " . gmdate("c") . " " . microtime() . ")");

	if ($logger->request !== NULL)
		$logger->request->write();

	echo $str;
	exit;
}

// backport from PHP 5.6
if (function_exists('hash_equals') === FALSE)
{
	function hash_equals($a, $b)
	{
		// hashes are a (known) fixed length,
		//	so this doesn't leak anything.
		if (strlen($a) != strlen($b))
			return false;

		$result = 0;

		for ($i = 0; $i < strlen($a); $i++)
			$result |= ord($a[$i]) ^ ord($b[$i]);

		return (0 === $result);
	}
}

/**
 * Return the total time taken to receive a response from a URL.
 *
 * @argument $url string
 * @return float|bool seconds or false on failure
 */
function total_time ($url)
{
	$opts = array(
		CURLOPT_URL => $url,
		CURLOPT_TIMEOUT => 3,
		CURLOPT_FORBID_REUSE => TRUE,
		CURLOPT_FRESH_CONNECT => TRUE,
		CURLOPT_RETURNTRANSFER => TRUE,
		CURLOPT_USERAGENT => 'ykval-munin-vallatency/1.0',
	);

	if (($ch = curl_init()) === FALSE)
		return false;

	if (curl_setopt_array($ch, $opts) === FALSE)
		return false;

	// we don't care about the actual response
	if (curl_exec($ch) === FALSE)
		return false;

	$total_time = curl_getinfo($ch, CURLINFO_TOTAL_TIME);

	curl_close($ch);

	if (is_float($total_time) === FALSE)
		return false;

	return $total_time;
}

/**
 * Given a list of urls, create internal and label names for munin.
 *
 * @argument $urls array
 * @return array|bool array or false on failure.
 */
function endpoints ($urls)
{
	$endpoints = array();

	foreach ($urls as $url)
	{
		// internal munin name must be a-zA-Z0-9_,
		//	so sha1 hex should be fine.
		//
		// munin also truncates at some length,
		//	so we just take the first few characters of the hashsum.
		$internal = substr(sha1($url), 0, 20);

		// actual label name shown for graph values
		if (($label = hostport($url)) === FALSE)
		{
			return false;
		}

		$endpoints[] = array($internal, $label, $url);
	}

	// check for truncated sha1 collisions (or actual duplicate URLs!)
	$internal = array();

	foreach($endpoints as $endpoint)
	{
		$internal[] = $endpoint[0];
	}

	if (count(array_unique($internal)) !== count($endpoints))
		return false;

	return $endpoints;
}

/**
 * Given a URL, if the port is defined or can be determined from the scheme,
 *	return the hostname and port.
 * Otherwise just return the hostname.
 *
 * @argument $url string
 * @return string|bool string or false on failure
 */
function hostport ($url)
{
	if (($url = parse_url($url)) === FALSE)
		return false;

	if (array_key_exists('host', $url) === FALSE || $url['host'] === NULL)
		return false;

	if (array_key_exists('port', $url) === TRUE && $url['port'] !== NULL)
		return $url['host'].':'.$url['port'];

	if (array_key_exists('scheme', $url) === TRUE
			&& strtolower($url['scheme']) === 'http')
		return $url['host'].':80';

	if (array_key_exists('scheme', $url) === TRUE
			&& strtolower($url['scheme']) === 'https')
		return $url['host'].':443';

	return $url['host'];
}