/* * Copyright 2013-2016 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.cluck.tcp; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.SocketException; import java.net.SocketTimeoutException; import java.util.LinkedList; import java.util.Random; import ccre.cluck.CluckConstants; import ccre.cluck.CluckLink; import ccre.cluck.CluckNode; import ccre.concurrency.ReporterThread; import ccre.log.Logger; import ccre.net.ClientSocket; import ccre.verifier.FlowPhase; /** * A static utility class for handling various encodings of Cluck packets. * * @author skeggsc */ public class CluckProtocol { /** * The current version of the protocol in use. Version 0 means the same * protocol as the 2.x.x Cluck. * * Changing this number will 100% break compatibility with anything older * than CCRE v3 - the header will seem to be corrupt from the perspective of * the older version. * * Changing this will not necessarily break compatibility with other CCRE v3 * versions. * * The side with the higher version (if they differ) is responsible for * providing a transformer to be compatible with the older version. * * Since the version has always been zero since it was introduced, this is * not currently done. */ static final byte CURRENT_VERSION = 0; /** * The timeout period for disconnected sockets. */ static final int TIMEOUT_PERIOD_MILLIS = 600; /** * The timeout period for sending keepalives when no other data is sent. * This should be set noticeably lower than {@link #TIMEOUT_PERIOD_MILLIS}. */ static final int KEEPALIVE_INTERVAL_MILLIS = 200; /** * Sets the appropriate timeout on sock, for disconnection reporting. * * @param sock the socket to modify. * @throws IOException if the timeout cannot be set. */ public static void setTimeoutOnSocket(ClientSocket sock) throws IOException { sock.setSocketTimeout(TIMEOUT_PERIOD_MILLIS); } /** * Start a Cluck connection. Must be run from both ends of the connection. * * @param din The connection's input. * @param dout The connection's output. * @param remoteHint The hint for what the remote node should call this * link, or null for no recommendation. * @return What the remote node hints that this link should be called. * @throws IOException If an IO error occurs. */ protected static String handleHeader(DataInputStream din, DataOutputStream dout, String remoteHint) throws IOException { dout.writeInt(0x154000CA | ((CURRENT_VERSION & 0xFF) << 8)); Random r = new Random(); int ra = r.nextInt(), rb = r.nextInt(); dout.writeInt(ra); dout.writeInt(rb); int raw_magic = din.readInt(); if ((raw_magic & 0xFFFF00FF) != 0x154000CA) { throw new IOException("Magic number did not match!"); } int version = (raw_magic & 0x0000FF00) >> 8;// always in [0, 255] if (version < CURRENT_VERSION) { // The side with the higher version (if they differ) is responsible // for providing a transformer to be compatible with the older // version. // But so far, we're still on version 0, so this shouldn't happen! throw new IOException("Remote end is on an older version of Cluck! I don't quite know how to deal with this."); } dout.writeInt(din.readInt() ^ din.readInt()); if (din.readInt() != (ra ^ rb)) { throw new IOException("Did not bounce properly!"); } dout.writeUTF(remoteHint == null ? "" : remoteHint); String rh = din.readUTF(); return rh.isEmpty() ? null : rh; } /** * Calculate a checksum from a basis and a byte array. * * @param data The data to checksum. * @param basis The basis initializer. * @return The checksum. */ protected static long checksum(byte[] data, long basis) { long h = basis; for (int i = 0; i < data.length; i++) { h = 43 * h + data[i]; } return h; } /** * Start a receive loop from the specified Connection input, link name, * node, and link to deny broadcasts to. * * @param din The connection input. * @param linkName The link name. * @param node The node to provide access to. * @param denyLink The link to deny transmits to, usually the link that * sends back to the other end of the connection. (To stop infinite loops) * @throws IOException If an IO error occurs */ protected static void handleRecv(DataInputStream din, String linkName, CluckNode node, CluckLink denyLink) throws IOException { try { boolean expectKeepAlives = false; long lastReceive = System.currentTimeMillis(); while (true) { try { String dest = readNullableString(din); String source = readNullableString(din); byte[] data = new byte[din.readInt()]; long checksumBase = din.readLong(); din.readFully(data); if (din.readLong() != checksum(data, checksumBase)) { throw new IOException("Checksums did not match!"); } if (!expectKeepAlives && "KEEPALIVE".equals(dest) && source == null && data.length >= 2 && data[0] == CluckConstants.RMT_NEGATIVE_ACK && data[1] == 0x6D) { expectKeepAlives = true; Logger.info("Detected KEEPALIVE message. Expecting future keepalives on " + linkName + "."); } source = prependLink(linkName, source); long start = System.currentTimeMillis(); node.transmit(dest, source, data, denyLink); long endAt = System.currentTimeMillis(); if (endAt - start > 1000) { Logger.warning("[LOCAL] Took a long time to process: " + dest + " <- " + source + " of " + (endAt - start) + " ms"); } lastReceive = System.currentTimeMillis(); } catch (SocketTimeoutException ex) { if (expectKeepAlives && System.currentTimeMillis() - lastReceive > TIMEOUT_PERIOD_MILLIS) { throw ex; } else { // otherwise, don't do anything - we don't know if this // is a timeout. } } } } catch (SocketTimeoutException ex) { Logger.fine("Link timed out: " + linkName); } catch (SocketException ex) { if ("Connection reset".equals(ex.getMessage())) { Logger.fine("Link receiving disconnected: " + linkName); } else { throw ex; } } } static String readNullableString(DataInputStream din) throws IOException { String out = din.readUTF(); return out.isEmpty() ? null : out; } static String prependLink(String linkName, String source) { return source == null ? linkName : linkName + "/" + source; } /** * Create and register a cluck link using the specified connection output, * link name, and node to get messages from. * * Returns the newly created link so that it can be denied from future * receives. * * @param dout The connection output. * @param linkName The link name. * @param node The node to provide access to. * @return The newly created link. */ protected static CluckLink handleSend(final DataOutputStream dout, final String linkName, CluckNode node) { final LinkedList<SendableEntry> queue = new LinkedList<SendableEntry>(); final ReporterThread main = new CluckSenderThread("Cluck-Send-" + linkName, queue, dout); main.start(); CluckLink clink = new CluckLink() { private boolean isRunning = false; @Override public synchronized boolean send(String dest, String source, byte[] data) { if (isRunning) { Logger.severe("[LOCAL] Already running transmit!"); return true; } isRunning = true; try { int size; synchronized (queue) { queue.addLast(new SendableEntry(source, dest, data)); queue.notifyAll(); size = queue.size(); } Thread.yield(); if (size > 1000) { Logger.warning("[LOCAL] Queue too long: " + size + " for " + dest + " at " + System.currentTimeMillis()); } } finally { isRunning = false; } return main.isAlive(); } }; node.addOrReplaceLink(clink, linkName); return clink; } private CluckProtocol() { } /** * Stored in a queue of the messages that need to be sent over a connection. * * @author skeggsc */ private static class SendableEntry { /** * The sender of this message. */ public final String src; /** * The receiver of this message. */ public final String dst; /** * The contents of this message. */ public final byte[] data; /** * Create a new SendableEntry with the specified attributes. * * @param src The source of the message. * @param dst The destination of the message. * @param data The contents of the message. */ @FlowPhase SendableEntry(String src, String dst, byte[] data) { super(); this.src = src; this.dst = dst; this.data = data; } @Override public String toString() { return "[" + src + "->" + dst + "#" + data.length + "]"; } } private static class CluckSenderThread extends ReporterThread { private final LinkedList<SendableEntry> queue; private final DataOutputStream dout; CluckSenderThread(String name, LinkedList<SendableEntry> queue, DataOutputStream dout) { super(name); this.queue = queue; this.dout = dout; } @Override protected void threadBody() throws InterruptedException { try { while (true) { long nextKeepAlive = System.currentTimeMillis() + KEEPALIVE_INTERVAL_MILLIS; SendableEntry ent; synchronized (queue) { while (queue.isEmpty() && System.currentTimeMillis() < nextKeepAlive) { queue.wait(200); } if (queue.isEmpty()) { // Send a "keep-alive" message. RMT_NEGATIVE_ACK // will never be complained about, so it works. ent = new SendableEntry(null, "KEEPALIVE", new byte[] { CluckConstants.RMT_NEGATIVE_ACK, 0x6D }); } else { ent = queue.removeFirst(); } } String source = ent.src, dest = ent.dst; byte[] data = ent.data; dout.writeUTF(dest == null ? "" : dest); dout.writeUTF(source == null ? "" : source); dout.writeInt(data.length); long begin = (((long) data.length) << 32) ^ (dest == null ? 0 : ((long) dest.hashCode()) << 16) ^ (source == null ? 0 : source.hashCode() ^ (((long) source.hashCode()) << 48)); dout.writeLong(begin); dout.write(data); dout.writeLong(checksum(data, begin)); } } catch (IOException ex) { Logger.warning("Bad IO in " + this + ": " + ex); } } } }