Repository: aws/aws-sdk-js
Pull request #3799 (2021-06-16 16:42:19)

support httpChecksumRequired trait
<!--
Thank you for your pull request. Please provide a description below.
-->
Add support for httpChecksumRequired trait. SDK will compute Content-MD5 header value for operations with this trait. To disable the behavior, users can unset computeChecksums config

## Test
- [x] manually tested with the S3 operation with the new trait

##### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->
- [x] `npm run test` passes
- [x] changelog is added, `npm run add-change`
- [x] run `bundle exec rake docs:api` and inspect `doc/latest/index.html` if documentation is changed


Patch:
diff --git a/.changes/next-release/feature-Model-750c473e.json b/.changes/next-release/feature-Model-750c473e.json
new file mode 100644
index 000000000..5f667ebfc
--- /dev/null
+++ b/.changes/next-release/feature-Model-750c473e.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "category": "Model",
+  "description": "Add support for httpChecksumRequired trait. SDK will insert Content-MD5 header value for operations with this trait. To disable the behavior, users can unset computeChecksums config"
+}
\ No newline at end of file
diff --git a/lib/event_listeners.js b/lib/event_listeners.js
index eccf1ae01..d847db26a 100644
--- a/lib/event_listeners.js
+++ b/lib/event_listeners.js
@@ -132,6 +132,29 @@ AWS.EventListeners = {
       new AWS.ParamValidator(validation).validate(rules, req.params);
     });
 
+    add('COMPUTE_CHECKSUM', 'afterBuild', function COMPUTE_CHECKSUM(req) {
+      if (!req.service.api.operations) {
+        return;
+      }
+      var operation = req.service.api.operations[req.operation];
+      if (!operation) {
+        return;
+      }
+      var body = req.httpRequest.body;
+      var isNonStreamingPayload = body && (AWS.util.Buffer.isBuffer(body) || typeof body === 'string');
+      var headers = req.httpRequest.headers;
+      if (
+        operation.httpChecksumRequired &&
+        req.service.config.computeChecksums &&
+        isNonStreamingPayload &&
+        req.service.getSignerClass(req) === AWS.Signers.V4 &&
+        !headers['Content-MD5']
+      ) {
+        var md5 = AWS.util.crypto.md5(body, 'base64');
+        headers['Content-MD5'] = md5;
+      }
+    });
+
     addAsync('COMPUTE_SHA256', 'afterBuild', function COMPUTE_SHA256(req, done) {
       req.haltHandlersOnError();
       if (!req.service.api.operations) {
diff --git a/lib/model/operation.js b/lib/model/operation.js
index 65758d0aa..858d6bcf9 100644
--- a/lib/model/operation.js
+++ b/lib/model/operation.js
@@ -23,6 +23,7 @@ function Operation(name, operation, options) {
       (operation.endpointdiscovery.required ? 'REQUIRED' : 'OPTIONAL') :
     'NULL'
   );
+  property(this, 'httpChecksumRequired', operation.httpChecksumRequired, false);
 
   memoizedProperty(this, 'input', function() {
     if (!operation.input) {
diff --git a/lib/services/s3.js b/lib/services/s3.js
index a53daa258..2347a94eb 100644
--- a/lib/services/s3.js
+++ b/lib/services/s3.js
@@ -446,45 +446,41 @@ AWS.util.update(AWS.S3.prototype, {
   },
 
   /**
-   * Checks whether checksums should be computed for the request.
-   * If the request requires checksums to be computed, this will always
-   * return true, otherwise it depends on whether {AWS.Config.computeChecksums}
-   * is set.
+   * Checks whether checksums should be computed for the request if it's not
+   * already set by {AWS.EventListeners.Core.COMPUTE_CHECKSUM}. It depends on
+   * whether {AWS.Config.computeChecksums} is set.
    *
    * @param req [AWS.Request] the request to check against
    * @return [Boolean] whether to compute checksums for a request.
    * @api private
    */
   willComputeChecksums: function willComputeChecksums(req) {
-    if (this.computableChecksumOperations[req.operation]) return true;
-    if (!this.config.computeChecksums) return false;
-
-    // TODO: compute checksums for Stream objects
-    if (!AWS.util.Buffer.isBuffer(req.httpRequest.body) &&
-        typeof req.httpRequest.body !== 'string') {
-      return false;
-    }
-
     var rules = req.service.api.operations[req.operation].input.members;
+    var body = req.httpRequest.body;
+    var needsContentMD5 = rules.ContentMD5 &&
+      !req.params.ContentMD5 &&
+      body &&
+      (AWS.util.Buffer.isBuffer(req.httpRequest.body) || typeof req.httpRequest.body === 'string');
 
     // Sha256 signing disabled, and not a presigned url
-    if (req.service.shouldDisableBodySigning(req) && !Object.prototype.hasOwnProperty.call(req.httpRequest.headers, 'presigned-expires')) {
-      if (rules.ContentMD5 && !req.params.ContentMD5) {
-        return true;
-      }
+    if (needsContentMD5 && req.service.shouldDisableBodySigning(req) && !req.isPresigned()) {
+      return true;
     }
 
-    // V4 signer uses SHA256 signatures so only compute MD5 if it is required
-    if (req.service.getSignerClass(req) === AWS.Signers.V4) {
-      if (rules.ContentMD5 && !rules.ContentMD5.required) return false;
+    // SigV2 and presign, for backwards compatibility purpose.
+    if (needsContentMD5 && this.getSignatureVersion(req) === 's3' && req.isPresigned()) {
+      return true;
     }
 
-    if (rules.ContentMD5 && !req.params.ContentMD5) return true;
+    return false;
   },
 
   /**
    * A listener that computes the Content-MD5 and sets it in the header.
-   * @see AWS.S3.willComputeChecksums
+   * This listener is to support S3-specific features like
+   * s3DisableBodySigning and SigV2 presign. Content MD5 logic for SigV4 is
+   * handled in AWS.EventListeners.Core.COMPUTE_CHECKSUM
+   *
    * @api private
    */
   computeContentMd5: function computeContentMd5(req) {
diff --git a/scripts/region-checker/allowlist.js b/scripts/region-checker/allowlist.js
index 410c95e75..f5453a3a0 100644
--- a/scripts/region-checker/allowlist.js
+++ b/scripts/region-checker/allowlist.js
@@ -37,13 +37,13 @@ var allowlist = {
         254,
         267,
         273,
-        648,
-        650,
-        769,
-        780,
-        781,
-        782,
-        787
+        644,
+        646,
+        765,
+        776,
+        777,
+        778,
+        783
     ]
 };
 


Test patch:
diff --git a/test/event_listeners.spec.js b/test/event_listeners.spec.js
index 25893df57..4d8727262 100644
--- a/test/event_listeners.spec.js
+++ b/test/event_listeners.spec.js
@@ -241,7 +241,7 @@
         };
 
         describe('when using unsigned authtype', function() {
-          it('when paylaod is a buffer', function() {
+          it('when payload is a buffer', function() {
             var service = new FooService();
             var req = service.putStream({
               Body: AWS.util.buffer.toBuffer('test')
@@ -253,7 +253,7 @@
             });
           });
 
-          it('when paylaod is a string', function() {
+          it('when payload is a string', function() {
             var service = new FooService();
             var req = service.putStream({
               Body: 'test'
@@ -266,7 +266,7 @@
           });
 
           if (AWS.util.isNode()) {
-            it('when paylaod is a file stream', function() {
+            it('when payload is a file stream', function() {
               var service = new FooService();
               var req = service.putStream({
                 Body: require('fs').createReadStream(__filename)
@@ -378,6 +378,51 @@
           });
         }
       });
+
+      describe('adds Content-MD5 header', function() {
+        it('when payload is a string', function() {
+          var service = new FooService();
+          var req = service.putWithChecksum({
+            Body: 'test'
+          });
+
+          req.runTo('sign', function(err) {
+            expect(req.httpRequest.headers['Content-MD5']).to.equal('mi9mZPtVgELD8CntO010Rw==');
+            expect(!err).to.equal(true);
+          });
+        });
+
+        it('should be disabled if computeChecksums set to false', function() {
+          var service = new FooService({
+            computeChecksums: false
+          });
+          var req = service.putWithChecksum({
+            Body: 'test'
+          });
+
+          req.runTo('sign', function(err) {
+            expect(req.httpRequest.headers['Content-MD5']).to.equal(undefined);
+            expect(!err).to.equal(true);
+          });
+        });
+
+        it('should not calculate checksum if provided', function() {
+          var service = new FooService({
+            computeChecksums: false
+          });
+          var req = service.putWithChecksum({
+            Body: 'test'
+          });
+          req.on('build', function(req) {
+            req.httpRequest.headers['Content-MD5'] = '000';
+          });
+
+          req.runTo('sign', function(err) {
+            expect(req.httpRequest.headers['Content-MD5']).to.equal('000');
+            expect(!err).to.equal(true);
+          });
+        });
+      });
     });
 
     describe('restart', function() {
diff --git a/test/foo-service.fixture.js b/test/foo-service.fixture.js
index a7bff4054..d6550fa9f 100644
--- a/test/foo-service.fixture.js
+++ b/test/foo-service.fixture.js
@@ -94,6 +94,22 @@ var model = {
         },
       },
     },
+    'PutWithChecksum': {
+      'http': {
+        'method': 'PUT',
+        'requestUri': '/'
+      },
+      'input': {
+        'type': 'structure',
+        'members': {
+          'Body': {
+            'shape': 'StringShape'
+          }
+        },
+      },
+      'httpChecksumRequired': true,
+      'payload': 'Body'
+    },
   },
   'shapes': {
     'StreamingBody': {
diff --git a/test/services/s3.spec.js b/test/services/s3.spec.js
index 5d76009ad..d4849e2a9 100644
--- a/test/services/s3.spec.js
+++ b/test/services/s3.spec.js
@@ -2532,19 +2532,6 @@ describe('AWS.S3', function() {
     });
   });
 
-  AWS.util.each(AWS.S3.prototype.computableChecksumOperations, function(operation) {
-    describe(operation, function() {
-      it('forces Content-MD5 header parameter', function() {
-        var req = s3[operation]({
-          Bucket: 'bucket',
-          ContentMD5: '000'
-        }).build();
-        var hash = AWS.util.crypto.md5(req.httpRequest.body, 'base64');
-        expect(req.httpRequest.headers['Content-MD5']).to.equal(hash);
-      });
-    });
-  });
-
   describe('willComputeChecksums', function() {
     var willCompute = function(operation, opts) {
       var compute = opts.computeChecksums;
@@ -2569,86 +2556,6 @@ describe('AWS.S3', function() {
       }
     };
 
-    it('computes checksums if the operation requires it', function() {
-      willCompute('deleteObjects', {
-        computeChecksums: true
-      });
-      willCompute('putBucketCors', {
-        computeChecksums: true
-      });
-      willCompute('putBucketLifecycle', {
-        computeChecksums: true
-      });
-      willCompute('putBucketLifecycleConfiguration', {
-        computeChecksums: true
-      });
-      willCompute('putBucketTagging', {
-        computeChecksums: true
-      });
-      willCompute('putBucketReplication', {
-        computeChecksums: true
-      });
-      willCompute('putObjectLegalHold', {
-        computeChecksums: true
-      });
-      willCompute('putObjectRetention', {
-        computeChecksums: true
-      });
-      willCompute('putObjectLockConfiguration', {
-        computeChecksums: true
-      });
-    });
-
-    it('computes checksums if computeChecksums is off and operation requires it', function() {
-      willCompute('deleteObjects', {
-        computeChecksums: false
-      });
-      willCompute('putBucketCors', {
-        computeChecksums: false
-      });
-      willCompute('putBucketLifecycle', {
-        computeChecksums: false
-      });
-      willCompute('putBucketLifecycleConfiguration', {
-        computeChecksums: false
-      });
-      willCompute('putBucketTagging', {
-        computeChecksums: false
-      });
-      willCompute('putBucketReplication', {
-        computeChecksums: false
-      });
-      willCompute('putObjectLegalHold', {
-        computeChecksums: false
-      });
-      willCompute('putObjectRetention', {
-        computeChecksums: false
-      });
-      willCompute('putObjectLockConfiguration', {
-        computeChecksums: false
-      });
-    });
-
-    it('does not compute checksums if computeChecksums is off', function() {
-      willCompute('putObject', {
-        computeChecksums: false,
-        hash: null
-      });
-    });
-
-    it('does not compute checksums if computeChecksums is on and ContentMD5 is provided', function() {
-      willCompute('putBucketAcl', {
-        computeChecksums: true,
-        hash: '000'
-      });
-    });
-
-    it('computes checksums if computeChecksums is on and ContentMD5 is not provided', function() {
-      willCompute('putBucketAcl', {
-        computeChecksums: true
-      });
-    });
-
     if (AWS.util.isNode()) {
       it('does not compute checksums for Stream objects', function() {
         s3 = new AWS.S3({