Skip to content

Commit e814602

Browse files
authored
Adds '+' character to allowed characters in baggage value (#4898)
* Adds '+' character to allowed characters in baggade value * Formatting fix * Adda BaggageCodec implementation * Additional cleanup * Removal of Nullable method parameters. * Additional tests for baggage decoding
1 parent 4b6df42 commit e814602

File tree

4 files changed

+176
-9
lines changed

4 files changed

+176
-9
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.baggage.propagation;
7+
8+
import java.io.ByteArrayOutputStream;
9+
import java.nio.charset.Charset;
10+
import java.nio.charset.StandardCharsets;
11+
12+
/**
13+
* Note: This class is based on code from Apache Commons Codec. It is comprised of code from these
14+
* classes:
15+
*
16+
* <ul>
17+
* <li><a
18+
* href="https://github.com/apache/commons-codec/blob/482df6cabfb288acb6ab3e4a732fdb93aecfa7c2/src/main/java/org/apache/commons/codec/net/URLCodec.java">org.apache.commons.codec.net.URLCodec</a>
19+
* <li><a
20+
* href="https://github.com/apache/commons-codec/blob/482df6cabfb288acb6ab3e4a732fdb93aecfa7c2/src/main/java/org/apache/commons/codec/net/Utils.java">org.apache.commons.codec.net.Utils</a>
21+
* </ul>
22+
*
23+
* <p>Implements baggage-octet decoding in accordance with th <a
24+
* href="https://w3c.github.io/baggage/#definition">Baggage header content</a> specification. All
25+
* US-ASCII characters excluding CTLs, whitespace, DQUOTE, comma, semicolon and backslash are
26+
* encoded in `www-form-urlencoded` encoding scheme.
27+
*/
28+
class BaggageCodec {
29+
30+
private static final byte ESCAPE_CHAR = '%';
31+
private static final int RADIX = 16;
32+
33+
private BaggageCodec() {}
34+
35+
/**
36+
* Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped
37+
* characters are converted back to their original representation.
38+
*
39+
* @param bytes array of URL safe characters
40+
* @return array of original bytes
41+
*/
42+
private static byte[] decode(byte[] bytes) {
43+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
44+
for (int i = 0; i < bytes.length; i++) {
45+
int b = bytes[i];
46+
if (b == ESCAPE_CHAR) {
47+
try {
48+
int u = digit16(bytes[++i]);
49+
int l = digit16(bytes[++i]);
50+
buffer.write((char) ((u << 4) + l));
51+
} catch (ArrayIndexOutOfBoundsException e) { // FIXME
52+
throw new IllegalArgumentException("Invalid URL encoding: ", e);
53+
}
54+
} else {
55+
buffer.write(b);
56+
}
57+
}
58+
return buffer.toByteArray();
59+
}
60+
61+
/**
62+
* Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped
63+
* characters are converted back to their original representation.
64+
*
65+
* @param value string of URL safe characters
66+
* @param charset encoding of given string
67+
* @return decoded value
68+
*/
69+
static String decode(String value, Charset charset) {
70+
byte[] bytes = decode(value.getBytes(StandardCharsets.US_ASCII));
71+
return new String(bytes, charset);
72+
}
73+
74+
/**
75+
* Returns the numeric value of the character {@code b} in radix 16.
76+
*
77+
* @param b The byte to be converted.
78+
* @return The numeric value represented by the character in radix 16.
79+
*/
80+
private static int digit16(byte b) {
81+
int i = Character.digit((char) b, RADIX);
82+
if (i == -1) {
83+
throw new IllegalArgumentException( // FIXME
84+
"Invalid URL encoding: not a valid digit (radix " + RADIX + "): " + b);
85+
}
86+
return i;
87+
}
88+
}

api/all/src/main/java/io/opentelemetry/api/baggage/propagation/Parser.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
import io.opentelemetry.api.baggage.BaggageBuilder;
99
import io.opentelemetry.api.baggage.BaggageEntryMetadata;
10-
import java.io.UnsupportedEncodingException;
11-
import java.net.URLDecoder;
1210
import java.nio.charset.StandardCharsets;
1311
import javax.annotation.Nullable;
1412

@@ -64,6 +62,8 @@ void parseInto(BaggageBuilder baggageBuilder) {
6462
} else {
6563
skipToNext = true;
6664
}
65+
} else if (state == State.VALUE) {
66+
skipToNext = !value.tryNextChar(current, i);
6767
}
6868
break;
6969
}
@@ -146,11 +146,7 @@ private static String decodeValue(@Nullable String value) {
146146
if (value == null) {
147147
return null;
148148
}
149-
try {
150-
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
151-
} catch (UnsupportedEncodingException e) {
152-
return null;
153-
}
149+
return BaggageCodec.decode(value, StandardCharsets.UTF_8);
154150
}
155151

156152
/**
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.baggage.propagation;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
11+
import java.nio.charset.StandardCharsets;
12+
import org.junit.jupiter.api.Test;
13+
import org.junit.jupiter.params.ParameterizedTest;
14+
import org.junit.jupiter.params.provider.CsvSource;
15+
16+
class BaggageCodecTest {
17+
18+
@ParameterizedTest
19+
@CsvSource(
20+
quoteCharacter = ';', // default is a quote character "'", which collides with %27 character.
21+
value = {
22+
"%21,!", "%23,#", "%24,$", "%25,%", "%26,&", "%27,'", "%28,(", "%29,)", "%2A,*", "%2B,+",
23+
"%2D,-", "%2E,.", "%2F,/", "%30,0", "%31,1", "%32,2", "%33,3", "%34,4", "%35,5", "%36,6",
24+
"%37,7", "%38,8", "%39,9", "%3A,:", "%3C,<", "%3D,=", "%3E,>", "%3F,?", "%40,@", "%41,A",
25+
"%42,B", "%43,C", "%44,D", "%45,E", "%46,F", "%47,G", "%48,H", "%49,I", "%4A,J", "%4B,K",
26+
"%4C,L", "%4D,M", "%4E,N", "%4F,O", "%50,P", "%51,Q", "%52,R", "%53,S", "%54,T", "%55,U",
27+
"%56,V", "%57,W", "%58,X", "%59,Y", "%5A,Z", "%5B,[", "%5D,]", "%5E,^", "%5F,_", "%60,`",
28+
"%61,a", "%62,b", "%63,c", "%64,d", "%65,e", "%66,f", "%67,g", "%68,h", "%69,i", "%6A,j",
29+
"%6B,k", "%6C,l", "%6D,m", "%6E,n", "%6F,o", "%70,p", "%71,q", "%72,r", "%73,s", "%74,t",
30+
"%75,u", "%76,v", "%77,w", "%78,x", "%79,y", "%7A,z", "%7B,{", "%7C,|", "%7D,}", "%7E,~",
31+
})
32+
void shouldDecodePercentEncodedValues(String percentEncoded, String expectedDecoded) {
33+
assertThat(BaggageCodec.decode(percentEncoded, StandardCharsets.UTF_8))
34+
.isEqualTo(expectedDecoded);
35+
}
36+
37+
@Test
38+
void shouldThrowIfMalformedData() {
39+
assertThatThrownBy(() -> BaggageCodec.decode("%1", StandardCharsets.UTF_8))
40+
.isInstanceOf(IllegalArgumentException.class);
41+
}
42+
}

api/all/src/test/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagatorFuzzTest.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
import io.opentelemetry.api.baggage.BaggageEntryMetadata;
1919
import io.opentelemetry.context.Context;
2020
import io.opentelemetry.context.propagation.TextMapGetter;
21+
import java.util.Arrays;
2122
import java.util.HashMap;
23+
import java.util.HashSet;
2224
import java.util.Map;
25+
import java.util.Set;
2326
import javax.annotation.Nullable;
2427
import org.junit.jupiter.api.Test;
2528
import org.junit.runner.Result;
@@ -62,6 +65,17 @@ public void roundTripAsciiValues(
6265
Baggage extractedBaggage = Baggage.fromContext(extractedContext);
6366
assertThat(extractedBaggage).isEqualTo(baggage);
6467
}
68+
69+
@Fuzz
70+
public void baggageOctet(@From(BaggageOctetGenerator.class) String baggageValue) {
71+
Map<String, String> carrier = new HashMap<>();
72+
carrier.put("baggage", "key=" + baggageValue);
73+
Context context =
74+
baggagePropagator.extract(Context.current(), carrier, new MapTextMapGetter());
75+
Baggage baggage = Baggage.fromContext(context);
76+
String value = baggage.getEntryValue("key");
77+
assertThat(value).isEqualTo(baggageValue);
78+
}
6579
}
6680

6781
// driver methods to avoid having to use the vintage junit engine, and to enable increasing the
@@ -79,9 +93,15 @@ void roundTripFuzzing() {
7993
assertThat(result.wasSuccessful()).isTrue();
8094
}
8195

82-
private static Result runTestCase(String roundTripRandomValues) {
96+
@Test
97+
void baggageOctetFuzzing() {
98+
Result result = runTestCase("baggageOctet");
99+
assertThat(result.wasSuccessful()).isTrue();
100+
}
101+
102+
private static Result runTestCase(String testCaseName) {
83103
return GuidedFuzzing.run(
84-
TestCases.class, roundTripRandomValues, new NoGuidance(10000, System.out), System.out);
104+
TestCases.class, testCaseName, new NoGuidance(10000, System.out), System.out);
85105
}
86106

87107
public static class AsciiGenerator extends AbstractStringGenerator {
@@ -109,4 +129,25 @@ public String get(Map<String, String> carrier, String key) {
109129
return carrier.get(key);
110130
}
111131
}
132+
133+
public static class BaggageOctetGenerator extends AbstractStringGenerator {
134+
135+
private static final Set<Character> excluded =
136+
new HashSet<>(Arrays.asList(' ', '"', ',', ';', '\\', '%'));
137+
138+
@Override
139+
protected int nextCodePoint(SourceOfRandomness random) {
140+
while (true) {
141+
char c = random.nextChar(' ', '~');
142+
if (!excluded.contains(c)) {
143+
return c;
144+
}
145+
}
146+
}
147+
148+
@Override
149+
protected boolean codePointInRange(int codePoint) {
150+
return !excluded.contains((char) codePoint);
151+
}
152+
}
112153
}

0 commit comments

Comments
 (0)