-
Notifications
You must be signed in to change notification settings - Fork 538
/
image-request.js
238 lines (227 loc) · 11 KB
/
image-request.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/*********************************************************************************************************************
* 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 ThumborMapping = require('./thumbor-mapping');
class ImageRequest {
/**
* Initializer function for creating a new image request, used by the image
* handler to perform image modifications.
* @param {Object} event - Lambda request body.
*/
async setup(event) {
try {
this.requestType = this.parseRequestType(event);
this.bucket = this.parseImageBucket(event, this.requestType);
this.key = this.parseImageKey(event, this.requestType);
this.edits = this.parseImageEdits(event, this.requestType);
this.originalImage = await this.getOriginalImage(this.bucket, this.key)
return Promise.resolve(this);
} catch (err) {
return Promise.reject(err);
}
}
/**
* Gets the original image from an Amazon S3 bucket.
* @param {String} bucket - The name of the bucket containing the image.
* @param {String} key - The key name corresponding to the image.
* @return {Promise} - The original image or an error.
*/
async getOriginalImage(bucket, key) {
const S3 = require('aws-sdk/clients/s3');
const s3 = new S3();
const imageLocation = { Bucket: bucket, Key: key };
const request = s3.getObject(imageLocation).promise();
try {
const originalImage = await request;
return Promise.resolve(originalImage.Body);
}
catch(err) {
return Promise.reject({
status: 500,
code: err.code,
message: err.message
})
}
}
/**
* Parses the name of the appropriate Amazon S3 bucket to source the
* original image from.
* @param {String} event - Lambda request body.
* @param {String} requestType - Image handler request type.
*/
parseImageBucket(event, requestType) {
if (requestType === "Default") {
// Decode the image request
const decoded = this.decodeRequest(event);
if (decoded.bucket !== undefined) {
// Check the provided bucket against the whitelist
const sourceBuckets = this.getAllowedSourceBuckets();
if (sourceBuckets.includes(decoded.bucket)) {
return decoded.bucket;
} else {
throw ({
status: 403,
code: 'ImageBucket::CannotAccessBucket',
message: 'The bucket you specified could not be accessed. Please check that the bucket is specified in your SOURCE_BUCKETS.'
});
}
} else {
// Try to use the default image source bucket env var
const sourceBuckets = this.getAllowedSourceBuckets();
return sourceBuckets[0];
}
} else if (requestType === "Thumbor" || requestType === "Custom") {
// Use the default image source bucket env var
const sourceBuckets = this.getAllowedSourceBuckets();
return sourceBuckets[0];
} else {
throw ({
status: 400,
code: 'ImageBucket::CannotFindBucket',
message: 'The bucket you specified could not be found. Please check the spelling of the bucket name in your request.'
});
}
}
/**
* Parses the edits to be made to the original image.
* @param {String} event - Lambda request body.
* @param {String} requestType - Image handler request type.
*/
parseImageEdits(event, requestType) {
if (requestType === "Default") {
const decoded = this.decodeRequest(event);
return decoded.edits;
} else if (requestType === "Thumbor") {
const thumborMapping = new ThumborMapping();
thumborMapping.process(event);
return thumborMapping.edits;
} else if (requestType === "Custom") {
const thumborMapping = new ThumborMapping();
const parsedPath = thumborMapping.parseCustomPath(event.path);
thumborMapping.process(parsedPath);
return thumborMapping.edits;
} else {
throw ({
status: 400,
code: 'ImageEdits::CannotParseEdits',
message: 'The edits you provided could not be parsed. Please check the syntax of your request and refer to the documentation for additional guidance.'
});
}
}
/**
* Parses the name of the appropriate Amazon S3 key corresponding to the
* original image.
* @param {String} event - Lambda request body.
* @param {String} requestType - Type, either "Default", "Thumbor", or "Custom".
*/
parseImageKey(event, requestType) {
if (requestType === "Default") {
// Decode the image request and return the image key
const decoded = this.decodeRequest(event);
return decoded.key;
} else if (requestType === "Thumbor" || requestType === "Custom") {
// Parse the key from the end of the path
const key = (event["path"]).split("/");
return key[key.length - 1];
} else {
// Return an error for all other conditions
throw ({
status: 400,
code: 'ImageEdits::CannotFindImage',
message: 'The image you specified could not be found. Please check your request syntax as well as the bucket you specified to ensure it exists.'
});
}
}
/**
* Determines how to handle the request being made based on the URL path
* 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.
*/
parseRequestType(event) {
const path = event["path"];
// ----
const matchDefault = new RegExp(/^(\/?)([0-9a-zA-Z+\/]{4})*(([0-9a-zA-Z+\/]{2}==)|([0-9a-zA-Z+\/]{3}=))?$/);
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_SUBSTITUTION !== undefined)
);
// ----
if (matchDefault.test(path)) { // use sharp
return 'Default';
} else if (matchCustom.test(path) && definedEnvironmentVariables) { // use rewrite function then thumbor mappings
return 'Custom';
} else if (matchThumbor.test(path)) { // use thumbor mappings
return 'Thumbor';
} else {
throw {
status: 400,
code: 'RequestTypeError',
message: 'The type of request you are making could not be processed. Please ensure that your original image is of a supported file type (jpg, png, tiff, webp) and that your image request is provided in the correct syntax. Refer to the documentation for additional guidance on forming image requests.'
};
}
}
/**
* Decodes the base64-encoded image request path associated with default
* image requests. Provides error handling for invalid or undefined path values.
* @param {Object} event - The proxied request object.
*/
decodeRequest(event) {
const path = event["path"];
if (path !== undefined) {
const splitPath = path.split("/");
const encoded = splitPath[splitPath.length - 1];
const toBuffer = new Buffer(encoded, 'base64');
try {
return JSON.parse(toBuffer.toString('ascii'));
} catch (e) {
throw ({
status: 400,
code: 'DecodeRequest::CannotDecodeRequest',
message: 'The image request you provided could not be decoded. Please check that your request is base64 encoded properly and refer to the documentation for additional guidance.'
});
}
} else {
throw ({
status: 400,
code: 'DecodeRequest::CannotReadPath',
message: 'The URL path you provided could not be read. Please ensure that it is properly formed according to the solution documentation.'
});
}
}
/**
* 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.
*/
getAllowedSourceBuckets() {
const sourceBuckets = process.env.SOURCE_BUCKETS;
if (sourceBuckets === undefined) {
throw ({
status: 400,
code: 'GetAllowedSourceBuckets::NoSourceBuckets',
message: 'The SOURCE_BUCKETS variable could not be read. Please check that it is not empty and contains at least one source bucket, or multiple buckets separated by commas. Spaces can be provided between commas and bucket names, these will be automatically parsed out when decoding.'
});
} else {
const formatted = sourceBuckets.replace(/\s+/g, '');
const buckets = formatted.split(',');
return buckets;
}
}
}
// Exports
module.exports = ImageRequest;