Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better error message for HostAndPort containing a protocol #983

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from

Conversation

ash211
Copy link
Contributor

@ash211 ash211 commented Apr 20, 2023

Before this PR

There was an internal report (ptcom/public-docs/pull/1359) of a long debugging session that root caused to incorrectly specifying hostAndPort: https://proxy-host.com:3128 instead of hostAndPort: proxy-host.com:3128 in this configuration.

Before this PR, that would be easy to miss in the error message:

Given hostname does not contain a port number: {hostname=[http://squid:3128]}

After this PR

==COMMIT_MSG==
Better error message for HostAndPort containing a protocol
==COMMIT_MSG==

With this PR, the error message would instead be:

hostPortString must not contain a protocol prefix like http:// or https://

To see the behavior change more clearly, look at just the second commit's diff (the first commit adds tests establishing current behavior).

Possible downsides?

This PR performs more validation in the HostAndPort class, so any existing instances of values containing :// would now start failing to deserialize. The only usage of HostAndPort in this repo is in ProxyConfiguration which already fails during deserialization:

HostAndPort host = HostAndPort.fromString(hostAndPort().get());
Preconditions.checkArgument(
host.hasPort(), "Given hostname does not contain a port number", SafeArg.of("hostname", host));
. So there would have to be a usage of HostAndPort directly, not via ProxyConfiguration. Searching internal sourcegraph for import com.palantir.conjure.java.api.config.service.HostAndPort I see only one repo doing this.

@changelog-app
Copy link

changelog-app bot commented Apr 20, 2023

Generate changelog in changelog/@unreleased

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

Better error message for HostAndPort containing a protocol

Check the box to generate changelog(s)

  • Generate changelog entry

@ash211 ash211 requested a review from carterkozak April 20, 2023 23:01
Comment on lines +97 to +105
// Additional validation for common errors. An alternative to this diff between conjure HostAndPort and
// Guava HostAndPort would be to use Guava's HostSpecifier.from() here instead. But that would require
// either re-adding a Guava dependency (removed earlier in this repo's history), or copying that class
// and its dependencies (InetAddresses, InternetDomainName, Ascii.toLowerCase, Splitter, Joiner, and more)
// into this repo like we did with HostAndPort.
Preconditions.checkArgument(
!host.contains("://"),
"hostPortString must not contain a protocol prefix like http:// or https://",
UnsafeArg.of("hostPortString", hostPortString));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Should this implement RFC 1123 hostname/IPv4/IPv6 validations for host?
  • Should we allow creation of HostAndPort from a valid RFC 2396 URI via java.net.URI#create(String) with something like the following (though I hate the exception as control flow)?
  • Should the proxy configuration deserialization & checks actually handle this more gracefully?

protected final void check() {
switch (type()) {
case MESH:
case HTTP:
Preconditions.checkArgument(
hostAndPort().isPresent(), "host-and-port must be configured for an HTTP proxy");
HostAndPort host = HostAndPort.fromString(hostAndPort().get());
Preconditions.checkArgument(
host.hasPort(), "Given hostname does not contain a port number", SafeArg.of("hostname", host));
break;

    @Nullable
    private static HostAndPort tryParseUri(String hostPortString) {
        try {
            URI uri = URI.create(hostPortString);
            return new HostAndPort(uri.getHost(), uri.getPort());
        } catch (RuntimeException e) {
            return null;
        }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think full standards-compliant host validation like RFC 1123 would be idea. This split host+port pair ends up being sent into InetSocketAddress.createUnresolved at https://github.com/palantir/conjure-java-runtime/blob/33525f67c7acc31106ef4311f2fd2f9ef3ac92ae/client-config/src/main/java/com/palantir/conjure/java/client/config/ClientConfigurations.java#L173

So we would preferably match whatever validation that the JDK does when opening the socket.

  1. that might be nice. So instead of providing a better exception message for hostAndPort: http://example.com:8080, accept it as equivalent to example.com:8080

  2. By handling deser and checks more gracefully, do you mean deserialize successfully but into an error state, rather than failing to deserialize?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the deserialization more lenient to permit host-and-port: https://your.proxy.host.example.com:1234 seems reasonable to me, but I'm not super close to all the consumers here and would defer to @carterkozak. I think we would still want to check and throw if one specified a URI scheme not in {http, https, mesh, socks} or if non-scheme/host/port elements of URI are specified (e.g. path, authority, etc.).

For 3, I was just referring to the serDe() test above and adding similar test cases that reads raw json (or yaml) with various valid & invalid host-and-port: URIs

@Test
public void serDe() throws Exception {
ProxyConfiguration config = ProxyConfiguration.of("host:80", BasicCredentials.of("username", "password"));
String camelCase = "{\"hostAndPort\":\"host:80\",\"credentials\":{\"username\":\"username\","
+ "\"password\":\"password\"},\"type\":\"HTTP\"}";
String kebabCase = "{\"host-and-port\":\"host:80\",\"credentials\":{\"username\":\"username\","
+ "\"password\":\"password\"},\"type\":\"HTTP\"}";
assertThat(ObjectMappers.newClientObjectMapper().writeValueAsString(config))
.isEqualTo(camelCase);
assertThat(ObjectMappers.newClientObjectMapper().readValue(camelCase, ProxyConfiguration.class))
.isEqualTo(config);
assertThat(ObjectMappers.newClientObjectMapper().readValue(kebabCase, ProxyConfiguration.class))
.isEqualTo(config);
}

Comment on lines +34 to +53
@Test
void fromString_onHostAndPortWithHttpProtocolPrefix_doesNotThrow() {
assertThatLoggableExceptionThrownBy(() -> HostAndPort.fromString("http://example.com:8080"))
.hasMessageContaining("hostPortString must not contain a protocol prefix like http:// or https://")
.containsArgs(UnsafeArg.of("hostPortString", "http://example.com:8080"));
}

@Test
void fromString_onHostAndPortWithHttpsProtocolPrefix_doesNotThrow() {
assertThatLoggableExceptionThrownBy(() -> HostAndPort.fromString("https://example.com:8080"))
.hasMessageContaining("hostPortString must not contain a protocol prefix like http:// or https://")
.containsArgs(UnsafeArg.of("hostPortString", "https://example.com:8080"));
}

@Test
void fromString_onHostAndPortWithAbcProtocolPrefix_doesNotThrow() {
assertThatLoggableExceptionThrownBy(() -> HostAndPort.fromString("abc://example.com:8080"))
.hasMessageContaining("hostPortString must not contain a protocol prefix like http:// or https://")
.containsArgs(UnsafeArg.of("hostPortString", "abc://example.com:8080"));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the test names *_doesNotThrow() don't seem to match expectation that this actually throws

Comment on lines +154 to +160
public void incorrectlyFormattedHostAndPort() {
assertThatLoggableExceptionThrownBy(() -> ProxyConfiguration.builder()
.hostAndPort("http://squid:3128")
.type(ProxyConfiguration.Type.HTTP)
.build())
.hasMessageContaining("hostPortString must not contain a protocol prefix like http:// or https://")
.containsArgs(UnsafeArg.of("hostPortString", "http://squid:3128"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add similar tests for these to serDe?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants