. */ namespace Xibo\Helper; use Xibo\Entity\Display; /** * S3 style links * inspired by https://gist.github.com/kelvinmo/d78be66c4f36415a6b80 */ class LinkSigner { /** * @param \Xibo\Entity\Display $display * @param string $encryptionKey * @param string|null $cdnUrl * @param $type * @param $itemId * @param string $storedAs * @param string|null $fileType * @return string * @throws \Xibo\Support\Exception\NotFoundException */ public static function generateSignedLink( Display $display, string $encryptionKey, ?string $cdnUrl, $type, $itemId, string $storedAs, string $fileType = null, bool $isRequestFromPwa = false, ): string { // Start with the base url, which should correctly account for running with a CMS_ALIAS $xmdsRoot = (new HttpsDetect())->getBaseUrl(); // PWA requests resources via `/pwa/getResource`, but the link should be served from `/xmds.php` if ($isRequestFromPwa) { $xmdsRoot = str_replace('/pwa/getResource', '/xmds.php', $xmdsRoot); } // Build the rest of the URL $saveAsPath = $xmdsRoot . '?file=' . $storedAs . '&displayId=' . $display->displayId . '&type=' . $type . '&itemId=' . $itemId; if ($fileType !== null) { $saveAsPath .= '&fileType=' . $fileType; } $saveAsPath .= '&' . LinkSigner::getSignature( parse_url($xmdsRoot, PHP_URL_HOST), $storedAs, time() + ($display->getSetting('collectionInterval', 300) * 2), $encryptionKey, ); // CDN? if (!empty($cdnUrl)) { // Serve a link to the CDN // CDN_URL has a `?dl=` parameter on the end already, so we just encode our string and concatenate it return 'http' . ( ( (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') ) ? 's' : '') . '://' . $cdnUrl . urlencode($saveAsPath); } else { // Serve a HTTP link to XMDS return $saveAsPath; } } /** * Get a S3 compatible signature */ public static function getSignature( string $host, string $uri, int $expires, string $secretKey, ?string $timeText = null, ?bool $isReturnSignature = false ): string { $encodedUri = str_replace('%2F', '/', rawurlencode($uri)); $headerString = 'host:' . $host . "\n"; $signedHeadersString = 'host'; if ($timeText === null) { $timestamp = time(); $dateText = gmdate('Ymd', $timestamp); $timeText = $dateText . 'T000000Z'; } else { $dateText = explode('T', $timeText)[0]; } $algorithm = 'AWS4-HMAC-SHA256'; $scope = $dateText . '/all/s3/aws4_request'; $amzParams = [ 'X-Amz-Algorithm' => $algorithm, 'X-Amz-Date' => $timeText, 'X-Amz-SignedHeaders' => $signedHeadersString ]; if ($expires > 0) { $amzParams['X-Amz-Expires'] = $expires; } ksort($amzParams); $queryStringItems = []; foreach ($amzParams as $key => $value) { $queryStringItems[] = rawurlencode($key) . '=' . rawurlencode($value); } $queryString = implode('&', $queryStringItems); $request = 'GET' . "\n" . $encodedUri . "\n" . $queryString . "\n" . $headerString . "\n" . $signedHeadersString . "\nUNSIGNED-PAYLOAD"; $stringToSign = $algorithm . "\n" . $timeText . "\n" . $scope . "\n" . hash('sha256', $request); $signingKey = hash_hmac( 'sha256', 'aws4_request', hash_hmac( 'sha256', 's3', hash_hmac( 'sha256', 'all', hash_hmac( 'sha256', $dateText, 'AWS4' . $secretKey, true ), true ), true ), true ); $signature = hash_hmac('sha256', $stringToSign, $signingKey); return ($isReturnSignature) ? $signature : $queryString . '&X-Amz-Signature=' . $signature; } }