Skip to content

Commit ff17057

Browse files
authored
Adjust encoding of Azure block IDs (#68981)
Today we represent block IDs sent to Azure using the URL-safe base-64 encoding. This makes sense: these IDs appear in URLs. It turns out that Azure rejects this encoding for block IDs and instead demands that they are represented using the regular, URL-unsafe, base-64 encoding instead, then further wrapped in %-encoding to deal with the URL-unsafe characters that inevitably result. Relates #66489 Backport of #68957
1 parent 61e5a81 commit ff17057

File tree

5 files changed

+43
-7
lines changed

5 files changed

+43
-7
lines changed

plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureBlobStore.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import java.util.ArrayDeque;
6161
import java.util.ArrayList;
6262
import java.util.Arrays;
63+
import java.util.Base64;
6364
import java.util.Collections;
6465
import java.util.HashMap;
6566
import java.util.List;
@@ -441,12 +442,14 @@ private void executeMultipartUpload(String blobName, InputStream inputStream, lo
441442
assert blobSize == (((nbParts - 1) * partSize) + lastPartSize) : "blobSize does not match multipart sizes";
442443

443444
final List<String> blockIds = new ArrayList<>(nbParts);
445+
final Base64.Encoder base64Encoder = Base64.getEncoder().withoutPadding();
446+
final Base64.Decoder base64UrlDecoder = Base64.getUrlDecoder();
444447
for (int i = 0; i < nbParts; i++) {
445448
final long length = i < nbParts - 1 ? partSize : lastPartSize;
446449
final Flux<ByteBuffer> byteBufferFlux =
447450
convertStreamToByteBuffer(inputStream, length, DEFAULT_UPLOAD_BUFFERS_SIZE);
448451

449-
final String blockId = UUIDs.base64UUID();
452+
final String blockId = base64Encoder.encodeToString(base64UrlDecoder.decode(UUIDs.base64UUID()));
450453
blockBlobAsyncClient.stageBlock(blockId, byteBufferFlux, length).block();
451454
blockIds.add(blockId);
452455
}

plugins/repository-azure/src/test/java/org/elasticsearch/repositories/azure/AzureBlobContainerRetriesTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,11 @@ public void testWriteLargeBlob() throws Exception {
341341

342342
if ("PUT".equals(exchange.getRequestMethod())) {
343343
final Map<String, String> params = new HashMap<>();
344-
RestUtils.decodeQueryString(exchange.getRequestURI().getQuery(), 0, params);
344+
RestUtils.decodeQueryString(exchange.getRequestURI().getRawQuery(), 0, params);
345345

346346
final String blockId = params.get("blockid");
347+
assert Strings.hasText(blockId) == false || AzureFixtureHelper.assertValidBlockId(blockId);
348+
347349
if (Strings.hasText(blockId) && (countDownUploads.decrementAndGet() % 2 == 0)) {
348350
blocks.put(blockId, Streams.readFully(exchange.getRequestBody()));
349351
exchange.sendResponseHeaders(RestStatus.CREATED.getStatus(), -1);

test/fixtures/azure-fixture/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ tasks.named("test").configure { enabled = false }
1313

1414
dependencies {
1515
api project(':server')
16+
api project(':test:framework')
1617
}
1718

1819
tasks.named("preProcessFixture").configure {

test/fixtures/azure-fixture/src/main/java/fixture/azure/AzureHttpHandler.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,20 @@
2222
import java.io.IOException;
2323
import java.io.InputStreamReader;
2424
import java.nio.charset.StandardCharsets;
25-
import java.time.ZoneOffset;
26-
import java.time.ZonedDateTime;
27-
import java.util.ArrayList;
2825
import java.util.Arrays;
2926
import java.util.HashMap;
3027
import java.util.HashSet;
3128
import java.util.List;
3229
import java.util.Map;
3330
import java.util.Objects;
3431
import java.util.Set;
35-
import java.util.UUID;
3632
import java.util.concurrent.ConcurrentHashMap;
3733
import java.util.regex.Matcher;
3834
import java.util.regex.Pattern;
3935
import java.util.stream.Collectors;
4036

37+
import static org.elasticsearch.repositories.azure.AzureFixtureHelper.assertValidBlockId;
38+
4139
/**
4240
* Minimal HTTP handler that acts as an Azure compliant server
4341
*/
@@ -64,9 +62,10 @@ public void handle(final HttpExchange exchange) throws IOException {
6462
if (Regex.simpleMatch("PUT /" + account + "/" + container + "/*blockid=*", request)) {
6563
// Put Block (https://docs.microsoft.com/en-us/rest/api/storageservices/put-block)
6664
final Map<String, String> params = new HashMap<>();
67-
RestUtils.decodeQueryString(exchange.getRequestURI().getQuery(), 0, params);
65+
RestUtils.decodeQueryString(exchange.getRequestURI().getRawQuery(), 0, params);
6866

6967
final String blockId = params.get("blockid");
68+
assert assertValidBlockId(blockId);
7069
blobs.put(blockId, Streams.readFully(exchange.getRequestBody()));
7170
exchange.sendResponseHeaders(RestStatus.CREATED.getStatus(), -1);
7271

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.repositories.azure;
10+
11+
import org.elasticsearch.common.Strings;
12+
13+
import java.util.Base64;
14+
15+
public class AzureFixtureHelper {
16+
private AzureFixtureHelper() {
17+
}
18+
19+
public static boolean assertValidBlockId(String blockId) {
20+
assert Strings.hasText(blockId) : "blockId missing";
21+
try {
22+
final byte[] decode = Base64.getDecoder().decode(blockId);
23+
// all block IDs for a blob must be the same length and <64 bytes prior to decoding.
24+
// Elasticsearch generates them all to be 15 bytes long so we can just assert that:
25+
assert decode.length == 15 : "blockid [" + blockId + "] decodes to [" + decode.length + "] bytes";
26+
} catch (Exception e) {
27+
assert false : new AssertionError("blockid [" + blockId + "] is not in base64", e);
28+
}
29+
return true;
30+
}
31+
}

0 commit comments

Comments
 (0)