Skip to content

Commit 2db9a7e

Browse files
authored
fix: Correctly handle ws connections for new path pattern (#1701)
* Correctly handle ws connections for new path pattern . * Handle new and old path patterns in unit tests Signed-off-by: Jakub Balhar <jakub@balhar.net>
1 parent 712ecd5 commit 2db9a7e

File tree

5 files changed

+218
-159
lines changed

5 files changed

+218
-159
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public GatewayWebSocketConfigurer(WebSocketProxyServerHandler webSocketProxyServ
2727

2828
@Override
2929
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
30-
String webSocketPath = "/ws/**"; // NOSONAR
30+
String webSocketPath = "/**/ws/**"; // NOSONAR
3131
log.debug("Registering WebSocket proxy handler to " + webSocketPath);
3232
registry.addHandler(webSocketProxyServerHandler, webSocketPath);
3333
}

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

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -89,36 +89,50 @@ public Map<String, WebSocketRoutedSession> getRoutedSessions() {
8989
}
9090

9191
@Override
92-
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
92+
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws IOException {
9393
String[] uriParts = getUriParts(webSocketSession);
94-
if (uriParts != null && uriParts.length == 5) {
95-
String majorVersion = uriParts[2];
96-
String serviceId = uriParts[3];
97-
String path = uriParts[4];
98-
99-
RoutedServices routedServices = routedServicesMap.get(serviceId);
100-
101-
if (routedServices != null) {
102-
RoutedService service = routedServices.findServiceByGatewayUrl("ws/" + majorVersion);
103-
if (service == null) {
104-
closeWebSocket(webSocketSession, CloseStatus.NOT_ACCEPTABLE,
105-
String.format("Requested ws/%s url is not known by the gateway", majorVersion));
106-
return;
107-
}
108-
109-
ServiceInstance serviceInstance = findServiceInstance(serviceId);
110-
if (serviceInstance != null) {
111-
openWebSocketConnection(service, serviceInstance, serviceInstance, path, webSocketSession);
112-
} else {
113-
closeWebSocket(webSocketSession, CloseStatus.SERVICE_RESTARTED,
114-
String.format("Requested service %s does not have available instance", serviceId));
115-
}
116-
} else {
117-
closeWebSocket(webSocketSession, CloseStatus.NOT_ACCEPTABLE,
118-
String.format("Requested service %s is not known by the gateway", serviceId));
119-
}
120-
} else {
94+
if (uriParts == null || uriParts.length != 5) {
12195
closeWebSocket(webSocketSession, CloseStatus.NOT_ACCEPTABLE, "Invalid URL format");
96+
return;
97+
}
98+
99+
String majorVersion;
100+
String serviceId;
101+
String path = uriParts[4];
102+
103+
if (uriParts[1].equals("ws")) {
104+
majorVersion = uriParts[2];
105+
serviceId = uriParts[3];
106+
} else {
107+
majorVersion = uriParts[3];
108+
serviceId = uriParts[1];
109+
}
110+
111+
routeToService(webSocketSession, serviceId, majorVersion, path);
112+
}
113+
114+
private void routeToService(WebSocketSession webSocketSession, String serviceId, String majorVersion, String path) throws IOException {
115+
RoutedServices routedServices = routedServicesMap.get(serviceId);
116+
117+
if (routedServices == null) {
118+
closeWebSocket(webSocketSession, CloseStatus.NOT_ACCEPTABLE,
119+
String.format("Requested service %s is not known by the gateway", serviceId));
120+
return;
121+
}
122+
123+
RoutedService service = routedServices.findServiceByGatewayUrl("ws/" + majorVersion);
124+
if (service == null) {
125+
closeWebSocket(webSocketSession, CloseStatus.NOT_ACCEPTABLE,
126+
String.format("Requested ws/%s url is not known by the gateway", majorVersion));
127+
return;
128+
}
129+
130+
ServiceInstance serviceInstance = findServiceInstance(serviceId);
131+
if (serviceInstance != null) {
132+
openWebSocketConnection(service, serviceInstance, serviceInstance, path, webSocketSession);
133+
} else {
134+
closeWebSocket(webSocketSession, CloseStatus.SERVICE_RESTARTED,
135+
String.format("Requested service %s does not have available instance", serviceId));
122136
}
123137
}
124138

gateway-service/src/main/resources/application.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ zuul:
122122
includeDebugHeader: false
123123
sensitiveHeaders: Expires,Date
124124
ignoredPatterns:
125-
- /ws/**
125+
- /**/ws/**
126126
host:
127127
connectTimeoutMillis: ${apiml.gateway.timeoutMillis}
128128
socketTimeoutMillis: ${apiml.gateway.timeoutMillis}

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

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

1313
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.jupiter.api.Nested;
1415
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.params.ParameterizedTest;
17+
import org.junit.jupiter.params.provider.ValueSource;
1518
import org.springframework.cloud.client.ServiceInstance;
1619
import org.springframework.cloud.client.discovery.DiscoveryClient;
1720
import org.springframework.web.socket.CloseStatus;
@@ -52,36 +55,7 @@ public void setup() {
5255
);
5356
}
5457

55-
/**
56-
* Happy Path
57-
* <p>
58-
* The Handler is properly created
59-
* Specified Route is added to the list
60-
* The connection is established
61-
* The URI contains the valid service Id
62-
* The service associated with given URI is retrieved
63-
* Proper WebSocketSession is stored.
64-
*/
65-
@Test
66-
void givenValidRoute_whenTheConnectionIsEstablished_thenTheValidSessionIsStoredInternally() throws Exception {
67-
RoutedServices routesForSpecificValidService = mock(RoutedServices.class);
68-
when(routesForSpecificValidService.findServiceByGatewayUrl("ws/1"))
69-
.thenReturn(new RoutedService("api-v1", "api/v1", "/api-v1/api/v1"));
70-
ServiceInstance foundService = validServiceInstance();
71-
when(discoveryClient.getInstances("api-v1")).thenReturn(Collections.singletonList(foundService));
72-
underTest.addRoutedServices("api-v1", routesForSpecificValidService);
73-
when(webSocketRoutedSessionFactory.session(any(), any(), any())).thenReturn(mock(WebSocketRoutedSession.class));
74-
75-
WebSocketSession establishedSession = mock(WebSocketSession.class);
76-
String establishedSessionId = "validAndUniqueId";
77-
when(establishedSession.getId()).thenReturn(establishedSessionId);
78-
when(establishedSession.getUri()).thenReturn(new URI("wss://gatewayHost:1443/gateway/1/api-v1/api/v1"));
79-
underTest.afterConnectionEstablished(establishedSession);
80-
81-
verify(webSocketRoutedSessionFactory).session(any(), any(), any());
82-
WebSocketRoutedSession preparedSession = routedSessions.get(establishedSessionId);
83-
assertThat(preparedSession, is(notNullValue()));
84-
}
58+
8559

8660
private ServiceInstance validServiceInstance() {
8761
ServiceInstance validService = mock(ServiceInstance.class);
@@ -92,95 +66,159 @@ private ServiceInstance validServiceInstance() {
9266
return validService;
9367
}
9468

95-
/**
96-
* Error Path
97-
* <p>
98-
* The Handler is properly created
99-
* The connection is established
100-
* The URI doesn't contain all needed parts
101-
* The WebSocketSession is closed
102-
*/
103-
@Test
104-
void givenInvalidURI_whenTheConnectionIsEstablished_thenTheSocketIsClosedAsNotAcceptable() throws Exception {
105-
WebSocketSession establishedSession = mock(WebSocketSession.class);
106-
when(establishedSession.isOpen()).thenReturn(true);
107-
when(establishedSession.getUri()).thenReturn(new URI("wss://gatewayHost:1443/invalidUrl"));
108-
109-
underTest.afterConnectionEstablished(establishedSession);
110-
111-
verify(establishedSession).close(new CloseStatus(CloseStatus.NOT_ACCEPTABLE.getCode(), "Invalid URL format"));
69+
@Nested
70+
class WhenTheConnectionIsEstablished {
71+
WebSocketSession establishedSession;
72+
73+
@BeforeEach
74+
void prepareSessionMock() {
75+
establishedSession = mock(WebSocketSession.class);
76+
}
77+
78+
@Nested
79+
class ThenTheValidSessionIsStoredInternally {
80+
@BeforeEach
81+
void prepareRoutedService() {
82+
String serviceId = "valid-service";
83+
84+
RoutedServices routesForSpecificValidService = mock(RoutedServices.class);
85+
when(routesForSpecificValidService.findServiceByGatewayUrl("ws/v1"))
86+
.thenReturn(new RoutedService("ws-v1", "ws/v1", "/valid-service/ws/v1"));
87+
ServiceInstance foundService = validServiceInstance();
88+
when(discoveryClient.getInstances(serviceId)).thenReturn(Collections.singletonList(foundService));
89+
90+
underTest.addRoutedServices(serviceId, routesForSpecificValidService);
91+
}
92+
93+
/**
94+
* Happy Path
95+
* <p>
96+
* The Handler is properly created
97+
* Specified Route is added to the list
98+
* The connection is established
99+
* The URI contains the valid service Id
100+
* The service associated with given URI is retrieved
101+
* Proper WebSocketSession is stored.
102+
*/
103+
@ParameterizedTest(name = "WhenTheConnectionIsEstablished.ThenTheValidSessionIsStoredInternally#givenValidRoute {0}")
104+
@ValueSource(strings = {"wss://gatewayHost:1443/valid-service/ws/v1/valid-path", "wss://gatewayHost:1443/ws/v1/valid-service/valid-path"})
105+
void givenValidRoute(String path) throws Exception {
106+
when(webSocketRoutedSessionFactory.session(any(), any(), any())).thenReturn(mock(WebSocketRoutedSession.class));
107+
108+
String establishedSessionId = "validAndUniqueId";
109+
when(establishedSession.getId()).thenReturn(establishedSessionId);
110+
when(establishedSession.getUri()).thenReturn(new URI(path));
111+
112+
underTest.afterConnectionEstablished(establishedSession);
113+
114+
verify(webSocketRoutedSessionFactory).session(any(), any(), any());
115+
WebSocketRoutedSession preparedSession = routedSessions.get(establishedSessionId);
116+
assertThat(preparedSession, is(notNullValue()));
117+
}
118+
}
119+
120+
121+
@Nested
122+
class ThenTheSocketIsClosed {
123+
@BeforeEach
124+
void sessionIsOpen() {
125+
when(establishedSession.isOpen()).thenReturn(true);
126+
}
127+
128+
/**
129+
* Error Path
130+
* <p>
131+
* The Handler is properly created
132+
* The connection is established
133+
* The URI doesn't contain all needed parts
134+
* The WebSocketSession is closed
135+
*/
136+
@Test
137+
void givenInvalidURI() throws Exception {
138+
when(establishedSession.getUri()).thenReturn(new URI("wss://gatewayHost:1443/invalidUrl"));
139+
140+
underTest.afterConnectionEstablished(establishedSession);
141+
142+
verify(establishedSession).close(new CloseStatus(CloseStatus.NOT_ACCEPTABLE.getCode(), "Invalid URL format"));
143+
}
144+
145+
/**
146+
* Error Path
147+
* <p>
148+
* The Handler is properly created
149+
* The connection is established
150+
* The URI contains the service Id for which there is no service
151+
* The WebSocketSession is closed
152+
*/
153+
@Test
154+
void givenInvalidRoute() throws Exception {
155+
when(establishedSession.getUri()).thenReturn(new URI("wss://gatewayHost:1443/ws/v1/non_existent_service/valid-path"));
156+
157+
underTest.afterConnectionEstablished(establishedSession);
158+
159+
verify(establishedSession).close(new CloseStatus(CloseStatus.NOT_ACCEPTABLE.getCode(), "Requested service non_existent_service is not known by the gateway"));
160+
}
161+
162+
/**
163+
* Error Path
164+
* <p>
165+
* The Handler is properly created
166+
* Specified Route is added to the list
167+
* The connection is established
168+
* The URI contains the valid service Id
169+
* The service associated with given URI is retrieved
170+
* The service isn't available in the Discovery Service
171+
* Proper WebSocketSession is stored.
172+
*/
173+
@Test
174+
void givenNoInstanceOfTheServiceIsInTheRepository() throws Exception {
175+
when(establishedSession.getUri()).thenReturn(new URI("wss://gatewayHost:1443/service-without-instance/ws/v1/valid-path"));
176+
177+
RoutedServices routesForSpecificValidService = mock(RoutedServices.class);
178+
when(routesForSpecificValidService.findServiceByGatewayUrl("ws/v1"))
179+
.thenReturn(new RoutedService("api-v1", "api/v1", "/api-v1/api/v1"));
180+
underTest.addRoutedServices("service-without-instance", routesForSpecificValidService);
181+
182+
underTest.afterConnectionEstablished(establishedSession);
183+
184+
verify(establishedSession).close(new CloseStatus(CloseStatus.SERVICE_RESTARTED.getCode(), "Requested service service-without-instance does not have available instance"));
185+
}
186+
}
112187
}
113188

114-
/**
115-
* Error Path
116-
* <p>
117-
* The Handler is properly created
118-
* The connection is established
119-
* The URI contains the service Id for which there is no service
120-
* The WebSocketSession is closed
121-
*/
122-
@Test
123-
void givenInvalidRoute_whenTheConnectionIsEstablished_thenTheSocketIsClosedAsNotAcceptable() throws Exception {
124-
WebSocketSession establishedSession = mock(WebSocketSession.class);
125-
when(establishedSession.isOpen()).thenReturn(true);
126-
when(establishedSession.getUri()).thenReturn(new URI("wss://gatewayHost:1443/api/v1/non_existent_service/api/v1"));
127-
128-
underTest.afterConnectionEstablished(establishedSession);
129-
130-
verify(establishedSession).close(new CloseStatus(CloseStatus.NOT_ACCEPTABLE.getCode(), "Requested service non_existent_service is not known by the gateway"));
131-
}
189+
@Nested
190+
class GivenValidExistingSession {
191+
WebSocketSession establishedSession;
192+
WebSocketRoutedSession internallyStoredSession;
132193

133-
/**
134-
* Error Path
135-
* <p>
136-
* The Handler is properly created
137-
* Specified Route is added to the list
138-
* The connection is established
139-
* The URI contains the valid service Id
140-
* The service associated with given URI is retrieved
141-
* The service isn't available in the Discovery Service
142-
* Proper WebSocketSession is stored.
143-
*/
144-
@Test
145-
void givenNoInstanceOfTheServiceIsInTheRepository_whenTheConnectionIsEstablished_thenTheSocketIsClosedAsServiceRestarted() throws Exception {
146-
WebSocketSession establishedSession = mock(WebSocketSession.class);
147-
when(establishedSession.isOpen()).thenReturn(true);
148-
when(establishedSession.getUri()).thenReturn(new URI("wss://gatewayHost:1443/api/v1/api-v1/api/v1"));
149-
RoutedServices routesForSpecificValidService = mock(RoutedServices.class);
150-
when(routesForSpecificValidService.findServiceByGatewayUrl("ws/v1"))
151-
.thenReturn(new RoutedService("api-v1", "api/v1", "/api-v1/api/v1"));
152-
underTest.addRoutedServices("api-v1", routesForSpecificValidService);
153-
154-
underTest.afterConnectionEstablished(establishedSession);
155-
156-
verify(establishedSession).close(new CloseStatus(CloseStatus.SERVICE_RESTARTED.getCode(), "Requested service api-v1 does not have available instance"));
157-
}
194+
@BeforeEach
195+
void prepareSessionMock() {
196+
establishedSession = mock(WebSocketSession.class);
197+
String validSessionId = "123";
198+
when(establishedSession.getId()).thenReturn(validSessionId);
158199

159-
@Test
160-
void givenValidSession_whenTheConnectionIsClosed_thenTheSessionIsClosedAndRemovedFromRepository() throws Exception {
161-
CloseStatus normalClose = CloseStatus.NORMAL;
162-
WebSocketSession establishedSession = mock(WebSocketSession.class);
163-
String validSessionId = "123";
164-
when(establishedSession.getId()).thenReturn(validSessionId);
165-
routedSessions.put(validSessionId, mock(WebSocketRoutedSession.class));
200+
internallyStoredSession = mock(WebSocketRoutedSession.class);
201+
routedSessions.put(validSessionId, internallyStoredSession);
202+
}
166203

167-
underTest.afterConnectionClosed(establishedSession, normalClose);
204+
@Test
205+
void whenTheConnectionIsClosed_thenTheSessionIsClosedAndRemovedFromRepository() throws Exception {
206+
CloseStatus normalClose = CloseStatus.NORMAL;
168207

169-
verify(establishedSession).close(normalClose);
170-
assertThat(routedSessions.entrySet(), hasSize(0));
171-
}
208+
underTest.afterConnectionClosed(establishedSession, normalClose);
209+
210+
verify(establishedSession).close(normalClose);
211+
assertThat(routedSessions.entrySet(), hasSize(0));
212+
}
213+
214+
@Test
215+
void whenTheMessageIsReceived_thenTheMessageIsPassedToTheSession() throws Exception {
216+
WebSocketMessage<String> passedMessage = mock(WebSocketMessage.class);
172217

173-
@Test
174-
void givenValidSession_whenTheMessageIsReceived_thenTheMessageIsPassedToTheSession() throws Exception {
175-
WebSocketSession establishedSession = mock(WebSocketSession.class);
176-
String validSessionId = "123";
177-
when(establishedSession.getId()).thenReturn(validSessionId);
178-
WebSocketRoutedSession internallyStoredSession = mock(WebSocketRoutedSession.class);
179-
routedSessions.put(validSessionId, internallyStoredSession);
180-
WebSocketMessage<String> passedMessage = mock(WebSocketMessage.class);
218+
underTest.handleMessage(establishedSession, passedMessage);
181219

182-
underTest.handleMessage(establishedSession, passedMessage);
220+
verify(internallyStoredSession).sendMessageToServer(passedMessage);
221+
}
183222

184-
verify(internallyStoredSession).sendMessageToServer(passedMessage);
185223
}
186224
}

0 commit comments

Comments
 (0)