Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
85.48% |
53 / 62 |
|
81.82% |
9 / 11 |
CRAP | |
0.00% |
0 / 1 |
Auth | |
85.48% |
53 / 62 |
|
81.82% |
9 / 11 |
26.91 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
signRequest | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getAuthorizationHeader | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
tryGetValueInsensitive | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
tryGetValue | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
3 | |||
startsWith | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
formatHeaders | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
computeSignature | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
computeCanonicalizedHeaders | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
computeCanonicalizedResourceForTable | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
computeCanonicalizedResource | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (http://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@nolainteractive.com> |
7 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
8 | * @license http://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Storage\Adapter\Azure; |
15 | |
16 | use Pop\Http\Client\Request; |
17 | |
18 | /** |
19 | * Azure storage auth class |
20 | * |
21 | * This class is ported over from the discontinued Azure Storage PHP library at |
22 | * https://github.com/Azure/azure-storage-php (EOL: 3/17/2024) |
23 | * |
24 | * @category Pop |
25 | * @package Pop\Storage |
26 | * @author Nick Sagona, III <dev@nolainteractive.com> |
27 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
28 | * @license http://www.popphp.org/license New BSD License |
29 | * @version 2.0.0 |
30 | */ |
31 | class Auth extends AbstractAuth |
32 | { |
33 | |
34 | /** |
35 | * The included headers |
36 | * @var array |
37 | */ |
38 | protected array $includedHeaders = [ |
39 | 'content-encoding', 'content-language', 'content-length', 'content-md5', 'content-type', 'date', |
40 | 'if-modified-since', 'if-match', 'if-none-match', 'if-unmodified-since', 'range', |
41 | ]; |
42 | |
43 | /** |
44 | * Constructor. |
45 | * |
46 | * @param string $accountName |
47 | * @param string $accountKey |
48 | */ |
49 | public function __construct(string $accountName, string $accountKey) |
50 | { |
51 | $this->setAccountName($accountName); |
52 | $this->setAccountKey($accountKey); |
53 | } |
54 | |
55 | /** |
56 | * Adds authentication header to the request headers. |
57 | * |
58 | * @param Request $request |
59 | * @return Request |
60 | */ |
61 | public function signRequest(Request $request): Request |
62 | { |
63 | $signedKey = $this->getAuthorizationHeader( |
64 | self::formatHeaders($request->getHeadersAsArray()), $request->getUriAsString(), |
65 | $request->getQuery(), $request->getMethod() |
66 | ); |
67 | |
68 | return $request->addHeader('authorization', $signedKey); |
69 | } |
70 | |
71 | /** |
72 | * Returns authorization header to be included in the request. |
73 | * |
74 | * @param array $headers |
75 | * @param string $url |
76 | * @param array $queryParams |
77 | * @param string $httpMethod |
78 | * @return string |
79 | */ |
80 | public function getAuthorizationHeader(array $headers, string $url, array $queryParams, string $httpMethod): string |
81 | { |
82 | $signature = $this->computeSignature($headers, $url, $queryParams, $httpMethod); |
83 | |
84 | return 'SharedKey ' . $this->accountName . ':' . base64_encode( |
85 | hash_hmac('sha256', $signature, base64_decode($this->accountKey), true) |
86 | ); |
87 | } |
88 | |
89 | /** |
90 | * Returns the specified value of the $key passed from $array and in case that |
91 | * this $key doesn't exist, the default value is returned. The key matching is |
92 | * done in a case-insensitive manner. |
93 | * |
94 | * @param string $key |
95 | * @param array $haystack |
96 | * @param mixed $default |
97 | * @return mixed |
98 | */ |
99 | public static function tryGetValueInsensitive(string $key, array $haystack, mixed $default = null): mixed |
100 | { |
101 | $array = array_change_key_case($haystack); |
102 | return self::tryGetValue($array, strtolower($key), $default); |
103 | } |
104 | |
105 | /** |
106 | * Returns the specified value of the $key passed from $array and in case that |
107 | * this $key doesn't exist, the default value is returned. |
108 | * |
109 | * @param array $array |
110 | * @param mixed $key |
111 | * @param mixed $default |
112 | * @return mixed |
113 | */ |
114 | public static function tryGetValue(array $array, mixed $key, mixed $default = null): mixed |
115 | { |
116 | return (!empty($array) && array_key_exists($key, $array)) ? $array[$key] : $default; |
117 | } |
118 | |
119 | /** |
120 | * Checks if the passed $string starts with $prefix |
121 | * |
122 | * @param string $string |
123 | * @param string $prefix |
124 | * @param bool $ignoreCase |
125 | * @return bool |
126 | */ |
127 | public static function startsWith(string $string, string $prefix, bool $ignoreCase = false): bool |
128 | { |
129 | if ($ignoreCase) { |
130 | $string = strtolower($string); |
131 | $prefix = strtolower($prefix); |
132 | } |
133 | return (str_starts_with($string, $prefix)); |
134 | } |
135 | |
136 | /** |
137 | * Convert a http headers array into a uniformed format for further process |
138 | * |
139 | * @param array $headers |
140 | * @return array |
141 | */ |
142 | public static function formatHeaders(array $headers): array |
143 | { |
144 | $result = []; |
145 | foreach ($headers as $key => $value) { |
146 | $result[strtolower($key)] = (is_array($value) && count($value) == 1) ? $value[0] : $value; |
147 | } |
148 | |
149 | return $result; |
150 | } |
151 | |
152 | |
153 | /** |
154 | * Computes the authorization signature for blob and queue shared key. |
155 | * |
156 | * @param array $headers |
157 | * @param string $url |
158 | * @param array $queryParams |
159 | * @param string $httpMethod |
160 | * @return string |
161 | */ |
162 | protected function computeSignature(array $headers, string $url, array $queryParams, string $httpMethod): string |
163 | { |
164 | $canonicalizedHeaders = $this->computeCanonicalizedHeaders($headers); |
165 | $canonicalizedResource = $this->computeCanonicalizedResource($url, $queryParams); |
166 | |
167 | $stringToSign = []; |
168 | $stringToSign[] = strtoupper($httpMethod); |
169 | |
170 | foreach ($this->includedHeaders as $header) { |
171 | $stringToSign[] = self::tryGetValueInsensitive($header, $headers); |
172 | } |
173 | |
174 | if (count($canonicalizedHeaders) > 0) { |
175 | $stringToSign[] = implode("\n", $canonicalizedHeaders); |
176 | } |
177 | |
178 | $stringToSign[] = $canonicalizedResource; |
179 | $string = implode("\n", $stringToSign); |
180 | |
181 | return $string; |
182 | } |
183 | |
184 | /** |
185 | * Computes canonicalized headers for headers array. |
186 | * |
187 | * @param array $headers |
188 | * @return array |
189 | */ |
190 | protected function computeCanonicalizedHeaders(array $headers): array |
191 | { |
192 | $canonicalizedHeaders = []; |
193 | $normalizedHeaders = []; |
194 | $validPrefix = 'x-ms-'; |
195 | |
196 | foreach ($headers as $header => $value) { |
197 | // Convert header to lower case. |
198 | $header = strtolower($header); |
199 | |
200 | // Retrieve all headers for the resource that begin with x-ms-, |
201 | // including the x-ms-date header. |
202 | if (self::startsWith($header, $validPrefix)) { |
203 | // Unfold the string by replacing any breaking white space |
204 | // (meaning what splits the headers, which is \r\n) with a single |
205 | // space. |
206 | $value = str_replace("\r\n", ' ', $value); |
207 | |
208 | // Trim any white space around the colon in the header. |
209 | $value = ltrim($value); |
210 | $header = rtrim($header); |
211 | |
212 | $normalizedHeaders[$header] = $value; |
213 | } |
214 | } |
215 | |
216 | // Sort the headers lexicographically by header name, in ascending order. |
217 | // Note that each header may appear only once in the string. |
218 | ksort($normalizedHeaders); |
219 | |
220 | foreach ($normalizedHeaders as $key => $value) { |
221 | $canonicalizedHeaders[] = $key . ':' . $value; |
222 | } |
223 | |
224 | return $canonicalizedHeaders; |
225 | } |
226 | |
227 | /** |
228 | * Computes canonicalized resources from URL using Table formar |
229 | * |
230 | * @param string $url |
231 | * @param array $queryParams |
232 | * @return string |
233 | */ |
234 | protected function computeCanonicalizedResourceForTable(string $url, array $queryParams): string |
235 | { |
236 | $queryParams = array_change_key_case($queryParams); |
237 | |
238 | // 1. Beginning with an empty string (""), append a forward slash (/), |
239 | // followed by the name of the account that owns the accessed resource. |
240 | $canonicalizedResource = '/' . $this->accountName; |
241 | |
242 | // 2. Append the resource's encoded URI path, without any query parameters. |
243 | $canonicalizedResource .= parse_url($url, PHP_URL_PATH); |
244 | |
245 | // 3. The query string should include the question mark and the comp |
246 | // parameter (for example, ?comp=metadata). No other parameters should |
247 | // be included on the query string. |
248 | if (array_key_exists('comp', $queryParams)) { |
249 | $canonicalizedResource .= '?comp='; |
250 | $canonicalizedResource .= $queryParams['comp']; |
251 | } |
252 | |
253 | return $canonicalizedResource; |
254 | } |
255 | |
256 | /** |
257 | * Computes canonicalized resources from URL. |
258 | * |
259 | * @param string $url |
260 | * @param array $queryParams |
261 | * @return string |
262 | */ |
263 | protected function computeCanonicalizedResource(string $url, array $queryParams): string |
264 | { |
265 | $queryParams = array_change_key_case($queryParams); |
266 | |
267 | // 1. Beginning with an empty string (""), append a forward slash (/), |
268 | // followed by the name of the account that owns the accessed resource. |
269 | $canonicalizedResource = '/' . $this->accountName; |
270 | |
271 | // 2. Append the resource's encoded URI path, without any query parameters. |
272 | $canonicalizedResource .= parse_url($url, PHP_URL_PATH); |
273 | |
274 | // 3. Retrieve all query parameters on the resource URI, including the comp |
275 | // parameter if it exists. |
276 | // 4. Sort the query parameters lexicographically by parameter name, in |
277 | // ascending order. |
278 | if (count($queryParams) > 0) { |
279 | ksort($queryParams); |
280 | } |
281 | |
282 | // 5. Convert all parameter names to lowercase. |
283 | // 6. URL-decode each query parameter name and value. |
284 | // 7. Append each query parameter name and value to the string in the |
285 | // following format: |
286 | // parameter-name:parameter-value |
287 | // 9. Group query parameters |
288 | // 10. Append a new line character (\n) after each name-value pair. |
289 | foreach ($queryParams as $key => $value) { |
290 | // $value must already be ordered lexicographically |
291 | // See: ServiceRestProxy::groupQueryValues |
292 | $canonicalizedResource .= "\n" . $key . ':' . $value; |
293 | } |
294 | |
295 | return $canonicalizedResource; |
296 | } |
297 | |
298 | } |