Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions lib/api/objectGet.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ function objectGet(authInfo, request, returnTagCount, log, callback) {
const bucketName = request.bucketName;
const objectKey = request.objectKey;

const checksumMode = request.headers['x-amz-checksum-mode'];
if (checksumMode !== undefined && checksumMode !== 'ENABLED') {
log.debug('invalid x-amz-checksum-mode', { checksumMode });
return callback(errors.InvalidArgument);
}

// returns name of location to get from and key if successful
const locCheckResult =
locationHeaderCheck(request.headers, objectKey, bucketName);
Expand Down Expand Up @@ -145,6 +151,7 @@ function objectGet(authInfo, request, returnTagCount, log, callback) {
request.serverAccessLog.objectSize = objLength;
}
let byteRange;
let partNumber = null;
const streamingParams = {};
if (request.headers.range) {
const { range, error } = parseRange(request.headers.range,
Expand Down Expand Up @@ -211,8 +218,6 @@ function objectGet(authInfo, request, returnTagCount, log, callback) {
if (dataLocator[0] && dataLocator[0].dataStoreType === 'azure') {
dataLocator[0].azureStreamingOptions = streamingParams;
}

let partNumber = null;
if (request.query && request.query.partNumber !== undefined) {
if (byteRange) {
const error = errorInstances.InvalidRequest
Expand Down Expand Up @@ -301,6 +306,16 @@ function objectGet(authInfo, request, returnTagCount, log, callback) {
dataLocator = setPartRanges(dataLocator, byteRange);
}
}
// Checksum is not returned for partNumber requests because per-part
// checksums are not yet stored (S3C-11073).
if (checksumMode === 'ENABLED' && !byteRange && !partNumber) {
const checksum = objMD.checksum;
if (checksum) {
responseMetaHeaders[`x-amz-checksum-${checksum.checksumAlgorithm}`]
= checksum.checksumValue;
responseMetaHeaders['x-amz-checksum-type'] = checksum.checksumType;
}
}
// Check KMS Key access and usability before checking data
// diff with AWS: for empty object (no dataLocator) KMS not checked
return async.each(dataLocator || [],
Expand Down
19 changes: 18 additions & 1 deletion lib/api/objectHead.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ function objectHead(authInfo, request, log, callback) {
const bucketName = request.bucketName;
const objectKey = request.objectKey;

const checksumMode = request.headers['x-amz-checksum-mode'];
if (checksumMode !== undefined && checksumMode !== 'ENABLED') {
log.debug('invalid x-amz-checksum-mode', { checksumMode });
return callback(errors.InvalidArgument);
}

const decodedVidResult = decodeVersionId(request.query);
if (decodedVidResult instanceof Error) {
log.trace('invalid versionId query', {
Expand Down Expand Up @@ -97,6 +103,18 @@ function objectHead(authInfo, request, log, callback) {
const responseHeaders = collectResponseHeaders(objMD, corsHeaders,
verCfg);

const partNumber = getPartNumber(request.query);
// Checksum is not returned for partNumber requests because per-part
// checksums are not yet stored (S3C-11073).
if (checksumMode === 'ENABLED' && !partNumber) {
const checksum = objMD.checksum;
if (checksum) {
responseHeaders[`x-amz-checksum-${checksum.checksumAlgorithm}`]
= checksum.checksumValue;
responseHeaders['x-amz-checksum-type'] = checksum.checksumType;
}
}

setExpirationHeaders(responseHeaders, {
lifecycleConfig: bucket.getLifecycleConfiguration(),
objectParams: {
Expand Down Expand Up @@ -135,7 +153,6 @@ function objectHead(authInfo, request, log, callback) {
`bytes ${range[0]}-${range[1]}/${objLength}`;
}
}
const partNumber = getPartNumber(request.query);
if (partNumber !== undefined) {
if (byteRange) {
const error = errorInstances.InvalidRequest
Expand Down
106 changes: 106 additions & 0 deletions tests/functional/aws-node-sdk/test/object/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ const {
const withV4 = require('../support/withV4');
const BucketUtility = require('../../lib/utility/bucket-util');
const changeObjectLock = require('../../../../utilities/objectLock-util');
const { algorithms } = require('../../../../../lib/api/apiUtils/integrity/validateChecksums');

const { crc64NvmeCrtContainer } = require('@aws-sdk/middleware-flexible-checksums');
if (crc64NvmeCrtContainer) {
const { CrtCrc64Nvme } = require('@aws-sdk/crc64-nvme-crt');
crc64NvmeCrtContainer.CrtCrc64Nvme = CrtCrc64Nvme;
}

const changeLockPromise = promisify(changeObjectLock);

Expand Down Expand Up @@ -1121,3 +1128,102 @@ describeSkipIfCeph('GET object with object lock', () => {
});
});
});

describe('GET object checksum mode', () => {
withV4(sigCfg => {
let bucketUtil;
let s3;
const checksumBucket = 'checksum-getobject-test';
const checksumKey = 'checksum-test-object';
const body = Buffer.from('checksum test body');

const expectedDigests = {};

const checksumAlgorithms = [
{ algorithm: 'SHA256', responseField: 'ChecksumSHA256', internalName: 'sha256' },
{ algorithm: 'SHA1', responseField: 'ChecksumSHA1', internalName: 'sha1' },
{ algorithm: 'CRC32', responseField: 'ChecksumCRC32', internalName: 'crc32' },
{ algorithm: 'CRC32C', responseField: 'ChecksumCRC32C', internalName: 'crc32c' },
{ algorithm: 'CRC64NVME', responseField: 'ChecksumCRC64NVME', internalName: 'crc64nvme' },
];

before(async () => {
// Disable automatic response checksum validation so the SDK does
// not silently add x-amz-checksum-mode: ENABLED to every GetObject
// request, which would interfere with the "mode not set" test.
bucketUtil = new BucketUtility('default',
{ ...sigCfg, responseChecksumValidation: 'WHEN_REQUIRED' });
s3 = bucketUtil.s3;
await s3.send(new CreateBucketCommand({ Bucket: checksumBucket }));

for (const { internalName } of checksumAlgorithms) {
expectedDigests[internalName] =
await algorithms[internalName].digest(body);
}
});

after(async () => {
await bucketUtil.empty(checksumBucket);
await s3.send(new DeleteBucketCommand({ Bucket: checksumBucket }));
});

checksumAlgorithms.forEach(({ algorithm, responseField, internalName }) => {
it(`should return ${responseField} and ChecksumType when ChecksumMode is ENABLED`, async () => {
const putRes = await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: body,
ChecksumAlgorithm: algorithm,
}));
const storedChecksum = putRes[responseField];
assert(storedChecksum, `Expected ${responseField} in PutObject response`);

const getRes = await s3.send(new GetObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
ChecksumMode: 'ENABLED',
}));
assert.strictEqual(getRes[responseField], expectedDigests[internalName],
`${responseField} value mismatch`);
assert.strictEqual(getRes[responseField], storedChecksum);
assert.strictEqual(getRes.ChecksumType, 'FULL_OBJECT');
});
});

it('should not return checksum headers when ChecksumMode is not set', async () => {
await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: body,
ChecksumAlgorithm: 'SHA256',
}));

const getRes = await s3.send(new GetObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
}));
assert.strictEqual(getRes.ChecksumSHA256, undefined);
assert.strictEqual(getRes.ChecksumType, undefined);
});

it('should return an error when ChecksumMode is not ENABLED', async () => {
await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: body,
}));

await assert.rejects(
s3.send(new GetObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
ChecksumMode: 'DISABLED',
})),
err => {
assert.strictEqual(err.name, 'InvalidArgument');
return true;
},
);
});
});
});
104 changes: 104 additions & 0 deletions tests/functional/aws-node-sdk/test/object/objectHead.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@ const {
UploadPartCommand,
CompleteMultipartUploadCommand,
} = require('@aws-sdk/client-s3');
// In older versions of @aws-sdk/middleware-flexible-checksums (<3.972), the
// CRC64NVME implementation must be patched in manually from the CRT package.
// Newer versions handle this internally via @aws-sdk/crc64-nvme, so the export
// no longer exists and no registration is needed.
const { crc64NvmeCrtContainer } = require('@aws-sdk/middleware-flexible-checksums');
if (crc64NvmeCrtContainer) {
const { CrtCrc64Nvme } = require('@aws-sdk/crc64-nvme-crt');
crc64NvmeCrtContainer.CrtCrc64Nvme = CrtCrc64Nvme;
}

const { algorithms } = require('../../../../../lib/api/apiUtils/integrity/validateChecksums');
const changeObjectLock = require('../../../../utilities/objectLock-util');
const withV4 = require('../support/withV4');
const BucketUtility = require('../../lib/utility/bucket-util');
Expand Down Expand Up @@ -538,6 +548,100 @@ describe('HEAD object, conditions', () => {
});
});

describe('HEAD object checksum mode', () => {
withV4(sigCfg => {
let bucketUtil;
let s3;
const checksumBucket = 'checksum-headobject-test';
const checksumKey = 'checksum-test-object';
const body = Buffer.from('checksum test body');

const checksumAlgorithms = [
{ algorithm: 'SHA256', responseField: 'ChecksumSHA256', internalName: 'sha256' },
{ algorithm: 'SHA1', responseField: 'ChecksumSHA1', internalName: 'sha1' },
{ algorithm: 'CRC32', responseField: 'ChecksumCRC32', internalName: 'crc32' },
{ algorithm: 'CRC32C', responseField: 'ChecksumCRC32C', internalName: 'crc32c' },
{ algorithm: 'CRC64NVME', responseField: 'ChecksumCRC64NVME', internalName: 'crc64nvme' },
];

// Expected base64-encoded digests of `body` for each algorithm,
// computed once in the before hook (crc64nvme digest is async).
const expectedDigests = {};

before(async () => {
bucketUtil = new BucketUtility('default', sigCfg);
s3 = bucketUtil.s3;
await s3.send(new CreateBucketCommand({ Bucket: checksumBucket }));

for (const { internalName } of checksumAlgorithms) {
// algorithms[internalName].digest() returns a base64 string
expectedDigests[internalName] =
await algorithms[internalName].digest(body);
}
});

after(async () => {
await bucketUtil.empty(checksumBucket);
await s3.send(new DeleteBucketCommand({ Bucket: checksumBucket }));
});

checksumAlgorithms.forEach(({ algorithm, responseField, internalName }) => {
it(`should return ${responseField} and ChecksumType when ChecksumMode is ENABLED`, async () => {
const putRes = await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: body,
ChecksumAlgorithm: algorithm,
}));
const storedChecksum = putRes[responseField];
assert(storedChecksum, `Expected ${responseField} in PutObject response`);

const headRes = await s3.send(new HeadObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
ChecksumMode: 'ENABLED',
}));
assert.strictEqual(headRes[responseField], expectedDigests[internalName],
`${responseField} value mismatch`);
assert.strictEqual(headRes[responseField], storedChecksum);
assert.strictEqual(headRes.ChecksumType, 'FULL_OBJECT');
});
});

it('should not return checksum headers when ChecksumMode is not set', async () => {
await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: body,
ChecksumAlgorithm: 'SHA256',
}));

const headRes = await s3.send(new HeadObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
}));
assert.strictEqual(headRes.ChecksumSHA256, undefined);
assert.strictEqual(headRes.ChecksumType, undefined);
});

it('should return an error when ChecksumMode is not ENABLED', async () => {
await s3.send(new PutObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
Body: body,
}));

await assert.rejects(
s3.send(new HeadObjectCommand({
Bucket: checksumBucket,
Key: checksumKey,
ChecksumMode: 'DISABLED',
})),
);
});
});
});

const isCEPH = process.env.CI_CEPH !== undefined;
const describeSkipIfCeph = isCEPH ? describe.skip : describe;

Expand Down
Loading
Loading