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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-983.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: improvement
improvement:
description: Better error message for HostAndPort containing a protocol
links:
- https://github.com/palantir/conjure-java-runtime-api/pull/983
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@

import com.palantir.logsafe.Preconditions;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.UnsafeArg;
import org.jetbrains.annotations.VisibleForTesting;

/** See Guava's {@code HostAndPort}. */
/** See Guava's {@code HostAndPort}. This class is a re-implementation that throws safelog exceptions. */
public final class HostAndPort {
/** Magic value indicating the absence of a port number. */
private static final int NO_PORT = -1;
Expand Down Expand Up @@ -92,6 +94,16 @@ public static HostAndPort fromString(String hostPortString) {
isValidPort(port), "Port number out of range", SafeArg.of("port", hostPortString));
}

// 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));
Comment on lines +97 to +105
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);
}


return new HostAndPort(host, port);
}

Expand Down Expand Up @@ -147,4 +159,14 @@ public String toString() {
private static boolean isValidPort(int port) {
return port >= 0 && port <= 65535;
}

@VisibleForTesting
String getHost() {
return host;
}

@VisibleForTesting
int getPort() {
return port;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* (c) Copyright 2023 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.conjure.java.api.config.service;

import static com.palantir.logsafe.testing.Assertions.assertThatLoggableExceptionThrownBy;
import static org.assertj.core.api.Assertions.assertThat;

import com.palantir.logsafe.UnsafeArg;
import org.junit.jupiter.api.Test;

class HostAndPortTest {

@Test
void fromString_onHostAndPort_succeeds() {
HostAndPort hostAndPort = HostAndPort.fromString("example.com:8080");
assertThat(hostAndPort.getHost()).isEqualTo("example.com");
assertThat(hostAndPort.getPort()).isEqualTo(8080);
}

@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"));
}
Comment on lines +34 to +53
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

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.palantir.conjure.java.api.config.service;

import static com.palantir.logsafe.testing.Assertions.assertThatLoggableExceptionThrownBy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

Expand All @@ -24,6 +25,7 @@
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.google.common.io.Resources;
import com.palantir.conjure.java.api.ext.jackson.ObjectMappers;
import com.palantir.logsafe.UnsafeArg;
import java.io.IOException;
import java.net.URL;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -147,4 +149,14 @@ public void serDe() throws Exception {
assertThat(ObjectMappers.newClientObjectMapper().readValue(kebabCase, ProxyConfiguration.class))
.isEqualTo(config);
}

@Test
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"));
Comment on lines +154 to +160
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?

}
}