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.IOException; import java.io.InterruptedIOException; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.NoRouteToHostException; import java.net.SocketTimeoutException; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.security.GeneralSecurityException; import java.util.Properties; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.streaming.I2PServerSocket; import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocketManager; import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; import net.i2p.util.I2PAppThread; import net.i2p.util.I2PSSLSocketFactory; import net.i2p.util.Log; /** * SAMv3 STREAM session class. * * @author mkvore */ class SAMv3StreamSession extends SAMStreamSession implements Session { private static final int BUFFER_SIZE = 1024; private static final int MAX_ACCEPT_QUEUE = 64; private final Object socketServerLock = new Object(); /** this is ONLY set for FORWARD, not for ACCEPT */ private I2PServerSocket socketServer; /** this is the count of active ACCEPT sockets */ private final AtomicInteger _acceptors = new AtomicInteger(); /** for subsession only, null otherwise */ private final LinkedBlockingQueue<I2PSocket> _acceptQueue; private static I2PSSLSocketFactory _sslSocketFactory; private final String nick ; public String getNick() { return nick ; } /** * Create a new SAM STREAM session, according to information * registered with the given nickname * * Caller MUST call start(). * * @param login The nickname * @throws IOException * @throws DataFormatException * @throws SAMException * @throws NullPointerException if login nickname is not registered */ public SAMv3StreamSession(String login) throws IOException, DataFormatException, SAMException { super(getDB().get(login).getDest(), "__v3__", getDB().get(login).getProps(), getDB().get(login).getHandler()); this.nick = login ; _acceptQueue = null; } /** * Build a Stream Session on an existing I2P session * registered with the given nickname * * Caller MUST call start(). * * @param login nickname of the session * @throws IOException * @throws DataFormatException * @since 0.9.25 */ public SAMv3StreamSession(String login, Properties props, SAMv3Handler handler, I2PSocketManager mgr, int listenPort) throws IOException, DataFormatException, SAMException { super(mgr, props, handler, listenPort); this.nick = login ; _acceptQueue = new LinkedBlockingQueue<I2PSocket>(MAX_ACCEPT_QUEUE); } /** * Put a socket on the accept queue. * Only for subsession, throws IllegalStateException otherwise. * * @return success, false if full * @since 0.9.25 */ public boolean queueSocket(I2PSocket sock) { if (_acceptQueue == null) throw new IllegalStateException(); return _acceptQueue.offer(sock); } /** * Take a socket from the accept queue. * Only for subsession, throws IllegalStateException otherwise. * * @since 0.9.25 */ private I2PSocket acceptSocket() throws ConnectException { if (_acceptQueue == null) throw new IllegalStateException(); try { // TODO there's no CoDel or expiration in this queue return _acceptQueue.take(); } catch (InterruptedException ie) { ConnectException ce = new ConnectException("interrupted"); ce.initCause(ie); throw ce; } } public static SessionsDB getDB() { return SAMv3Handler.sSessionsHash ; } /** * Connect the SAM STREAM session to the specified Destination * for a single connection, using the socket stolen from the handler. * * @param handler The handler that communicates with the requesting client * @param dest Base64-encoded Destination to connect to * @param props Options to be used for connection * * @throws DataFormatException if the destination is not valid * @throws ConnectException if the destination refuses connections * @throws NoRouteToHostException if the destination can't be reached * @throws InterruptedIOException if the connection timeouts * @throws I2PException if there's another I2P-related error * @throws IOException */ public void connect ( SAMv3Handler handler, String dest, Properties props ) throws I2PException, ConnectException, NoRouteToHostException, DataFormatException, InterruptedIOException, IOException { boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT")); Destination d = SAMUtils.getDest(dest); I2PSocketOptions opts = socketMgr.buildOptions(props); if (props.getProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT) == null) opts.setConnectTimeout(60 * 1000); String fromPort = props.getProperty("FROM_PORT"); if (fromPort != null) { try { opts.setLocalPort(Integer.parseInt(fromPort)); } catch (NumberFormatException nfe) { throw new I2PException("Bad port " + fromPort); } } String toPort = props.getProperty("TO_PORT"); if (toPort != null) { try { opts.setPort(Integer.parseInt(toPort)); } catch (NumberFormatException nfe) { throw new I2PException("Bad port " + toPort); } } if (_log.shouldLog(Log.DEBUG)) _log.debug("Connecting new I2PSocket..."); // blocking connection (SAMv3) I2PSocket i2ps = socketMgr.connect(d, opts); SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); if ( rec==null ) throw new InterruptedIOException() ; handler.notifyStreamResult(verbose, "OK", null) ; handler.stealSocket() ; ReadableByteChannel fromClient = handler.getClientSocket(); ReadableByteChannel fromI2P = Channels.newChannel(i2ps.getInputStream()); WritableByteChannel toClient = handler.getClientSocket(); WritableByteChannel toI2P = Channels.newChannel(i2ps.getOutputStream()); SAMBridge bridge = handler.getBridge(); (new I2PAppThread(rec.getThreadGroup(), new Pipe(fromClient, toI2P, bridge), "ConnectV3 SAMPipeClientToI2P")).start(); (new I2PAppThread(rec.getThreadGroup(), new Pipe(fromI2P, toClient, bridge), "ConnectV3 SAMPipeI2PToClient")).start(); } /** * Accept a single incoming STREAM on the socket stolen from the handler. * As of version 3.2 (0.9.24), multiple simultaneous accepts are allowed. * Accepts and forwarding may not be done at the same time. * * @param handler The handler that communicates with the requesting client * @param verbose If true, SAM will send the Base64-encoded peer Destination of an * incoming socket as the first line of data sent to its client * on the handler socket * * @throws DataFormatException if the destination is not valid * @throws ConnectException if the destination refuses connections * @throws NoRouteToHostException if the destination can't be reached * @throws InterruptedIOException if the connection timeouts * @throws I2PException if there's another I2P-related error * @throws IOException */ public void accept(SAMv3Handler handler, boolean verbose) throws I2PException, InterruptedIOException, IOException, SAMException { synchronized(this.socketServerLock) { if (this.socketServer != null) { if (_log.shouldWarn()) _log.warn("a forwarding server is already defined for this destination"); throw new SAMException("a forwarding server is already defined for this destination"); } } I2PSocket i2ps = null; _acceptors.incrementAndGet(); try { if (_acceptQueue != null) i2ps = acceptSocket(); else i2ps = socketMgr.getServerSocket().accept(); } finally { _acceptors.decrementAndGet(); } SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); if ( rec==null || i2ps==null ) throw new InterruptedIOException() ; if (verbose) { handler.notifyStreamIncomingConnection(i2ps.getPeerDestination(), i2ps.getPort(), i2ps.getLocalPort()); } handler.stealSocket() ; ReadableByteChannel fromClient = handler.getClientSocket(); ReadableByteChannel fromI2P = Channels.newChannel(i2ps.getInputStream()); WritableByteChannel toClient = handler.getClientSocket(); WritableByteChannel toI2P = Channels.newChannel(i2ps.getOutputStream()); SAMBridge bridge = handler.getBridge(); (new I2PAppThread(rec.getThreadGroup(), new Pipe(fromClient, toI2P, bridge), "AcceptV3 SAMPipeClientToI2P")).start(); (new I2PAppThread(rec.getThreadGroup(), new Pipe(fromI2P, toClient, bridge), "AcceptV3 SAMPipeI2PToClient")).start(); } /** * Forward sockets from I2P to the host/port provided. * Accepts and forwarding may not be done at the same time. */ public void startForwardingIncoming(Properties props, boolean sendPorts) throws SAMException, InterruptedIOException { SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT")); if ( rec==null ) throw new InterruptedIOException() ; String portStr = props.getProperty("PORT") ; if ( portStr==null ) { if (_log.shouldLog(Log.DEBUG)) _log.debug("receiver port not specified"); throw new SAMException("receiver port not specified"); } int port = Integer.parseInt(portStr); String host = props.getProperty("HOST"); if ( host==null ) { host = rec.getHandler().getClientIP(); if (_log.shouldLog(Log.DEBUG)) _log.debug("no host specified. Taken from the client socket : " + host +':'+port); } boolean isSSL = Boolean.parseBoolean(props.getProperty("SSL")); if (_acceptors.get() > 0) { if (_log.shouldWarn()) _log.warn("an accepting server is already defined for this destination"); throw new SAMException("an accepting server is already defined for this destination"); } synchronized(this.socketServerLock) { if (this.socketServer!=null) { if (_log.shouldWarn()) _log.warn("a forwarding server is already defined for this destination"); throw new SAMException("a forwarding server is already defined for this destination"); } this.socketServer = this.socketMgr.getServerSocket(); } SocketForwarder forwarder = new SocketForwarder(host, port, isSSL, verbose, sendPorts); (new I2PAppThread(rec.getThreadGroup(), forwarder, "SAMV3StreamForwarder")).start(); } /** * Forward sockets from I2P to the host/port provided */ private class SocketForwarder implements Runnable { private final String host; private final int port; private final boolean isSSL, verbose, sendPorts; SocketForwarder(String host, int port, boolean isSSL, boolean verbose, boolean sendPorts) { this.host = host ; this.port = port ; this.verbose = verbose ; this.sendPorts = sendPorts; this.isSSL = isSSL; } public void run() { while (getSocketServer() != null) { // wait and accept a connection from I2P side I2PSocket i2ps; try { if (_acceptQueue != null) i2ps = acceptSocket(); else i2ps = getSocketServer().accept(); if (i2ps == null) continue; } catch (SocketTimeoutException ste) { continue; } catch (ConnectException ce) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class); if (log.shouldLog(Log.WARN)) log.warn("Error accepting", ce); try { Thread.sleep(50); } catch (InterruptedException ie) {} continue; } catch (I2PException ipe) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class); if (log.shouldLog(Log.WARN)) log.warn("Error accepting", ipe); break; } // open a socket towards client SocketChannel clientServerSock; try { if (isSSL) { I2PAppContext ctx = I2PAppContext.getGlobalContext(); synchronized(SAMv3StreamSession.class) { if (_sslSocketFactory == null) { try { _sslSocketFactory = new I2PSSLSocketFactory( ctx, true, "certificates/sam"); } catch (GeneralSecurityException gse) { Log log = ctx.logManager().getLog(SAMv3StreamSession.class); log.error("SSL error", gse); try { i2ps.reset(); } catch (IOException ee) {} throw new RuntimeException("SSL error", gse); } } } SSLSocket sock = (SSLSocket) _sslSocketFactory.createSocket(host, port); I2PSSLSocketFactory.verifyHostname(ctx, sock, host); clientServerSock = new SSLSocketChannel(sock); } else { InetSocketAddress addr = new InetSocketAddress(host, port); clientServerSock = SocketChannel.open(addr) ; } } catch (IOException ioe) { Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class); if (log.shouldLog(Log.WARN)) log.warn("Error forwarding", ioe); try { i2ps.reset(); } catch (IOException ee) {} continue; } // build pipes between both sockets try { clientServerSock.socket().setKeepAlive(true); if (this.verbose) { if (sendPorts) { SAMv3Handler.notifyStreamIncomingConnection( clientServerSock, i2ps.getPeerDestination(), i2ps.getPort(), i2ps.getLocalPort()); } else { SAMv3Handler.notifyStreamIncomingConnection( clientServerSock, i2ps.getPeerDestination()); } } ReadableByteChannel fromClient = clientServerSock ; ReadableByteChannel fromI2P = Channels.newChannel(i2ps.getInputStream()); WritableByteChannel toClient = clientServerSock ; WritableByteChannel toI2P = Channels.newChannel(i2ps.getOutputStream()); (new I2PAppThread(new Pipe(fromClient, toI2P, null), "ForwardV3 SAMPipeClientToI2P")).start(); (new I2PAppThread(new Pipe(fromI2P,toClient, null), "ForwardV3 SAMPipeI2PToClient")).start(); } catch (IOException e) { try { clientServerSock.close(); } catch (IOException ee) {} try { i2ps.reset(); } catch (IOException ee) {} continue ; } } } } private static class Pipe implements Runnable, Handler { private final ReadableByteChannel in ; private final WritableByteChannel out ; private final ByteBuffer buf ; private final SAMBridge bridge; /** * @param bridge may be null */ public Pipe(ReadableByteChannel in, WritableByteChannel out, SAMBridge bridge) { this.in = in ; this.out = out ; this.buf = ByteBuffer.allocate(BUFFER_SIZE) ; this.bridge = bridge; } public void run() { if (bridge != null) bridge.register(this); try { while (!Thread.interrupted() && (in.read(buf)>=0 || buf.position() != 0)) { buf.flip(); out.write(buf); buf.compact(); } } catch (IOException ioe) { // ignore } finally { try { in.close(); } catch (IOException e) {} try { buf.flip(); while (buf.hasRemaining()) { out.write(buf); } } catch (IOException e) {} try { out.close(); } catch (IOException e) {} if (bridge != null) bridge.unregister(this); } } /** * Handler interface * @since 0.9.20 */ public void stopHandling() { try { in.close(); } catch (IOException e) {} } } protected I2PServerSocket getSocketServer() { synchronized ( this.socketServerLock ) { return this.socketServer ; } } /** * stop Forwarding Incoming connection coming from I2P * @throws SAMException * @throws InterruptedIOException */ public void stopForwardingIncoming() throws SAMException, InterruptedIOException { SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); if ( rec==null ) throw new InterruptedIOException() ; I2PServerSocket server = null ; synchronized( this.socketServerLock ) { if (this.socketServer==null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("no socket server is defined for this destination"); throw new SAMException("no socket server is defined for this destination"); } server = this.socketServer ; this.socketServer = null ; if (_log.shouldLog(Log.DEBUG)) _log.debug("nulling socketServer in stopForwardingIncoming. Object " + this ); } try { server.close(); } catch ( I2PException e) {} } /** * Close the stream session * TODO Why do we override? */ @Override public void close() { if (_isOwnSession) socketMgr.destroySocketManager(); } }