package net.i2p.sam;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.I2PException;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionMuxedListener;
import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
/**
* A session that does nothing, but implements interfaces for raw, datagram, and streaming
* for convenience.
*
* We extend SAMv3StreamSession as we must have it set up the I2PSession, in case
* user adds a STREAM session (and he probably will).
* This session receives all data from I2P, but you can't send any data on it.
*
* @since 0.9.25
*/
class MasterSession extends SAMv3StreamSession implements SAMDatagramReceiver, SAMRawReceiver,
SAMMessageSess, I2PSessionMuxedListener {
private final SAMv3Handler handler;
private final SAMv3DatagramServer dgs;
private final Map<String, SAMMessageSess> sessions;
private final StreamAcceptor streamAcceptor;
private static final String[] INVALID_OPTS = { "PORT", "HOST", "FROM_PORT", "TO_PORT",
"PROTOCOL", "LISTEN_PORT", "LISTEN_PROTOCOL" };
/**
* Build a Session according to information
* registered with the given nickname.
*
* Caller MUST call start().
*
* @param nick nickname of the session
* @throws IOException
* @throws DataFormatException
*/
public MasterSession(String nick, SAMv3DatagramServer dgServer, SAMv3Handler handler, Properties props)
throws IOException, DataFormatException, SAMException {
super(nick);
for (int i = 0; i < INVALID_OPTS.length; i++) {
String p = INVALID_OPTS[i];
if (props.containsKey(p))
throw new SAMException("MASTER session options may not contain " + p);
}
dgs = dgServer;
sessions = new ConcurrentHashMap<String, SAMMessageSess>(4);
this.handler = handler;
I2PSession isess = socketMgr.getSession();
// if we get a RAW session added with 0/0, it will replace this,
// and we won't add this back if removed.
isess.addMuxedSessionListener(this, I2PSession.PROTO_ANY, I2PSession.PORT_ANY);
streamAcceptor = new StreamAcceptor();
}
/**
* Overridden to start the acceptor.
*/
@Override
public void start() {
Thread t = new I2PAppThread(streamAcceptor, "SAMMasterAcceptor");
t.start();
}
/**
* Add a session
* @return null for success, or error message
*/
public synchronized String add(String nick, String style, Properties props) {
if (props.containsKey("DESTINATION"))
return "SESSION ADD may not contain DESTINATION";
SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick);
if (rec != null || sessions.containsKey(nick))
return "Duplicate ID " + nick;
int listenPort = I2PSession.PORT_ANY;
String slp = (String) props.remove("LISTEN_PORT");
if (slp == null)
slp = props.getProperty("FROM_PORT");
if (slp != null) {
try {
listenPort = Integer.parseInt(slp);
if (listenPort < 0 || listenPort > 65535)
return "Bad LISTEN_PORT " + slp;
// TODO enforce streaming listen port must be 0 or from port
} catch (NumberFormatException nfe) {
return "Bad LISTEN_PORT " + slp;
}
}
int listenProtocol;
SAMMessageSess sess;
SAMv3Handler subhandler;
try {
I2PSession isess = socketMgr.getSession();
subhandler = new SAMv3Handler(handler.getClientSocket(), handler.verMajor,
handler.verMinor, handler.getBridge());
if (style.equals("RAW")) {
if (!props.containsKey("PORT"))
return "RAW subsession must specify PORT";
listenProtocol = I2PSession.PROTO_DATAGRAM_RAW;
String spr = (String) props.remove("LISTEN_PROTOCOL");
if (spr == null)
spr = props.getProperty("PROTOCOL");
if (spr != null) {
try {
listenProtocol = Integer.parseInt(spr);
// RAW can't listen on streaming protocol
if (listenProtocol < 0 || listenProtocol > 255 ||
listenProtocol == I2PSession.PROTO_STREAMING)
return "Bad RAW LISTEN_PPROTOCOL " + spr;
} catch (NumberFormatException nfe) {
return "Bad LISTEN_PROTOCOL " + spr;
}
}
SAMv3RawSession ssess = new SAMv3RawSession(nick, props, handler, isess, listenProtocol, listenPort, dgs);
subhandler.setSession(ssess);
sess = ssess;
} else if (style.equals("DATAGRAM")) {
if (!props.containsKey("PORT"))
return "DATAGRAM subsession must specify PORT";
listenProtocol = I2PSession.PROTO_DATAGRAM;
SAMv3DatagramSession ssess = new SAMv3DatagramSession(nick, props, handler, isess, listenPort, dgs);
subhandler.setSession(ssess);
sess = ssess;
} else if (style.equals("STREAM")) {
listenProtocol = I2PSession.PROTO_STREAMING;
// FIXME need something that hangs off an existing dest
SAMv3StreamSession ssess = new SAMv3StreamSession(nick, props, handler, socketMgr, listenPort);
subhandler.setSession(ssess);
sess = ssess;
} else {
return "Unrecognized SESSION STYLE " + style;
}
} catch (IOException e) {
return e.toString();
} catch (DataFormatException e) {
return e.toString();
} catch (SAMException e) {
return e.toString();
} catch (I2PSessionException e) {
return e.toString();
}
for (SAMMessageSess s : sessions.values()) {
if (listenProtocol == s.getListenProtocol() &&
listenPort == s.getListenPort())
return "Duplicate protocol " + listenProtocol + " and port " + listenPort;
}
rec = new SessionRecord(getDestination().toBase64(), props, subhandler);
try {
SAMv3Handler.sSessionsHash.putDupDestOK(nick, rec);
sessions.put(nick, sess);
} catch (SessionsDB.ExistingIdException e) {
return "Duplicate ID " + nick;
}
if (_log.shouldWarn())
_log.warn("added " + style + " proto " + listenProtocol + " port " + listenPort);
sess.start();
// all ok
return null;
}
/**
* Remove a session
* @return null for success, or error message
*/
public synchronized String remove(String nick, Properties props) {
boolean ok;
SAMMessageSess sess = sessions.remove(nick);
if (sess != null) {
ok = SAMv3Handler.sSessionsHash.del(nick);
sess.close();
// TODO if 0/0, add back this as listener?
if (_log.shouldWarn())
_log.warn("removed " + sess + " proto " + sess.getListenProtocol() + " port " + sess.getListenPort());
} else {
ok = false;
}
if (!ok)
return "ID " + nick + " not found";
// all ok
return null;
}
/**
* @throws IOException always
*/
public void receiveDatagramBytes(Destination sender, byte[] data, int proto,
int fromPort, int toPort) throws IOException {
throw new IOException("master session");
}
/**
* Does nothing.
*/
public void stopDatagramReceiving() {}
/**
* @throws IOException always
*/
public void receiveRawBytes(byte[] data, int proto, int fromPort, int toPort) throws IOException {
throw new IOException("master session");
}
/**
* Does nothing.
*/
public void stopRawReceiving() {}
/////// stream session overrides
/** @throws I2PException always */
@Override
public void connect(SAMv3Handler handler, String dest, Properties props) throws I2PException {
throw new I2PException("master session");
}
/** @throws SAMException always */
@Override
public void accept(SAMv3Handler handler, boolean verbose) throws SAMException {
throw new SAMException("master session");
}
/** @throws SAMException always */
@Override
public void startForwardingIncoming(Properties props, boolean sendPorts) throws SAMException {
throw new SAMException("master session");
}
/** does nothing */
@Override
public void stopForwardingIncoming() {}
///// SAMMessageSess interface
@Override
public int getListenProtocol() {
return I2PSession.PROTO_ANY;
}
@Override
public int getListenPort() {
return I2PSession.PORT_ANY;
}
/**
* Close the master session
* Overridden to stop the acceptor.
*/
@Override
public void close() {
// close sessions?
streamAcceptor.stopRunning();
super.close();
}
// I2PSessionMuxedImpl interface
public void disconnected(I2PSession session) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("I2P session disconnected");
close();
}
public void errorOccurred(I2PSession session, String message,
Throwable error) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("I2P error: " + message, error);
close();
}
public void messageAvailable(I2PSession session, int msgId, long size) {
messageAvailable(session, msgId, size, I2PSession.PROTO_UNSPECIFIED,
I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
}
/** @since 0.9.24 */
public void messageAvailable(I2PSession session, int msgId, long size,
int proto, int fromPort, int toPort) {
try {
byte msg[] = session.receiveMessage(msgId);
if (msg == null)
return;
messageReceived(msg, proto, fromPort, toPort);
} catch (I2PSessionException e) {
_log.error("Error fetching I2P message", e);
close();
}
}
public void reportAbuse(I2PSession session, int severity) {
_log.warn("Abuse reported (severity: " + severity + ")");
close();
}
private void messageReceived(byte[] msg, int proto, int fromPort, int toPort) {
if (_log.shouldWarn())
_log.warn("Unhandled message received, length = " + msg.length +
" protocol: " + proto + " from port: " + fromPort + " to port: " + toPort);
}
private class StreamAcceptor implements Runnable {
private volatile boolean stop;
public StreamAcceptor() {
}
public void stopRunning() {
stop = true;
}
public void run() {
if (_log.shouldWarn())
_log.warn("Stream acceptor started");
final I2PServerSocket i2pss = socketMgr.getServerSocket();
while (!stop) {
// wait and accept a connection from I2P side
I2PSocket i2ps;
try {
i2ps = i2pss.accept();
if (i2ps == null) // never null as of 0.9.17
continue;
} catch (SocketTimeoutException ste) {
continue;
} catch (ConnectException ce) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error accepting", ce);
try { Thread.sleep(50); } catch (InterruptedException ie) {}
continue;
} catch (I2PException ipe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error accepting", ipe);
break;
}
int port = i2ps.getLocalPort();
SAMMessageSess foundSess = null;
Collection<SAMMessageSess> all = sessions.values();
for (Iterator<SAMMessageSess> iter = all.iterator(); iter.hasNext(); ) {
SAMMessageSess sess = iter.next();
if (sess.getListenProtocol() != I2PSession.PROTO_STREAMING) {
// remove as we may be going around again below
iter.remove();
continue;
}
if (sess.getListenPort() == port) {
foundSess = sess;
break;
}
}
// We never send streaming out as a raw packet to a default listener,
// and we don't allow raw to listen on streaming protocol,
// so we don't have to look for a default protocol,
// but we do have to look for a default port listener.
if (foundSess == null) {
for (SAMMessageSess sess : all) {
if (sess.getListenPort() == 0) {
foundSess = sess;
break;
}
}
}
if (foundSess != null) {
SAMv3StreamSession ssess = (SAMv3StreamSession) foundSess;
boolean ok = ssess.queueSocket(i2ps);
if (!ok) {
_log.logAlways(Log.WARN, "Accept queue overflow for " + ssess);
try { i2ps.reset(); } catch (IOException ioe) {}
}
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("No subsession found for incoming streaming connection on port " + port);
}
}
if (_log.shouldWarn())
_log.warn("Stream acceptor stopped");
}
}
}