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