/*
 * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.grizzly.websockets;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.glassfish.grizzly.CloseType;
import org.glassfish.grizzly.Closeable;
import org.glassfish.grizzly.Connection;
import org.glassfish.grizzly.GenericCloseListener;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.http.HttpContent;
import org.glassfish.grizzly.http.HttpRequestPacket;
import org.glassfish.grizzly.http.HttpResponsePacket;
import org.glassfish.grizzly.http.server.util.Mapper;
import org.glassfish.grizzly.http.util.HttpStatus;
import org.glassfish.grizzly.http.util.MimeHeaders;

WebSockets engine implementation (singleton), which handles WebSocketApplications registration, responsible for client and server handshake validation.
Author:Alexey Stashok
See Also:
/** * WebSockets engine implementation (singleton), which handles {@link WebSocketApplication}s registration, responsible * for client and server handshake validation. * * @author Alexey Stashok * @see WebSocket * @see WebSocketApplication */
public class WebSocketEngine { public static final Version DEFAULT_VERSION = Version.RFC6455; public static final int DEFAULT_TIMEOUT = 30; private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final WebSocketEngine engine = new WebSocketEngine(); static final Logger logger = Logger.getLogger(Constants.WEBSOCKET); private final List<WebSocketApplication> applications = new ArrayList<>(); // Association between WebSocketApplication and a value based on the // context path and url pattern. private final HashMap<WebSocketApplication, String> applicationMap = new HashMap<>(4); // Association between full path and registered application. private final HashMap<String, WebSocketApplication> fullPathToApplication = new HashMap<>(4); // Association between a particular context path and all applications with // sub-paths registered to that context path. private final HashMap<String, List<WebSocketApplication>> contextApplications = new HashMap<>(2); private final HttpResponsePacket.Builder unsupportedVersionsResponseBuilder; private Mapper mapper = new Mapper(); private WebSocketEngine() { mapper.setDefaultHostName("localhost"); unsupportedVersionsResponseBuilder = new HttpResponsePacket.Builder(); unsupportedVersionsResponseBuilder.status(HttpStatus.BAD_REQUEST_400.getStatusCode()); unsupportedVersionsResponseBuilder.header(Constants.SEC_WS_VERSION, Version.getSupportedWireProtocolVersions()); } public static WebSocketEngine getEngine() { return engine; } public WebSocketApplication getApplication(HttpRequestPacket request) { final WebSocketApplicationReg appReg = getApplication(request, mapper); return appReg != null ? appReg.app : null; } private WebSocketApplicationReg getApplication(final HttpRequestPacket request, final Mapper glassfishMapper) { final boolean isGlassfish = glassfishMapper != null; WebSocketApplication foundWebSocketApp = null; final WebSocketMappingData data = new WebSocketMappingData(isGlassfish); try { mapper.mapUriWithSemicolon(request, request.getRequestURIRef().getDecodedRequestURIBC(), data, 0); if (data.wrapper != null) { foundWebSocketApp = (WebSocketApplication) data.wrapper; } } catch (Exception e) { if (logger.isLoggable(Level.WARNING)) { logger.log(Level.WARNING, e.toString(), e); } } if (foundWebSocketApp == null) { for (WebSocketApplication application : applications) { if (application.upgrade(request)) { foundWebSocketApp = application; break; } } } if (foundWebSocketApp == null) { return null; } if (isGlassfish) { assert glassfishMapper != null; // do one more mapping, this time using GF Mapper to retrieve // correspondent web application try { data.recycle(); glassfishMapper.mapUriWithSemicolon(request, request.getRequestURIRef().getDecodedRequestURIBC(), data, 0); } catch (Exception e) { if (logger.isLoggable(Level.WARNING)) { logger.log(Level.WARNING, e.toString(), e); } } } return new WebSocketApplicationReg(foundWebSocketApp, // if contextPath == null - don't return any mapping info data.contextPath.isNull() ? null : data); } public boolean upgrade(FilterChainContext ctx, HttpContent requestContent) throws IOException { return upgrade(ctx, requestContent, null); } // Mapper will be non-null when integrated in GF. public boolean upgrade(FilterChainContext ctx, HttpContent requestContent, Mapper mapper) throws IOException { final HttpRequestPacket request = (HttpRequestPacket) requestContent.getHttpHeader(); final WebSocketApplicationReg reg = WebSocketEngine.getEngine().getApplication(request, mapper); WebSocket socket = null; try { if (reg != null) { final ProtocolHandler protocolHandler = loadHandler(request.getHeaders()); if (protocolHandler == null) { handleUnsupportedVersion(ctx, request); return false; } final Connection connection = ctx.getConnection(); protocolHandler.setFilterChainContext(ctx); protocolHandler.setConnection(connection); protocolHandler.setMappingData(reg.mappingData); ctx.setMessage(null); // remove the message from the context, so underlying layers will not try to update it. final WebSocketApplication app = reg.app; socket = app.createSocket(protocolHandler, request, app); WebSocketHolder holder = WebSocketHolder.set(connection, protocolHandler, socket); holder.application = app; protocolHandler.handshake(ctx, app, requestContent); request.getConnection().addCloseListener(new GenericCloseListener() { @Override public void onClosed(final Closeable closeable, final CloseType type) throws IOException { final WebSocket webSocket = WebSocketHolder.getWebSocket(connection); webSocket.close(); webSocket.onClose(new ClosingFrame(WebSocket.END_POINT_GOING_DOWN, "Close detected on connection")); } }); socket.onConnect(); return true; } } catch (HandshakeException e) { logger.log(Level.FINE, e.getMessage(), e); if (socket != null) { socket.close(); } throw e; } return false; } public static ProtocolHandler loadHandler(MimeHeaders headers) { for (Version version : Version.values()) { if (version.validate(headers)) { return version.createHandler(false); } } return null; }
Register a WebSocketApplication to a specific context path and url pattern. If you wish to associate this application with the root context, use an empty string for the contextPath argument.
Examples:
// WS application will be invoked:
// ws://localhost:8080/echo
// WS application will not be invoked:
// ws://localhost:8080/foo/echo
// ws://localhost:8080/echo/some/path
register("", "/echo", webSocketApplication);
// WS application will be invoked:
// ws://localhost:8080/echo
// ws://localhost:8080/echo/some/path
// WS application will not be invoked:
// ws://localhost:8080/foo/echo
register("", "/echo/*", webSocketApplication);
// WS application will be invoked:
// ws://localhost:8080/context/echo
// WS application will not be invoked:
// ws://localhost:8080/echo
// ws://localhost:8080/context/some/path
register("/context", "/echo", webSocketApplication);
Params:
  • contextPath – the context path (per servlet rules)
  • urlPattern – url pattern (per servlet rules)
  • app – the WebSocket application.
/** * Register a WebSocketApplication to a specific context path and url pattern. If you wish to associate this application * with the root context, use an empty string for the contextPath argument. * * <pre> * Examples: * // WS application will be invoked: * // ws://localhost:8080/echo * // WS application will not be invoked: * // ws://localhost:8080/foo/echo * // ws://localhost:8080/echo/some/path * register("", "/echo", webSocketApplication); * * // WS application will be invoked: * // ws://localhost:8080/echo * // ws://localhost:8080/echo/some/path * // WS application will not be invoked: * // ws://localhost:8080/foo/echo * register("", "/echo/*", webSocketApplication); * * // WS application will be invoked: * // ws://localhost:8080/context/echo * * // WS application will not be invoked: * // ws://localhost:8080/echo * // ws://localhost:8080/context/some/path * register("/context", "/echo", webSocketApplication); * </pre> * * @param contextPath the context path (per servlet rules) * @param urlPattern url pattern (per servlet rules) * @param app the WebSocket application. */
public synchronized void register(final String contextPath, final String urlPattern, final WebSocketApplication app) { if (contextPath == null || urlPattern == null) { throw new IllegalArgumentException("contextPath and urlPattern must not be null"); } if (!urlPattern.startsWith("/")) { throw new IllegalArgumentException("The urlPattern must start with '/'"); } String contextPathLocal = getContextPath(contextPath); final String fullPath = contextPathLocal + '|' + urlPattern; final WebSocketApplication oldApp = fullPathToApplication.get(fullPath); if (oldApp != null) { unregister(oldApp); } mapper.addContext("localhost", contextPathLocal, "[Context '" + contextPath + "']", EMPTY_STRING_ARRAY, null); mapper.addWrapper("localhost", contextPathLocal, urlPattern, app); applicationMap.put(app, fullPath); fullPathToApplication.put(fullPath, app); if (contextApplications.containsKey(contextPathLocal)) { contextApplications.get(contextPathLocal).add(app); } else { List<WebSocketApplication> apps = new ArrayList<>(4); apps.add(app); contextApplications.put(contextPathLocal, apps); } }
Deprecated:Use register(String, String, WebSocketApplication)
/** * * @deprecated Use {@link #register(String, String, WebSocketApplication)} */
@Deprecated public synchronized void register(WebSocketApplication app) { applications.add(app); } public synchronized void unregister(WebSocketApplication app) { String fullPath = applicationMap.remove(app); if (fullPath != null) { fullPathToApplication.remove(fullPath); String[] parts = fullPath.split("\\|"); mapper.removeWrapper("localhost", parts[0], parts[1]); List<WebSocketApplication> apps = contextApplications.get(parts[0]); apps.remove(app); if (apps.isEmpty()) { mapper.removeContext("localhost", parts[0]); contextApplications.remove(parts[0]); } return; } applications.remove(app); }
Un-registers all WebSocketApplication instances with the WebSocketEngine.
/** * Un-registers all {@link WebSocketApplication} instances with the {@link WebSocketEngine}. */
public synchronized void unregisterAll() { applicationMap.clear(); fullPathToApplication.clear(); contextApplications.clear(); applications.clear(); mapper = new Mapper(); mapper.setDefaultHostName("localhost"); } private void handleUnsupportedVersion(final FilterChainContext ctx, final HttpRequestPacket request) throws IOException { unsupportedVersionsResponseBuilder.requestPacket(request); ctx.write(unsupportedVersionsResponseBuilder.build()); } private static String getContextPath(String mapping) { String ctx; int slash = mapping.indexOf("/", 1); if (slash != -1) { ctx = mapping.substring(0, slash); } else { ctx = mapping; } if (ctx.startsWith("/*.") || ctx.startsWith("*.")) { if (ctx.indexOf("/") == ctx.lastIndexOf("/")) { ctx = ""; } else { ctx = ctx.substring(1); } } if (ctx.startsWith("/*") || ctx.startsWith("*")) { ctx = ""; } // Special case for the root context if (ctx.equals("/")) { ctx = ""; } return ctx; } private static class WebSocketApplicationReg { private final WebSocketApplication app; private final WebSocketMappingData mappingData; public WebSocketApplicationReg(final WebSocketApplication app, final WebSocketMappingData mappingData) { this.app = app; this.mappingData = mappingData; } } }