Skip to content

Commit 247f335

Browse files
Use 'Expires' header to time-limit URLs
1 parent b6a4f88 commit 247f335

File tree

2 files changed

+66
-0
lines changed

2 files changed

+66
-0
lines changed

source/image-handler/image-request.ts

+26
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class ImageRequest {
4141
imageRequestInfo = { ...imageRequestInfo, ...originalImage };
4242

4343
imageRequestInfo.headers = this.parseImageHeaders(event, imageRequestInfo.requestType);
44+
this.validateRequestExpires(imageRequestInfo);
4445

4546
// If the original image is SVG file and it has any edits but no output format, change the format to WebP.
4647
if (imageRequestInfo.contentType === 'image/svg+xml' && imageRequestInfo.edits && Object.keys(imageRequestInfo.edits).length > 0 && !imageRequestInfo.edits.toFormat) {
@@ -443,4 +444,29 @@ export class ImageRequest {
443444
}
444445
}
445446
}
447+
448+
private validateRequestExpires(requestInfo: ImageRequestInfo): void {
449+
try {
450+
const expires = requestInfo.headers?.expires;
451+
if (expires !== undefined) {
452+
const parsedDate = new Date(expires);
453+
if (isNaN(parsedDate.getTime())) {
454+
throw new ImageHandlerError(StatusCodes.BAD_REQUEST, 'ImageRequestExpiryFormat', 'Request has invalid expiry date.');
455+
}
456+
const now = new Date();
457+
if (now > parsedDate) {
458+
throw new ImageHandlerError(StatusCodes.FORBIDDEN, 'ImageRequestExpired', 'Request has expired.');
459+
}
460+
}
461+
} catch (error) {
462+
if (error.code === 'ImageRequestExpired') {
463+
throw error;
464+
}
465+
if (error.code === 'ImageRequestExpiryFormat') {
466+
throw error;
467+
}
468+
console.error('Error occurred while checking expiry.', error);
469+
throw new ImageHandlerError(StatusCodes.INTERNAL_SERVER_ERROR, 'ExpiryDateCheckFailure', 'Expiry date check failed.');
470+
}
471+
}
446472
}

source/image-handler/test/image-request.spec.ts

+40
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,46 @@ describe('setup()', () => {
853853
expect(imageRequestInfo).toEqual(expectedResult);
854854
});
855855
});
856+
857+
describe('011/expiryDate', () => {
858+
it.each([
859+
{
860+
path: '/eyJidWNrZXQiOiJ0ZXN0IiwicmVxdWVzdFR5cGUiOiJEZWZhdWx0Iiwia2V5IjoidGVzdC5wbmciLCJoZWFkZXJzIjp7ImV4cGlyZXMiOiJUaHUsIDAxIEphbiAxOTcwIDAwOjAwOjAwIEdNVCJ9fQ==',
861+
error: {
862+
code: 'ImageRequestExpired',
863+
message: 'Request has expired.',
864+
status: StatusCodes.FORBIDDEN,
865+
},
866+
},
867+
{
868+
path: '/eyJidWNrZXQiOiJ0ZXN0IiwicmVxdWVzdFR5cGUiOiJEZWZhdWx0Iiwia2V5IjoidGVzdC5wbmciLCJoZWFkZXJzIjp7ImV4cGlyZXMiOiJpbnZhbGlkS2V5In19',
869+
error: {
870+
code: 'ImageRequestExpiryFormat',
871+
message: 'Request has invalid expiry date.',
872+
status: StatusCodes.BAD_REQUEST,
873+
}
874+
}
875+
])("Should throw an error when $error.message", (async ({ path, error: expectedError }) => {
876+
// Arrange
877+
const event = {
878+
path,
879+
};
880+
// Mock
881+
mockAwsS3.getObject.mockImplementationOnce(() => ({
882+
promise() {
883+
return Promise.resolve({ Body: Buffer.from('SampleImageContent\n') });
884+
}
885+
}));
886+
// Act
887+
const imageRequest = new ImageRequest(s3Client, secretProvider);
888+
try {
889+
await imageRequest.setup(event);
890+
} catch (error) {
891+
// Assert
892+
expect(error).toMatchObject(expectedError);
893+
}
894+
}));
895+
});
856896
});
857897

858898
describe('getOriginalImage()', () => {

0 commit comments

Comments
 (0)