diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParser.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParser.java index 617b8d707ce..cbebc626e16 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParser.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParser.java @@ -1,6 +1,7 @@ package datadog.trace.bootstrap.instrumentation.jdbc; import static datadog.trace.bootstrap.instrumentation.jdbc.DBInfo.DEFAULT; +import static datadog.trace.util.IntStringUtils.parseNonNegativeInt; import static java.lang.Math.max; import datadog.trace.api.Pair; @@ -140,7 +141,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { final int portLoc = serverName.indexOf(':'); if (portLoc > 1) { - port = Integer.parseInt(serverName.substring(portLoc + 1)); + port = parsePort(serverName, portLoc + 1, serverName.length()); serverName = serverName.substring(0, portLoc); } @@ -221,10 +222,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { if (portLoc > 0) { hostEndLoc = portLoc; - try { - builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, dbLoc))); - } catch (final NumberFormatException ignored) { - } + setPort(builder, parsePort(jdbcUrl, portLoc + 1, dbLoc)); } else { hostEndLoc = dbLoc; } @@ -268,10 +266,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { if (portLoc > 0) { hostEndLoc = portLoc; final int portEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; - try { - builder.port(Integer.parseInt(jdbcUrl.substring(portLoc + 1, portEndLoc))); - } catch (final NumberFormatException ignored) { - } + setPort(builder, parsePort(jdbcUrl, portLoc + 1, portEndLoc)); } else { hostEndLoc = clusterSepLoc > 0 ? clusterSepLoc : dbLoc; } @@ -303,7 +298,7 @@ DBInfo.Builder doParse(String jdbcUrl, final DBInfo.Builder builder) { final Matcher portMatcher = PORT_REGEX.matcher(jdbcUrl); if (portMatcher.find()) { - builder.port(Integer.parseInt(portMatcher.group(1))); + setPort(builder, parsePort(portMatcher.group(1))); } final Matcher userMatcher = USER_REGEX.matcher(jdbcUrl); @@ -390,31 +385,37 @@ DBInfo.Builder doParse(String jdbcUrl, final DBInfo.Builder builder) { ORACLE_CONNECT_INFO() { @Override - DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { + DBInfo.Builder doParse(final String connectInfo, final DBInfo.Builder builder) { final String host; final Integer port; final String instance; - final int hostEnd = jdbcUrl.indexOf(':'); - final int instanceLoc = jdbcUrl.indexOf('/'); + // EZConnect Plus parameters (?key=value) are not part of the address + final int queryLoc = connectInfo.indexOf('?'); + final String jdbcUrl = queryLoc >= 0 ? connectInfo.substring(0, queryLoc) : connectInfo; + + // skip a bracketed IPv6 literal so its colons are not taken as the port separator + final int hostSearchStart = jdbcUrl.startsWith("[") ? Math.max(jdbcUrl.indexOf(']'), 0) : 0; + final int instanceLoc = jdbcUrl.indexOf('/', hostSearchStart); + final int colonLoc = jdbcUrl.indexOf(':', hostSearchStart); + // a ':' after the service name separates the server type, not the port + final int hostEnd = instanceLoc >= 0 && colonLoc > instanceLoc ? -1 : colonLoc; if (hostEnd > 0) { host = jdbcUrl.substring(0, hostEnd); final int afterHostEnd = jdbcUrl.indexOf(':', hostEnd + 1); - if (afterHostEnd > 0) { - port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, afterHostEnd)); + if (afterHostEnd > 0 && (instanceLoc < 0 || afterHostEnd < instanceLoc)) { + // host:port:sid + port = parsePort(jdbcUrl, hostEnd + 1, afterHostEnd); instance = jdbcUrl.substring(afterHostEnd + 1); } else { if (instanceLoc > 0) { - instance = jdbcUrl.substring(instanceLoc + 1); - port = Integer.parseInt(jdbcUrl.substring(hostEnd + 1, instanceLoc)); + // host:port/service[:server_type][/instance_name] + instance = serviceName(jdbcUrl.substring(instanceLoc + 1)); + port = parsePort(jdbcUrl, hostEnd + 1, instanceLoc); } else { final String portOrInstance = jdbcUrl.substring(hostEnd + 1); - Integer parsedPort = null; - try { - parsedPort = Integer.parseInt(portOrInstance); - } catch (final NumberFormatException ignored) { - } + final Integer parsedPort = parsePort(portOrInstance); if (parsedPort == null) { port = null; instance = portOrInstance; @@ -428,7 +429,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { if (instanceLoc > 0) { host = jdbcUrl.substring(0, instanceLoc); port = null; - instance = jdbcUrl.substring(instanceLoc + 1); + instance = serviceName(jdbcUrl.substring(instanceLoc + 1)); } else { if (jdbcUrl.isEmpty()) { return builder; @@ -447,6 +448,20 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { } return builder.instance(instance); } + + /** Drops the optional {@code :server_type} and {@code /instance_name} EZConnect suffixes. */ + private String serviceName(final String service) { + int end = service.length(); + final int serverTypeLoc = service.indexOf(':'); + if (serverTypeLoc >= 0) { + end = serverTypeLoc; + } + final int instanceNameLoc = service.indexOf('/'); + if (instanceNameLoc >= 0 && instanceNameLoc < end) { + end = instanceNameLoc; + } + return service.substring(0, end); + } }, ORACLE_AT() { @@ -469,10 +484,12 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { } final int hostStart; + final int protocolEnd = connectInfo.indexOf("://"); if (connectInfo.startsWith("//")) { hostStart = "//".length(); - } else if (connectInfo.startsWith("ldap://")) { - hostStart = "ldap://".length(); + } else if (protocolEnd > 0 && protocolEnd == connectInfo.indexOf(':')) { + // protocol prefix, e.g. ldap://, tcp://, tcps:// + hostStart = protocolEnd + "://".length(); } else { hostStart = 0; } @@ -511,7 +528,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { final Matcher portMatcher = PORT_REGEX.matcher(urlPart2); if (portMatcher.find()) { - builder.port(Integer.parseInt(portMatcher.group(1))); + setPort(builder, parsePort(portMatcher.group(1))); } final Matcher instanceMatcher = INSTANCE_REGEX.matcher(urlPart2); @@ -697,7 +714,7 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { final int portLoc = url.indexOf(':'); if (portLoc > 0) { host = url.substring(0, portLoc); - builder.port(Integer.parseInt(url.substring(portLoc + 1))); + setPort(builder, parsePort(url, portLoc + 1, url.length())); } else { host = url; } @@ -726,6 +743,10 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { final int protoLoc = jdbcUrl.indexOf("://"); final int typeEndLoc = dbInfo.getType().length(); + if (protoLoc <= typeEndLoc) { + // no "jtds:://" prefix to parse + return builder; + } final String subtype = jdbcUrl.substring(typeEndLoc + 1, protoLoc); builder.subtype(subtype); @@ -740,40 +761,34 @@ DBInfo.Builder doParse(final String jdbcUrl, final DBInfo.Builder builder) { } } + // [:][/][;=[;...]] final String details = jdbcUrl.substring(protoLoc + "://".length()); - final int hostEndLoc; - final int portLoc = details.indexOf(':', typeEndLoc + 1); - final int dbLoc = details.indexOf('/', typeEndLoc); - final int paramLoc = details.indexOf(';', dbLoc); - - if (paramLoc > 0) { + final int paramLoc = details.indexOf(';'); + final String address; + if (paramLoc >= 0) { populateStandardProperties(builder, splitQuery(details.substring(paramLoc + 1), ';')); - if (dbLoc > 0) { - builder.db(details.substring(dbLoc + 1, paramLoc)); - } + address = details.substring(0, paramLoc); } else { - if (dbLoc > 0) { - builder.db(details.substring(dbLoc + 1)); - } + address = details; } - if (portLoc > 0) { - hostEndLoc = portLoc; - final int portEndLoc = dbLoc > 0 ? dbLoc : (paramLoc > 0 ? paramLoc : details.length()); - try { - builder.port(Integer.parseInt(details.substring(portLoc + 1, portEndLoc))); - } catch (final NumberFormatException ignored) { - } - } else if (dbLoc > 0) { - hostEndLoc = dbLoc; - } else if (paramLoc > 0) { - hostEndLoc = paramLoc; + final int dbLoc = address.indexOf('/'); + final String hostAndPort; + if (dbLoc >= 0) { + builder.db(address.substring(dbLoc + 1)); + hostAndPort = address.substring(0, dbLoc); } else { - hostEndLoc = details.length(); + hostAndPort = address; } - builder.host(details.substring(0, hostEndLoc)); + final int portLoc = hostAndPort.indexOf(':'); + if (portLoc >= 0) { + setPort(builder, parsePort(hostAndPort, portLoc + 1, hostAndPort.length())); + builder.host(hostAndPort.substring(0, portLoc)); + } else { + builder.host(hostAndPort); + } return builder; } @@ -900,6 +915,29 @@ public static DBInfo parse(String connectionUrl, final Properties props) { // Source: https://stackoverflow.com/a/13592567 @SuppressForbidden + /** + * @return the port, or {@code null} if {@code s[start, end)} is not a valid port number + */ + private static Integer parsePort(final CharSequence s, final int start, final int end) { + final int port = parseNonNegativeInt(s, start, end - start); + return port >= 0 ? port : null; + } + + /** + * @return the port, or {@code null} if {@code s} is null or not a valid port number + */ + private static Integer parsePort(final String s) { + final int port = parseNonNegativeInt(s); + return port >= 0 ? port : null; + } + + /** Sets the port only when one was parsed, keeping any default already on the builder. */ + private static void setPort(final DBInfo.Builder builder, final Integer port) { + if (port != null) { + builder.port(port); + } + } + private static Map splitQuery(final String query, final char separator) { if (query == null || query.isEmpty()) { return Collections.emptyMap(); @@ -956,12 +994,7 @@ private static void populateStandardProperties( } if (props.containsKey("portnumber")) { - final String portNumber = (String) props.get("portnumber"); - try { - builder.port(Integer.parseInt(portNumber)); - } catch (final NumberFormatException e) { - ExceptionLogger.LOGGER.debug("Error parsing portnumber property: {}", portNumber, e); - } + setPort(builder, parsePort((String) props.get("portnumber"))); } if (props.containsKey("servicename")) { // this property is used to specify the db to use for Sybase connection strings @@ -969,12 +1002,7 @@ private static void populateStandardProperties( } if (props.containsKey("portNumber")) { - final String portNumber = (String) props.get("portNumber"); - try { - builder.port(Integer.parseInt(portNumber)); - } catch (final NumberFormatException e) { - ExceptionLogger.LOGGER.debug("Error parsing portNumber property: {}", portNumber, e); - } + setPort(builder, parsePort((String) props.get("portNumber"))); } } } diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserJtdsTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserJtdsTest.java new file mode 100644 index 00000000000..2c652abf324 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserJtdsTest.java @@ -0,0 +1,36 @@ +package datadog.trace.bootstrap.instrumentation.jdbc; + +import static datadog.trace.bootstrap.instrumentation.jdbc.JDBCConnectionUrlParser.extractDBInfo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.tabletest.junit.TableTest; + +/** + * Tests for jTDS URLs ({@code jdbc:jtds:://[:][/][;]}). + * + *

The port and database separators used to be searched for across the whole remainder of the + * URL, starting at an offset unrelated to it, so a ':' or '/' in a property value or a short host + * name produced a {@code StringIndexOutOfBoundsException} or a wrong host. + */ +class JDBCConnectionUrlParserJtdsTest { + + @TableTest({ + "scenario | url | subtype | host | port | db ", + "host port and db | jdbc:jtds:sqlserver://dbhost:1500/mydb | sqlserver | dbhost | 1500 | mydb", + "colon in property after db | jdbc:jtds:sqlserver://dbhost:1500/mydb;appname=a:b | sqlserver | dbhost | 1500 | mydb", + "colon in property, no port | jdbc:jtds:sqlserver://dbhost/mydb;password=p:w | sqlserver | dbhost | 1433 | mydb", + "colon in property, no db | jdbc:jtds:sqlserver://dbhost;password=p:w | sqlserver | dbhost | 1433 | ", + "slash in property, no db | jdbc:jtds:sqlserver://dbhost;password=p/w | sqlserver | dbhost | 1433 | ", + "short host with port | jdbc:jtds:sqlserver://db:1500/mydb | sqlserver | db | 1500 | mydb", + "sybase with port | jdbc:jtds:sybase://dbhost:7200/mydb | sybase | dbhost | 7200 | mydb", + "missing protocol separator | jdbc:jtds:sqlserver:dbhost | | | | " + }) + void parsesJtdsUrls(String url, String subtype, String host, Integer port, String db) { + DBInfo info = extractDBInfo(url, null); + assertEquals("jtds", info.getType()); + assertEquals(subtype, info.getSubtype()); + assertEquals(host, info.getHost()); + assertEquals(port, info.getPort()); + assertEquals(db, info.getDb()); + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserOracleTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserOracleTest.java new file mode 100644 index 00000000000..2f55579d3f6 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/jdbc/JDBCConnectionUrlParserOracleTest.java @@ -0,0 +1,39 @@ +package datadog.trace.bootstrap.instrumentation.jdbc; + +import static datadog.trace.bootstrap.instrumentation.jdbc.JDBCConnectionUrlParser.extractDBInfo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.tabletest.junit.TableTest; + +/** + * Tests for Oracle {@code @} connect strings that previously threw {@code NumberFormatException}. + * + *

A second ':' after the port (EZConnect {@code :server_type}, IPv6 literals, {@code ?params}) + * or a protocol prefix other than {@code ldap://} was taken as the {@code host:port:sid} form, so a + * non-numeric segment was parsed as the port and the whole URL fell back to defaults. + */ +class JDBCConnectionUrlParserOracleTest { + + @TableTest({ + "scenario | url | host | port | instance", + "SID form | jdbc:oracle:thin:@orcl.host:55:orclsn | orcl.host | 55 | orclsn ", + "service form | jdbc:oracle:thin:@//orcl.host:55/orclsn | orcl.host | 55 | orclsn ", + "service with server type | jdbc:oracle:thin:@//orcl.host:55/orclsn:dedicated | orcl.host | 55 | orclsn ", + "service with type and instance | jdbc:oracle:thin:@//orcl.host:55/orclsn:pooled/inst1 | orcl.host | 55 | orclsn ", + "service with instance name | jdbc:oracle:thin:@//orcl.host:55/orclsn/inst1 | orcl.host | 55 | orclsn ", + "service without port | jdbc:oracle:thin:@//orcl.host/orclsn:dedicated | orcl.host | 1521 | orclsn ", + "tcps protocol | jdbc:oracle:thin:@tcps://orcl.host:2484/orclsn | orcl.host | 2484 | orclsn ", + "tcp protocol | jdbc:oracle:thin:@tcp://orcl.host:55/orclsn | orcl.host | 55 | orclsn ", + "EZConnect Plus params | jdbc:oracle:thin:@tcps://orcl.host:2484/orclsn?wallet_location=c:/w | orcl.host | 2484 | orclsn ", + "IPv6 literal | jdbc:oracle:thin:@//[::1]:55/orclsn | '[::1]' | 55 | orclsn ", + "non-numeric port | jdbc:oracle:thin:@orcl.host:abc:orclsn | orcl.host | 1521 | orclsn " + }) + void parsesOracleConnectStrings(String url, String host, Integer port, String instance) { + DBInfo info = extractDBInfo(url, null); + assertEquals("oracle", info.getType()); + assertEquals("thin", info.getSubtype()); + assertEquals(host, info.getHost()); + assertEquals(port, info.getPort()); + assertEquals(instance, info.getInstance()); + } +} diff --git a/dd-java-agent/instrumentation/play/play-2.3/src/main/java/datadog/trace/instrumentation/play23/RequestURIDataAdapter.java b/dd-java-agent/instrumentation/play/play-2.3/src/main/java/datadog/trace/instrumentation/play23/RequestURIDataAdapter.java index f91358d52f4..08d9e681e29 100644 --- a/dd-java-agent/instrumentation/play/play-2.3/src/main/java/datadog/trace/instrumentation/play23/RequestURIDataAdapter.java +++ b/dd-java-agent/instrumentation/play/play-2.3/src/main/java/datadog/trace/instrumentation/play23/RequestURIDataAdapter.java @@ -1,5 +1,8 @@ package datadog.trace.instrumentation.play23; +import static java.lang.Math.max; + +import datadog.trace.bootstrap.instrumentation.api.HostHeader; import datadog.trace.bootstrap.instrumentation.api.URIRawDataAdapter; import play.api.mvc.Request; @@ -11,9 +14,10 @@ final class RequestURIDataAdapter extends URIRawDataAdapter { RequestURIDataAdapter(Request request) { this.request = request; - int split = request.host().lastIndexOf(':'); - this.host = split == -1 ? request.host() : request.host().substring(0, split); - this.port = split == -1 ? 0 : Integer.parseInt(request.host().substring(split + 1)); + // the Host header comes from the client, so the port may be missing or malformed + final String hostHeader = request.host(); + this.host = HostHeader.host(hostHeader); + this.port = max(HostHeader.port(hostHeader), 0); } @Override diff --git a/dd-java-agent/instrumentation/play/play-2.4/src/main/java/datadog/trace/instrumentation/play24/RequestURIDataAdapter.java b/dd-java-agent/instrumentation/play/play-2.4/src/main/java/datadog/trace/instrumentation/play24/RequestURIDataAdapter.java index 0145913ed7e..864b4caee72 100644 --- a/dd-java-agent/instrumentation/play/play-2.4/src/main/java/datadog/trace/instrumentation/play24/RequestURIDataAdapter.java +++ b/dd-java-agent/instrumentation/play/play-2.4/src/main/java/datadog/trace/instrumentation/play24/RequestURIDataAdapter.java @@ -1,5 +1,8 @@ package datadog.trace.instrumentation.play24; +import static java.lang.Math.max; + +import datadog.trace.bootstrap.instrumentation.api.HostHeader; import datadog.trace.bootstrap.instrumentation.api.URIRawDataAdapter; import play.api.mvc.Request; @@ -11,9 +14,10 @@ final class RequestURIDataAdapter extends URIRawDataAdapter { RequestURIDataAdapter(Request request) { this.request = request; - int split = request.host().lastIndexOf(':'); - this.host = split == -1 ? request.host() : request.host().substring(0, split); - this.port = split == -1 ? 0 : Integer.parseInt(request.host().substring(split + 1)); + // the Host header comes from the client, so the port may be missing or malformed + final String hostHeader = request.host(); + this.host = HostHeader.host(hostHeader); + this.port = max(HostHeader.port(hostHeader), 0); } @Override diff --git a/dd-java-agent/instrumentation/play/play-2.6/src/main/java/datadog/trace/instrumentation/play26/RequestURIDataAdapter.java b/dd-java-agent/instrumentation/play/play-2.6/src/main/java/datadog/trace/instrumentation/play26/RequestURIDataAdapter.java index d2bd4ba01a1..1535353b84b 100644 --- a/dd-java-agent/instrumentation/play/play-2.6/src/main/java/datadog/trace/instrumentation/play26/RequestURIDataAdapter.java +++ b/dd-java-agent/instrumentation/play/play-2.6/src/main/java/datadog/trace/instrumentation/play26/RequestURIDataAdapter.java @@ -1,5 +1,8 @@ package datadog.trace.instrumentation.play26; +import static java.lang.Math.max; + +import datadog.trace.bootstrap.instrumentation.api.HostHeader; import datadog.trace.bootstrap.instrumentation.api.URIRawDataAdapter; import play.api.mvc.Request; @@ -11,9 +14,10 @@ final class RequestURIDataAdapter extends URIRawDataAdapter { RequestURIDataAdapter(Request request) { this.request = request; - int split = request.host().lastIndexOf(':'); - this.host = split == -1 ? request.host() : request.host().substring(0, split); - this.port = split == -1 ? 0 : Integer.parseInt(request.host().substring(split + 1)); + // the Host header comes from the client, so the port may be missing or malformed + final String hostHeader = request.host(); + this.host = HostHeader.host(hostHeader); + this.port = max(HostHeader.port(hostHeader), 0); } @Override diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/HostHeader.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/HostHeader.java new file mode 100644 index 00000000000..59341e557b7 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/HostHeader.java @@ -0,0 +1,46 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static datadog.trace.util.IntStringUtils.parseNonNegativeInt; + +import javax.annotation.Nullable; + +/** + * Splits an HTTP {@code Host} header value ({@code host}, {@code host:port}, {@code [ipv6]} or + * {@code [ipv6]:port}) without throwing. The value comes from the client, so it may be malformed. + */ +public final class HostHeader { + + private HostHeader() {} + + /** + * @return the index of the ':' separating the host from the port, or {@code -1} if there is no + * port; a ':' inside a bracketed IPv6 literal is not a separator + */ + public static int portSeparator(@Nullable String hostHeader) { + if (hostHeader == null) { + return -1; + } + final int colon = hostHeader.lastIndexOf(':'); + return colon > hostHeader.lastIndexOf(']') ? colon : -1; + } + + /** + * @return the host part, or the whole value if there is no port + */ + @Nullable + public static String host(@Nullable String hostHeader) { + final int separator = portSeparator(hostHeader); + return separator == -1 ? hostHeader : hostHeader.substring(0, separator); + } + + /** + * @return the port, or {@code -1} if there is no port or it is not a valid number + */ + public static int port(@Nullable String hostHeader) { + final int separator = portSeparator(hostHeader); + if (separator == -1) { + return -1; + } + return parseNonNegativeInt(hostHeader, separator + 1, hostHeader.length() - separator - 1); + } +} diff --git a/internal-api/src/main/java/datadog/trace/util/IntStringUtils.java b/internal-api/src/main/java/datadog/trace/util/IntStringUtils.java new file mode 100644 index 00000000000..9d18f830d20 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/util/IntStringUtils.java @@ -0,0 +1,41 @@ +package datadog.trace.util; + +import javax.annotation.Nullable; + +/** Exception-free {@code int} parsing, the {@code int} counterpart of {@code LongStringUtils}. */ +public final class IntStringUtils { + + private IntStringUtils() {} + + /** + * Parses a non-negative decimal {@code int} from the whole sequence without throwing. + * + * @return the parsed value, or {@code -1} if {@code s} is null or empty, contains anything other + * than ASCII digits (including a sign), or overflows {@code int} + */ + public static int parseNonNegativeInt(@Nullable CharSequence s) { + return s == null ? -1 : parseNonNegativeInt(s, 0, s.length()); + } + + /** + * Parses a non-negative decimal {@code int} from the {@code len} characters of {@code s} starting + * at {@code start}, without throwing or allocating. + * + * @return the parsed value, or {@code -1} if the range is empty or out of bounds, contains + * anything other than ASCII digits (including a sign), or overflows {@code int} + */ + public static int parseNonNegativeInt(@Nullable CharSequence s, int start, int len) { + if (s == null || start < 0 || len <= 0 || start > s.length() - len) { + return -1; + } + int result = 0; + for (int i = start, end = start + len; i < end; i++) { + int digit = s.charAt(i) - '0'; + if (digit < 0 || digit > 9 || result > (Integer.MAX_VALUE - digit) / 10) { + return -1; + } + result = result * 10 + digit; + } + return result; + } +} diff --git a/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/HostHeaderTest.java b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/HostHeaderTest.java new file mode 100644 index 00000000000..7610ae48b69 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/bootstrap/instrumentation/api/HostHeaderTest.java @@ -0,0 +1,35 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class HostHeaderTest { + + @TableTest({ + "scenario | header | host | port", + "host only | example.com | example.com | -1 ", + "host and port | example.com:8080 | example.com | 8080", + "IPv4 and port | 10.0.0.1:80 | 10.0.0.1 | 80 ", + "IPv6 only | '[::1]' | '[::1]' | -1 ", + "IPv6 and port | '[::1]:8443' | '[::1]' | 8443", + "empty port | example.com: | example.com | -1 ", + "non-numeric port | example.com:http | example.com | -1 ", + "port overflow | example.com:99999999999 | example.com | -1 ", + "negative port | example.com:-1 | example.com | -1 ", + "empty | '' | '' | -1 " + }) + void splitsHostHeader(String header, String host, int port) { + assertEquals(host, HostHeader.host(header)); + assertEquals(port, HostHeader.port(header)); + } + + @Test + void nullHeader() { + assertNull(HostHeader.host(null)); + assertEquals(-1, HostHeader.port(null)); + assertEquals(-1, HostHeader.portSeparator(null)); + } +} diff --git a/internal-api/src/test/java/datadog/trace/util/IntStringUtilsTest.java b/internal-api/src/test/java/datadog/trace/util/IntStringUtilsTest.java new file mode 100644 index 00000000000..50e12c5e0b2 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/util/IntStringUtilsTest.java @@ -0,0 +1,51 @@ +package datadog.trace.util; + +import static datadog.trace.util.IntStringUtils.parseNonNegativeInt; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; + +class IntStringUtilsTest { + + @TableTest({ + "scenario | input | expected ", + "zero | 0 | 0 ", + "port | 1521 | 1521 ", + "leading zeros | 007 | 7 ", + "int max | 2147483647 | 2147483647", + "int max plus one | 2147483648 | -1 ", + "far overflow | 99999999999 | -1 ", + "empty | '' | -1 ", + "minus sign | -1 | -1 ", + "plus sign | +1 | -1 ", + "letters | abc | -1 ", + "trailing garbage | 12a | -1 ", + "whitespace | ' 1' | -1 ", + "non-ASCII digit | '\u0661' | -1 " + }) + void parseNonNegativeIntWholeSequence(String input, int expected) { + assertEquals(expected, parseNonNegativeInt(input)); + } + + @TableTest({ + "scenario | input | start | len | expected", + "middle of string | host:1521/db | 5 | 4 | 1521 ", + "to end | host:1521 | 5 | 4 | 1521 ", + "empty range | host:/db | 5 | 0 | -1 ", + "negative length | host:1521 | 6 | -1 | -1 ", + "negative start | 1521 | -1 | 4 | -1 ", + "past end | 1521 | 1 | 4 | -1 ", + "start past length | 1521 | 5 | 1 | -1 ", + "range spans garbage | host:1521/db | 4 | 5 | -1 " + }) + void parseNonNegativeIntRange(String input, int start, int len, int expected) { + assertEquals(expected, parseNonNegativeInt(input, start, len)); + } + + @Test + void parseNonNegativeIntNull() { + assertEquals(-1, parseNonNegativeInt(null)); + assertEquals(-1, parseNonNegativeInt(null, 0, 0)); + } +}