From 72f402780339754503c7d30d3cce05bb5a07cd0b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 6 Nov 2020 16:00:39 -0800 Subject: [PATCH 1/4] 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 00000000000..df777bf2001 --- /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 e5fb4a112d9..7f9fc1caf93 100644 --- a/api/src/main/java/io/grpc/Grpc.java +++ b/api/src/main/java/io/grpc/Grpc.java @@ -124,4 +124,15 @@ private static String authorityFromHostAndPort(String host, int port) { 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 00000000000..959f63537fe --- /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 00000000000..ccdaee38ab3 --- /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 a6a28dd86d6..f72880b375d 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 @@ */ @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 static ServerProvider provider() { * 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 00000000000..f267797e3f3 --- /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 00000000000..76197167646 --- /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 68d666ff107..9266559c0bb 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 00000000000..377f9eda737 --- /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(); + } + } +} From c8fbfb28db319128e6e8662d03cd3a85b4070044 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 6 Nov 2020 16:00:56 -0800 Subject: [PATCH 2/4] netty: Add ServerCredentials --- .../io/grpc/netty/shaded/ShadingTest.java | 14 +- .../InternalNettyChannelCredentials.java | 25 +--- .../netty/InternalNettyServerCredentials.java | 69 ++++++++++ .../netty/InternalProtocolNegotiator.java | 33 +++++ .../io/grpc/netty/NettyServerBuilder.java | 80 ++++++++--- .../io/grpc/netty/NettyServerCredentials.java | 37 +++++ .../io/grpc/netty/NettyServerProvider.java | 15 +- .../NettySslContextChannelCredentials.java | 4 + .../NettySslContextServerCredentials.java | 39 ++++++ .../io/grpc/netty/ProtocolNegotiator.java | 11 ++ .../io/grpc/netty/ProtocolNegotiators.java | 129 ++++++++++++++++++ .../grpc/netty/NettyServerProviderTest.java | 19 +++ .../grpc/netty/ProtocolNegotiatorsTest.java | 106 ++++++++++++-- 13 files changed, 522 insertions(+), 59 deletions(-) create mode 100644 netty/src/main/java/io/grpc/netty/InternalNettyServerCredentials.java create mode 100644 netty/src/main/java/io/grpc/netty/NettyServerCredentials.java create mode 100644 netty/src/main/java/io/grpc/netty/NettySslContextServerCredentials.java diff --git a/netty/shaded/src/testShadow/java/io/grpc/netty/shaded/ShadingTest.java b/netty/shaded/src/testShadow/java/io/grpc/netty/shaded/ShadingTest.java index ac9b3301eea..e4a84986976 100644 --- a/netty/shaded/src/testShadow/java/io/grpc/netty/shaded/ShadingTest.java +++ b/netty/shaded/src/testShadow/java/io/grpc/netty/shaded/ShadingTest.java @@ -21,9 +21,11 @@ import io.grpc.ChannelCredentials; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; +import io.grpc.InsecureServerCredentials; import io.grpc.ManagedChannel; import io.grpc.Server; -import io.grpc.ServerBuilder; +import io.grpc.ServerCredentials; +import io.grpc.TlsServerCredentials; import io.grpc.internal.testing.TestUtils; import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; @@ -69,14 +71,15 @@ public void noNormalNetty() throws Exception { @Test public void serviceLoaderFindsNetty() throws Exception { - assertThat(ServerBuilder.forPort(0)).isInstanceOf(NettyServerBuilder.class); + assertThat(Grpc.newServerBuilderForPort(0, InsecureServerCredentials.create())) + .isInstanceOf(NettyServerBuilder.class); assertThat(Grpc.newChannelBuilder("localhost:1234", InsecureChannelCredentials.create())) .isInstanceOf(NettyChannelBuilder.class); } @Test public void basic() throws Exception { - server = ServerBuilder.forPort(0) + server = Grpc.newServerBuilderForPort(0, InsecureServerCredentials.create()) .addService(new SimpleServiceImpl()) .build().start(); channel = Grpc.newChannelBuilder( @@ -89,8 +92,9 @@ public void basic() throws Exception { @Test public void tcnative() throws Exception { - server = NettyServerBuilder.forPort(0) - .useTransportSecurity(TestUtils.loadCert("server1.pem"), TestUtils.loadCert("server1.key")) + ServerCredentials serverCreds = TlsServerCredentials.create( + TestUtils.loadCert("server1.pem"), TestUtils.loadCert("server1.key")); + server = Grpc.newServerBuilderForPort(0, serverCreds) .addService(new SimpleServiceImpl()) .build().start(); ChannelCredentials creds = NettySslContextChannelCredentials.create( diff --git a/netty/src/main/java/io/grpc/netty/InternalNettyChannelCredentials.java b/netty/src/main/java/io/grpc/netty/InternalNettyChannelCredentials.java index d121c563009..ab962e59d59 100644 --- a/netty/src/main/java/io/grpc/netty/InternalNettyChannelCredentials.java +++ b/netty/src/main/java/io/grpc/netty/InternalNettyChannelCredentials.java @@ -18,8 +18,6 @@ import io.grpc.ChannelCredentials; import io.grpc.Internal; -import io.netty.channel.ChannelHandler; -import io.netty.util.AsciiString; /** * Internal {@link NettyChannelCredentials} accessor. This is intended for usage internal to the @@ -50,27 +48,8 @@ final class ClientFactory implements InternalProtocolNegotiator.ClientFactory { @Override public InternalProtocolNegotiator.ProtocolNegotiator newNegotiator() { - final ProtocolNegotiator pn = result.negotiator.newNegotiator(); - final class LocalProtocolNegotiator - implements InternalProtocolNegotiator.ProtocolNegotiator { - - @Override - public AsciiString scheme() { - return pn.scheme(); - } - - @Override - public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { - return pn.newHandler(grpcHandler); - } - - @Override - public void close() { - pn.close(); - } - } - - return new LocalProtocolNegotiator(); + return new InternalProtocolNegotiator.ProtocolNegotiatorAdapter( + result.negotiator.newNegotiator()); } @Override diff --git a/netty/src/main/java/io/grpc/netty/InternalNettyServerCredentials.java b/netty/src/main/java/io/grpc/netty/InternalNettyServerCredentials.java new file mode 100644 index 00000000000..16e58d94369 --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/InternalNettyServerCredentials.java @@ -0,0 +1,69 @@ +/* + * 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.netty; + +import io.grpc.Internal; +import io.grpc.ServerCredentials; +import io.grpc.internal.ObjectPool; +import java.util.concurrent.Executor; + +/** + * Internal {@link NettyServerCredentials} accessor. This is intended for usage internal to the + * gRPC team. If you *really* think you need to use this, contact the gRPC team first. + */ +@Internal +public final class InternalNettyServerCredentials { + private InternalNettyServerCredentials() {} + + /** Creates a {@link ServerCredentials} that will use the provided {@code negotiator}. */ + public static ServerCredentials create(InternalProtocolNegotiator.ProtocolNegotiator negotiator) { + return NettyServerCredentials.create(ProtocolNegotiators.fixedServerFactory(negotiator)); + } + + /** + * Creates a {@link ServerCredentials} that will use the provided {@code negotiator}. Use of + * {@link #create(io.grpc.netty.InternalProtocolNegotiator.ProtocolNegotiator)} is preferred over + * this method when possible. + */ + public static ServerCredentials create(InternalProtocolNegotiator.ServerFactory negotiator) { + return NettyServerCredentials.create(negotiator); + } + + /** + * Converts a {@link ServerCredentials} to a negotiator, in similar fashion as for a new server. + * + * @throws IllegalArgumentException if unable to convert + */ + public static InternalProtocolNegotiator.ServerFactory toNegotiator( + ServerCredentials channelCredentials) { + final ProtocolNegotiators.FromServerCredentialsResult result = + ProtocolNegotiators.from(channelCredentials); + if (result.error != null) { + throw new IllegalArgumentException(result.error); + } + final class ServerFactory implements InternalProtocolNegotiator.ServerFactory { + @Override + public InternalProtocolNegotiator.ProtocolNegotiator newNegotiator( + ObjectPool offloadExecutorPool) { + return new InternalProtocolNegotiator.ProtocolNegotiatorAdapter( + result.negotiator.newNegotiator(offloadExecutorPool)); + } + } + + return new ServerFactory(); + } +} diff --git a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiator.java b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiator.java index 0efa85eea75..1863ec07ced 100644 --- a/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiator.java +++ b/netty/src/main/java/io/grpc/netty/InternalProtocolNegotiator.java @@ -16,7 +16,12 @@ package io.grpc.netty; +import com.google.common.base.Preconditions; import io.grpc.Internal; +import io.grpc.internal.ObjectPool; +import io.netty.channel.ChannelHandler; +import io.netty.util.AsciiString; +import java.util.concurrent.Executor; /** * Internal accessor for {@link ProtocolNegotiator}. @@ -28,7 +33,35 @@ private InternalProtocolNegotiator() {} public interface ProtocolNegotiator extends io.grpc.netty.ProtocolNegotiator {} + static final class ProtocolNegotiatorAdapter + implements InternalProtocolNegotiator.ProtocolNegotiator { + private final io.grpc.netty.ProtocolNegotiator negotiator; + + public ProtocolNegotiatorAdapter(io.grpc.netty.ProtocolNegotiator negotiator) { + this.negotiator = Preconditions.checkNotNull(negotiator, "negotiator"); + } + + @Override + public AsciiString scheme() { + return negotiator.scheme(); + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + return negotiator.newHandler(grpcHandler); + } + + @Override + public void close() { + negotiator.close(); + } + } + public interface ClientFactory extends io.grpc.netty.ProtocolNegotiator.ClientFactory { @Override ProtocolNegotiator newNegotiator(); } + + public interface ServerFactory extends io.grpc.netty.ProtocolNegotiator.ServerFactory { + @Override ProtocolNegotiator newNegotiator(ObjectPool offloadExecutorPool); + } } diff --git a/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java b/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java index 520e3bfbd9a..70b97d8d5c3 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java @@ -29,6 +29,7 @@ import io.grpc.ExperimentalApi; import io.grpc.Internal; import io.grpc.ServerBuilder; +import io.grpc.ServerCredentials; import io.grpc.ServerStreamTracer; import io.grpc.internal.AbstractServerImplBuilder; import io.grpc.internal.FixedObjectPool; @@ -58,7 +59,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.CheckReturnValue; -import javax.annotation.Nullable; import javax.net.ssl.SSLException; /** @@ -98,8 +98,8 @@ public final class NettyServerBuilder extends AbstractServerImplBuilder workerEventLoopGroupPool = DEFAULT_WORKER_EVENT_LOOP_GROUP_POOL; private boolean forceHeapBuffer; - private SslContext sslContext; - private ProtocolNegotiator protocolNegotiator; + private ProtocolNegotiator.ServerFactory protocolNegotiatorFactory; + private final boolean freezeProtocolNegotiatorFactory; private int maxConcurrentCallsPerConnection = Integer.MAX_VALUE; private boolean autoFlowControl = true; private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW; @@ -121,7 +121,18 @@ public final class NettyServerBuilder extends AbstractServerImplBuilder buildClientTransportServers( @@ -144,15 +170,20 @@ public List buildClientTransportServers( } @CheckReturnValue - private NettyServerBuilder(int port) { + private NettyServerBuilder(SocketAddress address) { serverImplBuilder = new ServerImplBuilder(new NettyClientTransportServersBuilder()); - this.listenAddresses.add(new InetSocketAddress(port)); + this.listenAddresses.add(address); + this.protocolNegotiatorFactory = ProtocolNegotiators.serverPlaintextFactory(); + this.freezeProtocolNegotiatorFactory = false; } @CheckReturnValue - private NettyServerBuilder(SocketAddress address) { + NettyServerBuilder( + SocketAddress address, ProtocolNegotiator.ServerFactory negotiatorFactory) { serverImplBuilder = new ServerImplBuilder(new NettyClientTransportServersBuilder()); this.listenAddresses.add(address); + this.protocolNegotiatorFactory = checkNotNull(negotiatorFactory, "negotiatorFactory"); + this.freezeProtocolNegotiatorFactory = true; } @Internal @@ -317,25 +348,28 @@ void setForceHeapBuffer(boolean value) { * have been configured with {@link GrpcSslContexts}, but options could have been overridden. */ public NettyServerBuilder sslContext(SslContext sslContext) { + checkState(!freezeProtocolNegotiatorFactory, + "Cannot change security when using ServerCredentials"); if (sslContext != null) { checkArgument(sslContext.isServer(), "Client SSL context can not be used for server"); GrpcSslContexts.ensureAlpnAndH2Enabled(sslContext.applicationProtocolNegotiator()); + protocolNegotiatorFactory = ProtocolNegotiators.serverTlsFactory(sslContext); + } else { + protocolNegotiatorFactory = ProtocolNegotiators.serverPlaintextFactory(); } - this.sslContext = sslContext; return this; } /** - * Sets the {@link ProtocolNegotiator} to be used. If non-{@code null}, overrides the value - * specified in {@link #sslContext(SslContext)}. - * - *

Default: {@code null}. + * Sets the {@link ProtocolNegotiator} to be used. Overrides the value specified in {@link + * #sslContext(SslContext)}. */ @Internal - public final NettyServerBuilder protocolNegotiator( - @Nullable ProtocolNegotiator protocolNegotiator) { - this.protocolNegotiator = protocolNegotiator; + public final NettyServerBuilder protocolNegotiator(ProtocolNegotiator protocolNegotiator) { + checkState(!freezeProtocolNegotiatorFactory, + "Cannot change security when using ServerCredentials"); + this.protocolNegotiatorFactory = ProtocolNegotiators.fixedServerFactory(protocolNegotiator); return this; } @@ -586,12 +620,8 @@ List buildTransportServers( List streamTracerFactories) { assertEventLoopsAndChannelType(); - ProtocolNegotiator negotiator = protocolNegotiator; - if (negotiator == null) { - negotiator = sslContext != null - ? ProtocolNegotiators.serverTls(sslContext, this.serverImplBuilder.getExecutorPool()) - : ProtocolNegotiators.serverPlaintext(); - } + ProtocolNegotiator negotiator = protocolNegotiatorFactory.newNegotiator( + this.serverImplBuilder.getExecutorPool()); List transportServers = new ArrayList<>(listenAddresses.size()); for (SocketAddress listenAddress : listenAddresses) { @@ -631,23 +661,31 @@ NettyServerBuilder setTransportTracerFactory( @Override public NettyServerBuilder useTransportSecurity(File certChain, File privateKey) { + checkState(!freezeProtocolNegotiatorFactory, + "Cannot change security when using ServerCredentials"); + SslContext sslContext; try { sslContext = GrpcSslContexts.forServer(certChain, privateKey).build(); } catch (SSLException e) { // This should likely be some other, easier to catch exception. throw new RuntimeException(e); } + protocolNegotiatorFactory = ProtocolNegotiators.serverTlsFactory(sslContext); return this; } @Override public NettyServerBuilder useTransportSecurity(InputStream certChain, InputStream privateKey) { + checkState(!freezeProtocolNegotiatorFactory, + "Cannot change security when using ServerCredentials"); + SslContext sslContext; try { sslContext = GrpcSslContexts.forServer(certChain, privateKey).build(); } catch (SSLException e) { // This should likely be some other, easier to catch exception. throw new RuntimeException(e); } + protocolNegotiatorFactory = ProtocolNegotiators.serverTlsFactory(sslContext); return this; } } diff --git a/netty/src/main/java/io/grpc/netty/NettyServerCredentials.java b/netty/src/main/java/io/grpc/netty/NettyServerCredentials.java new file mode 100644 index 00000000000..bc8b4f5f621 --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/NettyServerCredentials.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.netty; + +import com.google.common.base.Preconditions; +import io.grpc.ServerCredentials; + +/** A credential with full control over the security handshake. */ +final class NettyServerCredentials extends ServerCredentials { + public static ServerCredentials create(ProtocolNegotiator.ServerFactory negotiator) { + return new NettyServerCredentials(negotiator); + } + + private final ProtocolNegotiator.ServerFactory negotiator; + + private NettyServerCredentials(ProtocolNegotiator.ServerFactory negotiator) { + this.negotiator = Preconditions.checkNotNull(negotiator, "negotiator"); + } + + public ProtocolNegotiator.ServerFactory getNegotiator() { + return negotiator; + } +} diff --git a/netty/src/main/java/io/grpc/netty/NettyServerProvider.java b/netty/src/main/java/io/grpc/netty/NettyServerProvider.java index 23b1e66c940..42d075d05cb 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerProvider.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerProvider.java @@ -17,10 +17,11 @@ package io.grpc.netty; import io.grpc.Internal; +import io.grpc.ServerCredentials; import io.grpc.ServerProvider; +import java.net.InetSocketAddress; - -/** Provider for {@link NettyChannelBuilder} instances. */ +/** Provider for {@link NettyServerBuilder} instances. */ @Internal public final class NettyServerProvider extends ServerProvider { @@ -38,5 +39,15 @@ protected int priority() { protected NettyServerBuilder builderForPort(int port) { return NettyServerBuilder.forPort(port); } + + @Override + protected NewServerBuilderResult newServerBuilderForPort(int port, ServerCredentials creds) { + ProtocolNegotiators.FromServerCredentialsResult result = ProtocolNegotiators.from(creds); + if (result.error != null) { + return NewServerBuilderResult.error(result.error); + } + return NewServerBuilderResult.serverBuilder( + new NettyServerBuilder(new InetSocketAddress(port), result.negotiator)); + } } diff --git a/netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java b/netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java index ff8a24669bc..ede511b68f6 100644 --- a/netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java +++ b/netty/src/main/java/io/grpc/netty/NettySslContextChannelCredentials.java @@ -16,6 +16,7 @@ package io.grpc.netty; +import com.google.common.base.Preconditions; import io.grpc.ChannelCredentials; import io.grpc.ExperimentalApi; import io.netty.handler.ssl.SslContext; @@ -30,6 +31,9 @@ private NettySslContextChannelCredentials() {} * with {@link GrpcSslContexts}, but options could have been overridden. */ public static ChannelCredentials create(SslContext sslContext) { + Preconditions.checkArgument(sslContext.isClient(), + "Server SSL context can not be used for client channel"); + GrpcSslContexts.ensureAlpnAndH2Enabled(sslContext.applicationProtocolNegotiator()); return NettyChannelCredentials.create(ProtocolNegotiators.tlsClientFactory(sslContext)); } } diff --git a/netty/src/main/java/io/grpc/netty/NettySslContextServerCredentials.java b/netty/src/main/java/io/grpc/netty/NettySslContextServerCredentials.java new file mode 100644 index 00000000000..9396cabdd23 --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/NettySslContextServerCredentials.java @@ -0,0 +1,39 @@ +/* + * 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.netty; + +import com.google.common.base.Preconditions; +import io.grpc.ExperimentalApi; +import io.grpc.ServerCredentials; +import io.netty.handler.ssl.SslContext; + +/** A credential that performs TLS with Netty's SslContext as configuration. */ +@ExperimentalApi("There is no plan to make this API stable, given transport API instability") +public final class NettySslContextServerCredentials { + private NettySslContextServerCredentials() {} + + /** + * Create a credential using Netty's SslContext as configuration. It must have been configured + * with {@link GrpcSslContexts}, but options could have been overridden. + */ + public static ServerCredentials create(SslContext sslContext) { + Preconditions.checkArgument(sslContext.isServer(), + "Client SSL context can not be used for server"); + GrpcSslContexts.ensureAlpnAndH2Enabled(sslContext.applicationProtocolNegotiator()); + return NettyServerCredentials.create(ProtocolNegotiators.serverTlsFactory(sslContext)); + } +} diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java index 6307b97c8a0..8a2c6f104b2 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiator.java @@ -16,8 +16,10 @@ package io.grpc.netty; +import io.grpc.internal.ObjectPool; import io.netty.channel.ChannelHandler; import io.netty.util.AsciiString; +import java.util.concurrent.Executor; /** * An class that provides a Netty handler to control protocol negotiation. @@ -52,4 +54,13 @@ interface ClientFactory { /** Returns the implicit port to use if no port was specified explicitly by the user. */ int getDefaultPort(); } + + interface ServerFactory { + /** + * Creates a new negotiator. + * + * @param offloadExecutorPool an executor pool for time-consuming tasks + */ + ProtocolNegotiator newNegotiator(ObjectPool offloadExecutorPool); + } } diff --git a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java index 7dbbddf9fc3..3779d050095 100644 --- a/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java +++ b/netty/src/main/java/io/grpc/netty/ProtocolNegotiators.java @@ -28,15 +28,19 @@ import io.grpc.ChannelLogger; import io.grpc.ChannelLogger.ChannelLogLevel; import io.grpc.ChoiceChannelCredentials; +import io.grpc.ChoiceServerCredentials; import io.grpc.CompositeCallCredentials; import io.grpc.CompositeChannelCredentials; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; +import io.grpc.InsecureServerCredentials; import io.grpc.InternalChannelz.Security; import io.grpc.InternalChannelz.Tls; import io.grpc.SecurityLevel; +import io.grpc.ServerCredentials; import io.grpc.Status; import io.grpc.TlsChannelCredentials; +import io.grpc.TlsServerCredentials; import io.grpc.internal.GrpcAttributes; import io.grpc.internal.GrpcUtil; import io.grpc.internal.ObjectPool; @@ -57,12 +61,14 @@ import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.OpenSslEngine; import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.handler.ssl.SslProvider; import io.netty.util.AsciiString; import io.netty.util.Attribute; import io.netty.util.AttributeMap; +import java.io.ByteArrayInputStream; import java.net.SocketAddress; import java.net.URI; import java.nio.channels.ClosedChannelException; @@ -85,6 +91,8 @@ final class ProtocolNegotiators { private static final Logger log = Logger.getLogger(ProtocolNegotiators.class.getName()); private static final EnumSet understoodTlsFeatures = EnumSet.noneOf(TlsChannelCredentials.Feature.class); + private static final EnumSet understoodServerTlsFeatures = + EnumSet.noneOf(TlsServerCredentials.Feature.class); private ProtocolNegotiators() { @@ -167,6 +175,72 @@ public FromChannelCredentialsResult withCallCredentials(CallCredentials callCred } } + public static FromServerCredentialsResult from(ServerCredentials creds) { + if (creds instanceof TlsServerCredentials) { + TlsServerCredentials tlsCreds = (TlsServerCredentials) creds; + Set incomprehensible = + tlsCreds.incomprehensible(understoodServerTlsFeatures); + if (!incomprehensible.isEmpty()) { + return FromServerCredentialsResult.error( + "TLS features not understood: " + incomprehensible); + } + SslContextBuilder builder = GrpcSslContexts.forServer( + new ByteArrayInputStream(tlsCreds.getCertificateChain()), + new ByteArrayInputStream(tlsCreds.getPrivateKey()), + tlsCreds.getPrivateKeyPassword()); + SslContext sslContext; + try { + sslContext = builder.build(); + } catch (SSLException ex) { + throw new IllegalArgumentException( + "Unexpected error converting ServerCredentials to Netty SslContext", ex); + } + return FromServerCredentialsResult.negotiator(serverTlsFactory(sslContext)); + + } else if (creds instanceof InsecureServerCredentials) { + return FromServerCredentialsResult.negotiator(serverPlaintextFactory()); + + } else if (creds instanceof NettyServerCredentials) { + NettyServerCredentials nettyCreds = (NettyServerCredentials) creds; + return FromServerCredentialsResult.negotiator(nettyCreds.getNegotiator()); + + } else if (creds instanceof ChoiceServerCredentials) { + ChoiceServerCredentials choiceCreds = (ChoiceServerCredentials) creds; + StringBuilder error = new StringBuilder(); + for (ServerCredentials innerCreds : choiceCreds.getCredentialsList()) { + FromServerCredentialsResult result = from(innerCreds); + if (result.error == null) { + return result; + } + error.append(", "); + error.append(result.error); + } + return FromServerCredentialsResult.error(error.substring(2)); + + } else { + return FromServerCredentialsResult.error( + "Unsupported credential type: " + creds.getClass().getName()); + } + } + + public static final class FromServerCredentialsResult { + public final ProtocolNegotiator.ServerFactory negotiator; + public final String error; + + private FromServerCredentialsResult(ProtocolNegotiator.ServerFactory negotiator, String error) { + this.negotiator = negotiator; + this.error = error; + } + + public static FromServerCredentialsResult error(String error) { + return new FromServerCredentialsResult(null, Preconditions.checkNotNull(error, "error")); + } + + public static FromServerCredentialsResult negotiator(ProtocolNegotiator.ServerFactory factory) { + return new FromServerCredentialsResult(Preconditions.checkNotNull(factory, "factory"), null); + } + } + static ChannelLogger negotiationLogger(ChannelHandlerContext ctx) { return negotiationLogger(ctx.channel()); } @@ -190,6 +264,26 @@ public void log(ChannelLogLevel level, String messageFormat, Object... args) {} return new NoopChannelLogger(); } + public static ProtocolNegotiator.ServerFactory fixedServerFactory( + ProtocolNegotiator negotiator) { + return new FixedProtocolNegotiatorServerFactory(negotiator); + } + + private static final class FixedProtocolNegotiatorServerFactory + implements ProtocolNegotiator.ServerFactory { + private final ProtocolNegotiator protocolNegotiator; + + public FixedProtocolNegotiatorServerFactory(ProtocolNegotiator protocolNegotiator) { + this.protocolNegotiator = + Preconditions.checkNotNull(protocolNegotiator, "protocolNegotiator"); + } + + @Override + public ProtocolNegotiator newNegotiator(ObjectPool offloadExecutorPool) { + return protocolNegotiator; + } + } + /** * Create a server plaintext handler for gRPC. */ @@ -197,6 +291,41 @@ public static ProtocolNegotiator serverPlaintext() { return new PlaintextProtocolNegotiator(); } + /** + * Create a server plaintext handler factory for gRPC. + */ + public static ProtocolNegotiator.ServerFactory serverPlaintextFactory() { + return new PlaintextProtocolNegotiatorServerFactory(); + } + + @VisibleForTesting + static final class PlaintextProtocolNegotiatorServerFactory + implements ProtocolNegotiator.ServerFactory { + @Override + public ProtocolNegotiator newNegotiator(ObjectPool offloadExecutorPool) { + return serverPlaintext(); + } + } + + public static ProtocolNegotiator.ServerFactory serverTlsFactory(SslContext sslContext) { + return new TlsProtocolNegotiatorServerFactory(sslContext); + } + + @VisibleForTesting + static final class TlsProtocolNegotiatorServerFactory + implements ProtocolNegotiator.ServerFactory { + private final SslContext sslContext; + + public TlsProtocolNegotiatorServerFactory(SslContext sslContext) { + this.sslContext = Preconditions.checkNotNull(sslContext, "sslContext"); + } + + @Override + public ProtocolNegotiator newNegotiator(ObjectPool offloadExecutorPool) { + return serverTls(sslContext, offloadExecutorPool); + } + } + /** * Create a server TLS handler for HTTP/2 capable of using ALPN/NPN. * @param executorPool a dedicated {@link Executor} pool for time-consuming TLS tasks diff --git a/netty/src/test/java/io/grpc/netty/NettyServerProviderTest.java b/netty/src/test/java/io/grpc/netty/NettyServerProviderTest.java index 70dbd643d58..6301ca51bef 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerProviderTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerProviderTest.java @@ -16,10 +16,13 @@ package io.grpc.netty; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import io.grpc.InsecureServerCredentials; +import io.grpc.ServerCredentials; import io.grpc.ServerProvider; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,4 +48,20 @@ public void basicMethods() { public void builderIsANettyBuilder() { assertSame(NettyServerBuilder.class, provider.builderForPort(443).getClass()); } + + @Test + public void newServerBuilderForPort_success() { + ServerProvider.NewServerBuilderResult result = + provider.newServerBuilderForPort(80, InsecureServerCredentials.create()); + assertThat(result.getServerBuilder()).isInstanceOf(NettyServerBuilder.class); + } + + @Test + public void newServerBuilderForPort_fail() { + ServerProvider.NewServerBuilderResult result = provider.newServerBuilderForPort( + 80, new FakeServerCredentials()); + assertThat(result.getError()).contains("FakeServerCredentials"); + } + + private static final class FakeServerCredentials extends ServerCredentials {} } diff --git a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java index 2e87a089e89..e5fc1b74c90 100644 --- a/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java +++ b/netty/src/test/java/io/grpc/netty/ProtocolNegotiatorsTest.java @@ -33,14 +33,18 @@ import io.grpc.CallCredentials; import io.grpc.ChannelCredentials; import io.grpc.ChoiceChannelCredentials; +import io.grpc.ChoiceServerCredentials; import io.grpc.CompositeChannelCredentials; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; +import io.grpc.InsecureServerCredentials; import io.grpc.InternalChannelz.Security; import io.grpc.SecurityLevel; +import io.grpc.ServerCredentials; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.TlsChannelCredentials; +import io.grpc.TlsServerCredentials; import io.grpc.internal.GrpcAttributes; import io.grpc.internal.testing.TestUtils; import io.grpc.netty.ProtocolNegotiators.ClientTlsHandler; @@ -110,6 +114,7 @@ import javax.net.ssl.SSLSession; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.DisableOnDebug; @@ -128,6 +133,15 @@ public class ProtocolNegotiatorsTest { @Override public void run() {} }; + private static File server1Cert; + private static File server1Key; + + @BeforeClass + public static void loadCerts() throws Exception { + server1Cert = TestUtils.loadCert("server1.pem"); + server1Key = TestUtils.loadCert("server1.key"); + } + private static final int TIMEOUT_SECONDS = 60; @Rule public final TestRule globalTimeout = new DisableOnDebug(Timeout.seconds(TIMEOUT_SECONDS)); @SuppressWarnings("deprecation") // https://github.com/grpc/grpc-java/issues/7467 @@ -168,7 +182,7 @@ public void tearDown() { } @Test - public void from_unknown() { + public void fromClient_unknown() { ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(new ChannelCredentials() {}); assertThat(result.error).isNotNull(); @@ -177,7 +191,7 @@ public void from_unknown() { } @Test - public void from_tls() { + public void fromClient_tls() { ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(TlsChannelCredentials.create()); assertThat(result.error).isNull(); @@ -187,7 +201,7 @@ public void from_tls() { } @Test - public void from_unspportedTls() { + public void fromClient_unsupportedTls() { ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(TlsChannelCredentials.newBuilder().requireFakeFeature().build()); assertThat(result.error).contains("FAKE"); @@ -196,7 +210,7 @@ public void from_unspportedTls() { } @Test - public void from_insecure() { + public void fromClient_insecure() { ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(InsecureChannelCredentials.create()); assertThat(result.error).isNull(); @@ -206,7 +220,7 @@ public void from_insecure() { } @Test - public void from_composite() { + public void fromClient_composite() { CallCredentials callCredentials = mock(CallCredentials.class); ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(CompositeChannelCredentials.create( @@ -225,7 +239,7 @@ public void from_composite() { } @Test - public void from_netty() { + public void fromClient_netty() { ProtocolNegotiator.ClientFactory factory = mock(ProtocolNegotiator.ClientFactory.class); ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(NettyChannelCredentials.create(factory)); @@ -235,7 +249,7 @@ public void from_netty() { } @Test - public void from_choice() { + public void fromClient_choice() { ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(ChoiceChannelCredentials.create( new ChannelCredentials() {}, @@ -257,7 +271,7 @@ public void from_choice() { } @Test - public void from_choice_unknown() { + public void fromClient_choice_unknown() { ProtocolNegotiators.FromChannelCredentialsResult result = ProtocolNegotiators.from(ChoiceChannelCredentials.create( new ChannelCredentials() {})); @@ -266,6 +280,82 @@ public void from_choice_unknown() { assertThat(result.negotiator).isNull(); } + @Test + public void fromServer_unknown() { + ProtocolNegotiators.FromServerCredentialsResult result = + ProtocolNegotiators.from(new ServerCredentials() {}); + assertThat(result.error).isNotNull(); + assertThat(result.negotiator).isNull(); + } + + @Test + public void fromServer_tls() throws Exception { + ProtocolNegotiators.FromServerCredentialsResult result = + ProtocolNegotiators.from(TlsServerCredentials.create(server1Cert, server1Key)); + assertThat(result.error).isNull(); + assertThat(result.negotiator) + .isInstanceOf(ProtocolNegotiators.TlsProtocolNegotiatorServerFactory.class); + } + + @Test + public void fromServer_unsupportedTls() throws Exception { + ProtocolNegotiators.FromServerCredentialsResult result = ProtocolNegotiators.from( + TlsServerCredentials.newBuilder() + .keyManager(server1Cert, server1Key) + .requireFakeFeature() + .build()); + assertThat(result.error).contains("FAKE"); + assertThat(result.negotiator).isNull(); + } + + @Test + public void fromServer_insecure() { + ProtocolNegotiators.FromServerCredentialsResult result = + ProtocolNegotiators.from(InsecureServerCredentials.create()); + assertThat(result.error).isNull(); + assertThat(result.negotiator) + .isInstanceOf(ProtocolNegotiators.PlaintextProtocolNegotiatorServerFactory.class); + } + + @Test + public void fromServer_netty() { + ProtocolNegotiator.ServerFactory factory = mock(ProtocolNegotiator.ServerFactory.class); + ProtocolNegotiators.FromServerCredentialsResult result = + ProtocolNegotiators.from(NettyServerCredentials.create(factory)); + assertThat(result.error).isNull(); + assertThat(result.negotiator).isSameInstanceAs(factory); + } + + @Test + public void fromServer_choice() throws Exception { + ProtocolNegotiators.FromServerCredentialsResult result = + ProtocolNegotiators.from(ChoiceServerCredentials.create( + new ServerCredentials() {}, + TlsServerCredentials.create(server1Cert, server1Key), + InsecureServerCredentials.create())); + assertThat(result.error).isNull(); + assertThat(result.negotiator) + .isInstanceOf(ProtocolNegotiators.TlsProtocolNegotiatorServerFactory.class); + + result = ProtocolNegotiators.from(ChoiceServerCredentials.create( + InsecureServerCredentials.create(), + new ServerCredentials() {}, + TlsServerCredentials.create(server1Cert, server1Key))); + assertThat(result.error).isNull(); + assertThat(result.negotiator) + .isInstanceOf(ProtocolNegotiators.PlaintextProtocolNegotiatorServerFactory.class); + } + + @Test + public void fromServer_choice_unknown() { + ProtocolNegotiators.FromServerCredentialsResult result = + ProtocolNegotiators.from(ChoiceServerCredentials.create( + new ServerCredentials() {})); + assertThat(result.error).isNotNull(); + assertThat(result.negotiator).isNull(); + } + + @Test public void waitUntilActiveHandler_handlerAdded() throws Exception { final CountDownLatch latch = new CountDownLatch(1); From 2261227d825da930c7dae1e48c1358f1d991d5d4 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 6 Nov 2020 16:33:57 -0800 Subject: [PATCH 3/4] alts: Add ServerCredentials --- .../io/grpc/alts/AltsChannelCredentials.java | 3 +- .../java/io/grpc/alts/AltsServerBuilder.java | 61 +----------- .../io/grpc/alts/AltsServerCredentials.java | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+), 57 deletions(-) create mode 100644 alts/src/main/java/io/grpc/alts/AltsServerCredentials.java diff --git a/alts/src/main/java/io/grpc/alts/AltsChannelCredentials.java b/alts/src/main/java/io/grpc/alts/AltsChannelCredentials.java index ca304c96070..0bb96378ccf 100644 --- a/alts/src/main/java/io/grpc/alts/AltsChannelCredentials.java +++ b/alts/src/main/java/io/grpc/alts/AltsChannelCredentials.java @@ -52,6 +52,7 @@ public static Builder newBuilder() { return new Builder(); } + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4151") public static final class Builder { private final ImmutableList.Builder targetServiceAccountsBuilder = ImmutableList.builder(); @@ -131,7 +132,7 @@ public int getDefaultPort() { private static final AsciiString SCHEME = AsciiString.of("https"); - private static final class FailingProtocolNegotiator implements ProtocolNegotiator { + static final class FailingProtocolNegotiator implements ProtocolNegotiator { private final Status status; public FailingProtocolNegotiator(Status status) { diff --git a/alts/src/main/java/io/grpc/alts/AltsServerBuilder.java b/alts/src/main/java/io/grpc/alts/AltsServerBuilder.java index f23e6612f9e..108b294e215 100644 --- a/alts/src/main/java/io/grpc/alts/AltsServerBuilder.java +++ b/alts/src/main/java/io/grpc/alts/AltsServerBuilder.java @@ -17,32 +17,21 @@ package io.grpc.alts; import io.grpc.BindableService; -import io.grpc.Channel; import io.grpc.CompressorRegistry; import io.grpc.DecompressorRegistry; import io.grpc.ExperimentalApi; import io.grpc.HandlerRegistry; -import io.grpc.Metadata; import io.grpc.Server; import io.grpc.ServerBuilder; -import io.grpc.ServerCall; -import io.grpc.ServerCall.Listener; -import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.ServerServiceDefinition; import io.grpc.ServerStreamTracer; import io.grpc.ServerTransportFilter; -import io.grpc.Status; -import io.grpc.alts.internal.AltsProtocolNegotiator; -import io.grpc.internal.ObjectPool; -import io.grpc.internal.SharedResourcePool; import io.grpc.netty.NettyServerBuilder; import java.io.File; import java.net.InetSocketAddress; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; /** * gRPC secure server builder used for ALTS. This class adds on the necessary ALTS support to create @@ -50,12 +39,9 @@ */ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4151") public final class AltsServerBuilder extends ServerBuilder { - - private static final Logger logger = Logger.getLogger(AltsServerBuilder.class.getName()); private final NettyServerBuilder delegate; - private ObjectPool handshakerChannelPool = - SharedResourcePool.forResource(HandshakerServiceChannel.SHARED_HANDSHAKER_CHANNEL); - private boolean enableUntrustedAlts; + private final AltsServerCredentials.Builder credentialsBuilder = + new AltsServerCredentials.Builder(); private AltsServerBuilder(NettyServerBuilder nettyDelegate) { this.delegate = nettyDelegate; @@ -72,17 +58,13 @@ public static AltsServerBuilder forPort(int port) { * is running on Google Cloud Platform. */ public AltsServerBuilder enableUntrustedAltsForTesting() { - enableUntrustedAlts = true; + credentialsBuilder.enableUntrustedAltsForTesting(); return this; } /** Sets a new handshaker service address for testing. */ public AltsServerBuilder setHandshakerAddressForTesting(String handshakerAddress) { - // Instead of using the default shared channel to the handshaker service, create a separate - // resource to the test address. - handshakerChannelPool = - SharedResourcePool.forResource( - HandshakerServiceChannel.getHandshakerChannelForTesting(handshakerAddress)); + credentialsBuilder.setHandshakerAddressForTesting(handshakerAddress); return this; } @@ -172,40 +154,7 @@ public AltsServerBuilder intercept(ServerInterceptor interceptor) { /** {@inheritDoc} */ @Override public Server build() { - if (!CheckGcpEnvironment.isOnGcp()) { - if (enableUntrustedAlts) { - logger.log( - Level.WARNING, - "Untrusted ALTS mode is enabled and we cannot guarantee the trustworthiness of the " - + "ALTS handshaker service"); - } else { - Status status = - Status.INTERNAL.withDescription("ALTS is only allowed to run on Google Cloud Platform"); - delegate.intercept(new FailingServerInterceptor(status)); - } - } - - delegate.protocolNegotiator( - AltsProtocolNegotiator.serverAltsProtocolNegotiator(handshakerChannelPool)); + delegate.protocolNegotiator(credentialsBuilder.buildProtocolNegotiator()); return delegate.build(); } - - /** An implementation of {@link ServerInterceptor} that fails each call. */ - static final class FailingServerInterceptor implements ServerInterceptor { - - private final Status status; - - public FailingServerInterceptor(Status status) { - this.status = status; - } - - @Override - public Listener interceptCall( - ServerCall serverCall, - Metadata metadata, - ServerCallHandler nextHandler) { - serverCall.close(status, new Metadata()); - return new Listener() {}; - } - } } diff --git a/alts/src/main/java/io/grpc/alts/AltsServerCredentials.java b/alts/src/main/java/io/grpc/alts/AltsServerCredentials.java new file mode 100644 index 00000000000..65a350daadd --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/AltsServerCredentials.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 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.alts; + +import io.grpc.Channel; +import io.grpc.ExperimentalApi; +import io.grpc.ServerCredentials; +import io.grpc.Status; +import io.grpc.alts.internal.AltsProtocolNegotiator; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.SharedResourcePool; +import io.grpc.netty.InternalNettyServerCredentials; +import io.grpc.netty.InternalProtocolNegotiator; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * gRPC secure server builder used for ALTS. This class adds on the necessary ALTS support to create + * a production server on Google Cloud Platform. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") +public final class AltsServerCredentials { + private static final Logger logger = Logger.getLogger(AltsServerCredentials.class.getName()); + + private AltsServerCredentials() {} + + public static ServerCredentials create() { + return newBuilder().build(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/7621") + public static final class Builder { + private ObjectPool handshakerChannelPool = + SharedResourcePool.forResource(HandshakerServiceChannel.SHARED_HANDSHAKER_CHANNEL); + private boolean enableUntrustedAlts; + + /** + * Enables untrusted ALTS for testing. If this function is called, we will not check whether + * ALTS is running on Google Cloud Platform. + */ + public Builder enableUntrustedAltsForTesting() { + enableUntrustedAlts = true; + return this; + } + + /** Sets a new handshaker service address for testing. */ + public Builder setHandshakerAddressForTesting(String handshakerAddress) { + // Instead of using the default shared channel to the handshaker service, create a separate + // resource to the test address. + handshakerChannelPool = + SharedResourcePool.forResource( + HandshakerServiceChannel.getHandshakerChannelForTesting(handshakerAddress)); + return this; + } + + public ServerCredentials build() { + return InternalNettyServerCredentials.create(buildProtocolNegotiator()); + } + + InternalProtocolNegotiator.ProtocolNegotiator buildProtocolNegotiator() { + if (!CheckGcpEnvironment.isOnGcp()) { + if (enableUntrustedAlts) { + logger.log( + Level.WARNING, + "Untrusted ALTS mode is enabled and we cannot guarantee the trustworthiness of the " + + "ALTS handshaker service"); + } else { + Status status = Status.INTERNAL.withDescription( + "ALTS is only allowed to run on Google Cloud Platform"); + return new AltsChannelCredentials.FailingProtocolNegotiator(status); + } + } + + return AltsProtocolNegotiator.serverAltsProtocolNegotiator(handshakerChannelPool); + } + } +} From c0137cc5a26a0c22becac6dee85e803094571119 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 6 Nov 2020 16:34:48 -0800 Subject: [PATCH 4/4] Migrate callers to ServerCredentials --- .../grpc/benchmarks/TransportBenchmark.java | 15 ++++--- .../benchmarks/netty/AbstractBenchmark.java | 9 ++-- .../io/grpc/benchmarks/driver/LoadWorker.java | 3 +- .../integration/TestServiceServer.java | 45 ++++++++----------- .../integration/AutoWindowSizingOnTest.java | 3 +- .../testing/integration/ConcurrencyTest.java | 24 +++++----- .../testing/integration/Http2NettyTest.java | 11 +++-- .../testing/integration/Http2OkHttpTest.java | 9 ++-- .../integration/TransportCompressionTest.java | 3 +- 9 files changed, 64 insertions(+), 58 deletions(-) diff --git a/benchmarks/src/jmh/java/io/grpc/benchmarks/TransportBenchmark.java b/benchmarks/src/jmh/java/io/grpc/benchmarks/TransportBenchmark.java index b673657cb35..1af821be837 100644 --- a/benchmarks/src/jmh/java/io/grpc/benchmarks/TransportBenchmark.java +++ b/benchmarks/src/jmh/java/io/grpc/benchmarks/TransportBenchmark.java @@ -19,10 +19,13 @@ import static io.grpc.benchmarks.Utils.pickUnusedPort; import com.google.protobuf.ByteString; +import io.grpc.InsecureChannelCredentials; +import io.grpc.InsecureServerCredentials; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.Server; import io.grpc.ServerBuilder; +import io.grpc.ServerCredentials; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.benchmarks.proto.BenchmarkServiceGrpc; @@ -80,6 +83,7 @@ public enum Transport { @Setup public void setUp() throws Exception { + ServerCredentials serverCreds = InsecureServerCredentials.create(); ServerBuilder serverBuilder; ManagedChannelBuilder channelBuilder; switch (transport) { @@ -93,7 +97,7 @@ public void setUp() throws Exception { case NETTY: { InetSocketAddress address = new InetSocketAddress("localhost", pickUnusedPort()); - serverBuilder = NettyServerBuilder.forAddress(address); + serverBuilder = NettyServerBuilder.forAddress(address, serverCreds); channelBuilder = NettyChannelBuilder.forAddress(address) .negotiationType(NegotiationType.PLAINTEXT); break; @@ -103,7 +107,7 @@ public void setUp() throws Exception { String name = "bench" + Math.random(); LocalAddress address = new LocalAddress(name); EventLoopGroup group = new DefaultEventLoopGroup(); - serverBuilder = NettyServerBuilder.forAddress(address) + serverBuilder = NettyServerBuilder.forAddress(address, serverCreds) .bossEventLoopGroup(group) .workerEventLoopGroup(group) .channelType(LocalServerChannel.class); @@ -125,7 +129,7 @@ public void setUp() throws Exception { Class serverChannelClass = Class.forName("io.netty.channel.epoll.EpollServerSocketChannel") .asSubclass(ServerChannel.class); - serverBuilder = NettyServerBuilder.forAddress(address) + serverBuilder = NettyServerBuilder.forAddress(address, serverCreds) .bossEventLoopGroup(group) .workerEventLoopGroup(group) .channelType(serverChannelClass); @@ -143,8 +147,9 @@ public void setUp() throws Exception { { int port = pickUnusedPort(); InetSocketAddress address = new InetSocketAddress("localhost", port); - serverBuilder = NettyServerBuilder.forAddress(address); - channelBuilder = OkHttpChannelBuilder.forAddress("localhost", port).usePlaintext(); + serverBuilder = NettyServerBuilder.forAddress(address, serverCreds); + channelBuilder = OkHttpChannelBuilder + .forAddress("localhost", port, InsecureChannelCredentials.create()); break; } default: diff --git a/benchmarks/src/jmh/java/io/grpc/benchmarks/netty/AbstractBenchmark.java b/benchmarks/src/jmh/java/io/grpc/benchmarks/netty/AbstractBenchmark.java index 0a4d4089d16..3edc92ac02a 100644 --- a/benchmarks/src/jmh/java/io/grpc/benchmarks/netty/AbstractBenchmark.java +++ b/benchmarks/src/jmh/java/io/grpc/benchmarks/netty/AbstractBenchmark.java @@ -18,6 +18,7 @@ import io.grpc.CallOptions; import io.grpc.ClientCall; +import io.grpc.InsecureServerCredentials; import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.MethodDescriptor; @@ -25,6 +26,7 @@ import io.grpc.Server; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; +import io.grpc.ServerCredentials; import io.grpc.ServerServiceDefinition; import io.grpc.ServiceDescriptor; import io.grpc.Status; @@ -195,11 +197,12 @@ public void setup(ExecutorType clientExecutor, ChannelType channelType, int maxConcurrentStreams, int channelCount) throws Exception { + ServerCredentials serverCreds = InsecureServerCredentials.create(); NettyServerBuilder serverBuilder; NettyChannelBuilder channelBuilder; if (channelType == ChannelType.LOCAL) { LocalAddress address = new LocalAddress("netty-e2e-benchmark"); - serverBuilder = NettyServerBuilder.forAddress(address); + serverBuilder = NettyServerBuilder.forAddress(address, serverCreds); serverBuilder.channelType(LocalServerChannel.class); channelBuilder = NettyChannelBuilder.forAddress(address); channelBuilder.channelType(LocalChannel.class); @@ -209,8 +212,8 @@ public void setup(ExecutorType clientExecutor, sock.bind(new InetSocketAddress(BENCHMARK_ADDR, 0)); SocketAddress address = sock.getLocalSocketAddress(); sock.close(); - serverBuilder = - NettyServerBuilder.forAddress(address).channelType(NioServerSocketChannel.class); + serverBuilder = NettyServerBuilder.forAddress(address, serverCreds) + .channelType(NioServerSocketChannel.class); channelBuilder = NettyChannelBuilder.forAddress(address).channelType(NioSocketChannel.class); } diff --git a/benchmarks/src/main/java/io/grpc/benchmarks/driver/LoadWorker.java b/benchmarks/src/main/java/io/grpc/benchmarks/driver/LoadWorker.java index 595782438ef..63fbef0cabe 100644 --- a/benchmarks/src/main/java/io/grpc/benchmarks/driver/LoadWorker.java +++ b/benchmarks/src/main/java/io/grpc/benchmarks/driver/LoadWorker.java @@ -17,6 +17,7 @@ package io.grpc.benchmarks.driver; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.grpc.InsecureServerCredentials; import io.grpc.Server; import io.grpc.Status; import io.grpc.benchmarks.proto.Control; @@ -49,7 +50,7 @@ public class LoadWorker { .setDaemon(true) .setNameFormat("load-worker-%d") .build()); - this.driverServer = NettyServerBuilder.forPort(driverPort) + this.driverServer = NettyServerBuilder.forPort(driverPort, InsecureServerCredentials.create()) .directExecutor() .channelType(NioServerSocketChannel.class) .workerEventLoopGroup(singleThreadGroup) diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java index f1d4d16173a..2a5c0ebe557 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceServer.java @@ -18,13 +18,14 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.Grpc; +import io.grpc.InsecureServerCredentials; import io.grpc.Server; +import io.grpc.ServerCredentials; import io.grpc.ServerInterceptors; -import io.grpc.alts.AltsServerBuilder; +import io.grpc.TlsServerCredentials; +import io.grpc.alts.AltsServerCredentials; import io.grpc.internal.testing.TestUtils; -import io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.NettyServerBuilder; -import io.netty.handler.ssl.SslContext; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -129,32 +130,22 @@ void parseArgs(String[] args) { @VisibleForTesting void start() throws Exception { executor = Executors.newSingleThreadScheduledExecutor(); - SslContext sslContext = null; + ServerCredentials serverCreds; if (useAlts) { - server = - AltsServerBuilder.forPort(port) - .addService( - ServerInterceptors.intercept( - new TestServiceImpl(executor), TestServiceImpl.interceptors())) - .build() - .start(); + serverCreds = AltsServerCredentials.create(); + } else if (useTls) { + serverCreds = TlsServerCredentials.create( + TestUtils.loadCert("server1.pem"), TestUtils.loadCert("server1.key")); } else { - if (useTls) { - sslContext = - GrpcSslContexts.forServer( - TestUtils.loadCert("server1.pem"), TestUtils.loadCert("server1.key")) - .build(); - } - server = - NettyServerBuilder.forPort(port) - .sslContext(sslContext) - .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) - .addService( - ServerInterceptors.intercept( - new TestServiceImpl(executor), TestServiceImpl.interceptors())) - .build() - .start(); + serverCreds = InsecureServerCredentials.create(); } + server = Grpc.newServerBuilderForPort(port, serverCreds) + .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) + .addService( + ServerInterceptors.intercept( + new TestServiceImpl(executor), TestServiceImpl.interceptors())) + .build() + .start(); } @VisibleForTesting diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/AutoWindowSizingOnTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/AutoWindowSizingOnTest.java index 05fd970e3e0..c240932767c 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/AutoWindowSizingOnTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/AutoWindowSizingOnTest.java @@ -16,6 +16,7 @@ package io.grpc.testing.integration; +import io.grpc.InsecureServerCredentials; import io.grpc.ServerBuilder; import io.grpc.netty.InternalNettyChannelBuilder; import io.grpc.netty.InternalNettyServerBuilder; @@ -30,7 +31,7 @@ public class AutoWindowSizingOnTest extends AbstractInteropTest { @Override protected ServerBuilder getServerBuilder() { - NettyServerBuilder builder = NettyServerBuilder.forPort(0) + NettyServerBuilder builder = NettyServerBuilder.forPort(0, InsecureServerCredentials.create()) .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE); // Disable the default census stats tracer, use testing tracer instead. InternalNettyServerBuilder.setStatsEnabled(builder, false); diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/ConcurrencyTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/ConcurrencyTest.java index 2b6e0b690c9..baffffedebd 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/ConcurrencyTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/ConcurrencyTest.java @@ -18,19 +18,20 @@ import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.ChannelCredentials; +import io.grpc.Grpc; import io.grpc.ManagedChannel; import io.grpc.Server; +import io.grpc.ServerCredentials; import io.grpc.internal.testing.TestUtils; import io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.NegotiationType; -import io.grpc.netty.NettyChannelBuilder; -import io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.NettySslContextChannelCredentials; +import io.grpc.netty.NettySslContextServerCredentials; import io.grpc.stub.StreamObserver; import io.grpc.testing.integration.Messages.ResponseParameters; import io.grpc.testing.integration.Messages.StreamingOutputCallRequest; import io.grpc.testing.integration.Messages.StreamingOutputCallResponse; import io.netty.handler.ssl.ClientAuth; -import io.netty.handler.ssl.SslContext; import java.io.File; import java.io.IOException; import java.security.cert.CertificateException; @@ -191,14 +192,13 @@ private Server newServer() throws CertificateException, IOException { TestUtils.loadX509Cert("ca.pem") }; - SslContext sslContext = + ServerCredentials serverCreds = NettySslContextServerCredentials.create( GrpcSslContexts.forServer(serverCertChainFile, serverPrivateKeyFile) .trustManager(serverTrustedCaCerts) .clientAuth(ClientAuth.REQUIRE) - .build(); + .build()); - return NettyServerBuilder.forPort(0) - .sslContext(sslContext) + return Grpc.newServerBuilderForPort(0, serverCreds) .addService(new TestServiceImpl(serverExecutor)) .build() .start(); @@ -211,16 +211,14 @@ private ManagedChannel newClientChannel() throws CertificateException, IOExcepti TestUtils.loadX509Cert("ca.pem") }; - SslContext sslContext = + ChannelCredentials channelCreds = NettySslContextChannelCredentials.create( GrpcSslContexts.forClient() .keyManager(clientCertChainFile, clientPrivateKeyFile) .trustManager(clientTrustedCaCerts) - .build(); + .build()); - return NettyChannelBuilder.forAddress("localhost", server.getPort()) + return Grpc.newChannelBuilder("localhost:" + server.getPort(), channelCreds) .overrideAuthority(TestUtils.TEST_SERVER_HOST) - .negotiationType(NegotiationType.TLS) - .sslContext(sslContext) .build(); } } diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/Http2NettyTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/Http2NettyTest.java index 0d391039105..00d7a22dc15 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/Http2NettyTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/Http2NettyTest.java @@ -20,12 +20,14 @@ import static org.junit.Assert.assertNotEquals; import io.grpc.ServerBuilder; +import io.grpc.ServerCredentials; import io.grpc.internal.testing.TestUtils; import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.InternalNettyChannelBuilder; import io.grpc.netty.InternalNettyServerBuilder; import io.grpc.netty.NettyChannelBuilder; import io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.NettySslContextServerCredentials; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SupportedCipherSuiteFilter; import java.io.IOException; @@ -45,15 +47,16 @@ public class Http2NettyTest extends AbstractInteropTest { protected ServerBuilder getServerBuilder() { // Starts the server with HTTPS. try { - NettyServerBuilder builder = NettyServerBuilder.forPort(0) - .flowControlWindow(AbstractInteropTest.TEST_FLOW_CONTROL_WINDOW) - .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) - .sslContext(GrpcSslContexts + ServerCredentials serverCreds = NettySslContextServerCredentials.create( + GrpcSslContexts .forServer(TestUtils.loadCert("server1.pem"), TestUtils.loadCert("server1.key")) .clientAuth(ClientAuth.REQUIRE) .trustManager(TestUtils.loadCert("ca.pem")) .ciphers(TestUtils.preferredTestCiphers(), SupportedCipherSuiteFilter.INSTANCE) .build()); + NettyServerBuilder builder = NettyServerBuilder.forPort(0, serverCreds) + .flowControlWindow(AbstractInteropTest.TEST_FLOW_CONTROL_WINDOW) + .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE); // Disable the default census stats tracer, use testing tracer instead. InternalNettyServerBuilder.setStatsEnabled(builder, false); return builder.addStreamTracerFactory(createCustomCensusTracerFactory()); diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/Http2OkHttpTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/Http2OkHttpTest.java index 612774c7564..10a829ff451 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/Http2OkHttpTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/Http2OkHttpTest.java @@ -24,12 +24,14 @@ import com.squareup.okhttp.ConnectionSpec; import io.grpc.ManagedChannel; import io.grpc.ServerBuilder; +import io.grpc.ServerCredentials; import io.grpc.internal.GrpcUtil; import io.grpc.internal.testing.StreamRecorder; import io.grpc.internal.testing.TestUtils; import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.InternalNettyServerBuilder; import io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.NettySslContextServerCredentials; import io.grpc.okhttp.InternalOkHttpChannelBuilder; import io.grpc.okhttp.OkHttpChannelBuilder; import io.grpc.okhttp.internal.Platform; @@ -78,10 +80,11 @@ protected ServerBuilder getServerBuilder() { .forServer(TestUtils.loadCert("server1.pem"), TestUtils.loadCert("server1.key")); GrpcSslContexts.configure(contextBuilder, sslProvider); contextBuilder.ciphers(TestUtils.preferredTestCiphers(), SupportedCipherSuiteFilter.INSTANCE); - NettyServerBuilder builder = NettyServerBuilder.forPort(0) + ServerCredentials serverCreds = + NettySslContextServerCredentials.create(contextBuilder.build()); + NettyServerBuilder builder = NettyServerBuilder.forPort(0, serverCreds) .flowControlWindow(AbstractInteropTest.TEST_FLOW_CONTROL_WINDOW) - .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) - .sslContext(contextBuilder.build()); + .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE); // Disable the default census stats tracer, use testing tracer instead. InternalNettyServerBuilder.setStatsEnabled(builder, false); return builder.addStreamTracerFactory(createCustomCensusTracerFactory()); diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java index 8d184dd4428..b9692383254 100644 --- a/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java +++ b/interop-testing/src/test/java/io/grpc/testing/integration/TransportCompressionTest.java @@ -29,6 +29,7 @@ import io.grpc.DecompressorRegistry; import io.grpc.ForwardingClientCall; import io.grpc.ForwardingClientCallListener; +import io.grpc.InsecureServerCredentials; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.ServerBuilder; @@ -85,7 +86,7 @@ public static void registerCompressors() { @Override protected ServerBuilder getServerBuilder() { - NettyServerBuilder builder = NettyServerBuilder.forPort(0) + NettyServerBuilder builder = NettyServerBuilder.forPort(0, InsecureServerCredentials.create()) .maxInboundMessageSize(AbstractInteropTest.MAX_MESSAGE_SIZE) .compressorRegistry(compressors) .decompressorRegistry(decompressors)