package net.i2p.sam; /* * free (adj.): unencumbered; not under the control of others * Written by human in 2004 and released into the public domain * with no warranty of any kind, either expressed or implied. * It probably won't make your computer catch on fire, or eat * your children, but it might. Use at your own risk. * */ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.NoRouteToHostException; import java.nio.channels.SocketChannel; import java.nio.ByteBuffer; import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.I2PClient; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.crypto.SigType; import net.i2p.data.Base64; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.util.Log; import net.i2p.util.I2PAppThread; import net.i2p.util.PasswordManager; /** * Class able to handle a SAM version 3 client connection. * * @author mkvore */ class SAMv3Handler extends SAMv1Handler { private Session session; // TODO remove singleton, hang off SAMBridge like dgserver public static final SessionsDB sSessionsHash = new SessionsDB(); private volatile boolean stolenSocket; private volatile boolean streamForwardingSocket; private final boolean sendPorts; private long _lastPing; private static final int FIRST_READ_TIMEOUT = 60*1000; private static final int READ_TIMEOUT = 3*60*1000; private static final String AUTH_ERROR = "AUTH STATUS RESULT=I2P_ERROR"; /** * Create a new SAM version 3 handler. This constructor expects * that the SAM HELLO message has been still answered (and * stripped) from the socket input stream. * * @param s Socket attached to a SAM client * @param verMajor SAM major version to manage (should be 3) * @param verMinor SAM minor version to manage */ public SAMv3Handler(SocketChannel s, int verMajor, int verMinor, SAMBridge parent) throws SAMException, IOException { this(s, verMajor, verMinor, new Properties(), parent); } /** * Create a new SAM version 3 handler. This constructor expects * that the SAM HELLO message has been still answered (and * stripped) from the socket input stream. * * @param s Socket attached to a SAM client * @param verMajor SAM major version to manage (should be 3) * @param verMinor SAM minor version to manage * @param i2cpProps properties to configure the I2CP connection (host, port, etc) */ public SAMv3Handler(SocketChannel s, int verMajor, int verMinor, Properties i2cpProps, SAMBridge parent) throws SAMException, IOException { super(s, verMajor, verMinor, i2cpProps, parent); sendPorts = (verMajor == 3 && verMinor >= 2) || verMajor > 3; if (_log.shouldLog(Log.DEBUG)) _log.debug("SAM version 3 handler instantiated"); } @Override public boolean verifVersion() { return (verMajor == 3); } public String getClientIP() { return this.socket.socket().getInetAddress().getHostAddress(); } /** * For SAMv3StreamSession connect and accept */ public void stealSocket() { stolenSocket = true ; if (sendPorts) { try { socket.socket().setSoTimeout(0); } catch (SocketException se) {} } this.stopHandling(); } /** * For SAMv3StreamSession * @since 0.9.20 */ SAMBridge getBridge() { return bridge; } /** * For SAMv3DatagramServer * @return may be null * @since 0.9.24 */ Session getSession() { return session; } /** * For subsessions created by MasterSession * @since 0.9.25 */ void setSession(SAMv3RawSession sess) { rawSession = sess; session = sess; } /** * For subsessions created by MasterSession * @since 0.9.25 */ void setSession(SAMv3DatagramSession sess) { datagramSession = sess; session = sess; } /** * For subsessions created by MasterSession * @since 0.9.25 */ void setSession(SAMv3StreamSession sess) { streamSession = sess; session = sess; } @Override public void handle() { String msg = null; String domain = null; String opcode = null; boolean canContinue = false; Properties props; this.thread.setName("SAMv3Handler " + _id); if (_log.shouldLog(Log.DEBUG)) _log.debug("SAMv3 handling started"); try { Socket socket = getClientSocket().socket(); InputStream in = socket.getInputStream(); StringBuilder buf = new StringBuilder(1024); boolean gotFirstLine = false; while (true) { if (shouldStop()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Stop request found"); break; } String line; if (sendPorts) { // client supports PING try { ReadLine.readLine(socket, buf, READ_TIMEOUT); line = buf.toString(); buf.setLength(0); } catch (SocketTimeoutException ste) { long now = System.currentTimeMillis(); if (buf.length() <= 0) { if (_lastPing > 0) { if (now - _lastPing >= READ_TIMEOUT) { if (_log.shouldWarn()) _log.warn("Failed to respond to PING"); writeString(SESSION_ERROR, "PONG timeout"); break; } } else { if (_log.shouldDebug()) _log.debug("Sendng PING " + now); _lastPing = now; if (!writeString("PING " + now + '\n')) break; } } else { if (_lastPing > 0) { if (now - _lastPing >= 2*READ_TIMEOUT) { if (_log.shouldWarn()) _log.warn("Failed to respond to PING"); writeString(SESSION_ERROR, "PONG timeout"); break; } } else if (_lastPing < 0) { if (_log.shouldWarn()) _log.warn("2nd timeout"); writeString(SESSION_ERROR, "command timeout, bye"); break; } else { // don't clear buffer, don't send ping, // go around again _lastPing = -1; if (_log.shouldWarn()) _log.warn("timeout after partial: " + buf); } } if (_log.shouldDebug()) _log.debug("loop after timeout"); continue; } } else { buf.setLength(0); // first time, set a timeout try { ReadLine.readLine(socket, buf, gotFirstLine ? 0 : FIRST_READ_TIMEOUT); socket.setSoTimeout(0); } catch (SocketTimeoutException ste) { writeString(SESSION_ERROR, "command timeout, bye"); break; } line = buf.toString(); } if (_log.shouldLog(Log.DEBUG)) _log.debug("New message received: [" + line + ']'); props = SAMUtils.parseParams(line); domain = (String) props.remove(SAMUtils.COMMAND); if (domain == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Ignoring newline"); continue; } gotFirstLine = true; opcode = (String) props.remove(SAMUtils.OPCODE); if (_log.shouldLog(Log.DEBUG)) { _log.debug("Parsing (domain: \"" + domain + "\"; opcode: \"" + opcode + "\")"); } // these may not have a second token if (domain.equals("PING")) { execPingMessage(opcode); continue; } else if (domain.equals("PONG")) { execPongMessage(opcode); continue; } else if (domain.equals("QUIT") || domain.equals("STOP") || domain.equals("EXIT")) { writeString(domain + " STATUS RESULT=OK MESSAGE=bye\n"); break; } if (opcode == null) { // This is not a correct message, for sure if (writeString(domain + " STATUS RESULT=I2P_ERROR", "command not specified")) continue; else break; } if (domain.equals("STREAM")) { canContinue = execStreamMessage(opcode, props); } else if (domain.equals("SESSION")) { if (i2cpProps != null) props.putAll(i2cpProps); // make sure we've got the i2cp settings canContinue = execSessionMessage(opcode, props); } else if (domain.equals("DEST")) { canContinue = execDestMessage(opcode, props); } else if (domain.equals("NAMING")) { canContinue = execNamingMessage(opcode, props); } else if (domain.equals("DATAGRAM")) { // TODO not yet overridden, ID is ignored, most recent DATAGRAM session is used canContinue = execDatagramMessage(opcode, props); } else if (domain.equals("RAW")) { // TODO not yet overridden, ID is ignored, most recent RAW session is used canContinue = execRawMessage(opcode, props); } else if (domain.equals("AUTH")) { canContinue = execAuthMessage(opcode, props); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Unrecognized message domain: \"" + domain + "\""); break; } if (!canContinue) { break; } } // while } catch (IOException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Caught IOException in handler", e); writeString(SESSION_ERROR, e.getMessage()); } catch (SAMException e) { _log.error("Unexpected exception for message [" + msg + ']', e); writeString(SESSION_ERROR, e.getMessage()); } catch (RuntimeException e) { _log.error("Unexpected exception for message [" + msg + ']', e); writeString(SESSION_ERROR, e.getMessage()); } finally { if (_log.shouldLog(Log.DEBUG)) _log.debug("Stopping handler"); if (!this.stolenSocket) { try { closeClientSocket(); } catch (IOException e) { if (_log.shouldWarn()) _log.warn("Error closing socket", e); } } if (streamForwardingSocket) { if (this.getStreamSession()!=null) { try { ((SAMv3StreamSession)streamSession).stopForwardingIncoming(); } catch (SAMException e) { if (_log.shouldWarn()) _log.warn("Error while stopping forwarding connections", e); } catch (InterruptedIOException e) { if (_log.shouldWarn()) _log.warn("Interrupted while stopping forwarding connections", e); } } } die(); } } /** * Stop the SAM handler, close the socket, * unregister with the bridge. * * Overridden to not close the client socket if stolen. * * @since 0.9.20 */ @Override public void stopHandling() { if (_log.shouldInfo()) _log.info("Stopping (stolen? " + stolenSocket + "): " + this, new Exception("I did it")); synchronized (stopLock) { stopHandler = true; } if (!stolenSocket) { try { closeClientSocket(); } catch (IOException e) {} } bridge.unregister(this); } private void die() { SessionRecord rec = null ; if (session!=null) { session.close(); rec = sSessionsHash.get(session.getNick()); } if (rec!=null) { rec.getThreadGroup().interrupt() ; while (rec.getThreadGroup().activeCount()>0) try { Thread.sleep(1000); } catch ( InterruptedException e) {} rec.getThreadGroup().destroy(); sSessionsHash.del(session.getNick()); } } /* Parse and execute a SESSION message */ @Override protected boolean execSessionMessage(String opcode, Properties props) { String dest = "BUG!"; boolean ok = false ; String nick = (String) props.remove("ID"); if (nick == null) return writeString(SESSION_ERROR, "ID not specified"); String style = (String) props.remove("STYLE"); if (style == null && !opcode.equals("REMOVE")) return writeString(SESSION_ERROR, "No SESSION STYLE specified"); try{ if (opcode.equals("CREATE")) { if ((this.getRawSession()!= null) || (this.getDatagramSession() != null) || (this.getStreamSession() != null)) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Trying to create a session, but one still exists"); return writeString(SESSION_ERROR, "Session already exists"); } if (props.isEmpty()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("No parameters specified in SESSION CREATE message"); return writeString(SESSION_ERROR, "No parameters for SESSION CREATE"); } dest = (String) props.remove("DESTINATION"); if (dest == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("SESSION DESTINATION parameter not specified"); return writeString(SESSION_ERROR, "DESTINATION not specified"); } if (dest.equals("TRANSIENT")) { if (_log.shouldLog(Log.DEBUG)) _log.debug("TRANSIENT destination requested"); String sigTypeStr = (String) props.remove("SIGNATURE_TYPE"); SigType sigType; if (sigTypeStr != null) { sigType = SigType.parseSigType(sigTypeStr); if (sigType == null) { return writeString(SESSION_ERROR, "SIGNATURE_TYPE " + sigTypeStr + " unsupported"); } } else { sigType = SigType.DSA_SHA1; } ByteArrayOutputStream priv = new ByteArrayOutputStream(663); SAMUtils.genRandomKey(priv, null, sigType); dest = Base64.encode(priv.toByteArray()); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Custom destination specified [" + dest + "]"); if (!SAMUtils.checkPrivateDestination(dest)) return writeString("SESSION STATUS RESULT=INVALID_KEY\n"); } // Unconditionally override what the client may have set // (iMule sets BestEffort) as None is more efficient // and the client has no way to access delivery notifications i2cpProps.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_NONE); i2cpProps.setProperty("i2cp.fastReceive", "true"); // Record the session in the database sSessionsHash Properties allProps = new Properties(); allProps.putAll(i2cpProps); allProps.putAll(props); if (style.equals("MASTER")) { // We must put these here, as SessionRecord.getProps() makes a copy, // and the socket manager is instantiated in the // SAMStreamSession constructor. allProps.setProperty("i2p.streaming.enforceProtocol", "true"); allProps.setProperty("i2cp.dontPublishLeaseSet", "false"); } try { sSessionsHash.put( nick, new SessionRecord(dest, allProps, this) ) ; } catch (SessionsDB.ExistingIdException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("SESSION ID parameter already in use"); return writeString("SESSION STATUS RESULT=DUPLICATED_ID\n"); } catch (SessionsDB.ExistingDestException e) { return writeString("SESSION STATUS RESULT=DUPLICATED_DEST\n"); } // Create the session if (style.equals("RAW")) { SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props); SAMv3RawSession v3 = new SAMv3RawSession(nick, dgs); rawSession = v3; this.session = v3; v3.start(); } else if (style.equals("DATAGRAM")) { SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props); SAMv3DatagramSession v3 = new SAMv3DatagramSession(nick, dgs); datagramSession = v3; this.session = v3; v3.start(); } else if (style.equals("STREAM")) { SAMv3StreamSession v3 = newSAMStreamSession(nick); streamSession = v3; this.session = v3; v3.start(); } else if (style.equals("MASTER")) { SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props); MasterSession v3 = new MasterSession(nick, dgs, this, allProps); streamSession = v3; datagramSession = v3; rawSession = v3; this.session = v3; v3.start(); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Unrecognized SESSION STYLE: \"" + style +"\""); return writeString(SESSION_ERROR, "Unrecognized SESSION STYLE"); } ok = true ; return writeString("SESSION STATUS RESULT=OK DESTINATION=" + dest + "\n"); } else if (opcode.equals("ADD") || opcode.equals("REMOVE")) { // prevent trouble in finally block ok = true; if (streamSession == null || datagramSession == null || rawSession == null) return writeString(SESSION_ERROR, "Not a MASTER session"); MasterSession msess = (MasterSession) session; String msg; if (opcode.equals("ADD")) { msg = msess.add(nick, style, props); } else { msg = msess.remove(nick, props); } if (msg == null) return writeString("SESSION STATUS RESULT=OK ID=\"" + nick + '"', opcode + ' ' + nick); else return writeString(SESSION_ERROR + " ID=\"" + nick + '"', msg); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Unrecognized SESSION message opcode: \"" + opcode + "\""); return writeString(SESSION_ERROR, "Unrecognized opcode"); } } catch (DataFormatException e) { _log.error("Invalid SAM destination specified", e); return writeString("SESSION STATUS RESULT=INVALID_KEY", e.getMessage()); } catch (I2PSessionException e) { _log.error("Failed to start SAM session", e); return writeString(SESSION_ERROR, e.getMessage()); } catch (SAMException e) { _log.error("Failed to start SAM session", e); return writeString(SESSION_ERROR, e.getMessage()); } catch (IOException e) { _log.error("Failed to start SAM session", e); return writeString(SESSION_ERROR, e.getMessage()); } finally { // unregister the session if it has not been created if ( !ok && nick!=null ) { sSessionsHash.del(nick) ; session = null ; } } } /** * @throws NPE if login nickname is not registered */ private static SAMv3StreamSession newSAMStreamSession(String login ) throws IOException, DataFormatException, SAMException { return new SAMv3StreamSession( login ) ; } /* Parse and execute a STREAM message */ @Override protected boolean execStreamMessage ( String opcode, Properties props ) { String nick = null ; SessionRecord rec = null ; if ( session != null ) { _log.error("v3 control socket cannot be used for STREAM"); try { notifyStreamResult(true, "I2P_ERROR", "v3 control socket cannot be used for STREAM"); } catch (IOException e) {} return false; } nick = (String) props.remove("ID"); if (nick == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("SESSION ID parameter not specified"); try { notifyStreamResult(true, "I2P_ERROR", "ID not specified"); } catch (IOException e) {} return false ; } rec = sSessionsHash.get(nick); if ( rec==null ) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM SESSION ID does not exist"); try { notifyStreamResult(true, "INVALID_ID", "STREAM SESSION ID " + nick + " does not exist"); } catch (IOException e) {} return false ; } streamSession = rec.getHandler().streamSession ; if (streamSession==null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("specified ID is not a stream session"); try { notifyStreamResult(true, "I2P_ERROR", "specified ID " + nick + " is not a STREAM session"); } catch (IOException e) {} return false ; } if ( opcode.equals ( "CONNECT" ) ) { return execStreamConnect ( props ); } else if ( opcode.equals ( "ACCEPT" ) ) { return execStreamAccept ( props ); } else if ( opcode.equals ( "FORWARD") ) { return execStreamForwardIncoming( props ); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug ( "Unrecognized STREAM message opcode: \"" + opcode + "\"" ); try { notifyStreamResult(true, "I2P_ERROR", "Unrecognized STREAM message opcode: "+opcode ); } catch (IOException e) {} return false; } } @Override protected boolean execStreamConnect( Properties props) { // Messages are NOT sent if SILENT=true, // The specs said that they were. boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT")); try { if (props.isEmpty()) { notifyStreamResult(verbose, "I2P_ERROR","No parameters specified in STREAM CONNECT message"); if (_log.shouldLog(Log.DEBUG)) _log.debug("No parameters specified in STREAM CONNECT message"); return false; } String dest = (String) props.remove("DESTINATION"); if (dest == null) { notifyStreamResult(verbose, "I2P_ERROR", "Destination not specified in STREAM CONNECT message"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Destination not specified in STREAM CONNECT message"); return false; } try { ((SAMv3StreamSession)streamSession).connect( this, dest, props ); return true ; } catch (DataFormatException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Invalid destination in STREAM CONNECT message"); notifyStreamResult ( verbose, "INVALID_KEY", e.getMessage()); } catch (ConnectException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamResult ( verbose, "CONNECTION_REFUSED", e.getMessage()); } catch (NoRouteToHostException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamResult ( verbose, "CANT_REACH_PEER", e.getMessage()); } catch (InterruptedIOException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamResult ( verbose, "TIMEOUT", e.getMessage()); } catch (I2PException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamResult ( verbose, "I2P_ERROR", e.getMessage() ); } } catch (IOException e) { } return false ; } private boolean execStreamForwardIncoming( Properties props ) { // Messages ARE sent if SILENT=true, // which is different from CONNECT and ACCEPT. // But this matched the specs. try { try { streamForwardingSocket = true ; ((SAMv3StreamSession)streamSession).startForwardingIncoming(props, sendPorts); notifyStreamResult( true, "OK", null ); return true ; } catch (SAMException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Forwarding STREAM connections failed", e); notifyStreamResult ( true, "I2P_ERROR", "Forwarding failed : " + e.getMessage() ); } } catch (IOException e) { } return false ; } private boolean execStreamAccept( Properties props ) { // Messages are NOT sent if SILENT=true, // The specs said that they were. boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT")); try { try { notifyStreamResult(verbose, "OK", null); ((SAMv3StreamSession)streamSession).accept(this, verbose); return true ; } catch (InterruptedIOException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM ACCEPT failed", e); notifyStreamResult( verbose, "TIMEOUT", e.getMessage() ); } catch (I2PException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM ACCEPT failed", e); notifyStreamResult ( verbose, "I2P_ERROR", e.getMessage() ); } catch (SAMException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM ACCEPT failed", e); notifyStreamResult ( verbose, "ALREADY_ACCEPTING", e.getMessage()); } } catch (IOException e) { } return false ; } /** * @param verbose if false, does nothing * @param result non-null * @param message may be null */ public void notifyStreamResult(boolean verbose, String result, String message) throws IOException { if (!verbose) return ; String msgString = createMessageString(message); String out = "STREAM STATUS RESULT=" + result + msgString + '\n'; if (!writeString(out)) { throw new IOException ( "Error notifying connection to SAM client" ); } } public void notifyStreamIncomingConnection(Destination d, int fromPort, int toPort) throws IOException { if (getStreamSession() == null) { _log.error("BUG! Received stream connection, but session is null!"); throw new NullPointerException("BUG! STREAM session is null!"); } StringBuilder buf = new StringBuilder(600); buf.append(d.toBase64()); if (sendPorts) { buf.append(" FROM_PORT=").append(fromPort).append(" TO_PORT=").append(toPort); } buf.append('\n'); if (!writeString(buf.toString())) { throw new IOException("Error notifying connection to SAM client"); } } public static void notifyStreamIncomingConnection(SocketChannel client, Destination d) throws IOException { if (!writeString(d.toBase64() + "\n", client)) { throw new IOException("Error notifying connection to SAM client"); } } /** @since 0.9.24 */ public static void notifyStreamIncomingConnection(SocketChannel client, Destination d, int fromPort, int toPort) throws IOException { if (!writeString(d.toBase64() + " FROM_PORT=" + fromPort + " TO_PORT=" + toPort + '\n', client)) { throw new IOException("Error notifying connection to SAM client"); } } /** @since 0.9.24 */ private boolean execAuthMessage(String opcode, Properties props) { if (opcode.equals("ENABLE")) { i2cpProps.setProperty(SAMBridge.PROP_AUTH, "true"); } else if (opcode.equals("DISABLE")) { i2cpProps.setProperty(SAMBridge.PROP_AUTH, "false"); } else if (opcode.equals("ADD")) { String user = props.getProperty("USER"); String pw = props.getProperty("PASSWORD"); if (user == null || pw == null) return writeString(AUTH_ERROR, "USER and PASSWORD required"); String prop = SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX; if (i2cpProps.containsKey(prop)) return writeString(AUTH_ERROR, "user " + user + " already exists"); PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext()); String shash = pm.createHash(pw); i2cpProps.setProperty(prop, shash); } else if (opcode.equals("REMOVE")) { String user = props.getProperty("USER"); if (user == null) return writeString(AUTH_ERROR, "USER required"); String prop = SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX; if (!i2cpProps.containsKey(prop)) return writeString(AUTH_ERROR, "user " + user + " not found"); i2cpProps.remove(prop); } else { return writeString(AUTH_ERROR, "Unknown AUTH command"); } try { bridge.saveConfig(); return writeString("AUTH STATUS RESULT=OK\n"); } catch (IOException ioe) { return writeString(AUTH_ERROR, "Config save failed: " + ioe); } } /** * Handle a PING. * Send a PONG. * * @param msg to append, may be null * @since 0.9.24 */ private void execPingMessage(String msg) { StringBuilder buf = new StringBuilder(); buf.append("PONG"); if (msg != null) { buf.append(' ').append(msg); } buf.append('\n'); writeString(buf.toString()); } /** * Handle a PONG. * * @param s received, may be null * @since 0.9.24 */ private void execPongMessage(String s) { if (s == null) { s = ""; } if (_lastPing > 0) { String expected = Long.toString(_lastPing); if (expected.equals(s)) { _lastPing = 0; if (_log.shouldInfo()) _log.warn("Got expected pong: " + s); } else { if (_log.shouldInfo()) _log.warn("Got unexpected pong: " + s); } } else { if (_log.shouldWarn()) _log.warn("Pong received without a ping: " + s); } } }