KNOX-1996: Adding changes to remove extra / while generating backedn … (#142)

* KNOX-1996: Adding changes to remove extra / while generating backedn url. Additionally, proxy messages to websocket even if service uri has http/https protocol

* KNOX-1996: Code review fixes

* KNOX-1996: More code review fixes. Creating base class for WebsocketEcho and WebsocketEcho1 tests

* KNOX-1996: Code review fixes - Renaming test case and adding new test case for backedn url validation

* KNOX-1996: Code review fixes in test case
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
index 138a3fe..7d8ce36 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/GatewayWebsocketHandler.java
@@ -114,7 +114,8 @@
       final String path = requestURI.getPath();
 
       /* URL used to connect to websocket backend */
-      final String backendURL = getMatchedBackendURL(path);
+      final String backendURL = getMatchedBackendURL(path, requestURI);
+      LOG.debugLog("Generated backend URL for websocket connection: " + backendURL);
 
       /* Upgrade happens here */
       return new ProxyWebSocketAdapter(URI.create(backendURL), pool, getClientEndpointConfig(req));
@@ -149,7 +150,7 @@
    * ws://{host}:{port} which might or might not be right.
    * @return Websocket backend url
    */
-  private synchronized String getMatchedBackendURL(final String path) {
+  protected synchronized String getMatchedBackendURL(final String path, URI requestURI) {
 
     final ServiceRegistry serviceRegistryService = services
         .getService(ServiceType.SERVICE_REGISTRY_SERVICE);
@@ -173,12 +174,28 @@
 
     /* URL used to connect to websocket backend */
     String backendURL = urlFromServiceDefinition(serviceRegistryService, entry, path);
+    LOG.debugLog("Url obtained from services definition: " + backendURL);
 
     StringBuilder backend = new StringBuilder();
     try {
-
-      /* if we do not find websocket URL we default to HTTP */
-      if (!StringUtils.containsAny(backendURL, WEBSOCKET_PROTOCOL_STRING, SECURE_WEBSOCKET_PROTOCOL_STRING)) {
+      if (StringUtils.containsAny(backendURL, WEBSOCKET_PROTOCOL_STRING, SECURE_WEBSOCKET_PROTOCOL_STRING)) {
+        LOG.debugLog("ws or wss protocol found in service url");
+        URI serviceUri = new URI(backendURL);
+        backend.append(serviceUri);
+        String pathSuffix = generateUrlSuffix(backend.toString(), pathService);
+        backend.append(pathSuffix);
+      } else if (StringUtils.containsAny(requestURI.toString(), WEBSOCKET_PROTOCOL_STRING, SECURE_WEBSOCKET_PROTOCOL_STRING)) {
+        LOG.debugLog("ws or wss protocol found in request url");
+        URL serviceUrl = new URL(backendURL);
+        final String protocol = (serviceUrl.getProtocol().equals("https")) ? "wss" : "ws";
+        backend.append(protocol).append("://");
+        backend.append(serviceUrl.getHost()).append(':');
+        backend.append(serviceUrl.getPort()).append('/');
+        backend.append(serviceUrl.getPath());
+        String pathSuffix = generateUrlSuffix(backend.toString(), pathService);
+        backend.append(pathSuffix);
+      } else {
+        LOG.debugLog("ws or wss protocol not found in service url or request url");
         URL serviceUrl = new URL(backendURL);
 
         /* Use http host:port if ws url not configured */
@@ -190,16 +207,7 @@
         backend.append(serviceUrl.getPort()).append('/');
         backend.append(serviceUrl.getPath());
       }
-      else {
-        URI serviceUri = new URI(backendURL);
-        backend.append(serviceUri);
-        /* Avoid Zeppelin Regression - as this would require ambari changes and break current knox websocket use case*/
-        if (!StringUtils.endsWith(backend.toString(), "/ws") && pathService.length > 0 && pathService[1] != null) {
-          backend.append(pathService[1]);
-        }
-      }
       backendURL = backend.toString();
-
     } catch (MalformedURLException e){
         LOG.badUrlError(e);
         throw new RuntimeException(e.toString());
@@ -224,4 +232,17 @@
     return serviceRegistry.lookupServiceURL(contexts[2],
         entry.getName().toUpperCase(Locale.ROOT));
   }
+
+  private String generateUrlSuffix(String backendPart, String[] pathService) {
+    /* Avoid Zeppelin Regression - as this would require ambari changes and break current knox websocket use case*/
+    if (!StringUtils.endsWith(backendPart, "/ws") && pathService.length > 0
+              &&  pathService[1] != null) {
+      String newPathSuffix = pathService[1];
+      if ((backendPart.endsWith("/")) && (pathService[1].startsWith("/"))) {
+        newPathSuffix = pathService[1].substring(1);
+      }
+      return newPathSuffix;
+    }
+    return "";
+  }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/WebsocketLogMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/WebsocketLogMessages.java
index 9681d9d..fc8484e 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/websockets/WebsocketLogMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/websockets/WebsocketLogMessages.java
@@ -56,4 +56,8 @@
       text = "Websocket connection to backend server {0} closed")
   void onConnectionClose(String backend);
 
+  @Message(level = MessageLevel.DEBUG,
+      text = "{0}")
+  void debugLog(String message);
+
 }
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java
new file mode 100644
index 0000000..492f07e
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketBackendUrlTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.knox.gateway.websockets;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.Locale;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * A basic test that attempts to test the backend url generated by gateway proxy.
+ */
+public class WebsocketBackendUrlTest extends WebsocketEchoTestBase {
+  private static Server backendServer;
+  public static URI backendServerUri;
+
+  public WebsocketBackendUrlTest() {
+    super();
+  }
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+    WebsocketEchoTestBase.setUpBeforeClass();
+
+    backendServer = new Server();
+    ServerConnector connector = new ServerConnector(backendServer);
+    backendServer.addConnector(connector);
+
+    String host = connector.getHost();
+    if (host == null) {
+      host = "localhost";
+    }
+    int port = connector.getLocalPort();
+    serverUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/", host, port));
+    backendServerUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/testpart/", host, port));
+    WebsocketEchoTestBase.setupGatewayConfig(backendServerUri.toString());
+  }
+
+  @AfterClass
+  public static void tearDownAfterClass() {
+    WebsocketEchoTestBase.cleanupFiles();
+  }
+
+  /*
+   * Test url generated for websocket backend connection
+   */
+  @Test
+  public void testWebsocketBackendUrl() throws Exception {
+    URI requestURI = new URI(serverUri.toString() + "gateway/websocket/123foo456bar/channels");
+    final String path = requestURI.getPath();
+    GatewayWebsocketHandler gwh = new GatewayWebsocketHandler(gatewayConfig, services);
+    String backendUrl = gwh.getMatchedBackendURL(path, requestURI);
+    String expectedBackendUrl = backendServerUri.toString() + "channels";
+    assertThat(backendUrl, is(expectedBackendUrl));
+  }
+}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHTTPServiceRoleTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHTTPServiceRoleTest.java
new file mode 100644
index 0000000..e66015a
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoHTTPServiceRoleTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.knox.gateway.websockets;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.websocket.ContainerProvider;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * A basic test that attempts to proxy websocket connections through Knox
+ * gateway.
+ * <p>
+ * The way the test is set up is as follows: <br>
+ * <ul>
+ * <li>A Mock Websocket server is setup which simply echos the responses sent by
+ * client.
+ * <li>Knox Gateway is set up with websocket handler
+ * {@link GatewayWebsocketHandler} that can proxy the requests.
+ * <li>Appropriate Topology and service definition files are set up with the
+ * address of the Websocket server.
+ * <li>A mock client is setup to connect to gateway.
+ * </ul>
+ *
+ * The test is to confirm whether the message is sent all the way to the backend
+ * Websocket server through Knox and back when backend service URI has http
+ * protocol defined in service definition
+ *
+ * @since 0.10
+ */
+public class WebsocketEchoHTTPServiceRoleTest extends WebsocketEchoTestBase {
+
+  public WebsocketEchoHTTPServiceRoleTest() {
+    super();
+  }
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+    WebsocketEchoTestBase.setUpBeforeClass();
+    WebsocketEchoTestBase.startServers("http");
+  }
+
+  @AfterClass
+  public static void tearDownAfterClass() {
+    WebsocketEchoTestBase.tearDownAfterClass();
+  }
+
+  /*
+   * Test websocket rewrite rules proxying through gateway.
+   */
+  @Test
+  public void testGatewayRewriteHttpEcho() throws Exception {
+    WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+
+    WebsocketClient client = new WebsocketClient();
+    Session session = container.connectToServer(client,
+            new URI(serverUri.toString() + "gateway/websocket/123foo456bar/channels"));
+
+    session.getBasicRemote().sendText("Echo");
+    client.messageQueue.awaitMessages(1, 1000, TimeUnit.MILLISECONDS);
+
+    assertThat(client.messageQueue.get(0), is("Echo"));
+  }
+}
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTest.java
index 58c09c9..7a08b9e 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTest.java
@@ -17,25 +17,6 @@
  */
 package org.apache.knox.gateway.websockets;
 
-import com.mycila.xmltool.XMLDoc;
-import com.mycila.xmltool.XMLTag;
-import org.apache.commons.io.FileUtils;
-import org.apache.knox.gateway.config.GatewayConfig;
-import org.apache.knox.gateway.config.impl.GatewayConfigImpl;
-import org.apache.knox.gateway.deploy.DeploymentFactory;
-import org.apache.knox.gateway.services.DefaultGatewayServices;
-import org.apache.knox.gateway.services.ServiceType;
-import org.apache.knox.gateway.services.GatewayServices;
-import org.apache.knox.gateway.services.ServiceLifecycleException;
-import org.apache.knox.gateway.services.topology.TopologyService;
-import org.apache.knox.gateway.topology.TopologyEvent;
-import org.apache.knox.gateway.topology.TopologyListener;
-import org.apache.knox.test.TestUtils;
-import org.easymock.EasyMock;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.server.handler.ContextHandler;
-import org.eclipse.jetty.server.handler.HandlerCollection;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -43,28 +24,9 @@
 import javax.websocket.ContainerProvider;
 import javax.websocket.Session;
 import javax.websocket.WebSocketContainer;
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
 import java.net.URI;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
-import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_SIGNING_KEYSTORE_PASSWORD_ALIAS;
-import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_SIGNING_KEYSTORE_TYPE;
-import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_SIGNING_KEY_PASSPHRASE_ALIAS;
-import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_IDENTITY_KEYSTORE_PASSWORD_ALIAS;
-import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_IDENTITY_KEYSTORE_TYPE;
-import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_IDENTITY_KEY_PASSPHRASE_ALIAS;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 
@@ -88,40 +50,7 @@
  *
  * @since 0.10
  */
-public class WebsocketEchoTest {
-  private static final String TEST_KEY_ALIAS = "test-identity";
-
-  /**
-   * Simulate backend websocket
-   */
-  private static Server backendServer;
-  /**
-   * URI for backend websocket server
-   */
-  private static URI backendServerUri;
-
-  /**
-   * Mock Gateway server
-   */
-  private static Server gatewayServer;
-
-  /**
-   * Mock gateway config
-   */
-  private static GatewayConfig gatewayConfig;
-
-  private static GatewayServices services;
-
-  /**
-   * URI for gateway server
-   */
-  private static URI serverUri;
-
-  private static File topoDir;
-  private static Path dataDir;
-  private static Path securityDir;
-  private static Path keystoresDir;
-  private static Path keystoreFile;
+public class WebsocketEchoTest extends WebsocketEchoTestBase {
 
   public WebsocketEchoTest() {
     super();
@@ -129,27 +58,13 @@
 
   @BeforeClass
   public static void setUpBeforeClass() throws Exception {
-    topoDir = createDir();
-    dataDir = Paths.get(topoDir.getAbsolutePath(), "data").toAbsolutePath();
-    securityDir = dataDir.resolve("security");
-    keystoresDir = securityDir.resolve("keystores");
-    keystoreFile = keystoresDir.resolve("tls.jks");
-
-    startWebsocketServer();
-    startGatewayServer();
+    WebsocketEchoTestBase.setUpBeforeClass();
+    WebsocketEchoTestBase.startServers("ws");
   }
 
   @AfterClass
   public static void tearDownAfterClass() {
-    try {
-      gatewayServer.stop();
-      backendServer.stop();
-    } catch (final Exception e) {
-      e.printStackTrace(System.err);
-    }
-
-    /* Cleanup the created files */
-    FileUtils.deleteQuietly(topoDir);
+    WebsocketEchoTestBase.tearDownAfterClass();
   }
 
   /*
@@ -200,242 +115,4 @@
 
     assertThat(client.messageQueue.get(0), is("Echo"));
   }
-
-  /**
-   * Start Mock Websocket server that acts as backend.
-   * @throws Exception exception on websocket server start
-   */
-  private static void startWebsocketServer() throws Exception {
-
-    backendServer = new Server();
-    ServerConnector connector = new ServerConnector(backendServer);
-    backendServer.addConnector(connector);
-
-    final WebsocketEchoHandler handler = new WebsocketEchoHandler();
-
-    ContextHandler context = new ContextHandler();
-    context.setContextPath("/");
-    context.setHandler(handler);
-    backendServer.setHandler(context);
-
-    // Start Server
-    backendServer.start();
-
-    String host = connector.getHost();
-    if (host == null) {
-      host = "localhost";
-    }
-    int port = connector.getLocalPort();
-    backendServerUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/ws", host, port));
-  }
-
-  /**
-   * Start Gateway Server.
-   * @throws Exception exception on server start
-   */
-  private static void startGatewayServer() throws Exception {
-    gatewayServer = new Server();
-    final ServerConnector connector = new ServerConnector(gatewayServer);
-    gatewayServer.addConnector(connector);
-
-    /* workaround so we can add our handler later at runtime */
-    HandlerCollection handlers = new HandlerCollection(true);
-
-    /* add some initial handlers */
-    ContextHandler context = new ContextHandler();
-    context.setContextPath("/");
-    handlers.addHandler(context);
-
-    gatewayServer.setHandler(handlers);
-
-    // Start Server
-    gatewayServer.start();
-
-    String host = connector.getHost();
-    if (host == null) {
-      host = "localhost";
-    }
-    int port = connector.getLocalPort();
-    serverUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/", host, port));
-
-    /* Setup websocket handler */
-    setupGatewayConfig(backendServerUri.toString());
-
-    final GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(
-        gatewayConfig, services);
-    handlers.addHandler(gatewayWebsocketHandler);
-    gatewayWebsocketHandler.start();
-  }
-
-  /**
-   * Initialize the configs and components required for this test.
-   * @param backend topology to use
-   * @throws IOException exception on setting up the gateway
-   */
-  private static void setupGatewayConfig(final String backend) throws IOException {
-    services = new DefaultGatewayServices();
-
-    URL serviceUrl = ClassLoader.getSystemResource("websocket-services");
-
-    final File descriptor = new File(topoDir, "websocket.xml");
-    try(OutputStream stream = Files.newOutputStream(descriptor.toPath())) {
-      createKnoxTopology(backend).toStream(stream);
-    }
-
-    final TestTopologyListener topoListener = new TestTopologyListener();
-
-    final Map<String, String> options = new HashMap<>();
-    options.put("persist-master", "false");
-    options.put("master", "password");
-
-    gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
-    EasyMock.expect(gatewayConfig.getGatewayTopologyDir())
-        .andReturn(topoDir.toString()).anyTimes();
-
-    EasyMock.expect(gatewayConfig.getGatewayProvidersConfigDir())
-            .andReturn(topoDir.getAbsolutePath() + "/shared-providers").anyTimes();
-
-    EasyMock.expect(gatewayConfig.getGatewayDescriptorsDir())
-            .andReturn(topoDir.getAbsolutePath() + "/descriptors").anyTimes();
-
-    EasyMock.expect(gatewayConfig.getGatewayServicesDir())
-        .andReturn(serviceUrl.getFile()).anyTimes();
-
-    EasyMock.expect(gatewayConfig.getEphemeralDHKeySize()).andReturn("2048")
-        .anyTimes();
-
-    /* Websocket configs */
-    EasyMock.expect(gatewayConfig.isWebsocketEnabled()).andReturn(true)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getWebsocketMaxTextMessageSize())
-        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_MAX_TEXT_MESSAGE_SIZE)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getWebsocketMaxBinaryMessageSize())
-        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_MAX_BINARY_MESSAGE_SIZE)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getWebsocketMaxTextMessageBufferSize())
-        .andReturn(
-            GatewayConfigImpl.DEFAULT_WEBSOCKET_MAX_TEXT_MESSAGE_BUFFER_SIZE)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getWebsocketMaxBinaryMessageBufferSize())
-        .andReturn(
-            GatewayConfigImpl.DEFAULT_WEBSOCKET_MAX_BINARY_MESSAGE_BUFFER_SIZE)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getWebsocketInputBufferSize())
-        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_INPUT_BUFFER_SIZE)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getWebsocketAsyncWriteTimeout())
-        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_ASYNC_WRITE_TIMEOUT)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getWebsocketIdleTimeout())
-        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_IDLE_TIMEOUT).anyTimes();
-
-    EasyMock.expect(gatewayConfig.getRemoteRegistryConfigurationNames())
-            .andReturn(Collections.emptyList())
-            .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getGatewayDataDir())
-        .andReturn(dataDir.toString())
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getGatewaySecurityDir())
-        .andReturn(securityDir.toString())
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getGatewayKeystoreDir())
-        .andReturn(keystoresDir.toString())
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getIdentityKeystorePath())
-        .andReturn(keystoreFile.toString())
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getIdentityKeystoreType())
-        .andReturn(DEFAULT_IDENTITY_KEYSTORE_TYPE)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getIdentityKeystorePasswordAlias())
-        .andReturn(DEFAULT_IDENTITY_KEYSTORE_PASSWORD_ALIAS)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getIdentityKeyAlias())
-        .andReturn(TEST_KEY_ALIAS)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getIdentityKeyPassphraseAlias())
-        .andReturn(DEFAULT_IDENTITY_KEY_PASSPHRASE_ALIAS)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getSigningKeystorePasswordAlias())
-        .andReturn(DEFAULT_SIGNING_KEYSTORE_PASSWORD_ALIAS)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getSigningKeyPassphraseAlias())
-        .andReturn(DEFAULT_SIGNING_KEY_PASSPHRASE_ALIAS)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getSigningKeystorePath())
-        .andReturn(keystoreFile.toString())
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getSigningKeystoreType())
-        .andReturn(DEFAULT_SIGNING_KEYSTORE_TYPE)
-        .anyTimes();
-
-    EasyMock.expect(gatewayConfig.getSigningKeyAlias())
-        .andReturn(TEST_KEY_ALIAS)
-        .anyTimes();
-
-
-    EasyMock.replay(gatewayConfig);
-
-    try {
-      services.init(gatewayConfig, options);
-    } catch (ServiceLifecycleException e) {
-      e.printStackTrace();
-    }
-
-    DeploymentFactory.setGatewayServices(services);
-    final TopologyService monitor = services
-        .getService(ServiceType.TOPOLOGY_SERVICE);
-    monitor.addTopologyChangeListener(topoListener);
-    monitor.reloadTopologies();
-  }
-
-  private static File createDir() throws IOException {
-    return TestUtils
-        .createTempDir(WebsocketEchoTest.class.getSimpleName() + "-");
-  }
-
-  private static XMLTag createKnoxTopology(final String backend) {
-    return XMLDoc.newDocument(true).addRoot("topology").addTag("service")
-        .addTag("role").addText("WEBSOCKET").addTag("url").addText(backend)
-        .gotoParent().gotoRoot();
-  }
-
-  private static class TestTopologyListener implements TopologyListener {
-    public List<List<TopologyEvent>> events = new ArrayList<>();
-
-    @Override
-    public void handleTopologyEvent(List<TopologyEvent> events) {
-      this.events.add(events);
-
-      synchronized (this) {
-        for (TopologyEvent event : events) {
-          if (!event.getType().equals(TopologyEvent.Type.DELETED)) {
-            /* for this test we only care about this part */
-            DeploymentFactory.createDeployment(gatewayConfig,
-                event.getTopology());
-          }
-        }
-      }
-    }
-  }
 }
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTestBase.java b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTestBase.java
new file mode 100644
index 0000000..a62d330
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/websockets/WebsocketEchoTestBase.java
@@ -0,0 +1,376 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.knox.gateway.websockets;
+
+import com.mycila.xmltool.XMLDoc;
+import com.mycila.xmltool.XMLTag;
+import org.apache.commons.io.FileUtils;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.config.impl.GatewayConfigImpl;
+import org.apache.knox.gateway.deploy.DeploymentFactory;
+import org.apache.knox.gateway.services.DefaultGatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.topology.TopologyService;
+import org.apache.knox.gateway.topology.TopologyEvent;
+import org.apache.knox.gateway.topology.TopologyListener;
+import org.apache.knox.test.TestUtils;
+import org.easymock.EasyMock;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_SIGNING_KEYSTORE_PASSWORD_ALIAS;
+import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_SIGNING_KEYSTORE_TYPE;
+import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_SIGNING_KEY_PASSPHRASE_ALIAS;
+import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_IDENTITY_KEYSTORE_PASSWORD_ALIAS;
+import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_IDENTITY_KEYSTORE_TYPE;
+import static org.apache.knox.gateway.config.GatewayConfig.DEFAULT_IDENTITY_KEY_PASSPHRASE_ALIAS;
+
+/**
+ * Base class for tests that attempt to proxy websocket connections through Knox
+ * gateway. It setups a websocket socket connection that simply echoes data back.
+ *
+ */
+public class WebsocketEchoTestBase {
+  private static final String TEST_KEY_ALIAS = "test-identity";
+
+  /**
+   * Simulate backend websocket
+   */
+  private static Server backendServer;
+  /**
+   * URI for backend websocket server
+   */
+  public static URI backendServerUri;
+
+  /**
+   * Mock Gateway server
+   */
+  private static Server gatewayServer;
+
+  /**
+   * Mock gateway config
+   */
+  public static GatewayConfig gatewayConfig;
+
+  public static GatewayServices services;
+
+  /**
+   * URI for gateway server
+   */
+  public static URI serverUri;
+
+  private static File topoDir;
+  private static Path dataDir;
+  private static Path securityDir;
+  private static Path keystoresDir;
+  private static Path keystoreFile;
+
+  public WebsocketEchoTestBase() {
+    super();
+  }
+
+  public static void setUpBeforeClass() throws Exception {
+    topoDir = createDir();
+    dataDir = Paths.get(topoDir.getAbsolutePath(), "data").toAbsolutePath();
+    securityDir = dataDir.resolve("security");
+    keystoresDir = securityDir.resolve("keystores");
+    keystoreFile = keystoresDir.resolve("tls.jks");
+  }
+
+  public static void startServers(String type) throws Exception {
+    startWebsocketServer(type);
+    startGatewayServer();
+  }
+
+  public static void tearDownAfterClass() {
+    try {
+      gatewayServer.stop();
+      backendServer.stop();
+    } catch (final Exception e) {
+      e.printStackTrace(System.err);
+    }
+
+    cleanupFiles();
+  }
+
+  public static void cleanupFiles() {
+    /* Cleanup the created files */
+    FileUtils.deleteQuietly(topoDir);
+  }
+
+  /**
+   * Start Mock Websocket server that acts as backend.
+   * @throws Exception exception on websocket server start
+   */
+  private static void startWebsocketServer(String type) throws Exception {
+
+    backendServer = new Server();
+    ServerConnector connector = new ServerConnector(backendServer);
+    backendServer.addConnector(connector);
+
+    final WebsocketEchoHandler handler = new WebsocketEchoHandler();
+
+    ContextHandler context = new ContextHandler();
+    context.setContextPath("/");
+    context.setHandler(handler);
+    backendServer.setHandler(context);
+
+    // Start Server
+    backendServer.start();
+
+    String host = connector.getHost();
+    if (host == null) {
+      host = "localhost";
+    }
+    int port = connector.getLocalPort();
+    if ("http".equals(type)) {
+      backendServerUri = new URI(String.format(Locale.ROOT, "http://%s:%d/ws", host, port));
+    } else {
+      backendServerUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/ws", host, port));
+    }
+  }
+
+  /**
+   * Start Gateway Server.
+   * @throws Exception exception on server start
+   */
+  private static void startGatewayServer() throws Exception {
+    gatewayServer = new Server();
+    final ServerConnector connector = new ServerConnector(gatewayServer);
+    gatewayServer.addConnector(connector);
+
+    /* workaround so we can add our handler later at runtime */
+    HandlerCollection handlers = new HandlerCollection(true);
+
+    /* add some initial handlers */
+    ContextHandler context = new ContextHandler();
+    context.setContextPath("/");
+    handlers.addHandler(context);
+
+    gatewayServer.setHandler(handlers);
+
+    // Start Server
+    gatewayServer.start();
+
+    String host = connector.getHost();
+    if (host == null) {
+      host = "localhost";
+    }
+    int port = connector.getLocalPort();
+    serverUri = new URI(String.format(Locale.ROOT, "ws://%s:%d/", host, port));
+
+    /* Setup websocket handler */
+    setupGatewayConfig(backendServerUri.toString());
+
+    final GatewayWebsocketHandler gatewayWebsocketHandler = new GatewayWebsocketHandler(
+        gatewayConfig, services);
+    handlers.addHandler(gatewayWebsocketHandler);
+    gatewayWebsocketHandler.start();
+  }
+
+  /**
+   * Initialize the configs and components required for this test.
+   * @param backend topology to use
+   * @throws IOException exception on setting up the gateway
+   */
+  public static void setupGatewayConfig(final String backend) throws IOException {
+    services = new DefaultGatewayServices();
+
+    URL serviceUrl = ClassLoader.getSystemResource("websocket-services");
+
+    final File descriptor = new File(topoDir, "websocket.xml");
+    try(OutputStream stream = Files.newOutputStream(descriptor.toPath())) {
+      createKnoxTopology(backend).toStream(stream);
+    }
+
+    final TestTopologyListener topoListener = new TestTopologyListener();
+
+    final Map<String, String> options = new HashMap<>();
+    options.put("persist-master", "false");
+    options.put("master", "password");
+
+    gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+    EasyMock.expect(gatewayConfig.getGatewayTopologyDir())
+        .andReturn(topoDir.toString()).anyTimes();
+
+    EasyMock.expect(gatewayConfig.getGatewayProvidersConfigDir())
+            .andReturn(topoDir.getAbsolutePath() + "/shared-providers").anyTimes();
+
+    EasyMock.expect(gatewayConfig.getGatewayDescriptorsDir())
+            .andReturn(topoDir.getAbsolutePath() + "/descriptors").anyTimes();
+
+    EasyMock.expect(gatewayConfig.getGatewayServicesDir())
+        .andReturn(serviceUrl.getFile()).anyTimes();
+
+    EasyMock.expect(gatewayConfig.getEphemeralDHKeySize()).andReturn("2048")
+        .anyTimes();
+
+    /* Websocket configs */
+    EasyMock.expect(gatewayConfig.isWebsocketEnabled()).andReturn(true)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getWebsocketMaxTextMessageSize())
+        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_MAX_TEXT_MESSAGE_SIZE)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getWebsocketMaxBinaryMessageSize())
+        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_MAX_BINARY_MESSAGE_SIZE)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getWebsocketMaxTextMessageBufferSize())
+        .andReturn(
+            GatewayConfigImpl.DEFAULT_WEBSOCKET_MAX_TEXT_MESSAGE_BUFFER_SIZE)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getWebsocketMaxBinaryMessageBufferSize())
+        .andReturn(
+            GatewayConfigImpl.DEFAULT_WEBSOCKET_MAX_BINARY_MESSAGE_BUFFER_SIZE)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getWebsocketInputBufferSize())
+        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_INPUT_BUFFER_SIZE)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getWebsocketAsyncWriteTimeout())
+        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_ASYNC_WRITE_TIMEOUT)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getWebsocketIdleTimeout())
+        .andReturn(GatewayConfigImpl.DEFAULT_WEBSOCKET_IDLE_TIMEOUT).anyTimes();
+
+    EasyMock.expect(gatewayConfig.getRemoteRegistryConfigurationNames())
+            .andReturn(Collections.emptyList())
+            .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getGatewayDataDir())
+        .andReturn(dataDir.toString())
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getGatewaySecurityDir())
+        .andReturn(securityDir.toString())
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getGatewayKeystoreDir())
+        .andReturn(keystoresDir.toString())
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getIdentityKeystorePath())
+        .andReturn(keystoreFile.toString())
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getIdentityKeystoreType())
+        .andReturn(DEFAULT_IDENTITY_KEYSTORE_TYPE)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getIdentityKeystorePasswordAlias())
+        .andReturn(DEFAULT_IDENTITY_KEYSTORE_PASSWORD_ALIAS)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getIdentityKeyAlias())
+        .andReturn(TEST_KEY_ALIAS)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getIdentityKeyPassphraseAlias())
+        .andReturn(DEFAULT_IDENTITY_KEY_PASSPHRASE_ALIAS)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getSigningKeystorePasswordAlias())
+        .andReturn(DEFAULT_SIGNING_KEYSTORE_PASSWORD_ALIAS)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getSigningKeyPassphraseAlias())
+        .andReturn(DEFAULT_SIGNING_KEY_PASSPHRASE_ALIAS)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getSigningKeystorePath())
+        .andReturn(keystoreFile.toString())
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getSigningKeystoreType())
+        .andReturn(DEFAULT_SIGNING_KEYSTORE_TYPE)
+        .anyTimes();
+
+    EasyMock.expect(gatewayConfig.getSigningKeyAlias())
+        .andReturn(TEST_KEY_ALIAS)
+        .anyTimes();
+
+
+    EasyMock.replay(gatewayConfig);
+
+    try {
+      services.init(gatewayConfig, options);
+    } catch (ServiceLifecycleException e) {
+      e.printStackTrace();
+    }
+
+    DeploymentFactory.setGatewayServices(services);
+    final TopologyService monitor = services
+        .getService(ServiceType.TOPOLOGY_SERVICE);
+    monitor.addTopologyChangeListener(topoListener);
+    monitor.reloadTopologies();
+  }
+
+  private static File createDir() throws IOException {
+    return TestUtils
+        .createTempDir(WebsocketEchoTest.class.getSimpleName() + "-");
+  }
+
+  private static XMLTag createKnoxTopology(final String backend) {
+    return XMLDoc.newDocument(true).addRoot("topology").addTag("service")
+        .addTag("role").addText("WEBSOCKET").addTag("url").addText(backend)
+        .gotoParent().gotoRoot();
+  }
+
+  private static class TestTopologyListener implements TopologyListener {
+    public List<List<TopologyEvent>> events = new ArrayList<>();
+
+    @Override
+    public void handleTopologyEvent(List<TopologyEvent> events) {
+      this.events.add(events);
+
+      synchronized (this) {
+        for (TopologyEvent event : events) {
+          if (!event.getType().equals(TopologyEvent.Type.DELETED)) {
+            /* for this test we only care about this part */
+            DeploymentFactory.createDeployment(gatewayConfig,
+                event.getTopology());
+          }
+        }
+      }
+    }
+  }
+}