git subrepo clone https://github.com/mailcow/mailcow-dockerized.git mailcow/src/mailcow-dockerized

subrepo: subdir:   "mailcow/src/mailcow-dockerized"
  merged:   "a832becb"
upstream: origin:   "https://github.com/mailcow/mailcow-dockerized.git"
  branch:   "master"
  commit:   "a832becb"
git-subrepo: version:  "0.4.3"
  origin:   "???"
  commit:   "???"
Change-Id: If5be2d621a211e164c9b6577adaa7884449f16b5
diff --git a/mailcow/src/mailcow-dockerized/data/web/inc/lib/Yubico.php b/mailcow/src/mailcow-dockerized/data/web/inc/lib/Yubico.php
new file mode 100644
index 0000000..cde9f4d
--- /dev/null
+++ b/mailcow/src/mailcow-dockerized/data/web/inc/lib/Yubico.php
@@ -0,0 +1,500 @@
+<?php
+  /**
+   * Class for verifying Yubico One-Time-Passcodes
+   *
+   * @category    Auth
+   * @package     Auth_Yubico
+   * @author      Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com>
+   * @copyright   2007-2020 Yubico AB
+   * @license     https://opensource.org/licenses/bsd-license.php New BSD License
+   * @version     2.0
+   * @link        https://www.yubico.com/
+   */
+
+require_once 'PEAR.php';
+
+/**
+ * Class for verifying Yubico One-Time-Passcodes
+ *
+ * Simple example:
+ * <code>
+ * require_once 'Auth/Yubico.php';
+ * $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif";
+ *
+ * # Generate a new id+key from https://api.yubico.com/get-api-key/
+ * $yubi = new Auth_Yubico('42', 'FOOBAR=');
+ * $auth = $yubi->verify($otp);
+ * if (PEAR::isError($auth)) {
+ *    print "<p>Authentication failed: " . $auth->getMessage();
+ *    print "<p>Debug output from server: " . $yubi->getLastResponse();
+ * } else {
+ *    print "<p>You are authenticated!";
+ * }
+ * </code>
+ */
+class Auth_Yubico
+{
+	/**#@+
+	 * @access private
+	 */
+
+	/**
+	 * Yubico client ID
+	 * @var string
+	 */
+	var $_id;
+
+	/**
+	 * Yubico client key
+	 * @var string
+	 */
+	var $_key;
+
+	/**
+	 * List with URL part of validation servers
+	 * @var array
+	 */
+	var $_url_list;
+
+	/**
+	 * index to _url_list
+	 * @var int
+	 */
+	var $_url_index;
+
+	/**
+	 * Last query to server
+	 * @var string
+	 */
+	var $_lastquery;
+
+	/**
+	 * Response from server
+	 * @var string
+	 */
+	var $_response;
+
+	/**
+	 * Number of times we retried in our last validation
+	 * @var int
+	 */
+	var $_retries;
+
+	/**
+	 * Flag whether to verify HTTPS server certificates or not.
+	 * @var boolean
+	 */
+	var $_httpsverify;
+
+	/**
+	 * Maximum number of times we will retry transient HTTP errors
+	 * @var int
+	 */
+	var $_max_retries;
+
+	/**
+	 * Constructor
+	 *
+	 * Sets up the object
+	 * @param    string  $id     The client identity
+	 * @param    string  $key    The client MAC key (optional)
+	 * @param    boolean $https  noop
+	 * @param    boolean $httpsverify  Flag whether to use verify HTTPS
+	 *                                 server certificates (optional,
+	 *                                 default true)
+	 * @access public
+	 */
+        public function __construct($id, $key = '', $https = 0, $httpsverify = 1, $max_retries = 3)
+	{
+		$this->_id =  $id;
+		$this->_key = base64_decode($key);
+		$this->_httpsverify = $httpsverify;
+		$this->_max_retries = $max_retries;
+	}
+
+	/**
+	 * Specify to use a different URL part for verification.
+	 * The default is "https://api.yubico.com/wsapi/2.0/verify".
+	 *
+	 * @param  string $url  New server URL part to use
+	 * @access public
+	 * @deprecated
+	 */
+	function setURLpart($url)
+	{
+	  $this->_url_list = array($url);
+	}
+
+	/**
+	 * Get next URL part from list to use for validation.
+	 *
+	 * @return mixed string with URL part or false if no more URLs in list
+	 * @access public
+	 */
+	function getNextURLpart()
+	{
+	  if ($this->_url_list) $url_list=$this->_url_list;
+	  else $url_list=array('https://api.yubico.com/wsapi/2.0/verify');
+
+	  if ($this->_url_index>=count($url_list)) return false;
+	  else return $url_list[$this->_url_index++];
+	}
+
+	/**
+	 * Resets index to URL list
+	 *
+	 * @access public
+	 */
+	function URLreset()
+	{
+	  $this->_url_index=0;
+	}
+
+	/**
+	 * Add another URLpart.
+	 *
+	 * @access public
+	 */
+	function addURLpart($URLpart) 
+	{
+	  $this->_url_list[]=$URLpart;
+	}
+	
+	/**
+	 * Return the last query sent to the server, if any.
+	 *
+	 * @return string  Request to server
+	 * @access public
+	 */
+	function getLastQuery()
+	{
+		return $this->_lastquery;
+	}
+
+	/**
+	 * Return the last data received from the server, if any.
+	 *
+	 * @return string  Output from server
+	 * @access public
+	 */
+	function getLastResponse()
+	{
+		return $this->_response;
+	}
+
+	/**
+	 * Return the number of retries that were used in the last validation
+	 *
+	 * @return int     Number of retries
+	 * @access public
+	 */
+	function getRetries()
+	{
+		return $this->_retries;
+	}
+
+	/**
+	 * Parse input string into password, yubikey prefix,
+	 * ciphertext, and OTP.
+	 *
+	 * @param  string    Input string to parse
+	 * @param  string    Optional delimiter re-class, default is '[:]'
+	 * @return array     Keyed array with fields
+	 * @access public
+	 */
+	function parsePasswordOTP($str, $delim = '[:]')
+	{
+	  if (!preg_match("/^((.*)" . $delim . ")?" .
+			  "(([cbdefghijklnrtuv]{0,16})" .
+			  "([cbdefghijklnrtuv]{32}))$/i",
+			  $str, $matches)) {
+	    /* Dvorak? */
+	    if (!preg_match("/^((.*)" . $delim . ")?" .
+			    "(([jxe\.uidchtnbpygk]{0,16})" .
+			    "([jxe\.uidchtnbpygk]{32}))$/i",
+			    $str, $matches)) {
+	      return false;
+	    } else {
+	      $ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");
+	    }
+	  } else {
+	    $ret['otp'] = $matches[3];
+	  }
+	  $ret['password'] = $matches[2];
+	  $ret['prefix'] = $matches[4];
+	  $ret['ciphertext'] = $matches[5];
+	  return $ret;
+	}
+
+	/* TODO? Add functions to get parsed parts of server response? */
+
+	/**
+	 * Parse parameters from last response
+	 *
+	 * example: getParameters("timestamp", "sessioncounter", "sessionuse");
+	 *
+	 * @param  array @parameters  Array with strings representing
+	 *                            parameters to parse
+	 * @return array  parameter array from last response
+	 * @access public
+	 */
+	function getParameters($parameters)
+	{
+	  if ($parameters == null) {
+	    $parameters = array('timestamp', 'sessioncounter', 'sessionuse');
+	  }
+	  $param_array = array();
+	  foreach ($parameters as $param) {
+	    if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {
+	      return PEAR::raiseError('Could not parse parameter ' . $param . ' from response');
+	    }
+	    $param_array[$param]=$out[1];
+	  }
+	  return $param_array;
+	}
+
+        function _make_curl_handle($query, $timeout=null)
+	{
+	    flush();
+	    $handle = curl_init($query);
+	    curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
+	    curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
+	    if (!$this->_httpsverify) {
+		curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
+		curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);
+	    }
+	    curl_setopt($handle, CURLOPT_FAILONERROR, true);
+	    /* If timeout is set, we better apply it here as well
+	     * in case the validation server fails to follow it. */
+	    if ($timeout) {
+		curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
+	    }
+
+	    return $handle;
+	}
+
+	/**
+	 * Verify Yubico OTP against multiple URLs
+	 * Protocol specification 2.0 is used to construct validation requests
+	 *
+	 * @param string $token        Yubico OTP
+	 * @param int $use_timestamp   1=>send request with &timestamp=1 to
+	 *                             get timestamp and session information
+	 *                             in the response
+	 * @param boolean $wait_for_all  If true, wait until all
+	 *                               servers responds (for debugging)
+	 * @param string $sl           Sync level in percentage between 0
+	 *                             and 100 or "fast" or "secure".
+	 * @param int $timeout         Max number of seconds to wait
+	 *                             for responses
+	 * @param int $max_retries     Max number of times we will retry on
+	 *                             transient errors.
+	 * @return mixed               PEAR error on error, true otherwise
+	 * @access public
+	 */
+	function verify($token, $use_timestamp=null, $wait_for_all=False,
+			$sl=null, $timeout=null, $max_retries=null)
+	{
+	  /* If maximum retries is not set, default from instance */
+	  if (is_null($max_retries)) {
+	    $max_retries = $this->_max_retries;
+	  }
+
+	  /* Construct parameters string */
+	  $ret = $this->parsePasswordOTP($token);
+	  if (!$ret) {
+	    return PEAR::raiseError('Could not parse Yubikey OTP');
+	  }
+	  $params = array('id'=>$this->_id, 
+			  'otp'=>$ret['otp'],
+			  'nonce'=>md5(uniqid(rand())));
+	  /* Take care of protocol version 2 parameters */
+	  if ($use_timestamp) $params['timestamp'] = 1;
+	  if ($sl) $params['sl'] = $sl;
+	  if ($timeout) $params['timeout'] = $timeout;
+	  ksort($params);
+	  $parameters = '';
+	  foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;
+	  $parameters = ltrim($parameters, "&");
+	  
+	  /* Generate signature. */
+	  if($this->_key <> "") {
+	    $signature = base64_encode(hash_hmac('sha1', $parameters,
+						 $this->_key, true));
+	    $signature = preg_replace('/\+/', '%2B', $signature);
+	    $parameters .= '&h=' . $signature;
+	  }
+
+	  /* Generate and prepare request. */
+	  $this->_lastquery = null;
+	  $this->_retries = 0;
+	  $this->URLreset();
+
+	  $mh = curl_multi_init();
+	  $ch = array();
+	  $retries = array();
+	  while($URLpart=$this->getNextURLpart()) 
+	    {
+	      $query = $URLpart . "?" . $parameters;
+
+	      if ($this->_lastquery) { $this->_lastquery .= " "; }
+	      $this->_lastquery .= $query;
+	      
+	      $handle = $this->_make_curl_handle($query, $timeout);
+	      curl_multi_add_handle($mh, $handle);
+	      
+	      $ch[(int)$handle] = $handle;
+	      $retries[$query] = 0;
+	    }
+
+	  /* Execute and read request. */
+	  $this->_response=null;
+	  $replay=False;
+	  $valid=False;
+	  do {
+	    /* Let curl do its work. */
+	    while (($mrc = curl_multi_exec($mh, $active))
+		   == CURLM_CALL_MULTI_PERFORM) {
+	      curl_multi_select($mh);
+	    }
+
+	    while ($info = curl_multi_info_read($mh)) {
+	      $cinfo = curl_getinfo ($info['handle']);
+	      if ($info['result'] == CURLE_OK) {
+		/* We have a complete response from one server. */
+
+		$str = curl_multi_getcontent($info['handle']);
+		
+		if ($wait_for_all) { # Better debug info
+		  $this->_response .= 'URL=' . $cinfo['url'] . ' HTTP_CODE='
+		    . $cinfo['http_code'] . "\n"
+		    . $str . "\n";
+		}
+
+		if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {
+		  $status = $out[1];
+
+		  /* 
+		   * There are 3 cases.
+		   *
+		   * 1. OTP or Nonce values doesn't match - ignore
+		   * response.
+		   *
+		   * 2. We have a HMAC key.  If signature is invalid -
+		   * ignore response.  Return if status=OK or
+		   * status=REPLAYED_OTP.
+		   *
+		   * 3. Return if status=OK or status=REPLAYED_OTP.
+		   */
+		  if (!preg_match("/otp=".$params['otp']."/", $str) ||
+		      !preg_match("/nonce=".$params['nonce']."/", $str)) {
+		    /* Case 1. Ignore response. */
+		  } 
+		  elseif ($this->_key <> "") {
+		    /* Case 2. Verify signature first */
+		    $rows = explode("\r\n", trim($str));
+		    $response=array();
+			foreach ($rows as $key => $val) {
+		      /* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
+		      $val = preg_replace('/=/', '#', $val, 1);
+		      $row = explode("#", $val);
+		      $response[$row[0]] = $row[1];
+		    }
+		    
+		    $parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');
+		    sort($parameters);
+		    $check=Null;
+		    foreach ($parameters as $param) {
+		      if (array_key_exists($param, $response)) {
+			if ($check) $check = $check . '&';
+			$check = $check . $param . '=' . $response[$param];
+		      }
+		    }
+
+		    $checksignature =
+		      base64_encode(hash_hmac('sha1', utf8_encode($check),
+					      $this->_key, true));
+
+		    if($response['h'] == $checksignature) {
+		      if ($status == 'REPLAYED_OTP') {
+			if (!$wait_for_all) { $this->_response = $str; }
+			$replay=True;
+		      } 
+		      if ($status == 'OK') {
+			if (!$wait_for_all) { $this->_response = $str; }
+			$valid=True;
+		      }
+		    }
+		  } else {
+		    /* Case 3. We check the status directly */
+		    if ($status == 'REPLAYED_OTP') {
+		      if (!$wait_for_all) { $this->_response = $str; }
+		      $replay=True;
+		    } 
+		    if ($status == 'OK') {
+		      if (!$wait_for_all) { $this->_response = $str; }
+		      $valid=True;
+		    }
+		  }
+		}
+		if (!$wait_for_all && ($valid || $replay))
+		  {
+		    /* We have status=OK or status=REPLAYED_OTP, return. */
+		    foreach ($ch as $h) {
+		      curl_multi_remove_handle($mh, $h);
+		      curl_close($h);
+		    }
+		    curl_multi_close($mh);
+		    if ($replay) return PEAR::raiseError('REPLAYED_OTP');
+		    if ($valid) return true;
+		    return PEAR::raiseError($status);
+		  }
+	      } else {
+		/* Some kind of error, but def. not a 200 response */
+		/* No status= in response body */
+		$http_status_code = (int)$cinfo['http_code'];
+		$query = $cinfo['url'];
+		if ($http_status_code == 400 ||
+		    ($http_status_code >= 500 && $http_status_code < 600)) {
+		  /* maybe retry */
+		  if ($retries[$query] < $max_retries) {
+		    $retries[$query]++;  // for this server
+		    $this->_retries++;   // for this validation attempt
+
+		    $newhandle = $this->_make_curl_handle($query, $timeout);
+
+		    curl_multi_add_handle($mh, $newhandle);
+		    $ch[(int)$newhandle] = $newhandle;
+
+		    // Loop back up to curl_multi_exec, even if this
+		    // was the last handle and curl_multi_exec _was_
+		    // no longer active, it's active again now we've
+		    // added a retry.
+		    $active = true;
+		  }
+		}
+	      }
+	      /* Done with this handle */
+	      curl_multi_remove_handle($mh, $info['handle']);
+	      curl_close($info['handle']);
+	      unset ($ch[(int)$info['handle']]);
+	    }
+	  } while ($active);
+
+	  /* Typically this is only reached for wait_for_all=true or
+	   * when the timeout is reached and there is no
+	   * OK/REPLAYED_REQUEST answer (think firewall).
+	   */
+
+	  foreach ($ch as $h) {
+	    curl_multi_remove_handle ($mh, $h);
+	    curl_close ($h);
+	  }
+	  curl_multi_close ($mh);
+	  
+	  if ($replay) return PEAR::raiseError('REPLAYED_OTP');
+	  if ($valid) return true;
+	  return PEAR::raiseError('NO_VALID_ANSWER');
+	}
+}
+?>