From fbffc42b96520b7b05004949d81475843cd59a93 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Wed, 17 Jan 2018 18:14:18 +0100 Subject: [PATCH 1/3] Icinga\Repository::retrieveGeneralizedTime(): comply w/ RFC4517 refs #2816 --- library/Icinga/Repository/Repository.php | 96 ++++++++++++++++++++---- 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 13000f058..83a073418 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -211,6 +211,36 @@ abstract class Repository implements Selectable */ protected $columnAliasMap; + /** + * "3.3.13. Generalized Time" syntax as specified by IETF RFC 4517 + * + * @var string + * + * @see https://tools.ietf.org/html/rfc4517#section-3.3.13 + */ + protected $generalizedTimePattern = << + [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[0-5][0-9]) # minute + (?P[0-5][0-9]|60)? # second or leap-second + )? + (?:[.,](?P[0-9]+))? # fraction + (?P # g-time-zone + Z + | + [-+] + (?:[01][0-9]|2[0-3]) # hour + (?:[0-5][0-9])? # minute + ) +\z/x +EOD; + /** * Create a new repository object * @@ -929,6 +959,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,21 +968,57 @@ 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() - )); + $matches = array(); + + if (preg_match($this->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) { + $timestamp = $dateTime->getTimeStamp(); + + if (isset($matches['frac'])) { + $timestamp += (int) round((float) ('0.' . $matches['frac']) * $fractionOfSeconds); + } + + return $timestamp; + } } + + Logger::debug(sprintf( + 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".', + $value, + $this->getName() + )); } /** From 5ce491d57a7e20dbd7f41b7d5c933a83fdff8325 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Thu, 18 Jan 2018 15:25:21 +0100 Subject: [PATCH 2/3] Icinga\Repository::retrieveGeneralizedTime(): outsource logic refs #2816 --- library/Icinga/Repository/Repository.php | 86 ++----------------- library/Icinga/Util/ASN1.php | 102 +++++++++++++++++++++++ 2 files changed, 108 insertions(+), 80 deletions(-) create mode 100644 library/Icinga/Util/ASN1.php diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 83a073418..bda93aa2a 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -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 @@ -211,36 +213,6 @@ abstract class Repository implements Selectable */ protected $columnAliasMap; - /** - * "3.3.13. Generalized Time" syntax as specified by IETF RFC 4517 - * - * @var string - * - * @see https://tools.ietf.org/html/rfc4517#section-3.3.13 - */ - protected $generalizedTimePattern = << - [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[0-5][0-9]) # minute - (?P[0-5][0-9]|60)? # second or leap-second - )? - (?:[.,](?P[0-9]+))? # fraction - (?P # g-time-zone - Z - | - [-+] - (?:[01][0-9]|2[0-3]) # hour - (?:[0-5][0-9])? # minute - ) -\z/x -EOD; - /** * Create a new repository object * @@ -968,57 +940,11 @@ EOD; return $value; } - $matches = array(); - - if (preg_match($this->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) { - $timestamp = $dateTime->getTimeStamp(); - - if (isset($matches['frac'])) { - $timestamp += (int) round((float) ('0.' . $matches['frac']) * $fractionOfSeconds); - } - - return $timestamp; - } + try { + return ASN1::parseGeneralizedTime($value)->getTimeStamp(); + } catch (InvalidArgumentException $e) { + Logger::debug(sprintf('Repository "%s": %s', $this->getName(), $e->getMessage())); } - - Logger::debug(sprintf( - 'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".', - $value, - $this->getName() - )); } /** diff --git a/library/Icinga/Util/ASN1.php b/library/Icinga/Util/ASN1.php new file mode 100644 index 000000000..9e00258de --- /dev/null +++ b/library/Icinga/Util/ASN1.php @@ -0,0 +1,102 @@ + + [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[0-5][0-9]) # minute + (?P[0-5][0-9]|60)? # second or leap-second + )? + (?:[.,](?P[0-9]+))? # fraction + (?P # 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) + )); + } +} From 1b2f3cf2800e3e2ad1b69f8f229e2cf166288c97 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Thu, 18 Jan 2018 13:19:08 +0100 Subject: [PATCH 3/3] Test ASN1 refs #2816 --- test/php/library/Icinga/Util/ASN1Test.php | 102 ++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 test/php/library/Icinga/Util/ASN1Test.php diff --git a/test/php/library/Icinga/Util/ASN1Test.php b/test/php/library/Icinga/Util/ASN1Test.php new file mode 100644 index 000000000..8608e75a9 --- /dev/null +++ b/test/php/library/Icinga/Util/ASN1Test.php @@ -0,0 +1,102 @@ +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" + ); + } +}