diff --git a/alts/BUILD.bazel b/alts/BUILD.bazel index 065063c4ff5..4e3d266b4d5 100644 --- a/alts/BUILD.bazel +++ b/alts/BUILD.bazel @@ -19,6 +19,7 @@ java_library( "@io_netty_netty_buffer//jar", "@io_netty_netty_codec//jar", "@io_netty_netty_common//jar", + "@io_netty_netty_handler//jar", "@io_netty_netty_transport//jar", ], ) @@ -35,10 +36,13 @@ java_library( ":handshaker_java_grpc", "//core", "//core:internal", + "//auth", "//netty", + "@com_google_auth_google_auth_library_oauth2_http//jar", "@com_google_code_findbugs_jsr305//jar", "@com_google_guava_guava//jar", "@io_netty_netty_common//jar", + "@io_netty_netty_handler//jar", "@io_netty_netty_transport//jar", "@org_apache_commons_commons_lang3//jar", ], diff --git a/alts/build.gradle b/alts/build.gradle index 55ba1c4cf2b..5a8c9e37665 100644 --- a/alts/build.gradle +++ b/alts/build.gradle @@ -19,18 +19,27 @@ buildscript { } dependencies { - compile project(':grpc-core'), + compile project(':grpc-auth'), + project(':grpc-core'), project(':grpc-netty'), project(':grpc-protobuf'), project(':grpc-stub'), libraries.lang, libraries.protobuf + compile (libraries.google_auth_oauth2_http) { + // prefer 3.0.0 from libraries instead of 1.3.9 + exclude group: 'com.google.code.findbugs', module: 'jsr305' + // prefer 20.0 from libraries instead of 19.0 + exclude group: 'com.google.guava', module: 'guava' + } runtime project(':grpc-grpclb') testCompile libraries.guava, libraries.guava_testlib, libraries.junit, libraries.mockito, libraries.truth + testRuntime libraries.netty_tcnative, + libraries.conscrypt signature 'org.codehaus.mojo.signature:java17:1.0@signature' } diff --git a/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelBuilder.java b/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelBuilder.java new file mode 100644 index 00000000000..3f5747d7b83 --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/GoogleDefaultChannelBuilder.java @@ -0,0 +1,218 @@ +/* + * 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 static com.google.common.base.Preconditions.checkArgument; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.annotations.VisibleForTesting; +import io.grpc.CallCredentials; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingChannelBuilder; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import io.grpc.alts.internal.AltsClientOptions; +import io.grpc.alts.internal.AltsTsiHandshaker; +import io.grpc.alts.internal.GoogleDefaultProtocolNegotiator; +import io.grpc.alts.internal.HandshakerServiceGrpc; +import io.grpc.alts.internal.RpcProtocolVersionsUtil; +import io.grpc.alts.internal.TsiHandshaker; +import io.grpc.alts.internal.TsiHandshakerFactory; +import io.grpc.auth.MoreCallCredentials; +import io.grpc.internal.GrpcUtil; +import io.grpc.internal.ProxyParameters; +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.InternalNettyChannelBuilder; +import io.grpc.netty.InternalNettyChannelBuilder.TransportCreationParamsFilter; +import io.grpc.netty.InternalNettyChannelBuilder.TransportCreationParamsFilterFactory; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.handler.ssl.SslContext; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import javax.annotation.Nullable; +import javax.net.ssl.SSLException; + +/** + * Google default version of {@code ManagedChannelBuilder}. This class sets up a secure channel + * using ALTS if applicable and using TLS as fallback. + */ +public final class GoogleDefaultChannelBuilder + extends ForwardingChannelBuilder { + + private final NettyChannelBuilder delegate; + private final TcpfFactory tcpfFactory = new TcpfFactory(); + + private GoogleDefaultChannelBuilder(String target) { + delegate = NettyChannelBuilder.forTarget(target); + InternalNettyChannelBuilder.setDynamicTransportParamsFactory(delegate(), tcpfFactory); + } + + /** "Overrides" the static method in {@link ManagedChannelBuilder}. */ + public static final GoogleDefaultChannelBuilder forTarget(String target) { + return new GoogleDefaultChannelBuilder(target); + } + + /** "Overrides" the static method in {@link ManagedChannelBuilder}. */ + public static GoogleDefaultChannelBuilder forAddress(String name, int port) { + return forTarget(GrpcUtil.authorityFromHostAndPort(name, port)); + } + + @Override + protected NettyChannelBuilder delegate() { + return delegate; + } + + @Override + public ManagedChannel build() { + @Nullable CallCredentials credentials = null; + Status status = Status.OK; + try { + credentials = MoreCallCredentials.from(GoogleCredentials.getApplicationDefault()); + } catch (IOException e) { + status = + Status.FAILED_PRECONDITION + .withDescription("Failed to get Google default credentials") + .withCause(e); + } + return delegate().intercept(new GoogleDefaultInterceptor(credentials, status)).build(); + } + + @VisibleForTesting + TransportCreationParamsFilterFactory getTcpfFactoryForTest() { + return tcpfFactory; + } + + private static final class TcpfFactory implements TransportCreationParamsFilterFactory { + + private final SslContext sslContext; + private final AltsClientOptions handshakerOptions = + new AltsClientOptions.Builder() + .setRpcProtocolVersions(RpcProtocolVersionsUtil.getRpcProtocolVersions()) + .build(); + + private final TsiHandshakerFactory altsHandshakerFactory = + new TsiHandshakerFactory() { + @Override + public TsiHandshaker newHandshaker() { + // Used the shared grpc channel to connecting to the ALTS handshaker service. + ManagedChannel channel = HandshakerServiceChannel.get(); + return AltsTsiHandshaker.newClient( + HandshakerServiceGrpc.newStub(channel), handshakerOptions); + } + }; + + private TcpfFactory() { + try { + sslContext = GrpcSslContexts.forClient().build(); + } catch (SSLException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public TransportCreationParamsFilter create( + final SocketAddress serverAddress, + final String authority, + final String userAgent, + final ProxyParameters proxy) { + checkArgument( + serverAddress instanceof InetSocketAddress, + "%s must be a InetSocketAddress", + serverAddress); + final GoogleDefaultProtocolNegotiator negotiator = + new GoogleDefaultProtocolNegotiator(altsHandshakerFactory, sslContext, authority); + return new TransportCreationParamsFilter() { + @Override + public SocketAddress getTargetServerAddress() { + return serverAddress; + } + + @Override + public String getAuthority() { + return authority; + } + + @Override + public String getUserAgent() { + return userAgent; + } + + @Override + public GoogleDefaultProtocolNegotiator getProtocolNegotiator() { + return negotiator; + } + }; + } + } + + /** + * An implementation of {@link ClientInterceptor} that adds Google call credentials on each call. + */ + static final class GoogleDefaultInterceptor implements ClientInterceptor { + + @Nullable private final CallCredentials credentials; + private final Status status; + + public GoogleDefaultInterceptor(@Nullable CallCredentials credentials, Status status) { + this.credentials = credentials; + this.status = status; + } + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + if (!status.isOk()) { + return new FailingClientCall<>(status); + } + return next.newCall(method, callOptions.withCallCredentials(credentials)); + } + } + + /** An implementation of {@link ClientCall} that fails when started. */ + static final class FailingClientCall extends ClientCall { + + private final Status error; + + public FailingClientCall(Status error) { + this.error = error; + } + + @Override + public void start(ClientCall.Listener listener, Metadata headers) { + listener.onClose(error, new Metadata()); + } + + @Override + public void request(int numMessages) {} + + @Override + public void cancel(String message, Throwable cause) {} + + @Override + public void halfClose() {} + + @Override + public void sendMessage(ReqT message) {} + } +} diff --git a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java index e6332861b86..7c640e19eef 100644 --- a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java +++ b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java @@ -28,7 +28,7 @@ * the handshaker service is local and is over plaintext. Each application will have at most one * connection to the handshaker service. * - *

TODO: Release the channel if it is not used. + *

TODO: Release the channel if it is not used. https://github.com/grpc/grpc-java/issues/4755. */ final class HandshakerServiceChannel { // Default handshaker service address. diff --git a/alts/src/main/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiator.java b/alts/src/main/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiator.java new file mode 100644 index 00000000000..aae9f34560a --- /dev/null +++ b/alts/src/main/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiator.java @@ -0,0 +1,53 @@ +/* + * 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.internal; + +import com.google.common.annotations.VisibleForTesting; +import io.grpc.internal.GrpcAttributes; +import io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.ProtocolNegotiator; +import io.grpc.netty.ProtocolNegotiators; +import io.netty.handler.ssl.SslContext; + +/** A client-side GPRC {@link ProtocolNegotiator} for Google Default Channel. */ +public final class GoogleDefaultProtocolNegotiator implements ProtocolNegotiator { + private final ProtocolNegotiator altsProtocolNegotiator; + private final ProtocolNegotiator tlsProtocolNegotiator; + + public GoogleDefaultProtocolNegotiator( + TsiHandshakerFactory altsFactory, SslContext sslContext, String authority) { + altsProtocolNegotiator = AltsProtocolNegotiator.create(altsFactory); + tlsProtocolNegotiator = ProtocolNegotiators.tls(sslContext, authority); + } + + @VisibleForTesting + GoogleDefaultProtocolNegotiator( + ProtocolNegotiator altsProtocolNegotiator, ProtocolNegotiator tlsProtocolNegotiator) { + this.altsProtocolNegotiator = altsProtocolNegotiator; + this.tlsProtocolNegotiator = tlsProtocolNegotiator; + } + + @Override + public Handler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + if (grpcHandler.getEagAttributes().get(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY) != null + || grpcHandler.getEagAttributes().get(GrpcAttributes.ATTR_LB_PROVIDED_BACKEND) != null) { + return altsProtocolNegotiator.newHandler(grpcHandler); + } else { + return tlsProtocolNegotiator.newHandler(grpcHandler); + } + } +} diff --git a/alts/src/test/java/io/grpc/alts/GoogleDefaultChannelBuilderTest.java b/alts/src/test/java/io/grpc/alts/GoogleDefaultChannelBuilderTest.java new file mode 100644 index 00000000000..b681c733b42 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/GoogleDefaultChannelBuilderTest.java @@ -0,0 +1,44 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import io.grpc.alts.internal.GoogleDefaultProtocolNegotiator; +import io.grpc.netty.InternalNettyChannelBuilder.TransportCreationParamsFilterFactory; +import io.grpc.netty.ProtocolNegotiator; +import java.net.InetSocketAddress; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class GoogleDefaultChannelBuilderTest { + + @Test + public void buildsNettyChannel() throws Exception { + GoogleDefaultChannelBuilder builder = GoogleDefaultChannelBuilder.forTarget("localhost:8080"); + + TransportCreationParamsFilterFactory tcpfFactory = builder.getTcpfFactoryForTest(); + assertThat(tcpfFactory).isNotNull(); + ProtocolNegotiator protocolNegotiator = + tcpfFactory + .create(new InetSocketAddress(8080), "fakeAuthority", "fakeUserAgent", null) + .getProtocolNegotiator(); + assertThat(protocolNegotiator).isInstanceOf(GoogleDefaultProtocolNegotiator.class); + } +} diff --git a/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java b/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java new file mode 100644 index 00000000000..74e342ece47 --- /dev/null +++ b/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java @@ -0,0 +1,69 @@ +/* + * 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.internal; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.grpc.Attributes; +import io.grpc.internal.GrpcAttributes; +import io.grpc.netty.GrpcHttp2ConnectionHandler; +import io.grpc.netty.ProtocolNegotiator; +import io.grpc.netty.ProtocolNegotiator.Handler; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class GoogleDefaultProtocolNegotiatorTest { + private ProtocolNegotiator altsProtocolNegotiator; + private ProtocolNegotiator tlsProtocolNegotiator; + private GoogleDefaultProtocolNegotiator googleProtocolNegotiator; + + @Before + public void setUp() { + altsProtocolNegotiator = mock(ProtocolNegotiator.class); + tlsProtocolNegotiator = mock(ProtocolNegotiator.class); + googleProtocolNegotiator = + new GoogleDefaultProtocolNegotiator(altsProtocolNegotiator, tlsProtocolNegotiator); + } + + @Test + public void altsHandler() { + Attributes eagAttributes = + Attributes.newBuilder().set(GrpcAttributes.ATTR_LB_PROVIDED_BACKEND, true).build(); + GrpcHttp2ConnectionHandler mockHandler = mock(GrpcHttp2ConnectionHandler.class); + when(mockHandler.getEagAttributes()).thenReturn(eagAttributes); + Handler handler = googleProtocolNegotiator.newHandler(mockHandler); + verify(altsProtocolNegotiator, times(1)).newHandler(mockHandler); + verify(tlsProtocolNegotiator, never()).newHandler(mockHandler); + } + + @Test + public void tlsHandler() { + Attributes eagAttributes = Attributes.EMPTY; + GrpcHttp2ConnectionHandler mockHandler = mock(GrpcHttp2ConnectionHandler.class); + when(mockHandler.getEagAttributes()).thenReturn(eagAttributes); + Handler handler = googleProtocolNegotiator.newHandler(mockHandler); + verify(altsProtocolNegotiator, never()).newHandler(mockHandler); + verify(tlsProtocolNegotiator, times(1)).newHandler(mockHandler); + } +} diff --git a/build.gradle b/build.gradle index 5afaf7219b2..705a76846ec 100644 --- a/build.gradle +++ b/build.gradle @@ -204,6 +204,7 @@ subprojects { oauth_client: 'com.google.auth:google-auth-library-oauth2-http:0.9.0', google_api_protos: 'com.google.api.grpc:proto-google-common-protos:1.0.0', google_auth_credentials: 'com.google.auth:google-auth-library-credentials:0.9.0', + google_auth_oauth2_http: 'com.google.auth:google-auth-library-oauth2-http:0.9.0', okhttp: 'com.squareup.okhttp:okhttp:2.5.0', okio: 'com.squareup.okio:okio:1.13.0', opencensus_api: "io.opencensus:opencensus-api:${opencensusVersion}", diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java index 02170c12210..21196217276 100644 --- a/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java +++ b/interop-testing/src/main/java/io/grpc/testing/integration/TestServiceClient.java @@ -20,6 +20,7 @@ import com.google.common.io.Files; import io.grpc.ManagedChannel; import io.grpc.alts.AltsChannelBuilder; +import io.grpc.alts.GoogleDefaultChannelBuilder; import io.grpc.internal.AbstractManagedChannelImplBuilder; import io.grpc.internal.GrpcUtil; import io.grpc.internal.testing.TestUtils; @@ -78,6 +79,7 @@ public void run() { private String testCase = "empty_unary"; private boolean useTls = true; private boolean useAlts = false; + private String customCredentialsType; private boolean useTestCa; private boolean useOkHttp; private String defaultServiceAccount; @@ -120,6 +122,8 @@ void parseArgs(String[] args) { useTls = Boolean.parseBoolean(value); } else if ("use_alts".equals(key)) { useAlts = Boolean.parseBoolean(value); + } else if ("custom_credentials_type".equals(key)) { + customCredentialsType = value; } else if ("use_test_ca".equals(key)) { useTestCa = Boolean.parseBoolean(value); } else if ("use_okhttp".equals(key)) { @@ -162,6 +166,8 @@ void parseArgs(String[] args) { + "\n --use_tls=true|false Whether to use TLS. Default " + c.useTls + "\n --use_alts=true|false Whether to use ALTS. Enable ALTS will disable TLS." + "\n Default " + c.useAlts + + "\n --custom_credentials_type Custom credentials type to use. Default " + + c.customCredentialsType + "\n --use_test_ca=true|false Whether to trust our fake CA. Requires --use_tls=true " + "\n to have effect. Default " + c.useTestCa + "\n --use_okhttp=true|false Whether to use OkHttp instead of Netty. Default " @@ -338,6 +344,10 @@ private void runTest(TestCases testCase) throws Exception { private class Tester extends AbstractInteropTest { @Override protected ManagedChannel createChannel() { + if (customCredentialsType != null + && customCredentialsType.equals("google_default_credentials")) { + return GoogleDefaultChannelBuilder.forAddress(serverHost, serverPort).build(); + } if (useAlts) { return AltsChannelBuilder.forAddress(serverHost, serverPort).build(); } diff --git a/repositories.bzl b/repositories.bzl index f45046812b2..c3bbdfd524e 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -3,6 +3,7 @@ def grpc_java_repositories( omit_com_google_api_grpc_google_common_protos = False, omit_com_google_auth_google_auth_library_credentials = False, + omit_com_google_auth_google_auth_library_oauth2_http = False, omit_com_google_code_findbugs_jsr305 = False, omit_com_google_code_gson = False, omit_com_google_errorprone_error_prone_annotations = False, @@ -37,6 +38,8 @@ def grpc_java_repositories( com_google_api_grpc_google_common_protos() if not omit_com_google_auth_google_auth_library_credentials: com_google_auth_google_auth_library_credentials() + if not omit_com_google_auth_google_auth_library_oauth2_http: + com_google_auth_google_auth_library_oauth2_http() if not omit_com_google_code_findbugs_jsr305: com_google_code_findbugs_jsr305() if not omit_com_google_code_gson: @@ -119,6 +122,13 @@ def com_google_auth_google_auth_library_credentials(): sha1 = "8e2b181feff6005c9cbc6f5c1c1e2d3ec9138d46", ) +def com_google_auth_google_auth_library_oauth2_http(): + native.maven_jar( + name = "com_google_auth_google_auth_library_oauth2_http", + artifact = "com.google.auth:google-auth-library-oauth2-http:0.9.0", + sha1 = "04e6152c3aead24148627e84f5651e79698c00d9", + ) + def com_google_code_findbugs_jsr305(): native.maven_jar( name = "com_google_code_findbugs_jsr305",