From c8a94d105900f1dea2480e05e3d484a385d5255b Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 31 Jul 2020 12:03:04 -0700 Subject: [PATCH] api: Add ChannelCredentials --- .../main/java/io/grpc/ChannelCredentials.java | 38 ++++ .../io/grpc/ChoiceChannelCredentials.java | 57 ++++++ .../io/grpc/CompositeCallCredentials.java | 101 ++++++++++ .../io/grpc/CompositeChannelCredentials.java | 49 +++++ api/src/main/java/io/grpc/Grpc.java | 62 ++++++ .../io/grpc/InsecureChannelCredentials.java | 27 +++ .../java/io/grpc/ManagedChannelBuilder.java | 2 + .../java/io/grpc/ManagedChannelProvider.java | 78 ++++---- .../java/io/grpc/ManagedChannelRegistry.java | 189 ++++++++++++++++++ .../java/io/grpc/TlsChannelCredentials.java | 122 +++++++++++ .../io/grpc/CompositeCallCredentialsTest.java | 115 +++++++++++ .../io/grpc/ManagedChannelProviderTest.java | 46 ----- .../grpc/ManagedChannelRegistryAccessor.java} | 12 +- .../io/grpc/ManagedChannelRegistryTest.java | 183 +++++++++++++++++ .../main/java/io/grpc/internal/GrpcUtil.java | 1 + netty/build.gradle | 1 + .../grpc/netty/NettyChannelProviderTest.java | 7 +- okhttp/build.gradle | 1 + .../okhttp/OkHttpChannelProviderTest.java | 7 +- 19 files changed, 998 insertions(+), 100 deletions(-) create mode 100644 api/src/main/java/io/grpc/ChannelCredentials.java create mode 100644 api/src/main/java/io/grpc/ChoiceChannelCredentials.java create mode 100644 api/src/main/java/io/grpc/CompositeCallCredentials.java create mode 100644 api/src/main/java/io/grpc/CompositeChannelCredentials.java create mode 100644 api/src/main/java/io/grpc/InsecureChannelCredentials.java create mode 100644 api/src/main/java/io/grpc/ManagedChannelRegistry.java create mode 100644 api/src/main/java/io/grpc/TlsChannelCredentials.java create mode 100644 api/src/test/java/io/grpc/CompositeCallCredentialsTest.java delete mode 100644 api/src/test/java/io/grpc/ManagedChannelProviderTest.java rename api/src/{main/java/io/grpc/InternalManagedChannelProvider.java => test/java/io/grpc/ManagedChannelRegistryAccessor.java} (64%) create mode 100644 api/src/test/java/io/grpc/ManagedChannelRegistryTest.java diff --git a/api/src/main/java/io/grpc/ChannelCredentials.java b/api/src/main/java/io/grpc/ChannelCredentials.java new file mode 100644 index 0000000000..a601fb1867 --- /dev/null +++ b/api/src/main/java/io/grpc/ChannelCredentials.java @@ -0,0 +1,38 @@ +/* + * 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 channels. There is no generic mechanism for + * processing arbitrary {@code ChannelCredentials}; the consumer of the credential (the channel) + * 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 ChoiceChannelCredentials}. + * + *

A {@code ChannelCredential} provides client identity and authenticates the server. This is + * different from {@link CallCredentials}, which only provides client 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 ChannelCredentials} type for + * variables instead of the concrete type. Freshly-constructed credentials should be returned as + * {@code ChannelCredentials} instead of a concrete type to encourage this pattern. Concrete types + * would only be used after {@code instanceof} checks (which must consider + * {@code ChoiceChannelCredentials}!). + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") +public abstract class ChannelCredentials {} diff --git a/api/src/main/java/io/grpc/ChoiceChannelCredentials.java b/api/src/main/java/io/grpc/ChoiceChannelCredentials.java new file mode 100644 index 0000000000..2fe00da86e --- /dev/null +++ b/api/src/main/java/io/grpc/ChoiceChannelCredentials.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 ChannelCredentials}, where any one may be used. The credentials are in + * preference order. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") +public final class ChoiceChannelCredentials extends ChannelCredentials { + /** + * Constructs with the provided {@code creds} as options, with preferred credentials first. + * + * @throws IllegalArgumentException if no creds are provided + */ + public static ChannelCredentials create(ChannelCredentials... creds) { + if (creds.length == 0) { + throw new IllegalArgumentException("At least one credential is required"); + } + return new ChoiceChannelCredentials(creds); + } + + private final List creds; + + private ChoiceChannelCredentials(ChannelCredentials... creds) { + for (ChannelCredentials 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/CompositeCallCredentials.java b/api/src/main/java/io/grpc/CompositeCallCredentials.java new file mode 100644 index 0000000000..1b3e5a52d5 --- /dev/null +++ b/api/src/main/java/io/grpc/CompositeCallCredentials.java @@ -0,0 +1,101 @@ +/* + * 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.base.Preconditions; +import java.util.concurrent.Executor; + +/** + * Uses multiple {@code CallCredentials} as if they were one. If the first credential fails, the + * second will not be used. Both must succeed to allow the RPC. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") +public final class CompositeCallCredentials extends CallCredentials { + private final CallCredentials credentials1; + private final CallCredentials credentials2; + + public CompositeCallCredentials(CallCredentials creds1, CallCredentials creds2) { + this.credentials1 = Preconditions.checkNotNull(creds1, "creds1"); + this.credentials2 = Preconditions.checkNotNull(creds2, "creds2"); + } + + @Override + public void applyRequestMetadata( + RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) { + credentials1.applyRequestMetadata(requestInfo, appExecutor, + new WrappingMetadataApplier(requestInfo, appExecutor, applier, Context.current())); + } + + @Override + public void thisUsesUnstableApi() {} + + private final class WrappingMetadataApplier extends MetadataApplier { + private final RequestInfo requestInfo; + private final Executor appExecutor; + private final MetadataApplier delegate; + private final Context context; + + public WrappingMetadataApplier( + RequestInfo requestInfo, Executor appExecutor, MetadataApplier delegate, Context context) { + this.requestInfo = requestInfo; + this.appExecutor = appExecutor; + this.delegate = Preconditions.checkNotNull(delegate, "delegate"); + this.context = Preconditions.checkNotNull(context, "context"); + } + + @Override + public void apply(Metadata headers) { + Preconditions.checkNotNull(headers, "headers"); + Context previous = context.attach(); + try { + credentials2.applyRequestMetadata( + requestInfo, appExecutor, new CombiningMetadataApplier(delegate, headers)); + } finally { + context.detach(previous); + } + } + + @Override + public void fail(Status status) { + delegate.fail(status); + } + } + + private static final class CombiningMetadataApplier extends MetadataApplier { + private final MetadataApplier delegate; + private final Metadata firstHeaders; + + public CombiningMetadataApplier(MetadataApplier delegate, Metadata firstHeaders) { + this.delegate = delegate; + this.firstHeaders = firstHeaders; + } + + @Override + public void apply(Metadata headers) { + Preconditions.checkNotNull(headers, "headers"); + Metadata combined = new Metadata(); + combined.merge(firstHeaders); + combined.merge(headers); + delegate.apply(combined); + } + + @Override + public void fail(Status status) { + delegate.fail(status); + } + } +} diff --git a/api/src/main/java/io/grpc/CompositeChannelCredentials.java b/api/src/main/java/io/grpc/CompositeChannelCredentials.java new file mode 100644 index 0000000000..a3819fccee --- /dev/null +++ b/api/src/main/java/io/grpc/CompositeChannelCredentials.java @@ -0,0 +1,49 @@ +/* + * 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.base.Preconditions; + +/** + * {@code ChannelCredentials} which use per-RPC {@link CallCredentials}. If the {@code + * ChannelCredentials} has multiple {@code CallCredentials} (e.g., a composite credential inside a + * composite credential), then all of the {@code CallCredentials} should be used; one {@code + * CallCredentials} does not override another. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") +public final class CompositeChannelCredentials extends ChannelCredentials { + public static ChannelCredentials create( + ChannelCredentials channelCreds, CallCredentials callCreds) { + return new CompositeChannelCredentials(channelCreds, callCreds); + } + + private final ChannelCredentials channelCredentials; + private final CallCredentials callCredentials; + + private CompositeChannelCredentials(ChannelCredentials channelCreds, CallCredentials callCreds) { + this.channelCredentials = Preconditions.checkNotNull(channelCreds, "channelCreds"); + this.callCredentials = Preconditions.checkNotNull(callCreds, "callCreds"); + } + + public ChannelCredentials getChannelCredentials() { + return channelCredentials; + } + + public CallCredentials getCallCredentials() { + return callCredentials; + } +} diff --git a/api/src/main/java/io/grpc/Grpc.java b/api/src/main/java/io/grpc/Grpc.java index 53ff28be35..e5fb4a112d 100644 --- a/api/src/main/java/io/grpc/Grpc.java +++ b/api/src/main/java/io/grpc/Grpc.java @@ -20,6 +20,8 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; import javax.net.ssl.SSLSession; /** @@ -62,4 +64,64 @@ public final class Grpc { @Retention(RetentionPolicy.SOURCE) @Documented public @interface TransportAttr {} + + /** + * Creates a channel builder with a target string and credentials. The target can be either a + * valid {@link NameResolver}-compliant URI, or an authority string. + * + *

A {@code NameResolver}-compliant URI is an absolute hierarchical URI as defined by {@link + * java.net.URI}. Example URIs: + *

+ * + *

An authority string will be converted to a {@code NameResolver}-compliant URI, which has + * the scheme from the name resolver with the highest priority (e.g. {@code "dns"}), + * no authority, and the original authority string as its path after properly escaped. + * We recommend libraries to specify the schema explicitly if it is known, since libraries cannot + * know which NameResolver will be default during runtime. + * Example authority strings: + *

+ */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") + public static ManagedChannelBuilder newChannelBuilder( + String target, ChannelCredentials creds) { + return ManagedChannelRegistry.getDefaultRegistry().newChannelBuilder(target, creds); + } + + /** + * Creates a channel builder from a host, port, and credentials. The host and port are combined to + * form an authority string and then passed to {@link #newChannelBuilder(String, + * ChannelCredentials)}. IPv6 addresses are properly surrounded by square brackets ("[]"). + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") + public static ManagedChannelBuilder newChannelBuilderForAddress( + String host, int port, ChannelCredentials creds) { + return newChannelBuilder(authorityFromHostAndPort(host, port), creds); + } + + /** + * Combine a host and port into an authority string. + */ + // A copy of GrpcUtil.authorityFromHostAndPort + private static String authorityFromHostAndPort(String host, int port) { + try { + return new URI(null, null, host, port, null, null, null).getAuthority(); + } catch (URISyntaxException ex) { + throw new IllegalArgumentException("Invalid host or port: " + host + " " + port, ex); + } + } } diff --git a/api/src/main/java/io/grpc/InsecureChannelCredentials.java b/api/src/main/java/io/grpc/InsecureChannelCredentials.java new file mode 100644 index 0000000000..9f67b0b249 --- /dev/null +++ b/api/src/main/java/io/grpc/InsecureChannelCredentials.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 client identity, authentication, or encryption is to be used. */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") +public final class InsecureChannelCredentials extends ChannelCredentials { + public static ChannelCredentials create() { + return new InsecureChannelCredentials(); + } + + private InsecureChannelCredentials() {} +} diff --git a/api/src/main/java/io/grpc/ManagedChannelBuilder.java b/api/src/main/java/io/grpc/ManagedChannelBuilder.java index e0c87d3071..6513083a5a 100644 --- a/api/src/main/java/io/grpc/ManagedChannelBuilder.java +++ b/api/src/main/java/io/grpc/ManagedChannelBuilder.java @@ -182,6 +182,7 @@ public abstract class ManagedChannelBuilder> * not perform HTTP/1.1 upgrades. * * @return this + * @throws IllegalStateException if ChannelCredentials were provided when constructing the builder * @throws UnsupportedOperationException if plaintext mode is not supported. * @since 1.11.0 */ @@ -193,6 +194,7 @@ public abstract class ManagedChannelBuilder> * Makes the client use TLS. * * @return this + * @throws IllegalStateException if ChannelCredentials were provided when constructing the builder * @throws UnsupportedOperationException if transport security is not supported. * @since 1.9.0 */ diff --git a/api/src/main/java/io/grpc/ManagedChannelProvider.java b/api/src/main/java/io/grpc/ManagedChannelProvider.java index 76a8d6707b..f57340d9ba 100644 --- a/api/src/main/java/io/grpc/ManagedChannelProvider.java +++ b/api/src/main/java/io/grpc/ManagedChannelProvider.java @@ -16,11 +16,7 @@ package io.grpc; -import com.google.common.annotations.VisibleForTesting; -import io.grpc.ServiceProviders.PriorityAccessor; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; +import com.google.common.base.Preconditions; /** * Provider of managed channels for transport agnostic consumption. @@ -36,31 +32,13 @@ import java.util.List; */ @Internal public abstract class ManagedChannelProvider { - @VisibleForTesting - static final Iterable> HARDCODED_CLASSES = new HardcodedClasses(); - - private static final ManagedChannelProvider provider = ServiceProviders.load( - ManagedChannelProvider.class, - HARDCODED_CLASSES, - ManagedChannelProvider.class.getClassLoader(), - new PriorityAccessor() { - @Override - public boolean isAvailable(ManagedChannelProvider provider) { - return provider.isAvailable(); - } - - @Override - public int getPriority(ManagedChannelProvider provider) { - return provider.priority(); - } - }); - /** * Returns the ClassLoader-wide default channel. * * @throws ProviderNotFoundException if no provider is available */ public static ManagedChannelProvider provider() { + ManagedChannelProvider provider = ManagedChannelRegistry.getDefaultRegistry().provider(); if (provider == null) { throw new ProviderNotFoundException("No functional channel service provider found. " + "Try adding a dependency on the grpc-okhttp, grpc-netty, or grpc-netty-shaded " @@ -93,6 +71,40 @@ public abstract class ManagedChannelProvider { */ protected abstract ManagedChannelBuilder builderForTarget(String target); + /** + * Creates a new builder with the given target URI and credentials. Returns an error-string result + * if unable to understand the credentials. + */ + protected NewChannelBuilderResult newChannelBuilder(String target, ChannelCredentials creds) { + return NewChannelBuilderResult.error("ChannelCredentials are unsupported"); + } + + public static final class NewChannelBuilderResult { + private final ManagedChannelBuilder channelBuilder; + private final String error; + + private NewChannelBuilderResult(ManagedChannelBuilder channelBuilder, String error) { + this.channelBuilder = channelBuilder; + this.error = error; + } + + public static NewChannelBuilderResult channelBuilder(ManagedChannelBuilder builder) { + return new NewChannelBuilderResult(Preconditions.checkNotNull(builder), null); + } + + public static NewChannelBuilderResult error(String error) { + return new NewChannelBuilderResult(null, Preconditions.checkNotNull(error)); + } + + public ManagedChannelBuilder getChannelBuilder() { + return channelBuilder; + } + + public String getError() { + return error; + } + } + /** * Thrown when no suitable {@link ManagedChannelProvider} objects can be found. */ @@ -103,22 +115,4 @@ public abstract class ManagedChannelProvider { super(msg); } } - - private static final class HardcodedClasses implements Iterable> { - @Override - public Iterator> iterator() { - List> list = new ArrayList<>(); - try { - list.add(Class.forName("io.grpc.okhttp.OkHttpChannelProvider")); - } catch (ClassNotFoundException ex) { - // ignore - } - try { - list.add(Class.forName("io.grpc.netty.NettyChannelProvider")); - } catch (ClassNotFoundException ex) { - // ignore - } - return list.iterator(); - } - } } diff --git a/api/src/main/java/io/grpc/ManagedChannelRegistry.java b/api/src/main/java/io/grpc/ManagedChannelRegistry.java new file mode 100644 index 0000000000..b13d47a90d --- /dev/null +++ b/api/src/main/java/io/grpc/ManagedChannelRegistry.java @@ -0,0 +1,189 @@ +/* + * 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.Level; +import java.util.logging.Logger; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Registry of {@link ManagedChannelProvider}s. The {@link #getDefaultRegistry default instance} + * loads providers at runtime through the Java service provider mechanism. + * + * @since 1.32.0 + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") +@ThreadSafe +public final class ManagedChannelRegistry { + private static final Logger logger = Logger.getLogger(ManagedChannelRegistry.class.getName()); + private static ManagedChannelRegistry 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 ManagedChannelProvider#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(ManagedChannelProvider provider) { + addProvider(provider); + refreshProviders(); + } + + private synchronized void addProvider(ManagedChannelProvider 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(ManagedChannelProvider 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(ManagedChannelProvider o1, ManagedChannelProvider 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 ManagedChannelRegistry getDefaultRegistry() { + if (instance == null) { + List providerList = ServiceProviders.loadAll( + ManagedChannelProvider.class, + getHardCodedClasses(), + ManagedChannelProvider.class.getClassLoader(), + new ManagedChannelPriorityAccessor()); + instance = new ManagedChannelRegistry(); + for (ManagedChannelProvider 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 ManagedChannelProvider.provider() + ManagedChannelProvider provider() { + List providers = providers(); + return providers.isEmpty() ? null : providers.get(0); + } + + @VisibleForTesting + static List> getHardCodedClasses() { + // Class.forName(String) is used to remove the need for ProGuard configuration. Note that + // ProGuard does not detect usages of Class.forName(String, boolean, ClassLoader): + // https://sourceforge.net/p/proguard/bugs/418/ + List> list = new ArrayList<>(); + try { + list.add(Class.forName("io.grpc.okhttp.OkHttpChannelProvider")); + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Unable to find OkHttpChannelProvider", e); + } + try { + list.add(Class.forName("io.grpc.netty.NettyChannelProvider")); + } catch (ClassNotFoundException e) { + logger.log(Level.FINE, "Unable to find NettyChannelProvider", e); + } + return Collections.unmodifiableList(list); + } + + ManagedChannelBuilder newChannelBuilder(String target, ChannelCredentials creds) { + List providers = providers(); + if (providers.isEmpty()) { + throw new ProviderNotFoundException("No functional channel service provider found. " + + "Try adding a dependency on the grpc-okhttp, grpc-netty, or grpc-netty-shaded " + + "artifact"); + } + StringBuilder error = new StringBuilder(); + for (ManagedChannelProvider provider : providers()) { + ManagedChannelProvider.NewChannelBuilderResult result + = provider.newChannelBuilder(target, creds); + if (result.getChannelBuilder() != null) { + return result.getChannelBuilder(); + } + error.append("; "); + error.append(provider.getClass().getName()); + error.append(": "); + error.append(result.getError()); + } + throw new ProviderNotFoundException(error.substring(2)); + } + + private static final class ManagedChannelPriorityAccessor + implements ServiceProviders.PriorityAccessor { + @Override + public boolean isAvailable(ManagedChannelProvider provider) { + return provider.isAvailable(); + } + + @Override + public int getPriority(ManagedChannelProvider provider) { + return provider.priority(); + } + } + + /** Thrown when no suitable {@link ManagedChannelProvider} 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/TlsChannelCredentials.java b/api/src/main/java/io/grpc/TlsChannelCredentials.java new file mode 100644 index 0000000000..91f57391cd --- /dev/null +++ b/api/src/main/java/io/grpc/TlsChannelCredentials.java @@ -0,0 +1,122 @@ +/* + * 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.Collections; +import java.util.EnumSet; +import java.util.Set; + +/** + * TLS credentials, providing server authentication 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 verification should + * use customary default root certificates. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") +public final class TlsChannelCredentials extends ChannelCredentials { + /** Use TLS with its defaults. */ + public static ChannelCredentials create() { + return newBuilder().build(); + } + + private final boolean fakeFeature; + + TlsChannelCredentials(Builder builder) { + fakeFeature = builder.fakeFeature; + } + + /** + * 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 TlsChannelCredentials}. */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/7479") + public static final class Builder { + private boolean fakeFeature; + + private Builder() {} + + /** + * Requires {@link Feature#FAKE} to be understood. For use in testing consumers of this + * credential. + */ + public Builder requireFakeFeature() { + fakeFeature = true; + return this; + } + + /** Construct the credentials. */ + public ChannelCredentials build() { + return new TlsChannelCredentials(this); + } + } +} diff --git a/api/src/test/java/io/grpc/CompositeCallCredentialsTest.java b/api/src/test/java/io/grpc/CompositeCallCredentialsTest.java new file mode 100644 index 0000000000..4e606c4166 --- /dev/null +++ b/api/src/test/java/io/grpc/CompositeCallCredentialsTest.java @@ -0,0 +1,115 @@ +/* + * 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 java.util.concurrent.Executor; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CompositeCallCredentialsTest { + private static final Metadata.Key METADATA_KEY = + Metadata.Key.of("key", Metadata.ASCII_STRING_MARSHALLER); + + private CompositeCallCredentials callCredentials; + private CallCredentials.RequestInfo requestInfo; + private Executor appExecutor; + private FakeMetadataApplier applier = new FakeMetadataApplier(); + + @Test + public void applyRequestMetadata_firstFails() { + Status failure = Status.UNAVAILABLE.withDescription("expected"); + callCredentials = new CompositeCallCredentials( + new FakeCallCredentials(failure), + new FakeCallCredentials(createMetadata(METADATA_KEY, "value2"))); + callCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + assertThat(applier.status).isSameInstanceAs(failure); + } + + @Test + public void applyRequestMetadata_secondFails() { + Status failure = Status.UNAVAILABLE.withDescription("expected"); + callCredentials = new CompositeCallCredentials( + new FakeCallCredentials(createMetadata(METADATA_KEY, "value1")), + new FakeCallCredentials(failure)); + callCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + assertThat(applier.status).isSameInstanceAs(failure); + } + + @Test + public void applyRequestMetadata_bothSucceed() { + callCredentials = new CompositeCallCredentials( + new FakeCallCredentials(createMetadata(METADATA_KEY, "value1")), + new FakeCallCredentials(createMetadata(METADATA_KEY, "value2"))); + callCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + assertThat(applier.headers).isNotNull(); + assertThat(applier.headers.getAll(METADATA_KEY)).containsExactly("value1", "value2"); + } + + private static Metadata createMetadata(Metadata.Key key, String value) { + Metadata metadata = new Metadata(); + metadata.put(key, value); + return metadata; + } + + private static class FakeMetadataApplier extends CallCredentials.MetadataApplier { + private Metadata headers; + private Status status; + + @Override public void apply(Metadata headers) { + assertThat(this.headers).isNull(); + assertThat(this.status).isNull(); + this.headers = headers; + } + + @Override public void fail(Status status) { + assertThat(this.headers).isNull(); + assertThat(this.status).isNull(); + this.status = status; + } + } + + private static class FakeCallCredentials extends CallCredentials { + private final Metadata headers; + private final Status status; + + public FakeCallCredentials(Metadata headers) { + this.headers = headers; + this.status = null; + } + + public FakeCallCredentials(Status status) { + this.headers = null; + this.status = status; + } + + @Override public void applyRequestMetadata( + CallCredentials.RequestInfo requestInfo, Executor appExecutor, + CallCredentials.MetadataApplier applier) { + if (headers != null) { + applier.apply(headers); + } else { + applier.fail(status); + } + } + + @Override public void thisUsesUnstableApi() {} + } +} diff --git a/api/src/test/java/io/grpc/ManagedChannelProviderTest.java b/api/src/test/java/io/grpc/ManagedChannelProviderTest.java deleted file mode 100644 index edb4a701eb..0000000000 --- a/api/src/test/java/io/grpc/ManagedChannelProviderTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015 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.collect.ImmutableSet; -import java.util.Iterator; -import java.util.concurrent.Callable; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Unit tests for {@link ManagedChannelProvider}. */ -@RunWith(JUnit4.class) -public class ManagedChannelProviderTest { - - @Test - public void getCandidatesViaHardCoded_triesToLoadClasses() throws Exception { - ServiceProvidersTestUtil.testHardcodedClasses( - HardcodedClassesCallable.class.getName(), - getClass().getClassLoader(), - ImmutableSet.of( - "io.grpc.okhttp.OkHttpChannelProvider", - "io.grpc.netty.NettyChannelProvider")); - } - - public static final class HardcodedClassesCallable implements Callable>> { - @Override - public Iterator> call() { - return ManagedChannelProvider.HARDCODED_CLASSES.iterator(); - } - } -} diff --git a/api/src/main/java/io/grpc/InternalManagedChannelProvider.java b/api/src/test/java/io/grpc/ManagedChannelRegistryAccessor.java similarity index 64% rename from api/src/main/java/io/grpc/InternalManagedChannelProvider.java rename to api/src/test/java/io/grpc/ManagedChannelRegistryAccessor.java index 37fd52a7a5..5c65c8e532 100644 --- a/api/src/main/java/io/grpc/InternalManagedChannelProvider.java +++ b/api/src/test/java/io/grpc/ManagedChannelRegistryAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 The gRPC Authors + * 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. @@ -16,7 +16,11 @@ package io.grpc; -public final class InternalManagedChannelProvider { - public static final Iterable> HARDCODED_CLASSES = - ManagedChannelProvider.HARDCODED_CLASSES; +/** Accesses test-only methods of {@link ManagedChannelRegistry}. */ +public final class ManagedChannelRegistryAccessor { + private ManagedChannelRegistryAccessor() {} + + public static Iterable> getHardCodedClasses() { + return ManagedChannelRegistry.getHardCodedClasses(); + } } diff --git a/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java b/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java new file mode 100644 index 0000000000..68d666ff10 --- /dev/null +++ b/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java @@ -0,0 +1,183 @@ +/* + * 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 ManagedChannelRegistry}. */ +@RunWith(JUnit4.class) +public class ManagedChannelRegistryTest { + private String target = "testing123"; + private ChannelCredentials creds = new ChannelCredentials() {}; + + @Test + public void register_unavilableProviderThrows() { + ManagedChannelRegistry reg = new ManagedChannelRegistry(); + 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() { + ManagedChannelRegistry reg = new ManagedChannelRegistry(); + ManagedChannelProvider p1 = new BaseProvider(true, 5); + ManagedChannelProvider p2 = new BaseProvider(true, 5); + ManagedChannelProvider 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() { + ManagedChannelRegistry reg = new ManagedChannelRegistry(); + ManagedChannelProvider p1 = new BaseProvider(true, 5); + ManagedChannelProvider p2 = new BaseProvider(true, 3); + ManagedChannelProvider p3 = new BaseProvider(true, 8); + ManagedChannelProvider p4 = new BaseProvider(true, 3); + ManagedChannelProvider 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 ManagedChannelRegistry().provider()).isNull(); + } + + @Test + public void newChannelBuilder_providerReturnsError() { + final String errorString = "brisking"; + class ErrorProvider extends BaseProvider { + ErrorProvider() { + super(true, 5); + } + + @Override + public NewChannelBuilderResult newChannelBuilder( + String passedTarget, ChannelCredentials passedCreds) { + assertThat(passedTarget).isSameInstanceAs(target); + assertThat(passedCreds).isSameInstanceAs(creds); + return NewChannelBuilderResult.error(errorString); + } + } + + ManagedChannelRegistry registry = new ManagedChannelRegistry(); + registry.register(new ErrorProvider()); + try { + registry.newChannelBuilder(target, creds); + fail("expected exception"); + } catch (ManagedChannelRegistry.ProviderNotFoundException ex) { + assertThat(ex).hasMessageThat().contains(errorString); + assertThat(ex).hasMessageThat().contains(ErrorProvider.class.getName()); + } + } + + @Test + public void newChannelBuilder_providerReturnsNonNull() { + ManagedChannelRegistry registry = new ManagedChannelRegistry(); + registry.register(new BaseProvider(true, 5) { + @Override + public NewChannelBuilderResult newChannelBuilder( + String passedTarget, ChannelCredentials passedCreds) { + return NewChannelBuilderResult.error("dodging"); + } + }); + class MockChannelBuilder extends ForwardingChannelBuilder { + @Override public ManagedChannelBuilder delegate() { + throw new UnsupportedOperationException(); + } + } + + final ManagedChannelBuilder mcb = new MockChannelBuilder(); + registry.register(new BaseProvider(true, 4) { + @Override + public NewChannelBuilderResult newChannelBuilder( + String passedTarget, ChannelCredentials passedCreds) { + return NewChannelBuilderResult.channelBuilder(mcb); + } + }); + registry.register(new BaseProvider(true, 3) { + @Override + public NewChannelBuilderResult newChannelBuilder( + String passedTarget, ChannelCredentials passedCreds) { + fail("Should not be called"); + throw new AssertionError(); + } + }); + assertThat(registry.newChannelBuilder(target, creds)).isSameInstanceAs(mcb); + } + + @Test + public void newChannelBuilder_noProvider() { + ManagedChannelRegistry registry = new ManagedChannelRegistry(); + try { + registry.newChannelBuilder(target, creds); + fail("expected exception"); + } catch (ManagedChannelRegistry.ProviderNotFoundException ex) { + assertThat(ex).hasMessageThat().contains("No functional channel service provider found"); + assertThat(ex).hasMessageThat().contains("grpc-netty"); + } + } + + private static class BaseProvider extends ManagedChannelProvider { + 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 ManagedChannelBuilder builderForAddress(String name, int port) { + throw new UnsupportedOperationException(); + } + + @Override + protected ManagedChannelBuilder builderForTarget(String target) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/core/src/main/java/io/grpc/internal/GrpcUtil.java b/core/src/main/java/io/grpc/internal/GrpcUtil.java index d3cdf8db2f..bc2fb51794 100644 --- a/core/src/main/java/io/grpc/internal/GrpcUtil.java +++ b/core/src/main/java/io/grpc/internal/GrpcUtil.java @@ -508,6 +508,7 @@ public final class GrpcUtil { /** * Combine a host and port into an authority string. */ + // There is a copy of this method in io.grpc.Grpc public static String authorityFromHostAndPort(String host, int port) { try { return new URI(null, null, host, port, null, null, null).getAuthority(); diff --git a/netty/build.gradle b/netty/build.gradle index 629dc823b3..20b35eb36d 100644 --- a/netty/build.gradle +++ b/netty/build.gradle @@ -24,6 +24,7 @@ dependencies { // Tests depend on base class defined by core module. testImplementation project(':grpc-core').sourceSets.test.output, + project(':grpc-api').sourceSets.test.output, project(':grpc-testing'), project(':grpc-testing-proto') testRuntimeOnly libraries.netty_tcnative, diff --git a/netty/src/test/java/io/grpc/netty/NettyChannelProviderTest.java b/netty/src/test/java/io/grpc/netty/NettyChannelProviderTest.java index 26dd6796a3..40bfeb2802 100644 --- a/netty/src/test/java/io/grpc/netty/NettyChannelProviderTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyChannelProviderTest.java @@ -21,9 +21,9 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import io.grpc.InternalManagedChannelProvider; import io.grpc.InternalServiceProviders; import io.grpc.ManagedChannelProvider; +import io.grpc.ManagedChannelRegistryAccessor; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -47,9 +47,8 @@ public class NettyChannelProviderTest { @Test public void providedHardCoded() { - for (ManagedChannelProvider current : InternalServiceProviders.getCandidatesViaHardCoded( - ManagedChannelProvider.class, InternalManagedChannelProvider.HARDCODED_CLASSES)) { - if (current instanceof NettyChannelProvider) { + for (Class current : ManagedChannelRegistryAccessor.getHardCodedClasses()) { + if (current == NettyChannelProvider.class) { return; } } diff --git a/okhttp/build.gradle b/okhttp/build.gradle index 0c23c3b9f8..f7d8c62284 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -21,6 +21,7 @@ dependencies { perfmarkDependency 'implementation' // Tests depend on base class defined by core module. testImplementation project(':grpc-core').sourceSets.test.output, + project(':grpc-api').sourceSets.test.output, project(':grpc-testing'), project(':grpc-netty') signature "org.codehaus.mojo.signature:java17:1.0@signature" diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelProviderTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelProviderTest.java index 53007762dd..3069eb1bda 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelProviderTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpChannelProviderTest.java @@ -20,9 +20,9 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import io.grpc.InternalManagedChannelProvider; import io.grpc.InternalServiceProviders; import io.grpc.ManagedChannelProvider; +import io.grpc.ManagedChannelRegistryAccessor; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -46,9 +46,8 @@ public class OkHttpChannelProviderTest { @Test public void providedHardCoded() { - for (ManagedChannelProvider current : InternalServiceProviders.getCandidatesViaHardCoded( - ManagedChannelProvider.class, InternalManagedChannelProvider.HARDCODED_CLASSES)) { - if (current instanceof OkHttpChannelProvider) { + for (Class current : ManagedChannelRegistryAccessor.getHardCodedClasses()) { + if (current == OkHttpChannelProvider.class) { return; } }