001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.console.bus.websocket;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import org.apache.juneau.console.bus.*;
022import org.apache.juneau.rest.server.filter.*;
023
024import jakarta.servlet.*;
025import jakarta.websocket.*;
026import jakarta.websocket.server.*;
027
028/**
029 * Registers the bus's WebSocket endpoint on the servlet container (Jakarta WebSocket 2.2).
030 *
031 * <p>
032 * The endpoint is context-relative ({@value #PATH}) because WebSocket endpoints are not servlet-mapped.  Origin is
033 * the CSRF control for the handshake, so registration refuses a policy with no Origin rule.  The rule is
034 * {@code LoopbackBoundary.origin()} for a {@code LoopbackBoundary}, the handshake's own scheme and Host for a
035 * {@code CsrfBoundary}, and {@code BusPolicy.allowedOrigins} when the policy has no boundary.  Any other boundary
036 * type is refused: the handshake cannot apply a boundary it does not know.
037 *
038 * <h5 class='section'>Example:</h5>
039 * <p class='bjava'>
040 *   <jc>// In a ServletContextListener (or the microservice's start hook):</jc>
041 *   <jk>public void</jk> contextInitialized(ServletContextEvent <jv>e</jv>) {
042 *     BusWebSockets.<jsm>register</jsm>(<jv>e</jv>.getServletContext(), JobsRest.<jsf>BUS</jsf>);   <jc>// "/juneau-bus/ws/{sessionId}"</jc>
043 *   }
044 *
045 *   <jc>// Jetty 12 (ee11), when you hold the ServletContextHandler:</jc>
046 *   JakartaWebSocketServletContainerInitializer.<jsm>configure</jsm>(<jv>context</jv>,
047 *     (<jv>servletContext</jv>, <jv>container</jv>) -&gt; BusWebSockets.<jsm>register</jsm>(<jv>container</jv>, JobsRest.<jsf>BUS</jsf>));
048 * </p>
049 *
050 * @since 10.0.0
051 */
052public final class BusWebSockets {
053
054   /** The context-relative endpoint path. */
055   @SuppressWarnings({
056      "java:S1075" // Documented default endpoint path; public constant
057   })
058   public static final String PATH = "/juneau-bus/ws/{sessionId}";
059
060   /** {@link #PATH} without its path parameter; passed to {@link ServerBus#enableWebSocket(String)}. */
061   @SuppressWarnings({
062      "java:S1075" // Documented default endpoint path; public constant
063   })
064   public static final String PREFIX = "/juneau-bus/ws";
065
066   static final String NO_CONTAINER = "BusWebSockets.register: the container exposes no jakarta.websocket ServerContainer (add the container's WebSocket module)";
067   static final String NO_ORIGIN = "BusWebSockets.register: the policy has neither a boundary nor allowedOrigins; WebSocket bridges need an exact Origin rule";
068
069   static final String UNSUPPORTED_BOUNDARY = "BusWebSockets.register: the policy's boundary type cannot be applied to a WebSocket handshake; use LoopbackBoundary or CsrfBoundary";
070
071   private BusWebSockets() {}
072
073   /**
074    * Registers the endpoint on the container found in the servlet context.
075    *
076    * @param ctx The servlet context.
077    * @param bus The bus whose sessions the endpoint serves.
078    * @throws IllegalStateException no {@link ServerContainer} attribute, no Origin rule, or a boundary type the
079    *    handshake cannot apply.
080    */
081   public static void register(ServletContext ctx, ServerBus bus) {
082      if (! (ctx.getAttribute(ServerContainer.class.getName()) instanceof ServerContainer container))
083         throw new IllegalStateException(NO_CONTAINER);
084      register(container, bus);
085   }
086
087   /**
088    * Registers the endpoint on a container.
089    *
090    * <p>
091    * Enables the bus's WebSocket transport only after the container accepted the endpoint, so a session POST never
092    * advertises a path that is not served.
093    *
094    * @param container The container.
095    * @param bus The bus whose sessions the endpoint serves.
096    * @throws IllegalStateException no Origin rule, a boundary type the handshake cannot apply, or the container
097    *    refused the endpoint.
098    */
099   public static void register(ServerContainer container, ServerBus bus) {
100      var boundary = bus.policy().boundary();
101      if (boundary != null && ! (boundary instanceof LoopbackBoundary) && ! (boundary instanceof CsrfBoundary))
102         throw new IllegalStateException(UNSUPPORTED_BOUNDARY);
103      var origins = BusWebSocketConfigurator.allowedOrigins(bus.policy());
104      if (origins.isEmpty() && ! (boundary instanceof CsrfBoundary))
105         throw new IllegalStateException(NO_ORIGIN);
106      var config = ServerEndpointConfig.Builder.create(BusWebSocketEndpoint.class, PATH)
107         .configurator(new BusWebSocketConfigurator(bus, origins))
108         .build();
109      try {
110         container.addEndpoint(config);
111      } catch (DeploymentException e) {
112         throw new IllegalStateException(f("BusWebSockets.register: could not add endpoint '%s': %s", PATH, e.getMessage()), e);
113      }
114      bus.enableWebSocket(PREFIX);
115   }
116}