diff --git a/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java b/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java index fe3fddb8..17e24025 100644 --- a/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java +++ b/src/main/java/com/emc/object/s3/jersey/S3JerseyClient.java @@ -805,6 +805,8 @@ public ListPartsResult listParts(ListPartsRequest request) { @Override public MultipartPartETag uploadPart(UploadPartRequest request) { + // enable checksum verification of the uploaded part + request.property(RestUtil.PROPERTY_VERIFY_WRITE_CHECKSUM, Boolean.TRUE); return new MultipartPartETag(request.getPartNumber(), executeAndClose(client, request).getEntityTag().getValue()); } diff --git a/src/test/java/com/emc/object/s3/S3JerseyClientTest.java b/src/test/java/com/emc/object/s3/S3JerseyClientTest.java index 339eca5b..02d8647c 100644 --- a/src/test/java/com/emc/object/s3/S3JerseyClientTest.java +++ b/src/test/java/com/emc/object/s3/S3JerseyClientTest.java @@ -3466,6 +3466,104 @@ public void testMultipartUploadWithTagging() { } } + // OBS04O-108: verify uploadPart() sets VERIFY_WRITE_CHECKSUM so ChecksumFilter validates the ETag + @Test + public void testUploadPartSetsVerifyWriteChecksumProperty() throws Exception { + String key = "mpu-verify-property.bin"; + byte[] data = new byte[5 * 1024 * 1024]; // 5 MB + new Random(42).nextBytes(data); + + String uploadId = client.initiateMultipartUpload(getTestBucket(), key); + + try { + UploadPartRequest request = new UploadPartRequest(getTestBucket(), key, uploadId, 1, + new ByteArrayInputStream(data)); + request.setContentLength((long) data.length); + + Assert.assertNull(request.getProperties().get(RestUtil.PROPERTY_VERIFY_WRITE_CHECKSUM)); + + MultipartPartETag result = client.uploadPart(request); + Assert.assertNotNull(result.getETag()); + + Assert.assertEquals(Boolean.TRUE, request.getProperties().get(RestUtil.PROPERTY_VERIFY_WRITE_CHECKSUM)); + } finally { + try { + client.abortMultipartUpload(new AbortMultipartUploadRequest(getTestBucket(), key, uploadId)); + } catch (Exception ignored) { + } + } + } + + // OBS04O-108: verify ChecksumFilter throws ChecksumError when response ETag doesn't match client-computed MD5 + @Test + public void testUploadPartWriteChecksumDetectsETagMismatch() throws Exception { + byte[] data = new byte[1024]; + new Random(42).nextBytes(data); + String wrongMd5 = "00000000000000000000000000000000"; + + org.glassfish.jersey.client.ClientConfig clientConfig = new org.glassfish.jersey.client.ClientConfig(); + clientConfig.connectorProvider(new MockETagConnectorProvider(wrongMd5)); + Client jerseyClient = javax.ws.rs.client.ClientBuilder.newClient(clientConfig); + jerseyClient.register(new com.emc.object.s3.jersey.ChecksumFilter(new S3Config())); + + try { + jerseyClient.target("http://localhost/test") + .request() + .property(RestUtil.PROPERTY_VERIFY_WRITE_CHECKSUM, Boolean.TRUE) + .put(javax.ws.rs.client.Entity.entity(data, "application/octet-stream")); + Assert.fail("Expected ChecksumError wrapped in ProcessingException"); + } catch (ProcessingException e) { + Assert.assertTrue("Root cause must be ChecksumError, was: " + e.getCause(), + e.getCause() instanceof com.emc.object.util.ChecksumError); + } finally { + jerseyClient.close(); + } + } + + // Mock connector that returns a fake 200 response with a caller-supplied ETag + static class MockETagConnectorProvider implements org.glassfish.jersey.client.spi.ConnectorProvider, + org.glassfish.jersey.client.spi.Connector { + private final String responseMd5; + + MockETagConnectorProvider(String responseMd5) { + this.responseMd5 = responseMd5; + } + + @Override + public org.glassfish.jersey.client.spi.Connector getConnector(Client client, + javax.ws.rs.core.Configuration runtimeConfig) { + return this; + } + + @Override + public org.glassfish.jersey.client.ClientResponse apply(org.glassfish.jersey.client.ClientRequest request) + throws ProcessingException { + request.setStreamProvider(contentLength -> new java.io.ByteArrayOutputStream()); + try { + request.writeEntity(); + } catch (java.io.IOException e) { + throw new ProcessingException(e); + } + org.glassfish.jersey.client.ClientResponse response = + new org.glassfish.jersey.client.ClientResponse(Response.Status.OK, request); + response.headers(RestUtil.HEADER_ETAG, "\"" + responseMd5 + "\""); + response.setEntityStream(new java.io.ByteArrayInputStream(new byte[0])); + return response; + } + + @Override + public java.util.concurrent.Future apply(org.glassfish.jersey.client.ClientRequest request, + org.glassfish.jersey.client.spi.AsyncConnectorCallback callback) { + throw new UnsupportedOperationException("async not supported"); + } + + @Override + public String getName() { return "MockETagConnectorProvider"; } + + @Override + public void close() { } + } + private String getContentMD5(Object obj) { String contentMD5 = null; try {