2323import java .util .concurrent .CopyOnWriteArraySet ;
2424import java .util .regex .Matcher ;
2525import java .util .regex .Pattern ;
26+ import java .util .stream .IntStream ;
27+ import java .util .stream .Stream ;
2628
2729/**
2830 * This is a simplistic logger that adds warning messages to HTTP headers.
@@ -39,7 +41,8 @@ public class HeaderWarning {
3941 "Elasticsearch-" + // warn agent
4042 "\\ d+\\ .\\ d+\\ .\\ d+(?:-(?:alpha|beta|rc)\\ d+)?(?:-SNAPSHOT)?-" + // warn agent
4143 "(?:[a-f0-9]{7}(?:[a-f0-9]{33})?|unknown) " + // warn agent
42- "\" ((?:\t | |!|[\\ x23-\\ x5B]|[\\ x5D-\\ x7E]|[\\ x80-\\ xFF]|\\ \\ |\\ \\ \" )*)\" ( " + // quoted warning value, captured
44+ // quoted warning value, captured. Do not add more greedy qualifiers later to avoid excessive backtracking
45+ "\" (?<quotedStringValue>.*)\" ( " +
4346 // quoted RFC 1123 date format
4447 "\" " + // opening quote
4548 "(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), " + // weekday
@@ -48,7 +51,31 @@ public class HeaderWarning {
4851 "\\ d{4} " + // 4-digit year
4952 "\\ d{2}:\\ d{2}:\\ d{2} " + // (two-digit hour):(two-digit minute):(two-digit second)
5053 "GMT" + // GMT
51- "\" )?" ); // closing quote (optional, since an older version can still send a warn-date)
54+ "\" )?" ,// closing quote (optional, since an older version can still send a warn-date)
55+ Pattern .DOTALL
56+ ); // in order to parse new line inside the qdText
57+
58+ /**
59+ * quoted-string is defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
60+ * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
61+ * qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
62+ * obs-text = %x80-FF
63+ * <p>
64+ * this was previously used in WARNING_HEADER_PATTERN, but can cause stackoverflow
65+ * "\"((?:\t| |!|[\\x23-\\x5B]|[\\x5D-\\x7E]|[\\x80-\\xFF]|\\\\|\\\\\")*)\"( " + // quoted warning value, captured
66+ * the individual chars from qdText can be validated using the set of chars
67+ * the \\\\|\\\\\" (escaped '\' 0x20 and '"' 0x5c) which is used for quoted-pair has to be validated as strings
68+ */
69+ private static BitSet qdTextChars = Stream .of (
70+ IntStream .of (0x09 ),// HTAB
71+ IntStream .of (0x20 ), // SPACE
72+ IntStream .of (0x21 ), // !
73+ // excluding 0x22 '"\"' which has to be escaped
74+ IntStream .rangeClosed (0x23 , 0x5B ),// ascii #-[
75+ // excluding 0x5c '\' which has to be escaped
76+ IntStream .rangeClosed (0x5D , 0x7E ),// ascii ]-~
77+ IntStream .rangeClosed (0x80 , 0xFF )// obs-text -bear in mind it contains 0x85 new line. Which requires DOT_ALL flag
78+ ).flatMapToInt (i -> i ).collect (BitSet ::new , BitSet ::set , BitSet ::or );
5279 public static final Pattern WARNING_XCONTENT_LOCATION_PATTERN = Pattern .compile ("^\\ [.*?]\\ [-?\\ d+:-?\\ d+] " );
5380
5481 /*
@@ -182,7 +209,26 @@ private static boolean assertWarningValue(final String s, final String warningVa
182209 final Matcher matcher = WARNING_HEADER_PATTERN .matcher (s );
183210 final boolean matches = matcher .matches ();
184211 assert matches ;
185- return matcher .group (1 ).equals (warningValue );
212+ String quotedStringValue = matcher .group ("quotedStringValue" );
213+ assert matchesQuotedString (quotedStringValue );
214+ return quotedStringValue .equals (warningValue );
215+ }
216+
217+ // this is meant to be in testing only
218+ public static boolean warningHeaderPatternMatches (final String s ) {
219+ final Matcher matcher = WARNING_HEADER_PATTERN .matcher (s );
220+ final boolean matches = matcher .matches ();
221+ if (matches ) {
222+ String quotedStringValue = matcher .group ("quotedStringValue" );
223+ return matchesQuotedString (quotedStringValue );
224+ }
225+ return false ;
226+ }
227+
228+ private static boolean matchesQuotedString (String qdtext ) {
229+ qdtext = qdtext .replaceAll ("\\ \\ \" " , "" );
230+ qdtext = qdtext .replaceAll ("\\ \\ " , "" );
231+ return qdtext .chars ().allMatch (c -> qdTextChars .get (c ));
186232 }
187233
188234 /**
@@ -330,9 +376,8 @@ static void addWarning(Set<ThreadContext> threadContexts, String message, Object
330376 if (iterator .hasNext ()) {
331377 final String formattedMessage = LoggerMessageFormat .format (message , params );
332378 final String warningHeaderValue = formatWarning (formattedMessage );
333- // TODO #95972: Temporarily commented out to avoid StackOverflowError, see https://github.com/elastic/elasticsearch/issues/95972
334- // assert WARNING_HEADER_PATTERN.matcher(warningHeaderValue).matches();
335- // assert extractWarningValueFromWarningHeader(warningHeaderValue, false).equals(escapeAndEncode(formattedMessage));
379+ assert warningHeaderPatternMatches (warningHeaderValue );
380+ assert extractWarningValueFromWarningHeader (warningHeaderValue , false ).equals (escapeAndEncode (formattedMessage ));
336381 while (iterator .hasNext ()) {
337382 try {
338383 final ThreadContext next = iterator .next ();
0 commit comments