Merge pull request #3283 from Icinga/bugfix/icinga-repository-retrievegeneralizedtime-rfc4517-2816

Icinga\Repository::retrieveGeneralizedTime(): comply w/ RFC4517
This commit is contained in:
lippserd 2018-01-18 16:15:21 +01:00 committed by GitHub
commit 00eeab5883
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 212 additions and 14 deletions

View File

@ -11,7 +11,9 @@ use Icinga\Data\Selectable;
use Icinga\Exception\ProgrammingError;
use Icinga\Exception\QueryException;
use Icinga\Exception\StatementException;
use Icinga\Util\ASN1;
use Icinga\Util\StringHelper;
use InvalidArgumentException;
/**
* Abstract base class for concrete repository implementations
@ -929,6 +931,8 @@ abstract class Repository implements Selectable
* @param string|null $value
*
* @return int
*
* @see https://tools.ietf.org/html/rfc4517#section-3.3.13
*/
protected function retrieveGeneralizedTime($value)
{
@ -936,20 +940,10 @@ abstract class Repository implements Selectable
return $value;
}
if (($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false
|| ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false
) {
return $dateTime->getTimeStamp();
} else {
Logger::debug(sprintf(
'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".',
$value,
$this->getName()
));
try {
return ASN1::parseGeneralizedTime($value)->getTimeStamp();
} catch (InvalidArgumentException $e) {
Logger::debug(sprintf('Repository "%s": %s', $this->getName(), $e->getMessage()));
}
}

View File

@ -0,0 +1,102 @@
<?php
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
namespace Icinga\Util;
use DateInterval;
use DateTime;
use InvalidArgumentException;
/**
* Parsers for ASN.1 types
*/
class ASN1
{
/**
* Parse the given value based on the "3.3.13. Generalized Time" syntax as specified by IETF RFC 4517
*
* @param string $value
*
* @return DateTime
*
* @throws InvalidArgumentException
*
* @see https://tools.ietf.org/html/rfc4517#section-3.3.13
*/
public static function parseGeneralizedTime($value)
{
$generalizedTimePattern = <<<EOD
/\A
(?P<YmdH>
[0-9]{4} # century year
(?:0[1-9]|1[0-2]) # month
(?:0[1-9]|[12][0-9]|3[0-1]) # day
(?:[01][0-9]|2[0-3]) # hour
)
(?:
(?P<i>[0-5][0-9]) # minute
(?P<s>[0-5][0-9]|60)? # second or leap-second
)?
(?:[.,](?P<frac>[0-9]+))? # fraction
(?P<tz> # g-time-zone
Z
|
[-+]
(?:[01][0-9]|2[0-3]) # hour
(?:[0-5][0-9])? # minute
)
\z/x
EOD;
$matches = array();
if (preg_match($generalizedTimePattern, $value, $matches)) {
$dateTimeRaw = $matches['YmdH'];
$dateTimeFormat = 'YmdH';
if ($matches['i'] !== '') {
$dateTimeRaw .= $matches['i'];
$dateTimeFormat .= 'i';
if ($matches['s'] !== '') {
$dateTimeRaw .= $matches['s'];
$dateTimeFormat .= 's';
$fractionOfSeconds = 1;
} else {
$fractionOfSeconds = 60;
}
} else {
$fractionOfSeconds = 3600;
}
$dateTimeFormat .= 'O';
if ($matches['tz'] === 'Z') {
$dateTimeRaw .= '+0000';
} else {
$dateTimeRaw .= $matches['tz'];
if (strlen($matches['tz']) === 3) {
$dateTimeRaw .= '00';
}
}
$dateTime = DateTime::createFromFormat($dateTimeFormat, $dateTimeRaw);
if ($dateTime !== false) {
if (isset($matches['frac'])) {
$dateTime->add(new DateInterval(
'PT' . round((float) ('0.' . $matches['frac']) * $fractionOfSeconds) . 'S'
));
}
return $dateTime;
}
}
throw new InvalidArgumentException(sprintf(
'Failed to parse %s based on the ASN.1 standard (GeneralizedTime)',
var_export($value, true)
));
}
}

View File

@ -0,0 +1,102 @@
<?php
/* Icinga Web 2 | (c) 2018 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\Util;
use Icinga\Util\ASN1;
use Icinga\Test\BaseTestCase;
use InvalidArgumentException;
class ASN1Test extends BaseTestCase
{
public function testAllValidGeneralizedTimeCombinations()
{
foreach (array('', '04', '0405', '0460') as $is) {
foreach (array('', ',7890', '.7890') as $frac) {
foreach (array('Z', '-07', '-0742', '+07', '+0742') as $tz) {
$this->assertValidGeneralizedTime("1970010203$is$frac$tz");
}
}
}
}
public function testAllGeneralizedTimeRangeBorders()
{
$this->assertBadGeneralizedTime('1970000203Z');
$this->assertValidGeneralizedTime('1970010203Z');
$this->assertValidGeneralizedTime('1970120203Z');
$this->assertBadGeneralizedTime('1970130203Z');
$this->assertBadGeneralizedTime('1970010003Z');
$this->assertValidGeneralizedTime('1970010103Z');
$this->assertValidGeneralizedTime('1970013103Z');
$this->assertBadGeneralizedTime('1970013203Z');
$this->assertValidGeneralizedTime('1970010200Z');
$this->assertValidGeneralizedTime('1970010223Z');
$this->assertBadGeneralizedTime('1970010224Z');
$this->assertValidGeneralizedTime('197001020300Z');
$this->assertValidGeneralizedTime('197001020359Z');
$this->assertBadGeneralizedTime('197001020360Z');
$this->assertValidGeneralizedTime('19700102030400Z');
$this->assertValidGeneralizedTime('19700102030460Z');
$this->assertBadGeneralizedTime('19700102030461Z');
foreach (array('-', '+') as $sign) {
$this->assertValidGeneralizedTime("1970010203{$sign}00");
$this->assertValidGeneralizedTime("1970010203{$sign}23");
$this->assertBadGeneralizedTime("1970010203{$sign}24");
$this->assertValidGeneralizedTime("1970010203{$sign}0000");
$this->assertValidGeneralizedTime("1970010203{$sign}0059");
$this->assertBadGeneralizedTime("1970010203{$sign}0060");
}
}
public function testGeneralizedTimeFractions()
{
$this->assertGeneralizedTimeDiff('19700102030405.9Z', '19700102030405Z', 1);
$this->assertGeneralizedTimeDiff('197001020304.5Z', '197001020304Z', 30);
$this->assertGeneralizedTimeDiff('1970010203.5Z', '1970010203Z', 1800);
}
protected function assertValidGeneralizedTime($value)
{
try {
$dateTime = ASN1::parseGeneralizedTime($value);
} catch (InvalidArgumentException $e) {
$dateTime = null;
}
$this->assertInstanceOf(
'\DateTime',
$dateTime,
'Failed asserting that ' . var_export($value, true) . ' is a valid date/time'
);
}
protected function assertBadGeneralizedTime($value)
{
$valid = true;
try {
ASN1::parseGeneralizedTime($value);
} catch (InvalidArgumentException $e) {
$valid = false;
}
$this->assertFalse($valid, 'Failed asserting that ' . var_export($value, true) . ' is not a valid date/time');
}
protected function assertGeneralizedTimeDiff($lhs, $rhs, $seconds)
{
$this->assertSame(
ASN1::parseGeneralizedTime($lhs)->getTimestamp() - ASN1::parseGeneralizedTime($rhs)->getTimestamp(),
$seconds,
'Failed asserting that ' . var_export($lhs, true) . ' and ' . var_export($rhs, true)
. " differ by $seconds seconds"
);
}
}