/* Copyright (c) 2011 Danish Maritime Authority. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.maritimecloud.mms.server.connection.transport; import net.maritimecloud.internal.mms.messages.spi.MmsMessage; import net.maritimecloud.message.MessageFormatType; import net.maritimecloud.mms.server.ServerEventListener; import net.maritimecloud.mms.server.connection.client.Client; import net.maritimecloud.mms.server.security.*; import net.maritimecloud.net.mms.MmsConnectionClosingCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.websocket.CloseReason; import javax.websocket.Session; import java.nio.ByteBuffer; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import static java.util.Objects.requireNonNull; /** * * @author Kasper Nielsen */ public final class ServerTransport { /** The logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(ServerTransport.class); /** An attachment that can be attached to the transport. */ private final ConcurrentHashMap<String, Object> attachments = new ConcurrentHashMap<>(); /** The security manager */ private final MmsSecurityManager securityManager; /** A listener of important server side events. */ private final ServerEventListener eventListener; /** The listener to invoke on incoming messages. */ private final ServerTransportListener listener; /** The system time of the last received message. */ volatile long timeOfLatestIncomingMessage; /** The creation time of this transport. */ private final long timeOfreationTime = System.nanoTime(); /** The current session. */ volatile Session wsSession; /** Sets the output format of messages. */ volatile MessageFormatType channelFormatType; /** The client subject */ Subject subject; ServerTransport(MmsSecurityManager securityManager, Session wsSession, ServerTransportListener listener, ServerEventListener eventListener) { this.securityManager = requireNonNull(securityManager); this.listener = requireNonNull(listener); this.wsSession = requireNonNull(wsSession); this.eventListener = requireNonNull(eventListener); // Initialize the client Subject initializeSubject(); } /** * Initializes the client subject */ private void initializeSubject() { // Instantiate the client subject subject = new Subject.Builder(securityManager) .setSession(wsSession) .build(); AuthenticationToken token = null; try { // Attempt to resolve an authentication token token = securityManager.resolveAuthenticationToken(wsSession); if (token != null) { // Log in the subject using the authentication token subject.login(token); LOGGER.info("Successfully authenticated " + token.getPrincipal()); } } catch (AuthenticationException e) { LOGGER.warn("Client authentication failed for " + token, e); close(MmsConnectionClosingCode.AUTHENTICATION_ERROR.withMessage(e.getMessage())); } } public void close(MmsConnectionClosingCode reason) { Session wsSession = this.wsSession; if (wsSession != null) { CloseReason cr = new CloseReason(reason::getId, reason.getMessage()); try { wsSession.close(cr); } catch (Exception e) { LOGGER.error("Failed to close connection", e); } } } void endpointOnBinaryMessage(byte[] binary) { timeOfLatestIncomingMessage = System.nanoTime(); if (channelFormatType == null) { channelFormatType = MessageFormatType.MACHINE_READABLE; } eventListener.transportBinaryMessageReceived(this, binary); endpointOnMessage(() -> MmsMessage.parseBinaryMessage(binary)); } void endpointOnClose(CloseReason closeReason) { wsSession = null; try { listener.onClose(this, MmsConnectionClosingCode.create(closeReason.getCloseCode().getCode(), closeReason.getReasonPhrase())); } catch (RuntimeException e) { LOGGER.error("Failed to process close request", e); close(MmsConnectionClosingCode.INTERNAL_ERROR.withMessage(e.getMessage())); } } private void endpointOnMessage(Callable<MmsMessage> c) { // Start by parsing the received message MmsMessage msg; try { msg = c.call(); } catch (Exception e) { LOGGER.error("Failed to parse incoming message", e); // TODO not technically an error, we don't care close(MmsConnectionClosingCode.BAD_DATA.withMessage(e.getMessage())); return; } eventListener.transportMessageReceived(this, msg); // process message try { listener.onMessageReceived(this, msg); } catch (RuntimeException e) { LOGGER.error("Failed to process message", e); close(MmsConnectionClosingCode.INTERNAL_ERROR.withMessage(e.getMessage())); } } void endpointOnOpen() { timeOfLatestIncomingMessage = System.nanoTime(); try { listener.onOpen(this); } catch (RuntimeException e) { LOGGER.error("Failed to process open request", e); close(MmsConnectionClosingCode.INTERNAL_ERROR.withMessage(e.getMessage())); } } void endpointOnTextMessage(String textMessage) { timeOfLatestIncomingMessage = System.nanoTime(); if (channelFormatType == null) { channelFormatType = MessageFormatType.HUMAN_READABLE; } eventListener.transportTextMessageReceived(this, textMessage); endpointOnMessage(() -> MmsMessage.parseTextMessage(textMessage)); } /** * @return the attachment */ public <T> T getAttachment(String key, Class<T> type) { return type.cast(attachments.get(key)); } /** * @return the creationTime */ public long getTimeOfCreation() { return timeOfreationTime; } /** * @return the latestReceivedMessage */ public long getTimeOfLatestIncomingMessage() { return timeOfLatestIncomingMessage; } /** * Send the specified message with the transport. * * @param message * the message to send */ public void sendMessage(MmsMessage message) { try { eventListener.transportMessageSend(this, message); } catch (RuntimeException e) { LOGGER.error("Event listener failed", e); } Session wsSession = this.wsSession; if (wsSession != null) { try { if (channelFormatType == MessageFormatType.MACHINE_READABLE) { byte[] data = message.toBinary(); eventListener.transportBinaryMessageSend(this, data); wsSession.getAsyncRemote().sendBinary(ByteBuffer.wrap(data)); } else { String textToSend = message.toText(); eventListener.transportTextMessageSend(this, textToSend); wsSession.getAsyncRemote().sendText(textToSend); } listener.onMessageSent(this, message); } catch (Exception e) { LOGGER.error("Failed to serialize data", e); close(MmsConnectionClosingCode.INTERNAL_ERROR.withMessage(e.getMessage())); } } } /** * Sets a named attachment for this transport. If the attachment is null, the attachment is cleared. * * @param key * the string of the attachment * @param attachment * the attachment * @throws NullPointerException * if the specified key is null */ public void setAttachment(String key, Object attachment) { if (attachment == null) { attachments.remove(key); } else { attachments.put(key, attachment); } } public MessageFormatType getChannelFormatType() { return channelFormatType; } /** * Called when the client has been resolved from a Hello message * @param client the client */ public void clientResolved(Client client) { try { subject.checkClient(client.getId()); LOGGER.info("Verified client " + client.getId() + " for principal " + subject.getPrincipal()); } catch (ClientVerificationException e) { LOGGER.warn("Client verification failed for client " + client.getId() + " and principal " + subject.getPrincipal(), e); close(MmsConnectionClosingCode.INVALID_CLIENT.withMessage(e.getMessage())); } } }