From 9ccdb7180c7881eb78c220f3958b671d968c16dd Mon Sep 17 00:00:00 2001 From: ssenior Date: Wed, 31 Jul 2019 15:02:15 -0400 Subject: [PATCH 1/2] Update readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 866be7ec8..8f6a448e7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +**_Important Notice:_** +Due to a [change in the AWS Lambda execution environment](https://aws.amazon.com/blogs/compute/upcoming-updates-to-the-aws-lambda-execution-environment/), Serverless Image Handler v3 deployments are functionally broken. To address the issue we have released [minor version update v3.1.1](https://solutions-reference.s3.amazonaws.com/serverless-image-handler/v3.1.1/serverless-image-handler.template). We recommend all users of v3 to run cloudformation stack update with v3.1.1. Additionally, we suggest you to look at v4 of the solution and migrate to v4 if it addresses all of your use cases. + # AWS Serverless Image Handler Lambda wrapper for SharpJS A solution to dynamically handle images on the fly, utilizing Sharp (https://sharp.pixelplumbing.com/en/stable/). Published version, additional details and documentation are available here: https://aws.amazon.com/solutions/serverless-image-handler/ @@ -35,7 +38,7 @@ cd serverless-image-handler/deployment * Now build the distributable ```bash -sudo ./build-s3-dist.sh $DIST_OUTPUT_BUCKET $TEMPLATE_OUTPUT_BUCKET $VERSION +sudo ./build-s3-dist.sh $DIST_OUTPUT_BUCKET $VERSION ``` * Deploy the distributable to an Amazon S3 bucket in your account. Note: you must have the AWS Command Line Interface installed. From a7ee26829f53688f2c8775084cb824bb7b50c7f9 Mon Sep 17 00:00:00 2001 From: georgebearden Date: Mon, 16 Dec 2019 13:42:29 -0500 Subject: [PATCH 2/2] Updated to Version 4.1. --- .gitignore | 26 +- CHANGELOG.md | 19 + CONTRIBUTING.md | 12 +- LICENSE.txt | 234 ++- NOTICE.txt | 23 +- README.md | 62 +- deployment/build-s3-dist.sh | 97 +- deployment/manifest-generator/app.js | 31 +- deployment/manifest-generator/package.json | 2 +- deployment/run-unit-tests.sh | 9 +- deployment/serverless-image-handler.template | 1269 ++++++++++------- source/custom-resource/index.js | 27 +- source/custom-resource/lib/s3-helper.js | 8 +- .../lib/usage-metrics/metrics.common.js | 6 +- .../lib/usage-metrics/test-setup.spec.js | 6 +- source/custom-resource/package.json | 11 +- source/demo-ui/scripts.js | 8 +- source/image-handler/image-handler.js | 271 ++-- source/image-handler/image-request.js | 18 +- source/image-handler/index.js | 10 +- source/image-handler/package.json | 8 +- .../image-handler/test/test-image-handler.js | 94 +- .../image-handler/test/test-image-request.js | 34 +- .../test/test-thumbor-mapping.js | 112 +- source/image-handler/thumbor-mapping.js | 54 +- 25 files changed, 1409 insertions(+), 1042 deletions(-) mode change 100644 => 100755 .gitignore create mode 100644 CHANGELOG.md diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 4ef6fdad0..ff9a9a40b --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,26 @@ +# compiled output +**/dist +**/global-s3-assets +**/regional-s3-assets +**/open-source +**/.zip +**/tmp +**/out-tsc + +# dependencies +**/node_modules + +# test assets +**/coverage +**/.nyc_output + +# misc +**/npm-debug.log +**/testem.log +**/package-lock.json +**/.vscode/settings.json +demo-ui-config.js + # System Files -**/.DS_Store \ No newline at end of file +**/.DS_Store +**/.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..34042aac1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [4.1] - 2019-12-31 +### Added +- CHANGELOG file +- Access logging to API Gateway + +### Changed +- Lambda functions runtime to nodejs12.x +- sharp version (from 0.21.3 to 0.23.3) +- Image handler function to use Composite API (https://sharp.pixelplumbing.com/en/stable/api-composite/) +- License to Apache-2.0 + +# Removed +- Reference to deprecated sharp function (overlayWith) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86c018430..99f3ecffb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,11 @@ # Contributing Guidelines - Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional documentation, we greatly value feedback and contributions from our community. Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. - ## Reporting Bugs/Feature Requests - We welcome you to use the GitHub issue tracker to report bugs or suggest features. When filing an issue, please check [existing open](https://github.com/awslabs/serverless-image-handler/issues), or [recently closed](https://github.com/awslabs/serverless-image-handler/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already @@ -19,7 +16,6 @@ reported the issue. Please try to include as much information as you can. Detail * Any modifications you've made relevant to the bug * Anything unusual about your environment or deployment - ## Contributing via Pull Requests Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: @@ -39,23 +35,17 @@ To send us a pull request, please: GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - ## Finding contributions to work on 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'](https://github.com/awslabs/serverless-image-handler/labels/help%20wanted) issues is a great place to start. - ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. - ## Security issue notifications 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. - ## Licensing - -See the [LICENSE](https://github.com/awslabs/serverless-image-handler/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. - +See the [LICENSE](https://github.com/awslabs/serverless-image-handler/blob/master/LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/LICENSE.txt b/LICENSE.txt index bb5315bab..f30fb6fb0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,32 +1,202 @@ -Amazon Software License -This Amazon Software License (“License”) governs your use, reproduction, and distribution of the accompanying software as specified below. -1. Definitions -“Licensor” means any person or entity that distributes its Work. - -“Software” means the original work of authorship made available under this License. - -“Work” means the Software and any additions to or derivative works of the Software that are made available under this License. - -The terms “reproduce,” “reproduction,” “derivative works,” and “distribution” have the meaning as provided under U.S. copyright law; provided, however, that for the purposes of this License, derivative works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work. - -Works, including the Software, are “made available” under this License by including in or with the Work either (a) a copyright notice referencing the applicability of this License to the Work, or (b) a copy of this License. -2. License Grants -2.1 Copyright Grant. Subject to the terms and conditions of this License, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free, copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense and distribute its Work and any resulting derivative works in any form. -2.2 Patent Grant. Subject to the terms and conditions of this License, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free patent license to make, have made, use, sell, offer for sale, import, and otherwise transfer its Work, in whole or in part. The foregoing license applies only to the patent claims licensable by Licensor that would be infringed by Licensor’s Work (or portion thereof) individually and excluding any combinations with any other materials or technology. -3. Limitations -3.1 Redistribution. You may reproduce or distribute the Work only if (a) you do so under this License, (b) you include a complete copy of this License with your distribution, and (c) you retain without modification any copyright, patent, trademark, or attribution notices that are present in the Work. -3.2 Derivative Works. You may specify that additional or different terms apply to the use, reproduction, and distribution of your derivative works of the Work (“Your Terms”) only if (a) Your Terms provide that the use limitation in Section 3.3 applies to your derivative works, and (b) you identify the specific derivative works that are subject to Your Terms. Notwithstanding Your Terms, this License (including the redistribution requirements in Section 3.1) will continue to apply to the Work itself. -3.3 Use Limitation. The Work and any derivative works thereof only may be used or intended for use with the web services, computing platforms or applications provided by Amazon.com, Inc. or its affiliates, including Amazon Web Services, Inc. -3.4 Patent Claims. If you bring or threaten to bring a patent claim against any Licensor (including any claim, cross-claim or counterclaim in a lawsuit) to enforce any patents that you allege are infringed by any Work, then your rights under this License from such Licensor (including the grants in Sections 2.1 and 2.2) will terminate immediately. -3.5 Trademarks. This License does not grant any rights to use any Licensor’s or its affiliates’ names, logos, or trademarks, except as necessary to reproduce the notices described in this License. -3.6 Termination. If you violate any term of this License, then your rights under this License (including the grants in Sections 2.1 and 2.2) will terminate immediately. -4. Disclaimer of Warranty. -THE WORK IS PROVIDED “AS IS” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF M ERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER THIS LICENSE. SOME STATES’ CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN IMPLIED WARRANTY, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. -5. Limitation of Liability. -EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -Effective Date – April 18, 2008 © 2008 Amazon.com, Inc. or its affiliates. All rights reserved. - -Note: Other license terms may apply to certain, identified software files contained within or -distributed with the accompanying software if such terms are included in the directory containing -the accompanying software. Such other license terms will then apply in lieu of the terms of the -software license above. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 - 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE.txt b/NOTICE.txt index 81e8fa11a..1e69eb65b 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,23 +1,22 @@ Serverless Image Handler + Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -Licensed under the Amazon Software License (the "License"). You may not use this file except -in compliance with the License. A copy of the License is located at http://aws.amazon.com/asl/ -or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the -specific language governing permissions and limitations under the License. ********************** THIRD PARTY COMPONENTS ********************** This software includes third party software subject to the following copyrights: -AWS SDK under the Apache License Version 2.0 -uuid under the Massachusetts Institute of Technology (MIT) license -sharp under the Apache License Version 2.0 -sinon under the BSD 3-Clause license +aws-sdk under the Apache License Version 2.0 +aws-sdk-mock under the Apache License Version 2.0 +Bootstrap under the Massachusetts Institute of Technology (MIT) license chai under the Massachusetts Institute of Technology (MIT) license -sinon-chai under the BSD 2-Clause license +minimist under the Massachusetts Institute of Technology (MIT) license mocha under the Massachusetts Institute of Technology (MIT) license +moment under the Massachusetts Institute of Technology (MIT) license +npm-run-all under the Massachusetts Institute of Technology (MIT) license nyc under the ISC license - -The licenses for these third party components are included in LICENSE.txt \ No newline at end of file +sharp under the Apache License Version 2.0 +sinon under the BSD-3-Clause license +sinon-chai under the BSD-2-Clause license +uuid under the Massachusetts Institute of Technology (MIT) license diff --git a/README.md b/README.md index 8f6a448e7..283b49f8e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ **_Important Notice:_** -Due to a [change in the AWS Lambda execution environment](https://aws.amazon.com/blogs/compute/upcoming-updates-to-the-aws-lambda-execution-environment/), Serverless Image Handler v3 deployments are functionally broken. To address the issue we have released [minor version update v3.1.1](https://solutions-reference.s3.amazonaws.com/serverless-image-handler/v3.1.1/serverless-image-handler.template). We recommend all users of v3 to run cloudformation stack update with v3.1.1. Additionally, we suggest you to look at v4 of the solution and migrate to v4 if it addresses all of your use cases. +Due to a [change in the AWS Lambda execution environment](https://aws.amazon.com/blogs/compute/upcoming-updates-to-the-aws-lambda-execution-environment/), Serverless Image Handler v3 deployments are functionally broken. To address the issue we have released [minor version update v3.1.1](https://solutions-reference.s3.amazonaws.com/serverless-image-handler/v3.1.1/serverless-image-handler.template). We recommend all users of v3 to run cloudformation stack update with v3.1.1. Additionally, we suggest you to look at v4 of the solution and migrate to v4 if it addresses all of your use cases. # AWS Serverless Image Handler Lambda wrapper for SharpJS A solution to dynamically handle images on the fly, utilizing Sharp (https://sharp.pixelplumbing.com/en/stable/). @@ -7,57 +7,53 @@ Published version, additional details and documentation are available here: http _Note:_ it is recommend to build the application binary on Amazon Linux. -## Running unit tests for customization +## Building distributable for customization * Clone the repository, then make the desired code changes -* Next, run unit tests to make sure added customization passes the tests -``` -cd ./deployment -chmod +x ./run-unit-tests.sh \n -./run-unit-tests.sh \n +```bash +git clone https://github.com/awslabs/serverless-image-handler.git ``` -## Building distributable for customization -* Configure the bucket name of your target Amazon S3 distribution bucket +* Run unit tests to make sure added customization passes the tests: ``` -export TEMPLATE_OUTPUT_BUCKET=my-bucket-name # bucket where cfn template will reside -export DIST_OUTPUT_BUCKET=my-bucket-name # bucket where customized code will reside -export VERSION=my-version # version number for the customized code +cd ./deployment +chmod +x ./run-unit-tests.sh +./run-unit-tests.sh ``` -_Note:_ You would have to create 2 buckets, one named 'my-bucket-name' and another regional bucket named 'my-bucket-name-'; aws_region is where you are testing the customized solution. Also, the assets in bucket should be publicly accessible. +* Create an Amazon S3 Bucket ``` -* Clone the github repo -```bash -git clone https://github.com/awslabs/serverless-image-handler.git +aws s3 mb s3://my-bucket-us-east-1 --region us-east-1 ``` -* Navigate to the deployment folder +* Navigate to the deployment folder and build the distributable ```bash -cd serverless-image-handler/deployment +chmod +x ./build-s3-dist.sh +./build-s3-dist.sh my-bucket serverless-image-handler my-version ``` -* Now build the distributable -```bash -sudo ./build-s3-dist.sh $DIST_OUTPUT_BUCKET $VERSION -``` +> Note: The build-s3-dist script expects the bucket name as one of its parameters, and this value should not include the region suffix. -* Deploy the distributable to an Amazon S3 bucket in your account. Note: you must have the AWS Command Line Interface installed. +* Deploy the distributable to an Amazon S3 bucket in your account (you must have the AWS CLI installed) ```bash -aws s3 cp ./dist/ s3://$DIST_OUTPUT_BUCKET-[region_name]/serverless-image-handler/$VERSION/ --recursive --exclude "*" --include "*.zip" -aws s3 cp ./dist/serverless-image-handler.template s3://$TEMPLATE_OUTPUT_BUCKET/serverless-image-handler/$VERSION/ +aws s3 cp ./regional-s3-assets/ s3://my-bucket-us-east-1/serverless-image-handler/my-version/ --recursive --acl bucket-owner-full-control ``` -_Note:_ In the above example, the solution template will expect the source code to be located in the my-bucket-name-[region_name] with prefix serverless-image-handler/my-version/serverless-image-handler.zip -* Get the link of the serverless-image-handler.template uploaded to your Amazon S3 bucket. +* Get the link of the serverless-image-handler.template uploaded to your Amazon S3 bucket + * Deploy the Serverless Image Handler solution to your account by launching a new AWS CloudFormation stack using the link of the serverless-image-handler.template -```bash -https://s3.amazonaws.com/my-bucket-name/serverless-image-handler/my-version/serverless-image-handler.template -``` + +*** Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://aws.amazon.com/asl/ + http://www.apache.org/licenses/LICENSE-2.0 -or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index 03849ad6d..d2bf65939 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -1,53 +1,88 @@ +#!/bin/bash +# +# This assumes all of the OS-level configuration has been completed and git repo has already been cloned +# +# This script should be run from the repo's deployment directory +# cd deployment +# ./build-s3-dist.sh source-bucket-base-name trademarked-solution-name version-code +# +# Paramenters: +# - source-bucket-base-name: Name for the S3 bucket location where the template will source the Lambda +# code from. The template will append '-[region_name]' to this bucket name. +# For example: ./build-s3-dist.sh solutions my-solution v1.0.0 +# The template will then expect the source code to be located in the solutions-[region_name] bucket +# +# - trademarked-solution-name: name of the solution for consistency +# +# - version-code: version of the package + +# Check to see if input has been provided: +if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then + echo "Please provide the base source bucket name, trademark approved solution name and version where the lambda code will eventually reside." + echo "For example: ./build-s3-dist.sh solutions trademarked-solution-name v1.0.0" + exit 1 +fi + +set -e + +# Get reference for all important folders +template_dir="$PWD" +template_dist_dir="$template_dir/global-s3-assets" +build_dist_dir="$template_dir/regional-s3-assets" +source_dir="$template_dir/../source" + echo "------------------------------------------------------------------------------" -echo "Setup the dist folder" +echo "Rebuild distribution" echo "------------------------------------------------------------------------------" -rm -r dist -mkdir dist -cd dist -mkdir demo-ui -cd .. +rm -rf $template_dist_dir +mkdir -p $template_dist_dir +rm -rf $build_dist_dir +mkdir -p $build_dist_dir echo "------------------------------------------------------------------------------" -echo "Copy in the template" +echo "CloudFormation Template" echo "------------------------------------------------------------------------------" -cp *.template dist/ - -replace="s/%%BUCKET_NAME%%/$1/g" -sed -i '' -e $replace dist/*.template - -replace="s/%%TEMPLATE_BUCKET%%/$2/g" -sed -i '' -e $replace dist/*.template - -replace="s/%%VERSION%%/$3/g" -sed -i '' -e $replace dist/*.template - -cd ../source +cp $template_dir/serverless-image-handler.template $template_dist_dir/ + +replace="s/%%BUCKET_NAME%%/$1/g" +echo "sed -i -e $replace" +sed -i -e $replace $template_dist_dir/serverless-image-handler.template + +replace="s/%%SOLUTION_NAME%%/$2/g" +echo "sed -i -e $replace" +sed -i -e $replace $template_dist_dir/serverless-image-handler.template + +replace="s/%%VERSION%%/$3/g" +echo "sed -i -e $replace" +sed -i -e $replace $template_dist_dir/serverless-image-handler.template + +cp $template_dist_dir/serverless-image-handler.template $build_dist_dir/ echo "------------------------------------------------------------------------------" echo "Package the image-handler code" echo "------------------------------------------------------------------------------" -cd image-handler -npm install -npm run build -cp dist/image-handler.zip ../../deployment/dist/image-handler.zip +cd $source_dir/image-handler +npm install +npm run build +cp dist/image-handler.zip $build_dist_dir/image-handler.zip echo "------------------------------------------------------------------------------" echo "Package the demo-ui assets" echo "------------------------------------------------------------------------------" -cd .. -cp -r ./demo-ui/** ../deployment/dist/demo-ui +mkdir $build_dist_dir/demo-ui/ +cp -r $source_dir/demo-ui/** $build_dist_dir/demo-ui/ echo "------------------------------------------------------------------------------" echo "Package the custom-resource code" echo "------------------------------------------------------------------------------" -cd custom-resource -npm install -npm run build -cp dist/custom-resource.zip ../../deployment/dist/custom-resource.zip +cd $source_dir/custom-resource +npm install +npm run build +cp dist/custom-resource.zip $build_dist_dir/custom-resource.zip echo "------------------------------------------------------------------------------" echo "Generate the demo-ui manifest document" echo "------------------------------------------------------------------------------" -cd ../../deployment/manifest-generator +cd $template_dir/manifest-generator npm install -node app.js --target ../../source/demo-ui --output ../dist/demo-ui-manifest.json \ No newline at end of file +node app.js --target ../../source/demo-ui --output $build_dist_dir/demo-ui-manifest.json diff --git a/deployment/manifest-generator/app.js b/deployment/manifest-generator/app.js index bb0419c94..8baa8c416 100644 --- a/deployment/manifest-generator/app.js +++ b/deployment/manifest-generator/app.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -21,29 +21,8 @@ const fs = require('fs'); const path = require('path'); const args = require('minimist')(process.argv.slice(2)); -let getFileList = function(path) { - let fileInfo; - let filesFound; - let fileList = []; - - filesFound = fs.readdirSync(path); - for (let i = 0; i < filesFound.length; i++) { - fileInfo = fs.lstatSync([path, filesFound[i]].join('/')); - if (fileInfo.isFile()) { - fileList.push(filesFound[i]); - } - - if (fileInfo.isDirectory()) { - console.log([path, filesFound[i]].join('/')); - } - } - - return fileList; -}; - // List all files in a directory in Node.js recursively in a synchronous fashion let walkSync = function(dir, filelist) { - // let filelist = []; //getFileList('./temp/site'); let files = fs.readdirSync(dir); filelist = filelist || []; files.forEach(function(file) { @@ -78,7 +57,7 @@ walkSync(args.target, _filelist); for (let i = 0; i < _filelist.length; i++) { _manifest.files.push(_filelist[i].replace(`${args.target}/`, '')); -}; +} fs.writeFileSync(args.output, JSON.stringify(_manifest, null, 4)); -console.log(`Manifest file ${args.output} generated.`); \ No newline at end of file +console.log(`Manifest file ${args.output} generated.`); diff --git a/deployment/manifest-generator/package.json b/deployment/manifest-generator/package.json index 38c4e5409..962c6d8bf 100644 --- a/deployment/manifest-generator/package.json +++ b/deployment/manifest-generator/package.json @@ -2,7 +2,7 @@ "name": "manifest-generator", "version": "0.0.0", "private": true, - "description": "Helper utility to create data lake site manifest for deployment", + "description": "Helper utility to create website manifest for deployment", "main": "app.js", "author": { "name": "aws-solutions-builder" diff --git a/deployment/run-unit-tests.sh b/deployment/run-unit-tests.sh index 61aa242e1..48b501274 100755 --- a/deployment/run-unit-tests.sh +++ b/deployment/run-unit-tests.sh @@ -1,7 +1,6 @@ -# Navigate into the source folder -cd .. -cd source +#!/bin/bash -# Run the Jasmine test suite -cd image-handler +set -e + +cd ../source/image-handler npm test diff --git a/deployment/serverless-image-handler.template b/deployment/serverless-image-handler.template index 3e655f49e..2c58d6c81 100644 --- a/deployment/serverless-image-handler.template +++ b/deployment/serverless-image-handler.template @@ -1,393 +1,535 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Description": "(SO0023) - Serverless Image Handler: This template deploys and configures a serverless architecture that is optimized for dynamic image manipulation and delivery at low latency and cost. Leverages SharpJS for image processing. Template version 4.", - "Parameters": { - "CorsEnabled" : { - "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.", - "Default" : "No", - "Type" : "String", - "AllowedValues" : [ "Yes", "No" ] - }, - "CorsOrigin" : { - "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin. We recommend specifying an origin (i.e. https://example.domain) to restrict cross-site access to your API.", - "Default" : "*", - "Type" : "String" - }, - "SourceBuckets" : { - "Description" : "(Required) List the buckets (comma-separated) within your account that contain original image files. If you plan to use Thumbor or Custom image requests with this solution, the source bucket for those requests will be the first bucket listed in this field.", - "Default" : "defaultBucket, bucketNo2, bucketNo3, ...", +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "(SO0023) - Serverless Image Handler: This template deploys and configures a serverless architecture that is optimized for dynamic image manipulation and delivery at low latency and cost. Leverages SharpJS for image processing. Template version %%VERSION%%", + "Parameters": { + "CorsEnabled" : { + "Description" : "Would you like to enable Cross-Origin Resource Sharing (CORS) for the image handler API? Select 'Yes' if so.", + "Default" : "No", + "Type" : "String", + "AllowedValues" : [ "Yes", "No" ] + }, + "CorsOrigin" : { + "Description" : "If you selected 'Yes' above, please specify an origin value here. A wildcard (*) value will support any origin. We recommend specifying an origin (i.e. https://example.domain) to restrict cross-site access to your API.", + "Default" : "*", + "Type" : "String" + }, + "SourceBuckets" : { + "Description" : "(Required) List the buckets (comma-separated) within your account that contain original image files. If you plan to use Thumbor or Custom image requests with this solution, the source bucket for those requests will be the first bucket listed in this field.", + "Default" : "defaultBucket, bucketNo2, bucketNo3, ...", "Type" : "String", "AllowedPattern" : ".+" - }, - "DeployDemoUI" : { - "Description" : "Would you like to deploy a demo UI to explore the features and capabilities of this solution? This will create an additional Amazon S3 bucket and Amazon CloudFront distribution in your account.", - "Default" : "Yes", - "Type" : "String", - "AllowedValues" : [ "Yes", "No" ] - }, - "LogRetentionPeriod" : { - "Description" : "This solution automatically logs events to Amazon CloudWatch. Select the amount of time for CloudWatch logs from this solution to be retained (in days).", - "Default" : 1, - "Type" : "Number", - "AllowedValues" : [ 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653 ] - } - }, - "Metadata": { - "AWS::CloudFormation::Interface": { - "ParameterGroups": [ - { - "Label": { "default": "CORS Options" }, - "Parameters": [ "CorsEnabled", "CorsOrigin" ] - }, - { - "Label": { "default": "Image Sources" }, - "Parameters": [ "SourceBuckets" ] - }, - { - "Label": { "default": "Demo UI" }, - "Parameters": [ "DeployDemoUI" ] - }, - { - "Label": { "default": "Event Logging" }, - "Parameters": [ "LogRetentionPeriod" ] - } - ] - } - }, - "Conditions": { - "DeployDemoUICondition": { "Fn::Equals": [ { "Ref": "DeployDemoUI" }, "Yes" ] }, - "EnableCorsCondition" : { "Fn::Equals": [ { "Ref": "CorsEnabled" }, "Yes" ] } - }, - "Mappings": { + }, + "DeployDemoUI" : { + "Description" : "Would you like to deploy a demo UI to explore the features and capabilities of this solution? This will create an additional Amazon S3 bucket and Amazon CloudFront distribution in your account.", + "Default" : "Yes", + "Type" : "String", + "AllowedValues" : [ "Yes", "No" ] + }, + "LogRetentionPeriod" : { + "Description" : "This solution automatically logs events to Amazon CloudWatch. Select the amount of time for CloudWatch logs from this solution to be retained (in days).", + "Default" : 1, + "Type" : "Number", + "AllowedValues" : [ 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653 ] + } + }, + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": { "default": "CORS Options" }, + "Parameters": [ "CorsEnabled", "CorsOrigin" ] + }, + { + "Label": { "default": "Image Sources" }, + "Parameters": [ "SourceBuckets" ] + }, + { + "Label": { "default": "Demo UI" }, + "Parameters": [ "DeployDemoUI" ] + }, + { + "Label": { "default": "Event Logging" }, + "Parameters": [ "LogRetentionPeriod" ] + } + ] + } + }, + "Conditions": { + "DeployDemoUICondition": { "Fn::Equals": [ { "Ref": "DeployDemoUI" }, "Yes" ] }, + "EnableCorsCondition" : { "Fn::Equals": [ { "Ref": "CorsEnabled" }, "Yes" ] } + }, + "Mappings": { "Send": { "AnonymousUsage": { "Data": "Yes" } }, - "SourceCode": { - "General": { - "S3Bucket": "%%BUCKET_NAME%%", - "KeyPrefix": "serverless-image-handler/%%VERSION%%" - } - } - }, - "Resources": { - "ImageHandlerDistribution": { - "Type": "AWS::CloudFront::Distribution", - "Properties": { - "DistributionConfig": { - "Origins": [{ - "DomainName": { "Fn::Sub": "${ImageHandlerApi}.execute-api.${AWS::Region}.amazonaws.com" }, - "Id": { "Ref": "ImageHandlerApi" }, + "SourceCode": { + "General": { + "S3Bucket": "%%BUCKET_NAME%%", + "KeyPrefix": "%%SOLUTION_NAME%%/%%VERSION%%" + } + } + }, + "Resources": { + "Logs": { + "DeletionPolicy": "Retain", + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "Used to store access logs for other buckets" + }, + { + "id": "W51", + "reason": "Bucket does not need a bucket policy" + } + ] + } + } + }, + "ImageHandlerDistribution": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "Origins": [{ + "DomainName": { "Fn::Sub": "${ImageHandlerApi}.execute-api.${AWS::Region}.amazonaws.com" }, + "Id": { "Ref": "ImageHandlerApi" }, "OriginPath": "/image", - "CustomOriginConfig": { - "HTTPSPort": 443, - "OriginProtocolPolicy": "https-only", - "OriginSSLProtocols": [ "TLSv1", "TLSv1.1", "TLSv1.2" ] - } - }], - "Enabled": true, - "HttpVersion": "http2", - "Comment": "Image handler distribution", - "DefaultCacheBehavior": { - "AllowedMethods": [ "GET", "HEAD" ], - "TargetOriginId": { "Fn::Sub": "${ImageHandlerApi}" }, - "ForwardedValues": { - "QueryString": false, + "CustomOriginConfig": { + "HTTPSPort": 443, + "OriginProtocolPolicy": "https-only", + "OriginSSLProtocols": [ "TLSv1", "TLSv1.1", "TLSv1.2" ] + } + }], + "Enabled": true, + "HttpVersion": "http2", + "Comment": "Image handler distribution", + "DefaultCacheBehavior": { + "AllowedMethods": [ "GET", "HEAD" ], + "TargetOriginId": { "Fn::Sub": "${ImageHandlerApi}" }, + "ForwardedValues": { + "QueryString": false, "Headers": [ "Origin" ], - "Cookies": { "Forward": "none" } - }, - "ViewerProtocolPolicy": "https-only" - }, - "CustomErrorResponses": [ - { - "ErrorCode": 500, - "ErrorCachingMinTTL": 10 - }, - { - "ErrorCode": 501, - "ErrorCachingMinTTL": 10 - }, - { - "ErrorCode": 502, - "ErrorCachingMinTTL": 10 - }, - { - "ErrorCode": 503, - "ErrorCachingMinTTL": 10 - }, - { - "ErrorCode": 504, - "ErrorCachingMinTTL": 10 - } - ], - "PriceClass": "PriceClass_All" - } - }, + "Cookies": { "Forward": "none" } + }, + "ViewerProtocolPolicy": "https-only" + }, + "CustomErrorResponses": [ + { + "ErrorCode": 500, + "ErrorCachingMinTTL": 10 + }, + { + "ErrorCode": 501, + "ErrorCachingMinTTL": 10 + }, + { + "ErrorCode": 502, + "ErrorCachingMinTTL": 10 + }, + { + "ErrorCode": 503, + "ErrorCachingMinTTL": 10 + }, + { + "ErrorCode": 504, + "ErrorCachingMinTTL": 10 + } + ], + "PriceClass": "PriceClass_All", + "Logging": { + "IncludeCookies": false, + "Bucket": { + "Fn::GetAtt": ["Logs", "DomainName"] + }, + "Prefix": "image-handler-cf-logs/" + } + } + } + }, + "ApiLoggingRole": { + "Type": "AWS::IAM::Role", "Metadata": { "cfn_nag": { "rules_to_suppress": [ { - "id": "W10", - "reason": "This CloudFront Distribution does not require access logging. API calls and image operations - are logged to CloudWatch with custom reporting." + "id": "W11", + "reason": "API Gateway requires these permissions for CloudWatch Logging (https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html#set-up-access-logging-permissions)" + } + ] + } + }, + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "apigateway.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] } ] + }, + "Path": "/", + "Policies": [ + { + "PolicyName": { + "Fn::Sub": "${AWS::StackName}-api-logging-policy" + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Resource": { + "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*" + } + } + ] + } + } + ] + } + }, + "ApiAccountConfig": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "ApiLoggingRole", + "Arn" + ] } } - }, - "ImageHandlerApi": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "EndpointConfiguration": { - "Types" : [ "REGIONAL" ] - }, - "Body": { - "swagger": "2.0", - "info": { - "version": "2017-01-27T21:44:58Z", - "title": "ServerlessImageHandler" - }, - "basePath": "/image", - "schemes": [ "https" ], - "paths": { - "/{proxy+}": { - "x-amazon-apigateway-any-method": { - "produces": [ "application/json" ], - "parameters": [ - { - "name": "proxy", - "in": "path", - "required": true, - "type": "string" - } - ], - "responses": {}, - "x-amazon-apigateway-integration": { - "responses": { - "default": { "statusCode": "200" } - }, - "uri": { - "Fn::Join": [ - "", - [ - "arn:aws:apigateway:", - { - "Ref": "AWS::Region" - }, - ":", - "lambda:path/2015-03-31/functions/", - { - "Fn::GetAtt": [ - "ImageHandlerFunction", - "Arn" - ] - }, - "/invocations" - ] - ] - }, - "passthroughBehavior": "when_no_match", - "httpMethod": "POST", - "cacheNamespace": "xh7gp9", - "cacheKeyParameters": [ "method.request.path.proxy" ], - "contentHandling": "CONVERT_TO_TEXT", - "type": "aws_proxy" - } - } - } - }, - "x-amazon-apigateway-binary-media-types": [ - "*/*" - ] - } - } - }, - "ImageHandlerApiDeployment": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { "Ref": "ImageHandlerApi" }, - "StageName": "image" - } - }, - "ImageHandlerFunction": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Description": "Serverless Image Handler - Function for performing image edits and manipulations.", - "Handler": "index.handler", - "Role": { "Fn::GetAtt": [ "ImageHandlerFunctionRole", "Arn" ] }, - "Code": { - "S3Bucket" : { "Fn::Join": [ "-", [{ "Fn::FindInMap": [ "SourceCode", "General", "S3Bucket" ]}, { "Ref": "AWS::Region" }]]}, - "S3Key" : "serverless-image-handler/%%VERSION%%/image-handler.zip" - }, - "Runtime": "nodejs8.10", + }, + "ApiLogs": { + "Type": "AWS::Logs::LogGroup" + }, + "ImageHandlerApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "EndpointConfiguration": { + "Types" : [ "REGIONAL" ] + }, + "Body": { + "swagger": "2.0", + "info": { + "version": "2017-01-27T21:44:58Z", + "title": "ServerlessImageHandler" + }, + "basePath": "/image", + "schemes": [ "https" ], + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "produces": [ "application/json" ], + "parameters": [ + { + "name": "proxy", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": {}, + "x-amazon-apigateway-integration": { + "responses": { + "default": { "statusCode": "200" } + }, + "uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { + "Ref": "AWS::Region" + }, + ":", + "lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "ImageHandlerFunction", + "Arn" + ] + }, + "/invocations" + ] + ] + }, + "passthroughBehavior": "when_no_match", + "httpMethod": "POST", + "cacheNamespace": "xh7gp9", + "cacheKeyParameters": [ "method.request.path.proxy" ], + "contentHandling": "CONVERT_TO_TEXT", + "type": "aws_proxy" + } + } + } + }, + "x-amazon-apigateway-binary-media-types": [ + "*/*" + ] + } + } + }, + "ImageHandlerApiDeployment": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { "Ref": "ImageHandlerApi" }, + "StageName": "image", + "StageDescription": { + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "ApiLogs", + "Arn" + ] + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] \"$context.httpMethod $context.resourcePath $context.protocol\" $context.status $context.responseLength $context.requestId" + } + } + } + }, + "ImageHandlerFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "Serverless Image Handler - Function for performing image edits and manipulations.", + "Handler": "index.handler", + "Role": { "Fn::GetAtt": [ "ImageHandlerFunctionRole", "Arn" ] }, + "Code": { + "S3Bucket": { + "Fn::Join": [ + "-", + [ + { + "Fn::FindInMap": [ + "SourceCode", + "General", + "S3Bucket" + ] + }, + { + "Ref": "AWS::Region" + } + ] + ] + }, + "S3Key": { + "Fn::Join": [ + "/", + [ + { + "Fn::FindInMap": [ + "SourceCode", + "General", + "KeyPrefix" + ] + }, + "image-handler.zip" + ] + ] + } + }, + "Runtime": "nodejs12.x", "MemorySize": 1024, "Timeout": 30, - "Environment" : { - "Variables" : { - "CORS_ENABLED" : { - "Ref" : "CorsEnabled" - }, - "CORS_ORIGIN" : { - "Ref" : "CorsOrigin" - }, - "SOURCE_BUCKETS" : { - "Ref" : "SourceBuckets" - }, - "REWRITE_MATCH_PATTERN" : "", - "REWRITE_SUBSTITUTION" : "" - } - } - } - }, - "ImageHandlerFunctionRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": [ "lambda.amazonaws.com" ] - }, - "Action": [ "sts:AssumeRole" ] - } - ] - }, - "Path": "/", - "RoleName": { - "Fn::Join": [ - "", [ { "Ref": "AWS::StackName" }, "ImageHandlerFunctionRole", "-", {"Ref": "AWS::Region"} ] ] - } + "Environment" : { + "Variables" : { + "CORS_ENABLED" : { + "Ref" : "CorsEnabled" + }, + "CORS_ORIGIN" : { + "Ref" : "CorsOrigin" + }, + "SOURCE_BUCKETS" : { + "Ref" : "SourceBuckets" + }, + "REWRITE_MATCH_PATTERN" : "", + "REWRITE_SUBSTITUTION" : "" + } + } + } + }, + "ImageHandlerFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ "lambda.amazonaws.com" ] + }, + "Action": [ "sts:AssumeRole" ] + } + ] + }, + "Path": "/", + "RoleName": { + "Fn::Join": [ + "", [ { "Ref": "AWS::StackName" }, "ImageHandlerFunctionRole", "-", {"Ref": "AWS::Region"} ] ] + } }, "Metadata": { "cfn_nag": { "rules_to_suppress": [ { "id": "W28", - "reason": "Resource name validated and found to pose no risk to updates that require - replacement of this resource." + "reason": "Resource name validated and found to pose no risk to updates that require replacement of this resource." } ] } - } - }, - "ImageHandlerPolicy" : { - "Type" : "AWS::IAM::Policy", - "Properties" : { - "PolicyDocument" : { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "logs:CreateLogStream", - "logs:CreateLogGroup", - "logs:PutLogEvents" - ], - "Resource": { "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" } - }, - { - "Effect": "Allow", - "Action": [ - "s3:GetObject", - "s3:PutObject", - "s3:ListBucket" - ], - "Resource": [ - "arn:aws:s3:::*" - ] - }, - { - "Effect": "Allow", - "Action": [ + } + }, + "ImageHandlerPolicy" : { + "Type" : "AWS::IAM::Policy", + "Properties" : { + "PolicyDocument" : { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents" + ], + "Resource": { "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" } + }, + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::*" + ] + }, + { + "Effect": "Allow", + "Action": [ "rekognition:DetectFaces" - ], - "Resource": [ - "*" - ] - } - ] - }, - "PolicyName" : { "Fn::Join": [ "", [ { "Ref": "AWS::StackName" }, "ImageHandlerPolicy" ] ] }, - "Roles" : [ { "Ref": "ImageHandlerFunctionRole" }] + ], + "Resource": [ + "*" + ] + } + ] + }, + "PolicyName" : { "Fn::Join": [ "", [ { "Ref": "AWS::StackName" }, "ImageHandlerPolicy" ] ] }, + "Roles" : [ { "Ref": "ImageHandlerFunctionRole" }] }, "Metadata": { "cfn_nag": { "rules_to_suppress": [ { "id": "F3", - "reason": "The rekognition:DetectFaces action requires the wildcard ('*') resource identifier to function properly. Supporting documentation - available at (https://docs.aws.amazon.com/rekognition/latest/dg/using-identity-based-policies.html) and - (https://docs.aws.amazon.com/rekognition/latest/dg/api-permissions-reference.html)." + "reason": "The rekognition:DetectFaces action requires the wildcard ('*') resource identifier to function properly. Supporting documentation available at (https://docs.aws.amazon.com/rekognition/latest/dg/using-identity-based-policies.html) and (https://docs.aws.amazon.com/rekognition/latest/dg/api-permissions-reference.html)." }, { "id": "W12", - "reason": "The ImageHandlerPolicy provides access to all Amazon S3 buckets within the user's account to enable sourcing image files from - multiple origins." + "reason": "The ImageHandlerPolicy provides access to all Amazon S3 buckets within the user's account to enable sourcing image files from multiple origins." } ] } } - }, - "ImageHandlerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": ["ImageHandlerFunction","Arn"] - }, - "Principal": "apigateway.amazonaws.com", - "SourceArn": { - "Fn::Join": [ - "", - [ - "arn:aws:execute-api:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":", - { - "Ref": "ImageHandlerApi" - }, - "/*/*/*" - ] - ] - } - } - }, - "ImageHandlerLogGroup": { - "Type": "AWS::Logs::LogGroup", - "Properties": { - "LogGroupName": { "Fn::Join": ["/", ["/aws/lambda", { "Ref" : "ImageHandlerFunction" }]]}, - "RetentionInDays": { "Ref" : "LogRetentionPeriod" } - } - }, - "DemoBucket": { - "Type": "AWS::S3::Bucket", - "Condition": "DeployDemoUICondition", - "DeletionPolicy": "Retain", - "Properties": { + }, + "ImageHandlerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": ["ImageHandlerFunction","Arn"] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "ImageHandlerApi" + }, + "/*/*/*" + ] + ] + } + } + }, + "ImageHandlerLogGroup": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "LogGroupName": { "Fn::Join": ["/", ["/aws/lambda", { "Ref" : "ImageHandlerFunction" }]]}, + "RetentionInDays": { "Ref" : "LogRetentionPeriod" } + } + }, + "DemoBucket": { + "Type": "AWS::S3::Bucket", + "Condition": "DeployDemoUICondition", + "DeletionPolicy": "Retain", + "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ { "ServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } } ] }, - "WebsiteConfiguration": { - "IndexDocument": "index.html", - "ErrorDocument": "index.html" - }, - "AccessControl": "Private", - "PublicAccessBlockConfiguration": { - "BlockPublicAcls": true, - "BlockPublicPolicy": true, - "IgnorePublicAcls": true, - "RestrictPublicBuckets": true + "WebsiteConfiguration": { + "IndexDocument": "index.html", + "ErrorDocument": "index.html" + }, + "AccessControl": "Private", + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true } }, "Metadata": { @@ -395,224 +537,247 @@ "rules_to_suppress": [ { "id": "W35", - "reason": "This S3 bucket does not require access logging. API calls and image operations - are logged to CloudWatch with custom reporting." + "reason": "This S3 bucket does not require access logging. API calls and image operations are logged to CloudWatch with custom reporting." } ] } - } - }, - "DemoBucketPolicy": { - "Type": "AWS::S3::BucketPolicy", - "Condition": "DeployDemoUICondition", - "Properties": { - "Bucket": { "Ref": "DemoBucket" }, - "PolicyDocument": { - "Statement": [ - { - "Action": [ "s3:GetObject" ], - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { "Ref": "DemoBucket" }, - "/*" - ] - ] - }, - "Principal": { - "CanonicalUser": { - "Fn::GetAtt": [ - "DemoOriginAccessIdentity", - "S3CanonicalUserId" - ] - } - } - } - ] - } - } - }, - "DemoOriginAccessIdentity": { - "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", - "Condition": "DeployDemoUICondition", - "Properties": { - "CloudFrontOriginAccessIdentityConfig": { - "Comment": { - "Fn::Sub": "access-identity-${DemoBucket}" - } - } - } - }, - "DemoDistribution": { - "Type": "AWS::CloudFront::Distribution", - "Condition": "DeployDemoUICondition", - "Properties": { - "DistributionConfig": { - "Comment": "Website distribution for solution", - "Origins": [ - { - "Id": "S3-solution-website", - "DomainName": { "Fn::Sub": "${DemoBucket}.s3.${AWS::Region}.amazonaws.com" }, - "S3OriginConfig": { - "OriginAccessIdentity": { "Fn::Sub": "origin-access-identity/cloudfront/${DemoOriginAccessIdentity}" } - } - } - ], - "DefaultCacheBehavior": { - "TargetOriginId": "S3-solution-website", - "AllowedMethods": [ "GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE" ], - "CachedMethods": [ "GET", "HEAD", "OPTIONS" ], - "ForwardedValues": { - "QueryString": false - }, - "ViewerProtocolPolicy": "redirect-to-https" - }, - "IPV6Enabled": true, - "ViewerCertificate": { "CloudFrontDefaultCertificate": true }, - "Enabled": true, - "HttpVersion": "http2" - } - }, - "Metadata": { - "cfn_nag": { - "rules_to_suppress": [ + } + }, + "DemoBucketPolicy": { + "Type": "AWS::S3::BucketPolicy", + "Condition": "DeployDemoUICondition", + "Properties": { + "Bucket": { "Ref": "DemoBucket" }, + "PolicyDocument": { + "Statement": [ { - "id": "W10", - "reason": "This CloudFront Distribution does not require access logging. API calls and image operations - are logged to CloudWatch with custom reporting." + "Action": [ "s3:GetObject" ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { "Ref": "DemoBucket" }, + "/*" + ] + ] + }, + "Principal": { + "CanonicalUser": { + "Fn::GetAtt": [ + "DemoOriginAccessIdentity", + "S3CanonicalUserId" + ] + } + } } ] } - } - }, - "CustomResourceFunction": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "S3Bucket": { "Fn::Join": [ "-", [{ "Fn::FindInMap": [ "SourceCode", "General", "S3Bucket" ]}, { "Ref": "AWS::Region" }]]}, - "S3Key": "serverless-image-handler/%%VERSION%%/custom-resource.zip" - }, - "Timeout": 30, - "Runtime": "nodejs8.10", - "Role": { "Fn::GetAtt": [ "CustomResourceRole", "Arn" ] }, + } + }, + "DemoOriginAccessIdentity": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Condition": "DeployDemoUICondition", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": { + "Fn::Sub": "access-identity-${DemoBucket}" + } + } + } + }, + "DemoDistribution": { + "Type": "AWS::CloudFront::Distribution", + "Condition": "DeployDemoUICondition", + "Properties": { + "DistributionConfig": { + "Comment": "Website distribution for solution", + "Origins": [ + { + "Id": "S3-solution-website", + "DomainName": { "Fn::Sub": "${DemoBucket}.s3.${AWS::Region}.amazonaws.com" }, + "S3OriginConfig": { + "OriginAccessIdentity": { "Fn::Sub": "origin-access-identity/cloudfront/${DemoOriginAccessIdentity}" } + } + } + ], + "DefaultCacheBehavior": { + "TargetOriginId": "S3-solution-website", + "AllowedMethods": [ "GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE" ], + "CachedMethods": [ "GET", "HEAD", "OPTIONS" ], + "ForwardedValues": { + "QueryString": false + }, + "ViewerProtocolPolicy": "redirect-to-https" + }, + "IPV6Enabled": true, + "ViewerCertificate": { "CloudFrontDefaultCertificate": true }, + "Enabled": true, + "HttpVersion": "http2", + "Logging": { + "IncludeCookies": false, + "Bucket": { + "Fn::GetAtt": ["Logs", "DomainName"] + }, + "Prefix": "demo-cf-logs/" + } + } + } + }, + "CustomResourceFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Join": [ + "-", + [ + { + "Fn::FindInMap": [ + "SourceCode", + "General", + "S3Bucket" + ] + }, + { + "Ref": "AWS::Region" + } + ] + ] + }, + "S3Key": { + "Fn::Join": [ + "/", + [ + { + "Fn::FindInMap": [ + "SourceCode", + "General", + "KeyPrefix" + ] + }, + "custom-resource.zip" + ] + ] + } + }, + "Timeout": 30, + "Runtime": "nodejs12.x", + "Role": { "Fn::GetAtt": [ "CustomResourceRole", "Arn" ] }, "Handler": "index.handler" - } - }, - "CustomResourceRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": [ "lambda.amazonaws.com" ] - }, - "Action": [ "sts:AssumeRole" ] - } - ] - }, - "Path": "/", - "RoleName": { - "Fn::Join": [ + } + }, + "CustomResourceRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ "lambda.amazonaws.com" ] + }, + "Action": [ "sts:AssumeRole" ] + } + ] + }, + "Path": "/", + "RoleName": { + "Fn::Join": [ "", [ { "Ref": "AWS::StackName" }, "CustomResourceRole", "-", {"Ref": "AWS::Region"} ] ] - } + } }, "Metadata": { "cfn_nag": { "rules_to_suppress": [ { "id": "W28", - "reason": "Resource name validated and found to pose no risk to updates that require replacement - of this resource." + "reason": "Resource name validated and found to pose no risk to updates that require replacement of this resource." } ] } } - }, - "CustomResourcePolicy" : { - "Type" : "AWS::IAM::Policy", - "Properties" : { - "PolicyDocument" : { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "logs:CreateLogStream", - "logs:CreateLogGroup", - "logs:PutLogEvents" - ], - "Resource": { "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" } - }, - { - "Effect": "Allow", - "Action": [ - "s3:GetObject", - "s3:PutObject", + }, + "CustomResourcePolicy" : { + "Type" : "AWS::IAM::Policy", + "Properties" : { + "PolicyDocument" : { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents" + ], + "Resource": { "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*" } + }, + { + "Effect": "Allow", + "Action": [ + "s3:GetObject", + "s3:PutObject", "s3:ListBucket" - ], - "Resource": [ - "arn:aws:s3:::*" - ] + ], + "Resource": [ + "arn:aws:s3:::*" + ] } - ] - }, - "PolicyName" : { "Fn::Join": [ "", [ { "Ref": "AWS::StackName" }, "CustomResourcePolicy" ] ] }, - "Roles" : [ { "Ref": "CustomResourceRole" } ] + ] + }, + "PolicyName" : { "Fn::Join": [ "", [ { "Ref": "AWS::StackName" }, "CustomResourcePolicy" ] ] }, + "Roles" : [ { "Ref": "CustomResourceRole" } ] }, "Metadata": { "cfn_nag": { "rules_to_suppress": [ { "id": "W12", - "reason": "The CustomResourcePolicy requires the * resource to validate that input from the SourceBuckets template parameter matches - existing buckets within the user's account." + "reason": "The CustomResourcePolicy requires the * resource to validate that input from the SourceBuckets template parameter matches existing buckets within the user's account." } ] } } - }, - "CustomResourceCopyS3": { - "Type": "Custom::CustomResource", - "Condition" : "DeployDemoUICondition", - "Properties": { - "ServiceToken": { "Fn::GetAtt" : [ "CustomResourceFunction", "Arn" ]}, - "Region": { "Ref" : "AWS::Region" }, - "manifestKey": { "Fn::Join": [ "/", [ { "Fn::FindInMap": [ "SourceCode", "General", "KeyPrefix" ]}, "demo-ui-manifest.json" ]]}, - "sourceS3Bucket": { "Fn::Join": [ "-", [{ "Fn::FindInMap": [ "SourceCode", "General", "S3Bucket" ]}, { "Ref": "AWS::Region" }]]}, - "sourceS3key": { "Fn::Join": ["/", [{ "Fn::FindInMap": [ "SourceCode", "General", "KeyPrefix" ]}, "demo-ui" ]]}, - "destS3Bucket": { "Ref" : "DemoBucket" }, - "version": "%%VERSION%%", - "customAction": "copyS3assets" + }, + "CustomResourceCopyS3": { + "Type": "Custom::CustomResource", + "Condition" : "DeployDemoUICondition", + "Properties": { + "ServiceToken": { "Fn::GetAtt" : [ "CustomResourceFunction", "Arn" ]}, + "Region": { "Ref" : "AWS::Region" }, + "manifestKey": { "Fn::Join": [ "/", [ { "Fn::FindInMap": [ "SourceCode", "General", "KeyPrefix" ]}, "demo-ui-manifest.json" ]]}, + "sourceS3Bucket": { "Fn::Join": [ "-", [{ "Fn::FindInMap": [ "SourceCode", "General", "S3Bucket" ]}, { "Ref": "AWS::Region" }]]}, + "sourceS3key": { "Fn::Join": ["/", [{ "Fn::FindInMap": [ "SourceCode", "General", "KeyPrefix" ]}, "demo-ui" ]]}, + "destS3Bucket": { "Ref" : "DemoBucket" }, + "version": "%%VERSION%%", + "customAction": "copyS3assets" }, "DependsOn": [ "CustomResourcePolicy", "CustomResourceRole" ] - }, - "CustomResourceConfig": { - "Type": "Custom::CustomResource", - "Condition" : "DeployDemoUICondition", - "Properties": { - "ServiceToken": { "Fn::GetAtt" : [ "CustomResourceFunction", "Arn" ]}, - "Region": { "Ref" : "AWS::Region" }, - "configItem" : { + }, + "CustomResourceConfig": { + "Type": "Custom::CustomResource", + "Condition" : "DeployDemoUICondition", + "Properties": { + "ServiceToken": { "Fn::GetAtt" : [ "CustomResourceFunction", "Arn" ]}, + "Region": { "Ref" : "AWS::Region" }, + "configItem" : { "apiEndpoint" : { "Fn::Sub" : "https://${ImageHandlerDistribution.DomainName}" } } , "destS3Bucket" : { "Ref" : "DemoBucket" }, "destS3key" : "demo-ui-config.js", - "customAction": "putConfigFile" + "customAction": "putConfigFile" }, "DependsOn": [ "CustomResourcePolicy", "CustomResourceRole" ] - }, - "CustomResourceLogGroup": { + }, + "CustomResourceLogGroup": { "Type": "AWS::Logs::LogGroup", - "Properties": { - "LogGroupName": { "Fn::Join": ["/", ["/aws/lambda", { "Ref" : "CustomResourceFunction" }]]}, - "RetentionInDays": { "Ref" : "LogRetentionPeriod" } - } + "Properties": { + "LogGroupName": { "Fn::Join": ["/", ["/aws/lambda", { "Ref" : "CustomResourceFunction" }]]}, + "RetentionInDays": { "Ref" : "LogRetentionPeriod" } + } }, "CustomResourceUuid": { "Type": "Custom::CustomResource", @@ -630,7 +795,7 @@ "Region": [{ "Ref": "AWS::Region" }], "solutionId": "SO0023", "UUID": {"Fn::GetAtt": ["CustomResourceUuid", "UUID"]}, - "version": "4.0", + "version": "%%VERSION%%", "anonymousData": {"Fn::FindInMap": ["Send", "AnonymousUsage", "Data"]}, "customAction": "sendMetric" }, @@ -646,33 +811,33 @@ }, "DependsOn": [ "CustomResourcePolicy", "CustomResourceRole" ] } - }, - "Outputs" : { - "ApiEndpoint" : { - "Description" : "Link to API endpoint for sending image requests to.", - "Value" : { "Fn::Sub" : "https://${ImageHandlerDistribution.DomainName}" } - }, - "DemoUrl" : { - "Condition" : "DeployDemoUICondition", - "Description" : "Link to the demo user interface for the solution.", - "Value" : { "Fn::Sub" : "https://${DemoDistribution.DomainName}/index.html" } - }, - "SourceBuckets" : { - "Description" : "Amazon S3 bucket location containing original image files.", - "Value" : { "Ref" : "SourceBuckets"} - }, - "CorsEnabled" : { - "Description" : "Indicates whether Cross-Origin Resource Sharing (CORS) has been enabled for the image handler API.", - "Value" : { "Ref" : "CorsEnabled" } - }, - "CorsOrigin" : { - "Condition" : "EnableCorsCondition", - "Description" : "Origin value returned in the Access-Control-Allow-Origin header of image handler API responses.", - "Value" : { "Ref" : "CorsOrigin" } - }, - "LogRetentionPeriod" : { - "Description" : "Number of days for event logs from Lambda to be retained in CloudWatch.", - "Value" : { "Ref" : "LogRetentionPeriod" } - } - } -} \ No newline at end of file + }, + "Outputs" : { + "ApiEndpoint" : { + "Description" : "Link to API endpoint for sending image requests to.", + "Value" : { "Fn::Sub" : "https://${ImageHandlerDistribution.DomainName}" } + }, + "DemoUrl" : { + "Condition" : "DeployDemoUICondition", + "Description" : "Link to the demo user interface for the solution.", + "Value" : { "Fn::Sub" : "https://${DemoDistribution.DomainName}/index.html" } + }, + "SourceBuckets" : { + "Description" : "Amazon S3 bucket location containing original image files.", + "Value" : { "Ref" : "SourceBuckets"} + }, + "CorsEnabled" : { + "Description" : "Indicates whether Cross-Origin Resource Sharing (CORS) has been enabled for the image handler API.", + "Value" : { "Ref" : "CorsEnabled" } + }, + "CorsOrigin" : { + "Condition" : "EnableCorsCondition", + "Description" : "Origin value returned in the Access-Control-Allow-Origin header of image handler API responses.", + "Value" : { "Ref" : "CorsOrigin" } + }, + "LogRetentionPeriod" : { + "Description" : "Number of days for event logs from Lambda to be retained in CloudWatch.", + "Value" : { "Ref" : "LogRetentionPeriod" } + } + } +} \ No newline at end of file diff --git a/source/custom-resource/index.js b/source/custom-resource/index.js index 552e25dfb..50ceea1ed 100644 --- a/source/custom-resource/index.js +++ b/source/custom-resource/index.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -21,7 +21,7 @@ const url = require('url'); const moment = require('moment'); const S3Helper = require('./lib/s3-helper.js'); const UsageMetrics = require('./lib/usage-metrics'); -const UUID = require('node-uuid'); +const uuidv4 = require('uuid/v4'); /** * Request handler. @@ -104,7 +104,7 @@ exports.handler = (event, context, callback) => { } else if (event.ResourceProperties.customAction === 'createUuid') { responseStatus = 'SUCCESS'; responseData = { - UUID: UUID.v4() + UUID: uuidv4() }; sendResponse(event, callback, context.logStreamName, responseStatus, responseData); @@ -139,16 +139,11 @@ exports.handler = (event, context, callback) => { _usageMetrics.sendAnonymousMetric(_metric).then((data) => { console.log(data); console.log('Annonymous metrics successfully sent.'); - responseStatus = 'SUCCESS'; - responseData = {}; - sendResponse(event, callback, context.logStreamName, responseStatus, responseData); }).catch((err) => { - responseData = { - Error: 'Sending anonymous launch metric failed' - }; - console.log([responseData.Error, ':\n', err].join('')); - sendResponse(event, callback, context.logStreamName, responseStatus, responseData); + console.log(`Sending anonymous launch metric failed: ${err}`); }); + + sendResponse(event, callback, context.logStreamName, 'SUCCESS', {}); } else { sendResponse(event, callback, context.logStreamName, 'SUCCESS'); } @@ -205,7 +200,7 @@ exports.handler = (event, context, callback) => { console.log(responseData.Error); sendResponse(event, callback, context.logStreamName, responseStatus, responseData, responseData.Error); }); - + } else { sendResponse(event, callback, context.logStreamName, 'SUCCESS'); } @@ -216,10 +211,10 @@ exports.handler = (event, context, callback) => { * Sends a response to the pre-signed S3 URL */ let sendResponse = function(event, callback, logStreamName, responseStatus, responseData, customReason) { - + const defaultReason = `See the details in CloudWatch Log Stream: ${logStreamName}`; const reason = (customReason !== undefined) ? customReason : defaultReason; - + const responseBody = JSON.stringify({ Status: responseStatus, Reason: reason, diff --git a/source/custom-resource/lib/s3-helper.js b/source/custom-resource/lib/s3-helper.js index 39729920a..822b4e464 100644 --- a/source/custom-resource/lib/s3-helper.js +++ b/source/custom-resource/lib/s3-helper.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -40,7 +40,7 @@ class s3Helper { /** * validateBuckets * Cross-checks provided bucket names against existing bucket names in the account for - * validation. + * validation. * @param {String} strBuckets - String of bucket names from the template params. */ async validateBuckets(strBuckets) { diff --git a/source/custom-resource/lib/usage-metrics/metrics.common.js b/source/custom-resource/lib/usage-metrics/metrics.common.js index 5464dc35b..c0705b5dd 100644 --- a/source/custom-resource/lib/usage-metrics/metrics.common.js +++ b/source/custom-resource/lib/usage-metrics/metrics.common.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ diff --git a/source/custom-resource/lib/usage-metrics/test-setup.spec.js b/source/custom-resource/lib/usage-metrics/test-setup.spec.js index 4cb0f02ab..6c7bb160f 100644 --- a/source/custom-resource/lib/usage-metrics/test-setup.spec.js +++ b/source/custom-resource/lib/usage-metrics/test-setup.spec.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ diff --git a/source/custom-resource/package.json b/source/custom-resource/package.json index 98c417a2f..b65cca379 100644 --- a/source/custom-resource/package.json +++ b/source/custom-resource/package.json @@ -9,9 +9,7 @@ "private": true, "dependencies": { "moment": "^2.24.0", - "node-uuid": "^1.4.8", - "password-generator": "*", - "underscore": "*", + "uuid": "^3.3.3", "usage-metrics": "file:lib/usage-metrics/" }, "devDependencies": { @@ -33,8 +31,7 @@ }, "bundledDependencies": [ "moment", - "underscore", - "password-generator", - "node-uuid" - ] + "uuid" + ], + "license": "Apache-2.0" } diff --git a/source/demo-ui/scripts.js b/source/demo-ui/scripts.js index d2dd80dc2..00437a669 100644 --- a/source/demo-ui/scripts.js +++ b/source/demo-ui/scripts.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -62,7 +62,7 @@ function getPreviewImage() { if (_negative) { _edits.negate = _negative } if (_flatten) { _edits.flatten = _flatten } if (_normalize) { _edits.normalise = _normalize } - if (_rgb !== "") { + if (_rgb !== "") { const input = _rgb.replace(/\s+/g, ''); const arr = input.split(','); const rgb = { r: Number(arr[0]), g: Number(arr[1]), b: Number(arr[2]) }; diff --git a/source/image-handler/image-handler.js b/source/image-handler/image-handler.js index 4b4b15c59..c85b6864f 100644 --- a/source/image-handler/image-handler.js +++ b/source/image-handler/image-handler.js @@ -1,65 +1,66 @@ -/********************************************************************************************************************* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * - * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * - * with the License. A copy of the License is located at * - * * - * http://aws.amazon.com/asl/ * - * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * - * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * - * and limitations under the License. * - *********************************************************************************************************************/ - -const AWS = require('aws-sdk'); -const sharp = require('sharp'); - -class ImageHandler { - - /** - * Main method for processing image requests and outputting modified images. - * @param {ImageRequest} request - An ImageRequest object. - */ - async process(request) { - const originalImage = request.originalImage; - const edits = request.edits; - if (edits !== undefined) { - const modifiedImage = await this.applyEdits(originalImage, edits); - if (request.outputFormat !== undefined) { - await modifiedImage.toFormat(request.outputFormat); - } - const bufferImage = await modifiedImage.toBuffer(); - return bufferImage.toString('base64'); - } else { - return originalImage.toString('base64'); - } - } - - /** - * Applies image modifications to the original image based on edits - * specified in the ImageRequest. - * @param {Buffer} originalImage - The original image. - * @param {Object} edits - The edits to be made to the original image. - */ - async applyEdits(originalImage, edits) { - const image = sharp(originalImage); - const keys = Object.keys(edits); - const values = Object.values(edits); - // Apply the image edits - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - if (key === 'overlayWith') { - const overlay = await this.getOverlayImage(value.bucket, value.key); - image.overlayWith(overlay, value.options); - } else if (key === 'smartCrop') { +/********************************************************************************************************************* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * + * with the License. A copy of the License is located at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * + * and limitations under the License. * + *********************************************************************************************************************/ + +const AWS = require('aws-sdk'); +const sharp = require('sharp'); + +class ImageHandler { + + /** + * Main method for processing image requests and outputting modified images. + * @param {ImageRequest} request - An ImageRequest object. + */ + async process(request) { + const originalImage = request.originalImage; + const edits = request.edits; + if (edits !== undefined) { + const modifiedImage = await this.applyEdits(originalImage, edits); + if (request.outputFormat !== undefined) { + await modifiedImage.toFormat(request.outputFormat); + } + const bufferImage = await modifiedImage.toBuffer(); + return bufferImage.toString('base64'); + } else { + return originalImage.toString('base64'); + } + } + + /** + * Applies image modifications to the original image based on edits + * specified in the ImageRequest. + * @param {Buffer} originalImage - The original image. + * @param {Object} edits - The edits to be made to the original image. + */ + async applyEdits(originalImage, edits) { + const image = sharp(originalImage); + const keys = Object.keys(edits); + const values = Object.values(edits); + // Apply the image edits + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + if (key === 'overlayWith') { + const overlay = await this.getOverlayImage(value.bucket, value.key); + const params = [{ ...value.options, input: overlay }]; + image.composite(params); + } else if (key === 'smartCrop') { const options = value; const imageBuffer = await image.toBuffer(); const metadata = await image.metadata(); // ---- - const boundingBox = await this.getBoundingBox(imageBuffer, options.faceIndex); - const cropArea = await this.getCropArea(boundingBox, options, metadata); - try { image.extract(cropArea) } + const boundingBox = await this.getBoundingBox(imageBuffer, options.faceIndex); + const cropArea = await this.getCropArea(boundingBox, options, metadata); + try { image.extract(cropArea) } catch (err) { throw ({ status: 400, @@ -67,94 +68,94 @@ class ImageHandler { message: 'The padding value you provided exceeds the boundaries of the original image. Please try choosing a smaller value or applying padding via Sharp for greater specificity.' }); } - } else { - image[key](value); - } - } - // Return the modified image - return image; - } - - /** - * Gets an image to be used as an overlay to the primary image from an - * Amazon S3 bucket. - * @param {string} bucket - The name of the bucket containing the overlay. - * @param {string} key - The keyname corresponding to the overlay. - */ - async getOverlayImage(bucket, key) { - const s3 = new AWS.S3(); - const params = { Bucket: bucket, Key: key }; - // Request - const request = s3.getObject(params).promise(); - // Response handling - try { - const overlayImage = await request; - return Promise.resolve(overlayImage.Body); - } catch (err) { - return Promise.reject({ - status: 500, - code: err.code, - message: err.message - }) - } - } - - /** + } else { + image[key](value); + } + } + // Return the modified image + return image; + } + + /** + * Gets an image to be used as an overlay to the primary image from an + * Amazon S3 bucket. + * @param {string} bucket - The name of the bucket containing the overlay. + * @param {string} key - The keyname corresponding to the overlay. + */ + async getOverlayImage(bucket, key) { + const s3 = new AWS.S3(); + const params = { Bucket: bucket, Key: key }; + // Request + const request = s3.getObject(params).promise(); + // Response handling + try { + const overlayImage = await request; + return Promise.resolve(overlayImage.Body); + } catch (err) { + return Promise.reject({ + status: 500, + code: err.code, + message: err.message + }) + } + } + + /** * Calculates the crop area for a smart-cropped image based on the bounding * box data returned by Amazon Rekognition, as well as padding options and * the image metadata. - * @param {Object} boundingBox - The boudning box of the detected face. - * @param {Object} options - Set of options for smart cropping. + * @param {Object} boundingBox - The boudning box of the detected face. + * @param {Object} options - Set of options for smart cropping. * @param {Object} metadata - Sharp image metadata. - */ - getCropArea(boundingBox, options, metadata) { + */ + getCropArea(boundingBox, options, metadata) { const padding = (options.padding !== undefined) ? parseFloat(options.padding) : 0; - // Calculate the smart crop area - const cropArea = { - left : parseInt((boundingBox.Left*metadata.width)-padding), - top : parseInt((boundingBox.Top*metadata.height)-padding), - width : parseInt((boundingBox.Width*metadata.width)+(padding*2)), - height : parseInt((boundingBox.Height*metadata.height)+(padding*2)), - } - // Return the crop area - return cropArea; - } - - /** - * Gets the bounding box of the specified face index within an image, if specified. - * @param {Sharp} imageBuffer - The original image. - * @param {Integer} faceIndex - The zero-based face index value, moving from 0 and up as - * confidence decreases for detected faces within the image. - */ - async getBoundingBox(imageBuffer, faceIndex) { - const rekognition = new AWS.Rekognition(); - const params = { Image: { Bytes: imageBuffer }}; + // Calculate the smart crop area + const cropArea = { + left : parseInt((boundingBox.Left*metadata.width)-padding), + top : parseInt((boundingBox.Top*metadata.height)-padding), + width : parseInt((boundingBox.Width*metadata.width)+(padding*2)), + height : parseInt((boundingBox.Height*metadata.height)+(padding*2)), + } + // Return the crop area + return cropArea; + } + + /** + * Gets the bounding box of the specified face index within an image, if specified. + * @param {Sharp} imageBuffer - The original image. + * @param {Integer} faceIndex - The zero-based face index value, moving from 0 and up as + * confidence decreases for detected faces within the image. + */ + async getBoundingBox(imageBuffer, faceIndex) { + const rekognition = new AWS.Rekognition(); + const params = { Image: { Bytes: imageBuffer }}; const faceIdx = (faceIndex !== undefined) ? faceIndex : 0; - // Request - const request = rekognition.detectFaces(params).promise(); - // Response handling - try { + // Request + const request = rekognition.detectFaces(params).promise(); + // Response handling + try { const response = (await request).FaceDetails[faceIdx].BoundingBox; - return Promise.resolve(await response); - } catch (err) { - console.log(err); + return Promise.resolve(await response); + } catch (err) { + console.log(err); if (err.message === "Cannot read property 'BoundingBox' of undefined") { - return Promise.reject({ + return Promise.reject({ status: 400, code: 'SmartCrop::FaceIndexOutOfRange', message: 'You have provided a FaceIndex value that exceeds the length of the zero-based detectedFaces array. Please specify a value that is in-range.' - }) + }) } else { - return Promise.reject({ - status: 500, - code: err.code, - message: err.message - }) + return Promise.reject({ + status: 500, + code: err.code, + message: err.message + }) } - } - } -} - -// Exports -module.exports = ImageHandler; + } + } +} + +// Exports +module.exports = ImageHandler; diff --git a/source/image-handler/image-request.js b/source/image-handler/image-request.js index 75b7da7e7..405ba6313 100644 --- a/source/image-handler/image-request.js +++ b/source/image-handler/image-request.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -14,7 +14,7 @@ const ThumborMapping = require('./thumbor-mapping'); class ImageRequest { - + /** * Initializer function for creating a new image request, used by the image * handler to perform image modifications. @@ -151,7 +151,7 @@ class ImageRequest { /** * Determines how to handle the request being made based on the URL path - * prefix to the image request. Categorizes a request as either "image" + * prefix to the image request. Categorizes a request as either "image" * (uses the Sharp library), "thumbor" (uses Thumbor mapping), or "custom" * (uses the rewrite function). * @param {Object} event - Lambda request body. @@ -163,9 +163,9 @@ class ImageRequest { const matchThumbor = new RegExp(/^(\/?)((fit-in)?|(filters:.+\(.?\))?|(unsafe)?).*(.+jpg|.+png|.+webp|.+tiff|.+jpeg)$/); const matchCustom = new RegExp(/(\/?)(.*)(jpg|png|webp|tiff|jpeg)/); const definedEnvironmentVariables = ( - (process.env.REWRITE_MATCH_PATTERN !== "") && - (process.env.REWRITE_SUBSTITUTION !== "") && - (process.env.REWRITE_MATCH_PATTERN !== undefined) && + (process.env.REWRITE_MATCH_PATTERN !== "") && + (process.env.REWRITE_SUBSTITUTION !== "") && + (process.env.REWRITE_MATCH_PATTERN !== undefined) && (process.env.REWRITE_SUBSTITUTION !== undefined) ); // ---- @@ -214,7 +214,7 @@ class ImageRequest { } /** - * Returns a formatted image source bucket whitelist as specified in the + * Returns a formatted image source bucket whitelist as specified in the * SOURCE_BUCKETS environment variable of the image handler Lambda * function. Provides error handling for missing/invalid values. */ diff --git a/source/image-handler/index.js b/source/image-handler/index.js index 840b5bf74..f6a9cba8a 100755 --- a/source/image-handler/index.js +++ b/source/image-handler/index.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -42,9 +42,9 @@ exports.handler = async (event) => { } /** - * Generates the appropriate set of response headers based on a success + * Generates the appropriate set of response headers based on a success * or error condition. - * @param {boolean} isErr - has an error been thrown? + * @param {boolean} isErr - has an error been thrown? */ const getResponseHeaders = (isErr) => { const corsEnabled = (process.env.CORS_ENABLED === "Yes"); diff --git a/source/image-handler/package.json b/source/image-handler/package.json index 3dee6a793..e1a5d0028 100644 --- a/source/image-handler/package.json +++ b/source/image-handler/package.json @@ -5,12 +5,11 @@ "author": { "name": "aws-solutions-builder" }, - "version": "4.0.0", + "version": "0.0.1", "private": true, - "license": "ISC", "dependencies": { "mocha": "^6.1.4", - "sharp": "^0.21.3", + "sharp": "^0.23.3", "sinon": "^7.3.2", "nyc": "^14.0.0" }, @@ -25,5 +24,6 @@ "build:zip": "zip -rq image-handler.zip . -x template.yml", "build:dist": "mkdir dist && mv image-handler.zip dist/", "build": "npm run build:init && npm install --production && npm run build:zip && npm run build:dist" - } + }, + "license": "Apache-2.0" } diff --git a/source/image-handler/test/test-image-handler.js b/source/image-handler/test/test-image-handler.js index eda63fc59..739d2777c 100644 --- a/source/image-handler/test/test-image-handler.js +++ b/source/image-handler/test/test-image-handler.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -112,7 +112,7 @@ describe('process()', function() { // ---------------------------------------------------------------------------- describe('applyEdits()', function() { describe('001/standardEdits', function() { - it(`Should pass if a series of standard edits are provided to the + it(`Should pass if a series of standard edits are provided to the function`, async function() { // Arrange const originalImage = Buffer.from('sampleImageContent'); @@ -154,10 +154,8 @@ describe('applyEdits()', function() { // Assert const imageHandler = new ImageHandler(); await imageHandler.applyEdits(originalImage, edits).then((result) => { - assert.deepEqual(result.options.overlay.buffer, originalImage); - }).catch((err) => { - console.log(err) - }) + assert.deepEqual(result.options.input.buffer, originalImage); + }); }); }); describe('003/smartCrop', function() { @@ -169,12 +167,12 @@ describe('applyEdits()', function() { const rekognition = require('aws-sdk/clients/rekognition'); const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); detectFaces.returns({ - promise: () => { return { + promise: () => { return { FaceDetails: [{ BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, + Height: 0.18, + Left: 0.55, + Top: 0.33, Width: 0.23 } }] @@ -201,7 +199,7 @@ describe('applyEdits()', function() { }); }); describe('004/smartCrop/paddingOutOfBoundsError', function() { - it(`Should pass if an excessive padding value is passed to the + it(`Should pass if an excessive padding value is passed to the smartCrop filter`, async function() { // Arrange const sinon = require('sinon'); @@ -209,12 +207,12 @@ describe('applyEdits()', function() { const rekognition = require('aws-sdk/clients/rekognition'); const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); detectFaces.returns({ - promise: () => { return { + promise: () => { return { FaceDetails: [{ BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, + Height: 0.18, + Left: 0.55, + Top: 0.33, Width: 0.23 } }] @@ -241,7 +239,7 @@ describe('applyEdits()', function() { }); }); describe('005/smartCrop/boundingBoxError', function() { - it(`Should pass if an excessive faceIndex value is passed to the + it(`Should pass if an excessive faceIndex value is passed to the smartCrop filter`, async function() { // Arrange const sinon = require('sinon'); @@ -249,12 +247,12 @@ describe('applyEdits()', function() { const rekognition = require('aws-sdk/clients/rekognition'); const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); detectFaces.returns({ - promise: () => { return { + promise: () => { return { FaceDetails: [{ BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, + Height: 0.18, + Left: 0.55, + Top: 0.33, Width: 0.23 } }] @@ -281,7 +279,7 @@ describe('applyEdits()', function() { }); }); describe('006/smartCrop/faceIndexUndefined', function() { - it(`Should pass if a faceIndex value of undefined is passed to the + it(`Should pass if a faceIndex value of undefined is passed to the smartCrop filter`, async function() { // Arrange const sinon = require('sinon'); @@ -289,12 +287,12 @@ describe('applyEdits()', function() { const rekognition = require('aws-sdk/clients/rekognition'); const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); detectFaces.returns({ - promise: () => { return { + promise: () => { return { FaceDetails: [{ BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, + Height: 0.18, + Left: 0.55, + Top: 0.33, Width: 0.23 } }] @@ -324,7 +322,7 @@ describe('applyEdits()', function() { // ---------------------------------------------------------------------------- describe('getOverlayImage()', function() { describe('001/validParameters', function() { - it(`Should pass if the proper bucket name and key are supplied, + it(`Should pass if the proper bucket name and key are supplied, simulating an image file that can be retrieved`, async function() { // Arrange const S3 = require('aws-sdk/clients/s3'); @@ -343,18 +341,18 @@ describe('getOverlayImage()', function() { }); }); describe('002/imageDoesNotExist', async function() { - it(`Should throw an error if an invalid bucket or key name is provided, + it(`Should throw an error if an invalid bucket or key name is provided, simulating a non-existant overlay image`, async function() { // Arrange const S3 = require('aws-sdk/clients/s3'); const sinon = require('sinon'); const getObject = S3.prototype.getObject = sinon.stub(); getObject.withArgs({Bucket: 'invalidBucket', Key: 'invalidKey'}).returns({ - promise: () => { + promise: () => { return Promise.reject({ code: 500, message: 'SimulatedInvalidParameterException' - }) + }) } }); // Act @@ -374,13 +372,13 @@ describe('getOverlayImage()', function() { // ---------------------------------------------------------------------------- describe('getCropArea()', function() { describe('001/validParameters', function() { - it(`Should pass if the crop area can be calculated using a series of + it(`Should pass if the crop area can be calculated using a series of valid inputs/parameters`, function() { // Arrange const boundingBox = { - Height: 0.18, - Left: 0.55, - Top: 0.33, + Height: 0.18, + Left: 0.55, + Top: 0.33, Width: 0.23 }; const options = { padding: 20 }; @@ -395,7 +393,7 @@ describe('getCropArea()', function() { const expectedResult = { left: 90, top: 112, - width: 86, + width: 86, height: 112 } assert.deepEqual(result, expectedResult); @@ -409,21 +407,21 @@ describe('getCropArea()', function() { // ---------------------------------------------------------------------------- describe('getBoundingBox()', function() { describe('001/validParameters', function() { - it(`Should pass if the proper parameters are passed to the function`, + it(`Should pass if the proper parameters are passed to the function`, async function() { // Arrange const sinon = require('sinon'); const rekognition = require('aws-sdk/clients/rekognition'); const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); - // ---- + // ---- const imageBytes = Buffer.from('TestImageData'); detectFaces.withArgs({Image: {Bytes: imageBytes}}).returns({ - promise: () => { return { + promise: () => { return { FaceDetails: [{ BoundingBox: { - Height: 0.18, - Left: 0.55, - Top: 0.33, + Height: 0.18, + Left: 0.55, + Top: 0.33, Width: 0.23 } }] @@ -437,27 +435,27 @@ describe('getBoundingBox()', function() { const result = await imageHandler.getBoundingBox(currentImage, faceIndex); // Assert const expectedResult = { - Height: 0.18, - Left: 0.55, - Top: 0.33, + Height: 0.18, + Left: 0.55, + Top: 0.33, Width: 0.23 }; assert.deepEqual(result, expectedResult); }); }); describe('002/errorHandling', function() { - it(`Should simulate an error condition returned by Rekognition`, + it(`Should simulate an error condition returned by Rekognition`, async function() { // Arrange const rekognition = require('aws-sdk/clients/rekognition'); const sinon = require('sinon'); const detectFaces = rekognition.prototype.detectFaces = sinon.stub(); detectFaces.returns({ - promise: () => { + promise: () => { return Promise.reject({ code: 500, message: 'SimulatedError' - }) + }) } }) // ---- diff --git a/source/image-handler/test/test-image-request.js b/source/image-handler/test/test-image-request.js index 122da5552..f748e8df8 100644 --- a/source/image-handler/test/test-image-request.js +++ b/source/image-handler/test/test-image-request.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -112,7 +112,7 @@ describe('setup()', function() { requestType: 'Custom', bucket: 'allowedBucket001', key: 'custom-image.jpg', - edits: { + edits: { grayscale: true, rotate: 90 }, @@ -154,7 +154,7 @@ describe('setup()', function() { // ---------------------------------------------------------------------------- describe('getOriginalImage()', function() { describe('001/imageExists', function() { - it(`Should pass if the proper bucket name and key are supplied, + it(`Should pass if the proper bucket name and key are supplied, simulating an image file that can be retrieved`, async function() { // Arrange const S3 = require('aws-sdk/clients/s3'); @@ -173,18 +173,18 @@ describe('getOriginalImage()', function() { }); }); describe('002/imageDoesNotExist', async function() { - it(`Should throw an error if an invalid bucket or key name is provided, + it(`Should throw an error if an invalid bucket or key name is provided, simulating a non-existant original image`, async function() { // Arrange const S3 = require('aws-sdk/clients/s3'); const sinon = require('sinon'); const getObject = S3.prototype.getObject = sinon.stub(); getObject.withArgs({Bucket: 'invalidBucket', Key: 'invalidKey'}).returns({ - promise: () => { + promise: () => { return Promise.reject({ code: 500, message: 'SimulatedInvalidParameterException' - }) + }) } }); // Act @@ -245,7 +245,7 @@ describe('parseImageBucket()', function() { }); describe('003/defaultRequestType/bucketNotSpecifiedInRequest', function() { it(`Should pass if the image request does not contain a source bucket - but SOURCE_BUCKETS contains at least one bucket that can be + but SOURCE_BUCKETS contains at least one bucket that can be used as a default`, function() { // Arrange const event = { @@ -263,7 +263,7 @@ describe('parseImageBucket()', function() { }); }); describe('004/thumborRequestType', function() { - it(`Should pass if there is at least one SOURCE_BUCKET specified that can + it(`Should pass if there is at least one SOURCE_BUCKET specified that can be used as the default for Thumbor requests`, function() { // Arrange const event = { @@ -281,7 +281,7 @@ describe('parseImageBucket()', function() { }); }); describe('005/customRequestType', function() { - it(`Should pass if there is at least one SOURCE_BUCKET specified that can + it(`Should pass if there is at least one SOURCE_BUCKET specified that can be used as the default for Custom requests`, function() { // Arrange const event = { @@ -299,7 +299,7 @@ describe('parseImageBucket()', function() { }); }); describe('006/invalidRequestType', function() { - it(`Should pass if there is at least one SOURCE_BUCKET specified that can + it(`Should pass if there is at least one SOURCE_BUCKET specified that can be used as the default for Custom requests`, function() { // Arrange const event = { @@ -346,7 +346,7 @@ describe('parseImageEdits()', function() { }); }); describe('002/thumborRequestType', function() { - it(`Should pass if the proper result is returned for a sample thumbor- + it(`Should pass if the proper result is returned for a sample thumbor- type image request`, function() { // Arrange const event = { @@ -364,7 +364,7 @@ describe('parseImageEdits()', function() { }); }); describe('003/customRequestType', function() { - it(`Should pass if the proper result is returned for a sample custom- + it(`Should pass if the proper result is returned for a sample custom- type image request`, function() { // Arrange const event = { @@ -553,7 +553,7 @@ describe('parseRequestType()', function() { // ---------------------------------------------------------------------------- describe('decodeRequest()', function() { describe('001/validRequestPathSpecified', function() { - it(`Should pass if a valid base64-encoded path has been specified`, + it(`Should pass if a valid base64-encoded path has been specified`, function() { // Arrange const event = { @@ -571,7 +571,7 @@ describe('decodeRequest()', function() { }); }); describe('002/invalidRequestPathSpecified', function() { - it(`Should throw an error if a valid base64-encoded path has not been specified`, + it(`Should throw an error if a valid base64-encoded path has not been specified`, function() { // Arrange const event = { @@ -590,7 +590,7 @@ describe('decodeRequest()', function() { }); }); describe('003/noPathSpecified', function() { - it(`Should throw an error if no path is specified at all`, + it(`Should throw an error if no path is specified at all`, function() { // Arrange const event = {} diff --git a/source/image-handler/test/test-thumbor-mapping.js b/source/image-handler/test/test-thumbor-mapping.js index 0968fd3ea..d12adc6a1 100644 --- a/source/image-handler/test/test-thumbor-mapping.js +++ b/source/image-handler/test/test-thumbor-mapping.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -19,7 +19,7 @@ let assert = require('assert'); // ---------------------------------------------------------------------------- describe('process()', function() { describe('001/thumborRequest', function() { - it(`Should pass if the proper edit translations are applied and in the + it(`Should pass if the proper edit translations are applied and in the correct order`, function() { // Arrange const event = { @@ -30,7 +30,7 @@ describe('process()', function() { thumborMapping.process(event); // Assert const expectedResult = { - edits: { + edits: { resize: { width: 200, height: 300 @@ -48,7 +48,7 @@ describe('process()', function() { // ---------------------------------------------------------------------------- describe('parseCustomPath()', function() { describe('001/validPath', function() { - it(`Should pass if the proper edit translations are applied and in the + it(`Should pass if the proper edit translations are applied and in the correct order`, function() { const event = { path : '/filters-rotate(90)/filters-grayscale()/thumbor-image.jpg' @@ -111,7 +111,7 @@ describe('parseCustomPath()', function() { // ---------------------------------------------------------------------------- describe('mapFilter()', function() { describe('001/autojpg', function() { - it(`Should pass if the filter is successfully converted from + it(`Should pass if the filter is successfully converted from Thumbor:autojpg()`, function() { // Arrange const edit = 'filters:autojpg()'; @@ -127,7 +127,7 @@ describe('mapFilter()', function() { }); }); describe('002/background_color', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:background_color()`, function() { // Arrange const edit = 'filters:background_color(#ffff)'; @@ -143,7 +143,7 @@ describe('mapFilter()', function() { }); }); describe('003/blur/singleParameter', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:blur()`, function() { // Arrange const edit = 'filters:blur(60)'; @@ -159,7 +159,7 @@ describe('mapFilter()', function() { }); }); describe('004/blur/doubleParameter', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:blur()`, function() { // Arrange const edit = 'filters:blur(60, 2)'; @@ -175,7 +175,7 @@ describe('mapFilter()', function() { }); }); describe('005/convolution', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:convolution()`, function() { // Arrange const edit = 'filters:convolution(1;2;1;2;4;2;1;2;1,3,true)'; @@ -187,7 +187,7 @@ describe('mapFilter()', function() { const expectedResult = { edits: { convolve: { width: 3, - height: 3, + height: 3, kernel: [1,2,1,2,4,2,1,2,1] }} }; @@ -195,7 +195,7 @@ describe('mapFilter()', function() { }); }); describe('006/equalize', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:equalize()`, function() { // Arrange const edit = 'filters:equalize()'; @@ -211,7 +211,7 @@ describe('mapFilter()', function() { }); }); describe('007/fill/resizeUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:fill()`, function() { // Arrange const edit = 'filters:fill(#fff)'; @@ -228,7 +228,7 @@ describe('mapFilter()', function() { }); describe('008/fill/resizeDefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:fill()`, function() { // Arrange const edit = 'filters:fill(#fff)'; @@ -245,7 +245,7 @@ describe('mapFilter()', function() { }); }); describe('009/format/supportedFileType', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:format()`, function() { // Arrange const edit = 'filters:format(png)'; @@ -277,7 +277,7 @@ describe('mapFilter()', function() { }); }); describe('011/no_upscale/resizeUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:no_upscale()`, function() { // Arrange const edit = 'filters:no_upscale()'; @@ -287,7 +287,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'inside', height: undefined, @@ -299,7 +299,7 @@ describe('mapFilter()', function() { }); }); describe('012/no_upscale/resizeDefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:no_upscale()`, function() { // Arrange const edit = 'filters:no_upscale()'; @@ -310,7 +310,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'inside', height: undefined, @@ -322,7 +322,7 @@ describe('mapFilter()', function() { }); }); describe('013/proportion/resizeDefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:proportion()`, function() { // Arrange const edit = 'filters:proportion(0.3)'; @@ -338,7 +338,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { height: 60, width: 60 @@ -349,7 +349,7 @@ describe('mapFilter()', function() { }); }); describe('014/proportion/resizeUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:resize()`, function() { // Arrange const edit = 'filters:proportion(0.3)'; @@ -364,7 +364,7 @@ describe('mapFilter()', function() { }); }); describe('015/quality/jpg', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:quality()`, function() { // Arrange const edit = 'filters:quality(50)'; @@ -374,7 +374,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { jpeg: { quality: 50 } @@ -384,7 +384,7 @@ describe('mapFilter()', function() { }); }); describe('016/quality/png', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:quality()`, function() { // Arrange const edit = 'filters:quality(50)'; @@ -394,7 +394,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { png: { quality: 50 } @@ -404,7 +404,7 @@ describe('mapFilter()', function() { }); }); describe('017/quality/webp', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:quality()`, function() { // Arrange const edit = 'filters:quality(50)'; @@ -414,7 +414,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { webp: { quality: 50 } @@ -424,7 +424,7 @@ describe('mapFilter()', function() { }); }); describe('018/quality/tiff', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:quality()`, function() { // Arrange const edit = 'filters:quality(50)'; @@ -434,7 +434,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { tiff: { quality: 50 } @@ -460,7 +460,7 @@ describe('mapFilter()', function() { }); }); describe('020/rgb', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:rgb()`, function() { // Arrange const edit = 'filters:rgb(10, 10, 10)'; @@ -470,7 +470,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { tint: { r: 25.5, g: 25.5, @@ -482,7 +482,7 @@ describe('mapFilter()', function() { }); }); describe('021/rotate', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:rotate()`, function() { // Arrange const edit = 'filters:rotate(75)'; @@ -492,7 +492,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { rotate: 75 } }; @@ -500,7 +500,7 @@ describe('mapFilter()', function() { }); }); describe('022/sharpen', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:sharpen()`, function() { // Arrange const edit = 'filters:sharpen(75, 5)'; @@ -510,7 +510,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { sharpen: 3.5 } }; @@ -518,7 +518,7 @@ describe('mapFilter()', function() { }); }); describe('023/stretch/default', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -528,7 +528,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'fill' } } }; @@ -536,7 +536,7 @@ describe('mapFilter()', function() { }); }); describe('024/stretch/resizeDefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -547,7 +547,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'fill' } } }; @@ -555,7 +555,7 @@ describe('mapFilter()', function() { }); }); describe('025/stretch/sizingMethodUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -567,7 +567,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'fill' } }, sizingMethod: undefined @@ -576,7 +576,7 @@ describe('mapFilter()', function() { }); }); describe('026/stretch/sizingMethodNotFitIn', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -588,7 +588,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'fill' } }, sizingMethod: "cover" @@ -597,7 +597,7 @@ describe('mapFilter()', function() { }); }); describe('027/stretch/sizingMethodFitIn', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:stretch()`, function() { // Arrange const edit = 'filters:stretch()'; @@ -609,7 +609,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: {} }, sizingMethod: "fit-in" @@ -618,7 +618,7 @@ describe('mapFilter()', function() { }); }); describe('028/strip_exif', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:strip_exif()`, function() { // Arrange const edit = 'filters:strip_exif()'; @@ -628,7 +628,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { rotate: 0 } }; @@ -636,7 +636,7 @@ describe('mapFilter()', function() { }); }); describe('029/strip_icc', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:strip_icc()`, function() { // Arrange const edit = 'filters:strip_icc()'; @@ -646,7 +646,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { rotate: 0 } }; @@ -654,7 +654,7 @@ describe('mapFilter()', function() { }); }); describe('030/upscale', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:upscale()`, function() { // Arrange const edit = 'filters:upscale()'; @@ -664,7 +664,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'inside' } @@ -674,7 +674,7 @@ describe('mapFilter()', function() { }); }); describe('031/upscale/resizeNotUndefined', function() { - it(`Should pass if the filter is successfully translated from + it(`Should pass if the filter is successfully translated from Thumbor:upscale()`, function() { // Arrange const edit = 'filters:upscale()'; @@ -685,7 +685,7 @@ describe('mapFilter()', function() { thumborMapping.mapFilter(edit, filetype); // Assert const expectedResult = { - edits: { + edits: { resize: { fit: 'inside' } @@ -695,7 +695,7 @@ describe('mapFilter()', function() { }); }); describe('032/elseCondition', function() { - it(`Should pass if undefined is returned for an unsupported filter`, + it(`Should pass if undefined is returned for an unsupported filter`, function() { // Arrange const edit = 'filters:notSupportedFilter()'; diff --git a/source/image-handler/thumbor-mapping.js b/source/image-handler/thumbor-mapping.js index dc9e8a76c..17b5bcfd2 100644 --- a/source/image-handler/thumbor-mapping.js +++ b/source/image-handler/thumbor-mapping.js @@ -1,12 +1,12 @@ /********************************************************************************************************************* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * - * Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance * * with the License. A copy of the License is located at * * * - * http://aws.amazon.com/asl/ * + * http://www.apache.org/licenses/LICENSE-2.0 * * * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES * * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions * * and limitations under the License. * *********************************************************************************************************************/ @@ -18,7 +18,7 @@ class ThumborMapping { this.edits = {}; this.sizingMethod; } - + /** * Initializer function for creating a new Thumbor mapping, used by the image * handler to perform image modifications based on legacy URL path requests. @@ -35,20 +35,20 @@ class ThumborMapping { if (edit === ('fit-in')) { this.edits.resize = {}; this.sizingMethod = edit; - } + } else if (edit.includes('x')) { this.edits.resize = {}; const dims = edit.split('x'); this.edits.resize.width = Number(dims[0]); this.edits.resize.height = Number(dims[1]); - } + } if (edit.includes('filters:')) { this.mapFilter(edit, filetype); } } return this; } - + /** * Enables users to migrate their current image request model to the SIH solution, * without changing their legacy application code to accomodate new image requests. @@ -81,14 +81,14 @@ class ThumborMapping { // Find the proper filter if (key === ('autojpg')) { this.edits.toFormat = 'jpg'; - } + } else if (key === ('background_color')) { this.edits.flatten = { background: value }; - } + } else if (key === ('blur')) { const val = value.split(','); this.edits.blur = (val.length > 1) ? Number(val[1]) : Number(val[0]) / 2; - } + } else if (key === ('convolution')) { const arr = value.split(','); const strMatrix = (arr[0]).split(';'); @@ -112,26 +112,26 @@ class ThumborMapping { height: Number(matrixHeight), kernel: matrix } - } + } else if (key === ('equalize')) { this.edits.normalize = "true"; - } + } else if (key === ('fill')) { if (this.edits.resize === undefined) { this.edits.resize = {}; } this.edits.resize.background = value; - } + } else if (key === ('format')) { const formattedValue = value.replace(/[^0-9a-z]/gi, ''); const acceptedValues = ['jpeg', 'gif', 'jpg', 'webp', 'png']; if (acceptedValues.includes(formattedValue)) { this.edits.toFormat = formattedValue; } - } + } else if (key === ('grayscale')) { this.edits.grayscale = true; - } + } else if (key === ('no_upscale')) { if (this.edits.resize === undefined) { this.edits.resize = {}; @@ -139,7 +139,7 @@ class ThumborMapping { this.edits.resize.fit = "inside" this.edits.resize.width = undefined; this.edits.resize.height = undefined; - } + } else if (key === ('proportion')) { if (this.edits.resize === undefined) { this.edits.resize = {}; @@ -147,7 +147,7 @@ class ThumborMapping { const prop = Number(value); this.edits.resize.width = Number(this.edits.resize.width * prop); this.edits.resize.height = Number(this.edits.resize.height * prop); - } + } else if (key === ('quality')) { if (filetype === 'jpg') { this.edits.jpeg = { quality: Number(value) } @@ -158,7 +158,7 @@ class ThumborMapping { } else if (filetype === 'tiff') { this.edits.tiff = { quality: Number(value) } } - } + } else if (key === ('rgb')) { const percentages = value.split(','); const values = []; @@ -170,33 +170,33 @@ class ThumborMapping { this.edits.tint = { r: values[0], g: values[1], b: values[2] }; } else if (key === ('rotate')) { - this.edits.rotate = Number(value); - } + this.edits.rotate = Number(value); + } else if (key === ('sharpen')) { const sh = value.split(','); const sigma = 1 + Number(sh[1]) / 2; - this.edits.sharpen = sigma; - } + this.edits.sharpen = sigma; + } else if (key === ('stretch')) { if (this.edits.resize === undefined) { this.edits.resize = {}; } if (this.sizingMethod === undefined || this.sizingMethod !== 'fit-in') { this.edits.resize.fit = "fill"; - } - } + } + } else if (key === ('strip_exif')) { this.edits.rotate = 0; - } + } else if (key === ('strip_icc')) { this.edits.rotate = 0; - } + } else if (key === ('upscale')) { if (this.edits.resize === undefined) { this.edits.resize = {}; } this.edits.resize.fit = "inside" - } + } else { return undefined; }