Skip to content

Commit

Permalink
LDEV-4707 . add addional arguments to the function
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeloffner committed Sep 28, 2023
1 parent 292810c commit 44b1ef1
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 44 deletions.
63 changes: 63 additions & 0 deletions source/fld/function.fld
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,69 @@
<type>datetime</type>
<required>No</required>
<description>Date time when the URL should expire.</description>
</argument>
<argument>
<name>httpMethod</name>
<alias>method</alias>
<type>string</type>
<required>No</required>
<description>The desired HTTP method (e.g., GET, PUT). Defaults to GET.</description>
</argument>
<argument>
<name>sseAlgorithm</name>
<alias>algorithm</alias>
<type>string</type>
<required>No</required>
<description>The desired server-side encryption algorithm, valid values are AES256 or KMS.</description>
</argument>
<argument>
<name>sseCustomerKey</name>
<alias>customerKey</alias>
<type>string</type>
<required>No</required>
<description>The server-side encryption customer-provided key.</description>
</argument>
<argument>
<name>checksum</name>
<alias>contentMd5</alias>
<type>string</type>
<required>No</required>
<description>The base64 encoded MD5 checksum of the object's content.</description>
</argument>
<argument>
<name>type</name>
<alias>contentType</alias>
<type>string</type>
<required>No</required>
<description>The MIME type of the object (e.g., "text/plain").</description>
</argument>
<argument>
<name>disposition</name>
<alias>contentDisposition</alias>
<type>string</type>
<required>No</required>
<description>Specifies presentational information for the object, like "attachment; filename=\"filename.pdf\"".</description>
</argument>
<argument>
<name>encoding</name>
<alias>contentEncoding</alias>
<type>string</type>
<required>No</required>
<description>Specifies content encodings applied to the object, like gzip.</description>
</argument>
<argument>
<name>version</name>
<alias>versionId</alias>
<type>string</type>
<required>No</required>
<description>The version ID of the object if versioning is enabled.</description>
</argument>
<argument>
<name>zeroByte</name>
<alias>zeroByteContent</alias>
<type>boolean</type>
<required>No</required>
<description>A flag to specify if the object has zero-byte content.</description>
</argument>
<argument>
<name>accessKeyId</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,25 @@ public class AmazonS3Client implements AmazonS3 {

private long liveTimeout;

private boolean pathStyleAccess;

public static AmazonS3Client get(String accessKeyId, String secretAccessKey, String host, org.lucee.extension.resource.s3.region.RegionFactory.Region region, long liveTimeout,
Log log) throws S3Exception {
String key = accessKeyId + ":" + secretAccessKey + ":" + host + ":" + (region == null ? "default-region" : S3.toString(region));
boolean pathStyleAccess, Log log) throws S3Exception {
String key = accessKeyId + ":" + secretAccessKey + ":" + host + ":" + (region == null ? "default-region" : S3.toString(region)) + ":" + pathStyleAccess;
AmazonS3Client client = pool.get(key);
if (client == null || client.isExpired()) {
pool.put(key, client = new AmazonS3Client(accessKeyId, secretAccessKey, host, region, key, liveTimeout, log));
pool.put(key, client = new AmazonS3Client(accessKeyId, secretAccessKey, host, region, key, liveTimeout, pathStyleAccess, log));
}
return client;
}

private AmazonS3Client(String accessKeyId, String secretAccessKey, String host, org.lucee.extension.resource.s3.region.RegionFactory.Region region, String key,
long liveTimeout, Log log) throws S3Exception {
long liveTimeout, boolean pathStyleAccess, Log log) throws S3Exception {
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
this.host = host;
this.region = region;
this.pathStyleAccess = pathStyleAccess;
this.log = log;
this.created = System.currentTimeMillis();
client = create();
Expand All @@ -91,6 +94,8 @@ public AmazonS3 create() throws S3Exception {

}
}
if (pathStyleAccess) builder.withPathStyleAccessEnabled(pathStyleAccess);

return builder.build();
}

Expand Down
126 changes: 121 additions & 5 deletions source/java/src/org/lucee/extension/resource/s3/S3.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@
import com.amazonaws.services.s3.model.Owner;
import com.amazonaws.services.s3.model.PartETag;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.ResponseHeaderOverrides;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.amazonaws.services.s3.model.S3VersionSummary;
import com.amazonaws.services.s3.model.SSEAlgorithm;
import com.amazonaws.services.s3.model.SSECustomerKey;
import com.amazonaws.services.s3.model.UploadPartRequest;
import com.amazonaws.services.s3.model.UploadPartResult;
import com.amazonaws.services.s3.model.VersionListing;
Expand Down Expand Up @@ -390,19 +393,128 @@ public S3ObjectSummary getInfo(String bucketName, String objectName) throws S3Ex
return null;
}

public URL generatePresignedURL(String bucketName, String objectName, Date expireDate) throws S3Exception {
/**
* Generates a pre-signed URL for Amazon S3 operations with various custom parameters.
*
* @param bucketName The name of the S3 bucket.
* @param objectName The key of the S3 object.
* @param expireDate The expiration date for the pre-signed URL.
* @param httpMethod The desired HTTP method (e.g., GET, PUT). Defaults to GET.
* @param sseAlgorithm The desired server-side encryption algorithm, valid values are AES256 or KMS.
* @param sseCustomerKey The server-side encryption customer-provided key.
* @param checksum The base64 encoded MD5 checksum of the object's content.
* @param contentType The MIME type of the object (e.g., "text/plain").
* @param contentDisposition Specifies presentational information for the object, like "attachment;
* filename=\"filename.pdf\"".
* @param contentEncoding Specifies content encodings applied to the object, like gzip.
* @param versionId The version ID of the object if versioning is enabled.
* @param zeroByteContent A flag to specify if the object has zero-byte content.
* @param responseHeaders Struct of response headers.
*
* @return The generated pre-signed URL.
*
* @throws S3Exception If there's an issue generating the pre-signed URL or invalid input
* parameters.
*/
public URL generatePresignedURL(String bucketName, String objectName, Date expireDate, String httpMethod, String sseAlgorithm, String sseCustomerKey, String checksum,
String contentType, String contentDisposition, String contentEncoding, String versionId, Boolean zeroByteContent) throws S3Exception {
bucketName = improveBucketName(bucketName);
objectName = improveObjectName(objectName);

AmazonS3Client client = getAmazonS3(bucketName, null);
// http method
HttpMethod method;
if (Util.isEmpty(httpMethod, true)) method = HttpMethod.GET;
else {
String tmp = httpMethod.trim().toUpperCase();
if ("DELETE".equals(tmp)) method = HttpMethod.DELETE;
else if ("GET".equals(tmp)) method = HttpMethod.GET;
else if ("HEAD".equals(tmp)) method = HttpMethod.HEAD;
else if ("PATCH".equals(tmp)) method = HttpMethod.PATCH;
else if ("POST".equals(tmp)) method = HttpMethod.POST;
else if ("PUT".equals(tmp)) method = HttpMethod.PUT;
else throw new S3Exception("invalid http method defintion [" + httpMethod + "], valid values are [DELETE, GET, HEAD, PATCH, POST, PUT]");
}

// sse algorithm
SSEAlgorithm algorithm = null;
if (!Util.isEmpty(sseAlgorithm, true)) {
String tmp = sseAlgorithm.trim().toUpperCase();
if ("AES256".equals(tmp)) algorithm = SSEAlgorithm.AES256;
else if ("KMS".equals(tmp)) algorithm = SSEAlgorithm.KMS;
else throw new S3Exception("invalid SSE Algorithm defintion [" + sseAlgorithm + "], valid values are [AES256,KMS]");
}

ResponseHeaderOverrides headers = null;
SSECustomerKey key;
// sse key
if (Util.isEmpty(sseCustomerKey, true)) key = null;
else key = new SSECustomerKey(sseCustomerKey.trim());

// checksum
if (Util.isEmpty(checksum, true)) checksum = null;
else checksum = checksum.trim();

// content disposition
if (!Util.isEmpty(contentDisposition, true)) {
if (headers == null) headers = new ResponseHeaderOverrides();
headers.setContentDisposition(contentDisposition.trim()); // example input: attachment; filename=\"filename.pdf\"
}

// content encoding
if (!Util.isEmpty(contentEncoding, true)) {
if (headers == null) headers = new ResponseHeaderOverrides();
headers.setContentEncoding(contentEncoding.trim()); // example input: gzip
}

// content type
if (Util.isEmpty(contentType, true)) contentType = null;
else {
if (headers != null) {
headers.setContentType(contentType.trim());
contentType = null; // not necessary to set this separatly anymore
}
else contentType = contentType.trim();
}

// version id
if (Util.isEmpty(versionId, true)) versionId = null;
else versionId = versionId.trim();

// . in bucket?
boolean isDotInBucket = bucketName.indexOf('.') != -1;

AmazonS3Client client = getAmazonS3(bucketName, null, isDotInBucket);
try {
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectName).withMethod(method);
if (versionId != null) {
generatePresignedUrlRequest.withVersionId(versionId);
}
if (contentType != null) {
generatePresignedUrlRequest.withContentType(contentType);
}
if (checksum != null) {
generatePresignedUrlRequest.withContentMd5(checksum);
}
if (zeroByteContent != null) {
generatePresignedUrlRequest.withZeroByteContent(zeroByteContent);
}

if (headers != null) {
generatePresignedUrlRequest.withResponseHeaders(headers);
}

GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectName).withMethod(HttpMethod.GET);
if (algorithm != null) {
generatePresignedUrlRequest.withSSEAlgorithm(algorithm);
if (key != null) {
generatePresignedUrlRequest.withSSECustomerKey(key);
}
}

if (expireDate != null) {
if (expireDate.getTime() < System.currentTimeMillis()) throw new S3Exception("the optional expire date must be un the future");
generatePresignedUrlRequest.withExpiration(expireDate);
}

return client.generatePresignedUrl(generatePresignedUrlRequest);
}
catch (AmazonServiceException ase) {
Expand Down Expand Up @@ -2024,12 +2136,16 @@ public URL url(String bucketName, String objectName, long time) throws S3Excepti
}
}

private AmazonS3Client getAmazonS3(String bucketName, String strRegion) throws S3Exception {
private AmazonS3Client getAmazonS3(String bucketName, String strRegion) throws S3Exception { // TODO remove
return getAmazonS3(bucketName, strRegion, false);
}

private AmazonS3Client getAmazonS3(String bucketName, String strRegion, boolean pathStyleAccess) throws S3Exception {
if (Util.isEmpty(accessKeyId) || Util.isEmpty(secretAccessKey)) throw new S3Exception("Could not found an accessKeyId/secretAccessKey");

Region region = toRegion(bucketName, strRegion);

return AmazonS3Client.get(accessKeyId, secretAccessKey, host, region, liveTimeout, log);
return AmazonS3Client.get(accessKeyId, secretAccessKey, host, region, liveTimeout, pathStyleAccess, log);
}

public Region getBucketRegion(String bucketName, boolean loadIfNecessary) throws S3Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,35 @@ public class S3GeneratePresignedURL extends S3Function {

private static final long serialVersionUID = 1L;

public static String call(PageContext pc, String bucketNameOrPath, String objectName, DateTime expireDate, String accessKeyId, String secretAccessKey, String host,
double timeout) throws PageException {
@Override
public Object invoke(PageContext pc, Object[] args) throws PageException {

CFMLEngine eng = CFMLEngineFactory.getInstance();
Cast cast = eng.getCastUtil();

if (args.length < 1 || args.length < 16) throw eng.getExceptionUtil().createFunctionException(pc, "S3GeneratePresignedURL", 1, 16, args.length);
String tmp;

// required
String bucketNameOrPath = cast.toString(args[0]);

// optional
String objectName = args.length > 1 && args[1] != null ? cast.toString(args[1]) : null;
DateTime expireDate = args.length > 2 && args[2] != null ? cast.toDateTime(args[2], pc.getTimeZone()) : null;
String httpMethod = args.length > 3 && args[3] != null ? cast.toString(args[3]) : null;
String sseAlgorithm = args.length > 4 && args[4] != null ? cast.toString(args[4]) : null;
String sseCustomerKey = args.length > 5 && args[5] != null ? cast.toString(args[5]) : null;
String checksum = args.length > 6 && args[6] != null ? cast.toString(args[6]) : null;
String contentType = args.length > 7 && args[7] != null ? cast.toString(args[7]) : null;
String contentDisposition = args.length > 8 && args[8] != null ? cast.toString(args[8]) : null;
String contentEncoding = args.length > 9 && args[9] != null ? cast.toString(args[9]) : null;
String versionId = args.length > 10 && args[10] != null ? cast.toString(args[10]) : null;
Boolean zeroByteContent = args.length > 11 && !isEmpty(args[11]) ? cast.toBoolean(args[11]) : null;
String accessKeyId = args.length > 12 && args[12] != null ? cast.toString(args[12]) : null;
String secretAccessKey = args.length > 13 && args[13] != null ? cast.toString(args[13]) : null;
String host = args.length > 14 && args[14] != null ? cast.toString(args[14]) : null;
double timeout = args.length > 15 && !isEmpty(args[15]) ? cast.toDoubleValue(args[15]) : null;

// for backward compatibility, when host was not existing
if (eng.getDecisionUtil().isNumber(host)) {
timeout = eng.getCastUtil().toDoubleValue(host);
Expand All @@ -40,44 +66,17 @@ public static String call(PageContext pc, String bucketNameOrPath, String object
if (objectName != null && objectName.endsWith("/")) objectName = objectName.substring(0, objectName.length() - 1);
}

return s3.generatePresignedURL(bucketNameOrPath, objectName, expireDate).toExternalForm();
return s3.generatePresignedURL(bucketNameOrPath, objectName, expireDate, httpMethod, sseAlgorithm, sseCustomerKey, checksum, contentType, contentDisposition,
contentEncoding, versionId, zeroByteContent).toExternalForm();

}
catch (Exception e) {
throw eng.getCastUtil().toPageException(e);
}
}

@Override
public Object invoke(PageContext pc, Object[] args) throws PageException {
CFMLEngine engine = CFMLEngineFactory.getInstance();
Cast cast = engine.getCastUtil();

if (args.length == 7) {
return call(pc, cast.toString(args[0]), cast.toString(args[1]), args[2] == null ? null : cast.toDateTime(args[2], pc.getTimeZone()), cast.toString(args[3]),
cast.toString(args[4]), cast.toString(args[5]), cast.toDoubleValue(args[6]));
}
if (args.length == 6) {
return call(pc, cast.toString(args[0]), cast.toString(args[1]), args[2] == null ? null : cast.toDateTime(args[2], pc.getTimeZone()), cast.toString(args[3]),
cast.toString(args[4]), cast.toString(args[5]), 0);
}
if (args.length == 5) {
return call(pc, cast.toString(args[0]), cast.toString(args[1]), args[2] == null ? null : cast.toDateTime(args[2], pc.getTimeZone()), cast.toString(args[3]),
cast.toString(args[4]), null, 0);
}
if (args.length == 4) {
return call(pc, cast.toString(args[0]), cast.toString(args[1]), args[2] == null ? null : cast.toDateTime(args[2], pc.getTimeZone()), cast.toString(args[3]), null, null,
0);
}
if (args.length == 3) {
return call(pc, cast.toString(args[0]), cast.toString(args[1]), args[2] == null ? null : cast.toDateTime(args[2], pc.getTimeZone()), null, null, null, 0);
}
if (args.length == 2) {
return call(pc, cast.toString(args[0]), cast.toString(args[1]), null, null, null, null, 0);
}
if (args.length == 1) {
return call(pc, cast.toString(args[0]), null, null, null, null, null, 0);
}

throw engine.getExceptionUtil().createFunctionException(pc, "S3GeneratePresignedURL", 1, 7, args.length);
private boolean isEmpty(Object object) {
if (object instanceof CharSequence) Util.isEmpty(object.toString(), true);
return object == null;
}
}
Loading

0 comments on commit 44b1ef1

Please sign in to comment.