Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions discoverable-client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies {
implementation libraries.spring_boot_starter_web
implementation libraries.spring_boot_starter_websocket
implementation libraries.spring_boot_starter_validation
implementation libraries.spring_boot_starter_security

implementation libraries.bootstrap
implementation libraries.jquery
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/
package org.zowe.apiml.client.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()

.authorizeRequests()
.antMatchers("/ws/**").authenticated()
.antMatchers("/**").permitAll()
.and().httpBasic();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password("{noop}pass").roles("ADMIN");
}

@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/api/**");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.UpgradeException;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHttpHeaders;
Expand All @@ -24,6 +26,7 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
Expand Down Expand Up @@ -86,11 +89,30 @@ private WebSocketSession createWebSocketClientSession(WebSocketSession webSocket
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw webSocketProxyException(targetUrl, e, webSocketServerSession, false);
} catch (ExecutionException e) {
throw handleExecutionException(targetUrl, e, webSocketServerSession, false);
} catch (Exception e) {
throw webSocketProxyException(targetUrl, e, webSocketServerSession, false);
}
}

private WebSocketProxyError handleExecutionException(String targetUrl, ExecutionException cause, WebSocketSession webSocketServerSession, boolean logError) {
if (cause.getCause() != null && cause.getCause().getCause() instanceof UpgradeException) {
UpgradeException upgradeException = (UpgradeException) cause.getCause().getCause();
if (upgradeException.getResponseStatusCode() == HttpStatus.UNAUTHORIZED.value()) {
String message = "Invalid login credentials";
if (logError) {
log.debug(message);
}
return new WebSocketProxyError(message, cause, webSocketServerSession);
} else {
return webSocketProxyException(targetUrl, cause, webSocketServerSession, logError);
}
} else {
return webSocketProxyException(targetUrl, cause, webSocketServerSession, logError);
}
}

private WebSocketProxyError webSocketProxyException(String targetUrl, Exception cause, WebSocketSession webSocketServerSession, boolean logError) {
String message = String.format("Error opening session to WebSocket service at %s: %s", targetUrl, cause.getMessage());
if (logError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
package org.zowe.apiml.integration.proxy;

import org.apache.http.client.utils.URIBuilder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
Expand All @@ -27,6 +29,7 @@

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

Expand All @@ -39,9 +42,26 @@
class WebSocketProxyTest implements TestWithStartedInstances {
private final GatewayServiceConfiguration serviceConfiguration = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration();

private final static int WAIT_TIMEOUT_MS = 10000;
private final static String UPPERCASE_URL = "/ws/v1/discoverableclient/uppercase";
private final static String HEADER_URL = "/ws/v1/discoverableclient/header";
private static final int WAIT_TIMEOUT_MS = 10000;
private static final String UPPERCASE_URL = "/ws/v1/discoverableclient/uppercase";
private static final String HEADER_URL = "/ws/v1/discoverableclient/header";

private static final WebSocketHttpHeaders VALID_AUTH_HEADERS = new WebSocketHttpHeaders();
private static final WebSocketHttpHeaders INVALID_AUTH_HEADERS = new WebSocketHttpHeaders();
private static final String validToken = "apimlAuthenticationToken=tokenValue";


@BeforeAll
static void setup() {
String plainCred = "user:pass";
String base64cred = Base64.getEncoder().encodeToString(plainCred.getBytes());
VALID_AUTH_HEADERS.add("Authorization", "Basic " + base64cred);

String invalidPlainCred = "user:invalidPass";
String invalidBase64cred = Base64.getEncoder().encodeToString(invalidPlainCred.getBytes());
INVALID_AUTH_HEADERS.add("Authorization", "Basic " + invalidBase64cred);

}

private TextWebSocketHandler appendResponseHandler(StringBuilder target, int countToNotify) {
final AtomicInteger counter = new AtomicInteger(countToNotify);
Expand Down Expand Up @@ -89,86 +109,127 @@ private WebSocketSession appendingWebSocketSession(String url, StringBuilder res
return appendingWebSocketSession(url, null, response, countToNotify);
}

@Test
void shouldRouteWebSocketSession() throws Exception {
final StringBuilder response = new StringBuilder();
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), response, 1);

session.sendMessage(new TextMessage("hello world!"));
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}
@Nested
class WhenRoutingSession {
@Nested
class Authentication {
@Nested
class WhenValid {
@Nested
class ReturnSuccess {
@Test
void message() throws Exception {
final StringBuilder response = new StringBuilder();

assertEquals("HELLO WORLD!", response.toString());
session.close();
}
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), VALID_AUTH_HEADERS, response, 1);

@Test
void shouldRouteHeaders() throws Exception {
final StringBuilder response = new StringBuilder();
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
headers.add("X-Test", "value");
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(HEADER_URL), headers, response, 1);
session.sendMessage(new TextMessage("hello world!"));
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}

session.sendMessage(new TextMessage("gimme those headers"));
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}
assertEquals("HELLO WORLD!", response.toString());
session.close();
}

assertTrue(response.toString().contains("x-test:\"value\""));
session.sendMessage(new TextMessage("bye"));
session.close();
}
@Test
void headers() throws Exception {
final StringBuilder response = new StringBuilder();
VALID_AUTH_HEADERS.add("X-Test", "value");
VALID_AUTH_HEADERS.add("Cookie", validToken);
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(HEADER_URL), VALID_AUTH_HEADERS, response, 1);

session.sendMessage(new TextMessage("gimme those headers"));
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}

assertTrue(response.toString().contains("x-test:\"value\""));
assertTrue(response.toString().contains(validToken));
session.sendMessage(new TextMessage("bye"));
session.close();
}
}

@Test
void shouldCloseSessionAfterClientServerCloses() throws Exception {
final StringBuilder response = new StringBuilder();
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), response, 2);
@Nested
class ReturnError {
@Test
void whenPathIsNotCorrect() throws Exception {
final StringBuilder response = new StringBuilder();
appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL + "bad"), VALID_AUTH_HEADERS, response, 1);

session.sendMessage(new TextMessage("bye"));
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}

assertEquals("BYECloseStatus[code=1000, reason=null]", response.toString());
}
System.out.println("Response: " + response.toString());
assertEquals(0, response.toString().indexOf("CloseStatus[code=1003,"));
}

@Test
void shouldFailIfPathIsNotCorrect() throws Exception {
final StringBuilder response = new StringBuilder();
appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL + "bad"), response, 1);
@Test
void whenServiceIsNotCorrect() throws Exception {
final StringBuilder response = new StringBuilder();
appendingWebSocketSession(discoverableClientGatewayUrl("/ws/v1/wrong-service/uppercase"), VALID_AUTH_HEADERS, response, 1);

synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}

System.out.println("Response: " + response.toString());
assertEquals(0, response.toString().indexOf("CloseStatus[code=1003,"));
}
assertEquals("CloseStatus[code=1003, reason=Requested service wrong-service is not known by the gateway]",
response.toString());
}

@Test
void whenUrlFormatIsNotCorrect() throws Exception {
final StringBuilder response = new StringBuilder();
appendingWebSocketSession(discoverableClientGatewayUrl("/ws/wrong"), response, 1);

synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}

assertEquals("CloseStatus[code=1003, reason=Invalid URL format]", response.toString());
}
}
}

@Nested
class WhenInvalid {
@Test
void returnError() throws Exception {
final StringBuilder response = new StringBuilder();

@Test
void shouldFailIfServiceIsNotCorrect() throws Exception {
final StringBuilder response = new StringBuilder();
appendingWebSocketSession(discoverableClientGatewayUrl("/ws/v1/wrong-service/uppercase"), response, 1);
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), INVALID_AUTH_HEADERS, response, 1);

synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
session.sendMessage(new TextMessage("hello world!"));
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}

assertEquals("CloseStatus[code=1003, reason=Invalid login credentials]", response.toString());
session.close();
}
}
}

assertEquals("CloseStatus[code=1003, reason=Requested service wrong-service is not known by the gateway]",
response.toString());
}

@Test
void shouldFailIfUrlFormatIsNotCorrect() throws Exception {
final StringBuilder response = new StringBuilder();
appendingWebSocketSession(discoverableClientGatewayUrl("/ws/wrong"), response, 1);
@Nested
class WhenClosingSession {
@Test
void getCorrectResponse() throws Exception {
final StringBuilder response = new StringBuilder();
WebSocketSession session = appendingWebSocketSession(discoverableClientGatewayUrl(UPPERCASE_URL), VALID_AUTH_HEADERS, response, 2);

synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}
session.sendMessage(new TextMessage("bye"));
synchronized (response) {
response.wait(WAIT_TIMEOUT_MS);
}

assertEquals("CloseStatus[code=1003, reason=Invalid URL format]", response.toString());
assertEquals("BYECloseStatus[code=1000, reason=null]", response.toString());
}
}


}