Skip to content

Commit 8d61342

Browse files
Control max size/count of warning headers (#28427) (#29516)
Add a static persistent cluster level setting "http.max_warning_header_count" to control the maximum number of warning headers in client HTTP responses. Defaults to unbounded. Add a static persistent cluster level setting "http.max_warning_header_size" to control the maximum total size of warning headers in client HTTP responses. Defaults to unbounded. With every warning header that exceeds these limits, a message will be logged in the main ES log, and any more warning headers for this response will be ignored.
1 parent 6c42af2 commit 8d61342

File tree

7 files changed

+130
-11
lines changed

7 files changed

+130
-11
lines changed

docs/reference/modules/cluster/misc.asciidoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,4 @@ Enable or disable allocation for persistent tasks:
8282
This setting does not affect the persistent tasks that are already being executed.
8383
Only newly created persistent tasks, or tasks that must be reassigned (after a node
8484
left the cluster, for example), are impacted by this setting.
85-
--
85+
--

docs/reference/modules/http.asciidoc

+7-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ http://en.wikipedia.org/wiki/Chunked_transfer_encoding[HTTP chunking].
2020

2121
The settings in the table below can be configured for HTTP. Note that none of
2222
them are dynamically updatable so for them to take effect they should be set in
23-
`elasticsearch.yml`.
23+
the Elasticsearch <<settings, configuration file>>.
2424

2525
[cols="<,<",options="header",]
2626
|=======================================================================
@@ -100,6 +100,12 @@ simple message will be returned. Defaults to `true`
100100

101101
|`http.pipelining.max_events` |The maximum number of events to be queued up in memory before a HTTP connection is closed, defaults to `10000`.
102102

103+
|`http.max_warning_header_count` |The maximum number of warning headers in
104+
client HTTP responses, defaults to unbounded.
105+
106+
|`http.max_warning_header_size` |The maximum total size of warning headers in
107+
client HTTP responses, defaults to unbounded.
108+
103109
|=======================================================================
104110

105111
It also uses the common

server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java

+2
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ public void apply(Settings value, Settings current, Settings previous) {
246246
HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH,
247247
HttpTransportSettings.SETTING_HTTP_MAX_CHUNK_SIZE,
248248
HttpTransportSettings.SETTING_HTTP_MAX_HEADER_SIZE,
249+
HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_COUNT,
250+
HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_SIZE,
249251
HttpTransportSettings.SETTING_HTTP_MAX_INITIAL_LINE_LENGTH,
250252
HttpTransportSettings.SETTING_HTTP_READ_TIMEOUT,
251253
HttpTransportSettings.SETTING_HTTP_RESET_COOKIES,

server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java

+60-8
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@
2323
import org.elasticsearch.common.io.stream.StreamInput;
2424
import org.elasticsearch.common.io.stream.StreamOutput;
2525
import org.elasticsearch.common.io.stream.Writeable;
26+
import org.elasticsearch.common.logging.DeprecationLogger;
2627
import org.elasticsearch.common.logging.ESLoggerFactory;
2728
import org.elasticsearch.common.settings.Setting;
2829
import org.elasticsearch.common.settings.Setting.Property;
2930
import org.elasticsearch.common.settings.Settings;
31+
import org.elasticsearch.common.unit.ByteSizeValue;
32+
import org.elasticsearch.http.HttpTransportSettings;
33+
34+
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_COUNT;
35+
import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_SIZE;
3036

3137
import java.io.Closeable;
3238
import java.io.IOException;
@@ -39,13 +45,14 @@
3945
import java.util.Set;
4046
import java.util.concurrent.CancellationException;
4147
import java.util.concurrent.ExecutionException;
42-
import java.util.concurrent.FutureTask;
4348
import java.util.concurrent.RunnableFuture;
4449
import java.util.concurrent.atomic.AtomicBoolean;
4550
import java.util.function.Function;
4651
import java.util.function.Supplier;
4752
import java.util.stream.Collectors;
4853
import java.util.stream.Stream;
54+
import java.nio.charset.StandardCharsets;
55+
4956

5057
/**
5158
* A ThreadContext is a map of string headers and a transient map of keyed objects that are associated with
@@ -81,6 +88,8 @@ public final class ThreadContext implements Closeable, Writeable {
8188
private static final ThreadContextStruct DEFAULT_CONTEXT = new ThreadContextStruct();
8289
private final Map<String, String> defaultHeader;
8390
private final ContextThreadLocal threadLocal;
91+
private final int maxWarningHeaderCount;
92+
private final long maxWarningHeaderSize;
8493

8594
/**
8695
* Creates a new ThreadContext instance
@@ -98,6 +107,8 @@ public ThreadContext(Settings settings) {
98107
this.defaultHeader = Collections.unmodifiableMap(defaultHeader);
99108
}
100109
threadLocal = new ContextThreadLocal();
110+
this.maxWarningHeaderCount = SETTING_HTTP_MAX_WARNING_HEADER_COUNT.get(settings);
111+
this.maxWarningHeaderSize = SETTING_HTTP_MAX_WARNING_HEADER_SIZE.get(settings).getBytes();
101112
}
102113

103114
@Override
@@ -282,7 +293,7 @@ public void addResponseHeader(final String key, final String value) {
282293
* @param uniqueValue the function that produces de-duplication values
283294
*/
284295
public void addResponseHeader(final String key, final String value, final Function<String, String> uniqueValue) {
285-
threadLocal.set(threadLocal.get().putResponse(key, value, uniqueValue));
296+
threadLocal.set(threadLocal.get().putResponse(key, value, uniqueValue, maxWarningHeaderCount, maxWarningHeaderSize));
286297
}
287298

288299
/**
@@ -359,7 +370,7 @@ private static final class ThreadContextStruct {
359370
private final Map<String, Object> transientHeaders;
360371
private final Map<String, List<String>> responseHeaders;
361372
private final boolean isSystemContext;
362-
373+
private long warningHeadersSize; //saving current warning headers' size not to recalculate the size with every new warning header
363374
private ThreadContextStruct(StreamInput in) throws IOException {
364375
final int numRequest = in.readVInt();
365376
Map<String, String> requestHeaders = numRequest == 0 ? Collections.emptyMap() : new HashMap<>(numRequest);
@@ -371,6 +382,7 @@ private ThreadContextStruct(StreamInput in) throws IOException {
371382
this.responseHeaders = in.readMapOfLists(StreamInput::readString, StreamInput::readString);
372383
this.transientHeaders = Collections.emptyMap();
373384
isSystemContext = false; // we never serialize this it's a transient flag
385+
this.warningHeadersSize = 0L;
374386
}
375387

376388
private ThreadContextStruct setSystemContext() {
@@ -387,6 +399,18 @@ private ThreadContextStruct(Map<String, String> requestHeaders,
387399
this.responseHeaders = responseHeaders;
388400
this.transientHeaders = transientHeaders;
389401
this.isSystemContext = isSystemContext;
402+
this.warningHeadersSize = 0L;
403+
}
404+
405+
private ThreadContextStruct(Map<String, String> requestHeaders,
406+
Map<String, List<String>> responseHeaders,
407+
Map<String, Object> transientHeaders, boolean isSystemContext,
408+
long warningHeadersSize) {
409+
this.requestHeaders = requestHeaders;
410+
this.responseHeaders = responseHeaders;
411+
this.transientHeaders = transientHeaders;
412+
this.isSystemContext = isSystemContext;
413+
this.warningHeadersSize = warningHeadersSize;
390414
}
391415

392416
/**
@@ -440,30 +464,58 @@ private ThreadContextStruct putResponseHeaders(Map<String, List<String>> headers
440464
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders, isSystemContext);
441465
}
442466

443-
private ThreadContextStruct putResponse(final String key, final String value, final Function<String, String> uniqueValue) {
467+
private ThreadContextStruct putResponse(final String key, final String value, final Function<String, String> uniqueValue,
468+
final int maxWarningHeaderCount, final long maxWarningHeaderSize) {
444469
assert value != null;
470+
long newWarningHeaderSize = warningHeadersSize;
471+
//check if we can add another warning header - if max size within limits
472+
if (key.equals("Warning") && (maxWarningHeaderSize != -1)) { //if size is NOT unbounded, check its limits
473+
if (warningHeadersSize > maxWarningHeaderSize) { // if max size has already been reached before
474+
final String message = "Dropping a warning header, as their total size reached the maximum allowed of [" +
475+
maxWarningHeaderSize + "] bytes set in [" +
476+
HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_SIZE.getKey() + "]!";
477+
ESLoggerFactory.getLogger(ThreadContext.class).warn(message);
478+
return this;
479+
}
480+
newWarningHeaderSize += "Warning".getBytes(StandardCharsets.UTF_8).length + value.getBytes(StandardCharsets.UTF_8).length;
481+
if (newWarningHeaderSize > maxWarningHeaderSize) {
482+
final String message = "Dropping a warning header, as their total size reached the maximum allowed of [" +
483+
maxWarningHeaderSize + "] bytes set in [" +
484+
HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_SIZE.getKey() + "]!";
485+
ESLoggerFactory.getLogger(ThreadContext.class).warn(message);
486+
return new ThreadContextStruct(requestHeaders, responseHeaders, transientHeaders, isSystemContext, newWarningHeaderSize);
487+
}
488+
}
445489

446490
final Map<String, List<String>> newResponseHeaders = new HashMap<>(this.responseHeaders);
447491
final List<String> existingValues = newResponseHeaders.get(key);
448-
449492
if (existingValues != null) {
450493
final Set<String> existingUniqueValues = existingValues.stream().map(uniqueValue).collect(Collectors.toSet());
451494
assert existingValues.size() == existingUniqueValues.size();
452495
if (existingUniqueValues.contains(uniqueValue.apply(value))) {
453496
return this;
454497
}
455-
456498
final List<String> newValues = new ArrayList<>(existingValues);
457499
newValues.add(value);
458-
459500
newResponseHeaders.put(key, Collections.unmodifiableList(newValues));
460501
} else {
461502
newResponseHeaders.put(key, Collections.singletonList(value));
462503
}
463504

464-
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders, isSystemContext);
505+
//check if we can add another warning header - if max count within limits
506+
if ((key.equals("Warning")) && (maxWarningHeaderCount != -1)) { //if count is NOT unbounded, check its limits
507+
final int warningHeaderCount = newResponseHeaders.containsKey("Warning") ? newResponseHeaders.get("Warning").size() : 0;
508+
if (warningHeaderCount > maxWarningHeaderCount) {
509+
final String message = "Dropping a warning header, as their total count reached the maximum allowed of [" +
510+
maxWarningHeaderCount + "] set in [" + HttpTransportSettings.SETTING_HTTP_MAX_WARNING_HEADER_COUNT.getKey() + "]!";
511+
ESLoggerFactory.getLogger(ThreadContext.class).warn(message);
512+
return this;
513+
}
514+
}
515+
return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders, isSystemContext, newWarningHeaderSize);
465516
}
466517

518+
467519
private ThreadContextStruct putTransient(String key, Object value) {
468520
Map<String, Object> newTransient = new HashMap<>(this.transientHeaders);
469521
if (newTransient.putIfAbsent(key, value) != null) {

server/src/main/java/org/elasticsearch/http/HttpTransportSettings.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.elasticsearch.common.unit.TimeValue;
3030

3131
import java.util.List;
32-
import java.util.concurrent.TimeUnit;
3332
import java.util.function.Function;
3433

3534
import static java.util.Collections.emptyList;
@@ -88,6 +87,10 @@ public final class HttpTransportSettings {
8887
Setting.byteSizeSetting("http.max_chunk_size", new ByteSizeValue(8, ByteSizeUnit.KB), Property.NodeScope);
8988
public static final Setting<ByteSizeValue> SETTING_HTTP_MAX_HEADER_SIZE =
9089
Setting.byteSizeSetting("http.max_header_size", new ByteSizeValue(8, ByteSizeUnit.KB), Property.NodeScope);
90+
public static final Setting<Integer> SETTING_HTTP_MAX_WARNING_HEADER_COUNT =
91+
Setting.intSetting("http.max_warning_header_count", -1, -1, Property.NodeScope);
92+
public static final Setting<ByteSizeValue> SETTING_HTTP_MAX_WARNING_HEADER_SIZE =
93+
Setting.byteSizeSetting("http.max_warning_header_size", new ByteSizeValue(-1), Property.NodeScope);
9194
public static final Setting<ByteSizeValue> SETTING_HTTP_MAX_INITIAL_LINE_LENGTH =
9295
Setting.byteSizeSetting("http.max_initial_line_length", new ByteSizeValue(4, ByteSizeUnit.KB), Property.NodeScope);
9396
// don't reset cookies by default, since I don't think we really need to

server/src/main/java/org/elasticsearch/node/Node.java

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
import org.elasticsearch.gateway.GatewayService;
9494
import org.elasticsearch.gateway.MetaStateService;
9595
import org.elasticsearch.http.HttpServerTransport;
96+
import org.elasticsearch.http.HttpTransportSettings;
9697
import org.elasticsearch.index.analysis.AnalysisRegistry;
9798
import org.elasticsearch.indices.IndicesModule;
9899
import org.elasticsearch.indices.IndicesService;

server/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java

+55
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Map;
3434
import java.util.Set;
3535
import java.util.stream.IntStream;
36+
import java.nio.charset.StandardCharsets;
3637

3738
import static org.elasticsearch.common.logging.DeprecationLogger.WARNING_HEADER_PATTERN;
3839
import static org.elasticsearch.test.hamcrest.RegexMatcher.matches;
@@ -246,6 +247,60 @@ public void testEncode() {
246247
assertThat(DeprecationLogger.encode(s), IsSame.sameInstance(s));
247248
}
248249

250+
251+
public void testWarningHeaderCountSetting() throws IOException{
252+
// Test that the number of warning headers don't exceed 'http.max_warning_header_count'
253+
final int maxWarningHeaderCount = 2;
254+
Settings settings = Settings.builder()
255+
.put("http.max_warning_header_count", maxWarningHeaderCount)
256+
.build();
257+
try (ThreadContext threadContext = new ThreadContext(settings)) {
258+
final Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
259+
// try to log three warning messages
260+
logger.deprecated(threadContexts, "A simple message 1");
261+
logger.deprecated(threadContexts, "A simple message 2");
262+
logger.deprecated(threadContexts, "A simple message 3");
263+
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
264+
final List<String> responses = responseHeaders.get("Warning");
265+
266+
assertEquals(maxWarningHeaderCount, responses.size());
267+
assertThat(responses.get(0), warningValueMatcher);
268+
assertThat(responses.get(0), containsString("\"A simple message 1"));
269+
assertThat(responses.get(1), warningValueMatcher);
270+
assertThat(responses.get(1), containsString("\"A simple message 2"));
271+
}
272+
}
273+
274+
public void testWarningHeaderSizeSetting() throws IOException{
275+
// Test that the size of warning headers don't exceed 'http.max_warning_header_size'
276+
Settings settings = Settings.builder()
277+
.put("http.max_warning_header_size", "1Kb")
278+
.build();
279+
280+
byte [] arr = new byte[300];
281+
String message1 = new String(arr, StandardCharsets.UTF_8) + "1";
282+
String message2 = new String(arr, StandardCharsets.UTF_8) + "2";
283+
String message3 = new String(arr, StandardCharsets.UTF_8) + "3";
284+
285+
try (ThreadContext threadContext = new ThreadContext(settings)) {
286+
final Set<ThreadContext> threadContexts = Collections.singleton(threadContext);
287+
// try to log three warning messages
288+
logger.deprecated(threadContexts, message1);
289+
logger.deprecated(threadContexts, message2);
290+
logger.deprecated(threadContexts, message3);
291+
final Map<String, List<String>> responseHeaders = threadContext.getResponseHeaders();
292+
final List<String> responses = responseHeaders.get("Warning");
293+
294+
long warningHeadersSize = 0L;
295+
for (String response : responses){
296+
warningHeadersSize += "Warning".getBytes(StandardCharsets.UTF_8).length +
297+
response.getBytes(StandardCharsets.UTF_8).length;
298+
}
299+
// assert that the size of all warning headers is less or equal to 1Kb
300+
assertTrue(warningHeadersSize <= 1024);
301+
}
302+
}
303+
249304
private String range(int lowerInclusive, int upperInclusive) {
250305
return IntStream
251306
.range(lowerInclusive, upperInclusive + 1)

0 commit comments

Comments
 (0)