Lines
95.45%
21 / 22
Methods
80.00%
4 / 5
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| redactCredentials | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 4 | ||
| looksLikeUrl | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| maskUserinfo | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | ||
| redactCredentialQueryParameters | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 5 | ||
| 10 | final class UrlRedactor | |
| 11 | { | |
| 12 | private const string MASK = '***'; | |
| 13 | ||
| 14 | private const string URL_PARTS = '#^([a-z][a-z0-9+.-]*)://([^/?\#]*)([^?\#]*)(?:\?([^\#]*))?(?:\#(.*))?$#is'; | |
| 15 | ||
| 16 | private function __construct() {} | |
| 17 | ||
| 18 | public static function redactCredentials(string $value): string | |
| 19 | { | |
| 20 | // parse_url() rejects host-less URLs such as unix:///run/valkey.sock, so the parts are split here. | |
| 21 | if (preg_match(self::URL_PARTS, $value, $parts, \PREG_UNMATCHED_AS_NULL) !== 1) { | |
| 22 | return $value; | |
| 23 | } | |
| 24 | ||
| 25 | [, $scheme, $authority, $path, $query, $fragment] = array_pad($parts, 6, null); | |
| 26 | ||
| 27 | return $scheme . '://' . self::maskUserinfo((string) $authority) . $path | |
| 28 | . ($query === null ? '' : '?' . self::redactCredentialQueryParameters($query)) | |
| 29 | . ($fragment === null ? '' : '#' . self::MASK); | |
| 30 | } | |
| 31 | ||
| 32 | public static function looksLikeUrl(string $value): bool | |
| 33 | { | |
| 34 | return preg_match('#^[a-z][a-z0-9+.-]*://#i', $value) === 1; | |
| 35 | } | |
| 36 | ||
| 37 | private static function maskUserinfo(string $authority): string | |
| 38 | { | |
| 39 | $at = strrpos($authority, '@'); | |
| 40 | ||
| 41 | if ($at === false) { | |
| 42 | return $authority; | |
| 43 | } | |
| 44 | ||
| 45 | $userinfo = str_contains(substr($authority, 0, $at), ':') ? self::MASK . ':' . self::MASK : self::MASK; | |
| 46 | ||
| 47 | return $userinfo . '@' . substr($authority, $at + 1); | |
| 48 | } | |
| 49 | ||
| 50 | private static function redactCredentialQueryParameters(string $query): string | |
| 51 | { | |
| 52 | $pairs = []; | |
| 53 | ||
| 54 | foreach (explode('&', $query) as $pair) { | |
| 55 | if ($pair === '') { | |
| 56 | continue; | |
| 57 | } | |
| 58 | ||
| 59 | $split = explode('=', $pair, 2); | |
| 60 | $name = $split[0]; | |
| 61 | ||
| 62 | $pairs[] = \count($split) === 1 || !SecretName::looksLikeCredentialParameter($name) | |
| 63 | ? $pair | |
| 64 | : $name . '=' . self::MASK; | |
| 65 | } | |
| 66 | ||
| 67 | return implode('&', $pairs); | |
| 68 | } | |
| 69 | } |