H3XED

PHP Amazon S3 File Upload Code AWS Signature Version 4

Apr 12, 2017   Programming   Nick Vogt   Comments (6)
Please note that this post is over a year old and may contain outdated information.
Amazon offers a PHP SDK for handling AWS and S3 requests, but it weighs in at over 500 files and nearly 5MB. If you just want to upload a file to an S3 bucket using PHP, you can create the HTTP POST request yourself using only about 50 lines of code. This is also useful if you want to understand how the request and authorization process work.

AWS Signature Version 4
This code uses Amazon AWS Signature Version 4. To view the old Version 2 code, see this post.

Sample Code
This is sample PHP code to help you understand and test uploading to Amazon S3. Requires PHP version 5.4 or newer due to array syntax. If you want to use this in your project, you should probably modify this and put it into a function or method. I have it formatted like this specifically to help show how the process works.

Read the inline code comments for an explanation of each section:

<?php

// USER OPTIONS
// Replace these values with ones appropriate to you.
$accessKeyId = 'YOUR_ACCESS_KEY_ID';
$secretKey = 'YOUR_SECRET_KEY';
$bucket = 'YOUR_BUCKET_NAME';
$region = 'BUCKET_AMAZON_REGION'; // us-west-2, us-east-1, etc
$acl = 'ACCESS_CONTROL_LIST'; // private, public-read, etc
$filePath = 'path/to/file.jpg';
$fileName = 'myimage.jpg';
$fileType = 'image/jpeg';

// VARIABLES
// These are used throughout the request.
$longDate = gmdate('Ymd\THis\Z');
$shortDate = gmdate('Ymd');
$credential = $accessKeyId . '/' . $shortDate . '/' . $region . '/s3/aws4_request';

// POST POLICY
// Amazon requires a base64-encoded POST policy written in JSON.
// This tells Amazon what is acceptable for this request. For
// simplicity, we set the expiration date to always be 24H in 
// the future. The two "starts-with" fields are used to restrict
// the content of "key" and "Content-Type", which are specified
// later in the POST fields. Again for simplicity, we use blank
// values ('') to not put any restrictions on those two fields.
$policy = base64_encode(json_encode([
    'expiration' => gmdate('Y-m-d\TH:i:s\Z', time() + 86400),
    'conditions' => [
        ['acl' => $acl],
        ['bucket' => $bucket],
        ['starts-with', '$Content-Type', ''],
        ['starts-with', '$key', ''],
        ['x-amz-algorithm' => 'AWS4-HMAC-SHA256'],
        ['x-amz-credential' => $credential],
        ['x-amz-date' => $longDate]
    ]
]));

// SIGNATURE
// A base64-encoded HMAC hashed signature with your secret key.
// This is used so Amazon can verify your request, and will be
// passed along in a POST field later.
$signingKey = hash_hmac('sha256', $shortDate, 'AWS4' . $secretKey, true);
$signingKey = hash_hmac('sha256', $region, $signingKey, true);
$signingKey = hash_hmac('sha256', 's3', $signingKey, true);
$signingKey = hash_hmac('sha256', 'aws4_request', $signingKey, true);
$signature = hash_hmac('sha256', $policy, $signingKey);

// CURL
// The cURL request. Passes in the full URL to your Amazon bucket.
// Sets RETURNTRANSFER and HEADER to true to see the full response from
// Amazon, including body and head. Sets POST fields for cURL.
// Then executes the cURL request.
$ch = curl_init('https://' . $bucket . '.s3-' . $region . '.amazonaws.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
    'Content-Type' =>  $fileType,
    'acl' => $acl,
    'key' => $fileName,
    'policy' =>  $policy,
    'x-amz-algorithm' => 'AWS4-HMAC-SHA256',
    'x-amz-credential' => $credential,
    'x-amz-date' => $longDate,
    'x-amz-signature' => $signature,
    'file' => new CurlFile(realpath($filePath), $fileType, $fileName)
]);
$response = curl_exec($ch);

// RESPONSE
// If Amazon returns a response code of 204, the request was
// successful and the file should be sitting in your Amazon S3
// bucket. If a code other than 204 is returned, there will be an
// XML-formatted error code in the body. For simplicity, we use
// substr to extract the error code and output it.
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == 204) {
    echo 'Success!';
} else {
    $error = substr($response, strpos($response, '<Code>') + 6);
    echo substr($error, 0, strpos($error, '</Code>'));
}

If you aren't receiving any response at all, check if curl_exec($ch) is returning false. If it is, chances are it's due to an SSL issue. You can check the error at curl_error($ch). For testing purposes, you can set these two additional cURL options and see if it works:

curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

But note that doing this is very insecure. Read this Stackoverflow post and this blog post.
Share This Post
Twitter

Comments (6)

Hely   Apr 23, 2018
Nice code. But, show me please, how DELETE file with CURL. Thanks
tim   Dec 11, 2017
Hi, this works nicely for uploading. Now I'm wondering how I would do something similar for a GET command (i.e. downloading a file from a private S3 bucket). Could you guide me how to adjust this request?
Nick   Nov 20, 2017
If curl_exec is returning blank, it is probably returning false. You can verify that by using var_dump(curl_exec($ch)). If it is returning false, then make sure the two SSL lines are in among the other curl_setopt lines and not at the end of the file.
Soubhik   Nov 20, 2017
curl_exec($ch) is giving response blank even after adding those 2 lines. Can you please tell me what may be the reason
Toni   Oct 03, 2017
Thanks, It's amazing!! If i want to download a file from S3 Bucket, is possible without SDK PHP?
Knut Globetrotter   May 06, 2017
Thank you! I appreciate your work alot!!!
Share This Post
Twitter
H3XED © Nick Vogt   RSS   Policies   Twitter