├── tests ├── .gitignore ├── src │ └── com │ │ └── amazon │ │ └── s3objectlambda │ │ └── defaultconfig │ │ ├── KeyConstants.java │ │ ├── Cleanup.java │ │ ├── SdkHelper.java │ │ ├── ObjectLambdaAccessPointTest.java │ │ └── SetupCloudFormationTest.java ├── getonly.xml ├── testng.xml └── pom.xml ├── function ├── python_3_9 │ ├── src │ │ ├── checksum │ │ │ ├── __init__.py │ │ │ └── checksum.py │ │ ├── handler │ │ │ ├── __init__.py │ │ │ └── get_object_handler.py │ │ ├── transform │ │ │ ├── __init__.py │ │ │ └── transform.py │ │ ├── request │ │ │ ├── __init__.py │ │ │ ├── validator.py │ │ │ └── utils.py │ │ ├── error │ │ │ ├── __init__.py │ │ │ └── error_response.py │ │ ├── response │ │ │ ├── mapper_response.py │ │ │ ├── __init__.py │ │ │ ├── part_number_mapper.py │ │ │ └── range_mapper.py │ │ └── s3objectlambda.py │ ├── requirements.txt │ ├── test │ │ ├── __init__.py │ │ ├── test_part_number.py │ │ ├── test_range.py │ │ └── test_utils.py │ └── .gitignore ├── java17 │ ├── src │ │ ├── .gitkeep │ │ ├── main │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── s3objectlambda │ │ │ │ ├── request │ │ │ │ ├── RequestHandler.java │ │ │ │ ├── GetObjectRequestWrapper.java │ │ │ │ ├── UserRequestWrapper.java │ │ │ │ └── S3PresignedUrlParserHelper.java │ │ │ │ ├── error │ │ │ │ ├── ErrorParser.java │ │ │ │ ├── S3RequestError.java │ │ │ │ ├── Error.java │ │ │ │ └── XMLErrorParser.java │ │ │ │ ├── exception │ │ │ │ ├── InvalidRangeException.java │ │ │ │ ├── InvalidPartNumberException.java │ │ │ │ ├── RequestException.java │ │ │ │ └── TransformationException.java │ │ │ │ ├── checksum │ │ │ │ ├── ChecksumGenerator.java │ │ │ │ ├── Md5Checksum.java │ │ │ │ └── Checksum.java │ │ │ │ ├── validator │ │ │ │ ├── RequestValidator.java │ │ │ │ └── GetObjectRequestValidator.java │ │ │ │ ├── response │ │ │ │ ├── ResponseHandler.java │ │ │ │ └── GetObjectResponseHandler.java │ │ │ │ ├── transform │ │ │ │ ├── Transformer.java │ │ │ │ ├── Range.java │ │ │ │ ├── PartNumberMapper.java │ │ │ │ ├── GetObjectTransformer.java │ │ │ │ └── RangeMapper.java │ │ │ │ └── Handler.java │ │ └── test │ │ │ ├── resources │ │ │ └── mock_responses │ │ │ │ └── mock_s3_error_response.txt │ │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── s3objectlambda │ │ │ ├── checksum │ │ │ └── Md5ChecksumTest.java │ │ │ ├── exception │ │ │ ├── InvalidRangeExceptionTest.java │ │ │ └── InvalidPartNumberExceptionTest.java │ │ │ ├── request │ │ │ ├── GetObjectRequestTest.java │ │ │ ├── S3PresignedUrlParserHelperTest.java │ │ │ └── GetObjectHandlerTest.java │ │ │ ├── transform │ │ │ ├── RangeTest.java │ │ │ ├── PartNumberMapperTest.java │ │ │ ├── TransformTest.java │ │ │ └── RangeMapperTest.java │ │ │ ├── validator │ │ │ └── RequestValidatorTest.java │ │ │ └── error │ │ │ └── S3RequestErrorTest.java │ ├── .gitignore │ └── pom.xml └── nodejs_20_x │ ├── .gitignore │ ├── release │ └── s3objectlambda_deployment_package.zip │ ├── jest.config.js │ ├── src │ ├── response │ │ ├── range_response.types.ts │ │ ├── part_number_mapper.ts │ │ ├── param_transformer.ts │ │ └── range_mapper.ts │ ├── s3objectlambda_response.types.ts │ ├── checksum │ │ └── checksum.ts │ ├── error │ │ ├── error_code.ts │ │ └── error_response.ts │ ├── request │ │ ├── validator.ts │ │ └── utils.ts │ ├── transform │ │ └── s3objectlambda_transformer.ts │ ├── s3objectlambda_list_type.ts │ ├── s3objectlambda_event.types.ts │ ├── utils │ │ └── listobject_xml_transformer.ts │ ├── s3objectlambda.ts │ └── handler │ │ ├── head_object_handler.ts │ │ ├── list_objects_base_handler.ts │ │ └── get_object_handler.ts │ ├── tst │ ├── checksum │ │ └── checksum.test.ts │ ├── transform │ │ └── s3objectlambda_transformer.test.ts │ ├── response │ │ ├── param_transformer.test.ts │ │ ├── part_number_mapper.test.ts │ │ └── range_mapper.test.ts │ └── request │ │ ├── validator.test.ts │ │ └── utils.test.ts │ ├── .eslintrc.yml │ ├── tsconfig.json │ └── package.json ├── .gitignore ├── CODE_OF_CONDUCT.md ├── .github ├── ISSUE_TEMPLATE │ └── bug-report-template.md └── workflows │ └── run_integration_tests.yaml ├── LICENSE ├── pull_request_template.md ├── CONTRIBUTING.md └── template └── s3objectlambda_defaultconfig.yaml /tests/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | my.secert -------------------------------------------------------------------------------- /function/python_3_9/src/checksum/__init__.py: -------------------------------------------------------------------------------- 1 | from .checksum import * 2 | -------------------------------------------------------------------------------- /function/python_3_9/src/handler/__init__.py: -------------------------------------------------------------------------------- 1 | from . import get_object_handler 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | tests/src/.gitkeep 4 | tests/target 5 | template/output -------------------------------------------------------------------------------- /function/python_3_9/src/transform/__init__.py: -------------------------------------------------------------------------------- 1 | from .transform import transform_object 2 | -------------------------------------------------------------------------------- /function/python_3_9/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3~=1.20.18 2 | requests~=2.31.0 3 | setuptools~=65.5.1 4 | -------------------------------------------------------------------------------- /function/python_3_9/src/request/__init__.py: -------------------------------------------------------------------------------- 1 | from . import utils 2 | from . import validator 3 | 4 | -------------------------------------------------------------------------------- /function/java17/src/.gitkeep: -------------------------------------------------------------------------------- 1 | Feel free to delete this file as soon as actual Java code is added to this directory. 2 | -------------------------------------------------------------------------------- /function/python_3_9/src/error/__init__.py: -------------------------------------------------------------------------------- 1 | from .error_response import write_error_response_for_s3, write_error_response 2 | -------------------------------------------------------------------------------- /function/nodejs_20_x/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | node_modules/ 3 | build 4 | dist/ 5 | s3objectlambda.js 6 | .vscode/ 7 | .idea/ 8 | coverage 9 | -------------------------------------------------------------------------------- /function/python_3_9/src/response/mapper_response.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | MapperResponse = namedtuple('MapperResponse', ['hasError', 'object', 'error_msg']) -------------------------------------------------------------------------------- /function/python_3_9/src/response/__init__.py: -------------------------------------------------------------------------------- 1 | from .mapper_response import MapperResponse 2 | from .part_number_mapper import map_part_number 3 | from .range_mapper import split_range_str, validate_range_str, map_range 4 | -------------------------------------------------------------------------------- /function/nodejs_20_x/release/s3objectlambda_deployment_package.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-s3-object-lambda-default-configuration/HEAD/function/nodejs_20_x/release/s3objectlambda_deployment_package.zip -------------------------------------------------------------------------------- /function/python_3_9/test/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | TEST_DIR = os.path.dirname(os.path.abspath(__file__)) 5 | PROJECT_DIR = os.path.abspath(os.path.join(TEST_DIR, os.pardir)) 6 | sys.path.insert(0, PROJECT_DIR) -------------------------------------------------------------------------------- /function/nodejs_20_x/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | transform: { 6 | '^.+\\.(ts|tsx)$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/response/range_response.types.ts: -------------------------------------------------------------------------------- 1 | import ErrorCode from '../error/error_code'; 2 | 3 | export interface RangeResponse { 4 | object?: Buffer 5 | headers?: Map 6 | hasError: boolean 7 | errorCode?: ErrorCode 8 | errorMessage?: string 9 | } 10 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/request/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.request; 2 | 3 | /** 4 | * This interface should be implemented by the class that handles the user request. 5 | */ 6 | public interface RequestHandler { 7 | void handleRequest() throws Exception; 8 | } 9 | -------------------------------------------------------------------------------- /function/python_3_9/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | *.swp 4 | 5 | *.DS_Store 6 | 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.egg-info/ 11 | 12 | /.coverage 13 | /.coverage.* 14 | /.cache 15 | /.pytest_cache 16 | /.mypy_cache 17 | 18 | /doc/_apidoc/ 19 | /build 20 | 21 | /release/lambda 22 | /release/package 23 | -------------------------------------------------------------------------------- /function/java17/src/test/resources/mock_responses/mock_s3_error_response.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | NoSuchMockKey 4 | The resource you requested does not exist 5 | /mybucket/myfoto.jpg 6 | 4442587FB7D0A2F9 7 | 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/error/ErrorParser.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.error; 2 | 3 | /** 4 | * This interface should be implemented by any parser that parses the error response from S3 getObject request. 5 | */ 6 | public interface ErrorParser { 7 | S3RequestError parse(String errorResponse) throws Exception; 8 | } 9 | -------------------------------------------------------------------------------- /function/nodejs_20_x/tst/checksum/checksum.test.ts: -------------------------------------------------------------------------------- 1 | import getChecksum from '../../src/checksum/checksum'; 2 | 3 | test('Checksum is MD5 and works', () => { 4 | const algorithm = 'md5'; 5 | const sample = 'sample'; 6 | const md5Sample = '5e8ff9bf55ba3508199d22e984129be6'; 7 | expect(getChecksum(Buffer.from(sample))).toEqual({ 8 | algorithm, 9 | digest: md5Sample 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /function/nodejs_20_x/tst/transform/ s3objectlambda_transformer.test.ts: -------------------------------------------------------------------------------- 1 | import { transformObject } from '../../src/transform/s3objectlambda_transformer'; 2 | 3 | test('Transform function returns same object', () => { 4 | expect(transformObject(Buffer.from('test-object'))).toStrictEqual( 5 | Buffer.from('test-object') 6 | ); 7 | }); 8 | 9 | // TODO Add tests for the other identity functions 10 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/exception/InvalidRangeException.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.exception; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | 5 | public class InvalidRangeException extends RequestException { 6 | 7 | public InvalidRangeException(String message) { 8 | super(message); 9 | this.setError(Error.INVALID_RANGE); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/exception/InvalidPartNumberException.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.exception; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | 5 | public class InvalidPartNumberException extends RequestException { 6 | public InvalidPartNumberException(String message) { 7 | super(message); 8 | this.setError(Error.INVALID_PART); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/checksum/ChecksumGenerator.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.checksum; 2 | 3 | /** 4 | * This interface represents the checksum generators for the response. 5 | * The implementing class method should return the Checksum object using the respective algorithm. 6 | */ 7 | public interface ChecksumGenerator { 8 | Checksum getChecksum(byte[] objectResponse) throws Exception; 9 | } 10 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/validator/RequestValidator.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.validator; 2 | import java.util.Optional; 3 | 4 | /** 5 | * This interface will be implemented by the respective user request validator class. 6 | * The method, validateUserRequest() should validate the user request. 7 | * 8 | */ 9 | public interface RequestValidator { 10 | Optional validateUserRequest(); 11 | } 12 | -------------------------------------------------------------------------------- /function/python_3_9/src/transform/transform.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | At present, this function simply passes back the original object without performing any transformations. 4 | Implement this function to add your custom logic to transform objects stored in Amazon S3. 5 | """ 6 | 7 | 8 | def transform_object(original_object): 9 | # TODO: Implement your own transform function 10 | transformed_object = original_object 11 | return transformed_object 12 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/s3objectlambda_response.types.ts: -------------------------------------------------------------------------------- 1 | export interface IResponse { 2 | statusCode: number 3 | } 4 | 5 | export interface IHeadObjectResponse extends IResponse { 6 | metadata?: object 7 | headers: object 8 | } 9 | 10 | export interface IListObjectsResponse extends IResponse{ 11 | listResultXml: string 12 | } 13 | 14 | export interface IErrorResponse extends IResponse { 15 | readonly errorCode?: string 16 | readonly errorMessage?: string 17 | } 18 | -------------------------------------------------------------------------------- /function/java17/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .idea 3 | *.iml 4 | .DS_Store 5 | /out 6 | target/ 7 | pom.xml.tag 8 | pom.xml.releaseBackup 9 | pom.xml.versionsBackup 10 | pom.xml.next 11 | release.properties 12 | dependency-reduced-pom.xml 13 | buildNumber.properties 14 | .mvn/timing.properties 15 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 16 | .mvn/wrapper/maven-wrapper.jar 17 | 18 | # Eclipse m2e generated files 19 | # Eclipse Core 20 | .project 21 | # JDT-specific (Eclipse Java Development Tools) 22 | .classpath -------------------------------------------------------------------------------- /function/python_3_9/src/checksum/checksum.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from collections import namedtuple 3 | 4 | 5 | def get_checksum(object): 6 | """ 7 | Get the md5 checksum of an object 8 | :param object: The object you would like to obtain checksum 9 | :return: Checksum object with algorithm name and digest 10 | """ 11 | hash_md5 = hashlib.md5() 12 | hash_md5.update(object) 13 | Checksum = namedtuple('Checksum', ['algorithm', 'digest']) 14 | return Checksum(hash_md5.name, hash_md5.hexdigest()) 15 | -------------------------------------------------------------------------------- /function/nodejs_20_x/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | - standard-with-typescript 6 | parser: '@typescript-eslint/parser' 7 | parserOptions: 8 | ecmaVersion: 2020 9 | sourceType: module 10 | project: ./tsconfig.json 11 | plugins: 12 | - '@typescript-eslint' 13 | - 'autofix' 14 | rules: { 15 | autofix/no-debugger: 'error', 16 | '@typescript-eslint/return-await': 'off', 17 | no-return-await: 'error', 18 | semi: [2, 'always'], 19 | '@typescript-eslint/semi': 'off' 20 | } 21 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/checksum/checksum.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | 3 | const CHECKSUM_ALGORITHM = 'md5'; 4 | 5 | interface Checksum { 6 | algorithm: string 7 | digest: string 8 | } 9 | 10 | /** 11 | * Generates a checksum for the given object. 12 | * 13 | * @param object 14 | */ 15 | export default function getChecksum (object: Buffer): Checksum { 16 | const hash = createHash(CHECKSUM_ALGORITHM); 17 | hash.update(object); 18 | return { algorithm: CHECKSUM_ALGORITHM, digest: hash.digest('hex') }; 19 | } 20 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/error/error_code.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The list of error codes returned from the Lambda function. We use the same error codes that are 3 | * supported by Amazon S3, where possible. 4 | * 5 | * See {@link https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html | Amazon S3 Error Responses} 6 | * for more information. 7 | */ 8 | enum ErrorCode { 9 | INVALID_REQUEST = 'InvalidRequest', 10 | INVALID_RANGE = 'InvalidRange', 11 | INVALID_PART = 'InvalidPart', 12 | NO_SUCH_KEY = 'NoSuchKey', 13 | } 14 | 15 | export default ErrorCode; 16 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/exception/RequestException.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.exception; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | 5 | public abstract class RequestException extends Exception { 6 | 7 | private Error error; 8 | 9 | public RequestException(String errorMessage) { 10 | super(errorMessage); 11 | } 12 | 13 | public Error getError() { 14 | return error; 15 | } 16 | 17 | public void setError(Error error) { 18 | this.error = error; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/request/validator.ts: -------------------------------------------------------------------------------- 1 | import { getPartNumber, getRange } from './utils'; 2 | import { UserRequest } from '../s3objectlambda_event.types'; 3 | 4 | /** 5 | * Responsible for validating the user request. Returns a string error message if there are errors or null if valid. 6 | */ 7 | export function validate (userRequest: UserRequest): string | null { 8 | const range = getRange(userRequest); 9 | const partNumber = getPartNumber(userRequest); 10 | 11 | if (range != null && partNumber != null) { 12 | return 'Cannot specify both Range header and partNumber query parameter'; 13 | } 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report Template 3 | about: Create a report to help us improve 4 | title: Enter Issue or Bug Title 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Expected Behaviour* 11 | 12 | *Actual Behaviour* 13 | 14 | *Steps to Reproduce the Problem* 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 20 | *Is this a security issue?* 21 | 22 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our vulnerability reporting page (http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. Please do not create a public GitHub issue. 23 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/exception/TransformationException.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.exception; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | 5 | /** 6 | * This exception class represents any exception related to, or that occurs during the transformation of the object. 7 | */ 8 | public class TransformationException extends RequestException { 9 | public TransformationException(String message) { 10 | super(message); 11 | this.setError(Error.SERVER_ERROR); 12 | } 13 | 14 | public TransformationException(String message, Error error) { 15 | super(message); 16 | this.setError(error); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /function/nodejs_20_x/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["es2016", "es2017.object", "es2017.string", "dom"], 7 | "declaration": true, 8 | "outDir": "./dist/src", 9 | "declarationDir": "./dist/types", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "noImplicitThis": true, 15 | "alwaysStrict": true, 16 | "noImplicitReturns": true, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization":false, 21 | "typeRoots": ["./node_modules/@types"] 22 | }, 23 | "exclude": ["dist"] 24 | } -------------------------------------------------------------------------------- /tests/src/com/amazon/s3objectlambda/defaultconfig/KeyConstants.java: -------------------------------------------------------------------------------- 1 | package com.amazon.s3objectlambda.defaultconfig; 2 | 3 | /** 4 | * Holds Constant key value for IContext to pass around resources name between test groups. 5 | */ 6 | public final class KeyConstants { 7 | /** 8 | * IContext Key for Cloudformation Stack name. 9 | */ 10 | public static final String STACK_NAME_KEY = "STACK_NAME_KEY"; 11 | /** 12 | * IContext Key for support Access Point name. 13 | */ 14 | public static final String SUPPORT_AP_NAME_KEY = "SUPPORT_AP_NAME_KEY"; 15 | /** 16 | * IContext Key for Object Lambda Access Point name. 17 | */ 18 | public static final String OL_AP_NAME_KEY = "OL_AP_NAME_KEY"; 19 | 20 | private KeyConstants() { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/response/ResponseHandler.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.response; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | 5 | import java.io.InputStream; 6 | import java.net.http.HttpResponse; 7 | 8 | /** 9 | * This interface represents the response handler. 10 | * The implementing class will be handling updating of the get object response with the transformed object 11 | * and writing error response. 12 | */ 13 | public interface ResponseHandler { 14 | void writeS3GetObjectErrorResponse(HttpResponse presignedResponse); 15 | void writeErrorResponse(String errorMessage, Error error); 16 | void writeObjectResponse(HttpResponse presignedResponse, byte[] responseObjectByteArray); 17 | } 18 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/transform/Transformer.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | 4 | import com.example.s3objectlambda.exception.InvalidPartNumberException; 5 | import com.example.s3objectlambda.exception.InvalidRangeException; 6 | import com.example.s3objectlambda.exception.TransformationException; 7 | 8 | import java.net.URISyntaxException; 9 | 10 | /** 11 | * This interface should be implemented by the class that transforms the response. 12 | */ 13 | public interface Transformer { 14 | byte[] transformObjectResponse(byte[] responseObjectByteArray) throws TransformationException; 15 | 16 | byte[] applyRangeOrPartNumber(byte[] responseObjectByteArray) 17 | throws URISyntaxException, InvalidRangeException, InvalidPartNumberException; 18 | } 19 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/checksum/Md5Checksum.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.checksum; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | import java.util.Base64; 6 | 7 | /** 8 | * Generates an MD5 checksum for the given object response. 9 | */ 10 | public class Md5Checksum implements ChecksumGenerator { 11 | 12 | private static final String ALGORITHM = "MD5"; 13 | public Checksum getChecksum(byte[] objectResponse) throws NoSuchAlgorithmException { 14 | 15 | MessageDigest md; 16 | md = MessageDigest.getInstance(ALGORITHM); 17 | var digest = md.digest(objectResponse); 18 | 19 | var checksum = Base64.getEncoder().encodeToString(digest); 20 | return new Checksum(ALGORITHM, checksum); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/checksum/Checksum.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.checksum; 2 | 3 | /** 4 | * This class represents the checksum for the response object using any algorithm that the generator implements. 5 | */ 6 | public class Checksum { 7 | 8 | private String algorithm; 9 | private String checksum; 10 | 11 | public Checksum(String algorithm, String checksum) { 12 | this.algorithm = algorithm; 13 | this.checksum = checksum; 14 | } 15 | 16 | public String getChecksum() { 17 | return checksum; 18 | } 19 | 20 | public void setChecksum(String checksum) { 21 | this.checksum = checksum; 22 | } 23 | 24 | public String getAlgorithm() { 25 | return algorithm; 26 | } 27 | 28 | public void setAlgorithm(String algorithm) { 29 | this.algorithm = algorithm; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /function/nodejs_20_x/tst/response/param_transformer.test.ts: -------------------------------------------------------------------------------- 1 | import { headerToWgorParam } from '../../src/response/param_transformer'; 2 | 3 | test('headerToWgorParam works for one dash', () => { 4 | expect(headerToWgorParam('content-type')).toStrictEqual('ContentType'); 5 | }); 6 | 7 | test('headerToWgorParam works for two dashes', () => { 8 | expect(headerToWgorParam('x-amz-server-side-encryption')).toStrictEqual('ServerSideEncryption'); 9 | }); 10 | 11 | test('headerToWgorParam works for CRC', () => { 12 | expect(headerToWgorParam('x-amz-checksum-crc32')).toStrictEqual('ChecksumCRC32'); 13 | expect(headerToWgorParam('x-amz-checksum-crc32c')).toStrictEqual('ChecksumCRC32C'); 14 | }); 15 | 16 | test('headerToWgorParam works for SHA', () => { 17 | expect(headerToWgorParam('x-amz-checksum-sha1')).toStrictEqual('ChecksumSHA1'); 18 | expect(headerToWgorParam('x-amz-checksum-sha256')).toStrictEqual('ChecksumSHA256'); 19 | }); 20 | -------------------------------------------------------------------------------- /function/python_3_9/src/request/validator.py: -------------------------------------------------------------------------------- 1 | from request import utils 2 | from response import range_mapper 3 | from collections import namedtuple 4 | 5 | RequestsValidation = namedtuple('RequestsValidation', ['is_valid', 'error_msg']) 6 | 7 | 8 | def validate_request(user_request): 9 | """ 10 | Validate the user request 11 | :param user_request: User request received from AWS Object Lambda 12 | :return: RequestsValidation Object 13 | """ 14 | range_request = utils.get_range(user_request) 15 | part_number_request = utils.get_part_number(user_request) 16 | 17 | if range_request and part_number_request: 18 | return RequestsValidation(False, 'Cannot specify both Range and Part Number in Query') 19 | if range_request and not range_mapper.validate_range_str(range_request): 20 | return RequestsValidation(False, 'Cannot process specific range: {}'.format(range_request)) 21 | return RequestsValidation(True, '') 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/error/S3RequestError.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.error; 2 | 3 | /** 4 | * This class represents the S3 Error response. 5 | * Each error attribute can be accessed using the get method. 6 | * 7 | * @see https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#RESTErrorResponses 8 | */ 9 | 10 | public class S3RequestError { 11 | 12 | private String code; 13 | private String message; 14 | private String requestId; 15 | 16 | public void setCode(String code) { 17 | this.code = code; 18 | } 19 | 20 | public void setMessage(String message) { 21 | this.message = message; 22 | } 23 | 24 | public void setRequestId(String requestId) { 25 | this.requestId = requestId; 26 | } 27 | 28 | public String getCode() { 29 | return this.code; 30 | } 31 | 32 | public String getMessage() { 33 | return this.message; 34 | } 35 | 36 | public String getRequestId() { 37 | return this.requestId; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /function/nodejs_20_x/tst/request/validator.test.ts: -------------------------------------------------------------------------------- 1 | import { validate } from '../../src/request/validator'; 2 | import { UserRequest } from '../../src/s3objectlambda_event.types'; 3 | 4 | test('Validation fails when both partNumber and range is provided', () => { 5 | const userRequest: UserRequest = { 6 | url: 'https://s3.amazonaws.com?partNumber=1', 7 | headers: { h1: 'v1', Range: 'bytes=2-' } 8 | }; 9 | 10 | expect(validate(userRequest)).toBe('Cannot specify both Range header and partNumber query parameter'); 11 | }); 12 | 13 | test('Validation is successful when partNumber is provided', () => { 14 | const userRequest: UserRequest = { 15 | url: 'https://s3.amazonaws.com?partNumber=1', 16 | headers: { h1: 'v1'} 17 | }; 18 | 19 | expect(validate(userRequest)).toBeNull(); 20 | }); 21 | 22 | test('Validation is successful when Range is provided', () => { 23 | const userRequest: UserRequest = { 24 | url: 'https://s3.amazonaws.com', 25 | headers: { h1: 'v1', Range: 'bytes=2-' } 26 | }; 27 | 28 | expect(validate(userRequest)).toBeNull(); 29 | }); 30 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/transform/s3objectlambda_transformer.ts: -------------------------------------------------------------------------------- 1 | import { IListObjectsV1, IListObjectsV2 } from '../s3objectlambda_list_type'; 2 | 3 | /* 4 | At present, this function simply passes back the original object without performing any transformations. 5 | Implement this function to add your custom logic to transform objects stored in Amazon S3. 6 | */ 7 | export function transformObject (originalObject: Buffer): Buffer { 8 | // TODO: Implement your transformation logic here. 9 | return originalObject; 10 | } 11 | 12 | export function transformListObjectsV1 (originalList: IListObjectsV1): IListObjectsV1 { 13 | // TODO: Implement your transformation logic here. 14 | return originalList; 15 | } 16 | 17 | export function transformListObjectsV2 (originalList: IListObjectsV2): IListObjectsV2 { 18 | // TODO: Implement your transformation logic here. 19 | return originalList; 20 | } 21 | 22 | export function transformHeaders (originalHeaders: Map): Map { 23 | // TODO: Implement your transformation logic here. 24 | return originalHeaders; 25 | } 26 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/checksum/Md5ChecksumTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.checksum; 2 | 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | import java.security.MessageDigest; 8 | import java.security.NoSuchAlgorithmException; 9 | import java.util.Base64; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | 14 | public class Md5ChecksumTest { 15 | 16 | @Test 17 | public void getChecksumTest() throws NoSuchAlgorithmException { 18 | var originalData = "12345678910!".repeat(1000); 19 | var responseInputStream = originalData.getBytes(StandardCharsets.UTF_16); 20 | 21 | 22 | var md5Hash = MessageDigest.getInstance("MD5").digest(responseInputStream); 23 | 24 | var expectedDigestString = Base64.getEncoder().encodeToString(md5Hash); 25 | 26 | var checksum = new Md5Checksum().getChecksum(responseInputStream); 27 | var digest = checksum.getChecksum(); 28 | 29 | assertEquals(expectedDigestString, digest); 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/error/Error.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.error; 2 | 3 | /** 4 | * The list of error codes returned from the Lambda function. We use the same error codes that are 5 | * supported by Amazon S3, where possible. 6 | * 7 | * @see List of Error Codes 8 | * for more information. 9 | * 10 | */ 11 | public enum Error { 12 | 13 | INVALID_REQUEST(400, "InvalidRequest"), 14 | INVALID_RANGE(416, "InvalidRange"), 15 | INVALID_PART(400, "InvalidPart"), 16 | NO_SUCH_KEY(404, "NoSuchKey"), 17 | SERVER_ERROR(500, "LambdaRuntimeError"); 18 | 19 | private final Integer statusCode; 20 | private final String errorCode; 21 | 22 | Error(Integer statusCode, String errorCode) { 23 | this.statusCode = statusCode; 24 | this.errorCode = errorCode; 25 | } 26 | 27 | public String getErrorCode() { 28 | return this.errorCode; 29 | } 30 | 31 | public Integer getStatusCode() { 32 | return this.statusCode; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/s3objectlambda_list_type.ts: -------------------------------------------------------------------------------- 1 | export interface IBaseListObject { 2 | IsTruncated: boolean 3 | EncodingType: string 4 | MaxKeys: number 5 | Prefix: string 6 | Contents: IObject[] 7 | Delimiter: string 8 | CommonPrefixes: ICommonPrefix[] 9 | } 10 | 11 | export interface IOwner { 12 | DisplayName?: string 13 | ID?: string 14 | } 15 | 16 | export interface IObject { 17 | ChecksumAlgorithm?: 'CRC32' | 'CRC32C' | 'SHA1' | 'SHA256' 18 | ETag?: string 19 | Key?: string 20 | LastModified?: string 21 | Size?: number 22 | StorageClass?: 'STANDARD' | 'REDUCED_REDUNDANCY' | 'GLACIER' | 'STANDARD_IA' | 'ONEZONE_IA' | 23 | 'INTELLIGENT_TIERING' | 'DEEP_ARCHIVE' | 'OUTPOSTS' | 'GLACIER_IR' 24 | Owner?: IOwner 25 | } 26 | 27 | export interface IListObjectsV1 extends IBaseListObject { 28 | Marker: string 29 | NextMarker: string 30 | } 31 | 32 | export interface IListObjectsV2 extends IBaseListObject { 33 | ContinuationToken: string 34 | NextContinuationToken: string 35 | StartAfter: string 36 | KeyCount: string 37 | } 38 | 39 | export interface ICommonPrefix { 40 | Prefix: string 41 | } 42 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/request/GetObjectRequestWrapper.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.request; 2 | 3 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 4 | 5 | import java.net.URISyntaxException; 6 | import java.util.Optional; 7 | 8 | /** 9 | * This class implements UserRequest and represents "getObject" user request. 10 | */ 11 | public class GetObjectRequestWrapper extends UserRequestWrapper { 12 | 13 | private static final String RANGE = "Range"; 14 | private static final String PART_NUMBER = "partNumber"; 15 | 16 | public GetObjectRequestWrapper(S3ObjectLambdaEvent.UserRequest userRequest) { 17 | super(userRequest); 18 | } 19 | 20 | public Optional getPartNumber() throws URISyntaxException { 21 | return this.getQueryParam(this.getUserRequest().getUrl(), PART_NUMBER); 22 | } 23 | 24 | public Optional getRange() throws URISyntaxException { 25 | var range = this.getUserRequest().getHeaders().get(RANGE); 26 | 27 | if (range == null) { 28 | return this.getQueryParam(this.getUserRequest().getUrl(), RANGE); 29 | } else { 30 | return Optional.of(range); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /function/python_3_9/src/response/part_number_mapper.py: -------------------------------------------------------------------------------- 1 | import math 2 | from response import MapperResponse 3 | 4 | 5 | DEFAULT_SIZE = 5242880 6 | 7 | 8 | def get_part_error_response(number, total): 9 | """Get a MapperResponse for Errors""" 10 | return MapperResponse(hasError=True, object=None, 11 | error_msg='Cannot specify part number: {}. Use part numbers 1 to {}.'.format(number, total)) 12 | 13 | 14 | def map_part_number(transformed_object, part_number): 15 | """ 16 | Map the part number of an object 17 | :param transformed_object: transformed object 18 | :param part_number: part number request string 19 | :return: MapperResponse 20 | """ 21 | object_length = len(transformed_object) 22 | total_part = math.ceil(object_length / DEFAULT_SIZE) 23 | try: 24 | part_number = int(part_number) 25 | except ValueError: 26 | return get_part_error_response(part_number, total_part) 27 | if part_number > total_part or part_number < 0: 28 | return get_part_error_response(part_number, total_part) 29 | 30 | start = (part_number - 1) * DEFAULT_SIZE 31 | end = min(start + DEFAULT_SIZE, object_length) 32 | return MapperResponse(hasError=False, object=transformed_object[start:end], 33 | error_msg=None) 34 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | # **Description** 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | Fixes # (issue) 5 | 6 | ## **Type of change** 7 | 8 | Please delete options that are not relevant. 9 | 10 | * Bug fix (non-breaking change which fixes an issue) 11 | * New feature (non-breaking change which adds functionality) 12 | * Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | * This change requires a documentation update 14 | 15 | # **How Has This Been Tested?** 16 | 17 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. 18 | 19 | * Test A 20 | * Test B 21 | 22 | # **Checklist** 23 | 24 | * My code follows the style guidelines of this project. 25 | * I have performed a self-review of my own code. 26 | * I have commented my code, particularly in hard-to-understand areas. 27 | * I have made corresponding changes to the documentation. 28 | * My changes generate no new warnings. 29 | * I have added tests that prove my fix is effective or that my feature works. 30 | * New and existing unit tests pass locally with my changes. 31 | 32 | -------------------------------------------------------------------------------- /function/python_3_9/test/test_part_number.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.response.part_number_mapper import * 3 | 4 | TEN_KB = 'helloworld' * 1024 5 | HUNDRED_KB = TEN_KB * 10 6 | 7 | def create_buffer(string): 8 | return bytes(string, encoding='utf8') 9 | 10 | 11 | def test_invalid_part_number(): 12 | part_number_response = map_part_number(create_buffer('Hello'), 'abc') 13 | assert part_number_response.hasError 14 | 15 | 16 | def test_negative_part_number(): 17 | part_number_response = map_part_number(create_buffer('Hello'), '-1') 18 | assert part_number_response.hasError 19 | 20 | 21 | def test_positive_non_integeer(): 22 | part_number_response = map_part_number(create_buffer('Hello'), '1.1') 23 | assert part_number_response.hasError 24 | 25 | 26 | def test_large_part_number(): 27 | part_number_response = map_part_number(create_buffer('Hello'), '10') 28 | assert part_number_response.hasError 29 | 30 | 31 | def test_valid_part_number(): 32 | part_number_response = map_part_number(create_buffer(HUNDRED_KB), '1') 33 | assert part_number_response.object == create_buffer(TEN_KB) 34 | 35 | 36 | def test_part_number_if_length_not_divisible(): 37 | part_number_response = map_part_number(create_buffer(HUNDRED_KB + 'hundredkilobytes'), '2') 38 | assert part_number_response.object == create_buffer(TEN_KB) -------------------------------------------------------------------------------- /function/python_3_9/src/s3objectlambda.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | import boto3 5 | import logging 6 | from handler import get_object_handler 7 | 8 | 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | s3_client = boto3.client('s3') 13 | 14 | 15 | def handler(event, context): 16 | """ handles the request from Amazon S3 Object Lambda""" 17 | 18 | """ 19 |

The getObjectContext object contains information about the GetObject request, 20 | which resulted in this Lambda function being invoked.

21 |

The userRequest object contains information related to the entity (user or application) 22 | that invoked Amazon S3 Object Lambda. This information can be used in multiple ways, for example, to allow or deny 23 | the request based on the entity. See the Respond with a 403 Forbidden example in 24 | {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/olap-writing-lambda.html|Writing Lambda functions} 25 | for sample code.

26 | """ 27 | if "getObjectContext" in event: 28 | return get_object_handler.get_object_handler(s3_client, event["getObjectContext"], event["userRequest"]) 29 | 30 | # There is nothing to return once the data has been sent to Amazon S3 Object Lambda, so just return None. 31 | return None 32 | 33 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/request/UserRequestWrapper.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.request; 2 | 3 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 4 | import org.apache.http.NameValuePair; 5 | import org.apache.http.client.utils.URLEncodedUtils; 6 | 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | import java.util.Optional; 10 | 11 | /** 12 | * This class is a wrapper over original Lambda event UserRequest. 13 | */ 14 | public class UserRequestWrapper { 15 | public void setUserRequest(S3ObjectLambdaEvent.UserRequest userRequest) { 16 | this.userRequest = userRequest; 17 | } 18 | 19 | private S3ObjectLambdaEvent.UserRequest userRequest; 20 | 21 | public UserRequestWrapper(S3ObjectLambdaEvent.UserRequest userRequest) { 22 | this.userRequest = userRequest; 23 | } 24 | public S3ObjectLambdaEvent.UserRequest getUserRequest() { 25 | return this.userRequest; 26 | } 27 | 28 | public Optional getQueryParam(String url, String partNumber) throws URISyntaxException { 29 | var params = URLEncodedUtils.parse(new URI(url), "UTF-8"); 30 | for (NameValuePair param : params) { 31 | if (param.getName().equals(partNumber)) { 32 | return Optional.of(param.getValue()); 33 | } 34 | } 35 | return Optional.empty(); 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tests/getonly.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/exception/InvalidRangeExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.exception; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | 10 | public class InvalidRangeExceptionTest { 11 | @Test 12 | @DisplayName("Test invalid range error") 13 | void testRangeGetError() { 14 | try { 15 | throw new InvalidRangeException("Invalid Range"); 16 | } catch (InvalidRangeException e) { 17 | assertEquals(Error.INVALID_RANGE, e.getError()); 18 | } 19 | } 20 | 21 | @Test 22 | @DisplayName("Test invalid range error message") 23 | void testRangeGetMessage() { 24 | try { 25 | throw new InvalidRangeException("Invalid Range Expected"); 26 | } catch (InvalidRangeException e) { 27 | assertEquals("Invalid Range Expected", e.getMessage()); 28 | } 29 | } 30 | 31 | @Test 32 | @DisplayName("Test invalid range exception to string.") 33 | void testRangeToString() { 34 | try { 35 | throw new InvalidRangeException("Invalid Range Expected"); 36 | } catch (InvalidRangeException e) { 37 | assertEquals("com.example.s3objectlambda.exception.InvalidRangeException: " + 38 | "Invalid Range Expected: added log message", e + ": added log message"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/exception/InvalidPartNumberExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.exception; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | 10 | public class InvalidPartNumberExceptionTest { 11 | @Test 12 | @DisplayName("Test invalid part error.") 13 | void testPartNumberGetError() { 14 | try { 15 | throw new InvalidPartNumberException("Invalid Part"); 16 | } catch (InvalidPartNumberException e) { 17 | assertEquals(Error.INVALID_PART, e.getError()); 18 | } 19 | } 20 | 21 | @Test 22 | @DisplayName("Test invalid part error message.") 23 | void testPartNumberGetMessage() { 24 | try { 25 | throw new InvalidPartNumberException("Invalid Part Expected"); 26 | } catch (InvalidPartNumberException e) { 27 | assertEquals("Invalid Part Expected", e.getMessage()); 28 | } 29 | } 30 | 31 | @Test 32 | @DisplayName("Test invalid part exception to string.") 33 | void testPartNumberToString() { 34 | try { 35 | throw new InvalidPartNumberException("Invalid Part Expected"); 36 | } catch (InvalidPartNumberException e) { 37 | assertEquals("com.example.s3objectlambda.exception.InvalidPartNumberException: " + 38 | "Invalid Part Expected: added log message", e + ": added log message"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /function/python_3_9/src/request/utils.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | from urllib.parse import parse_qsl 3 | 4 | PART_NUMBER = 'partNumber' 5 | RANGE = 'Range' 6 | SIGNED_HEADERS = 'X-Amz-SignedHeaders' 7 | 8 | 9 | def get_signed_headers_from_url(url): 10 | """ 11 | Get list of signed headers from user request 12 | :param url: url 13 | :return: list of signed headers or empty list 14 | """ 15 | signed_headers_as_str = get_query_param(url, SIGNED_HEADERS) 16 | signed_headers = signed_headers_as_str.split(';') if signed_headers_as_str is not None else [] 17 | return list(map(lambda x: x.lower(), signed_headers)) 18 | 19 | 20 | def get_part_number(user_request): 21 | """ 22 | Get the part number from user request 23 | :param user_request: User request 24 | :return: part number string or None 25 | """ 26 | return get_query_param(user_request['url'], PART_NUMBER) 27 | 28 | 29 | def get_range(user_request): 30 | """ 31 | Get range from user request which can be in headers or url query 32 | :param user_request: User request 33 | :return: range string or None 34 | """ 35 | request_header = {k.lower(): v for k, v in user_request["headers"].items()} 36 | if RANGE.lower() in request_header: 37 | return request_header[RANGE.lower()] 38 | return get_query_param(user_request['url'], RANGE) 39 | 40 | 41 | def get_query_param(url, name): 42 | """Get a specific query parameter from url""" 43 | url = url.lower() 44 | name = name.lower() 45 | parse_query = dict(parse_qsl(urlparse(url).query)) 46 | if name in parse_query: 47 | return parse_query[name] 48 | return None -------------------------------------------------------------------------------- /tests/src/com/amazon/s3objectlambda/defaultconfig/Cleanup.java: -------------------------------------------------------------------------------- 1 | package com.amazon.s3objectlambda.defaultconfig; 2 | 3 | import static com.amazon.s3objectlambda.defaultconfig.KeyConstants.*; 4 | 5 | import org.testng.Assert; 6 | import org.testng.ITestContext; 7 | import org.testng.annotations.Parameters; 8 | import org.testng.annotations.Test; 9 | import software.amazon.awssdk.services.cloudformation.model.DeleteStackRequest; 10 | import software.amazon.awssdk.services.cloudformation.model.DescribeStacksRequest; 11 | import software.amazon.awssdk.services.cloudformation.waiters.CloudFormationWaiter; 12 | 13 | @Test(groups = "cleanup") 14 | public class Cleanup { 15 | 16 | @Parameters({"region"}) 17 | @Test(groups = "cleanup", dependsOnGroups = {"setup"}, alwaysRun = true, 18 | description = "Delete the created stack and assert it's deleted") 19 | public void deleteStack(ITestContext context, String region) { 20 | var sdkHelper = new SdkHelper(); 21 | String stackName = (String) context.getAttribute(STACK_NAME_KEY); 22 | var cloudFormationClient = sdkHelper.getCloudFormationClient(region); 23 | DeleteStackRequest deleteRequest = DeleteStackRequest.builder().stackName(stackName).build(); 24 | cloudFormationClient.deleteStack(deleteRequest); 25 | CloudFormationWaiter waiter = cloudFormationClient.waiter(); 26 | DescribeStacksRequest describeStacksRequest = DescribeStacksRequest.builder().stackName(stackName).build(); 27 | var waiterResponse = waiter.waitUntilStackDeleteComplete(describeStacksRequest); 28 | Assert.assertFalse(waiterResponse.matched().response().isPresent()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /function/nodejs_20_x/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amzn/s3object-lambda-default-config-nodejs-function", 3 | "version": "0.1.0", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT-0", 7 | "repository": "ssh://git.amazon.com/pkg/S3ObjectLambdaDefaultConfigNodejsFunction", 8 | "homepage": "https://code.amazon.com/packages/S3ObjectLambdaDefaultConfigNodejsFunction", 9 | "scripts": { 10 | "clean": "rm -rf node_modules s3objectlambda.js release/s3objectlambda_deployment_package.zip", 11 | "test": "jest", 12 | "build": "./node_modules/.bin/esbuild src/s3objectlambda.ts --bundle --outfile=s3objectlambda.js --platform=node && eslint 'src/**/*.ts' && jest", 13 | "package": "zip -ur release/s3objectlambda_deployment_package.zip s3objectlambda.js", 14 | "lint": "eslint 'src/**/*.ts'" 15 | }, 16 | "files": [], 17 | "dependencies": { 18 | "@aws-sdk/client-s3": "^3.554.0", 19 | "@types/node-fetch": "^2.6.1", 20 | "node-fetch": "2.6.7", 21 | "xml2js": "^0.5.0" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "27.0.0", 25 | "@types/node": "16.7.1", 26 | "@types/xml2js": "^0.4.11", 27 | "@typescript-eslint/eslint-plugin": "4.30.0", 28 | "@typescript-eslint/parser": "4.30.0", 29 | "ansi-regex": "^5.0.1", 30 | "esbuild": "^0.12.25", 31 | "eslint": "7.32.0", 32 | "eslint-config-standard": "16.0.3", 33 | "eslint-config-standard-with-typescript": "21.0.1", 34 | "eslint-plugin-autofix": "1.0.5", 35 | "eslint-plugin-import": "2.24.2", 36 | "eslint-plugin-node": "11.1.0", 37 | "eslint-plugin-promise": "5.1.0", 38 | "jest": "27.0.0", 39 | "tmpl": "^1.0.5", 40 | "ts-jest": "27.0.0", 41 | "typescript": "4.3.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /function/python_3_9/src/error/error_response.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | 4 | def write_error_response_for_s3(s3_client, request_context, object_response): 5 | """ 6 | write WriteGetObjectResponse to the S3 client for error msg received from s3 7 | :param s3_client: s3 client 8 | :param request_context: Requests that was sent to supporting Access Point 9 | :param object_response: Response received from supporting Access Point 10 | :return: WriteGetObjectResponse 11 | """ 12 | root = ElementTree.fromstring(object_response.content.decode('utf-8')) 13 | error_code = root.find('Code').text 14 | return s3_client.write_get_object_response( 15 | RequestRoute=request_context['outputRoute'], 16 | RequestToken=request_context['outputToken'], 17 | StatusCode=object_response.status_code, 18 | ErrorCode=error_code, 19 | ErrorMessage='Received {} from the supporting Access Point.'.format(error_code), 20 | ) 21 | 22 | 23 | def write_error_response(s3_client, request_context, status_code, error_code, error_message): 24 | """ 25 | write WriteGetObjectResponse to the S3 client for AWS Lambda errors 26 | :param s3_client: s3 client 27 | :param request_context: Requests that was sent to supporting Access Point 28 | :param status_code: Http status code for the type of error 29 | :param error_code: Error Code 30 | :param error_message: Error Message 31 | :return: WriteGetObjectResponse 32 | """ 33 | return s3_client.write_get_object_response( 34 | RequestRoute=request_context['outputRoute'], 35 | RequestToken=request_context['outputToken'], 36 | StatusCode=status_code, 37 | ErrorCode=error_code, 38 | ErrorMessage=error_message 39 | ) 40 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/request/S3PresignedUrlParserHelper.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.request; 2 | 3 | import com.amazonaws.auth.internal.SignerConstants; 4 | 5 | import java.net.MalformedURLException; 6 | import java.net.URL; 7 | import java.net.URLDecoder; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.*; 10 | 11 | public class S3PresignedUrlParserHelper { 12 | 13 | static final String X_AMZN_SIGNED_HEADERS_DELIMETER = ";"; 14 | static final String QUERY_PARAM_DELIMETER = "&"; 15 | static final String QUERY_PARAM_KEY_VALUE_DELIMETER = "="; 16 | 17 | public static List retrieveSignedHeadersFromPresignedUrl( 18 | final String presignedUrl) throws MalformedURLException { 19 | 20 | URL url = new URL(presignedUrl); 21 | String query = url.getQuery(); 22 | if (query == null) { 23 | return Collections.emptyList(); 24 | } 25 | 26 | for (String queryParam : query.split(QUERY_PARAM_DELIMETER)) { 27 | 28 | String[] keyValuePair = queryParam.split(QUERY_PARAM_KEY_VALUE_DELIMETER, 2); 29 | if (keyValuePair.length != 2) { 30 | continue; 31 | } 32 | 33 | String key = keyValuePair[0].toLowerCase(); 34 | if (key.equals(SignerConstants.X_AMZ_SIGNED_HEADER.toLowerCase())) { 35 | String decodedValue = URLDecoder.decode(keyValuePair[1], StandardCharsets.UTF_8); 36 | List signedHeaders = Arrays.asList(decodedValue.split(X_AMZN_SIGNED_HEADERS_DELIMETER)); 37 | signedHeaders.replaceAll(String::toLowerCase); 38 | return signedHeaders; 39 | } 40 | } 41 | 42 | return Collections.emptyList(); 43 | } 44 | } -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/validator/GetObjectRequestValidator.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.validator; 2 | 3 | import com.example.s3objectlambda.request.GetObjectRequestWrapper; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.net.URISyntaxException; 8 | import java.util.Optional; 9 | 10 | /** 11 | * Responsible for validating the user request. Returns an Optional string error message if there are errors 12 | * or Optional empty if valid. 13 | */ 14 | public class GetObjectRequestValidator implements RequestValidator { 15 | 16 | private Logger logger; 17 | private GetObjectRequestWrapper userRequest; 18 | 19 | public GetObjectRequestValidator(GetObjectRequestWrapper userRequest) { 20 | this.logger = LoggerFactory.getLogger(GetObjectRequestValidator.class); 21 | this.userRequest = userRequest; 22 | } 23 | 24 | /** 25 | * This method validates the user request. 26 | * @return Optional error message if the request is invalid. And returns optional empty if the request is valid. 27 | */ 28 | public Optional validateUserRequest() { 29 | 30 | Optional range; 31 | Optional partNumber; 32 | 33 | try { 34 | range = this.userRequest.getRange(); 35 | partNumber = this.userRequest.getPartNumber(); 36 | } catch (URISyntaxException e) { 37 | this.logger.error("Exception in validation: " + e); 38 | return Optional.of("Invalid request URI"); 39 | } 40 | 41 | if (range.isPresent() && partNumber.isPresent()) { 42 | return Optional.of("Cannot specify both Range header and partNumber query parameter"); 43 | } 44 | 45 | return Optional.empty(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /function/python_3_9/test/test_range.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.response.range_mapper import * 3 | 4 | HAS_ERROR_KEY = 'hasError' 5 | OBJECT_KEY = 'object' 6 | 7 | def create_buffer(string): 8 | return bytes(string, encoding='utf8') 9 | 10 | 11 | def test_invalid_range_format(): 12 | range_response = map_range(create_buffer('hello'), '123') 13 | assert range_response.hasError 14 | 15 | 16 | def test_multiple_ranges_format(): 17 | range_response = map_range(create_buffer('hello'), 'bytes=1-2-3') 18 | assert range_response.hasError 19 | 20 | 21 | def test_unsupported_ranges(): 22 | range_response = map_range(create_buffer('hello'), 'kilobytes=1-2') 23 | assert range_response.hasError 24 | 25 | 26 | def test_invalid_range_value(): 27 | range_response = map_range(create_buffer('hello'), 'bytes=-') 28 | assert range_response.hasError 29 | 30 | 31 | def test_range_with_start(): 32 | range_response = map_range(create_buffer('hello'), 'bytes=2-') 33 | assert range_response.object == create_buffer('llo') 34 | 35 | 36 | def test_range_with_suffix(): 37 | range_response = map_range(create_buffer('hello'), 'bytes=-2') 38 | assert range_response.object == create_buffer('lo') 39 | 40 | 41 | def test_range_with_zero_start_and_end(): 42 | range_response = map_range(create_buffer('hello'), 'bytes=0-3') 43 | assert range_response.object == create_buffer('hell') 44 | 45 | 46 | def test_range_with__non_zero_start_and_end(): 47 | range_response = map_range(create_buffer('hello'), 'bytes=1-3') 48 | assert range_response.object == create_buffer('ell') 49 | 50 | 51 | def test_two_digits_range(): 52 | range_response = map_range(create_buffer('amazonwebservices'), 'bytes=10-15') 53 | assert range_response.object == create_buffer('ervice') 54 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/s3objectlambda_event.types.ts: -------------------------------------------------------------------------------- 1 | /* 2 |

The event object contains information about the request made to Amazon S3 Object Lambda. The Lambda function uses 3 | this information to identify the S3 operation and process the request appropriately. 4 | See {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/olap-writing-lambda.html#olap-event-context|Event context format and usage} 5 | for details.

6 | */ 7 | 8 | export interface S3ObjectLambdaEvent { 9 | readonly xAmzrequestId: string 10 | readonly getObjectContext: GetObjectContext 11 | readonly listObjectsContext: BaseObjectContext 12 | readonly listObjectsV2Context: BaseObjectContext 13 | readonly headObjectContext: BaseObjectContext 14 | readonly configuration: Configuration 15 | readonly userRequest: UserRequest 16 | readonly userIdentity: UserIdentity 17 | readonly protocolVersion: string 18 | } 19 | 20 | export interface GetObjectContext extends BaseObjectContext{ 21 | readonly outputRoute: string 22 | readonly outputToken: string 23 | } 24 | 25 | export interface BaseObjectContext { 26 | readonly inputS3Url: string 27 | } 28 | 29 | interface Configuration { 30 | readonly accessPointArn: string 31 | readonly supportingAccessPointArn: string 32 | readonly payload: string 33 | } 34 | 35 | export interface UserRequest { 36 | readonly url: string 37 | readonly headers: object 38 | } 39 | 40 | interface UserIdentity { 41 | readonly type: string 42 | readonly principalId: string 43 | readonly arn: string 44 | readonly accountId: string 45 | readonly accessKeyId: string 46 | readonly sessionContext: SessionContext 47 | readonly userName: string 48 | readonly invokedBy: string 49 | } 50 | 51 | interface SessionContext { 52 | readonly attributes: Map 53 | readonly sessionIssuer: Map 54 | } 55 | -------------------------------------------------------------------------------- /function/python_3_9/test/test_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from src.request.utils import * 3 | 4 | 5 | def test_get_part_number(): 6 | user_request = { 7 | 'url': 'https://s3.amazonaws.com?partNumber=1', 8 | 'headers': { 9 | 'h1': 'v1' 10 | } 11 | } 12 | assert get_part_number(user_request) == '1' 13 | 14 | 15 | def test_get_part_number_case_insensitive(): 16 | user_request = { 17 | 'url': 'https://s3.amazonaws.com?hello=world&PARTnumber=1', 18 | 'headers': { 19 | 'h1': 'v1' 20 | } 21 | } 22 | assert get_part_number(user_request) == '1' 23 | 24 | 25 | def test_get_part_number_not_exist(): 26 | user_request = { 27 | 'url': 'https://s3.amazonaws.com?hello=world&Range=1', 28 | 'headers': { 29 | 'h1': 'v1' 30 | } 31 | } 32 | assert get_part_number(user_request) is None 33 | 34 | 35 | def test_get_range_from_query_param(): 36 | user_request = { 37 | 'url': 'https://s3.amazonaws.com?range=bytes=1', 38 | 'headers': { 39 | 'h1': 'v1' 40 | } 41 | } 42 | assert get_range(user_request) == 'bytes=1' 43 | 44 | 45 | def test_get_range_from_query_param_case_insensitive(): 46 | user_request = { 47 | 'url': 'https://s3.amazonaws.com?raNGe=bytes=1', 48 | 'headers': { 49 | 'h1': 'v1' 50 | } 51 | } 52 | assert get_range(user_request) == 'bytes=1' 53 | 54 | 55 | def test_get_range_from_header(): 56 | user_request = { 57 | 'url': 'https://s3.amazonaws.com', 58 | 'headers': { 59 | 'Range': 'bytes=3-' 60 | } 61 | } 62 | assert get_range(user_request) == 'bytes=3-' 63 | 64 | 65 | def test_get_range_from_header_case_insensitive(): 66 | user_request = { 67 | 'url': 'https://s3.amazonaws.com', 68 | 'headers': { 69 | 'RANge': 'bytes=3-' 70 | } 71 | } 72 | assert get_range(user_request) == 'bytes=3-' 73 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/request/GetObjectRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.request; 2 | 3 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.net.URISyntaxException; 8 | import java.util.HashMap; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.mockito.Mockito.mock; 12 | import static org.mockito.Mockito.when; 13 | 14 | public class GetObjectRequestTest { 15 | 16 | @Test 17 | @DisplayName("getRange function returns the range correctly.") 18 | public void getRangeTest() throws URISyntaxException { 19 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 20 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?pageNumber=3&partNumber=1"); 21 | var headerMap = new HashMap(); 22 | headerMap.put("Range", "bytes=1-20"); 23 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 24 | var getObjectRequest = new GetObjectRequestWrapper(mockUserRequest); 25 | var range = getObjectRequest.getRange().get(); 26 | assertEquals("bytes=1-20", range); 27 | } 28 | 29 | @Test 30 | @DisplayName("getPartNumber function returns the part correctly.") 31 | public void getPartNumberTest() throws URISyntaxException { 32 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 33 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?pageNumber=3&partNumber=4"); 34 | var headerMap = new HashMap(); 35 | headerMap.put("Range", "bytes=1-20"); 36 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 37 | var getObjectRequest = new GetObjectRequestWrapper(mockUserRequest); 38 | var partNumber = getObjectRequest.getPartNumber().get(); 39 | assertEquals("4", partNumber); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /tests/testng.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/utils/listobject_xml_transformer.ts: -------------------------------------------------------------------------------- 1 | import { Builder, Parser } from 'xml2js'; 2 | import { parseBooleans } from 'xml2js/lib/processors'; 3 | import { IBaseListObject } from '../s3objectlambda_list_type'; 4 | 5 | export class ListObjectsXmlTransformer { 6 | /** 7 | * Converts back from a ListObject to a string in an XML format 8 | * @param listObject The ListObjects which we want to obtain an XML string from 9 | * @private 10 | */ 11 | public createListObjectsXmlResponse (listObject: T): string | null { 12 | const builder = new Builder({ 13 | rootName: 'ListBucketResult' 14 | }); 15 | 16 | const xmlListObject = builder.buildObject(listObject); 17 | 18 | if (xmlListObject == null) { 19 | console.log('Failed building back the XML'); 20 | return null; 21 | } 22 | 23 | return xmlListObject; 24 | } 25 | 26 | /** 27 | * Used to force the Contents property to be an array in the converted object from XML. 28 | * If the result is only one key, then the parser creates an object instead of an array 29 | * @param result The JSON object after the conversion from XML 30 | * @private 31 | */ 32 | private static makeContentsAsArray (result: any): void { 33 | if (result.Contents !== undefined && !Object.getOwnPropertyNames(result.Contents).includes('length') 34 | ) { 35 | result.Contents = [result.Contents]; 36 | } 37 | } 38 | 39 | /** 40 | * Transforms from a xml containing a string to the corresponding JSON object 41 | * @param xml A string in the XML format 42 | */ 43 | public async createListObjectsJsonResponse (xml: string): Promise < T | null > { 44 | const parser = new Parser({ 45 | explicitArray: false, 46 | explicitRoot: false, 47 | valueProcessors: [parseBooleans] 48 | }); 49 | 50 | const parsedXML = parser.parseStringPromise(xml); 51 | if (parsedXML === null 52 | ) { 53 | return null; 54 | } 55 | 56 | const listObjectResponse = await parsedXML as T; 57 | 58 | ListObjectsXmlTransformer.makeContentsAsArray(listObjectResponse); 59 | return listObjectResponse; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/src/com/amazon/s3objectlambda/defaultconfig/SdkHelper.java: -------------------------------------------------------------------------------- 1 | package com.amazon.s3objectlambda.defaultconfig; 2 | 3 | import org.apache.commons.lang3.RandomStringUtils; 4 | import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; 5 | import software.amazon.awssdk.regions.Region; 6 | import software.amazon.awssdk.services.cloudformation.CloudFormationClient; 7 | import software.amazon.awssdk.services.cloudformation.model.Parameter; 8 | import software.amazon.awssdk.services.s3.S3Client; 9 | import software.amazon.awssdk.services.sts.StsClient; 10 | 11 | public class SdkHelper { 12 | 13 | public DefaultCredentialsProvider getCredential() { 14 | return DefaultCredentialsProvider.builder().build(); 15 | } 16 | 17 | public S3Client getS3Client(String region) { 18 | return S3Client.builder() 19 | .credentialsProvider(getCredential()) 20 | .region(Region.of(region)) 21 | .build(); 22 | } 23 | 24 | public CloudFormationClient getCloudFormationClient(String region) { 25 | return CloudFormationClient.builder() 26 | .credentialsProvider(getCredential()) 27 | .region(Region.of(region)) 28 | .build(); 29 | } 30 | 31 | public Parameter buildParameter(String key, String value) { 32 | return Parameter.builder() 33 | .parameterKey(key) 34 | .parameterValue(value) 35 | .build(); 36 | } 37 | 38 | public String getOLAccessPointArn(Region region, String accessPointName) { 39 | String accountID = getAWSAccountID(region); 40 | return String.format("arn:aws:s3-object-lambda:%s:%s:accesspoint/%s", region, accountID, accessPointName); 41 | } 42 | 43 | public String generateRandomResourceName(int count) { 44 | return RandomStringUtils.randomAlphabetic(count).toLowerCase(); 45 | } 46 | 47 | public String getAWSAccountID(Region region) { 48 | var stsClient = StsClient.builder() 49 | .credentialsProvider(getCredential()) 50 | .region(region) 51 | .build(); 52 | return stsClient.getCallerIdentity().account(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/transform/Range.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | import com.example.s3objectlambda.exception.InvalidRangeException; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | /** 9 | * This class represents the Range object. 10 | * Range can be represented in one of the following formats. 11 | * * =- 12 | * * =- 13 | * * =- 14 | * The object has lastPart, firstPart and unit. 15 | * firstPart or lastPart can be empty depending on the type of range request ir represents. 16 | */ 17 | public class Range { 18 | 19 | private String lastPart; 20 | private String firstPart; 21 | private String unit; 22 | 23 | /** 24 | * This constructor accepts range as string in one of the valid formats. 25 | * @param range range as string. 26 | * @throws InvalidRangeException 27 | */ 28 | public Range(String range) throws InvalidRangeException { 29 | 30 | var rangeRegexMatcher = getRangeRegexMatcher(range); 31 | //First part of the range from regex match. This contains the unit in which range is requested. 32 | this.unit = rangeRegexMatcher.group(1); 33 | 34 | //Second and third part from the range value are and 35 | //Read this from the regex matcher. 36 | this.firstPart = rangeRegexMatcher.group(2); 37 | this.lastPart = rangeRegexMatcher.group(3); 38 | 39 | if (this.firstPart == null && this.lastPart == null) { 40 | throw new InvalidRangeException("No values found for start and end."); 41 | } 42 | } 43 | 44 | private Matcher getRangeRegexMatcher(String range) throws InvalidRangeException { 45 | var rangePattern = "([a-z]+)=(\\d+)?-(\\d+)?"; 46 | var rangeRegexPattern = Pattern.compile(rangePattern); 47 | var rangeRegexMatcher = rangeRegexPattern.matcher(range); 48 | 49 | if (!rangeRegexMatcher.find()) { 50 | throw new InvalidRangeException("Invalid Range: " + range); 51 | } 52 | return rangeRegexMatcher; 53 | } 54 | 55 | public String getLastPart() { 56 | return lastPart; 57 | } 58 | 59 | public String getFirstPart() { 60 | return firstPart; 61 | } 62 | 63 | public String getUnit() { 64 | return unit; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/response/part_number_mapper.ts: -------------------------------------------------------------------------------- 1 | import ErrorCode from '../error/error_code'; 2 | import { RangeResponse } from './range_response.types'; 3 | import { CONTENT_LENGTH } from '../request/utils'; 4 | 5 | const PART_SIZE = 5242880; // 5 MB 6 | 7 | export function mapPartNumber (partNumber: string, transformedObject: Buffer): RangeResponse { 8 | const objectLength = transformedObject.byteLength; 9 | const totalParts = Math.ceil(objectLength / PART_SIZE); // part numbers start at 1 10 | const requestedPart = Number(partNumber); 11 | 12 | if (isNaN(requestedPart) || !Number.isInteger(requestedPart) || 13 | requestedPart > totalParts || requestedPart <= 0) { 14 | return { 15 | hasError: true, 16 | errorCode: ErrorCode.INVALID_PART, 17 | errorMessage: `Cannot specify part number: ${requestedPart}. Use part numbers 1 to ${totalParts}.` 18 | }; 19 | } 20 | 21 | const partStart = (requestedPart - 1) * PART_SIZE; 22 | const partEnd = Math.min(partStart + PART_SIZE, objectLength); 23 | return { object: transformedObject.slice(partStart, partEnd), hasError: false }; 24 | } 25 | 26 | export function mapPartNumberHead (partNumber: string, transformedHeaders: Map): RangeResponse { 27 | let contentLength: number; 28 | // Create a copy of the headers in order to not alter the original object 29 | const newHeaders = new Map(transformedHeaders); 30 | if (!transformedHeaders.has(CONTENT_LENGTH)) { 31 | return { 32 | hasError: true, 33 | errorCode: ErrorCode.INVALID_REQUEST, 34 | errorMessage: 'No content-length was found in the headers' 35 | }; 36 | } else { 37 | contentLength = Number(transformedHeaders.get(CONTENT_LENGTH)); 38 | } 39 | const totalParts = Math.ceil(contentLength / PART_SIZE); // part numbers start at 1 40 | const requestedPart = Number(partNumber); 41 | 42 | if (isNaN(requestedPart) || !Number.isInteger(requestedPart) || 43 | requestedPart > totalParts || requestedPart <= 0) { 44 | return { 45 | hasError: true, 46 | errorCode: ErrorCode.INVALID_PART, 47 | errorMessage: `Cannot specify part number: ${requestedPart}. Use part numbers 1 to ${totalParts}.` 48 | }; 49 | } 50 | 51 | const partStart = (requestedPart - 1) * PART_SIZE; 52 | const partEnd = Math.min(partStart + PART_SIZE, contentLength); 53 | newHeaders.set(CONTENT_LENGTH, Object(partEnd - partStart)); 54 | return { headers: newHeaders, hasError: false }; 55 | } 56 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/transform/PartNumberMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | import com.example.s3objectlambda.exception.InvalidPartNumberException; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.Arrays; 9 | 10 | /** 11 | * Handles partNumber requests by applying the partNumber to the transformed object. 12 | */ 13 | public class PartNumberMapper { 14 | 15 | /* 16 | Minimum part size in a multipart upload is 5 MB. 17 | There is no size limit on the last part of the multipart upload. 18 | */ 19 | private final Integer partSize = 5242880; 20 | private Logger logger; 21 | 22 | public PartNumberMapper() { 23 | this.logger = LoggerFactory.getLogger(PartNumberMapper.class); 24 | } 25 | 26 | /** 27 | * This method returns the requested part from the response object. 28 | * @param partNumber Part number , this should be >0 and <= Total number of parts in the response object. 29 | * @param responseObjectByteArray Response object as byte array, from which a particular part is requested. 30 | * @return Returns the byte array object of the requested part. 31 | * @throws InvalidPartNumberException 32 | */ 33 | public byte[] mapPartNumber(String partNumber, byte[] responseObjectByteArray) 34 | throws InvalidPartNumberException { 35 | 36 | double objectLength; 37 | objectLength = responseObjectByteArray.length; 38 | double totalParts = Math.ceil(objectLength / this.partSize); 39 | int requestedPart; 40 | 41 | try { 42 | requestedPart = Integer.parseInt(partNumber); 43 | } catch (NumberFormatException nfe) { 44 | this.logger.error("Invalid partNumber" + nfe); 45 | throw new InvalidPartNumberException("Invalid partNumber: " + partNumber); 46 | } 47 | 48 | if (requestedPart > totalParts || requestedPart <= 0) { 49 | throw new InvalidPartNumberException(String.format("Cannot specify part number: %s. " + 50 | "Use part number from 1 to %s.", requestedPart, totalParts)); 51 | } 52 | 53 | int partStart = (requestedPart - 1) * this.partSize; 54 | int partEnd = (int) Math.min(partStart + this.partSize, objectLength); 55 | var objectPart = Arrays.copyOfRange(responseObjectByteArray, partStart, partEnd); 56 | return objectPart; 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/transform/GetObjectTransformer.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | 4 | import com.example.s3objectlambda.exception.InvalidPartNumberException; 5 | import com.example.s3objectlambda.exception.InvalidRangeException; 6 | import com.example.s3objectlambda.exception.TransformationException; 7 | import com.example.s3objectlambda.request.GetObjectRequestWrapper; 8 | 9 | import java.net.URISyntaxException; 10 | import java.util.Optional; 11 | 12 | /** 13 | * This is the transformer class for getObject requests. 14 | */ 15 | 16 | public class GetObjectTransformer implements Transformer { 17 | private GetObjectRequestWrapper userRequest; 18 | public GetObjectTransformer(GetObjectRequestWrapper userRequest) { 19 | this.userRequest = userRequest; 20 | } 21 | 22 | /** 23 | * TODO: Implement your transform object logic here. 24 | * 25 | * @param responseObjectByteArray object response as byte array to be transformed. 26 | * @return Transformed object as byte array. 27 | */ 28 | public byte[] transformObjectResponse(byte[] responseObjectByteArray) throws TransformationException { 29 | 30 | /** 31 | * Add your code to transform the responseObjectByteArray. 32 | * After transforming the responseObjectByteArray just return the transformed byte array. 33 | */ 34 | 35 | return responseObjectByteArray; 36 | } 37 | 38 | /** 39 | * 40 | * @param responseObjectByteArray Response object as byte array on which range/part number to be applied. 41 | * @param userRequest GetObjectRequest object 42 | * @return Returns responseObjectByteArray 43 | * @throws URISyntaxException 44 | * @throws InvalidRangeException 45 | * @throws InvalidPartNumberException 46 | */ 47 | 48 | @Override 49 | public byte[] applyRangeOrPartNumber(byte[] responseObjectByteArray) 50 | throws URISyntaxException, InvalidRangeException, InvalidPartNumberException { 51 | Optional range = this.userRequest.getRange(); 52 | Optional partNumber = this.userRequest.getPartNumber(); 53 | 54 | if (range.isPresent()) { 55 | return new RangeMapper(range.get()).mapRange(responseObjectByteArray); 56 | } else if (partNumber.isPresent()) { 57 | return new PartNumberMapper().mapPartNumber(partNumber.get(), responseObjectByteArray); 58 | } else { 59 | return responseObjectByteArray; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/s3objectlambda.ts: -------------------------------------------------------------------------------- 1 | import type { S3ObjectLambdaEvent } from './s3objectlambda_event.types'; 2 | import handleGetObjectRequest from './handler/get_object_handler'; 3 | import { S3Client } from '@aws-sdk/client-s3'; 4 | import handleHeadObjectRequest from './handler/head_object_handler'; 5 | import { ListObjectsHandler } from './handler/list_objects_base_handler'; 6 | import { 7 | transformListObjectsV1, 8 | transformListObjectsV2 9 | } from './transform/s3objectlambda_transformer'; 10 | import { IListObjectsV1, IListObjectsV2 } from './s3objectlambda_list_type'; 11 | 12 | /* Initialize clients outside Lambda handler to take advantage of execution environment reuse and improve function 13 | performance. See {@link https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html | Best practices with AWS 14 | Lambda functions} 15 | for details. 16 | **/ 17 | const s3 = new S3Client(); 18 | 19 | export async function handler (event: S3ObjectLambdaEvent): Promise { 20 | /* 21 |

The event object contains all information required to handle a request from Amazon S3 Object Lambda.

22 |

The context objects contain information about the request, which resulted in this Lambda function being 23 | invoked.

24 |

The userRequest object contains information related to the entity (user or application) 25 | that invoked Amazon S3 Object Lambda. This information can be used in multiple ways, for example, to allow or deny 26 | the request based on the entity. See the Respond with a 403 Forbidden example in 27 | {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/olap-writing-lambda.html | Writing Lambda functions} 28 | for sample code.

29 | */ 30 | 31 | if ('getObjectContext' in event) { 32 | await handleGetObjectRequest(s3, event.getObjectContext, event.userRequest); 33 | return null; 34 | } else if ('headObjectContext' in event) { 35 | return handleHeadObjectRequest(event.headObjectContext, event.userRequest); 36 | } else if ('listObjectsContext' in event) { 37 | const listObjectListObjectsHandler = new ListObjectsHandler(transformListObjectsV1); 38 | return listObjectListObjectsHandler.handleListObjectsRequest(event.listObjectsContext, event.userRequest); 39 | } else if ('listObjectsV2Context' in event) { 40 | const listObjectListObjectsHandler = new ListObjectsHandler(transformListObjectsV2); 41 | return listObjectListObjectsHandler.handleListObjectsRequest(event.listObjectsV2Context, event.userRequest); 42 | } 43 | 44 | return null; 45 | } 46 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/transform/RangeTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | import com.example.s3objectlambda.exception.InvalidRangeException; 4 | import org.junit.jupiter.api.DisplayName; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNull; 9 | import static org.junit.jupiter.api.Assertions.fail; 10 | 11 | 12 | public class RangeTest { 13 | @Test 14 | void testGetFirstPart() throws InvalidRangeException { 15 | var range = new Range("byte=12-25"); 16 | assertEquals("12", range.getFirstPart()); 17 | } 18 | 19 | @Test 20 | @DisplayName("First Part is null when only suffix length given.") 21 | void testGetFirstPartWhenEmpty() throws InvalidRangeException { 22 | var range = new Range("byte=-25"); 23 | assertNull(range.getFirstPart()); 24 | } 25 | 26 | @Test 27 | void testGetLastPart() throws InvalidRangeException { 28 | var range = new Range("byte=12-25"); 29 | assertEquals("25", range.getLastPart()); 30 | } 31 | 32 | @Test 33 | @DisplayName("Last Part is null when only first part is given.") 34 | void testGetLastPartWhenEmpty() throws InvalidRangeException { 35 | var range = new Range("byte=12-"); 36 | assertNull(range.getLastPart()); 37 | } 38 | 39 | @Test 40 | @DisplayName("Last Part is returned when first part is null.") 41 | void testGetLastPartWhenFirstIsEmpty() throws InvalidRangeException { 42 | var range = new Range("byte=-46"); 43 | assertEquals("46", range.getLastPart()); 44 | } 45 | 46 | @Test 47 | @DisplayName("First Part is returned when last part is null.") 48 | void testGetFirstPartWhenLastIsEmpty() throws InvalidRangeException { 49 | var range = new Range("byte=66-"); 50 | assertEquals("66", range.getFirstPart()); 51 | } 52 | 53 | @Test 54 | @DisplayName("Returns the correct unit.") 55 | void testGetUnit() throws InvalidRangeException { 56 | var range = new Range("meter=12-"); 57 | assertEquals("meter", range.getUnit()); 58 | } 59 | 60 | @Test 61 | @DisplayName("Throws InvalidRangeException .") 62 | void testInvalidRangeException() { 63 | try { 64 | new Range("meter=-"); 65 | fail("Range did not throw exception."); 66 | } catch (InvalidRangeException e) { 67 | assertEquals("No values found for start and end.", e.getMessage()); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/error/XMLErrorParser.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.error; 2 | 3 | import org.w3c.dom.Document; 4 | import org.xml.sax.InputSource; 5 | import org.xml.sax.SAXException; 6 | 7 | import javax.xml.parsers.DocumentBuilder; 8 | import javax.xml.parsers.DocumentBuilderFactory; 9 | import javax.xml.parsers.ParserConfigurationException; 10 | import java.io.IOException; 11 | import java.io.StringReader; 12 | 13 | /** 14 | * This class is the parser for the xml error response from S3 getObject request. 15 | */ 16 | public class XMLErrorParser implements ErrorParser { 17 | 18 | @Override 19 | public S3RequestError parse(String errorResponse) throws 20 | ParserConfigurationException, SAXException, IOException { 21 | var errorResponseDocument = getErrorResponseDocument(errorResponse); 22 | var s3RequestError = new S3RequestError(); 23 | s3RequestError.setCode(getErrorAttributeValue(errorResponseDocument, "Code")); 24 | s3RequestError.setMessage(getErrorAttributeValue(errorResponseDocument, "Message")); 25 | s3RequestError.setRequestId(getErrorAttributeValue(errorResponseDocument, "RequestId")); 26 | 27 | return s3RequestError; 28 | } 29 | 30 | private String getErrorAttributeValue(Document errorResponseDocument, String attribute) { 31 | var nList = errorResponseDocument.getElementsByTagName(attribute); 32 | var attributeValue = nList.item(0).getTextContent(); 33 | return attributeValue; 34 | } 35 | 36 | private Document getErrorResponseDocument(String errorResponse) throws 37 | ParserConfigurationException, SAXException, IOException { 38 | var factory = DocumentBuilderFactory.newInstance(); 39 | /* 40 | Prevent XML External Entity (XXE) Processing 41 | https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing 42 | */ 43 | 44 | factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 45 | factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); 46 | factory.setFeature("http://xml.org/sax/features/external-general-entities", false); 47 | 48 | factory.setXIncludeAware(false); 49 | factory.setExpandEntityReferences(false); 50 | 51 | var errorResponseInputSource = new InputSource(new StringReader(errorResponse)); 52 | DocumentBuilder builder; 53 | Document errorResponseDocument; 54 | 55 | builder = factory.newDocumentBuilder(); 56 | errorResponseDocument = builder.parse(errorResponseInputSource); 57 | 58 | return errorResponseDocument; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/response/param_transformer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Transforms a value got back from S3 as a Header to a proper parameter value 3 | * for WriteGetObjectResponse function. 4 | * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#writeGetObjectResponse-property 5 | * @param value Header received from fetch 6 | */ 7 | export function headerToWgorParam (value: string): string { 8 | switch (value) { 9 | case 'x-amz-checksum-crc32': 10 | return 'ChecksumCRC32'; 11 | case 'x-amz-checksum-crc32c': 12 | return 'ChecksumCRC32C'; 13 | case 'x-amz-checksum-sha1': 14 | return 'ChecksumSHA1'; 15 | case 'x-amz-checksum-sha256': 16 | return 'ChecksumSHA256'; 17 | case 'x-amz-tagging-count': 18 | return 'TagCount'; 19 | case 'x-amz-object-lock-legal-hold': 20 | return 'ObjectLockLegalHoldStatus'; 21 | case 'x-amz-server-side-encryption': 22 | return 'ServerSideEncryption'; 23 | case 'x-amz-server-side-encryption-customer-algorithm': 24 | return 'SSECustomerAlgorithm'; 25 | case 'x-amz-server-side-encryption-aws-kms-key-id': 26 | return 'SSEKMSKeyId'; 27 | case 'x-amz-server-side-encryption-customer-key-MD5': 28 | return 'SSECustomerKeyMD5'; 29 | default: 30 | value = value.replace('x-amz-', ''); 31 | return value.split('-').map((str) => { 32 | return upperFirst( 33 | str.split('/') 34 | .map(upperFirst) 35 | .join('/')); 36 | }).join(''); 37 | } 38 | } 39 | 40 | /** 41 | * Returns the same string with the first letter uppercase 42 | * @param value String to have the first letter uppercase 43 | */ 44 | function upperFirst (value: string): string { 45 | if (value.length > 0) { 46 | return value.slice(0, 1).toUpperCase() + value.slice(1, value.length); 47 | } 48 | return value; 49 | } 50 | 51 | export const ParamsKeys = [ 52 | 'RequestRoute', 53 | 'RequestToken', 54 | 'AcceptRanges', 55 | 'Body', 56 | 'BucketKeyEnabled', 57 | 'CacheControl', 58 | 'ChecksumCRC32', 59 | 'ChecksumCRC32C', 60 | 'ChecksumSHA1', 61 | 'ChecksumSHA256', 62 | 'ContentDisposition', 63 | 'ContentEncoding', 64 | 'ContentLanguage', 65 | 'ContentLength', 66 | 'ContentRange', 67 | 'ContentType', 68 | 'DeleteMarker', 69 | 'ETag', 70 | 'ErrorCode', 71 | 'ErrorMessage', 72 | 'Expiration', 73 | 'Expires', 74 | 'LastModified', 75 | 'Metadata', 76 | 'MissingMeta', 77 | 'ObjectLockLegalHoldStatus', 78 | 'ObjectLockMode', 79 | 'ObjectLockRetainUntilDate', 80 | 'PartsCount', 81 | 'ReplicationStatus', 82 | 'RequestCharged', 83 | 'Restore', 84 | 'SSECustomerAlgorithm', 85 | 'SSECustomerKeyMD5', 86 | 'SSEKMSKeyId', 87 | 'ServerSideEncryption', 88 | 'StatusCode', 89 | 'StorageClass', 90 | 'TagCount', 91 | 'VersionId' 92 | ]; 93 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/Handler.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda; 2 | 3 | import com.example.s3objectlambda.checksum.Md5Checksum; 4 | import com.example.s3objectlambda.request.GetObjectHandler; 5 | import com.example.s3objectlambda.request.GetObjectRequestWrapper; 6 | import com.example.s3objectlambda.response.GetObjectResponseHandler; 7 | import com.example.s3objectlambda.transform.GetObjectTransformer; 8 | import com.example.s3objectlambda.validator.GetObjectRequestValidator; 9 | 10 | import com.amazonaws.services.lambda.runtime.Context; 11 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 12 | import com.amazonaws.services.s3.AmazonS3; 13 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 14 | 15 | import java.net.http.HttpClient; 16 | 17 | /** 18 | * This is the main handler for your lambda function. 19 | **/ 20 | 21 | public class Handler { 22 | 23 | /** 24 | *

The event object contains all information required to handle a request from Amazon S3 Object Lambda.

25 | * 26 | *

The event object contains information about the GetObject request 27 | * which resulted in this Lambda function being invoked.

28 | * 29 | *

The userRequest (event.getUserRequest()) object contains information 30 | * related to the entity (user or application) 31 | * that invoked Amazon S3 Object Lambda. This information can be used in multiple ways, for example, 32 | * to allow or deny the request based on the entity. 33 | * See the Respond with a 403 Forbidden example in 34 | * Writing Lambda functions 35 | * for sample code.

36 | */ 37 | 38 | 39 | private AmazonS3 s3Client = AmazonS3ClientBuilder.standard().build(); 40 | 41 | public void handleRequest(S3ObjectLambdaEvent event, Context context) throws Exception { 42 | 43 | 44 | /* 45 | You can call handler from here depending on what the handler does and what the request is for. 46 | In this case, if the event has GetObjectContext we call the GetObjectHandler implementation. 47 | */ 48 | 49 | if (event.getGetObjectContext() != null) { 50 | 51 | var responseHandler = new GetObjectResponseHandler(this.s3Client, event, new Md5Checksum()); 52 | var userRequest = new GetObjectRequestWrapper(event.getUserRequest()); 53 | var requestValidator = new GetObjectRequestValidator(userRequest); 54 | var transformer = new GetObjectTransformer(userRequest); 55 | var httpClient = HttpClient.newBuilder().build(); 56 | 57 | new GetObjectHandler(this.s3Client, 58 | transformer, 59 | requestValidator, 60 | event, 61 | responseHandler, 62 | httpClient).handleRequest(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /function/python_3_9/src/response/range_mapper.py: -------------------------------------------------------------------------------- 1 | import re 2 | from response import MapperResponse 3 | 4 | """ 5 | * Handles range requests by applying the range to the transformed object. Supported range headers are: 6 | * 7 | * Range: =- 8 | * Range: =- 9 | * Range: =- 10 | * 11 | * Amazon S3 does not support retrieving multiple ranges of data per GetObject request. Please see 12 | * {@link https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html#API_GetObject_RequestSyntax|GetObject Request Syntax} 13 | * for more information. 14 | * 15 | * The only supported unit in this implementation is `bytes`. If other units are requested, we treat this as 16 | * an invalid request. 17 | """ 18 | 19 | 20 | def split_range_str(range_str): 21 | """ 22 | Split the range string to bytes, start and end. 23 | :param range_str: Range request string 24 | :return: tuple of (bytes, start, end) or None 25 | """ 26 | re_matcher = re.fullmatch(r'([a-z]+)=(\d+)?-(\d+)?', range_str) 27 | if not re_matcher or len(re_matcher.groups()) != 3: 28 | return None 29 | unit, start, end = re_matcher.groups() 30 | start = int(start) if type(start) == str else None 31 | end = int(end) if type(end) == str else None 32 | return unit, start, end 33 | 34 | 35 | def validate_range_str(range_str): 36 | """ 37 | Validate the range request string 38 | :param range_str: Range request string 39 | :return: A boolean value whether if range string is valid 40 | """ 41 | ranges = split_range_str(range_str) 42 | if ranges is None: 43 | return False 44 | unit, start, end = ranges 45 | if unit.lower() != 'bytes': 46 | return False 47 | if start is None and end is None: 48 | return False 49 | if start and start < 0: 50 | return False 51 | if start and end and start > end: 52 | return False 53 | return True 54 | 55 | 56 | def map_range(transformed_object, range_str): 57 | """ 58 | Map the range to an object 59 | :param transformed_object: Object to be mapped 60 | :param range_str: Range request string 61 | :return: MapperResponse object 62 | """ 63 | if not validate_range_str(range_str): 64 | return get_range_error_response(range_str) 65 | _, start, end = split_range_str(range_str) 66 | if start is None: 67 | new_object = transformed_object[-end:] 68 | elif end is None: 69 | new_object = transformed_object[start:] 70 | else: 71 | new_object = transformed_object[start:end + 1] 72 | return MapperResponse(hasError=False, object=new_object, 73 | error_msg=None) 74 | 75 | 76 | def get_range_error_response(range_str): 77 | """Get a MapperResponse for Errors""" 78 | return MapperResponse(hasError=True, object=None, 79 | error_msg='Cannot process specific range: {}'.format(range_str)) 80 | -------------------------------------------------------------------------------- /tests/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.amazon.s3objectlambda.defaultconfig 6 | IntegrationTest 7 | 1.0 8 | 9 | 10 | 16 11 | 16 12 | 13 | 14 | 15 | src 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-surefire-plugin 21 | 3.0.0-M5 22 | 23 | 24 | 25 | testng.xml 26 | 27 | 28 | 29 | 31 | 32 | maven-compiler-plugin 33 | 34 | 16 35 | 16 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.testng 43 | testng 44 | 7.7.0 45 | compile 46 | 47 | 48 | software.amazon.awssdk 49 | cloudformation 50 | 2.17.206 51 | 52 | 53 | software.amazon.awssdk 54 | s3 55 | 2.17.206 56 | 57 | 58 | software.amazon.awssdk 59 | sts 60 | 2.17.206 61 | 62 | 63 | org.apache.commons 64 | commons-lang3 65 | 3.12.0 66 | 67 | 68 | org.apache.httpcomponents 69 | httpclient 70 | 4.5.13 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /function/nodejs_20_x/tst/response/part_number_mapper.test.ts: -------------------------------------------------------------------------------- 1 | import { mapPartNumber, mapPartNumberHead } from '../../src/response/part_number_mapper'; 2 | import { CONTENT_LENGTH } from '../../src/request/utils'; 3 | 4 | const HAS_ERROR = 'hasError'; 5 | const FIVE_MB = 'a'.repeat(5242880); 6 | const TEN_MB = FIVE_MB.repeat(2); 7 | const FIVE_MB_CONTENT_LENGTH = 5242880; 8 | const TEN_MB_CONTENT_LENGTH = 10485760; 9 | 10 | test('Non-number part number returns error', () => { 11 | expect(mapPartNumber('abc', createBuffer('hello'))) 12 | .toHaveProperty(HAS_ERROR, true); 13 | }); 14 | 15 | test('Non-positive part number returns error', () => { 16 | expect(mapPartNumber('-1', createBuffer('hello'))) 17 | .toHaveProperty(HAS_ERROR, true); 18 | }); 19 | 20 | test('Non-integer part number returns error', () => { 21 | expect(mapPartNumber('1.5', createBuffer('hello'))) 22 | .toHaveProperty(HAS_ERROR, true); 23 | }); 24 | 25 | test('Large part number returns error', () => { 26 | expect(mapPartNumber('10', createBuffer('hello'))) 27 | .toHaveProperty(HAS_ERROR, true); 28 | }); 29 | 30 | test('Valid part number works', () => { 31 | expect(mapPartNumber('1', createBuffer(TEN_MB))) 32 | .toStrictEqual({ object: createBuffer(FIVE_MB), [HAS_ERROR]: false }); 33 | }); 34 | 35 | test('Non-number part number for head returns error', () => { 36 | expect(mapPartNumberHead('abc', createHeaders(FIVE_MB_CONTENT_LENGTH))) 37 | .toHaveProperty(HAS_ERROR, true); 38 | }); 39 | 40 | test('Non-positive part number for head returns error', () => { 41 | expect(mapPartNumberHead('-1', createHeaders(FIVE_MB_CONTENT_LENGTH))) 42 | .toHaveProperty(HAS_ERROR, true); 43 | }); 44 | 45 | test('Non-integer part number for head returns error', () => { 46 | expect(mapPartNumberHead('1.5', createHeaders(FIVE_MB_CONTENT_LENGTH))) 47 | .toHaveProperty(HAS_ERROR, true); 48 | }); 49 | 50 | test('Large part number returns error', () => { 51 | expect(mapPartNumberHead('10', createHeaders(FIVE_MB_CONTENT_LENGTH))) 52 | .toHaveProperty(HAS_ERROR, true); 53 | }); 54 | 55 | test('Valid part number for head works', () => { 56 | expect(mapPartNumberHead('1', createHeaders(TEN_MB_CONTENT_LENGTH))) 57 | .toStrictEqual({ 58 | headers: new Map([ 59 | [CONTENT_LENGTH, Object(FIVE_MB_CONTENT_LENGTH)] 60 | ]), 61 | [HAS_ERROR]: false 62 | }); 63 | }); 64 | 65 | test('Valid part number for head works even if length is not exactly divisible', () => { 66 | expect(mapPartNumberHead('2', createHeaders(FIVE_MB_CONTENT_LENGTH + 2))) 67 | .toStrictEqual({ 68 | headers: new Map([ 69 | [CONTENT_LENGTH, Object(2)] 70 | ]), 71 | [HAS_ERROR]: false 72 | }); 73 | }); 74 | 75 | function createBuffer (input: string): Buffer { 76 | return Buffer.from(input, 'utf-8'); 77 | } 78 | 79 | function createHeaders (contentLength?: number): Map { 80 | const headers = new Map(); 81 | if (contentLength !== undefined) { 82 | headers.set(CONTENT_LENGTH, Object(contentLength)); 83 | } 84 | return headers; 85 | } 86 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/transform/PartNumberMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | import com.example.s3objectlambda.exception.InvalidPartNumberException; 5 | import com.example.s3objectlambda.exception.InvalidRangeException; 6 | import com.example.s3objectlambda.request.GetObjectRequestWrapper; 7 | 8 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.net.URISyntaxException; 13 | import java.nio.charset.StandardCharsets; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.fail; 17 | import static org.mockito.Mockito.mock; 18 | import static org.mockito.Mockito.when; 19 | 20 | 21 | public class PartNumberMapperTest { 22 | 23 | private final String originalData = "12345678910!".repeat(300000); 24 | 25 | @Test 26 | @DisplayName("Get valid response for the final part number.") 27 | public void partNumberResponseObject() 28 | throws InvalidRangeException, URISyntaxException, InvalidPartNumberException { 29 | 30 | var responseInputStream = this.originalData.getBytes(StandardCharsets.UTF_16); 31 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 32 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?partNumber=2"); 33 | 34 | /*Total responseInputStream byte[] length = 7200002 //(responseInputStream.length) 35 | partSize = 5242880 (PartNumberMapper::partSize) 36 | Total Parts = Math.ceil(7200002 / 5242880) = 2 37 | Final part (2nd) length = 7200002 - (1 * 5242880 ) = 1957122 38 | **/ 39 | 40 | var totalParts = Math.ceil(responseInputStream.length / 5242880.0); 41 | var expectedPartLength = responseInputStream.length - ((totalParts - 1) * 5242880); 42 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 43 | 44 | var transformedResponseObject = new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 45 | responseInputStream); 46 | 47 | assertEquals((int) expectedPartLength, transformedResponseObject.length); 48 | } 49 | 50 | @Test 51 | @DisplayName("Get error response with correct status code when invalid part number is passed.") 52 | public void invalidPartNumberStatusCode() throws InvalidRangeException, URISyntaxException { 53 | 54 | var responseInputStream = this.originalData.getBytes(StandardCharsets.UTF_16); 55 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 56 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?partNumber=9"); 57 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 58 | 59 | try { 60 | new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 61 | responseInputStream); 62 | fail("InvalidPartNumberException was not thrown."); 63 | } catch (InvalidPartNumberException e) { 64 | assertEquals(Error.INVALID_PART.getStatusCode(), e.getError().getStatusCode()); 65 | } 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/handler/head_object_handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseObjectContext, UserRequest } from '../s3objectlambda_event.types'; 2 | import { validate } from '../request/validator'; 3 | import { errorResponse, responseForS3Errors } from '../error/error_response'; 4 | import ErrorCode from '../error/error_code'; 5 | import { transformHeaders } from '../transform/s3objectlambda_transformer'; 6 | import { IErrorResponse, IHeadObjectResponse, IResponse } from '../s3objectlambda_response.types'; 7 | import { applyRangeOrPartNumberHeaders, makeS3Request } from '../request/utils'; 8 | 9 | /** 10 | * Handles a HeadObject request, by performing the following steps: 11 | * 1. Validates the incoming user request. 12 | * 2. Retrieves the headers from Amazon S3. 13 | * 3. Applies a transformation. You can apply your custom transformation logic here. 14 | * 3. Sends the final transformed headers back to Amazon S3 Object Lambda. 15 | */ 16 | export default async function handleHeadObjectRequest (requestContext: BaseObjectContext, 17 | userRequest: UserRequest): Promise { 18 | // Validate user request and return error if invalid 19 | const errorMessage = validate(userRequest); 20 | if (errorMessage != null) { 21 | return errorResponse(requestContext, ErrorCode.INVALID_REQUEST, errorMessage); 22 | } 23 | 24 | // Read the original object from Amazon S3 25 | const objectResponse = await makeS3Request(requestContext.inputS3Url, userRequest, 'HEAD'); 26 | 27 | if (objectResponse.headers == null) { 28 | return errorResponse(requestContext, ErrorCode.NO_SUCH_KEY, 'Requested key does not exist'); 29 | } 30 | 31 | // Get the Headers as Map 32 | const rawResponseHeaders = objectResponse.headers.raw(); 33 | const originalHeaders = new Map(); 34 | Object.keys(rawResponseHeaders).forEach(key => originalHeaders.set(key, rawResponseHeaders[key][0])); 35 | 36 | if (objectResponse.status >= 400) { 37 | // Errors in the Amazon S3 response should be forwarded to the caller without invoking transformObject. 38 | return responseForS3Errors(objectResponse); 39 | } 40 | 41 | if (objectResponse.status >= 300 && objectResponse.status < 400) { 42 | // Handle the redirect scenarios here such as Not Modified (304), Moved Permanently (301) 43 | return getHeadResponse(objectResponse.status, originalHeaders); 44 | } 45 | // Transform the Headers 46 | const transformedHeaders = transformHeaders(originalHeaders); 47 | // Handling range or partNumber 48 | const transformedHeadersWithRange = applyRangeOrPartNumberHeaders(transformedHeaders, userRequest); 49 | if (transformedHeadersWithRange.hasError || transformedHeadersWithRange.headers === undefined) { 50 | return errorResponse(requestContext, 51 | transformedHeadersWithRange.errorCode !== undefined 52 | ? transformedHeadersWithRange.errorCode 53 | : ErrorCode.INVALID_REQUEST, 54 | String(transformedHeadersWithRange.errorMessage), 55 | transformedHeadersWithRange.headers 56 | ); 57 | } 58 | 59 | return getHeadResponse(200, transformedHeadersWithRange.headers); 60 | }; 61 | 62 | /** 63 | * Returns the object expected from Object Lambda after a HEAD request 64 | * @param statusCode The statusCode to be sent back. 65 | * @param headers The headers which will be sent back. 66 | */ 67 | function getHeadResponse (statusCode: number, headers: Map): IHeadObjectResponse | IErrorResponse { 68 | return { 69 | statusCode, 70 | headers: Object.fromEntries(headers.entries()) 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/validator/RequestValidatorTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.validator; 2 | 3 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 4 | import com.example.s3objectlambda.request.GetObjectRequestWrapper; 5 | import jdk.jfr.Description; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.HashMap; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertFalse; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.when; 15 | 16 | public class RequestValidatorTest { 17 | @Test 18 | @Description("The validateUserRequest should return error when both partNumber and Range (in the header) are given") 19 | public void userRequestWithPartNumberAndRange() { 20 | 21 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 22 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?pageNumber=3&partNumber=1"); 23 | var headerMap = new HashMap(); 24 | headerMap.put("Range", "bytes=1-20"); 25 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 26 | var getObjectUserRequest = new GetObjectRequestWrapper(mockUserRequest); 27 | var requestValid = new GetObjectRequestValidator(getObjectUserRequest).validateUserRequest(); 28 | assertFalse(requestValid.isEmpty()); 29 | } 30 | 31 | @Test 32 | @DisplayName("The validateUserRequest should return error when both partNumber and Range (query string) are given") 33 | public void userRequestWithPartNumberAndRangeInQueryString() { 34 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 35 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?pageNumber=3&partNumber=1&Range=bytes=-20"); 36 | var getObjectUserRequest = new GetObjectRequestWrapper(mockUserRequest); 37 | var requestValid = new GetObjectRequestValidator(getObjectUserRequest).validateUserRequest(); 38 | assertFalse(requestValid.isEmpty()); 39 | } 40 | 41 | @Test 42 | @DisplayName("The validateUserRequest function should return empty when " + 43 | "only Range (in the query string and in header) is given") 44 | public void validUserRequestWithRange() { 45 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 46 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?Range=bytes=-20"); 47 | 48 | var headerMap = new HashMap(); 49 | headerMap.put("Range", "bytes=1-20"); 50 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 51 | var getObjectUserRequest = new GetObjectRequestWrapper(mockUserRequest); 52 | var requestValid = new GetObjectRequestValidator(getObjectUserRequest).validateUserRequest(); 53 | assertTrue(requestValid.isEmpty()); 54 | } 55 | 56 | @Test 57 | @DisplayName("The validateUserRequest function should return empty when only partNumber is given in the request") 58 | public void validUserRequestWithPartNumber() { 59 | S3ObjectLambdaEvent.UserRequest mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 60 | var getObjectUserRequest = new GetObjectRequestWrapper(mockUserRequest); 61 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?partNumber=5"); 62 | var requestValid = new GetObjectRequestValidator(getObjectUserRequest).validateUserRequest(); 63 | assertTrue(requestValid.isEmpty()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/handler/list_objects_base_handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseObjectContext, UserRequest } from '../s3objectlambda_event.types'; 2 | import { makeS3Request } from '../request/utils'; 3 | import { errorResponse, responseForS3Errors } from '../error/error_response'; 4 | import ErrorCode from '../error/error_code'; 5 | import { Buffer } from 'buffer'; 6 | import { IBaseListObject } from '../s3objectlambda_list_type'; 7 | import { IErrorResponse, IListObjectsResponse, IResponse } from '../s3objectlambda_response.types'; 8 | import { ListObjectsXmlTransformer } from '../utils/listobject_xml_transformer'; 9 | 10 | /** 11 | * Class that handles ListObjects requests. Can be used 12 | * for both ListObjectsV1 and ListObjectsV2 requests 13 | */ 14 | export class ListObjectsHandler { 15 | private readonly transformObject: (listObject: T) => T; 16 | private readonly XMLTransformer = new ListObjectsXmlTransformer(); 17 | 18 | constructor (transformObject: (listObject: T) => T) { 19 | this.transformObject = transformObject; 20 | } 21 | 22 | /** 23 | * Handles a ListObjects request, by performing the following steps: 24 | * 1. Validates the incoming user request. 25 | * 2. Retrieves the original object from Amazon S3. Converts it into an Object. 26 | * 3. Applies a transformation. You can apply your custom transformation logic here. 27 | * 4. Sends the final transformed object back to Amazon S3 Object Lambda. 28 | */ 29 | async handleListObjectsRequest (requestContext: BaseObjectContext, userRequest: UserRequest): 30 | Promise { 31 | const objectResponse = await makeS3Request(requestContext.inputS3Url, userRequest, 'GET'); 32 | 33 | const originalObject = await objectResponse.arrayBuffer(); 34 | 35 | if (objectResponse.status >= 400) { 36 | // Errors in the Amazon S3 response should be forwarded to the caller without invoking transformObject. 37 | return responseForS3Errors(objectResponse); 38 | } 39 | 40 | const parsedObject = await this.XMLTransformer.createListObjectsJsonResponse(ListObjectsHandler.stringFromArrayBuffer(originalObject)); 41 | 42 | if (parsedObject == null) { 43 | console.log('Failure parsing the response from S3'); 44 | return errorResponse(requestContext, ErrorCode.NO_SUCH_KEY, 'Requested key does not exist'); 45 | } 46 | 47 | const transformedObject = this.transformObject(parsedObject); 48 | 49 | return this.writeResponse(transformedObject); 50 | } 51 | 52 | /** 53 | * Returns the response expected by Object Lambda on a LIST_OBJECTS request 54 | * @param objectResponse The response 55 | * @protected 56 | */ 57 | protected writeResponse (objectResponse: T): IListObjectsResponse | IErrorResponse { 58 | console.log('Sending transformed results to the Object Lambda Access Point'); 59 | const xmlListObject = this.XMLTransformer.createListObjectsXmlResponse(objectResponse); 60 | 61 | if (xmlListObject === null) { 62 | console.log('Failed transforming back to XML'); 63 | return { 64 | statusCode: 500, 65 | errorMessage: 'The Lambda function failed to transform the result to XML' 66 | }; 67 | } 68 | 69 | return { 70 | statusCode: 200, 71 | listResultXml: xmlListObject 72 | }; 73 | } 74 | 75 | /** 76 | * Converts from the array buffer received to a string object. 77 | * @param arrayBuffer The array buffer containing the string 78 | * @private 79 | */ 80 | private static stringFromArrayBuffer (arrayBuffer: ArrayBuffer): string { 81 | return Buffer.from(arrayBuffer).toString(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/src/com/amazon/s3objectlambda/defaultconfig/ObjectLambdaAccessPointTest.java: -------------------------------------------------------------------------------- 1 | package com.amazon.s3objectlambda.defaultconfig; 2 | 3 | import static com.amazon.s3objectlambda.defaultconfig.KeyConstants.*; 4 | 5 | import org.testng.ITestContext; 6 | import org.testng.annotations.*; 7 | import software.amazon.awssdk.core.sync.RequestBody; 8 | import software.amazon.awssdk.regions.Region; 9 | import software.amazon.awssdk.services.s3.S3Client; 10 | import software.amazon.awssdk.services.s3.model.*; 11 | 12 | import javax.crypto.KeyGenerator; 13 | import java.nio.charset.StandardCharsets; 14 | import java.security.SecureRandom; 15 | 16 | public class ObjectLambdaAccessPointTest { 17 | 18 | protected static final int DEFAULT_PART_SIZE = 5242880; // 5 MB 19 | protected static final String DUMMY_ACCOUNT_ID = "111122223333"; 20 | protected final String data = "1234567890".repeat(10); 21 | protected final String largeData = "123".repeat(DEFAULT_PART_SIZE); 22 | protected S3Client s3Client; 23 | protected String olAccessPointName; 24 | protected String olapArn; 25 | protected String s3BucketName; 26 | protected SdkHelper sdkHelper; 27 | protected Region region; 28 | 29 | @Parameters({"region", "s3BucketName"}) 30 | @BeforeClass(alwaysRun = true) 31 | void setup(ITestContext context, String region, String s3BucketName) { 32 | this.olAccessPointName = (String) context.getAttribute(OL_AP_NAME_KEY); 33 | this.sdkHelper = new SdkHelper(); 34 | this.s3Client = sdkHelper.getS3Client(region); 35 | this.region = Region.of(region); 36 | this.olapArn = sdkHelper.getOLAccessPointArn(this.region, olAccessPointName); 37 | this.s3BucketName = s3BucketName; 38 | } 39 | 40 | protected PutObjectResponse setupResource(String objectKey, String data) { 41 | return s3Client.putObject(builder -> builder.bucket(s3BucketName).key(objectKey).build(), 42 | RequestBody.fromString(data, StandardCharsets.UTF_8)); 43 | } 44 | 45 | protected void setupResources(String data, String... objectKeys) { 46 | for (String objectKey: objectKeys) { 47 | s3Client.putObject(builder -> builder.bucket(s3BucketName).key(objectKey).build(), 48 | RequestBody.fromString(data, StandardCharsets.UTF_8)); 49 | } 50 | } 51 | 52 | protected void cleanupResources(String... objectKeys) { 53 | for(String objectKey : objectKeys) { 54 | s3Client.deleteObject(builder -> builder.bucket(s3BucketName).key(objectKey).build()); 55 | } 56 | } 57 | 58 | protected PutObjectResponse setupResourceWithChecksum(String objectKey, String data) { 59 | return s3Client.putObject(builder -> builder.bucket(s3BucketName) 60 | .checksumAlgorithm(ChecksumAlgorithm.CRC32) 61 | .key(objectKey) 62 | .build(), 63 | RequestBody.fromString(data, StandardCharsets.UTF_8)); 64 | } 65 | 66 | protected void cleanupResource(String objectKey) { 67 | s3Client.deleteObject(builder -> builder.bucket(s3BucketName).key(objectKey).build()); 68 | } 69 | 70 | protected byte[] generateSecretKey() { 71 | KeyGenerator generator; 72 | try { 73 | generator = KeyGenerator.getInstance("AES"); 74 | generator.init(256, new SecureRandom()); 75 | return generator.generateKey().getEncoded(); 76 | } catch (Exception e) { 77 | return null; 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /tests/src/com/amazon/s3objectlambda/defaultconfig/SetupCloudFormationTest.java: -------------------------------------------------------------------------------- 1 | package com.amazon.s3objectlambda.defaultconfig; 2 | 3 | import static com.amazon.s3objectlambda.defaultconfig.KeyConstants.*; 4 | 5 | import org.testng.Assert; 6 | import org.testng.annotations.BeforeClass; 7 | import org.testng.annotations.Parameters; 8 | import org.testng.annotations.Test; 9 | import org.testng.ITestContext; 10 | import software.amazon.awssdk.services.cloudformation.model.*; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | 15 | 16 | @Test(groups = "setup") 17 | public class SetupCloudFormationTest { 18 | 19 | @BeforeClass(description = "Generate random name for Access Points and Stack name") 20 | public void generateName(ITestContext context) { 21 | var sdkHelper = new SdkHelper(); 22 | context.setAttribute(STACK_NAME_KEY, sdkHelper.generateRandomResourceName(8)); 23 | context.setAttribute(OL_AP_NAME_KEY, sdkHelper.generateRandomResourceName(8)); 24 | context.setAttribute(SUPPORT_AP_NAME_KEY, sdkHelper.generateRandomResourceName(8)); 25 | } 26 | 27 | @Parameters({"region", "templateUrl", "s3BucketName", "lambdaFunctionS3BucketName", 28 | "lambdaFunctionS3Key", "lambdaFunctionRuntime", "createNewSupportingAccessPoint", "lambdaVersion"}) 29 | @Test(description = "Deploy the CloudFormation template to set up s3ol access point") 30 | @SuppressWarnings("checkstyle:parameternumber") 31 | public void deployStack(ITestContext context, String region, String templateUrl, String s3BucketName, 32 | String lambdaFunctionS3BucketName, String lambdaFunctionS3Key, String lambdaFunctionRuntime, 33 | String createNewSupportingAccessPoint, String lambdaVersion) { 34 | var sdkHelper = new SdkHelper(); 35 | String stackName = (String) context.getAttribute(STACK_NAME_KEY); 36 | String olAccessPointName = (String) context.getAttribute(OL_AP_NAME_KEY); 37 | String supportingAccessPointName = (String) context.getAttribute(SUPPORT_AP_NAME_KEY); 38 | var cloudFormationClient = sdkHelper.getCloudFormationClient(region); 39 | ArrayList parameters = new ArrayList<>(Arrays.asList( 40 | sdkHelper.buildParameter("ObjectLambdaAccessPointName", olAccessPointName), 41 | sdkHelper.buildParameter("SupportingAccessPointName", supportingAccessPointName), 42 | sdkHelper.buildParameter("S3BucketName", s3BucketName), 43 | sdkHelper.buildParameter("LambdaFunctionS3BucketName", lambdaFunctionS3BucketName), 44 | sdkHelper.buildParameter("LambdaFunctionS3Key", lambdaFunctionS3Key), 45 | sdkHelper.buildParameter("LambdaFunctionRuntime", lambdaFunctionRuntime), 46 | sdkHelper.buildParameter("LambdaFunctionS3ObjectVersion", lambdaVersion), 47 | sdkHelper.buildParameter("CreateNewSupportingAccessPoint", createNewSupportingAccessPoint))); 48 | var createStackRequest = CreateStackRequest.builder() 49 | .templateURL(templateUrl) 50 | .stackName(stackName) 51 | .parameters(parameters) 52 | .capabilitiesWithStrings("CAPABILITY_NAMED_IAM") 53 | .build(); 54 | cloudFormationClient.createStack(createStackRequest); 55 | var cloudFormationWaiter = cloudFormationClient.waiter(); 56 | DescribeStacksRequest describeStacksRequest = DescribeStacksRequest.builder().stackName(stackName).build(); 57 | //waiter.response will either return a response or throwable 58 | var waiterResponse = cloudFormationWaiter.waitUntilStackCreateComplete(describeStacksRequest); 59 | Assert.assertTrue(waiterResponse.matched().response().isPresent()); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/error/error_response.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'xml2js'; 2 | import ErrorCode from './error_code'; 3 | import { BaseObjectContext, GetObjectContext } from '../s3objectlambda_event.types'; 4 | import { S3Client, WriteGetObjectResponseCommand, WriteGetObjectResponseCommandOutput } from '@aws-sdk/client-s3'; 5 | import { Response, Headers } from 'node-fetch'; 6 | import { IErrorResponse } from '../s3objectlambda_response.types'; 7 | 8 | /** 9 | * Generates a response to Amazon S3 Object Lambda when there is an error 10 | * with a GET_OBJECT request. 11 | */ 12 | export async function getErrorResponse (s3Client: S3Client, requestContext: GetObjectContext, 13 | errorCode: ErrorCode, errorMessage: string, headers: Headers = new Headers()): Promise { 14 | console.log(`Returning an error [${errorCode}] ${errorMessage} to the Object Lambda Access Point`); 15 | 16 | const wgorCommand = new WriteGetObjectResponseCommand({ 17 | RequestRoute: requestContext.outputRoute, 18 | RequestToken: requestContext.outputToken, 19 | StatusCode: ERROR_TO_STATUS_CODE_MAP[errorCode], 20 | ErrorCode: errorCode, 21 | ErrorMessage: errorMessage, 22 | ...headers 23 | }); 24 | 25 | return s3Client.send(wgorCommand); 26 | } 27 | 28 | /** 29 | * Generates a response to Amazon S3 Object Lambda when there is an error 30 | * with a HEAD_OBJECT / LIST_OBJECTS_V1 / LIST_OBJECTS_V2 request. 31 | */ 32 | export function errorResponse (requestContext: BaseObjectContext, 33 | errorCode: ErrorCode, errorMessage: string, headers: Map = new Map()): IErrorResponse { 34 | console.log(`Returning an error [${errorCode}] ${errorMessage} to the Object Lambda Access Point`); 35 | 36 | return { 37 | statusCode: ERROR_TO_STATUS_CODE_MAP[errorCode], 38 | errorCode: errorCode, 39 | errorMessage: errorMessage, 40 | ...Object.fromEntries(headers) 41 | }; 42 | } 43 | 44 | /** 45 | * Sends back the error that was received from S3 on a GET_OBJECT request 46 | */ 47 | export async function getResponseForS3Errors (s3Client: S3Client, requestContext: GetObjectContext, objectResponse: Response, 48 | headers: Headers, objectResponseBody: Buffer): Promise { 49 | const objectResponseData = await new Parser().parseStringPromise(objectResponseBody); 50 | console.log(`Encountered an S3 Error, status code: ${objectResponse.status}. Forwarding this to the Object Lambda Access Point.`); 51 | 52 | const wgorCommand = new WriteGetObjectResponseCommand({ 53 | RequestRoute: requestContext.outputRoute, 54 | RequestToken: requestContext.outputToken, 55 | StatusCode: objectResponse.status, 56 | ErrorCode: objectResponseData.Code, 57 | ErrorMessage: `Received ${objectResponse.statusText} from the supporting Access Point.`, 58 | ...headers 59 | }); 60 | 61 | return s3Client.send(wgorCommand); 62 | } 63 | 64 | /** 65 | * Sends back the error that was received from S3 on a HEAD_OBJECT / LIST_OBJECTS request 66 | */ 67 | export function responseForS3Errors (objectResponse: Response): IErrorResponse { 68 | console.log(`Encountered an S3 Error, status code: ${objectResponse.status}. Forwarding this to the Object Lambda Access Point.`); 69 | 70 | return { 71 | statusCode: objectResponse.status, 72 | errorMessage: `Received ${objectResponse.statusText} from the supporting Access Point.` 73 | }; 74 | } 75 | 76 | /** 77 | * Maps error codes to HTTP Status codes based on the {@link https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html | Error Responses} 78 | * mapping in Amazon S3. 79 | */ 80 | const ERROR_TO_STATUS_CODE_MAP = { 81 | [ErrorCode.INVALID_REQUEST]: 400, 82 | [ErrorCode.INVALID_PART]: 400, 83 | [ErrorCode.INVALID_RANGE]: 416, 84 | [ErrorCode.NO_SUCH_KEY]: 404 85 | }; 86 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/transform/TransformTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | import com.example.s3objectlambda.exception.InvalidPartNumberException; 4 | import com.example.s3objectlambda.exception.InvalidRangeException; 5 | import com.example.s3objectlambda.exception.TransformationException; 6 | import com.example.s3objectlambda.request.GetObjectRequestWrapper; 7 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 8 | 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.net.URISyntaxException; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.when; 20 | 21 | public class TransformTest { 22 | 23 | private static final String ORIGINAL_RESPONSE = "12345678910!"; 24 | 25 | @Test 26 | @DisplayName("TODO: Transform logic works as expected.") 27 | public void transformObjectResponseTest() throws TransformationException { 28 | //Todo: Rewrite this test based your transformation logic. 29 | 30 | byte[] responseInputStream = ORIGINAL_RESPONSE.getBytes(StandardCharsets.UTF_16); 31 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 32 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 33 | var transformedResponseObject = new GetObjectTransformer(objectRequest) 34 | .transformObjectResponse(responseInputStream); 35 | var transformedString = new String(transformedResponseObject, StandardCharsets.UTF_16); 36 | assertEquals(ORIGINAL_RESPONSE, transformedString); 37 | } 38 | 39 | @Test 40 | @DisplayName("Apply Range or partNumber on object and verify the size.") 41 | public void applyRangeOrPartNumberHasError() 42 | throws InvalidRangeException, URISyntaxException, InvalidPartNumberException { 43 | var responseInputStream = ORIGINAL_RESPONSE.getBytes(StandardCharsets.UTF_16); 44 | 45 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 46 | when(mockUserRequest.getUrl()).thenReturn("https://example.com?Range=bytes=1-3"); 47 | 48 | var headerMap = new HashMap(); 49 | headerMap.put("Range", "bytes=1-3"); 50 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 51 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 52 | var transformedResponseObject = new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 53 | responseInputStream); 54 | 55 | assertEquals(3, transformedResponseObject.length); 56 | 57 | } 58 | 59 | @Test 60 | @DisplayName("Apply Range or partNumber on object and verify response") 61 | public void applyRangeOrPartNumberResponseObject() 62 | throws InvalidRangeException, URISyntaxException, InvalidPartNumberException { 63 | byte[] responseInputStream = ORIGINAL_RESPONSE.getBytes(StandardCharsets.UTF_16); 64 | 65 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 66 | when(mockUserRequest.getUrl()).thenReturn("https://example.com"); 67 | 68 | Map headerMap = new HashMap(); 69 | headerMap.put("Range", "bytes=-26"); 70 | 71 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 72 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 73 | var transformedResponseObject = new GetObjectTransformer(objectRequest) 74 | .applyRangeOrPartNumber(responseInputStream); 75 | 76 | var transformedString = new String(transformedResponseObject, StandardCharsets.UTF_16); 77 | assertEquals(ORIGINAL_RESPONSE, transformedString); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/transform/RangeMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | import com.example.s3objectlambda.exception.InvalidRangeException; 4 | 5 | import java.util.Arrays; 6 | import java.util.Objects; 7 | 8 | 9 | /** 10 | * Handles range requests by applying the range to the transformed object. Supported range headers are: 11 | *

12 | * Range: =- 13 | * Range: =- 14 | * Range: =- 15 | *

16 | * Amazon S3 does not support retrieving multiple ranges of data per GetObject request. Please see 17 | * Request Syntax}. 18 | * for more information. 19 | *

20 | * The only supported unit in this implementation is `bytes`. If other units are requested, we treat this as 21 | * an invalid request. 22 | */ 23 | 24 | public class RangeMapper { 25 | 26 | private static final String BYTES_UNIT = "bytes"; 27 | private final String[] supportedUnits = {BYTES_UNIT}; 28 | private Range range; 29 | 30 | /** 31 | * This constructor accepts range as string and instantiate the object after instantiating Range. 32 | * @param range range as string in one of the format supported. 33 | * @throws InvalidRangeException 34 | */ 35 | public RangeMapper(String range) throws InvalidRangeException { 36 | this.range = new Range(range); 37 | } 38 | 39 | /** 40 | * This constructor accepts range object. 41 | * @param range Range object. 42 | */ 43 | public RangeMapper(Range range) { 44 | this.range = range; 45 | } 46 | 47 | /** 48 | * This function apply range on the response object byte array. 49 | * @param responseObjectByteArray Response object on which range to be applied. 50 | * @return Returns result byte array after range is applied. 51 | * @throws InvalidRangeException 52 | */ 53 | public byte[] mapRange(byte[] responseObjectByteArray) throws InvalidRangeException { 54 | 55 | if (!Arrays.asList(supportedUnits).contains(this.range.getUnit())) { 56 | throw new InvalidRangeException(String.format("Only %s as unit supported but %s was provided.", 57 | supportedUnits, this.range.getUnit())); 58 | } 59 | 60 | if (Objects.equals(this.range.getUnit(), BYTES_UNIT)) { 61 | return applyRangeOnBytes(responseObjectByteArray); 62 | } 63 | 64 | throw new RuntimeException("Not implemented range unit support:" + this.range.getUnit()); 65 | } 66 | 67 | private byte[] applyRangeOnBytes(byte[] responseObjectByteArray) throws InvalidRangeException { 68 | int rangeStart; 69 | int rangeEnd; 70 | int objectLength = responseObjectByteArray.length; 71 | 72 | if (this.range.getFirstPart() == null) { 73 | // Range request was of the form =- so we return the last `suffix-length` bytes. 74 | int suffixLength = Integer.parseInt(this.range.getLastPart()); 75 | 76 | // If the byte array length is 26, the last byte is at 25th position in the array. 77 | rangeEnd = objectLength - 1; 78 | rangeStart = objectLength - suffixLength; 79 | 80 | } else if (this.range.getLastPart() == null) { 81 | // Range request was of the form =- so we return from range-start to the end of the object. 82 | rangeStart = Integer.parseInt(this.range.getFirstPart()); 83 | rangeEnd = objectLength - 1; 84 | } else { 85 | rangeStart = Integer.parseInt(this.range.getFirstPart()); 86 | rangeEnd = Integer.parseInt(this.range.getLastPart()); 87 | rangeEnd = Math.min(objectLength - 1, rangeEnd); // Should not exceed object length 88 | } 89 | 90 | if (rangeEnd < rangeStart || rangeStart < 0) { 91 | throw new InvalidRangeException("Invalid Range"); 92 | } 93 | 94 | //Add 1 at the range end because Arrays.copyOfRange's is exclusive. 95 | var objectPart = Arrays.copyOfRange(responseObjectByteArray, rangeStart, rangeEnd + 1); 96 | return objectPart; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/request/S3PresignedUrlParserHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.request; 2 | 3 | import org.junit.jupiter.api.DisplayName; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | import java.net.MalformedURLException; 9 | import java.util.List; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertThrows; 13 | 14 | @ExtendWith(MockitoExtension.class) 15 | public class S3PresignedUrlParserHelperTest { 16 | 17 | static final String TEST_URL_1 = 18 | "https://test-access-point-012345678901.s3-accesspoint.us-east-1.amazonaws.com/test" + 19 | "?X-Amz-Security-Token=TestToken" + 20 | "&X-Amz-Algorithm=AWS4-HMAC-SHA256" + 21 | "&X-Amz-Date=20250220T175710Z" + 22 | "&X-Amz-SignedHeaders=host%3Bx-amz-checksum-mode" + 23 | "&X-Amz-Expires=61" + 24 | "&X-Amz-Credential=AKIAEXAMPLE/20250220/us-east-1/s3/aws4_request" + 25 | "&X-Amz-Signature=a7f9b2c8e4d1f3a6b5c9e2d7a8f4b3c1d6e5f2a9b7c8d3e1f6a2b9c7d5e8f3a1"; 26 | 27 | static final String TEST_URL_2 = 28 | "https://test-access-point-012345678901.s3-accesspoint.us-east-1.amazonaws.com/test" + 29 | "?X-Amz-Security-Token=TestToken" + 30 | "&X-Amz-Algorithm=AWS4-HMAC-SHA256" + 31 | "&X-Amz-Date=20250220T175710Z" + 32 | "&X-Amz-SignedHeaders=host" + 33 | "&X-Amz-Expires=61" + 34 | "&X-Amz-Credential=AKIAEXAMPLE/20250220/us-east-1/s3/aws4_request" + 35 | "&X-Amz-Signature=a7f9b2c8e4d1f3a6b5c9e2d7a8f4b3c1d6e5f2a9b7c8d3e1f6a2b9c7d5e8f3a1"; 36 | 37 | static final String TEST_URL_3 = 38 | "https://test-access-point-012345678901.s3-accesspoint.us-east-1.amazonaws.com/test" + 39 | "?X-Amz-Security-Token=TestToken" + 40 | "&X-Amz-Algorithm=AWS4-HMAC-SHA256" + 41 | "&X-Amz-Date=20250220T175710Z" + 42 | "&X-Amz-Expires=61" + 43 | "&X-Amz-Credential=AKIAEXAMPLE/20250220/us-east-1/s3/aws4_request" + 44 | "&X-Amz-Signature=a7f9b2c8e4d1f3a6b5c9e2d7a8f4b3c1d6e5f2a9b7c8d3e1f6a2b9c7d5e8f3a1"; 45 | 46 | static final String TEST_URL_MALFORMED = 47 | "htps://test-access-point-012345678901.s3-accesspoint.us-east-1.amazonaws.com/test" + 48 | "?X-Amz-Security-Token=TestToken" + 49 | "&X-Amz-Algorithm=AWS4-HMAC-SHA256" + 50 | "&X-Amz-Date=20250220T175710Z" + 51 | "&X-Amz-Expires=61" + 52 | "&X-Amz-Credential=AKIAEXAMPLE/20250220/us-east-1/s3/aws4_request" + 53 | "&X-Amz-Signature=a7f9b2c8e4d1f3a6b5c9e2d7a8f4b3c1d6e5f2a9b7c8d3e1f6a2b9c7d5e8f3a1"; 54 | 55 | static final String HEADER_HOST = "host"; 56 | static final String HEADER_CHECKSUM = "x-amz-checksum-mode"; 57 | 58 | @Test 59 | @DisplayName("Parse URL with checksum and host signed headers.") 60 | void testParsePresignedUrlWithChecksumAndHost() throws MalformedURLException { 61 | List signedHeaders = S3PresignedUrlParserHelper.retrieveSignedHeadersFromPresignedUrl(TEST_URL_1); 62 | assert signedHeaders != null; 63 | assertEquals(2, signedHeaders.size()); 64 | assertEquals(HEADER_HOST, signedHeaders.get(0)); 65 | assertEquals(HEADER_CHECKSUM, signedHeaders.get(1)); 66 | } 67 | 68 | @Test 69 | @DisplayName("Parse URL with host signed header.") 70 | void testParsePresignedUrlWithHost() throws MalformedURLException { 71 | List signedHeaders = S3PresignedUrlParserHelper.retrieveSignedHeadersFromPresignedUrl(TEST_URL_2); 72 | assert signedHeaders != null; 73 | assertEquals(1, signedHeaders.size()); 74 | assertEquals(HEADER_HOST, signedHeaders.get(0)); 75 | } 76 | 77 | @Test 78 | @DisplayName("Parse URL with no signed headers.") 79 | void testParsePresignedUrlWithNoSignedHeaders() throws MalformedURLException { 80 | List signedHeaders = S3PresignedUrlParserHelper.retrieveSignedHeadersFromPresignedUrl(TEST_URL_3); 81 | assert signedHeaders != null; 82 | assertEquals(0, signedHeaders.size()); 83 | } 84 | 85 | @Test 86 | @DisplayName("Try parse malformed URL.") 87 | void testParseMalformedPresignedUrl() { 88 | assertThrows( 89 | MalformedURLException.class, 90 | () -> S3PresignedUrlParserHelper.retrieveSignedHeadersFromPresignedUrl(TEST_URL_MALFORMED) 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/error/S3RequestErrorTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.error; 2 | 3 | 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.TestInstance; 8 | import org.xml.sax.SAXException; 9 | 10 | import javax.xml.parsers.ParserConfigurationException; 11 | import java.io.IOException; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.fail; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 18 | public class S3RequestErrorTest { 19 | private XMLErrorParser xmlErrorParser; 20 | private S3RequestError s3RequestError; 21 | 22 | @BeforeAll 23 | void setup() throws ParserConfigurationException, IOException, SAXException { 24 | this.xmlErrorParser = new XMLErrorParser(); 25 | var xmlResponse = getS3XMLErrorResponse(); 26 | this.s3RequestError = this.xmlErrorParser.parse(xmlResponse); 27 | } 28 | 29 | @Test 30 | void testGetCode() { 31 | assertEquals("No-SuchKey", this.s3RequestError.getCode()); 32 | } 33 | 34 | @Test 35 | void testGetMessage() { 36 | assertEquals("The resource you requested does not exist", this.s3RequestError.getMessage()); 37 | } 38 | 39 | @Test 40 | void testGetRequestId() { 41 | assertEquals("4442587FB7D0A2F9", this.s3RequestError.getRequestId()); 42 | } 43 | 44 | @Test 45 | @DisplayName("Parser Throws Exception on invalid XMl.") 46 | void testParseTrowsException() { 47 | try { 48 | this.xmlErrorParser.parse(""); 49 | fail("Parser did not throw exception."); 50 | } catch (ParserConfigurationException | SAXException | IOException e) { 51 | assertTrue(!e.getMessage().isEmpty()); 52 | } 53 | } 54 | 55 | @Test 56 | @DisplayName("Parser Throws Exception on valid XML but unexpected response.") 57 | void testParseTrowsExceptionInvalidStructure() { 58 | try { 59 | var xmlResponse = this.getS3XMLInvalidErrorResponse(); 60 | this.xmlErrorParser.parse(xmlResponse); 61 | fail("Parser did not throw exception."); 62 | } catch (ParserConfigurationException | SAXException | IOException | NullPointerException e) { 63 | 64 | } 65 | } 66 | 67 | @Test 68 | @DisplayName("Parser Throws Exception on valid XML but missing Code.") 69 | void testParseTrowsExceptionInvalidStructureMissingCode() { 70 | try { 71 | var xmlResponse = this.getS3XMLInvalidErrorResponseNoCode(); 72 | this.xmlErrorParser.parse(xmlResponse); 73 | fail("Parser did not throw exception."); 74 | } catch (ParserConfigurationException | SAXException | IOException | NullPointerException e) { 75 | 76 | } 77 | } 78 | 79 | private String getS3XMLErrorResponse() { 80 | return "" + 81 | "\n" + 82 | "\n" + 83 | " No-SuchKey\n" + 84 | " The resource you requested does not exist\n" + 85 | " /mybucket/myfoto.jpg \n" + 86 | " 4442587FB7D0A2F9\n" + 87 | ""; 88 | } 89 | 90 | private String getS3XMLInvalidErrorResponse() { 91 | return "" + 92 | "\n" + 93 | "\n" + 94 | " No-SuchKey\n" + 95 | " The resource you requested does not exist\n" + 96 | " /mybucket/myfoto.jpg \n" + 97 | " 4442587FB7D0A2F9\n" + 98 | ""; 99 | } 100 | 101 | private String getS3XMLInvalidErrorResponseNoCode() { 102 | return "" + 103 | "\n" + 104 | "\n" + 105 | " The resource you requested does not exist\n" + 106 | " /mybucket/myfoto.jpg \n" + 107 | " 4442587FB7D0A2F9\n" + 108 | ""; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /function/java17/src/main/java/com/example/s3objectlambda/response/GetObjectResponseHandler.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.response; 2 | 3 | import com.example.s3objectlambda.checksum.Checksum; 4 | import com.example.s3objectlambda.checksum.ChecksumGenerator; 5 | import com.example.s3objectlambda.error.Error; 6 | import com.example.s3objectlambda.error.S3RequestError; 7 | import com.example.s3objectlambda.error.XMLErrorParser; 8 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 9 | import com.amazonaws.services.s3.AmazonS3; 10 | import com.amazonaws.services.s3.model.ObjectMetadata; 11 | import com.amazonaws.services.s3.model.WriteGetObjectResponseRequest; 12 | import com.amazonaws.util.IOUtils; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.xml.sax.SAXException; 16 | 17 | import javax.xml.parsers.ParserConfigurationException; 18 | import java.io.ByteArrayInputStream; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.net.http.HttpResponse; 22 | import java.util.HashMap; 23 | 24 | /** 25 | * This handles writing of the object response by calling writeGetObjectResponse 26 | * which Passes transformed objects to a GetObject operation. 27 | */ 28 | 29 | public class GetObjectResponseHandler implements ResponseHandler { 30 | 31 | private Logger logger; 32 | private final AmazonS3 s3Client; 33 | private final S3ObjectLambdaEvent event; 34 | private final ChecksumGenerator checksumGenerator; 35 | 36 | public GetObjectResponseHandler(AmazonS3 s3Client, S3ObjectLambdaEvent event, ChecksumGenerator checksumGenerator) { 37 | this.s3Client = s3Client; 38 | this.event = event; 39 | this.checksumGenerator = checksumGenerator; 40 | this.logger = LoggerFactory.getLogger(GetObjectResponseHandler.class); 41 | } 42 | 43 | public void writeS3GetObjectErrorResponse(HttpResponse presignedResponse) { 44 | 45 | S3RequestError s3errorResponse; 46 | 47 | try { 48 | var xmlResponse = IOUtils.toString(presignedResponse.body()); 49 | s3errorResponse = new XMLErrorParser().parse(xmlResponse); 50 | } catch (IOException | ParserConfigurationException | SAXException | NullPointerException e) { 51 | this.logger.error("Error while reading the S3 error response body: " + e); 52 | writeErrorResponse("Unexpected error while reading the S3 error response", Error.SERVER_ERROR); 53 | return; 54 | } 55 | 56 | 57 | this.s3Client.writeGetObjectResponse(new WriteGetObjectResponseRequest() 58 | .withRequestRoute(this.event.outputRoute()) 59 | .withRequestToken(this.event.outputToken()) 60 | .withErrorCode(s3errorResponse.getCode()) 61 | .withContentLength(0L).withInputStream(new ByteArrayInputStream(new byte[0])) 62 | .withErrorMessage(s3errorResponse.getMessage()) 63 | .withStatusCode(presignedResponse.statusCode())); 64 | } 65 | 66 | 67 | public void writeErrorResponse(String errorMessage, Error error) { 68 | 69 | this.s3Client.writeGetObjectResponse(new WriteGetObjectResponseRequest() 70 | .withRequestRoute(event.outputRoute()) 71 | .withRequestToken(event.outputToken()) 72 | .withErrorCode(error.getErrorCode()) 73 | .withContentLength(0L).withInputStream(new ByteArrayInputStream(new byte[0])) 74 | .withErrorMessage(errorMessage) 75 | .withStatusCode(error.getStatusCode())); 76 | } 77 | 78 | public void writeObjectResponse(HttpResponse presignedResponse, byte[] responseObjectByteArray) { 79 | 80 | Checksum checksum; 81 | try { 82 | checksum = this.checksumGenerator.getChecksum(responseObjectByteArray); 83 | } catch (Exception e) { 84 | this.logger.error("Error while writing object response" + e); 85 | writeErrorResponse("Error while writing object response.", Error.SERVER_ERROR); 86 | return; 87 | } 88 | 89 | var checksumMap = new HashMap(); 90 | checksumMap.put("algorithm", checksum.getAlgorithm()); 91 | checksumMap.put("digest", checksum.getChecksum()); 92 | 93 | var checksumObjectMetaData = new ObjectMetadata(); 94 | checksumObjectMetaData.setUserMetadata(checksumMap); 95 | 96 | this.s3Client.writeGetObjectResponse(new WriteGetObjectResponseRequest() 97 | .withRequestRoute(event.outputRoute()) 98 | .withRequestToken(event.outputToken()) 99 | .withInputStream(new ByteArrayInputStream(responseObjectByteArray)) 100 | .withMetadata(checksumObjectMetaData) 101 | .withStatusCode(presignedResponse.statusCode())); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /function/java17/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.amazon.s3objectlambda.defaultconfig 8 | S3ObjectLambdaDefaultConfigJavaFunction 9 | 1.0 10 | 11 | 17 12 | 17 13 | UTF-8 14 | UTF-8 15 | 16 | 17 | 18 | 19 | 20 | maven-surefire-plugin 21 | 2.22.1 22 | 23 | 24 | maven-failsafe-plugin 25 | 2.22.2 26 | 27 | 28 | 29 | org.apache.maven.plugins 30 | maven-shade-plugin 31 | 3.2.2 32 | 33 | 34 | false 35 | 36 | 37 | 38 | package 39 | 40 | shade 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | com.amazonaws 51 | aws-lambda-java-events 52 | 3.11.0 53 | 54 | 55 | 56 | com.amazonaws 57 | aws-lambda-java-core 58 | 1.2.1 59 | 60 | 61 | 62 | org.apache.httpcomponents 63 | httpclient 64 | 4.5.13 65 | 66 | 67 | 68 | com.amazonaws 69 | aws-java-sdk-s3 70 | 1.12.261 71 | 72 | 73 | 74 | 75 | org.apache.logging.log4j 76 | log4j-api 77 | [2.31.1,) 78 | 79 | 80 | org.apache.logging.log4j 81 | log4j-core 82 | [2.31.1,) 83 | 84 | 85 | org.slf4j 86 | slf4j-api 87 | [2.0.13,) 88 | 89 | 90 | org.slf4j 91 | slf4j-log4j12 92 | [2.0.13,) 93 | 94 | 95 | 96 | org.mockito 97 | mockito-core 98 | 4.2.0 99 | test 100 | 101 | 102 | 103 | org.mockito 104 | mockito-junit-jupiter 105 | 4.2.0 106 | test 107 | 108 | 109 | 110 | com.amazonaws 111 | aws-lambda-java-log4j2 112 | 1.6.0 113 | 114 | 115 | 116 | junit 117 | junit 118 | 4.13.2 119 | test 120 | 121 | 122 | 123 | org.junit.jupiter 124 | junit-jupiter-engine 125 | 5.8.2 126 | test 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /function/nodejs_20_x/tst/response/range_mapper.test.ts: -------------------------------------------------------------------------------- 1 | import { mapRange, mapRangeHead } from '../../src/response/range_mapper'; 2 | import { CONTENT_LENGTH } from '../../src/request/utils'; 3 | 4 | const HAS_ERROR = 'hasError'; 5 | const FIVE_MB_CONTENT_LENGTH = 5242880; 6 | 7 | test('Invalid range format returns error', () => { 8 | expect(mapRange('123', createBuffer('hello'))) 9 | .toHaveProperty(HAS_ERROR, true); 10 | }); 11 | 12 | test('Multiple ranges returns error', () => { 13 | expect(mapRange('bytes=1-2-3', createBuffer('hello'))) 14 | .toHaveProperty(HAS_ERROR, true); 15 | }); 16 | 17 | test('Unsupported range unit returns error', () => { 18 | expect(mapRange('kilobytes=0-10', createBuffer('hello'))) 19 | .toHaveProperty(HAS_ERROR, true); 20 | }); 21 | 22 | test('Invalid range value returns error', () => { 23 | expect(mapRange('bytes=-', createBuffer('hello'))) 24 | .toHaveProperty(HAS_ERROR, true); 25 | }); 26 | 27 | test('Range with only start works', () => { 28 | expect(mapRange('bytes=2-', createBuffer('hello'))) 29 | .toStrictEqual({ [HAS_ERROR]: false, object: createBuffer('llo') }); 30 | }); 31 | 32 | test('Range with suffix length works', () => { 33 | expect(mapRange('bytes=-2', createBuffer('hello'))) 34 | .toStrictEqual({ [HAS_ERROR]: false, object: createBuffer('lo') }); 35 | }); 36 | 37 | test('Range with 0 start and end works', () => { 38 | expect(mapRange('bytes=0-3', createBuffer('hello'))) 39 | .toStrictEqual({ [HAS_ERROR]: false, object: createBuffer('hell') }); 40 | }); 41 | 42 | test('Range with non-zero start and end works', () => { 43 | expect(mapRange('bytes=1-3', createBuffer('hello'))) 44 | .toStrictEqual({ [HAS_ERROR]: false, object: createBuffer('ell') }); 45 | }); 46 | 47 | test('Two digit range value works', () => { 48 | expect(mapRange('bytes=10-15', createBuffer('amazonwebservices'))) 49 | .toStrictEqual({ [HAS_ERROR]: false, object: createBuffer('ervice') }); 50 | }); 51 | 52 | test('Range End > Range Start returns error', () => { 53 | expect(mapRange('bytes=15-10', createBuffer('amazonwebservices'))) 54 | .toHaveProperty(HAS_ERROR, true); 55 | }); 56 | 57 | test('Invalid head range format returns error', () => { 58 | expect(mapRangeHead('123', createHeaders(FIVE_MB_CONTENT_LENGTH))) 59 | .toHaveProperty(HAS_ERROR, true); 60 | }); 61 | 62 | test('Multiple head ranges returns error', () => { 63 | expect(mapRangeHead('bytes=1-2-3', createHeaders(FIVE_MB_CONTENT_LENGTH))) 64 | .toHaveProperty(HAS_ERROR, true); 65 | }); 66 | 67 | test('Unsupported head range unit returns error', () => { 68 | expect(mapRangeHead('kilobytes=0-10', createHeaders(FIVE_MB_CONTENT_LENGTH))) 69 | .toHaveProperty(HAS_ERROR, true); 70 | }); 71 | 72 | test('Invalid head range value returns error', () => { 73 | expect(mapRangeHead('bytes=-', createHeaders(FIVE_MB_CONTENT_LENGTH))) 74 | .toHaveProperty(HAS_ERROR, true); 75 | }); 76 | 77 | test('Head Range with only start works', () => { 78 | expect(mapRangeHead('bytes=2-', createHeaders(FIVE_MB_CONTENT_LENGTH))) 79 | .toStrictEqual({ 80 | headers: new Map([ 81 | [CONTENT_LENGTH, Object(FIVE_MB_CONTENT_LENGTH - 2)] 82 | ]), 83 | [HAS_ERROR]: false 84 | }); 85 | }); 86 | 87 | test('Head Range with suffix length works', () => { 88 | expect(mapRangeHead('bytes=-2', createHeaders(FIVE_MB_CONTENT_LENGTH))) 89 | .toStrictEqual({ 90 | headers: new Map([ 91 | [CONTENT_LENGTH, Object(2)] 92 | ]), 93 | [HAS_ERROR]: false 94 | }); 95 | }); 96 | 97 | test('Head Range with 0 start and end works', () => { 98 | expect(mapRangeHead('bytes=0-3', createHeaders(FIVE_MB_CONTENT_LENGTH))) 99 | .toStrictEqual({ 100 | headers: new Map([ 101 | [CONTENT_LENGTH, Object(4)] 102 | ]), 103 | [HAS_ERROR]: false 104 | }); 105 | }); 106 | 107 | test('Head Range with non-zero start and end works', () => { 108 | expect(mapRangeHead('bytes=1-3', createHeaders(FIVE_MB_CONTENT_LENGTH))) 109 | .toStrictEqual({ 110 | headers: new Map([ 111 | [CONTENT_LENGTH, Object(3)] 112 | ]), 113 | [HAS_ERROR]: false 114 | }); 115 | }); 116 | 117 | test('Two digit range head value works', () => { 118 | expect(mapRangeHead('bytes=10-15', createHeaders(FIVE_MB_CONTENT_LENGTH))) 119 | .toStrictEqual({ 120 | headers: new Map([ 121 | [CONTENT_LENGTH, Object(6)] 122 | ]), 123 | [HAS_ERROR]: false 124 | }); 125 | }); 126 | 127 | test('Head Range End > Range Start returns error', () => { 128 | expect(mapRangeHead('bytes=15-10', createHeaders(FIVE_MB_CONTENT_LENGTH))) 129 | .toHaveProperty(HAS_ERROR, true); 130 | }); 131 | 132 | function createBuffer (input: string): Buffer { 133 | return Buffer.from(input, 'utf-8'); 134 | } 135 | 136 | function createHeaders (contentLength?: number): Map { 137 | const headers = new Map(); 138 | if (contentLength !== undefined) { 139 | headers.set(CONTENT_LENGTH, Object(contentLength)); 140 | } 141 | return headers; 142 | } 143 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/handler/get_object_handler.ts: -------------------------------------------------------------------------------- 1 | import { GetObjectContext, UserRequest } from '../s3objectlambda_event.types'; 2 | import { makeS3Request, applyRangeOrPartNumber } from '../request/utils'; 3 | import ErrorCode from '../error/error_code'; 4 | import { Response, Headers } from 'node-fetch'; 5 | import { getErrorResponse, getResponseForS3Errors } from '../error/error_response'; 6 | import { S3Client, WriteGetObjectResponseCommand, WriteGetObjectResponseCommandOutput } from '@aws-sdk/client-s3'; 7 | import { transformObject } from '../transform/s3objectlambda_transformer'; 8 | import { validate } from '../request/validator'; 9 | import getChecksum from '../checksum/checksum'; 10 | import { headerToWgorParam, ParamsKeys } from '../response/param_transformer'; 11 | 12 | /** 13 | * Handles a GetObject request, by performing the following steps: 14 | * 1. Validates the incoming user request. 15 | * 2. Retrieves the original object from Amazon S3. 16 | * 3. Applies a transformation. You can apply your custom transformation logic here. 17 | * 4. Handles post-processing of the transformation, such as applying range or part numbers. 18 | * 5. Sends the final transformed object back to Amazon S3 Object Lambda. 19 | */ 20 | 21 | export default async function handleGetObjectRequest (s3Client: S3Client, requestContext: GetObjectContext, userRequest: UserRequest): 22 | Promise { 23 | // Validate user request and return error if invalid 24 | const errorMessage = validate(userRequest); 25 | if (errorMessage != null) { 26 | return getErrorResponse(s3Client, requestContext, ErrorCode.INVALID_REQUEST, errorMessage); 27 | } 28 | 29 | // Read the original object from Amazon S3 30 | const objectResponse = await makeS3Request(requestContext.inputS3Url, userRequest, 'GET'); 31 | const originalObject = Buffer.from(await objectResponse.arrayBuffer()); 32 | 33 | if (originalObject === null) { 34 | return getErrorResponse(s3Client, requestContext, ErrorCode.NO_SUCH_KEY, 'Requested key does not exist'); 35 | } 36 | 37 | const responseHeaders = getResponseHeaders(objectResponse.headers); 38 | if (objectResponse.status >= 400) { 39 | // Errors in the Amazon S3 response should be forwarded to the caller without invoking transformObject. 40 | return getResponseForS3Errors(s3Client, requestContext, objectResponse, responseHeaders, originalObject); 41 | } 42 | 43 | if (objectResponse.status >= 300 && objectResponse.status < 400) { 44 | // Handle the redirect scenarios here such as Not Modified (304), Moved Permanently (301) 45 | return writeResponse(s3Client, requestContext, originalObject, responseHeaders, objectResponse); 46 | } 47 | 48 | // Transform the object 49 | const transformedWholeObject = transformObject(originalObject); 50 | 51 | // Handle range or partNumber if present in the request 52 | const transformedObject = applyRangeOrPartNumber(transformedWholeObject, userRequest); 53 | 54 | // Send the transformed object or error back to Amazon S3 Object Lambda 55 | if (transformedObject.hasError) { 56 | return getErrorResponse(s3Client, requestContext, ErrorCode.INVALID_REQUEST, String(transformedObject.errorMessage), responseHeaders); 57 | } 58 | if (transformedObject.object !== undefined) { 59 | return writeResponse(s3Client, requestContext, transformedObject.object, responseHeaders, objectResponse); 60 | } 61 | return null; 62 | } 63 | 64 | /** 65 | * Removes headers that will be invalidated by the transformation eg: Content-Length and ETag. 66 | */ 67 | function getResponseHeaders (headers: Headers): Headers { 68 | headers.delete('Content-Length'); 69 | headers.delete('ETag'); 70 | 71 | return headers; 72 | } 73 | 74 | /** 75 | * Send the transformed object back to Amazon S3 Object Lambda, by invoking the WriteGetObjectResponse API. 76 | */ 77 | async function writeResponse (s3Client: S3Client, requestContext: GetObjectContext, transformedObject: Buffer, 78 | headers: Headers, objectResponse: Response): Promise { 79 | const { algorithm, digest } = getChecksum(transformedObject); 80 | 81 | const WGORParams = new Map(); 82 | 83 | // Create the Map with the params for WGOR 84 | headers.forEach((value, key) => { 85 | const paramKey = headerToWgorParam(key); 86 | if (ParamsKeys.includes(paramKey) && value !== '' && value !== null) { 87 | if (paramKey === 'LastModified') { 88 | WGORParams.set(paramKey, new Date(value)); 89 | } else { 90 | WGORParams.set(paramKey, value); 91 | } 92 | } 93 | }); 94 | 95 | console.log('Sending transformed results to the Object Lambda Access Point'); 96 | const wgorCommand = new WriteGetObjectResponseCommand({ 97 | RequestRoute: requestContext.outputRoute, 98 | RequestToken: requestContext.outputToken, 99 | StatusCode: objectResponse.status, 100 | Body: transformedObject, 101 | Metadata: { 102 | 'body-checksum-algorithm': algorithm, 103 | 'body-checksum-digest': digest 104 | 105 | }, 106 | ...Object.fromEntries(WGORParams) 107 | }); 108 | 109 | return s3Client.send(wgorCommand); 110 | } 111 | -------------------------------------------------------------------------------- /function/nodejs_20_x/tst/request/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { UserRequest } from '../../src/s3objectlambda_event.types'; 2 | import { applyRangeOrPartNumber, getPartNumber, getRange, getSignedHeaders } from '../../src/request/utils'; 3 | 4 | test('GetSignedHeaders works for a URL with a single signed header', () => { 5 | 6 | const presigned_url: string = 'https://test-access-point-012345678901.s3-accesspoint.us-east-1.amazonaws.com/test?' + 7 | 'X-Amz-Security-Token=TestToken' + 8 | '&X-Amz-Algorithm=AWS4-HMAC-SHA256' + 9 | '&X-Amz-Date=20250220T175710Z' + 10 | '&X-Amz-SignedHeaders=host%3Bx-amz-checksum-mode' + 11 | '&X-Amz-Expires=61' + 12 | '&X-Amz-Credential=AKIAEXAMPLE/20250220/us-east-1/s3/aws4_request' + 13 | '&X-Amz-Signature=a7f9b2c8e4d1f3a6b5c9e2d7a8f4b3c1d6e5f2a9b7c8d3e1f6a2b9c7d5e8f3a1'; 14 | 15 | const result = getSignedHeaders(presigned_url); 16 | expect(result.length).toBe(2); 17 | expect(result[0]).toBe("host"); 18 | expect(result[1]).toBe("x-amz-checksum-mode"); 19 | }); 20 | 21 | test('GetSignedHeaders works for a URL with a 2 signed headers', () => { 22 | 23 | const presigned_url: string = 'https://test-access-point-012345678901.s3-accesspoint.us-east-1.amazonaws.com/test?' + 24 | 'X-Amz-Security-Token=TestToken' + 25 | '&X-Amz-Algorithm=AWS4-HMAC-SHA256' + 26 | '&X-Amz-Date=20250220T175710Z' + 27 | '&X-Amz-SignedHeaders=host' + 28 | '&X-Amz-Expires=61' + 29 | '&X-Amz-Credential=AKIAEXAMPLE/20250220/us-east-1/s3/aws4_request' + 30 | '&X-Amz-Signature=a7f9b2c8e4d1f3a6b5c9e2d7a8f4b3c1d6e5f2a9b7c8d3e1f6a2b9c7d5e8f3a1'; 31 | 32 | const result = getSignedHeaders(presigned_url); 33 | expect(result.length).toBe(1); 34 | expect(result[0]).toBe("host"); 35 | }); 36 | 37 | test('GetSignedHeaders works for a URL with no signed headers', () => { 38 | 39 | const presigned_url: string = 'https://test-access-point-012345678901.s3-accesspoint.us-east-1.amazonaws.com/test?' + 40 | 'X-Amz-Security-Token=TestToken' + 41 | '&X-Amz-Algorithm=AWS4-HMAC-SHA256' + 42 | '&X-Amz-Date=20250220T175710Z' + 43 | '&X-Amz-Expires=61' + 44 | '&X-Amz-Credential=AKIAEXAMPLE/20250220/us-east-1/s3/aws4_request' + 45 | '&X-Amz-Signature=a7f9b2c8e4d1f3a6b5c9e2d7a8f4b3c1d6e5f2a9b7c8d3e1f6a2b9c7d5e8f3a1'; 46 | 47 | const result = getSignedHeaders(presigned_url); 48 | expect(result.length).toBe(0); 49 | }); 50 | 51 | test('Get PartNumber works', () => { 52 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com?partNumber=1', headers: { h1: 'v1' } }; 53 | expect(getPartNumber(userRequest)).toBe('1'); 54 | }); 55 | 56 | test('Get PartNumber works even when case is different', () => { 57 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com?hello=world&PARTnumber=1', headers: { h1: 'v1' } }; 58 | expect(getPartNumber(userRequest)).toBe('1'); 59 | }); 60 | 61 | test("PartNumber is null when it isn't present", () => { 62 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com?Range=1', headers: { h1: 'v1' } }; 63 | expect(getPartNumber(userRequest)).toBe(null); 64 | }); 65 | 66 | test('Get Range from query parameters works', () => { 67 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com?range=bytes=1', headers: { h1: 'v1' } }; 68 | expect(getRange(userRequest)).toBe('bytes=1'); 69 | }); 70 | 71 | test('Get Range from query parameters works even when case is different', () => { 72 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com?raNGe=bytes=1', headers: { h1: 'v1' } }; 73 | expect(getRange(userRequest)).toBe('bytes=1'); 74 | }); 75 | 76 | test('Get Range from headers works', () => { 77 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com', headers: { Range: 'bytes=3-' } }; 78 | expect(getRange(userRequest)).toBe('bytes=3-'); 79 | }); 80 | 81 | test('Get Range from headers works even when case is different', () => { 82 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com', headers: { RANge: 'bytes=3-' } }; 83 | expect(getRange(userRequest)).toBe('bytes=3-'); 84 | }); 85 | 86 | test('ApplyRangeOrPartNumber without range or part can return the original object', () => { 87 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com', headers: { h1: 'v1' } }; 88 | const transformedObject = Buffer.from('single-object'); 89 | expect(applyRangeOrPartNumber(transformedObject, userRequest)).toStrictEqual({ 90 | object: transformedObject, 91 | hasError: false 92 | }); 93 | }); 94 | 95 | test('Apply Range or Part Number applies range', () => { 96 | const rangeValue = 'bytes=3-'; 97 | const userRequest: UserRequest = { url: 'https://s3.amazonaws.com', headers: { Range: rangeValue } }; 98 | const transformedObject = Buffer.from('single-object'); 99 | const range = getRange(userRequest); 100 | expect(range).toBe(rangeValue); 101 | expect(applyRangeOrPartNumber(transformedObject, userRequest)).toStrictEqual( 102 | { 103 | object: Buffer.from('gle-object'), 104 | hasError: false 105 | }); 106 | }); 107 | 108 | test('Apply Range or Part Number applies part number', () => { 109 | const partNumber = '1'; 110 | const partSize = 5242880; // 5 MB in Bytes 111 | const userRequest: UserRequest = { url: `https://s3.amazonaws.com?partNumber=${partNumber}`, headers: { h1: 'v1' } }; 112 | const transformedObject = Buffer.from('0'.repeat(1 * partSize * 2)); // Create a 10MB object 113 | const appliedPartNumber = applyRangeOrPartNumber(transformedObject, userRequest); 114 | expect(appliedPartNumber).not.toBeNull(); 115 | expect(appliedPartNumber.object?.length).toBe(partSize); 116 | }); 117 | -------------------------------------------------------------------------------- /function/python_3_9/src/handler/get_object_handler.py: -------------------------------------------------------------------------------- 1 | import checksum 2 | import error 3 | import requests 4 | import transform 5 | from request import utils, validator 6 | from request.utils import get_signed_headers_from_url 7 | from response import MapperResponse, range_mapper, part_number_mapper 8 | 9 | 10 | def include_signed_headers(s3_presigned_url, user_headers, headers): 11 | # headers are case-insensitive as per RFC 9110 12 | signed_headers = get_signed_headers_from_url(s3_presigned_url) 13 | for key, value in user_headers.items(): 14 | if key.lower() in signed_headers: 15 | headers[key] = value 16 | 17 | 18 | def include_optional_headers(user_headers, headers): 19 | # headers are case-insensitive as per RFC 9110 20 | optional_headers = ['if-match', 'if-modified-since', 'if-none-match', 'if-unmodified-since'] 21 | for key, value in user_headers.items(): 22 | if key.lower() in optional_headers: 23 | headers[key] = value 24 | 25 | 26 | def get_request_header(user_headers, s3_presigned_url): 27 | """ 28 | Get all headers that should be included in the pre-signed S3 URL. We do not add headers that will be 29 | applied after transformation, such as Range. 30 | :param user_headers: headers from the GetObject request 31 | :param s3_presigned_url: presigned url 32 | :return: Headers to be sent with pre-signed-url 33 | """ 34 | headers = dict() 35 | include_signed_headers(s3_presigned_url, user_headers, headers) 36 | include_optional_headers(user_headers, headers) 37 | 38 | # Additionally, we need to filter out the "Host" header, as the client would retrieve the correct value from 39 | # the endpoint. 40 | headers = {k: v for k, v in headers.items() if k.lower() != "host"} 41 | return headers 42 | 43 | 44 | def get_object_handler(s3_client, request_context, user_request): 45 | """ 46 | Handler for the GetObject Operation 47 | :param s3_client: s3 client 48 | :param request_context: GetObject request context 49 | :param user_request: user request 50 | :return: WriteGetObjectResponse 51 | """ 52 | # Validate user request and return error if invalid 53 | requests_validation = validator.validate_request(user_request) 54 | if not requests_validation.is_valid: 55 | return error.write_error_response(s3_client, request_context, requests.codes.bad_request, 56 | 'InvalidRequest', requests_validation.error_msg) 57 | 58 | # Get the original object from Amazon S3 59 | s3_url = request_context["inputS3Url"] 60 | request_header = get_request_header(user_request["headers"], s3_url) 61 | 62 | object_response = requests.get(s3_url, headers=request_header) 63 | 64 | # Check if the get original object request from S3 is successful 65 | if object_response.status_code != requests.codes.ok: 66 | # For 304 Not Modified, Error Message dont need to be send 67 | if object_response.status_code == requests.codes.not_modified: 68 | return s3_client.write_get_object_response( 69 | RequestRoute=request_context["outputRoute"], 70 | RequestToken=request_context["outputToken"], 71 | StatusCode=object_response.status_code, 72 | ) 73 | return error.write_error_response_for_s3(s3_client, 74 | request_context, 75 | object_response) 76 | # Transform the object 77 | original_object = object_response.content 78 | 79 | transformed_whole_object = transform.transform_object(original_object) 80 | 81 | # Handle range or partNumber if present in the request 82 | partial_object_response = apply_range_or_part_number(transformed_whole_object, user_request) 83 | if partial_object_response.hasError: 84 | return error.write_error_response(s3_client, request_context, requests.codes.bad_request, 85 | 'InvalidRequest', partial_object_response.error_msg) 86 | 87 | transformed_object = partial_object_response.object 88 | 89 | # Send the transformed object back to Amazon S3 Object Lambda 90 | transformed_object_checksum = checksum.get_checksum(transformed_object) 91 | return s3_client.write_get_object_response(RequestRoute=request_context["outputRoute"], 92 | RequestToken=request_context["outputToken"], 93 | Body=transformed_object, 94 | Metadata={ 95 | 'body-checksum-algorithm': transformed_object_checksum.algorithm, 96 | 'body-checksum-digest': transformed_object_checksum.digest 97 | }) 98 | 99 | 100 | def apply_range_or_part_number(transformed_object, user_request): 101 | """ 102 | Apply range or part number request to transformed object 103 | :param transformed_object: object that need to be apply range or part number 104 | :param user_request: request from the user 105 | :return: MapperResponse - response of the range or part number mapper 106 | """ 107 | range_number = utils.get_range(user_request) 108 | part_number = utils.get_part_number(user_request) 109 | 110 | if part_number: 111 | return part_number_mapper.map_part_number(transformed_object, part_number) 112 | elif range_number: 113 | return range_mapper.map_range(transformed_object, range_number) 114 | return MapperResponse(hasError=False, object=transformed_object, 115 | error_msg=None) 116 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/response/range_mapper.ts: -------------------------------------------------------------------------------- 1 | import ErrorCode from '../error/error_code'; 2 | import { RangeResponse } from './range_response.types'; 3 | import { CONTENT_LENGTH } from '../request/utils'; 4 | 5 | /** 6 | * Handles range requests by applying the range to the transformed object. Supported range headers are: 7 | * 8 | * Range: =- 9 | * Range: =- 10 | * Range: =- 11 | * 12 | * Amazon S3 does not support retrieving multiple ranges of data per GetObject request. Please see 13 | * {@link https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html#API_GetObject_RequestSyntax|GetObject Request Syntax} 14 | * for more information. 15 | * 16 | * The only supported unit in this implementation is `bytes`. If other units are requested, we treat this as 17 | * an invalid request. 18 | */ 19 | const SUPPORTED_UNIT = 'bytes'; 20 | 21 | export function mapRange (rangeHeaderValue: string, transformedObject: Buffer): RangeResponse { 22 | const matchArray = rangeHeaderValue.match(/^([a-z]+)=(\d+)?-(\d+)?$/); 23 | 24 | if (matchArray === null) { // check if the range matches what we support 25 | return getRangeInvalidResponse(rangeHeaderValue); 26 | } 27 | 28 | const [, rangeUnitStr, rangeStartStr, rangeEndStr] = matchArray; 29 | let rangeStart: number; 30 | let rangeEnd: number; 31 | 32 | if (rangeUnitStr.toLowerCase() !== SUPPORTED_UNIT) { 33 | return getRangeInvalidResponse(rangeHeaderValue, 34 | `Cannot process units other than ${SUPPORTED_UNIT}`); 35 | } 36 | 37 | const isRangeStartPresent = rangeStartStr !== undefined; 38 | const isRangeEndPresent = rangeEndStr !== undefined; 39 | 40 | if (!isRangeStartPresent && !isRangeEndPresent) { // At least one should be present 41 | return getRangeInvalidResponse(rangeHeaderValue); 42 | } else if (!isRangeStartPresent) { 43 | /* Range request was of the form =- so we return the last `suffix-length` bytes. */ 44 | 45 | const suffixLength = Number(rangeEndStr); 46 | rangeEnd = transformedObject.byteLength; 47 | rangeStart = rangeEnd - suffixLength; 48 | } else if (!isRangeEndPresent) { 49 | /* Range request was of the form =- so we return from range-start to the end 50 | of the object. */ 51 | rangeStart = Number(rangeStartStr); 52 | rangeEnd = transformedObject.byteLength; 53 | } else { 54 | /* Range request was of the form =- so we process both. */ 55 | rangeStart = Number(rangeStartStr); 56 | const expectedLength = Number(rangeEndStr) + 1; // Add 1 as rangeEnd is inclusive 57 | rangeEnd = Math.min(transformedObject.byteLength, expectedLength); // Should not exceed object length 58 | } 59 | 60 | const isRangeValid = rangeStart >= 0 && rangeStart <= rangeEnd; 61 | if (!isRangeValid) { 62 | return getRangeInvalidResponse(rangeHeaderValue); 63 | } 64 | 65 | return { object: transformedObject.slice(rangeStart, rangeEnd), hasError: false }; 66 | } 67 | 68 | export function mapRangeHead (rangeHeaderValue: string, transformedHeaders: Map): RangeResponse { 69 | const matchArray = rangeHeaderValue.match(/^([a-z]+)=(\d+)?-(\d+)?$/); 70 | 71 | if (matchArray === null) { // check if the range matches what we support 72 | return getRangeInvalidResponse(rangeHeaderValue); 73 | } 74 | 75 | const [, rangeUnitStr, rangeStartStr, rangeEndStr] = matchArray; 76 | // Create a copy of the headers in order to not alter the original object 77 | const newHeaders = new Map(transformedHeaders); 78 | let rangeStart: number; 79 | let rangeEnd: number; 80 | let contentLength: number; 81 | 82 | if (!transformedHeaders.has(CONTENT_LENGTH)) { 83 | return getRangeInvalidResponse('No content-length was found in the headers'); 84 | } else { 85 | contentLength = Number(transformedHeaders.get(CONTENT_LENGTH)); 86 | } 87 | 88 | if (rangeUnitStr.toLowerCase() !== SUPPORTED_UNIT) { 89 | return getRangeInvalidResponse(rangeHeaderValue, 90 | `Cannot process units other than ${SUPPORTED_UNIT}`); 91 | } 92 | 93 | const isRangeStartPresent = rangeStartStr !== undefined; 94 | const isRangeEndPresent = rangeEndStr !== undefined; 95 | 96 | if (!isRangeStartPresent && !isRangeEndPresent) { // At least one should be present 97 | return getRangeInvalidResponse(rangeHeaderValue); 98 | } else if (!isRangeStartPresent) { 99 | /* Range request was of the form =- so we return the last `suffix-length` bytes. */ 100 | 101 | const suffixLength = Number(rangeEndStr); 102 | rangeEnd = contentLength; 103 | rangeStart = rangeEnd - suffixLength; 104 | } else if (!isRangeEndPresent) { 105 | /* Range request was of the form =- so we return from range-start to the end 106 | of the object. */ 107 | rangeStart = Number(rangeStartStr); 108 | rangeEnd = contentLength; 109 | } else { 110 | /* Range request was of the form =- so we process both. */ 111 | rangeStart = Number(rangeStartStr); 112 | const expectedLength = Number(rangeEndStr) + 1; // Add 1 as rangeEnd is inclusive 113 | rangeEnd = Math.min(contentLength, expectedLength); // Should not exceed object length 114 | } 115 | 116 | const isRangeValid = rangeStart >= 0 && rangeStart <= rangeEnd; 117 | if (!isRangeValid) { 118 | return getRangeInvalidResponse(rangeHeaderValue); 119 | } 120 | 121 | // Set the new Content-Length accordingly. 122 | newHeaders.set(CONTENT_LENGTH, Object(rangeEnd - rangeStart)); 123 | return { headers: newHeaders, hasError: false }; 124 | } 125 | 126 | function getRangeInvalidResponse (rangeHeaderValue: string, errorMessage?: string): RangeResponse { 127 | const message = (errorMessage === undefined) ? `Cannot process specified range: ${rangeHeaderValue}` : errorMessage; 128 | 129 | return { 130 | hasError: true, 131 | errorCode: ErrorCode.INVALID_RANGE, 132 | errorMessage: message 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /function/nodejs_20_x/src/request/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains utility methods for Request handling, such as extracting query parameters. 3 | */ 4 | import { mapPartNumber, mapPartNumberHead } from '../response/part_number_mapper'; 5 | import { mapRange, mapRangeHead } from '../response/range_mapper'; 6 | import { RangeResponse } from '../response/range_response.types'; 7 | import { UserRequest } from '../s3objectlambda_event.types'; 8 | import fetch, { Response } from 'node-fetch'; 9 | 10 | // Query parameters names 11 | const RANGE = 'Range'; 12 | const PART_NUMBER = 'partNumber'; 13 | const X_AMZ_SIGNED_HEADERS = 'X-Amz-SignedHeaders'; 14 | const X_AMZN_SIGNED_HEADERS_DELIMETER = ';'; 15 | 16 | // Header constants 17 | export const CONTENT_LENGTH = 'content-length'; 18 | 19 | /** 20 | * Get list of signed headers from the presigned url 21 | * @param url 22 | */ 23 | export function getSignedHeaders (url: string): string[] { 24 | const queryParam = getQueryParam(url, X_AMZ_SIGNED_HEADERS); 25 | return queryParam !== null ? queryParam.split(X_AMZN_SIGNED_HEADERS_DELIMETER) : []; 26 | } 27 | 28 | /** 29 | * Get the part number from the user request 30 | * @param userRequest The user request 31 | */ 32 | export function getPartNumber (userRequest: UserRequest): string | null { 33 | // PartNumber can be present as a request query parameter. 34 | return getQueryParam(userRequest.url, PART_NUMBER); 35 | } 36 | 37 | /** 38 | * Get the range from the user request 39 | * @param userRequest The user request 40 | */ 41 | export function getRange (userRequest: UserRequest): string | null { 42 | // Convert object to a TypeScript Map 43 | const headersMap = new Map(Object.entries(userRequest.headers).map(([k, v]) => [k.toLowerCase(), v])); 44 | 45 | // Range can be present as a request header or query parameter. 46 | if (headersMap.has(RANGE.toLowerCase())) { 47 | return headersMap.get(RANGE.toLowerCase()); 48 | } else { 49 | return getQueryParam(userRequest.url, RANGE); 50 | } 51 | } 52 | 53 | /** 54 | * Check if the request context has range or partNumber parameter. This helps us handle a ranged request 55 | * and return only the requested range to the GetObject caller. For more information on range and partNumber, 56 | * see {@link https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html#API_GetObject_RequestSyntax|GetObject Request Syntax} 57 | * in the Amazon S3 API Reference. 58 | * @param transformedObject Object on which the Range or Part number is going to be applied to 59 | * @param userRequest The user request where it's checking if range or part number is specified 60 | * @returns The object having range or part number applied on them if they have been specfied in the user request 61 | */ 62 | export function applyRangeOrPartNumber (transformedObject: Buffer, userRequest: UserRequest): RangeResponse { 63 | const range = getRange(userRequest); 64 | const partNumber = getPartNumber(userRequest); 65 | 66 | if (range != null) { 67 | return mapRange(range, transformedObject); 68 | } 69 | 70 | if (partNumber != null) { 71 | return mapPartNumber(partNumber, transformedObject); 72 | } 73 | 74 | // The request was made for the whole object, so return as is. 75 | return { object: transformedObject, hasError: false }; 76 | } 77 | 78 | /** 79 | * Check if the request context has range or partNumber parameter. This helps us handle a ranged request 80 | * and return only the requested range to the HeadObject caller. For more information on range and partNumber, 81 | * see {@link https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html#API_HeadObject_RequestSyntax|HeadObject 82 | * Request Syntax} 83 | * in the Amazon S3 API Reference. 84 | * @param transformedHeaders Map of the 85 | * @param userRequest The user request where it's checking if range or part number is specified 86 | * @returns The object having range or part number applied on them if they have been specfied in the user request 87 | */ 88 | export function applyRangeOrPartNumberHeaders (transformedHeaders: Map, 89 | userRequest: UserRequest): RangeResponse { 90 | const range = getRange(userRequest); 91 | const partNumber = getPartNumber(userRequest); 92 | 93 | if (range != null) { 94 | return mapRangeHead(range, transformedHeaders); 95 | } 96 | 97 | if (partNumber != null) { 98 | return mapPartNumberHead(partNumber, transformedHeaders); 99 | } 100 | 101 | // The request was made for the whole object, so return as is. 102 | return { headers: transformedHeaders, hasError: false }; 103 | } 104 | 105 | /** 106 | * Gets a query parameter from the url, converted to lower case. Returns null, in case it doesn't exist in the url. 107 | * @param url The url from where the query parameter is going to be extracted 108 | * @param name The name of the specific query parameter. 109 | */ 110 | function getQueryParam (url: string, name: string): string | null { 111 | url = url.toLowerCase(); 112 | name = name.toLowerCase(); 113 | return new URL(url).searchParams.get(name); 114 | } 115 | 116 | export async function makeS3Request (url: string, userRequest: UserRequest, method: 'GET' | 'HEAD'): Promise { 117 | const requestHeaders = getRequestHeaders(userRequest.headers, url); 118 | // TODO: handle fetch errors 119 | return fetch(url, { 120 | method, 121 | headers: Object.fromEntries(requestHeaders) 122 | }); 123 | } 124 | 125 | export function addOptionalHeaders (headersObj: object, httpHeaders: Map): void { 126 | let optionalHeaders = ['If-Match', 'If-Modified-Since', 'If-None-Match', 'If-Unmodified-Since']; 127 | optionalHeaders = optionalHeaders.map(header => header.toLowerCase()); 128 | 129 | new Map(Object.entries(headersObj)).forEach((value: string, key: string) => { 130 | if (optionalHeaders.includes(key.toLowerCase())) { 131 | httpHeaders.set(key, value); 132 | } 133 | }); 134 | } 135 | 136 | export function addSignedHeaders (headersObj: object, url: string, httpHeaders: Map): void { 137 | const signedHeaders = getSignedHeaders(url); 138 | new Map(Object.entries(headersObj)).forEach((value: string, key: string) => { 139 | if (signedHeaders.includes(key.toLowerCase())) { 140 | httpHeaders.set(key, value); 141 | } 142 | }); 143 | } 144 | 145 | /** 146 | * Get all headers that should be included in the pre-signed S3 URL. We do not add headers that will be 147 | * applied after transformation, such as Range. 148 | */ 149 | export function getRequestHeaders (headersObj: object, url: string): Map { 150 | const httpHeaders: Map = new Map(); 151 | 152 | // If a header is signed, then it must be included in the actual http call. 153 | // Otherwise, the lambda will get a signature error response. 154 | addSignedHeaders(headersObj, url, httpHeaders); 155 | 156 | // Some headers are not signed, but should be passed via a presigned url call to ensure desired behaviour. 157 | addOptionalHeaders(headersObj, httpHeaders); 158 | 159 | // Additionally, we need to filter out the "Host" header, as the client would retrieve the correct value from 160 | // the endpoint. 161 | for (const key of httpHeaders.keys()) { 162 | if (key.toLowerCase() === 'host') { 163 | httpHeaders.delete(key); 164 | } 165 | } 166 | 167 | return httpHeaders; 168 | } 169 | -------------------------------------------------------------------------------- /.github/workflows/run_integration_tests.yaml: -------------------------------------------------------------------------------- 1 | name: Default Config Integration Test 2 | on: 3 | pull_request_target: 4 | types: [labeled] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | IntegrationTest: 9 | runs-on: ubuntu-latest 10 | if: ${{contains(github.event.pull_request.labels.*.name, 'Manually Reviewed')}} 11 | permissions: 12 | id-token: write 13 | contents: read 14 | pull-requests: write 15 | statuses: write 16 | steps: 17 | - name: Check out pull request code base 18 | uses: actions/checkout@v2 19 | with: 20 | ref: ${{github.event.pull_request.head.sha}} 21 | repository: ${{github.event.pull_request.head.repo.full_name}} 22 | - name: Configure AWS Credentials 23 | uses: aws-actions/configure-aws-credentials@master 24 | with: 25 | aws-region: ${{ secrets.AWS_REGION }} 26 | role-to-assume: ${{ secrets.AWS_ASSUME_ROLE }} 27 | role-duration-seconds: 1200 28 | role-session-name: GithubActionIntegrationTest 29 | - name: Generate a Template Key name 30 | run: echo TEMPLATE_KEY=template_$(openssl rand -hex 8).yaml >> $GITHUB_ENV 31 | - name: Template Upload to S3 32 | run: aws s3 cp ./template/s3objectlambda_defaultconfig.yaml s3://${{ secrets.AWS_BUCKET_NAME }}/${{env.TEMPLATE_KEY}} 33 | - name: Set up NodeJS 20 34 | uses: actions/setup-node@v1 35 | with: 36 | node-version: '20.x' 37 | - name: Generate a random name for Lambda functions 38 | run: | 39 | echo LAMBDA_NODE_KEY=function_$(openssl rand -hex 8).zip >> $GITHUB_ENV 40 | echo LAMBDA_PYTHON_KEY=function_$(openssl rand -hex 8).zip >> $GITHUB_ENV 41 | echo LAMBDA_JAVA_KEY=function_$(openssl rand -hex 8).zip >> $GITHUB_ENV 42 | - name: Set up Node Lambda Function and upload to S3 43 | working-directory: function/nodejs_20_x 44 | run: | 45 | npm install 46 | npm run-script build 47 | mkdir -p release 48 | npm run-script package 49 | echo LAMBDA_VERSION=$(aws s3api put-object --bucket ${{ secrets.AWS_BUCKET_NAME }} --key ${{env.LAMBDA_NODE_KEY}} --body release/s3objectlambda_deployment_package.zip --output json | jq -r '.VersionId' ) >> $GITHUB_ENV 50 | - name: Set up JDK 17 51 | uses: actions/setup-java@v2 52 | with: 53 | java-version: '17' 54 | distribution: 'adopt' 55 | - name: Build with Maven and run test 56 | run: mvn test -f tests/pom.xml -Dregion=${{ secrets.AWS_REGION }} -DtemplateUrl=https://${{ secrets.AWS_BUCKET_NAME }}.s3.${{ secrets.AWS_REGION}}.amazonaws.com/${{env.TEMPLATE_KEY}} -Ds3BucketName=${{ secrets.AWS_BUCKET_NAME }} -DlambdaFunctionS3BucketName=${{ secrets.AWS_BUCKET_NAME }} -DlambdaFunctionS3Key=${{env.LAMBDA_NODE_KEY}} -DcreateNewSupportingAccessPoint=true -DlambdaVersion=${{env.LAMBDA_VERSION}} -DlambdaFunctionRuntime=nodejs20.x 57 | # Java Lambda Check 58 | - name: Check Java file existence 59 | id: check_java_files 60 | uses: andstor/file-existence-action@v1 61 | with: 62 | files: "function/java17" 63 | - name: Set up JDK 17 64 | if: steps.check_java_files.outputs.files_exists == 'true' 65 | uses: actions/setup-java@v2 66 | with: 67 | java-version: '17' 68 | distribution: 'adopt' 69 | - name: Set up Java Lambda Function and upload to S3 70 | if: steps.check_java_files.outputs.files_exists == 'true' 71 | working-directory: function/java17 72 | run: | 73 | mvn package 74 | echo LAMBDA_JAVA_VERSION=$(aws s3api put-object --bucket ${{ secrets.AWS_BUCKET_NAME }} --key ${{env.LAMBDA_JAVA_KEY}} --body target/S3ObjectLambdaDefaultConfigJavaFunction-1.0.jar --output json | jq -r '.VersionId' ) >> $GITHUB_ENV 75 | - name: Set up JDK 17 to run test against Java Function 76 | if: steps.check_java_files.outputs.files_exists == 'true' 77 | uses: actions/setup-java@v2 78 | with: 79 | java-version: '17' 80 | distribution: 'adopt' 81 | - name: Build with Maven and run test against Java Lambda function 82 | if: steps.check_java_files.outputs.files_exists == 'true' 83 | run: mvn test -f tests/pom.xml -Dregion=${{ secrets.AWS_REGION }} -DtemplateUrl=https://${{ secrets.AWS_BUCKET_NAME }}.s3.${{ secrets.AWS_REGION}}.amazonaws.com/${{env.TEMPLATE_KEY}} -Ds3BucketName=${{ secrets.AWS_BUCKET_NAME }} -DlambdaFunctionS3BucketName=${{ secrets.AWS_BUCKET_NAME }} -DlambdaFunctionS3Key=${{env.LAMBDA_JAVA_KEY}} -DcreateNewSupportingAccessPoint=true -DlambdaVersion=${{env.LAMBDA_JAVA_VERSION}} -DlambdaFunctionRuntime=java17 -Dsurefire.suiteXmlFiles=getonly.xml 84 | #Python Lambda Check 85 | - name: Check Python file existence 86 | id: check_python_files 87 | uses: andstor/file-existence-action@v1 88 | with: 89 | files: "function/python_3_9" 90 | - name: Set up Python 91 | if: steps.check_python_files.outputs.files_exists == 'true' 92 | uses: actions/setup-python@v2 93 | with: 94 | python-version: '3.9' 95 | - name: Set up Python Lambda Function and upload to S3 96 | if: steps.check_python_files.outputs.files_exists == 'true' 97 | working-directory: function/python_3_9 98 | run: | 99 | python -m pip install -r ./requirements.txt -t ./release/package 100 | cd ./release/package 101 | zip ../s3objectlambda_deployment_package.zip . -r 102 | cd ../../src 103 | zip ../release/s3objectlambda_deployment_package s3objectlambda.py -g 104 | zip ../release/s3objectlambda_deployment_package ./*/*.py -g 105 | cd ../ 106 | echo LAMBDA_PYTHON_VERSION=$(aws s3api put-object --bucket ${{ secrets.AWS_BUCKET_NAME }} --key ${{env.LAMBDA_PYTHON_KEY}} --body release/s3objectlambda_deployment_package.zip --output json | jq -r '.VersionId' ) >> $GITHUB_ENV 107 | - name: Set up JDK 17 to run tests against Python Function 108 | if: steps.check_python_files.outputs.files_exists == 'true' 109 | uses: actions/setup-java@v2 110 | with: 111 | java-version: '17' 112 | distribution: 'adopt' 113 | - name: Build with Maven and run test against Python Lambda function 114 | if: steps.check_python_files.outputs.files_exists == 'true' 115 | run: mvn test -f tests/pom.xml -Dregion=${{ secrets.AWS_REGION }} -DtemplateUrl=https://${{ secrets.AWS_BUCKET_NAME }}.s3.${{ secrets.AWS_REGION}}.amazonaws.com/${{env.TEMPLATE_KEY}} -Ds3BucketName=${{ secrets.AWS_BUCKET_NAME }} -DlambdaFunctionS3BucketName=${{ secrets.AWS_BUCKET_NAME }} -DlambdaFunctionS3Key=${{env.LAMBDA_PYTHON_KEY}} -DcreateNewSupportingAccessPoint=true -DlambdaVersion=${{env.LAMBDA_PYTHON_VERSION}} -DlambdaFunctionRuntime=python3.9 -Dsurefire.suiteXmlFiles=getonly.xml 116 | 117 | - name: Check Test Result Pass 118 | if: ${{ !failure() }} 119 | uses: actions-ecosystem/action-add-labels@v1 120 | with: 121 | labels: Test Successful 122 | - name: Check Test Result Fail 123 | if: ${{ failure() }} 124 | uses: actions-ecosystem/action-add-labels@v1 125 | with: 126 | labels: Test Failed 127 | - name: Remove Reviewed Label 128 | if: ${{ always() }} 129 | uses: actions-ecosystem/action-remove-labels@v1 130 | with: 131 | labels: Manually Reviewed 132 | - name: Remove Test Failed Label 133 | if: ${{ !failure() }} 134 | uses: actions-ecosystem/action-remove-labels@v1 135 | with: 136 | labels: Test Failed -------------------------------------------------------------------------------- /template/s3objectlambda_defaultconfig.yaml: -------------------------------------------------------------------------------- 1 | ##################################################################### 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | ##################################################################### 5 | 6 | Description: > 7 | Use this template to get started with Amazon S3 Object Lambda and automate the setup process. 8 | This template creates relevant resources, configures IAM roles, and sets up a Lambda function that handles requests 9 | through an S3 Object Lambda Access Point. You can implement best practices, improve your security posture, 10 | reduce errors caused by manual processes, and focus on innovation and implementing business logic 11 | instead of managing the setup process. 12 | 13 | Mappings: 14 | LambdaRuntimeHandlerMapping: 15 | nodejs20.x: 16 | handler: s3objectlambda.handler 17 | python3.9: 18 | handler: s3objectlambda.handler 19 | java17: 20 | handler: com.example.s3objectlambda.Handler::handleRequest 21 | Parameters: 22 | S3BucketName: 23 | Type: String 24 | Description: > 25 | An Amazon S3 bucket name to use with S3 Object Lambda. The bucket should exist in the same AWS account and 26 | AWS Region that will deploy this template. The bucket should also delegate access control to access points. 27 | 28 | ObjectLambdaAccessPointName: 29 | Type: String 30 | Description: Name of the Amazon S3 Object Lambda Access Point. 31 | 32 | CreateNewSupportingAccessPoint: 33 | Type: String 34 | Description: Flag that indicates a new supporting Access Point should be created. 35 | Default: false 36 | AllowedValues: [true, false] 37 | 38 | SupportingAccessPointName: 39 | Type: String 40 | Description: Name of the Amazon S3 Access Point associated with the S3 bucket passed in the S3BucketName parameter. 41 | 42 | LambdaFunctionS3BucketName: 43 | Type: String 44 | Description: > 45 | Name of the Amazon S3 bucket where you have uploaded the Lambda function deployment package. The bucket 46 | should be in the same AWS Region as your function, but can be in a different AWS account. 47 | 48 | LambdaFunctionS3Key: 49 | Type: String 50 | Description: The Amazon S3 key of the Lambda function deployment package. 51 | 52 | LambdaFunctionS3ObjectVersion: 53 | Type: String 54 | Description: The version id of the Lambda function deployment package object stored in Amazon S3. 55 | 56 | LambdaFunctionPayload: 57 | Type: String 58 | Default: "" 59 | Description: An optional static payload that provides supplemental data to the Lambda function used to transform objects. 60 | 61 | LambdaFunctionRuntime: 62 | Type: String 63 | AllowedValues: [ nodejs20.x, python3.9, java17 ] 64 | Description: Identifier for the Lambda function runtime 65 | 66 | EnableCloudWatchMonitoring: 67 | Type: String 68 | Description: > 69 | Flag to enable CloudWatch request metrics from S3 Object Lambda. This also creates CloudWatch alarms 70 | to monitor the request metrics. 71 | Default: false 72 | AllowedValues: [ true, false ] 73 | 74 | Conditions: 75 | ShouldCreateNewSupportingAccessPoint: !Equals [!Ref CreateNewSupportingAccessPoint, true] 76 | ShouldEnableMonitoring: !Equals [!Ref EnableCloudWatchMonitoring, true] 77 | 78 | Resources: 79 | ObjectLambdaAccessPoint: 80 | Type: AWS::S3ObjectLambda::AccessPoint 81 | Properties: 82 | Name: 83 | Ref: ObjectLambdaAccessPointName 84 | ObjectLambdaConfiguration: 85 | # If creating a new Supporting Access Point, get the Arn from the new resource. 86 | # Else construct the Arn using the SupportingAccessPointName input parameter. 87 | SupportingAccessPoint: 88 | !If 89 | - ShouldCreateNewSupportingAccessPoint 90 | - !GetAtt SupportingAccessPoint.Arn 91 | - !Sub "arn:${AWS::Partition}:s3:${AWS::Region}:${AWS::AccountId}:accesspoint/${SupportingAccessPointName}" 92 | AllowedFeatures: 93 | - GetObject-Range 94 | - GetObject-PartNumber 95 | - HeadObject-Range 96 | - HeadObject-PartNumber 97 | CloudWatchMetricsEnabled: 98 | Ref: EnableCloudWatchMonitoring 99 | TransformationConfigurations: 100 | - Actions: [ GetObject, ListObjects, ListObjectsV2, HeadObject ] 101 | ContentTransformation: 102 | AwsLambda: 103 | FunctionArn: !GetAtt LambdaFunction.Arn 104 | FunctionPayload: 105 | Ref: LambdaFunctionPayload 106 | 107 | SupportingAccessPoint: 108 | Type: AWS::S3::AccessPoint 109 | Condition: ShouldCreateNewSupportingAccessPoint 110 | Properties: 111 | Bucket: 112 | Ref: S3BucketName 113 | Name: 114 | Ref: SupportingAccessPointName 115 | 116 | LambdaExecutionRole: 117 | Type: "AWS::IAM::Role" 118 | Properties: 119 | ManagedPolicyArns: ["arn:aws:iam::aws:policy/service-role/AmazonS3ObjectLambdaExecutionRolePolicy"] 120 | AssumeRolePolicyDocument: 121 | Version: "2012-10-17" 122 | Statement: 123 | - Effect: Allow 124 | Principal: 125 | Service: 126 | - lambda.amazonaws.com 127 | Action: 128 | - "sts:AssumeRole" 129 | Tags: 130 | - Key: "CreatedBy" 131 | Value: "S3 Object Lambda Default Configuration" 132 | 133 | LambdaFunction: 134 | Type: AWS::Lambda::Function 135 | Properties: 136 | Code: 137 | S3Bucket: 138 | Ref: LambdaFunctionS3BucketName 139 | S3Key: 140 | Ref: LambdaFunctionS3Key 141 | S3ObjectVersion: 142 | Ref: LambdaFunctionS3ObjectVersion 143 | Handler: 144 | Fn::FindInMap: [LambdaRuntimeHandlerMapping , Ref: LambdaFunctionRuntime , handler ] 145 | MemorySize: 1024 146 | Timeout: 60 147 | PackageType: Zip 148 | Role: !GetAtt LambdaExecutionRole.Arn 149 | Runtime: 150 | Ref: LambdaFunctionRuntime 151 | Tags: 152 | - Key: "CreatedBy" 153 | Value: "S3 Object Lambda Default Configuration" 154 | 155 | ClientSideErrorsAlarm: 156 | Type: AWS::CloudWatch::Alarm 157 | Condition: ShouldEnableMonitoring 158 | Properties: 159 | AlarmDescription: Indicates that there are client-side errors (HTTP 4xx errors) returned by the S3 Object Lambda Access Point. 160 | ComparisonOperator: GreaterThanOrEqualToThreshold 161 | Dimensions: 162 | - Name: AccessPointName 163 | Value: !Ref ObjectLambdaAccessPoint 164 | - Name: LambdaARN 165 | Value: !GetAtt LambdaFunction.Arn 166 | MetricName: 4xxErrors 167 | Namespace: "AWS/S3ObjectLambda" 168 | EvaluationPeriods: 5 169 | Period: 60 # 1 minute 170 | Statistic: Average 171 | Threshold: 0.01 # Goes into alarm when there are more than 1% of requests resulting in client-side errors 172 | TreatMissingData: notBreaching 173 | 174 | ServerSideErrorsAlarm: 175 | Type: AWS::CloudWatch::Alarm 176 | Condition: ShouldEnableMonitoring 177 | Properties: 178 | AlarmDescription: Indicates that there are server-side errors (HTTP 5xx errors) returned by the S3 Object Lambda Access Point. 179 | ComparisonOperator: GreaterThanOrEqualToThreshold 180 | Dimensions: 181 | - Name: AccessPointName 182 | Value: !Ref ObjectLambdaAccessPoint 183 | - Name: LambdaARN 184 | Value: !GetAtt LambdaFunction.Arn 185 | MetricName: 5xxErrors 186 | Namespace: "AWS/S3ObjectLambda" 187 | EvaluationPeriods: 5 188 | Period: 60 189 | Statistic: Average 190 | Threshold: 0.01 191 | TreatMissingData: notBreaching 192 | 193 | Outputs: 194 | ObjectLambdaAccessPoint: 195 | Description: The Amazon S3 Object Lambda Access Point created by this CloudFormation stack. 196 | Value: !Ref ObjectLambdaAccessPoint 197 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/request/GetObjectHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.request; 2 | 3 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 4 | import com.amazonaws.services.s3.AmazonS3; 5 | import com.amazonaws.util.IOUtils; 6 | import com.example.s3objectlambda.checksum.Md5Checksum; 7 | import com.example.s3objectlambda.error.XMLErrorParser; 8 | import com.example.s3objectlambda.response.GetObjectResponseHandler; 9 | import com.example.s3objectlambda.transform.GetObjectTransformer; 10 | import com.example.s3objectlambda.validator.GetObjectRequestValidator; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.DisplayName; 13 | import org.junit.jupiter.api.Nested; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | import org.mockito.ArgumentCaptor; 17 | import org.mockito.InjectMocks; 18 | import org.mockito.Mock; 19 | import org.mockito.Mockito; 20 | import org.mockito.junit.jupiter.MockitoExtension; 21 | import org.slf4j.Logger; 22 | import org.xml.sax.SAXException; 23 | 24 | import javax.xml.parsers.ParserConfigurationException; 25 | import java.io.File; 26 | import java.io.FileInputStream; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.net.http.HttpClient; 30 | import java.net.http.HttpRequest; 31 | import java.net.http.HttpResponse; 32 | import java.nio.charset.StandardCharsets; 33 | import java.util.HashMap; 34 | 35 | import static org.junit.jupiter.api.Assertions.assertEquals; 36 | import static org.mockito.Mockito.mock; 37 | import static org.mockito.Mockito.lenient; 38 | import static org.mockito.Mockito.any; 39 | 40 | 41 | /** 42 | * This class perform unit test on the main GetObjectHandler. 43 | * This test try to mock the s3 getObject behaviour by 44 | * reading a mock s3 object (File: /src/test/resources/mock_s3_objects/mock_s3_object.txt) 45 | */ 46 | 47 | @ExtendWith(MockitoExtension.class) 48 | public class GetObjectHandlerTest { 49 | @Mock 50 | private Logger logger; 51 | @Mock 52 | private AmazonS3 s3Client; 53 | @Mock 54 | private GetObjectTransformer transformer; 55 | @Mock 56 | private GetObjectRequestValidator requestValidator; 57 | @Mock 58 | private GetObjectResponseHandler responseHandler; 59 | @Mock 60 | private S3ObjectLambdaEvent s3ObjectLambdaEvent; 61 | @Mock 62 | private HttpClient httpClient; 63 | 64 | @InjectMocks 65 | private GetObjectHandler underTest; 66 | 67 | 68 | @Nested 69 | class WhenHandlingGetObjectRequests { 70 | @BeforeEach 71 | void setup() throws IOException, InterruptedException { 72 | 73 | // Mock S3ObjectLambdaEvent 74 | // You can mock the details of the request here such as Range and partNumber. 75 | mockS3ObjectLambdaEvent(); 76 | 77 | // Mock getObjectResponse 78 | // This function mocks the original getObject S3 response including the http code. 79 | // If you want different responses for each test, call this function from the respective tests. 80 | mockHttpResponseFromS3(200, 81 | "src/test/resources/mock_s3_objects/mock_s3_object.txt"); 82 | 83 | var userRequest = new GetObjectRequestWrapper(s3ObjectLambdaEvent.getUserRequest()); 84 | 85 | //Mock Validator 86 | requestValidator = Mockito.spy(new GetObjectRequestValidator(userRequest)); 87 | 88 | //Mock Transformer 89 | transformer = Mockito.spy(new GetObjectTransformer(userRequest)); 90 | 91 | //Mock Checksum 92 | var md5Checksum = Mockito.spy(new Md5Checksum()); 93 | 94 | //Mock Response Handler 95 | responseHandler = Mockito.spy(new GetObjectResponseHandler(s3Client, s3ObjectLambdaEvent, md5Checksum)); 96 | } 97 | 98 | @Test 99 | void testHandleRequestTransformObject() { 100 | var getObjectHandler = new GetObjectHandler(s3Client, transformer, requestValidator, s3ObjectLambdaEvent, 101 | responseHandler, httpClient); 102 | 103 | // Response has already been mocked in the setup function. 104 | 105 | // Capture the second argument when the handler calls writeObjectResponse. 106 | // Second argument is the responseObjectArray after applying range and transformation. 107 | ArgumentCaptor responseObjectArray = ArgumentCaptor.forClass(byte[].class); 108 | lenient().doNothing().when(responseHandler).writeObjectResponse(any(), responseObjectArray.capture()); 109 | 110 | getObjectHandler.handleRequest(); 111 | 112 | //We applied range and transformation on original the mock S3 Object. 113 | // (/src/test/resources/mock_s3_objects/mock_s3_object.txt) 114 | 115 | var expectedBody = "What is Amazon S3?"; 116 | var transformedResponse = new String(responseObjectArray.getValue(), StandardCharsets.UTF_8); 117 | assertEquals(expectedBody, transformedResponse); 118 | 119 | } 120 | 121 | @Test 122 | @DisplayName("Correct error reaches writeS3GetObjectErrorResponse when s3 getObject returns >= 4**") 123 | void testHandleRequestWith400S3Error() throws IOException, InterruptedException, 124 | ParserConfigurationException, SAXException { 125 | 126 | var getObjectHandler = new GetObjectHandler(s3Client, transformer, requestValidator, s3ObjectLambdaEvent, 127 | responseHandler, httpClient); 128 | 129 | mockHttpResponseFromS3(404, 130 | "src/test/resources/mock_responses/mock_s3_error_response.txt"); 131 | 132 | ArgumentCaptor> presignedResponse = ArgumentCaptor.forClass(HttpResponse.class); 133 | lenient().doNothing().when(responseHandler).writeS3GetObjectErrorResponse(presignedResponse.capture()); 134 | getObjectHandler.handleRequest(); 135 | 136 | //This will be our mock error response. (/src/test/resources/mock_s3_error_response.txt) 137 | var xmlResponse = IOUtils.toString(presignedResponse.getValue().body()); 138 | var s3errorResponse = new XMLErrorParser().parse(xmlResponse); 139 | assertEquals("NoSuchMockKey", s3errorResponse.getCode()); 140 | 141 | } 142 | } 143 | 144 | /** 145 | * 146 | * @param httpStatusCode Http status code of the mock response 147 | * @param mockS3ObjectFilePath File path of the mock s3 object. Http response will be the content of this file. 148 | * @throws IOException 149 | * @throws InterruptedException 150 | */ 151 | private void mockHttpResponseFromS3(int httpStatusCode, String mockS3ObjectFilePath) throws IOException, 152 | InterruptedException { 153 | var httpResponse = mock(HttpResponse.class); 154 | lenient().when(httpClient.send(any(HttpRequest.class), 155 | any())).thenReturn(httpResponse); 156 | lenient().when(httpResponse.statusCode()).thenReturn(httpStatusCode); 157 | var responseBody = getFileInputStream(mockS3ObjectFilePath); 158 | lenient().when(httpResponse.body()).thenReturn(responseBody); 159 | } 160 | 161 | private InputStream getFileInputStream(String mockS3ObjectFilePath) throws IOException { 162 | File initialFile = new File(mockS3ObjectFilePath); 163 | InputStream targetStream = new FileInputStream(initialFile); 164 | return targetStream; 165 | } 166 | 167 | /** 168 | * Mocks the user S3ObjectLambdaEvent. 169 | * This contains the user request, which has Range and partNumber. 170 | */ 171 | private void mockS3ObjectLambdaEvent() { 172 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 173 | lenient().when(mockUserRequest.getUrl()).thenReturn("https://example.com?time=great!"); 174 | var headerMap = new HashMap(); 175 | headerMap.put("Range", "bytes=0-17"); 176 | lenient().when(mockUserRequest.getHeaders()).thenReturn(headerMap); 177 | lenient().when(s3ObjectLambdaEvent.getUserRequest()).thenReturn(mockUserRequest); 178 | lenient().when(s3ObjectLambdaEvent.inputS3Url()).thenReturn("https://aws-region.example.com/getObject.fakeurl"); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /function/java17/src/test/java/com/example/s3objectlambda/transform/RangeMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.example.s3objectlambda.transform; 2 | 3 | import com.example.s3objectlambda.error.Error; 4 | import com.example.s3objectlambda.exception.InvalidPartNumberException; 5 | import com.example.s3objectlambda.exception.InvalidRangeException; 6 | import com.example.s3objectlambda.request.GetObjectRequestWrapper; 7 | import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.net.URISyntaxException; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.HashMap; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.fail; 17 | import static org.mockito.Mockito.mock; 18 | import static org.mockito.Mockito.when; 19 | 20 | public class RangeMapperTest { 21 | 22 | @Test 23 | @DisplayName("InvalidRangeException thrown with right statusCode. Edge case: bytes=-27") 24 | public void applyRangeOrPartNumberResponseObject() throws URISyntaxException, InvalidPartNumberException { 25 | var stringOriginalResponse = "12345678910!"; 26 | var responseInputStream = stringOriginalResponse.getBytes(StandardCharsets.UTF_16); 27 | 28 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 29 | when(mockUserRequest.getUrl()).thenReturn("https://example.com"); 30 | 31 | var headerMap = new HashMap(); 32 | /* 33 | Total byte length of the original response (responseInputStream) is 26 34 | Range mapper should return an error object with correct status code. 35 | Status code for invalid range is 416 (Range Not Satisfiable). 36 | https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416 37 | */ 38 | headerMap.put("Range", "bytes=-27"); 39 | 40 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 41 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 42 | try { 43 | new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 44 | responseInputStream); 45 | fail("Did not throw InvalidRangeException"); 46 | } catch (InvalidRangeException e) { 47 | var invalidRangeStatusCode = 416; 48 | assertEquals(invalidRangeStatusCode, e.getError().getStatusCode()); 49 | } 50 | } 51 | 52 | @Test 53 | @DisplayName("InvalidRangeException thrown and appropriate statusCode is set: bytes=-") 54 | public void applyRangeOrPartNumberResponseObjectInvalidRange() 55 | throws URISyntaxException, InvalidPartNumberException { 56 | var stringOriginalResponse = "12345678910!"; 57 | var responseInputStream = stringOriginalResponse.getBytes(StandardCharsets.UTF_16); 58 | 59 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 60 | when(mockUserRequest.getUrl()).thenReturn("https://example.com"); 61 | 62 | var headerMap = new HashMap(); 63 | headerMap.put("Range", "bytes=-"); 64 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 65 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 66 | 67 | try { 68 | new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 69 | responseInputStream); 70 | } catch (InvalidRangeException e) { 71 | var invalidRangeStatusCode = 416; 72 | assertEquals(invalidRangeStatusCode, e.getError().getStatusCode()); 73 | } 74 | } 75 | 76 | @Test 77 | @DisplayName("Valid response when correct range is passed: bytes=2-5") 78 | public void applyRangeOrPartNumberResponseObjectValidRange() 79 | throws InvalidRangeException, URISyntaxException, InvalidPartNumberException { 80 | var stringOriginalResponse = "12345678910!12345678910!"; 81 | var responseInputStream = stringOriginalResponse.getBytes(StandardCharsets.UTF_16); 82 | 83 | S3ObjectLambdaEvent.UserRequest mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 84 | when(mockUserRequest.getUrl()).thenReturn("https://example.com"); 85 | 86 | var headerMap = new HashMap(); 87 | headerMap.put("Range", "bytes=2-5"); 88 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 89 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 90 | var transformedResponseObject = new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 91 | responseInputStream); 92 | var transformedString = new String(transformedResponseObject, StandardCharsets.UTF_16); 93 | 94 | assertEquals("12", transformedString); 95 | 96 | } 97 | 98 | @Test 99 | @DisplayName("Valid response when correct range is passed: bytes=6-") 100 | public void applyRangeOrPartNumberResponseObjectValidRangeFirstPart() 101 | throws InvalidRangeException, URISyntaxException, InvalidPartNumberException { 102 | var stringOriginalResponse = "12345678910!12345678910!"; 103 | var responseInputStream = stringOriginalResponse.getBytes(StandardCharsets.UTF_16); 104 | 105 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 106 | when(mockUserRequest.getUrl()).thenReturn("https://example.com"); 107 | 108 | var headerMap = new HashMap(); 109 | headerMap.put("Range", "bytes=6-"); 110 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 111 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 112 | var transformedResponseObject = new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 113 | responseInputStream); 114 | var transformedString = new String(transformedResponseObject, StandardCharsets.UTF_16); 115 | 116 | assertEquals("345678910!12345678910!", transformedString); 117 | 118 | } 119 | 120 | @Test 121 | @DisplayName("Valid response when correct range is passed: bytes=-12") 122 | public void applyRangeOrPartNumberResponseObjectValidRangeSuffixLength() 123 | throws InvalidRangeException, URISyntaxException, InvalidPartNumberException { 124 | var stringOriginalResponse = "S3 Object Lambda"; 125 | byte[] responseInputStream = stringOriginalResponse.getBytes(StandardCharsets.UTF_16); 126 | 127 | S3ObjectLambdaEvent.UserRequest mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 128 | when(mockUserRequest.getUrl()).thenReturn("https://example.com"); 129 | 130 | var headerMap = new HashMap(); 131 | headerMap.put("Range", "bytes=-12"); 132 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 133 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 134 | var transformedResponseObject = new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 135 | responseInputStream); 136 | var transformedString = new String(transformedResponseObject, StandardCharsets.UTF_16); 137 | 138 | assertEquals("Lambda", transformedString); 139 | 140 | } 141 | 142 | @Test 143 | @DisplayName("Error response when invalid unit is passed in range :bits=-12") 144 | public void applyRangeOrPartNumberResponseObjectInvalidUnit() 145 | throws URISyntaxException, InvalidPartNumberException { 146 | var stringOriginalResponse = "S3 Object Lambda"; 147 | var responseInputStream = stringOriginalResponse.getBytes(StandardCharsets.UTF_16); 148 | 149 | var mockUserRequest = mock(S3ObjectLambdaEvent.UserRequest.class); 150 | when(mockUserRequest.getUrl()).thenReturn("https://example.com"); 151 | 152 | var headerMap = new HashMap(); 153 | headerMap.put("Range", "bits=-12"); 154 | when(mockUserRequest.getHeaders()).thenReturn(headerMap); 155 | var objectRequest = new GetObjectRequestWrapper(mockUserRequest); 156 | 157 | try { 158 | new GetObjectTransformer(objectRequest).applyRangeOrPartNumber( 159 | responseInputStream); 160 | } catch (InvalidRangeException e) { 161 | assertEquals(Error.INVALID_RANGE.getErrorCode(), e.getError().getErrorCode()); 162 | } 163 | 164 | 165 | 166 | } 167 | 168 | } 169 | --------------------------------------------------------------------------------