diff --git a/sentry/src/main/java/io/sentry/SentryTraceHeader.java b/sentry/src/main/java/io/sentry/SentryTraceHeader.java index 49ffe25e414..71e6d01bc7c 100644 --- a/sentry/src/main/java/io/sentry/SentryTraceHeader.java +++ b/sentry/src/main/java/io/sentry/SentryTraceHeader.java @@ -2,6 +2,8 @@ import io.sentry.exception.InvalidSentryTraceHeaderException; import io.sentry.protocol.SentryId; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +15,11 @@ public final class SentryTraceHeader { private final @NotNull SpanId spanId; private final @Nullable Boolean sampled; + private static final Pattern SENTRY_TRACEPARENT_HEADER_REGEX = + Pattern.compile( + "^[ \\t]*(?[0-9a-f]{32})-(?[0-9a-f]{16})(?-[01])?[ \\t]*$", + Pattern.CASE_INSENSITIVE); + public SentryTraceHeader( final @NotNull SentryId traceId, final @NotNull SpanId spanId, @@ -23,20 +30,17 @@ public SentryTraceHeader( } public SentryTraceHeader(final @NotNull String value) throws InvalidSentryTraceHeaderException { - final String[] parts = value.split("-", -1); - if (parts.length < 2) { + Matcher matcher = SENTRY_TRACEPARENT_HEADER_REGEX.matcher(value); + boolean matchesExist = matcher.matches(); + + if (!matchesExist || matcher.group("traceId") == null || matcher.group("spanId") == null) { throw new InvalidSentryTraceHeaderException(value); - } else if (parts.length == 3) { - this.sampled = "1".equals(parts[2]); - } else { - this.sampled = null; - } - try { - this.traceId = new SentryId(parts[0]); - this.spanId = new SpanId(parts[1]); - } catch (Throwable e) { - throw new InvalidSentryTraceHeaderException(value, e); } + + this.traceId = new SentryId(matcher.group("traceId")); + this.spanId = new SpanId(matcher.group("spanId")); + this.sampled = + matcher.group("sampled") == null ? null : "1".equals(matcher.group("sampled").substring(1)); } public @NotNull String getName() { diff --git a/sentry/src/test/java/io/sentry/SentryTraceHeaderTest.kt b/sentry/src/test/java/io/sentry/SentryTraceHeaderTest.kt index ac01309ee22..f63ef07f33d 100644 --- a/sentry/src/test/java/io/sentry/SentryTraceHeaderTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTraceHeaderTest.kt @@ -6,6 +6,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull +import kotlin.text.substring class SentryTraceHeaderTest { @Test @@ -15,6 +16,92 @@ class SentryTraceHeaderTest { assertEquals("sentry-trace header does not conform to expected format: $sentryId", ex.message) } + @Test + fun `when there is a trailing dash without sampling decision throws exception`() { + val sentryId = SentryId() + val spanId = SpanId() + val ex = + assertFailsWith { SentryTraceHeader("$sentryId-$spanId-") } + assertEquals( + "sentry-trace header does not conform to expected format: $sentryId-$spanId-", + ex.message, + ) + } + + @Test + fun `when trace-id has less than 32 characters throws exception`() { + val sentryId = SentryId().toString().substring(0, 8) + val spanId = SpanId() + val ex = + assertFailsWith { SentryTraceHeader("$sentryId-$spanId") } + assertEquals( + "sentry-trace header does not conform to expected format: $sentryId-$spanId", + ex.message, + ) + } + + @Test + fun `when trace-id has more than 32 characters throws exception`() { + val sentryId = SentryId().toString() + "abc" + val spanId = SpanId() + val ex = + assertFailsWith { SentryTraceHeader("$sentryId-$spanId") } + assertEquals( + "sentry-trace header does not conform to expected format: $sentryId-$spanId", + ex.message, + ) + } + + @Test + fun `when trace-id contains invalid characters throws exception`() { + var sentryId = SentryId().toString() + sentryId = sentryId.substring(0, 8) + "g" + sentryId.substring(8) + val spanId = SpanId() + val ex = + assertFailsWith { SentryTraceHeader("$sentryId-$spanId") } + assertEquals( + "sentry-trace header does not conform to expected format: $sentryId-$spanId", + ex.message, + ) + } + + @Test + fun `when span-id has less than 16 characters throws exception`() { + val sentryId = SentryId() + val spanId = SpanId().toString().substring(0, 8) + val ex = + assertFailsWith { SentryTraceHeader("$sentryId-$spanId") } + assertEquals( + "sentry-trace header does not conform to expected format: $sentryId-$spanId", + ex.message, + ) + } + + @Test + fun `when span-id has more than 32 characters throws exception`() { + val sentryId = SentryId() + val spanId = SpanId().toString() + "abc" + val ex = + assertFailsWith { SentryTraceHeader("$sentryId-$spanId") } + assertEquals( + "sentry-trace header does not conform to expected format: $sentryId-$spanId", + ex.message, + ) + } + + @Test + fun `when span-id contains invalid characters throws exception`() { + val sentryId = SentryId() + var spanId = SpanId().toString() + spanId = spanId.substring(0, 8) + "g" + spanId.substring(8) + val ex = + assertFailsWith { SentryTraceHeader("$sentryId-$spanId") } + assertEquals( + "sentry-trace header does not conform to expected format: $sentryId-$spanId", + ex.message, + ) + } + @Test fun `handles header with positive sampling decision`() { val sentryId = SentryId()