szetszwo commented on code in PR #1561: URL: https://github.com/apache/ratis/pull/1561#discussion_r3876619778
########## ratis-grpc/src/main/java/org/apache/ratis/grpc/TlsHandshakeFailureEvent.java: ########## @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ratis.grpc; + +import java.net.SocketAddress; +import java.util.Objects; + +/** Information about a failed TLS handshake. */ +public final class TlsHandshakeFailureEvent { Review Comment: Let's move it as an inner class TlsHandshakeFailureServerCredentials.Event. ########## ratis-grpc/src/main/java/org/apache/ratis/grpc/TlsHandshakeFailureListener.java: ########## @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ratis.grpc; + +/** Receives failures detected while establishing TLS connections. */ +@FunctionalInterface +public interface TlsHandshakeFailureListener { + /** + * Invoked on a transport event-loop thread. Implementations must not block and should hand off + * expensive work to another thread. + */ + void onFailure(TlsHandshakeFailureEvent event); +} Review Comment: Let's use `Consumer<TlsHandshakeFailureEvent>` instead of adding a new interface. ########## ratis-grpc/src/main/java/org/apache/ratis/grpc/TlsHandshakeFailureServerCredentials.java: ########## @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ratis.grpc; + +import org.apache.ratis.thirdparty.io.grpc.Attributes; +import org.apache.ratis.thirdparty.io.grpc.Grpc; +import org.apache.ratis.thirdparty.io.grpc.InternalChannelz; +import org.apache.ratis.thirdparty.io.grpc.SecurityLevel; +import org.apache.ratis.thirdparty.io.grpc.ServerCredentials; +import org.apache.ratis.thirdparty.io.grpc.Status; +import org.apache.ratis.thirdparty.io.grpc.internal.GrpcAttributes; +import org.apache.ratis.thirdparty.io.grpc.internal.ObjectPool; +import org.apache.ratis.thirdparty.io.grpc.netty.GrpcHttp2ConnectionHandler; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalNettyServerCredentials; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiationEvent; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiator; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiators; +import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandler; +import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandlerContext; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslContext; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslHandler; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslHandshakeCompletionEvent; +import org.apache.ratis.thirdparty.io.netty.util.AsciiString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** Builds gRPC server credentials which report initial TLS handshake failures. */ +public final class TlsHandshakeFailureServerCredentials { + private static final Logger LOG = + LoggerFactory.getLogger(TlsHandshakeFailureServerCredentials.class); + private static final AsciiString HTTPS = AsciiString.of("https"); + + private TlsHandshakeFailureServerCredentials() {} + + /** + * Creates server credentials from the given TLS configuration and failure listener. + * + * @param tlsConfig the server TLS configuration + * @param listener the listener for TLS handshake failures + * @return server credentials reporting TLS handshake failures + */ + public static ServerCredentials create( + GrpcTlsConfig tlsConfig, TlsHandshakeFailureListener listener) { + Objects.requireNonNull(tlsConfig, "tlsConfig"); + Objects.requireNonNull(listener, "listener"); + final SslContext sslContext = GrpcUtil.buildSslContextForServer(tlsConfig); + return InternalNettyServerCredentials.create(new Factory(sslContext, listener)); + } + + private static boolean containsSslException(Throwable throwable) { + for (Throwable cause = throwable; cause != null; cause = cause.getCause()) { + if (cause instanceof SSLException) { + return true; + } + } + return false; + } + + private static final class Factory implements InternalProtocolNegotiator.ServerFactory { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + + private Factory(SslContext sslContext, TlsHandshakeFailureListener listener) { + this.sslContext = sslContext; + this.listener = listener; + } + + @Override + public InternalProtocolNegotiator.ProtocolNegotiator newNegotiator( + ObjectPool<? extends Executor> offloadExecutorPool) { + return new Negotiator(sslContext, listener, offloadExecutorPool); + } + } + + private static final class Negotiator implements InternalProtocolNegotiator.ProtocolNegotiator { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + private final ObjectPool<? extends Executor> offloadExecutorPool; + private final Executor executor; + + private Negotiator(SslContext sslContext, TlsHandshakeFailureListener listener, + ObjectPool<? extends Executor> offloadExecutorPool) { + this.sslContext = sslContext; + this.listener = listener; + this.offloadExecutorPool = offloadExecutorPool; + this.executor = offloadExecutorPool != null ? offloadExecutorPool.getObject() : null; + } + + @Override + public AsciiString scheme() { + return HTTPS; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + final ChannelHandler grpcNegotiationHandler = + InternalProtocolNegotiators.grpcNegotiationHandler(grpcHandler); + final ChannelHandler tlsHandler = new ServerTlsHandler(grpcNegotiationHandler, grpcHandler, + sslContext, listener, offloadExecutorPool); + return InternalProtocolNegotiators.waitUntilActiveHandler( + tlsHandler, grpcHandler.getNegotiationLogger()); + } + + @Override + public void close() { + if (offloadExecutorPool != null && executor != null) { + offloadExecutorPool.returnObject(executor); + } + } + } + + private static final class ServerTlsHandler + extends InternalProtocolNegotiators.ProtocolNegotiationHandler { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + private final Executor executor; + private boolean failureReported; + + private ServerTlsHandler(ChannelHandler next, GrpcHttp2ConnectionHandler grpcHandler, + SslContext sslContext, TlsHandshakeFailureListener listener, + ObjectPool<? extends Executor> offloadExecutorPool) { + super(next, grpcHandler.getNegotiationLogger()); + this.sslContext = sslContext; + this.listener = listener; + this.executor = offloadExecutorPool != null ? offloadExecutorPool.getObject() : null; + } + + @Override + protected void handlerAdded0(ChannelHandlerContext context) { + final SSLEngine sslEngine = sslContext.newEngine(context.alloc()); + final SslHandler sslHandler = executor != null + ? new SslHandler(sslEngine, false, executor) + : new SslHandler(sslEngine, false); + context.pipeline().addBefore(context.name(), null, sslHandler); + } + + @Override + protected void userEventTriggered0(ChannelHandlerContext context, Object event) + throws Exception { + if (!(event instanceof SslHandshakeCompletionEvent)) { + super.userEventTriggered0(context, event); + return; + } + + final SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) event; + if (!handshakeEvent.isSuccess()) { + final Throwable cause = handshakeEvent.cause(); + if (containsSslException(cause)) { + notifyListener(context, cause); + } + context.fireExceptionCaught(cause); + return; + } + + final SslHandler sslHandler = context.pipeline().get(SslHandler.class); + if (!sslContext.applicationProtocolNegotiator().protocols() + .contains(sslHandler.applicationProtocol())) { + final RuntimeException cause = Status.UNAVAILABLE + .withDescription("Failed protocol negotiation: Unable to find compatible protocol") + .asRuntimeException(); Review Comment: How about using `asException()`? ########## ratis-grpc/src/main/java/org/apache/ratis/grpc/TlsHandshakeFailureServerCredentials.java: ########## @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ratis.grpc; + +import org.apache.ratis.thirdparty.io.grpc.Attributes; +import org.apache.ratis.thirdparty.io.grpc.Grpc; +import org.apache.ratis.thirdparty.io.grpc.InternalChannelz; +import org.apache.ratis.thirdparty.io.grpc.SecurityLevel; +import org.apache.ratis.thirdparty.io.grpc.ServerCredentials; +import org.apache.ratis.thirdparty.io.grpc.Status; +import org.apache.ratis.thirdparty.io.grpc.internal.GrpcAttributes; +import org.apache.ratis.thirdparty.io.grpc.internal.ObjectPool; +import org.apache.ratis.thirdparty.io.grpc.netty.GrpcHttp2ConnectionHandler; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalNettyServerCredentials; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiationEvent; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiator; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiators; +import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandler; +import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandlerContext; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslContext; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslHandler; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslHandshakeCompletionEvent; +import org.apache.ratis.thirdparty.io.netty.util.AsciiString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** Builds gRPC server credentials which report initial TLS handshake failures. */ +public final class TlsHandshakeFailureServerCredentials { + private static final Logger LOG = + LoggerFactory.getLogger(TlsHandshakeFailureServerCredentials.class); + private static final AsciiString HTTPS = AsciiString.of("https"); + + private TlsHandshakeFailureServerCredentials() {} + + /** + * Creates server credentials from the given TLS configuration and failure listener. + * + * @param tlsConfig the server TLS configuration + * @param listener the listener for TLS handshake failures + * @return server credentials reporting TLS handshake failures + */ + public static ServerCredentials create( + GrpcTlsConfig tlsConfig, TlsHandshakeFailureListener listener) { + Objects.requireNonNull(tlsConfig, "tlsConfig"); + Objects.requireNonNull(listener, "listener"); + final SslContext sslContext = GrpcUtil.buildSslContextForServer(tlsConfig); + return InternalNettyServerCredentials.create(new Factory(sslContext, listener)); + } + + private static boolean containsSslException(Throwable throwable) { + for (Throwable cause = throwable; cause != null; cause = cause.getCause()) { + if (cause instanceof SSLException) { + return true; + } + } + return false; + } + + private static final class Factory implements InternalProtocolNegotiator.ServerFactory { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + + private Factory(SslContext sslContext, TlsHandshakeFailureListener listener) { + this.sslContext = sslContext; + this.listener = listener; + } + + @Override + public InternalProtocolNegotiator.ProtocolNegotiator newNegotiator( + ObjectPool<? extends Executor> offloadExecutorPool) { + return new Negotiator(sslContext, listener, offloadExecutorPool); + } + } + + private static final class Negotiator implements InternalProtocolNegotiator.ProtocolNegotiator { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + private final ObjectPool<? extends Executor> offloadExecutorPool; + private final Executor executor; + + private Negotiator(SslContext sslContext, TlsHandshakeFailureListener listener, + ObjectPool<? extends Executor> offloadExecutorPool) { + this.sslContext = sslContext; + this.listener = listener; + this.offloadExecutorPool = offloadExecutorPool; + this.executor = offloadExecutorPool != null ? offloadExecutorPool.getObject() : null; + } + + @Override + public AsciiString scheme() { + return HTTPS; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + final ChannelHandler grpcNegotiationHandler = + InternalProtocolNegotiators.grpcNegotiationHandler(grpcHandler); + final ChannelHandler tlsHandler = new ServerTlsHandler(grpcNegotiationHandler, grpcHandler, + sslContext, listener, offloadExecutorPool); + return InternalProtocolNegotiators.waitUntilActiveHandler( + tlsHandler, grpcHandler.getNegotiationLogger()); + } + + @Override + public void close() { + if (offloadExecutorPool != null && executor != null) { + offloadExecutorPool.returnObject(executor); + } + } + } + + private static final class ServerTlsHandler + extends InternalProtocolNegotiators.ProtocolNegotiationHandler { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + private final Executor executor; + private boolean failureReported; + + private ServerTlsHandler(ChannelHandler next, GrpcHttp2ConnectionHandler grpcHandler, + SslContext sslContext, TlsHandshakeFailureListener listener, + ObjectPool<? extends Executor> offloadExecutorPool) { + super(next, grpcHandler.getNegotiationLogger()); + this.sslContext = sslContext; + this.listener = listener; + this.executor = offloadExecutorPool != null ? offloadExecutorPool.getObject() : null; + } + + @Override + protected void handlerAdded0(ChannelHandlerContext context) { + final SSLEngine sslEngine = sslContext.newEngine(context.alloc()); + final SslHandler sslHandler = executor != null + ? new SslHandler(sslEngine, false, executor) + : new SslHandler(sslEngine, false); + context.pipeline().addBefore(context.name(), null, sslHandler); + } + + @Override + protected void userEventTriggered0(ChannelHandlerContext context, Object event) + throws Exception { + if (!(event instanceof SslHandshakeCompletionEvent)) { + super.userEventTriggered0(context, event); + return; + } + + final SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) event; + if (!handshakeEvent.isSuccess()) { + final Throwable cause = handshakeEvent.cause(); + if (containsSslException(cause)) { + notifyListener(context, cause); + } + context.fireExceptionCaught(cause); + return; + } + + final SslHandler sslHandler = context.pipeline().get(SslHandler.class); + if (!sslContext.applicationProtocolNegotiator().protocols() + .contains(sslHandler.applicationProtocol())) { + final RuntimeException cause = Status.UNAVAILABLE + .withDescription("Failed protocol negotiation: Unable to find compatible protocol") + .asRuntimeException(); + notifyListener(context, cause); + context.fireExceptionCaught(cause); + return; + } + propagateTlsComplete(context, sslHandler.engine().getSession()); + } + + private void notifyListener(ChannelHandlerContext context, Throwable cause) { + if (failureReported) { + return; + } + failureReported = true; Review Comment: It is better to use AtomicBoolean and compareAndSet: ```java if (failureReported.compareAndSet(false, true)) { return; } ``` ########## ratis-grpc/src/main/java/org/apache/ratis/grpc/server/GrpcServicesImpl.java: ########## @@ -218,9 +234,10 @@ private NettyServerBuilder newNettyServerBuilder(String hostname, int port, SslC nettyServerBuilder.workerEventLoopGroup(serverWorkers); } - if (sslContext != null) { + if (serverCredentials != null) { + LOG.info("Setting server credentials for {}", address); + } else if (sslContext != null) { LOG.info("Setting TLS for {}", address); - nettyServerBuilder.sslContext(sslContext); } Review Comment: Move the LOG to above if-statement. ```java if (serverCredentials != null) { LOG.info("Setting server credentials for {}", address); nettyServerBuilder = NettyServerBuilder.forAddress(address, serverCredentials); } else { nettyServerBuilder = NettyServerBuilder.forAddress(address); if (sslContext != null) { LOG.info("Setting sslContext for {}", address); nettyServerBuilder.sslContext(sslContext); } } ``` ########## ratis-grpc/src/main/java/org/apache/ratis/grpc/TlsHandshakeFailureServerCredentials.java: ########## @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ratis.grpc; + +import org.apache.ratis.thirdparty.io.grpc.Attributes; +import org.apache.ratis.thirdparty.io.grpc.Grpc; +import org.apache.ratis.thirdparty.io.grpc.InternalChannelz; +import org.apache.ratis.thirdparty.io.grpc.SecurityLevel; +import org.apache.ratis.thirdparty.io.grpc.ServerCredentials; +import org.apache.ratis.thirdparty.io.grpc.Status; +import org.apache.ratis.thirdparty.io.grpc.internal.GrpcAttributes; +import org.apache.ratis.thirdparty.io.grpc.internal.ObjectPool; +import org.apache.ratis.thirdparty.io.grpc.netty.GrpcHttp2ConnectionHandler; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalNettyServerCredentials; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiationEvent; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiator; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiators; +import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandler; +import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandlerContext; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslContext; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslHandler; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslHandshakeCompletionEvent; +import org.apache.ratis.thirdparty.io.netty.util.AsciiString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** Builds gRPC server credentials which report initial TLS handshake failures. */ +public final class TlsHandshakeFailureServerCredentials { + private static final Logger LOG = + LoggerFactory.getLogger(TlsHandshakeFailureServerCredentials.class); + private static final AsciiString HTTPS = AsciiString.of("https"); + + private TlsHandshakeFailureServerCredentials() {} + + /** + * Creates server credentials from the given TLS configuration and failure listener. + * + * @param tlsConfig the server TLS configuration + * @param listener the listener for TLS handshake failures + * @return server credentials reporting TLS handshake failures + */ + public static ServerCredentials create( + GrpcTlsConfig tlsConfig, TlsHandshakeFailureListener listener) { + Objects.requireNonNull(tlsConfig, "tlsConfig"); + Objects.requireNonNull(listener, "listener"); + final SslContext sslContext = GrpcUtil.buildSslContextForServer(tlsConfig); + return InternalNettyServerCredentials.create(new Factory(sslContext, listener)); + } + + private static boolean containsSslException(Throwable throwable) { + for (Throwable cause = throwable; cause != null; cause = cause.getCause()) { + if (cause instanceof SSLException) { + return true; + } + } + return false; + } + + private static final class Factory implements InternalProtocolNegotiator.ServerFactory { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + + private Factory(SslContext sslContext, TlsHandshakeFailureListener listener) { + this.sslContext = sslContext; + this.listener = listener; + } + + @Override + public InternalProtocolNegotiator.ProtocolNegotiator newNegotiator( + ObjectPool<? extends Executor> offloadExecutorPool) { + return new Negotiator(sslContext, listener, offloadExecutorPool); + } + } + + private static final class Negotiator implements InternalProtocolNegotiator.ProtocolNegotiator { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + private final ObjectPool<? extends Executor> offloadExecutorPool; + private final Executor executor; + + private Negotiator(SslContext sslContext, TlsHandshakeFailureListener listener, + ObjectPool<? extends Executor> offloadExecutorPool) { + this.sslContext = sslContext; + this.listener = listener; + this.offloadExecutorPool = offloadExecutorPool; + this.executor = offloadExecutorPool != null ? offloadExecutorPool.getObject() : null; + } + + @Override + public AsciiString scheme() { + return HTTPS; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + final ChannelHandler grpcNegotiationHandler = + InternalProtocolNegotiators.grpcNegotiationHandler(grpcHandler); + final ChannelHandler tlsHandler = new ServerTlsHandler(grpcNegotiationHandler, grpcHandler, + sslContext, listener, offloadExecutorPool); Review Comment: It seems better to pass the executor, instead of the offloadExecutorPool. ########## ratis-grpc/src/main/java/org/apache/ratis/grpc/TlsHandshakeFailureServerCredentials.java: ########## @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.ratis.grpc; + +import org.apache.ratis.thirdparty.io.grpc.Attributes; +import org.apache.ratis.thirdparty.io.grpc.Grpc; +import org.apache.ratis.thirdparty.io.grpc.InternalChannelz; +import org.apache.ratis.thirdparty.io.grpc.SecurityLevel; +import org.apache.ratis.thirdparty.io.grpc.ServerCredentials; +import org.apache.ratis.thirdparty.io.grpc.Status; +import org.apache.ratis.thirdparty.io.grpc.internal.GrpcAttributes; +import org.apache.ratis.thirdparty.io.grpc.internal.ObjectPool; +import org.apache.ratis.thirdparty.io.grpc.netty.GrpcHttp2ConnectionHandler; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalNettyServerCredentials; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiationEvent; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiator; +import org.apache.ratis.thirdparty.io.grpc.netty.InternalProtocolNegotiators; +import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandler; +import org.apache.ratis.thirdparty.io.netty.channel.ChannelHandlerContext; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslContext; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslHandler; +import org.apache.ratis.thirdparty.io.netty.handler.ssl.SslHandshakeCompletionEvent; +import org.apache.ratis.thirdparty.io.netty.util.AsciiString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** Builds gRPC server credentials which report initial TLS handshake failures. */ +public final class TlsHandshakeFailureServerCredentials { + private static final Logger LOG = + LoggerFactory.getLogger(TlsHandshakeFailureServerCredentials.class); + private static final AsciiString HTTPS = AsciiString.of("https"); + + private TlsHandshakeFailureServerCredentials() {} + + /** + * Creates server credentials from the given TLS configuration and failure listener. + * + * @param tlsConfig the server TLS configuration + * @param listener the listener for TLS handshake failures + * @return server credentials reporting TLS handshake failures + */ + public static ServerCredentials create( + GrpcTlsConfig tlsConfig, TlsHandshakeFailureListener listener) { + Objects.requireNonNull(tlsConfig, "tlsConfig"); + Objects.requireNonNull(listener, "listener"); + final SslContext sslContext = GrpcUtil.buildSslContextForServer(tlsConfig); + return InternalNettyServerCredentials.create(new Factory(sslContext, listener)); + } + + private static boolean containsSslException(Throwable throwable) { + for (Throwable cause = throwable; cause != null; cause = cause.getCause()) { + if (cause instanceof SSLException) { + return true; + } + } + return false; + } + + private static final class Factory implements InternalProtocolNegotiator.ServerFactory { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + + private Factory(SslContext sslContext, TlsHandshakeFailureListener listener) { + this.sslContext = sslContext; + this.listener = listener; + } + + @Override + public InternalProtocolNegotiator.ProtocolNegotiator newNegotiator( + ObjectPool<? extends Executor> offloadExecutorPool) { + return new Negotiator(sslContext, listener, offloadExecutorPool); + } + } + + private static final class Negotiator implements InternalProtocolNegotiator.ProtocolNegotiator { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + private final ObjectPool<? extends Executor> offloadExecutorPool; + private final Executor executor; + + private Negotiator(SslContext sslContext, TlsHandshakeFailureListener listener, + ObjectPool<? extends Executor> offloadExecutorPool) { + this.sslContext = sslContext; + this.listener = listener; + this.offloadExecutorPool = offloadExecutorPool; + this.executor = offloadExecutorPool != null ? offloadExecutorPool.getObject() : null; + } + + @Override + public AsciiString scheme() { + return HTTPS; + } + + @Override + public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) { + final ChannelHandler grpcNegotiationHandler = + InternalProtocolNegotiators.grpcNegotiationHandler(grpcHandler); + final ChannelHandler tlsHandler = new ServerTlsHandler(grpcNegotiationHandler, grpcHandler, + sslContext, listener, offloadExecutorPool); + return InternalProtocolNegotiators.waitUntilActiveHandler( + tlsHandler, grpcHandler.getNegotiationLogger()); + } + + @Override + public void close() { + if (offloadExecutorPool != null && executor != null) { + offloadExecutorPool.returnObject(executor); + } + } + } + + private static final class ServerTlsHandler + extends InternalProtocolNegotiators.ProtocolNegotiationHandler { + private final SslContext sslContext; + private final TlsHandshakeFailureListener listener; + private final Executor executor; + private boolean failureReported; + + private ServerTlsHandler(ChannelHandler next, GrpcHttp2ConnectionHandler grpcHandler, + SslContext sslContext, TlsHandshakeFailureListener listener, + ObjectPool<? extends Executor> offloadExecutorPool) { + super(next, grpcHandler.getNegotiationLogger()); + this.sslContext = sslContext; + this.listener = listener; + this.executor = offloadExecutorPool != null ? offloadExecutorPool.getObject() : null; + } + + @Override + protected void handlerAdded0(ChannelHandlerContext context) { + final SSLEngine sslEngine = sslContext.newEngine(context.alloc()); + final SslHandler sslHandler = executor != null + ? new SslHandler(sslEngine, false, executor) + : new SslHandler(sslEngine, false); + context.pipeline().addBefore(context.name(), null, sslHandler); + } + + @Override + protected void userEventTriggered0(ChannelHandlerContext context, Object event) + throws Exception { + if (!(event instanceof SslHandshakeCompletionEvent)) { + super.userEventTriggered0(context, event); + return; + } + + final SslHandshakeCompletionEvent handshakeEvent = (SslHandshakeCompletionEvent) event; + if (!handshakeEvent.isSuccess()) { + final Throwable cause = handshakeEvent.cause(); + if (containsSslException(cause)) { + notifyListener(context, cause); + } + context.fireExceptionCaught(cause); + return; + } + + final SslHandler sslHandler = context.pipeline().get(SslHandler.class); + if (!sslContext.applicationProtocolNegotiator().protocols() + .contains(sslHandler.applicationProtocol())) { + final RuntimeException cause = Status.UNAVAILABLE + .withDescription("Failed protocol negotiation: Unable to find compatible protocol") Review Comment: Include the protocol string in the exception: ```java final String protocol = sslHandler.applicationProtocol(); if (!sslContext.applicationProtocolNegotiator().protocols().contains(protocol)) { final Exception cause = Status.UNAVAILABLE .withDescription("Failed protocol negotiation: Unable to find compatible protocol for " + protocol) ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
