question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Issue with https proxy & http 2.0 / hardcoded SslHandler reference / unsupported multiple SSLHandlers

See original GitHub issue

Expected behavior

I’m trying to implement https proxy client with netty and it works well with http/1.1 but not 2.0. Pipeline looks like:

SSLHandler // for proxy
HttpProxyHandler
SSLHandler // for http
Http Codes

Actual behavior

It fails with http/2.0 due hardcoded reference to SslHandle io.netty.handler.ssl.ApplicationProtocolNegotiationHandler:

      SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
      // ...
      String protocol = sslHandler.applicationProtocol()

It takes incorrect SslHandler 1 and because of that uses incorrect application protocol (1.1, not 2.0). Also, because multiple SslHandshakeCompletionEvent triggered check should be more sophisticated than evt instanceof SslHandshakeCompletionEvent. The bug is very similar to #5070 which had a similar issue, only with http codecs. Possible workaround is to override userEventTriggered but I think this should be addressed in netty.

Netty version

netty 4.1.65

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:11 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
valodzkacommented, Jun 26, 2021

Basic reproducer is fairly simple, see below (some checks omitted for brevity) . You can switch isHttp2/withProxy to see different modes, it works in all modes except isHttp2=true/withProxy=true, in this case it throw new IllegalArgumentException("Must switch to http2") because of mentioned SslHandler hardcode.

boolean isHttp2 = true, withProxy = true;

SslContext sslCtx11 = SslContextBuilder.forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE).build();
SslContext sslCtx20 = SslContextBuilder.forClient()
        .sslProvider(SslProvider.OPENSSL)
        .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
        .trustManager(InsecureTrustManagerFactory.INSTANCE)
        .applicationProtocolConfig(new ApplicationProtocolConfig(
                ApplicationProtocolConfig.Protocol.ALPN,
                ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
                ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
                ApplicationProtocolNames.HTTP_2,
                ApplicationProtocolNames.HTTP_1_1))
        .build() ;

SslContext proxySsl = sslCtx11;
SslContext httpSsl = isHttp2 ? sslCtx20 : sslCtx11;

EventLoopGroup group = new NioEventLoopGroup();


DefaultHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/json");
request.headers().add("Host", "ipinfo.io");

if (isHttp2)
    request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "https");

Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
        .handler(new ChannelInitializer<>() {
            @Override
            protected void initChannel(Channel ch) {
                ChannelPipeline pipe = ch.pipeline();

                if (withProxy) {
                    pipe.addLast("proxySsl", proxySsl.newHandler(ch.alloc()));
                    pipe.addLast("proxy", new HttpProxyHandler(proxyAddr, proxyUser, proxyPass));
                }
                pipe.addLast("ssl", httpSsl.newHandler(ch.alloc()));
                if (isHttp2) {
                    pipe.addLast("http2", new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) {
                        @Override
                        protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
                            if (!protocol.equals("h2"))
                                throw new IllegalArgumentException("Must switch to http2");

                            final Http2Connection connection = new DefaultHttp2Connection(false);

                            pipe.addBefore("handler", "codec", new HttpToHttp2ConnectionHandlerBuilder()
                                    .frameListener(new DelegatingDecompressorFrameListener(
                                            connection,
                                            new InboundHttp2ToHttpAdapterBuilder(connection)
                                                    .maxContentLength(100000)
                                                    .propagateSettings(true)
                                                    .build()))
                                    .connection(connection)
                                    .build());
                            pipe.addBefore("handler", "settings", new SimpleChannelInboundHandler<Http2Settings>() {
                                        @Override
                                        protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) {
                                            ctx.pipeline().remove(this);
                                            ctx.channel().writeAndFlush(request);
                                        }
                            });
                        }
                    });
                }
                else {
                    pipe.addLast("codec", new HttpClientCodec());
                }
                pipe.addLast("handler", new SimpleChannelInboundHandler<HttpObject>(){
                    @Override
                    protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) {
                        System.out.println(httpObject);
                        if (httpObject instanceof HttpContent ct) {
                            System.out.println(ct.content().readCharSequence(ct.content().readableBytes(), StandardCharsets.UTF_8));
                        }
                    }
                });
            }
        });

ChannelFuture future = b.connect(new InetSocketAddress("ipinfo.io", 443));
Channel channel = future.sync().channel();
if (!isHttp2)
    channel.writeAndFlush(request);
channel.closeFuture().sync();
0reactions
valodzkacommented, Jul 13, 2021

Unfortunately I cannot provide full code because it’s coupled to other internal classes.

Read more comments on GitHub >

github_iconTop Results From Across the Web

HTTPS connections over proxy servers - Stack Overflow
The trick is, we turn an HTTP proxy into a TCP proxy with a special command named CONNECT . Not all HTTP proxies...
Read more >
Configure Docker to use a proxy server - Docker Documentation
This page describes how to configure the Docker CLI to configure proxies via environment variables in containers. For information on configuring Docker Desktop ......
Read more >
Duo Authentication Proxy Reference - Duo Security
Once the user approves the two-factor request (received as a push notification from Duo Mobile, or as a phone call, etc.), the Duo...
Read more >
API proxy configuration reference | Apigee Edge
See ProxyEndpoint. TargetEndpoint Configuration, Settings for the outbound HTTP connection (from Apigee Edge to the backend service), request and response flows ...
Read more >
Using an HTTP proxy - AWS Command Line Interface
To access AWS through proxy servers, you can configure the HTTP_PROXY and HTTPS_PROXY environment variables with either the DNS domain names or IP...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found