Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.48% covered (success)
85.48%
53 / 62
81.82% covered (success)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Auth
85.48% covered (success)
85.48%
53 / 62
81.82% covered (success)
81.82%
9 / 11
26.91
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 signRequest
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getAuthorizationHeader
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 tryGetValueInsensitive
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 tryGetValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 startsWith
50.00% covered (warning)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 formatHeaders
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 computeSignature
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 computeCanonicalizedHeaders
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 computeCanonicalizedResourceForTable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 computeCanonicalizedResource
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
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 */
14namespace Pop\Storage\Adapter\Azure;
15
16use 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 */
31class 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}