package net.i2p.client.impl;
/*
* public domain
*/
import java.io.InputStream;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionListener;
import net.i2p.client.I2PSessionMuxedListener;
import net.i2p.client.SendMessageOptions;
import net.i2p.client.SendMessageStatusListener;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import net.i2p.data.i2cp.MessagePayloadMessage;
import net.i2p.util.Log;
/**
* I2PSession with protocol and ports
*
* Streaming lib has been modified to send I2PSession.PROTO_STREAMING but
* still receives all. It sends with fromPort and toPort = 0, and receives on all ports.
*
* No datagram apps have been modified yet.
* Therefore the compatibility situation is as follows:
*
* Compatibility:
* old streaming -> new streaming: sends proto anything, rcvs proto anything
* new streaming -> old streaming: sends PROTO_STREAMING, ignores rcvd proto
* old datagram -> new datagram: sends proto anything, rcvs proto anything
* new datagram -> old datagram: sends PROTO_DATAGRAM, ignores rcvd proto
* In all the above cases, streaming and datagram receive traffic for the other
* protocol, same as before.
*
* old datagram -> new muxed: doesn't work because the old sends proto 0 but the udp side
* of the mux registers with PROTO_DATAGRAM, so the datagrams
* go to the streaming side, same as before.
* old streaming -> new muxed: works
*
* Typical Usage:
* Streaming + datagrams:
* I2PSocketManager sockMgr = getSocketManager();
* I2PSession session = sockMgr.getSession();
* session.addMuxedSessionListener(myI2PSessionMuxedListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);
* * or *
* session.addSessionListener(myI2PSessionListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);
* session.sendMessage(dest, payload, I2PSession.PROTO_DATAGRAM, fromPort, toPort);
*
* Datagrams only, with multiple ports:
* I2PClient client = I2PClientFactory.createClient();
* ...
* I2PSession session = client.createSession(...);
* session.addMuxedSessionListener(myI2PSessionMuxedListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);
* * or *
* session.addSessionListener(myI2PSessionListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);
* session.sendMessage(dest, payload, I2PSession.PROTO_DATAGRAM, fromPort, toPort);
*
* Multiple streaming ports:
* Needs some streaming lib hacking
*
* @author zzz
* @since 0.7.1
*/
class I2PSessionMuxedImpl extends I2PSessionImpl2 {
private final I2PSessionDemultiplexer _demultiplexer;
/*
* @param destKeyStream stream containing the private key data,
* format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
* @param options set of options to configure the router with, if null will use System properties
*/
public I2PSessionMuxedImpl(I2PAppContext ctx, InputStream destKeyStream, Properties options) throws I2PSessionException {
super(ctx, destKeyStream, options);
// also stored in _sessionListener but we keep it in _demultipexer
// as well so we don't have to keep casting
_demultiplexer = new I2PSessionDemultiplexer(ctx);
super.setSessionListener(_demultiplexer);
// discards the one in super(), sorry about that... (no it wasn't started yet)
_availabilityNotifier = new MuxedAvailabilityNotifier();
}
/*
* For extension by SubSession
*
* @param destKeyStream stream containing the private key data,
* format is specified in {@link net.i2p.data.PrivateKeyFile PrivateKeyFile}
* @param options set of options to configure the router with, if null will use System properties
* @since 0.9.21
*/
protected I2PSessionMuxedImpl(I2PSessionImpl primary, InputStream destKeyStream, Properties options) throws I2PSessionException {
super(primary, destKeyStream, options);
// also stored in _sessionListener but we keep it in _demultipexer
// as well so we don't have to keep casting
_demultiplexer = new I2PSessionDemultiplexer(primary.getContext());
super.setSessionListener(_demultiplexer);
// discards the one in super(), sorry about that... (no it wasn't started yet)
_availabilityNotifier = new MuxedAvailabilityNotifier();
}
/** listen on all protocols and ports */
@Override
public void setSessionListener(I2PSessionListener lsnr) {
_demultiplexer.addListener(lsnr, PROTO_ANY, PORT_ANY);
}
/**
* Listen on specified protocol and port.
*
* An existing listener with the same proto and port is replaced.
* Only the listener with the best match is called back for each message.
*
* @param proto 1-254 or PROTO_ANY (0) for all; recommended:
* I2PSession.PROTO_STREAMING
* I2PSession.PROTO_DATAGRAM
* 255 disallowed
* @param port 1-65535 or PORT_ANY (0) for all
*/
@Override
public void addSessionListener(I2PSessionListener lsnr, int proto, int port) {
_demultiplexer.addListener(lsnr, proto, port);
}
/**
* Listen on specified protocol and port, and receive notification
* of proto, fromPort, and toPort for every message.
* @param proto 1-254 or PROTO_ANY (0) for all; 255 disallowed
* @param port 1-65535 or PORT_ANY (0) for all
*/
@Override
public void addMuxedSessionListener(I2PSessionMuxedListener l, int proto, int port) {
_demultiplexer.addMuxedListener(l, proto, port);
}
/** removes the specified listener (only) */
@Override
public void removeListener(int proto, int port) {
_demultiplexer.removeListener(proto, port);
}
@Override
public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException {
return sendMessage(dest, payload, 0, payload.length, null, null,
0, PROTO_UNSPECIFIED, PORT_UNSPECIFIED, PORT_UNSPECIFIED);
}
@Override
public boolean sendMessage(Destination dest, byte[] payload, int proto, int fromport, int toport) throws I2PSessionException {
return sendMessage(dest, payload, 0, payload.length, null, null, 0, proto, fromport, toport);
}
/**
* @param keyUsed unused - no end-to-end crypto
* @param tagsSent unused - no end-to-end crypto
*/
@Override
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size,
SessionKey keyUsed, Set<SessionTag> tagsSent, long expires)
throws I2PSessionException {
return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0, PROTO_UNSPECIFIED, PORT_UNSPECIFIED, PORT_UNSPECIFIED);
}
/**
* @param keyUsed unused - no end-to-end crypto
* @param tagsSent unused - no end-to-end crypto
*/
@Override
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set<SessionTag> tagsSent,
int proto, int fromport, int toport) throws I2PSessionException {
return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0, proto, fromport, toport);
}
/**
* @param keyUsed unused - no end-to-end crypto
* @param tagsSent unused - no end-to-end crypto
* @param proto 1-254 or 0 for unset; recommended:
* I2PSession.PROTO_UNSPECIFIED
* I2PSession.PROTO_STREAMING
* I2PSession.PROTO_DATAGRAM
* 255 disallowed
* @param fromPort 1-65535 or 0 for unset
* @param toPort 1-65535 or 0 for unset
* @since 0.7.1
*/
@Override
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size,
SessionKey keyUsed, Set<SessionTag> tagsSent, long expires,
int proto, int fromPort, int toPort)
throws I2PSessionException {
return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0, proto, fromPort, toPort, 0);
}
/**
* @param keyUsed unused - no end-to-end crypto
* @param tagsSent unused - no end-to-end crypto
* @param proto 1-254 or 0 for unset; recommended:
* I2PSession.PROTO_UNSPECIFIED
* I2PSession.PROTO_STREAMING
* I2PSession.PROTO_DATAGRAM
* 255 disallowed
* @param fromPort 1-65535 or 0 for unset
* @param toPort 1-65535 or 0 for unset
* @param flags to be passed to the router
* @since 0.8.4
*/
@Override
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size,
SessionKey keyUsed, Set<SessionTag> tagsSent, long expires,
int proto, int fromPort, int toPort, int flags)
throws I2PSessionException {
payload = prepPayload(payload, offset, size, proto, fromPort, toPort);
if (_noEffort)
return sendNoEffort(dest, payload, expires, flags);
else
return sendBestEffort(dest, payload, expires, flags);
}
/**
* See SendMessageOptions for option details.
*
* Always uses sendNoEffort for now.
*
* @param proto 1-254 or 0 for unset; recommended:
* I2PSession.PROTO_UNSPECIFIED
* I2PSession.PROTO_STREAMING
* I2PSession.PROTO_DATAGRAM
* 255 disallowed
* @param fromPort 1-65535 or 0 for unset
* @param toPort 1-65535 or 0 for unset
* @param options to be passed to the router
* @since 0.9.2
*/
@Override
public boolean sendMessage(Destination dest, byte[] payload, int offset, int size,
int proto, int fromPort, int toPort, SendMessageOptions options) throws I2PSessionException {
payload = prepPayload(payload, offset, size, proto, fromPort, toPort);
//if (_noEffort) {
sendNoEffort(dest, payload, options);
return true;
//} else {
// unimplemented
//return sendBestEffort(dest, payload, options);
//}
}
/**
* Send a message and request an asynchronous notification of delivery status.
*
* See I2PSessionMuxedImpl for proto/port details.
* See SendMessageOptions for option details.
*
* @return the message ID to be used for later notification to the listener
* @throws I2PSessionException on all errors
* @since 0.9.14
*/
@Override
public long sendMessage(Destination dest, byte[] payload, int offset, int size,
int proto, int fromPort, int toPort,
SendMessageOptions options, SendMessageStatusListener listener) throws I2PSessionException {
payload = prepPayload(payload, offset, size, proto, fromPort, toPort);
long nonce = _sendMessageNonce.incrementAndGet();
long expires = Math.max(_context.clock().now() + 60*1000L, options.getTime());
MessageState state = new MessageState(_context, nonce, this, expires, listener);
_sendingStates.put(Long.valueOf(nonce), state);
_producer.sendMessage(this, dest, nonce, payload, options);
return nonce;
}
/**
* @return gzip compressed payload, ready to send
* @since 0.9.14
*/
private byte[] prepPayload(byte[] payload, int offset, int size, int proto, int fromPort, int toPort) throws I2PSessionException {
verifyOpen();
updateActivity();
if (shouldCompress(size))
payload = DataHelper.compress(payload, offset, size);
else
payload = DataHelper.compress(payload, offset, size, DataHelper.NO_COMPRESSION);
setProto(payload, proto);
setFromPort(payload, fromPort);
setToPort(payload, toPort);
_context.statManager().addRateData("i2cp.tx.msgCompressed", payload.length);
_context.statManager().addRateData("i2cp.tx.msgExpanded", size);
return payload;
}
/**
* @since 0.9.2
*/
private void sendNoEffort(Destination dest, byte payload[], SendMessageOptions options)
throws I2PSessionException {
// nonce always 0
_producer.sendMessage(this, dest, 0, payload, options);
}
/**
* Receive a payload message and let the app know its available
*/
@Override
public void addNewMessage(MessagePayloadMessage msg) {
Long mid = Long.valueOf(msg.getMessageId());
_availableMessages.put(mid, msg);
long id = msg.getMessageId();
byte data[] = msg.getPayload().getUnencryptedData();
if ((data == null) || (data.length <= 0)) {
if (_log.shouldLog(Log.CRIT))
_log.log(Log.CRIT, getPrefix() + "addNewMessage of a message with no unencrypted data",
new Exception("Empty message"));
return;
}
int size = data.length;
if (size < 10) {
_log.error(getPrefix() + "length too short for gzip header: " + size);
return;
}
((MuxedAvailabilityNotifier)_availabilityNotifier).available(id, size, getProto(msg),
getFromPort(msg), getToPort(msg));
}
protected class MuxedAvailabilityNotifier extends AvailabilityNotifier {
private final LinkedBlockingQueue<MsgData> _msgs;
private volatile boolean _alive;
private static final int POISON_SIZE = -99999;
private final AtomicBoolean stopping = new AtomicBoolean();
public MuxedAvailabilityNotifier() {
_msgs = new LinkedBlockingQueue<MsgData>();
}
@Override
public void stopNotifying() {
synchronized (stopping) {
if( !stopping.getAndSet(true)) {
_msgs.clear();
if (_alive) {
// System.out.println("I2PSessionMuxedImpl.stopNotifying()");
boolean again = true;
while(again) {
try {
_msgs.put(new MsgData(0, POISON_SIZE, 0, 0, 0));
again = false;
// System.out.println("I2PSessionMuxedImpl.stopNotifying() success.");
} catch (InterruptedException ie) {
continue;
}
}
_alive = false;
}
stopping.set(false);
}
// stopping.notifyAll();
}
}
/** unused */
@Override
public void available(long msgId, int size) { throw new IllegalArgumentException("no"); }
public void available(long msgId, int size, int proto, int fromPort, int toPort) {
try {
_msgs.put(new MsgData((int)(msgId & 0xffffffff), size, proto, fromPort, toPort));
} catch (InterruptedException ie) {}
if (!_alive && _log.shouldLog(Log.WARN))
_log.warn(getPrefix() + "message available but notifier not running");
}
@Override
public void run() {
if (_log.shouldLog(Log.DEBUG))
_log.debug(getPrefix() + "starting muxed availability notifier");
_msgs.clear();
_alive=true;
while (_alive) {
MsgData msg;
try {
msg = _msgs.take();
} catch (InterruptedException ie) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("I2PSessionMuxedImpl.run() InterruptedException " +
String.valueOf(_msgs.size()) + " Messages, Alive " + _alive);
continue;
}
if (msg.size == POISON_SIZE) {
// System.out.println("I2PSessionMuxedImpl.run() POISONED");
break;
}
try {
_demultiplexer.messageAvailable(I2PSessionMuxedImpl.this,
msg.id, msg.size, msg.proto, msg.fromPort, msg.toPort);
} catch (RuntimeException e) {
_log.error("Error notifying app of message availability", e);
}
}
}
}
/** let's keep this simple */
private static class MsgData {
public final int id, size, proto, fromPort, toPort;
public MsgData(int i, int s, int p, int f, int t) {
id = i;
size = s;
proto = p;
fromPort = f;
toPort = t;
}
}
/**
* No, we couldn't put any protocol byte in front of everything and
* keep backward compatibility. But there are several bytes that
* are unused AND unchecked in the gzip header in releases <= 0.7.
* So let's use 5 of them for a protocol and two 2-byte ports.
*
* Following are all the methods to hide the
* protocol, fromPort, and toPort in the gzip header
*
* The fields used are all ignored on receive in ResettableGzipInputStream
*
* See also ResettableGzipOutputStream.
* Ref: RFC 1952
*
*/
/** OS byte in gzip header */
private static final int PROTO_BYTE = 9;
/** Upper two bytes of MTIME in gzip header */
private static final int FROMPORT_BYTES = 4;
/** Lower two bytes of MTIME in gzip header */
private static final int TOPORT_BYTES = 6;
/** Non-muxed sets the OS byte to 0xff */
private static int getProto(MessagePayloadMessage msg) {
int rv = getByte(msg, PROTO_BYTE) & 0xff;
return rv == 0xff ? PROTO_UNSPECIFIED : rv;
}
/** Non-muxed sets the MTIME bytes to 0 */
private static int getFromPort(MessagePayloadMessage msg) {
return (((getByte(msg, FROMPORT_BYTES) & 0xff) << 8) |
(getByte(msg, FROMPORT_BYTES + 1) & 0xff));
}
/** Non-muxed sets the MTIME bytes to 0 */
private static int getToPort(MessagePayloadMessage msg) {
return (((getByte(msg, TOPORT_BYTES) & 0xff) << 8) |
(getByte(msg, TOPORT_BYTES + 1) & 0xff));
}
private static int getByte(MessagePayloadMessage msg, int i) {
return msg.getPayload().getUnencryptedData()[i] & 0xff;
}
private static void setProto(byte[] payload, int p) {
payload[PROTO_BYTE] = (byte) (p & 0xff);
}
private static void setFromPort(byte[] payload, int p) {
payload[FROMPORT_BYTES] = (byte) ((p >> 8) & 0xff);
payload[FROMPORT_BYTES + 1] = (byte) (p & 0xff);
}
private static void setToPort(byte[] payload, int p) {
payload[TOPORT_BYTES] = (byte) ((p >> 8) & 0xff);
payload[TOPORT_BYTES + 1] = (byte) (p & 0xff);
}
}