There was an internal decision to use Wasabi Cloud Storage instead of Amazon S3 and I needed to use ColdFusion to generate a pre-signed URL to allow access to AI-generated content for a limited time. I had used the Sv4Util.cfc and aws-cfml libraries before with Amazon and thought it was just as simple, but I got confused somewhere along the way and it just wasn't working.
The Wasabi documention listed several approaches to generate a valid pre-signed URL...
- Using the AWS CLI
- Using the AWS Tools for Powershell
- Using the S3 Browser
- Using Wasabi Explorer
- Using pre-signed S3 URLs for temporary, automated access in your application code
- Python and Boto3
- aws-sdk for Nodejs
- AWS SDK for PHP (V2)
... but none of these solutions were very helpful for my environment and I didn't want to have to fallback to using the command line.
I thought it'd be an easy CFML function for AI to generate, but the results still weren't working.
After some additional searches on Google, I came across an Amazon API reference regarding Authenticating Requests: Using Query Parameters (AWS Signature Version 4) and it outlined a step-by-step approach with detail instructions, detailed descriptions and static example (with example output). Whenever I'm working with a third-party API, I always look for basic CURL examples so that I can see all the explicit settings and this example was perfect.
I was able to quickly debug & identify the issues that caused the calculation to be wrong. After finding out how to do it right, I took another look at the aws-cfml library and shared the cfml example with Wasabi (but I don't think they'll update their webpage to include links to AWS documentation or CFML examples.)
While I'm not currently using this generateS3PresignedUrl UDF in production, I thought I'd share it in case other developers can benefit from it.
Source Code
https://gist.github.com/JamoCA/bbdb652e4390898ea27eee489923ede3
<cfscript> | |
/** | |
* generateS3PresignedUrl: Generates a pre-signed Wasabi URL (ColdFusion 2016+ compatible) | |
* documentation https://docs.wasabi.com/v1/docs/how-do-i-generate-pre-signed-urls-for-temporary-access-with-wasabi | |
* How to calculate: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html | |
* @displayname generateS3PresignedUrl | |
* @author James Moberg http://sunstarmedia.com, @sunstarmedia | |
* @version 1 | |
* @lastUpdate 3/20/2025 | |
* @gist https://gist.github.com/JamoCA/bbdb652e4390898ea27eee489923ede3 | |
* @blog https://dev.to/gamesover/using-coldfusion-to-generate-pre-signed-wasabi-download-url-1fe0 | |
* @twitter https://x.com/gamesover/status/1902748921388269703 | |
* @LinkedIn https://www.linkedin.com/posts/jamesmoberg_using-coldfusion-to-generate-pre-signed-wasabi-activity-7308517169632288770-vu4J | |
* @param accessKey Wasabi access key | |
* @param secretKey Wasabi secret key | |
* @param bucketName Wasabi bucket name | |
* @param objectKey Object (file) path | |
* @param region Wasabi region (e.g., us-west-1) | |
* @param expiresIn Expiration time in seconds (1 hour default) | |
*/ | |
public string function generateS3PresignedUrl( | |
required string accessKey, | |
required string secretKey, | |
required string bucketName, | |
required string objectKey, | |
string region = "us-west-1", | |
numeric expiresIn = 3600 | |
) hint="Function to generate a presigned Wasabi URL" { | |
local.endpoint = "https://" & arguments.bucketname & ".s3." & lcase(arguments.region) & ".wasabisys.com"; | |
local.objectKey = (len(trim(arguments.objectKey))) ? arguments.objectKey : "/"; | |
if (left(local.objectKey, 1) neq "/") { | |
local.objectKey = "/" & local.objectKey; | |
} | |
// Current timestamp in ISO 8601 format (e.g., 20250314T185200Z) | |
local.utcTime = dateconvert("local2Utc", now()); | |
local.amzDate = dateformat(local.utcTime, "yyyymmdd") & "T" & timeformat(local.utcTime, "HHmmss") & "Z"; | |
local.dateStamp = tostring(dateformat(local.utcTime, "yyyymmdd")); | |
// canonical URI | |
local.canonicalQueryString = [ | |
"X-Amz-Algorithm=AWS4-HMAC-SHA256" | |
,"X-Amz-Credential=" & arguments.accessKey & "%2F" & local.dateStamp & "%2F" & lcase(arguments.region) & "%2Fs3%2Faws4_request" | |
,"X-Amz-Date=" & local.amzDate | |
,"X-Amz-Expires=" & abs(val(arguments.expiresIn)) | |
,"X-Amz-SignedHeaders=host" | |
]; | |
// Canonical request | |
local.canonicalRequest = [ | |
"GET" | |
,local.objectKey | |
,arraytolist(local.canonicalQueryString, "&") | |
,"host:" & rereplacenocase(local.endpoint, "https?:\/\/", "") | |
,"" | |
,"host" | |
,"UNSIGNED-PAYLOAD" | |
]; | |
// String to sign | |
local.stringToSign = [ | |
"AWS4-HMAC-SHA256" | |
,local.amzDate | |
,local.dateStamp & "/" & lcase(arguments.region) & "/s3/aws4_request" | |
,lcase(hash(arraytolist(local.canonicalRequest, chr(10)), "SHA-256")) | |
]; | |
// Generates signing key for AWS Signature V4 | |
local.kSecret = charsetdecode("AWS4" & arguments.secretKey, "UTF-8"); | |
local.kDate = binarydecode(hmac(left(local.dateStamp,8), local.kSecret, "HMACSHA256", "utf-8"), "hex"); | |
local.kRegion = binarydecode(hmac(lcase(arguments.region), local.kDate, "HMACSHA256", "utf-8"), "hex"); | |
local.kService = binarydecode(hmac("s3", local.kRegion, "HMACSHA256", "utf-8"), "hex"); | |
local.kSigning = binarydecode(hmac("aws4_request", local.kService, "HMACSHA256", "utf-8"), "hex"); | |
local.signature = lcase(hmac(arraytolist(local.stringToSign, chr(10)), local.kSigning, "HMACSHA256", "utf-8")); | |
// Final presigned URL | |
local.presignedUrl = [ | |
local.endpoint | |
,local.objectKey | |
,"?" | |
,arraytolist(local.canonicalQueryString, "&") | |
,"&X-Amz-Signature=" | |
,local.signature | |
]; | |
return arraytolist(local.presignedUrl, ""); | |
} | |
// Example usage | |
args = [ | |
"accessKey": "my-access-key" | |
,"secretKey": "my-secret-key" | |
,"bucketName": "my-bucket-name" | |
,"objectKey": "mydir/myfile.mp3" | |
,"region": "us-west-1" | |
,"expiresIn": 3600 | |
]; | |
// writedump(var=args, label="args"); | |
signedUrl = generateS3PresignedUrl(argumentcollection=args); | |
</cfscript> | |
<cfoutput> | |
<div><a href="#signedUrl#" target="_blank">New Window</a></div> | |
<div><textarea style="height:90px; width:95%">#signedUrl#</textarea></div> | |
<iframe src="#signedUrl#" style="height:200px; width:95%"></iframe> | |
</cfoutput> |
Here's an example using the aws-cfml library.
https://gist.github.com/JamoCA/d2d38c5eaacba400decb21010124b159
<cfscript> | |
// configure awscfml CFC for Wasabi S3 | |
// https://github.com/jcberquist/aws-cfml | |
initConfig = { | |
"awskey": #accessKeyId# | |
,"awsSecretKey": #secretAccessKey# | |
,"constructorArgs": [ | |
"s3": [ | |
"host": "s3.#region#.wasabisys.com" | |
] | |
] | |
}; | |
aws = new awscfml.aws(argumentCollection=initConfig); | |
// identify & read local file | |
filePath = "d:\files\temporaryFile.zip"; | |
zipFileData = fileReadBinary(filePath); | |
remoteObjectKey = "temp/MyZipFile.zip"; | |
// configure file data for upload | |
uploadArgs = [ | |
"bucket": #bucketName# | |
,"objectKey": remoteObjectKey | |
,"fileContent": zipFileData | |
,"ContentType": fileGetMimeType(filePath) | |
]; | |
// perform upload | |
apiResponse = aws.s3.putObject(argumentcollection=args); | |
// dump API response | |
writedump(var=apiResponse, label="S3 putObject apiResponse"); | |
</cfscript> |
Top comments (0)