├── server ├── src │ ├── sql │ │ ├── database │ │ └── init.sql │ ├── main │ │ ├── java │ │ │ └── cc │ │ │ │ └── aguesuka │ │ │ │ └── btfind │ │ │ │ ├── dht │ │ │ │ ├── handler │ │ │ │ │ ├── DhtHandlerException.java │ │ │ │ │ ├── IBaseDhtChain.java │ │ │ │ │ ├── IDhtUnknownChain.java │ │ │ │ │ ├── IDhtQueryChain.java │ │ │ │ │ ├── IDhtHandlerChain.java │ │ │ │ │ ├── chain │ │ │ │ │ │ ├── SaveInfoHashChain.java │ │ │ │ │ │ ├── DhtRecordChain.java │ │ │ │ │ │ ├── DhtMainChain.java │ │ │ │ │ │ └── JoinDhtChain.java │ │ │ │ │ └── DhtHandler.java │ │ │ │ ├── beans │ │ │ │ │ └── KrpcMessage.java │ │ │ │ └── KrpcToken.java │ │ │ │ ├── connection │ │ │ │ ├── NioHandler.java │ │ │ │ ├── Bootstrap.java │ │ │ │ └── NioEventLoop.java │ │ │ │ ├── util │ │ │ │ ├── GetIp.java │ │ │ │ ├── Bep0042Impl.java │ │ │ │ ├── record │ │ │ │ │ ├── ActionEnum.java │ │ │ │ │ └── ActionRecord.java │ │ │ │ └── DhtServerConfig.java │ │ │ │ ├── ServiceApplication.java │ │ │ │ └── dao │ │ │ │ └── InfoHashDao.java │ │ └── resources │ │ │ └── application.yml │ └── test │ │ └── java │ │ └── cc │ │ └── aguesuka │ │ └── btfind │ │ └── util │ │ └── Bep0042ImplTest.java └── pom.xml ├── bencode ├── src │ ├── test │ │ └── java │ │ │ └── cc │ │ │ └── aguesuka │ │ │ └── bencode │ │ │ ├── util │ │ │ └── ByteUtilTest.java │ │ │ ├── BencodeListTest.java │ │ │ ├── IBencodeTest.java │ │ │ └── BencodeTest.java │ └── main │ │ └── java │ │ └── cc │ │ └── aguesuka │ │ └── bencode │ │ ├── BencodeToken.java │ │ ├── BencodeList.java │ │ ├── BencodeException.java │ │ ├── BencodeMap.java │ │ ├── IBencode.java │ │ ├── BencodeInteger.java │ │ ├── IBencodeContainer.java │ │ ├── BencodeByteArray.java │ │ ├── util │ │ ├── ByteUtil.java │ │ └── HexUtil.java │ │ ├── BencodeEncoder.java │ │ ├── Bencode.java │ │ └── BencodeParser.java ├── pom.xml └── README.md ├── .gitignore ├── README.md └── pom.xml /server/src/sql/database: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aguesuka/ague-dht/HEAD/server/src/sql/database -------------------------------------------------------------------------------- /server/src/sql/init.sql: -------------------------------------------------------------------------------- 1 | VACUUM; 2 | create table INFO_HASH 3 | ( 4 | PR_ID integer not null 5 | constraint INFO_HASH_pk 6 | primary key autoincrement, 7 | CREATE_TIME TEXT, 8 | HEX_HASH text, 9 | ADDRESS TEXT 10 | ); 11 | 12 | 13 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/DhtHandlerException.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler; 2 | 3 | /** 4 | * @author :aguesuka 5 | * 2019/9/11 11:17 6 | */ 7 | class DhtHandlerException extends RuntimeException { 8 | DhtHandlerException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bencode/src/test/java/cc/aguesuka/bencode/util/ByteUtilTest.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode.util; 2 | 3 | import org.junit.Test; 4 | 5 | /** 6 | * @author :aguesuka 7 | * 2019/9/1 19:25 8 | */ 9 | public class ByteUtilTest { 10 | 11 | @Test 12 | public void compareBytes() { 13 | } 14 | 15 | @Test 16 | public void isUtf8() { 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/IBaseDhtChain.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler; 2 | 3 | /** 4 | * @author :aguesuka 5 | * 2019/9/25 12:36 6 | */ 7 | public interface IBaseDhtChain { 8 | /** 9 | * 是否启用 10 | * 11 | * @return 是否启用 12 | */ 13 | default boolean enable(){ 14 | return true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/BencodeToken.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | /** 4 | * @author :aguesuka 5 | * 2019/9/3 17:23 6 | */ 7 | class BencodeToken { 8 | static final byte INT = 'i'; 9 | static final byte DICT = 'd'; 10 | static final byte END = 'e'; 11 | static final byte LIST = 'l'; 12 | static final byte SPLIT = ':'; 13 | static final String NULL = ""; 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | database 4 | # Log file 5 | *.log 6 | logs/ 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.jar 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | # torrent file 27 | *.torrent 28 | *.info 29 | 30 | # idea file 31 | .idea/ 32 | target/ 33 | *.iml -------------------------------------------------------------------------------- /server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | root: warn 4 | file: logs/log.log 5 | 6 | spring: 7 | aop: 8 | auto: true 9 | proxy-target-class: true 10 | dht: 11 | bootstrap-nodes: 12 | - router.bittorrent.com:6881 13 | - dht.transmissionbt.com:6881 14 | - router.utorrent.com:6881 15 | - dht.aelitis.com:6881 16 | - localhost:8888 17 | ip-for-bep0042: 223.155.69.210 18 | join-dht-interval: 3000 19 | join-dht-count: 30000 20 | join-dht-max-size: 100000 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ague-dht 2 | = 3 | ague-dht 是一个磁力链接嗅探器, 它伪装成 BT 下载客服端, 加入 DHT 网络, 嗅探磁力链接. 4 | 每秒发送1000条请求时, 平均 3 秒收到 1 次带有 infohash 的 announce_peer 请求; 10 次 get_peer请求. 5 | ## 环境要求 6 | JDK11, MAVEN3, 以及公网 IP. 7 | 8 | ## 快速开始 9 | 10 | - clone仓库 11 | ``` 12 | git clone https://github.com/aguesuka/ague-dht 13 | ``` 14 | - 使用maven打包 15 | ``` 16 | cd ague-dht 17 | mvn package 18 | ``` 19 | - 运行程序 20 | ``` 21 | java -jar ./server/target/server-1.2.jar 22 | ``` 23 | ## 磁力链接转种子 24 | https://github.com/aguesuka/torrent-finder 25 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/connection/NioHandler.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.connection; 2 | 3 | import java.nio.channels.SelectionKey; 4 | 5 | /** 6 | * @author :aguesuka 7 | * 2019/9/4 13:52 8 | */ 9 | public interface NioHandler { 10 | /** 11 | * {@link java.nio.channels.Selector}轮询后,调用该方法处理 12 | * 13 | * @param key SelectionKey 14 | * @throws InterruptedException 只有中断异常能打断循环 15 | * @throws Exception 其他异常将被打印 16 | */ 17 | void doHandler(SelectionKey key) throws Exception; 18 | } 19 | -------------------------------------------------------------------------------- /bencode/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | cc.aguesuka 7 | ague-dht 8 | 1.2 9 | 10 | 4.0.0 11 | jar 12 | bencode 13 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/BencodeList.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * @author :aguesuka 7 | * 2019/6/30 16:58 8 | */ 9 | @SuppressWarnings({"WeakerAccess", "unused"}) 10 | public final class BencodeList extends ArrayList implements IBencode, IBencodeContainer { 11 | public void addLong(long i) { 12 | add(new BencodeInteger(i)); 13 | } 14 | 15 | public void addByteArray(byte[] bytes) { 16 | add(new BencodeByteArray(bytes)); 17 | } 18 | 19 | @Override 20 | public IBencode getBencode(Integer key) { 21 | return get(key); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/util/GetIp.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.util; 2 | 3 | import cc.aguesuka.bencode.util.HexUtil; 4 | 5 | import java.net.InetAddress; 6 | import java.net.UnknownHostException; 7 | 8 | /** 9 | * the command util parser ip byte array to string 10 | * 11 | * @author :aguesuka 12 | * 2019/9/11 18:07 13 | */ 14 | public class GetIp { 15 | public static void main(String[] args) throws UnknownHostException { 16 | String ipHex = "DF9B45D2"; 17 | if (args.length > 0) { 18 | ipHex = args[0]; 19 | } 20 | System.out.println(InetAddress.getByAddress(HexUtil.decode(ipHex))); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/IDhtUnknownChain.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler; 2 | 3 | import cc.aguesuka.btfind.dht.beans.KrpcMessage; 4 | 5 | /** 6 | * @author :aguesuka 7 | * 2019/9/11 19:49 8 | */ 9 | public interface IDhtUnknownChain extends IBaseDhtChain{ 10 | /** 11 | * 收到未知类型的消息 12 | * 13 | * @param query 未知类型的消息 14 | */ 15 | void onUnknownTypeQuery(KrpcMessage query); 16 | 17 | /** 18 | * 收到错误消息 19 | * 20 | * @param error 错误消息 21 | */ 22 | void onRecvError(KrpcMessage error); 23 | 24 | /** 25 | * 收到未知类型的消息 26 | * 27 | * @param message 未知类型的消息 28 | */ 29 | void onUnknownType(KrpcMessage message); 30 | } 31 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/BencodeException.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | /** 4 | * @author :aguesuka 5 | * 2019/9/1 23:15 6 | */ 7 | public final class BencodeException extends RuntimeException { 8 | private final Object data; 9 | 10 | BencodeException(Object data, Throwable cause) { 11 | super(cause); 12 | this.data = data; 13 | } 14 | 15 | BencodeException(Object data, String msg, Throwable cause) { 16 | super(msg, cause); 17 | this.data = data; 18 | } 19 | 20 | 21 | BencodeException(Object data, String message) { 22 | super(message); 23 | this.data = data; 24 | } 25 | 26 | @SuppressWarnings("unused") 27 | public Object getData() { 28 | return data; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/BencodeMap.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import java.util.LinkedHashMap; 4 | 5 | /** 6 | * @author :aguesuka 7 | * 2019/6/30 16:57 8 | */ 9 | public final class BencodeMap extends LinkedHashMap implements IBencode, IBencodeContainer { 10 | public void putByteArray(String key, byte[] value) { 11 | put(key, new BencodeByteArray(value)); 12 | } 13 | 14 | public void putString(String key, String value) { 15 | putByteArray(key, value.getBytes(Bencode.getCharset())); 16 | } 17 | 18 | @SuppressWarnings("unused") 19 | public void putLong(String key, long value) { 20 | put(key, new BencodeInteger(value)); 21 | } 22 | 23 | @Override 24 | public IBencode getBencode(String key) { 25 | return get(key); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bencode/README.md: -------------------------------------------------------------------------------- 1 | Bencode 2 | = 3 | bencode编码是bt协议中最常用的数据格式 4 | http://www.bittorrent.org/beps/bep_0003.html#bencoding 5 | 6 | 7 | ## 四种数据类型的对应实现如下 8 | - Dict 对应 ```BencodeMap``` 9 | - List 对应 ```BencodeList``` 10 | - String 对应 ```BencodeByteArray``` 11 | - Integer 对应 ```BencodeInteger``` 12 | 13 | 14 | ## 使用 15 | ```$xslt 16 | // byte[] 转BenocdeMap对象 17 | Bencode.parse(byte[]) 18 | ``` 19 | ``` 20 | // 对象转byte[] 21 | // IBencode bencodeObject = new BencodeMap(); 22 | bencodeObject.toBencodeBytes(); 23 | ``` 24 | ``` 25 | // 从ByteBuff中读BenocdeMap对象 26 | Bencode.parse(ByteBuff) 27 | ``` 28 | ``` 29 | // 对象写入到ByteBuff中 30 | // IBencode bencodeObject = new BencodeMap(); 31 | bencodeObject.writeToBuffer(ByteBuff) 32 | ``` 33 | ## 其他 34 | - ```BencodeParser``` 使用非递归实现了byte[] 转对象. 35 | - ```BencodeEncoder``` 使用递归实现了对象转byte[]. 36 | 37 | 如果需要最简单的实现,可以只拷贝这两个类到你的项目,并将BencodeParser中的集合容改为java.util的集合 38 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/IDhtQueryChain.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler; 2 | 3 | import cc.aguesuka.btfind.dht.beans.KrpcMessage; 4 | 5 | /** 6 | * @author :aguesuka 7 | * 2019/9/11 20:00 8 | */ 9 | public interface IDhtQueryChain extends IBaseDhtChain{ 10 | /** 11 | * 收到请求时 12 | * 13 | * @param query 请求 14 | */ 15 | default void onQuery(KrpcMessage query){ 16 | 17 | } 18 | 19 | /** 20 | * 收到ping请求 21 | * 22 | * @param query 请求消息 23 | */ 24 | void onPing(KrpcMessage query); 25 | 26 | /** 27 | * findNode请求 28 | * 29 | * @param query 请求消息 30 | */ 31 | void onFindNodes(KrpcMessage query); 32 | 33 | /** 34 | * GetPeer请求 35 | * 36 | * @param query 请求消息 37 | */ 38 | void onGetPeer(KrpcMessage query); 39 | 40 | /** 41 | * AnnouncePeer请求 42 | * 43 | * @param query 请求消息 44 | */ 45 | void onAnnouncePeer(KrpcMessage query); 46 | } 47 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/beans/KrpcMessage.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.beans; 2 | 3 | import cc.aguesuka.bencode.BencodeMap; 4 | import cc.aguesuka.btfind.dht.KrpcToken; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | 8 | import java.net.SocketAddress; 9 | 10 | /** 11 | * @author :aguesuka 12 | * 2019/9/11 13:10 13 | */ 14 | @Data 15 | @AllArgsConstructor 16 | public class KrpcMessage { 17 | private BencodeMap message; 18 | private SocketAddress address; 19 | 20 | public String type() { 21 | return message.getString(KrpcToken.TYPE); 22 | } 23 | 24 | public String queryType() { 25 | return message.getString(KrpcToken.QUERY); 26 | } 27 | 28 | /** 29 | * find_nodes or get_peer response nodes 30 | * 31 | * @return byte array length integer multiple of 26(ipv4); id(20) + host(4) + port(2) 32 | */ 33 | public byte[] nodes() { 34 | return message.getBencodeMap(KrpcToken.RESPONSES_MAP).getByteArray(KrpcToken.NODES); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/IBencode.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | /** 6 | * @author :aguesuka 7 | * 2019/6/30 16:56 8 | */ 9 | public interface IBencode { 10 | /** 11 | * 转为bencode byte数组 12 | * 13 | * @return bencode byte数组 14 | * @throws BencodeException 转换失败 15 | */ 16 | default byte[] toBencodeBytes() throws BencodeException { 17 | return Bencode.toBytes(this); 18 | } 19 | 20 | /** 21 | * 转为byteBuffer类型 22 | * 23 | * @return byteBuffer 24 | * @throws BencodeException 转换失败 25 | */ 26 | default ByteBuffer toByteBuffer() throws BencodeException { 27 | return ByteBuffer.wrap(toBencodeBytes()); 28 | } 29 | 30 | /** 31 | * 将Bencode的内容写入buffer 32 | * 33 | * @param buffer buffer 34 | * @return 参数 buffer 35 | * @throws BencodeException 对象中有无法写入的类型 36 | */ 37 | default ByteBuffer writeToBuffer(ByteBuffer buffer) throws BencodeException { 38 | return Bencode.writeToBuffer(this, buffer); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bencode/src/test/java/cc/aguesuka/bencode/BencodeListTest.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | import java.security.SecureRandom; 7 | 8 | /** 9 | * @author :aguesuka 10 | * 2019/9/1 10:31 11 | */ 12 | public class BencodeListTest { 13 | 14 | private SecureRandom secureRandom = new SecureRandom(); 15 | 16 | @Test 17 | public void addLong() { 18 | } 19 | 20 | @Test 21 | public void addByteArray() { 22 | } 23 | 24 | @Test 25 | public void getByteArray() { 26 | 27 | byte[] bytes = new byte[0]; 28 | secureRandom.nextBytes(bytes); 29 | BencodeList test = new BencodeList(); 30 | test.add(null); 31 | test.addByteArray(bytes); 32 | assert null == test.getByteArray(0); 33 | Assert.assertArrayEquals(bytes, test.getByteArray(1)); 34 | } 35 | 36 | @Test 37 | public void getInteger() { 38 | 39 | } 40 | 41 | @Test 42 | public void getBencodeMap() { 43 | } 44 | 45 | @Test 46 | public void getBencodeList() { 47 | } 48 | } -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/IDhtHandlerChain.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler; 2 | 3 | import cc.aguesuka.btfind.dht.beans.KrpcMessage; 4 | 5 | import java.util.NoSuchElementException; 6 | 7 | /** 8 | * @author :aguesuka 9 | * 2019/9/11 19:34 10 | */ 11 | public interface IDhtHandlerChain extends IBaseDhtChain { 12 | 13 | /** 14 | * 权重,权重高的优先处理 15 | * 16 | * @return 权重 17 | */ 18 | default int weights() { 19 | return 0; 20 | } 21 | 22 | /** 23 | * 收到回复时 24 | * 25 | * @param response 回复 26 | */ 27 | default void onResponse(KrpcMessage response) { 28 | } 29 | 30 | /** 31 | * 可写时 32 | * 33 | * @return 消息 34 | * @throws NullPointerException 如果没有可写的消息则抛出异常 35 | * @throws NoSuchElementException 如果没有可写的消息则抛出异常 36 | * @throws UnsupportedOperationException 不支持的操作 37 | */ 38 | default KrpcMessage getMessage() { 39 | throw new UnsupportedOperationException(); 40 | } 41 | 42 | /** 43 | * 是否有消息可以发送 44 | * 45 | * @return 是否有消息可以发送 46 | */ 47 | default boolean isWriteAble() { 48 | return false; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/BencodeInteger.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | /** 4 | * 整数类型,实际取值范围和java int 不同,不可变对象 5 | * 6 | * @author :aguesuka 7 | * 2019/6/30 17:02 8 | */ 9 | public final class BencodeInteger extends Number implements IBencode { 10 | private final long value; 11 | 12 | BencodeInteger(String val) { 13 | value = Long.parseLong(val); 14 | } 15 | 16 | public BencodeInteger(long val) { 17 | value = val; 18 | } 19 | 20 | 21 | @Override 22 | public int intValue() { 23 | return (int) value; 24 | } 25 | 26 | @Override 27 | public long longValue() { 28 | return value; 29 | } 30 | 31 | @Override 32 | public float floatValue() { 33 | return value; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return Long.toString(value); 39 | } 40 | 41 | @Override 42 | public double doubleValue() { 43 | return (double) value; 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Long.hashCode(value); 49 | } 50 | 51 | @Override 52 | public boolean equals(Object o) { 53 | if (this == o) { 54 | return true; 55 | } 56 | if (!(o instanceof BencodeInteger)) { 57 | return false; 58 | } 59 | BencodeInteger that = (BencodeInteger) o; 60 | return this.value == that.value; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/src/test/java/cc/aguesuka/btfind/util/Bep0042ImplTest.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.util; 2 | 3 | import cc.aguesuka.bencode.util.HexUtil; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | import java.net.UnknownHostException; 8 | import java.util.Arrays; 9 | 10 | /** 11 | * @author :aguesuka 12 | * 2019/9/9 12:33 13 | */ 14 | public class Bep0042ImplTest { 15 | 16 | @Test 17 | public void changeId() throws UnknownHostException { 18 | testChangeId("124.31.75.21", "5fbfbff10c5d6a4ec8a88e4c6ab4c28b95eee401", true); 19 | testChangeId("21.75.31.124", "5a3ce9c14e7a08645677bbd1cfe7d8f956d53256", true); 20 | testChangeId("65.23.51.170", "a5d43220bc8f112a3d426c84764f8c2a1150e616", true); 21 | testChangeId("84.124.73.14", "1b0321dd1bb1fe518101ceef99462b947a01ff41", true); 22 | testChangeId("43.213.53.83", "e56f6cbf5b7c4be0237986d5243b87aa6d51305a", true); 23 | 24 | testChangeId("43.213.53.83", "e56f6cbf5b7c4be0237986d5243b87aa6d51305f", false); 25 | testChangeId("43.213.53.82", "e56f6cbf5b7c4be0237986d5243b87aa6d51305a", false); 26 | testChangeId("167.179.110.63", "DCCD90DA296D3E62E0961234BF39A63F895EF126", true); 27 | } 28 | 29 | private void testChangeId(String host, String id, boolean isEquals) throws UnknownHostException { 30 | byte[] src = HexUtil.decode(id); 31 | byte[] result = Bep0042Impl.changeId(host, src); 32 | if (isEquals) { 33 | Assert.assertArrayEquals(src, result); 34 | } else { 35 | assert !Arrays.equals(src, result); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/util/Bep0042Impl.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.util; 2 | 3 | import java.net.InetAddress; 4 | import java.net.UnknownHostException; 5 | import java.nio.ByteBuffer; 6 | import java.util.zip.CRC32C; 7 | 8 | /** 9 | * http://www.bittorrent.org/beps/bep_0042.html 10 | * 11 | * @author :aguesuka 12 | * 2019/9/9 12:30 13 | */ 14 | class Bep0042Impl { 15 | private static final byte[] V4_MASK = {0x03, 0x0f, 0x3f, (byte) 0xff}; 16 | 17 | /** 18 | * http://www.bittorrent.org/beps/bep_0042.html 19 | * 20 | * @return id 21 | */ 22 | static byte[] changeId(String hostString, byte[] id) throws UnknownHostException { 23 | int r = id[19]; 24 | CRC32C c = new CRC32C(); 25 | byte[] ip = InetAddress.getByName(hostString).getAddress(); 26 | byte[] mask = V4_MASK; 27 | if (ip.length != 4) { 28 | // todo ipv6 29 | throw new IllegalArgumentException("只支持ipv4协议"); 30 | } 31 | for (int i = 0; i < mask.length; i++) { 32 | ip[i] &= mask[i]; 33 | } 34 | ip[0] |= r << 5; 35 | c.reset(); 36 | c.update(ip, 0, ip.length); 37 | byte[] crc = i2b((int) c.getValue()); 38 | byte[] result = id.clone(); 39 | 40 | System.arraycopy(crc, 0, result, 0, 2); 41 | result[2] = (byte) (((byte) (crc[2] & 0xf8)) | ((byte) (result[2] & 0b111))); 42 | result[19] = (byte) r; 43 | return result; 44 | } 45 | 46 | private static byte[] i2b(int b) { 47 | return ByteBuffer.allocate(4).putInt(b).array(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cc.aguesuka 8 | ague-dht 9 | pom 10 | 1.2 11 | 12 | bencode 13 | server 14 | 15 | 16 | UTF-8 17 | UTF-8 18 | 19 | 20 | 21 | 22 | org.apache.maven.plugins 23 | maven-compiler-plugin 24 | 25 | 11 26 | 11 27 | 28 | 3.8.0 29 | 30 | 31 | 32 | 33 | 34 | junit 35 | junit 36 | 4.13.1 37 | 38 | 39 | org.projectlombok 40 | lombok 41 | 1.18.20 42 | 43 | 44 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/IBencodeContainer.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | /** 4 | * @author :aguesuka 5 | * 2019/6/30 17:25 6 | */ 7 | interface IBencodeContainer { 8 | /** 9 | * get 强转为 byte[] 10 | * 11 | * @param key 键 12 | * @return byte[] 13 | */ 14 | default byte[] getByteArray(T key) { 15 | IBencode bencode = getBencode(key); 16 | return bencode == null ? null : ((BencodeByteArray) bencode).getBytes(); 17 | } 18 | 19 | /** 20 | * get 强转为Integer 21 | * 22 | * @param key 键 23 | * @return int 24 | */ 25 | default BencodeInteger getInteger(T key) { 26 | return (BencodeInteger) getBencode(key); 27 | } 28 | 29 | /** 30 | * get 强转为BencodeMap 31 | * 32 | * @param key 键 33 | * @return BencodeMap 34 | */ 35 | default BencodeMap getBencodeMap(T key) { 36 | return (BencodeMap) getBencode(key); 37 | } 38 | 39 | /** 40 | * get 强转为 BencodeList 41 | * 42 | * @param key 键 43 | * @return BencodeList 44 | */ 45 | @SuppressWarnings("unused") 46 | default BencodeList getBencodeList(T key) { 47 | return (BencodeList) getBencode(key); 48 | } 49 | 50 | /** 51 | * get 强转为 52 | * 53 | * @param key 键 54 | * @return BencodeList 55 | */ 56 | IBencode getBencode(T key); 57 | 58 | /** 59 | * 获得String 60 | * 61 | * @param key k键 62 | * @return String 63 | */ 64 | default String getString(T key) { 65 | byte[] value = getByteArray(key); 66 | return value == null ? null : new String(value, Bencode.getCharset()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/ServiceApplication.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind; 2 | 3 | import cc.aguesuka.btfind.connection.Bootstrap; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.Banner; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.context.ConfigurableApplicationContext; 11 | import org.springframework.context.annotation.Bean; 12 | import org.sqlite.SQLiteDataSource; 13 | 14 | import javax.sql.DataSource; 15 | import java.io.IOException; 16 | import java.nio.file.Path; 17 | 18 | /** 19 | * @author aguesuka 20 | */ 21 | @SpringBootApplication 22 | public class ServiceApplication { 23 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 24 | @Value("${dht.databasePath:#{\"database\"}}") 25 | public String databasePath; 26 | 27 | public static void main(String[] args) throws IOException { 28 | SpringApplication app = new SpringApplication(ServiceApplication.class); 29 | app.setBannerMode(Banner.Mode.OFF); 30 | ConfigurableApplicationContext run = app.run(args); 31 | Bootstrap bootstrap = run.getBeanFactory().getBean(Bootstrap.class); 32 | bootstrap.start(); 33 | } 34 | 35 | 36 | @Bean 37 | public DataSource dataSource() { 38 | SQLiteDataSource dataSource = new SQLiteDataSource(); 39 | logger.error("the database is `{}`", Path.of(databasePath).toAbsolutePath().toString()); 40 | dataSource.setUrl("jdbc:sqlite:" + databasePath); 41 | return dataSource; 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/util/record/ActionEnum.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.util.record; 2 | 3 | /** 4 | * @author :aguesuka 5 | * 2019/9/4 21:22 6 | */ 7 | @SuppressWarnings("unused") 8 | public enum ActionEnum { 9 | /** 10 | * action type 11 | */ 12 | LOOP_SELECT(""), 13 | LOOP_KEYS(""), 14 | DHT_READABLE("dht可读次数"), 15 | DHT_WRITABLE("dht可写次数"), 16 | DHT_ADD_RESPONSE_QUEUE("添加到回复队列中"), 17 | DHT_RECV_UNKNOWN_TYPE("收到未知类型消息"), 18 | DHT_RECV_QUERY("收到请求"), 19 | DHT_RECV_RESPONSE("收到回复"), 20 | DHT_RECV_ERROR("收到错误"), 21 | DHT_SEND_SUCCESS("发送成功"), 22 | DHT_SEND_FAIL("发送失败"), 23 | DHT_RECV_UNKNOWN_QUERY("收到未知类型请求"), 24 | DHT_RECV_PING("收到ping"), 25 | DHT_RECV_FIND_NODE("收到find_node"), 26 | DHT_RECV_GET_PEERS("收到get_peers"), 27 | DHT_RECV_ANNOUNCE_PEER("收到announce_peer"), 28 | DHT_GET_REPEAT_ADDRESS("收到重复地址"), 29 | DHT_GET_NEW_ADDRESS("收到新地址"), 30 | DHT_RECV_LENGTH("收到消息长度kb"), 31 | DHT_SEND_LENGTH("发送消息长度kb"), 32 | DHT_EXCEPTION("错误次数"), 33 | DHT_FILTERED(""), 34 | JOIN_DHT_INTERVAL("join dht周期"), 35 | JOIN_DHT_CLEAR("join dht清空路由表"), 36 | JOIN_DHT_RESTART("join dht重新启动"), 37 | MD_ADD_TASK(""), 38 | MD_DHT_CLEAR_GREY("清空灰名单"), 39 | MD_FINISH_CONNECT(""), 40 | MD_SEND_HANDSHAKE(""), 41 | MD_RECV_HANDSHAKE(""), 42 | MD_SEND_SUPPORT(""), 43 | MD_RECV_METADATA(""), 44 | MD_RECV_PACIER(""), 45 | MD_CHECK_OVER(""), 46 | MD_SAVE(""), 47 | MD_RECV_PEER_INFO(""), 48 | ; 49 | private final String description; 50 | 51 | ActionEnum(String description) { 52 | this.description = description; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return description.isEmpty() ? name() : description; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/chain/SaveInfoHashChain.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler.chain; 2 | 3 | import cc.aguesuka.bencode.util.HexUtil; 4 | import cc.aguesuka.btfind.dao.InfoHashDao; 5 | import cc.aguesuka.btfind.dht.KrpcToken; 6 | import cc.aguesuka.btfind.dht.beans.KrpcMessage; 7 | import cc.aguesuka.btfind.dht.handler.IDhtQueryChain; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | /** 13 | * save hash info when other peer query server 14 | * you can implement it you self 15 | * 16 | * @author :aguesuka 17 | * 2019/9/12 16:43 18 | */ 19 | @Slf4j 20 | @Component 21 | public class SaveInfoHashChain implements IDhtQueryChain { 22 | 23 | private final InfoHashDao infoHashDao; 24 | 25 | @Autowired 26 | public SaveInfoHashChain(InfoHashDao infoHashDao) { 27 | this.infoHashDao = infoHashDao; 28 | } 29 | 30 | @Override 31 | public void onPing(KrpcMessage query) { 32 | 33 | } 34 | 35 | @Override 36 | public void onFindNodes(KrpcMessage query) { 37 | 38 | } 39 | 40 | @Override 41 | public void onGetPeer(KrpcMessage query) { 42 | // ignore 43 | } 44 | 45 | @Override 46 | public void onAnnouncePeer(KrpcMessage query) { 47 | try { 48 | byte[] infoHash = query.getMessage() 49 | .getBencodeMap(KrpcToken.ARGUMENTS_MAP) 50 | .getByteArray(KrpcToken.INFO_HASH); 51 | if (infoHash == null || infoHash.length != KrpcToken.ID_LENGTH) { 52 | return; 53 | } 54 | infoHashDao.save(HexUtil.encode(infoHash), query.getAddress()); 55 | } catch (RuntimeException e) { 56 | log.error(e.getMessage(), e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/BencodeByteArray.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | 4 | import cc.aguesuka.bencode.util.ByteUtil; 5 | import cc.aguesuka.bencode.util.HexUtil; 6 | 7 | import java.nio.ByteBuffer; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.Arrays; 10 | 11 | /** 12 | * 数组包装类,不可变对象,不能包装null 13 | * 14 | * @author :aguesuka 15 | * 2019/6/30 16:59 16 | */ 17 | public final class BencodeByteArray implements IBencode, Comparable { 18 | final private static BencodeByteArray BENCODE_BYTE_ARRAY = new BencodeByteArray(new byte[0]); 19 | public static BencodeByteArray empty(){ 20 | return BENCODE_BYTE_ARRAY; 21 | } 22 | private final byte[] data; 23 | 24 | public BencodeByteArray(byte[] data) { 25 | this.data = data.clone(); 26 | } 27 | 28 | @SuppressWarnings("WeakerAccess") 29 | public byte[] getBytes() { 30 | return data.clone(); 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | if (this == o) { 36 | return true; 37 | } 38 | if (!(o instanceof BencodeByteArray)) { 39 | return false; 40 | } 41 | BencodeByteArray that = (BencodeByteArray) o; 42 | return Arrays.equals(getBytes(), that.getBytes()); 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | return Arrays.hashCode(getBytes()); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | if (ByteUtil.isUtf8(ByteBuffer.wrap(getBytes()))) { 53 | return new String(getBytes(), StandardCharsets.UTF_8); 54 | } 55 | return HexUtil.encode(getBytes()); 56 | } 57 | 58 | @Override 59 | public int compareTo(BencodeByteArray o) { 60 | return ByteUtil.compareBytes(this.getBytes(), o.getBytes()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/connection/Bootstrap.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.connection; 2 | 3 | import cc.aguesuka.btfind.dht.handler.DhtHandler; 4 | import cc.aguesuka.btfind.util.DhtServerConfig; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PreDestroy; 10 | import java.io.IOException; 11 | import java.net.InetSocketAddress; 12 | import java.nio.channels.DatagramChannel; 13 | import java.nio.channels.SelectionKey; 14 | 15 | /** 16 | * bootstrap 17 | * 18 | * @author :aguesuka 19 | * 2019/9/9 13:39 20 | */ 21 | @Slf4j 22 | @Component 23 | public class Bootstrap { 24 | private final NioEventLoop nioEventLoop; 25 | private final DhtServerConfig config; 26 | private final DhtHandler dhtHandler; 27 | 28 | @Autowired 29 | public Bootstrap(NioEventLoop nioEventLoop, 30 | DhtServerConfig config, 31 | DhtHandler dhtHandler) { 32 | this.nioEventLoop = nioEventLoop; 33 | this.config = config; 34 | this.dhtHandler = dhtHandler; 35 | } 36 | 37 | /** 38 | * start dht network 39 | * 40 | * @throws IOException If an I/O error occurs 41 | */ 42 | public void start() throws IOException { 43 | nioEventLoop.init(); 44 | DatagramChannel udpChannel = DatagramChannel.open(); 45 | udpChannel.bind(new InetSocketAddress(config.getDhtPort())) 46 | .configureBlocking(false); 47 | udpChannel.register(nioEventLoop.getSelector(), SelectionKey.OP_WRITE | SelectionKey.OP_READ, dhtHandler); 48 | log.error("sever start"); 49 | nioEventLoop.loop(); 50 | log.error("sever stop"); 51 | } 52 | 53 | 54 | @PreDestroy 55 | public void end() throws IOException, InterruptedException { 56 | nioEventLoop.close(); 57 | nioEventLoop.getSelector().close(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dao/InfoHashDao.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dao; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.dao.DataAccessException; 8 | import org.springframework.jdbc.core.JdbcTemplate; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import javax.annotation.PostConstruct; 12 | import java.net.SocketAddress; 13 | import java.util.List; 14 | 15 | /** 16 | * @author :aguesuka 2019/9/12 16:31 17 | */ 18 | @Repository 19 | @Slf4j 20 | public class InfoHashDao { 21 | private final JdbcTemplate jdbcTemplate; 22 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 23 | @Autowired 24 | public InfoHashDao(JdbcTemplate jdbcTemplate) { 25 | this.jdbcTemplate = jdbcTemplate; 26 | } 27 | 28 | @PostConstruct 29 | public void creatTable() { 30 | try { 31 | // create table 32 | jdbcTemplate.execute("create table INFO_HASH\n" + 33 | "(\n" + 34 | " PR_ID integer not null\n" + 35 | " constraint INFO_HASH_pk\n" + 36 | " primary key autoincrement,\n" + 37 | " CREATE_TIME TEXT,\n" + 38 | " HEX_HASH text,\n" + 39 | " ADDRESS TEXT\n" + 40 | ");"); 41 | logger.info("create table INFO_HASH success"); 42 | } catch (DataAccessException e) { 43 | // table exsit ignore 44 | logger.info("table INFO_HASH already exists"); 45 | } 46 | } 47 | 48 | public void save(String infoHashHex, SocketAddress address) { 49 | jdbcTemplate.update( 50 | "insert into INFO_HASH (CREATE_TIME, HEX_HASH,ADDRESS) VALUES (current_timestamp, ?,?)", 51 | infoHashHex, address.toString()); 52 | } 53 | 54 | public List hexHash() { 55 | return jdbcTemplate.queryForList("select distinct HEX_HASH from INFO_HASH", String.class); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/chain/DhtRecordChain.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler.chain; 2 | 3 | import cc.aguesuka.btfind.dht.beans.KrpcMessage; 4 | import cc.aguesuka.btfind.dht.handler.IDhtHandlerChain; 5 | import cc.aguesuka.btfind.dht.handler.IDhtQueryChain; 6 | import cc.aguesuka.btfind.dht.handler.IDhtUnknownChain; 7 | import cc.aguesuka.btfind.util.record.ActionRecord; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | import static cc.aguesuka.btfind.util.record.ActionEnum.*; 12 | 13 | /** 14 | * @author :aguesuka 15 | * 2019/9/12 13:27 16 | */ 17 | @Component 18 | public class DhtRecordChain implements IDhtHandlerChain, IDhtQueryChain, IDhtUnknownChain { 19 | private final ActionRecord record; 20 | 21 | @Autowired 22 | public DhtRecordChain(ActionRecord record) { 23 | this.record = record; 24 | } 25 | 26 | @Override 27 | public void onResponse(KrpcMessage response) { 28 | record.doRecord(DHT_RECV_RESPONSE); 29 | } 30 | 31 | 32 | @Override 33 | public void onPing(KrpcMessage query) { 34 | record.doRecord(DHT_RECV_PING); 35 | } 36 | 37 | @Override 38 | public void onFindNodes(KrpcMessage query) { 39 | record.doRecord(DHT_RECV_FIND_NODE); 40 | 41 | } 42 | 43 | @Override 44 | public void onGetPeer(KrpcMessage query) { 45 | record.doRecord(DHT_RECV_GET_PEERS); 46 | 47 | } 48 | 49 | @Override 50 | public void onAnnouncePeer(KrpcMessage query) { 51 | record.doRecord(DHT_RECV_ANNOUNCE_PEER); 52 | } 53 | 54 | @Override 55 | public void onUnknownTypeQuery(KrpcMessage query) { 56 | record.doRecord(DHT_RECV_UNKNOWN_QUERY); 57 | } 58 | 59 | @Override 60 | public void onRecvError(KrpcMessage error) { 61 | record.doRecord(DHT_RECV_ERROR); 62 | 63 | } 64 | 65 | @Override 66 | public void onQuery(KrpcMessage query) { 67 | record.doRecord(DHT_RECV_QUERY); 68 | } 69 | 70 | @Override 71 | public void onUnknownType(KrpcMessage message) { 72 | record.doRecord(DHT_RECV_UNKNOWN_TYPE); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/util/ByteUtil.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode.util; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.security.MessageDigest; 5 | import java.security.NoSuchAlgorithmException; 6 | import java.util.Comparator; 7 | 8 | /** 9 | * @author aguesuka 10 | */ 11 | public final class ByteUtil { 12 | 13 | /** 14 | * 比较两个byte数组代表的大小.可理解为256进制正整数的大小比较 15 | * 16 | * @param a byte 数组1 17 | * @param b byte 数组2 18 | * @return {@link Comparator#compare(Object, Object)} 19 | */ 20 | public static int compareBytes(byte[] a, byte[] b) { 21 | Comparator comparator = Comparator.comparingInt( 22 | (byte[] bytes) -> bytes.length); 23 | for (int i = 0; i < a.length; i++) { 24 | int j = i; 25 | comparator = comparator.thenComparingInt((byte[] bytes) -> bytes[j] & 0xff); 26 | } 27 | return comparator.compare(a, b); 28 | } 29 | 30 | 31 | /** 32 | * 判断一个ByteBuffer剩余的bytes能否转为utf-8 33 | * 34 | * @param byteBuffer 封装byte array的buffer 35 | * @return ByteBuffer剩余的bytes能否转为utf-8 36 | */ 37 | public static boolean isUtf8(ByteBuffer byteBuffer) { 38 | int ii = 0b1100_0000; 39 | int io = 0b1000_0000; 40 | int i8 = 0b1111_1111; 41 | if (!byteBuffer.hasRemaining()) { 42 | return true; 43 | } 44 | while (byteBuffer.hasRemaining()) { 45 | int b = byteBuffer.get() & i8; 46 | if ((b & io) != io) { 47 | if (b < 32) { 48 | return false; 49 | } 50 | continue; 51 | } 52 | if ((b & ii) == io) { 53 | return false; 54 | } 55 | for (b <<= 1; (b & io) == io; b <<= 1) { 56 | 57 | if (!byteBuffer.hasRemaining() || (ii & byteBuffer.get()) != io) { 58 | return false; 59 | } 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | 66 | public static byte[] sha1(byte[] bytes) { 67 | try { 68 | MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); 69 | return messageDigest.digest(bytes); 70 | } catch (NoSuchAlgorithmException e) { 71 | throw new RuntimeException(e); 72 | } 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/util/DhtServerConfig.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.util; 2 | 3 | import cc.aguesuka.bencode.util.HexUtil; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | import java.net.InetSocketAddress; 11 | import java.net.UnknownHostException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * config 17 | * 18 | * @author :aguesuka 19 | * 2019/9/9 12:27 20 | */ 21 | @Getter 22 | @Setter 23 | @Component 24 | @ConfigurationProperties("dht") 25 | public class DhtServerConfig { 26 | /** 27 | * 日志记录刷新时间 28 | */ 29 | private int recordTime = 10; 30 | /** 31 | * 本机dht端口 32 | */ 33 | private int dhtPort = 11111; 34 | private byte[] selfNodeId; 35 | /** 36 | * 本机node id 37 | */ 38 | private String selfNodeIdHex = "57D438DA296D3E62E0961234BF39A63F895EF126"; 39 | /** 40 | * 起始节点 41 | */ 42 | private List bootstrapNodes; 43 | /** 44 | * 如果要支持bep0042,设定该配置项为本机ip 45 | */ 46 | private String ipForBep0042; 47 | /** 48 | * 路由表大小 49 | */ 50 | private int routingTableSize = 10_000; 51 | 52 | /** 53 | * 加入dht任务的周期(ms) 54 | */ 55 | private long joinDhtInterval = 1000; 56 | /** 57 | * 加入dht任务每周期的请求数 58 | */ 59 | private int joinDhtCount = 1000; 60 | /** 61 | * join dht 保存的最大地址数量 62 | */ 63 | private int joinDhtMaxSize = 10000; 64 | 65 | /** 66 | * dht起始节点,加入dht网络时使用 67 | * 68 | * @param bootstrapNodes host:port 格式的地址 69 | */ 70 | public void setBootstrapNodes(List bootstrapNodes) { 71 | this.bootstrapNodes = new ArrayList<>(); 72 | for (String rootNode : bootstrapNodes) { 73 | String[] split = rootNode.split(":"); 74 | String host = split[0]; 75 | int port = Integer.parseInt(split[1]); 76 | this.bootstrapNodes.add(new InetSocketAddress(host, port)); 77 | } 78 | 79 | } 80 | 81 | @PostConstruct 82 | public void init() throws UnknownHostException { 83 | if (selfNodeId == null) { 84 | selfNodeId = HexUtil.decode(selfNodeIdHex); 85 | } 86 | if (ipForBep0042 != null) { 87 | selfNodeId = Bep0042Impl.changeId(ipForBep0042, selfNodeId); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /bencode/src/test/java/cc/aguesuka/bencode/IBencodeTest.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import cc.aguesuka.bencode.util.HexUtil; 4 | import org.junit.Assert; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.net.URL; 9 | import java.nio.ByteBuffer; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * @author :aguesuka 18 | * 2019/9/3 17:46 19 | */ 20 | public class IBencodeTest { 21 | private ByteBuffer byteBuffer; 22 | private List bencodeData; 23 | private List bencodeMapList; 24 | 25 | private static byte[] getByteArrayFromByteBuffer(ByteBuffer byteBuffer) { 26 | byte[] bytesArray = new byte[byteBuffer.remaining()]; 27 | byteBuffer.get(bytesArray); 28 | return bytesArray; 29 | } 30 | 31 | @Before 32 | public void setUp() throws Exception { 33 | 34 | String dataFile = "bencode.txt"; 35 | URL resource = this.getClass().getClassLoader().getResource(dataFile); 36 | Objects.requireNonNull(resource); 37 | List lines = Files.readAllLines(Paths.get(resource.toURI())); 38 | bencodeData = lines.stream().map(HexUtil::decode).collect(Collectors.toList()); 39 | bencodeMapList = bencodeData.stream().map(b -> Bencode.parse(ByteBuffer.wrap(b))).collect(Collectors.toList()); 40 | int max = bencodeData.stream().mapToInt(array -> array.length).max().orElse(0); 41 | byteBuffer = ByteBuffer.allocate(max); 42 | } 43 | 44 | @Test 45 | public void toBencodeBytes() { 46 | for (int i = 0; i < bencodeData.size(); i++) { 47 | byteBuffer.clear(); 48 | byte[] bytes = bencodeMapList.get(i).toBencodeBytes(); 49 | byteBuffer.put(bytes); 50 | Assert.assertArrayEquals(bencodeData.get(i), bytes); 51 | } 52 | } 53 | 54 | @Test 55 | public void toByteBuffer() { 56 | for (int i = 0; i < bencodeData.size(); i++) { 57 | ByteBuffer byteBuffer = bencodeMapList.get(i).toByteBuffer(); 58 | byte[] byteArrayFromByteBuffer = getByteArrayFromByteBuffer(byteBuffer); 59 | Assert.assertArrayEquals(bencodeData.get(i), byteArrayFromByteBuffer); 60 | } 61 | } 62 | 63 | @Test 64 | public void writeToBuffer() { 65 | for (int i = 0; i < bencodeData.size(); i++) { 66 | byteBuffer.clear(); 67 | bencodeMapList.get(i).writeToBuffer(byteBuffer).flip(); 68 | byte[] byteArrayFromByteBuffer = getByteArrayFromByteBuffer(byteBuffer); 69 | Assert.assertArrayEquals(bencodeData.get(i), byteArrayFromByteBuffer); 70 | } 71 | } 72 | 73 | 74 | } -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/util/record/ActionRecord.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.util.record; 2 | 3 | import cc.aguesuka.btfind.util.DhtServerConfig; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.time.Duration; 8 | import java.time.LocalDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | import java.util.EnumMap; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * aggregation log 17 | * 18 | * @author :aguesuka 19 | * 2019/9/4 17:37 20 | */ 21 | @Component 22 | @Slf4j 23 | public class ActionRecord { 24 | private final DhtServerConfig config; 25 | private final Map sumMap; 26 | private Map lastTimeMap; 27 | private Map nowTimeMap; 28 | private LocalDateTime lastTime; 29 | private final LocalDateTime startTime; 30 | 31 | 32 | public ActionRecord(DhtServerConfig config) { 33 | this.config = config; 34 | startTime = LocalDateTime.now(); 35 | lastTime = LocalDateTime.now(); 36 | nowTimeMap = new EnumMap<>(ActionEnum.class); 37 | lastTimeMap = new EnumMap<>(ActionEnum.class); 38 | sumMap = new EnumMap<>(ActionEnum.class); 39 | } 40 | 41 | public void doRecord(ActionEnum action) { 42 | doRecord(action, 1); 43 | } 44 | 45 | 46 | public void doRecord(ActionEnum action, double count) { 47 | putSumMap(action, count); 48 | onUpdate(); 49 | putLastTimeMap(action, count); 50 | } 51 | 52 | private void putLastTimeMap(ActionEnum action, double count) { 53 | if (nowTimeMap.containsKey(action)) { 54 | nowTimeMap.put(action, nowTimeMap.get(action) + count); 55 | } else { 56 | nowTimeMap.put(action, count); 57 | } 58 | } 59 | 60 | private void putSumMap(ActionEnum action, double count) { 61 | if (sumMap.containsKey(action)) { 62 | sumMap.put(action, sumMap.get(action) + count); 63 | } else { 64 | sumMap.put(action, count); 65 | } 66 | } 67 | 68 | private void onUpdate() { 69 | int recordTime = config.getRecordTime(); 70 | long cost = TimeUnit.SECONDS.convert(Duration.between(lastTime, LocalDateTime.now())); 71 | if (cost >= recordTime) { 72 | nowTimeMap = new HashMap<>(lastTimeMap.size()); 73 | lastTime = LocalDateTime.now(); 74 | DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME; 75 | log.warn("\n from last time {} record{} \n from start time {} record{}", 76 | lastTime.format(formatter), lastTimeMap.toString(), 77 | startTime.format(formatter), sumMap.toString()); 78 | lastTimeMap = nowTimeMap; 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/util/HexUtil.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode.util; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | 6 | /** 7 | * 十六进制工具类 oracle jdk 有,open jdk 需要自己写 8 | * 9 | * @author :aguesuka 10 | * 2019/7/11 23:25 11 | */ 12 | public class HexUtil { 13 | 14 | 15 | private static final char[] B2H = new char[16]; 16 | private static final byte[] H2B = new byte[128]; 17 | 18 | static { 19 | byte num = 0; 20 | char h0 = '0'; 21 | char h9 = '9'; 22 | char h10 = 'A'; 23 | char h16 = 'F'; 24 | byte illegal = -1; 25 | for (char hex = h0; hex <= h9; hex++) { 26 | B2H[num++] = hex; 27 | } 28 | for (char i = h10; i <= h16; i++) { 29 | B2H[num++] = i; 30 | } 31 | 32 | for (int i = 0; i < H2B.length; i++) { 33 | H2B[i] = illegal; 34 | } 35 | num = 0; 36 | for (int i = h0; i <= h9; i++) { 37 | H2B[i] = num++; 38 | } 39 | for (int i = h10; i <= h16; i++) { 40 | H2B[i] = num++; 41 | } 42 | num = 0xA; 43 | h10 = 'a'; 44 | h16 = 'f'; 45 | for (int i = h10; i <= h16; i++) { 46 | H2B[i] = num++; 47 | } 48 | 49 | } 50 | 51 | /** 52 | * 十六进制转byte数组 53 | * 54 | * @param hex 十六进制字符串 55 | * @return byte数组 56 | */ 57 | public static byte[] decode(String hex) { 58 | Objects.requireNonNull(hex); 59 | char[] chars = hex.toCharArray(); 60 | if ((chars.length & 1) != 0) { 61 | throw new IllegalArgumentException(); 62 | } 63 | byte[] result = new byte[chars.length / 2]; 64 | for (int i = 0; i < result.length; i++) { 65 | byte b0 = h2b(chars[i * 2]); 66 | byte b1 = h2b(chars[i * 2 + 1]); 67 | result[i] = ((byte) (b0 << 4 | b1)); 68 | } 69 | return result; 70 | 71 | } 72 | 73 | private static byte h2b(char c) { 74 | if (c > H2B.length) { 75 | throw new IllegalArgumentException(); 76 | } 77 | byte b = H2B[(byte) c]; 78 | if (b == -1) { 79 | throw new IllegalArgumentException(); 80 | } 81 | return b; 82 | } 83 | 84 | /** 85 | * byte数组转十六进制 86 | * 87 | * @param bytes byte数组 88 | * @return 十六进制字符串 89 | */ 90 | public static String encode(byte[] bytes) { 91 | Objects.requireNonNull(bytes); 92 | char[] b = new char[bytes.length * 2]; 93 | for (int i = 0; i < bytes.length; i++) { 94 | byte bi = bytes[i]; 95 | b[i * 2] = B2H[(bi & 0xff) >>> 4]; 96 | b[i * 2 + 1] = B2H[bi & 0b0000_1111]; 97 | } 98 | return new String(b); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /bencode/src/test/java/cc/aguesuka/bencode/BencodeTest.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import cc.aguesuka.bencode.util.HexUtil; 4 | import org.junit.Assert; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import java.net.URL; 9 | import java.nio.ByteBuffer; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.stream.Collectors; 17 | 18 | /** 19 | * @author :aguesuka 20 | * 2019/8/27 14:19 21 | */ 22 | 23 | public class BencodeTest { 24 | private List bencodeData; 25 | private List bencodeMapList; 26 | private int max; 27 | 28 | @Before 29 | public void setUp() throws Exception { 30 | 31 | String dataFile = "bencode.txt"; 32 | URL resource = this.getClass().getClassLoader().getResource(dataFile); 33 | Objects.requireNonNull(resource); 34 | List lines = Files.readAllLines(Paths.get(resource.toURI())); 35 | bencodeData = lines.stream().map(HexUtil::decode).collect(Collectors.toList()); 36 | bencodeMapList = bencodeData.stream().map(b -> Bencode.parse(ByteBuffer.wrap(b))).collect(Collectors.toList()); 37 | max = bencodeData.stream().mapToInt(a -> a.length).max().orElse(0); 38 | } 39 | 40 | @Test 41 | public void parse() { 42 | for (byte[] bytes : bencodeData) { 43 | Assert.assertArrayEquals(bytes, Bencode.parse(ByteBuffer.wrap(bytes)).toBencodeBytes()); 44 | } 45 | } 46 | 47 | @Test 48 | public void parseBytes() { 49 | for (byte[] bytes : bencodeData) { 50 | BencodeMap parse = Bencode.parse(bytes); 51 | Assert.assertEquals(parse, Bencode.parse(ByteBuffer.wrap(bytes))); 52 | Assert.assertArrayEquals(bytes, parse.toBencodeBytes()); 53 | } 54 | } 55 | 56 | @Test 57 | public void toBytes() { 58 | for (BencodeMap bencodeMap : bencodeMapList) { 59 | Assert.assertEquals(bencodeMap, Bencode.parse(bencodeMap.toBencodeBytes())); 60 | } 61 | ArrayList list = new ArrayList<>(); 62 | BencodeList bList = new BencodeList(); 63 | list.add(1); 64 | bList.addLong(1); 65 | list.add(new HashMap<>()); 66 | bList.add(new BencodeMap()); 67 | list.add(new byte[]{1, 2, 3}); 68 | bList.addByteArray(new byte[]{1, 2, 3}); 69 | Assert.assertArrayEquals(Bencode.toBytes(list), Bencode.toBytes(bList)); 70 | } 71 | 72 | @Test 73 | public void writeToBuffer() { 74 | ByteBuffer buffer = ByteBuffer.allocate(max); 75 | for (BencodeMap bencodeMap : bencodeMapList) { 76 | buffer.clear(); 77 | Bencode.writeToBuffer(bencodeMap, buffer).flip(); 78 | Assert.assertEquals(bencodeMap, Bencode.parse(buffer)); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/BencodeEncoder.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | 9 | /** 10 | * java object to bencode byte array 11 | * todo Circular reference check 12 | * 13 | * @author aguesuka 14 | */ 15 | class BencodeEncoder { 16 | private Object bencode; 17 | private final ByteArrayOutputStream outputStream; 18 | 19 | BencodeEncoder() { 20 | this.outputStream = new ByteArrayOutputStream(); 21 | } 22 | 23 | private void write(byte[] bytes) { 24 | try { 25 | writeByteArray(bytes); 26 | } catch (Exception e) { 27 | throw new BencodeException(bencode, e); 28 | } 29 | } 30 | 31 | void writeByteArray(byte[] bytes) throws IOException { 32 | outputStream.write(bytes); 33 | } 34 | 35 | void writeByte(byte b) { 36 | outputStream.write(b); 37 | } 38 | 39 | byte[] getResult() { 40 | return outputStream.toByteArray(); 41 | } 42 | 43 | 44 | private void putInteger(Number i) { 45 | writeByte(BencodeToken.INT); 46 | write(i.toString().getBytes(Bencode.getCharset())); 47 | writeByte(BencodeToken.END); 48 | } 49 | 50 | private void putString(String s) { 51 | putByteArray(s.getBytes(Bencode.getCharset())); 52 | } 53 | 54 | private void putMap(Map map) { 55 | writeByte(BencodeToken.DICT); 56 | for (Map.Entry entry : map.entrySet()) { 57 | putString((String) entry.getKey()); 58 | putBencode(entry.getValue()); 59 | } 60 | writeByte(BencodeToken.END); 61 | } 62 | 63 | private void putList(List list) { 64 | writeByte(BencodeToken.LIST); 65 | for (Object o : list) { 66 | putBencode(o); 67 | } 68 | writeByte(BencodeToken.END); 69 | } 70 | 71 | 72 | void putBencode(Object o) { 73 | this.bencode = o; 74 | if (o == null) { 75 | putString(BencodeToken.NULL); 76 | return; 77 | } 78 | if (o instanceof Map) { 79 | putMap((Map) o); 80 | } else if (o instanceof BencodeByteArray) { 81 | putByteArray(((BencodeByteArray) o).getBytes()); 82 | } else if (o instanceof Number) { 83 | putInteger((Number) o); 84 | } else if (o instanceof List) { 85 | putList((List) o); 86 | } else if (o instanceof byte[]) { 87 | putByteArray(((byte[]) o)); 88 | } else { 89 | throw new BencodeException(bencode, "can not encode:" + o.getClass()); 90 | } 91 | } 92 | 93 | private void putByteArray(byte[] bytes) { 94 | write(Integer.toString(bytes.length).getBytes(Bencode.getCharset())); 95 | writeByte(BencodeToken.SPLIT); 96 | write(bytes); 97 | } 98 | 99 | 100 | } -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/connection/NioEventLoop.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.connection; 2 | 3 | import cc.aguesuka.btfind.util.record.ActionEnum; 4 | import cc.aguesuka.btfind.util.record.ActionRecord; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.io.IOException; 11 | import java.nio.channels.SelectionKey; 12 | import java.nio.channels.Selector; 13 | import java.util.Iterator; 14 | import java.util.Set; 15 | import java.util.concurrent.CountDownLatch; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * @author :aguesuka 20 | * 2019/9/9 12:19 21 | */ 22 | @Component 23 | @Slf4j 24 | public class NioEventLoop implements AutoCloseable { 25 | @Getter 26 | @Setter 27 | private volatile boolean isLoop; 28 | private CountDownLatch countDownLatch; 29 | private final ActionRecord record; 30 | @Getter 31 | private Selector selector; 32 | public NioEventLoop(ActionRecord record) { 33 | this.record = record; 34 | } 35 | 36 | private void stop() { 37 | log.info("stop loop at:"); 38 | for (StackTraceElement element : Thread.currentThread().getStackTrace()) { 39 | log.info(element.toString()); 40 | } 41 | isLoop = false; 42 | } 43 | 44 | void init() throws IOException { 45 | this.selector = Selector.open(); 46 | } 47 | 48 | 49 | void loop() { 50 | countDownLatch = new CountDownLatch(1); 51 | isLoop = true; 52 | while (isLoop) { 53 | try { 54 | select(); 55 | } catch (Throwable e) { 56 | log.error(e.getMessage(), e); 57 | stop(); 58 | } 59 | } 60 | log.error("loop stop"); 61 | countDownLatch.countDown(); 62 | } 63 | 64 | 65 | private void select() { 66 | record.doRecord(ActionEnum.LOOP_SELECT); 67 | int selectCount; 68 | try { 69 | selectCount = selector.select(100); 70 | } catch (IOException e) { 71 | stop(); 72 | log.error(e.getMessage(), e); 73 | return; 74 | } 75 | record.doRecord(ActionEnum.LOOP_KEYS, selectCount); 76 | if (selectCount > 0) { 77 | selectedKeys(); 78 | } else { 79 | allKeys(); 80 | } 81 | 82 | } 83 | 84 | private void selectedKeys() { 85 | Set selectionKeys = selector.selectedKeys(); 86 | Iterator iterator = selectionKeys.iterator(); 87 | while (iterator.hasNext()) { 88 | SelectionKey key = iterator.next(); 89 | doHandle(key); 90 | iterator.remove(); 91 | } 92 | } 93 | 94 | private void allKeys() { 95 | Set selectionKeys = selector.keys(); 96 | for (SelectionKey key : selectionKeys) { 97 | doHandle(key); 98 | } 99 | } 100 | 101 | private void doHandle(SelectionKey key) { 102 | Object attachment = key.attachment(); 103 | NioHandler nioHandler = (NioHandler) attachment; 104 | try { 105 | nioHandler.doHandler(key); 106 | } catch (Exception e) { 107 | log.error(e.getMessage(), e); 108 | } 109 | } 110 | 111 | @Override 112 | public void close() throws InterruptedException { 113 | stop(); 114 | selector.wakeup(); 115 | boolean notTimeout = countDownLatch.await(1000, TimeUnit.SECONDS); 116 | if(!notTimeout){ 117 | log.warn("loop closed"); 118 | }else{ 119 | log.error("close timeout"); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/chain/DhtMainChain.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler.chain; 2 | 3 | import cc.aguesuka.bencode.BencodeByteArray; 4 | import cc.aguesuka.bencode.BencodeMap; 5 | import cc.aguesuka.btfind.dht.KrpcToken; 6 | import cc.aguesuka.btfind.dht.beans.KrpcMessage; 7 | import cc.aguesuka.btfind.dht.handler.IDhtHandlerChain; 8 | import cc.aguesuka.btfind.dht.handler.IDhtQueryChain; 9 | import cc.aguesuka.btfind.util.DhtServerConfig; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.security.SecureRandom; 14 | import java.util.LinkedList; 15 | import java.util.Queue; 16 | import java.util.Random; 17 | 18 | /** 19 | * http://www.bittorrent.org/beps/bep_0005.html 20 | * response message for other peer query the server 21 | * 22 | * @author :aguesuka 23 | * 2019/9/11 21:56 24 | */ 25 | @Component 26 | public class DhtMainChain implements IDhtQueryChain, IDhtHandlerChain { 27 | private final Queue messagesQueue = new LinkedList<>(); 28 | private final DhtServerConfig config; 29 | private final Random random = new SecureRandom(); 30 | private final byte[] token = new byte[2]; 31 | 32 | @Autowired 33 | public DhtMainChain(DhtServerConfig config) { 34 | this.config = config; 35 | } 36 | 37 | private void addMessageQueue(KrpcMessage message) { 38 | messagesQueue.add(message); 39 | } 40 | 41 | /** 42 | * Maximum weight 43 | * 44 | * @return 10 45 | */ 46 | @Override 47 | public int weights() { 48 | return 10; 49 | } 50 | 51 | @Override 52 | public KrpcMessage getMessage() throws NullPointerException { 53 | return messagesQueue.remove(); 54 | } 55 | 56 | @Override 57 | public boolean isWriteAble() { 58 | return !messagesQueue.isEmpty(); 59 | } 60 | 61 | /** 62 | * 基本回复 63 | * {y=r, t=aa, r={id=self_id}} 64 | * 65 | * @param src 请求消息 66 | * @return 基本回复 67 | */ 68 | private KrpcMessage baseResponse(KrpcMessage src) { 69 | BencodeMap result = new BencodeMap(); 70 | BencodeMap content = new BencodeMap(); 71 | content.putByteArray(KrpcToken.ID, config.getSelfNodeId()); 72 | result.put(KrpcToken.RESPONSES_MAP, content); 73 | result.put(KrpcToken.TRANSACTION, src.getMessage().get(KrpcToken.TRANSACTION)); 74 | result.putString(KrpcToken.TYPE, KrpcToken.TYPE_RESPONSE); 75 | return new KrpcMessage(result, src.getAddress()); 76 | } 77 | 78 | @Override 79 | public void onPing(KrpcMessage query) { 80 | addMessageQueue(baseResponse(query)); 81 | } 82 | 83 | @Override 84 | public void onFindNodes(KrpcMessage query) { 85 | KrpcMessage response = baseResponse(query); 86 | response.getMessage() 87 | .getBencodeMap(KrpcToken.RESPONSES_MAP) 88 | .put(KrpcToken.NODES, BencodeByteArray.empty()); 89 | addMessageQueue(response); 90 | } 91 | 92 | private byte[] nextToken() { 93 | random.nextBytes(token); 94 | return token; 95 | } 96 | 97 | @Override 98 | public void onGetPeer(KrpcMessage query) { 99 | KrpcMessage response = baseResponse(query); 100 | BencodeMap responseMap = response.getMessage().getBencodeMap(KrpcToken.RESPONSES_MAP); 101 | responseMap.put(KrpcToken.NODES, BencodeByteArray.empty()); 102 | responseMap.putByteArray(KrpcToken.TOKEN, nextToken()); 103 | addMessageQueue(response); 104 | } 105 | 106 | @Override 107 | public void onAnnouncePeer(KrpcMessage query) { 108 | addMessageQueue(baseResponse(query)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/KrpcToken.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht; 2 | 3 | /** 4 | * http://www.bittorrent.org/beps/bep_0005.html#krpc-protocol 5 | *

6 | * The KRPC protocol is a simple RPC mechanism consisting of bencoded dictionaries sent over UDP. 7 | * UdpHandler single query packet is sent out and a single packet is sent in response. 8 | * There is no retry. There are three message types: query, response, and error. 9 | * For the DHT protocol, there are four queries: ping, find_node, get_peers, and announce_peer. 10 | * 11 | *

12 | * KRPC 消息的类型是 BencodeMap; key 用 string 表示 value 用 String 表示. 13 | * 14 | * @author :aguesuka 15 | * 2019/8/29 15:06 16 | */ 17 | @SuppressWarnings("unused") 18 | public class KrpcToken { 19 | public static final int ID_LENGTH = 20; 20 | /** 21 | * Every message has a key "t" with a string value representing a transaction ID. 22 | * This transaction ID is generated by the querying node and is echoed in the response, 23 | * so responses may be correlated with multiple queries to the same node. 24 | * The transaction ID should be encoded as a short string of binary numbers, 25 | * typically 2 characters are enough as they cover 2^16 outstanding queries. 26 | */ 27 | public static final String TRANSACTION = "t"; 28 | /** 29 | * Every message also has a key "y" with a single character value describing the type of message. 30 | */ 31 | public static final String TYPE = "y"; 32 | /** 33 | * The value of the "y" key is one of "q" for query 34 | */ 35 | public static final String TYPE_QUERY = "q"; 36 | /** 37 | * The value of the "y" key is one of "r" for response 38 | */ 39 | public static final String TYPE_RESPONSE = "r"; 40 | /** 41 | * The value of the "y" key is one of "e" for error 42 | */ 43 | public static final String TYPE_ERROR = "e"; 44 | /** 45 | * Queries, or KRPC message dictionaries with a "y" value of "q", 46 | * contain two additional keys; "q" and "a". 47 | * Key "a" has a dictionary value containing named arguments to the query. 48 | */ 49 | public static final String ARGUMENTS_MAP = "a"; 50 | /** 51 | * Key "q" has a string value containing the method name of the query. 52 | */ 53 | public static final String QUERY = "q"; 54 | 55 | /** 56 | * Responses, or KRPC message dictionaries with a "y" value of "r", contain one additional key "r". 57 | * The value of "r" is a dictionary containing named return values. 58 | * Response messages are sent upon successful completion of a query. 59 | */ 60 | public static final String RESPONSES_MAP = "r"; 61 | 62 | public static final String PING = "ping"; 63 | public static final String FIND_NODE = "find_node"; 64 | public static final String GET_PEERS = "get_peers"; 65 | public static final String ANNOUNCE_PEER = "announce_peer"; 66 | public static final String SAMPLE_INFOHASHES = "sample_infohashes"; 67 | public static final String ID = "id"; 68 | public static final String TARGET = "target"; 69 | public static final String NODES = "nodes"; 70 | public static final String INFO_HASH = "info_hash"; 71 | public static final String VALUES = "values"; 72 | public static final String TOKEN = "token"; 73 | public static final String IMPLIED_PORT = "implied_port"; 74 | public static final String PORT = "port"; 75 | 76 | /** 77 | * There is an optional argument called implied_port which value is either 0 or 1. 78 | * If it is present and non-zero, the port argument should be ignored and the source port of the 79 | * UDP packet should be used as the peer's port instead. 80 | * This is useful for peers behind a NAT that may not know their external port, and supporting uTP, they accept incoming 81 | */ 82 | public static final int NOT_IGNORED_PORT = 0; 83 | 84 | } 85 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ague-dht 7 | cc.aguesuka 8 | 1.2 9 | 10 | 4.0.0 11 | jar 12 | server 13 | 14 | 2.1.6.RELEASE 15 | 16 | 17 | 18 | cc.aguesuka 19 | bencode 20 | 1.2 21 | 22 | 23 | org.springframework.boot 24 | spring-boot 25 | ${sprint-boot.version} 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter 30 | ${sprint-boot.version} 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-configuration-processor 35 | ${sprint-boot.version} 36 | true 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-jdbc 41 | ${sprint-boot.version} 42 | 43 | 44 | org.xerial 45 | sqlite-jdbc 46 | 3.14.2.1 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-test 51 | ${sprint-boot.version} 52 | test 53 | 54 | 55 | org.springframework 56 | spring-test 57 | 5.1.8.RELEASE 58 | test 59 | 60 | 61 | 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-maven-plugin 66 | ${sprint-boot.version} 67 | 68 | cc.aguesuka.btfind.ServiceApplication 69 | 70 | 71 | org.projectlombok 72 | lombok 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-configuration-processor 77 | 78 | 79 | junit 80 | junit 81 | 82 | 83 | 84 | 85 | 86 | 87 | repackage 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | lib 96 | BOOT-INF/lib/ 97 | 98 | **/*.jar 99 | 100 | 101 | 102 | true 103 | src/main/resources 104 | 105 | *.yml 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/Bencode.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.charset.Charset; 5 | import java.nio.charset.StandardCharsets; 6 | 7 | /** 8 | * http://www.bittorrent.org/beps/bep_0003.html#bencoding 9 | *

10 | * bencode是bit-torrent协议的常用编码,消息和种子文件中都有用到. 11 | * 本类提供IBencode对象和byte数组/ByteBuffer的互相转换 12 | * {@link Bencode#toBytes(Object)} 13 | * {@link Bencode#writeToBuffer(IBencode, ByteBuffer)} 14 | * {@link Bencode#parse(ByteBuffer)} 15 | * {@link Bencode#parse(byte[])} 16 | *

17 | * bencode 有四种类型 18 | *

19 | * INTEGER 整数 {@link BencodeInteger} 20 | * Integers are represented by an 'i' followed by the number in base 10 followed by an 'e'. 21 | * For example i3e corresponds to 3 and i-3e corresponds to -3. Integers have no size limitation. 22 | * i-0e is invalid. All encodings with a leading zero, such as i03e, 23 | * are invalid, other than i0e, which of course corresponds to 0. 24 | *

25 | * STRING 对应Java中的byte[]而非字符串; {@link BencodeByteArray} 字符串可以转为BencodeByteArray反之则不一定 26 | * Strings are length-prefixed base ten followed by a colon and the string. For example 4:spam corresponds to 'spam'. 27 | *

28 | * Lists are encoded as an 'l' followed by their elements (also bencoded) followed by an 'e'. 29 | * For example l4:spam4:eggse corresponds to ['spam', 'eggs']. 30 | * LIST 数组 {@link BencodeList} 31 | *

32 | * Dictionaries are encoded as a 'd' followed by a list of alternating keys and their corresponding values followed by an 'e'. 33 | * For example, d3:cow3:moo4:spam4:eggse corresponds to {'cow': 'moo', 'spam': 'eggs'} 34 | * and d4:spaml1:a1:bee corresponds to {'spam': ['a', 'b']}. 35 | * Keys must be strings and appear in sorted order (sorted as raw strings, not alphanumerics). 36 | * DICT 字典 {@link BencodeMap} map的key是字符串 37 | *

38 | * bencode 没有定义null,但是在处理时不做校验,所以解析成对象时,map的最后一个value可能为null,编码为bencode时,null视为空字符串 39 | * 40 | * @author :aguesuka 41 | * 2019/6/26 17:47 42 | */ 43 | public final class Bencode { 44 | 45 | private static final Charset charset = StandardCharsets.UTF_8; 46 | 47 | static Charset getCharset() { 48 | return charset; 49 | } 50 | 51 | /** 52 | * 将 ByteBuff 对象转 BencodeMap ,只转换第一个 BencodeMap 并将 ByteBuff 的 offset 置为第一个map的结尾. 53 | * 54 | * @param buff byte buff 对象 55 | * @return BencodeMap 56 | */ 57 | public static BencodeMap parse(ByteBuffer buff) { 58 | try { 59 | return new BencodeParser(buff).parser(); 60 | } catch (BencodeException e) { 61 | throw e; 62 | } catch (RuntimeException e) { 63 | throw new BencodeException(null, e); 64 | } 65 | } 66 | 67 | /** 68 | * 将 ByteBuff 对象转 BencodeMap ,只转换第一个 BencodeMap 并将 ByteBuff 的 offset 置为第一个map的结尾. 69 | * 70 | * @param bytes byte[] 对象 71 | * @return BencodeMap 72 | */ 73 | @SuppressWarnings("WeakerAccess") 74 | public static BencodeMap parse(byte[] bytes) { 75 | try { 76 | return new BencodeParser() { 77 | int position = 0; 78 | 79 | @Override 80 | BencodeException ex(String msg, Throwable ex) { 81 | String m = "at position " + position + ":" + msg; 82 | return new BencodeException(rootMap, m, ex); 83 | } 84 | 85 | @Override 86 | byte byteFromBuffer() { 87 | return bytes[position++]; 88 | } 89 | 90 | @Override 91 | byte[] byteArrayFromBuffer(int length) { 92 | byte[] result = new byte[length]; 93 | System.arraycopy(bytes, position, result, 0, length); 94 | position += length; 95 | return result; 96 | } 97 | }.parser(); 98 | } catch (BencodeException e) { 99 | throw e; 100 | } catch (RuntimeException e) { 101 | throw new BencodeException(null, e); 102 | } 103 | 104 | } 105 | 106 | /** 107 | * 将IBencode对象转为byte数组 108 | * 109 | * @param o IBencode对象 110 | * @return byte 数组 111 | */ 112 | static byte[] toBytes(Object o) { 113 | BencodeEncoder bencodeEncode = new BencodeEncoder(); 114 | bencodeEncode.putBencode(o); 115 | return bencodeEncode.getResult(); 116 | } 117 | 118 | static ByteBuffer writeToBuffer(IBencode o, ByteBuffer buffer) { 119 | BencodeEncoder bencodeEncode = new BencodeEncoder() { 120 | @Override 121 | void writeByteArray(byte[] bytes) { 122 | buffer.put(bytes); 123 | } 124 | 125 | @Override 126 | void writeByte(byte b) { 127 | buffer.put(b); 128 | } 129 | 130 | @Override 131 | byte[] getResult() { 132 | return null; 133 | } 134 | }; 135 | bencodeEncode.putBencode(o); 136 | return buffer; 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /bencode/src/main/java/cc/aguesuka/bencode/BencodeParser.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.bencode; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.util.ArrayDeque; 5 | import java.util.Deque; 6 | import java.util.Objects; 7 | 8 | /** 9 | * @author :aguesuka 10 | * 2019/9/3 20:09 11 | */ 12 | class BencodeParser { 13 | BencodeMap rootMap = new BencodeMap(); 14 | private ByteBuffer buff; 15 | private boolean isReadMap = true; 16 | private final Deque> stack = new ArrayDeque<>(); 17 | 18 | BencodeParser() { 19 | } 20 | 21 | BencodeParser(ByteBuffer buff) { 22 | Objects.requireNonNull(buff); 23 | this.buff = buff; 24 | } 25 | 26 | BencodeMap parser() { 27 | try { 28 | 29 | 30 | stack.add(rootMap); 31 | if (byteFromBuffer() != BencodeToken.DICT) { 32 | throw ex("is not a bencode dict", null); 33 | } 34 | while (!stack.isEmpty()) { 35 | Object last = stack.getLast(); 36 | if (isReadMap) { 37 | String key = readStringOrEnd(); 38 | if (key != null) { 39 | IBencode bencodeObject = readObject(); 40 | Objects.requireNonNull(bencodeObject); 41 | ((BencodeMap) last).put(key, bencodeObject); 42 | } 43 | } else { 44 | IBencode bencode = readObject(); 45 | if (bencode != null) { 46 | ((BencodeList) last).add(bencode); 47 | } 48 | } 49 | } 50 | return rootMap; 51 | } catch (BencodeException e) { 52 | throw e; 53 | } catch (RuntimeException e) { 54 | throw ex(e.getMessage(), e); 55 | } 56 | } 57 | 58 | private void onEnd() { 59 | stack.removeLast(); 60 | isReadMap = stack.peekLast() instanceof BencodeMap; 61 | } 62 | 63 | private IBencode readObject() { 64 | byte firstByte = byteFromBuffer(); 65 | switch (firstByte) { 66 | case BencodeToken.END: 67 | onEnd(); 68 | return null; 69 | case BencodeToken.DICT: 70 | BencodeMap bencodeMap = new BencodeMap(); 71 | stack.addLast(bencodeMap); 72 | isReadMap = true; 73 | return bencodeMap; 74 | case BencodeToken.INT: 75 | return readIntegerAndCheckEnd(byteFromBuffer(), BencodeToken.END); 76 | case BencodeToken.LIST: 77 | BencodeList bencodeList = new BencodeList(); 78 | stack.addLast(bencodeList); 79 | isReadMap = false; 80 | return bencodeList; 81 | default: 82 | return new BencodeByteArray(readBytesAndCheckEnd(firstByte)); 83 | } 84 | } 85 | 86 | /** 87 | * 只有map的key是String 88 | * 在bencode中是 数字(较短,所以可以用int) + 冒号 + byte[] 类型; 89 | * 编码格式几乎都是ascii,不过为了保险依然使用utf-8 90 | * 91 | * @return String ,如果结束,则返回null 92 | */ 93 | private String readStringOrEnd() { 94 | byte b = byteFromBuffer(); 95 | if (b == BencodeToken.END) { 96 | onEnd(); 97 | return null; 98 | } else { 99 | return new String(readBytesAndCheckEnd(b), Bencode.getCharset()); 100 | } 101 | } 102 | 103 | private byte[] readBytesAndCheckEnd(byte firstByte) { 104 | BencodeInteger length = readIntegerAndCheckEnd(firstByte, BencodeToken.SPLIT); 105 | return byteArrayFromBuffer(length.intValue()); 106 | } 107 | 108 | private BencodeInteger readIntegerAndCheckEnd(byte firstByte, byte endChar) { 109 | char c = '-'; 110 | if (!Character.isDigit(firstByte) && firstByte != c) { 111 | throw ex("there must be digit or char '-' ", null); 112 | } 113 | char[] chars = new char[16]; 114 | int index = 1; 115 | chars[0] = (char) firstByte; 116 | while (true) { 117 | byte codePoint = byteFromBuffer(); 118 | if (!Character.isDigit(codePoint)) { 119 | if (codePoint != endChar) { 120 | throw ex("there must be digit", null); 121 | } 122 | break; 123 | } 124 | // 扩容 125 | if (chars.length <= index) { 126 | char[] temp = new char[chars.length * 2]; 127 | System.arraycopy(chars, 0, temp, 0, chars.length); 128 | chars = temp; 129 | } 130 | chars[index] = (char) codePoint; 131 | index++; 132 | } 133 | return new BencodeInteger(new String(chars, 0, index)); 134 | } 135 | 136 | BencodeException ex(String msg, Throwable ex) { 137 | String m = "at position " + buff.position() + ":" + msg; 138 | return new BencodeException(rootMap, m, ex); 139 | } 140 | 141 | byte byteFromBuffer() { 142 | return buff.get(); 143 | } 144 | 145 | 146 | byte[] byteArrayFromBuffer(int length) { 147 | byte[] bytes = new byte[length]; 148 | buff.get(bytes); 149 | return bytes; 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/chain/JoinDhtChain.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler.chain; 2 | 3 | import cc.aguesuka.bencode.BencodeMap; 4 | import cc.aguesuka.btfind.dht.KrpcToken; 5 | import cc.aguesuka.btfind.dht.beans.KrpcMessage; 6 | import cc.aguesuka.btfind.dht.handler.IDhtHandlerChain; 7 | import cc.aguesuka.btfind.util.DhtServerConfig; 8 | import cc.aguesuka.btfind.util.record.ActionEnum; 9 | import cc.aguesuka.btfind.util.record.ActionRecord; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.net.InetAddress; 15 | import java.net.InetSocketAddress; 16 | import java.net.SocketAddress; 17 | import java.net.UnknownHostException; 18 | import java.nio.ByteBuffer; 19 | import java.security.SecureRandom; 20 | import java.util.*; 21 | 22 | /** 23 | * @author :aguesuka 24 | * 2019/9/12 13:37 25 | */ 26 | @Slf4j 27 | @Component 28 | public class JoinDhtChain implements IDhtHandlerChain { 29 | 30 | private final Queue waitSend = new LinkedList<>(); 31 | private final Set blackList = new HashSet<>(); 32 | private final Set allNode = new HashSet<>(); 33 | private final Queue newNode = new LinkedList<>(); 34 | private final Queue goodNode = new LinkedList<>(); 35 | 36 | private final ActionRecord record; 37 | private final Random random = new SecureRandom(); 38 | private final byte[] target = new byte[20]; 39 | private final DhtServerConfig config; 40 | private long lastTime; 41 | 42 | @Autowired 43 | public JoinDhtChain(ActionRecord record, DhtServerConfig config) { 44 | this.record = record; 45 | this.config = config; 46 | } 47 | 48 | private void addQueue(Queue queue, E element) { 49 | if (queue.size() >= config.getJoinDhtMaxSize()) { 50 | queue.remove(); 51 | } 52 | queue.add(element); 53 | } 54 | 55 | private void moveUntil(Collection target, Queue src, int count) { 56 | while (target.size() < count && !src.isEmpty()) { 57 | target.add(src.remove()); 58 | } 59 | } 60 | 61 | private void updateQueue() { 62 | int count = config.getJoinDhtCount(); 63 | moveUntil(waitSend, newNode, count); 64 | if (waitSend.size() > 0 && allNode.size() <= config.getJoinDhtMaxSize()) { 65 | return; 66 | } 67 | allNode.clear(); 68 | moveUntil(waitSend, goodNode, count); 69 | if (waitSend.size() > 0) { 70 | record.doRecord(ActionEnum.JOIN_DHT_CLEAR); 71 | return; 72 | } 73 | record.doRecord(ActionEnum.JOIN_DHT_RESTART); 74 | waitSend.addAll(config.getBootstrapNodes()); 75 | } 76 | 77 | private void update() { 78 | if (System.currentTimeMillis() - lastTime > config.getJoinDhtInterval()) { 79 | updateQueue(); 80 | lastTime = System.currentTimeMillis(); 81 | record.doRecord(ActionEnum.JOIN_DHT_INTERVAL); 82 | } 83 | } 84 | 85 | private BencodeMap findNodeQuery() { 86 | BencodeMap result = new BencodeMap(); 87 | byte[] transaction = new byte[4]; 88 | random.nextBytes(transaction); 89 | result.putByteArray(KrpcToken.TRANSACTION, transaction); 90 | result.putString(KrpcToken.TYPE, KrpcToken.QUERY); 91 | result.putString(KrpcToken.QUERY, KrpcToken.FIND_NODE); 92 | BencodeMap argumentsMap = new BencodeMap(); 93 | argumentsMap.putByteArray(KrpcToken.ID, config.getSelfNodeId()); 94 | random.nextBytes(target); 95 | argumentsMap.putByteArray(KrpcToken.TARGET, target); 96 | result.put(KrpcToken.ARGUMENTS_MAP, argumentsMap); 97 | return result; 98 | } 99 | 100 | @Override 101 | public int weights() { 102 | return 0; 103 | } 104 | 105 | @Override 106 | public void onResponse(KrpcMessage response) { 107 | BencodeMap responseMap = response.getMessage().getBencodeMap(KrpcToken.RESPONSES_MAP); 108 | byte[] senderId = responseMap.getByteArray(KrpcToken.ID); 109 | if (Arrays.equals(senderId, config.getSelfNodeId())) { 110 | blackList.add(response.getAddress()); 111 | return; 112 | } 113 | addQueue(goodNode, response.getAddress()); 114 | 115 | byte[] nodes = response.nodes(); 116 | byte[] id = new byte[KrpcToken.ID_LENGTH]; 117 | byte[] ip = new byte[4]; 118 | if (nodes == null) { 119 | blackList.add(response.getAddress()); 120 | return; 121 | } 122 | ByteBuffer nodesBuffer = ByteBuffer.wrap(nodes); 123 | while (nodesBuffer.hasRemaining()) { 124 | nodesBuffer.get(id); 125 | nodesBuffer.get(ip); 126 | int port = nodesBuffer.getShort() & 0xffff; 127 | try { 128 | InetSocketAddress address = new InetSocketAddress(InetAddress.getByAddress(ip), port); 129 | if (allNode.contains(address) || blackList.contains(address)) { 130 | record.doRecord(ActionEnum.DHT_GET_REPEAT_ADDRESS); 131 | return; 132 | } 133 | record.doRecord(ActionEnum.DHT_GET_NEW_ADDRESS); 134 | 135 | allNode.add(address); 136 | addQueue(newNode, address); 137 | } catch (UnknownHostException e) { 138 | throw new RuntimeException(e); 139 | } 140 | } 141 | } 142 | 143 | @Override 144 | public KrpcMessage getMessage() { 145 | return new KrpcMessage(findNodeQuery(), waitSend.remove()); 146 | } 147 | 148 | @Override 149 | public boolean isWriteAble() { 150 | update(); 151 | return !waitSend.isEmpty(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /server/src/main/java/cc/aguesuka/btfind/dht/handler/DhtHandler.java: -------------------------------------------------------------------------------- 1 | package cc.aguesuka.btfind.dht.handler; 2 | 3 | import cc.aguesuka.bencode.Bencode; 4 | import cc.aguesuka.bencode.BencodeMap; 5 | import cc.aguesuka.btfind.connection.NioHandler; 6 | import cc.aguesuka.btfind.dht.KrpcToken; 7 | import cc.aguesuka.btfind.dht.beans.KrpcMessage; 8 | import cc.aguesuka.btfind.util.record.ActionEnum; 9 | import cc.aguesuka.btfind.util.record.ActionRecord; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.context.ApplicationContext; 13 | import org.springframework.stereotype.Component; 14 | 15 | import javax.annotation.PostConstruct; 16 | import java.io.IOException; 17 | import java.net.SocketAddress; 18 | import java.nio.ByteBuffer; 19 | import java.nio.channels.DatagramChannel; 20 | import java.nio.channels.SelectionKey; 21 | import java.util.Collection; 22 | import java.util.Comparator; 23 | import java.util.List; 24 | import java.util.stream.Collectors; 25 | 26 | /** 27 | * @author :aguesuka 28 | * 2019/9/10 14:22 29 | */ 30 | @Slf4j 31 | @Component 32 | public class DhtHandler implements NioHandler { 33 | private List handlerChains; 34 | private Collection queryChains; 35 | private Collection unknownChains; 36 | private final ApplicationContext applicationContext; 37 | private final ByteBuffer buffer = ByteBuffer.allocate(1024 * 64); 38 | private final ActionRecord record; 39 | 40 | @Autowired 41 | public DhtHandler(ApplicationContext applicationContext, 42 | ActionRecord record) { 43 | this.applicationContext = applicationContext; 44 | this.record = record; 45 | } 46 | 47 | /** 48 | * init chains 49 | */ 50 | @PostConstruct 51 | public void init() { 52 | handlerChains = applicationContext.getBeansOfType(IDhtHandlerChain.class).values() 53 | .stream() 54 | .filter(IBaseDhtChain::enable) 55 | .sorted(Comparator.comparingInt(IDhtHandlerChain::weights).reversed()) 56 | .collect(Collectors.toList()); 57 | queryChains = applicationContext.getBeansOfType(IDhtQueryChain.class).values() 58 | .stream() 59 | .filter(IBaseDhtChain::enable) 60 | .collect(Collectors.toList()); 61 | unknownChains = applicationContext.getBeansOfType(IDhtUnknownChain.class).values() 62 | .stream() 63 | .filter(IBaseDhtChain::enable) 64 | .collect(Collectors.toList()); 65 | } 66 | 67 | private KrpcMessage readMessage(SelectionKey key) { 68 | try { 69 | DatagramChannel channel = (DatagramChannel) key.channel(); 70 | buffer.clear(); 71 | SocketAddress address = channel.receive(buffer); 72 | buffer.flip(); 73 | if (address == null) { 74 | return null; 75 | } 76 | BencodeMap message = Bencode.parse(buffer); 77 | if (buffer.hasRemaining()) { 78 | return null; 79 | } 80 | 81 | KrpcMessage krpcMessage = new KrpcMessage(message, address); 82 | log.info("recv message `{}`", krpcMessage); 83 | record.doRecord(ActionEnum.DHT_RECV_LENGTH, buffer.position() / 1024.0); 84 | return krpcMessage; 85 | } catch (IOException | RuntimeException e) { 86 | log.warn(e.getMessage(), e); 87 | return null; 88 | } 89 | } 90 | 91 | private void sendMessage(SelectionKey key, KrpcMessage message) { 92 | try { 93 | DatagramChannel channel = (DatagramChannel) key.channel(); 94 | buffer.clear(); 95 | message.getMessage().writeToBuffer(buffer); 96 | buffer.flip(); 97 | // fixme DatagramChannelImpl#send0 throw exception 98 | channel.send(buffer, message.getAddress()); 99 | if (buffer.hasRemaining()) { 100 | throw new DhtHandlerException("send message fail:buff has remaining"); 101 | } 102 | log.info("send message `{}`", message); 103 | record.doRecord(ActionEnum.DHT_SEND_SUCCESS); 104 | record.doRecord(ActionEnum.DHT_SEND_LENGTH, buffer.position() / 1024.0); 105 | } catch (IOException e) { 106 | record.doRecord(ActionEnum.DHT_SEND_FAIL); 107 | log.warn(e.getMessage(), e); 108 | } 109 | } 110 | 111 | @Override 112 | public void doHandler(SelectionKey key) { 113 | try { 114 | if (key.isReadable()) { 115 | KrpcMessage krpcMessage = readMessage(key); 116 | if (krpcMessage != null) { 117 | onReadMessage(krpcMessage); 118 | } 119 | } else if (key.isWritable()) { 120 | for (IDhtHandlerChain chain : handlerChains) { 121 | if (chain.isWriteAble()) { 122 | KrpcMessage message = chain.getMessage(); 123 | sendMessage(key, message); 124 | break; 125 | } 126 | } 127 | } 128 | } finally { 129 | // update Operation-set 130 | if (handlerChains.stream().anyMatch(IDhtHandlerChain::isWriteAble)) { 131 | key.interestOps(SelectionKey.OP_WRITE | SelectionKey.OP_READ); 132 | } else { 133 | key.interestOps(SelectionKey.OP_READ); 134 | } 135 | } 136 | } 137 | 138 | private void onReadMessage(KrpcMessage message) { 139 | String type = message.type(); 140 | if (type == null) { 141 | unknownChains.forEach(c -> c.onUnknownType(message)); 142 | return; 143 | } 144 | switch (type) { 145 | default: 146 | unknownChains.forEach(c -> c.onUnknownType(message)); 147 | break; 148 | case KrpcToken.TYPE_ERROR: 149 | unknownChains.forEach(c -> c.onRecvError(message)); 150 | break; 151 | case KrpcToken.TYPE_QUERY: 152 | onQuery(message); 153 | queryChains.forEach(c -> c.onQuery(message)); 154 | break; 155 | case KrpcToken.TYPE_RESPONSE: 156 | handlerChains.forEach(c -> c.onResponse(message)); 157 | break; 158 | } 159 | } 160 | 161 | private void onQuery(KrpcMessage message) { 162 | String queryType = message.queryType(); 163 | switch (queryType) { 164 | default: 165 | unknownChains.forEach(c -> c.onUnknownTypeQuery(message)); 166 | break; 167 | case KrpcToken.PING: 168 | queryChains.forEach(c -> c.onPing(message)); 169 | break; 170 | case KrpcToken.FIND_NODE: 171 | queryChains.forEach(c -> c.onFindNodes(message)); 172 | break; 173 | case KrpcToken.GET_PEERS: 174 | queryChains.forEach(c -> c.onGetPeer(message)); 175 | break; 176 | case KrpcToken.ANNOUNCE_PEER: 177 | queryChains.forEach(c -> c.onAnnouncePeer(message)); 178 | break; 179 | } 180 | } 181 | } 182 | --------------------------------------------------------------------------------