diff --git a/changelog.md b/changelog.md index 757f5fdea4..27f41de3df 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed - [Remove unused import from Http2StateUtil](https://github.com/ballerina-platform/ballerina-library/issues/5966) +- [Fix client getting hanged when server closes connection in the ALPN handshake](https://github.com/ballerina-platform/ballerina-library/issues/6003) - [Fix client getting hanged when multiple requests are sent which exceed `maxHeaderSize`](https://github.com/ballerina-platform/ballerina-library/issues/6000) ## [2.10.5] - 2023-12-06 diff --git a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/HttpClientChannelInitializer.java b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/HttpClientChannelInitializer.java index 7f4b0c6b48..20734fbfe5 100644 --- a/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/HttpClientChannelInitializer.java +++ b/native/src/main/java/io/ballerina/stdlib/http/transport/contractimpl/sender/HttpClientChannelInitializer.java @@ -60,6 +60,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.channels.ClosedChannelException; + import javax.net.ssl.SSLEngine; import static io.ballerina.stdlib.http.transport.contract.Constants.MAX_ENTITY_BODY_VALIDATION_HANDLER; @@ -342,6 +344,12 @@ public ALPNClientHandler(TargetHandler targetHandler, this.connectionAvailabilityFuture = connectionAvailabilityFuture; } + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + connectionAvailabilityFuture.notifyFailure(new ClosedChannelException()); + ctx.close(); + } + /** * Configure pipeline after TLS handshake. */ diff --git a/native/src/test/java/io/ballerina/stdlib/http/transport/http2/frameleveltests/Http2ServerAbruptClosureInALPNScenarioTest.java b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/frameleveltests/Http2ServerAbruptClosureInALPNScenarioTest.java new file mode 100644 index 0000000000..17a4c7978c --- /dev/null +++ b/native/src/test/java/io/ballerina/stdlib/http/transport/http2/frameleveltests/Http2ServerAbruptClosureInALPNScenarioTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. 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 io.ballerina.stdlib.http.transport.http2.frameleveltests; + +import io.ballerina.stdlib.http.transport.contract.Constants; +import io.ballerina.stdlib.http.transport.contract.HttpClientConnector; +import io.ballerina.stdlib.http.transport.contract.HttpWsConnectorFactory; +import io.ballerina.stdlib.http.transport.contract.config.SenderConfiguration; +import io.ballerina.stdlib.http.transport.contract.config.TransportsConfiguration; +import io.ballerina.stdlib.http.transport.contractimpl.DefaultHttpWsConnectorFactory; +import io.ballerina.stdlib.http.transport.message.HttpConnectorUtil; +import io.ballerina.stdlib.http.transport.util.DefaultHttpConnectorListener; +import io.ballerina.stdlib.http.transport.util.Http2Util; +import io.ballerina.stdlib.http.transport.util.TestUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static io.ballerina.stdlib.http.transport.util.TestUtil.getErrorResponseMessage; +import static org.testng.Assert.fail; +import static org.testng.AssertJUnit.assertEquals; + +/** + * This contains a test case where the tcp server closes abruptly while the alpn upgrade is happening. + */ +public class Http2ServerAbruptClosureInALPNScenarioTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(Http2ServerAbruptClosureInALPNScenarioTest.class); + private HttpClientConnector h2ClientWithUpgrade; + private ServerSocket serverSocket; + private int numOfConnections = 0; + + @BeforeClass + public void setup() throws InterruptedException { + runTcpServer(TestUtil.HTTP_SERVER_PORT); + h2ClientWithUpgrade = setupHttp2UpgradeClient(); + } + + public HttpClientConnector setupHttp2UpgradeClient() { + HttpWsConnectorFactory connectorFactory = new DefaultHttpWsConnectorFactory(); + SenderConfiguration senderConfiguration = Http2Util.getSenderConfigs(Constants.HTTP_2_0); + senderConfiguration.setForceHttp2(false); + return connectorFactory.createHttpClientConnector( + HttpConnectorUtil.getTransportProperties(new TransportsConfiguration()), senderConfiguration); + } + + @Test + private void testServerAbruptClosureInALPNScenario() { + try { + CountDownLatch latch1 = new CountDownLatch(1); + DefaultHttpConnectorListener msgListener1 = TestUtil.sendRequestAsync(latch1, h2ClientWithUpgrade); + latch1.await(TestUtil.HTTP2_RESPONSE_TIME_OUT, TimeUnit.SECONDS); + + CountDownLatch latch2 = new CountDownLatch(1); + DefaultHttpConnectorListener msgListener2 = TestUtil.sendRequestAsync(latch2, h2ClientWithUpgrade); + latch2.await(TestUtil.HTTP2_RESPONSE_TIME_OUT, TimeUnit.SECONDS); + + assertEquals(getErrorResponseMessage(msgListener1), + "Remote host: localhost/127.0.0.1:9000 closed the connection while SSL handshake"); + assertEquals(getErrorResponseMessage(msgListener2), + "Remote host: localhost/127.0.0.1:9000 closed the connection while SSL handshake"); + } catch (InterruptedException e) { + LOGGER.error("Interrupted exception occurred"); + fail(); + } + } + + private void runTcpServer(int port) { + new Thread(() -> { + try { + serverSocket = new ServerSocket(port); + LOGGER.info("HTTP/2 TCP Server listening on port " + port); + while (numOfConnections < 2) { + Socket clientSocket = serverSocket.accept(); + LOGGER.info("Accepted connection from: " + clientSocket.getInetAddress()); + try (InputStream inputStream = clientSocket.getInputStream()) { + readSocketAndExit(inputStream); + numOfConnections += 1; + } catch (Exception e) { + LOGGER.error(e.getMessage()); + } + } + } catch (IOException e) { + LOGGER.error(e.getMessage()); + } + }).start(); + } + + private void readSocketAndExit(InputStream inputStream) throws IOException { + // This will just read the socket input content and exit the socket without sending any response + // which will trigger the channel inactive in the client side + byte[] buffer = new byte[4096]; + int bytesRead = 0; + try { + bytesRead = inputStream.read(buffer); + } catch (Exception exception) { + LOGGER.error(exception.getMessage()); + } + String data = new String(buffer, 0, bytesRead); + LOGGER.info("Received ALPN request: " + data); + } + + @AfterClass + public void cleanUp() throws IOException { + h2ClientWithUpgrade.close(); + serverSocket.close(); + } +} diff --git a/native/src/test/resources/testng.xml b/native/src/test/resources/testng.xml index bd4f525788..3b35eae3f3 100644 --- a/native/src/test/resources/testng.xml +++ b/native/src/test/resources/testng.xml @@ -166,6 +166,7 @@ +