Skip to content

Commit 47d40a6

Browse files
committed
Endpoint URI validation
1 parent 451bb4e commit 47d40a6

7 files changed

Lines changed: 133 additions & 27 deletions

File tree

src/main/java/emissary/grpc/GrpcRoutingPlace.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import com.google.common.util.concurrent.ListenableFuture;
1111
import com.google.protobuf.Message;
12+
import io.grpc.ChannelCredentials;
13+
import io.grpc.InsecureChannelCredentials;
1214
import io.grpc.ManagedChannel;
1315
import io.grpc.Status;
1416
import io.grpc.StatusRuntimeException;
@@ -113,9 +115,11 @@ private void configureGrpc() {
113115

114116
RetryHandler retryHandler = new RetryHandler(configG, this.getPlaceName(), this::retryOnException);
115117
String channelManagerName = configG.findStringEntry(CHANNEL_MANAGER_CLASS_NAME, PooledChannelManager.class.getName());
118+
ChannelCredentials channelCredentials = getChannelCredentials(configG);
116119

117120
for (String id : targetIds) {
118-
ChannelManager channelManager = ChannelManager.ofSubClass(channelManagerName, hosts.get(id), ports.get(id), configG);
121+
ChannelManager channelManager = ChannelManager.ofSubClass(
122+
channelManagerName, hosts.get(id), ports.get(id), configG, channelCredentials);
119123
GrpcInvoker grpcInvoker = new GrpcInvoker(channelManager, retryHandler);
120124
invokerTable.put(id, grpcInvoker);
121125
}
@@ -130,6 +134,18 @@ protected Map<String, Integer> getPortNumberConfigs() {
130134
.collect(Collectors.toMap(Map.Entry::getKey, entry -> Integer.parseInt(entry.getValue())));
131135
}
132136

137+
/**
138+
* Creates a new {@link ChannelCredentials} object with all security and encryption configurations necessary for
139+
* establishing a gRPC connection. Base class returns {@link InsecureChannelCredentials}, which asserts that no client
140+
* identity, authentication, or encryption is to be used. Subclasses should override this method as necessary.
141+
*
142+
* @param configG any necessary configurations for creating the credentials object
143+
* @return gRPC channel credentials
144+
*/
145+
protected ChannelCredentials getChannelCredentials(Configurator configG) {
146+
return InsecureChannelCredentials.create();
147+
}
148+
133149
/**
134150
* Determines if the {@link RetryHandler} should try again when an exception is thrown during gRPC invocation. Default
135151
* behavior attempts retries for any Exception with a {@link Status.Code} in {@link #RETRY_GRPC_CODES}. Subclasses may

src/main/java/emissary/grpc/channel/ChannelManager.java

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import io.grpc.ChannelCredentials;
66
import io.grpc.Grpc;
7-
import io.grpc.InsecureChannelCredentials;
87
import io.grpc.ManagedChannel;
98
import org.slf4j.Logger;
109
import org.slf4j.LoggerFactory;
@@ -34,6 +33,10 @@ public abstract class ChannelManager implements AutoCloseable {
3433
public static final String MAX_INBOUND_MESSAGE_BYTE_SIZE = GRPC_CHANNEL_PREFIX + "MAX_INBOUND_MESSAGE_BYTE_SIZE";
3534
public static final String MAX_INBOUND_METADATA_BYTE_SIZE = GRPC_CHANNEL_PREFIX + "MAX_INBOUND_METADATA_BYTE_SIZE";
3635

36+
protected static final String LOCALHOST = "localhost";
37+
protected static final String DNS_PREFIX = "dns:///";
38+
protected static final int MAX_PORT_NUMBER = 0xFFFF;
39+
3740
protected final Logger logger;
3841

3942
protected final String host;
@@ -54,16 +57,17 @@ public abstract class ChannelManager implements AutoCloseable {
5457
* @param host gRPC service hostname or DNS target
5558
* @param port gRPC service port
5659
* @param configG configuration provider for channel parameters
60+
* @param credentials transport-level credentials for the gRPC server
5761
* @see ChannelManager
5862
* @see <a href="https://docs.microsoft.com/en-us/aspnet/core/grpc/performance?view=aspnetcore-5.0">Source</a> for
5963
* default gRPC configurations.
6064
*/
61-
protected ChannelManager(String host, int port, Configurator configG) {
65+
protected ChannelManager(String host, int port, Configurator configG, ChannelCredentials credentials) {
6266
this.logger = LoggerFactory.getLogger(this.getClass().getName());
6367

64-
this.host = host;
65-
this.port = port;
66-
this.target = host + ":" + port; // target may be a host or dns service
68+
this.host = validateHostName(host);
69+
this.port = validatePortNumber(port);
70+
this.target = createTarget(host, port);
6771

6872
// How often (in milliseconds) to send pings when the connection is idle
6973
this.keepAliveMillis = configG.findLongEntry(KEEP_ALIVE_MILLIS, 60000L);
@@ -84,32 +88,42 @@ protected ChannelManager(String host, int port, Configurator configG) {
8488
this.maxInboundMessageByteSize = configG.findIntEntry(MAX_INBOUND_MESSAGE_BYTE_SIZE, 4 << 20); // 4 MiB
8589
this.maxInboundMetadataByteSize = configG.findIntEntry(MAX_INBOUND_METADATA_BYTE_SIZE, 8 << 10); // 8 KiB
8690

87-
this.channelCredentials = createChannelCredentials(configG);
91+
this.channelCredentials = credentials;
8892
}
8993

90-
/**
91-
* Creates a new {@link ChannelCredentials} object with all security and encryption configurations necessary for
92-
* establishing a gRPC connection. Base class returns {@link InsecureChannelCredentials}, which asserts that no client
93-
* identity, authentication, or encryption is to be used. Subclasses should override this method as necessary.
94-
*
95-
* @param configG any necessary configurations for creating the credentials object
96-
* @return gRPC channel credentials
97-
*/
98-
protected ChannelCredentials createChannelCredentials(Configurator configG) {
99-
return InsecureChannelCredentials.create();
94+
protected String validateHostName(String host) {
95+
if (host.equals(LOCALHOST) || host.startsWith(DNS_PREFIX)) {
96+
return host;
97+
}
98+
throw new IllegalArgumentException(String.format("Expected DNS URI prefix \"dns:///\" but got \"%s\"", host));
99+
}
100+
101+
protected int validatePortNumber(int port) {
102+
if (port > 0 && port <= MAX_PORT_NUMBER) {
103+
return port;
104+
}
105+
throw new IllegalArgumentException(String.format("Port \"%d\" is outside valid range [1, %d]", port, MAX_PORT_NUMBER));
106+
}
107+
108+
protected String createTarget(String host, int port) {
109+
return host + ":" + port;
100110
}
101111

102-
public static ChannelManager ofSubClass(String className, String host, int port, Configurator configG) {
112+
public static ChannelManager ofSubClass(
113+
String className, String host, int port, Configurator configG, ChannelCredentials credentials) {
103114
try {
104-
return ofSubClass(Class.forName(className).asSubclass(ChannelManager.class), host, port, configG);
115+
return ofSubClass(Class.forName(className).asSubclass(ChannelManager.class), host, port, configG, credentials);
105116
} catch (ClassNotFoundException e) {
106117
throw new IllegalArgumentException("Cannot find class: " + className, e);
107118
}
108119
}
109120

110-
public static ChannelManager ofSubClass(Class<? extends ChannelManager> clz, String host, int port, Configurator configG) {
121+
public static ChannelManager ofSubClass(
122+
Class<? extends ChannelManager> clz, String host, int port, Configurator configG, ChannelCredentials credentials) {
111123
try {
112-
return clz.getDeclaredConstructor(String.class, int.class, Configurator.class).newInstance(host, port, configG);
124+
return clz
125+
.getDeclaredConstructor(String.class, int.class, Configurator.class, ChannelCredentials.class)
126+
.newInstance(host, port, configG, credentials);
113127
} catch (ReflectiveOperationException e) {
114128
throw new IllegalStateException("Unable to instantiate new ChannelManager: " + e.getMessage(), e);
115129
}

src/main/java/emissary/grpc/channel/PooledChannelManager.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import emissary.config.Configurator;
44
import emissary.grpc.exceptions.GrpcExceptionUtils;
55

6+
import io.grpc.ChannelCredentials;
67
import io.grpc.ManagedChannel;
78
import org.apache.commons.pool2.ObjectPool;
89
import org.apache.commons.pool2.PoolUtils;
@@ -58,8 +59,8 @@ public class PooledChannelManager extends ChannelManager implements PooledObject
5859
* @see ChannelManager
5960
* @see PooledChannelManager
6061
*/
61-
public PooledChannelManager(String host, int port, Configurator configG) {
62-
super(host, port, configG);
62+
public PooledChannelManager(String host, int port, Configurator configG, ChannelCredentials credentials) {
63+
super(host, port, configG, credentials);
6364

6465
GenericObjectPoolConfig<ManagedChannel> poolConfig = new GenericObjectPoolConfig<>();
6566

src/main/java/emissary/grpc/channel/SingletonChannelManager.java

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

33
import emissary.config.Configurator;
44

5+
import io.grpc.ChannelCredentials;
56
import io.grpc.ManagedChannel;
67

78
/**
@@ -19,8 +20,8 @@ public class SingletonChannelManager extends ChannelManager {
1920
* @param configG configuration provider for channel parameters
2021
* @see ChannelManager
2122
*/
22-
public SingletonChannelManager(String host, int port, Configurator configG) {
23-
super(host, port, configG);
23+
public SingletonChannelManager(String host, int port, Configurator configG, ChannelCredentials credentials) {
24+
super(host, port, configG, credentials);
2425
channel = create();
2526
}
2627

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package emissary.grpc.channel;
2+
3+
import emissary.config.ServiceConfigGuide;
4+
import emissary.test.core.junit5.UnitTest;
5+
6+
import io.grpc.InsecureChannelCredentials;
7+
import io.grpc.ManagedChannel;
8+
import org.junit.jupiter.params.ParameterizedTest;
9+
import org.junit.jupiter.params.provider.ValueSource;
10+
11+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertThrows;
14+
15+
class ChannelManagerTest extends UnitTest {
16+
@ParameterizedTest
17+
@ValueSource(strings = {"localhost", "dns:///foo.bar"})
18+
void testGoodHostName(String host) {
19+
Runnable invocation = () -> new TestChannelManager(host, 1).close();
20+
assertDoesNotThrow(invocation::run);
21+
}
22+
23+
@ParameterizedTest
24+
@ValueSource(strings = {"dns:foo.bar", "dns:/foo.bar", "dns://foo.bar", "http:///foo.bar", "https:///foo.bar", "foo.bar"})
25+
void testBadHostName(String host) {
26+
Runnable invocation = () -> new TestChannelManager(host, 1).close();
27+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, invocation::run);
28+
assertEquals(String.format("Expected DNS URI prefix \"dns:///\" but got \"%s\"", host), e.getMessage());
29+
}
30+
31+
@ParameterizedTest
32+
@ValueSource(ints = {1, 8000, 8001, 8080, 65535})
33+
void testGoodPortNumber(int port) {
34+
Runnable invocation = () -> new TestChannelManager("dns:///foo.bar", port).close();
35+
assertDoesNotThrow(invocation::run);
36+
}
37+
38+
@ParameterizedTest
39+
@ValueSource(ints = {-1, 0, 65536})
40+
void testBadPortNumber(int port) {
41+
Runnable invocation = () -> new TestChannelManager("dns:///foo.bar", port).close();
42+
IllegalArgumentException e = assertThrows(IllegalArgumentException.class, invocation::run);
43+
assertEquals(String.format("Port \"%d\" is outside valid range [1, 65535]", port), e.getMessage());
44+
}
45+
46+
47+
private static class TestChannelManager extends ChannelManager {
48+
public TestChannelManager(String host, int port) {
49+
super(host, port, new ServiceConfigGuide(), InsecureChannelCredentials.create());
50+
}
51+
52+
@Override
53+
public ManagedChannel acquire() {
54+
return null;
55+
}
56+
57+
@Override
58+
public void release(ManagedChannel channel) {
59+
/* No-op */
60+
}
61+
62+
@Override
63+
public void shutdown(ManagedChannel channel) {
64+
/* No-op */
65+
}
66+
67+
@Override
68+
public void close() {
69+
/* No-op */
70+
}
71+
}
72+
}

src/test/java/emissary/grpc/channel/PooledChannelManagerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import emissary.grpc.channel.PooledChannelManager.PoolRetrievalOrdering;
77
import emissary.test.core.junit5.UnitTest;
88

9+
import io.grpc.InsecureChannelCredentials;
910
import io.grpc.ManagedChannel;
1011
import org.junit.jupiter.api.Test;
1112

@@ -22,7 +23,7 @@
2223
class PooledChannelManagerTest extends UnitTest {
2324

2425
private static PooledChannelManager newChannelManager(Configurator configT) {
25-
return new PooledChannelManager("localhost", 1234, configT);
26+
return new PooledChannelManager("localhost", 1234, configT, InsecureChannelCredentials.create());
2627
}
2728

2829
private static Configurator buildConfigs(ConfigEntry... configEntries) {

src/test/java/emissary/grpc/channel/SingletonChannelManagerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import emissary.config.ServiceConfigGuide;
55
import emissary.test.core.junit5.UnitTest;
66

7+
import io.grpc.InsecureChannelCredentials;
78
import io.grpc.ManagedChannel;
89
import org.junit.jupiter.api.Test;
910

@@ -12,7 +13,7 @@
1213

1314
class SingletonChannelManagerTest extends UnitTest {
1415
private static SingletonChannelManager newChannelManager(Configurator configT) {
15-
return new SingletonChannelManager("localhost", 1234, configT);
16+
return new SingletonChannelManager("localhost", 1234, configT, InsecureChannelCredentials.create());
1617
}
1718

1819
@Test

0 commit comments

Comments
 (0)