All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.dsf.fhir.websocket.ServerEndpoint Maven / Gradle / Ivy

package dev.dsf.fhir.websocket;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.Principal;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

import dev.dsf.common.auth.conf.Identity;
import dev.dsf.fhir.authentication.FhirServerRole;
import dev.dsf.fhir.subscription.WebSocketSubscriptionManager;
import jakarta.websocket.CloseReason;
import jakarta.websocket.CloseReason.CloseCodes;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.MessageHandler.Whole;
import jakarta.websocket.PongMessage;
import jakarta.websocket.Session;

public class ServerEndpoint extends Endpoint implements InitializingBean, DisposableBean
{
	private static final Logger logger = LoggerFactory.getLogger(ServerEndpoint.class);

	public static final String PATH = "/ws";
	public static final String USER_PROPERTY = ServerEndpoint.class.getName() + ".user";
	private static final String PINGER_PROPERTY = ServerEndpoint.class.getName() + ".pinger";
	private static final String BIND_MESSAGE_START = "bind ";

	private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

	private final WebSocketSubscriptionManager subscriptionManager;

	public ServerEndpoint(WebSocketSubscriptionManager subscriptionManager)
	{
		this.subscriptionManager = subscriptionManager;
	}

	@Override
	public void afterPropertiesSet() throws Exception
	{
		Objects.requireNonNull(subscriptionManager, "subscriptionManager");
	}

	@Override
	public void onOpen(Session session, EndpointConfig config)
	{
		Principal principal = session.getUserPrincipal();
		if (principal == null || !(principal instanceof Identity)
				|| !((Identity) principal).hasDsfRole(FhirServerRole.WEBSOCKET))
		{
			logger.warn("No user in session or user is missing role {}, closing websocket, session {}",
					FhirServerRole.WEBSOCKET, session.getId());
			try
			{
				session.close(new CloseReason(CloseCodes.VIOLATED_POLICY, "Forbidden"));
			}
			catch (IOException e)
			{
				logger.debug("Error while closing websocket, session {}", session.getId(), e);
				logger.warn("Error while closing websocket, session {}: {} - {}", session.getId(),
						e.getClass().getName(), e.getMessage());
			}

			return;
		}

		logger.info("Websocket open, session {}, identity '{}'", session.getId(), principal.getName());

		session.addMessageHandler(new Whole() // don't use lambda
		{
			@Override
			public void onMessage(String message)
			{
				logger.debug("Websocket message received, session {}: {}", session.getId(), message);

				if (message != null && !message.isBlank() && message.startsWith(BIND_MESSAGE_START))
				{
					String subscriptionIdPart = message.substring(BIND_MESSAGE_START.length());

					logger.debug("Websocket bind message received, session {}, subscription: {}", session.getId(),
							subscriptionIdPart);
					subscriptionManager.bind((Identity) principal, session, subscriptionIdPart);
				}
			}
		});

		ScheduledFuture pinger = scheduler.scheduleWithFixedDelay(() -> ping(session), 28, 28, TimeUnit.SECONDS);
		session.getUserProperties().put(PINGER_PROPERTY, pinger);
	}

	private void ping(Session session)
	{
		byte[] send = new byte[32];
		ThreadLocalRandom.current().nextBytes(send);

		session.addMessageHandler(new Whole()
		{
			@Override
			public void onMessage(PongMessage message)
			{
				byte[] read = new byte[32];
				message.getApplicationData().get(read);
				logger.trace("Pong frame received, session {}: {}", session.getId(), Hex.encodeHexString(read));

				if (!Arrays.equals(send, read))
					logger.warn("Ping frame data not equal to pong frame data, session {}: {} vs. {}", session.getId(),
							Hex.encodeHexString(send), Hex.encodeHexString(read));

				session.removeMessageHandler(this);
			}
		});

		try
		{
			logger.trace("Sending ping frame, session {}: {}", session.getId(), Hex.encodeHexString(send));
			session.getAsyncRemote().sendPing(ByteBuffer.wrap(send));
		}
		catch (IllegalArgumentException | IOException e)
		{
			logger.debug("Error while sending ping frame, session {}", session.getId(), e);
			logger.warn("Error while sending ping frame, session {}: {} - {}", session.getId(), e.getClass().getName(),
					e.getMessage());
		}
	}

	@Override
	public void onClose(Session session, CloseReason closeReason)
	{
		logger.info("Websocket closed, session {}: {} - {}", session.getId(), closeReason.getCloseCode().getCode(),
				closeReason.getReasonPhrase());
		subscriptionManager.close(session.getId());

		ScheduledFuture pinger = (ScheduledFuture) session.getUserProperties().get(PINGER_PROPERTY);
		if (pinger != null)
			pinger.cancel(true);
	}

	@Override
	public void onError(Session session, Throwable throwable)
	{
		if (throwable == null)
			logger.info("Websocket closed with error, session {}: unknown error", session.getId());
		else
		{
			logger.debug("Websocket closed with error, session {}", session.getId(), throwable);
			logger.info("Websocket closed with error, session {}: {} - {}", session.getId(),
					throwable.getClass().getName(), getMessages(throwable));
		}
	}

	private String getMessages(Throwable e)
	{
		StringBuilder b = new StringBuilder();
		if (e != null)
		{
			if (e.getMessage() != null)
				b.append(e.getMessage());

			Throwable cause = e.getCause();
			while (cause != null)
			{
				if (cause.getMessage() != null)
				{
					b.append(' ');
					b.append(cause.getMessage());
				}

				cause = cause.getCause();
			}
		}
		return b.toString();
	}


	@Override
	public void destroy() throws Exception
	{
		scheduler.shutdown();
		try
		{
			if (!scheduler.awaitTermination(60, TimeUnit.SECONDS))
			{
				scheduler.shutdownNow();
				if (!scheduler.awaitTermination(60, TimeUnit.SECONDS))
					logger.warn("EventEndpoint scheduler did not terminate");
			}
		}
		catch (InterruptedException ie)
		{
			scheduler.shutdownNow();
			Thread.currentThread().interrupt();
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy