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);
}
}
}