/*
* This file is part of mlDHT.
*
* mlDHT 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 of the License, or
* (at your option) any later version.
*
* mlDHT 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 mlDHT. If not, see <http://www.gnu.org/licenses/>.
*/
package the8472.bt;
import the8472.bencode.BDecoder;
import the8472.bencode.BEncoder;
import the8472.bencode.Tokenizer.BDecodingException;
import the8472.bt.MetadataPool.Completion;
import lbms.plugins.mldht.kad.DHT;
import lbms.plugins.mldht.kad.DHT.LogLevel;
import lbms.plugins.mldht.kad.Key;
import lbms.plugins.mldht.kad.utils.AddressUtils;
import lbms.plugins.mldht.kad.utils.ThreadLocalUtils;
import lbms.plugins.mldht.utils.NIOConnectionManager;
import lbms.plugins.mldht.utils.Selectable;
import static the8472.bt.PullMetaDataConnection.CONNECTION_STATE.*;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.function.Predicate;
public class PullMetaDataConnection implements Selectable {
public static interface MetaConnectionHandler {
void onTerminate();
default void onStateChange(CONNECTION_STATE oldState, CONNECTION_STATE newState) {};
void onConnect();
}
public final static byte[] preamble = "\u0013BitTorrent protocol".getBytes(StandardCharsets.ISO_8859_1);
public static final byte[] bitfield = new byte[8];
static {
// ltep
bitfield[5] |= 0x10;
// fast extension
bitfield[7] |= 0x04;
// BT port
bitfield[7] |= 0x01;
}
public enum CONNECTION_STATE {
STATE_INITIAL,
STATE_CONNECTING,
STATE_BASIC_HANDSHAKING,
STATE_IH_RECEIVED,
STATE_LTEP_HANDSHAKING,
STATE_PEX_ONLY,
STATE_GETTING_METADATA,
STATE_CLOSED;
public boolean neverConnected() {
return this != STATE_INITIAL && this != STATE_CONNECTING;
}
}
public enum CloseReason {
NO_LTEP,
NO_META_EXCHANGE,
CONNECT_FAILED,
OTHER
}
private static final int RCV_TIMEOUT = 25*1000;
private static final int LTEP_HEADER_ID = 20;
private static final int LTEP_HANDSHAKE_ID = 0;
private static final int LTEP_LOCAL_META_ID = 7;
private static final int LTEP_LOCAL_PEX_ID = 13;
private static final int BT_BITFIELD_ID = 5;
private static final int BT_HEADER_LENGTH = 4;
private static final int BT_MSG_ID_OFFSET = 4; // 0-3 length, 4 id
private static final int BT_LTEP_HEADER_OFFSET = 5; // 5 ltep id
boolean keepPexOnlyOpen;
SocketChannel channel;
NIOConnectionManager connManager;
boolean incoming;
Deque<ByteBuffer> outputBuffers = new ArrayDeque<>();
ByteBuffer inputBuffer;
boolean remoteSupportsFastExtension;
boolean remoteSupportsPort;
int ltepRemoteMetadataExchangeMessageId;
int ltepRemotePexId = -1;
public int dhtPort = -1;
public int ourListeningPort = -1;
MetadataPool pool;
Predicate<Key> checker;
byte[] infoHash;
int outstandingRequests;
int maxRequests = 1;
long lastReceivedTime;
long lastUsefulMessage;
AtomicReference<CONNECTION_STATE> state = new AtomicReference<>(STATE_INITIAL) ;
BDecoder decoder = new BDecoder();
MetaConnectionHandler metaHandler;
InetSocketAddress remoteAddress;
String remoteClient;
CloseReason closeReason;
public Consumer<List<InetSocketAddress>> pexConsumer = (x) -> {};
public IntFunction<MetadataPool> poolGenerator = (i) -> new MetadataPool(i);
static PrintWriter idWriter;
static {
try
{
idWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream("./peer_ids.log",true))),true);
} catch (FileNotFoundException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void keepPexOnlyOpen(boolean toggle) {
keepPexOnlyOpen = toggle;
}
boolean setState(CONNECTION_STATE expected, CONNECTION_STATE newState) {
return setState(EnumSet.of(expected), newState);
}
boolean setState(Set<CONNECTION_STATE> expected, CONNECTION_STATE newState) {
CONNECTION_STATE current;
do {
current = state.get();
if(!expected.contains(current))
return false;
} while(!state.weakCompareAndSet(current, newState));
if(metaHandler != null)
metaHandler.onStateChange(current, newState);
return true;
}
public boolean isState(CONNECTION_STATE toTest) {return state.get() == toTest; }
public boolean isIncoming() {return incoming;}
public CONNECTION_STATE getState() {
return state.get();
}
public Key getInfohash() {
return new Key(infoHash);
}
public InetSocketAddress remoteAddress() {
return remoteAddress;
}
public void setListener(MetaConnectionHandler handler) {
metaHandler = handler;
}
// incoming
public PullMetaDataConnection(SocketChannel chan)
{
channel = chan;
incoming = true;
try
{
channel.configureBlocking(false);
} catch (IOException e)
{
DHT.log(e, LogLevel.Error);
}
remoteAddress = (InetSocketAddress) chan.socket().getRemoteSocketAddress();
setState(STATE_INITIAL, STATE_BASIC_HANDSHAKING);
}
// outgoing
public PullMetaDataConnection(byte[] infoHash, InetSocketAddress dest) throws IOException {
this.infoHash = infoHash;
this.remoteAddress = dest;
channel = SocketChannel.open();
//channel.socket().setReuseAddress(true);
channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
channel.configureBlocking(false);
//channel.bind(new InetSocketAddress(49002));
setState(STATE_INITIAL, STATE_CONNECTING);
sendBTHandshake();
}
private void sendBTHandshake() {
ByteBuffer outputBuffer = ByteBuffer.allocate(20+8+20+20);
byte[] peerID = new byte[20];
ThreadLocalUtils.getThreadLocalRandom().nextBytes(peerID);
outputBuffer.put(preamble);
outputBuffer.put(bitfield);
outputBuffer.put(infoHash);
outputBuffer.put(peerID);
outputBuffer.flip();
outputBuffers.addLast(outputBuffer);
}
public SelectableChannel getChannel() {
return channel;
}
public void registrationEvent(NIOConnectionManager manager, SelectionKey key) throws IOException {
connManager = manager;
lastReceivedTime = System.currentTimeMillis();
if(isState(STATE_CONNECTING))
{
try {
if(channel.connect(remoteAddress))
connectEvent();
} catch (IOException e) {
terminate("connect failed " + e.getMessage(), CloseReason.CONNECT_FAILED);
}
} else
{ // incoming
metaHandler.onConnect();
}
connManager.interestOpsChanged(this);
//System.out.println("attempting connect "+dest);
}
@Override
public int calcInterestOps() {
int ops = SelectionKey.OP_READ;
if(isState(STATE_CONNECTING))
ops |= SelectionKey.OP_CONNECT;
if(!outputBuffers.isEmpty())
ops |= SelectionKey.OP_WRITE;
return ops;
}
@Override
public void selectionEvent(SelectionKey key) throws IOException {
if(key.isValid() && key.isConnectable())
connectEvent();
if(key.isValid() && key.isReadable())
canReadEvent();
if(key.isValid() && key.isWritable())
canWriteEvent();
}
public void connectEvent() throws IOException {
try {
if(channel.isConnectionPending() && channel.finishConnect())
{
if(!setState(STATE_CONNECTING, STATE_BASIC_HANDSHAKING))
return;
connManager.interestOpsChanged(this);
metaHandler.onConnect();
//System.out.println("connection!");
}
} catch (IOException e) {
//System.err.println("connect failed "+e.getMessage());
terminate("connect failed", CloseReason.CONNECT_FAILED);
}
}
public MetadataPool getMetaData() {
return pool;
}
private void processInput() throws IOException {
inputBuffer.flip();
if(isState(STATE_BASIC_HANDSHAKING))
{
lastUsefulMessage = System.currentTimeMillis();
boolean connectionMatches = true;
byte[] temp = new byte[20];
byte[] otherBitfield = new byte[8];
// check preamble
inputBuffer.get(temp);
connectionMatches &= Arrays.equals(temp, preamble);
// check LTEP support
inputBuffer.get(otherBitfield);
if((otherBitfield[5] & 0x10) == 0)
terminate("peer does not support LTEP", CloseReason.NO_LTEP);
remoteSupportsFastExtension = (otherBitfield[7] & 0x04) != 0;
remoteSupportsPort = (otherBitfield[7] & 0x01) != 0;
// check infohash
inputBuffer.get(temp);
if(infoHash != null) {
connectionMatches &= Arrays.equals(temp, infoHash);
} else {
infoHash = temp.clone();
setState(STATE_BASIC_HANDSHAKING, STATE_IH_RECEIVED);
// state callback may terminate
if(isState(STATE_CLOSED))
return;
}
// check peer ID
inputBuffer.get(temp);
// log
idWriter.append(System.currentTimeMillis()+ " " + new Key(temp).toString(false) + " " + remoteAddress.getAddress().getHostAddress()+ " \n");
if(temp[0] == '-' && temp[1] == 'S' && temp[2] == 'D' && temp[3] == '0' && temp[4] == '1' && temp[5] == '0' && temp[6] == '0' && temp[7] == '-') {
terminate("xunlei doesn't support ltep", CloseReason.NO_LTEP);
}
if(!connectionMatches)
{
terminate("connction mismatch");
return;
}
// start parsing BT messages
if(incoming)
sendBTHandshake();
Map<String,Object> ltepHandshake = new HashMap<>();
Map<String,Object> messages = new HashMap<>();
if(ourListeningPort > 0)
ltepHandshake.put("p", ourListeningPort);
ltepHandshake.put("m", messages);
ltepHandshake.put("v","mlDHT metadata fetcher");
ltepHandshake.put("metadata_size", 0);
ltepHandshake.put("reqq", 256);
messages.put("ut_metadata", LTEP_LOCAL_META_ID);
messages.put("ut_pex", LTEP_LOCAL_PEX_ID);
// send handshake
BEncoder encoder = new BEncoder();
ByteBuffer handshakeBody = encoder.encode(ltepHandshake, 1024);
ByteBuffer handshakeHeader = ByteBuffer.allocate(BT_HEADER_LENGTH + 2);
handshakeHeader.putInt(handshakeBody.limit() + 2);
handshakeHeader.put((byte) LTEP_HEADER_ID);
handshakeHeader.put((byte) LTEP_HANDSHAKE_ID);
handshakeHeader.flip();
outputBuffers.addLast(handshakeHeader);
outputBuffers.addLast(handshakeBody);
/*
if(remoteSupportsFastExtension) {
ByteBuffer haveNone = ByteBuffer.allocate(5);
haveNone.put(3, (byte) 1);
haveNone.put(4, (byte) 0x0f);
outputBuffers.addLast(haveNone);
}
if(remoteSupportsPort && dhtPort != -1) {
ByteBuffer btPort = ByteBuffer.allocate(7);
btPort.putInt(3);
btPort.put((byte) 0x09);
btPort.putShort((short) dhtPort);
btPort.flip();
outputBuffers.addLast(btPort);
}*/
canWriteEvent();
//System.out.println("got basic handshake");
inputBuffer.position(0);
inputBuffer.limit(BT_HEADER_LENGTH);
setState(EnumSet.of(STATE_BASIC_HANDSHAKING, STATE_IH_RECEIVED),STATE_LTEP_HANDSHAKING);
return;
}
// parse BT header
if(inputBuffer.limit() == BT_HEADER_LENGTH)
{
int msgLength = inputBuffer.getInt();
// keepalive... wait for next msg
if(msgLength == 0)
{
inputBuffer.flip();
return;
}
int newLength = BT_HEADER_LENGTH + msgLength;
if(newLength > inputBuffer.capacity() || newLength < 0)
{
terminate("message size too large or < 0");
return;
}
// read payload
inputBuffer.limit(newLength);
return;
}
// skip header, we already processed that
inputBuffer.position(4);
// received a full message, reset timeout
lastReceivedTime = System.currentTimeMillis();
// read BT msg ID
int btMsgID = inputBuffer.get() & 0xFF;
if(btMsgID == LTEP_HEADER_ID)
{
// read LTEP msg ID
int ltepMsgID = inputBuffer.get() & 0xFF;
if(isState(STATE_LTEP_HANDSHAKING) && ltepMsgID == LTEP_HANDSHAKE_ID)
{
//System.out.println("got ltep handshake");
lastUsefulMessage = System.currentTimeMillis();
BDecoder decoder = new BDecoder();
Map<String,Object> remoteHandshake;
try {
remoteHandshake = decoder.decode(inputBuffer);
} catch (BDecodingException ex) {
terminate("invalid bencoding in ltep handshake", CloseReason.OTHER);
return;
}
Map<String,Object> messages = (Map<String, Object>) remoteHandshake.get("m");
if(messages == null)
{
terminate("no LTEP messages defined", CloseReason.NO_META_EXCHANGE);
return;
}
Long metaMsgID = (Long) messages.get("ut_metadata");
Long pexMsgID = (Long) messages.get("ut_pex");
Long metaLength = (Long) remoteHandshake.get("metadata_size");
Long maxR = (Long) remoteHandshake.get("reqq");
byte[] ver = (byte[]) remoteHandshake.get("v");
//if(maxR != null)
//maxRequests = maxR.intValue();
if(ver != null)
remoteClient = new String(ver,StandardCharsets.UTF_8);
if(pexMsgID != null)
ltepRemotePexId = pexMsgID.intValue();
if(metaMsgID != null && metaLength != null)
{
int newInfoLength = metaLength.intValue();
if(newInfoLength < 10) {
terminate("indicated meta length too small to be a torrent");
return;
}
// 30MB ought to be enough for everyone!
if(newInfoLength > 30*1024*1024) {
terminate("indicated meta length too large ("+newInfoLength+"), might be a resource exhaustion attack");
return;
}
pool = poolGenerator.apply(newInfoLength);
ltepRemoteMetadataExchangeMessageId = metaMsgID.intValue();
setState(STATE_LTEP_HANDSHAKING,STATE_GETTING_METADATA);
doMetaRequests();
} else if(pexMsgID != null && keepPexOnlyOpen) {
setState(STATE_LTEP_HANDSHAKING, STATE_PEX_ONLY);
} else {
terminate("no metadata exchange advertised, keep open disabled", CloseReason.NO_META_EXCHANGE);
}
if(pexMsgID == null && (metaMsgID == null || metaLength == null)){
terminate("neither metadata exchange support nor pex detected in LTEP -> peer is useless");
return;
}
// send 1 keep-alive
//outputBuffers.add(ByteBuffer.wrap(new byte[4]));
}
if(!isState(STATE_LTEP_HANDSHAKING) && ltepMsgID == LTEP_LOCAL_PEX_ID) {
BDecoder decoder = new BDecoder();
Map<String, Object> params = decoder.decode(inputBuffer);
pexConsumer.accept(AddressUtils.unpackCompact((byte[])params.get("added"), Inet4Address.class));
pexConsumer.accept(AddressUtils.unpackCompact((byte[])params.get("added6"), Inet6Address.class));
if(isState(STATE_PEX_ONLY))
terminate("got 1 pex, this peer is not useful for anything else", CloseReason.OTHER);
}
if(isState(STATE_GETTING_METADATA) && ltepMsgID == LTEP_LOCAL_META_ID)
{
// consumes bytes as necessary for the bencoding
BDecoder decoder = new BDecoder();
Map<String, Object> params = decoder.decode(inputBuffer);
Long type = (Long) params.get("msg_type");
Long idx = (Long) params.get("piece");
if(type == 1)
{ // piece
outstandingRequests--;
ByteBuffer chunk = ByteBuffer.allocate(inputBuffer.remaining());
chunk.put(inputBuffer);
pool.addBuffer(idx.intValue(), chunk);
lastUsefulMessage = System.currentTimeMillis();
doMetaRequests();
checkMetaRequests();
} else if(type == 2)
{ // reject
pool.releasePiece(idx.intValue());
terminate("request was rejected");
return;
}
}
}
/*
if(btMsgID == BT_BITFIELD_ID & !remoteSupportsFastExtension)
{
// just duplicate whatever they've sent but with 0-bits
ByteBuffer bitfield = ByteBuffer.allocate(inputBuffer.limit());
bitfield.putInt(bitfield.limit() - BT_HEADER_LENGTH);
bitfield.put((byte) BT_BITFIELD_ID);
bitfield.rewind();
outputBuffers.addLast(bitfield);
canWriteEvent();
}
*/
// parse next BT header
inputBuffer.position(0);
inputBuffer.limit(BT_HEADER_LENGTH);
}
public void canReadEvent() throws IOException {
int bytesRead = 0;
if(inputBuffer == null)
{
inputBuffer = ByteBuffer.allocate(32 * 1024);
// await BT handshake on first allocation since this has to be the first read
inputBuffer.limit(20+8+20+20);
}
do {
try
{
bytesRead = channel.read(inputBuffer);
} catch (IOException e)
{
terminate("exception on read, cause: "+e.getMessage());
}
if(bytesRead == -1)
terminate("reached end of stream on read");
// message complete as far as we need it
else if(inputBuffer.remaining() == 0)
processInput();
} while(bytesRead > 0 && !isState(STATE_CLOSED));
}
void doMetaRequests() throws IOException {
if(!isState(STATE_GETTING_METADATA))
return;
while(outstandingRequests <= maxRequests)
{
int idx = pool.reservePiece(this);
if(idx < 0)
break;
Map<String,Object> req = new HashMap<>();
req.put("msg_type", 0);
req.put("piece", idx);
BEncoder encoder = new BEncoder();
ByteBuffer body = encoder.encode(req, 512);
ByteBuffer header = ByteBuffer.allocate(BT_HEADER_LENGTH + 1 + 1);
header.putInt(2 + body.remaining());
header.put((byte) LTEP_HEADER_ID);
header.put((byte) ltepRemoteMetadataExchangeMessageId);
header.flip();
outstandingRequests++;
outputBuffers.addLast(header);
outputBuffers.addLast(body);
}
canWriteEvent();
}
void checkMetaRequests() throws IOException {
if(pool == null)
return;
pool.checkComletion(infoHash);
if(pool.status != Completion.PROGRESS)
terminate("meta data exchange finished or failed");
}
public void canWriteEvent() throws IOException {
try
{
while(!outputBuffers.isEmpty())
{
if(!outputBuffers.peekFirst().hasRemaining()) {
outputBuffers.removeFirst();
continue;
}
long written = channel.write(outputBuffers.toArray(new ByteBuffer[outputBuffers.size()]));
if(written == 0) {
// socket buffer full, update selector
connManager.interestOpsChanged(this);
break;
}
}
} catch (IOException e)
{
terminate("error on write, cause: "+e.getMessage());
return;
}
// drained queue -> update selector
if(outputBuffers.isEmpty())
connManager.interestOpsChanged(this);
}
public void doStateChecks(long now) throws IOException {
// connections sharing a pool might get stalled if no more requests are left
doMetaRequests();
// hash check may have finished or failed due to other pool members
checkMetaRequests();
long timeSinceUsefulMessage = now - lastUsefulMessage;
long age = now - lastReceivedTime;
if(age > RCV_TIMEOUT || (lastUsefulMessage > 0 && timeSinceUsefulMessage > 2 * RCV_TIMEOUT)) {
terminate("closing idle connection "+age+" "+state+" "+outstandingRequests+" "+outputBuffers.size()+" "+inputBuffer);
}
else if(!channel.isOpen())
terminate("async close detected");
}
public void terminate(String reasonStr, CloseReason reason) throws IOException {
synchronized (this) {
CONNECTION_STATE oldState = state.get();
if(!setState(EnumSet.complementOf(EnumSet.of(STATE_CLOSED)), STATE_CLOSED))
return;
//if(!isState(STATE_CONNECTING))
//System.out.println("closing connection for "+(infoHash != null ? new Key(infoHash).toString(false) : null)+" to "+destination+"/"+remoteClient+" state:"+state+" reason:"+reason);
if(pool != null)
pool.deRegister(this);
closeReason = reason;
if(metaHandler != null)
metaHandler.onTerminate();
channel.close();
DHT.log(String.format("closing pull connection inc: %b reason: %s flag: %s state: %s ", incoming, reasonStr, reason, oldState), LogLevel.Debug);
}
}
@Deprecated
public void terminate(String reason) throws IOException {
terminate(reason, CloseReason.OTHER);
}
}