From 44b1ef16f7b99b5c76673f5598775d834a49b12d Mon Sep 17 00:00:00 2001 From: michaeloffner Date: Thu, 28 Sep 2023 12:39:37 +0200 Subject: [PATCH] LDEV-4707 . add addional arguments to the function --- source/fld/function.fld | 63 +++++++++ .../extension/resource/s3/AmazonS3Client.java | 13 +- .../org/lucee/extension/resource/s3/S3.java | 126 +++++++++++++++++- .../s3/function/S3GeneratePresignedURL.java | 69 +++++----- tests/functions/S3GeneratePresignedURL.cfc | 25 ++++ 5 files changed, 252 insertions(+), 44 deletions(-) diff --git a/source/fld/function.fld b/source/fld/function.fld index b041b4f..3a9a99f 100755 --- a/source/fld/function.fld +++ b/source/fld/function.fld @@ -848,6 +848,69 @@ datetime No Date time when the URL should expire. + + + httpMethod + method + string + No + The desired HTTP method (e.g., GET, PUT). Defaults to GET. + + + sseAlgorithm + algorithm + string + No + The desired server-side encryption algorithm, valid values are AES256 or KMS. + + + sseCustomerKey + customerKey + string + No + The server-side encryption customer-provided key. + + + checksum + contentMd5 + string + No + The base64 encoded MD5 checksum of the object's content. + + + type + contentType + string + No + The MIME type of the object (e.g., "text/plain"). + + + disposition + contentDisposition + string + No + Specifies presentational information for the object, like "attachment; filename=\"filename.pdf\"". + + + encoding + contentEncoding + string + No + Specifies content encodings applied to the object, like gzip. + + + version + versionId + string + No + The version ID of the object if versioning is enabled. + + + zeroByte + zeroByteContent + boolean + No + A flag to specify if the object has zero-byte content. accessKeyId diff --git a/source/java/src/org/lucee/extension/resource/s3/AmazonS3Client.java b/source/java/src/org/lucee/extension/resource/s3/AmazonS3Client.java index 372b46c..078c3d0 100644 --- a/source/java/src/org/lucee/extension/resource/s3/AmazonS3Client.java +++ b/source/java/src/org/lucee/extension/resource/s3/AmazonS3Client.java @@ -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(); @@ -91,6 +94,8 @@ public AmazonS3 create() throws S3Exception { } } + if (pathStyleAccess) builder.withPathStyleAccessEnabled(pathStyleAccess); + return builder.build(); } diff --git a/source/java/src/org/lucee/extension/resource/s3/S3.java b/source/java/src/org/lucee/extension/resource/s3/S3.java index 29f0184..d29895b 100755 --- a/source/java/src/org/lucee/extension/resource/s3/S3.java +++ b/source/java/src/org/lucee/extension/resource/s3/S3.java @@ -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; @@ -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) { @@ -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 { diff --git a/source/java/src/org/lucee/extension/resource/s3/function/S3GeneratePresignedURL.java b/source/java/src/org/lucee/extension/resource/s3/function/S3GeneratePresignedURL.java index cfa0e13..fd299e8 100644 --- a/source/java/src/org/lucee/extension/resource/s3/function/S3GeneratePresignedURL.java +++ b/source/java/src/org/lucee/extension/resource/s3/function/S3GeneratePresignedURL.java @@ -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); @@ -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; } } \ No newline at end of file diff --git a/tests/functions/S3GeneratePresignedURL.cfc b/tests/functions/S3GeneratePresignedURL.cfc index 2ca21f1..a3c3015 100644 --- a/tests/functions/S3GeneratePresignedURL.cfc +++ b/tests/functions/S3GeneratePresignedURL.cfc @@ -12,6 +12,18 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="s3" { http url=res result="local.res"; assertEquals(200, res.status_code); }); + it(title="checking function with a path containing dot notation bucket (not existing)", skip=isNotSupported(), body = function( currentSpec ) { + var cred=getCredentials(); + var res=S3GeneratePresignedURL( + path:"s3:///bundle.download/sentry-log4j-1.7.22.jar" + ,expire:dateAdd("n", 5, now()) + ,accessKeyId:cred.ACCESS_KEY_ID + ,secretAccessKey:cred.SECRET_KEY + ); + http url=res result="local.res"; + assertEquals(404, res.status_code); + }); + it(title="checking function with a bucketname/objectname",skip=isNotSupported(), body = function( currentSpec ) { var cred=getCredentials(); var res=S3GeneratePresignedURL( @@ -25,6 +37,19 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="s3" { assertEquals(200, res.status_code); }); + it(title="checking function with a bucketname/objectname with dot in bucketname",skip=isNotSupported(), body = function( currentSpec ) { + var cred=getCredentials(); + var res=S3GeneratePresignedURL( + bucketName:"bundle.download" + ,objectName:"sentry-log4j-1.7.22.jar" + ,expire:dateAdd("n", 5, now()) + ,accessKeyId:cred.ACCESS_KEY_ID + ,secretAccessKey:cred.SECRET_KEY + ); + http url=res result="local.res"; + assertEquals(404, res.status_code); + }); + it( title="should handle dots in bucket names ",skip=isNotSupported(), body=function(currentSpec){ var cred=getCredentials();