From 52ae002a177697be866b9c074c67070d30468549 Mon Sep 17 00:00:00 2001 From: Jesse von Doom Date: Thu, 29 Mar 2012 18:18:34 -0700 Subject: [PATCH 1/3] Enhancements to getAuthenticatedURL() method - Added a $headers argument that can take an assoociative array of response override headers (like content-disposition, etc.) Full AMZ documentation on the various options is at: http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectGET.html - Added a getAWSSystemTime() method to poll AWS servers for their clock time. Used in the case of a short-expiry requests (< 120 seconds) which can be problematic on heavy-load shared servers due to clock drift. (Shockingly significant in some situations.) - Added Date header to S3Response (needed by getAWSSystemTime()) --- S3.php | 53 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/S3.php b/S3.php index 6f1e0499..00c5188e 100644 --- a/S3.php +++ b/S3.php @@ -952,6 +952,20 @@ public static function deleteObject($bucket, $uri) } + /** + * Get the current system time on AWS servers, needed to check for sync issues + * between local clock and AWS system clock. + * + * @return int + */ + public static function getAWSSystemTime() + { + $rest = new S3Request('HEAD'); + $rest = $rest->getResponse(); + return $rest->headers['server-time']; + } + + /** * Get a query string authenticated URL * @@ -960,16 +974,39 @@ public static function deleteObject($bucket, $uri) * @param integer $lifetime Lifetime in seconds * @param boolean $hostBucket Use the bucket name as the hostname * @param boolean $https Use HTTPS ($hostBucket should be false for SSL verification) + * @param array $headers An associative array of headers/values to override in the response see: + * http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectGET.html * @return string */ - public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false) + public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket = false, $https = false, $headers = false) { - $expires = time() + $lifetime; - $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea) - return sprintf(($https ? 'https' : 'http').'://%s/%s?AWSAccessKeyId=%s&Expires=%u&Signature=%s', - // $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri, self::$__accessKey, $expires, - $hostBucket ? $bucket : 's3.amazonaws.com/'.$bucket, $uri, self::$__accessKey, $expires, - urlencode(self::__getHash("GET\n\n\n{$expires}\n/{$bucket}/{$uri}"))); + if ($lifetime < 180) { + /** + * Many shared servers are still running cron+rdate for clock sync, and drift can add up to + * a significant difference between system clock and AWS time. So for requests under 3 minutes + * total we get the clock time from AWS to prepare an expiration time, rather than system time. + */ + $expires = self::getAWSSystemTime() + $lifetime; + } else { + $expires = time() + $lifetime; + } + $uri = str_replace('%2F', '/', rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea) + + $finalUrl = sprintf(($https ? 'https' : 'http').'://%s/%s?', + $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri); + $requestToSign = "GET\n\n\n{$expires}\n/{$bucket}/{$uri}"; + if (is_array($headers)) { + ksort($headers); // AMZ servers reject signatures if headers are not in alphabetical order + $appendString = '?'; + foreach ($headers as $header => $value) { + $finalUrl .= $header . '=' . urlencode($value) . '&'; + $requestToSign .= $appendString . $header . '=' . $value; + $appendString = '&'; + } + echo $requestToSign; + } + $finalUrl .= 'AWSAccessKeyId=' . self::$__accessKey . '&Expires=' . $expires . '&Signature=' . urlencode(self::__getHash($requestToSign)); + return $finalUrl; } @@ -1945,6 +1982,8 @@ private function __responseHeaderCallback(&$curl, &$data) $this->response->headers['size'] = (int)$value; elseif ($header == 'Content-Type') $this->response->headers['type'] = $value; + elseif ($header == 'Date') + $this->response->headers['server-time'] = strtotime($value); elseif ($header == 'ETag') $this->response->headers['hash'] = $value{0} == '"' ? substr($value, 1, -1) : $value; elseif (preg_match('/^x-amz-meta-.*$/', $header)) From 4be679eab3dfdd0e80797a454fe283e27a493dde Mon Sep 17 00:00:00 2001 From: Jesse von Doom Date: Thu, 29 Mar 2012 18:34:10 -0700 Subject: [PATCH 2/3] =?UTF-8?q?removed=20a=20debugging=20echo=20=E2=80=94?= =?UTF-8?q?=20command-line=20testing=20and=20didnt=20notice=20the=20output?= =?UTF-8?q?=20in=20the=20test=20report...dumb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- S3.php | 1 - 1 file changed, 1 deletion(-) diff --git a/S3.php b/S3.php index 00c5188e..c5eee1e9 100644 --- a/S3.php +++ b/S3.php @@ -1003,7 +1003,6 @@ public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket $requestToSign .= $appendString . $header . '=' . $value; $appendString = '&'; } - echo $requestToSign; } $finalUrl .= 'AWSAccessKeyId=' . self::$__accessKey . '&Expires=' . $expires . '&Signature=' . urlencode(self::__getHash($requestToSign)); return $finalUrl; From 82ba817b956814df4f887d772ae87c503e7ab38c Mon Sep 17 00:00:00 2001 From: Jesse von Doom Date: Fri, 30 Mar 2012 11:35:52 -0700 Subject: [PATCH 3/3] manual fix for the final merge conflict -- up to date with upstream changes --- S3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S3.php b/S3.php index c5eee1e9..4974db5f 100644 --- a/S3.php +++ b/S3.php @@ -990,7 +990,7 @@ public static function getAuthenticatedURL($bucket, $uri, $lifetime, $hostBucket } else { $expires = time() + $lifetime; } - $uri = str_replace('%2F', '/', rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea) + $uri = str_replace(array('%2F', '%2B'), array('/', '+'), rawurlencode($uri)); // URI should be encoded (thanks Sean O'Dea) $finalUrl = sprintf(($https ? 'https' : 'http').'://%s/%s?', $hostBucket ? $bucket : $bucket.'.s3.amazonaws.com', $uri);