Skip to content

Commit

Permalink
Issue #1837920: Support Cloudfront signed URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
justafish committed Feb 16, 2014
1 parent c5d674b commit 4b85704
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 21 deletions.
78 changes: 61 additions & 17 deletions AmazonS3StreamWrapper.inc
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ class AmazonS3StreamWrapper implements DrupalStreamWrapperInterface {
*/
protected $rrs = array();

/**
* @var bool Whether or not to deliver URLs through CloudFront.
*/
protected $cloudfront = FALSE;

/**
* @var AmazonCF connection object
*/
protected $cf = NULL;

/**
* Object constructor
*
Expand All @@ -109,6 +119,7 @@ class AmazonS3StreamWrapper implements DrupalStreamWrapperInterface {
public function __construct() {
$this->hostname = variable_get('amazons3_hostname', '');
$this->bucket = $bucket = variable_get('amazons3_bucket', '');
$this->cloudfront = variable_get('amazons3_cloudfront', TRUE);

// CNAME support for customising S3 URLs
if (variable_get('amazons3_cname', 0)) {
Expand All @@ -132,7 +143,7 @@ class AmazonS3StreamWrapper implements DrupalStreamWrapperInterface {
// HTTPS list
$https = explode("\n", variable_get('amazons3_https', ''));
$https = array_map('trim', $https);
$torrents = array_filter($https, 'strlen');
$https = array_filter($https, 'strlen');
$this->https = $https;

// Torrent list
Expand Down Expand Up @@ -263,15 +274,23 @@ class AmazonS3StreamWrapper implements DrupalStreamWrapperInterface {
}
}

$timeout = ($info['presigned_url']) ? time() + $info['presigned_url_timeout'] : 0;
$torrent = ($info['download_type'] == 'torrent') ? TRUE : FALSE;
$response = ($info['response']) ? $info['response'] : array();
if ($info['presigned_url'] || $info['download_type'] != 'http' || !empty($info['response']) || $info['https']) {
$timeout = $info['presigned_url'] ? time() + $info['presigned_url_timeout'] : 0;
$torrent = $info['download_type'] == 'torrent' ? TRUE : FALSE;
$response = is_array($info['response']) ? $info['response'] : array();

// Generate the URL.
$url = $this->domain . '/' . $local_path;

if ($info['presigned_url'] && $this->cloudfront) {
$url = $this->getCF()->get_private_object_url($this->domain, $local_path, $timeout, array('Secure' => $info['https']));
}
else if ($info['presigned_url'] || $info['download_type'] !== 'http' || !empty($info['response'])) {
$url = $this->getS3()->get_object_url($this->bucket, $local_path, $timeout, array('https' => $info['https'], 'torrent' => $torrent, 'response' => $response));
return $url;
}
else if ($info['https']) {
$url = 'https:' . $url;
}

$url = $this->domain . '/' . $local_path;
return $url;
}

Expand Down Expand Up @@ -590,12 +609,17 @@ class AmazonS3StreamWrapper implements DrupalStreamWrapperInterface {
}
}

// Set the metadata.
$headers = array();
$headers = array_merge($headers, module_invoke_all('amazons3_save_headers', $local_path, $headers));

// Save the object.
$response = $this->getS3()->create_object($this->bucket, $local_path, array(
'body' => $this->buffer,
'acl' => AmazonS3::ACL_PUBLIC,
'contentType' => AmazonS3StreamWrapper::getMimeType($this->uri),
'storage' => $s3_storage_type,
'headers' => $headers,
));
if($response->isOK()) {
return TRUE;
Expand Down Expand Up @@ -993,13 +1017,11 @@ class AmazonS3StreamWrapper implements DrupalStreamWrapperInterface {
* @see http://docs.amazonwebservices.com/AWSSDKforPHP/latest/#i=AmazonS3
*/
protected function getS3() {
if($this->s3 == null) {
$bucket = variable_get('amazons3_bucket', '');

if(!libraries_load('awssdk') && !isset($bucket)) {
if ($this->s3 == NULL) {
if (!libraries_load('awssdk')) {
drupal_set_message('Unable to load the AWS SDK. Please check you have installed the library correctly and configured your S3 credentials.'. 'error');
}
else if(!isset($bucket)) {
else if (empty($this->bucket)) {
drupal_set_message('Bucket name not configured.'. 'error');
}
else {
Expand All @@ -1009,19 +1031,41 @@ class AmazonS3StreamWrapper implements DrupalStreamWrapperInterface {
$this->s3->set_hostname($this->hostname);
$this->s3->allow_hostname_override(FALSE);
}
$this->bucket = $bucket;
}
catch(RequestCore_Exception $e){
drupal_set_message('There was a problem connecting to S3', 'error');
}
catch(Exception $e) {
catch (Exception $e) {
drupal_set_message('There was a problem using S3: ' . $e->getMessage(), 'error');
}
}
}
return $this->s3;
}

/**
* Get the CloudFront connection object
*
* @return AmazonCloudFront
* CloudFront connection object.
*
* @see http://docs.amazonwebservices.com/AWSSDKforPHP/latest/#i=AmazonCloudFront
*/
protected function getCF() {
if ($this->cf === NULL) {
if (!libraries_load('awssdk')) {
drupal_set_message('Unable to load the AWS SDK. Please check you have installed the library correctly and configured your CloudFront settings.'. 'error');
}
else if (variable_get('amazons3_cname', 0) && variable_get('amazons3_domain', 0)) {
try {
$this->cf = new AmazonCloudFront();
}
catch (Exception $e) {
drupal_set_message('There was a problem using CloudFront: ' . $e->getMessage(), 'error');
}
}
}

return $this->cf;
}

/**
* Get file status
*
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ See [http://drupal.org/project/amazons3_cors](http://drupal.org/project/amazons3


## API
You can modify the generated URL and it's properties, this is very useful for setting Cache-Control and Expires headers. See amazons3.api.php
You can modify the generated URL and it's properties, this is very useful for setting Cache-Control and Expires headers (as long as you aren't using CloudFront).

You can also alter the metadata for each object saved to S3 with hook_amazons3_save_headers(). This is very useful for forcing the content-disposition header to force download files if they're being delivered through CloudFront presigned URLs.

See amazons3.api.php
20 changes: 20 additions & 0 deletions amazons3.api.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
* - 'presigned_url_timeout': (boolean) Time in seconds before an authenticated URL will time out.
* - 'response': array of additional options as described at
* http://docs.amazonwebservices.com/AWSSDKforPHP/latest/index.html#m=AmazonS3/get_object_url
* If you return anything other than an empty arrayhere, CloudFront
* support for these URLs will be disabled.
* @return
* The modified array of configuration items.
*/
Expand All @@ -45,3 +47,21 @@ function hook_amazons3_url_info($local_path, $info) {
return $info;
}

/**
* Allows other modules to change the headers/metadata used when saving an
* object to S3. See the headers array in the create_object documentation.
* http://docs.aws.amazon.com/AWSSDKforPHP/latest/#m=AmazonS3/create_object
* @param $local_path
* The local filesystem path.
* @param $headers
* Array of keyed header elements.
* @return The modified array of configuration items.
*/
function hook_amazons3_save_headers($local_path, $headers) {
$cache_time = 60 * 60 * 5;
$headers = array(
'content-disposition' => 'attachment; filename=' . basename($local_path),
);

return $headers;
}
27 changes: 24 additions & 3 deletions amazons3.module
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ function amazons3_admin() {
),
);

$form['amazons3_cloudfront'] = array(
'#type' => 'checkbox',
'#title' => t('Enable CloudFront'),
'#description' => t('Deliver URLs through a CloudFront domain when using presigned URLs.'),
'#default_value' => variable_get('amazons3_cloudfront', 0),
'#states' => array(
'visible' => array(
':input[id=edit-amazons3-cname]' => array('checked' => TRUE),
)
),
);

$form['amazons3_hostname'] = array(
'#type' => 'textfield',
'#title' => t('Custom Hostname'),
Expand All @@ -120,23 +132,23 @@ function amazons3_admin() {
$form['amazons3_torrents'] = array(
'#type' => 'textarea',
'#title' => t('Torrents'),
'#description' => t('A list of paths that should be delivered through a torrent url. Enter one value per line e.g. "mydir/*". Paths are relative to the Drupal file directory and use patterns as per <a href="@preg_match">preg_match</a>.', array('@preg_match' => 'http://php.net/preg_match')),
'#description' => t('A list of paths that should be delivered through a torrent url. Enter one value per line e.g. "mydir/*". Paths are relative to the Drupal file directory and use patterns as per <a href="@preg_match">preg_match</a>. This won\'t work for CloudFront presigned URLs.', array('@preg_match' => 'http://php.net/preg_match')),
'#default_value' => variable_get('amazons3_torrents', ''),
'#rows' => 10,
);

$form['amazons3_presigned_urls'] = array(
'#type' => 'textarea',
'#title' => t('Presigned URLs'),
'#description' => t('A list of timeouts and paths that should be delivered through a presigned url. Enter one value per line, in the format <timeout>|<path>|<protocol>. e.g. "60|mydir/*" or "60|mydir/*|https". Paths are relative to the Drupal file directory and use patterns as per <a href="@preg_match">preg_match</a>.', array('@preg_match' => 'http://php.net/preg_match')),
'#description' => t('A list of timeouts and paths that should be delivered through a presigned url. Enter one value per line, in the format &lt;timeout&gt;|&lt;path&gt;|&lt;protocol&gt;. e.g. "60|mydir/*" or "60|mydir/*|https". Paths are relative to the Drupal file directory and use patterns as per <a href="@preg_match">preg_match</a>.', array('@preg_match' => 'http://php.net/preg_match')),
'#default_value' => variable_get('amazons3_presigned_urls', ''),
'#rows' => 10,
);

$form['amazons3_saveas'] = array(
'#type' => 'textarea',
'#title' => t('Force Save As'),
'#description' => t('A list of paths that force the user to save the file by using Content-disposition header. Prevents autoplay of media. Enter one value per line. e.g. "mydir/*". Paths are relative to the Drupal file directory and use patterns as per <a href="@preg_match">preg_match</a>. Files must use a presigned url to use this.', array('@preg_match' => 'http://php.net/preg_match')),
'#description' => t('A list of paths that force the user to save the file by using Content-disposition header. Prevents autoplay of media. Enter one value per line. e.g. "mydir/*". Paths are relative to the Drupal file directory and use patterns as per <a href="@preg_match">preg_match</a>. Files must use a presigned url to use this, however it won\'t work for CloudFront presigned URLs and you\'ll need to set the content-disposition header in the file metadata before saving.', array('@preg_match' => 'http://php.net/preg_match')),
'#default_value' => variable_get('amazons3_saveas', ''),
'#rows' => 10,
);
Expand Down Expand Up @@ -165,6 +177,15 @@ function amazons3_admin() {

function amazons3_admin_validate($form, &$form_state) {
$bucket = $form_state['values']['amazons3_bucket'];
$cloudfront = $form_state['values']['amazons3_cloudfront'];

if ($cloudfront) {
$keypair = variable_get('aws_cloudfront_keypair', '');
$pem = variable_get('aws_cloudfront_pem', '');
if (empty($keypair) || empty($pem)) {
form_set_error('amazons3_cloudfront', t('You must configure your CloudFront credentials in the awksdk module.'));
}
}

if(!libraries_load('awssdk')) {
form_set_error('amazons3_bucket', t('Unable to load the AWS SDK. Please check you have installed the library correctly and configured your S3 credentials.'));
Expand Down

0 comments on commit 4b85704

Please sign in to comment.