Search code examples
javawebsockettomcat8

unable to use websocket in embeded tomcat because ServerContainer is null


I haven't used Java for over a decade and know I'm trying to use the embedded tomcat 8.5.95 (stuck with this version because of guacamole-common) together with websocket support.

Tried various ways to get it work, but I'm stuck and I have no idea what it is causing the issues.
The problem is that (ServerContainer)ctx.getAttribute("javax.websocket.server.ServerContainer") is always null.
I know there are a lot of questions here on SO with the same issue, but I tried a lot of them and none seems to work.
It doesn't matter if a call the code above in my main or in a ServletContainerInitializer

main

public static void main(String[] args) throws Exception {

        Tomcat tomcat = new Tomcat();
    
        tomcat.setPort(8080);
        // tried tomcat.addWebapp and tomcat.addContext
        var ctx = tomcat.addWebapp("", new File(".").getAbsolutePath());
        ctx.addServletContainerInitializer(new Init(), null);
        
       // normal http implementation for guacamole
        Tomcat.addServlet(ctx, "tunnel", new DummyGuacamoleTunnelServlet()); 
        ctx.addServletMappingDecoded("/tunnel", "tunnel");
        
        // tried this first, but according the internet ;) this should go in a 

//      String serverContainerClass = ServerContainer.class.getName();
//        ServerEndpointConfig config =
//                ServerEndpointConfig.Builder.create(DummyGuacamoleWebsocketEndpoint.class, "/websocket-tunnel")
//                       .subprotocols(Arrays.asList(new String[]{"guacamole"}))
//                       .build();
//      String serverContainerClass = ServerContainer.class.getName();
//        ServerEndpointConfig config =
//                ServerEndpointConfig.Builder.create(DummyGuacamoleWebsocketEndpoint.class, "/websocket-tunnel")
//                        .subprotocols(Arrays.asList(new String[]{"guacamole"}))
//                        .build();
//
//        ServerContainer container = (ServerContainer)ctx.getServletContext().getAttribute(serverContainerClass);
//        if (container == null) {
//            return;
//        }
//
//        try {
//
//            // Add configuration to container
//            container.addEndpoint(config);
//
//        }
//        catch (DeploymentException e) {
//            System.out.println("Unable to deploy WebSocket tunnel. " + e);
//        }
        tomcat.start();
        tomcat.getServer().await();
    }

Init (ServletContainerInitializer)

public class Init implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        String serverContainerClass = ServerContainer.class.getName();
                ServerContainer container = (ServerContainer)ctx.getAttribute(serverContainerClass);
        if (container == null) {
            return;
        }
        ServerEndpointConfig config =
                ServerEndpointConfig.Builder.create(DummyGuacamoleWebsocketEndpoint.class, "/websocket-tunnel")
                        .subprotocols(Arrays.asList(new String[]{"guacamole"}))
                        .build();


        try {

            // Add configuration to container
            container.addEndpoint(config);

        }
        catch (DeploymentException e) {
            System.out.println("Unable to deploy WebSocket tunnel. " + e);
        }

    }
}

Endpoint

// GuacamoleWebSocketTunnelEndpoint is from guacamole-common, which I can't change
public class DummyGuacamoleWebsocketEndpoint extends GuacamoleWebSocketTunnelEndpoint {
    @Override
    protected GuacamoleTunnel createTunnel(javax.websocket.Session session, javax.websocket.EndpointConfig config) throws GuacamoleException {
        return null;
    }
}

gradle dependencies

dependencies {
    implementation("org.apache.guacamole:guacamole-common:1.5.3")
    implementation("org.apache.tomcat.embed:tomcat-embed-core:8.5.95")
    implementation("org.apache.tomcat.embed:tomcat-embed-websocket:8.5.95")
    compileOnly("javax.websocket:javax.websocket-api:1.1")
    compileOnly("javax.websocket:javax.websocket-all:1.1")
}

EDIT I also tried to implement a ServletContextListener in which I tried to get the ServerContainer, to no avail.

How do I get the correct ServerContainer?


Solution

  • Ok, after some further searching and investigating I found the solution :)

    I don't need to the two javax.websocket... dependencies

    There are basically two possibilites to add the endpoint

    With annotations

    1. Create the context like that (in main)

      var ctx = tomcat.addWebapp("", new File(".").getAbsolutePath());
      

      This will also add support for jsp (but needs additional dependencies, e.g. jasper)

    2. add the ServletContainerInitializer provided by org.apache.tomcat.embed:tomcat-embed-websocket (in main)

      ctx.addServletContainerInitializer(new WsSci(), null);
      
    3. annotate the endpoint with @ServerEndpoint("/path")

    or you can explicitly add the endpoint to the initializer

    ctx.addServletContainerInitializer(new WsSci(), new HashSet<>(List.of(GuacamoleWebSocketTunnelEndpoint.class)));
    

    then you dont need the annotation

    without annotations

    1. Create the context like that (in main)

      var ctx = tomcat.addContext("", new File(".").getAbsolutePath());
      
      Wrapper defaultServlet = ctx.createWrapper();
      defaultServlet.setName("default");
      defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet");
      defaultServlet.setLoadOnStartup(1);
      ctx.addChild(defaultServlet);
      ctx.addServletMappingDecoded("/", "default");
      

      you have to add the default servlet as described here
      you won't get JSP support.

    2. add the ServletContainerInitializer provided by org.apache.tomcat.embed:tomcat-embed-websocket (in main)

      ctx.addServletContainerInitializer(new WsSci(), null);
      
    3. add a listener

      public class Listener implements ServletContextListener {
      
         @Override
         public void contextInitialized(ServletContextEvent event) {
           String serverContainerClass = ServerContainer.class.getName();
           ServerContainer container = (ServerContainer)event.getServletContext().getAttribute(serverContainerClass);
           ServerEndpointConfig config =
                  ServerEndpointConfig.Builder.create(DummyGuacamoleWebsocketEndpoint.class, "/websocket-tunnel")
                          .build();
      
            try {
              container.addEndpoint(config);
            } catch (DeploymentException e) {
              throw new RuntimeException(e);
            }
         }
      
         @Override
         public void contextDestroyed(ServletContextEvent event) {
          // Do stuff during server shutdown.
         }
      }
      
    4. register the listener in main

      ctx.addApplicationListener("full.name.to.ListenerClass");