I'm trying to generate presigned URLs for Amazon' S3 objects, but I can't find a GeneratePreSignedUrl class or method on Delphi 10.1 Data.Cloud.AmazonAPI as other languages have. Example on .NET : http://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURLDotNetSDK.html
I have only been able to locate a third-party DLL (Chilkat) that provides this functionality to Delphi : https://www.example-code.com/delphiDll/aws_pre_signed_url_v4.asp but I would prefer to avoid third-party libraries as much as possible (I will keep it as a last resort).
Has anyone generated those URLs using the AmazonAPI provided by Delphi 10.1 ?.
PS: I have to serve files stored on S3, at the moment my Datasnap server retrieves them and returns them to the client as big base64 strings, but the web developer says that he would prefer to just get an URL (the presigned URL to an S3 object).
Thank you.
I've programmed a function that generates presigned URLs from scratch, without dependencies on the AWS SDK or specific components for Amazon.
The calculations for the presigned URLs follow Amazon's documentation.
https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
SessionToken is used on temporary credentials and it can be left blank for long-term credentials. Operation must be "GET" or "PUT" to download or upload files respectively. And finally ExpirySeconds is the time period this URL is going to be valid, by default it's set to expire in 2 hours (7200 seconds).
uses
System.SysUtils,
System.DateUtils,
System.StrUtils,
System.Hash,
System.NetEncoding;
function GetPresignedURL(AccessKey, SecretAccess, SessionToken, Region, Bucket, ObjectKey, Operation: string; ExpirySeconds: integer = 7200): string;
var
Host, DateISO8601, DateYYYYMMDD, CredentialScope, SessionTokenParameter, CanonicalRequest, StringToSign, Signature: string;
SigningKey: TBytes;
SignedHeaders, CanonicalQuery, Algorithm, Credential: string;
function HMACSHA256(Data: string; Key: TBytes): TBytes;
begin
Result := THashSHA2.GetHMACAsBytes(TEncoding.UTF8.GetBytes(Data), Key, THashSHA2.TSHA2Version.SHA256);
end;
function BytesToHex(const ABytes: TBytes): string;
const HexChars: array[0..15] of Char = '0123456789abcdef';
begin
SetLength(Result, Length(ABytes) * 2);
for var i := 0 to Length(ABytes) - 1 do
begin
Result[i * 2 + 1] := HexChars[(ABytes[i] shr 4) and $0F]; // High nibble
Result[i * 2 + 2] := HexChars[ABytes[i] and $0F]; // Low nibble
end;
end;
begin
if (Operation.ToUpper <> 'GET') and (Operation.ToUpper <> 'PUT') then
raise Exception.CreateFmt('Only GET & PUT Operations are allowed. You requested a %s Operation', [Operation]);
// Construct basic parameters
DateISO8601 := FormatDateTime('YYYYMMDD"T"HHNNSS"Z"', TTimeZone.Local.ToUniversalTime(Now));
DateYYYYMMDD := Copy(DateISO8601, 1, 8);
Host := Format('%s.s3.%s.amazonaws.com', [Bucket, Region]);
// Credential Scope
CredentialScope := Format('%s/%s/s3/aws4_request', [DateYYYYMMDD, Region]);
Credential := Format('%s/%s', [AccessKey, CredentialScope]);
if SessionToken.Trim = '' then
SessionTokenParameter := ''
else
SessionTokenParameter := '&X-Amz-Security-Token=' + TNetEncoding.URL.Encode(SessionToken);
// Signed Headers
if SessionToken.Trim = '' then
SignedHeaders := 'host'
else
SignedHeaders := 'host;x-amz-security-token';
// Canonical Query String
CanonicalQuery :=
'X-Amz-Algorithm=AWS4-HMAC-SHA256' +
'&X-Amz-Credential=' + TNetEncoding.URL.Encode(Credential) +
'&X-Amz-Date=' + DateISO8601 +
'&X-Amz-Expires=' + IntToStr(ExpirySeconds) +
'&X-Amz-SignedHeaders=' + SignedHeaders +
SessionTokenParameter;
// Canonical Request
CanonicalRequest :=
Operation.ToUpper + #10 + '/' + TNetEncoding.URL.Encode(ObjectKey) + #10 +
CanonicalQuery + #10 +
'host:' + Host + #10 +
IfThen(SessionToken.Trim = '', '', 'x-amz-security-token:' + SessionToken + #10) + #10 +
SignedHeaders + #10 +
'UNSIGNED-PAYLOAD';
// String to Sign
Algorithm := 'AWS4-HMAC-SHA256';
StringToSign :=
Algorithm + #10 +
DateISO8601 + #10 +
CredentialScope + #10 +
THashSHA2.GetHashString(CanonicalRequest, THashSHA2.TSHA2Version.SHA256);
// Ensure HMAC uses string inputs and correctly works with string results
SigningKey := HMACSHA256(DateYYYYMMDD, TEncoding.UTF8.GetBytes('AWS4' + SecretAccess));
SigningKey := HMACSHA256(Region, SigningKey);
SigningKey := HMACSHA256('s3', SigningKey);
SigningKey := HMACSHA256('aws4_request', SigningKey);
// Compute Signature
Signature := BytesToHex(HMACSHA256(StringToSign, SigningKey));
// Construct the final URL
Result := Format('https://%s/%s?%s&X-Amz-Signature=%s%s', [Host, ObjectKey, CanonicalQuery, Signature, SessionTokenParameter])
end;
Alternatively, if you don't mind installing third-party components then you can also use the AWS SDK port to Delphi from Landgraf:
https://landgraf.dev/en/aws-s3-support-in-aws-sdk-for-delphi/
https://github.com/landgraf-dev/aws-sdk-delphi
function GetPresignedURL(const AccessKey, SecretAccess, Region, Bucket, Filename: string; Operation: THttpVerb; ExpirySeconds: integer = 7200): string;
begin
SetEnvironmentVariable('AWS_REGION', Region);
var S3Client := TAmazonS3Client.Create(AccessKey, SecretAccess);
var Request := TGetPreSignedUrlRequest.Create;
Request.BucketName := Bucket;
Request.Key := Filename;
Request.Expires := Now.IncSecond(ExpirySeconds);
Request.Verb := Operation;
Request.Protocol := TProtocol.HTTPS;
Result := S3Client.GetPreSignedURL(Request);
end;