package org.yamcs.api.ws; import java.io.IOException; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.api.ws.WebSocketClient.RequestResponsePair; import org.yamcs.protobuf.Web.WebSocketServerMessage; import org.yamcs.protobuf.Web.WebSocketServerMessage.WebSocketExceptionData; import org.yamcs.protobuf.Yamcs.NamedObjectId; import org.yamcs.protobuf.Yamcs.NamedObjectList; import io.netty.buffer.ByteBufInputStream; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.util.CharsetUtil; public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> { private static final Logger log = LoggerFactory.getLogger(WebSocketClientHandler.class); private final WebSocketClientHandshaker handshaker; private final WebSocketClient client; private WebSocketClientCallback callback; private ChannelPromise handshakeFuture; public WebSocketClientHandler(WebSocketClientHandshaker handshaker, WebSocketClient client, WebSocketClientCallback callback) { this.handshaker = handshaker; this.client = client; this.callback = callback; } public ChannelFuture handshakeFuture() { return handshakeFuture; } @Override public void handlerAdded(ChannelHandlerContext ctx) { handshakeFuture = ctx.newPromise(); } @Override public void channelActive(ChannelHandlerContext ctx) { handshaker.handshake(ctx.channel()); } @Override public void channelInactive(ChannelHandlerContext ctx) { log.info("WebSocket Client disconnected!"); callback.disconnected(); if (client.isReconnectionEnabled()) { ctx.channel().eventLoop().schedule(() -> client.connect(), client.reconnectionInterval, TimeUnit.MILLISECONDS); } } @Override public void channelRead0(ChannelHandlerContext ctx, Object msg) { Channel ch = ctx.channel(); if (!handshaker.isHandshakeComplete()) { handshaker.finishHandshake(ch, (FullHttpResponse) msg); log.info("WebSocket Client connected!!"); handshakeFuture.setSuccess(); callback.connected(); return; } if (msg instanceof FullHttpResponse) { FullHttpResponse response = (FullHttpResponse) msg; throw new IllegalStateException( "Unexpected FullHttpResponse (getStatus=" + response.getStatus() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')'); } WebSocketFrame frame = (WebSocketFrame) msg; if (frame instanceof BinaryWebSocketFrame) { BinaryWebSocketFrame binaryFrame = (BinaryWebSocketFrame) frame; log.trace("WebSocket Client received message of size {} ", binaryFrame.content().readableBytes()); processFrame(binaryFrame); } else if (frame instanceof PingWebSocketFrame) { frame.content().retain(); ch.writeAndFlush(new PongWebSocketFrame(frame.content())); } else if (frame instanceof PongWebSocketFrame) { log.info("WebSocket Client received pong"); } else if (frame instanceof CloseWebSocketFrame) { log.info("WebSocket Client received closing"); ch.close(); } else { log.error("Received unsupported web socket frame " + frame); System.out.println(((TextWebSocketFrame) frame).text()); } } private void processFrame(BinaryWebSocketFrame frame) { try { WebSocketServerMessage message = WebSocketServerMessage.newBuilder().mergeFrom(new ByteBufInputStream(frame.content())).build(); switch (message.getType()) { case REPLY: int reqId = message.getReply().getSequenceNumber(); RequestResponsePair pair = client.removeUpstreamRequest(reqId); if (pair == null) { log.warn("Received an exception for a request I did not send (or was already finished) seqNum: {}", reqId); } else if(pair.responseHandler!=null) { pair.responseHandler.onCompletion(); } break; case EXCEPTION: processExceptionData(message.getException()); break; case DATA: callback.onMessage(message.getData()); break; default: throw new IllegalStateException("Invalid message type received: " + message.getType()); } } catch (IOException e) { throw new RuntimeException(e); } } private void processExceptionData(WebSocketExceptionData exceptionData) throws IOException { int reqId = exceptionData.getSequenceNumber(); RequestResponsePair pair = client.getRequestResponsePair(reqId); if (pair == null) { log.warn("Received an exception for a request I did not send (or was already finished) seqNum: {}", reqId); return; } // TODO this doesn't belong here, we should make this an option in the request and do it server-side if ("InvalidIdentification".equals(exceptionData.getType())) { // Well that's unfortunate, we need to resend another subscription with // the invalid parameters excluded byte[] barray = exceptionData.getData().toByteArray(); NamedObjectList invalidList = NamedObjectList.newBuilder().mergeFrom(barray).build(); WebSocketRequest req = pair.request; if(!req.getResource().equals("parameter") || !req.getOperation().equals("subscribe")) { log.warn("Received an InvalidIdentification exception for a request that is not a parameter/subscribe request, seqNum: {}", reqId); return; } NamedObjectList requestedIdList = (NamedObjectList) req.getRequestData(); Set<NamedObjectId> requestedIds = new HashSet<>(requestedIdList.getListList()); for (NamedObjectId invalidId : invalidList.getListList()) { // Notify downstream callback.onInvalidIdentification(invalidId); requestedIds.remove(invalidId); } // Get rid of the current pending request client.removeUpstreamRequest(reqId); if(!requestedIds.isEmpty()) { // And have another go at it NamedObjectList nol = NamedObjectList.newBuilder().addAllList(requestedIds).build(); client.sendRequest(new WebSocketRequest("parameter", "subscribe", nol)); } } else { log.warn("Got exception message " + exceptionData.getMessage()); if (pair.responseHandler != null) { pair.responseHandler.onException(exceptionData); } } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { log.error("WebSocket exception. Closing channel", cause); if (!handshakeFuture.isDone()) { handshakeFuture.setFailure(cause); } ctx.close(); callback.connectionFailed(cause); } }