/* PeerAcceptor - Accepts incomming connections from peers. Copyright (C) 2003 Mark J. Wielaard This file is part of Snark. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.klomp.snark; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.SequenceInputStream; import net.i2p.I2PAppContext; import net.i2p.client.streaming.I2PSocket; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.util.Log; /** * Accepts incomming connections from peers. The ConnectionAcceptor * will call the connection() method when it detects an incomming BT * protocol connection. The PeerAcceptor will then create a new peer * if the PeerCoordinator wants more peers. */ class PeerAcceptor { private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerAcceptor.class); private final PeerCoordinator coordinator; final PeerCoordinatorSet coordinators; /** shorten timeout while reading handshake */ private static final long HASH_READ_TIMEOUT = 45*1000; public PeerAcceptor(PeerCoordinator coordinator) { this.coordinator = coordinator; this.coordinators = null; } public PeerAcceptor(PeerCoordinatorSet coordinators) { this.coordinators = coordinators; this.coordinator = null; } public void connection(I2PSocket socket, InputStream in, OutputStream out) throws IOException { // inside this Peer constructor's handshake is where you'd deal with the other // side saying they want to communicate with another torrent - aka multitorrent // support, but because of how the protocol works, we can get away with just reading // ahead the first $LOOKAHEAD_SIZE bytes to figure out which infohash they want to // talk about, and we can just look for that in our list of active torrents. byte peerInfoHash[] = null; if (in instanceof BufferedInputStream) { // multitorrent in.mark(LOOKAHEAD_SIZE); long timeout = socket.getReadTimeout(); socket.setReadTimeout(HASH_READ_TIMEOUT); try { peerInfoHash = readHash(in); } catch (IOException ioe) { // unique exception so ConnectionAcceptor can blame the peer throw new ProtocolException(ioe.toString()); } socket.setReadTimeout(timeout); in.reset(); } else { // Single torrent - is this working right? try { peerInfoHash = readHash(in); if (_log.shouldLog(Log.INFO)) _log.info("infohash read from " + socket.getPeerDestination().calculateHash().toBase64() + ": " + Base64.encode(peerInfoHash)); } catch (IOException ioe) { if (_log.shouldLog(Log.INFO)) _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64()); throw ioe; } in = new SequenceInputStream(new ByteArrayInputStream(peerInfoHash), in); } if (coordinator != null) { // single torrent capability if (DataHelper.eq(coordinator.getInfoHash(), peerInfoHash)) { if (coordinator.needPeers()) { Peer peer = new Peer(socket, in, out, coordinator.getID(), coordinator.getInfoHash(), coordinator.getMetaInfo()); coordinator.addPeer(peer); } else socket.close(); } else { // its for another infohash, but we are only single torrent capable. b0rk. throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash) + ") while we only support (" + Base64.encode(coordinator.getInfoHash()) + ")"); } } else { // multitorrent capable, so lets see what we can handle PeerCoordinator cur = coordinators.get(peerInfoHash); if (cur != null) { if (DataHelper.eq(cur.getInfoHash(), peerInfoHash)) { if (cur.needPeers()) { Peer peer = new Peer(socket, in, out, cur.getID(), cur.getInfoHash(), cur.getMetaInfo()); cur.addPeer(peer); return; } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Rejecting new peer for " + cur.getName()); socket.close(); return; } } } // this is only reached if none of the coordinators match the infohash throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash) + ") while we don't support that hash"); } } private static final String PROTO_STR = "BitTorrent protocol"; private static final int PROTO_STR_LEN = PROTO_STR.length(); private static final int PROTO_LEN = PROTO_STR_LEN + 1; private static final int[] PROTO = new int[PROTO_LEN]; static { PROTO[0] = PROTO_STR_LEN; for (int i = 0; i < PROTO_STR_LEN; i++) { PROTO[i+1] = PROTO_STR.charAt(i); } } /** 48 */ private static final int LOOKAHEAD_SIZE = PROTO_LEN + 8 + // blank, reserved 20; // infohash /** * Read ahead to the infohash, throwing an exception if there isn't enough data. * Also check the first 20 bytes for the correct protocol here and throw IOE if bad, * so we don't hang waiting for 48 bytes if it's not a bittorrent client. * The 20 bytes are checked again in Peer.handshake(). */ private static byte[] readHash(InputStream in) throws IOException { for (int i = 0; i < PROTO_LEN; i++) { int b = in.read(); if (b != PROTO[i]) throw new IOException("Bad protocol 0x" + Integer.toHexString(b) + " at byte " + i); } DataHelper.skip(in, 8); byte buf[] = new byte[20]; int read = DataHelper.read(in, buf); if (read != buf.length) throw new IOException("Unable to read the hash (read " + read + ")"); return buf; } /** * A unique exception so we can tell the ConnectionAcceptor about non-BT connections * @since 0.9.1 */ public static class ProtocolException extends IOException { public ProtocolException(String s) { super(s); } } }