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.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.util.Properties; import java.util.StringTokenizer; import net.i2p.I2PAppContext; import net.i2p.client.I2PSession; import net.i2p.data.DataHelper; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.PortMapper; /** * This is the thread listening on 127.0.0.1:7655 or as specified by * sam.udp.host and sam.udp.port properties. * This is used for both repliable and raw datagrams. * * @since 0.9.24 moved from SAMv3Handler */ class SAMv3DatagramServer implements Handler { private final DatagramChannel _server; private final Thread _listener; private final SAMBridge _parent; private final String _host; private final int _port; /** * Does not start listener. * Caller must call start(). * * @param parent may be null * @param props ignored for now */ public SAMv3DatagramServer(SAMBridge parent, String host, int port, Properties props) throws IOException { _parent = parent; _server = DatagramChannel.open(); _server.socket().bind(new InetSocketAddress(host, port)); _listener = new I2PAppThread(new Listener(_server), "SAM DatagramListener " + port); _host = host; _port = port; } /** * Only call once. * @since 0.9.22 */ public synchronized void start() { _listener.start(); if (_parent != null) _parent.register(this); } /** * Cannot be restarted. * @since 0.9.22 */ public synchronized void stopHandling() { try { _server.close(); } catch (IOException ioe) {} _listener.interrupt(); if (_parent != null) _parent.unregister(this); } public void send(SocketAddress addr, ByteBuffer msg) throws IOException { _server.send(msg, addr); } /** @since 0.9.24 */ public String getHost() { return _host; } /** @since 0.9.24 */ public int getPort() { return _port; } private class Listener implements Runnable { private final DatagramChannel server; public Listener(DatagramChannel server) { this.server = server ; } public void run() { I2PAppContext.getGlobalContext().portMapper().register(PortMapper.SVC_SAM_UDP, _host, _port); try { run2(); } finally { I2PAppContext.getGlobalContext().portMapper().unregister(PortMapper.SVC_SAM_UDP); } } private void run2() { ByteBuffer inBuf = ByteBuffer.allocateDirect(SAMRawSession.RAW_SIZE_MAX+1024); while (!Thread.interrupted()) { inBuf.clear(); try { server.receive(inBuf); } catch (IOException e) { break ; } inBuf.flip(); ByteBuffer outBuf = ByteBuffer.wrap(new byte[inBuf.remaining()]); outBuf.put(inBuf); outBuf.flip(); // A new thread for every message is wildly inefficient... //new I2PAppThread(new MessageDispatcher(outBuf.array()), "MessageDispatcher").start(); // inline // Even though we could be sending messages through multiple sessions, // that isn't a common use case, and blocking should be rare. // Inside router context, I2CP drops on overflow. (new MessageDispatcher(outBuf.array())).run(); } } } private static class MessageDispatcher implements Runnable { private final ByteArrayInputStream is; private static final int MAX_LINE_LENGTH = 2*1024; public MessageDispatcher(byte[] buf) { this.is = new ByteArrayInputStream(buf) ; } public void run() { try { // not UTF-8 //String header = DataHelper.readLine(is).trim(); // we cannot use SAMUtils.parseParams() here final UTF8Reader reader = new UTF8Reader(is); final StringBuilder buf = new StringBuilder(MAX_LINE_LENGTH); int c; int i = 0; while ((c = reader.read()) != -1) { if (++i > MAX_LINE_LENGTH) throw new IOException("Line too long - max " + MAX_LINE_LENGTH); if (c == '\n') break; buf.append((char)c); } String header = buf.toString(); StringTokenizer tok = new StringTokenizer(header, " "); if (tok.countTokens() < 3) { // This is not a correct message, for sure warn("Bad datagram header received"); return; } String version = tok.nextToken(); if (!version.startsWith("3.")) { warn("Bad datagram header received"); return; } String nick = tok.nextToken(); String dest = tok.nextToken(); SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); if (rec!=null) { Properties sprops = rec.getProps(); // 3.2 props String pr = sprops.getProperty("PROTOCOL"); String fp = sprops.getProperty("FROM_PORT"); String tp = sprops.getProperty("TO_PORT"); // 3.3 props // If this is a straight DATAGRAM or RAW session, we // don't need to send these, the router already got them in // the options, but if a subsession, we must, so just // do it all the time. String st = sprops.getProperty("crypto.tagsToSend"); String tt = sprops.getProperty("crypto.lowTagThreshold"); String sl = sprops.getProperty("shouldBundleReplyInfo"); String exms = sprops.getProperty("clientMessageTimeout"); // ms String exs = null; // seconds while (tok.hasMoreTokens()) { String t = tok.nextToken(); // 3.2 props if (t.startsWith("PROTOCOL=")) pr = t.substring("PROTOCOL=".length()); else if (t.startsWith("FROM_PORT=")) fp = t.substring("FROM_PORT=".length()); else if (t.startsWith("TO_PORT=")) tp = t.substring("TO_PORT=".length()); // 3.3 props else if (t.startsWith("SEND_TAGS=")) st = t.substring("SEND_TAGS=".length()); else if (t.startsWith("TAG_THRESHOLD=")) tt = t.substring("TAG_THRESHOLD=".length()); else if (t.startsWith("EXPIRES=")) exs = t.substring("EXPIRES=".length()); else if (t.startsWith("SEND_LEASESET=")) sl = t.substring("SEND_LEASESET=".length()); } // 3.2 props int proto = I2PSession.PROTO_UNSPECIFIED; int fromPort = I2PSession.PORT_UNSPECIFIED; int toPort = I2PSession.PORT_UNSPECIFIED; // 3.3 props int sendTags = 0; int tagThreshold = 0; int expires = 0; // seconds boolean sendLeaseSet = true; try { // 3.2 props if (pr != null) proto = Integer.parseInt(pr); if (fp != null) fromPort = Integer.parseInt(fp); if (tp != null) toPort = Integer.parseInt(tp); // 3.3 props if (st != null) sendTags = Integer.parseInt(st); if (tt != null) tagThreshold = Integer.parseInt(tt); if (exs != null) expires = Integer.parseInt(exs); else if (exms != null) expires = Integer.parseInt(exms) / 1000; if (sl != null) sendLeaseSet = Boolean.parseBoolean(sl); } catch (NumberFormatException nfe) { warn("Bad datagram header received"); return; } // TODO too many allocations and copies. One here and one in Listener above. byte[] data = new byte[is.available()]; is.read(data); Session sess = rec.getHandler().getSession(); if (sess != null) { if (sendTags > 0 || tagThreshold > 0 || expires > 0 || !sendLeaseSet) { sess.sendBytes(dest, data, proto, fromPort, toPort, sendLeaseSet, sendTags, tagThreshold, expires); } else { sess.sendBytes(dest, data, proto, fromPort, toPort); } } else { warn("Dropping datagram, no session for " + nick); } } else { warn("Dropping datagram, no session for " + nick); } } catch (Exception e) { warn("Error handling datagram", e); } } /** @since 0.9.22 */ private static void warn(String s) { warn(s, null); } /** @since 0.9.22 */ private static void warn(String s, Throwable t) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3DatagramServer.class); if (log.shouldLog(Log.WARN)) log.warn(s, t); } } }