diff --git a/config.json b/config.json
index 185d752d87..4ef04f7f6f 100644
--- a/config.json
+++ b/config.json
@@ -142,5 +142,8 @@
},
"kmip": {
"providerName": "thales"
+ },
+ "integrityChecks": {
+ "objectPutRetention": true
}
}
diff --git a/lib/Config.js b/lib/Config.js
index 23b02f6a8d..5fe7d79462 100644
--- a/lib/Config.js
+++ b/lib/Config.js
@@ -533,6 +533,37 @@ function bucketNotifAssert(bucketNotifConfig) {
return bucketNotifConfig;
}
+function parseIntegrityChecks(config) {
+ const integrityChecks = {
+ 'bucketPutACL': true,
+ 'bucketPutCors': true,
+ 'bucketPutEncryption': true,
+ 'bucketPutLifecycle': true,
+ 'bucketPutNotification': true,
+ 'bucketPutObjectLock': true,
+ 'bucketPutPolicy': true,
+ 'bucketPutReplication': true,
+ 'bucketPutVersioning': true,
+ 'bucketPutWebsite': true,
+ 'multiObjectDelete': true,
+ 'objectPutACL': true,
+ 'objectPutLegalHold': true,
+ 'objectPutTagging': true,
+ 'objectPutRetention': true,
+ };
+
+ if (config && config.integrityChecks) {
+ for (const method in integrityChecks) {
+ if (method in config.integrityChecks) {
+ assert(typeof config.integrityChecks[method] == 'boolean', `bad config: ${method} not boolean`);
+ integrityChecks[method] = config.integrityChecks[method];
+ }
+ }
+ }
+
+ return integrityChecks;
+}
+
/**
* Reads from a config file and returns the content as a config object
*/
@@ -1743,6 +1774,8 @@ class Config extends EventEmitter {
this.supportedLifecycleRules = parseSupportedLifecycleRules(config.supportedLifecycleRules);
+ this.integrityChecks = parseIntegrityChecks(config);
+
/**
* S3C-10336: PutObject max size of 5GB is new in 9.5.1
* Provides a way to bypass the new validation if it breaks customer workflows
@@ -2081,4 +2114,5 @@ module.exports = {
azureGetStorageAccountName,
azureGetLocationCredentials,
parseSupportedLifecycleRules,
+ parseIntegrityChecks,
};
diff --git a/lib/api/api.js b/lib/api/api.js
index bba3fe2e67..6b12d3cfde 100644
--- a/lib/api/api.js
+++ b/lib/api/api.js
@@ -74,6 +74,7 @@ const parseCopySource = require('./apiUtils/object/parseCopySource');
const { tagConditionKeyAuth } = require('./apiUtils/authorization/tagConditionKeys');
const { isRequesterASessionUser } = require('./apiUtils/authorization/permissionChecks');
const checkHttpHeadersSize = require('./apiUtils/object/checkHttpHeadersSize');
+const { validateMethodChecksumNoChunking } = require('./apiUtils/integrity/validateChecksums');
const monitoringMap = policies.actionMaps.actionMonitoringMapS3;
@@ -242,8 +243,16 @@ const api = {
{ postLength });
return next(errors.InvalidRequest);
}
+
+ const buff = Buffer.concat(post, postLength);
+
+ const err = validateMethodChecksumNoChunking(request, buff, log);
+ if (err) {
+ return next(err);
+ }
+
// Convert array of post buffers into one string
- request.post = Buffer.concat(post, postLength).toString();
+ request.post = buff.toString();
return next(null, userInfo, authorizationResults, streamingV4Params, infos);
});
return undefined;
diff --git a/lib/api/apiUtils/integrity/validateChecksums.js b/lib/api/apiUtils/integrity/validateChecksums.js
new file mode 100644
index 0000000000..36864604cf
--- /dev/null
+++ b/lib/api/apiUtils/integrity/validateChecksums.js
@@ -0,0 +1,82 @@
+const crypto = require('crypto');
+const { errors: ArsenalErrors } = require('arsenal');
+const { config } = require('../../../Config');
+
+const ChecksumError = Object.freeze({
+ MD5Mismatch: 'MD5Mismatch',
+ MissingChecksum: 'MissingChecksum',
+});
+
+/**
+ * validateChecksumsNoChunking - Validate the checksums of a request.
+ * @param {object} headers - http headers
+ * @param {Buffer} body - http request body
+ * @return {object} - error
+ */
+function validateChecksumsNoChunking(headers, body) {
+ if (headers && 'content-md5' in headers) {
+ const md5 = crypto.createHash('md5').update(body).digest('base64');
+ if (md5 !== headers['content-md5']) {
+ return { error: ChecksumError.MD5Mismatch, details: { calculated: md5, expected: headers['content-md5'] } };
+ }
+
+ return null;
+ }
+
+ return { error: ChecksumError.MissingChecksum, details: null };
+}
+
+function defaultValidationFunc(request, body, log) {
+ const err = validateChecksumsNoChunking(request.headers, body);
+ if (err && err.error !== ChecksumError.MissingChecksum) {
+ log.debug('failed checksum validation', { method: request.apiMethod }, err);
+ return ArsenalErrors.BadDigest;
+ }
+
+ return null;
+}
+
+const methodValidationFunc = Object.freeze({
+ 'bucketPutACL': defaultValidationFunc,
+ 'bucketPutCors': defaultValidationFunc,
+ 'bucketPutEncryption': defaultValidationFunc,
+ 'bucketPutLifecycle': defaultValidationFunc,
+ 'bucketPutNotification': defaultValidationFunc,
+ 'bucketPutObjectLock': defaultValidationFunc,
+ 'bucketPutPolicy': defaultValidationFunc,
+ 'bucketPutReplication': defaultValidationFunc,
+ 'bucketPutVersioning': defaultValidationFunc,
+ 'bucketPutWebsite': defaultValidationFunc,
+ // TODO: DeleteObjects requires a checksum. Should return an error if ChecksumError.MissingChecksum.
+ 'multiObjectDelete': defaultValidationFunc,
+ 'objectPutACL': defaultValidationFunc,
+ 'objectPutLegalHold': defaultValidationFunc,
+ 'objectPutTagging': defaultValidationFunc,
+ 'objectPutRetention': defaultValidationFunc,
+});
+
+/**
+ * validateMethodChecksumsNoChunking - Validate the checksums of a request.
+ * @param {object} request - http request
+ * @param {Buffer} body - http request body
+ * @param {object} log - logger
+ * @return {object} - error
+ */
+function validateMethodChecksumNoChunking(request, body, log) {
+ if (config.integrityChecks[request.apiMethod]) {
+ const validationFunc = methodValidationFunc[request.apiMethod];
+ if (!validationFunc) {
+ return null;
+ }
+
+ return validationFunc(request, body, log);
+ }
+
+ return null;
+}
+
+module.exports = {
+ ChecksumError,
+ validateChecksumsNoChunking,
+ validateMethodChecksumNoChunking,
+};
diff --git a/lib/api/bucketPutCors.js b/lib/api/bucketPutCors.js
index 256af6257b..03a34430de 100644
--- a/lib/api/bucketPutCors.js
+++ b/lib/api/bucketPutCors.js
@@ -1,4 +1,3 @@
-const crypto = require('crypto');
const async = require('async');
const { errors, errorInstances } = require('arsenal');
@@ -33,16 +32,6 @@ function bucketPutCors(authInfo, request, log, callback) {
return callback(errors.MissingRequestBodyError);
}
- if (request.headers['content-md5']) {
- const md5 = crypto.createHash('md5')
- .update(request.post, 'utf8').digest('base64');
- if (md5 !== request.headers['content-md5']) {
- log.debug('bad md5 digest', { error: errors.BadDigest });
- monitoring.promMetrics('PUT', bucketName, 400, 'putBucketCors');
- return callback(errors.BadDigest);
- }
- }
-
if (parseInt(request.headers['content-length'], 10) > 65536) {
const errMsg = 'The CORS XML document is limited to 64 KB in size.';
log.debug(errMsg, { error: errors.MalformedXML });
diff --git a/lib/api/bucketPutReplication.js b/lib/api/bucketPutReplication.js
index 5fca68aaee..c940e660b4 100644
--- a/lib/api/bucketPutReplication.js
+++ b/lib/api/bucketPutReplication.js
@@ -33,6 +33,7 @@ function bucketPutReplication(authInfo, request, log, callback) {
requestType: request.apiMethods || 'bucketPutReplication',
request,
};
+
return waterfall([
// Validate the request XML and return the replication configuration.
next => getReplicationConfiguration(post, log, next),
diff --git a/lib/api/multiObjectDelete.js b/lib/api/multiObjectDelete.js
index b2fdff61f4..63b7306465 100644
--- a/lib/api/multiObjectDelete.js
+++ b/lib/api/multiObjectDelete.js
@@ -1,5 +1,3 @@
-const crypto = require('crypto');
-
const async = require('async');
const { parseString } = require('xml2js');
const { auth, errors, versioning, s3middleware, policies } = require('arsenal');
@@ -475,16 +473,6 @@ function multiObjectDelete(authInfo, request, log, callback) {
return callback(errors.MissingRequestBodyError);
}
- if (request.headers['content-md5']) {
- const md5 = crypto.createHash('md5')
- .update(request.post, 'utf8').digest('base64');
- if (md5 !== request.headers['content-md5']) {
- monitoring.promMetrics('DELETE', request.bucketName, 400,
- 'multiObjectDelete');
- return callback(errors.BadDigest);
- }
- }
-
const inPlayInternal = [];
const bucketName = request.bucketName;
const canonicalID = authInfo.getCanonicalID();
diff --git a/lib/api/objectPutTagging.js b/lib/api/objectPutTagging.js
index fbb1bba96d..ef23dcf64d 100644
--- a/lib/api/objectPutTagging.js
+++ b/lib/api/objectPutTagging.js
@@ -27,7 +27,6 @@ function objectPutTagging(authInfo, request, log, callback) {
log.debug('processing request', { method: 'objectPutTagging' });
const { bucketName, objectKey } = request;
-
const decodedVidResult = decodeVersionId(request.query);
if (decodedVidResult instanceof Error) {
log.trace('invalid versionId query', {
diff --git a/package.json b/package.json
index 8b0b0759cf..362688e221 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@zenko/cloudserver",
- "version": "9.0.26",
+ "version": "9.0.27",
"description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol",
"main": "index.js",
"engines": {
diff --git a/tests/unit/Config.js b/tests/unit/Config.js
index e7d7d6e42a..d9374df3af 100644
--- a/tests/unit/Config.js
+++ b/tests/unit/Config.js
@@ -7,6 +7,7 @@ const {
azureGetLocationCredentials,
locationConstraintAssert,
parseSupportedLifecycleRules,
+ parseIntegrityChecks,
ConfigObject,
} = require('../../lib/Config');
@@ -885,4 +886,40 @@ describe('Config', () => {
assert.strictEqual(config.instanceId.length, 6);
});
});
+
+ describe('parse integrity checks', () => {
+ it('should replace default values with new values', () => {
+ const newConfig = {
+ integrityChecks: {
+ 'bucketPutACL': false,
+ 'bucketPutCors': false,
+ 'bucketPutEncryption': false,
+ 'bucketPutLifecycle': false,
+ 'bucketPutNotification': false,
+ 'bucketPutObjectLock': false,
+ 'bucketPutPolicy': false,
+ 'bucketPutReplication': false,
+ 'bucketPutVersioning': false,
+ 'bucketPutWebsite': false,
+ 'multiObjectDelete': false,
+ 'objectPutACL': false,
+ 'objectPutLegalHold': false,
+ 'objectPutTagging': false,
+ 'objectPutRetention': false,
+ },
+ };
+
+ const result = parseIntegrityChecks(newConfig);
+ for (const method in result) {
+ assert(result[method] == false, method);
+ }
+ });
+
+ it('default method value is true', () => {
+ const result = parseIntegrityChecks(null);
+ for (const method in result) {
+ assert(result[method] == true, method);
+ }
+ });
+ });
});
diff --git a/tests/unit/api/api.js b/tests/unit/api/api.js
index b9d860ef0d..78626bd651 100644
--- a/tests/unit/api/api.js
+++ b/tests/unit/api/api.js
@@ -4,6 +4,7 @@ const api = require('../../../lib/api/api');
const DummyRequest = require('../DummyRequest');
const { default: AuthInfo } = require('arsenal/build/lib/auth/AuthInfo');
const assert = require('assert');
+const crypto = require('crypto');
describe('api.callApiMethod', () => {
let sandbox;
@@ -49,6 +50,7 @@ describe('api.callApiMethod', () => {
sandbox.restore();
});
+
it('should attach apiMethod to request', done => {
const testMethod = 'bucketGet';
api.callApiMethod(testMethod, request, response, log, () => {
@@ -112,4 +114,101 @@ describe('api.callApiMethod', () => {
(userInfo, _request, streamingV4Params, log, cb) => cb);
api.callApiMethod('multipartDelete', request, response, log);
});
+
+ describe('MD5 checksum validation', () => {
+ const methodsWithChecksumValidation = [
+ 'bucketPutACL',
+ 'bucketPutCors',
+ 'bucketPutEncryption',
+ 'bucketPutLifecycle',
+ 'bucketPutNotification',
+ 'bucketPutObjectLock',
+ 'bucketPutPolicy',
+ 'bucketPutReplication',
+ 'bucketPutVersioning',
+ 'bucketPutWebsite',
+ 'multiObjectDelete',
+ 'objectPutACL',
+ 'objectPutLegalHold',
+ 'objectPutTagging',
+ 'objectPutRetention'
+ ];
+
+ methodsWithChecksumValidation.forEach(method => {
+ it(`should return BadDigest for ${method} when bad MD5 checksum is provided`, done => {
+ const body = '';
+ const headers = {
+ 'content-md5': 'badchecksum123=', // Invalid MD5
+ 'content-length': body.length.toString()
+ };
+
+ const requestWithBody = new DummyRequest({
+ headers,
+ query: {},
+ socket: { remoteAddress: '127.0.0.1', destroy: sandbox.stub() }
+ }, body);
+
+ sandbox.stub(api, method).callsFake(() => {
+ done(new Error(`${method} was called despite bad checksum`));
+ });
+
+ api.callApiMethod(method, requestWithBody, response, log, err => {
+ assert(err, `Expected error for ${method} with bad checksum`);
+ assert(err.is.BadDigest, `Expected BadDigest error for ${method}, got: ${err.code}`);
+ done();
+ });
+ });
+ });
+
+ methodsWithChecksumValidation.forEach(method => {
+ it(`should succeed for ${method} when correct MD5 checksum is provided`, done => {
+ const body = '';
+ const correctMd5 = crypto.createHash('md5').update(body).digest('base64');
+ const headers = {
+ 'content-md5': correctMd5,
+ 'content-length': body.length.toString()
+ };
+
+ const requestWithBody = new DummyRequest({
+ headers,
+ query: {},
+ socket: { remoteAddress: '127.0.0.1', destroy: sandbox.stub() }
+ }, body);
+
+ sandbox.stub(api, method).callsFake((userInfo, _request, log, cb) => {
+ cb();
+ });
+
+ api.callApiMethod(method, requestWithBody, response, log, err => {
+ assert.ifError(err, `Unexpected error for ${method} with good checksum: ${err}`);
+ done();
+ });
+ });
+ });
+
+ methodsWithChecksumValidation.forEach(method => {
+ it(`should fail for ${method} when empty MD5 checksum is provided`, done => {
+ const body = '';
+ const headers = {
+ 'content-md5': '',
+ 'content-length': body.length.toString()
+ };
+
+ const requestWithBody = new DummyRequest({
+ headers,
+ query: {},
+ socket: { remoteAddress: '127.0.0.1', destroy: sandbox.stub() }
+ }, body);
+
+ sandbox.stub(api, method).callsFake((userInfo, _request, log, cb) => {
+ cb();
+ });
+
+ api.callApiMethod(method, requestWithBody, response, log, err => {
+ assert(err, `expected error for ${method} with no checksum`);
+ done();
+ });
+ });
+ });
+ });
});
diff --git a/tests/unit/api/apiUtils/integrity/validateChecksums.js b/tests/unit/api/apiUtils/integrity/validateChecksums.js
new file mode 100644
index 0000000000..b3f2d89b99
--- /dev/null
+++ b/tests/unit/api/apiUtils/integrity/validateChecksums.js
@@ -0,0 +1,276 @@
+const assert = require('assert');
+const crypto = require('crypto');
+const sinon = require('sinon');
+
+const { validateChecksumsNoChunking, ChecksumError, validateMethodChecksumNoChunking } =
+ require('../../../../../lib/api/apiUtils/integrity/validateChecksums');
+const { errors: ArsenalErrors } = require('arsenal');
+const { config } = require('../../../../../lib/Config');
+
+describe('validateChecksumsNoChunking', () => {
+ describe('with valid Content-MD5 header', () => {
+ it('should return null when MD5 matches the body content', () => {
+ const body = 'Hello, World!';
+ const expectedMd5 = crypto.createHash('md5').update(body, 'utf8').digest('base64');
+ const headers = {
+ 'content-md5': expectedMd5
+ };
+
+ const result = validateChecksumsNoChunking(headers, body);
+ assert.strictEqual(result, null);
+ });
+ });
+
+ describe('with MD5 mismatch', () => {
+ it('should return MD5Mismatch error when checksums do not match', () => {
+ const body = 'Hello, World!';
+ const wrongMd5 = 'wrongchecksum123=';
+ const expectedMd5 = crypto.createHash('md5').update(body, 'utf8').digest('base64');
+ const headers = {
+ 'content-md5': wrongMd5
+ };
+
+ const result = validateChecksumsNoChunking(headers, body);
+ assert.strictEqual(result.error, ChecksumError.MD5Mismatch);
+ assert.strictEqual(result.details.calculated, expectedMd5);
+ assert.strictEqual(result.details.expected, wrongMd5);
+ });
+ });
+
+ describe('without Content-MD5 header', () => {
+ it('should return MissingChecksum error when no content-md5 header is present', () => {
+ const body = 'Hello, World!';
+ const headers = {};
+
+ const result = validateChecksumsNoChunking(headers, body);
+ assert.strictEqual(result.error, ChecksumError.MissingChecksum);
+ assert.strictEqual(result.details, null);
+ });
+
+ it('should return MissingChecksum error when headers object is undefined', () => {
+ const body = 'Hello, World!';
+ const headers = undefined;
+
+ const result = validateChecksumsNoChunking(headers, body);
+ assert.strictEqual(result.error, ChecksumError.MissingChecksum);
+ assert.strictEqual(result.details, null);
+ });
+
+ it('should return MD5Mismatch error when content-md5 header is undefined', () => {
+ const body = 'Hello, World!';
+ const headers = {
+ 'content-type': 'application/json',
+ 'content-md5': undefined
+ };
+ const calculatedMD5 = crypto.createHash('md5').update(body, 'utf8').digest('base64');
+
+ const result = validateChecksumsNoChunking(headers, body);
+ assert.strictEqual(result.error, ChecksumError.MD5Mismatch);
+ assert.strictEqual(result.details.calculated, calculatedMD5);
+ assert.strictEqual(result.details.expected, undefined);
+ });
+
+ it('should return MD5Mismatch error when content-md5 header is null', () => {
+ const body = 'Hello, World!';
+ const headers = {
+ 'content-type': 'application/json',
+ 'content-md5': null
+ };
+ const calculatedMD5 = crypto.createHash('md5').update(body, 'utf8').digest('base64');
+
+ const result = validateChecksumsNoChunking(headers, body);
+ assert.strictEqual(result.error, ChecksumError.MD5Mismatch);
+ assert.strictEqual(result.details.calculated, calculatedMD5);
+ assert.strictEqual(result.details.expected, null);
+ });
+
+ it('should return MD5Mismatch error when content-md5 header is empty string', () => {
+ const body = 'Hello, World!';
+ const headers = {
+ 'content-type': 'application/json',
+ 'content-md5': ''
+ };
+ const calculatedMD5 = crypto.createHash('md5').update(body, 'utf8').digest('base64');
+
+ const result = validateChecksumsNoChunking(headers, body);
+ assert.strictEqual(result.error, ChecksumError.MD5Mismatch);
+ assert.strictEqual(result.details.calculated, calculatedMD5);
+ assert.strictEqual(result.details.expected, '');
+ });
+ });
+});
+
+describe('validateMethodChecksumNoChunking', () => {
+ let sandbox;
+ let originalIntegrityChecks;
+
+ const supportedMethods = [
+ 'bucketPutACL',
+ 'bucketPutCors',
+ 'bucketPutEncryption',
+ 'bucketPutLifecycle',
+ 'bucketPutNotification',
+ 'bucketPutObjectLock',
+ 'bucketPutPolicy',
+ 'bucketPutReplication',
+ 'bucketPutVersioning',
+ 'bucketPutWebsite',
+ 'multiObjectDelete',
+ 'objectPutACL',
+ 'objectPutLegalHold',
+ 'objectPutTagging',
+ 'objectPutRetention'
+ ];
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ originalIntegrityChecks = { ...config.integrityChecks };
+ });
+
+ afterEach(() => {
+ sandbox.restore();
+ config.integrityChecks = originalIntegrityChecks;
+ });
+
+ describe('when checksum mismatches', () => {
+ supportedMethods.forEach(method => {
+ it(`should return BadDigest error for ${method} when checksum mismatch`, () => {
+ config.integrityChecks[method] = true;
+
+ const body = 'Hello, World!';
+ const wrongMd5 = 'wrongchecksum123=';
+ const request = {
+ apiMethod: method,
+ headers: {
+ 'content-md5': wrongMd5
+ }
+ };
+ const log = { debug: sandbox.stub() };
+
+ const result = validateMethodChecksumNoChunking(request, body, log);
+
+ assert.deepStrictEqual(result, ArsenalErrors.BadDigest, 'Expected BadDigest error');
+ assert(log.debug.calledOnce);
+ });
+ });
+ });
+
+ describe('when no checksum is provided', () => {
+ supportedMethods.forEach(method => {
+ it(`should return null for ${method} when no checksum is provided`, () => {
+ config.integrityChecks[method] = true;
+
+ const body = 'Hello, World!';
+ const request = {
+ apiMethod: method,
+ headers: {}
+ };
+ const log = { debug: sandbox.stub() };
+
+ const result = validateMethodChecksumNoChunking(request, body, log);
+
+ assert.strictEqual(result, null);
+ assert(log.debug.notCalled);
+ });
+ });
+ });
+
+ describe('when checksum matches', () => {
+ supportedMethods.forEach(method => {
+ it(`should return null for ${method} when checksum matches`, () => {
+ config.integrityChecks[method] = true;
+
+ const body = 'Hello, World!';
+ const correctMd5 = crypto.createHash('md5').update(body, 'utf8').digest('base64');
+ const request = {
+ apiMethod: method,
+ headers: {
+ 'content-md5': correctMd5
+ }
+ };
+ const log = { debug: sandbox.stub() };
+
+ const result = validateMethodChecksumNoChunking(request, body, log);
+
+ assert.strictEqual(result, null);
+ assert(log.debug.notCalled);
+ });
+ });
+ });
+
+ describe('when method is disabled in config', () => {
+ supportedMethods.forEach(method => {
+ it(`should return null for ${method} when disabled, even with checksum mismatch`, () => {
+ config.integrityChecks[method] = false;
+
+ const body = 'Hello, World!';
+ const wrongMd5 = 'wrongchecksum123=';
+ const request = {
+ apiMethod: method,
+ headers: {
+ 'content-md5': wrongMd5
+ }
+ };
+ const log = { debug: sandbox.stub() };
+
+ const result = validateMethodChecksumNoChunking(request, body, log);
+
+ assert.strictEqual(result, null);
+ assert(log.debug.notCalled);
+ });
+ });
+ });
+
+ describe('when method is not in validation function mapping', () => {
+ it('should return null for unsupported method even when enabled in config', () => {
+ const unsupportedMethod = 'someUnsupportedMethod';
+ config.integrityChecks[unsupportedMethod] = true;
+
+ const body = 'Hello, World!';
+ const wrongMd5 = 'wrongchecksum123=';
+ const request = {
+ apiMethod: unsupportedMethod,
+ headers: {
+ 'content-md5': wrongMd5
+ }
+ };
+ const log = { debug: sandbox.stub() };
+
+ const result = validateMethodChecksumNoChunking(request, body, log);
+
+ assert.strictEqual(result, null);
+ assert(log.debug.notCalled);
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should return null when request has no apiMethod', () => {
+ const body = 'Hello, World!';
+ const request = {
+ headers: {
+ 'content-md5': 'wrongchecksum123='
+ }
+ };
+ const log = { debug: sandbox.stub() };
+
+ const result = validateMethodChecksumNoChunking(request, body, log);
+
+ assert.strictEqual(result, null);
+ });
+
+ it('should return null when request apiMethod is undefined in config', () => {
+ const body = 'Hello, World!';
+ const request = {
+ apiMethod: 'nonExistentMethod',
+ headers: {
+ 'content-md5': 'wrongchecksum123='
+ }
+ };
+ const log = { debug: sandbox.stub() };
+
+ const result = validateMethodChecksumNoChunking(request, body, log);
+
+ assert.strictEqual(result, null);
+ });
+ });
+});
diff --git a/tests/unit/api/bucketPutACL.js b/tests/unit/api/bucketPutACL.js
index dfc507c2bd..7c08b760ca 100644
--- a/tests/unit/api/bucketPutACL.js
+++ b/tests/unit/api/bucketPutACL.js
@@ -1,5 +1,4 @@
const assert = require('assert');
-
const aclUtils = require('../../../lib/utilities/aclUtils');
const { bucketPut } = require('../../../lib/api/bucketPut');
const bucketPutACL = require('../../../lib/api/bucketPutACL');
diff --git a/tests/unit/api/bucketPutCors.js b/tests/unit/api/bucketPutCors.js
index 074532b609..fc9e7bdcf2 100644
--- a/tests/unit/api/bucketPutCors.js
+++ b/tests/unit/api/bucketPutCors.js
@@ -70,37 +70,6 @@ describe('putBucketCORS API', () => {
});
});
- it('should accept request if md5 is omitted', done => {
- const corsUtil = new CorsConfigTester();
- const testBucketPutCorsRequest = corsUtil
- .createBucketCorsRequest('PUT', bucketName);
- testBucketPutCorsRequest.headers['content-md5'] = undefined;
- bucketPutCors(authInfo, testBucketPutCorsRequest, log, err => {
- if (err) {
- process.stdout.write(`Err putting bucket cors ${err}`);
- return done(err);
- }
- return metadata.getBucket(bucketName, log, (err, bucket) => {
- if (err) {
- process.stdout.write(`Err retrieving bucket MD ${err}`);
- return done(err);
- }
- const uploadedCors = bucket.getCors();
- assert.deepStrictEqual(uploadedCors, corsUtil.getCors());
- return done();
- });
- });
- });
-
- it('should reject request if md5 is mismatch', done => {
- const corsUtil = new CorsConfigTester();
- const testBucketPutCorsRequest = corsUtil
- .createBucketCorsRequest('PUT', bucketName);
- testBucketPutCorsRequest.headers['content-md5'] = 'wrong md5';
- _testPutBucketCors(authInfo, testBucketPutCorsRequest,
- log, 'BadDigest', done);
- });
-
it('should return MalformedXML if body greater than 64KB', done => {
const corsUtil = new CorsConfigTester();
const body = Buffer.alloc(65537); // 64 * 1024 = 65536 bytes
diff --git a/tests/unit/api/multiObjectDelete.js b/tests/unit/api/multiObjectDelete.js
index 58de5c05fa..9652b03b4f 100644
--- a/tests/unit/api/multiObjectDelete.js
+++ b/tests/unit/api/multiObjectDelete.js
@@ -412,85 +412,6 @@ describe('multiObjectDelete function', () => {
done();
});
});
-
- it('should accept request when content-md5 header is missing', done => {
- const post = '';
- const testObjectKey = 'objectname';
- const testBucketName = 'test-bucket';
- const request = new DummyRequest({
- bucketName: testBucketName,
- objectKey: testObjectKey,
- parsedHost: 'localhost',
- headers: {
- // No content-md5 header
- },
- post,
- socket: {
- remoteAddress: '127.0.0.1',
- },
- url: `/${testBucketName}`,
- });
-
- // Use the same canonicalID for both authInfo and bucket owner to avoid AccessDenied
- const testAuthInfo = makeAuthInfo(canonicalID);
-
- // Create bucket with proper ownership
- const testBucketRequest = new DummyRequest({
- bucketName: testBucketName,
- namespace,
- headers: {},
- url: `/${testBucketName}`,
- });
-
- // Create object to delete
- const testObjectRequest = new DummyRequest({
- bucketName: testBucketName,
- namespace,
- objectKey: testObjectKey,
- headers: {},
- url: `/${testBucketName}/${testObjectKey}`,
- }, postBody);
-
- bucketPut(testAuthInfo, testBucketRequest, log, () => {
- objectPut(testAuthInfo, testObjectRequest, undefined, log, () => {
- multiObjectDelete.multiObjectDelete(testAuthInfo, request, log, (err, res) => {
- // Request should succeed even without content-md5 header
- assert.strictEqual(err, null);
- assert.strictEqual(typeof res, 'string');
- // Should contain successful deletion response
- assert.strictEqual(res.includes('objectname'), true);
- done();
- });
- });
- });
- });
-
- it('should reject request with BadDigest error when content-md5 header mismatches', done => {
- const post = '';
- const incorrectMd5 = 'incorrectMd5Hash';
-
- const request = new DummyRequest({
- bucketName: 'bucketname',
- objectKey: 'objectname',
- parsedHost: 'localhost',
- headers: {
- 'content-md5': incorrectMd5,
- },
- post,
- socket: {
- remoteAddress: '127.0.0.1',
- },
- url: '/bucketname',
- });
- const authInfo = makeAuthInfo('123456');
-
- multiObjectDelete.multiObjectDelete(authInfo, request, log, (err, res) => {
- // Should return BadDigest error for mismatched content-md5
- assert.strictEqual(err.is.BadDigest, true);
- assert.strictEqual(res, undefined);
- done();
- });
- });
});
describe('multiObjectDelete function', () => {
@@ -534,80 +455,4 @@ describe('multiObjectDelete function', () => {
done();
});
});
-
- it('should accept request when content-md5 header is missing', done => {
- const post = '';
- const testObjectKey = 'objectname';
- const testBucketName = 'test-bucket';
- const request = new DummyRequest({
- bucketName: testBucketName,
- objectKey: testObjectKey,
- parsedHost: 'localhost',
- headers: {
- // No content-md5 header
- },
- post,
- socket: {
- remoteAddress: '127.0.0.1',
- },
- url: `/${testBucketName}`,
- });
- // Use the same canonicalID for both authInfo and bucket owner to avoid AccessDenied
- const testAuthInfo = makeAuthInfo(canonicalID);
-
- // Create bucket with proper ownership
- const testBucketRequest = new DummyRequest({
- bucketName: testBucketName,
- namespace,
- headers: {},
- url: `/${testBucketName}`,
- });
- // Create object to delete
- const testObjectRequest = new DummyRequest({
- bucketName: testBucketName,
- namespace,
- objectKey: testObjectKey,
- headers: {},
- url: `/${testBucketName}/${testObjectKey}`,
- }, postBody);
-
- bucketPut(testAuthInfo, testBucketRequest, log, () => {
- objectPut(testAuthInfo, testObjectRequest, undefined, log, () => {
- multiObjectDelete.multiObjectDelete(testAuthInfo, request, log, (err, res) => {
- // Request should succeed even without content-md5 header
- assert.strictEqual(err, null);
- assert.strictEqual(typeof res, 'string');
- // Should contain successful deletion response
- assert.strictEqual(res.includes('objectname'), true);
- done();
- });
- });
- });
- });
-
- it('should reject request with BadDigest error when content-md5 header mismatches', done => {
- const post = '';
- const incorrectMd5 = 'incorrectMd5Hash';
- const request = new DummyRequest({
- bucketName: 'bucketname',
- objectKey: 'objectname',
- parsedHost: 'localhost',
- headers: {
- 'content-md5': incorrectMd5,
- },
- post,
- socket: {
- remoteAddress: '127.0.0.1',
- },
- url: '/bucketname',
- });
- const authInfo = makeAuthInfo('123456');
-
- multiObjectDelete.multiObjectDelete(authInfo, request, log, (err, res) => {
- // Should return BadDigest error for mismatched content-md5
- assert.strictEqual(err.is.BadDigest, true);
- assert.strictEqual(res, undefined);
- done();
- });
- });
});
diff --git a/tests/unit/api/objectPutACL.js b/tests/unit/api/objectPutACL.js
index a88cc93bc2..5ba89d8908 100644
--- a/tests/unit/api/objectPutACL.js
+++ b/tests/unit/api/objectPutACL.js
@@ -1,6 +1,5 @@
const assert = require('assert');
const async = require('async');
-
const { errorInstances } = require('arsenal');
const AuthInfo = require('arsenal').auth.AuthInfo;
diff --git a/tests/unit/api/objectPutRetention.js b/tests/unit/api/objectPutRetention.js
index 4bf24cbcea..e0f8b19ae3 100644
--- a/tests/unit/api/objectPutRetention.js
+++ b/tests/unit/api/objectPutRetention.js
@@ -1,6 +1,5 @@
const assert = require('assert');
const moment = require('moment');
-
const { bucketPut } = require('../../../lib/api/bucketPut');
const objectPut = require('../../../lib/api/objectPut');
const objectPutRetention = require('../../../lib/api/objectPutRetention');