From db49f04ac61806b26329b90063ee1069cbe58f9f Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Fri, 15 Oct 2021 20:41:03 -0400 Subject: [PATCH 1/3] :art: Fixed docblock formatting --- src/Exception/InvalidBodyLengthException.php | 9 +++++---- src/Exception/InvalidCSeqValueException.php | 9 +++++---- src/Exception/InvalidDuplicateHeaderException.php | 9 +++++---- src/Exception/InvalidHeaderLineException.php | 9 +++++---- src/Exception/InvalidHeaderParameterException.php | 9 +++++---- src/Exception/InvalidHeaderSectionException.php | 9 +++++---- src/Exception/InvalidHeaderValueException.php | 9 +++++---- src/Exception/InvalidMessageStartLineException.php | 9 +++++---- src/Exception/InvalidProtocolVersionException.php | 9 +++++---- src/Exception/InvalidRequestMethodException.php | 8 ++++---- src/Exception/InvalidRequestURIException.php | 8 ++++---- src/Exception/InvalidScalarValueException.php | 8 ++++---- src/Exception/InvalidStatusCodeException.php | 9 +++++---- src/Exception/InvalidURIException.php | 8 ++++---- src/Exception/SIPException.php | 8 ++++---- src/Header/CSeqHeader.php | 8 ++++---- src/Header/CallIdHeader.php | 8 ++++---- src/Header/ContactHeader.php | 8 ++++---- src/Header/ContactValue.php | 10 +++++----- src/Header/FromHeader.php | 8 ++++---- src/Header/Header.php | 8 ++++---- src/Header/MaxForwardsHeader.php | 8 ++++---- src/Header/MultiValueHeader.php | 8 ++++---- src/Header/MultiValueWithParamsHeader.php | 8 ++++---- src/Header/NameAddrHeader.php | 10 +++++----- src/Header/RAckHeader.php | 12 ++++++------ src/Header/ScalarHeader.php | 8 ++++---- src/Header/SingleValueWithParamsHeader.php | 10 +++++----- src/Header/ValueWithParams.php | 10 +++++----- src/Header/ViaHeader.php | 8 ++++---- src/Header/ViaValue.php | 10 +++++----- src/Request.php | 8 ++++---- src/Response.php | 8 ++++---- src/StreamParser.php | 8 ++++---- src/URI.php | 8 ++++---- tests/fixtures/stream/lioneagle-sipparser.txt | 2 +- 36 files changed, 158 insertions(+), 148 deletions(-) diff --git a/src/Exception/InvalidBodyLengthException.php b/src/Exception/InvalidBodyLengthException.php index 4210610..f6ee3bc 100644 --- a/src/Exception/InvalidBodyLengthException.php +++ b/src/Exception/InvalidBodyLengthException.php @@ -1,14 +1,15 @@ Contact value(s) */ diff --git a/src/Header/ContactValue.php b/src/Header/ContactValue.php index db3384f..8c2af4b 100644 --- a/src/Header/ContactValue.php +++ b/src/Header/ContactValue.php @@ -1,7 +1,7 @@ Additional/extension parameters */ + /** @var array Additional/extension parameters */ public array $params = []; } diff --git a/src/Header/FromHeader.php b/src/Header/FromHeader.php index f9bb293..c2add59 100644 --- a/src/Header/FromHeader.php +++ b/src/Header/FromHeader.php @@ -1,7 +1,7 @@ Generic header field values */ diff --git a/src/Header/MaxForwardsHeader.php b/src/Header/MaxForwardsHeader.php index c679ef8..8f83a38 100644 --- a/src/Header/MaxForwardsHeader.php +++ b/src/Header/MaxForwardsHeader.php @@ -1,14 +1,14 @@ Header field values */ diff --git a/src/Header/MultiValueWithParamsHeader.php b/src/Header/MultiValueWithParamsHeader.php index 38f73a3..ad2c18e 100644 --- a/src/Header/MultiValueWithParamsHeader.php +++ b/src/Header/MultiValueWithParamsHeader.php @@ -1,7 +1,7 @@ Header value(s) */ diff --git a/src/Header/NameAddrHeader.php b/src/Header/NameAddrHeader.php index 0ce239d..1b63975 100644 --- a/src/Header/NameAddrHeader.php +++ b/src/Header/NameAddrHeader.php @@ -1,7 +1,7 @@ Additional/extension parameters */ + /** @var array Additional/extension parameters */ public array $params = []; final public function __construct() {} diff --git a/src/Header/RAckHeader.php b/src/Header/RAckHeader.php index 12fd1d7..16a9c12 100644 --- a/src/Header/RAckHeader.php +++ b/src/Header/RAckHeader.php @@ -1,7 +1,7 @@ Parameters */ + /** @var array Parameters */ public array $params = []; final public function __construct() {} diff --git a/src/Header/ValueWithParams.php b/src/Header/ValueWithParams.php index 5b0929d..ca69547 100644 --- a/src/Header/ValueWithParams.php +++ b/src/Header/ValueWithParams.php @@ -1,19 +1,19 @@ Parameters */ + /** @var array Parameters */ public array $params = []; } diff --git a/src/Header/ViaHeader.php b/src/Header/ViaHeader.php index c395bc1..56cac83 100644 --- a/src/Header/ViaHeader.php +++ b/src/Header/ViaHeader.php @@ -1,7 +1,7 @@ Via value(s) */ diff --git a/src/Header/ViaValue.php b/src/Header/ViaValue.php index 9a3e5f9..cf5dbee 100644 --- a/src/Header/ViaValue.php +++ b/src/Header/ViaValue.php @@ -1,14 +1,14 @@ Additional parameters */ + /** @var array Additional parameters */ public array $params = []; } diff --git a/src/Request.php b/src/Request.php index 3b79754..71d5249 100644 --- a/src/Request.php +++ b/src/Request.php @@ -1,7 +1,7 @@ P-Charging-Vector: icid-value=0123456789-12345678-12-12345678-123;orig-ioi=void User-Agent: XXXX VOLTE -Authorization: Digest username="123012345678901@ims.mnc000.mcc123.3gppnetwork.org",realm="ims.mnc000.mcc123.3gppnetwork.org",nonce="",uri="sip:ims.mnc000.mcc123.3gppnetwork.org",response="",integrity-protection=no +Authorization: Digest username="123012345678901@ims.mnc000.mcc123.3gppnetwork.org",realm="ims.mnc000.mcc123.3gppnetwork.org",nonce="",uri="sip:ims.mnc000.mcc123.3gppnetwork.org",response="deadbeef",integrity-protection=no Feature-Caps: *;+g.3gpp.atcf="";+g.3gpp.atcf-mgmt-uri="";+g.3gpp.atcf-path="";+g.3gpp.srvcc;+3gpp.mid-call;+g.3gpp.srvcc-alerting;+g.3gpp.ps2cs-srcc-orig-pre-alerting P-Visited-Network-ID: 12345678901.xx.ims.mnc000.mcc123.3gppnetwork.org Allow: INVITE,CANCEL,BYE,ACK,REGISTER,OPTIONS,REFER,SUBSCRIBE,NOTIFY,MESSAGE,INFO,PRACK,UPDATE From d096ddc3af2c799017d06e08d4c61393dbe670f6 Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Mon, 18 Oct 2021 10:21:57 -0400 Subject: [PATCH 2/3] :zap: Authenticate/Authorization Header Overhaul --- src/Auth/Digest/AbstractParams.php | 281 ++++++++++++++++++++++ src/Auth/Digest/ChallengeParams.php | 22 ++ src/Auth/Digest/ResponseParams.php | 119 ++++++++++ src/Auth/OpaqueParams.php | 40 ++++ src/Auth/ParamsInterface.php | 28 +++ src/Exception/AuthException.php | 15 ++ src/Header/AbstractAuthHeader.php | 93 ++++++++ src/Header/AuthHeader.php | 273 ---------------------- src/Header/AuthValue.php | 53 +---- src/Header/AuthenticateHeader.php | 14 ++ src/Header/AuthorizationHeader.php | 14 ++ src/Message.php | 39 ++-- tests/Auth/Digest/ResponseParamsTest.php | 252 ++++++++++++++++++++ tests/Header/AuthHeaderTest.php | 283 ----------------------- tests/Header/AuthenticateHeaderTest.php | 196 ++++++++++++++++ tests/Header/AuthorizationHeaderTest.php | 71 ++++++ tests/MessageTest.php | 77 +++--- tests/RFC4475/S33070Test.php | 4 +- 18 files changed, 1222 insertions(+), 652 deletions(-) create mode 100644 src/Auth/Digest/AbstractParams.php create mode 100644 src/Auth/Digest/ChallengeParams.php create mode 100644 src/Auth/Digest/ResponseParams.php create mode 100644 src/Auth/OpaqueParams.php create mode 100644 src/Auth/ParamsInterface.php create mode 100644 src/Exception/AuthException.php create mode 100644 src/Header/AbstractAuthHeader.php delete mode 100644 src/Header/AuthHeader.php create mode 100644 src/Header/AuthenticateHeader.php create mode 100644 src/Header/AuthorizationHeader.php create mode 100644 tests/Auth/Digest/ResponseParamsTest.php delete mode 100644 tests/Header/AuthHeaderTest.php create mode 100644 tests/Header/AuthenticateHeaderTest.php create mode 100644 tests/Header/AuthorizationHeaderTest.php diff --git a/src/Auth/Digest/AbstractParams.php b/src/Auth/Digest/AbstractParams.php new file mode 100644 index 0000000..022b90b --- /dev/null +++ b/src/Auth/Digest/AbstractParams.php @@ -0,0 +1,281 @@ + Additional parameters */ + public array $extra = []; + + final public function __construct() {} + + /** + * Parses digest parameters out of a string input + * + * @param string $input Unparsed digest parameters + * @throws InvalidHeaderLineException + * @throws InvalidHeaderParameterException + * @return ParamsInterface Parsed Challenge or Response parameters + */ + public static function parse(string $input): ParamsInterface + { + $orig = $input; + $params = new static; + $challenge = static::class === ChallengeParams::class; + + while (strlen($input)) { + $pos = strpos($input, '='); + + if ($pos === false) { + throw new InvalidHeaderLineException('Invalid Auth header, valueless parameter', Response::BAD_REQUEST); + } + + $pk = rtrim(substr($input, 0, $pos)); + $pv = ''; + $input = ltrim(substr($input, $pos + 1)); + + if (!isset($input[0])) { + throw new InvalidHeaderLineException('Invalid Auth header, valueless parameter', Response::BAD_REQUEST); + } + + if ($input[0] === '"') { + $offset = 1; + $escQuotes = false; + + while (true) { + $pos = strpos($input, '"', $offset); + + if ($pos === false) { + throw new InvalidHeaderLineException('Invalid Auth header, unmatched parameter value enclosing', Response::BAD_REQUEST); + } + + if ($input[$pos - 1] !== '\\') { + break; + } + + $escQuotes = true; + $offset = $pos + 1; + } + + $pv = substr($input, 1, $pos - 1); + + if ($escQuotes) { + $pv = str_replace('\"', '"', $pv); + } + + $input = ltrim(substr($input, $pos + 1)); + + if (isset($input[0])) { + if ($input[0] !== ',') { + throw new InvalidHeaderLineException('Invalid Auth header, invalid parameter value enclosing', Response::BAD_REQUEST); + } + + $input = ltrim(substr($input, 1)); + } + } else { + $pos = strpos($input, ','); + + if ($pos !== false) { + $pv = rtrim(substr($input, 0, $pos)); + $input = ltrim(substr($input, $pos + 1)); + } else { + $pv = rtrim($input); + $input = ''; + } + } + + switch ($pk) { + case 'realm': + $params->realm = $pv; + break; + + case 'algorithm': + $params->algorithm = $pv; + break; + + case 'nonce': + $params->nonce = $pv; + break; + + case 'opaque': + $params->opaque = $pv; + break; + + default: + if ($challenge) { + /** @var ChallengeParams $params */ + switch ($pk) { + case 'domain': + $params->domain = $pv; + break 2; + + case 'stale': + $pv = strtolower($pv); + + if ($pv === 'true') { + $params->stale = true; + } else if ($pv === 'false') { + $params->stale = false; + } else { + throw new InvalidHeaderParameterException('Invalid Auth header, non-boolean stale parameter', Response::BAD_REQUEST); + } + break 2; + + case 'qop': + $params->qop = explode(',', $pv); + break 2; + } + } else { + /** @var ResponseParams $params */ + switch ($pk) { + case 'username': + $params->username = $pv; + break 2; + + case 'uri': + $params->uri = $pv; + break 2; + + case 'cnonce': + $params->cnonce = $pv; + break 2; + + case 'nc': + if (!ctype_xdigit($pv)) { + throw new InvalidHeaderParameterException('Invalid Auth header, non-hexadecimal nc parameter', Response::BAD_REQUEST); + } + + $params->nc = $pv; + break 2; + + case 'response': + if (!ctype_xdigit($pv)) { + throw new InvalidHeaderParameterException('Invalid Auth header, non-hexadecimal response parameter', Response::BAD_REQUEST); + } + + $params->response = $pv; + break 2; + + case 'qop': + $params->qop = $pv; + break 2; + } + } + + $params->extra[$pk] = $pv; + break; + } + } + + return $params; + } + + /** + * Renders digest authentication scheme parameters as string + * + * @return string Digest authentication parameters + */ + public function render(): string + { + $params = []; + + if (isset($this->realm)) { + $params[] = "realm=\"{$this->realm}\""; + } + + if (isset($this->algorithm)) { + $params[] = "algorithm={$this->algorithm}"; + } + + if (isset($this->nonce)) { + $params[] = "nonce=\"{$this->nonce}\""; + } + + if (isset($this->opaque)) { + $params[] = "opaque=\"{$this->opaque}\""; + } + + if (static::class === ChallengeParams::class) { + if (isset($this->domain)) { + $params[] = "domain=\"{$this->domain}\""; + } + + if (isset($this->stale)) { + $params[] = 'stale=' . ($this->stale ? 'TRUE' : 'FALSE'); + } + + if (isset($this->qop) && count($this->qop)) { + $params[] = 'qop="' . implode(',', $this->qop) . '"'; + } + } else { + if (isset($this->username)) { + $params[] = "username=\"{$this->username}\""; + } + + if (isset($this->uri)) { + $params[] = "uri=\"{$this->uri}\""; + } + + if (isset($this->response)) { + $params[] = "response=\"{$this->response}\""; + } + + if (isset($this->cnonce)) { + $params[] = "cnonce=\"{$this->cnonce}\""; + } + + if (isset($this->qop)) { + $params[] = "qop={$this->qop}"; + } + + if (isset($this->nc)) { + $params[] = "nc={$this->nc}"; + } + } + + foreach ($this->extra as $pk => $pv) { + $params[] = "{$pk}={$pv}"; + } + + return implode(',', $params); + } +} diff --git a/src/Auth/Digest/ChallengeParams.php b/src/Auth/Digest/ChallengeParams.php new file mode 100644 index 0000000..1f69326 --- /dev/null +++ b/src/Auth/Digest/ChallengeParams.php @@ -0,0 +1,22 @@ + Quality of protection options */ + public array $qop = []; +} diff --git a/src/Auth/Digest/ResponseParams.php b/src/Auth/Digest/ResponseParams.php new file mode 100644 index 0000000..8354451 --- /dev/null +++ b/src/Auth/Digest/ResponseParams.php @@ -0,0 +1,119 @@ + Alternative algorithm map */ + public const ALGORITHM_MAP = [ + 'SHA-256' => 'sha256', + 'SHA-512-256' => 'sha512/256', + ]; + + /** @var string Authentication user name */ + public string $username; + + /** @var string Effective request URI */ + public string $uri; + + /** @var string Client's number once */ + public string $cnonce; + + /** @var string Number once count */ + public string $nc; + + /** @var string Response hash */ + public string $response; + + /** @var string Quality of protection */ + public string $qop; + + /** + * Computes the response hash for the given parameters; missing key fields will throw exceptions + * + * @param string $method SIP request method + * @param ?string $secret Authentication secret + * @param ?string $hash Precalculated A1 hash + * @param ?string $body Request's body for auth-int Quality of Protection + * @throws AuthException + * @return string + */ + public function hash(string $method, ?string $secret, ?string $hash = null, ?string $body = null): string + { + if (!isset($this->algorithm)) { + $algo = self::DEFAULT_ALGORITHM; + } else { + $algo = $this->algorithm; + } + + $algo = strtoupper($algo); + $sPos = strpos($algo, self::ALGORITHM_SESS_SUFFIX); + $sess = $sPos !== false; + + if ($sess) { + $algo = substr($algo, 0, $sPos); + } + + if (isset($hash)) { + $a1 = $hash; + } else { + $a1 = $this->hashString($algo, "{$this->username}:{$this->realm}:{$secret}"); + } + + if ($sess) { + $a1 = $this->hashString($algo, "{$a1}:{$this->nonce}:{$this->cnonce}"); + } + + if (!isset($this->qop) || ($this->qop === self::QOP_AUTH)) { + $a2 = $this->hashString($algo, "{$method}:{$this->uri}"); + } else if ($this->qop === self::QOP_AUTH_INT) { + if (!isset($body)) { + $body = ''; + } + + $bodyHash = $this->hashString($algo, $body); + $a2 = $this->hashString($algo, "{$method}:{$this->uri}:{$bodyHash}"); + } else { + throw new AuthException('Unknown digest quality-of-protection: ' . $this->qop); + } + + if (!isset($this->qop)) { + return $this->hashString($algo, "{$a1}:{$this->nonce}:{$a2}"); + } + + return $this->hashString($algo, "{$a1}:{$this->nonce}:{$this->nc}:{$this->cnonce}:{$this->qop}:{$a2}"); + } + + /** + * Calculates the hash of a given string for a specific algorithm + * + * @param string $algo Algorithm + * @param string $input String to hash + * @throws AuthException + * @return string + */ + private function hashString(string $algo, string $input): string + { + if ($algo === self::DEFAULT_ALGORITHM) { + return md5($input); + } + + if (!extension_loaded('hash') || !isset(self::ALGORITHM_MAP[$algo])) { + throw new AuthException('Unsupported digest algorithm: ' . $algo); + } + + return hash(self::ALGORITHM_MAP[$algo], $input); + } +} diff --git a/src/Auth/OpaqueParams.php b/src/Auth/OpaqueParams.php new file mode 100644 index 0000000..0e413cb --- /dev/null +++ b/src/Auth/OpaqueParams.php @@ -0,0 +1,40 @@ +verbatim = $input; + + return $params; + } + + /** + * Renders opaque authentication scheme parameters as string + * + * @return string Opaque authentication parameters + */ + public function render(): string + { + return $this->verbatim; + } +} diff --git a/src/Auth/ParamsInterface.php b/src/Auth/ParamsInterface.php new file mode 100644 index 0000000..53f9eb4 --- /dev/null +++ b/src/Auth/ParamsInterface.php @@ -0,0 +1,28 @@ + Authentication/Authorization value(s) */ + public array $values = []; + + final public function __construct() {} + + /** + * Authentication/Authorization field value parser + * + * @param list $hbody Header body + * @throws InvalidHeaderLineException + * @throws InvalidHeaderParameterException + * @return AbstractAuthHeader + */ + public static function parse(array $hbody): AbstractAuthHeader + { + $ret = new static; + $challenge = static::class === AuthenticateHeader::class; + + foreach ($hbody as $hline) { + $hparts = explode(' ', trim($hline), 2); + + if (count($hparts) !== 2) { + throw new InvalidHeaderLineException('Invalid Auth header, missing scheme', Response::BAD_REQUEST); + } + + $val = new AuthValue; + $val->scheme = strtolower($hparts[0]); + + $params = ltrim($hparts[1]); + + switch ($val->scheme) { + case DigestChallengeParams::SCHEME_NAME: + if ($challenge) { + $val->params = DigestChallengeParams::parse($params); + } else { + $val->params = DigestResponseParams::parse($params); + } + break; + + default: + $val->params = OpaqueParams::parse($params); + break; + } + + $ret->values[] = $val; + } + + return $ret; + } + + /** + * Authentication/Authorization header field value renderer + * + * @param string $hname Header field name + * @throws InvalidHeaderValueException + * @return string + */ + public function render(string $hname): string + { + $ret = ''; + + foreach ($this->values as $key => $value) { + if (!isset($value->scheme, $value->params)) { + throw new InvalidHeaderValueException('Malformed auth header, missing scheme and/or parameters'); + } + + $ret .= "{$hname}: {$value->scheme} " . $value->params->render() . "\r\n"; + } + + return $ret; + } +} diff --git a/src/Header/AuthHeader.php b/src/Header/AuthHeader.php deleted file mode 100644 index 9c6814e..0000000 --- a/src/Header/AuthHeader.php +++ /dev/null @@ -1,273 +0,0 @@ - Authentication/Authorization value(s) */ - public array $values = []; - - final public function __construct() {} - - /** - * Authentication/Authorization field value parser - * - * @param list $hbody Header body - * @throws InvalidHeaderLineException - * @throws InvalidHeaderParameterException - * @return AuthHeader - */ - public static function parse(array $hbody): AuthHeader - { - $ret = new static; - - foreach ($hbody as $hline) { - $hparts = explode(' ', trim($hline), 2); - - if (count($hparts) !== 2) { - throw new InvalidHeaderLineException('Invalid Auth header, missing scheme', Response::BAD_REQUEST); - } - - $val = new AuthValue; - $val->scheme = $hparts[0]; - - $params = ltrim($hparts[1]); - - if (strtolower($val->scheme) === self::DIGEST_SCHEME) { - while (strlen($params)) { - $pos = strpos($params, '='); - - if ($pos === false) { - throw new InvalidHeaderLineException('Invalid Auth header, valueless parameter', Response::BAD_REQUEST); - } - - $pk = rtrim(substr($params, 0, $pos)); - $pv = ''; - $params = ltrim(substr($params, $pos + 1)); - - if (!isset($params[0])) { - throw new InvalidHeaderLineException('Invalid Auth header, valueless parameter', Response::BAD_REQUEST); - } - - if ($params[0] === '"') { - $offset = 1; - $escQuotes = false; - - while (true) { - $pos = strpos($params, '"', $offset); - - if ($pos === false) { - throw new InvalidHeaderLineException('Invalid Auth header, unmatched parameter value enclosing', Response::BAD_REQUEST); - } - - if ($params[$pos - 1] !== '\\') { - break; - } - - $escQuotes = true; - $offset = $pos + 1; - } - - $pv = substr($params, 1, $pos - 1); - - if ($escQuotes) { - $pv = str_replace('\"', '"', $pv); - } - - $params = ltrim(substr($params, $pos + 1)); - - if (isset($params[0])) { - if ($params[0] !== ',') { - throw new InvalidHeaderLineException('Invalid Auth header, invalid parameter value enclosing', Response::BAD_REQUEST); - } - - $params = ltrim(substr($params, 1)); - } - } else { - $pos = strpos($params, ','); - - if ($pos !== false) { - $pv = rtrim(substr($params, 0, $pos)); - $params = ltrim(substr($params, $pos + 1)); - } else { - $pv = rtrim($params); - $params = ''; - } - } - - switch ($pk) { - case 'username': - $val->username = $pv; - break; - - case 'realm': - $val->realm = $pv; - break; - - case 'domain': - $val->domain = $pv; - break; - - case 'nonce': - $val->nonce = $pv; - break; - - case 'uri': - $val->uri = $pv; - break; - - case 'response': - $val->response = $pv; - break; - - case 'stale': - $pv = strtolower($pv); - - if ($pv === 'true') { - $val->stale = true; - } else if ($pv === 'false') { - $val->stale = false; - } else { - throw new InvalidHeaderParameterException('Invalid Auth header, non-boolean stale parameter', Response::BAD_REQUEST); - } - break; - - case 'algorithm': - $val->algorithm = $pv; - break; - - case 'cnonce': - $val->cnonce = $pv; - break; - - case 'qop': - $val->qop = $pv; - break; - - case 'nc': - if (!ctype_xdigit($pv)) { - throw new InvalidHeaderParameterException('Invalid Auth header, non-hexadecimal nc parameter', Response::BAD_REQUEST); - } - - $val->nc = $pv; - break; - - case 'opaque': - $val->opaque = $pv; - break; - - default: - $val->params[$pk] = $pv; - break; - } - } - } else { - $val->credentials = $params; - } - - $ret->values[] = $val; - } - - return $ret; - } - - /** - * Authentication/Authorization header field value renderer - * - * @param string $hname Header field name - * @throws InvalidHeaderValueException - * @return string - */ - public function render(string $hname): string - { - $ret = ''; - - foreach ($this->values as $key => $value) { - if (!isset($value->scheme)) { - throw new InvalidHeaderValueException('Malformed auth header, missing scheme'); - } - - $params = []; - - if (strtolower($value->scheme) === self::DIGEST_SCHEME) { - if (isset($value->username)) { - $params[] = "username=\"{$value->username}\""; - } - - if (isset($value->realm)) { - $params[] = "realm=\"{$value->realm}\""; - } - - if (isset($value->domain)) { - $params[] = "domain=\"{$value->domain}\""; - } - - if (isset($value->nonce)) { - $params[] = "nonce=\"{$value->nonce}\""; - } - - if (isset($value->uri)) { - $params[] = "uri=\"{$value->uri}\""; - } - - if (isset($value->response)) { - $params[] = "response=\"{$value->response}\""; - } - - if (isset($value->stale)) { - $params[] = 'stale=' . ($value->stale ? 'TRUE' : 'FALSE'); - } - - if (isset($value->algorithm)) { - $params[] = "algorithm={$value->algorithm}"; - } - - if (isset($value->cnonce)) { - $params[] = "cnonce=\"{$value->cnonce}\""; - } - - if (isset($value->qop)) { - $params[] = "qop=\"{$value->qop}\""; - } - - if (isset($value->nc)) { - $params[] = "nc={$value->nc}"; - } - - if (isset($value->opaque)) { - $params[] = "opaque=\"{$value->opaque}\""; - } - } else { - if (!isset($value->credentials)) { - throw new InvalidHeaderValueException('Malformed auth header, missing credentials'); - } - - $params[] = $value->credentials; - } - - foreach ($value->params as $pk => $pv) { - $params[] = "{$pk}={$pv}"; - } - - $paramStr = implode(',', $params); - - $ret .= "{$hname}: {$value->scheme} {$paramStr}\r\n"; - } - - return $ret; - } -} diff --git a/src/Header/AuthValue.php b/src/Header/AuthValue.php index a6de1ac..76a8ad5 100644 --- a/src/Header/AuthValue.php +++ b/src/Header/AuthValue.php @@ -1,58 +1,21 @@ Additional parameters */ - public array $params = []; + /** @var ParamsInterface Scheme-specific parameters */ + public ParamsInterface $params; } diff --git a/src/Header/AuthenticateHeader.php b/src/Header/AuthenticateHeader.php new file mode 100644 index 0000000..6014746 --- /dev/null +++ b/src/Header/AuthenticateHeader.php @@ -0,0 +1,14 @@ + Additional/extension headers */ + /** @var array Additional/extension headers */ public array $extraHeaders = []; /** @@ -203,7 +204,7 @@ public static function parse(string $text, bool $ignoreBody = false): Message } if (!isset($boundary)) { - throw new InvalidHeaderSectionException('Malformed Message, missing CRLF separator after header section: ' . json_encode($lines), Response::BAD_REQUEST); + throw new InvalidHeaderSectionException('Malformed Message, missing CRLF separator after header section', Response::BAD_REQUEST); } foreach ($headers as $hname => $hbody) { @@ -401,7 +402,9 @@ public static function parse(string $text, bool $ignoreBody = false): Message /* https://tools.ietf.org/html/rfc3261#section-20.7 */ case 'authorization': - $msg->authorization = AuthHeader::parse($hbody); + /** @var AuthorizationHeader $aux */ + $aux = AuthorizationHeader::parse($hbody); + $msg->authorization = $aux; continue 2; @@ -419,13 +422,17 @@ public static function parse(string $text, bool $ignoreBody = false): Message /* https://tools.ietf.org/html/rfc3261#section-20.27 */ case 'proxy-authenticate': - $msg->proxyAuthenticate = AuthHeader::parse($hbody); + /** @var AuthenticateHeader $aux */ + $aux = AuthenticateHeader::parse($hbody); + $msg->proxyAuthenticate = $aux; continue 2; /* https://tools.ietf.org/html/rfc3261#section-20.28 */ case 'proxy-authorization': - $msg->proxyAuthorization = AuthHeader::parse($hbody); + /** @var AuthorizationHeader $aux */ + $aux = AuthorizationHeader::parse($hbody); + $msg->proxyAuthorization = $aux; continue 2; @@ -479,7 +486,9 @@ public static function parse(string $text, bool $ignoreBody = false): Message /* https://tools.ietf.org/html/rfc3261#section-20.44 */ case 'www-authenticate': - $msg->wwwAuthenticate = AuthHeader::parse($hbody); + /** @var AuthenticateHeader $aux */ + $aux = AuthenticateHeader::parse($hbody); + $msg->wwwAuthenticate = $aux; continue 2; diff --git a/tests/Auth/Digest/ResponseParamsTest.php b/tests/Auth/Digest/ResponseParamsTest.php new file mode 100644 index 0000000..a3cd1d8 --- /dev/null +++ b/tests/Auth/Digest/ResponseParamsTest.php @@ -0,0 +1,252 @@ +username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->uri = "sip:bob@biloxi.com"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + $params->response = $params->hash('INVITE', 'zanzibar'); + + $this->assertEquals('bf57e4e0d0bffc0fbaedce64d59add5e', $params->response); + } + + /** + * https://datatracker.ietf.org/doc/html/draft-smith-sipping-auth-examples-01#section-3.2 + */ + public function testAuthAlgorithmUnspecified() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->qop = "auth"; + $params->uri = "sip:bob@biloxi.com"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + $params->response = $params->hash('INVITE', 'zanzibar'); + + $this->assertEquals('89eb0059246c02b2f6ee02c7961d5ea3', $params->response); + } + + /** + * https://datatracker.ietf.org/doc/html/draft-smith-sipping-auth-examples-01#section-3.3 + */ + public function testAuthMD5() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->uri = "sip:bob@biloxi.com"; + $params->qop = "auth"; + $params->algorithm = "MD5"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + $params->response = $params->hash('INVITE', 'zanzibar'); + + $this->assertEquals('89eb0059246c02b2f6ee02c7961d5ea3', $params->response); + } + + /** + * https://datatracker.ietf.org/doc/html/draft-smith-sipping-auth-examples-01#section-3.4 + */ + public function testAuthMD5Sess() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->uri = "sip:bob@biloxi.com"; + $params->qop = "auth"; + $params->algorithm = "MD5-sess"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + $params->response = $params->hash('INVITE', 'zanzibar'); + + $this->assertEquals('e4e4ea61d186d07a92c9e1f6919902e9', $params->response); + } + + /** + * https://datatracker.ietf.org/doc/html/draft-smith-sipping-auth-examples-01#section-3.5 + */ + public function testAuthIntMD5() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->uri = "sip:bob@biloxi.com"; + $params->qop = "auth-int"; + $params->algorithm = "MD5"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + + $body = + 'v=0' . "\r\n" . + 'o=bob 2890844526 2890844526 IN IP4 media.biloxi.com' . "\r\n" . + 's=-' . "\r\n" . + 'c=IN IP4 media.biloxi.com' . "\r\n" . + 't=0 0' . "\r\n" . + 'm=audio 49170 RTP/AVP 0' . "\r\n" . + 'a=rtpmap:0 PCMU/8000' . "\r\n" . + 'm=video 51372 RTP/AVP 31' . "\r\n" . + 'a=rtpmap:31 H261/90000' . "\r\n" . + 'm=video 53000 RTP/AVP 32' . "\r\n" . + 'a=rtpmap:32 MPV/90000' . "\r\n"; + + $params->response = $params->hash('INVITE', 'zanzibar', null, $body); + + $this->assertEquals('bdbeebb2da6adb6bca02599c2239e192', $params->response); + } + + /** + * https://datatracker.ietf.org/doc/html/draft-smith-sipping-auth-examples-01#section-3.6 + */ + public function testAuthIntMD5Sess() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->uri = "sip:bob@biloxi.com"; + $params->qop = "auth-int"; + $params->algorithm = "MD5-sess"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + + $body = + 'v=0' . "\r\n" . + 'o=bob 2890844526 2890844526 IN IP4 media.biloxi.com' . "\r\n" . + 's=-' . "\r\n" . + 'c=IN IP4 media.biloxi.com' . "\r\n" . + 't=0 0' . "\r\n" . + 'm=audio 49170 RTP/AVP 0' . "\r\n" . + 'a=rtpmap:0 PCMU/8000' . "\r\n" . + 'm=video 51372 RTP/AVP 31' . "\r\n" . + 'a=rtpmap:31 H261/90000' . "\r\n" . + 'm=video 53000 RTP/AVP 32' . "\r\n" . + 'a=rtpmap:32 MPV/90000' . "\r\n"; + + $params->response = $params->hash('INVITE', 'zanzibar', null, $body); + + $this->assertEquals('91984da2d8663716e91554859c22ca70', $params->response); + } + + /** + * https://datatracker.ietf.org/doc/html/rfc7616#section-3.9.1 + */ + public function testSHA256MD5() + { + $params = new ResponseParams; + $params->username = "Mufasa"; + $params->realm = "http-auth@example.org"; + $params->nonce = "7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v"; + $params->uri = "/dir/index.html"; + $params->qop = "auth"; + $params->algorithm = "MD5"; + $params->nc = "00000001"; + $params->cnonce = "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"; + $params->opaque = "FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"; + $params->response = $params->hash('GET', 'Circle of Life'); + + $this->assertEquals('8ca523f5e9506fed4657c9700eebdbec', $params->response); + + $params->algorithm = "SHA-256"; + $params->response = $params->hash('GET', 'Circle of Life'); + + $this->assertEquals('753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1', $params->response); + } + + public function testA1Hash() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->qop = "auth"; + $params->uri = "sip:bob@biloxi.com"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + $params->response = $params->hash('INVITE', null, '12af60467a33e8518da5c68bbff12b11'); + + $this->assertEquals('89eb0059246c02b2f6ee02c7961d5ea3', $params->response); + } + + public function testAuthIntBodyNotPresent() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->uri = "sip:bob@biloxi.com"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + $params->qop = "auth-int"; + $params->response = $params->hash('INVITE', 'zanzibar'); + + $this->assertEquals('2d6fc6e788367208f746582b18a69618', $params->response); + } + + public function testFailUnknownQoP() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->uri = "sip:bob@biloxi.com"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + $params->qop = "auth-none"; + + $this->expectException(AuthException::class); + $params->response = $params->hash('INVITE', 'zanzibar'); + } + + public function testFailUnknownAlgo() + { + $params = new ResponseParams; + $params->username = "bob"; + $params->realm = "biloxi.com"; + $params->nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093"; + $params->uri = "sip:bob@biloxi.com"; + $params->nc = "00000001"; + $params->cnonce = "0a4f113b"; + $params->opaque = "5ccc069c403ebaf9f0171e9517f40e41"; + $params->algorithm = "BFRX*/8"; + + $this->expectException(AuthException::class); + $params->response = $params->hash('INVITE', 'zanzibar'); + } +} diff --git a/tests/Header/AuthHeaderTest.php b/tests/Header/AuthHeaderTest.php deleted file mode 100644 index b67495d..0000000 --- a/tests/Header/AuthHeaderTest.php +++ /dev/null @@ -1,283 +0,0 @@ -assertNotNull($auth); - $this->assertInstanceOf(AuthHeader::class, $auth); - $this->assertCount(2, $auth->values); - $this->assertEquals('Digest', $auth->values[0]->scheme); - $this->assertEquals('sip.domain.net', $auth->values[0]->realm); - $this->assertEquals('sip:sip.domain.net', $auth->values[0]->domain); - $this->assertEquals('auth', $auth->values[0]->qop); - $this->assertEquals('7900f98e-3d80-4504-adbc-a61e5e040207', $auth->values[0]->nonce); - $this->assertEquals(false, $auth->values[0]->stale); - $this->assertEquals('MD5', $auth->values[0]->algorithm); - $this->assertEquals('Basic', $auth->values[1]->scheme); - $this->assertEquals('YWxhZGRpbjpvcGVuc2VzYW1l', $auth->values[1]->credentials); - } - - public function testShouldParseVariousSpacingFormatting() - { - $scheme = 'Digest'; - $realm = 'sip.domain.net'; - $qop = 'auth'; - $cnonce = '7900f98e-3d80-4504-adbc-a61e5e040207'; - $stale = false; - $algorithm = 'MD5'; - - $auth = AuthHeader::parse([ - 'Digest realm="sip.domain.net",qop="auth",cnonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5', - ' Digest realm="sip.domain.net",qop="auth",cnonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5 ', - ' Digest realm="sip.domain.net",qop="auth",cnonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5', - ' Digest realm="sip.domain.net",qop="auth",cnonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5 ', - ' Digest realm ="sip.domain.net",qop="auth",cnonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5 ', - ' Digest realm= "sip.domain.net",qop="auth",cnonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5 ', - ' Digest realm="sip.domain.net",qop="auth",cnonce="7900f98e-3d80-4504-adbc-a61e5e040207", stale = FALSE ,algorithm= MD5 ', - 'Digest realm="sip.domain.net",qop="auth",cnonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE, algorithm=MD5', - ' Digest realm=sip.domain.net,qop=auth,cnonce=7900f98e-3d80-4504-adbc-a61e5e040207,stale=FALSE,algorithm=MD5 ', - ]); - - $count = count($auth->values); - - for ($i = 0; $i < $count; $i++) { - $this->assertEquals($scheme, $auth->values[$i]->scheme); - $this->assertEquals($realm, $auth->values[$i]->realm); - $this->assertEquals($qop, $auth->values[$i]->qop); - $this->assertEquals($cnonce, $auth->values[$i]->cnonce); - $this->assertEquals($stale, $auth->values[$i]->stale); - $this->assertEquals($algorithm, $auth->values[$i]->algorithm); - } - - $scheme = 'Basic'; - $credentials = 'YWxhZGRpbjpvcGVuc2VzYW1l'; - - $auth = AuthHeader::parse([ - 'Basic YWxhZGRpbjpvcGVuc2VzYW1l', - 'Basic YWxhZGRpbjpvcGVuc2VzYW1l ', - ' Basic YWxhZGRpbjpvcGVuc2VzYW1l', - ' Basic YWxhZGRpbjpvcGVuc2VzYW1l ', - ]); - - $count = count($auth->values); - - for ($i = 0; $i < $count; $i++) { - $this->assertEquals($scheme, $auth->values[$i]->scheme); - $this->assertEquals($credentials, $auth->values[$i]->credentials); - } - } - - public function testShouldParseEnclosedCharacters() - { - $auth = AuthHeader::parse([ - 'Digest qop="auth,auth-int"', - 'Digest response="QWxhZGRpbjpvcGVuIHNlc2FtZQ=="', - 'Digest response="enclosed\"quotes\"test"', - ]); - - $this->assertEquals('auth,auth-int', $auth->values[0]->qop); - $this->assertEquals('QWxhZGRpbjpvcGVuIHNlc2FtZQ==', $auth->values[1]->response); - $this->assertEquals('enclosed"quotes"test', $auth->values[2]->response); - } - - public function testShouldParseVariousNcValueFormatting() - { - $nc = hexdec('00000042'); - - $auth = AuthHeader::parse([ - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5,nc=42', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5,nc=00000042', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5,nc= "42"', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5,nc= "00000042"', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5,nc= 00000042 ', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5,nc= 42 ', - ]); - - $count = count($auth->values); - - for ($i = 0; $i < $count; $i++) { - $this->assertEquals($nc, hexdec($auth->values[$i]->nc)); - } - } - - public function testShouldParseVariousStaleValueFormatting() - { - $stale = false; - - $auth = AuthHeader::parse([ - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=False,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=false,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FaLsE,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale= false,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale= false ,algorithm=MD5', - ]); - - $count = count($auth->values); - - for ($i = 0; $i < $count; $i++) { - $this->assertEquals($stale, $auth->values[$i]->stale); - } - - $stale = true; - - $auth = AuthHeader::parse([ - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=TRUE,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=True,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=true,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=TrUe,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale= true,algorithm=MD5', - 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale= true ,algorithm=MD5', - ]); - - $count = count($auth->values); - - for ($i = 0; $i < $count; $i++) { - $this->assertEquals($stale, $auth->values[$i]->stale); - } - } - - public function testShouldRenderWellFormedValues() - { - $digest = new AuthHeader; - $digest->values[0] = new AuthValue; - $digest->values[0]->scheme = 'Digest'; - $digest->values[0]->username = 'bob'; - $digest->values[0]->realm = 'sip.domain.net'; - $digest->values[0]->domain = 'sip:sip.domain.net'; - $digest->values[0]->nonce = 'a61e5e040207'; - $digest->values[0]->uri = 'sip:sip.domain.net'; - $digest->values[0]->response = 'KJHAFgHFIUAG'; - $digest->values[0]->stale = true; - $digest->values[0]->algorithm = 'MD5'; - $digest->values[0]->cnonce = '7900f98e'; - $digest->values[0]->qop = 'auth-int'; - $digest->values[0]->nc = '42'; - $digest->values[0]->opaque = 'misc'; - - $rendered = $digest->render('Authorization'); - - $this->assertNotNull($rendered); - $this->assertIsString($rendered); - $this->assertEquals( - 'Authorization: Digest username="bob",realm="sip.domain.net",domain="sip:sip.domain.net",nonce="a61e5e040207",uri="sip:sip.domain.net",response="KJHAFgHFIUAG",stale=TRUE,algorithm=MD5,cnonce="7900f98e",qop="auth-int",nc=42,opaque="misc"' . "\r\n", - $rendered - ); - - $basic = new AuthHeader; - $basic->values[0] = new AuthValue; - $basic->values[0]->scheme = 'Basic'; - $basic->values[0]->credentials = 'MiScCreds'; - - $rendered = $basic->render('Authorization'); - - $this->assertNotNull($rendered); - $this->assertIsString($rendered); - $this->assertEquals( - 'Authorization: Basic MiScCreds' . "\r\n", - $rendered - ); - - $combined = new AuthHeader; - $combined->values = [$digest->values[0], $basic->values[0]]; - - $rendered = $combined->render('Authorization'); - - $this->assertNotNull($rendered); - $this->assertIsString($rendered); - $this->assertEquals( - 'Authorization: Digest username="bob",realm="sip.domain.net",domain="sip:sip.domain.net",nonce="a61e5e040207",uri="sip:sip.domain.net",response="KJHAFgHFIUAG",stale=TRUE,algorithm=MD5,cnonce="7900f98e",qop="auth-int",nc=42,opaque="misc"' . "\r\n" . - 'Authorization: Basic MiScCreds' . "\r\n", - $rendered - ); - } - - public function testShouldNotParseEmptyValue() - { - $this->expectException(InvalidHeaderLineException::class); - AuthHeader::parse(['']); - } - - public function testShouldNotParseMissingParameters() - { - $this->expectException(InvalidHeaderLineException::class); - AuthHeader::parse(['Digest']); - } - - public function testShouldNotParseValuelessParameters() - { - $this->expectException(InvalidHeaderLineException::class); - AuthHeader::parse(['Digest realm="sip.domain.net",username']); - } - - public function testShouldNotParseValuelessParameters2() - { - $this->expectException(InvalidHeaderLineException::class); - AuthHeader::parse(['Digest realm="sip.domain.net",username=']); - } - - public function testShouldNotParseMismatchedEnclosing() - { - $this->expectException(InvalidHeaderLineException::class); - AuthHeader::parse(['Digest realm="sip.domain.net']); - } - - public function testShouldNotParseMismatchedEnclosing2() - { - $this->expectException(InvalidHeaderLineException::class); - AuthHeader::parse(['Digest realm="sip.domain.net"bogus']); - } - - public function testShouldNotParseAlphaNcValues() - { - $this->expectException(InvalidHeaderParameterException::class); - AuthHeader::parse(['Digest nc=fortytwo']); - } - - public function testShouldNotParseMixedNcValues() - { - $this->expectException(InvalidHeaderParameterException::class); - AuthHeader::parse(['Digest nc=O042']); - } - - public function testShouldNotParseNonBoolStaleValues() - { - $this->expectException(InvalidHeaderParameterException::class); - AuthHeader::parse(['Digest stale=yes']); - } - - public function testShouldNotRenderWithoutScheme() - { - $this->expectException(InvalidHeaderValueException::class); - $auth = new AuthHeader; - $auth->values[0] = new AuthValue; - $auth->render('Authorize'); - } - - public function testShouldNotRenderNonDigestWithoutCredentials() - { - $this->expectException(InvalidHeaderValueException::class); - $auth = new AuthHeader; - $auth->values[0] = new AuthValue; - $auth->values[0]->scheme = 'Basic'; - $auth->render('Authorize'); - } -} diff --git a/tests/Header/AuthenticateHeaderTest.php b/tests/Header/AuthenticateHeaderTest.php new file mode 100644 index 0000000..3a580af --- /dev/null +++ b/tests/Header/AuthenticateHeaderTest.php @@ -0,0 +1,196 @@ +assertNotNull($auth); + $this->assertInstanceOf(AuthenticateHeader::class, $auth); + $this->assertCount(2, $auth->values); + $this->assertEquals('digest', $auth->values[0]->scheme); + $this->assertEquals('sip.domain.net', $auth->values[0]->params->realm); + $this->assertEquals('sip:sip.domain.net', $auth->values[0]->params->domain); + $this->assertEquals('auth', $auth->values[0]->params->qop[0]); + $this->assertEquals('7900f98e-3d80-4504-adbc-a61e5e040207', $auth->values[0]->params->nonce); + $this->assertEquals(true, $auth->values[0]->params->stale); + $this->assertEquals('MD5', $auth->values[0]->params->algorithm); + + /* Basic authentication scheme has been obsoleted by RFC 3261, now it's handled as an opaque scheme */ + $this->assertEquals('basic', $auth->values[1]->scheme); + $this->assertEquals('realm="Access to the staging site"', $auth->values[1]->params->verbatim); + } + + public function testShouldParseVariousSpacingFormatting() + { + $scheme = 'digest'; + $realm = 'sip.domain.net'; + $qop = 'auth'; + $nonce = '7900f98e-3d80-4504-adbc-a61e5e040207'; + $stale = false; + $algorithm = 'MD5'; + + $auth = AuthenticateHeader::parse([ + 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5', + ' Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5 ', + ' Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5', + ' Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5 ', + ' DiGeSt realm ="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5 ', + ' Digest realm= "sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE,algorithm=MD5 ', + ' Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207", stale = FALSE ,algorithm= MD5 ', + 'Digest realm="sip.domain.net",qop="auth",nonce="7900f98e-3d80-4504-adbc-a61e5e040207",stale=FALSE, algorithm=MD5', + ' Digest realm=sip.domain.net,qop=auth,nonce=7900f98e-3d80-4504-adbc-a61e5e040207,stale=FALSE,algorithm=MD5 ', + ]); + + $count = count($auth->values); + + for ($i = 0; $i < $count; $i++) { + $this->assertEquals($scheme, $auth->values[$i]->scheme); + $this->assertEquals($realm, $auth->values[$i]->params->realm); + $this->assertEquals($qop, $auth->values[$i]->params->qop[0]); + $this->assertEquals($nonce, $auth->values[$i]->params->nonce); + $this->assertEquals($stale, $auth->values[$i]->params->stale); + $this->assertEquals($algorithm, $auth->values[$i]->params->algorithm); + } + + $scheme = 'basic'; + $verbatim = 'realm="Access to the staging site"'; + + $auth = AuthenticateHeader::parse([ + 'Basic realm="Access to the staging site"', + 'BaSiC realm="Access to the staging site" ', + ' Basic realm="Access to the staging site"', + ' Basic realm="Access to the staging site" ', + ]); + + $count = count($auth->values); + + for ($i = 0; $i < $count; $i++) { + $this->assertEquals($scheme, $auth->values[$i]->scheme); + $this->assertEquals($verbatim, $auth->values[$i]->params->verbatim); + } + } + + public function testShouldRenderWellFormedValues() + { + $digest = new AuthenticateHeader; + $digest->values[0] = new AuthValue; + $digest->values[0]->scheme = 'Digest'; + $digest->values[0]->params = new ChallengeParams; + $digest->values[0]->params->realm = 'sip.domain.net'; + $digest->values[0]->params->domain = 'sip:sip.domain.net'; + $digest->values[0]->params->nonce = 'a61e5e040207'; + $digest->values[0]->params->stale = true; + $digest->values[0]->params->algorithm = 'MD5'; + $digest->values[0]->params->qop = ['auth', 'auth-int']; + $digest->values[0]->params->opaque = 'misc'; + + $rendered = $digest->render('WWW-Authenticate'); + + $this->assertNotNull($rendered); + $this->assertIsString($rendered); + $this->assertEquals( + 'WWW-Authenticate: Digest realm="sip.domain.net",algorithm=MD5,nonce="a61e5e040207",opaque="misc",domain="sip:sip.domain.net",stale=TRUE,qop="auth,auth-int"' . "\r\n", + $rendered + ); + + $digest = new AuthenticateHeader; + $digest->values[0] = new AuthValue; + $digest->values[0]->scheme = 'ExoticScheme'; + $digest->values[0]->params = new OpaqueParams; + $digest->values[0]->params->verbatim = 'd02acf84-bc19-4722-8370-faade42bb583'; + + $rendered = $digest->render('WWW-Authenticate'); + + $this->assertNotNull($rendered); + $this->assertIsString($rendered); + $this->assertEquals( + 'WWW-Authenticate: ExoticScheme d02acf84-bc19-4722-8370-faade42bb583' . "\r\n", + $rendered + ); + } + + public function testShouldParseEnclosedCharacters() + { + $auth = AuthenticateHeader::parse(['Digest qop="auth,auth-int"']); + + $this->assertEquals(['auth', 'auth-int'], $auth->values[0]->params->qop); + } + + public function testShouldNotParseEmptyValue() + { + $this->expectException(InvalidHeaderLineException::class); + AuthenticateHeader::parse(['']); + } + + public function testShouldNotParseMissingParameters() + { + $this->expectException(InvalidHeaderLineException::class); + AuthenticateHeader::parse(['Digest']); + } + + public function testShouldNotParseValuelessParameters() + { + $this->expectException(InvalidHeaderLineException::class); + AuthenticateHeader::parse(['Digest realm="sip.domain.net",domain']); + } + + public function testShouldNotParseValuelessParameters2() + { + $this->expectException(InvalidHeaderLineException::class); + AuthenticateHeader::parse(['Digest realm="sip.domain.net",domain=']); + } + + public function testShouldNotParseMismatchedEnclosing() + { + $this->expectException(InvalidHeaderLineException::class); + AuthenticateHeader::parse(['Digest realm="sip.domain.net']); + } + + public function testShouldNotParseMismatchedEnclosing2() + { + $this->expectException(InvalidHeaderLineException::class); + AuthenticateHeader::parse(['Digest realm="sip.domain.net"bogus']); + } + + public function testShouldNotParseNonBoolStaleValues() + { + $this->expectException(InvalidHeaderParameterException::class); + AuthenticateHeader::parse(['Digest stale=yes']); + } + + public function testShouldNotRenderWithoutScheme() + { + $this->expectException(InvalidHeaderValueException::class); + $auth = new AuthenticateHeader; + $auth->values[0] = new AuthValue; + $auth->render('Authorize'); + } + + public function testShouldNotRenderNonDigestWithoutParameters() + { + $this->expectException(InvalidHeaderValueException::class); + $auth = new AuthenticateHeader; + $auth->values[0] = new AuthValue; + $auth->values[0]->scheme = 'Basic'; + $auth->render('Authorize'); + } +} diff --git a/tests/Header/AuthorizationHeaderTest.php b/tests/Header/AuthorizationHeaderTest.php new file mode 100644 index 0000000..fe5b885 --- /dev/null +++ b/tests/Header/AuthorizationHeaderTest.php @@ -0,0 +1,71 @@ +assertNotNull($auth); + $this->assertInstanceOf(AuthorizationHeader::class, $auth); + $this->assertCount(2, $auth->values); + $this->assertEquals('digest', $auth->values[0]->scheme); + $this->assertEquals('sip.domain.net', $auth->values[0]->params->realm); + $this->assertEquals('xPgHm7ito95w8je2FGvPS-', $auth->values[0]->params->nonce); + $this->assertEquals('jdudlKi/EjqLMlSgUFIFyQ', $auth->values[0]->params->cnonce); + $this->assertEquals('MD5', $auth->values[0]->params->algorithm); + $this->assertEquals('sip:sip.domain.net:5566;transport=udp', $auth->values[0]->params->uri); + $this->assertEquals('d1ffd656b679aa3a0ace1c648b36ab7d', $auth->values[0]->params->response); + $this->assertEquals('auth', $auth->values[0]->params->qop); + $this->assertEquals('00000001', $auth->values[0]->params->nc); + + /* Basic authentication scheme has been obsoleted by RFC 3261, now it's handled as an opaque scheme */ + $this->assertEquals('basic', $auth->values[1]->scheme); + $this->assertEquals('d1ffd656b679aa3a0ace1c648b36ab7d', $auth->values[1]->params->verbatim); + } + + public function testShouldParseEnclosedCharacters() + { + $auth = AuthorizationHeader::parse([ + 'Digest username="QWxhZGRpbjpvcGVuIHNlc2FtZQ=="', + 'Digest username="enclosed\"quotes\"test"', + ]); + + $this->assertEquals('QWxhZGRpbjpvcGVuIHNlc2FtZQ==', $auth->values[0]->params->username); + $this->assertEquals('enclosed"quotes"test', $auth->values[1]->params->username); + } + + public function testShouldNotParseAlphaNcValues() + { + $this->expectException(InvalidHeaderParameterException::class); + AuthorizationHeader::parse(['Digest nc=fortytwo']); + } + + public function testShouldNotParseMixedNcValues() + { + $this->expectException(InvalidHeaderParameterException::class); + AuthorizationHeader::parse(['Digest nc=O042']); + } + + public function testShouldNotParseNonHexResponseValues() + { + $this->expectException(InvalidHeaderParameterException::class); + AuthorizationHeader::parse(['Digest response="deadbeef_-~"']); + } +} diff --git a/tests/MessageTest.php b/tests/MessageTest.php index d54cb48..3121b9d 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -4,6 +4,8 @@ namespace RTCKit\SIP; +use RTCKit\SIP\Auth\Digest\ChallengeParams; +use RTCKit\SIP\Auth\Digest\ResponseParams; use RTCKit\SIP\Exception\InvalidBodyLengthException; use RTCKit\SIP\Exception\InvalidCSeqValueException; use RTCKit\SIP\Exception\InvalidHeaderLineException; @@ -11,7 +13,8 @@ use RTCKit\SIP\Exception\InvalidScalarValueException; use RTCKit\SIP\Exception\SIPException; use RTCKit\SIP\Header\AuthValue; -use RTCKit\SIP\Header\AuthHeader; +use RTCKit\SIP\Header\AuthenticateHeader; +use RTCKit\SIP\Header\AuthorizationHeader; use RTCKit\SIP\Header\CallIdHeader; use RTCKit\SIP\Header\ContactHeader; use RTCKit\SIP\Header\ContactValue; @@ -365,15 +368,18 @@ public function testShouldRenderWellFormedRequests() $request->authenticationInfo = new Header; $request->authenticationInfo->values[] = 'nextnonce="47364c23432d2e131a5fb210812c"'; - $request->authorization = new AuthHeader; + $request->authorization = new AuthorizationHeader; $request->authorization->values[0] = new AuthValue; $request->authorization->values[0]->scheme = 'Digest'; - $request->authorization->values[0]->username = 'bob'; - $request->authorization->values[0]->realm = 'atlanta.example.com'; - $request->authorization->values[0]->nonce = 'ea9c8e88df84f1cec4341ae6cbe5a359'; - $request->authorization->values[0]->opaque = ''; - $request->authorization->values[0]->uri = 'sips:ss2.biloxi.example.com'; - $request->authorization->values[0]->response = 'dfe56131d1958046689d83306477ecc'; + $request->authorization->values[0]->params = new ResponseParams; + $request->authorization->values[0]->params->username = 'bob'; + $request->authorization->values[0]->params->realm = 'atlanta.example.com'; + $request->authorization->values[0]->params->cnonce = 'ea9c8e88df84f1cec4341ae6cbe5a359'; + $request->authorization->values[0]->params->nc = '00000042'; + $request->authorization->values[0]->params->qop = 'auth'; + $request->authorization->values[0]->params->opaque = ''; + $request->authorization->values[0]->params->uri = 'sips:ss2.biloxi.example.com'; + $request->authorization->values[0]->params->response = 'dfe56131d1958046689d83306477ecc'; $request->date = new Header; $request->date->values[] = 'Thu, 21 Feb 2002 13:02:03 GMT'; @@ -381,25 +387,27 @@ public function testShouldRenderWellFormedRequests() $request->errorInfo = new Header; $request->errorInfo->values[] = ''; - $request->proxyAuthenticate = new AuthHeader; + $request->proxyAuthenticate = new AuthenticateHeader; $request->proxyAuthenticate->values[0] = new AuthValue; $request->proxyAuthenticate->values[0]->scheme = 'Digest'; - $request->proxyAuthenticate->values[0]->realm = 'atlanta.example.com'; - $request->proxyAuthenticate->values[0]->qop = 'auth'; - $request->proxyAuthenticate->values[0]->nonce = 'f84f1cec41e6cbe5aea9c8e88d359'; - $request->proxyAuthenticate->values[0]->opaque = ''; - $request->proxyAuthenticate->values[0]->stale = false; - $request->proxyAuthenticate->values[0]->algorithm = 'MD5'; - - $request->proxyAuthorization = new AuthHeader; + $request->proxyAuthenticate->values[0]->params = new ChallengeParams; + $request->proxyAuthenticate->values[0]->params->realm = 'atlanta.example.com'; + $request->proxyAuthenticate->values[0]->params->qop = ['auth']; + $request->proxyAuthenticate->values[0]->params->nonce = 'f84f1cec41e6cbe5aea9c8e88d359'; + $request->proxyAuthenticate->values[0]->params->opaque = ''; + $request->proxyAuthenticate->values[0]->params->stale = false; + $request->proxyAuthenticate->values[0]->params->algorithm = 'MD5'; + + $request->proxyAuthorization = new AuthorizationHeader; $request->proxyAuthorization->values[0] = new AuthValue; $request->proxyAuthorization->values[0]->scheme = 'Digest'; - $request->proxyAuthorization->values[0]->username = 'alice'; - $request->proxyAuthorization->values[0]->realm = 'atlanta.example.com'; - $request->proxyAuthorization->values[0]->nonce = 'wf84f1ceczx41ae6cbe5aea9c8e88d359'; - $request->proxyAuthorization->values[0]->opaque = ''; - $request->proxyAuthorization->values[0]->uri = 'sip:bob@biloxi.example.com'; - $request->proxyAuthorization->values[0]->response = '42ce3cef44b22f50c6a6071bc8'; + $request->proxyAuthorization->values[0]->params = new ResponseParams; + $request->proxyAuthorization->values[0]->params->username = 'alice'; + $request->proxyAuthorization->values[0]->params->realm = 'atlanta.example.com'; + $request->proxyAuthorization->values[0]->params->cnonce = 'wf84f1ceczx41ae6cbe5aea9c8e88d359'; + $request->proxyAuthorization->values[0]->params->opaque = ''; + $request->proxyAuthorization->values[0]->params->uri = 'sip:bob@biloxi.example.com'; + $request->proxyAuthorization->values[0]->params->response = '42ce3cef44b22f50c6a6071bc8'; $request->recordRoute = new Header; $request->recordRoute->values[] = ''; @@ -425,15 +433,16 @@ public function testShouldRenderWellFormedRequests() $request->warning = new Header; $request->warning->values[] = '301 isi.edu "Incompatible network address type \'E.164\'"'; - $request->wwwAuthenticate = new AuthHeader; + $request->wwwAuthenticate = new AuthenticateHeader; $request->wwwAuthenticate->values[0] = new AuthValue; $request->wwwAuthenticate->values[0]->scheme = 'Digest'; - $request->wwwAuthenticate->values[0]->realm = 'atlanta.example.com'; - $request->wwwAuthenticate->values[0]->qop = 'auth'; - $request->wwwAuthenticate->values[0]->nonce = '84f1c1ae6cbe5ua9c8e88dfa3ecm3459'; - $request->wwwAuthenticate->values[0]->opaque = ''; - $request->wwwAuthenticate->values[0]->stale = false; - $request->wwwAuthenticate->values[0]->algorithm = 'MD5'; + $request->wwwAuthenticate->values[0]->params = new ChallengeParams; + $request->wwwAuthenticate->values[0]->params->realm = 'atlanta.example.com'; + $request->wwwAuthenticate->values[0]->params->qop = ['auth']; + $request->wwwAuthenticate->values[0]->params->nonce = '84f1c1ae6cbe5ua9c8e88dfa3ecm3459'; + $request->wwwAuthenticate->values[0]->params->opaque = ''; + $request->wwwAuthenticate->values[0]->params->stale = false; + $request->wwwAuthenticate->values[0]->params->algorithm = 'MD5'; $request->extraHeaders['X-Custom-Header'] = new Header; $request->extraHeaders['X-Custom-Header']->values[0] = 'Something truly important'; @@ -472,11 +481,11 @@ public function testShouldRenderWellFormedRequests() 'Reply-To: "Bob" ' . "\r\n" . 'Alert-Info: ' . "\r\n" . 'Authentication-Info: nextnonce="47364c23432d2e131a5fb210812c"' . "\r\n" . - 'Authorization: Digest username="bob",realm="atlanta.example.com",nonce="ea9c8e88df84f1cec4341ae6cbe5a359",uri="sips:ss2.biloxi.example.com",response="dfe56131d1958046689d83306477ecc",opaque=""' . "\r\n" . + 'Authorization: Digest realm="atlanta.example.com",opaque="",username="bob",uri="sips:ss2.biloxi.example.com",response="dfe56131d1958046689d83306477ecc",cnonce="ea9c8e88df84f1cec4341ae6cbe5a359",qop=auth,nc=00000042' . "\r\n" . 'Date: Thu, 21 Feb 2002 13:02:03 GMT' . "\r\n" . 'Error-Info: ' . "\r\n" . - 'Proxy-Authenticate: Digest realm="atlanta.example.com",nonce="f84f1cec41e6cbe5aea9c8e88d359",stale=FALSE,algorithm=MD5,qop="auth",opaque=""' . "\r\n" . - 'Proxy-Authorization: Digest username="alice",realm="atlanta.example.com",nonce="wf84f1ceczx41ae6cbe5aea9c8e88d359",uri="sip:bob@biloxi.example.com",response="42ce3cef44b22f50c6a6071bc8",opaque=""' . "\r\n" . + 'Proxy-Authenticate: Digest realm="atlanta.example.com",algorithm=MD5,nonce="f84f1cec41e6cbe5aea9c8e88d359",opaque="",stale=FALSE,qop="auth"' . "\r\n" . + 'Proxy-Authorization: Digest realm="atlanta.example.com",opaque="",username="alice",uri="sip:bob@biloxi.example.com",response="42ce3cef44b22f50c6a6071bc8",cnonce="wf84f1ceczx41ae6cbe5aea9c8e88d359"' . "\r\n" . 'Record-Route: ' . "\r\n" . 'MIME-Version: 1.0' . "\r\n" . 'Organization: RTCKit' . "\r\n" . @@ -485,7 +494,7 @@ public function testShouldRenderWellFormedRequests() 'Subject: Hello' . "\r\n" . 'User-Agent: RTCKit\\SIP' . "\r\n" . 'Warning: 301 isi.edu "Incompatible network address type \'E.164\'"' . "\r\n" . - 'WWW-Authenticate: Digest realm="atlanta.example.com",nonce="84f1c1ae6cbe5ua9c8e88dfa3ecm3459",stale=FALSE,algorithm=MD5,qop="auth",opaque=""' . "\r\n" . + 'WWW-Authenticate: Digest realm="atlanta.example.com",algorithm=MD5,nonce="84f1c1ae6cbe5ua9c8e88dfa3ecm3459",opaque="",stale=FALSE,qop="auth"' . "\r\n" . 'X-Custom-Header: Something truly important' . "\r\n\r\n", $rendered ); diff --git a/tests/RFC4475/S33070Test.php b/tests/RFC4475/S33070Test.php index d4fbb76..d1a2a1c 100644 --- a/tests/RFC4475/S33070Test.php +++ b/tests/RFC4475/S33070Test.php @@ -23,7 +23,7 @@ public function testShouldParse() $this->assertInstanceOf(Request::class, $msg); /* Make sure the application has enough visibility read the authorization */ - $this->assertEquals('NoOneKnowsThisScheme', $msg->authorization->values[0]->scheme); - $this->assertEquals('opaque-data=here', $msg->authorization->values[0]->credentials); + $this->assertEquals('nooneknowsthisscheme', $msg->authorization->values[0]->scheme); + $this->assertEquals('opaque-data=here', $msg->authorization->values[0]->params->verbatim); } } From c26ca55a96f90d557e7ddb8bee4f0a881f24e8ff Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Mon, 18 Oct 2021 10:24:48 -0400 Subject: [PATCH 3/3] :rocket: v0.7.0 --- composer.json | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 999a51a..01d2de1 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,15 @@ { "name": "rtckit/sip", - "description": "Parser/Renderer for SIP protocol written in PHP", - "version": "0.6.1", + "description": "SIP protocol implementation written in PHP", + "version": "0.7.0", "type": "library", "keywords": [ "sip", "session initiation protocol", - "voip" + "voip", + "rfc 3261", + "telephony", + "telco" ], "homepage": "https://github.com/rtckit/php-sip", "license": "MIT", @@ -20,13 +23,17 @@ "issues": "https://github.com/rtckit/php-sip/issues" }, "require": { - "php": ">=7.4.0" + "php": ">=7.4.0", + "ext-ctype": "*" }, "require-dev": { "phpstan/phpstan": "^0.12", "phpunit/phpunit": "^9.5", "symfony/yaml": "^5.3", - "vimeo/psalm": "^4.8" + "vimeo/psalm": "^4.10" + }, + "suggest": { + "ext-hash": "Enables RFC 8760 authentication via SHA(-512)-256 hashing" }, "autoload": { "psr-4": {