Skip to content

Commit 020da87

Browse files
pablocarlePablo Hernán Carlepj892031Petr Weinfurt
authored
feat: websocket timeout and close server on error (#2914)
Signed-off-by: Pablo Hernán Carle <pablo.carle@broadcom.com> Co-authored-by: Pavel Jareš <58428711+pj892031@users.noreply.github.com> Signed-off-by: Pablo Carle <pablocarle@users.noreply.github.com> --------- Signed-off-by: Pablo Hernán Carle <pablo.carle@broadcom.com> Signed-off-by: Pablo Carle <pablocarle@users.noreply.github.com> Co-authored-by: Pablo Hernán Carle <pablo.carle@broadcom.com> Co-authored-by: Pavel Jareš <58428711+pj892031@users.noreply.github.com> Co-authored-by: Petr Weinfurt <petr.weinfurt@broadcom.com>
1 parent 3809622 commit 020da87

File tree

9 files changed

+179
-38
lines changed

9 files changed

+179
-38
lines changed

gateway-package/src/main/resources/bin/start.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} java \
218218
-Dserver.address=0.0.0.0 \
219219
-Dserver.maxConnectionsPerRoute=${ZWE_configs_server_maxConnectionsPerRoute:-100} \
220220
-Dserver.maxTotalConnections=${ZWE_configs_server_maxTotalConnections:-1000} \
221+
-Dserver.webSocket.maxIdleTimeout=${ZWE_configs_server_webSocket_maxIdleTimeout:-3600000} \
221222
-Dserver.ssl.enabled=${ZWE_configs_server_ssl_enabled:-true} \
222223
-Dserver.ssl.protocol=${ZWE_configs_server_ssl_protocol:-"TLSv1.2"} \
223224
-Dserver.ssl.keyStore="${keystore_location}" \

gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketClientFactory.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@
1010

1111
package org.zowe.apiml.gateway.ws;
1212

13-
import lombok.AccessLevel;
14-
import lombok.RequiredArgsConstructor;
15-
import lombok.extern.slf4j.Slf4j;
13+
import javax.annotation.PreDestroy;
14+
1615
import org.eclipse.jetty.client.HttpClient;
1716
import org.eclipse.jetty.util.ssl.SslContextFactory;
1817
import org.eclipse.jetty.websocket.client.WebSocketClient;
1918
import org.springframework.beans.factory.annotation.Autowired;
19+
import org.springframework.beans.factory.annotation.Value;
2020
import org.springframework.stereotype.Component;
2121
import org.springframework.web.socket.client.jetty.JettyWebSocketClient;
2222

23-
import javax.annotation.PreDestroy;
23+
import lombok.AccessLevel;
24+
import lombok.RequiredArgsConstructor;
25+
import lombok.extern.slf4j.Slf4j;
2426

2527
/**
2628
* Factory for provisioning web socket client
@@ -35,10 +37,15 @@ public class WebSocketClientFactory {
3537
private final JettyWebSocketClient client;
3638

3739
@Autowired
38-
public WebSocketClientFactory(SslContextFactory.Client jettyClientSslContextFactory) {
40+
public WebSocketClientFactory(
41+
SslContextFactory.Client jettyClientSslContextFactory,
42+
@Value("${server.webSocket.maxIdleTimeout:3600000}") int maxIdleWebSocketTimeout
43+
) {
3944
log.debug("Creating Jetty WebSocket client, with SslFactory: {}",
4045
jettyClientSslContextFactory);
41-
client = new JettyWebSocketClient(new WebSocketClient(new HttpClient(jettyClientSslContextFactory)));
46+
WebSocketClient wsClient = new WebSocketClient(new HttpClient(jettyClientSslContextFactory));
47+
wsClient.setMaxIdleTimeout(maxIdleWebSocketTimeout);
48+
client = new JettyWebSocketClient(wsClient);
4249
client.start();
4350
}
4451

@@ -52,6 +59,7 @@ void closeClient() {
5259
log.debug("Closing Jetty WebSocket client");
5360
client.stop();
5461
}
62+
5563
}
5664

5765
}

gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyClientHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
package org.zowe.apiml.gateway.ws;
1212

1313
import lombok.extern.slf4j.Slf4j;
14+
15+
import java.util.concurrent.TimeoutException;
16+
17+
import org.eclipse.jetty.websocket.api.CloseException;
1418
import org.springframework.web.socket.CloseStatus;
1519
import org.springframework.web.socket.WebSocketMessage;
1620
import org.springframework.web.socket.WebSocketSession;
@@ -42,5 +46,12 @@ public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
4246
@Override
4347
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
4448
log.warn("WebSocket transport error in session {}: {}", session.getId(), exception.getMessage());
49+
if (exception instanceof CloseException && exception.getCause() instanceof TimeoutException) {
50+
// Idle timeout
51+
webSocketServerSession.close(CloseStatus.NORMAL);
52+
} else if (exception instanceof CloseException) {
53+
webSocketServerSession.close(new CloseStatus(((CloseException) exception).getStatusCode(), exception.getMessage()));
54+
}
55+
super.handleTransportError(session, exception);
4556
}
4657
}

gateway-service/src/main/java/org/zowe/apiml/gateway/ws/WebSocketProxyServerHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.net.URI;
3535
import java.util.List;
3636
import java.util.Map;
37+
import java.util.Optional;
3738
import java.util.concurrent.ConcurrentHashMap;
3839

3940
/**
@@ -185,6 +186,16 @@ private void openWebSocketConnection(RoutedService service, ServiceInstance serv
185186

186187
@Override
187188
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
189+
// if the browser closes the session, close the GWs client one as well.
190+
Optional.ofNullable(routedSessions.get(session.getId()))
191+
.map(WebSocketRoutedSession::getWebSocketClientSession)
192+
.ifPresent(clientSession -> {
193+
try {
194+
clientSession.close(status);
195+
} catch (IOException e) {
196+
log.debug("Error closing WebSocket client connection {}: {}", clientSession.getId(), e.getMessage());
197+
}
198+
});
188199
routedSessions.remove(session.getId());
189200
}
190201

gateway-service/src/test/java/org/zowe/apiml/gateway/ws/WebSocketClientFactoryTest.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
import org.junit.jupiter.api.BeforeEach;
1414
import org.junit.jupiter.api.Nested;
1515
import org.junit.jupiter.api.Test;
16+
import org.springframework.test.util.ReflectionTestUtils;
1617
import org.springframework.web.socket.client.jetty.JettyWebSocketClient;
1718

19+
import static org.junit.jupiter.api.Assertions.assertEquals;
1820
import static org.junit.jupiter.api.Assertions.assertSame;
1921
import static org.mockito.Mockito.*;
2022

23+
import org.eclipse.jetty.util.ssl.SslContextFactory;
24+
import org.eclipse.jetty.websocket.client.WebSocketClient;
25+
2126
class WebSocketClientFactoryTest {
2227

2328
@Nested
@@ -52,4 +57,23 @@ void whenGetClient_thenReturnInstance() {
5257

5358
}
5459

55-
}
60+
@Nested
61+
class CreatedInstanceWithConfig {
62+
63+
private WebSocketClientFactory webSocketClientFactory;
64+
65+
@BeforeEach
66+
void setUp() {
67+
SslContextFactory.Client sslClient = mock(SslContextFactory.Client.class);
68+
this.webSocketClientFactory = new WebSocketClientFactory(sslClient, 1234);
69+
}
70+
71+
@Test
72+
void givenInitilizedClient_thenHasNonDefaultIdleConfig() {
73+
WebSocketClient wsClient = (WebSocketClient) ReflectionTestUtils.getField(webSocketClientFactory.getClientInstance(), "client");
74+
assertEquals(1234, wsClient.getMaxIdleTimeout());
75+
}
76+
77+
}
78+
79+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*/
10+
11+
package org.zowe.apiml.gateway.ws;
12+
13+
import static org.mockito.Mockito.mock;
14+
import static org.mockito.Mockito.times;
15+
import static org.mockito.Mockito.verify;
16+
17+
import java.util.concurrent.TimeoutException;
18+
19+
import org.eclipse.jetty.websocket.api.CloseException;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Nested;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
import org.mockito.Mock;
25+
import org.mockito.junit.jupiter.MockitoExtension;
26+
import org.springframework.web.socket.CloseStatus;
27+
import org.springframework.web.socket.WebSocketSession;
28+
29+
@ExtendWith(MockitoExtension.class)
30+
public class WebSocketProxyClientHandlerTest {
31+
32+
@Mock
33+
private WebSocketSession serverSession;
34+
35+
private WebSocketProxyClientHandler webSocketProxyClientHandler;
36+
37+
@BeforeEach
38+
void setUp() {
39+
webSocketProxyClientHandler = new WebSocketProxyClientHandler(serverSession);
40+
}
41+
42+
@Nested
43+
class GivenHandler {
44+
45+
@Nested
46+
class AndConnectionIsClosed {
47+
48+
@Test
49+
void thenCloseServer() throws Exception {
50+
webSocketProxyClientHandler.afterConnectionClosed(mock(WebSocketSession.class), CloseStatus.NORMAL);
51+
verify(serverSession, times(1)).close(CloseStatus.NORMAL);
52+
}
53+
54+
}
55+
56+
@Nested
57+
class AndConnectionTransportError {
58+
59+
@Test
60+
void andTimeout_thenCloseNormal() throws Exception {
61+
webSocketProxyClientHandler.handleTransportError(mock(WebSocketSession.class), new CloseException(0, new TimeoutException("null")));
62+
verify(serverSession, times(1)).close(CloseStatus.NORMAL);
63+
}
64+
65+
@Test
66+
void andCloseException_thenForwardError() throws Exception {
67+
webSocketProxyClientHandler.handleTransportError(mock(WebSocketSession.class), new CloseException(CloseStatus.PROTOCOL_ERROR.getCode(), new Exception("message")));
68+
verify(serverSession, times(1)).close(new CloseStatus(1002, "java.lang.Exception: message"));
69+
}
70+
71+
}
72+
73+
}
74+
75+
}

gateway-service/src/test/java/org/zowe/apiml/gateway/ws/WebSocketProxyServerHandlerTest.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import static org.hamcrest.CoreMatchers.is;
3434
import static org.hamcrest.CoreMatchers.notNullValue;
3535
import static org.hamcrest.MatcherAssert.assertThat;
36-
import static org.hamcrest.Matchers.hasSize;
36+
import static org.hamcrest.Matchers.*;
3737
import static org.junit.jupiter.api.Assertions.assertTrue;
3838
import static org.mockito.ArgumentMatchers.any;
3939
import static org.mockito.Mockito.*;
@@ -59,16 +59,6 @@ public void setup() {
5959
ReflectionTestUtils.setField(underTest, "meAsProxy", underTest);
6060
}
6161

62-
63-
private ServiceInstance validServiceInstance() {
64-
ServiceInstance validService = mock(ServiceInstance.class);
65-
when(validService.getHost()).thenReturn("gatewayHost");
66-
when(validService.isSecure()).thenReturn(true);
67-
when(validService.getPort()).thenReturn(1443);
68-
69-
return validService;
70-
}
71-
7262
@Nested
7363
class WhenTheConnectionIsEstablished {
7464
WebSocketSession establishedSession;
@@ -87,7 +77,6 @@ void prepareRoutedService() {
8777
RoutedServices routesForSpecificValidService = mock(RoutedServices.class);
8878
when(routesForSpecificValidService.findServiceByGatewayUrl("ws/v1"))
8979
.thenReturn(new RoutedService("ws-v1", "ws/v1", "/valid-service/ws/v1"));
90-
ServiceInstance foundService = validServiceInstance();
9180

9281
underTest.addRoutedServices(serviceId, routesForSpecificValidService);
9382
}
@@ -223,11 +212,24 @@ void prepareSessionMock() {
223212
void whenTheConnectionIsClosed_thenTheSessionIsClosedAndRemovedFromRepository() {
224213
CloseStatus normalClose = CloseStatus.NORMAL;
225214

215+
assertThat(routedSessions.entrySet(), not(empty()));
216+
226217
underTest.afterConnectionClosed(establishedSession, normalClose);
227218

228219
assertThat(routedSessions.entrySet(), hasSize(0));
229220
}
230221

222+
@Test
223+
void whenTheConnectionIsClosed_thenClientSessionIsAlsoClosed() throws IOException {
224+
CloseStatus normalClose = CloseStatus.NORMAL;
225+
WebSocketSession clientSession = mock(WebSocketSession.class);
226+
when(internallyStoredSession.getWebSocketClientSession()).thenReturn(clientSession);
227+
228+
underTest.afterConnectionClosed(establishedSession, normalClose);
229+
verify(clientSession, times(1)).close(normalClose);
230+
assertThat(routedSessions.entrySet(), hasSize(0));
231+
}
232+
231233
@Test
232234
void whenTheMessageIsReceived_thenTheMessageIsPassedToTheSession() throws Exception {
233235
underTest.handleMessage(establishedSession, passedMessage);
@@ -282,7 +284,7 @@ void thenReturnThem() {
282284
class WhenGettingSubProtocols {
283285
@Test
284286
void thenReturnThem() {
285-
ArrayList protocol = new ArrayList();
287+
List<String> protocol = new ArrayList<>();
286288
protocol.add("protocol");
287289
ReflectionTestUtils.setField(underTest, "subProtocols", protocol);
288290
List<String> subProtocols = underTest.getSubProtocols();

integration-tests/src/test/java/org/zowe/apiml/integration/proxy/WebSocketProxyTest.java

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,26 @@
1010

1111
package org.zowe.apiml.integration.proxy;
1212

13-
import io.restassured.RestAssured;
13+
import static io.restassured.RestAssured.given;
14+
import static org.apache.http.HttpStatus.SC_OK;
15+
import static org.apache.tomcat.websocket.Constants.SSL_CONTEXT_PROPERTY;
16+
import static org.hamcrest.Matchers.is;
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
import static org.junit.jupiter.api.Assertions.assertTrue;
19+
import static org.zowe.apiml.util.requests.Endpoints.DISCOVERABLE_WS_HEADER;
20+
import static org.zowe.apiml.util.requests.Endpoints.DISCOVERABLE_WS_UPPERCASE;
21+
22+
import java.net.URI;
23+
import java.net.URISyntaxException;
24+
import java.util.Base64;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.concurrent.atomic.AtomicInteger;
27+
1428
import org.apache.http.client.utils.URIBuilder;
1529
import org.junit.jupiter.api.BeforeAll;
1630
import org.junit.jupiter.api.BeforeEach;
1731
import org.junit.jupiter.api.Nested;
1832
import org.junit.jupiter.api.Test;
19-
20-
21-
22-
2333
import org.springframework.web.socket.CloseStatus;
2434
import org.springframework.web.socket.TextMessage;
2535
import org.springframework.web.socket.WebSocketHttpHeaders;
@@ -35,19 +45,7 @@
3545
import org.zowe.apiml.util.http.HttpClientUtils;
3646
import org.zowe.apiml.util.http.HttpRequestUtils;
3747

38-
import java.net.URI;
39-
import java.net.URISyntaxException;
40-
import java.util.Base64;
41-
import java.util.concurrent.TimeUnit;
42-
import java.util.concurrent.atomic.AtomicInteger;
43-
44-
import static io.restassured.RestAssured.given;
45-
import static org.apache.http.HttpStatus.SC_OK;
46-
import static org.apache.tomcat.websocket.Constants.SSL_CONTEXT_PROPERTY;
47-
import static org.junit.jupiter.api.Assertions.assertEquals;
48-
import static org.junit.jupiter.api.Assertions.assertTrue;
49-
import static org.zowe.apiml.util.requests.Endpoints.*;
50-
import static org.hamcrest.Matchers.is;
48+
import io.restassured.RestAssured;
5149

5250
@TestsNotMeantForZowe
5351
@WebsocketTest

schemas/gateway-schema.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,17 @@
197197
"description": "How many connection should exists in total?",
198198
"default": 1000
199199
},
200+
"webSocket": {
201+
"type": "object",
202+
"description": "Customize websocket server parameters",
203+
"properties": {
204+
"maxIdleTimeout": {
205+
"type": "integer",
206+
"description": "The gateway acts as a server and client. This parameters customizes the default idle timeout for its client role.",
207+
"default": 3600000
208+
}
209+
}
210+
},
200211
"ssl": {
201212
"type": "object",
202213
"description": "Network encryption for gateway service connections.",

0 commit comments

Comments
 (0)