reactor-netty icon indicating copy to clipboard operation
reactor-netty copied to clipboard

NullPointerException using sniMapping with H2 + HTTP11

Open hamadodene opened this issue 1 year ago • 1 comments

We are trying to configure the Reactor Netty HTTP server with H2 and HTTP11, also using sniMapping. However, we are encountering a NullPointerException (NPE).

We have reproduced the problem at https://github.com/NiccoMlt/demo-reverse-proxy/tree/nullpointer-micrometer.

Our use case requires us to provide a different certificate based on the domain. Therefore, we thought to use sniMapping.

However, by enabling both H2 + HTTP11 on the server side and making requests with an HTTP11 or H2 client, we encounter the following NPE:

java.lang.NullPointerException: Cannot invoke "io.netty.handler.ssl.SslHandler.handshakeFuture()" because the return value of "io.netty.channel.ChannelPipeline.get(java.lang.Class)" is null
at reactor.netty.channel.MicrometerChannelMetricsHandler$TlsMetricsHandler.channelActive(MicrometerChannelMetricsHandler.java:275)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:262)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:238)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelActive(AbstractChannelHandlerContext.java:231)
at io.netty.handler.ssl.AbstractSniHandler.channelActive(AbstractSniHandler.java:157)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:260)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:238)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelActive(AbstractChannelHandlerContext.java:231)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelActive(DefaultChannelPipeline.java:1395)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:258)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:238)
at io.netty.channel.DefaultChannelPipeline.fireChannelActive(DefaultChannelPipeline.java:894)
at io.netty.channel.AbstractChannel$AbstractUnsafe.register0(AbstractChannel.java:521)
at io.netty.channel.AbstractChannel$AbstractUnsafe.access$200(AbstractChannel.java:428)
at io.netty.channel.AbstractChannel$AbstractUnsafe$1.run(AbstractChannel.java:485)
at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:173)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:166)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:469)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:994)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:1583)

If we instead configure the server only for H2 and make the request in H2, we get the following error:

ott 17, 2024 11:49:23 AM org.bouncycastle.jsse.provider.ProvTlsClient notifyConnectionClosed
INFORMAZIONI: [client #1 @3ecabaaf] disconnected from localhost:8443
Exception in thread "main" javax.net.ssl.SSLException: org.bouncycastle.tls.TlsFatalAlert: unexpected_message(10); Unsupported UNKNOWN(0)
at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:960)
at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:133)
at com.diennea.carapace.Main.main(Main.java:141)
Caused by: javax.net.ssl.SSLException: org.bouncycastle.tls.TlsFatalAlert: unexpected_message(10); Unsupported UNKNOWN(0)
at org.bouncycastle.jsse.provider.ProvSSLEngine.unwrap(Unknown Source)
at java.base/javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:679)
at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.unwrapBuffer(SSLFlowDelegate.java:542)
at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:438)
at java.net.http/jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:269)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$LockingRestartableTask.run(SequentialScheduler.java:182)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149)
at java.net.http/jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:207)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: org.bouncycastle.tls.TlsFatalAlert: unexpected_message(10); Unsupported UNKNOWN(0)
at org.bouncycastle.tls.RecordStream.checkRecordType(Unknown Source)
at org.bouncycastle.tls.RecordStream.previewRecordHeader(Unknown Source)
at org.bouncycastle.tls.TlsProtocol.safePreviewRecordHeader(Unknown Source)
at org.bouncycastle.tls.TlsProtocol.previewInputRecord(Unknown Source)
at org.bouncycastle.jsse.provider.ProvSSLEngine.getRecordPreview(Unknown Source)
... 11 more

In the repository, you will find the reproducible example.

hamadodene avatar Oct 17 '24 12:10 hamadodene

It also reproduces by modifying the test here by adding the H2 protocol.

What I've noticed is that when the Channel is registered in AbstractChannelMetricsHandler, the SslHandler is null... so then in MicrometerChannelMetricsHandler/TlsMetricsHandler, HandshakeFuture is done with sslHandler, which is null...

I hope this can help you.

hamadodene avatar Oct 17 '24 15:10 hamadodene

@hamadodene #3484 should be fixing the issue. If you are able to give it a try, it will be great!

Thanks a lot for the description and the reproducible example!

violetagg avatar Oct 24 '24 16:10 violetagg

@violetagg Thank you for the fix. We tried it, and now the NPE seems to be resolved. I’d like to ask you for some additional information if you could help us. We’re trying to enable OCSP stapling using handlerConfigurator:

        final HttpServer httpServer = HttpServer
                .create()
                .host(HOST)
                .port(PORT)
                .protocol(HttpProtocol.H2, HttpProtocol.HTTP11)
                .secure(sslContextSpec -> sslContextSpec
                        .sslContext(sslContext)
                        .handlerConfigurator(getSslHandlerConsumer(issuer, httpsCertificate))
                        .addSniMapping("localhost", sslContextSpec1 -> sslContextSpec1
                                .sslContext(sslContextLocalhost)
                                .handlerConfigurator(getSslHandlerConsumer(issuer, httpsCertificateSni)))
                )


    private static Consumer<SslHandler> getSslHandlerConsumer(final X509Certificate issuer, final X509Certificate httpsCertificateSni) {
        return sslHandler -> {
            if (!(sslHandler.engine() instanceof ReferenceCountedOpenSslEngine engine)) {
                throw new RuntimeException("Unexpected SSL handler type: " + sslHandler.engine());
            }

            // Attempt to retrieve and set the OCSP response here
            try {
                final byte[] ocspResponse = getOcspResponse(issuer, httpsCertificateSni);
                if (ocspResponse != null) {
                    engine.setOcspResponse(ocspResponse);
                } else {
                    System.err.println("Failed to retrieve OCSP response. It is null.");
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to set OCSP response: " + e.getMessage(), e);
            }
        };
    }
private static byte[] getOcspResponse(final X509Certificate issuer, final X509Certificate certificate) throws OCSPException, GeneralSecurityException, IOException, OperatorCreationException, InterruptedException {
      final OCSPReq ocspRequest = generateOCSPRequest(issuer, certificate);
      final OCSPResp ocspResp = sendOCSPRequest(OCSP_RESPONDER_URL, ocspRequest.getEncoded());

      // Check the response status
      if (ocspResp.getStatus() != OCSPRespBuilder.SUCCESSFUL) {
          System.err.println("OCSP response is not successful: " + ocspResp.getStatus());
          return null;
      }

      final BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResp.getResponseObject();
      return basicResponse.getEncoded(); // Return the DER-encoded OCSP response
  }

But we're having trouble getting it to work. The server’s response always lacks the OCSP check. Do you have any ideas?

You can also test on the nullpointer-micrometer branch of our repo: https://github.com/NiccoMlt/demo-reverse-proxy/tree/nullpointer-micrometer

We've already integrated your fix.

Thanks in advance

hamadodene avatar Oct 25 '24 08:10 hamadodene

In the end, we resolved the OCSP issue. The method we were using to verify the OCSP response was incorrect. In fact, it worked with OpenSSL but not with our code.

See https://github.com/NiccoMlt/demo-reverse-proxy/tree/nullpointer-micrometer-netty-client

Thanks a lot for your help

hamadodene avatar Oct 25 '24 15:10 hamadodene