/** * Amazon S3 REST Wrapper * Version Date: 2015-09-03 * * Copyright 2015 CF Webtools | cfwebtools.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Derived from "Amazon S3 REST Wrapper" (http://amazons3.riaforge.org) by Joe Danziger (joe@ajaxcf.com) * Requires Adobe ColdFusion 10+ equivalent (tested in Adobe ColdFusion 11) * * Currently uses Signature Version 2. (http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html) * Needs to be upgraded to Version 4. (http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) */ component accessors=true { /** * if using FW/1 & DI/1, arguments are populated using variables.framework.diConfig.constants in Application.cfc */ S3 function init( required string S3AccessKey, required string S3SecretAccessKey ) { variables.S3AccessKey = arguments.S3AccessKey; variables.S3SecretAccessKey = arguments.S3SecretAccessKey; return this; } /** * NSA SHA-1 Algorithm to hash a message being sent to AWS using the secret key. * Thanks to Adam Cameron and @jcberquist for help */ private binary function HMAC_SHA1( required string signKey, required string signMessage ) { // returns in hex var result = HMac( arguments.signMessage, arguments.signKey, 'HMACSHA1'); // convert to binary and return return binaryDecode( result, 'hex' ); } string function createSignature( required string stringIn) { // replace "\n" with "chr(10)" to get a correct digest var fixedData = replace( arguments.stringIn, "\n", chr(10), "all" ); // calculate the hash of the information var digest = HMAC_SHA1(variables.S3SecretAccessKey, fixedData); // fix the returned data to be a proper signature var signature = toBase64(digest); return signature; } /** * puts an object into the bucket * ex: putObject( bucketName = 'myBucket', ACL = 'private', keyName = 'test.jpg', uploadDir = 'D:\', serverSideEncryption = 'AES256'); * AWS S3 REST API: http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html * AWS S3 Meta Data: http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html * @ACL the canned ACL to apply to the object ( private | public-read | public-read-write | authenticated-read | bucket-owner-read | bucket-owner-full-control ) * @cacheControl cache the object in your CloudFront cache service for better performance. Additional fees may apply. * @cacheDays how many days the object will stay in the CloudFront. Max: 100 years * @contentType a standard MIME type describing the format of the contents * @fileName file name to retrieve for upload (ex: myFile.jpg). If left empty it uses the keyName attribute * @keyName uniquely identifies the object in the S3 bucket. Unicode characters whose UTF-8 encoding is at most 1024 bytes long. Safe: [0-9], [a-z], [A-Z], !, -, _, ., *, ', (, and ). "/" infers a folder in the S3 console. * @serverSideEncryption server-side encryption algorithm to use when S3 creates an object ( none | AES256 ) * @storageClass ( STANDARD | REDUCED_REDUNDANCY (noncritical, reproducible data at lower levels of redundancy) ) * @uploadDir local path the fileName attribute is stored including trailing "/" */ void function putObject( required string bucketName, string contentType = 'binary/octet-stream', boolean cacheControl = false, numeric cacheDays = 30, string ACL = 'public-read', string storageClass = 'STANDARD', required string keyName, string fileName = arguments.keyName, string uploadDir = expandPath('./'), string serverSideEncryption = 'none;' ) { var versionID = ''; var binaryFileData = ''; var dateTimeString = getHTTPTimeString( now() ); var cs = ''; var signature = ''; var httpService = ''; var cacheSeconds = cacheDays * 24 * 60 * 60; var result = ''; // put HTTP-Verb, Content-MD5(none), Content-Type and Date into signature cs = "PUT\n\n#arguments.contentType#\n#dateTimeString#\n"; // add CanonicalizedAmzHeaders sorted lexicographically seperated by a new line cs &= "x-amz-acl:#arguments.acl#\n"; if(arguments.serverSideEncryption == 'AES256') { cs &= "x-amz-server-side-encryption:AES256\n"; } cs &= "x-amz-storage-class:#arguments.storageClass#\n"; // add CanonicalizedResource cs &= "/#arguments.bucketName#/#arguments.keyName#"; // hash the signature signature = createSignature(cs); // read the file data into a variable // TODO: might need also use "ToBase64" per CF docs binaryFileData = fileReadBinary( arguments.uploadDir & arguments.fileName ); // send the file to amazon httpService = new http(); httpService.setMethod('PUT'); httpService.setURL('https://#arguments.bucketName#.s3.amazonaws.com/#arguments.keyName#'); httpService.addParam( type = 'header', name = 'Authorization', value = 'AWS #variables.S3AccessKey#:#signature#' ); httpService.addParam( type = 'header', name = 'Content-Type', value = arguments.contentType ); httpService.addParam( type = 'header', name = 'Date', value = dateTimeString ); httpService.addParam( type = 'header', name = 'x-amz-acl', value = arguments.acl ); httpService.addParam( type = 'header', name = 'x-amz-storage-class', value = arguments.storageClass ); if(arguments.serverSideEncryption == 'AES256') { httpService.addParam( type = 'header', name = 'x-amz-server-side-encryption', value = 'AES256' ); } if (arguments.cacheControl) { httpService.addParam( type = 'header', name = 'Cache-Control', value = 'max-age=#cacheSeconds#' ); } httpService.addParam( type = 'body', value = binaryFileData); result = httpService.send(); } /** * returns a link to an object * if you generate a link to a file that doesn't exist, an XML error will be returned with the code "AccessDenied", instead * of a not found error, upon accessing the URL */ string function getObjectLink( required string bucketName, required string keyName, string minutesValid = 1 ) { var timedAmazonLink = ""; var epochTime = dateDiff( "s", DateConvert("utc2Local", "January 1 1970 00:00"), now() ) + (arguments.minutesValid * 60); // create a canonical string to send var cs = "GET\n\n\n#epochTime#\n/#arguments.bucketName#/#arguments.keyName#"; // hash the signature var signature = createSignature(cs); timedAmazonLink = "https://#arguments.bucketName#.s3.amazonaws.com/#arguments.keyName#?AWSAccessKeyId=#URLEncodedFormat(variables.S3AccessKey)#&Expires=#epochTime#&Signature=#URLEncodedFormat(signature)#"; return timedAmazonLink; } /** * Download the file. Returns full file path. * @file file name to be given to saved file * @path physical path to store file including trailing slash (ex: c:\myfiles\) */ string function getObject( required string bucketName, required string keyName, string path = expandPath('./') ) { var httpService = new http(); httpService.setMethod('GET'); httpService.setURL( getObjectLink( bucketName = arguments.bucketName, keyName = arguments.keyName ) ); httpService.setPath(arguments.path); httpService.setFile(arguments.keyName); httpService.send(); return arguments.path & arguments.keyName; } /** * Deletes an object */ void function deleteObject( required string bucketName, required string keyName ) { var httpService = ''; var dateTimeString = GetHTTPTimeString( Now() ); // create a canonical string to send based on operation requested var cs = "DELETE\n\n\n#dateTimeString#\n/#arguments.bucketName#/#arguments.keyName#"; // create a proper signature var signature = createSignature(cs); // delete the object via REST httpService = new http(); httpService.setMethod('DELETE'); httpService.setURL('https://#arguments.bucketName#.s3.amazonaws.com/#arguments.keyName#'); httpService.addParam( type = 'header', name = 'Date', value = dateTimeString ); httpService.addParam( type = 'header', name = 'Authorization', value = 'AWS #variables.S3AccessKey#:#signature#' ); httpService.send(); } }