From 60319dad2dcbb15ca231efb340e6fbb57b9ca79a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 6 Nov 2020 16:00:39 -0800 Subject: [PATCH] api: Add ServerCredentials --- .../java/io/grpc/ChoiceServerCredentials.java | 57 +++++ api/src/main/java/io/grpc/Grpc.java | 11 + .../io/grpc/InsecureServerCredentials.java | 27 ++ .../main/java/io/grpc/ServerCredentials.java | 37 +++ api/src/main/java/io/grpc/ServerProvider.java | 54 ++-- api/src/main/java/io/grpc/ServerRegistry.java | 166 +++++++++++++ .../java/io/grpc/TlsServerCredentials.java | 231 ++++++++++++++++++ .../io/grpc/ManagedChannelRegistryTest.java | 2 +- .../test/java/io/grpc/ServerRegistryTest.java | 178 ++++++++++++++ 9 files changed, 744 insertions(+), 19 deletions(-) create mode 100644 api/src/main/java/io/grpc/ChoiceServerCredentials.java create mode 100644 api/src/main/java/io/grpc/InsecureServerCredentials.java create mode 100644 api/src/main/java/io/grpc/ServerCredentials.java create mode 100644 api/src/main/java/io/grpc/ServerRegistry.java create mode 100644 api/src/main/java/io/grpc/TlsServerCredentials.java create mode 100644 api/src/test/java/io/grpc/ServerRegistryTest.java diff --git a/api/src/main/java/io/grpc/ChoiceServerCredentials.java b/api/src/main/java/io/grpc/ChoiceServerCredentials.java new file mode 100644 index 0000000000..df777bf200 --- /dev/null +++ b/api/src/main/java/io/grpc/ChoiceServerCredentials.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Provides a list of {@link ServerCredentials}, where any one may be used. The credentials are in + * preference order. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") +public final class ChoiceServerCredentials extends ServerCredentials { + /** + * Constructs with the provided {@code creds} as options, with preferred credentials first. + * + * @throws IllegalArgumentException if no creds are provided + */ + public static ServerCredentials create(ServerCredentials... creds) { + if (creds.length == 0) { + throw new IllegalArgumentException("At least one credential is required"); + } + return new ChoiceServerCredentials(creds); + } + + private final List creds; + + private ChoiceServerCredentials(ServerCredentials... creds) { + for (ServerCredentials cred : creds) { + if (cred == null) { + throw new NullPointerException(); + } + } + this.creds = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(creds))); + } + + /** Non-empty list of credentials, in preference order. */ + public List getCredentialsList() { + return creds; + } +} diff --git a/api/src/main/java/io/grpc/Grpc.java b/api/src/main/java/io/grpc/Grpc.java index e5fb4a112d..7f9fc1caf9 100644 --- a/api/src/main/java/io/grpc/Grpc.java +++ b/api/src/main/java/io/grpc/Grpc.java @@ -124,4 +124,15 @@ public final class Grpc { throw new IllegalArgumentException("Invalid host or port: " + host + " " + port, ex); } } + + /** + * Static factory for creating a new ServerBuilder. + * + * @param port the port to listen on + * @param creds the server identity + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") + public static ServerBuilder newServerBuilderForPort(int port, ServerCredentials creds) { + return ServerRegistry.getDefaultRegistry().newServerBuilderForPort(port, creds); + } } diff --git a/api/src/main/java/io/grpc/InsecureServerCredentials.java b/api/src/main/java/io/grpc/InsecureServerCredentials.java new file mode 100644 index 0000000000..959f63537f --- /dev/null +++ b/api/src/main/java/io/grpc/InsecureServerCredentials.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc; + +/** No server identity or encryption is to be used. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") +public final class InsecureServerCredentials extends ServerCredentials { + public static ServerCredentials create() { + return new InsecureServerCredentials(); + } + + private InsecureServerCredentials() {} +} diff --git a/api/src/main/java/io/grpc/ServerCredentials.java b/api/src/main/java/io/grpc/ServerCredentials.java new file mode 100644 index 0000000000..ccdaee38ab --- /dev/null +++ b/api/src/main/java/io/grpc/ServerCredentials.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc; + +/** + * Represents a security configuration to be used for servers. There is no generic mechanism for + * processing arbitrary {@code ServerCredentials}; the consumer of the credential (the server) + * must support each implementation explicitly and separately. Consumers are not required to support + * all types or even all possible configurations for types that are partially supported, but they + * must at least fully support {@link ChoiceServerCredentials}. + * + *

A {@code ServerCredential} provides server identity. They can also influence types of + * encryption used and similar security configuration. + * + *

The concrete credential type should not be relevant to most users of the API and may be an + * implementation decision. Users should generally use the {@code ServerCredentials} type for + * variables instead of the concrete type. Freshly-constructed credentials should be returned as + * {@code ServerCredentials} instead of a concrete type to encourage this pattern. Concrete types + * would only be used after {@code instanceof} checks (which must consider + * {@code ChoiceServerCredentials}!). + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") +public abstract class ServerCredentials {} diff --git a/api/src/main/java/io/grpc/ServerProvider.java b/api/src/main/java/io/grpc/ServerProvider.java index a6a28dd86d..f72880b375 100644 --- a/api/src/main/java/io/grpc/ServerProvider.java +++ b/api/src/main/java/io/grpc/ServerProvider.java @@ -16,9 +16,8 @@ package io.grpc; +import com.google.common.base.Preconditions; import io.grpc.ManagedChannelProvider.ProviderNotFoundException; -import io.grpc.ServiceProviders.PriorityAccessor; -import java.util.Collections; /** * Provider of servers for transport agnostic consumption. @@ -34,28 +33,13 @@ import java.util.Collections; */ @Internal public abstract class ServerProvider { - private static final ServerProvider provider = ServiceProviders.load( - ServerProvider.class, - Collections.>emptyList(), - ServerProvider.class.getClassLoader(), - new PriorityAccessor() { - @Override - public boolean isAvailable(ServerProvider provider) { - return provider.isAvailable(); - } - - @Override - public int getPriority(ServerProvider provider) { - return provider.priority(); - } - }); - /** * Returns the ClassLoader-wide default server. * * @throws ProviderNotFoundException if no provider is available */ public static ServerProvider provider() { + ServerProvider provider = ServerRegistry.getDefaultRegistry().provider(); if (provider == null) { throw new ProviderNotFoundException("No functional server found. " + "Try adding a dependency on the grpc-netty or grpc-netty-shaded artifact"); @@ -81,4 +65,38 @@ public abstract class ServerProvider { * Creates a new builder with the given port. */ protected abstract ServerBuilder builderForPort(int port); + + /** + * Creates a new builder with the given port and credentials. Returns an error-string result if + * unable to understand the credentials. + */ + protected NewServerBuilderResult newServerBuilderForPort(int port, ServerCredentials creds) { + return NewServerBuilderResult.error("ServerCredentials are unsupported"); + } + + public static final class NewServerBuilderResult { + private final ServerBuilder serverBuilder; + private final String error; + + private NewServerBuilderResult(ServerBuilder serverBuilder, String error) { + this.serverBuilder = serverBuilder; + this.error = error; + } + + public static NewServerBuilderResult serverBuilder(ServerBuilder builder) { + return new NewServerBuilderResult(Preconditions.checkNotNull(builder), null); + } + + public static NewServerBuilderResult error(String error) { + return new NewServerBuilderResult(null, Preconditions.checkNotNull(error)); + } + + public ServerBuilder getServerBuilder() { + return serverBuilder; + } + + public String getError() { + return error; + } + } } diff --git a/api/src/main/java/io/grpc/ServerRegistry.java b/api/src/main/java/io/grpc/ServerRegistry.java new file mode 100644 index 0000000000..f267797e3f --- /dev/null +++ b/api/src/main/java/io/grpc/ServerRegistry.java @@ -0,0 +1,166 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.logging.Logger; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Registry of {@link ServerProvider}s. The {@link #getDefaultRegistry default instance} loads + * providers at runtime through the Java service provider mechanism. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") +@ThreadSafe +public final class ServerRegistry { + private static final Logger logger = Logger.getLogger(ServerRegistry.class.getName()); + private static ServerRegistry instance; + + @GuardedBy("this") + private final LinkedHashSet allProviders = new LinkedHashSet<>(); + /** Immutable, sorted version of {@code allProviders}. Is replaced instead of mutating. */ + @GuardedBy("this") + private List effectiveProviders = Collections.emptyList(); + + /** + * Register a provider. + * + *

If the provider's {@link ServerProvider#isAvailable isAvailable()} returns + * {@code false}, this method will throw {@link IllegalArgumentException}. + * + *

Providers will be used in priority order. In case of ties, providers are used in + * registration order. + */ + public synchronized void register(ServerProvider provider) { + addProvider(provider); + refreshProviders(); + } + + private synchronized void addProvider(ServerProvider provider) { + Preconditions.checkArgument(provider.isAvailable(), "isAvailable() returned false"); + allProviders.add(provider); + } + + /** + * Deregisters a provider. No-op if the provider is not in the registry. + * + * @param provider the provider that was added to the register via {@link #register}. + */ + public synchronized void deregister(ServerProvider provider) { + allProviders.remove(provider); + refreshProviders(); + } + + private synchronized void refreshProviders() { + List providers = new ArrayList<>(allProviders); + // Sort descending based on priority. + // sort() must be stable, as we prefer first-registered providers + Collections.sort(providers, Collections.reverseOrder(new Comparator() { + @Override + public int compare(ServerProvider o1, ServerProvider o2) { + return o1.priority() - o2.priority(); + } + })); + effectiveProviders = Collections.unmodifiableList(providers); + } + + /** + * Returns the default registry that loads providers via the Java service loader mechanism. + */ + public static synchronized ServerRegistry getDefaultRegistry() { + if (instance == null) { + List providerList = ServiceProviders.loadAll( + ServerProvider.class, + Collections.>emptyList(), + ServerProvider.class.getClassLoader(), + new ServerPriorityAccessor()); + instance = new ServerRegistry(); + for (ServerProvider provider : providerList) { + logger.fine("Service loader found " + provider); + if (provider.isAvailable()) { + instance.addProvider(provider); + } + } + instance.refreshProviders(); + } + return instance; + } + + /** + * Returns effective providers, in priority order. + */ + @VisibleForTesting + synchronized List providers() { + return effectiveProviders; + } + + // For emulating ServerProvider.provider() + ServerProvider provider() { + List providers = providers(); + return providers.isEmpty() ? null : providers.get(0); + } + + ServerBuilder newServerBuilderForPort(int port, ServerCredentials creds) { + List providers = providers(); + if (providers.isEmpty()) { + throw new ProviderNotFoundException("No functional server found. " + + "Try adding a dependency on the grpc-netty or grpc-netty-shaded artifact"); + } + StringBuilder error = new StringBuilder(); + for (ServerProvider provider : providers()) { + ServerProvider.NewServerBuilderResult result + = provider.newServerBuilderForPort(port, creds); + if (result.getServerBuilder() != null) { + return result.getServerBuilder(); + } + error.append("; "); + error.append(provider.getClass().getName()); + error.append(": "); + error.append(result.getError()); + } + throw new ProviderNotFoundException(error.substring(2)); + } + + private static final class ServerPriorityAccessor + implements ServiceProviders.PriorityAccessor { + @Override + public boolean isAvailable(ServerProvider provider) { + return provider.isAvailable(); + } + + @Override + public int getPriority(ServerProvider provider) { + return provider.priority(); + } + } + + /** Thrown when no suitable {@link ServerProvider} objects can be found. */ + public static final class ProviderNotFoundException extends RuntimeException { + private static final long serialVersionUID = 1; + + public ProviderNotFoundException(String msg) { + super(msg); + } + } +} diff --git a/api/src/main/java/io/grpc/TlsServerCredentials.java b/api/src/main/java/io/grpc/TlsServerCredentials.java new file mode 100644 index 0000000000..7619716764 --- /dev/null +++ b/api/src/main/java/io/grpc/TlsServerCredentials.java @@ -0,0 +1,231 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc; + +import com.google.common.io.ByteStreams; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +/** + * TLS credentials, providing server identity and encryption. Consumers of this credential must + * verify they understand the configuration via the {@link #incomprehensible incomprehensible()} + * method. Unless overridden by a {@code Feature}, server identity is provided via {@link + * #getCertificateChain}, {@link #getPrivateKey}, and {@link #getPrivateKeyPassword}. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") +public final class TlsServerCredentials extends ServerCredentials { + /** + * Creates an instance using provided certificate chain and private key. Generally they should be + * PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN CERTIFICATE" and + * "BEGIN PRIVATE KEY"). + */ + public static ServerCredentials create(File certChain, File privateKey) throws IOException { + return newBuilder().keyManager(certChain, privateKey).build(); + } + + /** + * Creates an instance using provided certificate chain and private key. Generally they should be + * PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN CERTIFICATE" and + * "BEGIN PRIVATE KEY"). + * + *

The streams will not be automatically closed. + */ + public static ServerCredentials create( + InputStream certChain, InputStream privateKey) throws IOException { + return newBuilder().keyManager(certChain, privateKey).build(); + } + + private final boolean fakeFeature; + private final byte[] certificateChain; + private final byte[] privateKey; + private final String privateKeyPassword; + + TlsServerCredentials(Builder builder) { + fakeFeature = builder.fakeFeature; + certificateChain = builder.certificateChain; + privateKey = builder.privateKey; + privateKeyPassword = builder.privateKeyPassword; + } + + /** + * The certificate chain, as a new byte array. Generally should be PEM-encoded. + */ + public byte[] getCertificateChain() { + return Arrays.copyOf(certificateChain, certificateChain.length); + } + + /** + * The private key, as a new byte array. Generally should be in PKCS#8 format. If encrypted, + * {@link #getPrivateKeyPassword} is the decryption key. If unencrypted, the password will be + * {@code null}. + */ + public byte[] getPrivateKey() { + return Arrays.copyOf(privateKey, privateKey.length); + } + + /** Returns the password to decrypt the private key, or {@code null} if unencrypted. */ + public String getPrivateKeyPassword() { + return privateKeyPassword; + } + + /** + * Returns an empty set if this credential can be adequately understood via + * the features listed, otherwise returns a hint of features that are lacking + * to understand the configuration to be used for manual debugging. + * + *

An "understood" feature does not imply the caller is able to fully + * handle the feature. It simply means the caller understands the feature + * enough to use the appropriate APIs to read the configuration. The caller + * may support just a subset of a feature, in which case the caller would + * need to look at the configuration to determine if only the supported + * subset is used. + * + *

This method may not be as simple as a set difference. There may be + * multiple features that can independently satisfy a piece of configuration. + * If the configuration is incomprehensible, all such features would be + * returned, even though only one may be necessary. + * + *

An empty set does not imply that the credentials are fully understood. + * There may be optional configuration that can be ignored if not understood. + * + *

Since {@code Feature} is an {@code enum}, {@code understoodFeatures} + * should generally be an {@link java.util.EnumSet}. {@code + * understoodFeatures} will not be modified. + * + * @param understoodFeatures the features understood by the caller + * @return empty set if the caller can adequately understand the configuration + */ + public Set incomprehensible(Set understoodFeatures) { + Set incomprehensible = EnumSet.noneOf(Feature.class); + if (fakeFeature) { + requiredFeature(understoodFeatures, incomprehensible, Feature.FAKE); + } + return Collections.unmodifiableSet(incomprehensible); + } + + private static void requiredFeature( + Set understoodFeatures, Set incomprehensible, Feature feature) { + if (!understoodFeatures.contains(feature)) { + incomprehensible.add(feature); + } + } + + /** + * Features to understand TLS configuration. Additional enum values may be added in the future. + */ + public enum Feature { + /** + * A feature that no consumer should understand. It should be used for unit testing to confirm + * a call to {@link #incomprehensible incomprehensible()} is implemented properly. + */ + FAKE, + ; + } + + /** Creates a builder for changing default configuration. */ + public static Builder newBuilder() { + return new Builder(); + } + + /** Builder for {@link TlsServerCredentials}. */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") + public static final class Builder { + private boolean fakeFeature; + private byte[] certificateChain; + private byte[] privateKey; + private String privateKeyPassword; + + private Builder() {} + + /** + * Requires {@link Feature#FAKE} to be understood. For use in testing consumers of this + * credential. + */ + public Builder requireFakeFeature() { + fakeFeature = true; + return this; + } + + /** + * Creates an instance using provided certificate chain and private key. Generally they should + * be PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN + * CERTIFICATE" and "BEGIN PRIVATE KEY"). + */ + public Builder keyManager(File certChain, File privateKey) throws IOException { + return keyManager(certChain, privateKey, null); + } + + /** + * Creates an instance using provided certificate chain and possibly-encrypted private key. + * Generally they should be PEM-encoded and the key is a PKCS#8 key. If the private key is + * unencrypted, then password must be {@code null}. + */ + public Builder keyManager(File certChain, File privateKey, String privateKeyPassword) + throws IOException { + InputStream certChainIs = new FileInputStream(certChain); + try { + InputStream privateKeyIs = new FileInputStream(privateKey); + try { + return keyManager(certChainIs, privateKeyIs, privateKeyPassword); + } finally { + privateKeyIs.close(); + } + } finally { + certChainIs.close(); + } + } + + /** + * Creates an instance using provided certificate chain and private key. Generally they should + * be PEM-encoded and the key is an unencrypted PKCS#8 key (file headers have "BEGIN + * CERTIFICATE" and "BEGIN PRIVATE KEY"). + */ + public Builder keyManager(InputStream certChain, InputStream privateKey) throws IOException { + return keyManager(certChain, privateKey, null); + } + + /** + * Creates an instance using provided certificate chain and possibly-encrypted private key. + * Generally they should be PEM-encoded and the key is a PKCS#8 key. If the private key is + * unencrypted, then password must be {@code null}. + */ + public Builder keyManager( + InputStream certChain, InputStream privateKey, String privateKeyPassword) + throws IOException { + byte[] certChainBytes = ByteStreams.toByteArray(certChain); + byte[] privateKeyBytes = ByteStreams.toByteArray(privateKey); + this.certificateChain = certChainBytes; + this.privateKey = privateKeyBytes; + this.privateKeyPassword = privateKeyPassword; + return this; + } + + /** Construct the credentials. */ + public ServerCredentials build() { + if (certificateChain == null) { + throw new IllegalStateException("A key manager is required"); + } + return new TlsServerCredentials(this); + } + } +} diff --git a/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java b/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java index 68d666ff10..9266559c0b 100644 --- a/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java +++ b/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java @@ -30,7 +30,7 @@ public class ManagedChannelRegistryTest { private ChannelCredentials creds = new ChannelCredentials() {}; @Test - public void register_unavilableProviderThrows() { + public void register_unavailableProviderThrows() { ManagedChannelRegistry reg = new ManagedChannelRegistry(); try { reg.register(new BaseProvider(false, 5)); diff --git a/api/src/test/java/io/grpc/ServerRegistryTest.java b/api/src/test/java/io/grpc/ServerRegistryTest.java new file mode 100644 index 0000000000..377f9eda73 --- /dev/null +++ b/api/src/test/java/io/grpc/ServerRegistryTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2020 The gRPC Authors + * + * 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 io.grpc; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ServerRegistry}. */ +@RunWith(JUnit4.class) +public class ServerRegistryTest { + private int port = 123; + private ServerCredentials creds = new ServerCredentials() {}; + + @Test + public void register_unavailableProviderThrows() { + ServerRegistry reg = new ServerRegistry(); + try { + reg.register(new BaseProvider(false, 5)); + fail("Should throw"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().contains("isAvailable() returned false"); + } + assertThat(reg.providers()).isEmpty(); + } + + @Test + public void deregister() { + ServerRegistry reg = new ServerRegistry(); + ServerProvider p1 = new BaseProvider(true, 5); + ServerProvider p2 = new BaseProvider(true, 5); + ServerProvider p3 = new BaseProvider(true, 5); + reg.register(p1); + reg.register(p2); + reg.register(p3); + assertThat(reg.providers()).containsExactly(p1, p2, p3).inOrder(); + reg.deregister(p2); + assertThat(reg.providers()).containsExactly(p1, p3).inOrder(); + } + + @Test + public void provider_sorted() { + ServerRegistry reg = new ServerRegistry(); + ServerProvider p1 = new BaseProvider(true, 5); + ServerProvider p2 = new BaseProvider(true, 3); + ServerProvider p3 = new BaseProvider(true, 8); + ServerProvider p4 = new BaseProvider(true, 3); + ServerProvider p5 = new BaseProvider(true, 8); + reg.register(p1); + reg.register(p2); + reg.register(p3); + reg.register(p4); + reg.register(p5); + assertThat(reg.providers()).containsExactly(p3, p5, p1, p2, p4).inOrder(); + } + + @Test + public void getProvider_noProvider() { + assertThat(new ServerRegistry().provider()).isNull(); + } + + @Test + public void newServerBuilderForPort_providerReturnsError() { + final String errorString = "brisking"; + class ErrorProvider extends BaseProvider { + ErrorProvider() { + super(true, 5); + } + + @Override + public NewServerBuilderResult newServerBuilderForPort( + int passedPort, ServerCredentials passedCreds) { + assertThat(passedPort).isEqualTo(port); + assertThat(passedCreds).isSameInstanceAs(creds); + return NewServerBuilderResult.error(errorString); + } + } + + ServerRegistry registry = new ServerRegistry(); + registry.register(new ErrorProvider()); + try { + registry.newServerBuilderForPort(port, creds); + fail("expected exception"); + } catch (ServerRegistry.ProviderNotFoundException ex) { + assertThat(ex).hasMessageThat().contains(errorString); + assertThat(ex).hasMessageThat().contains(ErrorProvider.class.getName()); + } + } + + @Test + public void newServerBuilderForPort_providerReturnsNonNull() { + ServerRegistry registry = new ServerRegistry(); + registry.register(new BaseProvider(true, 5) { + @Override + public NewServerBuilderResult newServerBuilderForPort( + int passedPort, ServerCredentials passedCreds) { + return NewServerBuilderResult.error("dodging"); + } + }); + class MockServerBuilder extends ForwardingServerBuilder { + @Override public ServerBuilder delegate() { + throw new UnsupportedOperationException(); + } + } + + final ServerBuilder mcb = new MockServerBuilder(); + registry.register(new BaseProvider(true, 4) { + @Override + public NewServerBuilderResult newServerBuilderForPort( + int passedPort, ServerCredentials passedCreds) { + return NewServerBuilderResult.serverBuilder(mcb); + } + }); + registry.register(new BaseProvider(true, 3) { + @Override + public NewServerBuilderResult newServerBuilderForPort( + int passedPort, ServerCredentials passedCreds) { + fail("Should not be called"); + throw new AssertionError(); + } + }); + assertThat(registry.newServerBuilderForPort(port, creds)).isSameInstanceAs(mcb); + } + + @Test + public void newServerBuilderForPort_noProvider() { + ServerRegistry registry = new ServerRegistry(); + try { + registry.newServerBuilderForPort(port, creds); + fail("expected exception"); + } catch (ServerRegistry.ProviderNotFoundException ex) { + assertThat(ex).hasMessageThat().contains("No functional server found"); + assertThat(ex).hasMessageThat().contains("grpc-netty"); + } + } + + private static class BaseProvider extends ServerProvider { + private final boolean isAvailable; + private final int priority; + + public BaseProvider(boolean isAvailable, int priority) { + this.isAvailable = isAvailable; + this.priority = priority; + } + + @Override + protected boolean isAvailable() { + return isAvailable; + } + + @Override + protected int priority() { + return priority; + } + + @Override + protected ServerBuilder builderForPort(int port) { + throw new UnsupportedOperationException(); + } + } +}