Skip to content

Commit d9fe250

Browse files
committed
CLDSRV-873: return checksum in HeadObject
1 parent 8f9c848 commit d9fe250

File tree

4 files changed

+221
-0
lines changed

4 files changed

+221
-0
lines changed

lib/api/objectHead.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ function objectHead(authInfo, request, log, callback) {
3232
const bucketName = request.bucketName;
3333
const objectKey = request.objectKey;
3434

35+
const checksumMode = request.headers['x-amz-checksum-mode'];
36+
if (checksumMode !== undefined && checksumMode !== 'ENABLED') {
37+
log.debug('invalid x-amz-checksum-mode', { checksumMode });
38+
return callback(errors.InvalidArgument);
39+
}
40+
3541
const decodedVidResult = decodeVersionId(request.query);
3642
if (decodedVidResult instanceof Error) {
3743
log.trace('invalid versionId query', {
@@ -97,6 +103,15 @@ function objectHead(authInfo, request, log, callback) {
97103
const responseHeaders = collectResponseHeaders(objMD, corsHeaders,
98104
verCfg);
99105

106+
if (checksumMode === 'ENABLED') {
107+
const checksum = objMD.checksum;
108+
if (checksum) {
109+
responseHeaders[`x-amz-checksum-${checksum.checksumAlgorithm}`]
110+
= checksum.checksumValue;
111+
responseHeaders['x-amz-checksum-type'] = checksum.checksumType;
112+
}
113+
}
114+
100115
setExpirationHeaders(responseHeaders, {
101116
lifecycleConfig: bucket.getLifecycleConfiguration(),
102117
objectParams: {

tests/functional/aws-node-sdk/test/object/objectHead.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,17 @@ const {
1414
UploadPartCommand,
1515
CompleteMultipartUploadCommand,
1616
} = require('@aws-sdk/client-s3');
17+
// In older versions of @aws-sdk/middleware-flexible-checksums (<3.972), the
18+
// CRC64NVME implementation must be patched in manually from the CRT package.
19+
// Newer versions handle this internally via @aws-sdk/crc64-nvme, so the export
20+
// no longer exists and no registration is needed.
21+
const { crc64NvmeCrtContainer } = require('@aws-sdk/middleware-flexible-checksums');
22+
if (crc64NvmeCrtContainer) {
23+
const { CrtCrc64Nvme } = require('@aws-sdk/crc64-nvme-crt');
24+
crc64NvmeCrtContainer.CrtCrc64Nvme = CrtCrc64Nvme;
25+
}
1726

27+
const { algorithms } = require('../../../../../lib/api/apiUtils/integrity/validateChecksums');
1828
const changeObjectLock = require('../../../../utilities/objectLock-util');
1929
const withV4 = require('../support/withV4');
2030
const BucketUtility = require('../../lib/utility/bucket-util');
@@ -538,6 +548,100 @@ describe('HEAD object, conditions', () => {
538548
});
539549
});
540550

551+
describe('HEAD object checksum mode', () => {
552+
withV4(sigCfg => {
553+
let bucketUtil;
554+
let s3;
555+
const checksumBucket = 'checksum-headobject-test';
556+
const checksumKey = 'checksum-test-object';
557+
const body = Buffer.from('checksum test body');
558+
559+
// Expected base64-encoded digests of `body` for each algorithm,
560+
// computed once in the before hook (crc64nvme digest is async).
561+
const expectedDigests = {};
562+
563+
before(async () => {
564+
bucketUtil = new BucketUtility('default', sigCfg);
565+
s3 = bucketUtil.s3;
566+
await s3.send(new CreateBucketCommand({ Bucket: checksumBucket }));
567+
568+
for (const { internalName } of checksumAlgorithms) {
569+
// algorithms[internalName].digest() returns a base64 string
570+
expectedDigests[internalName] =
571+
await algorithms[internalName].digest(body);
572+
}
573+
});
574+
575+
after(async () => {
576+
await bucketUtil.empty(checksumBucket);
577+
await s3.send(new DeleteBucketCommand({ Bucket: checksumBucket }));
578+
});
579+
580+
const checksumAlgorithms = [
581+
{ algorithm: 'SHA256', responseField: 'ChecksumSHA256', internalName: 'sha256' },
582+
{ algorithm: 'SHA1', responseField: 'ChecksumSHA1', internalName: 'sha1' },
583+
{ algorithm: 'CRC32', responseField: 'ChecksumCRC32', internalName: 'crc32' },
584+
{ algorithm: 'CRC32C', responseField: 'ChecksumCRC32C', internalName: 'crc32c' },
585+
{ algorithm: 'CRC64NVME', responseField: 'ChecksumCRC64NVME', internalName: 'crc64nvme' },
586+
];
587+
588+
checksumAlgorithms.forEach(({ algorithm, responseField, internalName }) => {
589+
it(`should return ${responseField} and ChecksumType when ChecksumMode is ENABLED`, async () => {
590+
const putRes = await s3.send(new PutObjectCommand({
591+
Bucket: checksumBucket,
592+
Key: checksumKey,
593+
Body: body,
594+
ChecksumAlgorithm: algorithm,
595+
}));
596+
const storedChecksum = putRes[responseField];
597+
assert(storedChecksum, `Expected ${responseField} in PutObject response`);
598+
599+
const headRes = await s3.send(new HeadObjectCommand({
600+
Bucket: checksumBucket,
601+
Key: checksumKey,
602+
ChecksumMode: 'ENABLED',
603+
}));
604+
assert.strictEqual(headRes[responseField], expectedDigests[internalName],
605+
`${responseField} value mismatch`);
606+
assert.strictEqual(headRes[responseField], storedChecksum);
607+
assert.strictEqual(headRes.ChecksumType, 'FULL_OBJECT');
608+
});
609+
});
610+
611+
it('should not return checksum headers when ChecksumMode is not set', async () => {
612+
await s3.send(new PutObjectCommand({
613+
Bucket: checksumBucket,
614+
Key: checksumKey,
615+
Body: body,
616+
ChecksumAlgorithm: 'SHA256',
617+
}));
618+
619+
const headRes = await s3.send(new HeadObjectCommand({
620+
Bucket: checksumBucket,
621+
Key: checksumKey,
622+
}));
623+
assert.strictEqual(headRes.ChecksumSHA256, undefined);
624+
assert.strictEqual(headRes.ChecksumType, undefined);
625+
});
626+
627+
it('should return an error when ChecksumMode is not ENABLED', async () => {
628+
await s3.send(new PutObjectCommand({
629+
Bucket: checksumBucket,
630+
Key: checksumKey,
631+
Body: body,
632+
}));
633+
634+
await assert.rejects(
635+
s3.send(new HeadObjectCommand({
636+
Bucket: checksumBucket,
637+
Key: checksumKey,
638+
ChecksumMode: 'DISABLED',
639+
})),
640+
);
641+
});
642+
});
643+
});
644+
541645
const isCEPH = process.env.CI_CEPH !== undefined;
542646
const describeSkipIfCeph = isCEPH ? describe.skip : describe;
543647

tests/unit/api/objectHead.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
const assert = require('assert');
2+
const { models } = require('arsenal');
3+
const { ObjectMD, ObjectMDChecksum } = models;
4+
const { algorithms } = require('../../../lib/api/apiUtils/integrity/validateChecksums');
25

36
const { bucketPut } = require('../../../lib/api/bucketPut');
47
const { cleanup, DummyRequestLogger, makeAuthInfo } = require('../helpers');
@@ -565,6 +568,104 @@ describe('objectHead API', () => {
565568
});
566569
});
567570

571+
describe('x-amz-checksum-mode', () => {
572+
const checksumAlgorithms = [
573+
{ name: 'sha256', header: 'x-amz-checksum-sha256' },
574+
{ name: 'sha1', header: 'x-amz-checksum-sha1' },
575+
{ name: 'crc32', header: 'x-amz-checksum-crc32' },
576+
{ name: 'crc32c', header: 'x-amz-checksum-crc32c' },
577+
{ name: 'crc64nvme', header: 'x-amz-checksum-crc64nvme' },
578+
];
579+
580+
// Digests of postBody ("I am a body") for each algorithm, computed once.
581+
const expectedDigests = {};
582+
583+
before(done => {
584+
Promise.all(checksumAlgorithms.map(async ({ name }) => {
585+
expectedDigests[name] = await algorithms[name].digest(postBody);
586+
})).then(() => done(), done);
587+
});
588+
589+
checksumAlgorithms.forEach(({ name, header }) => {
590+
it(`should return ${header} and x-amz-checksum-type when mode is ENABLED`, done => {
591+
const md = new ObjectMD(mdColdHelper.baseMd)
592+
.setChecksum(new ObjectMDChecksum(name, expectedDigests[name], 'FULL_OBJECT'));
593+
mdColdHelper.putBucketMock(bucketName, null, () =>
594+
mdColdHelper.putObjectMock(bucketName, objectName, md, () => {
595+
const req = {
596+
bucketName,
597+
namespace,
598+
objectKey: objectName,
599+
headers: { 'x-amz-checksum-mode': 'ENABLED' },
600+
url: `/${bucketName}/${objectName}`,
601+
};
602+
objectHead(authInfo, req, log, (err, res) => {
603+
assert.ifError(err);
604+
assert.strictEqual(res[header], expectedDigests[name]);
605+
assert.strictEqual(res['x-amz-checksum-type'], 'FULL_OBJECT');
606+
done();
607+
});
608+
}));
609+
});
610+
});
611+
612+
it('should not return checksum headers when mode is ENABLED but object has no checksum', done => {
613+
mdColdHelper.putBucketMock(bucketName, null, () =>
614+
mdColdHelper.putObjectMock(bucketName, objectName, undefined, () => {
615+
const req = {
616+
bucketName,
617+
namespace,
618+
objectKey: objectName,
619+
headers: { 'x-amz-checksum-mode': 'ENABLED' },
620+
url: `/${bucketName}/${objectName}`,
621+
};
622+
objectHead(authInfo, req, log, (err, res) => {
623+
assert.ifError(err);
624+
checksumAlgorithms.forEach(({ header }) =>
625+
assert.strictEqual(res[header], undefined));
626+
assert.strictEqual(res['x-amz-checksum-type'], undefined);
627+
done();
628+
});
629+
}));
630+
});
631+
632+
it('should not return checksum headers when x-amz-checksum-mode is not set', done => {
633+
const md = new ObjectMD(mdColdHelper.baseMd)
634+
.setChecksum(new ObjectMDChecksum('sha256', expectedDigests.sha256, 'FULL_OBJECT'));
635+
mdColdHelper.putBucketMock(bucketName, null, () =>
636+
mdColdHelper.putObjectMock(bucketName, objectName, md, () => {
637+
const req = {
638+
bucketName,
639+
namespace,
640+
objectKey: objectName,
641+
headers: {},
642+
url: `/${bucketName}/${objectName}`,
643+
};
644+
objectHead(authInfo, req, log, (err, res) => {
645+
assert.ifError(err);
646+
checksumAlgorithms.forEach(({ header }) =>
647+
assert.strictEqual(res[header], undefined));
648+
assert.strictEqual(res['x-amz-checksum-type'], undefined);
649+
done();
650+
});
651+
}));
652+
});
653+
654+
it('should return InvalidArgument when x-amz-checksum-mode is not ENABLED', done => {
655+
const req = {
656+
bucketName,
657+
namespace,
658+
objectKey: objectName,
659+
headers: { 'x-amz-checksum-mode': 'DISABLED' },
660+
url: `/${bucketName}/${objectName}`,
661+
};
662+
objectHead(authInfo, req, log, err => {
663+
assert.strictEqual(err.is.InvalidArgument, true);
664+
done();
665+
});
666+
});
667+
});
668+
568669
[
569670
{
570671
name: 'should return content-length of 0 when requesting part 1 of empty object',

tests/unit/api/utils/metadataMockColdStorage.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ function getDeleteMarkerObjectMD(versionId) {
202202
}
203203

204204
module.exports = {
205+
baseMd,
205206
putObjectMock,
206207
getArchivedObjectMD,
207208
getRestoringObjectMD,

0 commit comments

Comments
 (0)