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>) -> 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}