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.DataInputStream; import java.io.EOFException; import java.io.IOException; import java.io.InterruptedIOException; import java.net.ConnectException; import java.net.NoRouteToHostException; import java.net.Socket; import java.net.SocketTimeoutException; import java.nio.channels.SocketChannel; import java.nio.ByteBuffer; import java.util.Properties; import java.util.concurrent.atomic.AtomicLong; 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; /** * Class able to handle a SAM version 1 client connections. * * @author human */ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramReceiver, SAMStreamReceiver { protected SAMMessageSess rawSession; protected SAMMessageSess datagramSession; protected SAMStreamSession streamSession; protected final SAMMessageSess getRawSession() { return rawSession; } protected final SAMMessageSess getDatagramSession() { return datagramSession; } protected final SAMStreamSession getStreamSession() { return streamSession; } protected final long _id; private static final AtomicLong __id = new AtomicLong(); private static final int FIRST_READ_TIMEOUT = 60*1000; protected static final String SESSION_ERROR = "SESSION STATUS RESULT=I2P_ERROR"; /** * Create a new SAM version 1 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 1) * @param verMinor SAM minor version to manage * @throws SAMException * @throws IOException */ public SAMv1Handler(SocketChannel s, int verMajor, int verMinor, SAMBridge parent) throws SAMException, IOException { this(s, verMajor, verMinor, new Properties(), parent); } /** * Create a new SAM version 1 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 1) * @param verMinor SAM minor version to manage * @param i2cpProps properties to configure the I2CP connection (host, port, etc) * @throws SAMException * @throws IOException */ public SAMv1Handler(SocketChannel s, int verMajor, int verMinor, Properties i2cpProps, SAMBridge parent) throws SAMException, IOException { super(s, verMajor, verMinor, i2cpProps, parent); _id = __id.incrementAndGet(); if (_log.shouldLog(Log.DEBUG)) _log.debug("SAM version 1 handler instantiated"); if ( ! verifVersion() ) { throw new SAMException("BUG! Wrong protocol version!"); } } public boolean verifVersion() { return (verMajor == 1); } public void handle() { String msg = null; String domain = null; String opcode = null; boolean canContinue = false; Properties props; final StringBuilder buf = new StringBuilder(128); this.thread.setName("SAMv1Handler " + _id); if (_log.shouldLog(Log.DEBUG)) _log.debug("SAM handling started"); try { boolean gotFirstLine = false; while (true) { if (shouldStop()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Stop request found"); break; } SocketChannel clientSocketChannel = getClientSocket() ; if (clientSocketChannel == null) { _log.info("Connection closed by client"); break; } if (clientSocketChannel.socket() == null) { _log.info("Connection closed by client"); break; } buf.setLength(0); // first time, set a timeout try { Socket sock = clientSocketChannel.socket(); ReadLine.readLine(sock, buf, gotFirstLine ? 0 : FIRST_READ_TIMEOUT); sock.setSoTimeout(0); } catch (SocketTimeoutException ste) { writeString(SESSION_ERROR, "command timeout, bye"); break; } msg = buf.toString(); if (_log.shouldLog(Log.DEBUG)) { _log.debug("New message received: [" + msg + ']'); } props = SAMUtils.parseParams(msg); domain = (String) props.remove(SAMUtils.COMMAND); if (domain == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Ignoring newline"); continue; } opcode = (String) props.remove(SAMUtils.OPCODE); if (opcode == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Error in message format"); break; } if (_log.shouldLog(Log.DEBUG)) { _log.debug("Parsing (domain: \"" + domain + "\"; opcode: \"" + opcode + "\")"); } gotFirstLine = true; if (domain.equals("STREAM")) { canContinue = execStreamMessage(opcode, props); } else if (domain.equals("DATAGRAM")) { canContinue = execDatagramMessage(opcode, props); } else if (domain.equals("RAW")) { canContinue = execRawMessage(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 (_log.shouldLog(Log.DEBUG)) _log.debug("Unrecognized message domain: \"" + domain + "\""); break; } if (!canContinue) { break; } } } catch (IOException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Caught IOException for message [" + msg + "]", e); } catch (SAMException e) { _log.error("Unexpected exception for message [" + msg + "]", e); } catch (RuntimeException e) { _log.error("Unexpected exception for message [" + msg + "]", e); } finally { if (_log.shouldLog(Log.DEBUG)) _log.debug("Stopping handler"); try { closeClientSocket(); } catch (IOException e) { if (_log.shouldWarn()) _log.warn("Error closing socket", e); } if (rawSession != null) { rawSession.close(); } if (datagramSession != null) { datagramSession.close(); } if (streamSession != null) { streamSession.close(); } } } /* Parse and execute a SESSION message */ protected boolean execSessionMessage(String opcode, Properties props) { String dest = "BUG!"; try{ if (opcode.equals("CREATE")) { if ((rawSession != null) || (datagramSession != null) || (streamSession != 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"); } String destKeystream = null; if (dest.equals("TRANSIENT")) { _log.debug("TRANSIENT destination requested"); ByteArrayOutputStream priv = new ByteArrayOutputStream(640); SAMUtils.genRandomKey(priv, null); destKeystream = Base64.encode(priv.toByteArray()); } else { destKeystream = bridge.getKeystream(dest); if (destKeystream == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Custom destination specified [" + dest + "] but it isn't known, creating a new one"); ByteArrayOutputStream baos = new ByteArrayOutputStream(640); SAMUtils.genRandomKey(baos, null); destKeystream = Base64.encode(baos.toByteArray()); bridge.addKeystream(dest, destKeystream); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Custom destination specified [" + dest + "] and it is already known"); } } String style = (String) props.remove("STYLE"); if (style == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("SESSION STYLE parameter not specified"); return writeString(SESSION_ERROR, "No SESSION STYLE specified"); } // 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 props.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_NONE); if (style.equals("RAW")) { rawSession = new SAMRawSession(destKeystream, props, this); rawSession.start(); } else if (style.equals("DATAGRAM")) { datagramSession = new SAMDatagramSession(destKeystream, props,this); datagramSession.start(); } else if (style.equals("STREAM")) { String dir = (String) props.remove("DIRECTION"); if (dir == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("No DIRECTION parameter in STREAM session, defaulting to BOTH"); dir = "BOTH"; } else if (!dir.equals("CREATE") && !dir.equals("RECEIVE") && !dir.equals("BOTH")) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Unknown DIRECTION parameter value: [" + dir + "]"); return writeString(SESSION_ERROR, "Unknown DIRECTION parameter"); } streamSession = newSAMStreamSession(destKeystream, dir,props); streamSession.start(); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Unrecognized SESSION STYLE: \"" + style +"\""); return writeString(SESSION_ERROR, "Unrecognized SESSION STYLE"); } return writeString("SESSION STATUS RESULT=OK DESTINATION=" + dest + "\n"); } 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()); } } private SAMStreamSession newSAMStreamSession(String destKeystream, String direction, Properties props ) throws IOException, DataFormatException, SAMException { return new SAMStreamSession(destKeystream, direction, props, this) ; } /* Parse and execute a DEST message*/ protected boolean execDestMessage(String opcode, Properties props) { if (opcode.equals("GENERATE")) { String sigTypeStr = props.getProperty("SIGNATURE_TYPE"); SigType sigType; if (sigTypeStr != null) { sigType = SigType.parseSigType(sigTypeStr); if (sigType == null) { writeString("DEST REPLY RESULT=I2P_ERROR MESSAGE=\"SIGNATURE_TYPE " + sigTypeStr + " unsupported\"\n"); return false; } } else { sigType = SigType.DSA_SHA1; } ByteArrayOutputStream priv = new ByteArrayOutputStream(663); ByteArrayOutputStream pub = new ByteArrayOutputStream(387); SAMUtils.genRandomKey(priv, pub, sigType); return writeString("DEST REPLY" + " PUB=" + Base64.encode(pub.toByteArray()) + " PRIV=" + Base64.encode(priv.toByteArray()) + "\n"); } else { writeString("DEST REPLY RESULT=I2P_ERROR MESSAGE=\"DEST GENERATE required\""); if (_log.shouldLog(Log.DEBUG)) _log.debug("Unrecognized DEST message opcode: \"" + opcode + "\""); return false; } } /* Parse and execute a NAMING message */ protected boolean execNamingMessage(String opcode, Properties props) { if (opcode.equals("LOOKUP")) { String name = props.getProperty("NAME"); if (name == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Name to resolve not specified in NAMING message"); return writeString("NAMING REPLY RESULT=KEY_NOT_FOUND NAME=\"\" MESSAGE=\"Must specify NAME\"\n"); } Destination dest = null ; if (name.equals("ME")) { if (rawSession != null) { dest = rawSession.getDestination(); } else if (streamSession != null) { dest = streamSession.getDestination(); } else if (datagramSession != null) { dest = datagramSession.getDestination(); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Lookup for SESSION destination, but session is null"); return false; } } else { try { dest = SAMUtils.getDest(name); } catch (DataFormatException e) { } } if (dest == null) { return writeString("NAMING REPLY RESULT=KEY_NOT_FOUND NAME=" + name + "\n"); } return writeString("NAMING REPLY RESULT=OK NAME=" + name + " VALUE=" + dest.toBase64() + "\n"); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Unrecognized NAMING message opcode: \"" + opcode + "\""); return false; } } /* Parse and execute a DATAGRAM message */ protected boolean execDatagramMessage(String opcode, Properties props) { if (datagramSession == null) { _log.error("DATAGRAM message received, but no DATAGRAM session exists"); return false; } return execDgOrRawMessage(false, opcode, props); } /* Parse and execute a RAW message */ protected boolean execRawMessage(String opcode, Properties props) { if (rawSession == null) { _log.error("RAW message received, but no RAW session exists"); return false; } return execDgOrRawMessage(true, opcode, props); } /* * Parse and execute a RAW or DATAGRAM SEND message. * This is for v1/v2 compatible sending only. * For v3 sending, see SAMv3DatagramServer. * * Note that props are from the command line only. * Session defaults from CREATE are NOT honored here. * FIXME if we care, but nobody's probably using v3.2 options for v1/v2 sending. * * @since 0.9.25 consolidated from execDatagramMessage() and execRawMessage() */ private boolean execDgOrRawMessage(boolean isRaw, String opcode, Properties props) { if (opcode.equals("SEND")) { if (props.isEmpty()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("No parameters specified in SEND message"); return false; } String dest = props.getProperty("DESTINATION"); if (dest == null) { if (_log.shouldWarn()) _log.warn("Destination not specified in SEND message"); return false; } int size; String strsize = props.getProperty("SIZE"); if (strsize == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Size not specified in SEND message"); return false; } try { size = Integer.parseInt(strsize); } catch (NumberFormatException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Invalid SEND size specified: " + strsize); return false; } boolean ok = isRaw ? checkSize(size) : checkDatagramSize(size); if (!ok) { if (_log.shouldLog(Log.WARN)) _log.warn("Specified size (" + size + ") is out of protocol limits"); return false; } int fromPort = I2PSession.PORT_UNSPECIFIED; int toPort = I2PSession.PORT_UNSPECIFIED; int proto; if (isRaw) { proto = I2PSession.PROTO_DATAGRAM_RAW; String s = props.getProperty("PROTOCOL"); if (s != null) { try { proto = Integer.parseInt(s); } catch (NumberFormatException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Invalid SEND protocol specified: " + s); } } } else { proto = I2PSession.PROTO_DATAGRAM; } String s = props.getProperty("FROM_PORT"); if (s != null) { try { fromPort = Integer.parseInt(s); } catch (NumberFormatException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Invalid SEND port specified: " + s); } } s = props.getProperty("TO_PORT"); if (s != null) { try { toPort = Integer.parseInt(s); } catch (NumberFormatException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Invalid SEND port specified: " + s); } } try { DataInputStream in = new DataInputStream(getClientSocket().socket().getInputStream()); byte[] data = new byte[size]; in.readFully(data); SAMMessageSess sess = isRaw ? rawSession : datagramSession; if (!sess.sendBytes(dest, data, proto, fromPort, toPort)) { if (_log.shouldWarn()) _log.warn((isRaw ? "SEND RAW to " : "SEND DATAGRAM to ") + dest + " size " + size + " failed"); // a message send failure is no reason to drop the SAM session // for raw and repliable datagrams, just carry on our merry way } return true; } catch (EOFException e) { if (_log.shouldWarn()) _log.warn("Too few bytes with SEND message (expected: " + size, e); return false; } catch (IOException e) { if (_log.shouldWarn()) _log.warn("Caught IOException while parsing SEND message", e); return false; } catch (DataFormatException e) { if (_log.shouldWarn()) _log.warn("Invalid key specified with SEND message", e); return false; } catch (I2PSessionException e) { _log.error("Session error with SEND message", e); return false; } } else { if (_log.shouldWarn()) _log.warn("Unrecognized message opcode: \"" + opcode + "\""); return false; } } /* Parse and execute a STREAM message */ protected boolean execStreamMessage(String opcode, Properties props) { if (streamSession == null) { _log.error("STREAM message received, but no STREAM session exists"); return false; } if (opcode.equals("SEND")) { return execStreamSend(props); } else if (opcode.equals("CONNECT")) { return execStreamConnect(props); } else if (opcode.equals("CLOSE")) { return execStreamClose(props); } else { if (_log.shouldWarn()) _log.warn("Unrecognized STREAM message opcode: \"" + opcode + "\""); return false; } } protected boolean execStreamSend(Properties props) { if (props.isEmpty()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("No parameters specified in STREAM SEND message"); return false; } int id; { String strid = props.getProperty("ID"); if (strid == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("ID not specified in STREAM SEND message"); return false; } try { id = Integer.parseInt(strid); } catch (NumberFormatException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Invalid STREAM SEND ID specified: " + strid); return false; } } int size; { String strsize = props.getProperty("SIZE"); if (strsize == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Size not specified in STREAM SEND message"); return false; } try { size = Integer.parseInt(strsize); } catch (NumberFormatException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Invalid STREAM SEND size specified: "+strsize); return false; } if (!checkSize(size)) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Specified size (" + size + ") is out of protocol limits"); return false; } } try { if (!streamSession.sendBytes(id, getClientSocket().socket().getInputStream(), size)) { // data)) { if (_log.shouldLog(Log.WARN)) _log.warn("STREAM SEND [" + size + "] failed"); // a message send failure is no reason to drop the SAM session // for style=stream, tell the client the stream failed, and kill the virtual connection.. boolean rv = writeString("STREAM CLOSED RESULT=CANT_REACH_PEER ID=" + id + " MESSAGE=\"Send of " + size + " bytes failed\"\n"); streamSession.closeConnection(id); return rv; } return true; } catch (EOFException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Too few bytes with STREAM SEND message (expected: " + size); return false; } catch (IOException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Caught IOException while parsing STREAM SEND message", e); return false; } } protected boolean execStreamConnect(Properties props) { if (props.isEmpty()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("No parameters specified in STREAM CONNECT message"); return false; } int id; { String strid = (String) props.remove("ID"); if (strid == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("ID not specified in STREAM SEND message"); return false; } try { id = Integer.parseInt(strid); } catch (NumberFormatException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Invalid STREAM CONNECT ID specified: " +strid); return false; } if (id < 1) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Invalid STREAM CONNECT ID specified: " +strid); return false; } } String dest = (String) props.remove("DESTINATION"); if (dest == null) { _log.debug("Destination not specified in RAW SEND message"); return false; } try { try { if (!streamSession.connect(id, dest, props)) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM connection failed"); return false; } } catch (DataFormatException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Invalid destination in STREAM CONNECT message"); notifyStreamOutgoingConnection ( id, "INVALID_KEY", null ); } catch (SAMInvalidDirectionException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamOutgoingConnection ( id, "INVALID_DIRECTION", null ); } catch (ConnectException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamOutgoingConnection ( id, "CONNECTION_REFUSED", null ); } catch (NoRouteToHostException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamOutgoingConnection ( id, "CANT_REACH_PEER", null ); } catch (InterruptedIOException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamOutgoingConnection ( id, "TIMEOUT", null ); } catch (I2PException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("STREAM CONNECT failed", e); notifyStreamOutgoingConnection ( id, "I2P_ERROR", null ); } } catch (IOException e) { return false ; } return true ; } protected boolean execStreamClose(Properties props) { if (props.isEmpty()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("No parameters specified in STREAM CLOSE message"); return false; } int id; { String strid = props.getProperty("ID"); if (strid == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("ID not specified in STREAM CLOSE message"); return false; } try { id = Integer.parseInt(strid); } catch (NumberFormatException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Invalid STREAM CLOSE ID specified: " +strid); return false; } } boolean closed = streamSession.closeConnection(id); if ( (!closed) && (_log.shouldLog(Log.WARN)) ) _log.warn("Stream unable to be closed, but this is non fatal"); return true; } /* Check whether a size is inside the limits allowed by this protocol */ private boolean checkSize(int size) { return ((size >= 1) && (size <= 32768)); } /* Check whether a size is inside the limits allowed by this protocol */ private boolean checkDatagramSize(int size) { return ((size >= 1) && (size <= 31744)); } // SAMRawReceiver implementation public void receiveRawBytes(byte data[], int proto, int fromPort, int toPort) throws IOException { if (rawSession == null) { _log.error("BUG! Received raw bytes, but session is null!"); return; } ByteArrayOutputStream msg = new ByteArrayOutputStream(64 + data.length); String msgText = "RAW RECEIVED SIZE=" + data.length; msg.write(DataHelper.getASCII(msgText)); if ((verMajor == 3 && verMinor >= 2) || verMajor > 3) { msgText = " PROTOCOL=" + proto + " FROM_PORT=" + fromPort + " TO_PORT=" + toPort; msg.write(DataHelper.getASCII(msgText)); } msg.write((byte) '\n'); msg.write(data); if (_log.shouldLog(Log.DEBUG)) _log.debug("sending to client: " + msgText); writeBytes(ByteBuffer.wrap(msg.toByteArray())); } public void stopRawReceiving() { if (_log.shouldLog(Log.DEBUG)) _log.debug("stopRawReceiving() invoked"); if (rawSession == null) { _log.error("BUG! Got raw receiving stop, but session is null!"); return; } try { closeClientSocket(); } catch (IOException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Error closing socket", e); } } // SAMDatagramReceiver implementation public void receiveDatagramBytes(Destination sender, byte data[], int proto, int fromPort, int toPort) throws IOException { if (datagramSession == null) { _log.error("BUG! Received datagram bytes, but session is null!"); return; } ByteArrayOutputStream msg = new ByteArrayOutputStream(100 + data.length); String msgText = "DATAGRAM RECEIVED DESTINATION=" + sender.toBase64() + " SIZE=" + data.length; msg.write(DataHelper.getASCII(msgText)); if ((verMajor == 3 && verMinor >= 2) || verMajor > 3) { msgText = " FROM_PORT=" + fromPort + " TO_PORT=" + toPort; msg.write(DataHelper.getASCII(msgText)); } msg.write((byte) '\n'); if (_log.shouldLog(Log.DEBUG)) _log.debug("sending to client: " + msgText); msg.write(data); msg.flush(); writeBytes(ByteBuffer.wrap(msg.toByteArray())); } public void stopDatagramReceiving() { if (_log.shouldLog(Log.DEBUG)) _log.debug("stopDatagramReceiving() invoked"); if (datagramSession == null) { _log.error("BUG! Got datagram receiving stop, but session is null!"); return; } try { closeClientSocket(); } catch (IOException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Error closing socket", e); } } // SAMStreamReceiver implementation public void streamSendAnswer( int id, String result, String bufferState ) throws IOException { if ( streamSession == null ) { _log.error ( "BUG! Want to answer to stream SEND, but session is null!" ); return; } if ( !writeString ( "STREAM SEND ID=" + id + " RESULT=" + result + " STATE=" + bufferState + "\n" ) ) { throw new IOException ( "Error notifying connection to SAM client" ); } } public void notifyStreamSendBufferFree( int id ) throws IOException { if ( streamSession == null ) { _log.error ( "BUG! Stream outgoing buffer is free, but session is null!" ); return; } if ( !writeString ( "STREAM READY_TO_SEND ID=" + id + "\n" ) ) { throw new IOException ( "Error notifying connection to SAM client" ); } } public void notifyStreamIncomingConnection(int id, Destination d) throws IOException { if (streamSession == null) { _log.error("BUG! Received stream connection, but session is null!"); return; } if (!writeString("STREAM CONNECTED DESTINATION=" + d.toBase64() + " ID=" + id + "\n")) { throw new IOException("Error notifying connection to SAM client"); } } /** @param msg may be null */ public void notifyStreamOutgoingConnection ( int id, String result, String msg ) throws IOException { if ( streamSession == null ) { _log.error ( "BUG! Received stream connection, but session is null!" ); return; } String msgString = createMessageString(msg); if ( !writeString ( "STREAM STATUS RESULT=" + result + " ID=" + id + msgString + "\n" ) ) { throw new IOException ( "Error notifying connection to SAM client" ); } } /** * Create a string to be appended to a status. * * @param msg may be null * @return non-null, "" if msg is null, MESSAGE=msg or MESSAGE="msg a b c" * with leading space if msg is non-null * @since 0.9.20 */ protected static String createMessageString(String msg) { String rv; if ( msg != null ) { msg = msg.replace("\n", " "); msg = msg.replace("\r", " "); if (!msg.startsWith("\"")) { msg = msg.replace("\"", ""); if (msg.contains(" ") || msg.contains("\t")) msg = '"' + msg + '"'; } rv = " MESSAGE=" + msg; } else { rv = ""; } return rv; } /** * Write a string and message, escaping the message. * Writes s + createMessageString(msg) + \n * * @param s The string, non-null * @since 0.9.25 */ protected boolean writeString(String s, String msg) { return writeString(s + createMessageString(msg) + '\n'); } public void receiveStreamBytes(int id, ByteBuffer data) throws IOException { if (streamSession == null) { _log.error("Received stream bytes, but session is null!"); return; } String msgText = "STREAM RECEIVED ID=" + id +" SIZE=" + data.remaining() + "\n"; if (_log.shouldLog(Log.DEBUG)) _log.debug("sending to client: " + msgText); ByteBuffer prefix = ByteBuffer.wrap(DataHelper.getASCII(msgText)); Object writeLock = getWriteLock(); synchronized (writeLock) { while (prefix.hasRemaining()) socket.write(prefix); while (data.hasRemaining()) socket.write(data); socket.socket().getOutputStream().flush(); } } /** @param msg may be null */ public void notifyStreamDisconnection(int id, String result, String msg) throws IOException { if (streamSession == null) { _log.error("BUG! Received stream disconnection, but session is null!"); return; } String msgString = createMessageString(msg); if (!writeString("STREAM CLOSED ID=" + id + " RESULT=" + result + msgString + '\n')) { throw new IOException("Error notifying disconnection to SAM client"); } } public void stopStreamReceiving() { if (_log.shouldLog(Log.DEBUG)) _log.debug("stopStreamReceiving() invoked", new Exception("stopped")); if (streamSession == null) { _log.error("BUG! Got stream receiving stop, but session is null!"); return; } try { closeClientSocket(); } catch (IOException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Error closing socket", e); } } }