Last active
October 9, 2015 06:03
-
-
Save nmcgann/caaaca7a992fe7997ae3 to your computer and use it in GitHub Desktop.
Amazon S3 Version 4 API request signer script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//TEST CODE | |
//Simple example of links to files in a privately accessible s3 bucket being authorised. | |
//If the link is followed directly a 403 error will be returned with an xml error page. | |
//Instead jquery catches the click and creates an authorisation for the request assisted by | |
//a server that holds the access keys. The region, bucket and key are extracted and then a | |
//server-side authorisation query string is requested, appended to the link href and then the | |
//file is requested from s3. | |
// Bucket needs CORS turning on (for GET) and s3:GetObject permission needs to be present. | |
?> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset=utf-8> | |
</head> | |
<body> | |
<div id="s3-files"> | |
<a href="https://s3-eu-west-1.amazonaws.com/my-bucket/my-first-file.jpg">my-first-file.jpg</a><br /> | |
<a href="https://s3-eu-west-1.amazonaws.com/my-bucket/my-second-file.jpg">my-second-file.jpg</a> | |
</div> | |
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.js"></script> | |
<script> | |
$(document).ready(function(){ | |
$('#s3-files').on('click', 'a', function(e){ | |
e.preventDefault(); | |
//grab url, extract region, bucket and key | |
var href = $(this).attr('href'), | |
matches = href.match(/^.*\/\/s3-(.*?)\.amazonaws.com\/(.*?)\/(.*)$/); | |
//matches[1]=region, matches[2]=bucket, matches[3]=key | |
if(matches && matches.length === 4){ | |
//sign request with access_key and secret_key server side | |
$.get('s3-signer.php', | |
{type: 'GET', | |
bucket: matches[2], | |
key: matches[3], | |
region: matches[1] | |
},'json').done(function(data){ | |
var url = data.url + '?' + data.qstring; | |
//fetch the link with the appended authorisation | |
window.location.href = url; | |
}); | |
} | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
* s3 V4 API server-side request signer component for client-side javascript to use. | |
* | |
* Implements a generalised request signer for the query string auth version of the AWS V4 API | |
* http://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html | |
* http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html | |
* | |
* Access amd secret keys are either set here or in environment variables - implementation dependent. | |
* Called by javascript client side (GET) with the following parameters: | |
* type - request verb GET/PUT/DELETE/HEAD etc. | |
* key - key name. | |
* expiry - X-Amz-Expires header (300sec default) | |
* bucket - bucket name. | |
* region - aws region. | |
* extra - json array of additional headers to be added | |
* e.g. as part of a PUT request[{"Content-Length":"879568"},{"X-Amz-Storage-Class":"REDUCED_REDUNDANCY"}] | |
* | |
* Returns a json object with the target url, final query string and key name. | |
* url, qstring, key. | |
* The complete request is just url+"?"+qstring. (plus the body to be attached). | |
* | |
* Neil McGann. 8th Oct 2015 | |
* | |
* The MIT License (MIT) | |
* | |
* Copyright (c) 2015 Neil McGann | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in | |
* all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
* THE SOFTWARE. | |
*/ | |
define('TEST_MODE', false); //prints to screen instead of json for test purposes | |
//keys | |
$access_key = 'MY-ACCESS-KEY'; | |
$secret_key = 'MY-SECRET-KEY'; | |
//defaults | |
$bucket = 'MY-DEFAULT-BUCKET'; //default bucket name 'my-bucket' | |
$region = 'MY-DEFAULT-REGION'; //default region e.g. 'eu-west-1' | |
$expiry = 300; //expiry in seconds for X-Amz-Expires header | |
//dates | |
$short_date = gmdate('Ymd'); //short date | |
$iso_date = gmdate("Ymd\THis\Z"); //iso format date | |
//$expiration_date = gmdate('Y-m-d\TG:i:s\Z', strtotime('+5 minutes')); | |
// -------------------------------------------------------------------------- // | |
$requestType = isset($_GET['type']) ? trim($_GET['type']) : 'GET'; | |
$key = isset($_GET['key']) ? trim($_GET['key']) : ''; | |
$expiry = isset($_GET['expiry']) ? (int) $_GET['expiry'] : $expiry; | |
$bucket = isset($_GET['bucket']) ? trim($_GET['bucket']) : $bucket; | |
$region = isset($_GET['region']) ? trim($_GET['region']) : $region; | |
//see if a json array of extra headers has been passed as a parameter | |
$extra = []; | |
if(isset($_GET['extra'])) { | |
$extraArray = @json_decode($_GET['extra'], true); | |
//if duff json - ignore | |
$extra = ($extraArray !== null) ? $extraArray : $extra; | |
} | |
$uri = rawurlencode($key); //needs to be raw encode version (before qsp) | |
$credential = $access_key . "/" . $short_date . "/" . $region . "/s3/aws4_request"; | |
$headers = []; | |
$headers[] = ["X-Amz-Algorithm" => "AWS4-HMAC-SHA256"]; | |
$headers[] = ["X-Amz-Credential" => $credential]; | |
$headers[] = ["X-Amz-Date" => $iso_date]; | |
$headers[] = ["X-Amz-Expires" => $expiry]; | |
$headers[] = ["X-Amz-SignedHeaders" => "host"]; | |
$headers = array_merge($headers, $extra); | |
//sort headers into alphabetical order - must be sorted | |
usort($headers, function($a, $b){ | |
return strcmp(key($a), key($b)); | |
}); | |
$queryString = ""; | |
foreach($headers as $header){ | |
$queryString .= urlencode(key($header)) . "=" . urlencode(current($header)) . "&"; | |
} | |
$queryString = substr($queryString, 0, -1); //chop trailing & | |
$canonicalRequest = | |
"$requestType\n" . //verb | |
"/" . $bucket . "/" . $uri . "\n" . | |
$queryString . "\n" . //canonical query string | |
"host:" . "s3-" . $region . ".amazonaws.com\n" . //canonical headers | |
"\n" . //canonical headers | |
"host\n" . //signed headers | |
"UNSIGNED-PAYLOAD"; | |
$stringToSign = | |
"AWS4-HMAC-SHA256\n" . | |
$iso_date . "\n" . | |
$short_date . "/" . $region . "/s3/aws4_request" . "\n" . | |
hash("sha256", $canonicalRequest, false); | |
$kDate = hash_hmac('sha256', $short_date, 'AWS4' . $secret_key, true); | |
$kRegion = hash_hmac('sha256', $region, $kDate, true); | |
$kService = hash_hmac('sha256', "s3", $kRegion, true); | |
$kSigning = hash_hmac('sha256', "aws4_request", $kService, true); | |
$signature = hash_hmac('sha256', $stringToSign, $kSigning); | |
$url = "https://s3-" . $region . ".amazonaws.com/" . $bucket . "/" . $uri; | |
$qsp = $queryString . "&X-Amz-Signature=" . $signature; | |
//return | |
$response = ['url' => $url, 'qstring' => $qsp, 'key' => $key]; | |
//Test code to show params then complete signed url in a text box - can copy & | |
//test with Postman on Chrome or similar. Set define to false when not required | |
if(defined('TEST_MODE') && TEST_MODE == true){ | |
echo "<pre>"; | |
echo "\n##Params:\n"; | |
echo "access key: " . $access_key ."\n"; | |
echo "secret key: " . $secret_key ."\n"; | |
echo "type: " . $requestType ."\n"; | |
echo "key: " . $key ."\n"; | |
echo "bucket: " . $bucket ."\n"; | |
echo "region: " . $region ."\n"; | |
echo "expiry: " . $expiry ."\n"; | |
echo "extra: "; | |
print_r($extra); | |
echo "\n"; | |
echo "\n##Signed Url:\n"; | |
echo "<textarea rows=\"6\" cols=\"80\">" . $url . '?' . $qsp . "</textarea>"; | |
echo "\n\n"; | |
}else{ | |
header('Content-Type: application/json'); | |
exit(json_encode($response)); | |
} | |
/* end */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment