From 6eefab86dabcb1edfccb2fd7a7ed9a2c9e3e40b7 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 18 Jun 2025 13:27:25 +0800 Subject: [PATCH] first commit --- .gitignore | 12 + .gitmodules | 0 Dockerfile | 28 + Makefile | 50 + cmd/main.go | 194 ++ deploy/init.sql | 10 + deploy/intent-system.sql | 409 +++ deploy/run.sh | 43 + deploy/stop.sh | 2 + deploy/数据库部署要求.md | 165 + .../anacrolix/torrent/.circleci/config.yml | 64 + .../anacrolix/torrent/.deepsource.toml | 14 + .../.github/actions/go-common/action.yml | 61 + .../.github/workflows/codeql-analysis.yml | 54 + .../torrent/.github/workflows/go.yml | 115 + .../torrent/.github/workflows/linter.yml | 37 + deps/github.com/anacrolix/torrent/.gitignore | 5 + .../anacrolix/torrent/.golangci.yml | 8 + deps/github.com/anacrolix/torrent/LICENSE | 373 +++ deps/github.com/anacrolix/torrent/NOTES.md | 32 + deps/github.com/anacrolix/torrent/README.md | 102 + deps/github.com/anacrolix/torrent/SECURITY.md | 11 + deps/github.com/anacrolix/torrent/TODO | 5 + .../torrent/analysis/peer-upload-order.go | 103 + .../anacrolix/torrent/bad_storage.go | 56 + .../anacrolix/torrent/bencode/README.md | 38 + .../anacrolix/torrent/bencode/api.go | 164 + .../anacrolix/torrent/bencode/bench_test.go | 45 + .../anacrolix/torrent/bencode/both_test.go | 76 + .../anacrolix/torrent/bencode/bytes.go | 30 + .../anacrolix/torrent/bencode/bytes_test.go | 39 + .../anacrolix/torrent/bencode/decode.go | 752 +++++ .../anacrolix/torrent/bencode/decode_test.go | 267 ++ .../anacrolix/torrent/bencode/encode.go | 293 ++ .../anacrolix/torrent/bencode/encode_test.go | 91 + .../anacrolix/torrent/bencode/fuzz_test.go | 53 + .../anacrolix/torrent/bencode/misc.go | 11 + .../anacrolix/torrent/bencode/scanner.go | 38 + .../anacrolix/torrent/bencode/string.go | 9 + .../anacrolix/torrent/bencode/string_go120.go | 9 + .../anacrolix/torrent/bencode/tags.go | 44 + ...nux-2011.08.19-netinstall-i686.iso.torrent | Bin 0 -> 12792 bytes .../bencode/testdata/continuum.torrent | Bin 0 -> 31207 bytes ...f8e03701b8729a159063a9ca0884df18a5c9499715 | 2 + ...a448414897e30e595bce25ef2685aaf459f06afaf7 | 2 + ...3f173a59a4d4418d23d3db9af8abd3ea5412c629c5 | 2 + ...ed4bd0b23e2d9080499038d4353739f5c81b05fc0a | 2 + ...94a9106ad601e23fd484747898394a12bddba90615 | 2 + ...bee049320c697e6e7ae547e0a4c4ebd4c2cdd25c1b | 2 + ...5216675a7e1b7b8ef7b4e4d80ec7b7b5dce6dbbb38 | 2 + ...4ae105efbc87c9aee44b9a55dba7f1789d8d862f45 | 2 + ...4998a08e90a5c437df90e68caeea0650ee3c7e7b42 | 2 + ...f59e17e92d2936cda9f4b260994a830ec27cfb88c3 | 2 + deps/github.com/anacrolix/torrent/bep40.go | 85 + .../anacrolix/torrent/bep40_test.go | 34 + .../github.com/anacrolix/torrent/callbacks.go | 40 + .../anacrolix/torrent/client-nowasm_test.go | 71 + .../anacrolix/torrent/client-stats.go | 52 + deps/github.com/anacrolix/torrent/client.go | 1792 ++++++++++ .../anacrolix/torrent/client_test.go | 909 +++++ .../torrent/cmd/magnet-metainfo/main.go | 59 + .../torrent/cmd/torrent-pick/main.go | 189 ++ .../torrent/cmd/torrent-verify/main.go | 94 + .../anacrolix/torrent/cmd/torrent/announce.go | 40 + .../anacrolix/torrent/cmd/torrent/create.go | 70 + .../anacrolix/torrent/cmd/torrent/download.go | 394 +++ .../anacrolix/torrent/cmd/torrent/main.go | 152 + .../anacrolix/torrent/cmd/torrent/metainfo.go | 132 + .../anacrolix/torrent/cmd/torrent/scrape.go | 31 + .../anacrolix/torrent/cmd/torrent/serve.go | 89 + .../torrent/cmd/torrent/total-length.go | 21 + .../torrent/common/upverted_files.go | 18 + deps/github.com/anacrolix/torrent/config.go | 249 ++ .../anacrolix/torrent/conn_stats.go | 117 + deps/github.com/anacrolix/torrent/deferrwl.go | 36 + deps/github.com/anacrolix/torrent/dht.go | 62 + .../github.com/anacrolix/torrent/dial-pool.go | 43 + deps/github.com/anacrolix/torrent/dialer.go | 12 + .../anacrolix/torrent/dialer/dialer.go | 34 + deps/github.com/anacrolix/torrent/doc.go | 33 + .../anacrolix/torrent/example_test.go | 25 + deps/github.com/anacrolix/torrent/file.go | 206 ++ .../github.com/anacrolix/torrent/file_test.go | 118 + deps/github.com/anacrolix/torrent/fs/TODO | 1 + .../torrent/fs/cmd/torrentfs/main.go | 158 + .../anacrolix/torrent/fs/file_handle.go | 89 + .../anacrolix/torrent/fs/filenode.go | 27 + deps/github.com/anacrolix/torrent/fs/test.sh | 37 + .../anacrolix/torrent/fs/torrentfs.go | 215 ++ .../anacrolix/torrent/fs/torrentfs_test.go | 240 ++ .../anacrolix/torrent/fs/unwedge-tests.sh | 5 + deps/github.com/anacrolix/torrent/global.go | 65 + deps/github.com/anacrolix/torrent/go.mod | 127 + deps/github.com/anacrolix/torrent/go.sum | 908 +++++ .../github.com/anacrolix/torrent/handshake.go | 72 + .../anacrolix/torrent/handshake_test.go | 15 + .../internal/alloclim/alloclim_test.go | 93 + .../anacrolix/torrent/internal/alloclim/l.go | 80 + .../anacrolix/torrent/internal/alloclim/r.go | 101 + .../anacrolix/torrent/internal/check/check.go | 5 + .../torrent/internal/check/check_testing.go | 11 + .../torrent/internal/cmd/issue-464/main.go | 48 + .../torrent/internal/cmd/issue-465/main.go | 70 + .../torrent/internal/limiter/limiter.go | 64 + .../torrent/internal/nestedmaps/nestedmaps.go | 73 + .../internal/nestedmaps/nestedmaps_test.go | 41 + .../torrent/internal/panicif/panicif.go | 21 + .../torrent/internal/testutil/greeting.go | 50 + .../torrent/internal/testutil/spec.go | 67 + .../internal/testutil/status_writer.go | 55 + .../anacrolix/torrent/iplist/cidr.go | 41 + .../anacrolix/torrent/iplist/cidr_test.go | 41 + .../torrent/iplist/cmd/iplist/main.go | 33 + .../torrent/iplist/cmd/pack-blocklist/main.go | 27 + .../anacrolix/torrent/iplist/iplist.go | 185 ++ .../anacrolix/torrent/iplist/iplist_test.go | 124 + .../anacrolix/torrent/iplist/packed.go | 145 + .../anacrolix/torrent/iplist/packed_test.go | 35 + deps/github.com/anacrolix/torrent/ipport.go | 71 + .../anacrolix/torrent/issue211_test.go | 41 + .../anacrolix/torrent/issue97_test.go | 28 + deps/github.com/anacrolix/torrent/listen.go | 11 + .../anacrolix/torrent/logonce/logonce.go | 47 + .../github.com/anacrolix/torrent/main_test.go | 21 + .../anacrolix/torrent/metainfo/README | 1 + .../torrent/metainfo/announcelist.go | 35 + .../anacrolix/torrent/metainfo/fileinfo.go | 35 + .../anacrolix/torrent/metainfo/fuzz_test.go | 47 + .../anacrolix/torrent/metainfo/hash.go | 16 + .../anacrolix/torrent/metainfo/info.go | 165 + .../anacrolix/torrent/metainfo/info_test.go | 16 + .../anacrolix/torrent/metainfo/magnet.go | 120 + .../anacrolix/torrent/metainfo/magnet_test.go | 114 + .../anacrolix/torrent/metainfo/metainfo.go | 98 + .../torrent/metainfo/metainfo_test.go | 162 + .../anacrolix/torrent/metainfo/nodes.go | 38 + .../anacrolix/torrent/metainfo/nodes_test.go | 74 + .../torrent/metainfo/piece-length.go | 55 + .../anacrolix/torrent/metainfo/piece.go | 28 + .../anacrolix/torrent/metainfo/piece_key.go | 7 + .../anacrolix/torrent/metainfo/pieces.go | 22 + ...72685E8DB0C8F15553382A927F185C4F01.torrent | Bin 0 -> 41306 bytes .../SKODAOCTAVIA336x280_archive.torrent | Bin 0 -> 2826 bytes ...nux-2011.08.19-netinstall-i686.iso.torrent | Bin 0 -> 12792 bytes .../metainfo/testdata/continuum.torrent | Bin 0 -> 31207 bytes .../metainfo/testdata/flat-url-list.torrent | Bin 0 -> 2739 bytes .../metainfo/testdata/issue_65a.torrent | Bin 0 -> 29683 bytes .../metainfo/testdata/issue_65b.torrent | Bin 0 -> 41820 bytes .../metainfo/testdata/trackerless.torrent | 1 + .../anacrolix/torrent/metainfo/urllist.go | 25 + deps/github.com/anacrolix/torrent/misc.go | 194 ++ .../github.com/anacrolix/torrent/misc_test.go | 47 + .../anacrolix/torrent/mmap_span/mmap_span.go | 107 + .../anacrolix/torrent/mse/cmd/mse/main.go | 93 + deps/github.com/anacrolix/torrent/mse/mse.go | 595 ++++ .../anacrolix/torrent/mse/mse_test.go | 278 ++ .../anacrolix/torrent/netip-addrport.go | 52 + .../anacrolix/torrent/network_test.go | 81 + deps/github.com/anacrolix/torrent/networks.go | 57 + .../anacrolix/torrent/ordered-bitmap.go | 59 + deps/github.com/anacrolix/torrent/otel.go | 3 + .../anacrolix/torrent/peer-conn-msg-writer.go | 130 + .../github.com/anacrolix/torrent/peer-impl.go | 37 + deps/github.com/anacrolix/torrent/peer.go | 877 +++++ .../github.com/anacrolix/torrent/peer_info.go | 44 + .../anacrolix/torrent/peer_infos.go | 35 + .../torrent/peer_protocol/compactip.go | 22 + .../torrent/peer_protocol/decoder.go | 137 + .../torrent/peer_protocol/decoder_test.go | 93 + .../torrent/peer_protocol/extended.go | 40 + .../torrent/peer_protocol/fuzz_test.go | 65 + .../torrent/peer_protocol/handshake.go | 188 ++ .../anacrolix/torrent/peer_protocol/int.go | 50 + .../peer_protocol/messagetype_string.go | 30 + .../torrent/peer_protocol/metadata.go | 42 + .../anacrolix/torrent/peer_protocol/msg.go | 139 + .../anacrolix/torrent/peer_protocol/pex.go | 49 + .../torrent/peer_protocol/pex_test.go | 64 + .../torrent/peer_protocol/protocol.go | 52 + .../torrent/peer_protocol/protocol_test.go | 154 + .../torrent/peer_protocol/reqspec.go | 11 + .../fuzz/FuzzDecoder/18f327bd85f3ab06 | 2 + .../fuzz/FuzzDecoder/252f96643f6de0fc | 2 + .../fuzz/FuzzDecoder/44a1b6410e7ce227 | 2 + .../fuzz/FuzzDecoder/52452abe5ed3cb64 | 2 + .../fuzz/FuzzDecoder/9d2ec002df4eda28 | 2 + .../fuzz/FuzzDecoder/aceaaae6cd039fb5 | 2 + .../fuzz/FuzzDecoder/eb13c84d13ebb034 | 2 + .../peer_protocol/ut-holepunch/err-code.go | 31 + .../ut-holepunch/err-code_test.go | 10 + .../ut-holepunch/ut-holepunch.go | 97 + .../ut-holepunch/ut-holepunch_test.go | 63 + deps/github.com/anacrolix/torrent/peerconn.go | 1146 +++++++ .../anacrolix/torrent/peerconn_test.go | 368 +++ deps/github.com/anacrolix/torrent/peerid.go | 5 + .../anacrolix/torrent/peerid_test.go | 16 + .../anacrolix/torrent/pending-requests.go | 50 + .../torrent/pending-requests_test.go | 12 + deps/github.com/anacrolix/torrent/pex.go | 246 ++ deps/github.com/anacrolix/torrent/pex_test.go | 327 ++ deps/github.com/anacrolix/torrent/pexconn.go | 168 + .../anacrolix/torrent/pexconn_test.go | 61 + deps/github.com/anacrolix/torrent/piece.go | 257 ++ .../anacrolix/torrent/piecestate.go | 27 + deps/github.com/anacrolix/torrent/portfwd.go | 45 + .../anacrolix/torrent/prioritized-peers.go | 82 + .../torrent/prioritized-peers_test.go | 55 + deps/github.com/anacrolix/torrent/protocol.go | 9 + .../anacrolix/torrent/ratelimitreader.go | 53 + deps/github.com/anacrolix/torrent/reader.go | 332 ++ .../anacrolix/torrent/reader_test.go | 26 + .../torrent/request-strategy-impls.go | 77 + .../request-strategy/ajwerner-btree.go | 44 + .../torrent/request-strategy/order.go | 99 + .../torrent/request-strategy/peer.go | 32 + .../request-strategy/piece-request-order.go | 81 + .../piece-request-order_test.go | 106 + .../torrent/request-strategy/piece.go | 12 + .../torrent/request-strategy/tidwall-btree.go | 37 + .../torrent/request-strategy/torrent.go | 6 + .../anacrolix/torrent/requesting.go | 313 ++ .../anacrolix/torrent/requesting_test.go | 78 + .../anacrolix/torrent/reuse_test.go | 79 + .../anacrolix/torrent/rlreader_test.go | 128 + deps/github.com/anacrolix/torrent/roaring.go | 16 + .../anacrolix/torrent/segments/index.go | 45 + .../anacrolix/torrent/segments/segments.go | 63 + .../torrent/segments/segments_test.go | 92 + deps/github.com/anacrolix/torrent/smartban.go | 56 + .../anacrolix/torrent/smartban/smartban.go | 51 + deps/github.com/anacrolix/torrent/socket.go | 184 ++ deps/github.com/anacrolix/torrent/sockopts.go | 10 + .../anacrolix/torrent/sockopts_unix.go | 29 + .../anacrolix/torrent/sockopts_wasm.go | 12 + .../anacrolix/torrent/sockopts_windows.go | 15 + deps/github.com/anacrolix/torrent/sources.go | 80 + deps/github.com/anacrolix/torrent/spec.go | 90 + deps/github.com/anacrolix/torrent/stats.go | 12 + .../torrent/storage/bolt-piece-completion.go | 97 + .../storage/bolt-piece-completion_test.go | 36 + .../anacrolix/torrent/storage/bolt-piece.go | 112 + .../torrent/storage/bolt-piece_test.go | 12 + .../anacrolix/torrent/storage/bolt.go | 64 + .../default-dir-piece-completion-boltdb.go | 11 + .../default-dir-piece-completion-other.go | 14 + .../torrent/storage/disabled/disabled.go | 52 + .../anacrolix/torrent/storage/doc.go | 2 + .../torrent/storage/file-deprecated.go | 34 + .../anacrolix/torrent/storage/file-misc.go | 34 + .../torrent/storage/file-misc_test.go | 37 + .../anacrolix/torrent/storage/file-paths.go | 38 + .../anacrolix/torrent/storage/file-piece.go | 59 + .../anacrolix/torrent/storage/file.go | 212 ++ .../anacrolix/torrent/storage/file_test.go | 42 + .../anacrolix/torrent/storage/interface.go | 60 + .../anacrolix/torrent/storage/issue95_test.go | 51 + .../anacrolix/torrent/storage/issue96_test.go | 37 + .../torrent/storage/map-piece-completion.go | 34 + .../torrent/storage/mark-complete_test.go | 30 + .../anacrolix/torrent/storage/mmap.go | 230 ++ .../anacrolix/torrent/storage/mmap_test.go | 25 + .../torrent/storage/piece-completion.go | 27 + .../torrent/storage/piece-resource.go | 279 ++ .../anacrolix/torrent/storage/safe-path.go | 29 + .../torrent/storage/safe-path_test.go | 71 + .../storage/sqlite-piece-completion.go | 85 + .../torrent/storage/sqlite/deprecated.go | 10 + .../torrent/storage/sqlite/direct.go | 83 + .../anacrolix/torrent/storage/sqlite/dummy.go | 1 + .../storage/sqlite/sqlite-storage_test.go | 106 + .../anacrolix/torrent/storage/storage_test.go | 5 + .../storage/test/bench-piece-mark-complete.go | 91 + .../anacrolix/torrent/storage/wrappers.go | 103 + .../anacrolix/torrent/string-addr.go | 11 + .../anacrolix/torrent/struct_test.go | 12 + deps/github.com/anacrolix/torrent/t.go | 286 ++ .../anacrolix/torrent/test/init_test.go | 11 + .../anacrolix/torrent/test/issue377_test.go | 183 ++ .../anacrolix/torrent/test/leecher-storage.go | 255 ++ .../anacrolix/torrent/test/sqlite_test.go | 68 + .../anacrolix/torrent/test/transfer_test.go | 226 ++ .../anacrolix/torrent/test/unix_test.go | 42 + .../github.com/anacrolix/torrent/test_test.go | 23 + ...IRED CD - Rip. Sample. Mash. Share.torrent | Bin 0 -> 18593 bytes .../The-Fanimatrix-(DivX-5.1-HQ).avi.torrent | Bin 0 -> 10500 bytes .../torrent/testdata/bootstrap.dat.torrent | Bin 0 -> 215716 bytes .../debian-10.8.0-amd64-netinst.iso.torrent | Bin 0 -> 27426 bytes .../debian-9.1.0-amd64-netinst.iso.torrent | Bin 0 -> 23623 bytes .../anacrolix/torrent/testdata/sintel.torrent | Bin 0 -> 20792 bytes deps/github.com/anacrolix/torrent/testing.go | 37 + .../anacrolix/torrent/tests/issue-798/main.go | 19 + .../torrent/torrent-piece-request-order.go | 65 + .../anacrolix/torrent/torrent-stats.go | 17 + deps/github.com/anacrolix/torrent/torrent.go | 2919 +++++++++++++++++ .../anacrolix/torrent/torrent_mmap_test.go | 18 + .../anacrolix/torrent/torrent_test.go | 253 ++ .../anacrolix/torrent/tracker/client.go | 60 + .../anacrolix/torrent/tracker/http/client.go | 49 + .../anacrolix/torrent/tracker/http/http.go | 157 + .../torrent/tracker/http/http_test.go | 76 + .../anacrolix/torrent/tracker/http/peer.go | 46 + .../torrent/tracker/http/protocol.go | 83 + .../anacrolix/torrent/tracker/http/scrape.go | 47 + .../torrent/tracker/http/server/server.go | 125 + .../torrent/tracker/server/server.go | 324 ++ .../tracker/server/upstream-announcing.go | 18 + .../anacrolix/torrent/tracker/server/use.go | 9 + .../torrent/tracker/shared/shared.go | 10 + .../anacrolix/torrent/tracker/tracker.go | 89 + .../anacrolix/torrent/tracker/tracker_test.go | 13 + .../torrent/tracker/udp-server_test.go | 126 + .../anacrolix/torrent/tracker/udp.go | 51 + .../torrent/tracker/udp/addr-family.go | 26 + .../anacrolix/torrent/tracker/udp/announce.go | 53 + .../anacrolix/torrent/tracker/udp/client.go | 225 ++ .../torrent/tracker/udp/conn-client.go | 133 + .../torrent/tracker/udp/dispatcher.go | 71 + .../anacrolix/torrent/tracker/udp/options.go | 24 + .../anacrolix/torrent/tracker/udp/protocol.go | 82 + .../anacrolix/torrent/tracker/udp/scrape.go | 13 + .../torrent/tracker/udp/server/server.go | 241 ++ .../anacrolix/torrent/tracker/udp/timeout.go | 18 + .../torrent/tracker/udp/timeout_test.go | 15 + .../torrent/tracker/udp/transaction.go | 23 + .../anacrolix/torrent/tracker/udp/udp_test.go | 139 + .../anacrolix/torrent/tracker/udp/udp_unix.go | 14 + .../torrent/tracker/udp/udp_windows.go | 11 + .../anacrolix/torrent/tracker/udp_test.go | 194 ++ .../anacrolix/torrent/tracker_scraper.go | 265 ++ .../anacrolix/torrent/typed-roaring/bitmap.go | 48 + .../torrent/typed-roaring/constraints.go | 5 + .../torrent/typed-roaring/iterator.go | 21 + .../torrent/types/infohash/infohash.go | 80 + .../anacrolix/torrent/types/peerid.go | 14 + .../anacrolix/torrent/types/types.go | 52 + .../torrent/undirtied-chunks-iter.go | 23 + .../torrent/undirtied-chunks-iter_test.go | 19 + .../anacrolix/torrent/url-net-addr.go | 26 + .../anacrolix/torrent/ut-holepunching.go | 1 + .../anacrolix/torrent/ut-holepunching_test.go | 407 +++ .../torrent/util/dirwatch/dirwatch.go | 215 ++ .../torrent/util/dirwatch/dirwatch_test.go | 15 + deps/github.com/anacrolix/torrent/utp.go | 18 + deps/github.com/anacrolix/torrent/utp_go.go | 18 + .../anacrolix/torrent/utp_libutp.go | 23 + deps/github.com/anacrolix/torrent/utp_test.go | 16 + .../anacrolix/torrent/version/version.go | 61 + deps/github.com/anacrolix/torrent/webrtc.go | 85 + .../anacrolix/torrent/webseed-peer.go | 222 ++ .../anacrolix/torrent/webseed/client.go | 206 ++ .../anacrolix/torrent/webseed/request.go | 68 + .../anacrolix/torrent/webseed/request_test.go | 60 + .../anacrolix/torrent/webtorrent/LICENSE | 21 + .../anacrolix/torrent/webtorrent/fuzz_test.go | 31 + .../anacrolix/torrent/webtorrent/otel.go | 6 + .../torrent/webtorrent/setting-engine.go | 24 + .../torrent/webtorrent/setting-engine_js.go | 13 + ...25a6f37ba920daf479f86bcfbbb880cd06cbb2ecf8 | 2 + ...9a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4 | 2 + ...bf537d4d81f389524539f402d13aa01f93a65ac7e9 | 2 + .../torrent/webtorrent/tracker-client.go | 396 +++ .../torrent/webtorrent/tracker-protocol.go | 76 + .../anacrolix/torrent/webtorrent/transport.go | 265 ++ .../torrent/webtorrent/transport_test.go | 35 + .../anacrolix/torrent/worse-conns.go | 118 + .../anacrolix/torrent/worse-conns_test.go | 44 + .../github.com/anacrolix/torrent/wstracker.go | 92 + .../interfaces/.github/workflows/rust.yml | 54 + .../ledgerwatch/interfaces/.gitignore | 5 + .../ledgerwatch/interfaces/Cargo.toml | 26 + .../github.com/ledgerwatch/interfaces/LICENSE | 201 ++ .../ledgerwatch/interfaces/README.md | 28 + .../ledgerwatch/interfaces/_docs/README.md | 138 + .../interfaces/_docs/staged-sync.drawio | 1 + .../interfaces/_docs/staged-sync.md | 103 + .../interfaces/_docs/stages-batch-process.png | Bin 0 -> 83515 bytes .../interfaces/_docs/stages-commitment.png | Bin 0 -> 61030 bytes .../interfaces/_docs/stages-etl.png | Bin 0 -> 49802 bytes .../interfaces/_docs/stages-ordering.png | Bin 0 -> 12190 bytes .../interfaces/_docs/stages-overview.png | Bin 0 -> 16305 bytes .../interfaces/_docs/stages-rpc-methods.png | Bin 0 -> 19284 bytes .../ledgerwatch/interfaces/build.rs | 56 + .../interfaces/downloader/downloader.proto | 53 + .../ledgerwatch/interfaces/downloader/keep.go | 1 + .../interfaces/execution/execution.proto | 105 + .../ledgerwatch/interfaces/execution/keep.go | 1 + deps/github.com/ledgerwatch/interfaces/go.mod | 3 + .../github.com/ledgerwatch/interfaces/keep.go | 1 + .../interfaces/p2psentinel/keep.go | 1 + .../interfaces/p2psentinel/sentinel.proto | 57 + .../ledgerwatch/interfaces/p2psentry/keep.go | 1 + .../interfaces/p2psentry/sentry.proto | 200 ++ .../interfaces/remote/ethbackend.proto | 227 ++ .../ledgerwatch/interfaces/remote/keep.go | 1 + .../ledgerwatch/interfaces/remote/kv.proto | 251 ++ .../ledgerwatch/interfaces/src/lib.rs | 153 + .../interfaces/turbo-geth-architecture.png | Bin 0 -> 103260 bytes .../ledgerwatch/interfaces/turbo-geth.drawio | 1 + .../ledgerwatch/interfaces/txpool/README.md | 45 + .../ledgerwatch/interfaces/txpool/keep.go | 1 + .../interfaces/txpool/mining.proto | 103 + .../interfaces/txpool/txpool.proto | 92 + .../ledgerwatch/interfaces/types/keep.go | 1 + .../ledgerwatch/interfaces/types/types.proto | 130 + .../ledgerwatch/interfaces/web3/common.proto | 80 + .../ledgerwatch/interfaces/web3/debug.proto | 32 + .../ledgerwatch/interfaces/web3/eth.proto | 29 + .../ledgerwatch/interfaces/web3/keep.go | 1 + .../ledgerwatch/interfaces/web3/trace.proto | 312 ++ go.mod | 115 + go.sum | 1537 +++++++++ internal/hostid/disk.go | 72 + internal/licensecheck/licensecheck.go | 109 + pkg/api/biz_api.go | 34 + pkg/api/common_api.go | 7 + pkg/api/customer_api.go | 15 + pkg/api/deploy_api.go | 11 + pkg/api/gateway_api.go | 7 + pkg/api/manager_api.go | 9 + pkg/api/platform_api.go | 29 + pkg/api/ws_api.go | 7 + pkg/cache/bigcache_init.go | 55 + pkg/config/config.go | 64 + pkg/controllers/controller.go | 204 ++ pkg/controllers/controller_biz.go | 536 +++ pkg/controllers/controller_common.go | 21 + pkg/controllers/controller_customer.go | 189 ++ pkg/controllers/controller_deploy.go | 150 + pkg/controllers/controller_gateway.go | 30 + pkg/controllers/controller_platform.go | 612 ++++ pkg/controllers/controller_ws.go | 71 + pkg/crypto/crypto_aes.go | 44 + pkg/dal/core/core_biz.go | 865 +++++ pkg/dal/core/core_common.go | 93 + pkg/dal/core/core_customer.go | 388 +++ pkg/dal/core/core_deploy.go | 1257 +++++++ pkg/dal/core/core_deploy_task.go | 182 + pkg/dal/core/core_gateway.go | 252 ++ pkg/dal/core/core_platform.go | 1122 +++++++ pkg/dal/core/core_scheduler.go | 280 ++ pkg/dal/dao/casbin_rule.go | 55 + pkg/dal/dao/customer.go | 159 + pkg/dal/dao/deploy.go | 155 + pkg/dal/dao/dictionary.go | 123 + pkg/dal/dao/invite_code.go | 220 ++ pkg/dal/dao/login.go | 31 + pkg/dal/dao/news.go | 208 ++ pkg/dal/dao/news_draft.go | 104 + pkg/dal/dao/news_spider.go | 75 + pkg/dal/dao/news_subscribe.go | 167 + pkg/dal/dao/oper_log.go | 26 + pkg/dal/dao/privilege.go | 78 + pkg/dal/dao/question_answer.go | 129 + pkg/dal/dao/question_draft.go | 99 + pkg/dal/dao/role.go | 134 + pkg/dal/dao/run_config.go | 65 + pkg/dal/dao/subscriber.go | 87 + pkg/dal/dao/tag.go | 74 + pkg/dal/dao/template.go | 73 + pkg/dal/dao/user.go | 390 +++ pkg/dal/dao/user_role.go | 205 ++ pkg/dal/dao/wk_aig_news.go | 69 + pkg/dal/dao/wk_aig_qna.go | 69 + pkg/dal/dao/wk_spider_news.go | 69 + pkg/dal/dao/wk_spider_qna.go | 69 + pkg/dal/db2go/gen_models.sh | 21 + pkg/dal/models/casbin_rule_do.go | 60 + pkg/dal/models/customer_do.go | 144 + pkg/dal/models/deploy_do.go | 52 + pkg/dal/models/dictionary_do.go | 60 + pkg/dal/models/invite_code_do.go | 75 + pkg/dal/models/login_do.go | 54 + pkg/dal/models/news_do.go | 164 + pkg/dal/models/news_draft_do.go | 111 + pkg/dal/models/news_spider_do.go | 110 + pkg/dal/models/news_subscribe_do.go | 65 + pkg/dal/models/oper_log_do.go | 54 + pkg/dal/models/privilege_do.go | 74 + pkg/dal/models/public_do.go | 70 + pkg/dal/models/question_answer_do.go | 86 + pkg/dal/models/question_draft_do.go | 81 + pkg/dal/models/role_do.go | 70 + pkg/dal/models/run_config_do.go | 55 + pkg/dal/models/subscriber_do.go | 60 + pkg/dal/models/tag_do.go | 59 + pkg/dal/models/template_do.go | 59 + pkg/dal/models/user_do.go | 110 + pkg/dal/models/user_role_do.go | 60 + pkg/dal/models/wk_aig_news_do.go | 67 + pkg/dal/models/wk_aig_qna_do.go | 63 + pkg/dal/models/wk_spider_news_do.go | 62 + pkg/dal/models/wk_spider_qna_do.go | 62 + pkg/dal/ws/pool.go | 79 + pkg/email/email_sender.go | 124 + pkg/email/email_templates.go | 123 + pkg/email/welcome_email.html | 931 ++++++ pkg/email/welcome_email_cn.html | 909 +++++ pkg/itypes/biz_code.go | 97 + pkg/itypes/check_type.go | 34 + pkg/itypes/consts.go | 8 + pkg/itypes/date.go | 68 + pkg/itypes/error_type.go | 169 + pkg/itypes/http.go | 97 + pkg/itypes/session.go | 71 + pkg/itypes/storage_slice.go | 69 + pkg/itypes/task_type.go | 73 + pkg/itypes/types.go | 6 + pkg/itypes/websocket.go | 83 + pkg/middleware/cross.go | 28 + pkg/middleware/jwt.go | 171 + pkg/middleware/jwt_test.go | 28 + pkg/middleware/limiter.go | 19 + pkg/privilege/casbin.conf | 14 + pkg/privilege/casbin.go | 353 ++ pkg/privilege/privileges.go | 136 + pkg/proto/proto_biz.go | 281 ++ pkg/proto/proto_common.go | 18 + pkg/proto/proto_customer.go | 99 + pkg/proto/proto_deploy.go | 58 + pkg/proto/proto_platform.go | 264 ++ pkg/proto/proto_public.go | 60 + pkg/routers/router_biz.go | 94 + pkg/routers/router_common.go | 19 + pkg/routers/router_customer.go | 40 + pkg/routers/router_deploy.go | 38 + pkg/routers/router_gateway.go | 14 + pkg/routers/router_platform.go | 68 + pkg/routers/router_ws.go | 30 + pkg/services/service_manager.go | 335 ++ pkg/sessions/context.go | 84 + pkg/storage/local_storage.go | 138 + pkg/utils/color.go | 79 + pkg/utils/hash.go | 158 + pkg/utils/ip.go | 24 + pkg/utils/network.go | 145 + pkg/utils/passwd_md5.go | 22 + pkg/utils/round.go | 26 + pkg/utils/signature.go | 30 + pkg/utils/str.go | 29 + pkg/utils/time.go | 307 ++ pkg/utils/utils.go | 131 + pkg/utils/utils_test.go | 24 + run-system.sh | 29 + static/hello.html | 10 + 544 files changed, 57422 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 cmd/main.go create mode 100644 deploy/init.sql create mode 100644 deploy/intent-system.sql create mode 100644 deploy/run.sh create mode 100644 deploy/stop.sh create mode 100644 deploy/数据库部署要求.md create mode 100644 deps/github.com/anacrolix/torrent/.circleci/config.yml create mode 100644 deps/github.com/anacrolix/torrent/.deepsource.toml create mode 100644 deps/github.com/anacrolix/torrent/.github/actions/go-common/action.yml create mode 100644 deps/github.com/anacrolix/torrent/.github/workflows/codeql-analysis.yml create mode 100644 deps/github.com/anacrolix/torrent/.github/workflows/go.yml create mode 100644 deps/github.com/anacrolix/torrent/.github/workflows/linter.yml create mode 100644 deps/github.com/anacrolix/torrent/.gitignore create mode 100644 deps/github.com/anacrolix/torrent/.golangci.yml create mode 100644 deps/github.com/anacrolix/torrent/LICENSE create mode 100644 deps/github.com/anacrolix/torrent/NOTES.md create mode 100644 deps/github.com/anacrolix/torrent/README.md create mode 100644 deps/github.com/anacrolix/torrent/SECURITY.md create mode 100644 deps/github.com/anacrolix/torrent/TODO create mode 100644 deps/github.com/anacrolix/torrent/analysis/peer-upload-order.go create mode 100644 deps/github.com/anacrolix/torrent/bad_storage.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/README.md create mode 100644 deps/github.com/anacrolix/torrent/bencode/api.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/bench_test.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/both_test.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/bytes.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/bytes_test.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/decode.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/decode_test.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/encode.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/encode_test.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/fuzz_test.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/misc.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/scanner.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/string.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/string_go120.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/tags.go create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/continuum.torrent create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/Fuzz/65cfcaf31066e15825ace0f8e03701b8729a159063a9ca0884df18a5c9499715 create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/Fuzz/9d85a0638af39a02b96933a448414897e30e595bce25ef2685aaf459f06afaf7 create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/FuzzInterfaceRoundTrip/1d7fd8d6b4e9380abbaa373f173a59a4d4418d23d3db9af8abd3ea5412c629c5 create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/FuzzInterfaceRoundTrip/2674fedd58e056c9322ff8ed4bd0b23e2d9080499038d4353739f5c81b05fc0a create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/FuzzInterfaceRoundTrip/321f4f280d23ac90ccaf7894a9106ad601e23fd484747898394a12bddba90615 create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/FuzzInterfaceRoundTrip/49dbd4b139383deb718477bee049320c697e6e7ae547e0a4c4ebd4c2cdd25c1b create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/FuzzInterfaceRoundTrip/808391fc9a93d89909205a5216675a7e1b7b8ef7b4e4d80ec7b7b5dce6dbbb38 create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/FuzzInterfaceRoundTrip/b83176c6cec6b92f5c66774ae105efbc87c9aee44b9a55dba7f1789d8d862f45 create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/FuzzInterfaceRoundTrip/c73f26cbd996104c4e39ce4998a08e90a5c437df90e68caeea0650ee3c7e7b42 create mode 100644 deps/github.com/anacrolix/torrent/bencode/testdata/fuzz/FuzzInterfaceRoundTrip/eef53fca91deb00d4e30f4f59e17e92d2936cda9f4b260994a830ec27cfb88c3 create mode 100644 deps/github.com/anacrolix/torrent/bep40.go create mode 100644 deps/github.com/anacrolix/torrent/bep40_test.go create mode 100644 deps/github.com/anacrolix/torrent/callbacks.go create mode 100644 deps/github.com/anacrolix/torrent/client-nowasm_test.go create mode 100644 deps/github.com/anacrolix/torrent/client-stats.go create mode 100644 deps/github.com/anacrolix/torrent/client.go create mode 100644 deps/github.com/anacrolix/torrent/client_test.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/magnet-metainfo/main.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent-pick/main.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent-verify/main.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent/announce.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent/create.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent/download.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent/main.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent/metainfo.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent/scrape.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent/serve.go create mode 100644 deps/github.com/anacrolix/torrent/cmd/torrent/total-length.go create mode 100644 deps/github.com/anacrolix/torrent/common/upverted_files.go create mode 100644 deps/github.com/anacrolix/torrent/config.go create mode 100644 deps/github.com/anacrolix/torrent/conn_stats.go create mode 100644 deps/github.com/anacrolix/torrent/deferrwl.go create mode 100644 deps/github.com/anacrolix/torrent/dht.go create mode 100644 deps/github.com/anacrolix/torrent/dial-pool.go create mode 100644 deps/github.com/anacrolix/torrent/dialer.go create mode 100644 deps/github.com/anacrolix/torrent/dialer/dialer.go create mode 100644 deps/github.com/anacrolix/torrent/doc.go create mode 100644 deps/github.com/anacrolix/torrent/example_test.go create mode 100644 deps/github.com/anacrolix/torrent/file.go create mode 100644 deps/github.com/anacrolix/torrent/file_test.go create mode 100644 deps/github.com/anacrolix/torrent/fs/TODO create mode 100644 deps/github.com/anacrolix/torrent/fs/cmd/torrentfs/main.go create mode 100644 deps/github.com/anacrolix/torrent/fs/file_handle.go create mode 100644 deps/github.com/anacrolix/torrent/fs/filenode.go create mode 100644 deps/github.com/anacrolix/torrent/fs/test.sh create mode 100644 deps/github.com/anacrolix/torrent/fs/torrentfs.go create mode 100644 deps/github.com/anacrolix/torrent/fs/torrentfs_test.go create mode 100644 deps/github.com/anacrolix/torrent/fs/unwedge-tests.sh create mode 100644 deps/github.com/anacrolix/torrent/global.go create mode 100644 deps/github.com/anacrolix/torrent/go.mod create mode 100644 deps/github.com/anacrolix/torrent/go.sum create mode 100644 deps/github.com/anacrolix/torrent/handshake.go create mode 100644 deps/github.com/anacrolix/torrent/handshake_test.go create mode 100644 deps/github.com/anacrolix/torrent/internal/alloclim/alloclim_test.go create mode 100644 deps/github.com/anacrolix/torrent/internal/alloclim/l.go create mode 100644 deps/github.com/anacrolix/torrent/internal/alloclim/r.go create mode 100644 deps/github.com/anacrolix/torrent/internal/check/check.go create mode 100644 deps/github.com/anacrolix/torrent/internal/check/check_testing.go create mode 100644 deps/github.com/anacrolix/torrent/internal/cmd/issue-464/main.go create mode 100644 deps/github.com/anacrolix/torrent/internal/cmd/issue-465/main.go create mode 100644 deps/github.com/anacrolix/torrent/internal/limiter/limiter.go create mode 100644 deps/github.com/anacrolix/torrent/internal/nestedmaps/nestedmaps.go create mode 100644 deps/github.com/anacrolix/torrent/internal/nestedmaps/nestedmaps_test.go create mode 100644 deps/github.com/anacrolix/torrent/internal/panicif/panicif.go create mode 100644 deps/github.com/anacrolix/torrent/internal/testutil/greeting.go create mode 100644 deps/github.com/anacrolix/torrent/internal/testutil/spec.go create mode 100644 deps/github.com/anacrolix/torrent/internal/testutil/status_writer.go create mode 100644 deps/github.com/anacrolix/torrent/iplist/cidr.go create mode 100644 deps/github.com/anacrolix/torrent/iplist/cidr_test.go create mode 100644 deps/github.com/anacrolix/torrent/iplist/cmd/iplist/main.go create mode 100644 deps/github.com/anacrolix/torrent/iplist/cmd/pack-blocklist/main.go create mode 100644 deps/github.com/anacrolix/torrent/iplist/iplist.go create mode 100644 deps/github.com/anacrolix/torrent/iplist/iplist_test.go create mode 100644 deps/github.com/anacrolix/torrent/iplist/packed.go create mode 100644 deps/github.com/anacrolix/torrent/iplist/packed_test.go create mode 100644 deps/github.com/anacrolix/torrent/ipport.go create mode 100644 deps/github.com/anacrolix/torrent/issue211_test.go create mode 100644 deps/github.com/anacrolix/torrent/issue97_test.go create mode 100644 deps/github.com/anacrolix/torrent/listen.go create mode 100644 deps/github.com/anacrolix/torrent/logonce/logonce.go create mode 100644 deps/github.com/anacrolix/torrent/main_test.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/README create mode 100644 deps/github.com/anacrolix/torrent/metainfo/announcelist.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/fileinfo.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/fuzz_test.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/hash.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/info.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/info_test.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/magnet.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/magnet_test.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/metainfo.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/metainfo_test.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/nodes.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/nodes_test.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/piece-length.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/piece.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/piece_key.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/pieces.go create mode 100644 deps/github.com/anacrolix/torrent/metainfo/testdata/23516C72685E8DB0C8F15553382A927F185C4F01.torrent create mode 100644 deps/github.com/anacrolix/torrent/metainfo/testdata/SKODAOCTAVIA336x280_archive.torrent create mode 100644 deps/github.com/anacrolix/torrent/metainfo/testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent create mode 100644 deps/github.com/anacrolix/torrent/metainfo/testdata/continuum.torrent create mode 100644 deps/github.com/anacrolix/torrent/metainfo/testdata/flat-url-list.torrent create mode 100644 deps/github.com/anacrolix/torrent/metainfo/testdata/issue_65a.torrent create mode 100644 deps/github.com/anacrolix/torrent/metainfo/testdata/issue_65b.torrent create mode 100644 deps/github.com/anacrolix/torrent/metainfo/testdata/trackerless.torrent create mode 100644 deps/github.com/anacrolix/torrent/metainfo/urllist.go create mode 100644 deps/github.com/anacrolix/torrent/misc.go create mode 100644 deps/github.com/anacrolix/torrent/misc_test.go create mode 100644 deps/github.com/anacrolix/torrent/mmap_span/mmap_span.go create mode 100644 deps/github.com/anacrolix/torrent/mse/cmd/mse/main.go create mode 100644 deps/github.com/anacrolix/torrent/mse/mse.go create mode 100644 deps/github.com/anacrolix/torrent/mse/mse_test.go create mode 100644 deps/github.com/anacrolix/torrent/netip-addrport.go create mode 100644 deps/github.com/anacrolix/torrent/network_test.go create mode 100644 deps/github.com/anacrolix/torrent/networks.go create mode 100644 deps/github.com/anacrolix/torrent/ordered-bitmap.go create mode 100644 deps/github.com/anacrolix/torrent/otel.go create mode 100644 deps/github.com/anacrolix/torrent/peer-conn-msg-writer.go create mode 100644 deps/github.com/anacrolix/torrent/peer-impl.go create mode 100644 deps/github.com/anacrolix/torrent/peer.go create mode 100644 deps/github.com/anacrolix/torrent/peer_info.go create mode 100644 deps/github.com/anacrolix/torrent/peer_infos.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/compactip.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/decoder.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/decoder_test.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/extended.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/fuzz_test.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/handshake.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/int.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/messagetype_string.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/metadata.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/msg.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/pex.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/pex_test.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/protocol.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/protocol_test.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/reqspec.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/18f327bd85f3ab06 create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/252f96643f6de0fc create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/44a1b6410e7ce227 create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/52452abe5ed3cb64 create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/9d2ec002df4eda28 create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/aceaaae6cd039fb5 create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/eb13c84d13ebb034 create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/err-code.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/err-code_test.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/ut-holepunch.go create mode 100644 deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/ut-holepunch_test.go create mode 100644 deps/github.com/anacrolix/torrent/peerconn.go create mode 100644 deps/github.com/anacrolix/torrent/peerconn_test.go create mode 100644 deps/github.com/anacrolix/torrent/peerid.go create mode 100644 deps/github.com/anacrolix/torrent/peerid_test.go create mode 100644 deps/github.com/anacrolix/torrent/pending-requests.go create mode 100644 deps/github.com/anacrolix/torrent/pending-requests_test.go create mode 100644 deps/github.com/anacrolix/torrent/pex.go create mode 100644 deps/github.com/anacrolix/torrent/pex_test.go create mode 100644 deps/github.com/anacrolix/torrent/pexconn.go create mode 100644 deps/github.com/anacrolix/torrent/pexconn_test.go create mode 100644 deps/github.com/anacrolix/torrent/piece.go create mode 100644 deps/github.com/anacrolix/torrent/piecestate.go create mode 100644 deps/github.com/anacrolix/torrent/portfwd.go create mode 100644 deps/github.com/anacrolix/torrent/prioritized-peers.go create mode 100644 deps/github.com/anacrolix/torrent/prioritized-peers_test.go create mode 100644 deps/github.com/anacrolix/torrent/protocol.go create mode 100644 deps/github.com/anacrolix/torrent/ratelimitreader.go create mode 100644 deps/github.com/anacrolix/torrent/reader.go create mode 100644 deps/github.com/anacrolix/torrent/reader_test.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy-impls.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy/ajwerner-btree.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy/order.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy/peer.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy/piece-request-order.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy/piece-request-order_test.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy/piece.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy/tidwall-btree.go create mode 100644 deps/github.com/anacrolix/torrent/request-strategy/torrent.go create mode 100644 deps/github.com/anacrolix/torrent/requesting.go create mode 100644 deps/github.com/anacrolix/torrent/requesting_test.go create mode 100644 deps/github.com/anacrolix/torrent/reuse_test.go create mode 100644 deps/github.com/anacrolix/torrent/rlreader_test.go create mode 100644 deps/github.com/anacrolix/torrent/roaring.go create mode 100644 deps/github.com/anacrolix/torrent/segments/index.go create mode 100644 deps/github.com/anacrolix/torrent/segments/segments.go create mode 100644 deps/github.com/anacrolix/torrent/segments/segments_test.go create mode 100644 deps/github.com/anacrolix/torrent/smartban.go create mode 100644 deps/github.com/anacrolix/torrent/smartban/smartban.go create mode 100644 deps/github.com/anacrolix/torrent/socket.go create mode 100644 deps/github.com/anacrolix/torrent/sockopts.go create mode 100644 deps/github.com/anacrolix/torrent/sockopts_unix.go create mode 100644 deps/github.com/anacrolix/torrent/sockopts_wasm.go create mode 100644 deps/github.com/anacrolix/torrent/sockopts_windows.go create mode 100644 deps/github.com/anacrolix/torrent/sources.go create mode 100644 deps/github.com/anacrolix/torrent/spec.go create mode 100644 deps/github.com/anacrolix/torrent/stats.go create mode 100644 deps/github.com/anacrolix/torrent/storage/bolt-piece-completion.go create mode 100644 deps/github.com/anacrolix/torrent/storage/bolt-piece-completion_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/bolt-piece.go create mode 100644 deps/github.com/anacrolix/torrent/storage/bolt-piece_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/bolt.go create mode 100644 deps/github.com/anacrolix/torrent/storage/default-dir-piece-completion-boltdb.go create mode 100644 deps/github.com/anacrolix/torrent/storage/default-dir-piece-completion-other.go create mode 100644 deps/github.com/anacrolix/torrent/storage/disabled/disabled.go create mode 100644 deps/github.com/anacrolix/torrent/storage/doc.go create mode 100644 deps/github.com/anacrolix/torrent/storage/file-deprecated.go create mode 100644 deps/github.com/anacrolix/torrent/storage/file-misc.go create mode 100644 deps/github.com/anacrolix/torrent/storage/file-misc_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/file-paths.go create mode 100644 deps/github.com/anacrolix/torrent/storage/file-piece.go create mode 100644 deps/github.com/anacrolix/torrent/storage/file.go create mode 100644 deps/github.com/anacrolix/torrent/storage/file_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/interface.go create mode 100644 deps/github.com/anacrolix/torrent/storage/issue95_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/issue96_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/map-piece-completion.go create mode 100644 deps/github.com/anacrolix/torrent/storage/mark-complete_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/mmap.go create mode 100644 deps/github.com/anacrolix/torrent/storage/mmap_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/piece-completion.go create mode 100644 deps/github.com/anacrolix/torrent/storage/piece-resource.go create mode 100644 deps/github.com/anacrolix/torrent/storage/safe-path.go create mode 100644 deps/github.com/anacrolix/torrent/storage/safe-path_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/sqlite-piece-completion.go create mode 100644 deps/github.com/anacrolix/torrent/storage/sqlite/deprecated.go create mode 100644 deps/github.com/anacrolix/torrent/storage/sqlite/direct.go create mode 100644 deps/github.com/anacrolix/torrent/storage/sqlite/dummy.go create mode 100644 deps/github.com/anacrolix/torrent/storage/sqlite/sqlite-storage_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/storage_test.go create mode 100644 deps/github.com/anacrolix/torrent/storage/test/bench-piece-mark-complete.go create mode 100644 deps/github.com/anacrolix/torrent/storage/wrappers.go create mode 100644 deps/github.com/anacrolix/torrent/string-addr.go create mode 100644 deps/github.com/anacrolix/torrent/struct_test.go create mode 100644 deps/github.com/anacrolix/torrent/t.go create mode 100644 deps/github.com/anacrolix/torrent/test/init_test.go create mode 100644 deps/github.com/anacrolix/torrent/test/issue377_test.go create mode 100644 deps/github.com/anacrolix/torrent/test/leecher-storage.go create mode 100644 deps/github.com/anacrolix/torrent/test/sqlite_test.go create mode 100644 deps/github.com/anacrolix/torrent/test/transfer_test.go create mode 100644 deps/github.com/anacrolix/torrent/test/unix_test.go create mode 100644 deps/github.com/anacrolix/torrent/test_test.go create mode 100644 deps/github.com/anacrolix/torrent/testdata/The WIRED CD - Rip. Sample. Mash. Share.torrent create mode 100644 deps/github.com/anacrolix/torrent/testdata/The-Fanimatrix-(DivX-5.1-HQ).avi.torrent create mode 100644 deps/github.com/anacrolix/torrent/testdata/bootstrap.dat.torrent create mode 100644 deps/github.com/anacrolix/torrent/testdata/debian-10.8.0-amd64-netinst.iso.torrent create mode 100644 deps/github.com/anacrolix/torrent/testdata/debian-9.1.0-amd64-netinst.iso.torrent create mode 100644 deps/github.com/anacrolix/torrent/testdata/sintel.torrent create mode 100644 deps/github.com/anacrolix/torrent/testing.go create mode 100644 deps/github.com/anacrolix/torrent/tests/issue-798/main.go create mode 100644 deps/github.com/anacrolix/torrent/torrent-piece-request-order.go create mode 100644 deps/github.com/anacrolix/torrent/torrent-stats.go create mode 100644 deps/github.com/anacrolix/torrent/torrent.go create mode 100644 deps/github.com/anacrolix/torrent/torrent_mmap_test.go create mode 100644 deps/github.com/anacrolix/torrent/torrent_test.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/client.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/http/client.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/http/http.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/http/http_test.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/http/peer.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/http/protocol.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/http/scrape.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/http/server/server.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/server/server.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/server/upstream-announcing.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/server/use.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/shared/shared.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/tracker.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/tracker_test.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp-server_test.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/addr-family.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/announce.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/client.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/conn-client.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/dispatcher.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/options.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/protocol.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/scrape.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/server/server.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/timeout.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/timeout_test.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/transaction.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/udp_test.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/udp_unix.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp/udp_windows.go create mode 100644 deps/github.com/anacrolix/torrent/tracker/udp_test.go create mode 100644 deps/github.com/anacrolix/torrent/tracker_scraper.go create mode 100644 deps/github.com/anacrolix/torrent/typed-roaring/bitmap.go create mode 100644 deps/github.com/anacrolix/torrent/typed-roaring/constraints.go create mode 100644 deps/github.com/anacrolix/torrent/typed-roaring/iterator.go create mode 100644 deps/github.com/anacrolix/torrent/types/infohash/infohash.go create mode 100644 deps/github.com/anacrolix/torrent/types/peerid.go create mode 100644 deps/github.com/anacrolix/torrent/types/types.go create mode 100644 deps/github.com/anacrolix/torrent/undirtied-chunks-iter.go create mode 100644 deps/github.com/anacrolix/torrent/undirtied-chunks-iter_test.go create mode 100644 deps/github.com/anacrolix/torrent/url-net-addr.go create mode 100644 deps/github.com/anacrolix/torrent/ut-holepunching.go create mode 100644 deps/github.com/anacrolix/torrent/ut-holepunching_test.go create mode 100644 deps/github.com/anacrolix/torrent/util/dirwatch/dirwatch.go create mode 100644 deps/github.com/anacrolix/torrent/util/dirwatch/dirwatch_test.go create mode 100644 deps/github.com/anacrolix/torrent/utp.go create mode 100644 deps/github.com/anacrolix/torrent/utp_go.go create mode 100644 deps/github.com/anacrolix/torrent/utp_libutp.go create mode 100644 deps/github.com/anacrolix/torrent/utp_test.go create mode 100644 deps/github.com/anacrolix/torrent/version/version.go create mode 100644 deps/github.com/anacrolix/torrent/webrtc.go create mode 100644 deps/github.com/anacrolix/torrent/webseed-peer.go create mode 100644 deps/github.com/anacrolix/torrent/webseed/client.go create mode 100644 deps/github.com/anacrolix/torrent/webseed/request.go create mode 100644 deps/github.com/anacrolix/torrent/webseed/request_test.go create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/LICENSE create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/fuzz_test.go create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/otel.go create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/setting-engine.go create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/setting-engine_js.go create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/195b11403204772a785dfc25a6f37ba920daf479f86bcfbbb880cd06cbb2ecf8 create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4 create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/caf81e9797b19c76c1fc4dbf537d4d81f389524539f402d13aa01f93a65ac7e9 create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/tracker-client.go create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/tracker-protocol.go create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/transport.go create mode 100644 deps/github.com/anacrolix/torrent/webtorrent/transport_test.go create mode 100644 deps/github.com/anacrolix/torrent/worse-conns.go create mode 100644 deps/github.com/anacrolix/torrent/worse-conns_test.go create mode 100644 deps/github.com/anacrolix/torrent/wstracker.go create mode 100644 deps/github.com/ledgerwatch/interfaces/.github/workflows/rust.yml create mode 100644 deps/github.com/ledgerwatch/interfaces/.gitignore create mode 100644 deps/github.com/ledgerwatch/interfaces/Cargo.toml create mode 100644 deps/github.com/ledgerwatch/interfaces/LICENSE create mode 100644 deps/github.com/ledgerwatch/interfaces/README.md create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/README.md create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/staged-sync.drawio create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/staged-sync.md create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/stages-batch-process.png create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/stages-commitment.png create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/stages-etl.png create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/stages-ordering.png create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/stages-overview.png create mode 100644 deps/github.com/ledgerwatch/interfaces/_docs/stages-rpc-methods.png create mode 100644 deps/github.com/ledgerwatch/interfaces/build.rs create mode 100644 deps/github.com/ledgerwatch/interfaces/downloader/downloader.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/downloader/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/execution/execution.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/execution/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/go.mod create mode 100644 deps/github.com/ledgerwatch/interfaces/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/p2psentinel/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/p2psentinel/sentinel.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/p2psentry/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/p2psentry/sentry.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/remote/ethbackend.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/remote/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/remote/kv.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/src/lib.rs create mode 100644 deps/github.com/ledgerwatch/interfaces/turbo-geth-architecture.png create mode 100644 deps/github.com/ledgerwatch/interfaces/turbo-geth.drawio create mode 100644 deps/github.com/ledgerwatch/interfaces/txpool/README.md create mode 100644 deps/github.com/ledgerwatch/interfaces/txpool/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/txpool/mining.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/txpool/txpool.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/types/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/types/types.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/web3/common.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/web3/debug.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/web3/eth.proto create mode 100644 deps/github.com/ledgerwatch/interfaces/web3/keep.go create mode 100644 deps/github.com/ledgerwatch/interfaces/web3/trace.proto create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/hostid/disk.go create mode 100644 internal/licensecheck/licensecheck.go create mode 100644 pkg/api/biz_api.go create mode 100644 pkg/api/common_api.go create mode 100644 pkg/api/customer_api.go create mode 100644 pkg/api/deploy_api.go create mode 100644 pkg/api/gateway_api.go create mode 100644 pkg/api/manager_api.go create mode 100644 pkg/api/platform_api.go create mode 100644 pkg/api/ws_api.go create mode 100644 pkg/cache/bigcache_init.go create mode 100644 pkg/config/config.go create mode 100644 pkg/controllers/controller.go create mode 100644 pkg/controllers/controller_biz.go create mode 100644 pkg/controllers/controller_common.go create mode 100644 pkg/controllers/controller_customer.go create mode 100644 pkg/controllers/controller_deploy.go create mode 100644 pkg/controllers/controller_gateway.go create mode 100644 pkg/controllers/controller_platform.go create mode 100644 pkg/controllers/controller_ws.go create mode 100644 pkg/crypto/crypto_aes.go create mode 100644 pkg/dal/core/core_biz.go create mode 100644 pkg/dal/core/core_common.go create mode 100644 pkg/dal/core/core_customer.go create mode 100644 pkg/dal/core/core_deploy.go create mode 100644 pkg/dal/core/core_deploy_task.go create mode 100644 pkg/dal/core/core_gateway.go create mode 100644 pkg/dal/core/core_platform.go create mode 100644 pkg/dal/core/core_scheduler.go create mode 100644 pkg/dal/dao/casbin_rule.go create mode 100644 pkg/dal/dao/customer.go create mode 100644 pkg/dal/dao/deploy.go create mode 100644 pkg/dal/dao/dictionary.go create mode 100644 pkg/dal/dao/invite_code.go create mode 100644 pkg/dal/dao/login.go create mode 100644 pkg/dal/dao/news.go create mode 100644 pkg/dal/dao/news_draft.go create mode 100644 pkg/dal/dao/news_spider.go create mode 100644 pkg/dal/dao/news_subscribe.go create mode 100644 pkg/dal/dao/oper_log.go create mode 100644 pkg/dal/dao/privilege.go create mode 100644 pkg/dal/dao/question_answer.go create mode 100644 pkg/dal/dao/question_draft.go create mode 100644 pkg/dal/dao/role.go create mode 100644 pkg/dal/dao/run_config.go create mode 100644 pkg/dal/dao/subscriber.go create mode 100644 pkg/dal/dao/tag.go create mode 100644 pkg/dal/dao/template.go create mode 100644 pkg/dal/dao/user.go create mode 100644 pkg/dal/dao/user_role.go create mode 100644 pkg/dal/dao/wk_aig_news.go create mode 100644 pkg/dal/dao/wk_aig_qna.go create mode 100644 pkg/dal/dao/wk_spider_news.go create mode 100644 pkg/dal/dao/wk_spider_qna.go create mode 100644 pkg/dal/db2go/gen_models.sh create mode 100644 pkg/dal/models/casbin_rule_do.go create mode 100644 pkg/dal/models/customer_do.go create mode 100644 pkg/dal/models/deploy_do.go create mode 100644 pkg/dal/models/dictionary_do.go create mode 100644 pkg/dal/models/invite_code_do.go create mode 100644 pkg/dal/models/login_do.go create mode 100644 pkg/dal/models/news_do.go create mode 100644 pkg/dal/models/news_draft_do.go create mode 100644 pkg/dal/models/news_spider_do.go create mode 100644 pkg/dal/models/news_subscribe_do.go create mode 100644 pkg/dal/models/oper_log_do.go create mode 100644 pkg/dal/models/privilege_do.go create mode 100644 pkg/dal/models/public_do.go create mode 100644 pkg/dal/models/question_answer_do.go create mode 100644 pkg/dal/models/question_draft_do.go create mode 100644 pkg/dal/models/role_do.go create mode 100644 pkg/dal/models/run_config_do.go create mode 100644 pkg/dal/models/subscriber_do.go create mode 100644 pkg/dal/models/tag_do.go create mode 100644 pkg/dal/models/template_do.go create mode 100644 pkg/dal/models/user_do.go create mode 100644 pkg/dal/models/user_role_do.go create mode 100644 pkg/dal/models/wk_aig_news_do.go create mode 100644 pkg/dal/models/wk_aig_qna_do.go create mode 100644 pkg/dal/models/wk_spider_news_do.go create mode 100644 pkg/dal/models/wk_spider_qna_do.go create mode 100644 pkg/dal/ws/pool.go create mode 100644 pkg/email/email_sender.go create mode 100644 pkg/email/email_templates.go create mode 100644 pkg/email/welcome_email.html create mode 100644 pkg/email/welcome_email_cn.html create mode 100644 pkg/itypes/biz_code.go create mode 100644 pkg/itypes/check_type.go create mode 100644 pkg/itypes/consts.go create mode 100644 pkg/itypes/date.go create mode 100644 pkg/itypes/error_type.go create mode 100644 pkg/itypes/http.go create mode 100644 pkg/itypes/session.go create mode 100644 pkg/itypes/storage_slice.go create mode 100644 pkg/itypes/task_type.go create mode 100644 pkg/itypes/types.go create mode 100644 pkg/itypes/websocket.go create mode 100644 pkg/middleware/cross.go create mode 100644 pkg/middleware/jwt.go create mode 100644 pkg/middleware/jwt_test.go create mode 100644 pkg/middleware/limiter.go create mode 100644 pkg/privilege/casbin.conf create mode 100644 pkg/privilege/casbin.go create mode 100644 pkg/privilege/privileges.go create mode 100644 pkg/proto/proto_biz.go create mode 100644 pkg/proto/proto_common.go create mode 100644 pkg/proto/proto_customer.go create mode 100644 pkg/proto/proto_deploy.go create mode 100644 pkg/proto/proto_platform.go create mode 100644 pkg/proto/proto_public.go create mode 100644 pkg/routers/router_biz.go create mode 100644 pkg/routers/router_common.go create mode 100644 pkg/routers/router_customer.go create mode 100644 pkg/routers/router_deploy.go create mode 100644 pkg/routers/router_gateway.go create mode 100644 pkg/routers/router_platform.go create mode 100644 pkg/routers/router_ws.go create mode 100644 pkg/services/service_manager.go create mode 100644 pkg/sessions/context.go create mode 100644 pkg/storage/local_storage.go create mode 100644 pkg/utils/color.go create mode 100644 pkg/utils/hash.go create mode 100644 pkg/utils/ip.go create mode 100644 pkg/utils/network.go create mode 100644 pkg/utils/passwd_md5.go create mode 100644 pkg/utils/round.go create mode 100644 pkg/utils/signature.go create mode 100644 pkg/utils/str.go create mode 100644 pkg/utils/time.go create mode 100644 pkg/utils/utils.go create mode 100644 pkg/utils/utils_test.go create mode 100755 run-system.sh create mode 100644 static/hello.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba4fa66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/intent-system.exe +/intent-system +/doc/~$集群管理系统角色和权限明细.xlsx +/.idea +/static/static/ +/static/favicon.ico +/static/index.html +/node_modules +/cmd/intent-system +/cmd/intent-system.exe +/go.work +/go.work.local diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5af6922 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.21 AS builder +MAINTAINER lory + +RUN apt-get update && apt-get install -y ca-certificates make +ENV SRC_DIR /intent-system +RUN set -x \ + && cd /tmp + +RUN go env -w GOPROXY=https://goproxy.io + +COPY . $SRC_DIR +RUN cd $SRC_DIR && export GIT_SSL_NO_VERIFY=true && git config --global http.sslVerify "false" && make + +FROM ubuntu:22.04 + +#RUN ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone && apt-get update && apt-get install -y tzdata +#ENV TZ Asia/Shanghai +ENV SRC_DIR /intent-system + + +# 管理系统主程序 +COPY --from=builder $SRC_DIR/intent-system /usr/local/bin/intent-system +COPY --from=builder /etc/ssl/certs /etc/ssl/certs + + +ENV HOME_PATH /data + +VOLUME $HOME_PATH diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c01e836 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +#SHELL=/usr/bin/env bash + +CLEAN:= +BINS:= +DATE_TIME=`date +'%Y%m%d %H:%M:%S'` +COMMIT_ID=`git rev-parse --short HEAD` +MANAGER_DIR=${PWD} +CONSOLE_CODE=/tmp/intent-system-frontend + +build: + rm -f intent-system + go mod tidy && go build -ldflags "-s -w -X 'main.BuildTime=${DATE_TIME}' -X 'main.GitCommit=${COMMIT_ID}'" -o intent-system cmd/main.go +.PHONY: build +BINS+=intent-system + +nodejs: + curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - && sudo apt update && sudo apt install -y nodejs build-essential && sudo npm install -g yarn +.PHONY: nodejs + +console: + rm -rf ${CONSOLE_CODE} && git clone -b master https://git.your-enterprise.com/intent-system-frontend.git ${CONSOLE_CODE} + cd ${CONSOLE_CODE} && git log -2 && npm install && npm run build:prod +.PHONY: console + +docker-test: build + docker build --tag intent-system -f Dockerfile.test . +.PHONY: docker-test + +docker: + rm -f intent-system + docker build --tag intent-system -f Dockerfile . +.PHONY: docker + +# 检查环境变量 +env-%: + @ if [ "${${*}}" = "" ]; then \ + echo "Environment variable $* not set"; \ + exit 1; \ + fi + +db2go: + go install github.com/civet148/db2go@latest +.PHONY: db2go + +models: + cd pkg/dal/db2go && ./gen_models.sh + +clean: + rm -rf $(CLEAN) $(BINS) +.PHONY: clean diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..a9d333a --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,194 @@ +package main + +import ( + "fmt" + "intent-system/pkg/api" + "intent-system/pkg/config" + "intent-system/pkg/itypes" + "intent-system/pkg/services" + "os" + "os/signal" + + "github.com/civet148/log" + "github.com/urfave/cli/v2" +) + +const ( + Version = "v0.5.11" + ProgramName = "intent-system" +) + +var ( + BuildTime = "2024-05-11" + GitCommit = "" +) + +const ( + CMD_NAME_RUN = "run" + CMD_NAME_START = "start" +) + +const ( + CMD_FLAG_NAME_DSN = "dsn" + CMD_FLAG_NAME_POSTGRESQL = "pg" + CMD_FLAG_NAME_DEBUG = "debug" + CMD_FLAG_NAME_STATIC = "static" + CMD_FLAG_NAME_DOMAIN = "domain" + CMD_FLAG_NAME_IMAGE_PATH = "image-path" + CMD_FLAG_NAME_IMAGE_PREFIX = "image-prefix" + CMD_FLAG_NAME_GATEWAY_URL = "gateway-url" + CMD_FLAG_NAME_GATEWAY_KEY = "gateway-key" + CMD_FLAG_NAME_GATEWAY_SECRET = "gateway-secret" + CMD_FLAG_NAME_SUB_CRON = "sub-cron" +) + +var manager api.ManagerApi + +func init() { +} + +func grace() { + //capture signal of Ctrl+C and gracefully exit + sigChannel := make(chan os.Signal, 1) + signal.Notify(sigChannel, os.Interrupt) + go func() { + for { + select { + case s := <-sigChannel: + { + if s != nil && s == os.Interrupt { + fmt.Printf("Ctrl+C signal captured, program exiting...\n") + if manager != nil { + manager.Close() + } + close(sigChannel) + os.Exit(0) + } + } + } + } + }() +} + +func main() { + + grace() + + local := []*cli.Command{ + runCmd, + } + app := &cli.App{ + Name: ProgramName, + Version: fmt.Sprintf("%s %s commit %s", Version, BuildTime, GitCommit), + Flags: []cli.Flag{}, + Commands: local, + Action: nil, + } + if err := app.Run(os.Args); err != nil { + log.Errorf("exit in error %s", err) + os.Exit(1) + return + } +} + +var runCmd = &cli.Command{ + Name: CMD_NAME_RUN, + Usage: "run as a web service", + ArgsUsage: "[listen address]", + Aliases: []string{CMD_NAME_START}, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: CMD_FLAG_NAME_DEBUG, + Usage: "open debug log mode", + }, + &cli.StringFlag{ + Name: CMD_FLAG_NAME_DSN, + Usage: "data source name of database", + Value: itypes.DEFAULT_DATA_SOURCE_NAME, + Aliases: []string{"n"}, + }, + //&cli.StringFlag{ + // Name: CMD_FLAG_NAME_STATIC, + // Usage: "frontend static path", + // Value: itypes.DefaultStaticHome, + //}, + &cli.StringFlag{ + Name: CMD_FLAG_NAME_IMAGE_PATH, + Usage: "image saving path", + Value: itypes.DefaultImagesHome, + Aliases: []string{"i"}, + }, + &cli.StringFlag{ + Name: CMD_FLAG_NAME_DOMAIN, + Usage: "domain url", + Required: true, + Aliases: []string{"d"}, + }, + &cli.StringFlag{ + Name: CMD_FLAG_NAME_IMAGE_PREFIX, + Usage: "image url prefix", + Value: itypes.DEFAULT_IMAGE_PREFIX, + Aliases: []string{"p"}, + }, + &cli.StringFlag{ + Name: CMD_FLAG_NAME_GATEWAY_URL, + Usage: "sdk gateway url", + Aliases: []string{"g"}, + Required: true, + }, + &cli.StringFlag{ + Name: CMD_FLAG_NAME_GATEWAY_KEY, + Usage: "sdk gateway access key", + Aliases: []string{"k"}, + Required: true, + }, &cli.StringFlag{ + Name: CMD_FLAG_NAME_GATEWAY_SECRET, + Usage: "sdk gateway access secret", + Aliases: []string{"s"}, + Required: true, + }, + &cli.StringFlag{ + Name: CMD_FLAG_NAME_POSTGRESQL, + Usage: "Postgresql connection string for news sync", + Required: true, + }, + + &cli.StringFlag{ + Name: CMD_FLAG_NAME_SUB_CRON, + Usage: "cron task for email subscription", + Value: itypes.DEFAULT_SUB_CRON_EMAIL_PUSH, + }, + }, + Action: func(cctx *cli.Context) error { + cfg := &config.Config{ + Version: Version, + HttpAddr: itypes.DEFAULT_HTTP_LISTEN_ADDR, + DSN: cctx.String(CMD_FLAG_NAME_DSN), + Debug: cctx.Bool(CMD_FLAG_NAME_DEBUG), + Domain: cctx.String(CMD_FLAG_NAME_DOMAIN), + Static: cctx.String(CMD_FLAG_NAME_STATIC), + ImagePath: cctx.String(CMD_FLAG_NAME_IMAGE_PATH), + ImagePrefix: cctx.String(CMD_FLAG_NAME_IMAGE_PREFIX), + GatewayUrl: cctx.String(CMD_FLAG_NAME_GATEWAY_URL), + GatewayKey: cctx.String(CMD_FLAG_NAME_GATEWAY_KEY), + GatewaySecret: cctx.String(CMD_FLAG_NAME_GATEWAY_SECRET), + Postgresql: cctx.String(CMD_FLAG_NAME_POSTGRESQL), + SubCron: cctx.String(CMD_FLAG_NAME_SUB_CRON), + } + + cfg.Version = Version + if cfg.Debug { + log.SetLevel("debug") + } + log.Json("configuration", cfg) + if cctx.Args().First() != "" { + cfg.HttpAddr = cctx.Args().First() + } + if err := cfg.Save(); err != nil { + return err + } + //start up as a web server + manager = services.NewManager(cfg) + return manager.Run() + }, +} diff --git a/deploy/init.sql b/deploy/init.sql new file mode 100644 index 0000000..7e5d579 --- /dev/null +++ b/deploy/init.sql @@ -0,0 +1,10 @@ +USE `intent-system`; + +insert into `tag` (`name`, `name_cn`, `is_inherent`, `is_deleted`) values('#AI','#人工智能','1','0'); +insert into `tag` (`name`, `name_cn`, `is_inherent`, `is_deleted`) values('#Blockchain','#区块链','1','0'); + +INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] server', 'smtp_server', 'mail.jellydropsllc.com', '', '0'); +INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] port', 'smtp_port', '465', '', '0'); +INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] email address', 'smtp_name', 'it@jellydropsllc.com', '', '0'); +INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] auth code', 'auth_code', 'Z[yj4ri1tWRM', '', '0'); +INSERT INTO `dictionary` (`name`, `config_key`, `config_value`, `remark`, `deleted`) VALUES ('[SMTP] send name', 'send_name', 'Jelly AI', '', '0'); diff --git a/deploy/intent-system.sql b/deploy/intent-system.sql new file mode 100644 index 0000000..93cd62a --- /dev/null +++ b/deploy/intent-system.sql @@ -0,0 +1,409 @@ +/* +SQLyog Trial v13.1.8 (64 bit) +MySQL - 8.0.23 : Database - intent-system +********************************************************************* +*/ + +/*!40101 SET NAMES utf8 */; + +/*!40101 SET SQL_MODE=''*/; + +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +CREATE DATABASE /*!32312 IF NOT EXISTS*/`intent-system` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; + +USE `intent-system`; + +/*Table structure for table `casbin_rule` */ + +CREATE TABLE `casbin_rule` ( + `p_type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', + `v0` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', + `v1` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', + `v2` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', + `v3` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', + `v4` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', + `v5` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', + KEY `IDX_casbin_rule_v0` (`v0`) USING BTREE, + KEY `IDX_casbin_rule_v1` (`v1`) USING BTREE, + KEY `IDX_casbin_rule_v2` (`v2`) USING BTREE, + KEY `IDX_casbin_rule_v3` (`v3`) USING BTREE, + KEY `IDX_casbin_rule_v4` (`v4`) USING BTREE, + KEY `IDX_casbin_rule_v5` (`v5`) USING BTREE, + KEY `IDX_casbin_rule_p_type` (`p_type`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; + +/*Table structure for table `customer` */ + +CREATE TABLE `customer` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID(自增)', + `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录名称', + `user_alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '账户别名', + `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录密码(MD5+SALT)', + `first_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '姓', + `last_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名', + `title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '职称', + `company` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '公司名称', + `salt` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'MD5加密盐', + `phone_number` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '联系手机号', + `is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为超级管理员(0=普通账户 1=超级管理员)', + `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '邮箱地址', + `address` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '家庭住址/公司地址', + `remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已冻结(1=已启用 2=已冻结)', + `is_subscribed` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已订阅(0=未订阅 1=已订阅)', + `login_ip` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近登录IP', + `login_time` bigint NOT NULL DEFAULT '0' COMMENT '最近登录时间', + `create_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人', + `edit_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近编辑人', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `UNIQ_USER_NAME` (`user_name`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='客户信息表'; + +/*Table structure for table `dictionary` */ + +CREATE TABLE `dictionary` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称', + `config_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'KEY', + `config_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'VALUE', + `remark` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `key` (`config_key`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +/*Table structure for table `invite_code` */ + +CREATE TABLE `invite_code` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `user_id` int NOT NULL COMMENT '注册用户ID', + `user_acc` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '注册账户', + `random_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '认证码(5位字母和数字组合)', + `link_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '链接URL(保留字段)', + `state` tinyint(1) NOT NULL COMMENT '状态(1=等待校验 2=已校验)', + `expire_time` bigint NOT NULL DEFAULT '0' COMMENT '过期时间(UNIX时间戳)', + `action_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '操作类型(0=注册 1=重置密码)', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `UNIQ_INVITECODE` (`user_acc`,`random_code`,`deleted`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +/*Table structure for table `login` */ + +CREATE TABLE `login` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `login_type` tinyint NOT NULL DEFAULT '0' COMMENT '登录类型(0=管理用户 1=注册用户)', + `user_id` int NOT NULL COMMENT '登录用户ID', + `login_ip` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录IP', + `login_addr` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '登录地址', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='登录记录表'; + +/*Table structure for table `news` */ + +CREATE TABLE `news` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `org_id` bigint NOT NULL DEFAULT '0' COMMENT 'AI文章同步ID', + `spider_id` bigint NOT NULL DEFAULT '0' COMMENT '爬虫文章ID', + `tag` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文章标签(原始标签)', + `category` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分类', + `main_title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '主标题', + `sub_title` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '副标题', + `summary` text COLLATE utf8mb4_unicode_ci COMMENT '摘要', + `keywords` text COLLATE utf8mb4_unicode_ci COMMENT '文章关键词', + `seo_keywords` text COLLATE utf8mb4_unicode_ci COMMENT 'SEO关键词', + `tags` json DEFAULT NULL COMMENT '人工打标签(多选)', + `url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文章链接', + `image_url` text COLLATE utf8mb4_unicode_ci COMMENT '图片URL', + `content` longtext COLLATE utf8mb4_unicode_ci COMMENT '文章内容', + `is_hotspot` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否热门(0=否 1=是)', + `is_overwritten` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已被覆盖(0=否 1=是)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=否 1=是)', + `is_replicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为副本(0=否 1=是)', + `state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态(0=未发布订阅 1=已发布订阅 2=已推送)', + `language` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(zh-CN=中文 en=英文)', + `data_time` timestamp NOT NULL COMMENT '数据生成时间', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`), + KEY `INDEX_MAIN_TITLE` (`main_title`), + KEY `INDEX_SUB_TITLE` (`sub_title`), + KEY `INDEX_CREATED_TIME` (`created_time` DESC), + KEY `INDEX_TAG` (`tag`), + KEY `INDEX_HOTSPOT` (`is_hotspot`,`is_overwritten`,`is_deleted`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='新闻文章数据表(AI编辑)'; + +/*Table structure for table `news_draft` */ + +CREATE TABLE `news_draft` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `news_id` bigint NOT NULL DEFAULT '0' COMMENT '新闻ID(对应news表id字段)', + `org_id` bigint NOT NULL DEFAULT '0' COMMENT '源新闻ID(对应news表org_id字段)', + `category` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分类', + `main_title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '主标题', + `sub_title` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '副标题', + `summary` text COLLATE utf8mb4_unicode_ci COMMENT '摘要', + `keywords` text COLLATE utf8mb4_unicode_ci COMMENT '关键字(JSON数组)', + `seo_keywords` text COLLATE utf8mb4_unicode_ci COMMENT 'SEO关键字(JSON数组)', + `tags` json DEFAULT NULL COMMENT '标签(JSON数组)', + `image_url` text COLLATE utf8mb4_unicode_ci COMMENT '图片URL', + `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '文章内容', + `language` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(zh-CN=中文 en=英文)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=否 1=是)', + `is_replicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为副本(0=否 1=是)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`), + KEY `INDEX_CREATED_TIME` (`created_time` DESC), + KEY `INDEX_HOTSPOT` (`is_deleted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='草稿箱'; + +/*Table structure for table `news_spider` */ + +CREATE TABLE `news_spider` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `org_id` bigint NOT NULL DEFAULT '0' COMMENT '新闻同步ID', + `tag` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '文章标签', + `category` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '分类', + `main_title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '主标题', + `sub_title` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '副标题', + `summary` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '摘要', + `keywords` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '文章关键词', + `seo_keywords` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'SEO关键词', + `url` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文章链接', + `image_url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '图片URL', + `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '文章内容', + `is_hotspot` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否热门(0=否 1=是)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=否 1=是)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`), + KEY `INDEX_MAIN_TITLE` (`main_title`), + KEY `INDEX_SUB_TITLE` (`sub_title`), + KEY `INDEX_CREATED_TIME` (`created_time` DESC), + KEY `INDEX_TAG` (`tag`), + KEY `INDEX_UPDATED_TIME` (`updated_time` DESC), + KEY `INDEX_NEWS_ID` (`org_id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='新闻文章数据表(爬虫)'; + +/*Table structure for table `news_subscribe` */ + +CREATE TABLE `news_subscribe` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `news_id` bigint NOT NULL COMMENT '订阅推送新闻ID(对应news表id字段)', + `news_subject` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '邮件主题', + `news_url` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '订阅新闻推送URL', + `is_pushed` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已推送(0=否 1=是)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=否 1=是)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`), + KEY `INDEX_NEWS_ID` (`news_id`,`is_deleted`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +/*Table structure for table `oper_log` */ + +CREATE TABLE `oper_log` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `oper_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作用户', + `oper_type` tinyint(1) NOT NULL COMMENT '操作类型(1=首页 2=系统管理 3=存储管理 4=资源管理 5=告警中心)', + `oper_time` timestamp NOT NULL COMMENT '操作时间', + `oper_content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作内容', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +/*Table structure for table `privilege` */ + +CREATE TABLE `privilege` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `category` tinyint(1) NOT NULL DEFAULT '0' COMMENT '权限分类(保留字段)', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限名称', + `label` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限标签', + `path` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限访问路径', + `children` mediumtext COLLATE utf8mb4_unicode_ci COMMENT '子权限树', + `is_inherent` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否固有权限(0=否 1=是)', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限备注', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='角色-菜单关系表'; + +/*Table structure for table `question_answer` */ + +CREATE TABLE `question_answer` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `org_id` bigint NOT NULL DEFAULT '0' COMMENT 'Q&A源ID(同步ID)', + `question` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '问题', + `answer` mediumtext COLLATE utf8mb4_unicode_ci COMMENT '答案', + `state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已发布(0=草稿 1=已发布 2=已下架)', + `language` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(zh-CN=中文 en=英文)', + `is_overwritten` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已被覆盖(0=否 1=是)', + `is_replicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为副本(0=否 1=是)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `data_time` timestamp NOT NULL COMMENT '数据生成时间', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`), + KEY `INDEX_CREATED_TIME` (`created_time` DESC), + KEY `INDEX_UPDATED_TIME` (`updated_time` DESC) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +/*Table structure for table `question_draft` */ + +CREATE TABLE `question_draft` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `qa_id` bigint NOT NULL DEFAULT '0' COMMENT '源ID(对应question_answer表id字段)', + `org_id` bigint NOT NULL DEFAULT '0' COMMENT 'Q&A同步ID', + `question` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '问题', + `answer` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '答案', + `language` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(zh-CN=中文 en=英文)', + `is_overwritten` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已被覆盖(0=否 1=是)', + `is_replicate` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为副本(0=否 1=是)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`), + KEY `INDEX_CREATED_TIME` (`created_time` DESC), + KEY `INDEX_UPDATED_TIME` (`updated_time` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +/*Table structure for table `role` */ + +CREATE TABLE `role` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '角色ID(自增)', + `role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称', + `role_alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '角色别名', + `create_user` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人', + `edit_user` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近编辑人', + `remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注', + `is_inherent` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否固有角色(0=自定义角色 1=平台固有角色)', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `UNIQ_ROLE_NAME` (`role_name`) COMMENT '角色名称唯一约束' +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='角色信息表'; + +/*Table structure for table `run_config` */ + +CREATE TABLE `run_config` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT 'incr id', + `config_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'config name', + `config_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'config key', + `config_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'config value', + `remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'remark', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'is deleted(0=false 1=true)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `UNIQ_NAME_KEY` (`config_name`,`config_key`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='run config table'; + +/*Table structure for table `subscriber` */ + +CREATE TABLE `subscriber` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `customer_id` int NOT NULL DEFAULT '0' COMMENT '订阅者ID(对应customer标id字段,可为空)', + `email` varchar(256) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '订阅者邮箱', + `tags` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '订阅标签(主题)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已取消订阅(0=否 1=是)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`), + UNIQUE KEY `UNIQ_CUSTOMER_EMAIL` (`email`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +/*Table structure for table `tag` */ + +CREATE TABLE `tag` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标签名', + `name_cn` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '中文名', + `is_inherent` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为固有标签(0=否 1=是)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `extra_data` json DEFAULT NULL COMMENT '附带数据(JSON)', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +/*Table structure for table `template` */ + +CREATE TABLE `template` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `template_type` int NOT NULL COMMENT '模板类型(1=订阅欢迎邮件[英文] 2=订阅欢迎邮件[中午])', + `subject` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主题', + `content` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '内容', + `language` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '语言(英语=en 中文=zh-CN)', + `editor_user` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最后编辑人', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +/*Table structure for table `user` */ + +CREATE TABLE `user` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '用户ID(自增)', + `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录名称', + `user_alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '账户别名', + `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '登录密码(MD5+SALT)', + `salt` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'MD5加密盐', + `phone_number` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '联系手机号', + `is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否为超级管理员(0=普通账户 1=超级管理员)', + `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '邮箱地址', + `address` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '家庭住址/公司地址', + `remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '备注', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `state` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已冻结(1=已启用 2=已冻结)', + `login_ip` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近登录IP', + `login_time` bigint NOT NULL DEFAULT '0' COMMENT '最近登录时间', + `create_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人', + `edit_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近编辑人', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `UNIQ_USER_NAME` (`user_name`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='登录账户信息表'; + +/*Table structure for table `user_role` */ + +CREATE TABLE `user_role` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名', + `create_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人', + `edit_user` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '最近编辑人', + `deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已删除(0=未删除 1=已删除)', + `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `UNIQ_USER_NAME` (`user_name`) COMMENT '用户唯一约束' +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户角色关系表'; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; diff --git a/deploy/run.sh b/deploy/run.sh new file mode 100644 index 0000000..4d7d92b --- /dev/null +++ b/deploy/run.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +#镜像地址和版本(本地) +IMAGE_URL=intent-system:latest + +#容器名称 +CONTAINER_NAME=intent-system + +# 数据源(正式环境需修改成实际数据库配置) +DSN='mysql://root:123456@127.0.0.1:3306/intent-system?charset=utf8mb4' +PG='postgres://dong:Pg2#123321@14.17.80.241:5432/webdb?sslmode=disable&search_path=public' + +# 管理系统HTTP服务监听地址 +LISTEN_ADDR="0.0.0.0:8083" + +# 数据挂载目录 +DATA_DIR=/data/intent-system + +# 订阅邮件访问链接域名 +DOMAIN="http://103.39.218.177:3008/blog" + +# 图片存储域名+后缀 +#IMAGE_PREFIX=https://www.your-enterprise.com/images + +# 网关URL +GATEWAY_URL="ws://127.0.0.1:12345" + +# 网关访问KEY +GATEWAY_KEY="bAkYh0JVe2Kph0ot" + +# 网关访问密码 +GATEWAY_SECRET="1EWKBne2LCX0TJBXkrOWSzSDkzaQmoR3xuXBrc41JsdjorpM" + +# 订阅邮件定时任务 +SUB_CRON="0 0 * * *" + +#删除原来的容器 +docker rm -f "${CONTAINER_NAME}" + +docker run -p 8083:8083 -v ${DATA_DIR}:~/.intent-system --restart always --name "${CONTAINER_NAME}" -d "$IMAGE_URL" \ + intent-system run --debug -n "${DSN}" --pg "${PG}" -d "${DOMAIN}" -g "${GATEWAY_URL}" -k "${GATEWAY_KEY}" -s "${GATEWAY_SECRET}" --sub-cron "${SUB_CRON}" "$LISTEN_ADDR" + + diff --git a/deploy/stop.sh b/deploy/stop.sh new file mode 100644 index 0000000..ad8133c --- /dev/null +++ b/deploy/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker rm -f intent-system \ No newline at end of file diff --git a/deploy/数据库部署要求.md b/deploy/数据库部署要求.md new file mode 100644 index 0000000..d08871e --- /dev/null +++ b/deploy/数据库部署要求.md @@ -0,0 +1,165 @@ +# 部署步骤 + +## 1. 安装MySQL8 + +### 1.1 方案一 物理机安装MySQL8 + +- *操作系统* + +*Ubuntu20.04LTS* + +如果操作系统不是Ubuntu20则需要更新MySQL官方源信息 + +#### 1.1.1 apt安装MySQL + +```sh +$ sudo apt update && sudo apt install mysql-server + +``` +#### 1.1.2. 检查安装后是否启动成功 + +```sh +$ ps -ef | grep mysqld +mysql 65492 1 0 17:46 ? 00:00:00 /usr/sbin/mysqld --daemonize --pid-file=/run/mysqld/mysqld.pid +``` + +#### 1.1.3. 修改MySQL监听地址和端口 + +```sh +$ sudo netstat -alnt | grep 3306 +tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN + +# 编辑mysqld.cnf文件将bind-address对应的值由127.0.0.1改成0.0.0.0或局域网IP地址(端口视情况决定是否修改) +$ sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf + +# 重启MySQL服务 +$ sudo service mysql restart +``` + +#### 1.1.4. 创建oss用户和权限 + +```sh +# MySQL安装完会在/etc/mysql目录下有一个debian.cnf文件可用于本地登录并修改root密码或创建用户 +$ sudo cat /etc/mysql/debian.cnf + +[client] +host = localhost +user = debian-sys-maint +password = xgf1OdcBzRy0LaEP +socket = /var/run/mysqld/mysqld.sock + +# 本地登录MySQL,执行下面的命令行 +$ mysql -udebian-sys-maint -pxgf1OdcBzRy0LaEP mysql + +mysql> select host,user,plugin,authentication_string from user; ++-----------+------------------+-----------------------+-------------------------------------------+ +| host | user | plugin | authentication_string | ++-----------+------------------+-----------------------+-------------------------------------------+ +| localhost | root | auth_socket | | +| localhost | mysql.session | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | +| localhost | mysql.sys | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | +| localhost | debian-sys-maint | mysql_native_password | *22CC5F671040F19FF9FB1E5A9B94D2576C4A1A24 | +| % | node | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 | ++-----------+------------------+-----------------------+-------------------------------------------+ + +4 rows in set (0.00 sec) + +# 创建一个admin账户并允许远程登录, 密码是123456(生产环境请设置复杂密码) +mysql> create user 'admin'@'%' identified by '123456'; +Query OK, 0 rows affected (0.00 sec) + +# 赋予oss所有权限 +mysql> grant all on *.* to 'admin'@'%'; +mysql> flush privileges; + +# 修改root密码和口令加密方式并开启远程登录(视实际情况而定,如果无必要可以只修改密码不开启远程登录) +# host='%'表示开启远程访问,如果不开启就不要这个SQL字句 +mysql> update user set plugin='mysql_native_password', authentication_string='', host='%' where user='root'; +Query OK, 1 row affected (0.00 sec) +Rows matched: 1 Changed: 1 Warnings: 0 + +# 重置root账户登录密码并刷新权限 +mysql> alter user 'root'@'%' IDENTIFIED BY '123456'; #适用于8.x版本修改密码(设置不成功可能是需要复杂密码) +mysql> flush privileges; + +# 查看账户信息 +mysql> select host,user,plugin,authentication_string from user; ++-----------+------------------+-----------------------+-------------------------------------------+ +| host | user | plugin | authentication_string | ++-----------+------------------+-----------------------+-------------------------------------------+ +| % | root | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 | +| localhost | mysql.session | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | +| localhost | mysql.sys | mysql_native_password | *THISISNOTAVALIDPASSWORDTHATCANBEUSEDHERE | +| localhost | debian-sys-maint | mysql_native_password | *22CC5F671040F19FF9FB1E5A9B94D2576C4A1A24 | +| % | admin | mysql_native_password | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 | ++-----------+------------------+-----------------------+-------------------------------------------+ +5 rows in set (0.00 sec) + +``` + +### 1.2 方案二 docker安装MySQL8 + +```sh +# 创建本地数据库目录 +$ sudo mkdir -p /data/mysql/{mysql-files,conf,logs,data} + +# 启动容器(设置root初始密码为123456) +$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 --restart always \ + -e TZ=Asia/Shanghai \ + -v /data/mysql/mysql-files:/var/lib/mysql-files \ + -v /data/mysql/conf:/etc/mysql \ + -v /data/mysql/logs:/var/log/mysql \ + -v /data/mysql/data:/var/lib/mysql \ + --name mysql -d mysql:8.0.23 +``` + +```shell +# 登录mysql终端(手动输入初始密码123456登录MySQL控制台) +$ docker exec -it mysql mysql -uroot -p mysql + +Enter password: +Reading table information for completion of table and column names +You can turn off this feature to get a quicker startup with -A + +Welcome to the MySQL monitor. Commands end with ; or \g. +Your MySQL connection id is 8 +Server version: 8.0.23 MySQL Community Server - GPL + +Copyright (c) 2000, 2021, Oracle and/or its affiliates. + +Oracle is a registered trademark of Oracle Corporation and/or its +affiliates. Other names may be trademarks of their respective +owners. + +Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + +# 更改数据库root账户密码为123456并开启远程访问(密码可以自行修改成其他也可以保持原密码,主要是通过%符号开启root远程访问) +mysql> USE mysql; +mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; +mysql> FLUSH PRIVILEGES; +mysql> exit +``` + +## 2. 初始化数据表 + +将oss-manager.sql和init-data.sql文件上传到服务器/tmp目录 + +```shell script +# 服务器登录MySQL命令行终端执行sql文件 +mysql> source /path/to/intent-system.sql +``` + +## 3. 解除MySQL分组查询限制 + +```bash +# 打开mysqld.cnf文件并在[mysqld]选项范围内加一行下面的参数(如果sql_mode已存在则去掉ONLY_FULL_GROUP_BY) +$ sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf +sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION' + +# 重启mysql服务 +$ sudo service mysql restart +``` + +## 4. 更改服务器/容器时区 + + MySQL运行服务器或容器时区改为UTC+8 (北京时间) diff --git a/deps/github.com/anacrolix/torrent/.circleci/config.yml b/deps/github.com/anacrolix/torrent/.circleci/config.yml new file mode 100644 index 0000000..f25b011 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/.circleci/config.yml @@ -0,0 +1,64 @@ +version: 2 +jobs: + build: + machine: true + # This would be for if we didn't have machine: true. Could help with circleci local execute, which doesn't support VMs? + # docker: + # - image: cimg/go:1.13 + environment: + GO_BRANCH: release-branch.go1.16 + steps: + - run: echo $CIRCLE_WORKING_DIRECTORY + - run: echo $PWD + - run: echo $GOPATH + - run: echo 'export GOPATH=$HOME/go' >> $BASH_ENV + - run: echo 'export PATH="$GOPATH/bin:$PATH"' >> $BASH_ENV + - run: echo $GOPATH + - run: which go || sudo apt install golang-go + - run: go version + - run: | + cd /usr/local + sudo mkdir go.local + sudo chown `whoami` go.local + - restore_cache: + key: go-local- + - run: | + cd /usr/local + git clone git://github.com/golang/go go.local || true + cd go.local + git fetch + git checkout "$GO_BRANCH" + [[ -x bin/go && `git rev-parse HEAD` == `cat anacrolix.built` ]] && exit + cd src + ./make.bash || exit + git rev-parse HEAD > ../anacrolix.built + - save_cache: + paths: /usr/local/go.local + key: go-local-{{ checksum "/usr/local/go.local/anacrolix.built" }} + - run: echo 'export PATH="/usr/local/go.local/bin:$PATH"' >> $BASH_ENV + - run: go version + - checkout + - run: sudo apt-get update + - run: sudo apt install fuse pv + - restore_cache: + keys: + - go-pkg- + - restore_cache: + keys: + - go-cache- + - run: go get -d ./... + - run: go test -v -race ./... -count 2 + - run: go test -bench . ./... + - run: set +e; CGO_ENABLED=0 go test -v ./...; true + - run: GOARCH=386 go test ./... -count 2 -bench . || true + - run: go install github.com/anacrolix/godo@latest + - save_cache: + key: go-pkg-{{ checksum "go.mod" }} + paths: + - ~/go/pkg + - run: sudo modprobe fuse + - run: fs/test.sh + - save_cache: + key: go-cache-{{ .Revision }} + paths: + - ~/.cache/go-build diff --git a/deps/github.com/anacrolix/torrent/.deepsource.toml b/deps/github.com/anacrolix/torrent/.deepsource.toml new file mode 100644 index 0000000..e72f983 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/.deepsource.toml @@ -0,0 +1,14 @@ +version = 1 + +test_patterns = ["**/*_test.go"] + +[[analyzers]] +name = "go" +enabled = true + + [analyzers.meta] + import_root = "github.com/anacrolix/torrent" + +[[transformers]] +name = "gofmt" +enabled = true \ No newline at end of file diff --git a/deps/github.com/anacrolix/torrent/.github/actions/go-common/action.yml b/deps/github.com/anacrolix/torrent/.github/actions/go-common/action.yml new file mode 100644 index 0000000..2b374de --- /dev/null +++ b/deps/github.com/anacrolix/torrent/.github/actions/go-common/action.yml @@ -0,0 +1,61 @@ +name: 'Common Go' +description: 'Checks out, and handles Go setup and caching' +runs: + using: "composite" + steps: + - name: Set up Go + if: matrix.go-version != 'tip' + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - uses: actions/cache@v2 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + # The OS defines the directories to use, then this is specific to go. The go version could + # affect the dependencies. The job can affect what is actually downloaded, and provides + # collision resistance. Finally, the hash of the go.sum files ensures a new cache is created + # when the dependencies change. Note if this were just a mod cache, we might do this based + # on time or something. + key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ github.job }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ matrix.go-version }}-${{ github.job }}- + ${{ runner.os }}-go-${{ matrix.go-version }}- + ${{ runner.os }}-go- + - run: | + echo GOTIP_REVISION="`git ls-remote https://github.com/golang/go refs/heads/master | cut -f1`" >> "$GITHUB_ENV" + echo GOTIP_PATH="$HOME/gotip" >> "$GITHUB_ENV" + if: matrix.go-version == 'tip' + shell: bash + - uses: actions/cache@v2 + if: matrix.go-version == 'tip' + with: + path: | + ${{ env.GOTIP_PATH }} + # The build varies by OS (and arch, but I haven't bothered to add that yet.) We always want + # the latest snapshot that works for us, the revision is only used to store differentiate + # builds. + key: gotip-ls-remote-${{ runner.os }}-${{ env.GOTIP_REVISION }} + restore-keys: | + gotip-ls-remote-${{ runner.os }}-${{ env.GOTIP_REVISION }} + gotip-ls-remote-${{ runner.os }}- + gotip-env-home-${{ runner.os }}- + gotip-${{ runner.os }}- + - name: Install gotip + if: matrix.go-version == 'tip' + run: | + git clone --depth=1 https://github.com/golang/go "$GOTIP_PATH" || true + cd "$GOTIP_PATH" + git pull + echo "GOROOT=$GOTIP_PATH" >> "$GITHUB_ENV" + echo "$(go env GOPATH)/bin:$PATH" >> "$GITHUB_PATH" + echo "$GOTIP_PATH/bin:$PATH" >> "$GITHUB_PATH" + echo "anacrolix.built:" $(cat anacrolix.built) + [[ -x bin/go && `git rev-parse HEAD` == `cat anacrolix.built` ]] && exit + cd src + ./make.bash || exit + git rev-parse HEAD > ../anacrolix.built + env + shell: bash + diff --git a/deps/github.com/anacrolix/torrent/.github/workflows/codeql-analysis.yml b/deps/github.com/anacrolix/torrent/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..31f4653 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/.github/workflows/codeql-analysis.yml @@ -0,0 +1,54 @@ +name: "CodeQL" + +on: + push: + branches: [master, ] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 10 * * 4' + +jobs: + analyse: + name: Analyse + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/deps/github.com/anacrolix/torrent/.github/workflows/go.yml b/deps/github.com/anacrolix/torrent/.github/workflows/go.yml new file mode 100644 index 0000000..e95f826 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/.github/workflows/go.yml @@ -0,0 +1,115 @@ +name: Go + +on: [push, pull_request] + +jobs: + + test: + timeout-minutes: 10 + runs-on: ${{ matrix.os }} + strategy: + matrix: + go-version: [ '1.21' ] + os: [windows-latest, macos-latest, ubuntu-latest] + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/go-common + - run: go test -race -count 2 $(go list ./... | grep -v /fs) + - run: go test -race -count 2 ./fs/... + if: ${{ ! contains(matrix.os, 'windows') }} + + test-benchmarks: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ '1.21' ] + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/go-common + - run: go test -race -run @ -bench . -benchtime 2x ./... + + bench: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ '1.21' ] + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/go-common + - run: go test -run @ -bench . ./... + + test-386: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ '1.21' ] + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/go-common + - run: GOARCH=386 go test ./... + - run: GOARCH=386 go test ./... -run @ -bench . + + build-wasm: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ '1.21' ] + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/go-common + - name: Some packages compile for WebAssembly + run: GOOS=js GOARCH=wasm go build . ./storage ./tracker/... + + torrentfs-linux: + timeout-minutes: 5 + runs-on: ${{ matrix.os }} + strategy: + matrix: + go-version: [ '1.21' ] + os: [ubuntu-latest] + fail-fast: false + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/go-common + + - name: Install godo + run: | + # Need master for cross-compiling fix + go install -v -x github.com/anacrolix/godo@master + echo $PATH + + - name: Apt packages + run: sudo apt install pv fuse + + - name: torrentfs end-to-end test + # Test on 386 for atomic alignment and other bad 64-bit assumptions + run: GOARCH=386 fs/test.sh + +# Github broke FUSE on MacOS, I'm not sure what the state is. + +# torrentfs-macos: +# timeout-minutes: 15 +# runs-on: ${{ matrix.os }} +# strategy: +# matrix: +# go-version: [ '1.20' ] +# os: [macos-latest] +# fail-fast: false +# steps: +# - uses: actions/checkout@v2 +# - uses: ./.github/actions/go-common +# +# - run: brew install macfuse pv md5sha1sum bash +# +# - name: Install godo +# run: go install -v github.com/anacrolix/godo@master +# +# - name: torrentfs end-to-end test +# run: fs/test.sh +# # Pretty sure macos on GitHub CI has issues with the fuse driver now. +# continue-on-error: true diff --git a/deps/github.com/anacrolix/torrent/.github/workflows/linter.yml b/deps/github.com/anacrolix/torrent/.github/workflows/linter.yml new file mode 100644 index 0000000..b5342ac --- /dev/null +++ b/deps/github.com/anacrolix/torrent/.github/workflows/linter.yml @@ -0,0 +1,37 @@ +name: GolangCI-Lint + +on: + push: + branches: [ '!master' ] + pull_request: + branches: [ '!master' ] + +jobs: + golint: + name: Lint + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + - uses: actions/checkout@v2 + - uses: golangci/golangci-lint-action@v2 + with: + version: latest + + # Optional: working directory, useful for monorepos + # working-directory: somedir + + # Optional: golangci-lint command line arguments. + args: -D errcheck,unused,structcheck,deadcode + + # Optional: show only new issues if it's a pull request. The default value is `false`. + only-new-issues: true + + # Optional: if set to true then the action will use pre-installed Go. + skip-go-installation: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true diff --git a/deps/github.com/anacrolix/torrent/.gitignore b/deps/github.com/anacrolix/torrent/.gitignore new file mode 100644 index 0000000..0c15585 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/.gitignore @@ -0,0 +1,5 @@ +.idea +*-run.gob +.envrc* +.DS_Store +go.work* \ No newline at end of file diff --git a/deps/github.com/anacrolix/torrent/.golangci.yml b/deps/github.com/anacrolix/torrent/.golangci.yml new file mode 100644 index 0000000..eb429fc --- /dev/null +++ b/deps/github.com/anacrolix/torrent/.golangci.yml @@ -0,0 +1,8 @@ +linters-settings: + staticcheck: + go: "1.16" + checks: ["all", "-U1000"] + + govet: + disable: + - composites diff --git a/deps/github.com/anacrolix/torrent/LICENSE b/deps/github.com/anacrolix/torrent/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/deps/github.com/anacrolix/torrent/NOTES.md b/deps/github.com/anacrolix/torrent/NOTES.md new file mode 100644 index 0000000..80da84b --- /dev/null +++ b/deps/github.com/anacrolix/torrent/NOTES.md @@ -0,0 +1,32 @@ +### Literature + +* [arvid on writing a fast piece picker](https://blog.libtorrent.org/2011/11/writing-a-fast-piece-picker/) + + Uses C++ for examples. + +* [On Piece Selection for Streaming BitTorrent](https://www.diva-portal.org/smash/get/diva2:835742/FULLTEXT01.pdf) + + Some simulations by some Swedes on piece selection. + +* [A South American paper on peer-selection strategies for uploading](https://arxiv.org/pdf/1402.2187.pdf) + + Has some useful overviews of piece-selection. + +### Hole-punching + +Holepunching is tracked in Torrent, rather than in Client because if we send a rendezvous message, and subsequently receive a connect message, we do not know if a peer sent a rendezvous message to our relay and we're receiving the connect message for their rendezvous or ours. Relays are not required to respond to rendezvous, so we can't enforce a timeout. If we don't know if who sent the rendezvous that triggered a connect, then we don't know what infohash to use in the handshake. Once we send a rendezvous, and never receive a reply, we would have to always perform handshakes with our original infohash, or always copy the infohash the remote sends. Handling connects by always being the passive side in the handshake won't work since the other side might use the same behaviour and neither will initiate. + +If we only perform rendezvous through relays for the same torrent as the relay, then all the handshake can be done actively for all connect messages. All connect messages received from a peer can only be for the same torrent for which we are connected to the peer. + +In 2006, approximately 70% of clients were behind NAT (https://web.archive.org/web/20100724011252/http://illuminati.coralcdn.org/stats/). According to https://fosdem.org/2023/schedule/event/network_hole_punching_in_the_wild/, hole punching (in libp2p) 70% of NAT can be defeated by relay mechanisms. + +If either or both peers in a potential peer do not have NAT, or are full cone NAT, then NAT doesn't matter at least for BitTorrent, as both parties are trying to connect to each other and connections will always work in one direction. + +The chance that 2 peers can connect to each other would be 1-(badnat)^2, and 1-unrelayable*(badnat)^2 where unrelayable is the chance they can't work even with a relay, and badnat is the chance a peer has a bad NAT (not full cone). For example if unrelayable is 0.3 per the libp2p study, and badnat is 0.5 (i made this up), 92.5% of peers can connect with each other if they use "relay mechanisms", and 75% if they don't. as long as any peers in the swarm are not badnat, they can relay those that are, and and act as super nodes for peers that can't or don't implement hole punching. + +The DHT is a bit different: you can't be an active node if you are a badnat, but you can still query the network to get what you need, you just don't contribute to it. It also doesn't matter what the swarm looks like for a given torrent on the DHT, because you don't have to be in the swarm to host its data. all that matters is that there are some peers that aren't badnat that are in the DHT, of which there are millions (for BitTorrent). + +- https://blog.ipfs.tech/2022-01-20-libp2p-hole-punching/ +- https://www.bittorrent.org/beps/bep_0055.html +- https://github.com/anacrolix/torrent/issues/685 +- https://stackoverflow.com/questions/38786438/libutp-%C2%B5tp-and-nat-traversal-udp-hole-punching diff --git a/deps/github.com/anacrolix/torrent/README.md b/deps/github.com/anacrolix/torrent/README.md new file mode 100644 index 0000000..399b936 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/README.md @@ -0,0 +1,102 @@ +# torrent + +[![PkgGoDev](https://pkg.go.dev/badge/github.com/anacrolix/torrent)](https://pkg.go.dev/github.com/anacrolix/torrent) + +This repository implements BitTorrent-related packages and command-line utilities in Go. The emphasis is on use as a library from other projects. It's been used 24/7 in production by downstream services since late 2014. The implementation was specifically created to explore Go's concurrency capabilities, and to include the ability to stream data directly from the BitTorrent network. To this end it [supports seeking, readaheads and other features](https://godoc.org/github.com/anacrolix/torrent#Reader) exposing torrents and their files with the various Go idiomatic `io` package interfaces. This is also demonstrated through [torrentfs](#torrentfs). + +There is [support for protocol encryption, DHT, PEX, uTP, and various extensions](https://godoc.org/github.com/anacrolix/torrent). There are [several data storage backends provided](https://godoc.org/github.com/anacrolix/torrent/storage): blob, file, bolt, mmap, and sqlite, to name a few. You can [write your own](https://godoc.org/github.com/anacrolix/torrent/storage#ClientImpl) to store data for example on S3, or in a database. + +Some noteworthy package dependencies that can be used for other purposes include: + + * [go-libutp](https://github.com/anacrolix/go-libutp) + * [dht](https://github.com/anacrolix/dht) + * [bencode](https://godoc.org/github.com/anacrolix/torrent/bencode) + * [tracker](https://godoc.org/github.com/anacrolix/torrent/tracker) + +## Installation + +Install the library package with `go get github.com/anacrolix/torrent`, or the provided cmds with `go install github.com/anacrolix/torrent/cmd/...@latest`. + +## Library examples + +There are some small [examples](https://godoc.org/github.com/anacrolix/torrent#pkg-examples) in the package documentation. + +## Mentions + + * [@anacrolix](https://github.com/anacrolix) is interviewed about this repo in [Console 32](https://console.substack.com/p/console-32). + +### Downstream projects + +There are several web-frontends, sites, Android clients, storage backends and supporting services among the known public projects: + + * [cove](https://coveapp.info): Personal torrent browser with streaming, DHT search, video transcoding and casting. + * [confluence](https://github.com/anacrolix/confluence): torrent client as a HTTP service + * [Gopeed](https://github.com/GopeedLab/gopeed): Gopeed (full name Go Speed), a high-speed downloader developed by Golang + Flutter, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. + * [Erigon](https://github.com/ledgerwatch/erigon): an implementation of Ethereum (execution layer with embeddable consensus layer), on the efficiency frontier. + * [exatorrent](https://github.com/varbhat/exatorrent): Elegant self-hostable torrent client + * [bitmagnet](https://github.com/bitmagnet-io/bitmagnet): A self-hosted BitTorrent indexer, DHT crawler, content classifier and torrent search engine with web UI, GraphQL API and Servarr stack integration. + * [TorrServer](https://github.com/YouROK/TorrServer): Torrent streaming server over http + * [distribyted](https://github.com/distribyted/distribyted): Distribyted is an alternative torrent client. It can expose torrent files as a standard FUSE, webDAV or HTTP endpoint and download them on demand, allowing random reads using a fixed amount of disk space. + * [Simple Torrent](https://github.com/boypt/simple-torrent): self-hosted HTTP remote torrent client + * [autobrr](https://github.com/autobrr/autobrr): autobrr redefines download automation for torrents and Usenet, drawing inspiration from tools like trackarr, autodl-irssi, and flexget. + * [mabel](https://github.com/smmr-software/mabel): Fancy BitTorrent client for the terminal + * [webtor.io](https://webtor.io/): free cloud BitTorrent-client + * [Android Torrent Client](https://gitlab.com/axet/android-torrent-client): Android torrent client + * [libtorrent](https://gitlab.com/axet/libtorrent): gomobile wrapper + * [Go-PeersToHTTP](https://github.com/WinPooh32/peerstohttp): Simple torrent proxy to http stream controlled over REST-like api + * [CortexFoundation/torrentfs](https://github.com/CortexFoundation/torrentfs): Independent HTTP service for file seeding and P2P file system of cortex full node + * [goTorrent](https://github.com/deranjer/goTorrent): torrenting server with a React web frontend + * [Go Peerflix](https://github.com/Sioro-Neoku/go-peerflix): Start watching the movie while your torrent is still downloading! + * [hTorrent](https://github.com/pojntfx/htorrent): HTTP to BitTorrent gateway with seeking support. + * [Remote-Torrent](https://github.com/BruceWangNo1/remote-torrent): Download Remotely and Retrieve Files Over HTTP + * [Trickl](https://github.com/arranlomas/Trickl): torrent client for android + * [ANT-Downloader](https://github.com/anatasluo/ant): ANT Downloader is a BitTorrent Client developed by golang, angular 7, and electron + * [Elementum](http://elementum.surge.sh/) (up to version 0.0.71) + +## Help + +Communication about the project is primarily through [Discussions](https://github.com/anacrolix/torrent/discussions) and the [issue tracker](https://github.com/anacrolix/torrent/issues). + +## Command packages + +Here I'll describe what some of the packages in `./cmd` do. See [installation](#installation) to make them available. + +### `torrent` + +#### `torrent download` + +Downloads torrents from the command-line. + + $ torrent download 'magnet:?xt=urn:btih:KRWPCX3SJUM4IMM4YF5RPHL6ANPYTQPU' + ... lots of jibber jabber ... + downloading "ubuntu-14.04.2-desktop-amd64.iso": 1.0 GB/1.0 GB, 1989/1992 pieces completed (1 partial) + 2015/04/01 02:08:20 main.go:137: downloaded ALL the torrents + $ md5sum ubuntu-14.04.2-desktop-amd64.iso + 1b305d585b1918f297164add46784116 ubuntu-14.04.2-desktop-amd64.iso + $ echo such amaze + wow + +#### `torrent metainfo magnet` + +Creates a magnet link from a torrent file. Note the extracted trackers, display name, and info hash. + + $ torrent metainfo testdata/debian-10.8.0-amd64-netinst.iso.torrent magnet + magnet:?xt=urn:btih:4090c3c2a394a49974dfbbf2ce7ad0db3cdeddd7&dn=debian-10.8.0-amd64-netinst.iso&tr=http%3A%2F%2Fbttracker.debian.org%3A6969%2Fannounce + +See `torrent metainfo --help` for other metainfo related commands. + +### `torrentfs` + +torrentfs mounts a FUSE filesystem at `-mountDir`. The contents are the torrents described by the torrent files and magnet links at `-metainfoDir`. Data for read requests is fetched only as required from the torrent network, and stored at `-downloadDir`. + + $ mkdir mnt torrents + $ torrentfs -mountDir=mnt -metainfoDir=torrents & + $ cd torrents + $ wget http://releases.ubuntu.com/14.04.2/ubuntu-14.04.2-desktop-amd64.iso.torrent + $ cd .. + $ ls mnt + ubuntu-14.04.2-desktop-amd64.iso + $ pv mnt/ubuntu-14.04.2-desktop-amd64.iso | md5sum + 996MB 0:04:40 [3.55MB/s] [========================================>] 100% + 1b305d585b1918f297164add46784116 - + diff --git a/deps/github.com/anacrolix/torrent/SECURITY.md b/deps/github.com/anacrolix/torrent/SECURITY.md new file mode 100644 index 0000000..688b1cf --- /dev/null +++ b/deps/github.com/anacrolix/torrent/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +The two most recent minor releases are supported, with older versions receiving updates subject to contributor discretion. +Please also report issues in master, but there are no guarantees of stability there. + +## Reporting a Vulnerability + +All vulnerability reports are welcomed. Use your discretion in providing information to Discussions, an Issue, or message a maintainer directly. +For a non-trivial issue, you should receive a response within a week, but more than likely a day or two. diff --git a/deps/github.com/anacrolix/torrent/TODO b/deps/github.com/anacrolix/torrent/TODO new file mode 100644 index 0000000..02f983a --- /dev/null +++ b/deps/github.com/anacrolix/torrent/TODO @@ -0,0 +1,5 @@ + * Make use of sparse file regions in download data for faster hashing. This is available as whence 3 and 4 on some OSs? + * When we're choked and interested, are we not interested if there's no longer anything that we want? + * dht: Randomize triedAddrs bloom filter to allow different Addr sets on each Announce. + * data/blob: Deleting incomplete data triggers io.ErrUnexpectedEOF that isn't recovered from. + * Handle wanted pieces more efficiently, it's slow in in fillRequests, since the prioritization system was changed. diff --git a/deps/github.com/anacrolix/torrent/analysis/peer-upload-order.go b/deps/github.com/anacrolix/torrent/analysis/peer-upload-order.go new file mode 100644 index 0000000..8713804 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/analysis/peer-upload-order.go @@ -0,0 +1,103 @@ +package analysis + +import ( + "fmt" + "log" + "sync" + + "github.com/elliotchance/orderedmap" + + "github.com/anacrolix/torrent" + pp "github.com/anacrolix/torrent/peer_protocol" +) + +type peerData struct { + requested *orderedmap.OrderedMap + haveDeleted map[torrent.Request]bool +} + +// Tracks the order that peers upload requests that we've sent them. +type PeerUploadOrder struct { + mu sync.Mutex + peers map[*torrent.Peer]*peerData +} + +func (me *PeerUploadOrder) Init() { + me.peers = make(map[*torrent.Peer]*peerData) +} + +func (me *PeerUploadOrder) onNewPeer(p *torrent.Peer) { + me.mu.Lock() + defer me.mu.Unlock() + if _, ok := me.peers[p]; ok { + panic("already have peer") + } + me.peers[p] = &peerData{ + requested: orderedmap.NewOrderedMap(), + haveDeleted: make(map[torrent.Request]bool), + } +} + +func (me *PeerUploadOrder) onSentRequest(event torrent.PeerRequestEvent) { + me.mu.Lock() + defer me.mu.Unlock() + if !me.peers[event.Peer].requested.Set(event.Request, nil) { + panic("duplicate request sent") + } +} + +func (me *PeerUploadOrder) Install(cbs *torrent.Callbacks) { + cbs.NewPeer = append(cbs.NewPeer, me.onNewPeer) + cbs.SentRequest = append(cbs.SentRequest, me.onSentRequest) + cbs.ReceivedRequested = append(cbs.ReceivedRequested, me.onReceivedRequested) + cbs.DeletedRequest = append(cbs.DeletedRequest, me.deletedRequest) +} + +func (me *PeerUploadOrder) report(desc string, req torrent.Request, peer *torrent.Peer) { + peerConn, ok := peer.TryAsPeerConn() + var peerId *torrent.PeerID + if ok { + peerId = &peerConn.PeerID + } + log.Printf("%s: %v, %v", desc, req, peerId) +} + +func (me *PeerUploadOrder) onReceivedRequested(event torrent.PeerMessageEvent) { + req := torrent.Request{ + event.Message.Index, + torrent.ChunkSpec{ + Begin: event.Message.Begin, + Length: pp.Integer(len(event.Message.Piece)), + }, + } + makeLogMsg := func(desc string) string { + peerConn, ok := event.Peer.TryAsPeerConn() + var peerId *torrent.PeerID + if ok { + peerId = &peerConn.PeerID + } + return fmt.Sprintf("%s: %q, %v", desc, peerId, req) + } + me.mu.Lock() + defer me.mu.Unlock() + peerData := me.peers[event.Peer] + if peerData.requested.Front().Key.(torrent.Request) == req { + log.Print(makeLogMsg("got next requested piece")) + } else if _, ok := peerData.requested.Get(req); ok { + log.Print(makeLogMsg(fmt.Sprintf( + "got requested piece but not next (previous delete=%v)", + peerData.haveDeleted[req]))) + } else { + panic(makeLogMsg("got unrequested piece")) + } +} + +func (me *PeerUploadOrder) deletedRequest(event torrent.PeerRequestEvent) { + me.mu.Lock() + defer me.mu.Unlock() + peerData := me.peers[event.Peer] + if !peerData.requested.Delete(event.Request) { + panic("nothing to delete") + } + peerData.haveDeleted[event.Request] = true +} diff --git a/deps/github.com/anacrolix/torrent/bad_storage.go b/deps/github.com/anacrolix/torrent/bad_storage.go new file mode 100644 index 0000000..fc15beb --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bad_storage.go @@ -0,0 +1,56 @@ +package torrent + +import ( + "errors" + "math/rand" + "strings" + + "github.com/anacrolix/torrent/internal/testutil" + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" +) + +type badStorage struct{} + +var _ storage.ClientImpl = badStorage{} + +func (bs badStorage) OpenTorrent(*metainfo.Info, metainfo.Hash) (storage.TorrentImpl, error) { + return storage.TorrentImpl{ + Piece: bs.Piece, + }, nil +} + +func (bs badStorage) Piece(p metainfo.Piece) storage.PieceImpl { + return badStoragePiece{p} +} + +type badStoragePiece struct { + p metainfo.Piece +} + +var _ storage.PieceImpl = badStoragePiece{} + +func (p badStoragePiece) WriteAt(b []byte, off int64) (int, error) { + return 0, nil +} + +func (p badStoragePiece) Completion() storage.Completion { + return storage.Completion{Complete: true, Ok: true} +} + +func (p badStoragePiece) MarkComplete() error { + return errors.New("psyyyyyyyche") +} + +func (p badStoragePiece) MarkNotComplete() error { + return errors.New("psyyyyyyyche") +} + +func (p badStoragePiece) randomlyTruncatedDataString() string { + return testutil.GreetingFileContents[:rand.Intn(14)] +} + +func (p badStoragePiece) ReadAt(b []byte, off int64) (n int, err error) { + r := strings.NewReader(p.randomlyTruncatedDataString()) + return r.ReadAt(b, off+p.p.Offset()) +} diff --git a/deps/github.com/anacrolix/torrent/bencode/README.md b/deps/github.com/anacrolix/torrent/bencode/README.md new file mode 100644 index 0000000..4dbc67b --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/README.md @@ -0,0 +1,38 @@ +Bencode encoding/decoding sub package. Uses similar API design to Go's json package. + +## Install + +```sh +go get github.com/anacrolix/torrent +``` + +## Usage + +```go +package demo + +import ( + bencode "github.com/anacrolix/torrent/bencode" +) + +type Message struct { + Query string `json:"q,omitempty" bencode:"q,omitempty"` +} + +var v Message + +func main(){ + // encode + data, err := bencode.Marshal(v) + if err != nil { + log.Fatal(err) + } + + //decode + err := bencode.Unmarshal(data, &v) + if err != nil { + log.Fatal(err) + } + fmt.Println(v) +} +``` diff --git a/deps/github.com/anacrolix/torrent/bencode/api.go b/deps/github.com/anacrolix/torrent/bencode/api.go new file mode 100644 index 0000000..3c379ab --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/api.go @@ -0,0 +1,164 @@ +package bencode + +import ( + "bytes" + "fmt" + "io" + "reflect" + + "github.com/anacrolix/missinggo/expect" +) + +//---------------------------------------------------------------------------- +// Errors +//---------------------------------------------------------------------------- + +// In case if marshaler cannot encode a type, it will return this error. Typical +// example of such type is float32/float64 which has no bencode representation. +type MarshalTypeError struct { + Type reflect.Type +} + +func (e *MarshalTypeError) Error() string { + return "bencode: unsupported type: " + e.Type.String() +} + +// Unmarshal argument must be a non-nil value of some pointer type. +type UnmarshalInvalidArgError struct { + Type reflect.Type +} + +func (e *UnmarshalInvalidArgError) Error() string { + if e.Type == nil { + return "bencode: Unmarshal(nil)" + } + + if e.Type.Kind() != reflect.Ptr { + return "bencode: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "bencode: Unmarshal(nil " + e.Type.String() + ")" +} + +// Unmarshaler spotted a value that was not appropriate for a given Go value. +type UnmarshalTypeError struct { + BencodeTypeName string + UnmarshalTargetType reflect.Type +} + +// This could probably be a value type, but we may already have users assuming +// that it's passed by pointer. +func (e *UnmarshalTypeError) Error() string { + return fmt.Sprintf( + "can't unmarshal a bencode %v into a %v", + e.BencodeTypeName, + e.UnmarshalTargetType, + ) +} + +// Unmarshaler tried to write to an unexported (therefore unwritable) field. +type UnmarshalFieldError struct { + Key string + Type reflect.Type + Field reflect.StructField +} + +func (e *UnmarshalFieldError) Error() string { + return "bencode: key \"" + e.Key + "\" led to an unexported field \"" + + e.Field.Name + "\" in type: " + e.Type.String() +} + +// Malformed bencode input, unmarshaler failed to parse it. +type SyntaxError struct { + Offset int64 // location of the error + What error // error description +} + +func (e *SyntaxError) Error() string { + return fmt.Sprintf("bencode: syntax error (offset: %d): %s", e.Offset, e.What) +} + +// A non-nil error was returned after calling MarshalBencode on a type which +// implements the Marshaler interface. +type MarshalerError struct { + Type reflect.Type + Err error +} + +func (e *MarshalerError) Error() string { + return "bencode: error calling MarshalBencode for type " + e.Type.String() + ": " + e.Err.Error() +} + +// A non-nil error was returned after calling UnmarshalBencode on a type which +// implements the Unmarshaler interface. +type UnmarshalerError struct { + Type reflect.Type + Err error +} + +func (e *UnmarshalerError) Error() string { + return "bencode: error calling UnmarshalBencode for type " + e.Type.String() + ": " + e.Err.Error() +} + +//---------------------------------------------------------------------------- +// Interfaces +//---------------------------------------------------------------------------- + +// Any type which implements this interface, will be marshaled using the +// specified method. +type Marshaler interface { + MarshalBencode() ([]byte, error) +} + +// Any type which implements this interface, will be unmarshaled using the +// specified method. +type Unmarshaler interface { + UnmarshalBencode([]byte) error +} + +// Marshal the value 'v' to the bencode form, return the result as []byte and +// an error if any. +func Marshal(v interface{}) ([]byte, error) { + var buf bytes.Buffer + e := Encoder{w: &buf} + err := e.Encode(v) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func MustMarshal(v interface{}) []byte { + b, err := Marshal(v) + expect.Nil(err) + return b +} + +// Unmarshal the bencode value in the 'data' to a value pointed by the 'v' pointer, return a non-nil +// error if any. If there are trailing bytes, this results in ErrUnusedTrailingBytes, but the value +// will be valid. It's probably more consistent to use Decoder.Decode if you want to rely on this +// behaviour (inspired by Rust's serde here). +func Unmarshal(data []byte, v interface{}) (err error) { + buf := bytes.NewReader(data) + e := Decoder{r: buf} + err = e.Decode(v) + if err == nil && buf.Len() != 0 { + err = ErrUnusedTrailingBytes{buf.Len()} + } + return +} + +type ErrUnusedTrailingBytes struct { + NumUnusedBytes int +} + +func (me ErrUnusedTrailingBytes) Error() string { + return fmt.Sprintf("%d unused trailing bytes", me.NumUnusedBytes) +} + +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{r: &scanner{r: r}} +} + +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{w: w} +} diff --git a/deps/github.com/anacrolix/torrent/bencode/bench_test.go b/deps/github.com/anacrolix/torrent/bencode/bench_test.go new file mode 100644 index 0000000..5cf1ce8 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/bench_test.go @@ -0,0 +1,45 @@ +package bencode_test + +import ( + "net" + "reflect" + "testing" + + "github.com/anacrolix/dht/v2/krpc" + + "github.com/anacrolix/torrent/bencode" +) + +func marshalAndUnmarshal(tb testing.TB, orig krpc.Msg) (ret krpc.Msg) { + b, err := bencode.Marshal(orig) + if err != nil { + tb.Fatal(err) + } + err = bencode.Unmarshal(b, &ret) + if err != nil { + tb.Fatal(err) + } + // ret.Q = "what" + return +} + +func BenchmarkMarshalThenUnmarshalKrpcMsg(tb *testing.B) { + orig := krpc.Msg{ + T: "420", + Y: "r", + R: &krpc.Return{ + Token: func() *string { t := "re-up"; return &t }(), + }, + IP: krpc.NodeAddr{IP: net.ParseIP("1.2.3.4"), Port: 1337}, + ReadOnly: true, + } + first := marshalAndUnmarshal(tb, orig) + if !reflect.DeepEqual(orig, first) { + tb.Fail() + } + tb.ReportAllocs() + tb.ResetTimer() + for i := 0; i < tb.N; i += 1 { + marshalAndUnmarshal(tb, orig) + } +} diff --git a/deps/github.com/anacrolix/torrent/bencode/both_test.go b/deps/github.com/anacrolix/torrent/bencode/both_test.go new file mode 100644 index 0000000..fdcb90d --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/both_test.go @@ -0,0 +1,76 @@ +package bencode + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func loadFile(name string, t *testing.T) []byte { + data, err := os.ReadFile(name) + require.NoError(t, err) + return data +} + +func testFileInterface(t *testing.T, filename string) { + data1 := loadFile(filename, t) + + var iface interface{} + err := Unmarshal(data1, &iface) + require.NoError(t, err) + + data2, err := Marshal(iface) + require.NoError(t, err) + + assert.EqualValues(t, data1, data2) +} + +func TestBothInterface(t *testing.T) { + testFileInterface(t, "testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent") + testFileInterface(t, "testdata/continuum.torrent") +} + +type torrentFile struct { + Info struct { + Name string `bencode:"name"` + Length int64 `bencode:"length"` + MD5Sum string `bencode:"md5sum,omitempty"` + PieceLength int64 `bencode:"piece length"` + Pieces string `bencode:"pieces"` + Private bool `bencode:"private,omitempty"` + } `bencode:"info"` + + Announce string `bencode:"announce"` + AnnounceList [][]string `bencode:"announce-list,omitempty"` + CreationDate int64 `bencode:"creation date,omitempty"` + Comment string `bencode:"comment,omitempty"` + CreatedBy string `bencode:"created by,omitempty"` + URLList interface{} `bencode:"url-list,omitempty"` +} + +func testFile(t *testing.T, filename string) { + data1 := loadFile(filename, t) + var f torrentFile + + err := Unmarshal(data1, &f) + if err != nil { + t.Fatal(err) + } + + data2, err := Marshal(&f) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(data1, data2) { + println(string(data2)) + t.Fatalf("equality expected") + } +} + +func TestBoth(t *testing.T) { + testFile(t, "testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent") +} diff --git a/deps/github.com/anacrolix/torrent/bencode/bytes.go b/deps/github.com/anacrolix/torrent/bencode/bytes.go new file mode 100644 index 0000000..42a1db2 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/bytes.go @@ -0,0 +1,30 @@ +package bencode + +import ( + "errors" + "fmt" +) + +type Bytes []byte + +var ( + _ Unmarshaler = (*Bytes)(nil) + _ Marshaler = (*Bytes)(nil) + _ Marshaler = Bytes{} +) + +func (me *Bytes) UnmarshalBencode(b []byte) error { + *me = append([]byte(nil), b...) + return nil +} + +func (me Bytes) MarshalBencode() ([]byte, error) { + if len(me) == 0 { + return nil, errors.New("marshalled Bytes should not be zero-length") + } + return me, nil +} + +func (me Bytes) GoString() string { + return fmt.Sprintf("bencode.Bytes(%q)", []byte(me)) +} diff --git a/deps/github.com/anacrolix/torrent/bencode/bytes_test.go b/deps/github.com/anacrolix/torrent/bencode/bytes_test.go new file mode 100644 index 0000000..08b4f98 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/bytes_test.go @@ -0,0 +1,39 @@ +package bencode + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestBytesMarshalNil(t *testing.T) { + var b Bytes + Marshal(b) +} + +type structWithBytes struct { + A Bytes + B Bytes +} + +func TestMarshalNilStructBytes(t *testing.T) { + _, err := Marshal(structWithBytes{B: Bytes("i42e")}) + c := qt.New(t) + c.Assert(err, qt.IsNotNil) +} + +type structWithOmitEmptyBytes struct { + A Bytes `bencode:",omitempty"` + B Bytes `bencode:",omitempty"` +} + +func TestMarshalNilStructBytesOmitEmpty(t *testing.T) { + c := qt.New(t) + b, err := Marshal(structWithOmitEmptyBytes{B: Bytes("i42e")}) + c.Assert(err, qt.IsNil) + t.Logf("%q", b) + var s structWithBytes + err = Unmarshal(b, &s) + c.Assert(err, qt.IsNil) + c.Check(s.B, qt.DeepEquals, Bytes("i42e")) +} diff --git a/deps/github.com/anacrolix/torrent/bencode/decode.go b/deps/github.com/anacrolix/torrent/bencode/decode.go new file mode 100644 index 0000000..3839b84 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/decode.go @@ -0,0 +1,752 @@ +package bencode + +import ( + "bytes" + "errors" + "fmt" + "io" + "math/big" + "reflect" + "runtime" + "strconv" + "sync" +) + +// The default bencode string length limit. This is a poor attempt to prevent excessive memory +// allocation when parsing, but also leaves the window open to implement a better solution. +const DefaultDecodeMaxStrLen = 1<<27 - 1 // ~128MiB + +type MaxStrLen = int64 + +type Decoder struct { + // Maximum parsed bencode string length. Defaults to DefaultMaxStrLen if zero. + MaxStrLen MaxStrLen + + r interface { + io.ByteScanner + io.Reader + } + // Sum of bytes used to Decode values. + Offset int64 + buf bytes.Buffer +} + +func (d *Decoder) Decode(v interface{}) (err error) { + defer func() { + if err != nil { + return + } + r := recover() + if r == nil { + return + } + _, ok := r.(runtime.Error) + if ok { + panic(r) + } + if err, ok = r.(error); !ok { + panic(r) + } + // Errors thrown from deeper in parsing are unexpected. At value boundaries, errors should + // be returned directly (at least until all the panic nonsense is removed entirely). + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + pv := reflect.ValueOf(v) + if pv.Kind() != reflect.Ptr || pv.IsNil() { + return &UnmarshalInvalidArgError{reflect.TypeOf(v)} + } + + ok, err := d.parseValue(pv.Elem()) + if err != nil { + return + } + if !ok { + d.throwSyntaxError(d.Offset-1, errors.New("unexpected 'e'")) + } + return +} + +func checkForUnexpectedEOF(err error, offset int64) { + if err == io.EOF { + panic(&SyntaxError{ + Offset: offset, + What: io.ErrUnexpectedEOF, + }) + } +} + +func (d *Decoder) readByte() byte { + b, err := d.r.ReadByte() + if err != nil { + checkForUnexpectedEOF(err, d.Offset) + panic(err) + } + + d.Offset++ + return b +} + +// reads data writing it to 'd.buf' until 'sep' byte is encountered, 'sep' byte +// is consumed, but not included into the 'd.buf' +func (d *Decoder) readUntil(sep byte) { + for { + b := d.readByte() + if b == sep { + return + } + d.buf.WriteByte(b) + } +} + +func checkForIntParseError(err error, offset int64) { + if err != nil { + panic(&SyntaxError{ + Offset: offset, + What: err, + }) + } +} + +func (d *Decoder) throwSyntaxError(offset int64, err error) { + panic(&SyntaxError{ + Offset: offset, + What: err, + }) +} + +// Assume the 'i' is already consumed. Read and validate the rest of an int into the buffer. +func (d *Decoder) readInt() error { + // start := d.Offset - 1 + d.readUntil('e') + if err := d.checkBufferedInt(); err != nil { + return err + } + // if d.buf.Len() == 0 { + // panic(&SyntaxError{ + // Offset: start, + // What: errors.New("empty integer value"), + // }) + // } + return nil +} + +// called when 'i' was consumed, for the integer type in v. +func (d *Decoder) parseInt(v reflect.Value) error { + start := d.Offset - 1 + + if err := d.readInt(); err != nil { + return err + } + s := bytesAsString(d.buf.Bytes()) + + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + n, err := strconv.ParseInt(s, 10, 64) + checkForIntParseError(err, start) + + if v.OverflowInt(n) { + return &UnmarshalTypeError{ + BencodeTypeName: "int", + UnmarshalTargetType: v.Type(), + } + } + v.SetInt(n) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + n, err := strconv.ParseUint(s, 10, 64) + checkForIntParseError(err, start) + + if v.OverflowUint(n) { + return &UnmarshalTypeError{ + BencodeTypeName: "int", + UnmarshalTargetType: v.Type(), + } + } + v.SetUint(n) + case reflect.Bool: + v.SetBool(s != "0") + default: + return &UnmarshalTypeError{ + BencodeTypeName: "int", + UnmarshalTargetType: v.Type(), + } + } + d.buf.Reset() + return nil +} + +func (d *Decoder) checkBufferedInt() error { + b := d.buf.Bytes() + if len(b) <= 1 { + return nil + } + if b[0] == '-' { + b = b[1:] + } + if b[0] < '1' || b[0] > '9' { + return errors.New("invalid leading digit") + } + return nil +} + +func (d *Decoder) parseStringLength() (int, error) { + // We should have already consumed the first byte of the length into the Decoder buf. + start := d.Offset - 1 + d.readUntil(':') + if err := d.checkBufferedInt(); err != nil { + return 0, err + } + // Really the limit should be the uint size for the platform. But we can't pass in an allocator, + // or limit total memory use in Go, the best we might hope to do is limit the size of a single + // decoded value (by reading it in in-place and then operating on a view). + length, err := strconv.ParseInt(bytesAsString(d.buf.Bytes()), 10, 0) + checkForIntParseError(err, start) + if int64(length) > d.getMaxStrLen() { + err = fmt.Errorf("parsed string length %v exceeds limit (%v)", length, DefaultDecodeMaxStrLen) + } + d.buf.Reset() + return int(length), err +} + +func (d *Decoder) parseString(v reflect.Value) error { + length, err := d.parseStringLength() + if err != nil { + return err + } + defer d.buf.Reset() + read := func(b []byte) { + n, err := io.ReadFull(d.r, b) + d.Offset += int64(n) + if err != nil { + checkForUnexpectedEOF(err, d.Offset) + panic(&SyntaxError{ + Offset: d.Offset, + What: errors.New("unexpected I/O error: " + err.Error()), + }) + } + } + + switch v.Kind() { + case reflect.String: + b := make([]byte, length) + read(b) + v.SetString(bytesAsString(b)) + return nil + case reflect.Slice: + if v.Type().Elem().Kind() != reflect.Uint8 { + break + } + b := make([]byte, length) + read(b) + v.SetBytes(b) + return nil + case reflect.Array: + if v.Type().Elem().Kind() != reflect.Uint8 { + break + } + d.buf.Grow(length) + b := d.buf.Bytes()[:length] + read(b) + reflect.Copy(v, reflect.ValueOf(b)) + return nil + case reflect.Bool: + d.buf.Grow(length) + b := d.buf.Bytes()[:length] + read(b) + x, err := strconv.ParseBool(bytesAsString(b)) + if err != nil { + x = length != 0 + } + v.SetBool(x) + return nil + } + // Can't move this into default clause because some cases above fail through to here after + // additional checks. + d.buf.Grow(length) + read(d.buf.Bytes()[:length]) + // I believe we return here to support "ignore_unmarshal_type_error". + return &UnmarshalTypeError{ + BencodeTypeName: "string", + UnmarshalTargetType: v.Type(), + } +} + +// Info for parsing a dict value. +type dictField struct { + Type reflect.Type + Get func(value reflect.Value) func(reflect.Value) + Tags tag +} + +// Returns specifics for parsing a dict field value. +func getDictField(dict reflect.Type, key string) (_ dictField, err error) { + // get valuev as a map value or as a struct field + switch k := dict.Kind(); k { + case reflect.Map: + return dictField{ + Type: dict.Elem(), + Get: func(mapValue reflect.Value) func(reflect.Value) { + return func(value reflect.Value) { + if mapValue.IsNil() { + mapValue.Set(reflect.MakeMap(dict)) + } + // Assigns the value into the map. + // log.Printf("map type: %v", mapValue.Type()) + mapValue.SetMapIndex(reflect.ValueOf(key).Convert(dict.Key()), value) + } + }, + }, nil + case reflect.Struct: + return getStructFieldForKey(dict, key), nil + // if sf.r.PkgPath != "" { + // panic(&UnmarshalFieldError{ + // Key: key, + // Type: dict.Type(), + // Field: sf.r, + // }) + // } + default: + err = fmt.Errorf("can't assign bencode dict items into a %v", k) + return + } +} + +var ( + structFieldsMu sync.Mutex + structFields = map[reflect.Type]map[string]dictField{} +) + +func parseStructFields(struct_ reflect.Type, each func(key string, df dictField)) { + for _i, n := 0, struct_.NumField(); _i < n; _i++ { + i := _i + f := struct_.Field(i) + if f.Anonymous { + t := f.Type + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + parseStructFields(t, func(key string, df dictField) { + innerGet := df.Get + df.Get = func(value reflect.Value) func(reflect.Value) { + anonPtr := value.Field(i) + if anonPtr.Kind() == reflect.Ptr && anonPtr.IsNil() { + anonPtr.Set(reflect.New(f.Type.Elem())) + anonPtr = anonPtr.Elem() + } + return innerGet(anonPtr) + } + each(key, df) + }) + continue + } + tagStr := f.Tag.Get("bencode") + if tagStr == "-" { + continue + } + tag := parseTag(tagStr) + key := tag.Key() + if key == "" { + key = f.Name + } + each(key, dictField{f.Type, func(value reflect.Value) func(reflect.Value) { + return value.Field(i).Set + }, tag}) + } +} + +func saveStructFields(struct_ reflect.Type) { + m := make(map[string]dictField) + parseStructFields(struct_, func(key string, sf dictField) { + m[key] = sf + }) + structFields[struct_] = m +} + +func getStructFieldForKey(struct_ reflect.Type, key string) (f dictField) { + structFieldsMu.Lock() + if _, ok := structFields[struct_]; !ok { + saveStructFields(struct_) + } + f, ok := structFields[struct_][key] + structFieldsMu.Unlock() + if !ok { + var discard interface{} + return dictField{ + Type: reflect.TypeOf(discard), + Get: func(reflect.Value) func(reflect.Value) { return func(reflect.Value) {} }, + Tags: nil, + } + } + return +} + +func (d *Decoder) parseDict(v reflect.Value) error { + // At this point 'd' byte was consumed, now read key/value pairs + for { + var keyStr string + keyValue := reflect.ValueOf(&keyStr).Elem() + ok, err := d.parseValue(keyValue) + if err != nil { + return fmt.Errorf("error parsing dict key: %w", err) + } + if !ok { + return nil + } + + df, err := getDictField(v.Type(), keyStr) + if err != nil { + return fmt.Errorf("parsing bencode dict into %v: %w", v.Type(), err) + } + + // now we need to actually parse it + if df.Type == nil { + // Discard the value, there's nowhere to put it. + var if_ interface{} + if_, ok = d.parseValueInterface() + if if_ == nil { + return fmt.Errorf("error parsing value for key %q", keyStr) + } + if !ok { + return fmt.Errorf("missing value for key %q", keyStr) + } + continue + } + setValue := reflect.New(df.Type).Elem() + // log.Printf("parsing into %v", setValue.Type()) + ok, err = d.parseValue(setValue) + if err != nil { + var target *UnmarshalTypeError + if !(errors.As(err, &target) && df.Tags.IgnoreUnmarshalTypeError()) { + return fmt.Errorf("parsing value for key %q: %w", keyStr, err) + } + } + if !ok { + return fmt.Errorf("missing value for key %q", keyStr) + } + df.Get(v)(setValue) + } +} + +func (d *Decoder) parseList(v reflect.Value) error { + switch v.Kind() { + default: + // If the list is a singleton of the expected type, use that value. See + // https://github.com/anacrolix/torrent/issues/297. + l := reflect.New(reflect.SliceOf(v.Type())) + if err := d.parseList(l.Elem()); err != nil { + return err + } + if l.Elem().Len() != 1 { + return &UnmarshalTypeError{ + BencodeTypeName: "list", + UnmarshalTargetType: v.Type(), + } + } + v.Set(l.Elem().Index(0)) + return nil + case reflect.Array, reflect.Slice: + // We can work with this. Normal case, fallthrough. + } + + i := 0 + for ; ; i++ { + if v.Kind() == reflect.Slice && i >= v.Len() { + v.Set(reflect.Append(v, reflect.Zero(v.Type().Elem()))) + } + + if i < v.Len() { + ok, err := d.parseValue(v.Index(i)) + if err != nil { + return err + } + if !ok { + break + } + } else { + _, ok := d.parseValueInterface() + if !ok { + break + } + } + } + + if i < v.Len() { + if v.Kind() == reflect.Array { + z := reflect.Zero(v.Type().Elem()) + for n := v.Len(); i < n; i++ { + v.Index(i).Set(z) + } + } else { + v.SetLen(i) + } + } + + if i == 0 && v.Kind() == reflect.Slice { + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) + } + return nil +} + +func (d *Decoder) readOneValue() bool { + b, err := d.r.ReadByte() + if err != nil { + panic(err) + } + if b == 'e' { + d.r.UnreadByte() + return false + } else { + d.Offset++ + d.buf.WriteByte(b) + } + + switch b { + case 'd', 'l': + // read until there is nothing to read + for d.readOneValue() { + } + // consume 'e' as well + b = d.readByte() + d.buf.WriteByte(b) + case 'i': + d.readUntil('e') + d.buf.WriteString("e") + default: + if b >= '0' && b <= '9' { + start := d.buf.Len() - 1 + d.readUntil(':') + length, err := strconv.ParseInt(bytesAsString(d.buf.Bytes()[start:]), 10, 64) + checkForIntParseError(err, d.Offset-1) + + d.buf.WriteString(":") + n, err := io.CopyN(&d.buf, d.r, length) + d.Offset += n + if err != nil { + checkForUnexpectedEOF(err, d.Offset) + panic(&SyntaxError{ + Offset: d.Offset, + What: errors.New("unexpected I/O error: " + err.Error()), + }) + } + break + } + + d.raiseUnknownValueType(b, d.Offset-1) + } + + return true +} + +func (d *Decoder) parseUnmarshaler(v reflect.Value) bool { + if !v.Type().Implements(unmarshalerType) { + if v.Addr().Type().Implements(unmarshalerType) { + v = v.Addr() + } else { + return false + } + } + d.buf.Reset() + if !d.readOneValue() { + return false + } + m := v.Interface().(Unmarshaler) + err := m.UnmarshalBencode(d.buf.Bytes()) + if err != nil { + panic(&UnmarshalerError{v.Type(), err}) + } + return true +} + +// Returns true if there was a value and it's now stored in 'v', otherwise +// there was an end symbol ("e") and no value was stored. +func (d *Decoder) parseValue(v reflect.Value) (bool, error) { + // we support one level of indirection at the moment + if v.Kind() == reflect.Ptr { + // if the pointer is nil, allocate a new element of the type it + // points to + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + + if d.parseUnmarshaler(v) { + return true, nil + } + + // common case: interface{} + if v.Kind() == reflect.Interface && v.NumMethod() == 0 { + iface, _ := d.parseValueInterface() + v.Set(reflect.ValueOf(iface)) + return true, nil + } + + b, err := d.r.ReadByte() + if err != nil { + return false, err + } + d.Offset++ + + switch b { + case 'e': + return false, nil + case 'd': + return true, d.parseDict(v) + case 'l': + return true, d.parseList(v) + case 'i': + return true, d.parseInt(v) + default: + if b >= '0' && b <= '9' { + // It's a string. + d.buf.Reset() + // Write the first digit of the length to the buffer. + d.buf.WriteByte(b) + return true, d.parseString(v) + } + + d.raiseUnknownValueType(b, d.Offset-1) + } + panic("unreachable") +} + +// An unknown bencode type character was encountered. +func (d *Decoder) raiseUnknownValueType(b byte, offset int64) { + panic(&SyntaxError{ + Offset: offset, + What: fmt.Errorf("unknown value type %+q", b), + }) +} + +func (d *Decoder) parseValueInterface() (interface{}, bool) { + b, err := d.r.ReadByte() + if err != nil { + panic(err) + } + d.Offset++ + + switch b { + case 'e': + return nil, false + case 'd': + return d.parseDictInterface(), true + case 'l': + return d.parseListInterface(), true + case 'i': + return d.parseIntInterface(), true + default: + if b >= '0' && b <= '9' { + // string + // append first digit of the length to the buffer + d.buf.WriteByte(b) + return d.parseStringInterface(), true + } + + d.raiseUnknownValueType(b, d.Offset-1) + panic("unreachable") + } +} + +// Called after 'i', for an arbitrary integer size. +func (d *Decoder) parseIntInterface() (ret interface{}) { + start := d.Offset - 1 + + if err := d.readInt(); err != nil { + panic(err) + } + n, err := strconv.ParseInt(d.buf.String(), 10, 64) + if ne, ok := err.(*strconv.NumError); ok && ne.Err == strconv.ErrRange { + i := new(big.Int) + _, ok := i.SetString(d.buf.String(), 10) + if !ok { + panic(&SyntaxError{ + Offset: start, + What: errors.New("failed to parse integer"), + }) + } + ret = i + } else { + checkForIntParseError(err, start) + ret = n + } + + d.buf.Reset() + return +} + +func (d *Decoder) readBytes(length int) []byte { + b, err := io.ReadAll(io.LimitReader(d.r, int64(length))) + if err != nil { + panic(err) + } + if len(b) != length { + panic(fmt.Errorf("read %v bytes expected %v", len(b), length)) + } + return b +} + +func (d *Decoder) parseStringInterface() string { + length, err := d.parseStringLength() + if err != nil { + panic(err) + } + b := d.readBytes(int(length)) + d.Offset += int64(len(b)) + if err != nil { + panic(&SyntaxError{Offset: d.Offset, What: err}) + } + return bytesAsString(b) +} + +func (d *Decoder) parseDictInterface() interface{} { + dict := make(map[string]interface{}) + var lastKey string + lastKeyOk := false + for { + start := d.Offset + keyi, ok := d.parseValueInterface() + if !ok { + break + } + + key, ok := keyi.(string) + if !ok { + panic(&SyntaxError{ + Offset: d.Offset, + What: errors.New("non-string key in a dict"), + }) + } + if lastKeyOk && key <= lastKey { + d.throwSyntaxError(start, fmt.Errorf("dict keys unsorted: %q <= %q", key, lastKey)) + } + start = d.Offset + valuei, ok := d.parseValueInterface() + if !ok { + d.throwSyntaxError(start, fmt.Errorf("dict elem missing value [key=%v]", key)) + } + + lastKey = key + lastKeyOk = true + dict[key] = valuei + } + return dict +} + +func (d *Decoder) parseListInterface() (list []interface{}) { + list = []interface{}{} + valuei, ok := d.parseValueInterface() + for ok { + list = append(list, valuei) + valuei, ok = d.parseValueInterface() + } + return +} + +func (d *Decoder) getMaxStrLen() int64 { + if d.MaxStrLen == 0 { + return DefaultDecodeMaxStrLen + } + return d.MaxStrLen +} diff --git a/deps/github.com/anacrolix/torrent/bencode/decode_test.go b/deps/github.com/anacrolix/torrent/bencode/decode_test.go new file mode 100644 index 0000000..4d05d2b --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/decode_test.go @@ -0,0 +1,267 @@ +package bencode + +import ( + "bytes" + "fmt" + "io" + "math/big" + "reflect" + "testing" + + qt "github.com/frankban/quicktest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type random_decode_test struct { + data string + expected interface{} +} + +var random_decode_tests = []random_decode_test{ + {"i57e", int64(57)}, + {"i-9223372036854775808e", int64(-9223372036854775808)}, + {"5:hello", "hello"}, + {"29:unicode test проверка", "unicode test проверка"}, + {"d1:ai5e1:b5:helloe", map[string]interface{}{"a": int64(5), "b": "hello"}}, + { + "li5ei10ei15ei20e7:bencodee", + []interface{}{int64(5), int64(10), int64(15), int64(20), "bencode"}, + }, + {"ldedee", []interface{}{map[string]interface{}{}, map[string]interface{}{}}}, + {"le", []interface{}{}}, + {"i604919719469385652980544193299329427705624352086e", func() *big.Int { + ret, _ := big.NewInt(-1).SetString("604919719469385652980544193299329427705624352086", 10) + return ret + }()}, + {"d1:rd6:\xd4/\xe2F\x00\x01i42ee1:t3:\x9a\x87\x011:v4:TR%=1:y1:re", map[string]interface{}{ + "r": map[string]interface{}{"\xd4/\xe2F\x00\x01": int64(42)}, + "t": "\x9a\x87\x01", + "v": "TR%=", + "y": "r", + }}, + {"d0:i420ee", map[string]interface{}{"": int64(420)}}, +} + +func TestRandomDecode(t *testing.T) { + for _, test := range random_decode_tests { + var value interface{} + err := Unmarshal([]byte(test.data), &value) + if err != nil { + t.Error(err, test.data) + continue + } + assert.EqualValues(t, test.expected, value) + } +} + +func TestLoneE(t *testing.T) { + var v int + err := Unmarshal([]byte("e"), &v) + se := err.(*SyntaxError) + require.EqualValues(t, 0, se.Offset) +} + +func TestDecoderConsecutive(t *testing.T) { + d := NewDecoder(bytes.NewReader([]byte("i1ei2e"))) + var i int + err := d.Decode(&i) + require.NoError(t, err) + require.EqualValues(t, 1, i) + err = d.Decode(&i) + require.NoError(t, err) + require.EqualValues(t, 2, i) + err = d.Decode(&i) + require.Equal(t, io.EOF, err) +} + +func TestDecoderConsecutiveDicts(t *testing.T) { + bb := bytes.NewBufferString("d4:herp4:derped3:wat1:ke17:oh baby a triple!") + + d := NewDecoder(bb) + assert.EqualValues(t, "d4:herp4:derped3:wat1:ke17:oh baby a triple!", bb.Bytes()) + assert.EqualValues(t, 0, d.Offset) + + var m map[string]interface{} + + require.NoError(t, d.Decode(&m)) + assert.Len(t, m, 1) + assert.Equal(t, "derp", m["herp"]) + assert.Equal(t, "d3:wat1:ke17:oh baby a triple!", bb.String()) + assert.EqualValues(t, 14, d.Offset) + + require.NoError(t, d.Decode(&m)) + assert.Equal(t, "k", m["wat"]) + assert.Equal(t, "17:oh baby a triple!", bb.String()) + assert.EqualValues(t, 24, d.Offset) + + var s string + require.NoError(t, d.Decode(&s)) + assert.Equal(t, "oh baby a triple!", s) + assert.EqualValues(t, 44, d.Offset) +} + +func check_error(t *testing.T, err error) { + if err != nil { + t.Error(err) + } +} + +func assert_equal(t *testing.T, x, y interface{}) { + if !reflect.DeepEqual(x, y) { + t.Errorf("got: %v (%T), expected: %v (%T)\n", x, x, y, y) + } +} + +type unmarshalerInt struct { + x int +} + +func (me *unmarshalerInt) UnmarshalBencode(data []byte) error { + return Unmarshal(data, &me.x) +} + +type unmarshalerString struct { + x string +} + +func (me *unmarshalerString) UnmarshalBencode(data []byte) error { + me.x = string(data) + return nil +} + +func TestUnmarshalerBencode(t *testing.T) { + var i unmarshalerInt + var ss []unmarshalerString + check_error(t, Unmarshal([]byte("i71e"), &i)) + assert_equal(t, i.x, 71) + check_error(t, Unmarshal([]byte("l5:hello5:fruit3:waye"), &ss)) + assert_equal(t, ss[0].x, "5:hello") + assert_equal(t, ss[1].x, "5:fruit") + assert_equal(t, ss[2].x, "3:way") +} + +func TestIgnoreUnmarshalTypeError(t *testing.T) { + s := struct { + Ignore int `bencode:",ignore_unmarshal_type_error"` + Normal int + }{} + require.Error(t, Unmarshal([]byte("d6:Normal5:helloe"), &s)) + assert.NoError(t, Unmarshal([]byte("d6:Ignore5:helloe"), &s)) + qt.Assert(t, Unmarshal([]byte("d6:Ignorei42ee"), &s), qt.IsNil) + assert.EqualValues(t, 42, s.Ignore) +} + +// Test unmarshalling []byte into something that has the same kind but +// different type. +func TestDecodeCustomSlice(t *testing.T) { + type flag byte + var fs3, fs2 []flag + // We do a longer slice then a shorter slice to see if the buffers are + // shared. + d := NewDecoder(bytes.NewBufferString("3:\x01\x10\xff2:\x04\x0f")) + require.NoError(t, d.Decode(&fs3)) + require.NoError(t, d.Decode(&fs2)) + assert.EqualValues(t, []flag{1, 16, 255}, fs3) + assert.EqualValues(t, []flag{4, 15}, fs2) +} + +func TestUnmarshalUnusedBytes(t *testing.T) { + var i int + require.EqualValues(t, ErrUnusedTrailingBytes{1}, Unmarshal([]byte("i42ee"), &i)) + assert.EqualValues(t, 42, i) +} + +func TestUnmarshalByteArray(t *testing.T) { + var ba [2]byte + assert.NoError(t, Unmarshal([]byte("2:hi"), &ba)) + assert.EqualValues(t, "hi", ba[:]) +} + +func TestDecodeDictIntoUnsupported(t *testing.T) { + // Any type that a dict shouldn't be unmarshallable into. + var i int + c := qt.New(t) + err := Unmarshal([]byte("d1:a1:be"), &i) + t.Log(err) + c.Check(err, qt.Not(qt.IsNil)) +} + +func TestUnmarshalDictKeyNotString(t *testing.T) { + // Any type that a dict shouldn't be unmarshallable into. + var i int + c := qt.New(t) + err := Unmarshal([]byte("di42e3:yese"), &i) + t.Log(err) + c.Check(err, qt.Not(qt.IsNil)) +} + +type arbitraryReader struct{} + +func (arbitraryReader) Read(b []byte) (int, error) { + return len(b), nil +} + +func decodeHugeString(t *testing.T, strLen int64, header, tail string, v interface{}, maxStrLen MaxStrLen) error { + r, w := io.Pipe() + go func() { + fmt.Fprintf(w, header, strLen) + io.CopyN(w, arbitraryReader{}, strLen) + w.Write([]byte(tail)) + w.Close() + }() + d := NewDecoder(r) + d.MaxStrLen = maxStrLen + return d.Decode(v) +} + +// Ensure that bencode strings in various places obey the Decoder.MaxStrLen field. +func TestDecodeMaxStrLen(t *testing.T) { + t.Parallel() + c := qt.New(t) + test := func(header, tail string, v interface{}, maxStrLen MaxStrLen) { + strLen := maxStrLen + if strLen == 0 { + strLen = DefaultDecodeMaxStrLen + } + c.Assert(decodeHugeString(t, strLen, header, tail, v, maxStrLen), qt.IsNil) + c.Assert(decodeHugeString(t, strLen+1, header, tail, v, maxStrLen), qt.IsNotNil) + } + test("d%d:", "i0ee", new(interface{}), 0) + test("%d:", "", new(interface{}), DefaultDecodeMaxStrLen) + test("%d:", "", new([]byte), 1) + test("d3:420%d:", "e", new(struct { + Hi []byte `bencode:"420"` + }), 69) +} + +// This is for the "github.com/anacrolix/torrent/metainfo".Info.Private field. +func TestDecodeStringIntoBoolPtr(t *testing.T) { + var m struct { + Private *bool `bencode:"private,omitempty"` + } + c := qt.New(t) + check := func(msg string, expectNil, expectTrue bool) { + m.Private = nil + c.Check(Unmarshal([]byte(msg), &m), qt.IsNil, qt.Commentf("%q", msg)) + if expectNil { + c.Check(m.Private, qt.IsNil) + } else { + if c.Check(m.Private, qt.IsNotNil, qt.Commentf("%q", msg)) { + c.Check(*m.Private, qt.Equals, expectTrue, qt.Commentf("%q", msg)) + } + } + } + check("d7:privatei1ee", false, true) + check("d7:privatei0ee", false, false) + check("d7:privatei42ee", false, true) + // This is a weird case. We could not allocate the bool to indicate it was bad (maybe a bad + // serializer which isn't uncommon), but that requires reworking the decoder to handle + // automatically. I think if we cared enough we'd create a custom Unmarshaler. Also if we were + // worried enough about performance I'd completely rewrite this package. + check("d7:private0:e", false, false) + check("d7:private1:te", false, true) + check("d7:private5:falsee", false, false) + check("d7:private1:Fe", false, false) + check("d7:private11:bunnyfoofooe", false, true) +} diff --git a/deps/github.com/anacrolix/torrent/bencode/encode.go b/deps/github.com/anacrolix/torrent/bencode/encode.go new file mode 100644 index 0000000..5e80cb1 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/encode.go @@ -0,0 +1,293 @@ +package bencode + +import ( + "io" + "math/big" + "reflect" + "runtime" + "sort" + "strconv" + "sync" + + "github.com/anacrolix/missinggo" +) + +func isEmptyValue(v reflect.Value) bool { + return missinggo.IsEmptyValue(v) +} + +type Encoder struct { + w io.Writer + scratch [64]byte +} + +func (e *Encoder) Encode(v interface{}) (err error) { + if v == nil { + return + } + defer func() { + if e := recover(); e != nil { + if _, ok := e.(runtime.Error); ok { + panic(e) + } + var ok bool + err, ok = e.(error) + if !ok { + panic(e) + } + } + }() + e.reflectValue(reflect.ValueOf(v)) + return nil +} + +type stringValues []reflect.Value + +func (sv stringValues) Len() int { return len(sv) } +func (sv stringValues) Swap(i, j int) { sv[i], sv[j] = sv[j], sv[i] } +func (sv stringValues) Less(i, j int) bool { return sv.get(i) < sv.get(j) } +func (sv stringValues) get(i int) string { return sv[i].String() } + +func (e *Encoder) write(s []byte) { + _, err := e.w.Write(s) + if err != nil { + panic(err) + } +} + +func (e *Encoder) writeString(s string) { + for s != "" { + n := copy(e.scratch[:], s) + s = s[n:] + e.write(e.scratch[:n]) + } +} + +func (e *Encoder) reflectString(s string) { + e.writeStringPrefix(int64(len(s))) + e.writeString(s) +} + +func (e *Encoder) writeStringPrefix(l int64) { + b := strconv.AppendInt(e.scratch[:0], l, 10) + e.write(b) + e.writeString(":") +} + +func (e *Encoder) reflectByteSlice(s []byte) { + e.writeStringPrefix(int64(len(s))) + e.write(s) +} + +// Returns true if the value implements Marshaler interface and marshaling was +// done successfully. +func (e *Encoder) reflectMarshaler(v reflect.Value) bool { + if !v.Type().Implements(marshalerType) { + if v.Kind() != reflect.Ptr && v.CanAddr() && v.Addr().Type().Implements(marshalerType) { + v = v.Addr() + } else { + return false + } + } + m := v.Interface().(Marshaler) + data, err := m.MarshalBencode() + if err != nil { + panic(&MarshalerError{v.Type(), err}) + } + e.write(data) + return true +} + +var bigIntType = reflect.TypeOf((*big.Int)(nil)).Elem() + +func (e *Encoder) reflectValue(v reflect.Value) { + if e.reflectMarshaler(v) { + return + } + + if v.Type() == bigIntType { + e.writeString("i") + bi := v.Interface().(big.Int) + e.writeString(bi.String()) + e.writeString("e") + return + } + + switch v.Kind() { + case reflect.Bool: + if v.Bool() { + e.writeString("i1e") + } else { + e.writeString("i0e") + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + e.writeString("i") + b := strconv.AppendInt(e.scratch[:0], v.Int(), 10) + e.write(b) + e.writeString("e") + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + e.writeString("i") + b := strconv.AppendUint(e.scratch[:0], v.Uint(), 10) + e.write(b) + e.writeString("e") + case reflect.String: + e.reflectString(v.String()) + case reflect.Struct: + e.writeString("d") + for _, ef := range getEncodeFields(v.Type()) { + fieldValue := ef.i(v) + if !fieldValue.IsValid() { + continue + } + if ef.omitEmpty && isEmptyValue(fieldValue) { + continue + } + e.reflectString(ef.tag) + e.reflectValue(fieldValue) + } + e.writeString("e") + case reflect.Map: + if v.Type().Key().Kind() != reflect.String { + panic(&MarshalTypeError{v.Type()}) + } + if v.IsNil() { + e.writeString("de") + break + } + e.writeString("d") + sv := stringValues(v.MapKeys()) + sort.Sort(sv) + for _, key := range sv { + e.reflectString(key.String()) + e.reflectValue(v.MapIndex(key)) + } + e.writeString("e") + case reflect.Slice, reflect.Array: + e.reflectSequence(v) + case reflect.Interface: + e.reflectValue(v.Elem()) + case reflect.Ptr: + if v.IsNil() { + v = reflect.Zero(v.Type().Elem()) + } else { + v = v.Elem() + } + e.reflectValue(v) + default: + panic(&MarshalTypeError{v.Type()}) + } +} + +func (e *Encoder) reflectSequence(v reflect.Value) { + // Use bencode string-type + if v.Type().Elem().Kind() == reflect.Uint8 { + if v.Kind() != reflect.Slice { + // Can't use []byte optimization + if !v.CanAddr() { + e.writeStringPrefix(int64(v.Len())) + for i := 0; i < v.Len(); i++ { + var b [1]byte + b[0] = byte(v.Index(i).Uint()) + e.write(b[:]) + } + return + } + v = v.Slice(0, v.Len()) + } + s := v.Bytes() + e.reflectByteSlice(s) + return + } + if v.IsNil() { + e.writeString("le") + return + } + e.writeString("l") + for i, n := 0, v.Len(); i < n; i++ { + e.reflectValue(v.Index(i)) + } + e.writeString("e") +} + +type encodeField struct { + i func(v reflect.Value) reflect.Value + tag string + omitEmpty bool +} + +type encodeFieldsSortType []encodeField + +func (ef encodeFieldsSortType) Len() int { return len(ef) } +func (ef encodeFieldsSortType) Swap(i, j int) { ef[i], ef[j] = ef[j], ef[i] } +func (ef encodeFieldsSortType) Less(i, j int) bool { return ef[i].tag < ef[j].tag } + +var ( + typeCacheLock sync.RWMutex + encodeFieldsCache = make(map[reflect.Type][]encodeField) +) + +func getEncodeFields(t reflect.Type) []encodeField { + typeCacheLock.RLock() + fs, ok := encodeFieldsCache[t] + typeCacheLock.RUnlock() + if ok { + return fs + } + fs = makeEncodeFields(t) + typeCacheLock.Lock() + defer typeCacheLock.Unlock() + encodeFieldsCache[t] = fs + return fs +} + +func makeEncodeFields(t reflect.Type) (fs []encodeField) { + for _i, n := 0, t.NumField(); _i < n; _i++ { + i := _i + f := t.Field(i) + if f.PkgPath != "" { + continue + } + if f.Anonymous { + t := f.Type + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + anonEFs := makeEncodeFields(t) + for aefi := range anonEFs { + anonEF := anonEFs[aefi] + bottomField := anonEF + bottomField.i = func(v reflect.Value) reflect.Value { + v = v.Field(i) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + // This will skip serializing this value. + return reflect.Value{} + } + v = v.Elem() + } + return anonEF.i(v) + } + fs = append(fs, bottomField) + } + continue + } + var ef encodeField + ef.i = func(v reflect.Value) reflect.Value { + return v.Field(i) + } + ef.tag = f.Name + + tv := getTag(f.Tag) + if tv.Ignore() { + continue + } + if tv.Key() != "" { + ef.tag = tv.Key() + } + ef.omitEmpty = tv.OmitEmpty() + fs = append(fs, ef) + } + fss := encodeFieldsSortType(fs) + sort.Sort(fss) + return fs +} diff --git a/deps/github.com/anacrolix/torrent/bencode/encode_test.go b/deps/github.com/anacrolix/torrent/bencode/encode_test.go new file mode 100644 index 0000000..b0fabc4 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/encode_test.go @@ -0,0 +1,91 @@ +package bencode + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +type random_encode_test struct { + value interface{} + expected string +} + +type random_struct struct { + ABC int `bencode:"abc"` + SkipThisOne string `bencode:"-"` + CDE string +} + +type dummy struct { + a, b, c int +} + +func (d *dummy) MarshalBencode() ([]byte, error) { + var b bytes.Buffer + _, err := fmt.Fprintf(&b, "i%dei%dei%de", d.a+1, d.b+1, d.c+1) + if err != nil { + return nil, err + } + return b.Bytes(), nil +} + +var random_encode_tests = []random_encode_test{ + {int(10), "i10e"}, + {uint(10), "i10e"}, + {"hello, world", "12:hello, world"}, + {true, "i1e"}, + {false, "i0e"}, + {int8(-8), "i-8e"}, + {int16(-16), "i-16e"}, + {int32(32), "i32e"}, + {int64(-64), "i-64e"}, + {uint8(8), "i8e"}, + {uint16(16), "i16e"}, + {uint32(32), "i32e"}, + {uint64(64), "i64e"}, + {random_struct{123, "nono", "hello"}, "d3:CDE5:hello3:abci123ee"}, + {map[string]string{"a": "b", "c": "d"}, "d1:a1:b1:c1:de"}, + {[]byte{1, 2, 3, 4}, "4:\x01\x02\x03\x04"}, + {&[4]byte{1, 2, 3, 4}, "4:\x01\x02\x03\x04"}, + {nil, ""}, + {[]byte{}, "0:"}, + {[]byte(nil), "0:"}, + {"", "0:"}, + {[]int{}, "le"}, + {map[string]int{}, "de"}, + {&dummy{1, 2, 3}, "i2ei3ei4e"}, + {struct { + A *string + }{nil}, "d1:A0:e"}, + {struct { + A *string + }{new(string)}, "d1:A0:e"}, + {struct { + A *string `bencode:",omitempty"` + }{nil}, "de"}, + {struct { + A *string `bencode:",omitempty"` + }{new(string)}, "d1:A0:e"}, + {bigIntFromString("62208002200000000000"), "i62208002200000000000e"}, + {*bigIntFromString("62208002200000000000"), "i62208002200000000000e"}, +} + +func bigIntFromString(s string) *big.Int { + bi, ok := new(big.Int).SetString(s, 10) + if !ok { + panic(s) + } + return bi +} + +func TestRandomEncode(t *testing.T) { + for _, test := range random_encode_tests { + data, err := Marshal(test.value) + assert.NoError(t, err, "%s", test) + assert.EqualValues(t, test.expected, string(data)) + } +} diff --git a/deps/github.com/anacrolix/torrent/bencode/fuzz_test.go b/deps/github.com/anacrolix/torrent/bencode/fuzz_test.go new file mode 100644 index 0000000..d0dce71 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/fuzz_test.go @@ -0,0 +1,53 @@ +//go:build go1.18 +// +build go1.18 + +package bencode + +import ( + "math/big" + "testing" + + qt "github.com/frankban/quicktest" + "github.com/google/go-cmp/cmp" +) + +var bencodeInterfaceChecker = qt.CmpEquals(cmp.Comparer(func(a, b *big.Int) bool { + return a.Cmp(b) == 0 +})) + +func Fuzz(f *testing.F) { + for _, ret := range random_encode_tests { + f.Add([]byte(ret.expected)) + } + f.Fuzz(func(t *testing.T, b []byte) { + c := qt.New(t) + var d interface{} + err := Unmarshal(b, &d) + if err != nil { + t.Skip() + } + b0, err := Marshal(d) + c.Assert(err, qt.IsNil) + var d0 interface{} + err = Unmarshal(b0, &d0) + c.Assert(err, qt.IsNil) + c.Assert(d0, bencodeInterfaceChecker, d) + }) +} + +func FuzzInterfaceRoundTrip(f *testing.F) { + for _, ret := range random_encode_tests { + f.Add([]byte(ret.expected)) + } + f.Fuzz(func(t *testing.T, b []byte) { + c := qt.New(t) + var d interface{} + err := Unmarshal(b, &d) + if err != nil { + c.Skip(err) + } + b0, err := Marshal(d) + c.Assert(err, qt.IsNil) + c.Check(b0, qt.DeepEquals, b) + }) +} diff --git a/deps/github.com/anacrolix/torrent/bencode/misc.go b/deps/github.com/anacrolix/torrent/bencode/misc.go new file mode 100644 index 0000000..6690008 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/misc.go @@ -0,0 +1,11 @@ +package bencode + +import ( + "reflect" +) + +// Wow Go is retarded. +var ( + marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem() + unmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem() +) diff --git a/deps/github.com/anacrolix/torrent/bencode/scanner.go b/deps/github.com/anacrolix/torrent/bencode/scanner.go new file mode 100644 index 0000000..967d5a5 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/scanner.go @@ -0,0 +1,38 @@ +package bencode + +import ( + "errors" + "io" +) + +// Implements io.ByteScanner over io.Reader, for use in Decoder, to ensure +// that as little as the undecoded input Reader is consumed as possible. +type scanner struct { + r io.Reader + b [1]byte // Buffer for ReadByte + unread bool // True if b has been unread, and so should be returned next +} + +func (me *scanner) Read(b []byte) (int, error) { + return me.r.Read(b) +} + +func (me *scanner) ReadByte() (byte, error) { + if me.unread { + me.unread = false + return me.b[0], nil + } + n, err := me.r.Read(me.b[:]) + if n == 1 { + err = nil + } + return me.b[0], err +} + +func (me *scanner) UnreadByte() error { + if me.unread { + return errors.New("byte already unread") + } + me.unread = true + return nil +} diff --git a/deps/github.com/anacrolix/torrent/bencode/string.go b/deps/github.com/anacrolix/torrent/bencode/string.go new file mode 100644 index 0000000..0c6e307 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/string.go @@ -0,0 +1,9 @@ +//go:build !go1.20 + +package bencode + +import "unsafe" + +func bytesAsString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/deps/github.com/anacrolix/torrent/bencode/string_go120.go b/deps/github.com/anacrolix/torrent/bencode/string_go120.go new file mode 100644 index 0000000..1688d9b --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/string_go120.go @@ -0,0 +1,9 @@ +//go:build go1.20 + +package bencode + +import "unsafe" + +func bytesAsString(b []byte) string { + return unsafe.String(unsafe.SliceData(b), len(b)) +} diff --git a/deps/github.com/anacrolix/torrent/bencode/tags.go b/deps/github.com/anacrolix/torrent/bencode/tags.go new file mode 100644 index 0000000..d4adeb2 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bencode/tags.go @@ -0,0 +1,44 @@ +package bencode + +import ( + "reflect" + "strings" +) + +func getTag(st reflect.StructTag) tag { + return parseTag(st.Get("bencode")) +} + +type tag []string + +func parseTag(tagStr string) tag { + return strings.Split(tagStr, ",") +} + +func (me tag) Ignore() bool { + return me[0] == "-" +} + +func (me tag) Key() string { + return me[0] +} + +func (me tag) HasOpt(opt string) bool { + if len(me) < 1 { + return false + } + for _, s := range me[1:] { + if s == opt { + return true + } + } + return false +} + +func (me tag) OmitEmpty() bool { + return me.HasOpt("omitempty") +} + +func (me tag) IgnoreUnmarshalTypeError() bool { + return me.HasOpt("ignore_unmarshal_type_error") +} diff --git a/deps/github.com/anacrolix/torrent/bencode/testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent b/deps/github.com/anacrolix/torrent/bencode/testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent new file mode 100644 index 0000000000000000000000000000000000000000..9ce7748aaa46f90aa56c29699b763896fc08af91 GIT binary patch literal 12792 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%0D*f1Euc8hO)={iLER+R%jSr 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 0 { + bs = append(_b, _a...) + } + return crc32.Checksum(bs, table), nil +} + +func bep40PriorityIgnoreError(a, b IpPort) peerPriority { + prio, _ := bep40Priority(a, b) + return prio +} diff --git a/deps/github.com/anacrolix/torrent/bep40_test.go b/deps/github.com/anacrolix/torrent/bep40_test.go new file mode 100644 index 0000000..48d5fdd --- /dev/null +++ b/deps/github.com/anacrolix/torrent/bep40_test.go @@ -0,0 +1,34 @@ +package torrent + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBep40Priority(t *testing.T) { + assert.EqualValues(t, peerPriority(0xec2d7224), bep40PriorityIgnoreError( + IpPort{IP: net.ParseIP("123.213.32.10"), Port: 0}, + IpPort{IP: net.ParseIP("98.76.54.32"), Port: 0}, + )) + assert.EqualValues(t, peerPriority(0xec2d7224), bep40PriorityIgnoreError( + IpPort{IP: net.ParseIP("98.76.54.32"), Port: 0}, + IpPort{IP: net.ParseIP("123.213.32.10"), Port: 0}, + )) + assert.Equal(t, peerPriority(0x99568189), bep40PriorityIgnoreError( + IpPort{IP: net.ParseIP("123.213.32.10"), Port: 0}, + IpPort{IP: net.ParseIP("123.213.32.234"), Port: 0}, + )) + assert.Equal(t, peerPriority(0x2b41d456), bep40PriorityIgnoreError( + IpPort{IP: net.ParseIP("206.248.98.111"), Port: 0}, + IpPort{IP: net.ParseIP("142.147.89.224"), Port: 0}, + )) + assert.EqualValues(t, "\x00\x00\x00\x00", func() []byte { + b, _ := bep40PriorityBytes( + IpPort{IP: net.ParseIP("123.213.32.234"), Port: 0}, + IpPort{IP: net.ParseIP("123.213.32.234"), Port: 0}, + ) + return b + }()) +} diff --git a/deps/github.com/anacrolix/torrent/callbacks.go b/deps/github.com/anacrolix/torrent/callbacks.go new file mode 100644 index 0000000..f9ba131 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/callbacks.go @@ -0,0 +1,40 @@ +package torrent + +import ( + "github.com/anacrolix/torrent/mse" + pp "github.com/anacrolix/torrent/peer_protocol" +) + +// These are called synchronously, and do not pass ownership of arguments (do not expect to retain +// data after returning from the callback). The Client and other locks may still be held. nil +// functions are not called. +type Callbacks struct { + // Called after a peer connection completes the BitTorrent handshake. The Client lock is not + // held. + CompletedHandshake func(*PeerConn, InfoHash) + ReadMessage func(*PeerConn, *pp.Message) + ReadExtendedHandshake func(*PeerConn, *pp.ExtendedHandshakeMessage) + PeerConnClosed func(*PeerConn) + + // Provides secret keys to be tried against incoming encrypted connections. + ReceiveEncryptedHandshakeSkeys mse.SecretKeyIter + + ReceivedUsefulData []func(ReceivedUsefulDataEvent) + ReceivedRequested []func(PeerMessageEvent) + DeletedRequest []func(PeerRequestEvent) + SentRequest []func(PeerRequestEvent) + PeerClosed []func(*Peer) + NewPeer []func(*Peer) +} + +type ReceivedUsefulDataEvent = PeerMessageEvent + +type PeerMessageEvent struct { + Peer *Peer + Message *pp.Message +} + +type PeerRequestEvent struct { + Peer *Peer + Request +} diff --git a/deps/github.com/anacrolix/torrent/client-nowasm_test.go b/deps/github.com/anacrolix/torrent/client-nowasm_test.go new file mode 100644 index 0000000..9b93139 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/client-nowasm_test.go @@ -0,0 +1,71 @@ +//go:build !wasm +// +build !wasm + +package torrent + +import ( + "os" + "testing" + + qt "github.com/frankban/quicktest" + "github.com/stretchr/testify/require" + + "github.com/anacrolix/torrent/internal/testutil" + "github.com/anacrolix/torrent/storage" +) + +func TestBoltPieceCompletionClosedWhenClientClosed(t *testing.T) { + cfg := TestingConfig(t) + pc, err := storage.NewBoltPieceCompletion(cfg.DataDir) + require.NoError(t, err) + ci := storage.NewFileWithCompletion(cfg.DataDir, pc) + defer ci.Close() + cfg.DefaultStorage = ci + cl, err := NewClient(cfg) + require.NoError(t, err) + cl.Close() + // And again, https://github.com/anacrolix/torrent/issues/158 + cl, err = NewClient(cfg) + require.NoError(t, err) + cl.Close() +} + +func TestIssue335(t *testing.T) { + dir, mi := testutil.GreetingTestTorrent() + defer func() { + err := os.RemoveAll(dir) + if err != nil { + t.Fatalf("removing torrent dummy data dir: %v", err) + } + }() + logErr := func(f func() error, msg string) { + err := f() + t.Logf("%s: %v", msg, err) + if err != nil { + t.Fail() + } + } + cfg := TestingConfig(t) + cfg.Seed = false + cfg.Debug = true + cfg.DataDir = dir + comp, err := storage.NewBoltPieceCompletion(dir) + c := qt.New(t) + c.Assert(err, qt.IsNil) + defer logErr(comp.Close, "closing bolt piece completion") + mmapStorage := storage.NewMMapWithCompletion(dir, comp) + defer logErr(mmapStorage.Close, "closing mmap storage") + cfg.DefaultStorage = mmapStorage + cl, err := NewClient(cfg) + c.Assert(err, qt.IsNil) + defer cl.Close() + tor, new, err := cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) + c.Assert(err, qt.IsNil) + c.Assert(new, qt.IsTrue) + c.Assert(cl.WaitAll(), qt.IsTrue) + tor.Drop() + _, new, err = cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) + c.Assert(err, qt.IsNil) + c.Assert(new, qt.IsTrue) + c.Assert(cl.WaitAll(), qt.IsTrue) +} diff --git a/deps/github.com/anacrolix/torrent/client-stats.go b/deps/github.com/anacrolix/torrent/client-stats.go new file mode 100644 index 0000000..bfa6994 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/client-stats.go @@ -0,0 +1,52 @@ +package torrent + +import ( + "net/netip" + + g "github.com/anacrolix/generics" +) + +func setAdd[K comparable](m *map[K]struct{}, elem K) { + g.MakeMapIfNilAndSet(m, elem, struct{}{}) +} + +type clientHolepunchAddrSets struct { + undialableWithoutHolepunch map[netip.AddrPort]struct{} + undialableWithoutHolepunchDialedAfterHolepunchConnect map[netip.AddrPort]struct{} + dialableOnlyAfterHolepunch map[netip.AddrPort]struct{} + dialedSuccessfullyAfterHolepunchConnect map[netip.AddrPort]struct{} + probablyOnlyConnectedDueToHolepunch map[netip.AddrPort]struct{} + accepted map[netip.AddrPort]struct{} +} + +type ClientStats struct { + ConnStats + + // Ongoing outgoing dial attempts. There may be more than one dial going on per peer address due + // to hole-punch connect requests. The total may not match the sum of attempts for all Torrents + // if a Torrent is dropped while there are outstanding dials. + ActiveHalfOpenAttempts int + + NumPeersUndialableWithoutHolepunch int + // Number of unique peer addresses that were dialed after receiving a holepunch connect message, + // that have previously been undialable without any hole-punching attempts. + NumPeersUndialableWithoutHolepunchDialedAfterHolepunchConnect int + // Number of unique peer addresses that were successfully dialed and connected after a holepunch + // connect message and previously failing to connect without holepunching. + NumPeersDialableOnlyAfterHolepunch int + NumPeersDialedSuccessfullyAfterHolepunchConnect int + NumPeersProbablyOnlyConnectedDueToHolepunch int +} + +func (cl *Client) statsLocked() (stats ClientStats) { + stats.ConnStats = cl.connStats.Copy() + stats.ActiveHalfOpenAttempts = cl.numHalfOpen + + stats.NumPeersUndialableWithoutHolepunch = len(cl.undialableWithoutHolepunch) + stats.NumPeersUndialableWithoutHolepunchDialedAfterHolepunchConnect = len(cl.undialableWithoutHolepunchDialedAfterHolepunchConnect) + stats.NumPeersDialableOnlyAfterHolepunch = len(cl.dialableOnlyAfterHolepunch) + stats.NumPeersDialedSuccessfullyAfterHolepunchConnect = len(cl.dialedSuccessfullyAfterHolepunchConnect) + stats.NumPeersProbablyOnlyConnectedDueToHolepunch = len(cl.probablyOnlyConnectedDueToHolepunch) + + return +} diff --git a/deps/github.com/anacrolix/torrent/client.go b/deps/github.com/anacrolix/torrent/client.go new file mode 100644 index 0000000..62c0d2b --- /dev/null +++ b/deps/github.com/anacrolix/torrent/client.go @@ -0,0 +1,1792 @@ +package torrent + +import ( + "bufio" + "context" + "crypto/rand" + "crypto/sha1" + "encoding/binary" + "encoding/hex" + "errors" + "expvar" + "fmt" + "io" + "math" + "net" + "net/http" + "net/netip" + "sort" + "strconv" + "time" + + "github.com/anacrolix/chansync" + "github.com/anacrolix/chansync/events" + "github.com/anacrolix/dht/v2" + "github.com/anacrolix/dht/v2/krpc" + . "github.com/anacrolix/generics" + g "github.com/anacrolix/generics" + "github.com/anacrolix/log" + "github.com/anacrolix/missinggo/perf" + "github.com/anacrolix/missinggo/v2" + "github.com/anacrolix/missinggo/v2/bitmap" + "github.com/anacrolix/missinggo/v2/pproffd" + "github.com/anacrolix/sync" + "github.com/davecgh/go-spew/spew" + "github.com/dustin/go-humanize" + gbtree "github.com/google/btree" + "github.com/pion/datachannel" + + "github.com/anacrolix/torrent/bencode" + "github.com/anacrolix/torrent/internal/check" + "github.com/anacrolix/torrent/internal/limiter" + "github.com/anacrolix/torrent/iplist" + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/mse" + pp "github.com/anacrolix/torrent/peer_protocol" + utHolepunch "github.com/anacrolix/torrent/peer_protocol/ut-holepunch" + request_strategy "github.com/anacrolix/torrent/request-strategy" + "github.com/anacrolix/torrent/storage" + "github.com/anacrolix/torrent/tracker" + "github.com/anacrolix/torrent/types/infohash" + "github.com/anacrolix/torrent/webtorrent" +) + +// Clients contain zero or more Torrents. A Client manages a blocklist, the +// TCP/UDP protocol ports, and DHT as desired. +type Client struct { + // An aggregate of stats over all connections. First in struct to ensure 64-bit alignment of + // fields. See #262. + connStats ConnStats + + _mu lockWithDeferreds + event sync.Cond + closed chansync.SetOnce + + config *ClientConfig + logger log.Logger + + peerID PeerID + defaultStorage *storage.Client + onClose []func() + dialers []Dialer + listeners []Listener + dhtServers []DhtServer + ipBlockList iplist.Ranger + + // Set of addresses that have our client ID. This intentionally will + // include ourselves if we end up trying to connect to our own address + // through legitimate channels. + dopplegangerAddrs map[string]struct{} + badPeerIPs map[netip.Addr]struct{} + torrents map[InfoHash]*Torrent + pieceRequestOrder map[interface{}]*request_strategy.PieceRequestOrder + + acceptLimiter map[ipStr]int + numHalfOpen int + + websocketTrackers websocketTrackers + + activeAnnounceLimiter limiter.Instance + httpClient *http.Client + + clientHolepunchAddrSets +} + +type ipStr string + +func (cl *Client) BadPeerIPs() (ips []string) { + cl.rLock() + ips = cl.badPeerIPsLocked() + cl.rUnlock() + return +} + +func (cl *Client) badPeerIPsLocked() (ips []string) { + ips = make([]string, len(cl.badPeerIPs)) + i := 0 + for k := range cl.badPeerIPs { + ips[i] = k.String() + i += 1 + } + return +} + +func (cl *Client) PeerID() PeerID { + return cl.peerID +} + +// Returns the port number for the first listener that has one. No longer assumes that all port +// numbers are the same, due to support for custom listeners. Returns zero if no port number is +// found. +func (cl *Client) LocalPort() (port int) { + for i := 0; i < len(cl.listeners); i += 1 { + if port = addrPortOrZero(cl.listeners[i].Addr()); port != 0 { + return + } + } + return +} + +func writeDhtServerStatus(w io.Writer, s DhtServer) { + dhtStats := s.Stats() + fmt.Fprintf(w, " ID: %x\n", s.ID()) + spew.Fdump(w, dhtStats) +} + +// Writes out a human readable status of the client, such as for writing to a +// HTTP status page. +func (cl *Client) WriteStatus(_w io.Writer) { + cl.rLock() + defer cl.rUnlock() + w := bufio.NewWriter(_w) + defer w.Flush() + fmt.Fprintf(w, "Listen port: %d\n", cl.LocalPort()) + fmt.Fprintf(w, "Peer ID: %+q\n", cl.PeerID()) + fmt.Fprintf(w, "Extension bits: %v\n", cl.config.Extensions) + fmt.Fprintf(w, "Announce key: %x\n", cl.announceKey()) + fmt.Fprintf(w, "Banned IPs: %d\n", len(cl.badPeerIPsLocked())) + cl.eachDhtServer(func(s DhtServer) { + fmt.Fprintf(w, "%s DHT server at %s:\n", s.Addr().Network(), s.Addr().String()) + writeDhtServerStatus(w, s) + }) + dumpStats(w, cl.statsLocked()) + torrentsSlice := cl.torrentsAsSlice() + fmt.Fprintf(w, "# Torrents: %d\n", len(torrentsSlice)) + fmt.Fprintln(w) + sort.Slice(torrentsSlice, func(l, r int) bool { + return torrentsSlice[l].infoHash.AsString() < torrentsSlice[r].infoHash.AsString() + }) + for _, t := range torrentsSlice { + if t.name() == "" { + fmt.Fprint(w, "") + } else { + fmt.Fprint(w, t.name()) + } + fmt.Fprint(w, "\n") + if t.info != nil { + fmt.Fprintf( + w, + "%f%% of %d bytes (%s)", + 100*(1-float64(t.bytesMissingLocked())/float64(t.info.TotalLength())), + t.length(), + humanize.Bytes(uint64(t.length()))) + } else { + w.WriteString("") + } + fmt.Fprint(w, "\n") + t.writeStatus(w) + fmt.Fprintln(w) + } +} + +func (cl *Client) initLogger() { + logger := cl.config.Logger + if logger.IsZero() { + logger = log.Default + } + if cl.config.Debug { + logger = logger.FilterLevel(log.Debug) + } + cl.logger = logger.WithValues(cl) +} + +func (cl *Client) announceKey() int32 { + return int32(binary.BigEndian.Uint32(cl.peerID[16:20])) +} + +// Initializes a bare minimum Client. *Client and *ClientConfig must not be nil. +func (cl *Client) init(cfg *ClientConfig) { + cl.config = cfg + g.MakeMap(&cl.dopplegangerAddrs) + cl.torrents = make(map[metainfo.Hash]*Torrent) + cl.activeAnnounceLimiter.SlotsPerKey = 2 + cl.event.L = cl.locker() + cl.ipBlockList = cfg.IPBlocklist + cl.httpClient = &http.Client{ + Transport: cfg.WebTransport, + } + if cl.httpClient.Transport == nil { + cl.httpClient.Transport = &http.Transport{ + Proxy: cfg.HTTPProxy, + DialContext: cfg.HTTPDialContext, + // I think this value was observed from some webseeds. It seems reasonable to extend it + // to other uses of HTTP from the client. + MaxConnsPerHost: 10, + } + } +} + +func NewClient(cfg *ClientConfig) (cl *Client, err error) { + if cfg == nil { + cfg = NewDefaultClientConfig() + cfg.ListenPort = 0 + } + var client Client + client.init(cfg) + cl = &client + go cl.acceptLimitClearer() + cl.initLogger() + defer func() { + if err != nil { + cl.Close() + cl = nil + } + }() + + storageImpl := cfg.DefaultStorage + if storageImpl == nil { + // We'd use mmap by default but HFS+ doesn't support sparse files. + storageImplCloser := storage.NewFile(cfg.DataDir) + cl.onClose = append(cl.onClose, func() { + if err := storageImplCloser.Close(); err != nil { + cl.logger.Printf("error closing default storage: %s", err) + } + }) + storageImpl = storageImplCloser + } + cl.defaultStorage = storage.NewClient(storageImpl) + + if cfg.PeerID != "" { + missinggo.CopyExact(&cl.peerID, cfg.PeerID) + } else { + o := copy(cl.peerID[:], cfg.Bep20) + _, err = rand.Read(cl.peerID[o:]) + if err != nil { + panic("error generating peer id") + } + } + + sockets, err := listenAll(cl.listenNetworks(), cl.config.ListenHost, cl.config.ListenPort, cl.firewallCallback, cl.logger) + if err != nil { + return + } + + // Check for panics. + cl.LocalPort() + + for _, _s := range sockets { + s := _s // Go is fucking retarded. + cl.onClose = append(cl.onClose, func() { go s.Close() }) + if peerNetworkEnabled(parseNetworkString(s.Addr().Network()), cl.config) { + cl.dialers = append(cl.dialers, s) + cl.listeners = append(cl.listeners, s) + if cl.config.AcceptPeerConnections { + go cl.acceptConnections(s) + } + } + } + + go cl.forwardPort() + if !cfg.NoDHT { + for _, s := range sockets { + if pc, ok := s.(net.PacketConn); ok { + ds, err := cl.NewAnacrolixDhtServer(pc) + if err != nil { + panic(err) + } + cl.dhtServers = append(cl.dhtServers, AnacrolixDhtServerWrapper{ds}) + cl.onClose = append(cl.onClose, func() { ds.Close() }) + } + } + } + + cl.websocketTrackers = websocketTrackers{ + PeerId: cl.peerID, + Logger: cl.logger, + GetAnnounceRequest: func(event tracker.AnnounceEvent, infoHash [20]byte) (tracker.AnnounceRequest, error) { + cl.lock() + defer cl.unlock() + t, ok := cl.torrents[infoHash] + if !ok { + return tracker.AnnounceRequest{}, errors.New("torrent not tracked by client") + } + return t.announceRequest(event), nil + }, + Proxy: cl.config.HTTPProxy, + WebsocketTrackerHttpHeader: cl.config.WebsocketTrackerHttpHeader, + ICEServers: cl.config.ICEServers, + DialContext: cl.config.TrackerDialContext, + OnConn: func(dc datachannel.ReadWriteCloser, dcc webtorrent.DataChannelContext) { + cl.lock() + defer cl.unlock() + t, ok := cl.torrents[dcc.InfoHash] + if !ok { + cl.logger.WithDefaultLevel(log.Warning).Printf( + "got webrtc conn for unloaded torrent with infohash %x", + dcc.InfoHash, + ) + dc.Close() + return + } + go t.onWebRtcConn(dc, dcc) + }, + } + + return +} + +func (cl *Client) AddDhtServer(d DhtServer) { + cl.dhtServers = append(cl.dhtServers, d) +} + +// Adds a Dialer for outgoing connections. All Dialers are used when attempting to connect to a +// given address for any Torrent. +func (cl *Client) AddDialer(d Dialer) { + cl.lock() + defer cl.unlock() + cl.dialers = append(cl.dialers, d) + for _, t := range cl.torrents { + t.openNewConns() + } +} + +func (cl *Client) Listeners() []Listener { + return cl.listeners +} + +// Registers a Listener, and starts Accepting on it. You must Close Listeners provided this way +// yourself. +func (cl *Client) AddListener(l Listener) { + cl.listeners = append(cl.listeners, l) + if cl.config.AcceptPeerConnections { + go cl.acceptConnections(l) + } +} + +func (cl *Client) firewallCallback(net.Addr) bool { + cl.rLock() + block := !cl.wantConns() || !cl.config.AcceptPeerConnections + cl.rUnlock() + if block { + torrent.Add("connections firewalled", 1) + } else { + torrent.Add("connections not firewalled", 1) + } + return block +} + +func (cl *Client) listenOnNetwork(n network) bool { + if n.Ipv4 && cl.config.DisableIPv4 { + return false + } + if n.Ipv6 && cl.config.DisableIPv6 { + return false + } + if n.Tcp && cl.config.DisableTCP { + return false + } + if n.Udp && cl.config.DisableUTP && cl.config.NoDHT { + return false + } + return true +} + +func (cl *Client) listenNetworks() (ns []network) { + for _, n := range allPeerNetworks { + if cl.listenOnNetwork(n) { + ns = append(ns, n) + } + } + return +} + +// Creates an anacrolix/dht Server, as would be done internally in NewClient, for the given conn. +func (cl *Client) NewAnacrolixDhtServer(conn net.PacketConn) (s *dht.Server, err error) { + logger := cl.logger.WithNames("dht", conn.LocalAddr().String()) + cfg := dht.ServerConfig{ + IPBlocklist: cl.ipBlockList, + Conn: conn, + OnAnnouncePeer: cl.onDHTAnnouncePeer, + PublicIP: func() net.IP { + if connIsIpv6(conn) && cl.config.PublicIp6 != nil { + return cl.config.PublicIp6 + } + return cl.config.PublicIp4 + }(), + StartingNodes: cl.config.DhtStartingNodes(conn.LocalAddr().Network()), + OnQuery: cl.config.DHTOnQuery, + Logger: logger, + } + if f := cl.config.ConfigureAnacrolixDhtServer; f != nil { + f(&cfg) + } + s, err = dht.NewServer(&cfg) + if err == nil { + go s.TableMaintainer() + } + return +} + +func (cl *Client) Closed() events.Done { + return cl.closed.Done() +} + +func (cl *Client) eachDhtServer(f func(DhtServer)) { + for _, ds := range cl.dhtServers { + f(ds) + } +} + +// Stops the client. All connections to peers are closed and all activity will come to a halt. +func (cl *Client) Close() (errs []error) { + var closeGroup sync.WaitGroup // For concurrent cleanup to complete before returning + cl.lock() + for _, t := range cl.torrents { + err := t.close(&closeGroup) + if err != nil { + errs = append(errs, err) + } + } + for i := range cl.onClose { + cl.onClose[len(cl.onClose)-1-i]() + } + cl.closed.Set() + cl.unlock() + cl.event.Broadcast() + closeGroup.Wait() // defer is LIFO. We want to Wait() after cl.unlock() + return +} + +func (cl *Client) ipBlockRange(ip net.IP) (r iplist.Range, blocked bool) { + if cl.ipBlockList == nil { + return + } + return cl.ipBlockList.Lookup(ip) +} + +func (cl *Client) ipIsBlocked(ip net.IP) bool { + _, blocked := cl.ipBlockRange(ip) + return blocked +} + +func (cl *Client) wantConns() bool { + if cl.config.AlwaysWantConns { + return true + } + for _, t := range cl.torrents { + if t.wantIncomingConns() { + return true + } + } + return false +} + +// TODO: Apply filters for non-standard networks, particularly rate-limiting. +func (cl *Client) rejectAccepted(conn net.Conn) error { + if !cl.wantConns() { + return errors.New("don't want conns right now") + } + ra := conn.RemoteAddr() + if rip := addrIpOrNil(ra); rip != nil { + if cl.config.DisableIPv4Peers && rip.To4() != nil { + return errors.New("ipv4 peers disabled") + } + if cl.config.DisableIPv4 && len(rip) == net.IPv4len { + return errors.New("ipv4 disabled") + } + if cl.config.DisableIPv6 && len(rip) == net.IPv6len && rip.To4() == nil { + return errors.New("ipv6 disabled") + } + if cl.rateLimitAccept(rip) { + return errors.New("source IP accepted rate limited") + } + if cl.badPeerIPPort(rip, missinggo.AddrPort(ra)) { + return errors.New("bad source addr") + } + } + return nil +} + +func (cl *Client) acceptConnections(l Listener) { + for { + conn, err := l.Accept() + torrent.Add("client listener accepts", 1) + if err == nil { + holepunchAddr, holepunchErr := addrPortFromPeerRemoteAddr(conn.RemoteAddr()) + if holepunchErr == nil { + cl.lock() + if g.MapContains(cl.undialableWithoutHolepunch, holepunchAddr) { + setAdd(&cl.accepted, holepunchAddr) + } + if g.MapContains( + cl.undialableWithoutHolepunchDialedAfterHolepunchConnect, + holepunchAddr, + ) { + setAdd(&cl.probablyOnlyConnectedDueToHolepunch, holepunchAddr) + } + cl.unlock() + } + } + conn = pproffd.WrapNetConn(conn) + cl.rLock() + closed := cl.closed.IsSet() + var reject error + if !closed && conn != nil { + reject = cl.rejectAccepted(conn) + } + cl.rUnlock() + if closed { + if conn != nil { + conn.Close() + } + return + } + if err != nil { + log.Fmsg("error accepting connection: %s", err).LogLevel(log.Debug, cl.logger) + continue + } + go func() { + if reject != nil { + torrent.Add("rejected accepted connections", 1) + cl.logger.LazyLog(log.Debug, func() log.Msg { + return log.Fmsg("rejecting accepted conn: %v", reject) + }) + conn.Close() + } else { + go cl.incomingConnection(conn) + } + cl.logger.LazyLog(log.Debug, func() log.Msg { + return log.Fmsg("accepted %q connection at %q from %q", + l.Addr().Network(), + conn.LocalAddr(), + conn.RemoteAddr(), + ) + }) + torrent.Add(fmt.Sprintf("accepted conn remote IP len=%d", len(addrIpOrNil(conn.RemoteAddr()))), 1) + torrent.Add(fmt.Sprintf("accepted conn network=%s", conn.RemoteAddr().Network()), 1) + torrent.Add(fmt.Sprintf("accepted on %s listener", l.Addr().Network()), 1) + }() + } +} + +// Creates the PeerConn.connString for a regular net.Conn PeerConn. +func regularNetConnPeerConnConnString(nc net.Conn) string { + return fmt.Sprintf("%s-%s", nc.LocalAddr(), nc.RemoteAddr()) +} + +func (cl *Client) incomingConnection(nc net.Conn) { + defer nc.Close() + if tc, ok := nc.(*net.TCPConn); ok { + tc.SetLinger(0) + } + remoteAddr, _ := tryIpPortFromNetAddr(nc.RemoteAddr()) + c := cl.newConnection( + nc, + newConnectionOpts{ + outgoing: false, + remoteAddr: nc.RemoteAddr(), + localPublicAddr: cl.publicAddr(remoteAddr.IP), + network: nc.RemoteAddr().Network(), + connString: regularNetConnPeerConnConnString(nc), + }) + defer func() { + cl.lock() + defer cl.unlock() + c.close() + }() + c.Discovery = PeerSourceIncoming + cl.runReceivedConn(c) +} + +// Returns a handle to the given torrent, if it's present in the client. +func (cl *Client) Torrent(ih metainfo.Hash) (t *Torrent, ok bool) { + cl.rLock() + defer cl.rUnlock() + t, ok = cl.torrents[ih] + return +} + +func (cl *Client) torrent(ih metainfo.Hash) *Torrent { + return cl.torrents[ih] +} + +type DialResult struct { + Conn net.Conn + Dialer Dialer +} + +func countDialResult(err error) { + if err == nil { + torrent.Add("successful dials", 1) + } else { + torrent.Add("unsuccessful dials", 1) + } +} + +func reducedDialTimeout(minDialTimeout, max time.Duration, halfOpenLimit, pendingPeers int) (ret time.Duration) { + ret = max / time.Duration((pendingPeers+halfOpenLimit)/halfOpenLimit) + if ret < minDialTimeout { + ret = minDialTimeout + } + return +} + +// Returns whether an address is known to connect to a client with our own ID. +func (cl *Client) dopplegangerAddr(addr string) bool { + _, ok := cl.dopplegangerAddrs[addr] + return ok +} + +// Returns a connection over UTP or TCP, whichever is first to connect. +func (cl *Client) dialFirst(ctx context.Context, addr string) (res DialResult) { + return DialFirst(ctx, addr, cl.dialers) +} + +// Returns a connection over UTP or TCP, whichever is first to connect. +func DialFirst(ctx context.Context, addr string, dialers []Dialer) (res DialResult) { + pool := dialPool{ + addr: addr, + } + defer pool.startDrainer() + for _, _s := range dialers { + pool.add(ctx, _s) + } + return pool.getFirst() +} + +func dialFromSocket(ctx context.Context, s Dialer, addr string) net.Conn { + c, err := s.Dial(ctx, addr) + if err != nil { + log.Levelf(log.Debug, "error dialing %q: %v", addr, err) + } + // This is a bit optimistic, but it looks non-trivial to thread this through the proxy code. Set + // it now in case we close the connection forthwith. Note this is also done in the TCP dialer + // code to increase the chance it's done. + if tc, ok := c.(*net.TCPConn); ok { + tc.SetLinger(0) + } + countDialResult(err) + return c +} + +func (cl *Client) noLongerHalfOpen(t *Torrent, addr string, attemptKey outgoingConnAttemptKey) { + path := t.getHalfOpenPath(addr, attemptKey) + if !path.Exists() { + panic("should exist") + } + path.Delete() + cl.numHalfOpen-- + if cl.numHalfOpen < 0 { + panic("should not be possible") + } + for _, t := range cl.torrents { + t.openNewConns() + } +} + +func (cl *Client) countHalfOpenFromTorrents() (count int) { + for _, t := range cl.torrents { + count += t.numHalfOpenAttempts() + } + return +} + +// Performs initiator handshakes and returns a connection. Returns nil *PeerConn if no connection +// for valid reasons. +func (cl *Client) initiateProtocolHandshakes( + ctx context.Context, + nc net.Conn, + t *Torrent, + encryptHeader bool, + newConnOpts newConnectionOpts, +) ( + c *PeerConn, err error, +) { + c = cl.newConnection(nc, newConnOpts) + c.headerEncrypted = encryptHeader + ctx, cancel := context.WithTimeout(ctx, cl.config.HandshakesTimeout) + defer cancel() + dl, ok := ctx.Deadline() + if !ok { + panic(ctx) + } + err = nc.SetDeadline(dl) + if err != nil { + panic(err) + } + err = cl.initiateHandshakes(c, t) + return +} + +func doProtocolHandshakeOnDialResult( + t *Torrent, + obfuscatedHeader bool, + addr PeerRemoteAddr, + dr DialResult, +) ( + c *PeerConn, err error, +) { + cl := t.cl + nc := dr.Conn + addrIpPort, _ := tryIpPortFromNetAddr(addr) + c, err = cl.initiateProtocolHandshakes( + context.Background(), nc, t, obfuscatedHeader, + newConnectionOpts{ + outgoing: true, + remoteAddr: addr, + // It would be possible to retrieve a public IP from the dialer used here? + localPublicAddr: cl.publicAddr(addrIpPort.IP), + network: dr.Dialer.DialerNetwork(), + connString: regularNetConnPeerConnConnString(nc), + }) + if err != nil { + nc.Close() + } + return c, err +} + +// Returns nil connection and nil error if no connection could be established for valid reasons. +func (cl *Client) dialAndCompleteHandshake(opts outgoingConnOpts) (c *PeerConn, err error) { + // It would be better if dial rate limiting could be tested when considering to open connections + // instead. Doing it here means if the limit is low, and the half-open limit is high, we could + // end up with lots of outgoing connection attempts pending that were initiated on stale data. + { + dialReservation := cl.config.DialRateLimiter.Reserve() + if !opts.receivedHolepunchConnect { + if !dialReservation.OK() { + err = errors.New("can't make dial limit reservation") + return + } + time.Sleep(dialReservation.Delay()) + } + } + torrent.Add("establish outgoing connection", 1) + addr := opts.peerInfo.Addr + dialPool := dialPool{ + resCh: make(chan DialResult), + addr: addr.String(), + } + defer dialPool.startDrainer() + dialTimeout := opts.t.getDialTimeoutUnlocked() + { + ctx, cancel := context.WithTimeout(context.Background(), dialTimeout) + defer cancel() + for _, d := range cl.dialers { + dialPool.add(ctx, d) + } + } + holepunchAddr, holepunchAddrErr := addrPortFromPeerRemoteAddr(addr) + headerObfuscationPolicy := opts.HeaderObfuscationPolicy + obfuscatedHeaderFirst := headerObfuscationPolicy.Preferred + firstDialResult := dialPool.getFirst() + if firstDialResult.Conn == nil { + // No dialers worked. Try to initiate a holepunching rendezvous. + if holepunchAddrErr == nil { + cl.lock() + if !opts.receivedHolepunchConnect { + g.MakeMapIfNilAndSet(&cl.undialableWithoutHolepunch, holepunchAddr, struct{}{}) + } + if !opts.skipHolepunchRendezvous { + opts.t.trySendHolepunchRendezvous(holepunchAddr) + } + cl.unlock() + } + err = fmt.Errorf("all initial dials failed") + return + } + if opts.receivedHolepunchConnect && holepunchAddrErr == nil { + cl.lock() + if g.MapContains(cl.undialableWithoutHolepunch, holepunchAddr) { + g.MakeMapIfNilAndSet(&cl.dialableOnlyAfterHolepunch, holepunchAddr, struct{}{}) + } + g.MakeMapIfNil(&cl.dialedSuccessfullyAfterHolepunchConnect) + g.MapInsert(cl.dialedSuccessfullyAfterHolepunchConnect, holepunchAddr, struct{}{}) + cl.unlock() + } + c, err = doProtocolHandshakeOnDialResult( + opts.t, + obfuscatedHeaderFirst, + addr, + firstDialResult, + ) + if err == nil { + torrent.Add("initiated conn with preferred header obfuscation", 1) + return + } + c.logger.Levelf( + log.Debug, + "error doing protocol handshake with header obfuscation %v", + obfuscatedHeaderFirst, + ) + firstDialResult.Conn.Close() + // We should have just tried with the preferred header obfuscation. If it was required, there's nothing else to try. + if headerObfuscationPolicy.RequirePreferred { + return + } + // Reuse the dialer that returned already but failed to handshake. + { + ctx, cancel := context.WithTimeout(context.Background(), dialTimeout) + defer cancel() + dialPool.add(ctx, firstDialResult.Dialer) + } + secondDialResult := dialPool.getFirst() + if secondDialResult.Conn == nil { + return + } + c, err = doProtocolHandshakeOnDialResult( + opts.t, + !obfuscatedHeaderFirst, + addr, + secondDialResult, + ) + if err == nil { + torrent.Add("initiated conn with fallback header obfuscation", 1) + return + } + c.logger.Levelf( + log.Debug, + "error doing protocol handshake with header obfuscation %v", + !obfuscatedHeaderFirst, + ) + secondDialResult.Conn.Close() + return +} + +type outgoingConnOpts struct { + peerInfo PeerInfo + t *Torrent + // Don't attempt to connect unless a connect message is received after initiating a rendezvous. + requireRendezvous bool + // Don't send rendezvous requests to eligible relays. + skipHolepunchRendezvous bool + // Outgoing connection attempt is in response to holepunch connect message. + receivedHolepunchConnect bool + HeaderObfuscationPolicy HeaderObfuscationPolicy +} + +// Called to dial out and run a connection. The addr we're given is already +// considered half-open. +func (cl *Client) outgoingConnection( + opts outgoingConnOpts, + attemptKey outgoingConnAttemptKey, +) { + c, err := cl.dialAndCompleteHandshake(opts) + if err == nil { + c.conn.SetWriteDeadline(time.Time{}) + } + cl.lock() + defer cl.unlock() + // Don't release lock between here and addPeerConn, unless it's for failure. + cl.noLongerHalfOpen(opts.t, opts.peerInfo.Addr.String(), attemptKey) + if err != nil { + if cl.config.Debug { + cl.logger.Levelf( + log.Debug, + "error establishing outgoing connection to %v: %v", + opts.peerInfo.Addr, + err, + ) + } + return + } + defer c.close() + c.Discovery = opts.peerInfo.Source + c.trusted = opts.peerInfo.Trusted + opts.t.runHandshookConnLoggingErr(c) +} + +// The port number for incoming peer connections. 0 if the client isn't listening. +func (cl *Client) incomingPeerPort() int { + return cl.LocalPort() +} + +func (cl *Client) initiateHandshakes(c *PeerConn, t *Torrent) error { + if c.headerEncrypted { + var rw io.ReadWriter + var err error + rw, c.cryptoMethod, err = mse.InitiateHandshake( + struct { + io.Reader + io.Writer + }{c.r, c.w}, + t.infoHash[:], + nil, + cl.config.CryptoProvides, + ) + c.setRW(rw) + if err != nil { + return fmt.Errorf("header obfuscation handshake: %w", err) + } + } + ih, err := cl.connBtHandshake(c, &t.infoHash) + if err != nil { + return fmt.Errorf("bittorrent protocol handshake: %w", err) + } + if ih != t.infoHash { + return errors.New("bittorrent protocol handshake: peer infohash didn't match") + } + return nil +} + +// Calls f with any secret keys. Note that it takes the Client lock, and so must be used from code +// that won't also try to take the lock. This saves us copying all the infohashes everytime. +func (cl *Client) forSkeys(f func([]byte) bool) { + cl.rLock() + defer cl.rUnlock() + if false { // Emulate the bug from #114 + var firstIh InfoHash + for ih := range cl.torrents { + firstIh = ih + break + } + for range cl.torrents { + if !f(firstIh[:]) { + break + } + } + return + } + for ih := range cl.torrents { + if !f(ih[:]) { + break + } + } +} + +func (cl *Client) handshakeReceiverSecretKeys() mse.SecretKeyIter { + if ret := cl.config.Callbacks.ReceiveEncryptedHandshakeSkeys; ret != nil { + return ret + } + return cl.forSkeys +} + +// Do encryption and bittorrent handshakes as receiver. +func (cl *Client) receiveHandshakes(c *PeerConn) (t *Torrent, err error) { + defer perf.ScopeTimerErr(&err)() + var rw io.ReadWriter + rw, c.headerEncrypted, c.cryptoMethod, err = handleEncryption(c.rw(), cl.handshakeReceiverSecretKeys(), cl.config.HeaderObfuscationPolicy, cl.config.CryptoSelector) + c.setRW(rw) + if err == nil || err == mse.ErrNoSecretKeyMatch { + if c.headerEncrypted { + torrent.Add("handshakes received encrypted", 1) + } else { + torrent.Add("handshakes received unencrypted", 1) + } + } else { + torrent.Add("handshakes received with error while handling encryption", 1) + } + if err != nil { + if err == mse.ErrNoSecretKeyMatch { + err = nil + } + return + } + if cl.config.HeaderObfuscationPolicy.RequirePreferred && c.headerEncrypted != cl.config.HeaderObfuscationPolicy.Preferred { + err = errors.New("connection does not have required header obfuscation") + return + } + ih, err := cl.connBtHandshake(c, nil) + if err != nil { + return nil, fmt.Errorf("during bt handshake: %w", err) + } + cl.lock() + t = cl.torrents[ih] + cl.unlock() + return +} + +var successfulPeerWireProtocolHandshakePeerReservedBytes expvar.Map + +func init() { + torrent.Set( + "successful_peer_wire_protocol_handshake_peer_reserved_bytes", + &successfulPeerWireProtocolHandshakePeerReservedBytes) +} + +func (cl *Client) connBtHandshake(c *PeerConn, ih *metainfo.Hash) (ret metainfo.Hash, err error) { + res, err := pp.Handshake(c.rw(), ih, cl.peerID, cl.config.Extensions) + if err != nil { + return + } + successfulPeerWireProtocolHandshakePeerReservedBytes.Add( + hex.EncodeToString(res.PeerExtensionBits[:]), 1) + ret = res.Hash + c.PeerExtensionBytes = res.PeerExtensionBits + c.PeerID = res.PeerID + c.completedHandshake = time.Now() + if cb := cl.config.Callbacks.CompletedHandshake; cb != nil { + cb(c, res.Hash) + } + return +} + +func (cl *Client) runReceivedConn(c *PeerConn) { + err := c.conn.SetDeadline(time.Now().Add(cl.config.HandshakesTimeout)) + if err != nil { + panic(err) + } + t, err := cl.receiveHandshakes(c) + if err != nil { + cl.logger.LazyLog(log.Debug, func() log.Msg { + return log.Fmsg( + "error receiving handshakes on %v: %s", c, err, + ).Add( + "network", c.Network, + ) + }) + torrent.Add("error receiving handshake", 1) + cl.lock() + cl.onBadAccept(c.RemoteAddr) + cl.unlock() + return + } + if t == nil { + torrent.Add("received handshake for unloaded torrent", 1) + cl.logger.LazyLog(log.Debug, func() log.Msg { + return log.Fmsg("received handshake for unloaded torrent") + }) + cl.lock() + cl.onBadAccept(c.RemoteAddr) + cl.unlock() + return + } + torrent.Add("received handshake for loaded torrent", 1) + c.conn.SetWriteDeadline(time.Time{}) + cl.lock() + defer cl.unlock() + t.runHandshookConnLoggingErr(c) +} + +// Client lock must be held before entering this. +func (t *Torrent) runHandshookConn(pc *PeerConn) error { + pc.setTorrent(t) + cl := t.cl + for i, b := range cl.config.MinPeerExtensions { + if pc.PeerExtensionBytes[i]&b != b { + return fmt.Errorf("peer did not meet minimum peer extensions: %x", pc.PeerExtensionBytes[:]) + } + } + if pc.PeerID == cl.peerID { + if pc.outgoing { + connsToSelf.Add(1) + addr := pc.RemoteAddr.String() + cl.dopplegangerAddrs[addr] = struct{}{} + } /* else { + // Because the remote address is not necessarily the same as its client's torrent listen + // address, we won't record the remote address as a doppleganger. Instead, the initiator + // can record *us* as the doppleganger. + } */ + t.logger.Levelf(log.Debug, "local and remote peer ids are the same") + return nil + } + pc.r = deadlineReader{pc.conn, pc.r} + completedHandshakeConnectionFlags.Add(pc.connectionFlags(), 1) + if connIsIpv6(pc.conn) { + torrent.Add("completed handshake over ipv6", 1) + } + if err := t.addPeerConn(pc); err != nil { + return fmt.Errorf("adding connection: %w", err) + } + defer t.dropConnection(pc) + pc.startMessageWriter() + pc.sendInitialMessages() + pc.initUpdateRequestsTimer() + err := pc.mainReadLoop() + if err != nil { + return fmt.Errorf("main read loop: %w", err) + } + return nil +} + +func (p *Peer) initUpdateRequestsTimer() { + if check.Enabled { + if p.updateRequestsTimer != nil { + panic(p.updateRequestsTimer) + } + } + if enableUpdateRequestsTimer { + p.updateRequestsTimer = time.AfterFunc(math.MaxInt64, p.updateRequestsTimerFunc) + } +} + +const peerUpdateRequestsTimerReason = "updateRequestsTimer" + +func (c *Peer) updateRequestsTimerFunc() { + c.locker().Lock() + defer c.locker().Unlock() + if c.closed.IsSet() { + return + } + if c.isLowOnRequests() { + // If there are no outstanding requests, then a request update should have already run. + return + } + if d := time.Since(c.lastRequestUpdate); d < updateRequestsTimerDuration { + // These should be benign, Timer.Stop doesn't guarantee that its function won't run if it's + // already been fired. + torrent.Add("spurious timer requests updates", 1) + return + } + c.updateRequests(peerUpdateRequestsTimerReason) +} + +// Maximum pending requests we allow peers to send us. If peer requests are buffered on read, this +// instructs the amount of memory that might be used to cache pending writes. Assuming 512KiB +// (1<<19) cached for sending, for 16KiB (1<<14) chunks. +const localClientReqq = 1024 + +// See the order given in Transmission's tr_peerMsgsNew. +func (pc *PeerConn) sendInitialMessages() { + t := pc.t + cl := t.cl + if pc.PeerExtensionBytes.SupportsExtended() && cl.config.Extensions.SupportsExtended() { + pc.write(pp.Message{ + Type: pp.Extended, + ExtendedID: pp.HandshakeExtendedID, + ExtendedPayload: func() []byte { + msg := pp.ExtendedHandshakeMessage{ + M: map[pp.ExtensionName]pp.ExtensionNumber{ + pp.ExtensionNameMetadata: metadataExtendedId, + utHolepunch.ExtensionName: utHolepunchExtendedId, + }, + V: cl.config.ExtendedHandshakeClientVersion, + Reqq: localClientReqq, + YourIp: pp.CompactIp(pc.remoteIp()), + Encryption: cl.config.HeaderObfuscationPolicy.Preferred || !cl.config.HeaderObfuscationPolicy.RequirePreferred, + Port: cl.incomingPeerPort(), + MetadataSize: t.metadataSize(), + // TODO: We can figure these out specific to the socket used. + Ipv4: pp.CompactIp(cl.config.PublicIp4.To4()), + Ipv6: cl.config.PublicIp6.To16(), + } + if !cl.config.DisablePEX { + msg.M[pp.ExtensionNamePex] = pexExtendedId + } + return bencode.MustMarshal(msg) + }(), + }) + } + func() { + if pc.fastEnabled() { + if t.haveAllPieces() { + pc.write(pp.Message{Type: pp.HaveAll}) + pc.sentHaves.AddRange(0, bitmap.BitRange(pc.t.NumPieces())) + return + } else if !t.haveAnyPieces() { + pc.write(pp.Message{Type: pp.HaveNone}) + pc.sentHaves.Clear() + return + } + } + pc.postBitfield() + }() + if pc.PeerExtensionBytes.SupportsDHT() && cl.config.Extensions.SupportsDHT() && cl.haveDhtServer() { + pc.write(pp.Message{ + Type: pp.Port, + Port: cl.dhtPort(), + }) + } +} + +func (cl *Client) dhtPort() (ret uint16) { + if len(cl.dhtServers) == 0 { + return + } + return uint16(missinggo.AddrPort(cl.dhtServers[len(cl.dhtServers)-1].Addr())) +} + +func (cl *Client) haveDhtServer() bool { + return len(cl.dhtServers) > 0 +} + +// Process incoming ut_metadata message. +func (cl *Client) gotMetadataExtensionMsg(payload []byte, t *Torrent, c *PeerConn) error { + var d pp.ExtendedMetadataRequestMsg + err := bencode.Unmarshal(payload, &d) + if _, ok := err.(bencode.ErrUnusedTrailingBytes); ok { + } else if err != nil { + return fmt.Errorf("error unmarshalling bencode: %s", err) + } + piece := d.Piece + switch d.Type { + case pp.DataMetadataExtensionMsgType: + c.allStats(add(1, func(cs *ConnStats) *Count { return &cs.MetadataChunksRead })) + if !c.requestedMetadataPiece(piece) { + return fmt.Errorf("got unexpected piece %d", piece) + } + c.metadataRequests[piece] = false + begin := len(payload) - d.PieceSize() + if begin < 0 || begin >= len(payload) { + return fmt.Errorf("data has bad offset in payload: %d", begin) + } + t.saveMetadataPiece(piece, payload[begin:]) + c.lastUsefulChunkReceived = time.Now() + err = t.maybeCompleteMetadata() + if err != nil { + // Log this at the Torrent-level, as we don't partition metadata by Peer yet, so we + // don't know who to blame. TODO: Also errors can be returned here that aren't related + // to verifying metadata, which should be fixed. This should be tagged with metadata, so + // log consumers can filter for this message. + t.logger.WithDefaultLevel(log.Warning).Printf("error completing metadata: %v", err) + } + return err + case pp.RequestMetadataExtensionMsgType: + if !t.haveMetadataPiece(piece) { + c.write(t.newMetadataExtensionMessage(c, pp.RejectMetadataExtensionMsgType, d.Piece, nil)) + return nil + } + start := (1 << 14) * piece + c.logger.WithDefaultLevel(log.Debug).Printf("sending metadata piece %d", piece) + c.write(t.newMetadataExtensionMessage(c, pp.DataMetadataExtensionMsgType, piece, t.metadataBytes[start:start+t.metadataPieceSize(piece)])) + return nil + case pp.RejectMetadataExtensionMsgType: + return nil + default: + return errors.New("unknown msg_type value") + } +} + +func (cl *Client) badPeerAddr(addr PeerRemoteAddr) bool { + if ipa, ok := tryIpPortFromNetAddr(addr); ok { + return cl.badPeerIPPort(ipa.IP, ipa.Port) + } + return false +} + +// Returns whether the IP address and port are considered "bad". +func (cl *Client) badPeerIPPort(ip net.IP, port int) bool { + if port == 0 || ip == nil { + return true + } + if cl.dopplegangerAddr(net.JoinHostPort(ip.String(), strconv.FormatInt(int64(port), 10))) { + return true + } + if _, ok := cl.ipBlockRange(ip); ok { + return true + } + ipAddr, ok := netip.AddrFromSlice(ip) + if !ok { + panic(ip) + } + if _, ok := cl.badPeerIPs[ipAddr]; ok { + return true + } + return false +} + +// Return a Torrent ready for insertion into a Client. +func (cl *Client) newTorrent(ih metainfo.Hash, specStorage storage.ClientImpl) (t *Torrent) { + return cl.newTorrentOpt(AddTorrentOpts{ + InfoHash: ih, + Storage: specStorage, + }) +} + +// Return a Torrent ready for insertion into a Client. +func (cl *Client) newTorrentOpt(opts AddTorrentOpts) (t *Torrent) { + // use provided storage, if provided + storageClient := cl.defaultStorage + if opts.Storage != nil { + storageClient = storage.NewClient(opts.Storage) + } + + t = &Torrent{ + cl: cl, + infoHash: opts.InfoHash, + peers: prioritizedPeers{ + om: gbtree.New(32), + getPrio: func(p PeerInfo) peerPriority { + ipPort := p.addr() + return bep40PriorityIgnoreError(cl.publicAddr(ipPort.IP), ipPort) + }, + }, + conns: make(map[*PeerConn]struct{}, 2*cl.config.EstablishedConnsPerTorrent), + + storageOpener: storageClient, + maxEstablishedConns: cl.config.EstablishedConnsPerTorrent, + + metadataChanged: sync.Cond{ + L: cl.locker(), + }, + webSeeds: make(map[string]*Peer), + gotMetainfoC: make(chan struct{}), + } + t.smartBanCache.Hash = sha1.Sum + t.smartBanCache.Init() + t.networkingEnabled.Set() + t.logger = cl.logger.WithDefaultLevel(log.Debug) + t.sourcesLogger = t.logger.WithNames("sources") + if opts.ChunkSize == 0 { + opts.ChunkSize = defaultChunkSize + } + t.setChunkSize(opts.ChunkSize) + return +} + +// A file-like handle to some torrent data resource. +type Handle interface { + io.Reader + io.Seeker + io.Closer + io.ReaderAt +} + +func (cl *Client) AddTorrentInfoHash(infoHash metainfo.Hash) (t *Torrent, new bool) { + return cl.AddTorrentInfoHashWithStorage(infoHash, nil) +} + +// Adds a torrent by InfoHash with a custom Storage implementation. +// If the torrent already exists then this Storage is ignored and the +// existing torrent returned with `new` set to `false` +func (cl *Client) AddTorrentInfoHashWithStorage(infoHash metainfo.Hash, specStorage storage.ClientImpl) (t *Torrent, new bool) { + cl.lock() + defer cl.unlock() + t, ok := cl.torrents[infoHash] + if ok { + return + } + new = true + + t = cl.newTorrent(infoHash, specStorage) + cl.eachDhtServer(func(s DhtServer) { + if cl.config.PeriodicallyAnnounceTorrentsToDht { + go t.dhtAnnouncer(s) + } + }) + cl.torrents[infoHash] = t + cl.clearAcceptLimits() + t.updateWantPeersEvent() + // Tickle Client.waitAccept, new torrent may want conns. + cl.event.Broadcast() + return +} + +// Adds a torrent by InfoHash with a custom Storage implementation. If the torrent already exists +// then this Storage is ignored and the existing torrent returned with `new` set to `false`. +func (cl *Client) AddTorrentOpt(opts AddTorrentOpts) (t *Torrent, new bool) { + infoHash := opts.InfoHash + cl.lock() + defer cl.unlock() + t, ok := cl.torrents[infoHash] + if ok { + return + } + new = true + + t = cl.newTorrentOpt(opts) + cl.eachDhtServer(func(s DhtServer) { + if cl.config.PeriodicallyAnnounceTorrentsToDht { + go t.dhtAnnouncer(s) + } + }) + cl.torrents[infoHash] = t + t.setInfoBytesLocked(opts.InfoBytes) + cl.clearAcceptLimits() + t.updateWantPeersEvent() + // Tickle Client.waitAccept, new torrent may want conns. + cl.event.Broadcast() + return +} + +type AddTorrentOpts struct { + InfoHash infohash.T + Storage storage.ClientImpl + ChunkSize pp.Integer + InfoBytes []byte +} + +// Add or merge a torrent spec. Returns new if the torrent wasn't already in the client. See also +// Torrent.MergeSpec. +func (cl *Client) AddTorrentSpec(spec *TorrentSpec) (t *Torrent, new bool, err error) { + t, new = cl.AddTorrentOpt(AddTorrentOpts{ + InfoHash: spec.InfoHash, + Storage: spec.Storage, + ChunkSize: spec.ChunkSize, + }) + modSpec := *spec + if new { + // ChunkSize was already applied by adding a new Torrent, and MergeSpec disallows changing + // it. + modSpec.ChunkSize = 0 + } + err = t.MergeSpec(&modSpec) + if err != nil && new { + t.Drop() + } + return +} + +// The trackers will be merged with the existing ones. If the Info isn't yet known, it will be set. +// spec.DisallowDataDownload/Upload will be read and applied +// The display name is replaced if the new spec provides one. Note that any `Storage` is ignored. +func (t *Torrent) MergeSpec(spec *TorrentSpec) error { + if spec.DisplayName != "" { + t.SetDisplayName(spec.DisplayName) + } + if spec.InfoBytes != nil { + err := t.SetInfoBytes(spec.InfoBytes) + if err != nil { + return err + } + } + cl := t.cl + cl.AddDhtNodes(spec.DhtNodes) + t.UseSources(spec.Sources) + cl.lock() + defer cl.unlock() + t.initialPieceCheckDisabled = spec.DisableInitialPieceCheck + for _, url := range spec.Webseeds { + t.addWebSeed(url) + } + for _, peerAddr := range spec.PeerAddrs { + t.addPeer(PeerInfo{ + Addr: StringAddr(peerAddr), + Source: PeerSourceDirect, + Trusted: true, + }) + } + if spec.ChunkSize != 0 { + panic("chunk size cannot be changed for existing Torrent") + } + t.addTrackers(spec.Trackers) + t.maybeNewConns() + t.dataDownloadDisallowed.SetBool(spec.DisallowDataDownload) + t.dataUploadDisallowed = spec.DisallowDataUpload + return nil +} + +func (cl *Client) dropTorrent(infoHash metainfo.Hash, wg *sync.WaitGroup) (err error) { + t, ok := cl.torrents[infoHash] + if !ok { + err = fmt.Errorf("no such torrent") + return + } + err = t.close(wg) + delete(cl.torrents, infoHash) + return +} + +func (cl *Client) allTorrentsCompleted() bool { + for _, t := range cl.torrents { + if !t.haveInfo() { + return false + } + if !t.haveAllPieces() { + return false + } + } + return true +} + +// Returns true when all torrents are completely downloaded and false if the +// client is stopped before that. +func (cl *Client) WaitAll() bool { + cl.lock() + defer cl.unlock() + for !cl.allTorrentsCompleted() { + if cl.closed.IsSet() { + return false + } + cl.event.Wait() + } + return true +} + +// Returns handles to all the torrents loaded in the Client. +func (cl *Client) Torrents() []*Torrent { + cl.rLock() + defer cl.rUnlock() + return cl.torrentsAsSlice() +} + +func (cl *Client) torrentsAsSlice() (ret []*Torrent) { + for _, t := range cl.torrents { + ret = append(ret, t) + } + return +} + +func (cl *Client) AddMagnet(uri string) (T *Torrent, err error) { + spec, err := TorrentSpecFromMagnetUri(uri) + if err != nil { + return + } + T, _, err = cl.AddTorrentSpec(spec) + return +} + +func (cl *Client) AddTorrent(mi *metainfo.MetaInfo) (T *Torrent, err error) { + ts, err := TorrentSpecFromMetaInfoErr(mi) + if err != nil { + return + } + T, _, err = cl.AddTorrentSpec(ts) + return +} + +func (cl *Client) AddTorrentFromFile(filename string) (T *Torrent, err error) { + mi, err := metainfo.LoadFromFile(filename) + if err != nil { + return + } + return cl.AddTorrent(mi) +} + +func (cl *Client) DhtServers() []DhtServer { + return cl.dhtServers +} + +func (cl *Client) AddDhtNodes(nodes []string) { + for _, n := range nodes { + hmp := missinggo.SplitHostMaybePort(n) + ip := net.ParseIP(hmp.Host) + if ip == nil { + cl.logger.Printf("won't add DHT node with bad IP: %q", hmp.Host) + continue + } + ni := krpc.NodeInfo{ + Addr: krpc.NodeAddr{ + IP: ip, + Port: hmp.Port, + }, + } + cl.eachDhtServer(func(s DhtServer) { + s.AddNode(ni) + }) + } +} + +func (cl *Client) banPeerIP(ip net.IP) { + // We can't take this from string, because it will lose netip's v4on6. net.ParseIP parses v4 + // addresses directly to v4on6, which doesn't compare equal with v4. + ipAddr, ok := netip.AddrFromSlice(ip) + if !ok { + panic(ip) + } + g.MakeMapIfNilAndSet(&cl.badPeerIPs, ipAddr, struct{}{}) + for _, t := range cl.torrents { + t.iterPeers(func(p *Peer) { + if p.remoteIp().Equal(ip) { + t.logger.Levelf(log.Warning, "dropping peer %v with banned ip %v", p, ip) + // Should this be a close? + p.drop() + } + }) + } +} + +type newConnectionOpts struct { + outgoing bool + remoteAddr PeerRemoteAddr + localPublicAddr peerLocalPublicAddr + network string + connString string +} + +func (cl *Client) newConnection(nc net.Conn, opts newConnectionOpts) (c *PeerConn) { + if opts.network == "" { + panic(opts.remoteAddr) + } + c = &PeerConn{ + Peer: Peer{ + outgoing: opts.outgoing, + choking: true, + peerChoking: true, + PeerMaxRequests: 250, + + RemoteAddr: opts.remoteAddr, + localPublicAddr: opts.localPublicAddr, + Network: opts.network, + callbacks: &cl.config.Callbacks, + }, + connString: opts.connString, + conn: nc, + } + c.peerRequestDataAllocLimiter.Max = cl.config.MaxAllocPeerRequestDataPerConn + c.initRequestState() + // TODO: Need to be much more explicit about this, including allowing non-IP bannable addresses. + if opts.remoteAddr != nil { + netipAddrPort, err := netip.ParseAddrPort(opts.remoteAddr.String()) + if err == nil { + c.bannableAddr = Some(netipAddrPort.Addr()) + } + } + c.peerImpl = c + c.logger = cl.logger.WithDefaultLevel(log.Warning) + c.logger = c.logger.WithContextText(fmt.Sprintf("%T %p", c, c)) + c.setRW(connStatsReadWriter{nc, c}) + c.r = &rateLimitedReader{ + l: cl.config.DownloadRateLimiter, + r: c.r, + } + c.logger.Levelf( + log.Debug, + "inited with remoteAddr %v network %v outgoing %t", + opts.remoteAddr, opts.network, opts.outgoing, + ) + for _, f := range cl.config.Callbacks.NewPeer { + f(&c.Peer) + } + return +} + +func (cl *Client) onDHTAnnouncePeer(ih metainfo.Hash, ip net.IP, port int, portOk bool) { + cl.lock() + defer cl.unlock() + t := cl.torrent(ih) + if t == nil { + return + } + t.addPeers([]PeerInfo{{ + Addr: ipPortAddr{ip, port}, + Source: PeerSourceDhtAnnouncePeer, + }}) +} + +func firstNotNil(ips ...net.IP) net.IP { + for _, ip := range ips { + if ip != nil { + return ip + } + } + return nil +} + +func (cl *Client) eachListener(f func(Listener) bool) { + for _, s := range cl.listeners { + if !f(s) { + break + } + } +} + +func (cl *Client) findListener(f func(Listener) bool) (ret Listener) { + for i := 0; i < len(cl.listeners); i += 1 { + if ret = cl.listeners[i]; f(ret) { + return + } + } + return nil +} + +func (cl *Client) publicIp(peer net.IP) net.IP { + // TODO: Use BEP 10 to determine how peers are seeing us. + if peer.To4() != nil { + return firstNotNil( + cl.config.PublicIp4, + cl.findListenerIp(func(ip net.IP) bool { return ip.To4() != nil }), + ) + } + + return firstNotNil( + cl.config.PublicIp6, + cl.findListenerIp(func(ip net.IP) bool { return ip.To4() == nil }), + ) +} + +func (cl *Client) findListenerIp(f func(net.IP) bool) net.IP { + l := cl.findListener( + func(l Listener) bool { + return f(addrIpOrNil(l.Addr())) + }, + ) + if l == nil { + return nil + } + return addrIpOrNil(l.Addr()) +} + +// Our IP as a peer should see it. +func (cl *Client) publicAddr(peer net.IP) IpPort { + return IpPort{IP: cl.publicIp(peer), Port: uint16(cl.incomingPeerPort())} +} + +// ListenAddrs addresses currently being listened to. +func (cl *Client) ListenAddrs() (ret []net.Addr) { + cl.lock() + ret = make([]net.Addr, len(cl.listeners)) + for i := 0; i < len(cl.listeners); i += 1 { + ret[i] = cl.listeners[i].Addr() + } + cl.unlock() + return +} + +func (cl *Client) PublicIPs() (ips []net.IP) { + if ip := cl.config.PublicIp4; ip != nil { + ips = append(ips, ip) + } + if ip := cl.config.PublicIp6; ip != nil { + ips = append(ips, ip) + } + return +} + +func (cl *Client) onBadAccept(addr PeerRemoteAddr) { + ipa, ok := tryIpPortFromNetAddr(addr) + if !ok { + return + } + ip := maskIpForAcceptLimiting(ipa.IP) + if cl.acceptLimiter == nil { + cl.acceptLimiter = make(map[ipStr]int) + } + cl.acceptLimiter[ipStr(ip.String())]++ +} + +func maskIpForAcceptLimiting(ip net.IP) net.IP { + if ip4 := ip.To4(); ip4 != nil { + return ip4.Mask(net.CIDRMask(24, 32)) + } + return ip +} + +func (cl *Client) clearAcceptLimits() { + cl.acceptLimiter = nil +} + +func (cl *Client) acceptLimitClearer() { + for { + select { + case <-cl.closed.Done(): + return + case <-time.After(15 * time.Minute): + cl.lock() + cl.clearAcceptLimits() + cl.unlock() + } + } +} + +func (cl *Client) rateLimitAccept(ip net.IP) bool { + if cl.config.DisableAcceptRateLimiting { + return false + } + return cl.acceptLimiter[ipStr(maskIpForAcceptLimiting(ip).String())] > 0 +} + +func (cl *Client) rLock() { + cl._mu.RLock() +} + +func (cl *Client) rUnlock() { + cl._mu.RUnlock() +} + +func (cl *Client) lock() { + cl._mu.Lock() +} + +func (cl *Client) unlock() { + cl._mu.Unlock() +} + +func (cl *Client) locker() *lockWithDeferreds { + return &cl._mu +} + +func (cl *Client) String() string { + return fmt.Sprintf("<%[1]T %[1]p>", cl) +} + +// Returns connection-level aggregate connStats at the Client level. See the comment on +// TorrentStats.ConnStats. +func (cl *Client) ConnStats() ConnStats { + return cl.connStats.Copy() +} + +func (cl *Client) Stats() ClientStats { + cl.rLock() + defer cl.rUnlock() + return cl.statsLocked() +} diff --git a/deps/github.com/anacrolix/torrent/client_test.go b/deps/github.com/anacrolix/torrent/client_test.go new file mode 100644 index 0000000..d2a88e9 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/client_test.go @@ -0,0 +1,909 @@ +package torrent + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "net/netip" + "os" + "path/filepath" + "reflect" + "testing" + "testing/iotest" + "time" + + "github.com/anacrolix/dht/v2" + "github.com/anacrolix/log" + "github.com/anacrolix/missinggo/v2" + "github.com/anacrolix/missinggo/v2/filecache" + "github.com/frankban/quicktest" + qt "github.com/frankban/quicktest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anacrolix/torrent/bencode" + "github.com/anacrolix/torrent/internal/testutil" + "github.com/anacrolix/torrent/iplist" + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" +) + +func TestClientDefault(t *testing.T) { + cl, err := NewClient(TestingConfig(t)) + require.NoError(t, err) + require.Empty(t, cl.Close()) +} + +func TestClientNilConfig(t *testing.T) { + // The default config will put crap in the working directory. + origDir, _ := os.Getwd() + defer os.Chdir(origDir) + os.Chdir(t.TempDir()) + cl, err := NewClient(nil) + require.NoError(t, err) + require.Empty(t, cl.Close()) +} + +func TestAddDropTorrent(t *testing.T) { + cl, err := NewClient(TestingConfig(t)) + require.NoError(t, err) + defer cl.Close() + dir, mi := testutil.GreetingTestTorrent() + defer os.RemoveAll(dir) + tt, new, err := cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) + require.NoError(t, err) + assert.True(t, new) + tt.SetMaxEstablishedConns(0) + tt.SetMaxEstablishedConns(1) + tt.Drop() +} + +func TestAddTorrentNoSupportedTrackerSchemes(t *testing.T) { + // TODO? + t.SkipNow() +} + +func TestAddTorrentNoUsableURLs(t *testing.T) { + // TODO? + t.SkipNow() +} + +func TestAddPeersToUnknownTorrent(t *testing.T) { + // TODO? + t.SkipNow() +} + +func TestPieceHashSize(t *testing.T) { + assert.Equal(t, 20, pieceHash.Size()) +} + +func TestTorrentInitialState(t *testing.T) { + dir, mi := testutil.GreetingTestTorrent() + defer os.RemoveAll(dir) + var cl Client + cl.init(TestingConfig(t)) + cl.initLogger() + tor := cl.newTorrent( + mi.HashInfoBytes(), + storage.NewFileWithCompletion(t.TempDir(), storage.NewMapPieceCompletion()), + ) + tor.setChunkSize(2) + tor.cl.lock() + err := tor.setInfoBytesLocked(mi.InfoBytes) + tor.cl.unlock() + require.NoError(t, err) + require.Len(t, tor.pieces, 3) + tor.pendAllChunkSpecs(0) + tor.cl.lock() + assert.EqualValues(t, 3, tor.pieceNumPendingChunks(0)) + tor.cl.unlock() + assert.EqualValues(t, ChunkSpec{4, 1}, chunkIndexSpec(2, tor.pieceLength(0), tor.chunkSize)) +} + +func TestReducedDialTimeout(t *testing.T) { + cfg := NewDefaultClientConfig() + for _, _case := range []struct { + Max time.Duration + HalfOpenLimit int + PendingPeers int + ExpectedReduced time.Duration + }{ + {cfg.NominalDialTimeout, 40, 0, cfg.NominalDialTimeout}, + {cfg.NominalDialTimeout, 40, 1, cfg.NominalDialTimeout}, + {cfg.NominalDialTimeout, 40, 39, cfg.NominalDialTimeout}, + {cfg.NominalDialTimeout, 40, 40, cfg.NominalDialTimeout / 2}, + {cfg.NominalDialTimeout, 40, 80, cfg.NominalDialTimeout / 3}, + {cfg.NominalDialTimeout, 40, 4000, cfg.NominalDialTimeout / 101}, + } { + reduced := reducedDialTimeout(cfg.MinDialTimeout, _case.Max, _case.HalfOpenLimit, _case.PendingPeers) + expected := _case.ExpectedReduced + if expected < cfg.MinDialTimeout { + expected = cfg.MinDialTimeout + } + if reduced != expected { + t.Fatalf("expected %s, got %s", _case.ExpectedReduced, reduced) + } + } +} + +func TestAddDropManyTorrents(t *testing.T) { + cl, err := NewClient(TestingConfig(t)) + require.NoError(t, err) + defer cl.Close() + for i := 0; i < 1000; i += 1 { + var spec TorrentSpec + binary.PutVarint(spec.InfoHash[:], int64(i)) + tt, new, err := cl.AddTorrentSpec(&spec) + assert.NoError(t, err) + assert.True(t, new) + defer tt.Drop() + } +} + +func fileCachePieceResourceStorage(fc *filecache.Cache) storage.ClientImpl { + return storage.NewResourcePiecesOpts( + fc.AsResourceProvider(), + storage.ResourcePiecesOpts{ + LeaveIncompleteChunks: true, + }, + ) +} + +func TestMergingTrackersByAddingSpecs(t *testing.T) { + cl, err := NewClient(TestingConfig(t)) + require.NoError(t, err) + defer cl.Close() + spec := TorrentSpec{} + T, new, _ := cl.AddTorrentSpec(&spec) + if !new { + t.FailNow() + } + spec.Trackers = [][]string{{"http://a"}, {"udp://b"}} + _, new, _ = cl.AddTorrentSpec(&spec) + assert.False(t, new) + assert.EqualValues(t, [][]string{{"http://a"}, {"udp://b"}}, T.metainfo.AnnounceList) + // Because trackers are disabled in TestingConfig. + assert.EqualValues(t, 0, len(T.trackerAnnouncers)) +} + +// We read from a piece which is marked completed, but is missing data. +func TestCompletedPieceWrongSize(t *testing.T) { + cfg := TestingConfig(t) + cfg.DefaultStorage = badStorage{} + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + info := metainfo.Info{ + PieceLength: 15, + Pieces: make([]byte, 20), + Files: []metainfo.FileInfo{ + {Path: []string{"greeting"}, Length: 13}, + }, + } + b, err := bencode.Marshal(info) + require.NoError(t, err) + tt, new, err := cl.AddTorrentSpec(&TorrentSpec{ + InfoBytes: b, + InfoHash: metainfo.HashBytes(b), + }) + require.NoError(t, err) + defer tt.Drop() + assert.True(t, new) + r := tt.NewReader() + defer r.Close() + quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil) +} + +func BenchmarkAddLargeTorrent(b *testing.B) { + cfg := TestingConfig(b) + cfg.DisableTCP = true + cfg.DisableUTP = true + cl, err := NewClient(cfg) + require.NoError(b, err) + defer cl.Close() + b.ReportAllocs() + for i := 0; i < b.N; i += 1 { + t, err := cl.AddTorrentFromFile("testdata/bootstrap.dat.torrent") + if err != nil { + b.Fatal(err) + } + t.Drop() + } +} + +func TestResponsive(t *testing.T) { + seederDataDir, mi := testutil.GreetingTestTorrent() + defer os.RemoveAll(seederDataDir) + cfg := TestingConfig(t) + cfg.Seed = true + cfg.DataDir = seederDataDir + seeder, err := NewClient(cfg) + require.Nil(t, err) + defer seeder.Close() + seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) + seederTorrent.VerifyData() + leecherDataDir := t.TempDir() + cfg = TestingConfig(t) + cfg.DataDir = leecherDataDir + leecher, err := NewClient(cfg) + require.Nil(t, err) + defer leecher.Close() + leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { + ret = TorrentSpecFromMetaInfo(mi) + ret.ChunkSize = 2 + return + }()) + leecherTorrent.AddClientPeer(seeder) + reader := leecherTorrent.NewReader() + defer reader.Close() + reader.SetReadahead(0) + reader.SetResponsive() + b := make([]byte, 2) + _, err = reader.Seek(3, io.SeekStart) + require.NoError(t, err) + _, err = io.ReadFull(reader, b) + assert.Nil(t, err) + assert.EqualValues(t, "lo", string(b)) + _, err = reader.Seek(11, io.SeekStart) + require.NoError(t, err) + n, err := io.ReadFull(reader, b) + assert.Nil(t, err) + assert.EqualValues(t, 2, n) + assert.EqualValues(t, "d\n", string(b)) +} + +// TestResponsive was the first test to fail if uTP is disabled and TCP sockets dial from the +// listening port. +func TestResponsiveTcpOnly(t *testing.T) { + seederDataDir, mi := testutil.GreetingTestTorrent() + defer os.RemoveAll(seederDataDir) + cfg := TestingConfig(t) + cfg.DisableUTP = true + cfg.Seed = true + cfg.DataDir = seederDataDir + seeder, err := NewClient(cfg) + require.Nil(t, err) + defer seeder.Close() + seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) + seederTorrent.VerifyData() + leecherDataDir := t.TempDir() + cfg = TestingConfig(t) + cfg.DataDir = leecherDataDir + leecher, err := NewClient(cfg) + require.Nil(t, err) + defer leecher.Close() + leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { + ret = TorrentSpecFromMetaInfo(mi) + ret.ChunkSize = 2 + return + }()) + leecherTorrent.AddClientPeer(seeder) + reader := leecherTorrent.NewReader() + defer reader.Close() + reader.SetReadahead(0) + reader.SetResponsive() + b := make([]byte, 2) + _, err = reader.Seek(3, io.SeekStart) + require.NoError(t, err) + _, err = io.ReadFull(reader, b) + assert.Nil(t, err) + assert.EqualValues(t, "lo", string(b)) + _, err = reader.Seek(11, io.SeekStart) + require.NoError(t, err) + n, err := io.ReadFull(reader, b) + assert.Nil(t, err) + assert.EqualValues(t, 2, n) + assert.EqualValues(t, "d\n", string(b)) +} + +func TestTorrentDroppedDuringResponsiveRead(t *testing.T) { + seederDataDir, mi := testutil.GreetingTestTorrent() + defer os.RemoveAll(seederDataDir) + cfg := TestingConfig(t) + cfg.Seed = true + cfg.DataDir = seederDataDir + seeder, err := NewClient(cfg) + require.Nil(t, err) + defer seeder.Close() + seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) + seederTorrent.VerifyData() + leecherDataDir := t.TempDir() + cfg = TestingConfig(t) + cfg.DataDir = leecherDataDir + leecher, err := NewClient(cfg) + require.Nil(t, err) + defer leecher.Close() + leecherTorrent, _, _ := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { + ret = TorrentSpecFromMetaInfo(mi) + ret.ChunkSize = 2 + return + }()) + leecherTorrent.AddClientPeer(seeder) + reader := leecherTorrent.NewReader() + defer reader.Close() + reader.SetReadahead(0) + reader.SetResponsive() + b := make([]byte, 2) + _, err = reader.Seek(3, io.SeekStart) + require.NoError(t, err) + _, err = io.ReadFull(reader, b) + assert.Nil(t, err) + assert.EqualValues(t, "lo", string(b)) + _, err = reader.Seek(11, io.SeekStart) + require.NoError(t, err) + leecherTorrent.Drop() + n, err := reader.Read(b) + assert.EqualError(t, err, "torrent closed") + assert.EqualValues(t, 0, n) +} + +func TestDhtInheritBlocklist(t *testing.T) { + ipl := iplist.New(nil) + require.NotNil(t, ipl) + cfg := TestingConfig(t) + cfg.IPBlocklist = ipl + cfg.NoDHT = false + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + numServers := 0 + cl.eachDhtServer(func(s DhtServer) { + t.Log(s) + assert.Equal(t, ipl, s.(AnacrolixDhtServerWrapper).Server.IPBlocklist()) + numServers++ + }) + assert.EqualValues(t, 2, numServers) +} + +// Check that stuff is merged in subsequent AddTorrentSpec for the same +// infohash. +func TestAddTorrentSpecMerging(t *testing.T) { + cl, err := NewClient(TestingConfig(t)) + require.NoError(t, err) + defer cl.Close() + dir, mi := testutil.GreetingTestTorrent() + defer os.RemoveAll(dir) + tt, new, err := cl.AddTorrentSpec(&TorrentSpec{ + InfoHash: mi.HashInfoBytes(), + }) + require.NoError(t, err) + require.True(t, new) + require.Nil(t, tt.Info()) + _, new, err = cl.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) + require.NoError(t, err) + require.False(t, new) + require.NotNil(t, tt.Info()) +} + +func TestTorrentDroppedBeforeGotInfo(t *testing.T) { + dir, mi := testutil.GreetingTestTorrent() + os.RemoveAll(dir) + cl, _ := NewClient(TestingConfig(t)) + defer cl.Close() + tt, _, _ := cl.AddTorrentSpec(&TorrentSpec{ + InfoHash: mi.HashInfoBytes(), + }) + tt.Drop() + assert.EqualValues(t, 0, len(cl.Torrents())) + select { + case <-tt.GotInfo(): + t.FailNow() + default: + } +} + +func writeTorrentData(ts *storage.Torrent, info metainfo.Info, b []byte) { + for i := 0; i < info.NumPieces(); i += 1 { + p := info.Piece(i) + ts.Piece(p).WriteAt(b[p.Offset():p.Offset()+p.Length()], 0) + } +} + +func testAddTorrentPriorPieceCompletion(t *testing.T, alreadyCompleted bool, csf func(*filecache.Cache) storage.ClientImpl) { + fileCacheDir := t.TempDir() + fileCache, err := filecache.NewCache(fileCacheDir) + require.NoError(t, err) + greetingDataTempDir, greetingMetainfo := testutil.GreetingTestTorrent() + defer os.RemoveAll(greetingDataTempDir) + filePieceStore := csf(fileCache) + info, err := greetingMetainfo.UnmarshalInfo() + require.NoError(t, err) + ih := greetingMetainfo.HashInfoBytes() + greetingData, err := storage.NewClient(filePieceStore).OpenTorrent(&info, ih) + require.NoError(t, err) + writeTorrentData(greetingData, info, []byte(testutil.GreetingFileContents)) + // require.Equal(t, len(testutil.GreetingFileContents), written) + // require.NoError(t, err) + for i := 0; i < info.NumPieces(); i++ { + p := info.Piece(i) + if alreadyCompleted { + require.NoError(t, greetingData.Piece(p).MarkComplete()) + } + } + cfg := TestingConfig(t) + // TODO: Disable network option? + cfg.DisableTCP = true + cfg.DisableUTP = true + cfg.DefaultStorage = filePieceStore + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + tt, err := cl.AddTorrent(greetingMetainfo) + require.NoError(t, err) + psrs := tt.PieceStateRuns() + assert.Len(t, psrs, 1) + assert.EqualValues(t, 3, psrs[0].Length) + assert.Equal(t, alreadyCompleted, psrs[0].Complete) + if alreadyCompleted { + r := tt.NewReader() + quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil) + } +} + +func TestAddTorrentPiecesAlreadyCompleted(t *testing.T) { + testAddTorrentPriorPieceCompletion(t, true, fileCachePieceResourceStorage) +} + +func TestAddTorrentPiecesNotAlreadyCompleted(t *testing.T) { + testAddTorrentPriorPieceCompletion(t, false, fileCachePieceResourceStorage) +} + +func TestAddMetainfoWithNodes(t *testing.T) { + cfg := TestingConfig(t) + cfg.ListenHost = func(string) string { return "" } + cfg.NoDHT = false + cfg.DhtStartingNodes = func(string) dht.StartingNodesGetter { return func() ([]dht.Addr, error) { return nil, nil } } + // For now, we want to just jam the nodes into the table, without verifying them first. Also the + // DHT code doesn't support mixing secure and insecure nodes if security is enabled (yet). + // cfg.DHTConfig.NoSecurity = true + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + sum := func() (ret int64) { + cl.eachDhtServer(func(s DhtServer) { + ret += s.Stats().(dht.ServerStats).OutboundQueriesAttempted + }) + return + } + assert.EqualValues(t, 0, sum()) + tt, err := cl.AddTorrentFromFile("metainfo/testdata/issue_65a.torrent") + require.NoError(t, err) + // Nodes are not added or exposed in Torrent's metainfo. We just randomly + // check if the announce-list is here instead. TODO: Add nodes. + assert.Len(t, tt.metainfo.AnnounceList, 5) + // There are 6 nodes in the torrent file. + for sum() != int64(6*len(cl.dhtServers)) { + time.Sleep(time.Millisecond) + } +} + +type testDownloadCancelParams struct { + SetLeecherStorageCapacity bool + LeecherStorageCapacity int64 + Cancel bool +} + +func testDownloadCancel(t *testing.T, ps testDownloadCancelParams) { + greetingTempDir, mi := testutil.GreetingTestTorrent() + defer os.RemoveAll(greetingTempDir) + cfg := TestingConfig(t) + cfg.Seed = true + cfg.DataDir = greetingTempDir + seeder, err := NewClient(cfg) + require.NoError(t, err) + defer seeder.Close() + defer testutil.ExportStatusWriter(seeder, "s", t)() + seederTorrent, _, _ := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi)) + seederTorrent.VerifyData() + leecherDataDir := t.TempDir() + fc, err := filecache.NewCache(leecherDataDir) + require.NoError(t, err) + if ps.SetLeecherStorageCapacity { + fc.SetCapacity(ps.LeecherStorageCapacity) + } + cfg.DefaultStorage = storage.NewResourcePieces(fc.AsResourceProvider()) + cfg.DataDir = leecherDataDir + leecher, err := NewClient(cfg) + require.NoError(t, err) + defer leecher.Close() + defer testutil.ExportStatusWriter(leecher, "l", t)() + leecherGreeting, new, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) { + ret = TorrentSpecFromMetaInfo(mi) + ret.ChunkSize = 2 + return + }()) + require.NoError(t, err) + assert.True(t, new) + psc := leecherGreeting.SubscribePieceStateChanges() + defer psc.Close() + + leecherGreeting.cl.lock() + leecherGreeting.downloadPiecesLocked(0, leecherGreeting.numPieces()) + if ps.Cancel { + leecherGreeting.cancelPiecesLocked(0, leecherGreeting.NumPieces(), "") + } + leecherGreeting.cl.unlock() + done := make(chan struct{}) + defer close(done) + go leecherGreeting.AddClientPeer(seeder) + completes := make(map[int]bool, 3) + expected := func() map[int]bool { + if ps.Cancel { + return map[int]bool{0: false, 1: false, 2: false} + } else { + return map[int]bool{0: true, 1: true, 2: true} + } + }() + for !reflect.DeepEqual(completes, expected) { + v := <-psc.Values + completes[v.Index] = v.Complete + } +} + +func TestTorrentDownloadAll(t *testing.T) { + testDownloadCancel(t, testDownloadCancelParams{}) +} + +func TestTorrentDownloadAllThenCancel(t *testing.T) { + testDownloadCancel(t, testDownloadCancelParams{ + Cancel: true, + }) +} + +// Ensure that it's an error for a peer to send an invalid have message. +func TestPeerInvalidHave(t *testing.T) { + cfg := TestingConfig(t) + cfg.DropMutuallyCompletePeers = false + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + info := metainfo.Info{ + PieceLength: 1, + Pieces: make([]byte, 20), + Files: []metainfo.FileInfo{{Length: 1}}, + } + infoBytes, err := bencode.Marshal(info) + require.NoError(t, err) + tt, _new, err := cl.AddTorrentSpec(&TorrentSpec{ + InfoBytes: infoBytes, + InfoHash: metainfo.HashBytes(infoBytes), + Storage: badStorage{}, + }) + require.NoError(t, err) + assert.True(t, _new) + defer tt.Drop() + cn := &PeerConn{Peer: Peer{ + t: tt, + callbacks: &cfg.Callbacks, + }} + tt.conns[cn] = struct{}{} + cn.peerImpl = cn + cl.lock() + defer cl.unlock() + assert.NoError(t, cn.peerSentHave(0)) + assert.Error(t, cn.peerSentHave(1)) +} + +func TestPieceCompletedInStorageButNotClient(t *testing.T) { + greetingTempDir, greetingMetainfo := testutil.GreetingTestTorrent() + defer os.RemoveAll(greetingTempDir) + cfg := TestingConfig(t) + cfg.DataDir = greetingTempDir + seeder, err := NewClient(TestingConfig(t)) + require.NoError(t, err) + defer seeder.Close() + seeder.AddTorrentSpec(&TorrentSpec{ + InfoBytes: greetingMetainfo.InfoBytes, + }) +} + +// Check that when the listen port is 0, all the protocols listened on have +// the same port, and it isn't zero. +func TestClientDynamicListenPortAllProtocols(t *testing.T) { + cl, err := NewClient(TestingConfig(t)) + require.NoError(t, err) + defer cl.Close() + port := cl.LocalPort() + assert.NotEqual(t, 0, port) + cl.eachListener(func(s Listener) bool { + assert.Equal(t, port, missinggo.AddrPort(s.Addr())) + return true + }) +} + +func TestClientDynamicListenTCPOnly(t *testing.T) { + cfg := TestingConfig(t) + cfg.DisableUTP = true + cfg.DisableTCP = false + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + assert.NotEqual(t, 0, cl.LocalPort()) +} + +func TestClientDynamicListenUTPOnly(t *testing.T) { + cfg := TestingConfig(t) + cfg.DisableTCP = true + cfg.DisableUTP = false + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + assert.NotEqual(t, 0, cl.LocalPort()) +} + +func totalConns(tts []*Torrent) (ret int) { + for _, tt := range tts { + tt.cl.lock() + ret += len(tt.conns) + tt.cl.unlock() + } + return +} + +func TestSetMaxEstablishedConn(t *testing.T) { + var tts []*Torrent + ih := testutil.GreetingMetaInfo().HashInfoBytes() + cfg := TestingConfig(t) + cfg.DisableAcceptRateLimiting = true + cfg.DropDuplicatePeerIds = true + for i := 0; i < 3; i += 1 { + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + tt, _ := cl.AddTorrentInfoHash(ih) + tt.SetMaxEstablishedConns(2) + defer testutil.ExportStatusWriter(cl, fmt.Sprintf("%d", i), t)() + tts = append(tts, tt) + } + addPeers := func() { + for _, tt := range tts { + for _, _tt := range tts { + // if tt != _tt { + tt.AddClientPeer(_tt.cl) + // } + } + } + } + waitTotalConns := func(num int) { + for totalConns(tts) != num { + addPeers() + time.Sleep(time.Millisecond) + } + } + addPeers() + waitTotalConns(6) + tts[0].SetMaxEstablishedConns(1) + waitTotalConns(4) + tts[0].SetMaxEstablishedConns(0) + waitTotalConns(2) + tts[0].SetMaxEstablishedConns(1) + addPeers() + waitTotalConns(4) + tts[0].SetMaxEstablishedConns(2) + addPeers() + waitTotalConns(6) +} + +// Creates a file containing its own name as data. Make a metainfo from that, adds it to the given +// client, and returns a magnet link. +func makeMagnet(t *testing.T, cl *Client, dir, name string) string { + os.MkdirAll(dir, 0o770) + file, err := os.Create(filepath.Join(dir, name)) + require.NoError(t, err) + file.Write([]byte(name)) + file.Close() + mi := metainfo.MetaInfo{} + mi.SetDefaults() + info := metainfo.Info{PieceLength: 256 * 1024} + err = info.BuildFromFilePath(filepath.Join(dir, name)) + require.NoError(t, err) + mi.InfoBytes, err = bencode.Marshal(info) + require.NoError(t, err) + magnet := mi.Magnet(nil, &info).String() + tr, err := cl.AddTorrent(&mi) + require.NoError(t, err) + require.True(t, tr.Seeding()) + tr.VerifyData() + return magnet +} + +// https://github.com/anacrolix/torrent/issues/114 +func TestMultipleTorrentsWithEncryption(t *testing.T) { + testSeederLeecherPair( + t, + func(cfg *ClientConfig) { + cfg.HeaderObfuscationPolicy.Preferred = true + cfg.HeaderObfuscationPolicy.RequirePreferred = true + }, + func(cfg *ClientConfig) { + cfg.HeaderObfuscationPolicy.RequirePreferred = false + }, + ) +} + +// Test that the leecher can download a torrent in its entirety from the seeder. Note that the +// seeder config is done first. +func testSeederLeecherPair(t *testing.T, seeder, leecher func(*ClientConfig)) { + cfg := TestingConfig(t) + cfg.Seed = true + cfg.DataDir = filepath.Join(cfg.DataDir, "server") + os.Mkdir(cfg.DataDir, 0o755) + seeder(cfg) + server, err := NewClient(cfg) + require.NoError(t, err) + defer server.Close() + defer testutil.ExportStatusWriter(server, "s", t)() + magnet1 := makeMagnet(t, server, cfg.DataDir, "test1") + // Extra torrents are added to test the seeder having to match incoming obfuscated headers + // against more than one torrent. See issue #114 + makeMagnet(t, server, cfg.DataDir, "test2") + for i := 0; i < 100; i++ { + makeMagnet(t, server, cfg.DataDir, fmt.Sprintf("test%d", i+2)) + } + cfg = TestingConfig(t) + cfg.DataDir = filepath.Join(cfg.DataDir, "client") + leecher(cfg) + client, err := NewClient(cfg) + require.NoError(t, err) + defer client.Close() + defer testutil.ExportStatusWriter(client, "c", t)() + tr, err := client.AddMagnet(magnet1) + require.NoError(t, err) + tr.AddClientPeer(server) + <-tr.GotInfo() + tr.DownloadAll() + client.WaitAll() +} + +// This appears to be the situation with the S3 BitTorrent client. +func TestObfuscatedHeaderFallbackSeederDisallowsLeecherPrefers(t *testing.T) { + // Leecher prefers obfuscation, but the seeder does not allow it. + testSeederLeecherPair( + t, + func(cfg *ClientConfig) { + cfg.HeaderObfuscationPolicy.Preferred = false + cfg.HeaderObfuscationPolicy.RequirePreferred = true + }, + func(cfg *ClientConfig) { + cfg.HeaderObfuscationPolicy.Preferred = true + cfg.HeaderObfuscationPolicy.RequirePreferred = false + }, + ) +} + +func TestObfuscatedHeaderFallbackSeederRequiresLeecherPrefersNot(t *testing.T) { + // Leecher prefers no obfuscation, but the seeder enforces it. + testSeederLeecherPair( + t, + func(cfg *ClientConfig) { + cfg.HeaderObfuscationPolicy.Preferred = true + cfg.HeaderObfuscationPolicy.RequirePreferred = true + }, + func(cfg *ClientConfig) { + cfg.HeaderObfuscationPolicy.Preferred = false + cfg.HeaderObfuscationPolicy.RequirePreferred = false + }, + ) +} + +func TestClientAddressInUse(t *testing.T) { + s, _ := NewUtpSocket("udp", "localhost:50007", nil, log.Default) + if s != nil { + defer s.Close() + } + cfg := TestingConfig(t).SetListenAddr("localhost:50007") + cfg.DisableUTP = false + cl, err := NewClient(cfg) + if err == nil { + assert.Nil(t, cl.Close()) + } + require.Error(t, err) + require.Nil(t, cl) +} + +func TestClientHasDhtServersWhenUtpDisabled(t *testing.T) { + cc := TestingConfig(t) + cc.DisableUTP = true + cc.NoDHT = false + cl, err := NewClient(cc) + require.NoError(t, err) + defer cl.Close() + assert.NotEmpty(t, cl.DhtServers()) +} + +func TestClientDisabledImplicitNetworksButDhtEnabled(t *testing.T) { + cfg := TestingConfig(t) + cfg.DisableTCP = true + cfg.DisableUTP = true + cfg.NoDHT = false + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + assert.Empty(t, cl.listeners) + assert.NotEmpty(t, cl.DhtServers()) +} + +func TestBadPeerIpPort(t *testing.T) { + for _, tc := range []struct { + title string + ip net.IP + port int + expectedOk bool + setup func(*Client) + }{ + {"empty both", nil, 0, true, func(*Client) {}}, + {"empty/nil ip", nil, 6666, true, func(*Client) {}}, + { + "empty port", + net.ParseIP("127.0.0.1/32"), + 0, true, + func(*Client) {}, + }, + { + "in doppleganger addresses", + net.ParseIP("127.0.0.1/32"), + 2322, + true, + func(cl *Client) { + cl.dopplegangerAddrs["10.0.0.1:2322"] = struct{}{} + }, + }, + { + "in IP block list", + net.ParseIP("10.0.0.1"), + 2322, + true, + func(cl *Client) { + cl.ipBlockList = iplist.New([]iplist.Range{ + {First: net.ParseIP("10.0.0.1"), Last: net.ParseIP("10.0.0.255")}, + }) + }, + }, + { + "in bad peer IPs", + net.ParseIP("10.0.0.1"), + 2322, + true, + func(cl *Client) { + ipAddr, ok := netip.AddrFromSlice(net.ParseIP("10.0.0.1")) + require.True(t, ok) + cl.badPeerIPs = map[netip.Addr]struct{}{} + cl.badPeerIPs[ipAddr] = struct{}{} + }, + }, + { + "good", + net.ParseIP("10.0.0.1"), + 2322, + false, + func(cl *Client) {}, + }, + } { + t.Run(tc.title, func(t *testing.T) { + cfg := TestingConfig(t) + cfg.DisableTCP = true + cfg.DisableUTP = true + cfg.NoDHT = false + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + + tc.setup(cl) + require.Equal(t, tc.expectedOk, cl.badPeerIPPort(tc.ip, tc.port)) + }) + } +} + +// https://github.com/anacrolix/torrent/issues/837 +func TestClientConfigSetHandlerNotIgnored(t *testing.T) { + cfg := TestingConfig(t) + cfg.Logger.SetHandlers(log.DiscardHandler) + c := qt.New(t) + cl, err := NewClient(cfg) + c.Assert(err, qt.IsNil) + defer cl.Close() + c.Assert(cl.logger.Handlers, qt.HasLen, 1) + h := cl.logger.Handlers[0].(log.StreamHandler) + c.Check(h.W, qt.Equals, io.Discard) +} diff --git a/deps/github.com/anacrolix/torrent/cmd/magnet-metainfo/main.go b/deps/github.com/anacrolix/torrent/cmd/magnet-metainfo/main.go new file mode 100644 index 0000000..536f7ab --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/magnet-metainfo/main.go @@ -0,0 +1,59 @@ +// Converts magnet URIs and info hashes into torrent metainfo files. +package main + +import ( + "log" + "net/http" + "os" + "sync" + + _ "github.com/anacrolix/envpprof" + "github.com/anacrolix/tagflag" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/bencode" +) + +func main() { + args := struct { + tagflag.StartPos + Magnet []string + }{} + tagflag.Parse(&args) + cl, err := torrent.NewClient(nil) + if err != nil { + log.Fatalf("error creating client: %s", err) + } + http.HandleFunc("/torrent", func(w http.ResponseWriter, r *http.Request) { + cl.WriteStatus(w) + }) + http.HandleFunc("/dht", func(w http.ResponseWriter, r *http.Request) { + for _, ds := range cl.DhtServers() { + ds.WriteStatus(w) + } + }) + wg := sync.WaitGroup{} + for _, arg := range args.Magnet { + t, err := cl.AddMagnet(arg) + if err != nil { + log.Fatalf("error adding magnet to client: %s", err) + } + wg.Add(1) + go func() { + defer wg.Done() + <-t.GotInfo() + mi := t.Metainfo() + t.Drop() + f, err := os.Create(t.Info().Name + ".torrent") + if err != nil { + log.Fatalf("error creating torrent metainfo file: %s", err) + } + defer f.Close() + err = bencode.NewEncoder(f).Encode(mi) + if err != nil { + log.Fatalf("error writing torrent metainfo file: %s", err) + } + }() + } + wg.Wait() +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent-pick/main.go b/deps/github.com/anacrolix/torrent/cmd/torrent-pick/main.go new file mode 100644 index 0000000..73a597f --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent-pick/main.go @@ -0,0 +1,189 @@ +// Downloads torrents from the command-line. +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "net" + "net/http" + _ "net/http/pprof" + "os" + "strings" + "time" + + _ "github.com/anacrolix/envpprof" + "github.com/dustin/go-humanize" + "github.com/jessevdk/go-flags" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" +) + +// fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) + +func resolvedPeerAddrs(ss []string) (ret []torrent.PeerInfo, err error) { + for _, s := range ss { + var addr *net.TCPAddr + addr, err = net.ResolveTCPAddr("tcp", s) + if err != nil { + return + } + ret = append(ret, torrent.PeerInfo{ + Addr: addr, + }) + } + return +} + +func bytesCompleted(tc *torrent.Client) (ret int64) { + for _, t := range tc.Torrents() { + if t.Info() != nil { + ret += t.BytesCompleted() + } + } + return +} + +// Returns an estimate of the total bytes for all torrents. +func totalBytesEstimate(tc *torrent.Client) (ret int64) { + var noInfo, hadInfo int64 + for _, t := range tc.Torrents() { + info := t.Info() + if info == nil { + noInfo++ + continue + } + ret += info.TotalLength() + hadInfo++ + } + if hadInfo != 0 { + // Treat each torrent without info as the average of those with, + // rounded up. + ret += (noInfo*ret + hadInfo - 1) / hadInfo + } + return +} + +func progressLine(tc *torrent.Client) string { + return fmt.Sprintf("\033[K%s / %s\r", humanize.Bytes(uint64(bytesCompleted(tc))), humanize.Bytes(uint64(totalBytesEstimate(tc)))) +} + +func dstFileName(picked string) string { + parts := strings.Split(picked, "/") + return parts[len(parts)-1] +} + +func main() { + log.SetFlags(log.LstdFlags | log.Lshortfile) + rootGroup := struct { + Client *torrent.ClientConfig `group:"Client Options"` + TestPeers []string `long:"test-peer" description:"address of peer to inject to every torrent"` + Pick string `long:"pick" description:"filename to pick"` + }{ + Client: torrent.NewDefaultClientConfig(), + } + // Don't pass flags.PrintError because it's inconsistent with printing. + // https://github.com/jessevdk/go-flags/issues/132 + parser := flags.NewParser(&rootGroup, flags.HelpFlag|flags.PassDoubleDash) + parser.Usage = "[OPTIONS] (magnet URI or .torrent file path)..." + posArgs, err := parser.Parse() + if err != nil { + fmt.Fprintf(os.Stderr, "%s", "Download from the BitTorrent network.\n\n") + fmt.Println(err) + os.Exit(2) + } + log.Printf("File to pick: %s", rootGroup.Pick) + + testPeers, err := resolvedPeerAddrs(rootGroup.TestPeers) + if err != nil { + log.Fatal(err) + } + + if len(posArgs) == 0 { + fmt.Fprintln(os.Stderr, "no torrents specified") + return + } + + tmpdir, err := os.MkdirTemp("", "torrent-pick-") + if err != nil { + log.Fatal(err) + } + + defer os.RemoveAll(tmpdir) + + rootGroup.Client.DataDir = tmpdir + + client, err := torrent.NewClient(rootGroup.Client) + if err != nil { + log.Fatalf("error creating client: %s", err) + } + http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + client.WriteStatus(w) + }) + defer client.Close() + + dstName := dstFileName(rootGroup.Pick) + + f, err := os.Create(dstName) + if err != nil { + log.Fatal(err) + } + dstWriter := bufio.NewWriter(f) + + done := make(chan struct{}) + for _, arg := range posArgs { + t := func() *torrent.Torrent { + if strings.HasPrefix(arg, "magnet:") { + t, err := client.AddMagnet(arg) + if err != nil { + log.Fatalf("error adding magnet: %s", err) + } + return t + } else { + metaInfo, err := metainfo.LoadFromFile(arg) + if err != nil { + log.Fatal(err) + } + t, err := client.AddTorrent(metaInfo) + if err != nil { + log.Fatal(err) + } + return t + } + }() + t.AddPeers(testPeers) + + go func() { + defer close(done) + <-t.GotInfo() + for _, file := range t.Files() { + if file.DisplayPath() != rootGroup.Pick { + continue + } + file.Download() + srcReader := file.NewReader() + defer srcReader.Close() + io.Copy(dstWriter, srcReader) + return + } + log.Print("file not found") + }() + } + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() +waitDone: + for { + select { + case <-done: + break waitDone + case <-ticker.C: + os.Stdout.WriteString(progressLine(client)) + } + } + if rootGroup.Client.Seed { + select {} + } +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent-verify/main.go b/deps/github.com/anacrolix/torrent/cmd/torrent-verify/main.go new file mode 100644 index 0000000..0fbf024 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent-verify/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "bytes" + "crypto/sha1" + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/anacrolix/tagflag" + "github.com/edsrzf/mmap-go" + + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/mmap_span" + "github.com/anacrolix/torrent/storage" +) + +func mmapFile(name string) (mm storage.FileMapping, err error) { + f, err := os.Open(name) + if err != nil { + return + } + defer func() { + if err != nil { + f.Close() + } + }() + fi, err := f.Stat() + if err != nil { + return + } + if fi.Size() == 0 { + return + } + reg, err := mmap.MapRegion(f, -1, mmap.RDONLY, mmap.COPY, 0) + if err != nil { + return + } + return storage.WrapFileMapping(reg, f), nil +} + +func verifyTorrent(info *metainfo.Info, root string) error { + span := new(mmap_span.MMapSpan) + for _, file := range info.UpvertedFiles() { + filename := filepath.Join(append([]string{root, info.Name}, file.Path...)...) + mm, err := mmapFile(filename) + if err != nil { + return err + } + if int64(len(mm.Bytes())) != file.Length { + return fmt.Errorf("file %q has wrong length", filename) + } + span.Append(mm) + } + span.InitIndex() + for i, numPieces := 0, info.NumPieces(); i < numPieces; i += 1 { + p := info.Piece(i) + hash := sha1.New() + _, err := io.Copy(hash, io.NewSectionReader(span, p.Offset(), p.Length())) + if err != nil { + return err + } + good := bytes.Equal(hash.Sum(nil), p.Hash().Bytes()) + if !good { + return fmt.Errorf("hash mismatch at piece %d", i) + } + fmt.Printf("%d: %v: %v\n", i, p.Hash(), good) + } + return nil +} + +func main() { + log.SetFlags(log.Flags() | log.Lshortfile) + flags := struct { + DataDir string + tagflag.StartPos + TorrentFile string + }{} + tagflag.Parse(&flags) + metaInfo, err := metainfo.LoadFromFile(flags.TorrentFile) + if err != nil { + log.Fatal(err) + } + info, err := metaInfo.UnmarshalInfo() + if err != nil { + log.Fatalf("error unmarshalling info: %s", err) + } + err = verifyTorrent(&info, flags.DataDir) + if err != nil { + log.Fatalf("torrent failed verification: %s", err) + } +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent/announce.go b/deps/github.com/anacrolix/torrent/cmd/torrent/announce.go new file mode 100644 index 0000000..31676d9 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent/announce.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + + "github.com/davecgh/go-spew/spew" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/tracker" + "github.com/anacrolix/torrent/tracker/udp" +) + +type AnnounceCmd struct { + Event udp.AnnounceEvent + Port *uint16 + Tracker string `arg:"positional"` + InfoHash torrent.InfoHash `arg:"positional"` +} + +func announceErr(flags AnnounceCmd) error { + req := tracker.AnnounceRequest{ + InfoHash: flags.InfoHash, + Port: uint16(torrent.NewDefaultClientConfig().ListenPort), + NumWant: -1, + Event: flags.Event, + Left: -1, + } + if flags.Port != nil { + req.Port = *flags.Port + } + response, err := tracker.Announce{ + TrackerUrl: flags.Tracker, + Request: req, + }.Do() + if err != nil { + return fmt.Errorf("doing announce: %w", err) + } + spew.Dump(response) + return nil +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent/create.go b/deps/github.com/anacrolix/torrent/cmd/torrent/create.go new file mode 100644 index 0000000..5169a1f --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent/create.go @@ -0,0 +1,70 @@ +package main + +import ( + "os" + + "github.com/anacrolix/bargle" + "github.com/anacrolix/tagflag" + + "github.com/anacrolix/torrent/bencode" + "github.com/anacrolix/torrent/metainfo" +) + +var builtinAnnounceList = [][]string{ + {"http://p4p.arenabg.com:1337/announce"}, + {"udp://tracker.opentrackr.org:1337/announce"}, + {"udp://tracker.openbittorrent.com:6969/announce"}, +} + +func create() (cmd bargle.Command) { + var args struct { + AnnounceList []string `name:"a" help:"extra announce-list tier entry"` + EmptyAnnounceList bool `name:"n" help:"exclude default announce-list entries"` + Comment string `name:"t" help:"comment"` + CreatedBy string `name:"c" help:"created by"` + InfoName *string `name:"i" help:"override info name (defaults to ROOT)"` + PieceLength tagflag.Bytes + Url []string `name:"u" help:"add webseed url"` + Private *bool + Root string `arg:"positional"` + } + cmd = bargle.FromStruct(&args) + cmd.Desc = "Creates a torrent metainfo for the file system rooted at ROOT, and outputs it to stdout" + cmd.DefaultAction = func() (err error) { + mi := metainfo.MetaInfo{ + AnnounceList: builtinAnnounceList, + } + if args.EmptyAnnounceList { + mi.AnnounceList = make([][]string, 0) + } + for _, a := range args.AnnounceList { + mi.AnnounceList = append(mi.AnnounceList, []string{a}) + } + mi.SetDefaults() + if len(args.Comment) > 0 { + mi.Comment = args.Comment + } + if len(args.CreatedBy) > 0 { + mi.CreatedBy = args.CreatedBy + } + mi.UrlList = args.Url + info := metainfo.Info{ + PieceLength: args.PieceLength.Int64(), + Private: args.Private, + } + err = info.BuildFromFilePath(args.Root) + if err != nil { + return + } + if args.InfoName != nil { + info.Name = *args.InfoName + } + mi.InfoBytes, err = bencode.Marshal(info) + if err != nil { + return + } + err = mi.Write(os.Stdout) + return + } + return +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent/download.go b/deps/github.com/anacrolix/torrent/cmd/torrent/download.go new file mode 100644 index 0000000..cb3ca58 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent/download.go @@ -0,0 +1,394 @@ +package main + +import ( + "context" + "expvar" + "fmt" + "io" + "net" + "net/http" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/anacrolix/log" + "github.com/anacrolix/tagflag" + "github.com/davecgh/go-spew/spew" + "github.com/dustin/go-humanize" + "golang.org/x/time/rate" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/iplist" + "github.com/anacrolix/torrent/metainfo" + pp "github.com/anacrolix/torrent/peer_protocol" + "github.com/anacrolix/torrent/storage" +) + +func torrentBar(t *torrent.Torrent, pieceStates bool) { + go func() { + start := time.Now() + if t.Info() == nil { + fmt.Printf("%v: getting torrent info for %q\n", time.Since(start), t.Name()) + <-t.GotInfo() + } + lastStats := t.Stats() + var lastLine string + interval := 3 * time.Second + for range time.Tick(interval) { + var completedPieces, partialPieces int + psrs := t.PieceStateRuns() + for _, r := range psrs { + if r.Complete { + completedPieces += r.Length + } + if r.Partial { + partialPieces += r.Length + } + } + stats := t.Stats() + byteRate := int64(time.Second) + byteRate *= stats.BytesReadUsefulData.Int64() - lastStats.BytesReadUsefulData.Int64() + byteRate /= int64(interval) + line := fmt.Sprintf( + "%v: downloading %q: %s/%s, %d/%d pieces completed (%d partial): %v/s\n", + time.Since(start), + t.Name(), + humanize.Bytes(uint64(t.BytesCompleted())), + humanize.Bytes(uint64(t.Length())), + completedPieces, + t.NumPieces(), + partialPieces, + humanize.Bytes(uint64(byteRate)), + ) + if line != lastLine { + lastLine = line + os.Stdout.WriteString(line) + } + if pieceStates { + fmt.Println(psrs) + } + lastStats = stats + } + }() +} + +type stringAddr string + +func (stringAddr) Network() string { return "" } +func (me stringAddr) String() string { return string(me) } + +func resolveTestPeers(addrs []string) (ret []torrent.PeerInfo) { + for _, ta := range addrs { + ret = append(ret, torrent.PeerInfo{ + Addr: stringAddr(ta), + }) + } + return +} + +func addTorrents(ctx context.Context, client *torrent.Client, flags downloadFlags, wg *sync.WaitGroup) error { + testPeers := resolveTestPeers(flags.TestPeer) + for _, arg := range flags.Torrent { + t, err := func() (*torrent.Torrent, error) { + if strings.HasPrefix(arg, "magnet:") { + t, err := client.AddMagnet(arg) + if err != nil { + return nil, fmt.Errorf("error adding magnet: %w", err) + } + return t, nil + } else if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") { + response, err := http.Get(arg) + if err != nil { + return nil, fmt.Errorf("Error downloading torrent file: %s", err) + } + + metaInfo, err := metainfo.Load(response.Body) + defer response.Body.Close() + if err != nil { + return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err) + } + t, err := client.AddTorrent(metaInfo) + if err != nil { + return nil, fmt.Errorf("adding torrent: %w", err) + } + return t, nil + } else if strings.HasPrefix(arg, "infohash:") { + t, _ := client.AddTorrentInfoHash(metainfo.NewHashFromHex(strings.TrimPrefix(arg, "infohash:"))) + return t, nil + } else { + metaInfo, err := metainfo.LoadFromFile(arg) + if err != nil { + return nil, fmt.Errorf("error loading torrent file %q: %s\n", arg, err) + } + t, err := client.AddTorrent(metaInfo) + if err != nil { + return nil, fmt.Errorf("adding torrent: %w", err) + } + return t, nil + } + }() + if err != nil { + return fmt.Errorf("adding torrent for %q: %w", arg, err) + } + if flags.Progress { + torrentBar(t, flags.PieceStates) + } + t.AddPeers(testPeers) + wg.Add(1) + go func() { + defer wg.Done() + select { + case <-ctx.Done(): + return + case <-t.GotInfo(): + } + if flags.SaveMetainfos { + path := fmt.Sprintf("%v.torrent", t.InfoHash().HexString()) + err := writeMetainfoToFile(t.Metainfo(), path) + if err == nil { + log.Printf("wrote %q", path) + } else { + log.Printf("error writing %q: %v", path, err) + } + } + if len(flags.File) == 0 { + t.DownloadAll() + wg.Add(1) + go func() { + defer wg.Done() + waitForPieces(ctx, t, 0, t.NumPieces()) + }() + if flags.LinearDiscard { + r := t.NewReader() + io.Copy(io.Discard, r) + r.Close() + } + } else { + for _, f := range t.Files() { + for _, fileArg := range flags.File { + if f.DisplayPath() == fileArg { + wg.Add(1) + go func() { + defer wg.Done() + waitForPieces(ctx, t, f.BeginPieceIndex(), f.EndPieceIndex()) + }() + f.Download() + if flags.LinearDiscard { + r := f.NewReader() + go func() { + defer r.Close() + io.Copy(io.Discard, r) + }() + } + } + } + } + } + }() + } + return nil +} + +func waitForPieces(ctx context.Context, t *torrent.Torrent, beginIndex, endIndex int) { + sub := t.SubscribePieceStateChanges() + defer sub.Close() + expected := storage.Completion{ + Complete: true, + Ok: true, + } + pending := make(map[int]struct{}) + for i := beginIndex; i < endIndex; i++ { + if t.Piece(i).State().Completion != expected { + pending[i] = struct{}{} + } + } + for { + if len(pending) == 0 { + return + } + select { + case ev := <-sub.Values: + if ev.Completion == expected { + delete(pending, ev.Index) + } + case <-ctx.Done(): + return + } + } +} + +func writeMetainfoToFile(mi metainfo.MetaInfo, path string) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o640) + if err != nil { + return err + } + defer f.Close() + err = mi.Write(f) + if err != nil { + return err + } + return f.Close() +} + +type downloadFlags struct { + Debug bool + DownloadCmd +} + +type DownloadCmd struct { + SaveMetainfos bool + Mmap bool `help:"memory-map torrent data"` + Seed bool `help:"seed after download is complete"` + Addr string `help:"network listen addr"` + MaxUnverifiedBytes *tagflag.Bytes `help:"maximum number bytes to have pending verification"` + UploadRate *tagflag.Bytes `help:"max piece bytes to send per second"` + DownloadRate *tagflag.Bytes `help:"max bytes per second down from peers"` + PackedBlocklist string + PublicIP net.IP + Progress bool `default:"true"` + PieceStates bool `help:"Output piece state runs at progress intervals."` + Quiet bool `help:"discard client logging"` + Stats bool `help:"print stats at termination"` + Dht bool `default:"true"` + PortForward bool `default:"true"` + + TcpPeers bool `default:"true"` + UtpPeers bool `default:"true"` + Webtorrent bool `default:"true"` + DisableWebseeds bool + // Don't progress past handshake for peer connections where the peer doesn't offer the fast + // extension. + RequireFastExtension bool + + Ipv4 bool `default:"true"` + Ipv6 bool `default:"true"` + Pex bool `default:"true"` + + LinearDiscard bool `help:"Read and discard selected regions from start to finish. Useful for testing simultaneous Reader and static file prioritization."` + TestPeer []string `help:"addresses of some starting peers"` + + File []string + Torrent []string `arity:"+" help:"torrent file path or magnet uri" arg:"positional"` +} + +func statsEnabled(flags downloadFlags) bool { + return flags.Stats +} + +func downloadErr(flags downloadFlags) error { + clientConfig := torrent.NewDefaultClientConfig() + clientConfig.DisableWebseeds = flags.DisableWebseeds + clientConfig.DisableTCP = !flags.TcpPeers + clientConfig.DisableUTP = !flags.UtpPeers + clientConfig.DisableIPv4 = !flags.Ipv4 + clientConfig.DisableIPv6 = !flags.Ipv6 + clientConfig.DisableAcceptRateLimiting = true + clientConfig.NoDHT = !flags.Dht + clientConfig.Debug = flags.Debug + clientConfig.Seed = flags.Seed + clientConfig.PublicIp4 = flags.PublicIP.To4() + clientConfig.PublicIp6 = flags.PublicIP + clientConfig.DisablePEX = !flags.Pex + clientConfig.DisableWebtorrent = !flags.Webtorrent + clientConfig.NoDefaultPortForwarding = !flags.PortForward + if flags.PackedBlocklist != "" { + blocklist, err := iplist.MMapPackedFile(flags.PackedBlocklist) + if err != nil { + return fmt.Errorf("loading blocklist: %v", err) + } + defer blocklist.Close() + clientConfig.IPBlocklist = blocklist + } + if flags.Mmap { + clientConfig.DefaultStorage = storage.NewMMap("") + } + if flags.Addr != "" { + clientConfig.SetListenAddr(flags.Addr) + } + if flags.UploadRate != nil { + // TODO: I think the upload rate limit could be much lower. + clientConfig.UploadRateLimiter = rate.NewLimiter(rate.Limit(*flags.UploadRate), 256<<10) + } + if flags.DownloadRate != nil { + clientConfig.DownloadRateLimiter = rate.NewLimiter(rate.Limit(*flags.DownloadRate), 1<<16) + } + { + logger := log.Default.WithNames("main", "client") + if flags.Quiet { + logger = logger.FilterLevel(log.Critical) + } + clientConfig.Logger = logger + } + if flags.RequireFastExtension { + clientConfig.MinPeerExtensions.SetBit(pp.ExtensionBitFast, true) + } + if flags.MaxUnverifiedBytes != nil { + clientConfig.MaxUnverifiedBytes = flags.MaxUnverifiedBytes.Int64() + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + client, err := torrent.NewClient(clientConfig) + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + defer client.Close() + + // Write status on the root path on the default HTTP muxer. This will be bound to localhost + // somewhere if GOPPROF is set, thanks to the envpprof import. + http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + client.WriteStatus(w) + }) + var wg sync.WaitGroup + err = addTorrents(ctx, client, flags, &wg) + if err != nil { + return fmt.Errorf("adding torrents: %w", err) + } + started := time.Now() + defer outputStats(client, flags) + wg.Wait() + if ctx.Err() == nil { + log.Print("downloaded ALL the torrents") + } else { + err = ctx.Err() + } + clientConnStats := client.ConnStats() + log.Printf( + "average download rate: %v/s", + humanize.Bytes(uint64(float64( + clientConnStats.BytesReadUsefulData.Int64(), + )/time.Since(started).Seconds())), + ) + if flags.Seed { + if len(client.Torrents()) == 0 { + log.Print("no torrents to seed") + } else { + outputStats(client, flags) + <-ctx.Done() + } + } + spew.Dump(expvar.Get("torrent").(*expvar.Map).Get("chunks received")) + spew.Dump(client.ConnStats()) + clStats := client.ConnStats() + sentOverhead := clStats.BytesWritten.Int64() - clStats.BytesWrittenData.Int64() + log.Printf( + "client read %v, %.1f%% was useful data. sent %v non-data bytes", + humanize.Bytes(uint64(clStats.BytesRead.Int64())), + 100*float64(clStats.BytesReadUsefulData.Int64())/float64(clStats.BytesRead.Int64()), + humanize.Bytes(uint64(sentOverhead))) + return err +} + +func outputStats(cl *torrent.Client, args downloadFlags) { + if !statsEnabled(args) { + return + } + expvar.Do(func(kv expvar.KeyValue) { + fmt.Printf("%s: %s\n", kv.Key, kv.Value) + }) + cl.WriteStatus(os.Stdout) +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent/main.go b/deps/github.com/anacrolix/torrent/cmd/torrent/main.go new file mode 100644 index 0000000..2c1081b --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent/main.go @@ -0,0 +1,152 @@ +// Downloads torrents from the command-line. +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + stdLog "log" + "net/http" + "os" + "time" + + "github.com/anacrolix/bargle" + "github.com/anacrolix/envpprof" + "github.com/anacrolix/log" + xprometheus "github.com/anacrolix/missinggo/v2/prometheus" + "github.com/davecgh/go-spew/spew" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/sdk/trace" + + "github.com/anacrolix/torrent/bencode" + "github.com/anacrolix/torrent/version" +) + +func init() { + prometheus.MustRegister(xprometheus.NewExpvarCollector()) + http.Handle("/metrics", promhttp.Handler()) +} + +func shutdownTracerProvider(ctx context.Context, tp *trace.TracerProvider) { + started := time.Now() + err := tp.Shutdown(ctx) + elapsed := time.Since(started) + log.Levelf(log.Error, "shutting down tracer provider (took %v): %v", elapsed, err) +} + +func main() { + defer stdLog.SetFlags(stdLog.Flags() | stdLog.Lshortfile) + + ctx := context.Background() + tracingExporter, err := otlptracegrpc.New(ctx) + if err != nil { + log.Fatalf("creating tracing exporter: %v", err) + } + tracerProvider := trace.NewTracerProvider(trace.WithBatcher(tracingExporter)) + defer shutdownTracerProvider(ctx, tracerProvider) + otel.SetTracerProvider(tracerProvider) + + main := bargle.Main{} + main.Defer(envpprof.Stop) + main.Defer(func() { shutdownTracerProvider(ctx, tracerProvider) }) + debug := false + debugFlag := bargle.NewFlag(&debug) + debugFlag.AddLong("debug") + main.Options = append(main.Options, debugFlag.Make()) + main.Positionals = append(main.Positionals, + bargle.Subcommand{Name: "metainfo", Command: metainfoCmd()}, + bargle.Subcommand{Name: "announce", Command: func() bargle.Command { + var ac AnnounceCmd + cmd := bargle.FromStruct(&ac) + cmd.DefaultAction = func() error { + return announceErr(ac) + } + return cmd + }()}, + bargle.Subcommand{Name: "scrape", Command: func() bargle.Command { + var scrapeCfg scrapeCfg + cmd := bargle.FromStruct(&scrapeCfg) + cmd.Desc = "fetch swarm metrics for info-hashes from tracker" + cmd.DefaultAction = func() error { + return scrape(scrapeCfg) + } + return cmd + }()}, + bargle.Subcommand{Name: "download", Command: func() bargle.Command { + var dlc DownloadCmd + cmd := bargle.FromStruct(&dlc) + cmd.DefaultAction = func() error { + return downloadErr(downloadFlags{ + Debug: debug, + DownloadCmd: dlc, + }) + } + return cmd + }()}, + bargle.Subcommand{ + Name: "bencode", + Command: func() (cmd bargle.Command) { + var print func(interface{}) error + cmd.Positionals = append(cmd.Positionals, + bargle.Subcommand{Name: "json", Command: func() (cmd bargle.Command) { + cmd.DefaultAction = func() error { + je := json.NewEncoder(os.Stdout) + je.SetIndent("", " ") + print = je.Encode + return nil + } + return + }()}, + bargle.Subcommand{Name: "spew", Command: func() (cmd bargle.Command) { + cmd.DefaultAction = func() error { + config := spew.NewDefaultConfig() + config.DisableCapacities = true + config.Indent = " " + print = func(v interface{}) error { + config.Dump(v) + return nil + } + return nil + } + return + }()}) + d := bencode.NewDecoder(os.Stdin) + cmd.AfterParseFunc = func(ctx bargle.Context) error { + ctx.AfterParse(func() error { + for i := 0; ; i++ { + var v interface{} + err := d.Decode(&v) + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("decoding message index %d: %w", i, err) + } + print(v) + } + return nil + }) + return nil + } + cmd.Desc = "reads bencoding from stdin into Go native types and spews the result" + return + }(), + }, + bargle.Subcommand{Name: "version", Command: bargle.Command{ + DefaultAction: func() error { + fmt.Printf("HTTP User-Agent: %q\n", version.DefaultHttpUserAgent) + fmt.Printf("Torrent client version: %q\n", version.DefaultExtendedHandshakeClientVersion) + fmt.Printf("Torrent version prefix: %q\n", version.DefaultBep20Prefix) + return nil + }, + Desc: "prints various protocol default version strings", + }}, + bargle.Subcommand{Name: "serve", Command: serve()}, + bargle.Subcommand{Name: "create", Command: create()}, + ) + main.Run() +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent/metainfo.go b/deps/github.com/anacrolix/torrent/cmd/torrent/metainfo.go new file mode 100644 index 0000000..929cb45 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent/metainfo.go @@ -0,0 +1,132 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + + "github.com/anacrolix/bargle" + "github.com/bradfitz/iter" + + "github.com/anacrolix/torrent/metainfo" +) + +type pprintMetainfoFlags struct { + JustName bool + PieceHashes bool + Files bool +} + +func metainfoCmd() (cmd bargle.Command) { + var metainfoPath string + var mi *metainfo.MetaInfo + // TODO: Test if bargle treats no subcommand as a failure. + cmd.Positionals = append(cmd.Positionals, + &bargle.Positional{ + Name: "torrent file", + Value: &bargle.String{Target: &metainfoPath}, + AfterParseFunc: func(ctx bargle.Context) error { + ctx.AfterParse(func() (err error) { + if strings.HasPrefix(metainfoPath, "http://") || strings.HasPrefix(metainfoPath, "https://") { + response, err := http.Get(metainfoPath) + if err != nil { + return nil + } + mi, err = metainfo.Load(response.Body) + if err != nil { + return nil + } + } else { + mi, err = metainfo.LoadFromFile(metainfoPath) + } + return + }) + return nil + }, + }, + bargle.Subcommand{Name: "magnet", Command: func() (cmd bargle.Command) { + cmd.DefaultAction = func() (err error) { + info, err := mi.UnmarshalInfo() + if err != nil { + return + } + fmt.Fprintf(os.Stdout, "%s\n", mi.Magnet(nil, &info).String()) + return nil + } + return + }()}, + bargle.Subcommand{Name: "pprint", Command: func() (cmd bargle.Command) { + var flags pprintMetainfoFlags + cmd = bargle.FromStruct(&flags) + cmd.DefaultAction = func() (err error) { + err = pprintMetainfo(mi, flags) + if err != nil { + return + } + if !flags.JustName { + os.Stdout.WriteString("\n") + } + return + } + return + }()}, + //bargle.Subcommand{Name: "infohash", Command: func(ctx args.SubCmdCtx) (err error) { + // fmt.Printf("%s: %s\n", mi.HashInfoBytes().HexString(), metainfoPath) + // return nil + //}}, + //bargle.Subcommand{Name: "list-files", Command: func(ctx args.SubCmdCtx) (err error) { + // info, err := mi.UnmarshalInfo() + // if err != nil { + // return fmt.Errorf("unmarshalling info from metainfo at %q: %v", metainfoPath, err) + // } + // for _, f := range info.UpvertedFiles() { + // fmt.Println(f.DisplayPath(&info)) + // } + // return nil + //}}, + ) + return +} + +func pprintMetainfo(metainfo *metainfo.MetaInfo, flags pprintMetainfoFlags) error { + info, err := metainfo.UnmarshalInfo() + if err != nil { + return fmt.Errorf("error unmarshalling info: %s", err) + } + if flags.JustName { + fmt.Printf("%s\n", info.Name) + return nil + } + d := map[string]interface{}{ + "Name": info.Name, + "Name.Utf8": info.NameUtf8, + "NumPieces": info.NumPieces(), + "PieceLength": info.PieceLength, + "InfoHash": metainfo.HashInfoBytes().HexString(), + "NumFiles": len(info.UpvertedFiles()), + "TotalLength": info.TotalLength(), + "Announce": metainfo.Announce, + "AnnounceList": metainfo.AnnounceList, + "UrlList": metainfo.UrlList, + } + if len(metainfo.Nodes) > 0 { + d["Nodes"] = metainfo.Nodes + } + if flags.Files { + d["Files"] = info.UpvertedFiles() + } + if flags.PieceHashes { + d["PieceHashes"] = func() (ret []string) { + for i := range iter.N(info.NumPieces()) { + ret = append(ret, hex.EncodeToString(info.Pieces[i*20:(i+1)*20])) + } + return + }() + } + b, _ := json.MarshalIndent(d, "", " ") + _, err = os.Stdout.Write(b) + return err +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent/scrape.go b/deps/github.com/anacrolix/torrent/cmd/torrent/scrape.go new file mode 100644 index 0000000..01a59b7 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent/scrape.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "fmt" + + "github.com/davecgh/go-spew/spew" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/tracker" +) + +type scrapeCfg struct { + Tracker string `arg:"positional"` + InfoHashes []torrent.InfoHash `arity:"+" arg:"positional"` +} + +func scrape(flags scrapeCfg) error { + cc, err := tracker.NewClient(flags.Tracker, tracker.NewClientOpts{}) + if err != nil { + err = fmt.Errorf("creating new tracker client: %w", err) + return err + } + defer cc.Close() + scrapeOut, err := cc.Scrape(context.TODO(), flags.InfoHashes) + if err != nil { + return fmt.Errorf("scraping: %w", err) + } + spew.Dump(scrapeOut) + return nil +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent/serve.go b/deps/github.com/anacrolix/torrent/cmd/torrent/serve.go new file mode 100644 index 0000000..bdb1559 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent/serve.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "net/http" + "path/filepath" + + "github.com/anacrolix/bargle" + "github.com/anacrolix/log" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/bencode" + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" +) + +func serve() (cmd bargle.Command) { + var filePaths []string + cmd.Positionals = append(cmd.Positionals, &bargle.Positional{ + Value: bargle.AutoUnmarshaler(&filePaths), + }) + cmd.Desc = "creates and seeds a torrent from a filepath" + cmd.DefaultAction = func() error { + cfg := torrent.NewDefaultClientConfig() + cfg.Seed = true + cl, err := torrent.NewClient(cfg) + if err != nil { + return fmt.Errorf("new torrent client: %w", err) + } + defer cl.Close() + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + cl.WriteStatus(w) + }) + for _, filePath := range filePaths { + totalLength, err := totalLength(filePath) + if err != nil { + return fmt.Errorf("calculating total length of %q: %v", filePath, err) + } + pieceLength := metainfo.ChoosePieceLength(totalLength) + info := metainfo.Info{ + PieceLength: pieceLength, + } + err = info.BuildFromFilePath(filePath) + if err != nil { + return fmt.Errorf("building info from path %q: %w", filePath, err) + } + for _, fi := range info.Files { + log.Printf("added %q", fi.Path) + } + mi := metainfo.MetaInfo{ + InfoBytes: bencode.MustMarshal(info), + } + pc, err := storage.NewDefaultPieceCompletionForDir(".") + if err != nil { + return fmt.Errorf("new piece completion: %w", err) + } + defer pc.Close() + ih := mi.HashInfoBytes() + to, _ := cl.AddTorrentOpt(torrent.AddTorrentOpts{ + InfoHash: ih, + Storage: storage.NewFileOpts(storage.NewFileClientOpts{ + ClientBaseDir: filePath, + FilePathMaker: func(opts storage.FilePathMakerOpts) string { + return filepath.Join(opts.File.Path...) + }, + TorrentDirMaker: nil, + PieceCompletion: pc, + }), + }) + defer to.Drop() + err = to.MergeSpec(&torrent.TorrentSpec{ + InfoBytes: mi.InfoBytes, + Trackers: [][]string{{ + `wss://tracker.btorrent.xyz`, + `wss://tracker.openwebtorrent.com`, + "http://p4p.arenabg.com:1337/announce", + "udp://tracker.opentrackr.org:1337/announce", + "udp://tracker.openbittorrent.com:6969/announce", + }}, + }) + if err != nil { + return fmt.Errorf("setting trackers: %w", err) + } + fmt.Printf("%v: %v\n", to, to.Metainfo().Magnet(&ih, &info)) + } + select {} + } + return +} diff --git a/deps/github.com/anacrolix/torrent/cmd/torrent/total-length.go b/deps/github.com/anacrolix/torrent/cmd/torrent/total-length.go new file mode 100644 index 0000000..52888ee --- /dev/null +++ b/deps/github.com/anacrolix/torrent/cmd/torrent/total-length.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" +) + +func totalLength(path string) (totalLength int64, err error) { + err = filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + totalLength += info.Size() + return nil + }) + if err != nil { + return 0, fmt.Errorf("walking path, %w", err) + } + return totalLength, nil +} diff --git a/deps/github.com/anacrolix/torrent/common/upverted_files.go b/deps/github.com/anacrolix/torrent/common/upverted_files.go new file mode 100644 index 0000000..1933e16 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/common/upverted_files.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/segments" +) + +func LengthIterFromUpvertedFiles(fis []metainfo.FileInfo) segments.LengthIter { + i := 0 + return func() (segments.Length, bool) { + if i == len(fis) { + return -1, false + } + l := fis[i].Length + i++ + return l, true + } +} diff --git a/deps/github.com/anacrolix/torrent/config.go b/deps/github.com/anacrolix/torrent/config.go new file mode 100644 index 0000000..e2d0ea1 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/config.go @@ -0,0 +1,249 @@ +package torrent + +import ( + "context" + "net" + "net/http" + "net/url" + "time" + + "github.com/anacrolix/dht/v2" + "github.com/anacrolix/dht/v2/krpc" + "github.com/anacrolix/log" + "github.com/anacrolix/missinggo/v2" + "golang.org/x/time/rate" + + "github.com/anacrolix/torrent/iplist" + "github.com/anacrolix/torrent/mse" + "github.com/anacrolix/torrent/storage" + "github.com/anacrolix/torrent/version" +) + +// Contains config elements that are exclusive to tracker handling. There may be other fields in +// ClientConfig that are also relevant. +type ClientTrackerConfig struct { + // Don't announce to trackers. This only leaves DHT to discover peers. + DisableTrackers bool `long:"disable-trackers"` + // Defines DialContext func to use for HTTP tracker announcements + TrackerDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + // Defines ListenPacket func to use for UDP tracker announcements + TrackerListenPacket func(network, addr string) (net.PacketConn, error) + // Takes a tracker's hostname and requests DNS A and AAAA records. + // Used in case DNS lookups require a special setup (i.e., dns-over-https) + LookupTrackerIp func(*url.URL) ([]net.IP, error) +} + +type ClientDhtConfig struct { + // Don't create a DHT. + NoDHT bool `long:"disable-dht"` + DhtStartingNodes func(network string) dht.StartingNodesGetter + // Called for each anacrolix/dht Server created for the Client. + ConfigureAnacrolixDhtServer func(*dht.ServerConfig) + PeriodicallyAnnounceTorrentsToDht bool + // OnQuery hook func + DHTOnQuery func(query *krpc.Msg, source net.Addr) (propagate bool) +} + +// Probably not safe to modify this after it's given to a Client. +type ClientConfig struct { + ClientTrackerConfig + ClientDhtConfig + + // Store torrent file data in this directory unless .DefaultStorage is + // specified. + DataDir string `long:"data-dir" description:"directory to store downloaded torrent data"` + // The address to listen for new uTP and TCP BitTorrent protocol connections. DHT shares a UDP + // socket with uTP unless configured otherwise. + ListenHost func(network string) string + ListenPort int + NoDefaultPortForwarding bool + UpnpID string + DisablePEX bool `long:"disable-pex"` + + // Never send chunks to peers. + NoUpload bool `long:"no-upload"` + // Disable uploading even when it isn't fair. + DisableAggressiveUpload bool `long:"disable-aggressive-upload"` + // Upload even after there's nothing in it for us. By default uploading is + // not altruistic, we'll only upload to encourage the peer to reciprocate. + Seed bool `long:"seed"` + // Only applies to chunks uploaded to peers, to maintain responsiveness + // communicating local Client state to peers. Each limiter token + // represents one byte. The Limiter's burst must be large enough to fit a + // whole chunk, which is usually 16 KiB (see TorrentSpec.ChunkSize). + UploadRateLimiter *rate.Limiter + // Rate limits all reads from connections to peers. Each limiter token + // represents one byte. The Limiter's burst must be bigger than the + // largest Read performed on a the underlying rate-limiting io.Reader + // minus one. This is likely to be the larger of the main read loop buffer + // (~4096), and the requested chunk size (~16KiB, see + // TorrentSpec.ChunkSize). + DownloadRateLimiter *rate.Limiter + // Maximum unverified bytes across all torrents. Not used if zero. + MaxUnverifiedBytes int64 + + // User-provided Client peer ID. If not present, one is generated automatically. + PeerID string + // For the bittorrent protocol. + DisableUTP bool + // For the bittorrent protocol. + DisableTCP bool `long:"disable-tcp"` + // Called to instantiate storage for each added torrent. Builtin backends + // are in the storage package. If not set, the "file" implementation is + // used (and Closed when the Client is Closed). + DefaultStorage storage.ClientImpl + + HeaderObfuscationPolicy HeaderObfuscationPolicy + // The crypto methods to offer when initiating connections with header obfuscation. + CryptoProvides mse.CryptoMethod + // Chooses the crypto method to use when receiving connections with header obfuscation. + CryptoSelector mse.CryptoSelector + + IPBlocklist iplist.Ranger + DisableIPv6 bool `long:"disable-ipv6"` + DisableIPv4 bool + DisableIPv4Peers bool + // Perform logging and any other behaviour that will help debug. + Debug bool `help:"enable debugging"` + Logger log.Logger + + // Used for torrent sources and webseeding if set. + WebTransport http.RoundTripper + // Defines proxy for HTTP requests, such as for trackers. It's commonly set from the result of + // "net/http".ProxyURL(HTTPProxy). + HTTPProxy func(*http.Request) (*url.URL, error) + // Defines DialContext func to use for HTTP requests, such as for fetching metainfo and webtorrent seeds + HTTPDialContext func(ctx context.Context, network, addr string) (net.Conn, error) + // HTTPUserAgent changes default UserAgent for HTTP requests + HTTPUserAgent string + // HttpRequestDirector modifies the request before it's sent. + // Useful for adding authentication headers, for example + HttpRequestDirector func(*http.Request) error + // WebsocketTrackerHttpHeader returns a custom header to be used when dialing a websocket connection + // to the tracker. Useful for adding authentication headers + WebsocketTrackerHttpHeader func() http.Header + // Updated occasionally to when there's been some changes to client + // behaviour in case other clients are assuming anything of us. See also + // `bep20`. + ExtendedHandshakeClientVersion string + // Peer ID client identifier prefix. We'll update this occasionally to + // reflect changes to client behaviour that other clients may depend on. + // Also see `extendedHandshakeClientVersion`. + Bep20 string + + // Peer dial timeout to use when there are limited peers. + NominalDialTimeout time.Duration + // Minimum peer dial timeout to use (even if we have lots of peers). + MinDialTimeout time.Duration + EstablishedConnsPerTorrent int + HalfOpenConnsPerTorrent int + TotalHalfOpenConns int + // Maximum number of peer addresses in reserve. + TorrentPeersHighWater int + // Minumum number of peers before effort is made to obtain more peers. + TorrentPeersLowWater int + + // Limit how long handshake can take. This is to reduce the lingering + // impact of a few bad apples. 4s loses 1% of successful handshakes that + // are obtained with 60s timeout, and 5% of unsuccessful handshakes. + HandshakesTimeout time.Duration + // How long between writes before sending a keep alive message on a peer connection that we want + // to maintain. + KeepAliveTimeout time.Duration + // Maximum bytes to buffer per peer connection for peer request data before it is sent. + MaxAllocPeerRequestDataPerConn int64 + + // The IP addresses as our peers should see them. May differ from the + // local interfaces due to NAT or other network configurations. + PublicIp4 net.IP + PublicIp6 net.IP + + // Accept rate limiting affects excessive connection attempts from IPs that fail during + // handshakes or request torrents that we don't have. + DisableAcceptRateLimiting bool + // Don't add connections that have the same peer ID as an existing + // connection for a given Torrent. + DropDuplicatePeerIds bool + // Drop peers that are complete if we are also complete and have no use for the peer. This is a + // bit of a special case, since a peer could also be useless if they're just not interested, or + // we don't intend to obtain all of a torrent's data. + DropMutuallyCompletePeers bool + // Whether to accept peer connections at all. + AcceptPeerConnections bool + // Whether a Client should want conns without delegating to any attached Torrents. This is + // useful when torrents might be added dynamically in callbacks for example. + AlwaysWantConns bool + + Extensions PeerExtensionBits + // Bits that peers must have set to proceed past handshakes. + MinPeerExtensions PeerExtensionBits + + DisableWebtorrent bool + DisableWebseeds bool + + Callbacks Callbacks + + // ICEServers defines a slice describing servers available to be used by + // ICE, such as STUN and TURN servers. + ICEServers []string + + DialRateLimiter *rate.Limiter + + PieceHashersPerTorrent int // default: 2 +} + +func (cfg *ClientConfig) SetListenAddr(addr string) *ClientConfig { + host, port, err := missinggo.ParseHostPort(addr) + if err != nil { + panic(err) + } + cfg.ListenHost = func(string) string { return host } + cfg.ListenPort = port + return cfg +} + +func NewDefaultClientConfig() *ClientConfig { + cc := &ClientConfig{ + HTTPUserAgent: version.DefaultHttpUserAgent, + ExtendedHandshakeClientVersion: version.DefaultExtendedHandshakeClientVersion, + Bep20: version.DefaultBep20Prefix, + UpnpID: version.DefaultUpnpId, + NominalDialTimeout: 20 * time.Second, + MinDialTimeout: 3 * time.Second, + EstablishedConnsPerTorrent: 50, + HalfOpenConnsPerTorrent: 25, + TotalHalfOpenConns: 100, + TorrentPeersHighWater: 500, + TorrentPeersLowWater: 50, + HandshakesTimeout: 4 * time.Second, + KeepAliveTimeout: time.Minute, + MaxAllocPeerRequestDataPerConn: 1 << 20, + ListenHost: func(string) string { return "" }, + UploadRateLimiter: unlimited, + DownloadRateLimiter: unlimited, + DisableAcceptRateLimiting: true, + DropMutuallyCompletePeers: true, + HeaderObfuscationPolicy: HeaderObfuscationPolicy{ + Preferred: true, + RequirePreferred: false, + }, + CryptoSelector: mse.DefaultCryptoSelector, + CryptoProvides: mse.AllSupportedCrypto, + ListenPort: 42069, + Extensions: defaultPeerExtensionBytes(), + AcceptPeerConnections: true, + MaxUnverifiedBytes: 64 << 20, + DialRateLimiter: rate.NewLimiter(10, 10), + PieceHashersPerTorrent: 2, + } + cc.DhtStartingNodes = func(network string) dht.StartingNodesGetter { + return func() ([]dht.Addr, error) { return dht.GlobalBootstrapAddrs(network) } + } + cc.PeriodicallyAnnounceTorrentsToDht = true + return cc +} + +type HeaderObfuscationPolicy struct { + RequirePreferred bool // Whether the value of Preferred is a strict requirement. + Preferred bool // Whether header obfuscation is preferred. +} diff --git a/deps/github.com/anacrolix/torrent/conn_stats.go b/deps/github.com/anacrolix/torrent/conn_stats.go new file mode 100644 index 0000000..0c5bfc7 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/conn_stats.go @@ -0,0 +1,117 @@ +package torrent + +import ( + "encoding/json" + "fmt" + "io" + "reflect" + "sync/atomic" + + pp "github.com/anacrolix/torrent/peer_protocol" +) + +// Various connection-level metrics. At the Torrent level these are aggregates. Chunks are messages +// with data payloads. Data is actual torrent content without any overhead. Useful is something we +// needed locally. Unwanted is something we didn't ask for (but may still be useful). Written is +// things sent to the peer, and Read is stuff received from them. Due to the implementation of +// Count, must be aligned on some platforms: See https://github.com/anacrolix/torrent/issues/262. +type ConnStats struct { + // Total bytes on the wire. Includes handshakes and encryption. + BytesWritten Count + BytesWrittenData Count + + BytesRead Count + BytesReadData Count + BytesReadUsefulData Count + BytesReadUsefulIntendedData Count + + ChunksWritten Count + + ChunksRead Count + ChunksReadUseful Count + ChunksReadWasted Count + + MetadataChunksRead Count + + // Number of pieces data was written to, that subsequently passed verification. + PiecesDirtiedGood Count + // Number of pieces data was written to, that subsequently failed verification. Note that a + // connection may not have been the sole dirtier of a piece. + PiecesDirtiedBad Count +} + +func (me *ConnStats) Copy() (ret ConnStats) { + for i := 0; i < reflect.TypeOf(ConnStats{}).NumField(); i++ { + n := reflect.ValueOf(me).Elem().Field(i).Addr().Interface().(*Count).Int64() + reflect.ValueOf(&ret).Elem().Field(i).Addr().Interface().(*Count).Add(n) + } + return +} + +type Count struct { + n int64 +} + +var _ fmt.Stringer = (*Count)(nil) + +func (me *Count) Add(n int64) { + atomic.AddInt64(&me.n, n) +} + +func (me *Count) Int64() int64 { + return atomic.LoadInt64(&me.n) +} + +func (me *Count) String() string { + return fmt.Sprintf("%v", me.Int64()) +} + +func (me *Count) MarshalJSON() ([]byte, error) { + return json.Marshal(me.n) +} + +func (cs *ConnStats) wroteMsg(msg *pp.Message) { + // TODO: Track messages and not just chunks. + switch msg.Type { + case pp.Piece: + cs.ChunksWritten.Add(1) + cs.BytesWrittenData.Add(int64(len(msg.Piece))) + } +} + +func (cs *ConnStats) receivedChunk(size int64) { + cs.ChunksRead.Add(1) + cs.BytesReadData.Add(size) +} + +func (cs *ConnStats) incrementPiecesDirtiedGood() { + cs.PiecesDirtiedGood.Add(1) +} + +func (cs *ConnStats) incrementPiecesDirtiedBad() { + cs.PiecesDirtiedBad.Add(1) +} + +func add(n int64, f func(*ConnStats) *Count) func(*ConnStats) { + return func(cs *ConnStats) { + p := f(cs) + p.Add(n) + } +} + +type connStatsReadWriter struct { + rw io.ReadWriter + c *PeerConn +} + +func (me connStatsReadWriter) Write(b []byte) (n int, err error) { + n, err = me.rw.Write(b) + me.c.wroteBytes(int64(n)) + return +} + +func (me connStatsReadWriter) Read(b []byte) (n int, err error) { + n, err = me.rw.Read(b) + me.c.readBytes(int64(n)) + return +} diff --git a/deps/github.com/anacrolix/torrent/deferrwl.go b/deps/github.com/anacrolix/torrent/deferrwl.go new file mode 100644 index 0000000..bf95be2 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/deferrwl.go @@ -0,0 +1,36 @@ +package torrent + +import "github.com/anacrolix/sync" + +// Runs deferred actions on Unlock. Note that actions are assumed to be the results of changes that +// would only occur with a write lock at present. The race detector should catch instances of defers +// without the write lock being held. +type lockWithDeferreds struct { + internal sync.RWMutex + unlockActions []func() +} + +func (me *lockWithDeferreds) Lock() { + me.internal.Lock() +} + +func (me *lockWithDeferreds) Unlock() { + unlockActions := me.unlockActions + for i := 0; i < len(unlockActions); i += 1 { + unlockActions[i]() + } + me.unlockActions = unlockActions[:0] + me.internal.Unlock() +} + +func (me *lockWithDeferreds) RLock() { + me.internal.RLock() +} + +func (me *lockWithDeferreds) RUnlock() { + me.internal.RUnlock() +} + +func (me *lockWithDeferreds) Defer(action func()) { + me.unlockActions = append(me.unlockActions, action) +} diff --git a/deps/github.com/anacrolix/torrent/dht.go b/deps/github.com/anacrolix/torrent/dht.go new file mode 100644 index 0000000..77975a2 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/dht.go @@ -0,0 +1,62 @@ +package torrent + +import ( + "io" + "net" + + "github.com/anacrolix/dht/v2" + "github.com/anacrolix/dht/v2/krpc" + peer_store "github.com/anacrolix/dht/v2/peer-store" +) + +// DHT server interface for use by a Torrent or Client. It's reasonable for this to make assumptions +// for torrent-use that might not be the default behaviour for the DHT server. +type DhtServer interface { + Stats() interface{} + ID() [20]byte + Addr() net.Addr + AddNode(ni krpc.NodeInfo) error + // This is called asynchronously when receiving PORT messages. + Ping(addr *net.UDPAddr) + Announce(hash [20]byte, port int, impliedPort bool) (DhtAnnounce, error) + WriteStatus(io.Writer) +} + +// Optional interface for DhtServer's that can expose their peer store (if any). +type PeerStorer interface { + PeerStore() peer_store.Interface +} + +type DhtAnnounce interface { + Close() + Peers() <-chan dht.PeersValues +} + +type AnacrolixDhtServerWrapper struct { + *dht.Server +} + +func (me AnacrolixDhtServerWrapper) Stats() interface{} { + return me.Server.Stats() +} + +type anacrolixDhtAnnounceWrapper struct { + *dht.Announce +} + +func (me anacrolixDhtAnnounceWrapper) Peers() <-chan dht.PeersValues { + return me.Announce.Peers +} + +func (me AnacrolixDhtServerWrapper) Announce(hash [20]byte, port int, impliedPort bool) (DhtAnnounce, error) { + ann, err := me.Server.Announce(hash, port, impliedPort) + return anacrolixDhtAnnounceWrapper{ann}, err +} + +func (me AnacrolixDhtServerWrapper) Ping(addr *net.UDPAddr) { + me.Server.PingQueryInput(addr, dht.QueryInput{ + RateLimiting: dht.QueryRateLimiting{NoWaitFirst: true}, + }) +} + +var _ DhtServer = AnacrolixDhtServerWrapper{} diff --git a/deps/github.com/anacrolix/torrent/dial-pool.go b/deps/github.com/anacrolix/torrent/dial-pool.go new file mode 100644 index 0000000..c0c233e --- /dev/null +++ b/deps/github.com/anacrolix/torrent/dial-pool.go @@ -0,0 +1,43 @@ +package torrent + +import ( + "context" +) + +type dialPool struct { + resCh chan DialResult + addr string + left int +} + +func (me *dialPool) getFirst() (res DialResult) { + for me.left > 0 && res.Conn == nil { + res = <-me.resCh + me.left-- + } + return +} + +func (me *dialPool) add(ctx context.Context, dialer Dialer) { + me.left++ + go func() { + me.resCh <- DialResult{ + dialFromSocket(ctx, dialer, me.addr), + dialer, + } + }() +} + +func (me *dialPool) startDrainer() { + go me.drainAndCloseRemainingDials() +} + +func (me *dialPool) drainAndCloseRemainingDials() { + for me.left > 0 { + conn := (<-me.resCh).Conn + me.left-- + if conn != nil { + conn.Close() + } + } +} diff --git a/deps/github.com/anacrolix/torrent/dialer.go b/deps/github.com/anacrolix/torrent/dialer.go new file mode 100644 index 0000000..5cdf3fc --- /dev/null +++ b/deps/github.com/anacrolix/torrent/dialer.go @@ -0,0 +1,12 @@ +package torrent + +import ( + "github.com/anacrolix/torrent/dialer" +) + +type ( + Dialer = dialer.T + NetworkDialer = dialer.WithNetwork +) + +var DefaultNetDialer = &dialer.Default diff --git a/deps/github.com/anacrolix/torrent/dialer/dialer.go b/deps/github.com/anacrolix/torrent/dialer/dialer.go new file mode 100644 index 0000000..5e5dff4 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/dialer/dialer.go @@ -0,0 +1,34 @@ +package dialer + +import ( + "context" + "net" +) + +// Dialers have the network locked in. +type T interface { + Dial(_ context.Context, addr string) (net.Conn, error) + DialerNetwork() string +} + +// An interface to ease wrapping dialers that explicitly include a network parameter. +type WithContext interface { + DialContext(ctx context.Context, network, addr string) (net.Conn, error) +} + +// Used by wrappers of standard library network types. +var Default = &net.Dialer{} + +// Adapts a WithContext to the Dial interface in this package. +type WithNetwork struct { + Network string + Dialer WithContext +} + +func (me WithNetwork) DialerNetwork() string { + return me.Network +} + +func (me WithNetwork) Dial(ctx context.Context, addr string) (_ net.Conn, err error) { + return me.Dialer.DialContext(ctx, me.Network, addr) +} diff --git a/deps/github.com/anacrolix/torrent/doc.go b/deps/github.com/anacrolix/torrent/doc.go new file mode 100644 index 0000000..bc90c0d --- /dev/null +++ b/deps/github.com/anacrolix/torrent/doc.go @@ -0,0 +1,33 @@ +/* +Package torrent implements a torrent client. Goals include: + - Configurable data storage, such as file, mmap, and piece-based. + - Downloading on demand: torrent.Reader will request only the data required to + satisfy Reads, which is ideal for streaming and torrentfs. + +BitTorrent features implemented include: + - Protocol obfuscation + - DHT + - uTP + - PEX + - Magnet links + - IP Blocklists + - Some IPv6 + - HTTP and UDP tracker clients + - BEPs: + - 3: Basic BitTorrent protocol + - 5: DHT + - 6: Fast Extension (have all/none only) + - 7: IPv6 Tracker Extension + - 9: ut_metadata + - 10: Extension protocol + - 11: PEX + - 12: Multitracker metadata extension + - 15: UDP Tracker Protocol + - 20: Peer ID convention ("-GTnnnn-") + - 23: Tracker Returns Compact Peer Lists + - 29: uTorrent transport protocol + - 41: UDP Tracker Protocol Extensions + - 42: DHT Security extension + - 43: Read-only DHT Nodes +*/ +package torrent diff --git a/deps/github.com/anacrolix/torrent/example_test.go b/deps/github.com/anacrolix/torrent/example_test.go new file mode 100644 index 0000000..54cb719 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/example_test.go @@ -0,0 +1,25 @@ +package torrent_test + +import ( + "log" + + "github.com/anacrolix/torrent" +) + +func Example() { + c, _ := torrent.NewClient(nil) + defer c.Close() + t, _ := c.AddMagnet("magnet:?xt=urn:btih:ZOCMZQIPFFW7OLLMIC5HUB6BPCSDEOQU") + <-t.GotInfo() + t.DownloadAll() + c.WaitAll() + log.Print("ermahgerd, torrent downloaded") +} + +func Example_fileReader() { + var f torrent.File + // Accesses the parts of the torrent pertaining to f. Data will be + // downloaded as required, per the configuration of the torrent.Reader. + r := f.NewReader() + defer r.Close() +} diff --git a/deps/github.com/anacrolix/torrent/file.go b/deps/github.com/anacrolix/torrent/file.go new file mode 100644 index 0000000..bea4b13 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/file.go @@ -0,0 +1,206 @@ +package torrent + +import ( + "github.com/RoaringBitmap/roaring" + "github.com/anacrolix/missinggo/v2/bitmap" + + "github.com/anacrolix/torrent/metainfo" +) + +// Provides access to regions of torrent data that correspond to its files. +type File struct { + t *Torrent + path string + offset int64 + length int64 + fi metainfo.FileInfo + displayPath string + prio piecePriority +} + +func (f *File) Torrent() *Torrent { + return f.t +} + +// Data for this file begins this many bytes into the Torrent. +func (f *File) Offset() int64 { + return f.offset +} + +// The FileInfo from the metainfo.Info to which this file corresponds. +func (f File) FileInfo() metainfo.FileInfo { + return f.fi +} + +// The file's path components joined by '/'. +func (f File) Path() string { + return f.path +} + +// The file's length in bytes. +func (f *File) Length() int64 { + return f.length +} + +// Number of bytes of the entire file we have completed. This is the sum of +// completed pieces, and dirtied chunks of incomplete pieces. +func (f *File) BytesCompleted() (n int64) { + f.t.cl.rLock() + n = f.bytesCompletedLocked() + f.t.cl.rUnlock() + return +} + +func (f *File) bytesCompletedLocked() int64 { + return f.length - f.bytesLeft() +} + +func fileBytesLeft( + torrentUsualPieceSize int64, + fileFirstPieceIndex int, + fileEndPieceIndex int, + fileTorrentOffset int64, + fileLength int64, + torrentCompletedPieces *roaring.Bitmap, + pieceSizeCompletedFn func(pieceIndex int) int64, +) (left int64) { + if fileLength == 0 { + return + } + + noCompletedMiddlePieces := roaring.New() + noCompletedMiddlePieces.AddRange(bitmap.BitRange(fileFirstPieceIndex), bitmap.BitRange(fileEndPieceIndex)) + noCompletedMiddlePieces.AndNot(torrentCompletedPieces) + noCompletedMiddlePieces.Iterate(func(pieceIndex uint32) bool { + i := int(pieceIndex) + pieceSizeCompleted := pieceSizeCompletedFn(i) + if i == fileFirstPieceIndex { + beginOffset := fileTorrentOffset % torrentUsualPieceSize + beginSize := torrentUsualPieceSize - beginOffset + beginDownLoaded := pieceSizeCompleted - beginOffset + if beginDownLoaded < 0 { + beginDownLoaded = 0 + } + left += beginSize - beginDownLoaded + } else if i == fileEndPieceIndex-1 { + endSize := (fileTorrentOffset + fileLength) % torrentUsualPieceSize + if endSize == 0 { + endSize = torrentUsualPieceSize + } + endDownloaded := pieceSizeCompleted + if endDownloaded > endSize { + endDownloaded = endSize + } + left += endSize - endDownloaded + } else { + left += torrentUsualPieceSize - pieceSizeCompleted + } + return true + }) + + if left > fileLength { + left = fileLength + } + // + //numPiecesSpanned := f.EndPieceIndex() - f.BeginPieceIndex() + //completedMiddlePieces := f.t._completedPieces.Clone() + //completedMiddlePieces.RemoveRange(0, bitmap.BitRange(f.BeginPieceIndex()+1)) + //completedMiddlePieces.RemoveRange(bitmap.BitRange(f.EndPieceIndex()-1), bitmap.ToEnd) + //left += int64(numPiecesSpanned-2-pieceIndex(completedMiddlePieces.GetCardinality())) * torrentUsualPieceSize + return +} + +func (f *File) bytesLeft() (left int64) { + return fileBytesLeft(int64(f.t.usualPieceSize()), f.BeginPieceIndex(), f.EndPieceIndex(), f.offset, f.length, &f.t._completedPieces, func(pieceIndex int) int64 { + return int64(f.t.piece(pieceIndex).numDirtyBytes()) + }) +} + +// The relative file path for a multi-file torrent, and the torrent name for a +// single-file torrent. Dir separators are '/'. +func (f *File) DisplayPath() string { + return f.displayPath +} + +// The download status of a piece that comprises part of a File. +type FilePieceState struct { + Bytes int64 // Bytes within the piece that are part of this File. + PieceState +} + +// Returns the state of pieces in this file. +func (f *File) State() (ret []FilePieceState) { + f.t.cl.rLock() + defer f.t.cl.rUnlock() + pieceSize := int64(f.t.usualPieceSize()) + off := f.offset % pieceSize + remaining := f.length + for i := pieceIndex(f.offset / pieceSize); ; i++ { + if remaining == 0 { + break + } + len1 := pieceSize - off + if len1 > remaining { + len1 = remaining + } + ps := f.t.pieceState(i) + ret = append(ret, FilePieceState{len1, ps}) + off = 0 + remaining -= len1 + } + return +} + +// Requests that all pieces containing data in the file be downloaded. +func (f *File) Download() { + f.SetPriority(PiecePriorityNormal) +} + +func byteRegionExclusivePieces(off, size, pieceSize int64) (begin, end int) { + begin = int((off + pieceSize - 1) / pieceSize) + end = int((off + size) / pieceSize) + return +} + +// Deprecated: Use File.SetPriority. +func (f *File) Cancel() { + f.SetPriority(PiecePriorityNone) +} + +func (f *File) NewReader() Reader { + return f.t.newReader(f.Offset(), f.Length()) +} + +// Sets the minimum priority for pieces in the File. +func (f *File) SetPriority(prio piecePriority) { + f.t.cl.lock() + if prio != f.prio { + f.prio = prio + f.t.updatePiecePriorities(f.BeginPieceIndex(), f.EndPieceIndex(), "File.SetPriority") + } + f.t.cl.unlock() +} + +// Returns the priority per File.SetPriority. +func (f *File) Priority() (prio piecePriority) { + f.t.cl.rLock() + prio = f.prio + f.t.cl.rUnlock() + return +} + +// Returns the index of the first piece containing data for the file. +func (f *File) BeginPieceIndex() int { + if f.t.usualPieceSize() == 0 { + return 0 + } + return pieceIndex(f.offset / int64(f.t.usualPieceSize())) +} + +// Returns the index of the piece after the last one containing data for the file. +func (f *File) EndPieceIndex() int { + if f.t.usualPieceSize() == 0 { + return 0 + } + return pieceIndex((f.offset + f.length + int64(f.t.usualPieceSize()) - 1) / int64(f.t.usualPieceSize())) +} diff --git a/deps/github.com/anacrolix/torrent/file_test.go b/deps/github.com/anacrolix/torrent/file_test.go new file mode 100644 index 0000000..2f57bcf --- /dev/null +++ b/deps/github.com/anacrolix/torrent/file_test.go @@ -0,0 +1,118 @@ +package torrent + +import ( + "testing" + + "github.com/RoaringBitmap/roaring" + "github.com/stretchr/testify/assert" +) + +func TestFileExclusivePieces(t *testing.T) { + for _, _case := range []struct { + off, size, pieceSize int64 + begin, end int + }{ + {0, 2, 2, 0, 1}, + {1, 2, 2, 1, 1}, + {1, 4, 2, 1, 2}, + } { + begin, end := byteRegionExclusivePieces(_case.off, _case.size, _case.pieceSize) + assert.EqualValues(t, _case.begin, begin) + assert.EqualValues(t, _case.end, end) + } +} + +type testFileBytesLeft struct { + usualPieceSize int64 + firstPieceIndex int + endPieceIndex int + fileOffset int64 + fileLength int64 + completedPieces roaring.Bitmap + expected int64 + name string +} + +func (me testFileBytesLeft) Run(t *testing.T) { + t.Run(me.name, func(t *testing.T) { + assert.EqualValues(t, me.expected, fileBytesLeft(me.usualPieceSize, me.firstPieceIndex, me.endPieceIndex, me.fileOffset, me.fileLength, &me.completedPieces, func(pieceIndex int) int64 { + return 0 + })) + }) +} + +func TestFileBytesLeft(t *testing.T) { + testFileBytesLeft{ + usualPieceSize: 3, + firstPieceIndex: 1, + endPieceIndex: 1, + fileOffset: 1, + fileLength: 0, + expected: 0, + name: "ZeroLengthFile", + }.Run(t) + + testFileBytesLeft{ + usualPieceSize: 2, + firstPieceIndex: 1, + endPieceIndex: 2, + fileOffset: 1, + fileLength: 1, + expected: 1, + name: "EndOfSecondPiece", + }.Run(t) + + testFileBytesLeft{ + usualPieceSize: 3, + firstPieceIndex: 0, + endPieceIndex: 1, + fileOffset: 1, + fileLength: 1, + expected: 1, + name: "FileInFirstPiece", + }.Run(t) + + testFileBytesLeft{ + usualPieceSize: 3, + firstPieceIndex: 0, + endPieceIndex: 1, + fileOffset: 1, + fileLength: 1, + expected: 1, + name: "LandLocked", + }.Run(t) + + testFileBytesLeft{ + usualPieceSize: 3, + firstPieceIndex: 1, + endPieceIndex: 3, + fileOffset: 4, + fileLength: 4, + expected: 4, + name: "TwoPieces", + }.Run(t) + + testFileBytesLeft{ + usualPieceSize: 3, + firstPieceIndex: 1, + endPieceIndex: 4, + fileOffset: 5, + fileLength: 7, + expected: 7, + name: "ThreePieces", + }.Run(t) + + testFileBytesLeft{ + usualPieceSize: 3, + firstPieceIndex: 1, + endPieceIndex: 4, + fileOffset: 5, + fileLength: 7, + expected: 0, + completedPieces: func() (ret roaring.Bitmap) { + ret.AddRange(0, 5) + return + }(), + name: "ThreePiecesCompletedAll", + }.Run(t) +} diff --git a/deps/github.com/anacrolix/torrent/fs/TODO b/deps/github.com/anacrolix/torrent/fs/TODO new file mode 100644 index 0000000..9ab12b5 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/fs/TODO @@ -0,0 +1 @@ + * Reinstate InitAsyncRead, or find out if it's worth it. Upstream made it a PITA to apply it automatically. diff --git a/deps/github.com/anacrolix/torrent/fs/cmd/torrentfs/main.go b/deps/github.com/anacrolix/torrent/fs/cmd/torrentfs/main.go new file mode 100644 index 0000000..d35f5c2 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/fs/cmd/torrentfs/main.go @@ -0,0 +1,158 @@ +// Mounts a FUSE filesystem backed by torrents and magnet links. +package main + +import ( + "fmt" + "net" + "net/http" + _ "net/http/pprof" + "os" + "os/signal" + "os/user" + "path/filepath" + "syscall" + "time" + + "github.com/anacrolix/envpprof" + _ "github.com/anacrolix/envpprof" + "github.com/anacrolix/fuse" + fusefs "github.com/anacrolix/fuse/fs" + "github.com/anacrolix/log" + "github.com/anacrolix/tagflag" + + "github.com/anacrolix/torrent" + torrentfs "github.com/anacrolix/torrent/fs" + "github.com/anacrolix/torrent/util/dirwatch" +) + +var logger = log.Default.WithNames("main") + +var args = struct { + MetainfoDir string `help:"torrent files in this location describe the contents of the mounted filesystem"` + DownloadDir string `help:"location to save torrent data"` + MountDir string `help:"location the torrent contents are made available"` + + DisableTrackers bool + TestPeer *net.TCPAddr + ReadaheadBytes tagflag.Bytes + ListenAddr *net.TCPAddr +}{ + MetainfoDir: func() string { + _user, err := user.Current() + if err != nil { + panic(err) + } + return filepath.Join(_user.HomeDir, ".config/transmission/torrents") + }(), + ReadaheadBytes: 10 << 20, + ListenAddr: &net.TCPAddr{}, +} + +func exitSignalHandlers(fs *torrentfs.TorrentFS) { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + for { + <-c + fs.Destroy() + err := fuse.Unmount(args.MountDir) + if err != nil { + log.Print(err) + } + } +} + +func addTestPeer(client *torrent.Client) { + for _, t := range client.Torrents() { + t.AddPeers([]torrent.PeerInfo{{ + Addr: args.TestPeer, + }}) + } +} + +func main() { + defer envpprof.Stop() + err := mainErr() + if err != nil { + logger.Levelf(log.Error, "error in main: %v", err) + os.Exit(1) + } +} + +func mainErr() error { + tagflag.Parse(&args) + if args.MountDir == "" { + os.Stderr.WriteString("y u no specify mountpoint?\n") + os.Exit(2) + } + conn, err := fuse.Mount(args.MountDir, fuse.ReadOnly()) + if err != nil { + return fmt.Errorf("mounting: %w", err) + } + defer fuse.Unmount(args.MountDir) + // TODO: Think about the ramifications of exiting not due to a signal. + defer conn.Close() + cfg := torrent.NewDefaultClientConfig() + cfg.DataDir = args.DownloadDir + cfg.DisableTrackers = args.DisableTrackers + cfg.NoUpload = true // Ensure that downloads are responsive. + cfg.SetListenAddr(args.ListenAddr.String()) + client, err := torrent.NewClient(cfg) + if err != nil { + return fmt.Errorf("creating torrent client: %w", err) + } + // This is naturally exported via GOPPROF=http. + http.DefaultServeMux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + client.WriteStatus(w) + }) + dw, err := dirwatch.New(args.MetainfoDir) + if err != nil { + return fmt.Errorf("watching torrent dir: %w", err) + } + dw.Logger = dw.Logger.FilterLevel(log.Info) + go func() { + for ev := range dw.Events { + switch ev.Change { + case dirwatch.Added: + if ev.TorrentFilePath != "" { + _, err := client.AddTorrentFromFile(ev.TorrentFilePath) + if err != nil { + log.Printf("error adding torrent from file %q to client: %v", ev.TorrentFilePath, err) + } + } else if ev.MagnetURI != "" { + _, err := client.AddMagnet(ev.MagnetURI) + if err != nil { + log.Printf("error adding magnet: %s", err) + } + } + case dirwatch.Removed: + T, ok := client.Torrent(ev.InfoHash) + if !ok { + break + } + T.Drop() + } + } + }() + fs := torrentfs.New(client) + go exitSignalHandlers(fs) + + if args.TestPeer != nil { + go func() { + for { + addTestPeer(client) + time.Sleep(10 * time.Second) + } + }() + } + + logger.Levelf(log.Debug, "serving fuse fs") + if err := fusefs.Serve(conn, fs); err != nil { + return fmt.Errorf("serving fuse fs: %w", err) + } + logger.Levelf(log.Debug, "fuse fs completed successfully. waiting for conn ready") + <-conn.Ready + if err := conn.MountError; err != nil { + return fmt.Errorf("mount error: %w", err) + } + return nil +} diff --git a/deps/github.com/anacrolix/torrent/fs/file_handle.go b/deps/github.com/anacrolix/torrent/fs/file_handle.go new file mode 100644 index 0000000..ce5ded0 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/fs/file_handle.go @@ -0,0 +1,89 @@ +package torrentfs + +import ( + "context" + "io" + + "github.com/anacrolix/fuse" + "github.com/anacrolix/fuse/fs" + "github.com/anacrolix/missinggo/v2" + + "github.com/anacrolix/torrent" +) + +type fileHandle struct { + fn fileNode + tf *torrent.File +} + +var _ interface { + fs.HandleReader + fs.HandleReleaser +} = fileHandle{} + +func (me fileHandle) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { + torrentfsReadRequests.Add(1) + if req.Dir { + panic("read on directory") + } + r := me.tf.NewReader() + defer r.Close() + pos, err := r.Seek(req.Offset, io.SeekStart) + if err != nil { + panic(err) + } + if pos != req.Offset { + panic("seek failed") + } + resp.Data = resp.Data[:req.Size] + readDone := make(chan struct{}) + ctx, cancel := context.WithCancel(ctx) + var readErr error + go func() { + defer close(readDone) + me.fn.FS.mu.Lock() + me.fn.FS.blockedReads++ + me.fn.FS.event.Broadcast() + me.fn.FS.mu.Unlock() + var n int + r := missinggo.ContextedReader{r, ctx} + // log.Printf("reading %v bytes at %v", len(resp.Data), req.Offset) + if true { + // A user reported on that on freebsd 12.2, the system requires that reads are + // completely filled. Their system only asks for 64KiB at a time. I've seen systems that + // can demand up to 16MiB at a time, so this gets tricky. For now, I'll restore the old + // behaviour from before 2a7352a, which nobody reported problems with. + n, readErr = io.ReadFull(r, resp.Data) + if readErr == io.ErrUnexpectedEOF { + readErr = nil + } + } else { + n, readErr = r.Read(resp.Data) + if readErr == io.EOF { + readErr = nil + } + } + resp.Data = resp.Data[:n] + }() + defer func() { + <-readDone + me.fn.FS.mu.Lock() + me.fn.FS.blockedReads-- + me.fn.FS.event.Broadcast() + me.fn.FS.mu.Unlock() + }() + defer cancel() + + select { + case <-readDone: + return readErr + case <-me.fn.FS.destroyed: + return fuse.EIO + case <-ctx.Done(): + return fuse.EINTR + } +} + +func (me fileHandle) Release(context.Context, *fuse.ReleaseRequest) error { + return nil +} diff --git a/deps/github.com/anacrolix/torrent/fs/filenode.go b/deps/github.com/anacrolix/torrent/fs/filenode.go new file mode 100644 index 0000000..28a433e --- /dev/null +++ b/deps/github.com/anacrolix/torrent/fs/filenode.go @@ -0,0 +1,27 @@ +package torrentfs + +import ( + "context" + + "github.com/anacrolix/fuse" + fusefs "github.com/anacrolix/fuse/fs" + + "github.com/anacrolix/torrent" +) + +type fileNode struct { + node + f *torrent.File +} + +var _ fusefs.NodeOpener = fileNode{} + +func (fn fileNode) Attr(ctx context.Context, attr *fuse.Attr) error { + attr.Size = uint64(fn.f.Length()) + attr.Mode = defaultMode + return nil +} + +func (fn fileNode) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (fusefs.Handle, error) { + return fileHandle{fn, fn.f}, nil +} diff --git a/deps/github.com/anacrolix/torrent/fs/test.sh b/deps/github.com/anacrolix/torrent/fs/test.sh new file mode 100644 index 0000000..5374ed8 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/fs/test.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +echo $BASH_VERSION +set -eux +repopath="$(cd "$(dirname "$0")/.."; pwd)" +debian_file=debian-10.8.0-amd64-netinst.iso +mkdir -p mnt torrents +# I think the timing can cause torrents to not get added correctly to the torrentfs client, so add +# them first and start the fs afterwards. +pushd torrents +cp "$repopath/testdata/$debian_file.torrent" . +godo -v -- "$repopath/cmd/torrent" metainfo "$repopath/testdata/sintel.torrent" magnet > sintel.magnet +popd +#file="$debian_file" +file=Sintel/Sintel.mp4 + +GOPPROF=http godo -v -- "$repopath/fs/cmd/torrentfs" -mountDir=mnt -metainfoDir=torrents & +torrentfs_pid=$! +trap "kill $torrentfs_pid" EXIT + +check_file() { + while [ ! -e "mnt/$file" ]; do sleep 1; done + pv -f "mnt/$file" | md5sum -c <(cat <<-EOF + 083e808d56aa7b146f513b3458658292 - + EOF + ) +} + +( check_file ) & +check_file_pid=$! + +trap "kill $torrentfs_pid $check_file_pid" EXIT +wait -n +status=$? +sudo umount mnt +trap - EXIT +echo "wait returned" $status +exit $status diff --git a/deps/github.com/anacrolix/torrent/fs/torrentfs.go b/deps/github.com/anacrolix/torrent/fs/torrentfs.go new file mode 100644 index 0000000..5e0b75e --- /dev/null +++ b/deps/github.com/anacrolix/torrent/fs/torrentfs.go @@ -0,0 +1,215 @@ +package torrentfs + +import ( + "context" + "expvar" + "os" + "strings" + "sync" + + "github.com/anacrolix/fuse" + fusefs "github.com/anacrolix/fuse/fs" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" +) + +const ( + defaultMode = 0o555 +) + +var torrentfsReadRequests = expvar.NewInt("torrentfsReadRequests") + +type TorrentFS struct { + Client *torrent.Client + destroyed chan struct{} + mu sync.Mutex + blockedReads int + event sync.Cond +} + +var ( + _ fusefs.FSDestroyer = &TorrentFS{} + + _ fusefs.NodeForgetter = rootNode{} + _ fusefs.HandleReadDirAller = rootNode{} + _ fusefs.HandleReadDirAller = dirNode{} +) + +// Is a directory node that lists all torrents and handles destruction of the +// filesystem. +type rootNode struct { + fs *TorrentFS +} + +type node struct { + path string + metadata *metainfo.Info + FS *TorrentFS + t *torrent.Torrent +} + +type dirNode struct { + node +} + +var _ fusefs.HandleReadDirAller = dirNode{} + +func isSubPath(parent, child string) bool { + if parent == "" { + return len(child) > 0 + } + if !strings.HasPrefix(child, parent) { + return false + } + extra := child[len(parent):] + if extra == "" { + return false + } + // Not just a file with more stuff on the end. + return extra[0] == '/' +} + +func (dn dirNode) ReadDirAll(ctx context.Context) (des []fuse.Dirent, err error) { + names := map[string]bool{} + for _, fi := range dn.metadata.Files { + filePathname := strings.Join(fi.Path, "/") + if !isSubPath(dn.path, filePathname) { + continue + } + var name string + if dn.path == "" { + name = fi.Path[0] + } else { + dirPathname := strings.Split(dn.path, "/") + name = fi.Path[len(dirPathname)] + } + if names[name] { + continue + } + names[name] = true + de := fuse.Dirent{ + Name: name, + } + if len(fi.Path) == len(dn.path)+1 { + de.Type = fuse.DT_File + } else { + de.Type = fuse.DT_Dir + } + des = append(des, de) + } + return +} + +func (dn dirNode) Lookup(_ context.Context, name string) (fusefs.Node, error) { + dir := false + var file *torrent.File + var fullPath string + if dn.path != "" { + fullPath = dn.path + "/" + name + } else { + fullPath = name + } + for _, f := range dn.t.Files() { + if f.DisplayPath() == fullPath { + file = f + } + if isSubPath(fullPath, f.DisplayPath()) { + dir = true + } + } + n := dn.node + n.path = fullPath + if dir && file != nil { + panic("both dir and file") + } + if file != nil { + return fileNode{n, file}, nil + } + if dir { + return dirNode{n}, nil + } + return nil, fuse.ENOENT +} + +func (dn dirNode) Attr(ctx context.Context, attr *fuse.Attr) error { + attr.Mode = os.ModeDir | defaultMode + return nil +} + +func (rn rootNode) Lookup(ctx context.Context, name string) (_node fusefs.Node, err error) { + for _, t := range rn.fs.Client.Torrents() { + info := t.Info() + if t.Name() != name || info == nil { + continue + } + __node := node{ + metadata: info, + FS: rn.fs, + t: t, + } + if !info.IsDir() { + _node = fileNode{__node, t.Files()[0]} + } else { + _node = dirNode{__node} + } + break + } + if _node == nil { + err = fuse.ENOENT + } + return +} + +func (rn rootNode) ReadDirAll(ctx context.Context) (dirents []fuse.Dirent, err error) { + for _, t := range rn.fs.Client.Torrents() { + info := t.Info() + if info == nil { + continue + } + dirents = append(dirents, fuse.Dirent{ + Name: info.Name, + Type: func() fuse.DirentType { + if !info.IsDir() { + return fuse.DT_File + } else { + return fuse.DT_Dir + } + }(), + }) + } + return +} + +func (rn rootNode) Attr(ctx context.Context, attr *fuse.Attr) error { + attr.Mode = os.ModeDir | defaultMode + return nil +} + +// TODO(anacrolix): Why should rootNode implement this? +func (rn rootNode) Forget() { + rn.fs.Destroy() +} + +func (tfs *TorrentFS) Root() (fusefs.Node, error) { + return rootNode{tfs}, nil +} + +func (tfs *TorrentFS) Destroy() { + tfs.mu.Lock() + select { + case <-tfs.destroyed: + default: + close(tfs.destroyed) + } + tfs.mu.Unlock() +} + +func New(cl *torrent.Client) *TorrentFS { + fs := &TorrentFS{ + Client: cl, + destroyed: make(chan struct{}), + } + fs.event.L = &fs.mu + return fs +} diff --git a/deps/github.com/anacrolix/torrent/fs/torrentfs_test.go b/deps/github.com/anacrolix/torrent/fs/torrentfs_test.go new file mode 100644 index 0000000..097f1bb --- /dev/null +++ b/deps/github.com/anacrolix/torrent/fs/torrentfs_test.go @@ -0,0 +1,240 @@ +package torrentfs + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "net" + _ "net/http/pprof" + "os" + "path/filepath" + "testing" + "time" + + _ "github.com/anacrolix/envpprof" + "github.com/anacrolix/fuse" + fusefs "github.com/anacrolix/fuse/fs" + "github.com/anacrolix/missinggo/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/internal/testutil" + "github.com/anacrolix/torrent/metainfo" + "github.com/anacrolix/torrent/storage" +) + +func init() { + log.SetFlags(log.Flags() | log.Lshortfile) +} + +func TestTCPAddrString(t *testing.T) { + l, err := net.Listen("tcp4", "localhost:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + c, err := net.Dial("tcp", l.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer c.Close() + ras := c.RemoteAddr().String() + ta := &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: missinggo.AddrPort(l.Addr()), + } + s := ta.String() + if ras != s { + t.FailNow() + } +} + +type testLayout struct { + BaseDir string + MountDir string + Completed string + Metainfo *metainfo.MetaInfo +} + +func (tl *testLayout) Destroy() error { + return os.RemoveAll(tl.BaseDir) +} + +func newGreetingLayout(t *testing.T) (tl testLayout, err error) { + tl.BaseDir = t.TempDir() + tl.Completed = filepath.Join(tl.BaseDir, "completed") + os.Mkdir(tl.Completed, 0o777) + tl.MountDir = filepath.Join(tl.BaseDir, "mnt") + os.Mkdir(tl.MountDir, 0o777) + testutil.CreateDummyTorrentData(tl.Completed) + tl.Metainfo = testutil.GreetingMetaInfo() + return +} + +// Unmount without first killing the FUSE connection while there are FUSE +// operations blocked inside the filesystem code. +func TestUnmountWedged(t *testing.T) { + layout, err := newGreetingLayout(t) + require.NoError(t, err) + defer func() { + err := layout.Destroy() + if err != nil { + t.Log(err) + } + }() + cfg := torrent.NewDefaultClientConfig() + cfg.DataDir = filepath.Join(layout.BaseDir, "incomplete") + cfg.DisableTrackers = true + cfg.NoDHT = true + cfg.DisableTCP = true + cfg.DisableUTP = true + client, err := torrent.NewClient(cfg) + require.NoError(t, err) + defer client.Close() + tt, err := client.AddTorrent(layout.Metainfo) + require.NoError(t, err) + fs := New(client) + fuseConn, err := fuse.Mount(layout.MountDir) + if err != nil { + switch err.Error() { + case "cannot locate OSXFUSE": + fallthrough + case "fusermount: exit status 1": + t.Skip(err) + } + t.Fatal(err) + } + go func() { + server := fusefs.New(fuseConn, &fusefs.Config{ + Debug: func(msg interface{}) { + t.Log(msg) + }, + }) + server.Serve(fs) + }() + <-fuseConn.Ready + if err := fuseConn.MountError; err != nil { + t.Fatalf("mount error: %s", err) + } + ctx, cancel := context.WithCancel(context.Background()) + // Read the greeting file, though it will never be available. This should + // "wedge" FUSE, requiring the fs object to be forcibly destroyed. The + // read call will return with a FS error. + go func() { + <-ctx.Done() + fs.mu.Lock() + fs.event.Broadcast() + fs.mu.Unlock() + }() + go func() { + defer cancel() + _, err := ioutil.ReadFile(filepath.Join(layout.MountDir, tt.Info().Name)) + require.Error(t, err) + }() + + // Wait until the read has blocked inside the filesystem code. + fs.mu.Lock() + for fs.blockedReads != 1 && ctx.Err() == nil { + fs.event.Wait() + } + fs.mu.Unlock() + + fs.Destroy() + + for { + err = fuse.Unmount(layout.MountDir) + if err != nil { + t.Logf("error unmounting: %s", err) + time.Sleep(time.Millisecond) + } else { + break + } + } + + err = fuseConn.Close() + assert.NoError(t, err) +} + +func TestDownloadOnDemand(t *testing.T) { + layout, err := newGreetingLayout(t) + require.NoError(t, err) + defer layout.Destroy() + cfg := torrent.NewDefaultClientConfig() + cfg.DataDir = layout.Completed + cfg.DisableTrackers = true + cfg.NoDHT = true + cfg.Seed = true + cfg.ListenPort = 0 + cfg.ListenHost = torrent.LoopbackListenHost + seeder, err := torrent.NewClient(cfg) + require.NoError(t, err) + defer seeder.Close() + defer testutil.ExportStatusWriter(seeder, "s", t)() + // Just to mix things up, the seeder starts with the data, but the leecher + // starts with the metainfo. + seederTorrent, err := seeder.AddMagnet(fmt.Sprintf("magnet:?xt=urn:btih:%s", layout.Metainfo.HashInfoBytes().HexString())) + require.NoError(t, err) + go func() { + // Wait until we get the metainfo, then check for the data. + <-seederTorrent.GotInfo() + seederTorrent.VerifyData() + }() + cfg = torrent.NewDefaultClientConfig() + cfg.DisableTrackers = true + cfg.NoDHT = true + cfg.DisableTCP = true + cfg.DefaultStorage = storage.NewMMap(filepath.Join(layout.BaseDir, "download")) + cfg.ListenHost = torrent.LoopbackListenHost + cfg.ListenPort = 0 + leecher, err := torrent.NewClient(cfg) + require.NoError(t, err) + testutil.ExportStatusWriter(leecher, "l", t)() + defer leecher.Close() + leecherTorrent, err := leecher.AddTorrent(layout.Metainfo) + require.NoError(t, err) + leecherTorrent.AddClientPeer(seeder) + fs := New(leecher) + defer fs.Destroy() + root, _ := fs.Root() + node, _ := root.(fusefs.NodeStringLookuper).Lookup(context.Background(), "greeting") + var attr fuse.Attr + node.Attr(context.Background(), &attr) + size := attr.Size + data := make([]byte, size) + h, err := node.(fusefs.NodeOpener).Open(context.TODO(), nil, nil) + require.NoError(t, err) + + // torrent.Reader.Read no longer tries to fill the entire read buffer, so this is a ReadFull for + // fusefs. + var n int + for n < len(data) { + resp := fuse.ReadResponse{Data: data[n:]} + err := h.(fusefs.HandleReader).Read(context.Background(), &fuse.ReadRequest{ + Size: int(size) - n, + Offset: int64(n), + }, &resp) + assert.NoError(t, err) + n += len(resp.Data) + } + + assert.EqualValues(t, testutil.GreetingFileContents, data) +} + +func TestIsSubPath(t *testing.T) { + for _, case_ := range []struct { + parent, child string + is bool + }{ + {"", "", false}, + {"", "/", true}, + {"", "a", true}, + {"a/b", "a/bc", false}, + {"a/b", "a/b", false}, + {"a/b", "a/b/c", true}, + {"a/b", "a//b", false}, + } { + assert.Equal(t, case_.is, isSubPath(case_.parent, case_.child)) + } +} diff --git a/deps/github.com/anacrolix/torrent/fs/unwedge-tests.sh b/deps/github.com/anacrolix/torrent/fs/unwedge-tests.sh new file mode 100644 index 0000000..322a280 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/fs/unwedge-tests.sh @@ -0,0 +1,5 @@ +shopt -s nullglob +for a in "${TMPDIR:-/tmp}"/torrentfs*; do + sudo umount -f "$a/mnt" + rm -r -- "$a" +done diff --git a/deps/github.com/anacrolix/torrent/global.go b/deps/github.com/anacrolix/torrent/global.go new file mode 100644 index 0000000..5a5bddb --- /dev/null +++ b/deps/github.com/anacrolix/torrent/global.go @@ -0,0 +1,65 @@ +package torrent + +import ( + "crypto" + "expvar" + + pp "github.com/anacrolix/torrent/peer_protocol" +) + +const ( + pieceHash = crypto.SHA1 + defaultChunkSize = 0x4000 // 16KiB + + // Arbitrary maximum of "metadata_size" (see https://www.bittorrent.org/beps/bep_0009.html) + // libtorrent-rasterbar uses 4MiB at last check. TODO: Add links to values used by other + // implementations here. I saw 14143527 in the metainfo for + // 3597f16e239aeb8f8524a1a1c4e4725a0a96b470. Large values for legitimate torrents should be + // recorded here for consideration. + maxMetadataSize uint32 = 16 * 1024 * 1024 +) + +// These are our extended message IDs. Peers will use these values to +// select which extension a message is intended for. +const ( + metadataExtendedId = iota + 1 // 0 is reserved for deleting keys + pexExtendedId + utHolepunchExtendedId +) + +func defaultPeerExtensionBytes() PeerExtensionBits { + return pp.NewPeerExtensionBytes(pp.ExtensionBitDht, pp.ExtensionBitLtep, pp.ExtensionBitFast) +} + +func init() { + torrent.Set("peers supporting extension", &peersSupportingExtension) + torrent.Set("chunks received", &chunksReceived) +} + +// I could move a lot of these counters to their own file, but I suspect they +// may be attached to a Client someday. +var ( + torrent = expvar.NewMap("torrent") + peersSupportingExtension expvar.Map + chunksReceived expvar.Map + + pieceHashedCorrect = expvar.NewInt("pieceHashedCorrect") + pieceHashedNotCorrect = expvar.NewInt("pieceHashedNotCorrect") + + completedHandshakeConnectionFlags = expvar.NewMap("completedHandshakeConnectionFlags") + // Count of connections to peer with same client ID. + connsToSelf = expvar.NewInt("connsToSelf") + receivedKeepalives = expvar.NewInt("receivedKeepalives") + // Requests received for pieces we don't have. + requestsReceivedForMissingPieces = expvar.NewInt("requestsReceivedForMissingPieces") + requestedChunkLengths = expvar.NewMap("requestedChunkLengths") + + messageTypesReceived = expvar.NewMap("messageTypesReceived") + + // Track the effectiveness of Torrent.connPieceInclinationPool. + pieceInclinationsReused = expvar.NewInt("pieceInclinationsReused") + pieceInclinationsNew = expvar.NewInt("pieceInclinationsNew") + pieceInclinationsPut = expvar.NewInt("pieceInclinationsPut") + + concurrentChunkWrites = expvar.NewInt("torrentConcurrentChunkWrites") +) diff --git a/deps/github.com/anacrolix/torrent/go.mod b/deps/github.com/anacrolix/torrent/go.mod new file mode 100644 index 0000000..6812000 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/go.mod @@ -0,0 +1,127 @@ +module github.com/anacrolix/torrent + +go 1.20 + +require ( + github.com/RoaringBitmap/roaring v1.2.3 + github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 + github.com/alexflint/go-arg v1.4.3 + github.com/anacrolix/bargle v0.0.0-20220630015206-d7a4d433886a + github.com/anacrolix/chansync v0.3.0 + github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 + github.com/anacrolix/envpprof v1.3.0 + github.com/anacrolix/fuse v0.2.0 + github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 + github.com/anacrolix/go-libutp v1.3.1 + github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 + github.com/anacrolix/missinggo v1.3.0 + github.com/anacrolix/missinggo/perf v1.0.0 + github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 + github.com/anacrolix/multiless v0.3.0 + github.com/anacrolix/squirrel v0.6.0 + github.com/anacrolix/sync v0.5.1 + github.com/anacrolix/tagflag v1.3.0 + github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 + github.com/anacrolix/utp v0.1.0 + github.com/bahlo/generic-list-go v0.2.0 + github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 + github.com/davecgh/go-spew v1.1.1 + github.com/dustin/go-humanize v1.0.0 + github.com/edsrzf/mmap-go v1.1.0 + github.com/elliotchance/orderedmap v1.4.0 + github.com/frankban/quicktest v1.14.6 + github.com/fsnotify/fsnotify v1.5.4 + github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 + github.com/google/btree v1.1.2 + github.com/google/go-cmp v0.5.9 + github.com/gorilla/websocket v1.5.0 + github.com/jessevdk/go-flags v1.5.0 + github.com/pion/datachannel v1.5.2 + github.com/pion/logging v0.2.2 + github.com/pion/webrtc/v3 v3.1.42 + github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.12.2 + github.com/stretchr/testify v1.8.1 + github.com/tidwall/btree v1.6.0 + go.etcd.io/bbolt v1.3.6 + go.opentelemetry.io/otel v1.8.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.8.0 + go.opentelemetry.io/otel/sdk v1.8.0 + go.opentelemetry.io/otel/trace v1.8.0 + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df + golang.org/x/sys v0.15.0 + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 +) + +require ( + github.com/alecthomas/atomic v0.1.0-alpha2 // indirect + github.com/alexflint/go-scalar v1.1.0 // indirect + github.com/anacrolix/mmsg v1.0.0 // indirect + github.com/anacrolix/stm v0.4.0 // indirect + github.com/benbjohnson/immutable v0.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.2.2 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/go-llsqlite/crawshaw v0.4.0 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mschoch/smat v0.2.0 // indirect + github.com/pion/dtls/v2 v2.2.4 // indirect + github.com/pion/ice/v2 v2.2.6 // indirect + github.com/pion/interceptor v0.1.11 // indirect + github.com/pion/mdns v0.0.5 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.9 // indirect + github.com/pion/rtp v1.7.13 // indirect + github.com/pion/sctp v1.8.2 // indirect + github.com/pion/sdp/v3 v3.0.5 // indirect + github.com/pion/srtp/v2 v2.0.9 // indirect + github.com/pion/stun v0.3.5 // indirect + github.com/pion/transport v0.13.1 // indirect + github.com/pion/transport/v2 v2.0.0 // indirect + github.com/pion/turn/v2 v2.0.8 // indirect + github.com/pion/udp v0.1.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.35.0 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect + github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect + go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0 // indirect + go.opentelemetry.io/proto/otlp v0.18.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect + google.golang.org/grpc v1.46.2 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.3 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.21.1 // indirect + zombiezen.com/go/sqlite v0.13.1 // indirect +) + +retract ( + // Doesn't signal interest to peers if choked when piece priorities change. + v1.39.0 + // peer-requesting doesn't scale + [v1.34.0, v1.38.1] + // Indefinite outgoing requests on storage write errors. https://github.com/anacrolix/torrent/issues/889 + [v1.29.0, v1.53.2] +) diff --git a/deps/github.com/anacrolix/torrent/go.sum b/deps/github.com/anacrolix/torrent/go.sum new file mode 100644 index 0000000..ecf8519 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/go.sum @@ -0,0 +1,908 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= +crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= +github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= +github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= +github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY= +github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0= +github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k= +github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk= +github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= +github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo= +github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA= +github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM= +github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/anacrolix/bargle v0.0.0-20220630015206-d7a4d433886a h1:KCP9QvHlLoUQBOaTf/YCuOzG91Ym1cPB6S68O4Q3puo= +github.com/anacrolix/bargle v0.0.0-20220630015206-d7a4d433886a/go.mod h1:9xUiZbkh+94FbiIAL1HXpAIBa832f3Mp07rRPl5c5RQ= +github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U= +github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= +github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE= +github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew= +github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= +github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= +github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= +github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk= +github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0= +github.com/anacrolix/fuse v0.2.0 h1:pc+To78kI2d/WUjIyrsdqeJQAesuwpGxlI3h1nAv3Do= +github.com/anacrolix/fuse v0.2.0/go.mod h1:Kfu02xBwnySDpH3N23BmrP3MDfwAQGRLUCj6XyeOvBQ= +github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 h1:Kmcl3I9K2+5AdnnR7hvrnVT0TLeFWWMa9bxnm55aVIg= +github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8= +github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0= +github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o= +github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4= +github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68= +github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 h1:CdVK9IoqoqklXQQ4+L2aew64xsz14KdOD+rnKdTQajg= +github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY= +github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM= +github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM= +github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s= +github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y= +github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw= +github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc= +github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy6BqESAJVw= +github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= +github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY= +github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA= +github.com/anacrolix/missinggo/v2 v2.5.2/go.mod h1:yNvsLrtZYRYCOI+KRH/JM8TodHjtIE/bjOGhQaLOWIE= +github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 h1:W/oGeHhYwxueeiDjQfmK9G+X9M2xJgfTtow62v0TWAs= +github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac= +github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw= +github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg= +github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc= +github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis= +github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= +github.com/anacrolix/squirrel v0.6.0 h1:ovfWW42wcGzrVYYI9s56pEYzfeTwtXxCCvSd+KwvUEA= +github.com/anacrolix/squirrel v0.6.0/go.mod h1:60vdNPUbK1jYWePp39Wqn9whHm12Yb9JEuwOXzLMDuY= +github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= +github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY= +github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8= +github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk= +github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= +github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc= +github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= +github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/tagflag v1.3.0 h1:5NI+9CniDnEH0BWA4UcQbERyFPjKJqZnVkItGVIDy/s= +github.com/anacrolix/tagflag v1.3.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA= +github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs= +github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4= +github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= +github.com/benbjohnson/immutable v0.3.0 h1:TVRhuZx2wG9SZ0LRdqlbs9S5BZ6Y24hJEHTCgWHZEIw= +github.com/benbjohnson/immutable v0.3.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk= +github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= +github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/elliotchance/orderedmap v1.4.0 h1:wZtfeEONCbx6in1CZyE6bELEt/vFayMvsxqI5SgsR+A= +github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 h1:OyQmpAN302wAopDgwVjgs2HkFawP9ahIEqkUYz7V7CA= +github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU= +github.com/go-llsqlite/crawshaw v0.4.0 h1:L02s2jZBBJj80xm1VkkdyB/JlQ/Fi0kLbNHfXA8yrec= +github.com/go-llsqlite/crawshaw v0.4.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E= +github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ= +github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus= +github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY= +github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg= +github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw= +github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig= +github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE= +github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs= +github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= +github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U= +github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= +github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= +github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= +github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA= +github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= +github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU= +github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= +github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ= +github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4= +github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= +github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= +github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= +github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA= +github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg= +github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4= +github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= +github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw= +github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= +github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= +github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= +github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA= +github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.35.0 h1:Eyr+Pw2VymWejHqCugNaQXkAi6KayVNxaHeu6khmFBE= +github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= +github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= +github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= +github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg= +go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0 h1:ao8CJIShCaIbaMsGxy+jp2YHSudketpDgDRcbirov78= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.8.0/go.mod h1:78XhIg8Ht9vR4tbLNUhXsiOnE2HOuSeKAiAcoVQEpOY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0 h1:LrHL1A3KqIgAgi6mK7Q0aczmzU414AONAGT5xtnp+uo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.8.0/go.mod h1:w8aZL87GMOvOBa2lU/JlVXE1q4chk/0FX+8ai4513bw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.8.0 h1:00hCSGLIxdYK/Z7r8GkaX0QIlfvgU3tmnLlQvcnix6U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.8.0/go.mod h1:twhIvtDQW2sWP1O2cT1N8nkSBgKCRZv2z6COTTBrf8Q= +go.opentelemetry.io/otel/sdk v1.8.0 h1:xwu69/fNuwbSHWe/0PGS888RmjWY181OmcXDQKu7ZQk= +go.opentelemetry.io/otel/sdk v1.8.0/go.mod h1:uPSfc+yfDH2StDM/Rm35WE8gXSNdvCg023J6HeGNO0c= +go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY= +go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.18.0 h1:W5hyXNComRa23tGpKwG+FRAc4rfF6ZUg1JReK+QHS80= +go.opentelemetry.io/proto/otlp v0.18.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200423201157-2723c5de0d66/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 h1:b9mVrqYfq3P4bCdaLg1qtBnPzUYgglsIdjZkL/fQVOE= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY= +modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU= +modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o= +zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4= diff --git a/deps/github.com/anacrolix/torrent/handshake.go b/deps/github.com/anacrolix/torrent/handshake.go new file mode 100644 index 0000000..b38a708 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/handshake.go @@ -0,0 +1,72 @@ +package torrent + +import ( + "bytes" + "fmt" + "io" + "net" + "time" + + "github.com/anacrolix/torrent/mse" + pp "github.com/anacrolix/torrent/peer_protocol" +) + +// Wraps a raw connection and provides the interface we want for using the +// connection in the message loop. +type deadlineReader struct { + nc net.Conn + r io.Reader +} + +func (r deadlineReader) Read(b []byte) (int, error) { + // Keep-alives should be received every 2 mins. Give a bit of gracetime. + err := r.nc.SetReadDeadline(time.Now().Add(150 * time.Second)) + if err != nil { + return 0, fmt.Errorf("error setting read deadline: %s", err) + } + return r.r.Read(b) +} + +// Handles stream encryption for inbound connections. +func handleEncryption( + rw io.ReadWriter, + skeys mse.SecretKeyIter, + policy HeaderObfuscationPolicy, + selector mse.CryptoSelector, +) ( + ret io.ReadWriter, + headerEncrypted bool, + cryptoMethod mse.CryptoMethod, + err error, +) { + // Tries to start an unencrypted stream. + if !policy.RequirePreferred || !policy.Preferred { + var protocol [len(pp.Protocol)]byte + _, err = io.ReadFull(rw, protocol[:]) + if err != nil { + return + } + // Put the protocol back into the stream. + rw = struct { + io.Reader + io.Writer + }{ + io.MultiReader(bytes.NewReader(protocol[:]), rw), + rw, + } + if string(protocol[:]) == pp.Protocol { + ret = rw + return + } + if policy.RequirePreferred { + // We are here because we require unencrypted connections. + err = fmt.Errorf("unexpected protocol string %q and header obfuscation disabled", protocol) + return + } + } + headerEncrypted = true + ret, cryptoMethod, err = mse.ReceiveHandshake(rw, skeys, selector) + return +} + +type PeerExtensionBits = pp.PeerExtensionBits diff --git a/deps/github.com/anacrolix/torrent/handshake_test.go b/deps/github.com/anacrolix/torrent/handshake_test.go new file mode 100644 index 0000000..8c2c6d2 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/handshake_test.go @@ -0,0 +1,15 @@ +package torrent + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultExtensionBytes(t *testing.T) { + pex := defaultPeerExtensionBytes() + assert.True(t, pex.SupportsDHT()) + assert.True(t, pex.SupportsExtended()) + assert.False(t, pex.GetBit(63)) + assert.Panics(t, func() { pex.GetBit(64) }) +} diff --git a/deps/github.com/anacrolix/torrent/internal/alloclim/alloclim_test.go b/deps/github.com/anacrolix/torrent/internal/alloclim/alloclim_test.go new file mode 100644 index 0000000..5952804 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/alloclim/alloclim_test.go @@ -0,0 +1,93 @@ +package alloclim + +import ( + "context" + "testing" + "time" + + _ "github.com/anacrolix/envpprof" + qt "github.com/frankban/quicktest" +) + +func TestReserveOverMax(t *testing.T) { + c := qt.New(t) + l := &Limiter{Max: 10} + r := l.Reserve(20) + c.Assert(r.Wait(context.Background()), qt.IsNotNil) +} + +func TestImmediateAllow(t *testing.T) { + c := qt.New(t) + l := &Limiter{Max: 10} + r := l.Reserve(10) + c.Assert(r.Wait(context.Background()), qt.IsNil) +} + +func TestSimpleSequence(t *testing.T) { + c := qt.New(t) + l := &Limiter{Max: 10} + rs := make([]*Reservation, 0) + rs = append(rs, l.Reserve(6)) + rs = append(rs, l.Reserve(5)) + rs = append(rs, l.Reserve(5)) + c.Assert(rs[0].Wait(context.Background()), qt.IsNil) + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Nanosecond)) + c.Assert(rs[1].Wait(ctx), qt.Equals, context.DeadlineExceeded) + go cancel() + ctx, cancel = context.WithCancel(context.Background()) + go cancel() + c.Assert(rs[2].Wait(ctx), qt.Equals, context.Canceled) + go rs[0].Release() + ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Second)) + c.Assert(rs[1].Wait(ctx), qt.IsNil) + go rs[1].Release() + c.Assert(rs[2].Wait(ctx), qt.IsNil) + go rs[2].Release() + go cancel() + rs[2].Release() + rs[1].Release() + c.Assert(l.Value(), qt.Equals, l.Max) +} + +func TestSequenceWithCancel(t *testing.T) { + c := qt.New(t) + l := &Limiter{Max: 10} + rs := make([]*Reservation, 0) + rs = append(rs, l.Reserve(6)) + rs = append(rs, l.Reserve(6)) + rs = append(rs, l.Reserve(4)) + rs = append(rs, l.Reserve(4)) + c.Assert(rs[0].Cancel(), qt.IsFalse) + c.Assert(func() { rs[1].Release() }, qt.PanicMatches, "not resolved") + c.Assert(rs[1].Cancel(), qt.IsTrue) + c.Assert(rs[2].Wait(context.Background()), qt.IsNil) + rs[0].Release() + c.Assert(rs[3].Wait(context.Background()), qt.IsNil) + c.Assert(l.Value(), qt.Equals, int64(2)) + rs[1].Release() + rs[2].Release() + rs[3].Release() + c.Assert(l.Value(), qt.Equals, l.Max) +} + +func TestCancelWhileWaiting(t *testing.T) { + c := qt.New(t) + l := &Limiter{Max: 10} + rs := make([]*Reservation, 0) + rs = append(rs, l.Reserve(6)) + rs = append(rs, l.Reserve(6)) + rs = append(rs, l.Reserve(4)) + rs = append(rs, l.Reserve(4)) + go rs[1].Cancel() + err := rs[1].Wait(context.Background()) + c.Assert(err, qt.IsNotNil) + err = rs[2].Wait(context.Background()) + c.Assert(err, qt.IsNil) + ctx, cancel := context.WithCancel(context.Background()) + go cancel() + err = rs[3].Wait(ctx) + c.Assert(err, qt.Equals, context.Canceled) + rs[0].Drop() + err = rs[3].Wait(ctx) + c.Assert(err, qt.IsNil) +} diff --git a/deps/github.com/anacrolix/torrent/internal/alloclim/l.go b/deps/github.com/anacrolix/torrent/internal/alloclim/l.go new file mode 100644 index 0000000..98be1a1 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/alloclim/l.go @@ -0,0 +1,80 @@ +package alloclim + +import "sync" + +// Manages reservations sharing a common allocation limit. +type Limiter struct { + // Maximum outstanding allocation space. + Max int64 + initOnce sync.Once + mu sync.Mutex + // Current unallocated space. + value int64 + // Reservations waiting to in the order they arrived. + waiting []*Reservation +} + +func (me *Limiter) initValue() { + me.value = me.Max +} + +func (me *Limiter) init() { + me.initOnce.Do(func() { + me.initValue() + }) +} + +func (me *Limiter) Reserve(n int64) *Reservation { + r := &Reservation{ + l: me, + n: n, + } + me.init() + me.mu.Lock() + if n <= me.value { + me.value -= n + r.granted.Set() + } else { + me.waiting = append(me.waiting, r) + } + me.mu.Unlock() + return r +} + +func (me *Limiter) doWakesLocked() { + for { + if len(me.waiting) == 0 { + break + } + r := me.waiting[0] + switch { + case r.cancelled.IsSet(): + case r.n <= me.value: + if r.wake() { + me.value -= r.n + } + default: + return + } + me.waiting = me.waiting[1:] + } +} + +func (me *Limiter) doWakes() { + me.mu.Lock() + me.doWakesLocked() + me.mu.Unlock() +} + +func (me *Limiter) addValue(n int64) { + me.mu.Lock() + me.value += n + me.doWakesLocked() + me.mu.Unlock() +} + +func (me *Limiter) Value() int64 { + me.mu.Lock() + defer me.mu.Unlock() + return me.value +} diff --git a/deps/github.com/anacrolix/torrent/internal/alloclim/r.go b/deps/github.com/anacrolix/torrent/internal/alloclim/r.go new file mode 100644 index 0000000..71a4dd7 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/alloclim/r.go @@ -0,0 +1,101 @@ +package alloclim + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/anacrolix/chansync" + "github.com/anacrolix/log" +) + +type Reservation struct { + l *Limiter + n int64 + releaseOnce sync.Once + mu sync.Mutex + granted chansync.SetOnce + cancelled chansync.SetOnce +} + +// Releases the alloc claim if the reservation has been granted. Does nothing if it was cancelled. +// Otherwise panics. +func (me *Reservation) Release() { + me.mu.Lock() + defer me.mu.Unlock() + switch { + default: + panic("not resolved") + case me.cancelled.IsSet(): + return + case me.granted.IsSet(): + } + me.releaseOnce.Do(func() { + me.l.addValue(me.n) + }) +} + +// Cancel the reservation, returns false if it was already granted. You must still release if that's +// the case. See Drop. +func (me *Reservation) Cancel() bool { + me.mu.Lock() + defer me.mu.Unlock() + if me.granted.IsSet() { + return false + } + if me.cancelled.Set() { + go me.l.doWakes() + } + return true +} + +// If the reservation is granted, release it, otherwise cancel the reservation. +func (me *Reservation) Drop() { + me.mu.Lock() + defer me.mu.Unlock() + if me.granted.IsSet() { + me.releaseOnce.Do(func() { + me.l.addValue(me.n) + }) + return + } + if me.cancelled.Set() { + go me.l.doWakes() + } +} + +func (me *Reservation) wake() bool { + me.mu.Lock() + defer me.mu.Unlock() + if me.cancelled.IsSet() { + return false + } + return me.granted.Set() +} + +func (me *Reservation) Wait(ctx context.Context) error { + if me.n > me.l.Max { + return log.WithLevel( + log.Warning, + fmt.Errorf("reservation for %v exceeds limiter max %v", me.n, me.l.Max), + ) + } + select { + case <-ctx.Done(): + case <-me.granted.Done(): + case <-me.cancelled.Done(): + } + defer me.mu.Unlock() + me.mu.Lock() + switch { + case me.granted.IsSet(): + return nil + case me.cancelled.IsSet(): + return errors.New("reservation cancelled") + case ctx.Err() != nil: + return ctx.Err() + default: + panic("unexpected") + } +} diff --git a/deps/github.com/anacrolix/torrent/internal/check/check.go b/deps/github.com/anacrolix/torrent/internal/check/check.go new file mode 100644 index 0000000..aa75e59 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/check/check.go @@ -0,0 +1,5 @@ +package check + +// A flag for doing extra checks at runtime that are potentially expensive. Should be enabled for +// testing and debugging. +var Enabled = false diff --git a/deps/github.com/anacrolix/torrent/internal/check/check_testing.go b/deps/github.com/anacrolix/torrent/internal/check/check_testing.go new file mode 100644 index 0000000..3ec404e --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/check/check_testing.go @@ -0,0 +1,11 @@ +//go:build go1.21 + +package check + +import "testing" + +func init() { + if testing.Testing() { + Enabled = true + } +} diff --git a/deps/github.com/anacrolix/torrent/internal/cmd/issue-464/main.go b/deps/github.com/anacrolix/torrent/internal/cmd/issue-464/main.go new file mode 100644 index 0000000..fbac1b4 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/cmd/issue-464/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "io" + "log" + + "github.com/anacrolix/torrent" +) + +const testMagnet = "magnet:?xt=urn:btih:a88fda5954e89178c372716a6a78b8180ed4dad3&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F" + +func main() { + err := mainErr() + if err != nil { + log.Fatalf("error in main: %v", err) + } +} + +func mainErr() error { + cfg := torrent.NewDefaultClientConfig() + // We could disable non-webseed peer types here, to force any errors. + client, _ := torrent.NewClient(cfg) + + // Add directly from metainfo, because we want to force webseeding to serve data, and webseeding + // won't get us the metainfo. + t, err := client.AddTorrentFromFile("testdata/The WIRED CD - Rip. Sample. Mash. Share.torrent") + if err != nil { + return err + } + <-t.GotInfo() + + fmt.Println("GOT INFO") + + f := t.Files()[0] + + r := f.NewReader() + + r.Seek(5, io.SeekStart) + buf := make([]byte, 5) + n, err := r.Read(buf) + + fmt.Println("END", n, buf, err) + + t.DownloadAll() + client.WaitAll() + return nil +} diff --git a/deps/github.com/anacrolix/torrent/internal/cmd/issue-465/main.go b/deps/github.com/anacrolix/torrent/internal/cmd/issue-465/main.go new file mode 100644 index 0000000..5407535 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/cmd/issue-465/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "os" + + "github.com/anacrolix/torrent" + "github.com/anacrolix/torrent/metainfo" +) + +func main() { + if err := dlTorrents("."); err != nil { + fmt.Fprintf(os.Stderr, "fatal error: %v\n", err) + os.Exit(1) + } +} + +func dlTorrents(dir string) error { + conf := torrent.NewDefaultClientConfig() + conf.DataDir = dir + cl, err := torrent.NewClient(conf) + if err != nil { + return err + } + http.HandleFunc("/torrentClientStatus", func(w http.ResponseWriter, r *http.Request) { + cl.WriteStatus(w) + }) + ids := []string{ + "urlteam_2021-02-03-21-17-02", + "urlteam_2021-02-02-11-17-02", + "urlteam_2021-01-31-11-17-02", + "urlteam_2021-01-30-21-17-01", + "urlteam_2021-01-29-21-17-01", + "urlteam_2021-01-28-11-17-01", + "urlteam_2021-01-27-11-17-02", + "urlteam_2021-01-26-11-17-02", + "urlteam_2021-01-25-03-17-02", + "urlteam_2021-01-24-03-17-02", + } + for _, id := range ids { + t, err := addTorrentFromURL(cl, fmt.Sprintf("https://archive.org/download/%s/%s_archive.torrent", id, id)) + if err != nil { + return fmt.Errorf("downloading metainfo for %q: %w", id, err) + } + t.DownloadAll() + } + if !cl.WaitAll() { + return errors.New("client stopped early") + } + return nil +} + +func addTorrentFromURL(cl *torrent.Client, url string) (*torrent.Torrent, error) { + fmt.Printf("Adding torrent: %s\n", url) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status %s", resp.Status) + } + defer resp.Body.Close() + meta, err := metainfo.Load(resp.Body) + if err != nil { + return nil, err + } + return cl.AddTorrent(meta) +} diff --git a/deps/github.com/anacrolix/torrent/internal/limiter/limiter.go b/deps/github.com/anacrolix/torrent/internal/limiter/limiter.go new file mode 100644 index 0000000..1fd29db --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/limiter/limiter.go @@ -0,0 +1,64 @@ +package limiter + +import "sync" + +type Key = interface{} + +// Manages resources with a limited number of concurrent slots for use for each key. +type Instance struct { + SlotsPerKey int + + mu sync.Mutex + // Limits concurrent use of a resource. Push into the channel to use a slot, and receive to free + // up a slot. + active map[Key]*activeValueType +} + +type activeValueType struct { + ch chan struct{} + refs int +} + +type ActiveValueRef struct { + v *activeValueType + k Key + i *Instance +} + +// Returns the limiting channel. Send to it to obtain a slot, and receive to release the slot. +func (me ActiveValueRef) C() chan struct{} { + return me.v.ch +} + +// Drop the reference to a key, this allows keys to be reclaimed when they're no longer in use. +func (me ActiveValueRef) Drop() { + me.i.mu.Lock() + defer me.i.mu.Unlock() + me.v.refs-- + if me.v.refs == 0 { + delete(me.i.active, me.k) + } +} + +// Get a reference to the values for a key. You should make sure to call Drop exactly once on the +// returned value when done. +func (i *Instance) GetRef(key Key) ActiveValueRef { + i.mu.Lock() + defer i.mu.Unlock() + if i.active == nil { + i.active = make(map[Key]*activeValueType) + } + v, ok := i.active[key] + if !ok { + v = &activeValueType{ + ch: make(chan struct{}, i.SlotsPerKey), + } + i.active[key] = v + } + v.refs++ + return ActiveValueRef{ + v: v, + k: key, + i: i, + } +} diff --git a/deps/github.com/anacrolix/torrent/internal/nestedmaps/nestedmaps.go b/deps/github.com/anacrolix/torrent/internal/nestedmaps/nestedmaps.go new file mode 100644 index 0000000..62ebdcc --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/nestedmaps/nestedmaps.go @@ -0,0 +1,73 @@ +package nestedmaps + +type next[NK comparable, M ~map[NK]NV, NV any] struct { + last Path[M] + key NK +} + +func (me next[NK, CV, NV]) Exists() bool { + _, ok := me.last.Get()[me.key] + return ok +} + +func (me next[NK, CV, NV]) Get() NV { + return me.last.Get()[me.key] +} + +func (me next[NK, CV, NV]) Set(value NV) { + if me.last.Get() == nil { + me.last.Set(make(CV)) + } + me.last.Get()[me.key] = value +} + +func (me next[NK, CV, NV]) Delete() { + m := me.last.Get() + delete(m, me.key) + if len(m) == 0 { + me.last.Delete() + } +} + +func Next[K comparable, M ~map[K]V, V any]( + last Path[M], + key K, +) Path[V] { + ret := next[K, M, V]{} + ret.last = last + ret.key = key + return ret +} + +type root[K comparable, V any, M ~map[K]V] struct { + m *M +} + +func (me root[K, V, M]) Exists() bool { + return *me.m != nil +} + +func (me root[K, V, M]) Get() M { + return *me.m +} + +func (me root[K, V, M]) Set(value M) { + *me.m = value +} + +func (me root[K, V, M]) Delete() { + *me.m = nil +} + +func Begin[K comparable, M ~map[K]V, V any](m *M) Path[M] { + ret := root[K, V, M]{} + ret.m = m + return ret +} + +type Path[V any] interface { + Set(V) + Get() V + Exists() bool + Delete() +} diff --git a/deps/github.com/anacrolix/torrent/internal/nestedmaps/nestedmaps_test.go b/deps/github.com/anacrolix/torrent/internal/nestedmaps/nestedmaps_test.go new file mode 100644 index 0000000..97916af --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/nestedmaps/nestedmaps_test.go @@ -0,0 +1,41 @@ +package nestedmaps + +import ( + "testing" + + g "github.com/anacrolix/generics" + qt "github.com/frankban/quicktest" +) + +func TestNestedMaps(t *testing.T) { + c := qt.New(t) + var nest map[string]map[*int]map[byte][]int64 + intKey := g.PtrTo(420) + var root = Begin(&nest) + var first = Next(root, "answer") + var second = Next(first, intKey) + var last = Next(second, 69) + c.Assert(root.Exists(), qt.IsFalse) + c.Assert(first.Exists(), qt.IsFalse) + c.Assert(second.Exists(), qt.IsFalse) + c.Assert(last.Exists(), qt.IsFalse) + last.Set([]int64{4, 8, 15, 16, 23, 42}) + c.Assert(root.Exists(), qt.IsTrue) + c.Assert(first.Exists(), qt.IsTrue) + c.Assert(second.Exists(), qt.IsTrue) + c.Assert(last.Exists(), qt.IsTrue) + c.Assert(Next(second, 70).Exists(), qt.IsFalse) + secondIntKey := g.PtrTo(1337) + secondPath := Next(Next(Next(Begin(&nest), "answer"), secondIntKey), 42) + secondPath.Set(nil) + c.Assert(secondPath.Exists(), qt.IsTrue) + last.Delete() + c.Assert(last.Exists(), qt.IsFalse) + c.Assert(second.Exists(), qt.IsFalse) + c.Assert(root.Exists(), qt.IsTrue) + c.Assert(first.Exists(), qt.IsTrue) + // See if we get panics deleting an already deleted item. + last.Delete() + secondPath.Delete() + c.Assert(root.Exists(), qt.IsFalse) +} diff --git a/deps/github.com/anacrolix/torrent/internal/panicif/panicif.go b/deps/github.com/anacrolix/torrent/internal/panicif/panicif.go new file mode 100644 index 0000000..c12d09c --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/panicif/panicif.go @@ -0,0 +1,21 @@ +package panicif + +import "fmt" + +func NotEqual[T comparable](a, b T) { + if a != b { + panic(fmt.Sprintf("%v != %v", a, b)) + } +} + +func False(b bool) { + if !b { + panic("is false") + } +} + +func True(b bool) { + if b { + panic("is true") + } +} diff --git a/deps/github.com/anacrolix/torrent/internal/testutil/greeting.go b/deps/github.com/anacrolix/torrent/internal/testutil/greeting.go new file mode 100644 index 0000000..6544483 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/testutil/greeting.go @@ -0,0 +1,50 @@ +// Package testutil contains stuff for testing torrent-related behaviour. +// +// "greeting" is a single-file torrent of a file called "greeting" that +// "contains "hello, world\n". + +package testutil + +import ( + "os" + "path/filepath" + + "github.com/anacrolix/torrent/metainfo" +) + +var Greeting = Torrent{ + Files: []File{{ + Data: GreetingFileContents, + }}, + Name: GreetingFileName, +} + +const ( + // A null in the middle triggers an error if SQLite stores data as text instead of blob. + GreetingFileContents = "hello,\x00world\n" + GreetingFileName = "greeting" +) + +func CreateDummyTorrentData(dirName string) string { + f, _ := os.Create(filepath.Join(dirName, "greeting")) + defer f.Close() + f.WriteString(GreetingFileContents) + return f.Name() +} + +func GreetingMetaInfo() *metainfo.MetaInfo { + return Greeting.Metainfo(5) +} + +// Gives a temporary directory containing the completed "greeting" torrent, +// and a corresponding metainfo describing it. The temporary directory can be +// cleaned away with os.RemoveAll. +func GreetingTestTorrent() (tempDir string, metaInfo *metainfo.MetaInfo) { + tempDir, err := os.MkdirTemp(os.TempDir(), "") + if err != nil { + panic(err) + } + CreateDummyTorrentData(tempDir) + metaInfo = GreetingMetaInfo() + return +} diff --git a/deps/github.com/anacrolix/torrent/internal/testutil/spec.go b/deps/github.com/anacrolix/torrent/internal/testutil/spec.go new file mode 100644 index 0000000..63e4a74 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/testutil/spec.go @@ -0,0 +1,67 @@ +package testutil + +import ( + "io" + "strings" + + "github.com/anacrolix/missinggo/expect" + + "github.com/anacrolix/torrent/bencode" + "github.com/anacrolix/torrent/metainfo" +) + +type File struct { + Name string + Data string +} + +type Torrent struct { + Files []File + Name string +} + +func (t *Torrent) IsDir() bool { + return len(t.Files) == 1 && t.Files[0].Name == "" +} + +func (t *Torrent) GetFile(name string) *File { + if t.IsDir() && t.Name == name { + return &t.Files[0] + } + for _, f := range t.Files { + if f.Name == name { + return &f + } + } + return nil +} + +func (t *Torrent) Info(pieceLength int64) metainfo.Info { + info := metainfo.Info{ + Name: t.Name, + PieceLength: pieceLength, + } + if t.IsDir() { + info.Length = int64(len(t.Files[0].Data)) + } else { + for _, f := range t.Files { + info.Files = append(info.Files, metainfo.FileInfo{ + Path: []string{f.Name}, + Length: int64(len(f.Data)), + }) + } + } + err := info.GeneratePieces(func(fi metainfo.FileInfo) (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(t.GetFile(strings.Join(fi.Path, "/")).Data)), nil + }) + expect.Nil(err) + return info +} + +func (t *Torrent) Metainfo(pieceLength int64) *metainfo.MetaInfo { + mi := metainfo.MetaInfo{} + var err error + mi.InfoBytes, err = bencode.Marshal(t.Info(pieceLength)) + expect.Nil(err) + return &mi +} diff --git a/deps/github.com/anacrolix/torrent/internal/testutil/status_writer.go b/deps/github.com/anacrolix/torrent/internal/testutil/status_writer.go new file mode 100644 index 0000000..bcc5c2b --- /dev/null +++ b/deps/github.com/anacrolix/torrent/internal/testutil/status_writer.go @@ -0,0 +1,55 @@ +package testutil + +import ( + "fmt" + "io" + "net/http" + "sync" + "testing" + + _ "github.com/anacrolix/envpprof" +) + +type StatusWriter interface { + WriteStatus(io.Writer) +} + +// The key is the route pattern. The value is nil when the resource is released. +var ( + mu sync.Mutex + sws = map[string]StatusWriter{} +) + +func ExportStatusWriter(sw StatusWriter, path string, t testing.TB) (release func()) { + pattern := fmt.Sprintf("/%s/%s", t.Name(), path) + t.Logf("exporting status path %q", pattern) + release = func() { + mu.Lock() + defer mu.Unlock() + sws[pattern] = nil + } + mu.Lock() + defer mu.Unlock() + if curSw, ok := sws[pattern]; ok { + if curSw != nil { + panic(fmt.Sprintf("%q still in use", pattern)) + } + sws[pattern] = sw + return + } + http.HandleFunc( + pattern, + func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + sw := sws[pattern] + mu.Unlock() + if sw == nil { + http.NotFound(w, r) + return + } + sw.WriteStatus(w) + }, + ) + sws[pattern] = sw + return +} diff --git a/deps/github.com/anacrolix/torrent/iplist/cidr.go b/deps/github.com/anacrolix/torrent/iplist/cidr.go new file mode 100644 index 0000000..e131964 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/iplist/cidr.go @@ -0,0 +1,41 @@ +package iplist + +import ( + "bufio" + "io" + "net" +) + +func ParseCIDRListReader(r io.Reader) (ret []Range, err error) { + s := bufio.NewScanner(r) + for s.Scan() { + err = func() (err error) { + _, in, err := net.ParseCIDR(s.Text()) + if err != nil { + return + } + ret = append(ret, Range{ + First: in.IP, + Last: IPNetLast(in), + }) + return + }() + if err != nil { + return + } + } + return +} + +// Returns the last, inclusive IP in a net.IPNet. +func IPNetLast(in *net.IPNet) (last net.IP) { + n := len(in.IP) + if n != len(in.Mask) { + panic("wat") + } + last = make(net.IP, n) + for i := 0; i < n; i++ { + last[i] = in.IP[i] | ^in.Mask[i] + } + return +} diff --git a/deps/github.com/anacrolix/torrent/iplist/cidr_test.go b/deps/github.com/anacrolix/torrent/iplist/cidr_test.go new file mode 100644 index 0000000..ad904f5 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/iplist/cidr_test.go @@ -0,0 +1,41 @@ +package iplist + +import ( + "bytes" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIPNetLast(t *testing.T) { + _, in, err := net.ParseCIDR("138.255.252.0/22") + require.NoError(t, err) + assert.EqualValues(t, []byte{138, 255, 252, 0}, in.IP) + assert.EqualValues(t, []byte{255, 255, 252, 0}, in.Mask) + assert.EqualValues(t, []byte{138, 255, 255, 255}, IPNetLast(in)) + _, in, err = net.ParseCIDR("2400:cb00::/31") + require.NoError(t, err) + assert.EqualValues(t, []byte{0x24, 0, 0xcb, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, in.IP) + assert.EqualValues(t, []byte{255, 255, 255, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, in.Mask) + assert.EqualValues(t, []byte{0x24, 0, 0xcb, 1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, IPNetLast(in)) +} + +func TestParseCIDRList(t *testing.T) { + r := bytes.NewBufferString(`2400:cb00::/32 +2405:8100::/32 +2405:b500::/32 +2606:4700::/32 +2803:f800::/32 +2c0f:f248::/32 +2a06:98c0::/29 +`) + rs, err := ParseCIDRListReader(r) + require.NoError(t, err) + require.Len(t, rs, 7) + assert.EqualValues(t, Range{ + First: net.IP{0x28, 3, 0xf8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Last: net.IP{0x28, 3, 0xf8, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, rs[4]) +} diff --git a/deps/github.com/anacrolix/torrent/iplist/cmd/iplist/main.go b/deps/github.com/anacrolix/torrent/iplist/cmd/iplist/main.go new file mode 100644 index 0000000..7117b46 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/iplist/cmd/iplist/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "log" + "net" + "os" + + "github.com/anacrolix/tagflag" + + "github.com/anacrolix/torrent/iplist" +) + +func main() { + flags := struct { + tagflag.StartPos + Ips []net.IP + }{} + tagflag.Parse(&flags) + il, err := iplist.NewFromReader(os.Stdin) + if err != nil { + log.Fatalf("error loading ip list: %s", err) + } + log.Printf("loaded %d ranges", il.NumRanges()) + for _, ip := range flags.Ips { + r, ok := il.Lookup(ip) + if ok { + fmt.Printf("%s is in %v\n", ip, r) + } else { + fmt.Printf("%s not found\n", ip) + } + } +} diff --git a/deps/github.com/anacrolix/torrent/iplist/cmd/pack-blocklist/main.go b/deps/github.com/anacrolix/torrent/iplist/cmd/pack-blocklist/main.go new file mode 100644 index 0000000..9a97503 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/iplist/cmd/pack-blocklist/main.go @@ -0,0 +1,27 @@ +// Takes P2P blocklist text format in stdin, and outputs the packed format +// from the iplist package. +package main + +import ( + "bufio" + "os" + + "github.com/anacrolix/missinggo/v2" + "github.com/anacrolix/tagflag" + + "github.com/anacrolix/torrent/iplist" +) + +func main() { + tagflag.Parse(nil) + l, err := iplist.NewFromReader(os.Stdin) + if err != nil { + missinggo.Fatal(err) + } + wb := bufio.NewWriter(os.Stdout) + defer wb.Flush() + err = l.WritePacked(wb) + if err != nil { + missinggo.Fatal(err) + } +} diff --git a/deps/github.com/anacrolix/torrent/iplist/iplist.go b/deps/github.com/anacrolix/torrent/iplist/iplist.go new file mode 100644 index 0000000..d6d70a9 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/iplist/iplist.go @@ -0,0 +1,185 @@ +// Package iplist handles the P2P Plaintext Format described by +// https://en.wikipedia.org/wiki/PeerGuardian#P2P_plaintext_format. +package iplist + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "net" + "sort" +) + +// An abstraction of IP list implementations. +type Ranger interface { + // Return a Range containing the IP. + Lookup(net.IP) (r Range, ok bool) + // If your ranges hurt, use this. + NumRanges() int +} + +type IPList struct { + ranges []Range +} + +type Range struct { + First, Last net.IP + Description string +} + +func (r Range) String() string { + return fmt.Sprintf("%s-%s: %s", r.First, r.Last, r.Description) +} + +// Create a new IP list. The given ranges must already sorted by the lower +// bound IP in each range. Behaviour is undefined for lists of overlapping +// ranges. +func New(initSorted []Range) *IPList { + return &IPList{ + ranges: initSorted, + } +} + +func (ipl *IPList) NumRanges() int { + if ipl == nil { + return 0 + } + return len(ipl.ranges) +} + +// Return the range the given IP is in. ok if false if no range is found. +func (ipl *IPList) Lookup(ip net.IP) (r Range, ok bool) { + if ipl == nil { + return + } + // TODO: Perhaps all addresses should be converted to IPv6, if the future + // of IP is to always be backwards compatible. But this will cost 4x the + // memory for IPv4 addresses? + v4 := ip.To4() + if v4 != nil { + r, ok = ipl.lookup(v4) + if ok { + return + } + } + v6 := ip.To16() + if v6 != nil { + return ipl.lookup(v6) + } + if v4 == nil && v6 == nil { + r = Range{ + Description: "bad IP", + } + ok = true + } + return +} + +// Return a range that contains ip, or nil. +func lookup( + first func(i int) net.IP, + full func(i int) Range, + n int, + ip net.IP, +) ( + r Range, ok bool, +) { + // Find the index of the first range for which the following range exceeds + // it. + i := sort.Search(n, func(i int) bool { + if i+1 >= n { + return true + } + return bytes.Compare(ip, first(i+1)) < 0 + }) + if i == n { + return + } + r = full(i) + ok = bytes.Compare(r.First, ip) <= 0 && bytes.Compare(ip, r.Last) <= 0 + return +} + +// Return the range the given IP is in. Returns nil if no range is found. +func (ipl *IPList) lookup(ip net.IP) (Range, bool) { + return lookup(func(i int) net.IP { + return ipl.ranges[i].First + }, func(i int) Range { + return ipl.ranges[i] + }, len(ipl.ranges), ip) +} + +func minifyIP(ip *net.IP) { + v4 := ip.To4() + if v4 != nil { + *ip = append(make([]byte, 0, 4), v4...) + } +} + +// Parse a line of the PeerGuardian Text Lists (P2P) Format. Returns !ok but +// no error if a line doesn't contain a range but isn't erroneous, such as +// comment and blank lines. +func ParseBlocklistP2PLine(l []byte) (r Range, ok bool, err error) { + l = bytes.TrimSpace(l) + if len(l) == 0 || bytes.HasPrefix(l, []byte("#")) { + return + } + // TODO: Check this when IPv6 blocklists are available. + colon := bytes.LastIndexAny(l, ":") + if colon == -1 { + err = errors.New("missing colon") + return + } + hyphen := bytes.IndexByte(l[colon+1:], '-') + if hyphen == -1 { + err = errors.New("missing hyphen") + return + } + hyphen += colon + 1 + r.Description = string(l[:colon]) + r.First = net.ParseIP(string(l[colon+1 : hyphen])) + minifyIP(&r.First) + r.Last = net.ParseIP(string(l[hyphen+1:])) + minifyIP(&r.Last) + if r.First == nil || r.Last == nil || len(r.First) != len(r.Last) { + err = errors.New("bad IP range") + return + } + ok = true + return +} + +// Creates an IPList from a line-delimited P2P Plaintext file. +func NewFromReader(f io.Reader) (ret *IPList, err error) { + var ranges []Range + // There's a lot of similar descriptions, so we maintain a pool and reuse + // them to reduce memory overhead. + uniqStrs := make(map[string]string) + scanner := bufio.NewScanner(f) + lineNum := 1 + for scanner.Scan() { + r, ok, lineErr := ParseBlocklistP2PLine(scanner.Bytes()) + if lineErr != nil { + err = fmt.Errorf("error parsing line %d: %s", lineNum, lineErr) + return + } + lineNum++ + if !ok { + continue + } + if s, ok := uniqStrs[r.Description]; ok { + r.Description = s + } else { + uniqStrs[r.Description] = r.Description + } + ranges = append(ranges, r) + } + err = scanner.Err() + if err != nil { + return + } + ret = New(ranges) + return +} diff --git a/deps/github.com/anacrolix/torrent/iplist/iplist_test.go b/deps/github.com/anacrolix/torrent/iplist/iplist_test.go new file mode 100644 index 0000000..4e36e0a --- /dev/null +++ b/deps/github.com/anacrolix/torrent/iplist/iplist_test.go @@ -0,0 +1,124 @@ +package iplist + +import ( + "bufio" + "bytes" + "net" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + // Note the shared description "eff". The overlapping ranges at 1.2.8.2 + // will cause problems. Don't overlap your ranges. + sample = ` +# List distributed by iblocklist.com + +a:1.2.4.0-1.2.4.255 +b:1.2.8.0-1.2.8.255 +eff:1.2.8.2-1.2.8.2 +something:more detail:86.59.95.195-86.59.95.195 +eff:127.0.0.0-127.0.0.1` + packedSample []byte +) + +func init() { + var buf bytes.Buffer + list, err := NewFromReader(strings.NewReader(sample)) + if err != nil { + panic(err) + } + err = list.WritePacked(&buf) + if err != nil { + panic(err) + } + packedSample = buf.Bytes() +} + +func TestIPv4RangeLen(t *testing.T) { + ranges, _ := sampleRanges(t) + for i := 0; i < 3; i += 1 { + if len(ranges[i].First) != 4 { + t.FailNow() + } + if len(ranges[i].Last) != 4 { + t.FailNow() + } + } +} + +func sampleRanges(tb testing.TB) (ranges []Range, err error) { + scanner := bufio.NewScanner(strings.NewReader(sample)) + for scanner.Scan() { + r, ok, err := ParseBlocklistP2PLine(scanner.Bytes()) + if err != nil { + tb.Fatal(err) + } + if ok { + ranges = append(ranges, r) + } + } + err = scanner.Err() + return +} + +func BenchmarkParseP2pBlocklist(b *testing.B) { + for i := 0; i < b.N; i++ { + sampleRanges(b) + } +} + +func lookupOk(r Range, ok bool) bool { + return ok +} + +func TestBadIP(t *testing.T) { + for _, iplist := range []Ranger{ + // New(nil), + NewFromPacked([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")), + } { + assert.False(t, lookupOk(iplist.Lookup(net.IP(make([]byte, 4)))), "%v", iplist) + assert.False(t, lookupOk(iplist.Lookup(net.IP(make([]byte, 16))))) + assert.Panics(t, func() { iplist.Lookup(nil) }) + assert.Panics(t, func() { iplist.Lookup(net.IP(make([]byte, 5))) }) + } +} + +func testLookuperSimple(t *testing.T, iplist Ranger) { + for _, _case := range []struct { + IP string + Hit bool + Desc string + }{ + {"1.2.3.255", false, ""}, + {"1.2.8.0", true, "b"}, + {"1.2.4.255", true, "a"}, + // Try to roll over to the next octet on the parse. Note the final + // octet is overbounds. In the next case. + // {"1.2.7.256", true, "bad IP"}, + {"1.2.8.1", true, "b"}, + {"1.2.8.2", true, "eff"}, + } { + ip := net.ParseIP(_case.IP) + require.NotNil(t, ip, _case.IP) + r, ok := iplist.Lookup(ip) + assert.Equal(t, _case.Hit, ok, "%s", _case) + if !_case.Hit { + continue + } + assert.Equal(t, _case.Desc, r.Description, "%T", iplist) + } +} + +func TestSimple(t *testing.T) { + ranges, err := sampleRanges(t) + require.NoError(t, err) + require.Len(t, ranges, 5) + iplist := New(ranges) + testLookuperSimple(t, iplist) + packed := NewFromPacked(packedSample) + testLookuperSimple(t, packed) +} diff --git a/deps/github.com/anacrolix/torrent/iplist/packed.go b/deps/github.com/anacrolix/torrent/iplist/packed.go new file mode 100644 index 0000000..5ae1fae --- /dev/null +++ b/deps/github.com/anacrolix/torrent/iplist/packed.go @@ -0,0 +1,145 @@ +//go:build !wasm +// +build !wasm + +package iplist + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "os" + + "github.com/edsrzf/mmap-go" +) + +// The packed format is an 8 byte integer of the number of ranges. Then 20 +// bytes per range, consisting of 4 byte packed IP being the lower bound IP of +// the range, then 4 bytes of the upper, inclusive bound, 8 bytes for the +// offset of the description from the end of the packed ranges, and 4 bytes +// for the length of the description. After these packed ranges, are the +// concatenated descriptions. + +const ( + packedRangesOffset = 8 + packedRangeLen = 44 +) + +func (ipl *IPList) WritePacked(w io.Writer) (err error) { + descOffsets := make(map[string]int64, len(ipl.ranges)) + descs := make([]string, 0, len(ipl.ranges)) + var nextOffset int64 + // This is a little monadic, no? + write := func(b []byte, expectedLen int) { + if err != nil { + return + } + var n int + n, err = w.Write(b) + if err != nil { + return + } + if n != expectedLen { + panic(n) + } + } + var b [8]byte + binary.LittleEndian.PutUint64(b[:], uint64(len(ipl.ranges))) + write(b[:], 8) + for _, r := range ipl.ranges { + write(r.First.To16(), 16) + write(r.Last.To16(), 16) + descOff, ok := descOffsets[r.Description] + if !ok { + descOff = nextOffset + descOffsets[r.Description] = descOff + descs = append(descs, r.Description) + nextOffset += int64(len(r.Description)) + } + binary.LittleEndian.PutUint64(b[:], uint64(descOff)) + write(b[:], 8) + binary.LittleEndian.PutUint32(b[:], uint32(len(r.Description))) + write(b[:4], 4) + } + for _, d := range descs { + write([]byte(d), len(d)) + } + return +} + +func NewFromPacked(b []byte) PackedIPList { + ret := PackedIPList(b) + minLen := packedRangesOffset + ret.len()*packedRangeLen + if len(b) < minLen { + panic(fmt.Sprintf("packed len %d < %d", len(b), minLen)) + } + return ret +} + +type PackedIPList []byte + +var _ Ranger = PackedIPList{} + +func (pil PackedIPList) len() int { + return int(binary.LittleEndian.Uint64(pil[:8])) +} + +func (pil PackedIPList) NumRanges() int { + return pil.len() +} + +func (pil PackedIPList) getFirst(i int) net.IP { + off := packedRangesOffset + packedRangeLen*i + return net.IP(pil[off : off+16]) +} + +func (pil PackedIPList) getRange(i int) (ret Range) { + rOff := packedRangesOffset + packedRangeLen*i + last := pil[rOff+16 : rOff+32] + descOff := int(binary.LittleEndian.Uint64(pil[rOff+32:])) + descLen := int(binary.LittleEndian.Uint32(pil[rOff+40:])) + descOff += packedRangesOffset + packedRangeLen*pil.len() + ret = Range{ + pil.getFirst(i), + net.IP(last), + string(pil[descOff : descOff+descLen]), + } + return +} + +func (pil PackedIPList) Lookup(ip net.IP) (r Range, ok bool) { + ip16 := ip.To16() + if ip16 == nil { + panic(ip) + } + return lookup(pil.getFirst, pil.getRange, pil.len(), ip16) +} + +type closerFunc func() error + +func (me closerFunc) Close() error { + return me() +} + +func MMapPackedFile(filename string) ( + ret interface { + Ranger + io.Closer + }, + err error, +) { + f, err := os.Open(filename) + if err != nil { + return + } + defer f.Close() + mm, err := mmap.Map(f, mmap.RDONLY, 0) + if err != nil { + return + } + ret = struct { + Ranger + io.Closer + }{NewFromPacked(mm), closerFunc(mm.Unmap)} + return +} diff --git a/deps/github.com/anacrolix/torrent/iplist/packed_test.go b/deps/github.com/anacrolix/torrent/iplist/packed_test.go new file mode 100644 index 0000000..abc9e29 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/iplist/packed_test.go @@ -0,0 +1,35 @@ +package iplist + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// The active ingredients in the sample P2P blocklist file contents `sample`, +// for reference: +// +// a:1.2.4.0-1.2.4.255 +// b:1.2.8.0-1.2.8.255 +// eff:1.2.8.2-1.2.8.2 +// something:more detail:86.59.95.195-86.59.95.195 +// eff:127.0.0.0-127.0.0.1` + +func TestWritePacked(t *testing.T) { + l, err := NewFromReader(strings.NewReader(sample)) + require.NoError(t, err) + var buf bytes.Buffer + err = l.WritePacked(&buf) + require.NoError(t, err) + require.Equal(t, + "\x05\x00\x00\x00\x00\x00\x00\x00"+ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x04\x00"+"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x04\xff"+"\x00\x00\x00\x00\x00\x00\x00\x00"+"\x01\x00\x00\x00"+ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x08\x00"+"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x08\xff"+"\x01\x00\x00\x00\x00\x00\x00\x00"+"\x01\x00\x00\x00"+ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x08\x02"+"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x08\x02"+"\x02\x00\x00\x00\x00\x00\x00\x00"+"\x03\x00\x00\x00"+ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x56\x3b\x5f\xc3"+"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x56\x3b\x5f\xc3"+"\x05\x00\x00\x00\x00\x00\x00\x00"+"\x15\x00\x00\x00"+ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x7f\x00\x00\x00"+"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x7f\x00\x00\x01"+"\x02\x00\x00\x00\x00\x00\x00\x00"+"\x03\x00\x00\x00"+ + "abeffsomething:more detail", + buf.String()) +} diff --git a/deps/github.com/anacrolix/torrent/ipport.go b/deps/github.com/anacrolix/torrent/ipport.go new file mode 100644 index 0000000..a85a97f --- /dev/null +++ b/deps/github.com/anacrolix/torrent/ipport.go @@ -0,0 +1,71 @@ +package torrent + +import ( + "net" + "strconv" +) + +// Extracts the port as an integer from an address string. +func addrPortOrZero(addr net.Addr) int { + switch raw := addr.(type) { + case *net.UDPAddr: + return raw.Port + case *net.TCPAddr: + return raw.Port + default: + // Consider a unix socket on Windows with a name like "C:notanint". + _, port, err := net.SplitHostPort(addr.String()) + if err != nil { + return 0 + } + i64, err := strconv.ParseUint(port, 0, 16) + if err != nil { + return 0 + } + return int(i64) + } +} + +func addrIpOrNil(addr net.Addr) net.IP { + if addr == nil { + return nil + } + switch raw := addr.(type) { + case *net.UDPAddr: + return raw.IP + case *net.TCPAddr: + return raw.IP + default: + host, _, err := net.SplitHostPort(addr.String()) + if err != nil { + return nil + } + return net.ParseIP(host) + } +} + +type ipPortAddr struct { + IP net.IP + Port int +} + +func (ipPortAddr) Network() string { + return "" +} + +func (me ipPortAddr) String() string { + return net.JoinHostPort(me.IP.String(), strconv.FormatInt(int64(me.Port), 10)) +} + +func tryIpPortFromNetAddr(addr PeerRemoteAddr) (ipPortAddr, bool) { + ok := true + host, port, err := net.SplitHostPort(addr.String()) + if err != nil { + ok = false + } + portI64, err := strconv.ParseInt(port, 10, 0) + if err != nil { + ok = false + } + return ipPortAddr{net.ParseIP(host), int(portI64)}, ok +} diff --git a/deps/github.com/anacrolix/torrent/issue211_test.go b/deps/github.com/anacrolix/torrent/issue211_test.go new file mode 100644 index 0000000..a76be07 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/issue211_test.go @@ -0,0 +1,41 @@ +//go:build !wasm +// +build !wasm + +package torrent + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/time/rate" + + "github.com/anacrolix/torrent/internal/testutil" + "github.com/anacrolix/torrent/storage" +) + +func TestDropTorrentWithMmapStorageWhileHashing(t *testing.T) { + cfg := TestingConfig(t) + // Ensure the data is present when the torrent is added, and not obtained + // over the network as the test runs. + cfg.DownloadRateLimiter = rate.NewLimiter(0, 0) + cl, err := NewClient(cfg) + require.NoError(t, err) + defer cl.Close() + + td, mi := testutil.GreetingTestTorrent() + mms := storage.NewMMap(td) + defer mms.Close() + tt, new, err := cl.AddTorrentSpec(&TorrentSpec{ + Storage: mms, + InfoHash: mi.HashInfoBytes(), + InfoBytes: mi.InfoBytes, + }) + require.NoError(t, err) + assert.True(t, new) + + r := tt.NewReader() + go tt.Drop() + io.Copy(io.Discard, r) +} diff --git a/deps/github.com/anacrolix/torrent/issue97_test.go b/deps/github.com/anacrolix/torrent/issue97_test.go new file mode 100644 index 0000000..ee8107c --- /dev/null +++ b/deps/github.com/anacrolix/torrent/issue97_test.go @@ -0,0 +1,28 @@ +package torrent + +import ( + "testing" + + "github.com/anacrolix/log" + "github.com/stretchr/testify/require" + + "github.com/anacrolix/torrent/internal/testutil" + "github.com/anacrolix/torrent/storage" +) + +func TestHashPieceAfterStorageClosed(t *testing.T) { + td := t.TempDir() + cs := storage.NewFile(td) + defer cs.Close() + tt := &Torrent{ + storageOpener: storage.NewClient(cs), + logger: log.Default, + chunkSize: defaultChunkSize, + } + mi := testutil.GreetingMetaInfo() + info, err := mi.UnmarshalInfo() + require.NoError(t, err) + require.NoError(t, tt.setInfo(&info)) + require.NoError(t, tt.storage.Close()) + tt.hashPiece(0) +} diff --git a/deps/github.com/anacrolix/torrent/listen.go b/deps/github.com/anacrolix/torrent/listen.go new file mode 100644 index 0000000..3840cc1 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/listen.go @@ -0,0 +1,11 @@ +package torrent + +import "strings" + +func LoopbackListenHost(network string) string { + if strings.IndexByte(network, '4') != -1 { + return "127.0.0.1" + } else { + return "::1" + } +} diff --git a/deps/github.com/anacrolix/torrent/logonce/logonce.go b/deps/github.com/anacrolix/torrent/logonce/logonce.go new file mode 100644 index 0000000..7d44edc --- /dev/null +++ b/deps/github.com/anacrolix/torrent/logonce/logonce.go @@ -0,0 +1,47 @@ +// Package logonce implements an io.Writer facade that only performs distinct +// writes. This can be used by log.Loggers as they're guaranteed to make a +// single Write method call for each message. This is useful for loggers that +// print useful information about unexpected conditions that aren't fatal in +// code. +package logonce + +import ( + "io" + "log" + "os" +) + +// A default logger similar to the default logger in the log package. +var Stderr *log.Logger + +func init() { + // This should emulate the default logger in the log package where + // possible. No time flag so that messages don't differ by time. Code + // debug information is useful. + Stderr = log.New(Writer(os.Stderr), "logonce: ", log.Lshortfile) +} + +type writer struct { + w io.Writer + writes map[string]struct{} +} + +func (w writer) Write(p []byte) (n int, err error) { + s := string(p) + if _, ok := w.writes[s]; ok { + return + } + n, err = w.w.Write(p) + if n != len(s) { + s = string(p[:n]) + } + w.writes[s] = struct{}{} + return +} + +func Writer(w io.Writer) io.Writer { + return writer{ + w: w, + writes: make(map[string]struct{}), + } +} diff --git a/deps/github.com/anacrolix/torrent/main_test.go b/deps/github.com/anacrolix/torrent/main_test.go new file mode 100644 index 0000000..578d992 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/main_test.go @@ -0,0 +1,21 @@ +package torrent + +import ( + "log" + "os" + "testing" + + _ "github.com/anacrolix/envpprof" + analog "github.com/anacrolix/log" +) + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) + analog.DefaultTimeFormatter = analog.TimeFormatSecondsSinceInit +} + +func TestMain(m *testing.M) { + code := m.Run() + // select {} + os.Exit(code) +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/README b/deps/github.com/anacrolix/torrent/metainfo/README new file mode 100644 index 0000000..6da37b8 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/README @@ -0,0 +1 @@ +A library for manipulating ".torrent" files. diff --git a/deps/github.com/anacrolix/torrent/metainfo/announcelist.go b/deps/github.com/anacrolix/torrent/metainfo/announcelist.go new file mode 100644 index 0000000..f19af14 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/announcelist.go @@ -0,0 +1,35 @@ +package metainfo + +type AnnounceList [][]string + +func (al AnnounceList) Clone() (ret AnnounceList) { + for _, tier := range al { + ret = append(ret, append([]string(nil), tier...)) + } + return +} + +// Whether the AnnounceList should be preferred over a single URL announce. +func (al AnnounceList) OverridesAnnounce(announce string) bool { + for _, tier := range al { + for _, url := range tier { + if url != "" || announce == "" { + return true + } + } + } + return false +} + +func (al AnnounceList) DistinctValues() (ret []string) { + seen := make(map[string]struct{}) + for _, tier := range al { + for _, v := range tier { + if _, ok := seen[v]; !ok { + seen[v] = struct{}{} + ret = append(ret, v) + } + } + } + return +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/fileinfo.go b/deps/github.com/anacrolix/torrent/metainfo/fileinfo.go new file mode 100644 index 0000000..2a5ea01 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/fileinfo.go @@ -0,0 +1,35 @@ +package metainfo + +import "strings" + +// Information specific to a single file inside the MetaInfo structure. +type FileInfo struct { + Length int64 `bencode:"length"` // BEP3 + Path []string `bencode:"path"` // BEP3 + PathUtf8 []string `bencode:"path.utf-8,omitempty"` +} + +func (fi *FileInfo) DisplayPath(info *Info) string { + if info.IsDir() { + return strings.Join(fi.BestPath(), "/") + } else { + return info.BestName() + } +} + +func (me FileInfo) Offset(info *Info) (ret int64) { + for _, fi := range info.UpvertedFiles() { + if me.DisplayPath(info) == fi.DisplayPath(info) { + return + } + ret += fi.Length + } + panic("not found") +} + +func (fi FileInfo) BestPath() []string { + if len(fi.PathUtf8) != 0 { + return fi.PathUtf8 + } + return fi.Path +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/fuzz_test.go b/deps/github.com/anacrolix/torrent/metainfo/fuzz_test.go new file mode 100644 index 0000000..c01ab6c --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/fuzz_test.go @@ -0,0 +1,47 @@ +//go:build go1.18 +// +build go1.18 + +package metainfo + +import ( + "os" + "path/filepath" + "testing" + + "github.com/anacrolix/torrent/bencode" +) + +func Fuzz(f *testing.F) { + // Is there an OS-agnostic version of Glob? + matches, err := filepath.Glob(filepath.FromSlash("testdata/*.torrent")) + if err != nil { + f.Fatal(err) + } + for _, m := range matches { + b, err := os.ReadFile(m) + if err != nil { + f.Fatal(err) + } + f.Logf("adding %q", m) + f.Add(b) + } + f.Fuzz(func(t *testing.T, b []byte) { + var mi MetaInfo + err := bencode.Unmarshal(b, &mi) + if err != nil { + t.Skip(err) + } + _, err = bencode.Marshal(mi) + if err != nil { + panic(err) + } + info, err := mi.UnmarshalInfo() + if err != nil { + t.Skip(err) + } + _, err = bencode.Marshal(info) + if err != nil { + panic(err) + } + }) +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/hash.go b/deps/github.com/anacrolix/torrent/metainfo/hash.go new file mode 100644 index 0000000..39daf6f --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/hash.go @@ -0,0 +1,16 @@ +package metainfo + +import ( + "github.com/anacrolix/torrent/types/infohash" +) + +// This type has been moved to allow avoiding importing everything in metainfo to get at it. + +const HashSize = infohash.Size + +type Hash = infohash.T + +var ( + NewHashFromHex = infohash.FromHexString + HashBytes = infohash.HashBytes +) diff --git a/deps/github.com/anacrolix/torrent/metainfo/info.go b/deps/github.com/anacrolix/torrent/metainfo/info.go new file mode 100644 index 0000000..1ee2704 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/info.go @@ -0,0 +1,165 @@ +package metainfo + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/anacrolix/missinggo/v2/slices" +) + +// The info dictionary. +type Info struct { + PieceLength int64 `bencode:"piece length"` // BEP3 + Pieces []byte `bencode:"pieces"` // BEP3 + Name string `bencode:"name"` // BEP3 + NameUtf8 string `bencode:"name.utf-8,omitempty"` + Length int64 `bencode:"length,omitempty"` // BEP3, mutually exclusive with Files + Private *bool `bencode:"private,omitempty"` // BEP27 + // TODO: Document this field. + Source string `bencode:"source,omitempty"` + Files []FileInfo `bencode:"files,omitempty"` // BEP3, mutually exclusive with Length +} + +// The Info.Name field is "advisory". For multi-file torrents it's usually a suggested directory +// name. There are situations where we don't want a directory (like using the contents of a torrent +// as the immediate contents of a directory), or the name is invalid. Transmission will inject the +// name of the torrent file if it doesn't like the name, resulting in a different infohash +// (https://github.com/transmission/transmission/issues/1775). To work around these situations, we +// will use a sentinel name for compatibility with Transmission and to signal to our own client that +// we intended to have no directory name. By exposing it in the API we can check for references to +// this behaviour within this implementation. +const NoName = "-" + +// This is a helper that sets Files and Pieces from a root path and its children. +func (info *Info) BuildFromFilePath(root string) (err error) { + info.Name = func() string { + b := filepath.Base(root) + switch b { + case ".", "..", string(filepath.Separator): + return NoName + default: + return b + } + }() + info.Files = nil + err = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.IsDir() { + // Directories are implicit in torrent files. + return nil + } else if path == root { + // The root is a file. + info.Length = fi.Size() + return nil + } + relPath, err := filepath.Rel(root, path) + if err != nil { + return fmt.Errorf("error getting relative path: %s", err) + } + info.Files = append(info.Files, FileInfo{ + Path: strings.Split(relPath, string(filepath.Separator)), + Length: fi.Size(), + }) + return nil + }) + if err != nil { + return + } + slices.Sort(info.Files, func(l, r FileInfo) bool { + return strings.Join(l.Path, "/") < strings.Join(r.Path, "/") + }) + if info.PieceLength == 0 { + info.PieceLength = ChoosePieceLength(info.TotalLength()) + } + err = info.GeneratePieces(func(fi FileInfo) (io.ReadCloser, error) { + return os.Open(filepath.Join(root, strings.Join(fi.Path, string(filepath.Separator)))) + }) + if err != nil { + err = fmt.Errorf("error generating pieces: %s", err) + } + return +} + +// Concatenates all the files in the torrent into w. open is a function that +// gets at the contents of the given file. +func (info *Info) writeFiles(w io.Writer, open func(fi FileInfo) (io.ReadCloser, error)) error { + for _, fi := range info.UpvertedFiles() { + r, err := open(fi) + if err != nil { + return fmt.Errorf("error opening %v: %s", fi, err) + } + wn, err := io.CopyN(w, r, fi.Length) + r.Close() + if wn != fi.Length { + return fmt.Errorf("error copying %v: %s", fi, err) + } + } + return nil +} + +// Sets Pieces (the block of piece hashes in the Info) by using the passed +// function to get at the torrent data. +func (info *Info) GeneratePieces(open func(fi FileInfo) (io.ReadCloser, error)) (err error) { + if info.PieceLength == 0 { + return errors.New("piece length must be non-zero") + } + pr, pw := io.Pipe() + go func() { + err := info.writeFiles(pw, open) + pw.CloseWithError(err) + }() + defer pr.Close() + info.Pieces, err = GeneratePieces(pr, info.PieceLength, nil) + return +} + +func (info *Info) TotalLength() (ret int64) { + if info.IsDir() { + for _, fi := range info.Files { + ret += fi.Length + } + } else { + ret = info.Length + } + return +} + +func (info *Info) NumPieces() int { + return len(info.Pieces) / 20 +} + +func (info *Info) IsDir() bool { + return len(info.Files) != 0 +} + +// The files field, converted up from the old single-file in the parent info +// dict if necessary. This is a helper to avoid having to conditionally handle +// single and multi-file torrent infos. +func (info *Info) UpvertedFiles() []FileInfo { + if len(info.Files) == 0 { + return []FileInfo{{ + Length: info.Length, + // Callers should determine that Info.Name is the basename, and + // thus a regular file. + Path: nil, + }} + } + return info.Files +} + +func (info *Info) Piece(index int) Piece { + return Piece{info, pieceIndex(index)} +} + +func (info Info) BestName() string { + if info.NameUtf8 != "" { + return info.NameUtf8 + } + return info.Name +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/info_test.go b/deps/github.com/anacrolix/torrent/metainfo/info_test.go new file mode 100644 index 0000000..d65ac2f --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/info_test.go @@ -0,0 +1,16 @@ +package metainfo + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anacrolix/torrent/bencode" +) + +func TestMarshalInfo(t *testing.T) { + var info Info + b, err := bencode.Marshal(info) + assert.NoError(t, err) + assert.EqualValues(t, "d4:name0:12:piece lengthi0e6:pieces0:e", string(b)) +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/magnet.go b/deps/github.com/anacrolix/torrent/metainfo/magnet.go new file mode 100644 index 0000000..48dc148 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/magnet.go @@ -0,0 +1,120 @@ +package metainfo + +import ( + "encoding/base32" + "encoding/hex" + "errors" + "fmt" + "net/url" + "strings" +) + +// Magnet link components. +type Magnet struct { + InfoHash Hash // Expected in this implementation + Trackers []string // "tr" values + DisplayName string // "dn" value, if not empty + Params url.Values // All other values, such as "x.pe", "as", "xs" etc. +} + +const xtPrefix = "urn:btih:" + +func (m Magnet) String() string { + // Deep-copy m.Params + vs := make(url.Values, len(m.Params)+len(m.Trackers)+2) + for k, v := range m.Params { + vs[k] = append([]string(nil), v...) + } + + for _, tr := range m.Trackers { + vs.Add("tr", tr) + } + if m.DisplayName != "" { + vs.Add("dn", m.DisplayName) + } + + // Transmission and Deluge both expect "urn:btih:" to be unescaped. Deluge wants it to be at the + // start of the magnet link. The InfoHash field is expected to be BitTorrent in this + // implementation. + u := url.URL{ + Scheme: "magnet", + RawQuery: "xt=" + xtPrefix + m.InfoHash.HexString(), + } + if len(vs) != 0 { + u.RawQuery += "&" + vs.Encode() + } + return u.String() +} + +// Deprecated: Use ParseMagnetUri. +var ParseMagnetURI = ParseMagnetUri + +// ParseMagnetUri parses Magnet-formatted URIs into a Magnet instance +func ParseMagnetUri(uri string) (m Magnet, err error) { + u, err := url.Parse(uri) + if err != nil { + err = fmt.Errorf("error parsing uri: %w", err) + return + } + if u.Scheme != "magnet" { + err = fmt.Errorf("unexpected scheme %q", u.Scheme) + return + } + q := u.Query() + xt := q.Get("xt") + m.InfoHash, err = parseInfohash(q.Get("xt")) + if err != nil { + err = fmt.Errorf("error parsing infohash %q: %w", xt, err) + return + } + dropFirst(q, "xt") + m.DisplayName = q.Get("dn") + dropFirst(q, "dn") + m.Trackers = q["tr"] + delete(q, "tr") + if len(q) == 0 { + q = nil + } + m.Params = q + return +} + +func parseInfohash(xt string) (ih Hash, err error) { + if !strings.HasPrefix(xt, xtPrefix) { + err = errors.New("bad xt parameter prefix") + return + } + encoded := xt[len(xtPrefix):] + decode := func() func(dst, src []byte) (int, error) { + switch len(encoded) { + case 40: + return hex.Decode + case 32: + return base32.StdEncoding.Decode + } + return nil + }() + if decode == nil { + err = fmt.Errorf("unhandled xt parameter encoding (encoded length %d)", len(encoded)) + return + } + n, err := decode(ih[:], []byte(encoded)) + if err != nil { + err = fmt.Errorf("error decoding xt: %w", err) + return + } + if n != 20 { + panic(n) + } + return +} + +func dropFirst(vs url.Values, key string) { + sl := vs[key] + switch len(sl) { + case 0, 1: + vs.Del(key) + default: + vs[key] = sl[1:] + } +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/magnet_test.go b/deps/github.com/anacrolix/torrent/metainfo/magnet_test.go new file mode 100644 index 0000000..24ab15b --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/magnet_test.go @@ -0,0 +1,114 @@ +package metainfo + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + exampleMagnetURI = `magnet:?xt=urn:btih:51340689c960f0778a4387aef9b4b52fd08390cd&dn=Shit+Movie+%281985%29+1337p+-+Eru&tr=http%3A%2F%2Fhttp.was.great%21&tr=udp%3A%2F%2Fanti.piracy.honeypot%3A6969` + exampleMagnet = Magnet{ + DisplayName: "Shit Movie (1985) 1337p - Eru", + Trackers: []string{ + "http://http.was.great!", + "udp://anti.piracy.honeypot:6969", + }, + } +) + +func init() { + hex.Decode(exampleMagnet.InfoHash[:], []byte("51340689c960f0778a4387aef9b4b52fd08390cd")) +} + +// Converting from our Magnet type to URL string. +func TestMagnetString(t *testing.T) { + m, err := ParseMagnetUri(exampleMagnet.String()) + require.NoError(t, err) + assert.EqualValues(t, exampleMagnet, m) +} + +func TestParseMagnetURI(t *testing.T) { + var uri string + var m Magnet + var err error + + // parsing the legit Magnet URI with btih-formatted xt should not return errors + uri = "magnet:?xt=urn:btih:ZOCMZQIPFFW7OLLMIC5HUB6BPCSDEOQU" + _, err = ParseMagnetUri(uri) + if err != nil { + t.Errorf("Attempting parsing the proper Magnet btih URI:\"%v\" failed with err: %v", uri, err) + } + + // Checking if the magnet instance struct is built correctly from parsing + m, err = ParseMagnetUri(exampleMagnetURI) + assert.EqualValues(t, exampleMagnet, m) + assert.NoError(t, err) + + // empty string URI case + _, err = ParseMagnetUri("") + if err == nil { + t.Errorf("Parsing empty string as URI should have returned an error but didn't") + } + + // only BTIH (BitTorrent info hash)-formatted magnet links are currently supported + // must return error correctly when encountering other URN formats + uri = "magnet:?xt=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C" + _, err = ParseMagnetUri(uri) + if err == nil { + t.Errorf("Magnet URI with non-BTIH URNs (like \"%v\") are not supported and should return an error", uri) + } + + // resilience to the broken hash + uri = "magnet:?xt=urn:btih:this hash is really broken" + _, err = ParseMagnetUri(uri) + if err == nil { + t.Errorf("Failed to detect broken Magnet URI: %v", uri) + } +} + +func TestMagnetize(t *testing.T) { + mi, err := LoadFromFile("../testdata/bootstrap.dat.torrent") + require.NoError(t, err) + + info, err := mi.UnmarshalInfo() + require.NoError(t, err) + m := mi.Magnet(nil, &info) + + assert.EqualValues(t, "bootstrap.dat", m.DisplayName) + + ih := [20]byte{ + 54, 113, 155, 162, 206, 207, 159, 59, 215, 197, + 171, 251, 122, 136, 233, 57, 97, 27, 83, 108, + } + + if m.InfoHash != ih { + t.Errorf("Magnet infohash is incorrect") + } + + trackers := []string{ + "udp://tracker.openbittorrent.com:80", + "udp://tracker.openbittorrent.com:80", + "udp://tracker.publicbt.com:80", + "udp://coppersurfer.tk:6969/announce", + "udp://open.demonii.com:1337", + "http://bttracker.crunchbanglinux.org:6969/announce", + } + + for _, expected := range trackers { + if !contains(m.Trackers, expected) { + t.Errorf("Magnet does not contain expected tracker: %s", expected) + } + } +} + +func contains(haystack []string, needle string) bool { + for _, s := range haystack { + if s == needle { + return true + } + } + return false +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/metainfo.go b/deps/github.com/anacrolix/torrent/metainfo/metainfo.go new file mode 100644 index 0000000..93f9103 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/metainfo.go @@ -0,0 +1,98 @@ +package metainfo + +import ( + "bufio" + "io" + "net/url" + "os" + "time" + + "github.com/anacrolix/torrent/bencode" +) + +type MetaInfo struct { + InfoBytes bencode.Bytes `bencode:"info,omitempty"` // BEP 3 + Announce string `bencode:"announce,omitempty"` // BEP 3 + AnnounceList AnnounceList `bencode:"announce-list,omitempty"` // BEP 12 + Nodes []Node `bencode:"nodes,omitempty,ignore_unmarshal_type_error"` // BEP 5 + // Where's this specified? Mentioned at + // https://wiki.theory.org/index.php/BitTorrentSpecification: (optional) the creation time of + // the torrent, in standard UNIX epoch format (integer, seconds since 1-Jan-1970 00:00:00 UTC) + CreationDate int64 `bencode:"creation date,omitempty,ignore_unmarshal_type_error"` + Comment string `bencode:"comment,omitempty"` + CreatedBy string `bencode:"created by,omitempty"` + Encoding string `bencode:"encoding,omitempty"` + UrlList UrlList `bencode:"url-list,omitempty"` // BEP 19 WebSeeds +} + +// Load a MetaInfo from an io.Reader. Returns a non-nil error in case of +// failure. +func Load(r io.Reader) (*MetaInfo, error) { + var mi MetaInfo + d := bencode.NewDecoder(r) + err := d.Decode(&mi) + if err != nil { + return nil, err + } + return &mi, nil +} + +// Convenience function for loading a MetaInfo from a file. +func LoadFromFile(filename string) (*MetaInfo, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + var buf bufio.Reader + buf.Reset(f) + return Load(&buf) +} + +func (mi MetaInfo) UnmarshalInfo() (info Info, err error) { + err = bencode.Unmarshal(mi.InfoBytes, &info) + return +} + +func (mi MetaInfo) HashInfoBytes() (infoHash Hash) { + return HashBytes(mi.InfoBytes) +} + +// Encode to bencoded form. +func (mi MetaInfo) Write(w io.Writer) error { + return bencode.NewEncoder(w).Encode(mi) +} + +// Set good default values in preparation for creating a new MetaInfo file. +func (mi *MetaInfo) SetDefaults() { + mi.CreatedBy = "github.com/anacrolix/torrent" + mi.CreationDate = time.Now().Unix() +} + +// Creates a Magnet from a MetaInfo. Optional infohash and parsed info can be provided. +func (mi MetaInfo) Magnet(infoHash *Hash, info *Info) (m Magnet) { + m.Trackers = append(m.Trackers, mi.UpvertedAnnounceList().DistinctValues()...) + if info != nil { + m.DisplayName = info.BestName() + } + if infoHash != nil { + m.InfoHash = *infoHash + } else { + m.InfoHash = mi.HashInfoBytes() + } + m.Params = make(url.Values) + m.Params["ws"] = mi.UrlList + return +} + +// Returns the announce list converted from the old single announce field if +// necessary. +func (mi *MetaInfo) UpvertedAnnounceList() AnnounceList { + if mi.AnnounceList.OverridesAnnounce(mi.Announce) { + return mi.AnnounceList + } + if mi.Announce != "" { + return [][]string{{mi.Announce}} + } + return nil +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/metainfo_test.go b/deps/github.com/anacrolix/torrent/metainfo/metainfo_test.go new file mode 100644 index 0000000..335631f --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/metainfo_test.go @@ -0,0 +1,162 @@ +package metainfo + +import ( + "io" + "os" + "path" + "path/filepath" + "strings" + "testing" + + "github.com/anacrolix/missinggo/v2" + qt "github.com/frankban/quicktest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anacrolix/torrent/bencode" +) + +func testFile(t *testing.T, filename string) { + mi, err := LoadFromFile(filename) + require.NoError(t, err) + info, err := mi.UnmarshalInfo() + require.NoError(t, err) + + if len(info.Files) == 1 { + t.Logf("Single file: %s (length: %d)\n", info.Name, info.Files[0].Length) + } else { + t.Logf("Multiple files: %s\n", info.Name) + for _, f := range info.Files { + t.Logf(" - %s (length: %d)\n", path.Join(f.Path...), f.Length) + } + } + + for _, group := range mi.AnnounceList { + for _, tracker := range group { + t.Logf("Tracker: %s\n", tracker) + } + } + + b, err := bencode.Marshal(&info) + require.NoError(t, err) + assert.EqualValues(t, string(b), string(mi.InfoBytes)) +} + +func TestFile(t *testing.T) { + testFile(t, "testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent") + testFile(t, "testdata/continuum.torrent") + testFile(t, "testdata/23516C72685E8DB0C8F15553382A927F185C4F01.torrent") + testFile(t, "testdata/trackerless.torrent") +} + +// Ensure that the correct number of pieces are generated when hashing files. +func TestNumPieces(t *testing.T) { + for _, _case := range []struct { + PieceLength int64 + Files []FileInfo + NumPieces int + }{ + {256 * 1024, []FileInfo{{Length: 1024*1024 + -1}}, 4}, + {256 * 1024, []FileInfo{{Length: 1024 * 1024}}, 4}, + {256 * 1024, []FileInfo{{Length: 1024*1024 + 1}}, 5}, + {5, []FileInfo{{Length: 1}, {Length: 12}}, 3}, + {5, []FileInfo{{Length: 4}, {Length: 12}}, 4}, + } { + info := Info{ + Files: _case.Files, + PieceLength: _case.PieceLength, + } + err := info.GeneratePieces(func(fi FileInfo) (io.ReadCloser, error) { + return io.NopCloser(missinggo.ZeroReader), nil + }) + assert.NoError(t, err) + assert.EqualValues(t, _case.NumPieces, info.NumPieces()) + } +} + +func touchFile(path string) (err error) { + f, err := os.Create(path) + if err != nil { + return + } + err = f.Close() + return +} + +func TestBuildFromFilePathOrder(t *testing.T) { + td := t.TempDir() + require.NoError(t, touchFile(filepath.Join(td, "b"))) + require.NoError(t, touchFile(filepath.Join(td, "a"))) + info := Info{ + PieceLength: 1, + } + require.NoError(t, info.BuildFromFilePath(td)) + assert.EqualValues(t, []FileInfo{{ + Path: []string{"a"}, + }, { + Path: []string{"b"}, + }}, info.Files) +} + +func testUnmarshal(t *testing.T, input string, expected *MetaInfo) { + var actual MetaInfo + err := bencode.Unmarshal([]byte(input), &actual) + if expected == nil { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.EqualValues(t, *expected, actual) +} + +func TestUnmarshal(t *testing.T) { + testUnmarshal(t, `de`, &MetaInfo{}) + testUnmarshal(t, `d4:infoe`, nil) + testUnmarshal(t, `d4:infoabce`, nil) + testUnmarshal(t, `d4:infodee`, &MetaInfo{InfoBytes: []byte("de")}) +} + +func TestMetainfoWithListURLList(t *testing.T) { + mi, err := LoadFromFile("testdata/SKODAOCTAVIA336x280_archive.torrent") + require.NoError(t, err) + assert.Len(t, mi.UrlList, 3) + qt.Assert(t, mi.Magnet(nil, nil).String(), qt.ContentEquals, + strings.Join([]string{ + "magnet:?xt=urn:btih:d4b197dff199aad447a9a352e31528adbbd97922", + "tr=http%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce", + "tr=http%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce", + "ws=https%3A%2F%2Farchive.org%2Fdownload%2F", + "ws=http%3A%2F%2Fia601600.us.archive.org%2F26%2Fitems%2F", + "ws=http%3A%2F%2Fia801600.us.archive.org%2F26%2Fitems%2F", + }, "&")) +} + +func TestMetainfoWithStringURLList(t *testing.T) { + mi, err := LoadFromFile("testdata/flat-url-list.torrent") + require.NoError(t, err) + assert.Len(t, mi.UrlList, 1) + qt.Assert(t, mi.Magnet(nil, nil).String(), qt.ContentEquals, + strings.Join([]string{ + "magnet:?xt=urn:btih:9da24e606e4ed9c7b91c1772fb5bf98f82bd9687", + "tr=http%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce", + "tr=http%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce", + "ws=https%3A%2F%2Farchive.org%2Fdownload%2F", + }, "&")) +} + +// https://github.com/anacrolix/torrent/issues/247 +// +// The decoder buffer wasn't cleared before starting the next dict item after +// a syntax error on a field with the ignore_unmarshal_type_error tag. +func TestStringCreationDate(t *testing.T) { + var mi MetaInfo + assert.NoError(t, bencode.Unmarshal([]byte("d13:creation date23:29.03.2018 22:18:14 UTC4:infodee"), &mi)) +} + +// See https://github.com/anacrolix/torrent/issues/843. +func TestUnmarshalEmptyStringNodes(t *testing.T) { + var mi MetaInfo + c := qt.New(t) + err := bencode.Unmarshal([]byte("d5:nodes0:e"), &mi) + c.Assert(err, qt.IsNil) +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/nodes.go b/deps/github.com/anacrolix/torrent/metainfo/nodes.go new file mode 100644 index 0000000..06c3b3f --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/nodes.go @@ -0,0 +1,38 @@ +package metainfo + +import ( + "fmt" + "net" + "strconv" + + "github.com/anacrolix/torrent/bencode" +) + +type Node string + +var _ bencode.Unmarshaler = (*Node)(nil) + +func (n *Node) UnmarshalBencode(b []byte) (err error) { + var iface interface{} + err = bencode.Unmarshal(b, &iface) + if err != nil { + return + } + switch v := iface.(type) { + case string: + *n = Node(v) + case []interface{}: + func() { + defer func() { + r := recover() + if r != nil { + err = r.(error) + } + }() + *n = Node(net.JoinHostPort(v[0].(string), strconv.FormatInt(v[1].(int64), 10))) + }() + default: + err = fmt.Errorf("unsupported type: %T", iface) + } + return +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/nodes_test.go b/deps/github.com/anacrolix/torrent/metainfo/nodes_test.go new file mode 100644 index 0000000..adebbb3 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/nodes_test.go @@ -0,0 +1,74 @@ +package metainfo + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anacrolix/torrent/bencode" +) + +func testFileNodesMatch(t *testing.T, file string, nodes []Node) { + mi, err := LoadFromFile(file) + require.NoError(t, err) + assert.EqualValues(t, nodes, mi.Nodes) +} + +func TestNodesListStrings(t *testing.T) { + testFileNodesMatch(t, "testdata/trackerless.torrent", []Node{ + "udp://tracker.openbittorrent.com:80", + "udp://tracker.openbittorrent.com:80", + }) +} + +func TestNodesListPairsBEP5(t *testing.T) { + testFileNodesMatch(t, "testdata/issue_65a.torrent", []Node{ + "185.34.3.132:5680", + "185.34.3.103:12340", + "94.209.253.165:47232", + "78.46.103.11:34319", + "195.154.162.70:55011", + "185.34.3.137:3732", + }) + testFileNodesMatch(t, "testdata/issue_65b.torrent", []Node{ + "95.211.203.130:6881", + "84.72.116.169:6889", + "204.83.98.77:7000", + "101.187.175.163:19665", + "37.187.118.32:6881", + "83.128.223.71:23865", + }) +} + +func testMarshalMetainfo(t *testing.T, expected string, mi *MetaInfo) { + b, err := bencode.Marshal(*mi) + assert.NoError(t, err) + assert.EqualValues(t, expected, string(b)) +} + +func TestMarshalMetainfoNodes(t *testing.T) { + testMarshalMetainfo(t, "d4:infodee", &MetaInfo{InfoBytes: []byte("de")}) + testMarshalMetainfo(t, "d4:infod2:hi5:theree5:nodesl12:1.2.3.4:555514:not a hostportee", &MetaInfo{ + Nodes: []Node{"1.2.3.4:5555", "not a hostport"}, + InfoBytes: []byte("d2:hi5:theree"), + }) +} + +func TestUnmarshalBadMetainfoNodes(t *testing.T) { + var mi MetaInfo + // Should barf on the integer in the nodes list. + err := bencode.Unmarshal([]byte("d5:nodesl1:ai42eee"), &mi) + require.Error(t, err) +} + +func TestMetainfoEmptyInfoBytes(t *testing.T) { + var buf bytes.Buffer + require.NoError(t, (&MetaInfo{ + // Include a non-empty field that comes after "info". + UrlList: []string{"hello"}, + }).Write(&buf)) + var mi MetaInfo + require.NoError(t, bencode.Unmarshal(buf.Bytes(), &mi)) +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/piece-length.go b/deps/github.com/anacrolix/torrent/metainfo/piece-length.go new file mode 100644 index 0000000..183b4d8 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/piece-length.go @@ -0,0 +1,55 @@ +// From https://github.com/jackpal/Taipei-Torrent + +// Copyright (c) 2010 Jack Palevich. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package metainfo + +// For more context on why these numbers, see http://wiki.vuze.com/w/Torrent_Piece_Size +const ( + minimumPieceLength = 16 * 1024 + targetPieceCountLog2 = 10 + targetPieceCountMin = 1 << targetPieceCountLog2 +) + +// Target piece count should be < targetPieceCountMax +const targetPieceCountMax = targetPieceCountMin << 1 + +// Choose a good piecelength. +func ChoosePieceLength(totalLength int64) (pieceLength int64) { + // Must be a power of 2. + // Must be a multiple of 16KB + // Prefer to provide around 1024..2048 pieces. + pieceLength = minimumPieceLength + pieces := totalLength / pieceLength + for pieces >= targetPieceCountMax { + pieceLength <<= 1 + pieces >>= 1 + } + return +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/piece.go b/deps/github.com/anacrolix/torrent/metainfo/piece.go new file mode 100644 index 0000000..d889538 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/piece.go @@ -0,0 +1,28 @@ +package metainfo + +type Piece struct { + Info *Info // Can we embed the fields here instead, or is it something to do with saving memory? + i pieceIndex +} + +type pieceIndex = int + +func (p Piece) Length() int64 { + if int(p.i) == p.Info.NumPieces()-1 { + return p.Info.TotalLength() - int64(p.i)*p.Info.PieceLength + } + return p.Info.PieceLength +} + +func (p Piece) Offset() int64 { + return int64(p.i) * p.Info.PieceLength +} + +func (p Piece) Hash() (ret Hash) { + copy(ret[:], p.Info.Pieces[p.i*HashSize:(p.i+1)*HashSize]) + return +} + +func (p Piece) Index() pieceIndex { + return p.i +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/piece_key.go b/deps/github.com/anacrolix/torrent/metainfo/piece_key.go new file mode 100644 index 0000000..6ddf065 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/piece_key.go @@ -0,0 +1,7 @@ +package metainfo + +// Uniquely identifies a piece. +type PieceKey struct { + InfoHash Hash + Index pieceIndex +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/pieces.go b/deps/github.com/anacrolix/torrent/metainfo/pieces.go new file mode 100644 index 0000000..812f3f4 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/pieces.go @@ -0,0 +1,22 @@ +package metainfo + +import ( + "crypto/sha1" + "io" +) + +func GeneratePieces(r io.Reader, pieceLength int64, b []byte) ([]byte, error) { + for { + h := sha1.New() + written, err := io.CopyN(h, r, pieceLength) + if written > 0 { + b = h.Sum(b) + } + if err == io.EOF { + return b, nil + } + if err != nil { + return b, err + } + } +} diff --git a/deps/github.com/anacrolix/torrent/metainfo/testdata/23516C72685E8DB0C8F15553382A927F185C4F01.torrent b/deps/github.com/anacrolix/torrent/metainfo/testdata/23516C72685E8DB0C8F15553382A927F185C4F01.torrent new file mode 100644 index 0000000000000000000000000000000000000000..492908cab99e1a5ee7a63ec2040d2f06ee177dd3 GIT binary patch literal 41306 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 z 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 literal 0 HcmV?d00001 diff --git a/deps/github.com/anacrolix/torrent/metainfo/testdata/SKODAOCTAVIA336x280_archive.torrent b/deps/github.com/anacrolix/torrent/metainfo/testdata/SKODAOCTAVIA336x280_archive.torrent new file mode 100644 index 0000000000000000000000000000000000000000..40ec11f6e8b833034e980af28a3264ada1da5bee GIT binary patch literal 2826 zcma);Tc{n?8OQa(NY#=;AG}~-2yLUmoU_)Nb)7zx(mzUp?NY-P%``A*&>zA9UMPdZi8LP%_zv>iwe;9y-so6N1z4#o#D zG#JEG0J8yFn-|UOq3aLsojdru`MFyT%%!PZW|~~GKQEw3x^^Y$*hNTu$Ce8au1l6W zx$h*s8WwTiCTXj>9zMjn>L#IDDvQd7yj(yu{NmeUC8=tB>Gld}p;-BKf)BPCtO)){ z67i!glYTGQK>(xWwgeunzZvmnV~5Alj@G^8hN^`usiCcNShPjbZ~C@C#2W7G=QRdw z6JO*5McWRF*7SS5s>U#vz%>BFxUTsecpEP-@F|r#6bczN*Iy~{W%rQSmkES zG+rUuB*TZhnEjdMZZR|wqG7gi1oL1M>#F`h0(96YfL#->H8YfCzJ_7G+zJ(0ZVz|u z5jZ@Wij^?@oL6Ns$ZAfxQCw;z6!0uBV-+YxCl>|Aj+2@qr5V=K#QB@Rg2;wS6hYwP zK4m&%)Pa;F%B+BL0a+DGwV{w^-5^qy0nwaB&Wv%!GQu5+jHbqDW~_11h^*V@UBF-{ z*NO>gIOkcFCO1)<=#SBqvE7zGRQB)Z{fu<}5B7=9rfR+I~?rNO3k!ayu)MrC0Ld^M~tA)jN zXya(=y!SyUj8}lLW?b#*V_C> zN4r!Lt7+guV(CB(&IdH8PrXCMt(HdXNJyL#nvx($Ku?dEQD{Q>dKvo2RIn+e;DFNr zXcHBq3xgi8 zQZTLu1@5pJpiLaM2M6f&z|b7j%PDYbI52{3Z>+ZD>%Bvz*n6UrSc}dW#yv4#>RHKB{rTA>&&R4AJAEESW0bRL>my1F_m&kSd5BFE`=V1Sz7F_X0wGU z%X&HB(KYJar~uO%@_4LepZ+`sLj?bkk2-xhbyAK7+NKJxC46Tke+?$cM?{n@GOe)EfW ze|zfnKi~hsuf9F;;bZSSd&`+uzPEJQ{AQ^t`x&%w#-2C_Z_TNR{xZ|sjAO3XTy$`>z@9F2~Et)NPqJ(NwEEOTudW_?VE*xku73U4jgK5Xb|wA!`|;wpp1bkB zd4F?#@Xfh5x4r#O478}cEdURxQe3tUPI5h!i{f4cp#0gskxO{ dVuZ}}o5_)i$LlPUv*^EWGdn$1|Np4~{{uoGn)v_# literal 0 HcmV?d00001 diff --git a/deps/github.com/anacrolix/torrent/metainfo/testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent b/deps/github.com/anacrolix/torrent/metainfo/testdata/archlinux-2011.08.19-netinstall-i686.iso.torrent new file mode 100644 index 0000000000000000000000000000000000000000..9ce7748aaa46f90aa56c29699b763896fc08af91 GIT binary patch literal 12792 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%0D*f1Euc8hO)={iLER+R%jSr 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 literal 0 HcmV?d00001 diff --git a/deps/github.com/anacrolix/torrent/metainfo/testdata/issue_65a.torrent b/deps/github.com/anacrolix/torrent/metainfo/testdata/issue_65a.torrent new file mode 100644 index 0000000000000000000000000000000000000000..8fc13ecd6803b84788e5265aaba8983cb30f3864 GIT binary patch literal 29683 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
    U0h{&K@O;g=S+z>?LhHT)q^@~*lO&LSWZ{+lv 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 C@aE+J literal 0 HcmV?d00001 diff --git a/deps/github.com/anacrolix/torrent/metainfo/testdata/trackerless.torrent b/deps/github.com/anacrolix/torrent/metainfo/testdata/trackerless.torrent new file mode 100644 index 0000000..6537276 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/testdata/trackerless.torrent @@ -0,0 +1 @@ +d7:comment19:This is just a test10:created by12:Johnny Bravo13:creation datei1430648794e8:encoding5:UTF-84:infod6:lengthi1128e4:name12:testfile.bin12:piece lengthi32768e6:pieces20:Ո =Ui^栰E?e5:nodesl35:udp://tracker.openbittorrent.com:8035:udp://tracker.openbittorrent.com:80ee diff --git a/deps/github.com/anacrolix/torrent/metainfo/urllist.go b/deps/github.com/anacrolix/torrent/metainfo/urllist.go new file mode 100644 index 0000000..ed7c36d --- /dev/null +++ b/deps/github.com/anacrolix/torrent/metainfo/urllist.go @@ -0,0 +1,25 @@ +package metainfo + +import ( + "github.com/anacrolix/torrent/bencode" +) + +type UrlList []string + +var _ bencode.Unmarshaler = (*UrlList)(nil) + +func (me *UrlList) UnmarshalBencode(b []byte) error { + if len(b) == 0 { + return nil + } + if b[0] == 'l' { + var l []string + err := bencode.Unmarshal(b, &l) + *me = l + return err + } + var s string + err := bencode.Unmarshal(b, &s) + *me = []string{s} + return err +} diff --git a/deps/github.com/anacrolix/torrent/misc.go b/deps/github.com/anacrolix/torrent/misc.go new file mode 100644 index 0000000..7d3007e --- /dev/null +++ b/deps/github.com/anacrolix/torrent/misc.go @@ -0,0 +1,194 @@ +package torrent + +import ( + "errors" + "net" + + "github.com/RoaringBitmap/roaring" + "github.com/anacrolix/missinggo/v2" + "golang.org/x/time/rate" + + "github.com/anacrolix/torrent/metainfo" + pp "github.com/anacrolix/torrent/peer_protocol" + "github.com/anacrolix/torrent/types" + "github.com/anacrolix/torrent/types/infohash" +) + +type ( + Request = types.Request + ChunkSpec = types.ChunkSpec + piecePriority = types.PiecePriority +) + +const ( + PiecePriorityNormal = types.PiecePriorityNormal + PiecePriorityNone = types.PiecePriorityNone + PiecePriorityNow = types.PiecePriorityNow + PiecePriorityReadahead = types.PiecePriorityReadahead + PiecePriorityNext = types.PiecePriorityNext + PiecePriorityHigh = types.PiecePriorityHigh +) + +func newRequest(index, begin, length pp.Integer) Request { + return Request{index, ChunkSpec{begin, length}} +} + +func newRequestFromMessage(msg *pp.Message) Request { + switch msg.Type { + case pp.Request, pp.Cancel, pp.Reject: + return newRequest(msg.Index, msg.Begin, msg.Length) + case pp.Piece: + return newRequest(msg.Index, msg.Begin, pp.Integer(len(msg.Piece))) + default: + panic(msg.Type) + } +} + +// The size in bytes of a metadata extension piece. +func metadataPieceSize(totalSize, piece int) int { + ret := totalSize - piece*(1<<14) + if ret > 1<<14 { + ret = 1 << 14 + } + return ret +} + +// Return the request that would include the given offset into the torrent data. +func torrentOffsetRequest( + torrentLength, pieceSize, chunkSize, offset int64, +) ( + r Request, ok bool, +) { + if offset < 0 || offset >= torrentLength { + return + } + r.Index = pp.Integer(offset / pieceSize) + r.Begin = pp.Integer(offset % pieceSize / chunkSize * chunkSize) + r.Length = pp.Integer(chunkSize) + pieceLeft := pp.Integer(pieceSize - int64(r.Begin)) + if r.Length > pieceLeft { + r.Length = pieceLeft + } + torrentLeft := torrentLength - int64(r.Index)*pieceSize - int64(r.Begin) + if int64(r.Length) > torrentLeft { + r.Length = pp.Integer(torrentLeft) + } + ok = true + return +} + +func torrentRequestOffset(torrentLength, pieceSize int64, r Request) (off int64) { + off = int64(r.Index)*pieceSize + int64(r.Begin) + if off < 0 || off >= torrentLength { + panic("invalid Request") + } + return +} + +func validateInfo(info *metainfo.Info) error { + if len(info.Pieces)%20 != 0 { + return errors.New("pieces has invalid length") + } + if info.PieceLength == 0 { + if info.TotalLength() != 0 { + return errors.New("zero piece length") + } + } else { + if int((info.TotalLength()+info.PieceLength-1)/info.PieceLength) != info.NumPieces() { + return errors.New("piece count and file lengths are at odds") + } + } + return nil +} + +func chunkIndexSpec(index, pieceLength, chunkSize pp.Integer) ChunkSpec { + ret := ChunkSpec{pp.Integer(index) * chunkSize, chunkSize} + if ret.Begin+ret.Length > pieceLength { + ret.Length = pieceLength - ret.Begin + } + return ret +} + +func connLessTrusted(l, r *Peer) bool { + return l.trust().Less(r.trust()) +} + +func connIsIpv6(nc interface { + LocalAddr() net.Addr +}, +) bool { + ra := nc.LocalAddr() + rip := addrIpOrNil(ra) + return rip.To4() == nil && rip.To16() != nil +} + +func clamp(min, value, max int64) int64 { + if min > max { + panic("harumph") + } + if value < min { + value = min + } + if value > max { + value = max + } + return value +} + +func max(as ...int64) int64 { + ret := as[0] + for _, a := range as[1:] { + if a > ret { + ret = a + } + } + return ret +} + +func maxInt(as ...int) int { + ret := as[0] + for _, a := range as[1:] { + if a > ret { + ret = a + } + } + return ret +} + +func min(as ...int64) int64 { + ret := as[0] + for _, a := range as[1:] { + if a < ret { + ret = a + } + } + return ret +} + +func minInt(as ...int) int { + ret := as[0] + for _, a := range as[1:] { + if a < ret { + ret = a + } + } + return ret +} + +var unlimited = rate.NewLimiter(rate.Inf, 0) + +type ( + pieceIndex = int + // Deprecated: Use infohash.T directly to avoid unnecessary imports. + InfoHash = infohash.T + IpPort = missinggo.IpPort +) + +func boolSliceToBitmap(slice []bool) (rb roaring.Bitmap) { + for i, b := range slice { + if b { + rb.AddInt(i) + } + } + return +} diff --git a/deps/github.com/anacrolix/torrent/misc_test.go b/deps/github.com/anacrolix/torrent/misc_test.go new file mode 100644 index 0000000..d8c0c7a --- /dev/null +++ b/deps/github.com/anacrolix/torrent/misc_test.go @@ -0,0 +1,47 @@ +package torrent + +import ( + "reflect" + "strings" + "testing" + + "github.com/anacrolix/missinggo/iter" + "github.com/anacrolix/missinggo/v2/bitmap" + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/assert" +) + +func TestTorrentOffsetRequest(t *testing.T) { + check := func(tl, ps, off int64, expected Request, ok bool) { + req, _ok := torrentOffsetRequest(tl, ps, defaultChunkSize, off) + assert.Equal(t, _ok, ok) + assert.Equal(t, req, expected) + } + check(13, 5, 0, newRequest(0, 0, 5), true) + check(13, 5, 3, newRequest(0, 0, 5), true) + check(13, 5, 11, newRequest(2, 0, 3), true) + check(13, 5, 13, Request{}, false) +} + +func BenchmarkIterBitmapsDistinct(t *testing.B) { + t.ReportAllocs() + for i := 0; i < t.N; i += 1 { + var skip, first, second bitmap.Bitmap + skip.Add(1) + first.Add(1, 0, 3) + second.Add(1, 2, 0) + skipCopy := skip.Copy() + t.StartTimer() + output := iter.ToSlice(iterBitmapsDistinct(&skipCopy, first, second)) + t.StopTimer() + assert.Equal(t, []interface{}{0, 3, 2}, output) + assert.Equal(t, []bitmap.BitIndex{1}, skip.ToSortedSlice()) + } +} + +func TestSpewConnStats(t *testing.T) { + s := spew.Sdump(ConnStats{}) + t.Logf("\n%s", s) + lines := strings.Count(s, "\n") + assert.EqualValues(t, 2+reflect.ValueOf(ConnStats{}).NumField(), lines) +} diff --git a/deps/github.com/anacrolix/torrent/mmap_span/mmap_span.go b/deps/github.com/anacrolix/torrent/mmap_span/mmap_span.go new file mode 100644 index 0000000..22c394f --- /dev/null +++ b/deps/github.com/anacrolix/torrent/mmap_span/mmap_span.go @@ -0,0 +1,107 @@ +package mmap_span + +import ( + "fmt" + "io" + "sync" + + "github.com/anacrolix/torrent/segments" +) + +type Mmap interface { + Flush() error + Unmap() error + Bytes() []byte +} + +type MMapSpan struct { + mu sync.RWMutex + mMaps []Mmap + segmentLocater segments.Index +} + +func (ms *MMapSpan) Append(mMap Mmap) { + ms.mMaps = append(ms.mMaps, mMap) +} + +func (ms *MMapSpan) Flush() (errs []error) { + ms.mu.RLock() + defer ms.mu.RUnlock() + for _, mMap := range ms.mMaps { + err := mMap.Flush() + if err != nil { + errs = append(errs, err) + } + } + return +} + +func (ms *MMapSpan) Close() (errs []error) { + ms.mu.Lock() + defer ms.mu.Unlock() + for _, mMap := range ms.mMaps { + err := mMap.Unmap() + if err != nil { + errs = append(errs, err) + } + } + // This is for issue 211. + ms.mMaps = nil + ms.InitIndex() + return +} + +func (me *MMapSpan) InitIndex() { + i := 0 + me.segmentLocater = segments.NewIndex(func() (segments.Length, bool) { + if i == len(me.mMaps) { + return -1, false + } + l := int64(len(me.mMaps[i].Bytes())) + i++ + return l, true + }) + // log.Printf("made mmapspan index: %v", me.segmentLocater) +} + +func (ms *MMapSpan) ReadAt(p []byte, off int64) (n int, err error) { + // log.Printf("reading %v bytes at %v", len(p), off) + ms.mu.RLock() + defer ms.mu.RUnlock() + n = ms.locateCopy(func(a, b []byte) (_, _ []byte) { return a, b }, p, off) + if n != len(p) { + err = io.EOF + } + return +} + +func copyBytes(dst, src []byte) int { + return copy(dst, src) +} + +func (ms *MMapSpan) locateCopy(copyArgs func(remainingArgument, mmapped []byte) (dst, src []byte), p []byte, off int64) (n int) { + ms.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool { + mMapBytes := ms.mMaps[i].Bytes()[e.Start:] + // log.Printf("got segment %v: %v, copying %v, %v", i, e, len(p), len(mMapBytes)) + _n := copyBytes(copyArgs(p, mMapBytes)) + p = p[_n:] + n += _n + + if segments.Int(_n) != e.Length { + panic(fmt.Sprintf("did %d bytes, expected to do %d", _n, e.Length)) + } + return true + }) + return +} + +func (ms *MMapSpan) WriteAt(p []byte, off int64) (n int, err error) { + // log.Printf("writing %v bytes at %v", len(p), off) + ms.mu.RLock() + defer ms.mu.RUnlock() + n = ms.locateCopy(func(a, b []byte) (_, _ []byte) { return b, a }, p, off) + if n != len(p) { + err = io.ErrShortWrite + } + return +} diff --git a/deps/github.com/anacrolix/torrent/mse/cmd/mse/main.go b/deps/github.com/anacrolix/torrent/mse/cmd/mse/main.go new file mode 100644 index 0000000..7d10a26 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/mse/cmd/mse/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "fmt" + "io" + "log" + "net" + "os" + "sync" + + "github.com/alexflint/go-arg" + + "github.com/anacrolix/torrent/mse" +) + +func main() { + err := mainErr() + if err != nil { + log.Fatalf("fatal error: %v", err) + } +} + +func mainErr() error { + args := struct { + CryptoMethod mse.CryptoMethod + Dial *struct { + Network string `arg:"positional"` + Address string `arg:"positional"` + SecretKey string `arg:"positional"` + InitialPayload []byte + } `arg:"subcommand"` + Listen *struct { + Network string `arg:"positional"` + Address string `arg:"positional"` + SecretKeys []string `arg:"positional"` + } `arg:"subcommand"` + }{ + CryptoMethod: mse.AllSupportedCrypto, + } + p := arg.MustParse(&args) + if args.Dial != nil { + cn, err := net.Dial(args.Dial.Network, args.Dial.Address) + if err != nil { + return fmt.Errorf("dialing: %w", err) + } + defer cn.Close() + rw, _, err := mse.InitiateHandshake(cn, []byte(args.Dial.SecretKey), args.Dial.InitialPayload, args.CryptoMethod) + if err != nil { + return fmt.Errorf("initiating handshake: %w", err) + } + doStreaming(rw) + } + if args.Listen != nil { + l, err := net.Listen(args.Listen.Network, args.Listen.Address) + if err != nil { + return fmt.Errorf("listening: %w", err) + } + defer l.Close() + cn, err := l.Accept() + l.Close() + if err != nil { + return fmt.Errorf("accepting: %w", err) + } + defer cn.Close() + rw, _, err := mse.ReceiveHandshake(cn, func(f func([]byte) bool) { + for _, sk := range args.Listen.SecretKeys { + f([]byte(sk)) + } + }, mse.DefaultCryptoSelector) + if err != nil { + log.Fatalf("error receiving: %v", err) + } + doStreaming(rw) + } + if p.Subcommand() == nil { + p.Fail("missing subcommand") + } + return nil +} + +func doStreaming(rw io.ReadWriter) { + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + log.Println(io.Copy(rw, os.Stdin)) + }() + go func() { + defer wg.Done() + log.Println(io.Copy(os.Stdout, rw)) + }() + wg.Wait() +} diff --git a/deps/github.com/anacrolix/torrent/mse/mse.go b/deps/github.com/anacrolix/torrent/mse/mse.go new file mode 100644 index 0000000..c3a9f3d --- /dev/null +++ b/deps/github.com/anacrolix/torrent/mse/mse.go @@ -0,0 +1,595 @@ +// https://wiki.vuze.com/w/Message_Stream_Encryption + +package mse + +import ( + "bytes" + "crypto/rand" + "crypto/rc4" + "crypto/sha1" + "encoding/binary" + "errors" + "expvar" + "fmt" + "io" + "math" + "math/big" + "strconv" + "sync" + + "github.com/anacrolix/missinggo/perf" +) + +const ( + maxPadLen = 512 + + CryptoMethodPlaintext CryptoMethod = 1 // After header obfuscation, drop into plaintext + CryptoMethodRC4 CryptoMethod = 2 // After header obfuscation, use RC4 for the rest of the stream + AllSupportedCrypto = CryptoMethodPlaintext | CryptoMethodRC4 +) + +type CryptoMethod uint32 + +var ( + // Prime P according to the spec, and G, the generator. + p, g big.Int + // The rand.Int max arg for use in newPadLen() + newPadLenMax big.Int + // For use in initer's hashes + req1 = []byte("req1") + req2 = []byte("req2") + req3 = []byte("req3") + // Verification constant "VC" which is all zeroes in the bittorrent + // implementation. + vc [8]byte + // Zero padding + zeroPad [512]byte + // Tracks counts of received crypto_provides + cryptoProvidesCount = expvar.NewMap("mseCryptoProvides") +) + +func init() { + p.SetString("0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A63A36210000000000090563", 0) + g.SetInt64(2) + newPadLenMax.SetInt64(maxPadLen + 1) +} + +func hash(parts ...[]byte) []byte { + h := sha1.New() + for _, p := range parts { + n, err := h.Write(p) + if err != nil { + panic(err) + } + if n != len(p) { + panic(n) + } + } + return h.Sum(nil) +} + +func newEncrypt(initer bool, s, skey []byte) (c *rc4.Cipher) { + c, err := rc4.NewCipher(hash([]byte(func() string { + if initer { + return "keyA" + } else { + return "keyB" + } + }()), s, skey)) + if err != nil { + panic(err) + } + var burnSrc, burnDst [1024]byte + c.XORKeyStream(burnDst[:], burnSrc[:]) + return +} + +type cipherReader struct { + c *rc4.Cipher + r io.Reader + mu sync.Mutex + be []byte +} + +func (cr *cipherReader) Read(b []byte) (n int, err error) { + var be []byte + cr.mu.Lock() + if len(cr.be) >= len(b) { + be = cr.be + cr.be = nil + cr.mu.Unlock() + } else { + cr.mu.Unlock() + be = make([]byte, len(b)) + } + n, err = cr.r.Read(be[:len(b)]) + cr.c.XORKeyStream(b[:n], be[:n]) + cr.mu.Lock() + if len(be) > len(cr.be) { + cr.be = be + } + cr.mu.Unlock() + return +} + +func newCipherReader(c *rc4.Cipher, r io.Reader) io.Reader { + return &cipherReader{c: c, r: r} +} + +type cipherWriter struct { + c *rc4.Cipher + w io.Writer + b []byte +} + +func (cr *cipherWriter) Write(b []byte) (n int, err error) { + be := func() []byte { + if len(cr.b) < len(b) { + return make([]byte, len(b)) + } else { + ret := cr.b + cr.b = nil + return ret + } + }() + cr.c.XORKeyStream(be, b) + n, err = cr.w.Write(be[:len(b)]) + if n != len(b) { + // The cipher will have advanced beyond the callers stream position. + // We can't use the cipher anymore. + cr.c = nil + } + if len(be) > len(cr.b) { + cr.b = be + } + return +} + +func newX() big.Int { + var X big.Int + X.SetBytes(func() []byte { + var b [20]byte + _, err := rand.Read(b[:]) + if err != nil { + panic(err) + } + return b[:] + }()) + return X +} + +func paddedLeft(b []byte, _len int) []byte { + if len(b) == _len { + return b + } + ret := make([]byte, _len) + if n := copy(ret[_len-len(b):], b); n != len(b) { + panic(n) + } + return ret +} + +// Calculate, and send Y, our public key. +func (h *handshake) postY(x *big.Int) error { + var y big.Int + y.Exp(&g, x, &p) + return h.postWrite(paddedLeft(y.Bytes(), 96)) +} + +func (h *handshake) establishS() error { + x := newX() + h.postY(&x) + var b [96]byte + _, err := io.ReadFull(h.conn, b[:]) + if err != nil { + return fmt.Errorf("error reading Y: %w", err) + } + var Y, S big.Int + Y.SetBytes(b[:]) + S.Exp(&Y, &x, &p) + sBytes := S.Bytes() + copy(h.s[96-len(sBytes):96], sBytes) + return nil +} + +func newPadLen() int64 { + i, err := rand.Int(rand.Reader, &newPadLenMax) + if err != nil { + panic(err) + } + ret := i.Int64() + if ret < 0 || ret > maxPadLen { + panic(ret) + } + return ret +} + +// Manages state for both initiating and receiving handshakes. +type handshake struct { + conn io.ReadWriter + s [96]byte + initer bool // Whether we're initiating or receiving. + skeys SecretKeyIter // Skeys we'll accept if receiving. + skey []byte // Skey we're initiating with. + ia []byte // Initial payload. Only used by the initiator. + // Return the bit for the crypto method the receiver wants to use. + chooseMethod CryptoSelector + // Sent to the receiver. + cryptoProvides CryptoMethod + + writeMu sync.Mutex + writes [][]byte + writeErr error + writeCond sync.Cond + writeClose bool + + writerMu sync.Mutex + writerCond sync.Cond + writerDone bool +} + +func (h *handshake) finishWriting() { + h.writeMu.Lock() + h.writeClose = true + h.writeCond.Broadcast() + h.writeMu.Unlock() + + h.writerMu.Lock() + for !h.writerDone { + h.writerCond.Wait() + } + h.writerMu.Unlock() +} + +func (h *handshake) writer() { + defer func() { + h.writerMu.Lock() + h.writerDone = true + h.writerCond.Broadcast() + h.writerMu.Unlock() + }() + for { + h.writeMu.Lock() + for { + if len(h.writes) != 0 { + break + } + if h.writeClose { + h.writeMu.Unlock() + return + } + h.writeCond.Wait() + } + b := h.writes[0] + h.writes = h.writes[1:] + h.writeMu.Unlock() + _, err := h.conn.Write(b) + if err != nil { + h.writeMu.Lock() + h.writeErr = err + h.writeMu.Unlock() + return + } + } +} + +func (h *handshake) postWrite(b []byte) error { + h.writeMu.Lock() + defer h.writeMu.Unlock() + if h.writeErr != nil { + return h.writeErr + } + h.writes = append(h.writes, b) + h.writeCond.Signal() + return nil +} + +func xor(a, b []byte) (ret []byte) { + max := len(a) + if max > len(b) { + max = len(b) + } + ret = make([]byte, max) + xorInPlace(ret, a, b) + return +} + +func xorInPlace(dst, a, b []byte) { + for i := range dst { + dst[i] = a[i] ^ b[i] + } +} + +func marshal(w io.Writer, data ...interface{}) (err error) { + for _, data := range data { + err = binary.Write(w, binary.BigEndian, data) + if err != nil { + break + } + } + return +} + +func unmarshal(r io.Reader, data ...interface{}) (err error) { + for _, data := range data { + err = binary.Read(r, binary.BigEndian, data) + if err != nil { + break + } + } + return +} + +// Looking for b at the end of a. +func suffixMatchLen(a, b []byte) int { + if len(b) > len(a) { + b = b[:len(a)] + } + // i is how much of b to try to match + for i := len(b); i > 0; i-- { + // j is how many chars we've compared + j := 0 + for ; j < i; j++ { + if b[i-1-j] != a[len(a)-1-j] { + goto shorter + } + } + return j + shorter: + } + return 0 +} + +// Reads from r until b has been seen. Keeps the minimum amount of data in +// memory. +func readUntil(r io.Reader, b []byte) error { + b1 := make([]byte, len(b)) + i := 0 + for { + _, err := io.ReadFull(r, b1[i:]) + if err != nil { + return err + } + i = suffixMatchLen(b1, b) + if i == len(b) { + break + } + if copy(b1, b1[len(b1)-i:]) != i { + panic("wat") + } + } + return nil +} + +type readWriter struct { + io.Reader + io.Writer +} + +func (h *handshake) newEncrypt(initer bool) *rc4.Cipher { + return newEncrypt(initer, h.s[:], h.skey) +} + +func (h *handshake) initerSteps() (ret io.ReadWriter, selected CryptoMethod, err error) { + h.postWrite(hash(req1, h.s[:])) + h.postWrite(xor(hash(req2, h.skey), hash(req3, h.s[:]))) + buf := &bytes.Buffer{} + padLen := uint16(newPadLen()) + if len(h.ia) > math.MaxUint16 { + err = errors.New("initial payload too large") + return + } + err = marshal(buf, vc[:], h.cryptoProvides, padLen, zeroPad[:padLen], uint16(len(h.ia)), h.ia) + if err != nil { + return + } + e := h.newEncrypt(true) + be := make([]byte, buf.Len()) + e.XORKeyStream(be, buf.Bytes()) + h.postWrite(be) + bC := h.newEncrypt(false) + var eVC [8]byte + bC.XORKeyStream(eVC[:], vc[:]) + // Read until the all zero VC. At this point we've only read the 96 byte + // public key, Y. There is potentially 512 byte padding, between us and + // the 8 byte verification constant. + err = readUntil(io.LimitReader(h.conn, 520), eVC[:]) + if err != nil { + if err == io.EOF { + err = errors.New("failed to synchronize on VC") + } else { + err = fmt.Errorf("error reading until VC: %s", err) + } + return + } + r := newCipherReader(bC, h.conn) + var method CryptoMethod + err = unmarshal(r, &method, &padLen) + if err != nil { + return + } + _, err = io.CopyN(io.Discard, r, int64(padLen)) + if err != nil { + return + } + selected = method & h.cryptoProvides + switch selected { + case CryptoMethodRC4: + ret = readWriter{r, &cipherWriter{e, h.conn, nil}} + case CryptoMethodPlaintext: + ret = h.conn + default: + err = fmt.Errorf("receiver chose unsupported method: %x", method) + } + return +} + +var ErrNoSecretKeyMatch = errors.New("no skey matched") + +func (h *handshake) receiverSteps() (ret io.ReadWriter, chosen CryptoMethod, err error) { + // There is up to 512 bytes of padding, then the 20 byte hash. + err = readUntil(io.LimitReader(h.conn, 532), hash(req1, h.s[:])) + if err != nil { + if err == io.EOF { + err = errors.New("failed to synchronize on S hash") + } + return + } + var b [20]byte + _, err = io.ReadFull(h.conn, b[:]) + if err != nil { + return + } + expectedHash := hash(req3, h.s[:]) + eachHash := sha1.New() + var sum, xored [sha1.Size]byte + err = ErrNoSecretKeyMatch + h.skeys(func(skey []byte) bool { + eachHash.Reset() + eachHash.Write(req2) + eachHash.Write(skey) + eachHash.Sum(sum[:0]) + xorInPlace(xored[:], sum[:], expectedHash) + if bytes.Equal(xored[:], b[:]) { + h.skey = skey + err = nil + return false + } + return true + }) + if err != nil { + return + } + r := newCipherReader(newEncrypt(true, h.s[:], h.skey), h.conn) + var ( + vc [8]byte + provides CryptoMethod + padLen uint16 + ) + + err = unmarshal(r, vc[:], &provides, &padLen) + if err != nil { + return + } + cryptoProvidesCount.Add(strconv.FormatUint(uint64(provides), 16), 1) + chosen = h.chooseMethod(provides) + _, err = io.CopyN(io.Discard, r, int64(padLen)) + if err != nil { + return + } + var lenIA uint16 + unmarshal(r, &lenIA) + if lenIA != 0 { + h.ia = make([]byte, lenIA) + unmarshal(r, h.ia) + } + buf := &bytes.Buffer{} + w := cipherWriter{h.newEncrypt(false), buf, nil} + padLen = uint16(newPadLen()) + err = marshal(&w, &vc, uint32(chosen), padLen, zeroPad[:padLen]) + if err != nil { + return + } + err = h.postWrite(buf.Bytes()) + if err != nil { + return + } + switch chosen { + case CryptoMethodRC4: + ret = readWriter{ + io.MultiReader(bytes.NewReader(h.ia), r), + &cipherWriter{w.c, h.conn, nil}, + } + case CryptoMethodPlaintext: + ret = readWriter{ + io.MultiReader(bytes.NewReader(h.ia), h.conn), + h.conn, + } + default: + err = errors.New("chosen crypto method is not supported") + } + return +} + +func (h *handshake) Do() (ret io.ReadWriter, method CryptoMethod, err error) { + h.writeCond.L = &h.writeMu + h.writerCond.L = &h.writerMu + go h.writer() + defer func() { + h.finishWriting() + if err == nil { + err = h.writeErr + } + }() + err = h.establishS() + if err != nil { + err = fmt.Errorf("error while establishing secret: %w", err) + return + } + pad := make([]byte, newPadLen()) + io.ReadFull(rand.Reader, pad) + err = h.postWrite(pad) + if err != nil { + return + } + if h.initer { + ret, method, err = h.initerSteps() + } else { + ret, method, err = h.receiverSteps() + } + return +} + +func InitiateHandshake( + rw io.ReadWriter, skey, initialPayload []byte, cryptoProvides CryptoMethod, +) ( + ret io.ReadWriter, method CryptoMethod, err error, +) { + h := handshake{ + conn: rw, + initer: true, + skey: skey, + ia: initialPayload, + cryptoProvides: cryptoProvides, + } + defer perf.ScopeTimerErr(&err)() + return h.Do() +} + +type HandshakeResult struct { + io.ReadWriter + CryptoMethod + error + SecretKey []byte +} + +func ReceiveHandshake(rw io.ReadWriter, skeys SecretKeyIter, selectCrypto CryptoSelector) (io.ReadWriter, CryptoMethod, error) { + res := ReceiveHandshakeEx(rw, skeys, selectCrypto) + return res.ReadWriter, res.CryptoMethod, res.error +} + +func ReceiveHandshakeEx(rw io.ReadWriter, skeys SecretKeyIter, selectCrypto CryptoSelector) (ret HandshakeResult) { + h := handshake{ + conn: rw, + initer: false, + skeys: skeys, + chooseMethod: selectCrypto, + } + ret.ReadWriter, ret.CryptoMethod, ret.error = h.Do() + ret.SecretKey = h.skey + return +} + +// A function that given a function, calls it with secret keys until it +// returns false or exhausted. +type SecretKeyIter func(callback func(skey []byte) (more bool)) + +func DefaultCryptoSelector(provided CryptoMethod) CryptoMethod { + // We prefer plaintext for performance reasons. + if provided&CryptoMethodPlaintext != 0 { + return CryptoMethodPlaintext + } + return CryptoMethodRC4 +} + +type CryptoSelector func(CryptoMethod) CryptoMethod diff --git a/deps/github.com/anacrolix/torrent/mse/mse_test.go b/deps/github.com/anacrolix/torrent/mse/mse_test.go new file mode 100644 index 0000000..f7f7fe7 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/mse/mse_test.go @@ -0,0 +1,278 @@ +package mse + +import ( + "bytes" + "crypto/rand" + "crypto/rc4" + "io" + "net" + "sync" + "testing" + + _ "github.com/anacrolix/envpprof" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func sliceIter(skeys [][]byte) SecretKeyIter { + return func(callback func([]byte) bool) { + for _, sk := range skeys { + if !callback(sk) { + break + } + } + } +} + +func TestReadUntil(t *testing.T) { + test := func(data, until string, leftover int, expectedErr error) { + r := bytes.NewReader([]byte(data)) + err := readUntil(r, []byte(until)) + if err != expectedErr { + t.Fatal(err) + } + if r.Len() != leftover { + t.Fatal(r.Len()) + } + } + test("feakjfeafeafegbaabc00", "abc", 2, nil) + test("feakjfeafeafegbaadc00", "abc", 0, io.EOF) +} + +func TestSuffixMatchLen(t *testing.T) { + test := func(a, b string, expected int) { + actual := suffixMatchLen([]byte(a), []byte(b)) + if actual != expected { + t.Fatalf("expected %d, got %d for %q and %q", expected, actual, a, b) + } + } + test("hello", "world", 0) + test("hello", "lo", 2) + test("hello", "llo", 3) + test("hello", "hell", 0) + test("hello", "helloooo!", 5) + test("hello", "lol!", 2) + test("hello", "mondo", 0) + test("mongo", "webscale", 0) + test("sup", "person", 1) +} + +func handshakeTest(t testing.TB, ia []byte, aData, bData string, cryptoProvides CryptoMethod, cryptoSelect CryptoSelector) { + a, b := net.Pipe() + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + defer wg.Done() + a, cm, err := InitiateHandshake(a, []byte("yep"), ia, cryptoProvides) + require.NoError(t, err) + assert.Equal(t, cryptoSelect(cryptoProvides), cm) + go a.Write([]byte(aData)) + + var msg [20]byte + n, _ := a.Read(msg[:]) + if n != len(bData) { + t.FailNow() + } + // t.Log(string(msg[:n])) + }() + go func() { + defer wg.Done() + res := ReceiveHandshakeEx(b, sliceIter([][]byte{[]byte("nope"), []byte("yep"), []byte("maybe")}), cryptoSelect) + require.NoError(t, res.error) + assert.EqualValues(t, "yep", res.SecretKey) + b := res.ReadWriter + assert.Equal(t, cryptoSelect(cryptoProvides), res.CryptoMethod) + go b.Write([]byte(bData)) + // Need to be exact here, as there are several reads, and net.Pipe is most synchronous. + msg := make([]byte, len(ia)+len(aData)) + n, _ := io.ReadFull(b, msg) + if n != len(msg) { + t.FailNow() + } + // t.Log(string(msg[:n])) + }() + wg.Wait() + a.Close() + b.Close() +} + +func allHandshakeTests(t testing.TB, provides CryptoMethod, selector CryptoSelector) { + handshakeTest(t, []byte("jump the gun, "), "hello world", "yo dawg", provides, selector) + handshakeTest(t, nil, "hello world", "yo dawg", provides, selector) + handshakeTest(t, []byte{}, "hello world", "yo dawg", provides, selector) +} + +func TestHandshakeDefault(t *testing.T) { + allHandshakeTests(t, AllSupportedCrypto, DefaultCryptoSelector) + t.Logf("crypto provides encountered: %s", cryptoProvidesCount) +} + +func TestHandshakeSelectPlaintext(t *testing.T) { + allHandshakeTests(t, AllSupportedCrypto, func(CryptoMethod) CryptoMethod { return CryptoMethodPlaintext }) +} + +func BenchmarkHandshakeDefault(b *testing.B) { + for i := 0; i < b.N; i += 1 { + allHandshakeTests(b, AllSupportedCrypto, DefaultCryptoSelector) + } +} + +type trackReader struct { + r io.Reader + n int64 +} + +func (tr *trackReader) Read(b []byte) (n int, err error) { + n, err = tr.r.Read(b) + tr.n += int64(n) + return +} + +func TestReceiveRandomData(t *testing.T) { + tr := trackReader{rand.Reader, 0} + _, _, err := ReceiveHandshake(readWriter{&tr, io.Discard}, nil, DefaultCryptoSelector) + // No skey matches + require.Error(t, err) + // Establishing S, and then reading the maximum padding for giving up on + // synchronizing. + require.EqualValues(t, 96+532, tr.n) +} + +func fillRand(t testing.TB, bs ...[]byte) { + for _, b := range bs { + _, err := rand.Read(b) + require.NoError(t, err) + } +} + +func readAndWrite(rw io.ReadWriter, r, w []byte) error { + var wg sync.WaitGroup + wg.Add(1) + var wErr error + go func() { + defer wg.Done() + _, wErr = rw.Write(w) + }() + _, err := io.ReadFull(rw, r) + if err != nil { + return err + } + wg.Wait() + return wErr +} + +func benchmarkStream(t *testing.B, crypto CryptoMethod) { + ia := make([]byte, 0x1000) + a := make([]byte, 1<<20) + b := make([]byte, 1<<20) + fillRand(t, ia, a, b) + t.StopTimer() + t.SetBytes(int64(len(ia) + len(a) + len(b))) + t.ResetTimer() + for i := 0; i < t.N; i += 1 { + ac, bc := net.Pipe() + ar := make([]byte, len(b)) + br := make([]byte, len(ia)+len(a)) + t.StartTimer() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer ac.Close() + defer wg.Done() + rw, _, err := InitiateHandshake(ac, []byte("cats"), ia, crypto) + require.NoError(t, err) + require.NoError(t, readAndWrite(rw, ar, a)) + }() + func() { + defer bc.Close() + rw, _, err := ReceiveHandshake(bc, sliceIter([][]byte{[]byte("cats")}), func(CryptoMethod) CryptoMethod { return crypto }) + require.NoError(t, err) + require.NoError(t, readAndWrite(rw, br, b)) + }() + wg.Wait() + t.StopTimer() + if !bytes.Equal(ar, b) { + t.Fatalf("A read the wrong bytes") + } + if !bytes.Equal(br[:len(ia)], ia) { + t.Fatalf("B read the wrong IA") + } + if !bytes.Equal(br[len(ia):], a) { + t.Fatalf("B read the wrong A") + } + // require.Equal(t, b, ar) + // require.Equal(t, ia, br[:len(ia)]) + // require.Equal(t, a, br[len(ia):]) + } +} + +func BenchmarkStreamRC4(t *testing.B) { + benchmarkStream(t, CryptoMethodRC4) +} + +func BenchmarkStreamPlaintext(t *testing.B) { + benchmarkStream(t, CryptoMethodPlaintext) +} + +func BenchmarkPipeRC4(t *testing.B) { + key := make([]byte, 20) + n, _ := rand.Read(key) + require.Equal(t, len(key), n) + var buf bytes.Buffer + c, err := rc4.NewCipher(key) + require.NoError(t, err) + r := cipherReader{ + c: c, + r: &buf, + } + c, err = rc4.NewCipher(key) + require.NoError(t, err) + w := cipherWriter{ + c: c, + w: &buf, + } + a := make([]byte, 0x1000) + n, _ = io.ReadFull(rand.Reader, a) + require.Equal(t, len(a), n) + b := make([]byte, len(a)) + t.SetBytes(int64(len(a))) + t.ResetTimer() + for i := 0; i < t.N; i += 1 { + n, _ = w.Write(a) + if n != len(a) { + t.FailNow() + } + n, _ = r.Read(b) + if n != len(b) { + t.FailNow() + } + if !bytes.Equal(a, b) { + t.FailNow() + } + } +} + +func BenchmarkSkeysReceive(b *testing.B) { + var skeys [][]byte + for i := 0; i < 100000; i += 1 { + skeys = append(skeys, make([]byte, 20)) + } + fillRand(b, skeys...) + initSkey := skeys[len(skeys)/2] + // c := qt.New(b) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i += 1 { + initiator, receiver := net.Pipe() + go func() { + _, _, err := InitiateHandshake(initiator, initSkey, nil, AllSupportedCrypto) + if err != nil { + panic(err) + } + }() + res := ReceiveHandshakeEx(receiver, sliceIter(skeys), DefaultCryptoSelector) + if res.error != nil { + panic(res.error) + } + } +} diff --git a/deps/github.com/anacrolix/torrent/netip-addrport.go b/deps/github.com/anacrolix/torrent/netip-addrport.go new file mode 100644 index 0000000..e438db7 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/netip-addrport.go @@ -0,0 +1,52 @@ +package torrent + +import ( + "fmt" + "net/netip" + + "github.com/anacrolix/dht/v2/krpc" +) + +type addrPorter interface { + AddrPort() netip.AddrPort +} + +func ipv4AddrPortFromKrpcNodeAddr(na krpc.NodeAddr) (_ netip.AddrPort, err error) { + ip4 := na.IP.To4() + if ip4 == nil { + err = fmt.Errorf("not an ipv4 address: %v", na.IP) + return + } + addr := netip.AddrFrom4(*(*[4]byte)(ip4)) + addrPort := netip.AddrPortFrom(addr, uint16(na.Port)) + return addrPort, nil +} + +func ipv6AddrPortFromKrpcNodeAddr(na krpc.NodeAddr) (_ netip.AddrPort, err error) { + ip6 := na.IP.To16() + if ip6 == nil { + err = fmt.Errorf("not an ipv4 address: %v", na.IP) + return + } + addr := netip.AddrFrom16(*(*[16]byte)(ip6)) + addrPort := netip.AddrPortFrom(addr, uint16(na.Port)) + return addrPort, nil +} + +func addrPortFromPeerRemoteAddr(pra PeerRemoteAddr) (netip.AddrPort, error) { + switch v := pra.(type) { + case addrPorter: + return v.AddrPort(), nil + case netip.AddrPort: + return v, nil + default: + return netip.ParseAddrPort(pra.String()) + } +} + +func krpcNodeAddrFromAddrPort(addrPort netip.AddrPort) krpc.NodeAddr { + return krpc.NodeAddr{ + IP: addrPort.Addr().AsSlice(), + Port: int(addrPort.Port()), + } +} diff --git a/deps/github.com/anacrolix/torrent/network_test.go b/deps/github.com/anacrolix/torrent/network_test.go new file mode 100644 index 0000000..a1fd880 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/network_test.go @@ -0,0 +1,81 @@ +package torrent + +import ( + "net" + "testing" + + "github.com/anacrolix/log" + "github.com/anacrolix/missinggo/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testListenerNetwork( + t *testing.T, + listenFunc func(net, addr string) (net.Listener, error), + expectedNet, givenNet, addr string, validIp4 bool, +) { + l, err := listenFunc(givenNet, addr) + require.NoError(t, err) + defer l.Close() + assert.EqualValues(t, expectedNet, l.Addr().Network()) + ip := missinggo.AddrIP(l.Addr()) + assert.Equal(t, validIp4, ip.To4() != nil, ip) +} + +func listenUtpListener(net, addr string) (l net.Listener, err error) { + l, err = NewUtpSocket(net, addr, nil, log.Default) + return +} + +func testAcceptedConnAddr( + t *testing.T, + network string, valid4 bool, + dial func(addr string) (net.Conn, error), + listen func() (net.Listener, error), +) { + l, err := listen() + require.NoError(t, err) + defer l.Close() + done := make(chan struct{}) + defer close(done) + go func() { + c, err := dial(l.Addr().String()) + require.NoError(t, err) + <-done + c.Close() + }() + c, err := l.Accept() + require.NoError(t, err) + defer c.Close() + assert.EqualValues(t, network, c.RemoteAddr().Network()) + assert.Equal(t, valid4, missinggo.AddrIP(c.RemoteAddr()).To4() != nil) +} + +func listenClosure(rawListenFunc func(string, string) (net.Listener, error), network, addr string) func() (net.Listener, error) { + return func() (net.Listener, error) { + return rawListenFunc(network, addr) + } +} + +func dialClosure(f func(net, addr string) (net.Conn, error), network string) func(addr string) (net.Conn, error) { + return func(addr string) (net.Conn, error) { + return f(network, addr) + } +} + +func TestListenLocalhostNetwork(t *testing.T) { + testListenerNetwork(t, net.Listen, "tcp", "tcp", "0.0.0.0:0", false) + testListenerNetwork(t, net.Listen, "tcp", "tcp", "[::1]:0", false) + testListenerNetwork(t, listenUtpListener, "udp", "udp6", "[::1]:0", false) + testListenerNetwork(t, listenUtpListener, "udp", "udp6", "[::]:0", false) + testListenerNetwork(t, listenUtpListener, "udp", "udp4", "localhost:0", true) + + testAcceptedConnAddr( + t, + "tcp", + false, + dialClosure(net.Dial, "tcp"), + listenClosure(net.Listen, "tcp6", "localhost:0"), + ) +} diff --git a/deps/github.com/anacrolix/torrent/networks.go b/deps/github.com/anacrolix/torrent/networks.go new file mode 100644 index 0000000..068a9a5 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/networks.go @@ -0,0 +1,57 @@ +package torrent + +import "strings" + +var allPeerNetworks = func() (ret []network) { + for _, s := range []string{"tcp4", "tcp6", "udp4", "udp6"} { + ret = append(ret, parseNetworkString(s)) + } + return +}() + +type network struct { + Ipv4 bool + Ipv6 bool + Udp bool + Tcp bool +} + +func (n network) String() (ret string) { + a := func(b bool, s string) { + if b { + ret += s + } + } + a(n.Udp, "udp") + a(n.Tcp, "tcp") + a(n.Ipv4, "4") + a(n.Ipv6, "6") + return +} + +func parseNetworkString(network string) (ret network) { + c := func(s string) bool { + return strings.Contains(network, s) + } + ret.Ipv4 = c("4") + ret.Ipv6 = c("6") + ret.Udp = c("udp") + ret.Tcp = c("tcp") + return +} + +func peerNetworkEnabled(n network, cfg *ClientConfig) bool { + if cfg.DisableUTP && n.Udp { + return false + } + if cfg.DisableTCP && n.Tcp { + return false + } + if cfg.DisableIPv6 && n.Ipv6 { + return false + } + if cfg.DisableIPv4 && n.Ipv4 { + return false + } + return true +} diff --git a/deps/github.com/anacrolix/torrent/ordered-bitmap.go b/deps/github.com/anacrolix/torrent/ordered-bitmap.go new file mode 100644 index 0000000..7410671 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/ordered-bitmap.go @@ -0,0 +1,59 @@ +package torrent + +import ( + g "github.com/anacrolix/generics" + list "github.com/bahlo/generic-list-go" + + "github.com/anacrolix/torrent/typed-roaring" +) + +type orderedBitmap[T typedRoaring.BitConstraint] struct { + bitmap typedRoaring.Bitmap[T] + // There should be way more efficient ways to do this. + order list.List[T] + elements map[T]*list.Element[T] +} + +func (o *orderedBitmap[T]) IterateSnapshot(f func(T) bool) { + o.bitmap.Clone().Iterate(f) +} + +func (o *orderedBitmap[T]) IsEmpty() bool { + return o.bitmap.IsEmpty() +} + +func (o *orderedBitmap[T]) GetCardinality() uint64 { + return uint64(o.order.Len()) +} + +func (o *orderedBitmap[T]) Contains(index T) bool { + return o.bitmap.Contains(index) +} + +func (o *orderedBitmap[T]) Add(index T) { + o.bitmap.Add(index) + if _, ok := o.elements[index]; !ok { + g.MakeMapIfNilAndSet(&o.elements, index, o.order.PushBack(index)) + } +} + +func (o *orderedBitmap[T]) Rank(index T) uint64 { + return o.bitmap.Rank(index) +} + +func (o *orderedBitmap[T]) Iterate(f func(T) bool) { + for e := o.order.Front(); e != nil; e = e.Next() { + if !f(e.Value) { + return + } + } +} + +func (o *orderedBitmap[T]) CheckedRemove(index T) bool { + if !o.bitmap.CheckedRemove(index) { + return false + } + o.order.Remove(o.elements[index]) + delete(o.elements, index) + return true +} diff --git a/deps/github.com/anacrolix/torrent/otel.go b/deps/github.com/anacrolix/torrent/otel.go new file mode 100644 index 0000000..5dddd6a --- /dev/null +++ b/deps/github.com/anacrolix/torrent/otel.go @@ -0,0 +1,3 @@ +package torrent + +const tracerName = "anacrolix.torrent" diff --git a/deps/github.com/anacrolix/torrent/peer-conn-msg-writer.go b/deps/github.com/anacrolix/torrent/peer-conn-msg-writer.go new file mode 100644 index 0000000..1bacc59 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/peer-conn-msg-writer.go @@ -0,0 +1,130 @@ +package torrent + +import ( + "bytes" + "io" + "time" + + "github.com/anacrolix/chansync" + "github.com/anacrolix/log" + "github.com/anacrolix/sync" + + pp "github.com/anacrolix/torrent/peer_protocol" +) + +func (pc *PeerConn) initMessageWriter() { + w := &pc.messageWriter + *w = peerConnMsgWriter{ + fillWriteBuffer: func() { + pc.locker().Lock() + defer pc.locker().Unlock() + if pc.closed.IsSet() { + return + } + pc.fillWriteBuffer() + }, + closed: &pc.closed, + logger: pc.logger, + w: pc.w, + keepAlive: func() bool { + pc.locker().RLock() + defer pc.locker().RUnlock() + return pc.useful() + }, + writeBuffer: new(bytes.Buffer), + } +} + +func (pc *PeerConn) startMessageWriter() { + pc.initMessageWriter() + go pc.messageWriterRunner() +} + +func (pc *PeerConn) messageWriterRunner() { + defer pc.locker().Unlock() + defer pc.close() + defer pc.locker().Lock() + pc.messageWriter.run(pc.t.cl.config.KeepAliveTimeout) +} + +type peerConnMsgWriter struct { + // Must not be called with the local mutex held, as it will call back into the write method. + fillWriteBuffer func() + closed *chansync.SetOnce + logger log.Logger + w io.Writer + keepAlive func() bool + + mu sync.Mutex + writeCond chansync.BroadcastCond + // Pointer so we can swap with the "front buffer". + writeBuffer *bytes.Buffer +} + +// Routine that writes to the peer. Some of what to write is buffered by +// activity elsewhere in the Client, and some is determined locally when the +// connection is writable. +func (cn *peerConnMsgWriter) run(keepAliveTimeout time.Duration) { + lastWrite := time.Now() + keepAliveTimer := time.NewTimer(keepAliveTimeout) + frontBuf := new(bytes.Buffer) + for { + if cn.closed.IsSet() { + return + } + cn.fillWriteBuffer() + keepAlive := cn.keepAlive() + cn.mu.Lock() + if cn.writeBuffer.Len() == 0 && time.Since(lastWrite) >= keepAliveTimeout && keepAlive { + cn.writeBuffer.Write(pp.Message{Keepalive: true}.MustMarshalBinary()) + torrent.Add("written keepalives", 1) + } + if cn.writeBuffer.Len() == 0 { + writeCond := cn.writeCond.Signaled() + cn.mu.Unlock() + select { + case <-cn.closed.Done(): + case <-writeCond: + case <-keepAliveTimer.C: + } + continue + } + // Flip the buffers. + frontBuf, cn.writeBuffer = cn.writeBuffer, frontBuf + cn.mu.Unlock() + if frontBuf.Len() == 0 { + panic("expected non-empty front buffer") + } + var err error + for frontBuf.Len() != 0 { + // Limit write size for WebRTC. See https://github.com/pion/datachannel/issues/59. + next := frontBuf.Next(1<<16 - 1) + var n int + n, err = cn.w.Write(next) + if err == nil && n != len(next) { + panic("expected full write") + } + if err != nil { + break + } + } + if err != nil { + cn.logger.WithDefaultLevel(log.Debug).Printf("error writing: %v", err) + return + } + lastWrite = time.Now() + keepAliveTimer.Reset(keepAliveTimeout) + } +} + +func (cn *peerConnMsgWriter) write(msg pp.Message) bool { + cn.mu.Lock() + defer cn.mu.Unlock() + cn.writeBuffer.Write(msg.MustMarshalBinary()) + cn.writeCond.Broadcast() + return !cn.writeBufferFull() +} + +func (cn *peerConnMsgWriter) writeBufferFull() bool { + return cn.writeBuffer.Len() >= writeBufferHighWaterLen +} diff --git a/deps/github.com/anacrolix/torrent/peer-impl.go b/deps/github.com/anacrolix/torrent/peer-impl.go new file mode 100644 index 0000000..f9f9096 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/peer-impl.go @@ -0,0 +1,37 @@ +package torrent + +import ( + "github.com/RoaringBitmap/roaring" + + "github.com/anacrolix/torrent/metainfo" +) + +// Contains implementation details that differ between peer types, like Webseeds and regular +// BitTorrent protocol connections. Some methods are underlined so as to avoid collisions with +// legacy PeerConn methods. +type peerImpl interface { + // Trigger the actual request state to get updated + handleUpdateRequests() + writeInterested(interested bool) bool + + // _cancel initiates cancellation of a request and returns acked if it expects the cancel to be + // handled by a follow-up event. + _cancel(RequestIndex) (acked bool) + _request(Request) bool + connectionFlags() string + onClose() + onGotInfo(*metainfo.Info) + // Drop connection. This may be a no-op if there is no connection. + drop() + // Rebuke the peer + ban() + String() string + peerImplStatusLines() []string + + // All if the peer should have everything, known if we know that for a fact. For example, we can + // guess at how many pieces are in a torrent, and assume they have all pieces based on them + // having sent haves for everything, but we don't know for sure. But if they send a have-all + // message, then it's clear that they do. + peerHasAllPieces() (all, known bool) + peerPieces() *roaring.Bitmap +} diff --git a/deps/github.com/anacrolix/torrent/peer.go b/deps/github.com/anacrolix/torrent/peer.go new file mode 100644 index 0000000..608ccb1 --- /dev/null +++ b/deps/github.com/anacrolix/torrent/peer.go @@ -0,0 +1,877 @@ +package torrent + +import ( + "errors" + "fmt" + "io" + "net" + "strings" + "time" + + "github.com/RoaringBitmap/roaring" + "github.com/anacrolix/chansync" + . "github.com/anacrolix/generics" + "github.com/anacrolix/log" + "github.com/anacrolix/missinggo/iter" + "github.com/anacrolix/missinggo/v2/bitmap" + "github.com/anacrolix/multiless" + + "github.com/anacrolix/torrent/internal/alloclim" + "github.com/anacrolix/torrent/mse" + pp "github.com/anacrolix/torrent/peer_protocol" + request_strategy "github.com/anacrolix/torrent/request-strategy" + typedRoaring "github.com/anacrolix/torrent/typed-roaring" +) + +type ( + Peer struct { + // First to ensure 64-bit alignment for atomics. See #262. + _stats ConnStats + + t *Torrent + + peerImpl + callbacks *Callbacks + + outgoing bool + Network string + RemoteAddr PeerRemoteAddr + // The local address as observed by the remote peer. WebRTC seems to get this right without needing hints from the + // config. + localPublicAddr peerLocalPublicAddr + bannableAddr Option[bannableAddr] + // True if the connection is operating over MSE obfuscation. + headerEncrypted bool + cryptoMethod mse.CryptoMethod + Discovery PeerSource + trusted bool + closed chansync.SetOnce + // Set true after we've added our ConnStats generated during handshake to + // other ConnStat instances as determined when the *Torrent became known. + reconciledHandshakeStats bool + + lastMessageReceived time.Time + completedHandshake time.Time + lastUsefulChunkReceived time.Time + lastChunkSent time.Time + + // Stuff controlled by the local peer. + needRequestUpdate string + requestState request_strategy.PeerRequestState + updateRequestsTimer *time.Timer + lastRequestUpdate time.Time + peakRequests maxRequests + lastBecameInterested time.Time + priorInterest time.Duration + + lastStartedExpectingToReceiveChunks time.Time + cumulativeExpectedToReceiveChunks time.Duration + _chunksReceivedWhileExpecting int64 + + choking bool + piecesReceivedSinceLastRequestUpdate maxRequests + maxPiecesReceivedBetweenRequestUpdates maxRequests + // Chunks that we might reasonably expect to receive from the peer. Due to latency, buffering, + // and implementation differences, we may receive chunks that are no longer in the set of + // requests actually want. This could use a roaring.BSI if the memory use becomes noticeable. + validReceiveChunks map[RequestIndex]int + // Indexed by metadata piece, set to true if posted and pending a + // response. + metadataRequests []bool + sentHaves bitmap.Bitmap + + // Stuff controlled by the remote peer. + peerInterested bool + peerChoking bool + peerRequests map[Request]*peerRequestState + PeerPrefersEncryption bool // as indicated by 'e' field in extension handshake + // The highest possible number of pieces the torrent could have based on + // communication with the peer. Generally only useful until we have the + // torrent info. + peerMinPieces pieceIndex + // Pieces we've accepted chunks for from the peer. + peerTouchedPieces map[pieceIndex]struct{} + peerAllowedFast typedRoaring.Bitmap[pieceIndex] + + PeerMaxRequests maxRequests // Maximum pending requests the peer allows. + + logger log.Logger + } + + PeerSource string + + peerRequestState struct { + data []byte + allocReservation *alloclim.Reservation + } + + PeerRemoteAddr interface { + String() string + } + + peerRequests = orderedBitmap[RequestIndex] +) + +const ( + PeerSourceUtHolepunch = "C" + PeerSourceTracker = "Tr" + PeerSourceIncoming = "I" + PeerSourceDhtGetPeers = "Hg" // Peers we found by searching a DHT. + PeerSourceDhtAnnouncePeer = "Ha" // Peers that were announced to us by a DHT. + PeerSourcePex = "X" + // The peer was given directly, such as through a magnet link. + PeerSourceDirect = "M" +) + +// Returns the Torrent a Peer belongs to. Shouldn't change for the lifetime of the Peer. May be nil +// if we are the receiving end of a connection and the handshake hasn't been received or accepted +// yet. +func (p *Peer) Torrent() *Torrent { + return p.t +} + +func (p *Peer) initRequestState() { + p.requestState.Requests = &peerRequests{} +} + +func (cn *Peer) updateExpectingChunks() { + if cn.expectingChunks() { + if cn.lastStartedExpectingToReceiveChunks.IsZero() { + cn.lastStartedExpectingToReceiveChunks = time.Now() + } + } else { + if !cn.lastStartedExpectingToReceiveChunks.IsZero() { + cn.cumulativeExpectedToReceiveChunks += time.Since(cn.lastStartedExpectingToReceiveChunks) + cn.lastStartedExpectingToReceiveChunks = time.Time{} + } + } +} + +func (cn *Peer) expectingChunks() bool { + if cn.requestState.Requests.IsEmpty() { + return false + } + if !cn.requestState.Interested { + return false + } + if !cn.peerChoking { + return true + } + haveAllowedFastRequests := false + cn.peerAllowedFast.Iterate(func(i pieceIndex) bool { + haveAllowedFastRequests = roaringBitmapRangeCardinality[RequestIndex]( + cn.requestState.Requests, + cn.t.pieceRequestIndexOffset(i), + cn.t.pieceRequestIndexOffset(i+1), + ) == 0 + return !haveAllowedFastRequests + }) + return haveAllowedFastRequests +} + +func (cn *Peer) remoteChokingPiece(piece pieceIndex) bool { + return cn.peerChoking && !cn.peerAllowedFast.Contains(piece) +} + +func (cn *Peer) cumInterest() time.Duration { + ret := cn.priorInterest + if cn.requestState.Interested { + ret += time.Since(cn.lastBecameInterested) + } + return ret +} + +func (cn *Peer) locker() *lockWithDeferreds { + return cn.t.cl.locker() +} + +func (cn *PeerConn) supportsExtension(ext pp.ExtensionName) bool { + _, ok := cn.PeerExtensionIDs[ext] + return ok +} + +// The best guess at number of pieces in the torrent for this peer. +func (cn *Peer) bestPeerNumPieces() pieceIndex { + if cn.t.haveInfo() { + return cn.t.numPieces() + } + return cn.peerMinPieces +} + +func (cn *Peer) completedString() string { + have := pieceIndex(cn.peerPieces().GetCardinality()) + if all, _ := cn.peerHasAllPieces(); all { + have = cn.bestPeerNumPieces() + } + return fmt.Sprintf("%d/%d", have, cn.bestPeerNumPieces()) +} + +func eventAgeString(t time.Time) string { + if t.IsZero() { + return "never" + } + return fmt.Sprintf("%.2fs ago", time.Since(t).Seconds()) +} + +// Inspired by https://github.com/transmission/transmission/wiki/Peer-Status-Text. +func (cn *Peer) statusFlags() (ret string) { + c := func(b byte) { + ret += string([]byte{b}) + } + if cn.requestState.Interested { + c('i') + } + if cn.choking { + c('c') + } + c('-') + ret += cn.connectionFlags() + c('-') + if cn.peerInterested { + c('i') + } + if cn.peerChoking { + c('c') + } + return +} + +func (cn *Peer) downloadRate() float64 { + num := cn._stats.BytesReadUsefulData.Int64() + if num == 0 { + return 0 + } + return float64(num) / cn.totalExpectingTime().Seconds() +} + +func (p *Peer) DownloadRate() float64 { + p.locker().RLock() + defer p.locker().RUnlock() + + return p.downloadRate() +} + +func (cn *Peer) iterContiguousPieceRequests(f func(piece pieceIndex, count int)) { + var last Option[pieceIndex] + var count int + next := func(item Option[pieceIndex]) { + if item == last { + count++ + } else { + if count != 0 { + f(last.Value, count) + } + last = item + count = 1 + } + } + cn.requestState.Requests.Iterate(func(requestIndex request_strategy.RequestIndex) bool { + next(Some(cn.t.pieceIndexOfRequestIndex(requestIndex))) + return true + }) + next(None[pieceIndex]()) +} + +func (cn *Peer) writeStatus(w io.Writer) { + // \t isn't preserved in
     blocks?
    +	if cn.closed.IsSet() {
    +		fmt.Fprint(w, "CLOSED: ")
    +	}
    +	fmt.Fprintln(w, strings.Join(cn.peerImplStatusLines(), "\n"))
    +	prio, err := cn.peerPriority()
    +	prioStr := fmt.Sprintf("%08x", prio)
    +	if err != nil {
    +		prioStr += ": " + err.Error()
    +	}
    +	fmt.Fprintf(w, "bep40-prio: %v\n", prioStr)
    +	fmt.Fprintf(w, "last msg: %s, connected: %s, last helpful: %s, itime: %s, etime: %s\n",
    +		eventAgeString(cn.lastMessageReceived),
    +		eventAgeString(cn.completedHandshake),
    +		eventAgeString(cn.lastHelpful()),
    +		cn.cumInterest(),
    +		cn.totalExpectingTime(),
    +	)
    +	fmt.Fprintf(w,
    +		"%s completed, %d pieces touched, good chunks: %v/%v:%v reqq: %d+%v/(%d/%d):%d/%d, flags: %s, dr: %.1f KiB/s\n",
    +		cn.completedString(),
    +		len(cn.peerTouchedPieces),
    +		&cn._stats.ChunksReadUseful,
    +		&cn._stats.ChunksRead,
    +		&cn._stats.ChunksWritten,
    +		cn.requestState.Requests.GetCardinality(),
    +		cn.requestState.Cancelled.GetCardinality(),
    +		cn.nominalMaxRequests(),
    +		cn.PeerMaxRequests,
    +		len(cn.peerRequests),
    +		localClientReqq,
    +		cn.statusFlags(),
    +		cn.downloadRate()/(1<<10),
    +	)
    +	fmt.Fprintf(w, "requested pieces:")
    +	cn.iterContiguousPieceRequests(func(piece pieceIndex, count int) {
    +		fmt.Fprintf(w, " %v(%v)", piece, count)
    +	})
    +	fmt.Fprintf(w, "\n")
    +}
    +
    +func (p *Peer) close() {
    +	if !p.closed.Set() {
    +		return
    +	}
    +	if p.updateRequestsTimer != nil {
    +		p.updateRequestsTimer.Stop()
    +	}
    +	for _, prs := range p.peerRequests {
    +		prs.allocReservation.Drop()
    +	}
    +	p.peerImpl.onClose()
    +	if p.t != nil {
    +		p.t.decPeerPieceAvailability(p)
    +	}
    +	for _, f := range p.callbacks.PeerClosed {
    +		f(p)
    +	}
    +}
    +
    +// Peer definitely has a piece, for purposes of requesting. So it's not sufficient that we think
    +// they do (known=true).
    +func (cn *Peer) peerHasPiece(piece pieceIndex) bool {
    +	if all, known := cn.peerHasAllPieces(); all && known {
    +		return true
    +	}
    +	return cn.peerPieces().ContainsInt(piece)
    +}
    +
    +// 64KiB, but temporarily less to work around an issue with WebRTC. TODO: Update when
    +// https://github.com/pion/datachannel/issues/59 is fixed.
    +const (
    +	writeBufferHighWaterLen = 1 << 15
    +	writeBufferLowWaterLen  = writeBufferHighWaterLen / 2
    +)
    +
    +var (
    +	interestedMsgLen = len(pp.Message{Type: pp.Interested}.MustMarshalBinary())
    +	requestMsgLen    = len(pp.Message{Type: pp.Request}.MustMarshalBinary())
    +	// This is the maximum request count that could fit in the write buffer if it's at or below the
    +	// low water mark when we run maybeUpdateActualRequestState.
    +	maxLocalToRemoteRequests = (writeBufferHighWaterLen - writeBufferLowWaterLen - interestedMsgLen) / requestMsgLen
    +)
    +
    +// The actual value to use as the maximum outbound requests.
    +func (cn *Peer) nominalMaxRequests() maxRequests {
    +	return maxInt(1, minInt(cn.PeerMaxRequests, cn.peakRequests*2, maxLocalToRemoteRequests))
    +}
    +
    +func (cn *Peer) totalExpectingTime() (ret time.Duration) {
    +	ret = cn.cumulativeExpectedToReceiveChunks
    +	if !cn.lastStartedExpectingToReceiveChunks.IsZero() {
    +		ret += time.Since(cn.lastStartedExpectingToReceiveChunks)
    +	}
    +	return
    +}
    +
    +func (cn *Peer) setInterested(interested bool) bool {
    +	if cn.requestState.Interested == interested {
    +		return true
    +	}
    +	cn.requestState.Interested = interested
    +	if interested {
    +		cn.lastBecameInterested = time.Now()
    +	} else if !cn.lastBecameInterested.IsZero() {
    +		cn.priorInterest += time.Since(cn.lastBecameInterested)
    +	}
    +	cn.updateExpectingChunks()
    +	// log.Printf("%p: setting interest: %v", cn, interested)
    +	return cn.writeInterested(interested)
    +}
    +
    +// The function takes a message to be sent, and returns true if more messages
    +// are okay.
    +type messageWriter func(pp.Message) bool
    +
    +// This function seems to only used by Peer.request. It's all logic checks, so maybe we can no-op it
    +// when we want to go fast.
    +func (cn *Peer) shouldRequest(r RequestIndex) error {
    +	err := cn.t.checkValidReceiveChunk(cn.t.requestIndexToRequest(r))
    +	if err != nil {
    +		return err
    +	}
    +	pi := cn.t.pieceIndexOfRequestIndex(r)
    +	if cn.requestState.Cancelled.Contains(r) {
    +		return errors.New("request is cancelled and waiting acknowledgement")
    +	}
    +	if !cn.peerHasPiece(pi) {
    +		return errors.New("requesting piece peer doesn't have")
    +	}
    +	if !cn.t.peerIsActive(cn) {
    +		panic("requesting but not in active conns")
    +	}
    +	if cn.closed.IsSet() {
    +		panic("requesting when connection is closed")
    +	}
    +	if cn.t.hashingPiece(pi) {
    +		panic("piece is being hashed")
    +	}
    +	if cn.t.pieceQueuedForHash(pi) {
    +		panic("piece is queued for hash")
    +	}
    +	if cn.peerChoking && !cn.peerAllowedFast.Contains(pi) {
    +		// This could occur if we made a request with the fast extension, and then got choked and
    +		// haven't had the request rejected yet.
    +		if !cn.requestState.Requests.Contains(r) {
    +			panic("peer choking and piece not allowed fast")
    +		}
    +	}
    +	return nil
    +}
    +
    +func (cn *Peer) mustRequest(r RequestIndex) bool {
    +	more, err := cn.request(r)
    +	if err != nil {
    +		panic(err)
    +	}
    +	return more
    +}
    +
    +func (cn *Peer) request(r RequestIndex) (more bool, err error) {
    +	if err := cn.shouldRequest(r); err != nil {
    +		panic(err)
    +	}
    +	if cn.requestState.Requests.Contains(r) {
    +		return true, nil
    +	}
    +	if maxRequests(cn.requestState.Requests.GetCardinality()) >= cn.nominalMaxRequests() {
    +		return true, errors.New("too many outstanding requests")
    +	}
    +	cn.requestState.Requests.Add(r)
    +	if cn.validReceiveChunks == nil {
    +		cn.validReceiveChunks = make(map[RequestIndex]int)
    +	}
    +	cn.validReceiveChunks[r]++
    +	cn.t.requestState[r] = requestState{
    +		peer: cn,
    +		when: time.Now(),
    +	}
    +	cn.updateExpectingChunks()
    +	ppReq := cn.t.requestIndexToRequest(r)
    +	for _, f := range cn.callbacks.SentRequest {
    +		f(PeerRequestEvent{cn, ppReq})
    +	}
    +	return cn.peerImpl._request(ppReq), nil
    +}
    +
    +func (me *Peer) cancel(r RequestIndex) {
    +	if !me.deleteRequest(r) {
    +		panic("request not existing should have been guarded")
    +	}
    +	if me._cancel(r) {
    +		// Record that we expect to get a cancel ack.
    +		if !me.requestState.Cancelled.CheckedAdd(r) {
    +			panic("request already cancelled")
    +		}
    +	}
    +	me.decPeakRequests()
    +	if me.isLowOnRequests() {
    +		me.updateRequests("Peer.cancel")
    +	}
    +}
    +
    +// Sets a reason to update requests, and if there wasn't already one, handle it.
    +func (cn *Peer) updateRequests(reason string) {
    +	if cn.needRequestUpdate != "" {
    +		return
    +	}
    +	cn.needRequestUpdate = reason
    +	cn.handleUpdateRequests()
    +}
    +
    +// Emits the indices in the Bitmaps bms in order, never repeating any index.
    +// skip is mutated during execution, and its initial values will never be
    +// emitted.
    +func iterBitmapsDistinct(skip *bitmap.Bitmap, bms ...bitmap.Bitmap) iter.Func {
    +	return func(cb iter.Callback) {
    +		for _, bm := range bms {
    +			if !iter.All(
    +				func(_i interface{}) bool {
    +					i := _i.(int)
    +					if skip.Contains(bitmap.BitIndex(i)) {
    +						return true
    +					}
    +					skip.Add(bitmap.BitIndex(i))
    +					return cb(i)
    +				},
    +				bm.Iter,
    +			) {
    +				return
    +			}
    +		}
    +	}
    +}
    +
    +// After handshake, we know what Torrent and Client stats to include for a
    +// connection.
    +func (cn *Peer) postHandshakeStats(f func(*ConnStats)) {
    +	t := cn.t
    +	f(&t.stats)
    +	f(&t.cl.connStats)
    +}
    +
    +// All ConnStats that include this connection. Some objects are not known
    +// until the handshake is complete, after which it's expected to reconcile the
    +// differences.
    +func (cn *Peer) allStats(f func(*ConnStats)) {
    +	f(&cn._stats)
    +	if cn.reconciledHandshakeStats {
    +		cn.postHandshakeStats(f)
    +	}
    +}
    +
    +func (cn *Peer) readBytes(n int64) {
    +	cn.allStats(add(n, func(cs *ConnStats) *Count { return &cs.BytesRead }))
    +}
    +
    +func (c *Peer) lastHelpful() (ret time.Time) {
    +	ret = c.lastUsefulChunkReceived
    +	if c.t.seeding() && c.lastChunkSent.After(ret) {
    +		ret = c.lastChunkSent
    +	}
    +	return
    +}
    +
    +// Returns whether any part of the chunk would lie outside a piece of the given length.
    +func chunkOverflowsPiece(cs ChunkSpec, pieceLength pp.Integer) bool {
    +	switch {
    +	default:
    +		return false
    +	case cs.Begin+cs.Length > pieceLength:
    +	// Check for integer overflow
    +	case cs.Begin > pp.IntegerMax-cs.Length:
    +	}
    +	return true
    +}
    +
    +func runSafeExtraneous(f func()) {
    +	if true {
    +		go f()
    +	} else {
    +		f()
    +	}
    +}
    +
    +// Returns true if it was valid to reject the request.
    +func (c *Peer) remoteRejectedRequest(r RequestIndex) bool {
    +	if c.deleteRequest(r) {
    +		c.decPeakRequests()
    +	} else if !c.requestState.Cancelled.CheckedRemove(r) {
    +		return false
    +	}
    +	if c.isLowOnRequests() {
    +		c.updateRequests("Peer.remoteRejectedRequest")
    +	}
    +	c.decExpectedChunkReceive(r)
    +	return true
    +}
    +
    +func (c *Peer) decExpectedChunkReceive(r RequestIndex) {
    +	count := c.validReceiveChunks[r]
    +	if count == 1 {
    +		delete(c.validReceiveChunks, r)
    +	} else if count > 1 {
    +		c.validReceiveChunks[r] = count - 1
    +	} else {
    +		panic(r)
    +	}
    +}
    +
    +func (c *Peer) doChunkReadStats(size int64) {
    +	c.allStats(func(cs *ConnStats) { cs.receivedChunk(size) })
    +}
    +
    +// Handle a received chunk from a peer.
    +func (c *Peer) receiveChunk(msg *pp.Message) error {
    +	chunksReceived.Add("total", 1)
    +
    +	ppReq := newRequestFromMessage(msg)
    +	t := c.t
    +	err := t.checkValidReceiveChunk(ppReq)
    +	if err != nil {
    +		err = log.WithLevel(log.Warning, err)
    +		return err
    +	}
    +	req := c.t.requestIndexFromRequest(ppReq)
    +
    +	if c.bannableAddr.Ok {
    +		t.smartBanCache.RecordBlock(c.bannableAddr.Value, req, msg.Piece)
    +	}
    +
    +	if c.peerChoking {
    +		chunksReceived.Add("while choked", 1)
    +	}
    +
    +	if c.validReceiveChunks[req] <= 0 {
    +		chunksReceived.Add("unexpected", 1)
    +		return errors.New("received unexpected chunk")
    +	}
    +	c.decExpectedChunkReceive(req)
    +
    +	if c.peerChoking && c.peerAllowedFast.Contains(pieceIndex(ppReq.Index)) {
    +		chunksReceived.Add("due to allowed fast", 1)
    +	}
    +
    +	// The request needs to be deleted immediately to prevent cancels occurring asynchronously when
    +	// have actually already received the piece, while we have the Client unlocked to write the data
    +	// out.
    +	intended := false
    +	{
    +		if c.requestState.Requests.Contains(req) {
    +			for _, f := range c.callbacks.ReceivedRequested {
    +				f(PeerMessageEvent{c, msg})
    +			}
    +		}
    +		// Request has been satisfied.
    +		if c.deleteRequest(req) || c.requestState.Cancelled.CheckedRemove(req) {
    +			intended = true
    +			if !c.peerChoking {
    +				c._chunksReceivedWhileExpecting++
    +			}
    +			if c.isLowOnRequests() {
    +				c.updateRequests("Peer.receiveChunk deleted request")
    +			}
    +		} else {
    +			chunksReceived.Add("unintended", 1)
    +		}
    +	}
    +
    +	cl := t.cl
    +
    +	// Do we actually want this chunk?
    +	if t.haveChunk(ppReq) {
    +		// panic(fmt.Sprintf("%+v", ppReq))
    +		chunksReceived.Add("redundant", 1)
    +		c.allStats(add(1, func(cs *ConnStats) *Count { return &cs.ChunksReadWasted }))
    +		return nil
    +	}
    +
    +	piece := &t.pieces[ppReq.Index]
    +
    +	c.allStats(add(1, func(cs *ConnStats) *Count { return &cs.ChunksReadUseful }))
    +	c.allStats(add(int64(len(msg.Piece)), func(cs *ConnStats) *Count { return &cs.BytesReadUsefulData }))
    +	if intended {
    +		c.piecesReceivedSinceLastRequestUpdate++
    +		c.allStats(add(int64(len(msg.Piece)), func(cs *ConnStats) *Count { return &cs.BytesReadUsefulIntendedData }))
    +	}
    +	for _, f := range c.t.cl.config.Callbacks.ReceivedUsefulData {
    +		f(ReceivedUsefulDataEvent{c, msg})
    +	}
    +	c.lastUsefulChunkReceived = time.Now()
    +
    +	// Need to record that it hasn't been written yet, before we attempt to do
    +	// anything with it.
    +	piece.incrementPendingWrites()
    +	// Record that we have the chunk, so we aren't trying to download it while
    +	// waiting for it to be written to storage.
    +	piece.unpendChunkIndex(chunkIndexFromChunkSpec(ppReq.ChunkSpec, t.chunkSize))
    +
    +	// Cancel pending requests for this chunk from *other* peers.
    +	if p := t.requestingPeer(req); p != nil {
    +		if p == c {
    +			panic("should not be pending request from conn that just received it")
    +		}
    +		p.cancel(req)
    +	}
    +
    +	err = func() error {
    +		cl.unlock()
    +		defer cl.lock()
    +		concurrentChunkWrites.Add(1)
    +		defer concurrentChunkWrites.Add(-1)
    +		// Write the chunk out. Note that the upper bound on chunk writing concurrency will be the
    +		// number of connections. We write inline with receiving the chunk (with this lock dance),
    +		// because we want to handle errors synchronously and I haven't thought of a nice way to
    +		// defer any concurrency to the storage and have that notify the client of errors. TODO: Do
    +		// that instead.
    +		return t.writeChunk(int(msg.Index), int64(msg.Begin), msg.Piece)
    +	}()
    +
    +	piece.decrementPendingWrites()
    +
    +	if err != nil {
    +		c.logger.WithDefaultLevel(log.Error).Printf("writing received chunk %v: %v", req, err)
    +		t.pendRequest(req)
    +		// Necessary to pass TestReceiveChunkStorageFailureSeederFastExtensionDisabled. I think a
    +		// request update runs while we're writing the chunk that just failed. Then we never do a
    +		// fresh update after pending the failed request.
    +		c.updateRequests("Peer.receiveChunk error writing chunk")
    +		t.onWriteChunkErr(err)
    +		return nil
    +	}
    +
    +	c.onDirtiedPiece(pieceIndex(ppReq.Index))
    +
    +	// We need to ensure the piece is only queued once, so only the last chunk writer gets this job.
    +	if t.pieceAllDirty(pieceIndex(ppReq.Index)) && piece.pendingWrites == 0 {
    +		t.queuePieceCheck(pieceIndex(ppReq.Index))
    +		// We don't pend all chunks here anymore because we don't want code dependent on the dirty
    +		// chunk status (such as the haveChunk call above) to have to check all the various other
    +		// piece states like queued for hash, hashing etc. This does mean that we need to be sure
    +		// that chunk pieces are pended at an appropriate time later however.
    +	}
    +
    +	cl.event.Broadcast()
    +	// We do this because we've written a chunk, and may change PieceState.Partial.
    +	t.publishPieceChange(pieceIndex(ppReq.Index))
    +
    +	return nil
    +}
    +
    +func (c *Peer) onDirtiedPiece(piece pieceIndex) {
    +	if c.peerTouchedPieces == nil {
    +		c.peerTouchedPieces = make(map[pieceIndex]struct{})
    +	}
    +	c.peerTouchedPieces[piece] = struct{}{}
    +	ds := &c.t.pieces[piece].dirtiers
    +	if *ds == nil {
    +		*ds = make(map[*Peer]struct{})
    +	}
    +	(*ds)[c] = struct{}{}
    +}
    +
    +func (cn *Peer) netGoodPiecesDirtied() int64 {
    +	return cn._stats.PiecesDirtiedGood.Int64() - cn._stats.PiecesDirtiedBad.Int64()
    +}
    +
    +func (c *Peer) peerHasWantedPieces() bool {
    +	if all, _ := c.peerHasAllPieces(); all {
    +		return !c.t.haveAllPieces() && !c.t._pendingPieces.IsEmpty()
    +	}
    +	if !c.t.haveInfo() {
    +		return !c.peerPieces().IsEmpty()
    +	}
    +	return c.peerPieces().Intersects(&c.t._pendingPieces)
    +}
    +
    +// Returns true if an outstanding request is removed. Cancelled requests should be handled
    +// separately.
    +func (c *Peer) deleteRequest(r RequestIndex) bool {
    +	if !c.requestState.Requests.CheckedRemove(r) {
    +		return false
    +	}
    +	for _, f := range c.callbacks.DeletedRequest {
    +		f(PeerRequestEvent{c, c.t.requestIndexToRequest(r)})
    +	}
    +	c.updateExpectingChunks()
    +	if c.t.requestingPeer(r) != c {
    +		panic("only one peer should have a given request at a time")
    +	}
    +	delete(c.t.requestState, r)
    +	// c.t.iterPeers(func(p *Peer) {
    +	// 	if p.isLowOnRequests() {
    +	// 		p.updateRequests("Peer.deleteRequest")
    +	// 	}
    +	// })
    +	return true
    +}
    +
    +func (c *Peer) deleteAllRequests(reason string) {
    +	if c.requestState.Requests.IsEmpty() {
    +		return
    +	}
    +	c.requestState.Requests.IterateSnapshot(func(x RequestIndex) bool {
    +		if !c.deleteRequest(x) {
    +			panic("request should exist")
    +		}
    +		return true
    +	})
    +	c.assertNoRequests()
    +	c.t.iterPeers(func(p *Peer) {
    +		if p.isLowOnRequests() {
    +			p.updateRequests(reason)
    +		}
    +	})
    +	return
    +}
    +
    +func (c *Peer) assertNoRequests() {
    +	if !c.requestState.Requests.IsEmpty() {
    +		panic(c.requestState.Requests.GetCardinality())
    +	}
    +}
    +
    +func (c *Peer) cancelAllRequests() {
    +	c.requestState.Requests.IterateSnapshot(func(x RequestIndex) bool {
    +		c.cancel(x)
    +		return true
    +	})
    +	c.assertNoRequests()
    +	return
    +}
    +
    +func (c *Peer) peerPriority() (peerPriority, error) {
    +	return bep40Priority(c.remoteIpPort(), c.localPublicAddr)
    +}
    +
    +func (c *Peer) remoteIp() net.IP {
    +	host, _, _ := net.SplitHostPort(c.RemoteAddr.String())
    +	return net.ParseIP(host)
    +}
    +
    +func (c *Peer) remoteIpPort() IpPort {
    +	ipa, _ := tryIpPortFromNetAddr(c.RemoteAddr)
    +	return IpPort{ipa.IP, uint16(ipa.Port)}
    +}
    +
    +func (c *Peer) trust() connectionTrust {
    +	return connectionTrust{c.trusted, c.netGoodPiecesDirtied()}
    +}
    +
    +type connectionTrust struct {
    +	Implicit            bool
    +	NetGoodPiecesDirted int64
    +}
    +
    +func (l connectionTrust) Less(r connectionTrust) bool {
    +	return multiless.New().Bool(l.Implicit, r.Implicit).Int64(l.NetGoodPiecesDirted, r.NetGoodPiecesDirted).Less()
    +}
    +
    +// Returns a new Bitmap that includes bits for all pieces the peer could have based on their claims.
    +func (cn *Peer) newPeerPieces() *roaring.Bitmap {
    +	// TODO: Can we use copy on write?
    +	ret := cn.peerPieces().Clone()
    +	if all, _ := cn.peerHasAllPieces(); all {
    +		if cn.t.haveInfo() {
    +			ret.AddRange(0, bitmap.BitRange(cn.t.numPieces()))
    +		} else {
    +			ret.AddRange(0, bitmap.ToEnd)
    +		}
    +	}
    +	return ret
    +}
    +
    +func (cn *Peer) stats() *ConnStats {
    +	return &cn._stats
    +}
    +
    +func (p *Peer) TryAsPeerConn() (*PeerConn, bool) {
    +	pc, ok := p.peerImpl.(*PeerConn)
    +	return pc, ok
    +}
    +
    +func (p *Peer) uncancelledRequests() uint64 {
    +	return p.requestState.Requests.GetCardinality()
    +}
    +
    +type peerLocalPublicAddr = IpPort
    +
    +func (p *Peer) isLowOnRequests() bool {
    +	return p.requestState.Requests.IsEmpty() && p.requestState.Cancelled.IsEmpty()
    +}
    +
    +func (p *Peer) decPeakRequests() {
    +	// // This can occur when peak requests are altered by the update request timer to be lower than
    +	// // the actual number of outstanding requests. Let's let it go negative and see what happens. I
    +	// // wonder what happens if maxRequests is not signed.
    +	// if p.peakRequests < 1 {
    +	// 	panic(p.peakRequests)
    +	// }
    +	p.peakRequests--
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_info.go b/deps/github.com/anacrolix/torrent/peer_info.go
    new file mode 100644
    index 0000000..e7b1b7c
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_info.go
    @@ -0,0 +1,44 @@
    +package torrent
    +
    +import (
    +	"github.com/anacrolix/dht/v2/krpc"
    +
    +	"github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +// Peer connection info, handed about publicly.
    +type PeerInfo struct {
    +	Id     [20]byte
    +	Addr   PeerRemoteAddr
    +	Source PeerSource
    +	// Peer is known to support encryption.
    +	SupportsEncryption bool
    +	peer_protocol.PexPeerFlags
    +	// Whether we can ignore poor or bad behaviour from the peer.
    +	Trusted bool
    +}
    +
    +func (me PeerInfo) equal(other PeerInfo) bool {
    +	return me.Id == other.Id &&
    +		me.Addr.String() == other.Addr.String() &&
    +		me.Source == other.Source &&
    +		me.SupportsEncryption == other.SupportsEncryption &&
    +		me.PexPeerFlags == other.PexPeerFlags &&
    +		me.Trusted == other.Trusted
    +}
    +
    +// Generate PeerInfo from peer exchange
    +func (me *PeerInfo) FromPex(na krpc.NodeAddr, fs peer_protocol.PexPeerFlags) {
    +	me.Addr = ipPortAddr{append([]byte(nil), na.IP...), na.Port}
    +	me.Source = PeerSourcePex
    +	// If they prefer encryption, they must support it.
    +	if fs.Get(peer_protocol.PexPrefersEncryption) {
    +		me.SupportsEncryption = true
    +	}
    +	me.PexPeerFlags = fs
    +}
    +
    +func (me PeerInfo) addr() IpPort {
    +	ipPort, _ := tryIpPortFromNetAddr(me.Addr)
    +	return IpPort{ipPort.IP, uint16(ipPort.Port)}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_infos.go b/deps/github.com/anacrolix/torrent/peer_infos.go
    new file mode 100644
    index 0000000..f3da64e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_infos.go
    @@ -0,0 +1,35 @@
    +package torrent
    +
    +import (
    +	"github.com/anacrolix/dht/v2/krpc"
    +
    +	"github.com/anacrolix/torrent/peer_protocol"
    +	"github.com/anacrolix/torrent/tracker"
    +)
    +
    +// Helper-type used to bulk-manage PeerInfos.
    +type peerInfos []PeerInfo
    +
    +func (me *peerInfos) AppendFromPex(nas []krpc.NodeAddr, fs []peer_protocol.PexPeerFlags) {
    +	for i, na := range nas {
    +		var p PeerInfo
    +		var f peer_protocol.PexPeerFlags
    +		if i < len(fs) {
    +			f = fs[i]
    +		}
    +		p.FromPex(na, f)
    +		*me = append(*me, p)
    +	}
    +}
    +
    +func (ret peerInfos) AppendFromTracker(ps []tracker.Peer) peerInfos {
    +	for _, p := range ps {
    +		_p := PeerInfo{
    +			Addr:   ipPortAddr{p.IP, p.Port},
    +			Source: PeerSourceTracker,
    +		}
    +		copy(_p.Id[:], p.ID)
    +		ret = append(ret, _p)
    +	}
    +	return ret
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/compactip.go b/deps/github.com/anacrolix/torrent/peer_protocol/compactip.go
    new file mode 100644
    index 0000000..7dddc53
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/compactip.go
    @@ -0,0 +1,22 @@
    +package peer_protocol
    +
    +import (
    +	"net"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +)
    +
    +// Marshals to the smallest compact byte representation.
    +type CompactIp net.IP
    +
    +var _ bencode.Marshaler = CompactIp{}
    +
    +func (me CompactIp) MarshalBencode() ([]byte, error) {
    +	return bencode.Marshal(func() []byte {
    +		if ip4 := net.IP(me).To4(); ip4 != nil {
    +			return ip4
    +		} else {
    +			return me
    +		}
    +	}())
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/decoder.go b/deps/github.com/anacrolix/torrent/peer_protocol/decoder.go
    new file mode 100644
    index 0000000..9dfe125
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/decoder.go
    @@ -0,0 +1,137 @@
    +package peer_protocol
    +
    +import (
    +	"bufio"
    +	"encoding/binary"
    +	"fmt"
    +	"io"
    +	"sync"
    +
    +	"github.com/pkg/errors"
    +)
    +
    +type Decoder struct {
    +	R *bufio.Reader
    +	// This must return *[]byte where the slices can fit data for piece messages. I think we store
    +	// *[]byte in the pool to avoid an extra allocation every time we put the slice back into the
    +	// pool. The chunk size should not change for the life of the decoder.
    +	Pool      *sync.Pool
    +	MaxLength Integer // TODO: Should this include the length header or not?
    +}
    +
    +// io.EOF is returned if the source terminates cleanly on a message boundary.
    +func (d *Decoder) Decode(msg *Message) (err error) {
    +	var length Integer
    +	err = length.Read(d.R)
    +	if err != nil {
    +		return fmt.Errorf("reading message length: %w", err)
    +	}
    +	if length > d.MaxLength {
    +		return errors.New("message too long")
    +	}
    +	if length == 0 {
    +		msg.Keepalive = true
    +		return
    +	}
    +	r := d.R
    +	readByte := func() (byte, error) {
    +		length--
    +		return d.R.ReadByte()
    +	}
    +	// From this point onwards, EOF is unexpected
    +	defer func() {
    +		if err == io.EOF {
    +			err = io.ErrUnexpectedEOF
    +		}
    +	}()
    +	c, err := readByte()
    +	if err != nil {
    +		return
    +	}
    +	msg.Type = MessageType(c)
    +	// Can return directly in cases when err is not nil, or length is known to be zero.
    +	switch msg.Type {
    +	case Choke, Unchoke, Interested, NotInterested, HaveAll, HaveNone:
    +	case Have, AllowedFast, Suggest:
    +		length -= 4
    +		err = msg.Index.Read(r)
    +	case Request, Cancel, Reject:
    +		for _, data := range []*Integer{&msg.Index, &msg.Begin, &msg.Length} {
    +			err = data.Read(r)
    +			if err != nil {
    +				break
    +			}
    +		}
    +		length -= 12
    +	case Bitfield:
    +		b := make([]byte, length)
    +		_, err = io.ReadFull(r, b)
    +		length = 0
    +		msg.Bitfield = unmarshalBitfield(b)
    +		return
    +	case Piece:
    +		for _, pi := range []*Integer{&msg.Index, &msg.Begin} {
    +			err := pi.Read(r)
    +			if err != nil {
    +				return err
    +			}
    +		}
    +		length -= 8
    +		dataLen := int64(length)
    +		if d.Pool == nil {
    +			msg.Piece = make([]byte, dataLen)
    +		} else {
    +			msg.Piece = *d.Pool.Get().(*[]byte)
    +			if int64(cap(msg.Piece)) < dataLen {
    +				return errors.New("piece data longer than expected")
    +			}
    +			msg.Piece = msg.Piece[:dataLen]
    +		}
    +		_, err = io.ReadFull(r, msg.Piece)
    +		length = 0
    +		return
    +	case Extended:
    +		var b byte
    +		b, err = readByte()
    +		if err != nil {
    +			break
    +		}
    +		msg.ExtendedID = ExtensionNumber(b)
    +		msg.ExtendedPayload = make([]byte, length)
    +		_, err = io.ReadFull(r, msg.ExtendedPayload)
    +		length = 0
    +		return
    +	case Port:
    +		err = binary.Read(r, binary.BigEndian, &msg.Port)
    +		length -= 2
    +	default:
    +		err = fmt.Errorf("unknown message type %#v", c)
    +	}
    +	if err == nil && length != 0 {
    +		err = fmt.Errorf("%v unused bytes in message type %v", length, msg.Type)
    +	}
    +	return
    +}
    +
    +func readByte(r io.Reader) (b byte, err error) {
    +	var arr [1]byte
    +	n, err := r.Read(arr[:])
    +	b = arr[0]
    +	if n == 1 {
    +		err = nil
    +		return
    +	}
    +	if err == nil {
    +		panic(err)
    +	}
    +	return
    +}
    +
    +func unmarshalBitfield(b []byte) (bf []bool) {
    +	for _, c := range b {
    +		for i := 7; i >= 0; i-- {
    +			bf = append(bf, (c>>uint(i))&1 == 1)
    +		}
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/decoder_test.go b/deps/github.com/anacrolix/torrent/peer_protocol/decoder_test.go
    new file mode 100644
    index 0000000..39b54c1
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/decoder_test.go
    @@ -0,0 +1,93 @@
    +package peer_protocol
    +
    +import (
    +	"bufio"
    +	"bytes"
    +	"io"
    +	"sync"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func BenchmarkDecodePieces(t *testing.B) {
    +	const pieceLen = 1 << 14
    +	inputMsg := Message{
    +		Type:  Piece,
    +		Index: 0,
    +		Begin: 1,
    +		Piece: make([]byte, pieceLen),
    +	}
    +	b := inputMsg.MustMarshalBinary()
    +	t.SetBytes(int64(len(b)))
    +	var r bytes.Reader
    +	// Try to somewhat emulate what torrent.Client would do. But the goal is to get decoding as fast
    +	// as possible and let consumers apply their own adjustments.
    +	d := Decoder{
    +		R:         bufio.NewReaderSize(&r, 1<<10),
    +		MaxLength: 1 << 18,
    +		Pool: &sync.Pool{
    +			New: func() interface{} {
    +				b := make([]byte, pieceLen)
    +				return &b
    +			},
    +		},
    +	}
    +	c := qt.New(t)
    +	t.ReportAllocs()
    +	t.ResetTimer()
    +	for i := 0; i < t.N; i += 1 {
    +		r.Reset(b)
    +		var msg Message
    +		err := d.Decode(&msg)
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +		// This is very expensive, and should be discovered in tests rather than a benchmark.
    +		if false {
    +			c.Assert(msg, qt.DeepEquals, inputMsg)
    +		}
    +		// WWJD
    +		d.Pool.Put(&msg.Piece)
    +	}
    +}
    +
    +func TestDecodeShortPieceEOF(t *testing.T) {
    +	r, w := io.Pipe()
    +	go func() {
    +		w.Write(Message{Type: Piece, Piece: make([]byte, 1)}.MustMarshalBinary())
    +		w.Close()
    +	}()
    +	d := Decoder{
    +		R:         bufio.NewReader(r),
    +		MaxLength: 1 << 15,
    +		Pool: &sync.Pool{New: func() interface{} {
    +			b := make([]byte, 2)
    +			return &b
    +		}},
    +	}
    +	var m Message
    +	require.NoError(t, d.Decode(&m))
    +	assert.Len(t, m.Piece, 1)
    +	assert.ErrorIs(t, d.Decode(&m), io.EOF)
    +}
    +
    +func TestDecodeOverlongPiece(t *testing.T) {
    +	r, w := io.Pipe()
    +	go func() {
    +		w.Write(Message{Type: Piece, Piece: make([]byte, 3)}.MustMarshalBinary())
    +		w.Close()
    +	}()
    +	d := Decoder{
    +		R:         bufio.NewReader(r),
    +		MaxLength: 1 << 15,
    +		Pool: &sync.Pool{New: func() interface{} {
    +			b := make([]byte, 2)
    +			return &b
    +		}},
    +	}
    +	var m Message
    +	require.Error(t, d.Decode(&m))
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/extended.go b/deps/github.com/anacrolix/torrent/peer_protocol/extended.go
    new file mode 100644
    index 0000000..8bc5181
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/extended.go
    @@ -0,0 +1,40 @@
    +package peer_protocol
    +
    +import (
    +	"net"
    +)
    +
    +// http://www.bittorrent.org/beps/bep_0010.html
    +type (
    +	ExtendedHandshakeMessage struct {
    +		M    map[ExtensionName]ExtensionNumber `bencode:"m"`
    +		V    string                            `bencode:"v,omitempty"`
    +		Reqq int                               `bencode:"reqq,omitempty"`
    +		// The only mention of this I can find is in https://www.bittorrent.org/beps/bep_0011.html
    +		// for bit 0x01.
    +		Encryption bool `bencode:"e"`
    +		// BEP 9
    +		MetadataSize int `bencode:"metadata_size,omitempty"`
    +		// The local client port. It would be redundant for the receiving side of
    +		// a connection to send this.
    +		Port   int       `bencode:"p,omitempty"`
    +		YourIp CompactIp `bencode:"yourip,omitempty"`
    +		Ipv4   CompactIp `bencode:"ipv4,omitempty"`
    +		Ipv6   net.IP    `bencode:"ipv6,omitempty"`
    +	}
    +
    +	ExtensionName   string
    +	ExtensionNumber int
    +)
    +
    +const (
    +	// http://www.bittorrent.org/beps/bep_0011.html
    +	ExtensionNamePex ExtensionName = "ut_pex"
    +
    +	ExtensionDeleteNumber ExtensionNumber = 0
    +)
    +
    +func (me *ExtensionNumber) UnmarshalBinary(b []byte) error {
    +	*me = ExtensionNumber(b[0])
    +	return nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/fuzz_test.go b/deps/github.com/anacrolix/torrent/peer_protocol/fuzz_test.go
    new file mode 100644
    index 0000000..5241504
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/fuzz_test.go
    @@ -0,0 +1,65 @@
    +//go:build go1.18
    +// +build go1.18
    +
    +package peer_protocol
    +
    +import (
    +	"bufio"
    +	"bytes"
    +	"errors"
    +	"io"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +)
    +
    +func FuzzDecoder(f *testing.F) {
    +	f.Add([]byte("\x00\x00\x00\x00"))
    +	f.Add([]byte("\x00\x00\x00\x01\x00"))
    +	f.Add([]byte("\x00\x00\x00\x03\x14\x00"))
    +	f.Add([]byte("\x00\x00\x00\x01\x07"))
    +	f.Fuzz(func(t *testing.T, b []byte) {
    +		t.Logf("%q", b)
    +		c := qt.New(t)
    +		d := Decoder{
    +			R:         bufio.NewReader(bytes.NewReader(b)),
    +			MaxLength: 0x100,
    +		}
    +		var ms []Message
    +		for {
    +			var m Message
    +			err := d.Decode(&m)
    +			t.Log(err)
    +			if errors.Is(err, io.EOF) {
    +				break
    +			}
    +			if err == nil {
    +				c.Assert(m, qt.Not(qt.Equals), Message{})
    +				ms = append(ms, m)
    +				continue
    +			} else {
    +				t.Skip(err)
    +			}
    +		}
    +		var buf bytes.Buffer
    +		for _, m := range ms {
    +			buf.Write(m.MustMarshalBinary())
    +		}
    +		if len(b) == 0 {
    +			c.Assert(buf.Bytes(), qt.HasLen, 0)
    +		} else {
    +			c.Assert(buf.Bytes(), qt.DeepEquals, b)
    +		}
    +	})
    +}
    +
    +func FuzzMessageMarshalBinary(f *testing.F) {
    +	f.Fuzz(func(t *testing.T, b []byte) {
    +		var m Message
    +		if err := m.UnmarshalBinary(b); err != nil {
    +			t.Skip(err)
    +		}
    +		b0 := m.MustMarshalBinary()
    +		qt.Assert(t, b0, qt.DeepEquals, b)
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/handshake.go b/deps/github.com/anacrolix/torrent/peer_protocol/handshake.go
    new file mode 100644
    index 0000000..a6f648c
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/handshake.go
    @@ -0,0 +1,188 @@
    +package peer_protocol
    +
    +import (
    +	"encoding/hex"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"math/bits"
    +	"strconv"
    +	"strings"
    +	"unsafe"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type ExtensionBit uint
    +
    +// https://www.bittorrent.org/beps/bep_0004.html
    +// https://wiki.theory.org/BitTorrentSpecification.html#Reserved_Bytes
    +const (
    +	ExtensionBitDht                          = 0 // http://www.bittorrent.org/beps/bep_0005.html
    +	ExtensionBitFast                         = 2 // http://www.bittorrent.org/beps/bep_0006.html
    +	ExtensionBitV2                           = 7 // "Hybrid torrent legacy to v2 upgrade"
    +	ExtensionBitAzureusExtensionNegotiation1 = 16
    +	ExtensionBitAzureusExtensionNegotiation2 = 17
    +	// LibTorrent Extension Protocol, http://www.bittorrent.org/beps/bep_0010.html
    +	ExtensionBitLtep = 20
    +	// https://wiki.theory.org/BitTorrent_Location-aware_Protocol_1
    +	ExtensionBitLocationAwareProtocol    = 43
    +	ExtensionBitAzureusMessagingProtocol = 63 // https://www.bittorrent.org/beps/bep_0004.html
    +
    +)
    +
    +func handshakeWriter(w io.Writer, bb <-chan []byte, done chan<- error) {
    +	var err error
    +	for b := range bb {
    +		_, err = w.Write(b)
    +		if err != nil {
    +			break
    +		}
    +	}
    +	done <- err
    +}
    +
    +type (
    +	PeerExtensionBits [8]byte
    +)
    +
    +var bitTags = []struct {
    +	bit ExtensionBit
    +	tag string
    +}{
    +	// Ordered by their bit position left to right.
    +	{ExtensionBitAzureusMessagingProtocol, "amp"},
    +	{ExtensionBitLocationAwareProtocol, "loc"},
    +	{ExtensionBitLtep, "ltep"},
    +	{ExtensionBitAzureusExtensionNegotiation2, "azen2"},
    +	{ExtensionBitAzureusExtensionNegotiation1, "azen1"},
    +	{ExtensionBitV2, "v2"},
    +	{ExtensionBitFast, "fast"},
    +	{ExtensionBitDht, "dht"},
    +}
    +
    +func (pex PeerExtensionBits) String() string {
    +	pexHex := hex.EncodeToString(pex[:])
    +	tags := make([]string, 0, len(bitTags)+1)
    +	for _, bitTag := range bitTags {
    +		if pex.GetBit(bitTag.bit) {
    +			tags = append(tags, bitTag.tag)
    +			pex.SetBit(bitTag.bit, false)
    +		}
    +	}
    +	unknownCount := bits.OnesCount64(*(*uint64)((unsafe.Pointer(&pex[0]))))
    +	if unknownCount != 0 {
    +		tags = append(tags, fmt.Sprintf("%v unknown", unknownCount))
    +	}
    +	return fmt.Sprintf("%v (%s)", pexHex, strings.Join(tags, ", "))
    +
    +}
    +
    +func NewPeerExtensionBytes(bits ...ExtensionBit) (ret PeerExtensionBits) {
    +	for _, b := range bits {
    +		ret.SetBit(b, true)
    +	}
    +	return
    +}
    +
    +func (pex PeerExtensionBits) SupportsExtended() bool {
    +	return pex.GetBit(ExtensionBitLtep)
    +}
    +
    +func (pex PeerExtensionBits) SupportsDHT() bool {
    +	return pex.GetBit(ExtensionBitDht)
    +}
    +
    +func (pex PeerExtensionBits) SupportsFast() bool {
    +	return pex.GetBit(ExtensionBitFast)
    +}
    +
    +func (pex *PeerExtensionBits) SetBit(bit ExtensionBit, on bool) {
    +	if on {
    +		pex[7-bit/8] |= 1 << (bit % 8)
    +	} else {
    +		pex[7-bit/8] &^= 1 << (bit % 8)
    +	}
    +}
    +
    +func (pex PeerExtensionBits) GetBit(bit ExtensionBit) bool {
    +	return pex[7-bit/8]&(1<<(bit%8)) != 0
    +}
    +
    +type HandshakeResult struct {
    +	PeerExtensionBits
    +	PeerID [20]byte
    +	metainfo.Hash
    +}
    +
    +// ih is nil if we expect the peer to declare the InfoHash, such as when the peer initiated the
    +// connection. Returns ok if the Handshake was successful, and err if there was an unexpected
    +// condition other than the peer simply abandoning the Handshake.
    +func Handshake(
    +	sock io.ReadWriter, ih *metainfo.Hash, peerID [20]byte, extensions PeerExtensionBits,
    +) (
    +	res HandshakeResult, err error,
    +) {
    +	// Bytes to be sent to the peer. Should never block the sender.
    +	postCh := make(chan []byte, 4)
    +	// A single error value sent when the writer completes.
    +	writeDone := make(chan error, 1)
    +	// Performs writes to the socket and ensures posts don't block.
    +	go handshakeWriter(sock, postCh, writeDone)
    +
    +	defer func() {
    +		close(postCh) // Done writing.
    +		if err != nil {
    +			return
    +		}
    +		// Wait until writes complete before returning from handshake.
    +		err = <-writeDone
    +		if err != nil {
    +			err = fmt.Errorf("error writing: %w", err)
    +		}
    +	}()
    +
    +	post := func(bb []byte) {
    +		select {
    +		case postCh <- bb:
    +		default:
    +			panic("mustn't block while posting")
    +		}
    +	}
    +
    +	post([]byte(Protocol))
    +	post(extensions[:])
    +	if ih != nil { // We already know what we want.
    +		post(ih[:])
    +		post(peerID[:])
    +	}
    +	var b [68]byte
    +	_, err = io.ReadFull(sock, b[:68])
    +	if err != nil {
    +		return res, fmt.Errorf("while reading: %w", err)
    +	}
    +	if string(b[:20]) != Protocol {
    +		return res, errors.New("unexpected protocol string")
    +	}
    +
    +	copyExact := func(dst, src []byte) {
    +		if dstLen, srcLen := uint64(len(dst)), uint64(len(src)); dstLen != srcLen {
    +			panic("dst len " + strconv.FormatUint(dstLen, 10) + " != src len " + strconv.FormatUint(srcLen, 10))
    +		}
    +		copy(dst, src)
    +	}
    +	copyExact(res.PeerExtensionBits[:], b[20:28])
    +	copyExact(res.Hash[:], b[28:48])
    +	copyExact(res.PeerID[:], b[48:68])
    +	// peerExtensions.Add(res.PeerExtensionBits.String(), 1)
    +
    +	// TODO: Maybe we can just drop peers here if we're not interested. This
    +	// could prevent them trying to reconnect, falsely believing there was
    +	// just a problem.
    +	if ih == nil { // We were waiting for the peer to tell us what they wanted.
    +		post(res.Hash[:])
    +		post(peerID[:])
    +	}
    +
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/int.go b/deps/github.com/anacrolix/torrent/peer_protocol/int.go
    new file mode 100644
    index 0000000..ebcf603
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/int.go
    @@ -0,0 +1,50 @@
    +package peer_protocol
    +
    +import (
    +	"encoding/binary"
    +	"io"
    +	"math"
    +
    +	"github.com/pkg/errors"
    +)
    +
    +type (
    +	// An alias for the underlying type of Integer. This is needed for fuzzing.
    +	IntegerKind = uint32
    +	Integer     IntegerKind
    +)
    +
    +const IntegerMax = math.MaxUint32
    +
    +func (i *Integer) UnmarshalBinary(b []byte) error {
    +	if len(b) != 4 {
    +		return errors.New("expected 4 bytes")
    +	}
    +	*i = Integer(binary.BigEndian.Uint32(b))
    +	return nil
    +}
    +
    +func (i *Integer) Read(r io.Reader) error {
    +	var b [4]byte
    +	n, err := io.ReadFull(r, b[:])
    +	if err == nil {
    +		if n != 4 {
    +			panic(n)
    +		}
    +		return i.UnmarshalBinary(b[:])
    +	}
    +	return err
    +}
    +
    +// It's perfectly fine to cast these to an int. TODO: Or is it?
    +func (i Integer) Int() int {
    +	return int(i)
    +}
    +
    +func (i Integer) Uint64() uint64 {
    +	return uint64(i)
    +}
    +
    +func (i Integer) Uint32() uint32 {
    +	return uint32(i)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/messagetype_string.go b/deps/github.com/anacrolix/torrent/peer_protocol/messagetype_string.go
    new file mode 100644
    index 0000000..7be19f4
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/messagetype_string.go
    @@ -0,0 +1,30 @@
    +// Code generated by "stringer -type=MessageType"; DO NOT EDIT.
    +
    +package peer_protocol
    +
    +import "strconv"
    +
    +const (
    +	_MessageType_name_0 = "ChokeUnchokeInterestedNotInterestedHaveBitfieldRequestPieceCancelPort"
    +	_MessageType_name_1 = "SuggestHaveAllHaveNoneRejectAllowedFast"
    +	_MessageType_name_2 = "Extended"
    +)
    +
    +var (
    +	_MessageType_index_0 = [...]uint8{0, 5, 12, 22, 35, 39, 47, 54, 59, 65, 69}
    +	_MessageType_index_1 = [...]uint8{0, 7, 14, 22, 28, 39}
    +)
    +
    +func (i MessageType) String() string {
    +	switch {
    +	case i <= 9:
    +		return _MessageType_name_0[_MessageType_index_0[i]:_MessageType_index_0[i+1]]
    +	case 13 <= i && i <= 17:
    +		i -= 13
    +		return _MessageType_name_1[_MessageType_index_1[i]:_MessageType_index_1[i+1]]
    +	case i == 20:
    +		return _MessageType_name_2
    +	default:
    +		return "MessageType(" + strconv.FormatInt(int64(i), 10) + ")"
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/metadata.go b/deps/github.com/anacrolix/torrent/peer_protocol/metadata.go
    new file mode 100644
    index 0000000..c480091
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/metadata.go
    @@ -0,0 +1,42 @@
    +package peer_protocol
    +
    +import (
    +	"github.com/anacrolix/torrent/bencode"
    +)
    +
    +const (
    +	// http://bittorrent.org/beps/bep_0009.html. Note that there's an
    +	// LT_metadata, but I've never implemented it.
    +	ExtensionNameMetadata = "ut_metadata"
    +)
    +
    +type (
    +	ExtendedMetadataRequestMsg struct {
    +		Piece     int                            `bencode:"piece"`
    +		TotalSize int                            `bencode:"total_size"`
    +		Type      ExtendedMetadataRequestMsgType `bencode:"msg_type"`
    +	}
    +
    +	ExtendedMetadataRequestMsgType int
    +)
    +
    +func MetadataExtensionRequestMsg(peerMetadataExtensionId ExtensionNumber, piece int) Message {
    +	return Message{
    +		Type:       Extended,
    +		ExtendedID: peerMetadataExtensionId,
    +		ExtendedPayload: bencode.MustMarshal(ExtendedMetadataRequestMsg{
    +			Piece: piece,
    +			Type:  RequestMetadataExtensionMsgType,
    +		}),
    +	}
    +}
    +
    +// Returns the expected piece size for this request message. This is needed to determine the offset
    +// into an extension message payload that the request metadata piece data starts.
    +func (me ExtendedMetadataRequestMsg) PieceSize() int {
    +	ret := me.TotalSize - me.Piece*(1<<14)
    +	if ret > 1<<14 {
    +		ret = 1 << 14
    +	}
    +	return ret
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/msg.go b/deps/github.com/anacrolix/torrent/peer_protocol/msg.go
    new file mode 100644
    index 0000000..f1b1f10
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/msg.go
    @@ -0,0 +1,139 @@
    +package peer_protocol
    +
    +import (
    +	"bufio"
    +	"bytes"
    +	"encoding"
    +	"encoding/binary"
    +	"fmt"
    +)
    +
    +// This is a lazy union representing all the possible fields for messages. Go doesn't have ADTs, and
    +// I didn't choose to use type-assertions.
    +type Message struct {
    +	Keepalive            bool
    +	Type                 MessageType
    +	Index, Begin, Length Integer
    +	Piece                []byte
    +	Bitfield             []bool
    +	ExtendedID           ExtensionNumber
    +	ExtendedPayload      []byte
    +	Port                 uint16
    +}
    +
    +var _ interface {
    +	encoding.BinaryUnmarshaler
    +	encoding.BinaryMarshaler
    +} = (*Message)(nil)
    +
    +func MakeCancelMessage(piece, offset, length Integer) Message {
    +	return Message{
    +		Type:   Cancel,
    +		Index:  piece,
    +		Begin:  offset,
    +		Length: length,
    +	}
    +}
    +
    +func (msg Message) RequestSpec() (ret RequestSpec) {
    +	return RequestSpec{
    +		msg.Index,
    +		msg.Begin,
    +		func() Integer {
    +			if msg.Type == Piece {
    +				return Integer(len(msg.Piece))
    +			} else {
    +				return msg.Length
    +			}
    +		}(),
    +	}
    +}
    +
    +func (msg Message) MustMarshalBinary() []byte {
    +	b, err := msg.MarshalBinary()
    +	if err != nil {
    +		panic(err)
    +	}
    +	return b
    +}
    +
    +func (msg Message) MarshalBinary() (data []byte, err error) {
    +	var buf bytes.Buffer
    +	if !msg.Keepalive {
    +		err = buf.WriteByte(byte(msg.Type))
    +		if err != nil {
    +			return
    +		}
    +		switch msg.Type {
    +		case Choke, Unchoke, Interested, NotInterested, HaveAll, HaveNone:
    +		case Have, AllowedFast, Suggest:
    +			err = binary.Write(&buf, binary.BigEndian, msg.Index)
    +		case Request, Cancel, Reject:
    +			for _, i := range []Integer{msg.Index, msg.Begin, msg.Length} {
    +				err = binary.Write(&buf, binary.BigEndian, i)
    +				if err != nil {
    +					break
    +				}
    +			}
    +		case Bitfield:
    +			_, err = buf.Write(marshalBitfield(msg.Bitfield))
    +		case Piece:
    +			for _, i := range []Integer{msg.Index, msg.Begin} {
    +				err = binary.Write(&buf, binary.BigEndian, i)
    +				if err != nil {
    +					return
    +				}
    +			}
    +			n, err := buf.Write(msg.Piece)
    +			if err != nil {
    +				break
    +			}
    +			if n != len(msg.Piece) {
    +				panic(n)
    +			}
    +		case Extended:
    +			err = buf.WriteByte(byte(msg.ExtendedID))
    +			if err != nil {
    +				return
    +			}
    +			_, err = buf.Write(msg.ExtendedPayload)
    +		case Port:
    +			err = binary.Write(&buf, binary.BigEndian, msg.Port)
    +		default:
    +			err = fmt.Errorf("unknown message type: %v", msg.Type)
    +		}
    +	}
    +	data = make([]byte, 4+buf.Len())
    +	binary.BigEndian.PutUint32(data, uint32(buf.Len()))
    +	if buf.Len() != copy(data[4:], buf.Bytes()) {
    +		panic("bad copy")
    +	}
    +	return
    +}
    +
    +func marshalBitfield(bf []bool) (b []byte) {
    +	b = make([]byte, (len(bf)+7)/8)
    +	for i, have := range bf {
    +		if !have {
    +			continue
    +		}
    +		c := b[i/8]
    +		c |= 1 << uint(7-i%8)
    +		b[i/8] = c
    +	}
    +	return
    +}
    +
    +func (me *Message) UnmarshalBinary(b []byte) error {
    +	d := Decoder{
    +		R: bufio.NewReader(bytes.NewReader(b)),
    +	}
    +	err := d.Decode(me)
    +	if err != nil {
    +		return err
    +	}
    +	if d.R.Buffered() != 0 {
    +		return fmt.Errorf("%d trailing bytes", d.R.Buffered())
    +	}
    +	return nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/pex.go b/deps/github.com/anacrolix/torrent/peer_protocol/pex.go
    new file mode 100644
    index 0000000..466548a
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/pex.go
    @@ -0,0 +1,49 @@
    +package peer_protocol
    +
    +import (
    +	"github.com/anacrolix/dht/v2/krpc"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +)
    +
    +type PexMsg struct {
    +	Added       krpc.CompactIPv4NodeAddrs `bencode:"added"`
    +	AddedFlags  []PexPeerFlags            `bencode:"added.f"`
    +	Added6      krpc.CompactIPv6NodeAddrs `bencode:"added6"`
    +	Added6Flags []PexPeerFlags            `bencode:"added6.f"`
    +	Dropped     krpc.CompactIPv4NodeAddrs `bencode:"dropped"`
    +	Dropped6    krpc.CompactIPv6NodeAddrs `bencode:"dropped6"`
    +}
    +
    +func (m *PexMsg) Len() int {
    +	return len(m.Added) + len(m.Added6) + len(m.Dropped) + len(m.Dropped6)
    +}
    +
    +func (m *PexMsg) Message(pexExtendedId ExtensionNumber) Message {
    +	payload := bencode.MustMarshal(m)
    +	return Message{
    +		Type:            Extended,
    +		ExtendedID:      pexExtendedId,
    +		ExtendedPayload: payload,
    +	}
    +}
    +
    +// Unmarshals and returns a PEX message.
    +func LoadPexMsg(b []byte) (ret PexMsg, err error) {
    +	err = bencode.Unmarshal(b, &ret)
    +	return
    +}
    +
    +type PexPeerFlags byte
    +
    +func (me PexPeerFlags) Get(f PexPeerFlags) bool {
    +	return me&f == f
    +}
    +
    +const (
    +	PexPrefersEncryption PexPeerFlags = 1 << iota
    +	PexSeedUploadOnly
    +	PexSupportsUtp
    +	PexHolepunchSupport
    +	PexOutgoingConn
    +)
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/pex_test.go b/deps/github.com/anacrolix/torrent/peer_protocol/pex_test.go
    new file mode 100644
    index 0000000..5e5e96c
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/pex_test.go
    @@ -0,0 +1,64 @@
    +package peer_protocol
    +
    +import (
    +	"bufio"
    +	"bytes"
    +	"net"
    +	"testing"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +)
    +
    +func TestUnmarshalPex(t *testing.T) {
    +	var pem PexMsg
    +	err := bencode.Unmarshal([]byte("d5:added12:\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0ce"), &pem)
    +	require.NoError(t, err)
    +	require.EqualValues(t, 2, len(pem.Added))
    +	require.EqualValues(t, 1286, pem.Added[0].Port)
    +	require.EqualValues(t, 0x100*0xb+0xc, pem.Added[1].Port)
    +}
    +
    +func TestEmptyPexMsg(t *testing.T) {
    +	pm := PexMsg{}
    +	b, err := bencode.Marshal(pm)
    +	t.Logf("%q", b)
    +	require.NoError(t, err)
    +	require.NoError(t, bencode.Unmarshal(b, &pm))
    +}
    +
    +func TestMarshalPexMessage(t *testing.T) {
    +	addr := krpc.NodeAddr{IP: net.IP{127, 0, 0, 1}, Port: 0x55aa}
    +	f := PexPrefersEncryption | PexOutgoingConn
    +	pm := new(PexMsg)
    +	pm.Added = append(pm.Added, addr)
    +	pm.AddedFlags = append(pm.AddedFlags, f)
    +
    +	_, err := bencode.Marshal(pm)
    +	require.NoError(t, err)
    +
    +	pexExtendedId := ExtensionNumber(7)
    +	msg := pm.Message(pexExtendedId)
    +	expected := []byte("\x00\x00\x00\x4c\x14\x07d5:added6:\x7f\x00\x00\x01\x55\xaa7:added.f1:\x116:added60:8:added6.f0:7:dropped0:8:dropped60:e")
    +	b, err := msg.MarshalBinary()
    +	require.NoError(t, err)
    +	require.EqualValues(t, b, expected)
    +
    +	msg = Message{}
    +	dec := Decoder{
    +		R:         bufio.NewReader(bytes.NewReader(b)),
    +		MaxLength: 128,
    +	}
    +	pmOut := PexMsg{}
    +	err = dec.Decode(&msg)
    +	require.NoError(t, err)
    +	require.EqualValues(t, Extended, msg.Type)
    +	require.EqualValues(t, pexExtendedId, msg.ExtendedID)
    +	err = bencode.Unmarshal(msg.ExtendedPayload, &pmOut)
    +	require.NoError(t, err)
    +	require.EqualValues(t, len(pm.Added), len(pmOut.Added))
    +	require.EqualValues(t, pm.Added[0].IP, pmOut.Added[0].IP)
    +	require.EqualValues(t, pm.Added[0].Port, pmOut.Added[0].Port)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/protocol.go b/deps/github.com/anacrolix/torrent/peer_protocol/protocol.go
    new file mode 100644
    index 0000000..bfeb6a0
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/protocol.go
    @@ -0,0 +1,52 @@
    +package peer_protocol
    +
    +const (
    +	Protocol = "\x13BitTorrent protocol"
    +)
    +
    +type MessageType byte
    +
    +//go:generate stringer -type=MessageType
    +
    +func (mt MessageType) FastExtension() bool {
    +	return mt >= Suggest && mt <= AllowedFast
    +}
    +
    +func (mt *MessageType) UnmarshalBinary(b []byte) error {
    +	*mt = MessageType(b[0])
    +	return nil
    +}
    +
    +const (
    +	// BEP 3
    +	Choke         MessageType = 0
    +	Unchoke       MessageType = 1
    +	Interested    MessageType = 2
    +	NotInterested MessageType = 3
    +	Have          MessageType = 4
    +	Bitfield      MessageType = 5
    +	Request       MessageType = 6
    +	Piece         MessageType = 7
    +	Cancel        MessageType = 8
    +
    +	// BEP 5
    +	Port MessageType = 9
    +
    +	// BEP 6 - Fast extension
    +	Suggest     MessageType = 0x0d // 13
    +	HaveAll     MessageType = 0x0e // 14
    +	HaveNone    MessageType = 0x0f // 15
    +	Reject      MessageType = 0x10 // 16
    +	AllowedFast MessageType = 0x11 // 17
    +
    +	// BEP 10
    +	Extended MessageType = 20
    +)
    +
    +const (
    +	HandshakeExtendedID = 0
    +
    +	RequestMetadataExtensionMsgType ExtendedMetadataRequestMsgType = 0
    +	DataMetadataExtensionMsgType    ExtendedMetadataRequestMsgType = 1
    +	RejectMetadataExtensionMsgType  ExtendedMetadataRequestMsgType = 2
    +)
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/protocol_test.go b/deps/github.com/anacrolix/torrent/peer_protocol/protocol_test.go
    new file mode 100644
    index 0000000..df01a1a
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/protocol_test.go
    @@ -0,0 +1,154 @@
    +package peer_protocol
    +
    +import (
    +	"bufio"
    +	"bytes"
    +	"strings"
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +)
    +
    +func TestBinaryReadSliceOfPointers(t *testing.T) {
    +	var msg Message
    +	r := bytes.NewBufferString("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00")
    +	if r.Len() != 12 {
    +		t.Fatalf("expected 12 bytes left, but there %d", r.Len())
    +	}
    +	for _, data := range []*Integer{&msg.Index, &msg.Begin, &msg.Length} {
    +		err := data.Read(r)
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +	}
    +	if r.Len() != 0 {
    +		t.FailNow()
    +	}
    +}
    +
    +func TestConstants(t *testing.T) {
    +	assert.EqualValues(t, 3, NotInterested)
    +	assert.EqualValues(t, 14, HaveAll)
    +}
    +
    +func TestBitfieldEncode(t *testing.T) {
    +	bf := make([]bool, 37)
    +	bf[2] = true
    +	bf[7] = true
    +	bf[32] = true
    +	s := string(marshalBitfield(bf))
    +	const expected = "\x21\x00\x00\x00\x80"
    +	if s != expected {
    +		t.Fatalf("got %#v, expected %#v", s, expected)
    +	}
    +}
    +
    +func TestBitfieldUnmarshal(t *testing.T) {
    +	bf := unmarshalBitfield([]byte("\x81\x06"))
    +	expected := make([]bool, 16)
    +	expected[0] = true
    +	expected[7] = true
    +	expected[13] = true
    +	expected[14] = true
    +	if len(bf) != len(expected) {
    +		t.FailNow()
    +	}
    +	for i := range expected {
    +		if bf[i] != expected[i] {
    +			t.FailNow()
    +		}
    +	}
    +}
    +
    +func TestHaveEncode(t *testing.T) {
    +	actualBytes, err := Message{
    +		Type:  Have,
    +		Index: 42,
    +	}.MarshalBinary()
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	actualString := string(actualBytes)
    +	expected := "\x00\x00\x00\x05\x04\x00\x00\x00\x2a"
    +	if actualString != expected {
    +		t.Fatalf("expected %#v, got %#v", expected, actualString)
    +	}
    +}
    +
    +func TestShortRead(t *testing.T) {
    +	dec := Decoder{
    +		R:         bufio.NewReader(bytes.NewBufferString("\x00\x00\x00\x02\x00!")),
    +		MaxLength: 2,
    +	}
    +	msg := new(Message)
    +	err := dec.Decode(msg)
    +	if !strings.Contains(err.Error(), "1 unused bytes in message type Choke") {
    +		t.Fatal(err)
    +	}
    +}
    +
    +func TestUnexpectedEOF(t *testing.T) {
    +	msg := new(Message)
    +	for _, stream := range []string{
    +		"\x00\x00\x00",     // Header truncated.
    +		"\x00\x00\x00\x01", // Expecting 1 more byte.
    +		// Request with wrong length, and too short anyway.
    +		"\x00\x00\x00\x06\x06\x00\x00\x00\x00\x00",
    +		// Request truncated.
    +		"\x00\x00\x00\x0b\x06\x00\x00\x00\x00\x00",
    +	} {
    +		dec := Decoder{
    +			R:         bufio.NewReader(bytes.NewBufferString(stream)),
    +			MaxLength: 42,
    +		}
    +		err := dec.Decode(msg)
    +		if err == nil {
    +			t.Fatalf("expected an error decoding %q", stream)
    +		}
    +	}
    +}
    +
    +func TestMarshalKeepalive(t *testing.T) {
    +	b, err := (Message{
    +		Keepalive: true,
    +	}).MarshalBinary()
    +	if err != nil {
    +		t.Fatalf("error marshalling keepalive: %s", err)
    +	}
    +	bs := string(b)
    +	const expected = "\x00\x00\x00\x00"
    +	if bs != expected {
    +		t.Fatalf("marshalled keepalive is %q, expected %q", bs, expected)
    +	}
    +}
    +
    +func TestMarshalPortMsg(t *testing.T) {
    +	b, err := (Message{
    +		Type: Port,
    +		Port: 0xaabb,
    +	}).MarshalBinary()
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	if string(b) != "\x00\x00\x00\x03\x09\xaa\xbb" {
    +		t.FailNow()
    +	}
    +}
    +
    +func TestUnmarshalPortMsg(t *testing.T) {
    +	var m Message
    +	d := Decoder{
    +		R:         bufio.NewReader(bytes.NewBufferString("\x00\x00\x00\x03\x09\xaa\xbb")),
    +		MaxLength: 8,
    +	}
    +	err := d.Decode(&m)
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	if m.Type != Port {
    +		t.FailNow()
    +	}
    +	if m.Port != 0xaabb {
    +		t.FailNow()
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/reqspec.go b/deps/github.com/anacrolix/torrent/peer_protocol/reqspec.go
    new file mode 100644
    index 0000000..f9989a2
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/reqspec.go
    @@ -0,0 +1,11 @@
    +package peer_protocol
    +
    +import "fmt"
    +
    +type RequestSpec struct {
    +	Index, Begin, Length Integer
    +}
    +
    +func (me RequestSpec) String() string {
    +	return fmt.Sprintf("{%d %d %d}", me.Index, me.Begin, me.Length)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/18f327bd85f3ab06 b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/18f327bd85f3ab06
    new file mode 100644
    index 0000000..d214fc2
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/18f327bd85f3ab06
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("\x00\x00\x000\a")
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/252f96643f6de0fc b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/252f96643f6de0fc
    new file mode 100644
    index 0000000..2d3ac2e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/252f96643f6de0fc
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("\x00\x00\x000\a00000000")
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/44a1b6410e7ce227 b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/44a1b6410e7ce227
    new file mode 100644
    index 0000000..a6bf562
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/44a1b6410e7ce227
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("\x00\x00\x00\x05\x110000")
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/52452abe5ed3cb64 b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/52452abe5ed3cb64
    new file mode 100644
    index 0000000..6109993
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/52452abe5ed3cb64
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("\x00\x00\x000\x05")
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/9d2ec002df4eda28 b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/9d2ec002df4eda28
    new file mode 100644
    index 0000000..6345fd4
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/9d2ec002df4eda28
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("\x00\x00\x003\a\x17\b\x92\xf3\x02\xd5\x1896%~\xd2Q\x84b\x18")
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/aceaaae6cd039fb5 b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/aceaaae6cd039fb5
    new file mode 100644
    index 0000000..3a76846
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/aceaaae6cd039fb5
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("\x00\x00\x00\x01\x10")
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/eb13c84d13ebb034 b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/eb13c84d13ebb034
    new file mode 100644
    index 0000000..89e0e61
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/testdata/fuzz/FuzzDecoder/eb13c84d13ebb034
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("\x00\x00\x000\x14")
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/err-code.go b/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/err-code.go
    new file mode 100644
    index 0000000..7cc61db
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/err-code.go
    @@ -0,0 +1,31 @@
    +package utHolepunch
    +
    +import (
    +	"fmt"
    +)
    +
    +type ErrCode uint32
    +
    +var _ error = ErrCode(0)
    +
    +const (
    +	NoSuchPeer ErrCode = iota + 1
    +	NotConnected
    +	NoSupport
    +	NoSelf
    +)
    +
    +func (ec ErrCode) Error() string {
    +	switch ec {
    +	case NoSuchPeer:
    +		return "target endpoint is invalid"
    +	case NotConnected:
    +		return "the relaying peer is not connected to the target peer"
    +	case NoSupport:
    +		return "the target peer does not support the holepunch extension"
    +	case NoSelf:
    +		return "the target endpoint belongs to the relaying peer"
    +	default:
    +		return fmt.Sprintf("error code %d", ec)
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/err-code_test.go b/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/err-code_test.go
    new file mode 100644
    index 0000000..4553810
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/err-code_test.go
    @@ -0,0 +1,10 @@
    +package utHolepunch
    +
    +import (
    +	"math/rand"
    +	"testing"
    +)
    +
    +func TestUnknownErrCodeError(t *testing.T) {
    +	ErrCode(rand.Uint32()).Error()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/ut-holepunch.go b/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/ut-holepunch.go
    new file mode 100644
    index 0000000..3051fc0
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/ut-holepunch.go
    @@ -0,0 +1,97 @@
    +package utHolepunch
    +
    +import (
    +	"bytes"
    +	"encoding/binary"
    +	"fmt"
    +	"net/netip"
    +)
    +
    +const ExtensionName = "ut_holepunch"
    +
    +type (
    +	Msg struct {
    +		MsgType  MsgType
    +		AddrPort netip.AddrPort
    +		ErrCode  ErrCode
    +	}
    +	MsgType  byte
    +	AddrType byte
    +)
    +
    +const (
    +	Rendezvous MsgType = iota
    +	Connect
    +	Error
    +)
    +
    +func (me MsgType) String() string {
    +	switch me {
    +	case Rendezvous:
    +		return "rendezvous"
    +	case Connect:
    +		return "connect"
    +	case Error:
    +		return "error"
    +	default:
    +		return fmt.Sprintf("unknown %d", me)
    +	}
    +}
    +
    +const (
    +	Ipv4 AddrType = iota
    +	Ipv6 AddrType = iota
    +)
    +
    +func (m *Msg) UnmarshalBinary(b []byte) error {
    +	if len(b) < 12 {
    +		return fmt.Errorf("buffer too small to be valid")
    +	}
    +	m.MsgType = MsgType(b[0])
    +	b = b[1:]
    +	addrType := AddrType(b[0])
    +	b = b[1:]
    +	var addr netip.Addr
    +	switch addrType {
    +	case Ipv4:
    +		addr = netip.AddrFrom4(*(*[4]byte)(b[:4]))
    +		b = b[4:]
    +	case Ipv6:
    +		if len(b) < 22 {
    +			return fmt.Errorf("not enough bytes")
    +		}
    +		addr = netip.AddrFrom16(*(*[16]byte)(b[:16]))
    +		b = b[16:]
    +	default:
    +		return fmt.Errorf("unhandled addr type value %v", addrType)
    +	}
    +	port := binary.BigEndian.Uint16(b[:])
    +	b = b[2:]
    +	m.AddrPort = netip.AddrPortFrom(addr, port)
    +	m.ErrCode = ErrCode(binary.BigEndian.Uint32(b[:]))
    +	b = b[4:]
    +	if len(b) != 0 {
    +		return fmt.Errorf("%v trailing unused bytes", len(b))
    +	}
    +	return nil
    +}
    +
    +func (m *Msg) MarshalBinary() (_ []byte, err error) {
    +	var buf bytes.Buffer
    +	buf.Grow(24)
    +	buf.WriteByte(byte(m.MsgType))
    +	addr := m.AddrPort.Addr()
    +	switch {
    +	case addr.Is4():
    +		buf.WriteByte(byte(Ipv4))
    +	case addr.Is6():
    +		buf.WriteByte(byte(Ipv6))
    +	default:
    +		err = fmt.Errorf("unhandled addr type: %v", addr)
    +		return
    +	}
    +	buf.Write(addr.AsSlice())
    +	binary.Write(&buf, binary.BigEndian, m.AddrPort.Port())
    +	binary.Write(&buf, binary.BigEndian, m.ErrCode)
    +	return buf.Bytes(), nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/ut-holepunch_test.go b/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/ut-holepunch_test.go
    new file mode 100644
    index 0000000..7221e1f
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peer_protocol/ut-holepunch/ut-holepunch_test.go
    @@ -0,0 +1,63 @@
    +package utHolepunch
    +
    +import (
    +	"bytes"
    +	"net/netip"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +)
    +
    +var exampleMsgs = []Msg{
    +	{
    +		MsgType:  Rendezvous,
    +		AddrPort: netip.MustParseAddrPort("[1234::1]:42069"),
    +		ErrCode:  16777216,
    +	},
    +	{
    +		MsgType:  Connect,
    +		AddrPort: netip.MustParseAddrPort("1.2.3.4:42069"),
    +		ErrCode:  16777216,
    +	},
    +}
    +
    +func TestUnmarshalMsg(t *testing.T) {
    +	c := qt.New(t)
    +	for _, m := range exampleMsgs {
    +		b, err := m.MarshalBinary()
    +		c.Assert(err, qt.IsNil)
    +		expectedLen := 24
    +		if m.AddrPort.Addr().Is4() {
    +			expectedLen = 12
    +		}
    +		c.Check(b, qt.HasLen, expectedLen)
    +		var um Msg
    +		err = um.UnmarshalBinary(b)
    +		c.Assert(err, qt.IsNil)
    +		c.Check(um, qt.Equals, m)
    +	}
    +}
    +
    +func FuzzMsg(f *testing.F) {
    +	for _, m := range exampleMsgs {
    +		emb, err := m.MarshalBinary()
    +		if err != nil {
    +			f.Fatal(err)
    +		}
    +		f.Add(emb)
    +	}
    +	f.Fuzz(func(t *testing.T, b []byte) {
    +		var m Msg
    +		err := m.UnmarshalBinary(b)
    +		if err != nil {
    +			t.SkipNow()
    +		}
    +		mb, err := m.MarshalBinary()
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +		if !bytes.Equal(b, mb) {
    +			t.FailNow()
    +		}
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peerconn.go b/deps/github.com/anacrolix/torrent/peerconn.go
    new file mode 100644
    index 0000000..ee47dd1
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peerconn.go
    @@ -0,0 +1,1146 @@
    +package torrent
    +
    +import (
    +	"bufio"
    +	"bytes"
    +	"context"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"math/rand"
    +	"net"
    +	"net/netip"
    +	"strconv"
    +	"strings"
    +	"sync/atomic"
    +	"time"
    +
    +	"github.com/RoaringBitmap/roaring"
    +	"github.com/anacrolix/generics"
    +	. "github.com/anacrolix/generics"
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/v2/bitmap"
    +	"github.com/anacrolix/multiless"
    +	"golang.org/x/exp/maps"
    +	"golang.org/x/time/rate"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/internal/alloclim"
    +	"github.com/anacrolix/torrent/metainfo"
    +	"github.com/anacrolix/torrent/mse"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +	utHolepunch "github.com/anacrolix/torrent/peer_protocol/ut-holepunch"
    +)
    +
    +// Maintains the state of a BitTorrent-protocol based connection with a peer.
    +type PeerConn struct {
    +	Peer
    +
    +	// A string that should identify the PeerConn's net.Conn endpoints. The net.Conn could
    +	// be wrapping WebRTC, uTP, or TCP etc. Used in writing the conn status for peers.
    +	connString string
    +
    +	// See BEP 3 etc.
    +	PeerID             PeerID
    +	PeerExtensionBytes pp.PeerExtensionBits
    +	PeerListenPort     int
    +
    +	// The actual Conn, used for closing, and setting socket options. Do not use methods on this
    +	// while holding any mutexes.
    +	conn net.Conn
    +	// The Reader and Writer for this Conn, with hooks installed for stats,
    +	// limiting, deadlines etc.
    +	w io.Writer
    +	r io.Reader
    +
    +	messageWriter peerConnMsgWriter
    +
    +	PeerExtensionIDs map[pp.ExtensionName]pp.ExtensionNumber
    +	PeerClientName   atomic.Value
    +	uploadTimer      *time.Timer
    +	pex              pexConnState
    +
    +	// The pieces the peer has claimed to have.
    +	_peerPieces roaring.Bitmap
    +	// The peer has everything. This can occur due to a special message, when
    +	// we may not even know the number of pieces in the torrent yet.
    +	peerSentHaveAll bool
    +
    +	peerRequestDataAllocLimiter alloclim.Limiter
    +
    +	outstandingHolepunchingRendezvous map[netip.AddrPort]struct{}
    +}
    +
    +func (cn *PeerConn) pexStatus() string {
    +	if !cn.bitExtensionEnabled(pp.ExtensionBitLtep) {
    +		return "extended protocol disabled"
    +	}
    +	if cn.PeerExtensionIDs == nil {
    +		return "pending extended handshake"
    +	}
    +	if !cn.supportsExtension(pp.ExtensionNamePex) {
    +		return "unsupported"
    +	}
    +	if true {
    +		return fmt.Sprintf(
    +			"%v conns, %v unsent events",
    +			len(cn.pex.remoteLiveConns),
    +			cn.pex.numPending(),
    +		)
    +	} else {
    +		// This alternative branch prints out the remote live conn addresses.
    +		return fmt.Sprintf(
    +			"%v conns, %v unsent events",
    +			strings.Join(generics.SliceMap(
    +				maps.Keys(cn.pex.remoteLiveConns),
    +				func(from netip.AddrPort) string {
    +					return from.String()
    +				}), ","),
    +			cn.pex.numPending(),
    +		)
    +	}
    +}
    +
    +func (cn *PeerConn) peerImplStatusLines() []string {
    +	return []string{
    +		cn.connString,
    +		fmt.Sprintf("peer id: %+q", cn.PeerID),
    +		fmt.Sprintf("extensions: %v", cn.PeerExtensionBytes),
    +		fmt.Sprintf("ltep extensions: %v", cn.PeerExtensionIDs),
    +		fmt.Sprintf("pex: %s", cn.pexStatus()),
    +	}
    +}
    +
    +// Returns true if the connection is over IPv6.
    +func (cn *PeerConn) ipv6() bool {
    +	ip := cn.remoteIp()
    +	if ip.To4() != nil {
    +		return false
    +	}
    +	return len(ip) == net.IPv6len
    +}
    +
    +// Returns true the if the dialer/initiator has the higher client peer ID. See
    +// https://github.com/arvidn/libtorrent/blame/272828e1cc37b042dfbbafa539222d8533e99755/src/bt_peer_connection.cpp#L3536-L3557.
    +// As far as I can tell, Transmission just keeps the oldest connection.
    +func (cn *PeerConn) isPreferredDirection() bool {
    +	// True if our client peer ID is higher than the remote's peer ID.
    +	return bytes.Compare(cn.PeerID[:], cn.t.cl.peerID[:]) < 0 == cn.outgoing
    +}
    +
    +// Returns whether the left connection should be preferred over the right one,
    +// considering only their networking properties. If ok is false, we can't
    +// decide.
    +func (l *PeerConn) hasPreferredNetworkOver(r *PeerConn) bool {
    +	var ml multiless.Computation
    +	ml = ml.Bool(r.isPreferredDirection(), l.isPreferredDirection())
    +	ml = ml.Bool(l.utp(), r.utp())
    +	ml = ml.Bool(r.ipv6(), l.ipv6())
    +	return ml.Less()
    +}
    +
    +func (cn *PeerConn) peerHasAllPieces() (all, known bool) {
    +	if cn.peerSentHaveAll {
    +		return true, true
    +	}
    +	if !cn.t.haveInfo() {
    +		return false, false
    +	}
    +	return cn._peerPieces.GetCardinality() == uint64(cn.t.numPieces()), true
    +}
    +
    +func (cn *PeerConn) onGotInfo(info *metainfo.Info) {
    +	cn.setNumPieces(info.NumPieces())
    +}
    +
    +// Correct the PeerPieces slice length. Return false if the existing slice is invalid, such as by
    +// receiving badly sized BITFIELD, or invalid HAVE messages.
    +func (cn *PeerConn) setNumPieces(num pieceIndex) {
    +	cn._peerPieces.RemoveRange(bitmap.BitRange(num), bitmap.ToEnd)
    +	cn.peerPiecesChanged()
    +}
    +
    +func (cn *PeerConn) peerPieces() *roaring.Bitmap {
    +	return &cn._peerPieces
    +}
    +
    +func (cn *PeerConn) connectionFlags() (ret string) {
    +	c := func(b byte) {
    +		ret += string([]byte{b})
    +	}
    +	if cn.cryptoMethod == mse.CryptoMethodRC4 {
    +		c('E')
    +	} else if cn.headerEncrypted {
    +		c('e')
    +	}
    +	ret += string(cn.Discovery)
    +	if cn.utp() {
    +		c('U')
    +	}
    +	return
    +}
    +
    +func (cn *PeerConn) utp() bool {
    +	return parseNetworkString(cn.Network).Udp
    +}
    +
    +func (cn *PeerConn) onClose() {
    +	if cn.pex.IsEnabled() {
    +		cn.pex.Close()
    +	}
    +	cn.tickleWriter()
    +	if cn.conn != nil {
    +		go cn.conn.Close()
    +	}
    +	if cb := cn.callbacks.PeerConnClosed; cb != nil {
    +		cb(cn)
    +	}
    +}
    +
    +// Writes a message into the write buffer. Returns whether it's okay to keep writing. Writing is
    +// done asynchronously, so it may be that we're not able to honour backpressure from this method.
    +func (cn *PeerConn) write(msg pp.Message) bool {
    +	torrent.Add(fmt.Sprintf("messages written of type %s", msg.Type.String()), 1)
    +	// We don't need to track bytes here because the connection's Writer has that behaviour injected
    +	// (although there's some delay between us buffering the message, and the connection writer
    +	// flushing it out.).
    +	notFull := cn.messageWriter.write(msg)
    +	// Last I checked only Piece messages affect stats, and we don't write those.
    +	cn.wroteMsg(&msg)
    +	cn.tickleWriter()
    +	return notFull
    +}
    +
    +func (cn *PeerConn) requestMetadataPiece(index int) {
    +	eID := cn.PeerExtensionIDs[pp.ExtensionNameMetadata]
    +	if eID == pp.ExtensionDeleteNumber {
    +		return
    +	}
    +	if index < len(cn.metadataRequests) && cn.metadataRequests[index] {
    +		return
    +	}
    +	cn.logger.WithDefaultLevel(log.Debug).Printf("requesting metadata piece %d", index)
    +	cn.write(pp.MetadataExtensionRequestMsg(eID, index))
    +	for index >= len(cn.metadataRequests) {
    +		cn.metadataRequests = append(cn.metadataRequests, false)
    +	}
    +	cn.metadataRequests[index] = true
    +}
    +
    +func (cn *PeerConn) requestedMetadataPiece(index int) bool {
    +	return index < len(cn.metadataRequests) && cn.metadataRequests[index]
    +}
    +
    +func (cn *PeerConn) onPeerSentCancel(r Request) {
    +	if _, ok := cn.peerRequests[r]; !ok {
    +		torrent.Add("unexpected cancels received", 1)
    +		return
    +	}
    +	if cn.fastEnabled() {
    +		cn.reject(r)
    +	} else {
    +		delete(cn.peerRequests, r)
    +	}
    +}
    +
    +func (cn *PeerConn) choke(msg messageWriter) (more bool) {
    +	if cn.choking {
    +		return true
    +	}
    +	cn.choking = true
    +	more = msg(pp.Message{
    +		Type: pp.Choke,
    +	})
    +	if !cn.fastEnabled() {
    +		cn.deleteAllPeerRequests()
    +	}
    +	return
    +}
    +
    +func (cn *PeerConn) deleteAllPeerRequests() {
    +	for _, state := range cn.peerRequests {
    +		state.allocReservation.Drop()
    +	}
    +	cn.peerRequests = nil
    +}
    +
    +func (cn *PeerConn) unchoke(msg func(pp.Message) bool) bool {
    +	if !cn.choking {
    +		return true
    +	}
    +	cn.choking = false
    +	return msg(pp.Message{
    +		Type: pp.Unchoke,
    +	})
    +}
    +
    +func (pc *PeerConn) writeInterested(interested bool) bool {
    +	return pc.write(pp.Message{
    +		Type: func() pp.MessageType {
    +			if interested {
    +				return pp.Interested
    +			} else {
    +				return pp.NotInterested
    +			}
    +		}(),
    +	})
    +}
    +
    +func (me *PeerConn) _request(r Request) bool {
    +	return me.write(pp.Message{
    +		Type:   pp.Request,
    +		Index:  r.Index,
    +		Begin:  r.Begin,
    +		Length: r.Length,
    +	})
    +}
    +
    +func (me *PeerConn) _cancel(r RequestIndex) bool {
    +	me.write(makeCancelMessage(me.t.requestIndexToRequest(r)))
    +	return me.remoteRejectsCancels()
    +}
    +
    +// Whether we should expect a reject message after sending a cancel.
    +func (me *PeerConn) remoteRejectsCancels() bool {
    +	if !me.fastEnabled() {
    +		return false
    +	}
    +	if me.remoteIsTransmission() {
    +		// Transmission did not send rejects for received cancels. See
    +		// https://github.com/transmission/transmission/pull/2275. Fixed in 4.0.0-beta.1 onward in
    +		// https://github.com/transmission/transmission/commit/76719bf34c255da4fca991c2ad3fa4b65d2154b1.
    +		// Peer ID prefix scheme described
    +		// https://github.com/transmission/transmission/blob/7ec7607bbcf0fa99bd4b157b9b0f0c411d59f45d/CMakeLists.txt#L128-L149.
    +		return me.PeerID[3] >= '4'
    +	}
    +	return true
    +}
    +
    +func (cn *PeerConn) fillWriteBuffer() {
    +	if cn.messageWriter.writeBuffer.Len() > writeBufferLowWaterLen {
    +		// Fully committing to our max requests requires sufficient space (see
    +		// maxLocalToRemoteRequests). Flush what we have instead. We also prefer always to make
    +		// requests than to do PEX or upload, so we short-circuit before handling those. Any update
    +		// request reason will not be cleared, so we'll come right back here when there's space. We
    +		// can't do this in maybeUpdateActualRequestState because it's a method on Peer and has no
    +		// knowledge of write buffers.
    +		return
    +	}
    +	cn.maybeUpdateActualRequestState()
    +	if cn.pex.IsEnabled() {
    +		if flow := cn.pex.Share(cn.write); !flow {
    +			return
    +		}
    +	}
    +	cn.upload(cn.write)
    +}
    +
    +func (cn *PeerConn) have(piece pieceIndex) {
    +	if cn.sentHaves.Get(bitmap.BitIndex(piece)) {
    +		return
    +	}
    +	cn.write(pp.Message{
    +		Type:  pp.Have,
    +		Index: pp.Integer(piece),
    +	})
    +	cn.sentHaves.Add(bitmap.BitIndex(piece))
    +}
    +
    +func (cn *PeerConn) postBitfield() {
    +	if cn.sentHaves.Len() != 0 {
    +		panic("bitfield must be first have-related message sent")
    +	}
    +	if !cn.t.haveAnyPieces() {
    +		return
    +	}
    +	cn.write(pp.Message{
    +		Type:     pp.Bitfield,
    +		Bitfield: cn.t.bitfield(),
    +	})
    +	cn.sentHaves = bitmap.Bitmap{cn.t._completedPieces.Clone()}
    +}
    +
    +func (cn *PeerConn) handleUpdateRequests() {
    +	// The writer determines the request state as needed when it can write.
    +	cn.tickleWriter()
    +}
    +
    +func (cn *PeerConn) raisePeerMinPieces(newMin pieceIndex) {
    +	if newMin > cn.peerMinPieces {
    +		cn.peerMinPieces = newMin
    +	}
    +}
    +
    +func (cn *PeerConn) peerSentHave(piece pieceIndex) error {
    +	if cn.t.haveInfo() && piece >= cn.t.numPieces() || piece < 0 {
    +		return errors.New("invalid piece")
    +	}
    +	if cn.peerHasPiece(piece) {
    +		return nil
    +	}
    +	cn.raisePeerMinPieces(piece + 1)
    +	if !cn.peerHasPiece(piece) {
    +		cn.t.incPieceAvailability(piece)
    +	}
    +	cn._peerPieces.Add(uint32(piece))
    +	if cn.t.wantPieceIndex(piece) {
    +		cn.updateRequests("have")
    +	}
    +	cn.peerPiecesChanged()
    +	return nil
    +}
    +
    +func (cn *PeerConn) peerSentBitfield(bf []bool) error {
    +	if len(bf)%8 != 0 {
    +		panic("expected bitfield length divisible by 8")
    +	}
    +	// We know that the last byte means that at most the last 7 bits are wasted.
    +	cn.raisePeerMinPieces(pieceIndex(len(bf) - 7))
    +	if cn.t.haveInfo() && len(bf) > int(cn.t.numPieces()) {
    +		// Ignore known excess pieces.
    +		bf = bf[:cn.t.numPieces()]
    +	}
    +	bm := boolSliceToBitmap(bf)
    +	if cn.t.haveInfo() && pieceIndex(bm.GetCardinality()) == cn.t.numPieces() {
    +		cn.onPeerHasAllPieces()
    +		return nil
    +	}
    +	if !bm.IsEmpty() {
    +		cn.raisePeerMinPieces(pieceIndex(bm.Maximum()) + 1)
    +	}
    +	shouldUpdateRequests := false
    +	if cn.peerSentHaveAll {
    +		if !cn.t.deleteConnWithAllPieces(&cn.Peer) {
    +			panic(cn)
    +		}
    +		cn.peerSentHaveAll = false
    +		if !cn._peerPieces.IsEmpty() {
    +			panic("if peer has all, we expect no individual peer pieces to be set")
    +		}
    +	} else {
    +		bm.Xor(&cn._peerPieces)
    +	}
    +	cn.peerSentHaveAll = false
    +	// bm is now 'on' for pieces that are changing
    +	bm.Iterate(func(x uint32) bool {
    +		pi := pieceIndex(x)
    +		if cn._peerPieces.Contains(x) {
    +			// Then we must be losing this piece
    +			cn.t.decPieceAvailability(pi)
    +		} else {
    +			if !shouldUpdateRequests && cn.t.wantPieceIndex(pieceIndex(x)) {
    +				shouldUpdateRequests = true
    +			}
    +			// We must be gaining this piece
    +			cn.t.incPieceAvailability(pieceIndex(x))
    +		}
    +		return true
    +	})
    +	// Apply the changes. If we had everything previously, this should be empty, so xor is the same
    +	// as or.
    +	cn._peerPieces.Xor(&bm)
    +	if shouldUpdateRequests {
    +		cn.updateRequests("bitfield")
    +	}
    +	// We didn't guard this before, I see no reason to do it now.
    +	cn.peerPiecesChanged()
    +	return nil
    +}
    +
    +func (cn *PeerConn) onPeerHasAllPieces() {
    +	t := cn.t
    +	if t.haveInfo() {
    +		cn._peerPieces.Iterate(func(x uint32) bool {
    +			t.decPieceAvailability(pieceIndex(x))
    +			return true
    +		})
    +	}
    +	t.addConnWithAllPieces(&cn.Peer)
    +	cn.peerSentHaveAll = true
    +	cn._peerPieces.Clear()
    +	if !cn.t._pendingPieces.IsEmpty() {
    +		cn.updateRequests("Peer.onPeerHasAllPieces")
    +	}
    +	cn.peerPiecesChanged()
    +}
    +
    +func (cn *PeerConn) onPeerSentHaveAll() error {
    +	cn.onPeerHasAllPieces()
    +	return nil
    +}
    +
    +func (cn *PeerConn) peerSentHaveNone() error {
    +	if !cn.peerSentHaveAll {
    +		cn.t.decPeerPieceAvailability(&cn.Peer)
    +	}
    +	cn._peerPieces.Clear()
    +	cn.peerSentHaveAll = false
    +	cn.peerPiecesChanged()
    +	return nil
    +}
    +
    +func (c *PeerConn) requestPendingMetadata() {
    +	if c.t.haveInfo() {
    +		return
    +	}
    +	if c.PeerExtensionIDs[pp.ExtensionNameMetadata] == 0 {
    +		// Peer doesn't support this.
    +		return
    +	}
    +	// Request metadata pieces that we don't have in a random order.
    +	var pending []int
    +	for index := 0; index < c.t.metadataPieceCount(); index++ {
    +		if !c.t.haveMetadataPiece(index) && !c.requestedMetadataPiece(index) {
    +			pending = append(pending, index)
    +		}
    +	}
    +	rand.Shuffle(len(pending), func(i, j int) { pending[i], pending[j] = pending[j], pending[i] })
    +	for _, i := range pending {
    +		c.requestMetadataPiece(i)
    +	}
    +}
    +
    +func (cn *PeerConn) wroteMsg(msg *pp.Message) {
    +	torrent.Add(fmt.Sprintf("messages written of type %s", msg.Type.String()), 1)
    +	if msg.Type == pp.Extended {
    +		for name, id := range cn.PeerExtensionIDs {
    +			if id != msg.ExtendedID {
    +				continue
    +			}
    +			torrent.Add(fmt.Sprintf("Extended messages written for protocol %q", name), 1)
    +		}
    +	}
    +	cn.allStats(func(cs *ConnStats) { cs.wroteMsg(msg) })
    +}
    +
    +func (cn *PeerConn) wroteBytes(n int64) {
    +	cn.allStats(add(n, func(cs *ConnStats) *Count { return &cs.BytesWritten }))
    +}
    +
    +func (c *PeerConn) fastEnabled() bool {
    +	return c.PeerExtensionBytes.SupportsFast() && c.t.cl.config.Extensions.SupportsFast()
    +}
    +
    +func (c *PeerConn) reject(r Request) {
    +	if !c.fastEnabled() {
    +		panic("fast not enabled")
    +	}
    +	c.write(r.ToMsg(pp.Reject))
    +	// It is possible to reject a request before it is added to peer requests due to being invalid.
    +	if state, ok := c.peerRequests[r]; ok {
    +		state.allocReservation.Drop()
    +		delete(c.peerRequests, r)
    +	}
    +}
    +
    +func (c *PeerConn) maximumPeerRequestChunkLength() (_ Option[int]) {
    +	uploadRateLimiter := c.t.cl.config.UploadRateLimiter
    +	if uploadRateLimiter.Limit() == rate.Inf {
    +		return
    +	}
    +	return Some(uploadRateLimiter.Burst())
    +}
    +
    +// startFetch is for testing purposes currently.
    +func (c *PeerConn) onReadRequest(r Request, startFetch bool) error {
    +	requestedChunkLengths.Add(strconv.FormatUint(r.Length.Uint64(), 10), 1)
    +	if _, ok := c.peerRequests[r]; ok {
    +		torrent.Add("duplicate requests received", 1)
    +		if c.fastEnabled() {
    +			return errors.New("received duplicate request with fast enabled")
    +		}
    +		return nil
    +	}
    +	if c.choking {
    +		torrent.Add("requests received while choking", 1)
    +		if c.fastEnabled() {
    +			torrent.Add("requests rejected while choking", 1)
    +			c.reject(r)
    +		}
    +		return nil
    +	}
    +	// TODO: What if they've already requested this?
    +	if len(c.peerRequests) >= localClientReqq {
    +		torrent.Add("requests received while queue full", 1)
    +		if c.fastEnabled() {
    +			c.reject(r)
    +		}
    +		// BEP 6 says we may close here if we choose.
    +		return nil
    +	}
    +	if opt := c.maximumPeerRequestChunkLength(); opt.Ok && int(r.Length) > opt.Value {
    +		err := fmt.Errorf("peer requested chunk too long (%v)", r.Length)
    +		c.logger.Levelf(log.Warning, err.Error())
    +		if c.fastEnabled() {
    +			c.reject(r)
    +			return nil
    +		} else {
    +			return err
    +		}
    +	}
    +	if !c.t.havePiece(pieceIndex(r.Index)) {
    +		// TODO: Tell the peer we don't have the piece, and reject this request.
    +		requestsReceivedForMissingPieces.Add(1)
    +		return fmt.Errorf("peer requested piece we don't have: %v", r.Index.Int())
    +	}
    +	pieceLength := c.t.pieceLength(pieceIndex(r.Index))
    +	// Check this after we know we have the piece, so that the piece length will be known.
    +	if chunkOverflowsPiece(r.ChunkSpec, pieceLength) {
    +		torrent.Add("bad requests received", 1)
    +		return errors.New("chunk overflows piece")
    +	}
    +	if c.peerRequests == nil {
    +		c.peerRequests = make(map[Request]*peerRequestState, localClientReqq)
    +	}
    +	value := &peerRequestState{
    +		allocReservation: c.peerRequestDataAllocLimiter.Reserve(int64(r.Length)),
    +	}
    +	c.peerRequests[r] = value
    +	if startFetch {
    +		// TODO: Limit peer request data read concurrency.
    +		go c.peerRequestDataReader(r, value)
    +	}
    +	return nil
    +}
    +
    +func (c *PeerConn) peerRequestDataReader(r Request, prs *peerRequestState) {
    +	// Should we depend on Torrent closure here? I think it's okay to get cancelled from elsewhere,
    +	// or fail to read and then cleanup. Also, we used to hang here if the reservation was never
    +	// dropped, that was fixed.
    +	ctx := context.Background()
    +	err := prs.allocReservation.Wait(ctx)
    +	if err != nil {
    +		c.logger.WithDefaultLevel(log.Debug).Levelf(log.ErrorLevel(err), "waiting for alloc limit reservation: %v", err)
    +		return
    +	}
    +	b, err := c.readPeerRequestData(r)
    +	c.locker().Lock()
    +	defer c.locker().Unlock()
    +	if err != nil {
    +		c.peerRequestDataReadFailed(err, r)
    +	} else {
    +		if b == nil {
    +			panic("data must be non-nil to trigger send")
    +		}
    +		torrent.Add("peer request data read successes", 1)
    +		prs.data = b
    +		// This might be required for the error case too (#752 and #753).
    +		c.tickleWriter()
    +	}
    +}
    +
    +// If this is maintained correctly, we might be able to support optional synchronous reading for
    +// chunk sending, the way it used to work.
    +func (c *PeerConn) peerRequestDataReadFailed(err error, r Request) {
    +	torrent.Add("peer request data read failures", 1)
    +	logLevel := log.Warning
    +	if c.t.hasStorageCap() {
    +		// It's expected that pieces might drop. See
    +		// https://github.com/anacrolix/torrent/issues/702#issuecomment-1000953313.
    +		logLevel = log.Debug
    +	}
    +	c.logger.Levelf(logLevel, "error reading chunk for peer Request %v: %v", r, err)
    +	if c.t.closed.IsSet() {
    +		return
    +	}
    +	i := pieceIndex(r.Index)
    +	if c.t.pieceComplete(i) {
    +		// There used to be more code here that just duplicated the following break. Piece
    +		// completions are currently cached, so I'm not sure how helpful this update is, except to
    +		// pull any completion changes pushed to the storage backend in failed reads that got us
    +		// here.
    +		c.t.updatePieceCompletion(i)
    +	}
    +	// We've probably dropped a piece from storage, but there's no way to communicate this to the
    +	// peer. If they ask for it again, we kick them allowing us to send them updated piece states if
    +	// we reconnect. TODO: Instead, we could just try to update them with Bitfield or HaveNone and
    +	// if they kick us for breaking protocol, on reconnect we will be compliant again (at least
    +	// initially).
    +	if c.fastEnabled() {
    +		c.reject(r)
    +	} else {
    +		if c.choking {
    +			// If fast isn't enabled, I think we would have wiped all peer requests when we last
    +			// choked, and requests while we're choking would be ignored. It could be possible that
    +			// a peer request data read completed concurrently to it being deleted elsewhere.
    +			c.logger.WithDefaultLevel(log.Warning).Printf("already choking peer, requests might not be rejected correctly")
    +		}
    +		// Choking a non-fast peer should cause them to flush all their requests.
    +		c.choke(c.write)
    +	}
    +}
    +
    +func (c *PeerConn) readPeerRequestData(r Request) ([]byte, error) {
    +	b := make([]byte, r.Length)
    +	p := c.t.info.Piece(int(r.Index))
    +	n, err := c.t.readAt(b, p.Offset()+int64(r.Begin))
    +	if n == len(b) {
    +		if err == io.EOF {
    +			err = nil
    +		}
    +	} else {
    +		if err == nil {
    +			panic("expected error")
    +		}
    +	}
    +	return b, err
    +}
    +
    +func (c *PeerConn) logProtocolBehaviour(level log.Level, format string, arg ...interface{}) {
    +	c.logger.WithContextText(fmt.Sprintf(
    +		"peer id %q, ext v %q", c.PeerID, c.PeerClientName.Load(),
    +	)).SkipCallers(1).Levelf(level, format, arg...)
    +}
    +
    +// Processes incoming BitTorrent wire-protocol messages. The client lock is held upon entry and
    +// exit. Returning will end the connection.
    +func (c *PeerConn) mainReadLoop() (err error) {
    +	defer func() {
    +		if err != nil {
    +			torrent.Add("connection.mainReadLoop returned with error", 1)
    +		} else {
    +			torrent.Add("connection.mainReadLoop returned with no error", 1)
    +		}
    +	}()
    +	t := c.t
    +	cl := t.cl
    +
    +	decoder := pp.Decoder{
    +		R:         bufio.NewReaderSize(c.r, 1<<17),
    +		MaxLength: 4 * pp.Integer(max(int64(t.chunkSize), defaultChunkSize)),
    +		Pool:      &t.chunkPool,
    +	}
    +	for {
    +		var msg pp.Message
    +		func() {
    +			cl.unlock()
    +			defer cl.lock()
    +			err = decoder.Decode(&msg)
    +		}()
    +		if cb := c.callbacks.ReadMessage; cb != nil && err == nil {
    +			cb(c, &msg)
    +		}
    +		if t.closed.IsSet() || c.closed.IsSet() {
    +			return nil
    +		}
    +		if err != nil {
    +			return err
    +		}
    +		c.lastMessageReceived = time.Now()
    +		if msg.Keepalive {
    +			receivedKeepalives.Add(1)
    +			continue
    +		}
    +		messageTypesReceived.Add(msg.Type.String(), 1)
    +		if msg.Type.FastExtension() && !c.fastEnabled() {
    +			runSafeExtraneous(func() { torrent.Add("fast messages received when extension is disabled", 1) })
    +			return fmt.Errorf("received fast extension message (type=%v) but extension is disabled", msg.Type)
    +		}
    +		switch msg.Type {
    +		case pp.Choke:
    +			if c.peerChoking {
    +				break
    +			}
    +			if !c.fastEnabled() {
    +				c.deleteAllRequests("choked by non-fast PeerConn")
    +			} else {
    +				// We don't decrement pending requests here, let's wait for the peer to either
    +				// reject or satisfy the outstanding requests. Additionally, some peers may unchoke
    +				// us and resume where they left off, we don't want to have piled on to those chunks
    +				// in the meanwhile. I think a peer's ability to abuse this should be limited: they
    +				// could let us request a lot of stuff, then choke us and never reject, but they're
    +				// only a single peer, our chunk balancing should smooth over this abuse.
    +			}
    +			c.peerChoking = true
    +			c.updateExpectingChunks()
    +		case pp.Unchoke:
    +			if !c.peerChoking {
    +				// Some clients do this for some reason. Transmission doesn't error on this, so we
    +				// won't for consistency.
    +				c.logProtocolBehaviour(log.Debug, "received unchoke when already unchoked")
    +				break
    +			}
    +			c.peerChoking = false
    +			preservedCount := 0
    +			c.requestState.Requests.Iterate(func(x RequestIndex) bool {
    +				if !c.peerAllowedFast.Contains(c.t.pieceIndexOfRequestIndex(x)) {
    +					preservedCount++
    +				}
    +				return true
    +			})
    +			if preservedCount != 0 {
    +				// TODO: Yes this is a debug log but I'm not happy with the state of the logging lib
    +				// right now.
    +				c.logger.Levelf(log.Debug,
    +					"%v requests were preserved while being choked (fast=%v)",
    +					preservedCount,
    +					c.fastEnabled())
    +
    +				torrent.Add("requestsPreservedThroughChoking", int64(preservedCount))
    +			}
    +			if !c.t._pendingPieces.IsEmpty() {
    +				c.updateRequests("unchoked")
    +			}
    +			c.updateExpectingChunks()
    +		case pp.Interested:
    +			c.peerInterested = true
    +			c.tickleWriter()
    +		case pp.NotInterested:
    +			c.peerInterested = false
    +			// We don't clear their requests since it isn't clear in the spec.
    +			// We'll probably choke them for this, which will clear them if
    +			// appropriate, and is clearly specified.
    +		case pp.Have:
    +			err = c.peerSentHave(pieceIndex(msg.Index))
    +		case pp.Bitfield:
    +			err = c.peerSentBitfield(msg.Bitfield)
    +		case pp.Request:
    +			r := newRequestFromMessage(&msg)
    +			err = c.onReadRequest(r, true)
    +			if err != nil {
    +				err = fmt.Errorf("on reading request %v: %w", r, err)
    +			}
    +		case pp.Piece:
    +			c.doChunkReadStats(int64(len(msg.Piece)))
    +			err = c.receiveChunk(&msg)
    +			if len(msg.Piece) == int(t.chunkSize) {
    +				t.chunkPool.Put(&msg.Piece)
    +			}
    +			if err != nil {
    +				err = fmt.Errorf("receiving chunk: %w", err)
    +			}
    +		case pp.Cancel:
    +			req := newRequestFromMessage(&msg)
    +			c.onPeerSentCancel(req)
    +		case pp.Port:
    +			ipa, ok := tryIpPortFromNetAddr(c.RemoteAddr)
    +			if !ok {
    +				break
    +			}
    +			pingAddr := net.UDPAddr{
    +				IP:   ipa.IP,
    +				Port: ipa.Port,
    +			}
    +			if msg.Port != 0 {
    +				pingAddr.Port = int(msg.Port)
    +			}
    +			cl.eachDhtServer(func(s DhtServer) {
    +				go s.Ping(&pingAddr)
    +			})
    +		case pp.Suggest:
    +			torrent.Add("suggests received", 1)
    +			log.Fmsg("peer suggested piece %d", msg.Index).AddValues(c, msg.Index).LogLevel(log.Debug, c.t.logger)
    +			c.updateRequests("suggested")
    +		case pp.HaveAll:
    +			err = c.onPeerSentHaveAll()
    +		case pp.HaveNone:
    +			err = c.peerSentHaveNone()
    +		case pp.Reject:
    +			req := newRequestFromMessage(&msg)
    +			if !c.remoteRejectedRequest(c.t.requestIndexFromRequest(req)) {
    +				err = fmt.Errorf("received invalid reject for request %v", req)
    +				c.logger.Levelf(log.Debug, "%v", err)
    +			}
    +		case pp.AllowedFast:
    +			torrent.Add("allowed fasts received", 1)
    +			log.Fmsg("peer allowed fast: %d", msg.Index).AddValues(c).LogLevel(log.Debug, c.t.logger)
    +			c.updateRequests("PeerConn.mainReadLoop allowed fast")
    +		case pp.Extended:
    +			err = c.onReadExtendedMsg(msg.ExtendedID, msg.ExtendedPayload)
    +		default:
    +			err = fmt.Errorf("received unknown message type: %#v", msg.Type)
    +		}
    +		if err != nil {
    +			return err
    +		}
    +	}
    +}
    +
    +func (c *PeerConn) onReadExtendedMsg(id pp.ExtensionNumber, payload []byte) (err error) {
    +	defer func() {
    +		// TODO: Should we still do this?
    +		if err != nil {
    +			// These clients use their own extension IDs for outgoing message
    +			// types, which is incorrect.
    +			if bytes.HasPrefix(c.PeerID[:], []byte("-SD0100-")) || strings.HasPrefix(string(c.PeerID[:]), "-XL0012-") {
    +				err = nil
    +			}
    +		}
    +	}()
    +	t := c.t
    +	cl := t.cl
    +	switch id {
    +	case pp.HandshakeExtendedID:
    +		var d pp.ExtendedHandshakeMessage
    +		if err := bencode.Unmarshal(payload, &d); err != nil {
    +			c.logger.Printf("error parsing extended handshake message %q: %s", payload, err)
    +			return fmt.Errorf("unmarshalling extended handshake payload: %w", err)
    +		}
    +		if cb := c.callbacks.ReadExtendedHandshake; cb != nil {
    +			cb(c, &d)
    +		}
    +		// c.logger.WithDefaultLevel(log.Debug).Printf("received extended handshake message:\n%s", spew.Sdump(d))
    +		if d.Reqq != 0 {
    +			c.PeerMaxRequests = d.Reqq
    +		}
    +		c.PeerClientName.Store(d.V)
    +		if c.PeerExtensionIDs == nil {
    +			c.PeerExtensionIDs = make(map[pp.ExtensionName]pp.ExtensionNumber, len(d.M))
    +		}
    +		c.PeerListenPort = d.Port
    +		c.PeerPrefersEncryption = d.Encryption
    +		for name, id := range d.M {
    +			if _, ok := c.PeerExtensionIDs[name]; !ok {
    +				peersSupportingExtension.Add(
    +					// expvar.Var.String must produce valid JSON. "ut_payme\xeet_address" was being
    +					// entered here which caused problems later when unmarshalling.
    +					strconv.Quote(string(name)),
    +					1)
    +			}
    +			c.PeerExtensionIDs[name] = id
    +		}
    +		if d.MetadataSize != 0 {
    +			if err = t.setMetadataSize(d.MetadataSize); err != nil {
    +				return fmt.Errorf("setting metadata size to %d: %w", d.MetadataSize, err)
    +			}
    +		}
    +		c.requestPendingMetadata()
    +		if !t.cl.config.DisablePEX {
    +			t.pex.Add(c) // we learnt enough now
    +			// This checks the extension is supported internally.
    +			c.pex.Init(c)
    +		}
    +		return nil
    +	case metadataExtendedId:
    +		err := cl.gotMetadataExtensionMsg(payload, t, c)
    +		if err != nil {
    +			return fmt.Errorf("handling metadata extension message: %w", err)
    +		}
    +		return nil
    +	case pexExtendedId:
    +		if !c.pex.IsEnabled() {
    +			return nil // or hang-up maybe?
    +		}
    +		err = c.pex.Recv(payload)
    +		if err != nil {
    +			err = fmt.Errorf("receiving pex message: %w", err)
    +		}
    +		return
    +	case utHolepunchExtendedId:
    +		var msg utHolepunch.Msg
    +		err = msg.UnmarshalBinary(payload)
    +		if err != nil {
    +			err = fmt.Errorf("unmarshalling ut_holepunch message: %w", err)
    +			return
    +		}
    +		err = c.t.handleReceivedUtHolepunchMsg(msg, c)
    +		return
    +	default:
    +		return fmt.Errorf("unexpected extended message ID: %v", id)
    +	}
    +}
    +
    +// Set both the Reader and Writer for the connection from a single ReadWriter.
    +func (cn *PeerConn) setRW(rw io.ReadWriter) {
    +	cn.r = rw
    +	cn.w = rw
    +}
    +
    +// Returns the Reader and Writer as a combined ReadWriter.
    +func (cn *PeerConn) rw() io.ReadWriter {
    +	return struct {
    +		io.Reader
    +		io.Writer
    +	}{cn.r, cn.w}
    +}
    +
    +func (c *PeerConn) uploadAllowed() bool {
    +	if c.t.cl.config.NoUpload {
    +		return false
    +	}
    +	if c.t.dataUploadDisallowed {
    +		return false
    +	}
    +	if c.t.seeding() {
    +		return true
    +	}
    +	if !c.peerHasWantedPieces() {
    +		return false
    +	}
    +	// Don't upload more than 100 KiB more than we download.
    +	if c._stats.BytesWrittenData.Int64() >= c._stats.BytesReadData.Int64()+100<<10 {
    +		return false
    +	}
    +	return true
    +}
    +
    +func (c *PeerConn) setRetryUploadTimer(delay time.Duration) {
    +	if c.uploadTimer == nil {
    +		c.uploadTimer = time.AfterFunc(delay, c.tickleWriter)
    +	} else {
    +		c.uploadTimer.Reset(delay)
    +	}
    +}
    +
    +// Also handles choking and unchoking of the remote peer.
    +func (c *PeerConn) upload(msg func(pp.Message) bool) bool {
    +	// Breaking or completing this loop means we don't want to upload to the peer anymore, and we
    +	// choke them.
    +another:
    +	for c.uploadAllowed() {
    +		// We want to upload to the peer.
    +		if !c.unchoke(msg) {
    +			return false
    +		}
    +		for r, state := range c.peerRequests {
    +			if state.data == nil {
    +				continue
    +			}
    +			res := c.t.cl.config.UploadRateLimiter.ReserveN(time.Now(), int(r.Length))
    +			if !res.OK() {
    +				panic(fmt.Sprintf("upload rate limiter burst size < %d", r.Length))
    +			}
    +			delay := res.Delay()
    +			if delay > 0 {
    +				res.Cancel()
    +				c.setRetryUploadTimer(delay)
    +				// Hard to say what to return here.
    +				return true
    +			}
    +			more := c.sendChunk(r, msg, state)
    +			delete(c.peerRequests, r)
    +			if !more {
    +				return false
    +			}
    +			goto another
    +		}
    +		return true
    +	}
    +	return c.choke(msg)
    +}
    +
    +func (cn *PeerConn) drop() {
    +	cn.t.dropConnection(cn)
    +}
    +
    +func (cn *PeerConn) ban() {
    +	cn.t.cl.banPeerIP(cn.remoteIp())
    +}
    +
    +// This is called when something has changed that should wake the writer, such as putting stuff into
    +// the writeBuffer, or changing some state that the writer can act on.
    +func (c *PeerConn) tickleWriter() {
    +	c.messageWriter.writeCond.Broadcast()
    +}
    +
    +func (c *PeerConn) sendChunk(r Request, msg func(pp.Message) bool, state *peerRequestState) (more bool) {
    +	c.lastChunkSent = time.Now()
    +	state.allocReservation.Release()
    +	return msg(pp.Message{
    +		Type:  pp.Piece,
    +		Index: r.Index,
    +		Begin: r.Begin,
    +		Piece: state.data,
    +	})
    +}
    +
    +func (c *PeerConn) setTorrent(t *Torrent) {
    +	if c.t != nil {
    +		panic("connection already associated with a torrent")
    +	}
    +	c.t = t
    +	c.logger.WithDefaultLevel(log.Debug).Printf("set torrent=%v", t)
    +	t.reconcileHandshakeStats(c)
    +}
    +
    +func (c *PeerConn) pexPeerFlags() pp.PexPeerFlags {
    +	f := pp.PexPeerFlags(0)
    +	if c.PeerPrefersEncryption {
    +		f |= pp.PexPrefersEncryption
    +	}
    +	if c.outgoing {
    +		f |= pp.PexOutgoingConn
    +	}
    +	if c.utp() {
    +		f |= pp.PexSupportsUtp
    +	}
    +	return f
    +}
    +
    +// This returns the address to use if we want to dial the peer again. It incorporates the peer's
    +// advertised listen port.
    +func (c *PeerConn) dialAddr() PeerRemoteAddr {
    +	if c.outgoing || c.PeerListenPort == 0 {
    +		return c.RemoteAddr
    +	}
    +	addrPort, err := addrPortFromPeerRemoteAddr(c.RemoteAddr)
    +	if err != nil {
    +		c.logger.Levelf(
    +			log.Warning,
    +			"error parsing %q for alternate dial port: %v",
    +			c.RemoteAddr,
    +			err,
    +		)
    +		return c.RemoteAddr
    +	}
    +	return netip.AddrPortFrom(addrPort.Addr(), uint16(c.PeerListenPort))
    +}
    +
    +func (c *PeerConn) pexEvent(t pexEventType) (_ pexEvent, err error) {
    +	f := c.pexPeerFlags()
    +	dialAddr := c.dialAddr()
    +	addr, err := addrPortFromPeerRemoteAddr(dialAddr)
    +	if err != nil || !addr.IsValid() {
    +		err = fmt.Errorf("parsing dial addr %q: %w", dialAddr, err)
    +		return
    +	}
    +	return pexEvent{t, addr, f, nil}, nil
    +}
    +
    +func (pc *PeerConn) String() string {
    +	return fmt.Sprintf("%T %p [id=%+q, exts=%v, v=%q]", pc, pc, pc.PeerID, pc.PeerExtensionBytes, pc.PeerClientName.Load())
    +}
    +
    +// Returns the pieces the peer could have based on their claims. If we don't know how many pieces
    +// are in the torrent, it could be a very large range if the peer has sent HaveAll.
    +func (pc *PeerConn) PeerPieces() *roaring.Bitmap {
    +	pc.locker().RLock()
    +	defer pc.locker().RUnlock()
    +	return pc.newPeerPieces()
    +}
    +
    +func (pc *PeerConn) remoteIsTransmission() bool {
    +	return bytes.HasPrefix(pc.PeerID[:], []byte("-TR")) && pc.PeerID[7] == '-'
    +}
    +
    +func (pc *PeerConn) remoteDialAddrPort() (netip.AddrPort, error) {
    +	dialAddr := pc.dialAddr()
    +	return addrPortFromPeerRemoteAddr(dialAddr)
    +}
    +
    +func (pc *PeerConn) bitExtensionEnabled(bit pp.ExtensionBit) bool {
    +	return pc.t.cl.config.Extensions.GetBit(bit) && pc.PeerExtensionBytes.GetBit(bit)
    +}
    +
    +func (cn *PeerConn) peerPiecesChanged() {
    +	cn.t.maybeDropMutuallyCompletePeer(cn)
    +}
    +
    +// Returns whether the connection could be useful to us. We're seeding and
    +// they want data, we don't have metainfo and they can provide it, etc.
    +func (c *PeerConn) useful() bool {
    +	t := c.t
    +	if c.closed.IsSet() {
    +		return false
    +	}
    +	if !t.haveInfo() {
    +		return c.supportsExtension("ut_metadata")
    +	}
    +	if t.seeding() && c.peerInterested {
    +		return true
    +	}
    +	if c.peerHasWantedPieces() {
    +		return true
    +	}
    +	return false
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peerconn_test.go b/deps/github.com/anacrolix/torrent/peerconn_test.go
    new file mode 100644
    index 0000000..e294b6b
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peerconn_test.go
    @@ -0,0 +1,368 @@
    +package torrent
    +
    +import (
    +	"encoding/binary"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"net"
    +	"sync"
    +	"testing"
    +
    +	"github.com/frankban/quicktest"
    +	qt "github.com/frankban/quicktest"
    +	"github.com/stretchr/testify/require"
    +	"golang.org/x/time/rate"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +// Ensure that no race exists between sending a bitfield, and a subsequent
    +// Have that would potentially alter it.
    +func TestSendBitfieldThenHave(t *testing.T) {
    +	var cl Client
    +	cl.init(TestingConfig(t))
    +	cl.initLogger()
    +	c := cl.newConnection(nil, newConnectionOpts{network: "io.Pipe"})
    +	c.setTorrent(cl.newTorrent(metainfo.Hash{}, nil))
    +	if err := c.t.setInfo(&metainfo.Info{Pieces: make([]byte, metainfo.HashSize*3)}); err != nil {
    +		t.Log(err)
    +	}
    +	r, w := io.Pipe()
    +	// c.r = r
    +	c.w = w
    +	c.startMessageWriter()
    +	c.locker().Lock()
    +	c.t._completedPieces.Add(1)
    +	c.postBitfield( /*[]bool{false, true, false}*/ )
    +	c.locker().Unlock()
    +	c.locker().Lock()
    +	c.have(2)
    +	c.locker().Unlock()
    +	b := make([]byte, 15)
    +	n, err := io.ReadFull(r, b)
    +	c.locker().Lock()
    +	// This will cause connection.writer to terminate.
    +	c.closed.Set()
    +	c.locker().Unlock()
    +	require.NoError(t, err)
    +	require.EqualValues(t, 15, n)
    +	// Here we see that the bitfield doesn't have piece 2 set, as that should
    +	// arrive in the following Have message.
    +	require.EqualValues(t, "\x00\x00\x00\x02\x05@\x00\x00\x00\x05\x04\x00\x00\x00\x02", string(b))
    +}
    +
    +type torrentStorage struct {
    +	writeSem sync.Mutex
    +}
    +
    +func (me *torrentStorage) Close() error { return nil }
    +
    +func (me *torrentStorage) Piece(mp metainfo.Piece) storage.PieceImpl {
    +	return me
    +}
    +
    +func (me *torrentStorage) Completion() storage.Completion {
    +	return storage.Completion{}
    +}
    +
    +func (me *torrentStorage) MarkComplete() error {
    +	return nil
    +}
    +
    +func (me *torrentStorage) MarkNotComplete() error {
    +	return nil
    +}
    +
    +func (me *torrentStorage) ReadAt([]byte, int64) (int, error) {
    +	panic("shouldn't be called")
    +}
    +
    +func (me *torrentStorage) WriteAt(b []byte, _ int64) (int, error) {
    +	if len(b) != defaultChunkSize {
    +		panic(len(b))
    +	}
    +	me.writeSem.Unlock()
    +	return len(b), nil
    +}
    +
    +func BenchmarkConnectionMainReadLoop(b *testing.B) {
    +	c := quicktest.New(b)
    +	var cl Client
    +	cl.init(&ClientConfig{
    +		DownloadRateLimiter: unlimited,
    +	})
    +	cl.initLogger()
    +	ts := &torrentStorage{}
    +	t := cl.newTorrent(metainfo.Hash{}, nil)
    +	t.initialPieceCheckDisabled = true
    +	require.NoError(b, t.setInfo(&metainfo.Info{
    +		Pieces:      make([]byte, 20),
    +		Length:      1 << 20,
    +		PieceLength: 1 << 20,
    +	}))
    +	t.storage = &storage.Torrent{TorrentImpl: storage.TorrentImpl{Piece: ts.Piece, Close: ts.Close}}
    +	t.onSetInfo()
    +	t._pendingPieces.Add(0)
    +	r, w := net.Pipe()
    +	cn := cl.newConnection(r, newConnectionOpts{
    +		outgoing:   true,
    +		remoteAddr: r.RemoteAddr(),
    +		network:    r.RemoteAddr().Network(),
    +		connString: regularNetConnPeerConnConnString(r),
    +	})
    +	cn.setTorrent(t)
    +	mrlErrChan := make(chan error)
    +	msg := pp.Message{
    +		Type:  pp.Piece,
    +		Piece: make([]byte, defaultChunkSize),
    +	}
    +	go func() {
    +		cl.lock()
    +		err := cn.mainReadLoop()
    +		if err != nil {
    +			mrlErrChan <- err
    +		}
    +		close(mrlErrChan)
    +	}()
    +	wb := msg.MustMarshalBinary()
    +	b.SetBytes(int64(len(msg.Piece)))
    +	go func() {
    +		ts.writeSem.Lock()
    +		for i := 0; i < b.N; i += 1 {
    +			cl.lock()
    +			// The chunk must be written to storage everytime, to ensure the
    +			// writeSem is unlocked.
    +			t.pendAllChunkSpecs(0)
    +			cn.validReceiveChunks = map[RequestIndex]int{
    +				t.requestIndexFromRequest(newRequestFromMessage(&msg)): 1,
    +			}
    +			cl.unlock()
    +			n, err := w.Write(wb)
    +			require.NoError(b, err)
    +			require.EqualValues(b, len(wb), n)
    +			ts.writeSem.Lock()
    +		}
    +		if err := w.Close(); err != nil {
    +			panic(err)
    +		}
    +	}()
    +	mrlErr := <-mrlErrChan
    +	if mrlErr != nil && !errors.Is(mrlErr, io.EOF) {
    +		c.Fatal(mrlErr)
    +	}
    +	c.Assert(cn._stats.ChunksReadUseful.Int64(), quicktest.Equals, int64(b.N))
    +}
    +
    +func TestConnPexPeerFlags(t *testing.T) {
    +	var (
    +		tcpAddr = &net.TCPAddr{IP: net.IPv6loopback, Port: 4848}
    +		udpAddr = &net.UDPAddr{IP: net.IPv6loopback, Port: 4848}
    +	)
    +	testcases := []struct {
    +		conn *PeerConn
    +		f    pp.PexPeerFlags
    +	}{
    +		{&PeerConn{Peer: Peer{outgoing: false, PeerPrefersEncryption: false}}, 0},
    +		{&PeerConn{Peer: Peer{outgoing: false, PeerPrefersEncryption: true}}, pp.PexPrefersEncryption},
    +		{&PeerConn{Peer: Peer{outgoing: true, PeerPrefersEncryption: false}}, pp.PexOutgoingConn},
    +		{&PeerConn{Peer: Peer{outgoing: true, PeerPrefersEncryption: true}}, pp.PexOutgoingConn | pp.PexPrefersEncryption},
    +		{&PeerConn{Peer: Peer{RemoteAddr: udpAddr, Network: udpAddr.Network()}}, pp.PexSupportsUtp},
    +		{&PeerConn{Peer: Peer{RemoteAddr: udpAddr, Network: udpAddr.Network(), outgoing: true}}, pp.PexOutgoingConn | pp.PexSupportsUtp},
    +		{&PeerConn{Peer: Peer{RemoteAddr: tcpAddr, Network: tcpAddr.Network(), outgoing: true}}, pp.PexOutgoingConn},
    +		{&PeerConn{Peer: Peer{RemoteAddr: tcpAddr, Network: tcpAddr.Network()}}, 0},
    +	}
    +	for i, tc := range testcases {
    +		f := tc.conn.pexPeerFlags()
    +		require.EqualValues(t, tc.f, f, i)
    +	}
    +}
    +
    +func TestConnPexEvent(t *testing.T) {
    +	c := qt.New(t)
    +	var (
    +		udpAddr     = &net.UDPAddr{IP: net.IPv6loopback, Port: 4848}
    +		tcpAddr     = &net.TCPAddr{IP: net.IPv6loopback, Port: 4848}
    +		dialTcpAddr = &net.TCPAddr{IP: net.IPv6loopback, Port: 4747}
    +		dialUdpAddr = &net.UDPAddr{IP: net.IPv6loopback, Port: 4747}
    +	)
    +	testcases := []struct {
    +		t pexEventType
    +		c *PeerConn
    +		e pexEvent
    +	}{
    +		{
    +			pexAdd,
    +			&PeerConn{Peer: Peer{RemoteAddr: udpAddr, Network: udpAddr.Network()}},
    +			pexEvent{pexAdd, udpAddr.AddrPort(), pp.PexSupportsUtp, nil},
    +		},
    +		{
    +			pexDrop,
    +			&PeerConn{
    +				Peer:           Peer{RemoteAddr: tcpAddr, Network: tcpAddr.Network(), outgoing: true},
    +				PeerListenPort: dialTcpAddr.Port,
    +			},
    +			pexEvent{pexDrop, tcpAddr.AddrPort(), pp.PexOutgoingConn, nil},
    +		},
    +		{
    +			pexAdd,
    +			&PeerConn{
    +				Peer:           Peer{RemoteAddr: tcpAddr, Network: tcpAddr.Network()},
    +				PeerListenPort: dialTcpAddr.Port,
    +			},
    +			pexEvent{pexAdd, dialTcpAddr.AddrPort(), 0, nil},
    +		},
    +		{
    +			pexDrop,
    +			&PeerConn{
    +				Peer:           Peer{RemoteAddr: udpAddr, Network: udpAddr.Network()},
    +				PeerListenPort: dialUdpAddr.Port,
    +			},
    +			pexEvent{pexDrop, dialUdpAddr.AddrPort(), pp.PexSupportsUtp, nil},
    +		},
    +	}
    +	for i, tc := range testcases {
    +		c.Run(fmt.Sprintf("%v", i), func(c *qt.C) {
    +			e, err := tc.c.pexEvent(tc.t)
    +			c.Assert(err, qt.IsNil)
    +			c.Check(e, qt.Equals, tc.e)
    +		})
    +	}
    +}
    +
    +func TestHaveAllThenBitfield(t *testing.T) {
    +	c := qt.New(t)
    +	cl := newTestingClient(t)
    +	tt := cl.newTorrentForTesting()
    +	// cl.newConnection()
    +	pc := PeerConn{
    +		Peer: Peer{t: tt},
    +	}
    +	pc.initRequestState()
    +	pc.peerImpl = &pc
    +	tt.conns[&pc] = struct{}{}
    +	c.Assert(pc.onPeerSentHaveAll(), qt.IsNil)
    +	c.Check(pc.t.connsWithAllPieces, qt.DeepEquals, map[*Peer]struct{}{&pc.Peer: {}})
    +	pc.peerSentBitfield([]bool{false, false, true, false, true, true, false, false})
    +	c.Check(pc.peerMinPieces, qt.Equals, 6)
    +	c.Check(pc.t.connsWithAllPieces, qt.HasLen, 0)
    +	c.Assert(pc.t.setInfo(&metainfo.Info{
    +		PieceLength: 0,
    +		Pieces:      make([]byte, pieceHash.Size()*7),
    +	}), qt.IsNil)
    +	pc.t.onSetInfo()
    +	c.Check(tt.numPieces(), qt.Equals, 7)
    +	c.Check(tt.pieceAvailabilityRuns(), qt.DeepEquals, []pieceAvailabilityRun{
    +		// The last element of the bitfield is irrelevant, as the Torrent actually only has 7
    +		// pieces.
    +		{2, 0}, {1, 1}, {1, 0}, {2, 1}, {1, 0},
    +	})
    +}
    +
    +func TestApplyRequestStateWriteBufferConstraints(t *testing.T) {
    +	c := qt.New(t)
    +	c.Check(interestedMsgLen, qt.Equals, 5)
    +	c.Check(requestMsgLen, qt.Equals, 17)
    +	c.Check(maxLocalToRemoteRequests >= 8, qt.IsTrue)
    +	c.Logf("max local to remote requests: %v", maxLocalToRemoteRequests)
    +}
    +
    +func peerConnForPreferredNetworkDirection(
    +	localPeerId, remotePeerId int,
    +	outgoing, utp, ipv6 bool,
    +) *PeerConn {
    +	pc := PeerConn{}
    +	pc.outgoing = outgoing
    +	if utp {
    +		pc.Network = "udp"
    +	}
    +	if ipv6 {
    +		pc.RemoteAddr = &net.TCPAddr{IP: net.ParseIP("::420")}
    +	} else {
    +		pc.RemoteAddr = &net.TCPAddr{IP: net.IPv4(1, 2, 3, 4)}
    +	}
    +	binary.BigEndian.PutUint64(pc.PeerID[:], uint64(remotePeerId))
    +	cl := Client{}
    +	binary.BigEndian.PutUint64(cl.peerID[:], uint64(localPeerId))
    +	pc.t = &Torrent{cl: &cl}
    +	return &pc
    +}
    +
    +func TestPreferredNetworkDirection(t *testing.T) {
    +	pc := peerConnForPreferredNetworkDirection
    +	c := qt.New(t)
    +
    +	// Prefer outgoing to lower peer ID
    +
    +	c.Check(
    +		pc(1, 2, true, false, false).hasPreferredNetworkOver(pc(1, 2, false, false, false)),
    +		qt.IsFalse,
    +	)
    +	c.Check(
    +		pc(1, 2, false, false, false).hasPreferredNetworkOver(pc(1, 2, true, false, false)),
    +		qt.IsTrue,
    +	)
    +	c.Check(
    +		pc(2, 1, false, false, false).hasPreferredNetworkOver(pc(2, 1, true, false, false)),
    +		qt.IsFalse,
    +	)
    +
    +	// Don't prefer uTP
    +	c.Check(
    +		pc(1, 2, false, true, false).hasPreferredNetworkOver(pc(1, 2, false, false, false)),
    +		qt.IsFalse,
    +	)
    +	// Prefer IPv6
    +	c.Check(
    +		pc(1, 2, false, false, false).hasPreferredNetworkOver(pc(1, 2, false, false, true)),
    +		qt.IsFalse,
    +	)
    +	// No difference
    +	c.Check(
    +		pc(1, 2, false, false, false).hasPreferredNetworkOver(pc(1, 2, false, false, false)),
    +		qt.IsFalse,
    +	)
    +}
    +
    +func TestReceiveLargeRequest(t *testing.T) {
    +	c := qt.New(t)
    +	cl := newTestingClient(t)
    +	pc := cl.newConnection(nil, newConnectionOpts{network: "test"})
    +	tor := cl.newTorrentForTesting()
    +	tor.info = &metainfo.Info{PieceLength: 3 << 20}
    +	pc.setTorrent(tor)
    +	tor._completedPieces.Add(0)
    +	pc.PeerExtensionBytes.SetBit(pp.ExtensionBitFast, true)
    +	pc.choking = false
    +	pc.initMessageWriter()
    +	req := Request{}
    +	req.Length = defaultChunkSize
    +	c.Assert(pc.fastEnabled(), qt.IsTrue)
    +	c.Check(pc.onReadRequest(req, false), qt.IsNil)
    +	c.Check(pc.peerRequests, qt.HasLen, 1)
    +	req.Length = 2 << 20
    +	c.Check(pc.onReadRequest(req, false), qt.IsNil)
    +	c.Check(pc.peerRequests, qt.HasLen, 2)
    +	pc.peerRequests = nil
    +	pc.t.cl.config.UploadRateLimiter = rate.NewLimiter(1, defaultChunkSize)
    +	req.Length = defaultChunkSize
    +	c.Check(pc.onReadRequest(req, false), qt.IsNil)
    +	c.Check(pc.peerRequests, qt.HasLen, 1)
    +	req.Length = 2 << 20
    +	c.Check(pc.onReadRequest(req, false), qt.IsNil)
    +	c.Check(pc.messageWriter.writeBuffer.Len(), qt.Equals, 17)
    +}
    +
    +func TestChunkOverflowsPiece(t *testing.T) {
    +	c := qt.New(t)
    +	check := func(begin, length, limit pp.Integer, expected bool) {
    +		c.Check(chunkOverflowsPiece(ChunkSpec{begin, length}, limit), qt.Equals, expected)
    +	}
    +	check(2, 3, 1, true)
    +	check(2, pp.IntegerMax, 1, true)
    +	check(2, pp.IntegerMax, 3, true)
    +	check(2, pp.IntegerMax, pp.IntegerMax, true)
    +	check(2, pp.IntegerMax-2, pp.IntegerMax, false)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/peerid.go b/deps/github.com/anacrolix/torrent/peerid.go
    new file mode 100644
    index 0000000..301c0e9
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peerid.go
    @@ -0,0 +1,5 @@
    +package torrent
    +
    +import "github.com/anacrolix/torrent/types"
    +
    +type PeerID = types.PeerID
    diff --git a/deps/github.com/anacrolix/torrent/peerid_test.go b/deps/github.com/anacrolix/torrent/peerid_test.go
    new file mode 100644
    index 0000000..bcf0999
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/peerid_test.go
    @@ -0,0 +1,16 @@
    +package torrent
    +
    +// func TestPeerIdString(t *testing.T) {
    +// 	for _, _case := range []struct {
    +// 		id string
    +// 		s  string
    +// 	}{
    +// 		{"\x1cNJ}\x9c\xc7\xc4o\x94<\x9b\x8c\xc2!I\x1c\a\xec\x98n", "\"\x1cNJ}\x9c\xc7\xc4o\x94<\x9b\x8c\xc2!I\x1c\a\xec\x98n\""},
    +// 		{"-FD51W\xe4-LaZMk0N8ZLA7", "-FD51W\xe4-LaZMk0N8ZLA7"},
    +// 	} {
    +// 		var pi PeerID
    +// 		missinggo.CopyExact(&pi, _case.id)
    +// 		assert.EqualValues(t, _case.s, pi.String())
    +// 		assert.EqualValues(t, fmt.Sprintf("%q", _case.s), fmt.Sprintf("%q", pi))
    +// 	}
    +// }
    diff --git a/deps/github.com/anacrolix/torrent/pending-requests.go b/deps/github.com/anacrolix/torrent/pending-requests.go
    new file mode 100644
    index 0000000..dcb1faf
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/pending-requests.go
    @@ -0,0 +1,50 @@
    +package torrent
    +
    +import (
    +	rbm "github.com/RoaringBitmap/roaring"
    +	roaring "github.com/RoaringBitmap/roaring/BitSliceIndexing"
    +)
    +
    +type pendingRequests struct {
    +	m *roaring.BSI
    +}
    +
    +func (p *pendingRequests) Dec(r RequestIndex) {
    +	_r := uint64(r)
    +	prev, _ := p.m.GetValue(_r)
    +	if prev <= 0 {
    +		panic(prev)
    +	}
    +	p.m.SetValue(_r, prev-1)
    +}
    +
    +func (p *pendingRequests) Inc(r RequestIndex) {
    +	_r := uint64(r)
    +	prev, _ := p.m.GetValue(_r)
    +	p.m.SetValue(_r, prev+1)
    +}
    +
    +func (p *pendingRequests) Init(maxIndex RequestIndex) {
    +	p.m = roaring.NewDefaultBSI()
    +}
    +
    +var allBits rbm.Bitmap
    +
    +func init() {
    +	allBits.AddRange(0, rbm.MaxRange)
    +}
    +
    +func (p *pendingRequests) AssertEmpty() {
    +	if p.m == nil {
    +		panic(p.m)
    +	}
    +	sum, _ := p.m.Sum(&allBits)
    +	if sum != 0 {
    +		panic(sum)
    +	}
    +}
    +
    +func (p *pendingRequests) Get(r RequestIndex) int {
    +	count, _ := p.m.GetValue(uint64(r))
    +	return int(count)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/pending-requests_test.go b/deps/github.com/anacrolix/torrent/pending-requests_test.go
    new file mode 100644
    index 0000000..6c9572e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/pending-requests_test.go
    @@ -0,0 +1,12 @@
    +package torrent
    +
    +// // Ensure that cmp.Diff will detect errors as required.
    +// func TestPendingRequestsDiff(t *testing.T) {
    +// 	var a, b pendingRequests
    +// 	c := qt.New(t)
    +// 	diff := func() string { return cmp.Diff(a.m, b.m) }
    +// 	c.Check(diff(), qt.ContentEquals, "")
    +// 	a.m = []int{1, 3}
    +// 	b.m = []int{1, 2, 3}
    +// 	c.Check(diff(), qt.Not(qt.Equals), "")
    +// }
    diff --git a/deps/github.com/anacrolix/torrent/pex.go b/deps/github.com/anacrolix/torrent/pex.go
    new file mode 100644
    index 0000000..a0a5f49
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/pex.go
    @@ -0,0 +1,246 @@
    +package torrent
    +
    +import (
    +	"net"
    +	"net/netip"
    +	"sync"
    +	"time"
    +
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +type pexEventType int
    +
    +const (
    +	pexAdd pexEventType = iota
    +	pexDrop
    +)
    +
    +// internal, based on BEP11
    +const (
    +	pexTargAdded = 25 // put drops on hold when the number of alive connections is lower than this
    +	pexMaxHold   = 25 // length of the drop hold-back buffer
    +	pexMaxDelta  = 50 // upper bound on added+added6 and dropped+dropped6 in a single PEX message
    +)
    +
    +// represents a single connection (t=pexAdd) or disconnection (t=pexDrop) event
    +type pexEvent struct {
    +	t    pexEventType
    +	addr netip.AddrPort
    +	f    pp.PexPeerFlags
    +	next *pexEvent // event feed list
    +}
    +
    +// facilitates efficient de-duplication while generating PEX messages
    +type pexMsgFactory struct {
    +	msg     pp.PexMsg
    +	added   map[netip.AddrPort]struct{}
    +	dropped map[netip.AddrPort]struct{}
    +}
    +
    +func (me *pexMsgFactory) DeltaLen() int {
    +	return int(max(
    +		int64(len(me.added)),
    +		int64(len(me.dropped))))
    +}
    +
    +type addrKey = netip.AddrPort
    +
    +// Returns the key to use to identify a given addr in the factory.
    +func (me *pexMsgFactory) addrKey(addr netip.AddrPort) addrKey {
    +	return addr
    +}
    +
    +// Returns whether the entry was added (we can check if we're cancelling out another entry and so
    +// won't hit the limit consuming this event).
    +func (me *pexMsgFactory) add(e pexEvent) {
    +	key := me.addrKey(e.addr)
    +	if _, ok := me.added[key]; ok {
    +		return
    +	}
    +	if me.added == nil {
    +		me.added = make(map[addrKey]struct{}, pexMaxDelta)
    +	}
    +	addr := krpcNodeAddrFromAddrPort(e.addr)
    +	m := &me.msg
    +	switch {
    +	case addr.IP.To4() != nil:
    +		if _, ok := me.dropped[key]; ok {
    +			if i := m.Dropped.Index(addr); i >= 0 {
    +				m.Dropped = append(m.Dropped[:i], m.Dropped[i+1:]...)
    +			}
    +			delete(me.dropped, key)
    +			return
    +		}
    +		m.Added = append(m.Added, addr)
    +		m.AddedFlags = append(m.AddedFlags, e.f)
    +	case len(addr.IP) == net.IPv6len:
    +		if _, ok := me.dropped[key]; ok {
    +			if i := m.Dropped6.Index(addr); i >= 0 {
    +				m.Dropped6 = append(m.Dropped6[:i], m.Dropped6[i+1:]...)
    +			}
    +			delete(me.dropped, key)
    +			return
    +		}
    +		m.Added6 = append(m.Added6, addr)
    +		m.Added6Flags = append(m.Added6Flags, e.f)
    +	default:
    +		panic(addr)
    +	}
    +	me.added[key] = struct{}{}
    +}
    +
    +// Returns whether the entry was added (we can check if we're cancelling out another entry and so
    +// won't hit the limit consuming this event).
    +func (me *pexMsgFactory) drop(e pexEvent) {
    +	addr := krpcNodeAddrFromAddrPort(e.addr)
    +	key := me.addrKey(e.addr)
    +	if me.dropped == nil {
    +		me.dropped = make(map[addrKey]struct{}, pexMaxDelta)
    +	}
    +	if _, ok := me.dropped[key]; ok {
    +		return
    +	}
    +	m := &me.msg
    +	switch {
    +	case addr.IP.To4() != nil:
    +		if _, ok := me.added[key]; ok {
    +			if i := m.Added.Index(addr); i >= 0 {
    +				m.Added = append(m.Added[:i], m.Added[i+1:]...)
    +				m.AddedFlags = append(m.AddedFlags[:i], m.AddedFlags[i+1:]...)
    +			}
    +			delete(me.added, key)
    +			return
    +		}
    +		m.Dropped = append(m.Dropped, addr)
    +	case len(addr.IP) == net.IPv6len:
    +		if _, ok := me.added[key]; ok {
    +			if i := m.Added6.Index(addr); i >= 0 {
    +				m.Added6 = append(m.Added6[:i], m.Added6[i+1:]...)
    +				m.Added6Flags = append(m.Added6Flags[:i], m.Added6Flags[i+1:]...)
    +			}
    +			delete(me.added, key)
    +			return
    +		}
    +		m.Dropped6 = append(m.Dropped6, addr)
    +	}
    +	me.dropped[key] = struct{}{}
    +}
    +
    +func (me *pexMsgFactory) append(event pexEvent) {
    +	switch event.t {
    +	case pexAdd:
    +		me.add(event)
    +	case pexDrop:
    +		me.drop(event)
    +	default:
    +		panic(event.t)
    +	}
    +}
    +
    +func (me *pexMsgFactory) PexMsg() *pp.PexMsg {
    +	return &me.msg
    +}
    +
    +// Per-torrent PEX state
    +type pexState struct {
    +	sync.RWMutex
    +	tail *pexEvent  // event feed list
    +	hold []pexEvent // delayed drops
    +	// Torrent-wide cooldown deadline on inbound. This exists to prevent PEX from drowning out other
    +	// peer address sources, until that is fixed.
    +	rest time.Time
    +	nc   int           // net number of alive conns
    +	msg0 pexMsgFactory // initial message
    +}
    +
    +// Reset wipes the state clean, releasing resources. Called from Torrent.Close().
    +func (s *pexState) Reset() {
    +	s.Lock()
    +	defer s.Unlock()
    +	s.tail = nil
    +	s.hold = nil
    +	s.nc = 0
    +	s.rest = time.Time{}
    +	s.msg0 = pexMsgFactory{}
    +}
    +
    +func (s *pexState) append(e *pexEvent) {
    +	if s.tail != nil {
    +		s.tail.next = e
    +	}
    +	s.tail = e
    +	s.msg0.append(*e)
    +}
    +
    +func (s *pexState) Add(c *PeerConn) {
    +	e, err := c.pexEvent(pexAdd)
    +	if err != nil {
    +		return
    +	}
    +	s.Lock()
    +	defer s.Unlock()
    +	s.nc++
    +	if s.nc >= pexTargAdded {
    +		for _, e := range s.hold {
    +			ne := e
    +			s.append(&ne)
    +		}
    +		s.hold = s.hold[:0]
    +	}
    +	c.pex.Listed = true
    +	s.append(&e)
    +}
    +
    +func (s *pexState) Drop(c *PeerConn) {
    +	if !c.pex.Listed {
    +		// skip connections which were not previously Added
    +		return
    +	}
    +	e, err := c.pexEvent(pexDrop)
    +	if err != nil {
    +		return
    +	}
    +	s.Lock()
    +	defer s.Unlock()
    +	s.nc--
    +	if s.nc < pexTargAdded && len(s.hold) < pexMaxHold {
    +		s.hold = append(s.hold, e)
    +	} else {
    +		s.append(&e)
    +	}
    +}
    +
    +// Generate a PEX message based on the event feed.
    +// Also returns a pointer to pass to the subsequent calls
    +// to produce incremental deltas.
    +func (s *pexState) Genmsg(start *pexEvent) (pp.PexMsg, *pexEvent) {
    +	s.RLock()
    +	defer s.RUnlock()
    +	if start == nil {
    +		return *s.msg0.PexMsg(), s.tail
    +	}
    +	var msg pexMsgFactory
    +	last := start
    +	for e := start.next; e != nil; e = e.next {
    +		if msg.DeltaLen() >= pexMaxDelta {
    +			break
    +		}
    +		msg.append(*e)
    +		last = e
    +	}
    +	return *msg.PexMsg(), last
    +}
    +
    +// The same as Genmsg but just counts up the distinct events that haven't been sent.
    +func (s *pexState) numPending(start *pexEvent) (num int) {
    +	s.RLock()
    +	defer s.RUnlock()
    +	if start == nil {
    +		return s.msg0.PexMsg().Len()
    +	}
    +	for e := start.next; e != nil; e = e.next {
    +		num++
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/pex_test.go b/deps/github.com/anacrolix/torrent/pex_test.go
    new file mode 100644
    index 0000000..089e0df
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/pex_test.go
    @@ -0,0 +1,327 @@
    +package torrent
    +
    +import (
    +	"net"
    +	"testing"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +var (
    +	addrs6 = []net.Addr{
    +		&net.TCPAddr{IP: net.IPv6loopback, Port: 4747},
    +		&net.TCPAddr{IP: net.IPv6loopback, Port: 4748},
    +		&net.TCPAddr{IP: net.IPv6loopback, Port: 4749},
    +		&net.TCPAddr{IP: net.IPv6loopback, Port: 4750},
    +	}
    +	addrs4 = []net.Addr{
    +		&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 4747},
    +		&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 4748},
    +		&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 4749},
    +		&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 4750},
    +	}
    +	addrs = []net.Addr{
    +		addrs6[0],
    +		addrs6[1],
    +		addrs4[0],
    +		addrs4[1],
    +	}
    +)
    +
    +func TestPexReset(t *testing.T) {
    +	s := &pexState{}
    +	conns := []PeerConn{
    +		{Peer: Peer{RemoteAddr: addrs[0]}},
    +		{Peer: Peer{RemoteAddr: addrs[1]}},
    +		{Peer: Peer{RemoteAddr: addrs[2]}},
    +	}
    +	s.Add(&conns[0])
    +	s.Add(&conns[1])
    +	s.Drop(&conns[0])
    +	s.Reset()
    +	targ := new(pexState)
    +	require.EqualValues(t, targ, s)
    +}
    +
    +func krpcNodeAddrFromNetAddr(addr net.Addr) krpc.NodeAddr {
    +	addrPort, err := addrPortFromPeerRemoteAddr(addr)
    +	if err != nil {
    +		panic(err)
    +	}
    +	return krpcNodeAddrFromAddrPort(addrPort)
    +}
    +
    +var testcases = []struct {
    +	name   string
    +	in     *pexState
    +	targ   pp.PexMsg
    +	update func(*pexState)
    +	targ1  pp.PexMsg
    +}{
    +	{
    +		name: "empty",
    +		in:   &pexState{},
    +		targ: pp.PexMsg{},
    +	},
    +	{
    +		name: "add0",
    +		in: func() *pexState {
    +			s := new(pexState)
    +			nullAddr := &net.TCPAddr{}
    +			s.Add(&PeerConn{Peer: Peer{RemoteAddr: nullAddr}})
    +			return s
    +		}(),
    +		targ: pp.PexMsg{},
    +	},
    +	{
    +		name: "drop0",
    +		in: func() *pexState {
    +			nullAddr := &net.TCPAddr{}
    +			s := new(pexState)
    +			s.Drop(&PeerConn{Peer: Peer{RemoteAddr: nullAddr}, pex: pexConnState{Listed: true}})
    +			return s
    +		}(),
    +		targ: pp.PexMsg{},
    +	},
    +	{
    +		name: "add4",
    +		in: func() *pexState {
    +			s := new(pexState)
    +			s.Add(&PeerConn{Peer: Peer{RemoteAddr: addrs[0]}})
    +			s.Add(&PeerConn{Peer: Peer{RemoteAddr: addrs[1], outgoing: true}})
    +			s.Add(&PeerConn{Peer: Peer{RemoteAddr: addrs[2], outgoing: true}})
    +			s.Add(&PeerConn{Peer: Peer{RemoteAddr: addrs[3]}})
    +			return s
    +		}(),
    +		targ: pp.PexMsg{
    +			Added: krpc.CompactIPv4NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[2]),
    +				krpcNodeAddrFromNetAddr(addrs[3]),
    +			},
    +			AddedFlags: []pp.PexPeerFlags{pp.PexOutgoingConn, 0},
    +			Added6: krpc.CompactIPv6NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[0]),
    +				krpcNodeAddrFromNetAddr(addrs[1]),
    +			},
    +			Added6Flags: []pp.PexPeerFlags{0, pp.PexOutgoingConn},
    +		},
    +	},
    +	{
    +		name: "drop2",
    +		in: func() *pexState {
    +			s := &pexState{nc: pexTargAdded + 2}
    +			s.Drop(&PeerConn{Peer: Peer{RemoteAddr: addrs[0]}, pex: pexConnState{Listed: true}})
    +			s.Drop(&PeerConn{Peer: Peer{RemoteAddr: addrs[2]}, pex: pexConnState{Listed: true}})
    +			return s
    +		}(),
    +		targ: pp.PexMsg{
    +			Dropped: krpc.CompactIPv4NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[2]),
    +			},
    +			Dropped6: krpc.CompactIPv6NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[0]),
    +			},
    +		},
    +	},
    +	{
    +		name: "add2drop1",
    +		in: func() *pexState {
    +			conns := []PeerConn{
    +				{Peer: Peer{RemoteAddr: addrs[0]}},
    +				{Peer: Peer{RemoteAddr: addrs[1]}},
    +				{Peer: Peer{RemoteAddr: addrs[2]}},
    +			}
    +			s := &pexState{nc: pexTargAdded}
    +			s.Add(&conns[0])
    +			s.Add(&conns[1])
    +			s.Drop(&conns[0])
    +			s.Drop(&conns[2]) // to be ignored: it wasn't added
    +			return s
    +		}(),
    +		targ: pp.PexMsg{
    +			Added6: krpc.CompactIPv6NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[1]),
    +			},
    +			Added6Flags: []pp.PexPeerFlags{0},
    +		},
    +	},
    +	{
    +		name: "delayed",
    +		in: func() *pexState {
    +			conns := []PeerConn{
    +				{Peer: Peer{RemoteAddr: addrs[0]}},
    +				{Peer: Peer{RemoteAddr: addrs[1]}},
    +				{Peer: Peer{RemoteAddr: addrs[2]}},
    +			}
    +			s := new(pexState)
    +			s.Add(&conns[0])
    +			s.Add(&conns[1])
    +			s.Add(&conns[2])
    +			s.Drop(&conns[0]) // on hold: s.nc < pexTargAdded
    +			s.Drop(&conns[2])
    +			s.Drop(&conns[1])
    +			return s
    +		}(),
    +		targ: pp.PexMsg{
    +			Added: krpc.CompactIPv4NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[2]),
    +			},
    +			AddedFlags: []pp.PexPeerFlags{0},
    +			Added6: krpc.CompactIPv6NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[0]),
    +				krpcNodeAddrFromNetAddr(addrs[1]),
    +			},
    +			Added6Flags: []pp.PexPeerFlags{0, 0},
    +		},
    +	},
    +	{
    +		name: "unheld",
    +		in: func() *pexState {
    +			conns := []PeerConn{
    +				{Peer: Peer{RemoteAddr: addrs[0]}},
    +				{Peer: Peer{RemoteAddr: addrs[1]}},
    +			}
    +			s := &pexState{nc: pexTargAdded - 1}
    +			s.Add(&conns[0])
    +			s.Drop(&conns[0]) // on hold: s.nc < pexTargAdded
    +			s.Add(&conns[1])  // unholds the above
    +			return s
    +		}(),
    +		targ: pp.PexMsg{
    +			Added6: krpc.CompactIPv6NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[1]),
    +			},
    +			Added6Flags: []pp.PexPeerFlags{0},
    +		},
    +	},
    +	{
    +		name: "followup",
    +		in: func() *pexState {
    +			s := new(pexState)
    +			s.Add(&PeerConn{Peer: Peer{RemoteAddr: addrs[0]}})
    +			return s
    +		}(),
    +		targ: pp.PexMsg{
    +			Added6: krpc.CompactIPv6NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[0]),
    +			},
    +			Added6Flags: []pp.PexPeerFlags{0},
    +		},
    +		update: func(s *pexState) {
    +			s.Add(&PeerConn{Peer: Peer{RemoteAddr: addrs[1]}})
    +		},
    +		targ1: pp.PexMsg{
    +			Added6: krpc.CompactIPv6NodeAddrs{
    +				krpcNodeAddrFromNetAddr(addrs[1]),
    +			},
    +			Added6Flags: []pp.PexPeerFlags{0},
    +		},
    +	},
    +}
    +
    +// Represents the contents of a PexMsg in a way that supports equivalence checking in tests. This is
    +// necessary because pexMsgFactory uses maps and so ordering of the resultant PexMsg isn't
    +// deterministic. Because the flags are in a different array, we can't just use testify's
    +// ElementsMatch because the ordering *does* still matter between an added addr and its flags.
    +type comparablePexMsg struct {
    +	added, added6           []krpc.NodeAddr
    +	addedFlags, added6Flags []pp.PexPeerFlags
    +	dropped, dropped6       []krpc.NodeAddr
    +}
    +
    +// Such Rust-inspired.
    +func (me *comparablePexMsg) From(f pp.PexMsg) {
    +	me.added = f.Added
    +	me.addedFlags = f.AddedFlags
    +	me.added6 = f.Added6
    +	me.added6Flags = f.Added6Flags
    +	me.dropped = f.Dropped
    +	me.dropped6 = f.Dropped6
    +}
    +
    +// For PexMsg created by pexMsgFactory, this is as good as it can get without using data structures
    +// in pexMsgFactory that preserve insert ordering.
    +func (actual comparablePexMsg) AssertEqual(t *testing.T, expected comparablePexMsg) {
    +	assert.ElementsMatch(t, expected.added, actual.added)
    +	assert.ElementsMatch(t, expected.addedFlags, actual.addedFlags)
    +	assert.ElementsMatch(t, expected.added6, actual.added6)
    +	assert.ElementsMatch(t, expected.added6Flags, actual.added6Flags)
    +	assert.ElementsMatch(t, expected.dropped, actual.dropped)
    +	assert.ElementsMatch(t, expected.dropped6, actual.dropped6)
    +}
    +
    +func assertPexMsgsEqual(t *testing.T, expected, actual pp.PexMsg) {
    +	var ec, ac comparablePexMsg
    +	ec.From(expected)
    +	ac.From(actual)
    +	ac.AssertEqual(t, ec)
    +}
    +
    +func TestPexGenmsg0(t *testing.T) {
    +	for _, tc := range testcases {
    +		t.Run(tc.name, func(t *testing.T) {
    +			s := *tc.in
    +			m, last := s.Genmsg(nil)
    +			assertPexMsgsEqual(t, tc.targ, m)
    +			if tc.update != nil {
    +				tc.update(&s)
    +				m1, last := s.Genmsg(last)
    +				assertPexMsgsEqual(t, tc.targ1, m1)
    +				assert.NotNil(t, last)
    +			}
    +		})
    +	}
    +}
    +
    +// generate 𝑛 distinct values of net.Addr
    +func addrgen(n int) chan net.Addr {
    +	c := make(chan net.Addr)
    +	go func() {
    +		defer close(c)
    +		for i := 4747; i < 65535 && n > 0; i++ {
    +			c <- &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: i}
    +			n--
    +		}
    +	}()
    +	return c
    +}
    +
    +func TestPexInitialNoCutoff(t *testing.T) {
    +	const n = 2 * pexMaxDelta
    +	var s pexState
    +
    +	c := addrgen(n)
    +	for addr := range c {
    +		s.Add(&PeerConn{Peer: Peer{RemoteAddr: addr}})
    +	}
    +	m, _ := s.Genmsg(nil)
    +
    +	require.EqualValues(t, n, len(m.Added))
    +	require.EqualValues(t, n, len(m.AddedFlags))
    +	require.EqualValues(t, 0, len(m.Added6))
    +	require.EqualValues(t, 0, len(m.Added6Flags))
    +	require.EqualValues(t, 0, len(m.Dropped))
    +	require.EqualValues(t, 0, len(m.Dropped6))
    +}
    +
    +func benchmarkPexInitialN(b *testing.B, npeers int) {
    +	for i := 0; i < b.N; i++ {
    +		var s pexState
    +		c := addrgen(npeers)
    +		for addr := range c {
    +			s.Add(&PeerConn{Peer: Peer{RemoteAddr: addr}})
    +			s.Genmsg(nil)
    +		}
    +	}
    +}
    +
    +// obtain at least 5 points, e.g. to plot a graph
    +func BenchmarkPexInitial4(b *testing.B)   { benchmarkPexInitialN(b, 4) }
    +func BenchmarkPexInitial50(b *testing.B)  { benchmarkPexInitialN(b, 50) }
    +func BenchmarkPexInitial100(b *testing.B) { benchmarkPexInitialN(b, 100) }
    +func BenchmarkPexInitial200(b *testing.B) { benchmarkPexInitialN(b, 200) }
    +func BenchmarkPexInitial400(b *testing.B) { benchmarkPexInitialN(b, 400) }
    diff --git a/deps/github.com/anacrolix/torrent/pexconn.go b/deps/github.com/anacrolix/torrent/pexconn.go
    new file mode 100644
    index 0000000..9254f5e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/pexconn.go
    @@ -0,0 +1,168 @@
    +package torrent
    +
    +import (
    +	"fmt"
    +	"net/netip"
    +	"time"
    +
    +	g "github.com/anacrolix/generics"
    +	"github.com/anacrolix/log"
    +
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +const (
    +	pexRetryDelay = 10 * time.Second
    +	pexInterval   = 1 * time.Minute
    +)
    +
    +// per-connection PEX state
    +type pexConnState struct {
    +	enabled bool
    +	xid     pp.ExtensionNumber
    +	last    *pexEvent
    +	timer   *time.Timer
    +	gate    chan struct{}
    +	readyfn func()
    +	torrent *Torrent
    +	Listed  bool
    +	info    log.Logger
    +	dbg     log.Logger
    +	// Running record of live connections the remote end of the connection purports to have.
    +	remoteLiveConns map[netip.AddrPort]g.Option[pp.PexPeerFlags]
    +	lastRecv        time.Time
    +}
    +
    +func (s *pexConnState) IsEnabled() bool {
    +	return s.enabled
    +}
    +
    +// Init is called from the reader goroutine upon the extended handshake completion
    +func (s *pexConnState) Init(c *PeerConn) {
    +	xid, ok := c.PeerExtensionIDs[pp.ExtensionNamePex]
    +	if !ok || xid == 0 || c.t.cl.config.DisablePEX {
    +		return
    +	}
    +	s.xid = xid
    +	s.last = nil
    +	s.torrent = c.t
    +	s.info = c.t.cl.logger.WithDefaultLevel(log.Info)
    +	s.dbg = c.logger.WithDefaultLevel(log.Debug)
    +	s.readyfn = c.tickleWriter
    +	s.gate = make(chan struct{}, 1)
    +	s.timer = time.AfterFunc(0, func() {
    +		s.gate <- struct{}{}
    +		s.readyfn() // wake up the writer
    +	})
    +	s.enabled = true
    +}
    +
    +// schedule next PEX message
    +func (s *pexConnState) sched(delay time.Duration) {
    +	s.timer.Reset(delay)
    +}
    +
    +// generate next PEX message for the peer; returns nil if nothing yet to send
    +func (s *pexConnState) genmsg() *pp.PexMsg {
    +	tx, last := s.torrent.pex.Genmsg(s.last)
    +	if tx.Len() == 0 {
    +		return nil
    +	}
    +	s.last = last
    +	return &tx
    +}
    +
    +func (s *pexConnState) numPending() int {
    +	if s.torrent == nil {
    +		return 0
    +	}
    +	return s.torrent.pex.numPending(s.last)
    +}
    +
    +// Share is called from the writer goroutine if when it is woken up with the write buffers empty
    +// Returns whether there's more room on the send buffer to write to.
    +func (s *pexConnState) Share(postfn messageWriter) bool {
    +	select {
    +	case <-s.gate:
    +		if tx := s.genmsg(); tx != nil {
    +			s.dbg.Print("sending PEX message: ", tx)
    +			flow := postfn(tx.Message(s.xid))
    +			s.sched(pexInterval)
    +			return flow
    +		} else {
    +			// no PEX to send this time - try again shortly
    +			s.sched(pexRetryDelay)
    +		}
    +	default:
    +	}
    +	return true
    +}
    +
    +func (s *pexConnState) updateRemoteLiveConns(rx pp.PexMsg) (errs []error) {
    +	for _, dropped := range rx.Dropped {
    +		addrPort, _ := ipv4AddrPortFromKrpcNodeAddr(dropped)
    +		delete(s.remoteLiveConns, addrPort)
    +	}
    +	for _, dropped := range rx.Dropped6 {
    +		addrPort, _ := ipv6AddrPortFromKrpcNodeAddr(dropped)
    +		delete(s.remoteLiveConns, addrPort)
    +	}
    +	for i, added := range rx.Added {
    +		addr := netip.AddrFrom4(*(*[4]byte)(added.IP.To4()))
    +		addrPort := netip.AddrPortFrom(addr, uint16(added.Port))
    +		flags := g.SliceGet(rx.AddedFlags, i)
    +		g.MakeMapIfNilAndSet(&s.remoteLiveConns, addrPort, flags)
    +	}
    +	for i, added := range rx.Added6 {
    +		addr := netip.AddrFrom16(*(*[16]byte)(added.IP.To16()))
    +		addrPort := netip.AddrPortFrom(addr, uint16(added.Port))
    +		flags := g.SliceGet(rx.Added6Flags, i)
    +		g.MakeMapIfNilAndSet(&s.remoteLiveConns, addrPort, flags)
    +	}
    +	return
    +}
    +
    +// Recv is called from the reader goroutine
    +func (s *pexConnState) Recv(payload []byte) error {
    +	rx, err := pp.LoadPexMsg(payload)
    +	if err != nil {
    +		return fmt.Errorf("unmarshalling pex message: %w", err)
    +	}
    +	s.dbg.Printf("received pex message: %v", rx)
    +	torrent.Add("pex added peers received", int64(len(rx.Added)))
    +	torrent.Add("pex added6 peers received", int64(len(rx.Added6)))
    +
    +	// "Clients must batch updates to send no more than 1 PEX message per minute."
    +	timeSinceLastRecv := time.Since(s.lastRecv)
    +	if timeSinceLastRecv < 45*time.Second {
    +		return fmt.Errorf("last received only %v ago", timeSinceLastRecv)
    +	}
    +	s.lastRecv = time.Now()
    +	s.updateRemoteLiveConns(rx)
    +
    +	var peers peerInfos
    +	peers.AppendFromPex(rx.Added6, rx.Added6Flags)
    +	peers.AppendFromPex(rx.Added, rx.AddedFlags)
    +	if time.Now().Before(s.torrent.pex.rest) {
    +		s.dbg.Printf("in cooldown period, incoming PEX discarded")
    +		return nil
    +	}
    +	added := s.torrent.addPeers(peers)
    +	s.dbg.Printf("got %v peers over pex, added %v", len(peers), added)
    +
    +	if len(peers) > 0 {
    +		s.torrent.pex.rest = time.Now().Add(pexInterval)
    +	}
    +
    +	// one day we may also want to:
    +	// - handle drops somehow
    +	// - detect malicious peers
    +
    +	return nil
    +}
    +
    +func (s *pexConnState) Close() {
    +	if s.timer != nil {
    +		s.timer.Stop()
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/pexconn_test.go b/deps/github.com/anacrolix/torrent/pexconn_test.go
    new file mode 100644
    index 0000000..f8b9c9e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/pexconn_test.go
    @@ -0,0 +1,61 @@
    +package torrent
    +
    +import (
    +	"net"
    +	"testing"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +func TestPexConnState(t *testing.T) {
    +	var cl Client
    +	cl.init(TestingConfig(t))
    +	cl.initLogger()
    +	torrent := cl.newTorrent(metainfo.Hash{}, nil)
    +	addr := &net.TCPAddr{IP: net.IPv6loopback, Port: 4747}
    +	c := cl.newConnection(nil, newConnectionOpts{
    +		remoteAddr: addr,
    +		network:    addr.Network(),
    +	})
    +	c.PeerExtensionIDs = make(map[pp.ExtensionName]pp.ExtensionNumber)
    +	c.PeerExtensionIDs[pp.ExtensionNamePex] = pexExtendedId
    +	c.messageWriter.mu.Lock()
    +	c.setTorrent(torrent)
    +	if err := torrent.addPeerConn(c); err != nil {
    +		t.Log(err)
    +	}
    +
    +	connWriteCond := c.messageWriter.writeCond.Signaled()
    +	c.pex.Init(c)
    +	require.True(t, c.pex.IsEnabled(), "should get enabled")
    +	defer c.pex.Close()
    +
    +	var out pp.Message
    +	writerCalled := false
    +	testWriter := func(m pp.Message) bool {
    +		writerCalled = true
    +		out = m
    +		return true
    +	}
    +	<-connWriteCond
    +	c.pex.Share(testWriter)
    +	require.True(t, writerCalled)
    +	require.EqualValues(t, pp.Extended, out.Type)
    +	require.EqualValues(t, pexExtendedId, out.ExtendedID)
    +
    +	x, err := pp.LoadPexMsg(out.ExtendedPayload)
    +	require.NoError(t, err)
    +	targx := pp.PexMsg{
    +		Added:      krpc.CompactIPv4NodeAddrs(nil),
    +		AddedFlags: []pp.PexPeerFlags{},
    +		Added6: krpc.CompactIPv6NodeAddrs{
    +			krpcNodeAddrFromNetAddr(addr),
    +		},
    +		Added6Flags: []pp.PexPeerFlags{0},
    +	}
    +	require.EqualValues(t, targx, x)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/piece.go b/deps/github.com/anacrolix/torrent/piece.go
    new file mode 100644
    index 0000000..e08b260
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/piece.go
    @@ -0,0 +1,257 @@
    +package torrent
    +
    +import (
    +	"fmt"
    +	"sync"
    +
    +	"github.com/anacrolix/chansync"
    +	"github.com/anacrolix/missinggo/v2/bitmap"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +type Piece struct {
    +	// The completed piece SHA1 hash, from the metainfo "pieces" field.
    +	hash  *metainfo.Hash
    +	t     *Torrent
    +	index pieceIndex
    +	files []*File
    +
    +	readerCond chansync.BroadcastCond
    +
    +	numVerifies         int64
    +	hashing             bool
    +	marking             bool
    +	storageCompletionOk bool
    +
    +	publicPieceState PieceState
    +	priority         piecePriority
    +	// Availability adjustment for this piece relative to len(Torrent.connsWithAllPieces). This is
    +	// incremented for any piece a peer has when a peer has a piece, Torrent.haveInfo is true, and
    +	// the Peer isn't recorded in Torrent.connsWithAllPieces.
    +	relativeAvailability int
    +
    +	// This can be locked when the Client lock is taken, but probably not vice versa.
    +	pendingWritesMutex sync.Mutex
    +	pendingWrites      int
    +	noPendingWrites    sync.Cond
    +
    +	// Connections that have written data to this piece since its last check.
    +	// This can include connections that have closed.
    +	dirtiers map[*Peer]struct{}
    +}
    +
    +func (p *Piece) String() string {
    +	return fmt.Sprintf("%s/%d", p.t.infoHash.HexString(), p.index)
    +}
    +
    +func (p *Piece) Info() metainfo.Piece {
    +	return p.t.info.Piece(int(p.index))
    +}
    +
    +func (p *Piece) Storage() storage.Piece {
    +	return p.t.storage.Piece(p.Info())
    +}
    +
    +func (p *Piece) Flush() {
    +	if p.t.storage.Flush != nil {
    +		_ = p.t.storage.Flush()
    +	}
    +}
    +
    +func (p *Piece) pendingChunkIndex(chunkIndex chunkIndexType) bool {
    +	return !p.chunkIndexDirty(chunkIndex)
    +}
    +
    +func (p *Piece) pendingChunk(cs ChunkSpec, chunkSize pp.Integer) bool {
    +	return p.pendingChunkIndex(chunkIndexFromChunkSpec(cs, chunkSize))
    +}
    +
    +func (p *Piece) hasDirtyChunks() bool {
    +	return p.numDirtyChunks() != 0
    +}
    +
    +func (p *Piece) numDirtyChunks() chunkIndexType {
    +	return chunkIndexType(roaringBitmapRangeCardinality[RequestIndex](
    +		&p.t.dirtyChunks,
    +		p.requestIndexOffset(),
    +		p.t.pieceRequestIndexOffset(p.index+1)))
    +}
    +
    +func (p *Piece) unpendChunkIndex(i chunkIndexType) {
    +	p.t.dirtyChunks.Add(p.requestIndexOffset() + i)
    +	p.t.updatePieceRequestOrder(p.index)
    +	p.readerCond.Broadcast()
    +}
    +
    +func (p *Piece) pendChunkIndex(i RequestIndex) {
    +	p.t.dirtyChunks.Remove(p.requestIndexOffset() + i)
    +	p.t.updatePieceRequestOrder(p.index)
    +}
    +
    +func (p *Piece) numChunks() chunkIndexType {
    +	return p.t.pieceNumChunks(p.index)
    +}
    +
    +func (p *Piece) incrementPendingWrites() {
    +	p.pendingWritesMutex.Lock()
    +	p.pendingWrites++
    +	p.pendingWritesMutex.Unlock()
    +}
    +
    +func (p *Piece) decrementPendingWrites() {
    +	p.pendingWritesMutex.Lock()
    +	if p.pendingWrites == 0 {
    +		panic("assertion")
    +	}
    +	p.pendingWrites--
    +	if p.pendingWrites == 0 {
    +		p.noPendingWrites.Broadcast()
    +	}
    +	p.pendingWritesMutex.Unlock()
    +}
    +
    +func (p *Piece) waitNoPendingWrites() {
    +	p.pendingWritesMutex.Lock()
    +	for p.pendingWrites != 0 {
    +		p.noPendingWrites.Wait()
    +	}
    +	p.pendingWritesMutex.Unlock()
    +}
    +
    +func (p *Piece) chunkIndexDirty(chunk chunkIndexType) bool {
    +	return p.t.dirtyChunks.Contains(p.requestIndexOffset() + chunk)
    +}
    +
    +func (p *Piece) chunkIndexSpec(chunk chunkIndexType) ChunkSpec {
    +	return chunkIndexSpec(pp.Integer(chunk), p.length(), p.chunkSize())
    +}
    +
    +func (p *Piece) numDirtyBytes() (ret pp.Integer) {
    +	// defer func() {
    +	// 	if ret > p.length() {
    +	// 		panic("too many dirty bytes")
    +	// 	}
    +	// }()
    +	numRegularDirtyChunks := p.numDirtyChunks()
    +	if p.chunkIndexDirty(p.numChunks() - 1) {
    +		numRegularDirtyChunks--
    +		ret += p.chunkIndexSpec(p.lastChunkIndex()).Length
    +	}
    +	ret += pp.Integer(numRegularDirtyChunks) * p.chunkSize()
    +	return
    +}
    +
    +func (p *Piece) length() pp.Integer {
    +	return p.t.pieceLength(p.index)
    +}
    +
    +func (p *Piece) chunkSize() pp.Integer {
    +	return p.t.chunkSize
    +}
    +
    +func (p *Piece) lastChunkIndex() chunkIndexType {
    +	return p.numChunks() - 1
    +}
    +
    +func (p *Piece) bytesLeft() (ret pp.Integer) {
    +	if p.t.pieceComplete(p.index) {
    +		return 0
    +	}
    +	return p.length() - p.numDirtyBytes()
    +}
    +
    +// Forces the piece data to be rehashed.
    +func (p *Piece) VerifyData() {
    +	p.t.cl.lock()
    +	defer p.t.cl.unlock()
    +	target := p.numVerifies + 1
    +	if p.hashing {
    +		target++
    +	}
    +	// log.Printf("target: %d", target)
    +	p.t.queuePieceCheck(p.index)
    +	for {
    +		// log.Printf("got %d verifies", p.numVerifies)
    +		if p.numVerifies >= target {
    +			break
    +		}
    +		p.t.cl.event.Wait()
    +	}
    +	// log.Print("done")
    +}
    +
    +func (p *Piece) queuedForHash() bool {
    +	return p.t.piecesQueuedForHash.Get(bitmap.BitIndex(p.index))
    +}
    +
    +func (p *Piece) torrentBeginOffset() int64 {
    +	return int64(p.index) * p.t.info.PieceLength
    +}
    +
    +func (p *Piece) torrentEndOffset() int64 {
    +	return p.torrentBeginOffset() + int64(p.length())
    +}
    +
    +func (p *Piece) SetPriority(prio piecePriority) {
    +	p.t.cl.lock()
    +	defer p.t.cl.unlock()
    +	p.priority = prio
    +	p.t.updatePiecePriority(p.index, "Piece.SetPriority")
    +}
    +
    +func (p *Piece) purePriority() (ret piecePriority) {
    +	for _, f := range p.files {
    +		ret.Raise(f.prio)
    +	}
    +	if p.t.readerNowPieces().Contains(bitmap.BitIndex(p.index)) {
    +		ret.Raise(PiecePriorityNow)
    +	}
    +	// if t._readerNowPieces.Contains(piece - 1) {
    +	// 	return PiecePriorityNext
    +	// }
    +	if p.t.readerReadaheadPieces().Contains(bitmap.BitIndex(p.index)) {
    +		ret.Raise(PiecePriorityReadahead)
    +	}
    +	ret.Raise(p.priority)
    +	return
    +}
    +
    +func (p *Piece) uncachedPriority() (ret piecePriority) {
    +	if p.hashing || p.marking || p.t.pieceComplete(p.index) || p.queuedForHash() {
    +		return PiecePriorityNone
    +	}
    +	return p.purePriority()
    +}
    +
    +// Tells the Client to refetch the completion status from storage, updating priority etc. if
    +// necessary. Might be useful if you know the state of the piece data has changed externally.
    +func (p *Piece) UpdateCompletion() {
    +	p.t.cl.lock()
    +	defer p.t.cl.unlock()
    +	p.t.updatePieceCompletion(p.index)
    +}
    +
    +func (p *Piece) completion() (ret storage.Completion) {
    +	ret.Complete = p.t.pieceComplete(p.index)
    +	ret.Ok = p.storageCompletionOk
    +	return
    +}
    +
    +func (p *Piece) allChunksDirty() bool {
    +	return p.numDirtyChunks() == p.numChunks()
    +}
    +
    +func (p *Piece) State() PieceState {
    +	return p.t.PieceState(p.index)
    +}
    +
    +func (p *Piece) requestIndexOffset() RequestIndex {
    +	return p.t.pieceRequestIndexOffset(p.index)
    +}
    +
    +func (p *Piece) availability() int {
    +	return len(p.t.connsWithAllPieces) + p.relativeAvailability
    +}
    diff --git a/deps/github.com/anacrolix/torrent/piecestate.go b/deps/github.com/anacrolix/torrent/piecestate.go
    new file mode 100644
    index 0000000..089adca
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/piecestate.go
    @@ -0,0 +1,27 @@
    +package torrent
    +
    +import (
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +// The current state of a piece.
    +type PieceState struct {
    +	Priority piecePriority
    +	storage.Completion
    +	// The piece is being hashed, or is queued for hash. Deprecated: Use those fields instead.
    +	Checking bool
    +
    +	Hashing       bool
    +	QueuedForHash bool
    +	// The piece state is being marked in the storage.
    +	Marking bool
    +
    +	// Some of the piece has been obtained.
    +	Partial bool
    +}
    +
    +// Represents a series of consecutive pieces with the same state.
    +type PieceStateRun struct {
    +	PieceState
    +	Length int // How many consecutive pieces have this state.
    +}
    diff --git a/deps/github.com/anacrolix/torrent/portfwd.go b/deps/github.com/anacrolix/torrent/portfwd.go
    new file mode 100644
    index 0000000..0136578
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/portfwd.go
    @@ -0,0 +1,45 @@
    +package torrent
    +
    +import (
    +	"fmt"
    +	"time"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/upnp"
    +)
    +
    +const UpnpDiscoverLogTag = "upnp-discover"
    +
    +func (cl *Client) addPortMapping(d upnp.Device, proto upnp.Protocol, internalPort int, upnpID string) {
    +	logger := cl.logger.WithContextText(fmt.Sprintf("UPnP device at %v: mapping internal %v port %v", d.GetLocalIPAddress(), proto, internalPort))
    +	externalPort, err := d.AddPortMapping(proto, internalPort, internalPort, upnpID, 0)
    +	if err != nil {
    +		logger.WithDefaultLevel(log.Warning).Printf("error: %v", err)
    +		return
    +	}
    +	level := log.Info
    +	if externalPort != internalPort {
    +		level = log.Warning
    +	}
    +	logger.WithDefaultLevel(level).Printf("success: external port %v", externalPort)
    +}
    +
    +func (cl *Client) forwardPort() {
    +	cl.lock()
    +	defer cl.unlock()
    +	if cl.config.NoDefaultPortForwarding {
    +		return
    +	}
    +	cl.unlock()
    +	ds := upnp.Discover(0, 2*time.Second, cl.logger.WithValues(UpnpDiscoverLogTag))
    +	cl.lock()
    +	cl.logger.WithDefaultLevel(log.Debug).Printf("discovered %d upnp devices", len(ds))
    +	port := cl.incomingPeerPort()
    +	id := cl.config.UpnpID
    +	cl.unlock()
    +	for _, d := range ds {
    +		go cl.addPortMapping(d, upnp.TCP, port, id)
    +		go cl.addPortMapping(d, upnp.UDP, port, id)
    +	}
    +	cl.lock()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/prioritized-peers.go b/deps/github.com/anacrolix/torrent/prioritized-peers.go
    new file mode 100644
    index 0000000..443d720
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/prioritized-peers.go
    @@ -0,0 +1,82 @@
    +package torrent
    +
    +import (
    +	"hash/maphash"
    +
    +	"github.com/anacrolix/multiless"
    +	"github.com/google/btree"
    +)
    +
    +// Peers are stored with their priority at insertion. Their priority may
    +// change if our apparent IP changes, we don't currently handle that.
    +type prioritizedPeersItem struct {
    +	prio peerPriority
    +	p    PeerInfo
    +}
    +
    +var hashSeed = maphash.MakeSeed()
    +
    +func (me prioritizedPeersItem) addrHash() int64 {
    +	var h maphash.Hash
    +	h.SetSeed(hashSeed)
    +	h.WriteString(me.p.Addr.String())
    +	return int64(h.Sum64())
    +}
    +
    +func (me prioritizedPeersItem) Less(than btree.Item) bool {
    +	other := than.(prioritizedPeersItem)
    +	return multiless.New().Bool(
    +		me.p.Trusted, other.p.Trusted).Uint32(
    +		me.prio, other.prio).Int64(
    +		me.addrHash(), other.addrHash(),
    +	).Less()
    +}
    +
    +type prioritizedPeers struct {
    +	om      *btree.BTree
    +	getPrio func(PeerInfo) peerPriority
    +}
    +
    +func (me *prioritizedPeers) Each(f func(PeerInfo)) {
    +	me.om.Ascend(func(i btree.Item) bool {
    +		f(i.(prioritizedPeersItem).p)
    +		return true
    +	})
    +}
    +
    +func (me *prioritizedPeers) Len() int {
    +	if me == nil || me.om == nil {
    +		return 0
    +	}
    +	return me.om.Len()
    +}
    +
    +// Returns true if a peer is replaced.
    +func (me *prioritizedPeers) Add(p PeerInfo) bool {
    +	return me.om.ReplaceOrInsert(prioritizedPeersItem{me.getPrio(p), p}) != nil
    +}
    +
    +// Returns true if a peer is replaced.
    +func (me *prioritizedPeers) AddReturningReplacedPeer(p PeerInfo) (ret PeerInfo, ok bool) {
    +	item := me.om.ReplaceOrInsert(prioritizedPeersItem{me.getPrio(p), p})
    +	if item == nil {
    +		return
    +	}
    +	ret = item.(prioritizedPeersItem).p
    +	ok = true
    +	return
    +}
    +
    +func (me *prioritizedPeers) DeleteMin() (ret prioritizedPeersItem, ok bool) {
    +	i := me.om.DeleteMin()
    +	if i == nil {
    +		return
    +	}
    +	ret = i.(prioritizedPeersItem)
    +	ok = true
    +	return
    +}
    +
    +func (me *prioritizedPeers) PopMax() PeerInfo {
    +	return me.om.DeleteMax().(prioritizedPeersItem).p
    +}
    diff --git a/deps/github.com/anacrolix/torrent/prioritized-peers_test.go b/deps/github.com/anacrolix/torrent/prioritized-peers_test.go
    new file mode 100644
    index 0000000..5e61c25
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/prioritized-peers_test.go
    @@ -0,0 +1,55 @@
    +package torrent
    +
    +import (
    +	"net"
    +	"testing"
    +
    +	"github.com/google/btree"
    +	"github.com/stretchr/testify/assert"
    +)
    +
    +func TestPrioritizedPeers(t *testing.T) {
    +	pp := prioritizedPeers{
    +		om: btree.New(3),
    +		getPrio: func(p PeerInfo) peerPriority {
    +			return bep40PriorityIgnoreError(p.addr(), IpPort{IP: net.ParseIP("0.0.0.0")})
    +		},
    +	}
    +	_, ok := pp.DeleteMin()
    +	assert.Panics(t, func() { pp.PopMax() })
    +	assert.False(t, ok)
    +	ps := []PeerInfo{
    +		{Addr: ipPortAddr{IP: net.ParseIP("1.2.3.4")}},
    +		{Addr: ipPortAddr{IP: net.ParseIP("1::2")}},
    +		{Addr: ipPortAddr{IP: net.ParseIP("")}},
    +		{Addr: ipPortAddr{IP: net.ParseIP("")}, Trusted: true},
    +	}
    +	for i, p := range ps {
    +		t.Logf("peer %d priority: %08x trusted: %t\n", i, pp.getPrio(p), p.Trusted)
    +		assert.False(t, pp.Add(p))
    +		assert.True(t, pp.Add(p))
    +		assert.Equal(t, i+1, pp.Len())
    +	}
    +	pop := func(expected *PeerInfo) {
    +		if expected == nil {
    +			assert.Panics(t, func() { pp.PopMax() })
    +		} else {
    +			assert.Equal(t, *expected, pp.PopMax())
    +		}
    +	}
    +	min := func(expected *PeerInfo) {
    +		i, ok := pp.DeleteMin()
    +		if expected == nil {
    +			assert.False(t, ok)
    +		} else {
    +			assert.True(t, ok)
    +			assert.Equal(t, *expected, i.p)
    +		}
    +	}
    +	pop(&ps[3])
    +	pop(&ps[1])
    +	min(&ps[2])
    +	pop(&ps[0])
    +	min(nil)
    +	pop(nil)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/protocol.go b/deps/github.com/anacrolix/torrent/protocol.go
    new file mode 100644
    index 0000000..82e36d3
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/protocol.go
    @@ -0,0 +1,9 @@
    +package torrent
    +
    +import (
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +func makeCancelMessage(r Request) pp.Message {
    +	return pp.MakeCancelMessage(r.Index, r.Begin, r.Length)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/ratelimitreader.go b/deps/github.com/anacrolix/torrent/ratelimitreader.go
    new file mode 100644
    index 0000000..7d9e6d8
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/ratelimitreader.go
    @@ -0,0 +1,53 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"fmt"
    +	"io"
    +	"time"
    +
    +	"golang.org/x/time/rate"
    +)
    +
    +type rateLimitedReader struct {
    +	l *rate.Limiter
    +	r io.Reader
    +
    +	// This is the time of the last Read's reservation.
    +	lastRead time.Time
    +}
    +
    +func (me *rateLimitedReader) Read(b []byte) (n int, err error) {
    +	const oldStyle = false // Retained for future reference.
    +	if oldStyle {
    +		// Wait until we can read at all.
    +		if err := me.l.WaitN(context.Background(), 1); err != nil {
    +			panic(err)
    +		}
    +		// Limit the read to within the burst.
    +		if me.l.Limit() != rate.Inf && len(b) > me.l.Burst() {
    +			b = b[:me.l.Burst()]
    +		}
    +		n, err = me.r.Read(b)
    +		// Pay the piper.
    +		now := time.Now()
    +		me.lastRead = now
    +		if !me.l.ReserveN(now, n-1).OK() {
    +			panic(fmt.Sprintf("burst exceeded?: %d", n-1))
    +		}
    +	} else {
    +		// Limit the read to within the burst.
    +		if me.l.Limit() != rate.Inf && len(b) > me.l.Burst() {
    +			b = b[:me.l.Burst()]
    +		}
    +		n, err = me.r.Read(b)
    +		now := time.Now()
    +		r := me.l.ReserveN(now, n)
    +		if !r.OK() {
    +			panic(n)
    +		}
    +		me.lastRead = now
    +		time.Sleep(r.Delay())
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/reader.go b/deps/github.com/anacrolix/torrent/reader.go
    new file mode 100644
    index 0000000..4b20206
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/reader.go
    @@ -0,0 +1,332 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"sync"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/v2"
    +)
    +
    +// Accesses Torrent data via a Client. Reads block until the data is available. Seeks and readahead
    +// also drive Client behaviour. Not safe for concurrent use.
    +type Reader interface {
    +	io.ReadSeekCloser
    +	missinggo.ReadContexter
    +	// Configure the number of bytes ahead of a read that should also be prioritized in preparation
    +	// for further reads. Overridden by non-nil readahead func, see SetReadaheadFunc.
    +	SetReadahead(int64)
    +	// If non-nil, the provided function is called when the implementation needs to know the
    +	// readahead for the current reader. Calls occur during Reads and Seeks, and while the Client is
    +	// locked.
    +	SetReadaheadFunc(ReadaheadFunc)
    +	// Don't wait for pieces to complete and be verified. Read calls return as soon as they can when
    +	// the underlying chunks become available.
    +	SetResponsive()
    +}
    +
    +// Piece range by piece index, [begin, end).
    +type pieceRange struct {
    +	begin, end pieceIndex
    +}
    +
    +type ReadaheadContext struct {
    +	ContiguousReadStartPos int64
    +	CurrentPos             int64
    +}
    +
    +// Returns the desired readahead for a Reader.
    +type ReadaheadFunc func(ReadaheadContext) int64
    +
    +type reader struct {
    +	t *Torrent
    +	// Adjust the read/seek window to handle Readers locked to File extents and the like.
    +	offset, length int64
    +
    +	// Function to dynamically calculate readahead. If nil, readahead is static.
    +	readaheadFunc ReadaheadFunc
    +
    +	// Required when modifying pos and readahead.
    +	mu sync.Locker
    +
    +	readahead, pos int64
    +	// Position that reads have continued contiguously from.
    +	contiguousReadStartPos int64
    +	// The cached piece range this reader wants downloaded. The zero value corresponds to nothing.
    +	// We cache this so that changes can be detected, and bubbled up to the Torrent only as
    +	// required.
    +	pieces pieceRange
    +
    +	// Reads have been initiated since the last seek. This is used to prevent readaheads occurring
    +	// after a seek or with a new reader at the starting position.
    +	reading    bool
    +	responsive bool
    +}
    +
    +var _ io.ReadSeekCloser = (*reader)(nil)
    +
    +func (r *reader) SetResponsive() {
    +	r.responsive = true
    +	r.t.cl.event.Broadcast()
    +}
    +
    +// Disable responsive mode. TODO: Remove?
    +func (r *reader) SetNonResponsive() {
    +	r.responsive = false
    +	r.t.cl.event.Broadcast()
    +}
    +
    +func (r *reader) SetReadahead(readahead int64) {
    +	r.mu.Lock()
    +	r.readahead = readahead
    +	r.readaheadFunc = nil
    +	r.posChanged()
    +	r.mu.Unlock()
    +}
    +
    +func (r *reader) SetReadaheadFunc(f ReadaheadFunc) {
    +	r.mu.Lock()
    +	r.readaheadFunc = f
    +	r.posChanged()
    +	r.mu.Unlock()
    +}
    +
    +// How many bytes are available to read. Max is the most we could require.
    +func (r *reader) available(off, max int64) (ret int64) {
    +	off += r.offset
    +	for max > 0 {
    +		req, ok := r.t.offsetRequest(off)
    +		if !ok {
    +			break
    +		}
    +		if !r.responsive && !r.t.pieceComplete(pieceIndex(req.Index)) {
    +			break
    +		}
    +		if !r.t.haveChunk(req) {
    +			break
    +		}
    +		len1 := int64(req.Length) - (off - r.t.requestOffset(req))
    +		max -= len1
    +		ret += len1
    +		off += len1
    +	}
    +	// Ensure that ret hasn't exceeded our original max.
    +	if max < 0 {
    +		ret += max
    +	}
    +	return
    +}
    +
    +// Calculates the pieces this reader wants downloaded, ignoring the cached value at r.pieces.
    +func (r *reader) piecesUncached() (ret pieceRange) {
    +	ra := r.readahead
    +	if r.readaheadFunc != nil {
    +		ra = r.readaheadFunc(ReadaheadContext{
    +			ContiguousReadStartPos: r.contiguousReadStartPos,
    +			CurrentPos:             r.pos,
    +		})
    +	}
    +	if ra < 1 {
    +		// Needs to be at least 1, because [x, x) means we don't want
    +		// anything.
    +		ra = 1
    +	}
    +	if !r.reading {
    +		ra = 0
    +	}
    +	if ra > r.length-r.pos {
    +		ra = r.length - r.pos
    +	}
    +	ret.begin, ret.end = r.t.byteRegionPieces(r.torrentOffset(r.pos), ra)
    +	return
    +}
    +
    +func (r *reader) Read(b []byte) (n int, err error) {
    +	return r.ReadContext(context.Background(), b)
    +}
    +
    +func (r *reader) ReadContext(ctx context.Context, b []byte) (n int, err error) {
    +	if len(b) > 0 {
    +		r.reading = true
    +		// TODO: Rework reader piece priorities so we don't have to push updates in to the Client
    +		// and take the lock here.
    +		r.mu.Lock()
    +		r.posChanged()
    +		r.mu.Unlock()
    +	}
    +	n, err = r.readOnceAt(ctx, b, r.pos)
    +	if n == 0 {
    +		if err == nil && len(b) > 0 {
    +			panic("expected error")
    +		} else {
    +			return
    +		}
    +	}
    +
    +	r.mu.Lock()
    +	r.pos += int64(n)
    +	r.posChanged()
    +	r.mu.Unlock()
    +	if r.pos >= r.length {
    +		err = io.EOF
    +	} else if err == io.EOF {
    +		err = io.ErrUnexpectedEOF
    +	}
    +	return
    +}
    +
    +var closedChan = make(chan struct{})
    +
    +func init() {
    +	close(closedChan)
    +}
    +
    +// Wait until some data should be available to read. Tickles the client if it isn't. Returns how
    +// much should be readable without blocking.
    +func (r *reader) waitAvailable(ctx context.Context, pos, wanted int64, wait bool) (avail int64, err error) {
    +	t := r.t
    +	for {
    +		r.t.cl.rLock()
    +		avail = r.available(pos, wanted)
    +		readerCond := t.piece(int((r.offset + pos) / t.info.PieceLength)).readerCond.Signaled()
    +		r.t.cl.rUnlock()
    +		if avail != 0 {
    +			return
    +		}
    +		var dontWait <-chan struct{}
    +		if !wait || wanted == 0 {
    +			dontWait = closedChan
    +		}
    +		select {
    +		case <-r.t.closed.Done():
    +			err = errors.New("torrent closed")
    +			return
    +		case <-ctx.Done():
    +			err = ctx.Err()
    +			return
    +		case <-r.t.dataDownloadDisallowed.On():
    +			err = errors.New("torrent data downloading disabled")
    +		case <-r.t.networkingEnabled.Off():
    +			err = errors.New("torrent networking disabled")
    +			return
    +		case <-dontWait:
    +			return
    +		case <-readerCond:
    +		}
    +	}
    +}
    +
    +// Adds the reader's torrent offset to the reader object offset (for example the reader might be
    +// constrainted to a particular file within the torrent).
    +func (r *reader) torrentOffset(readerPos int64) int64 {
    +	return r.offset + readerPos
    +}
    +
    +// Performs at most one successful read to torrent storage.
    +func (r *reader) readOnceAt(ctx context.Context, b []byte, pos int64) (n int, err error) {
    +	if pos >= r.length {
    +		err = io.EOF
    +		return
    +	}
    +	for {
    +		var avail int64
    +		avail, err = r.waitAvailable(ctx, pos, int64(len(b)), n == 0)
    +		if avail == 0 {
    +			return
    +		}
    +		firstPieceIndex := pieceIndex(r.torrentOffset(pos) / r.t.info.PieceLength)
    +		firstPieceOffset := r.torrentOffset(pos) % r.t.info.PieceLength
    +		b1 := missinggo.LimitLen(b, avail)
    +		n, err = r.t.readAt(b1, r.torrentOffset(pos))
    +		if n != 0 {
    +			err = nil
    +			return
    +		}
    +		if r.t.closed.IsSet() {
    +			err = fmt.Errorf("reading from closed torrent: %w", err)
    +			return
    +		}
    +		r.t.cl.lock()
    +		// I think there's a panic here caused by the Client being closed before obtaining this
    +		// lock. TestDropTorrentWithMmapStorageWhileHashing seems to tickle occasionally in CI.
    +		func() {
    +			// Just add exceptions already.
    +			defer r.t.cl.unlock()
    +			if r.t.closed.IsSet() {
    +				// Can't update because Torrent's piece order is removed from Client.
    +				return
    +			}
    +			// TODO: Just reset pieces in the readahead window. This might help
    +			// prevent thrashing with small caches and file and piece priorities.
    +			r.log(log.Fstr("error reading torrent %s piece %d offset %d, %d bytes: %v",
    +				r.t.infoHash.HexString(), firstPieceIndex, firstPieceOffset, len(b1), err))
    +			if !r.t.updatePieceCompletion(firstPieceIndex) {
    +				r.log(log.Fstr("piece %d completion unchanged", firstPieceIndex))
    +			}
    +			// Update the rest of the piece completions in the readahead window, without alerting to
    +			// changes (since only the first piece, the one above, could have generated the read error
    +			// we're currently handling).
    +			if r.pieces.begin != firstPieceIndex {
    +				panic(fmt.Sprint(r.pieces.begin, firstPieceIndex))
    +			}
    +			for index := r.pieces.begin + 1; index < r.pieces.end; index++ {
    +				r.t.updatePieceCompletion(index)
    +			}
    +		}()
    +	}
    +}
    +
    +// Hodor
    +func (r *reader) Close() error {
    +	r.t.cl.lock()
    +	r.t.deleteReader(r)
    +	r.t.cl.unlock()
    +	return nil
    +}
    +
    +func (r *reader) posChanged() {
    +	to := r.piecesUncached()
    +	from := r.pieces
    +	if to == from {
    +		return
    +	}
    +	r.pieces = to
    +	// log.Printf("reader pos changed %v->%v", from, to)
    +	r.t.readerPosChanged(from, to)
    +}
    +
    +func (r *reader) Seek(off int64, whence int) (newPos int64, err error) {
    +	switch whence {
    +	case io.SeekStart:
    +		newPos = off
    +		r.mu.Lock()
    +	case io.SeekCurrent:
    +		r.mu.Lock()
    +		newPos = r.pos + off
    +	case io.SeekEnd:
    +		newPos = r.length + off
    +		r.mu.Lock()
    +	default:
    +		return 0, errors.New("bad whence")
    +	}
    +	if newPos != r.pos {
    +		r.reading = false
    +		r.pos = newPos
    +		r.contiguousReadStartPos = newPos
    +		r.posChanged()
    +	}
    +	r.mu.Unlock()
    +	return
    +}
    +
    +func (r *reader) log(m log.Msg) {
    +	r.t.logger.LogLevel(log.Debug, m.Skip(1))
    +}
    +
    +// Implementation inspired by https://news.ycombinator.com/item?id=27019613.
    +func defaultReadaheadFunc(r ReadaheadContext) int64 {
    +	return r.CurrentPos - r.ContiguousReadStartPos
    +}
    diff --git a/deps/github.com/anacrolix/torrent/reader_test.go b/deps/github.com/anacrolix/torrent/reader_test.go
    new file mode 100644
    index 0000000..c1017a0
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/reader_test.go
    @@ -0,0 +1,26 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"testing"
    +	"time"
    +
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/internal/testutil"
    +)
    +
    +func TestReaderReadContext(t *testing.T) {
    +	cl, err := NewClient(TestingConfig(t))
    +	require.NoError(t, err)
    +	defer cl.Close()
    +	tt, err := cl.AddTorrent(testutil.GreetingMetaInfo())
    +	require.NoError(t, err)
    +	defer tt.Drop()
    +	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond))
    +	defer cancel()
    +	r := tt.Files()[0].NewReader()
    +	defer r.Close()
    +	_, err = r.ReadContext(ctx, make([]byte, 1))
    +	require.EqualValues(t, context.DeadlineExceeded, err)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy-impls.go b/deps/github.com/anacrolix/torrent/request-strategy-impls.go
    new file mode 100644
    index 0000000..0b05ed4
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy-impls.go
    @@ -0,0 +1,77 @@
    +package torrent
    +
    +import (
    +	"github.com/anacrolix/torrent/metainfo"
    +	request_strategy "github.com/anacrolix/torrent/request-strategy"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +type requestStrategyInput struct {
    +	cl      *Client
    +	capFunc storage.TorrentCapacity
    +}
    +
    +func (r requestStrategyInput) Torrent(ih metainfo.Hash) request_strategy.Torrent {
    +	return requestStrategyTorrent{r.cl.torrents[ih]}
    +}
    +
    +func (r requestStrategyInput) Capacity() (int64, bool) {
    +	if r.capFunc == nil {
    +		return 0, false
    +	}
    +	return (*r.capFunc)()
    +}
    +
    +func (r requestStrategyInput) MaxUnverifiedBytes() int64 {
    +	return r.cl.config.MaxUnverifiedBytes
    +}
    +
    +var _ request_strategy.Input = requestStrategyInput{}
    +
    +// Returns what is necessary to run request_strategy.GetRequestablePieces for primaryTorrent.
    +func (cl *Client) getRequestStrategyInput(primaryTorrent *Torrent) (input request_strategy.Input) {
    +	return requestStrategyInput{
    +		cl:      cl,
    +		capFunc: primaryTorrent.storage.Capacity,
    +	}
    +}
    +
    +func (t *Torrent) getRequestStrategyInput() request_strategy.Input {
    +	return t.cl.getRequestStrategyInput(t)
    +}
    +
    +type requestStrategyTorrent struct {
    +	t *Torrent
    +}
    +
    +func (r requestStrategyTorrent) IgnorePiece(i int) bool {
    +	if r.t.ignorePieceForRequests(i) {
    +		return true
    +	}
    +	if r.t.pieceNumPendingChunks(i) == 0 {
    +		return true
    +	}
    +
    +	return false
    +}
    +
    +func (r requestStrategyTorrent) PieceLength() int64 {
    +	return r.t.info.PieceLength
    +}
    +
    +var _ request_strategy.Torrent = requestStrategyTorrent{}
    +
    +type requestStrategyPiece struct {
    +	t *Torrent
    +	i pieceIndex
    +}
    +
    +func (r requestStrategyPiece) Request() bool {
    +	return !r.t.ignorePieceForRequests(r.i)
    +}
    +
    +func (r requestStrategyPiece) NumPendingChunks() int {
    +	return int(r.t.pieceNumPendingChunks(r.i))
    +}
    +
    +var _ request_strategy.Piece = requestStrategyPiece{}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy/ajwerner-btree.go b/deps/github.com/anacrolix/torrent/request-strategy/ajwerner-btree.go
    new file mode 100644
    index 0000000..183e2b9
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy/ajwerner-btree.go
    @@ -0,0 +1,44 @@
    +package requestStrategy
    +
    +import (
    +	"github.com/ajwerner/btree"
    +)
    +
    +type ajwernerBtree struct {
    +	btree btree.Set[pieceRequestOrderItem]
    +}
    +
    +var _ Btree = (*ajwernerBtree)(nil)
    +
    +func NewAjwernerBtree() *ajwernerBtree {
    +	return &ajwernerBtree{
    +		btree: btree.MakeSet(func(t, t2 pieceRequestOrderItem) int {
    +			return pieceOrderLess(&t, &t2).OrderingInt()
    +		}),
    +	}
    +}
    +
    +func mustValue[V any](b bool, panicValue V) {
    +	if !b {
    +		panic(panicValue)
    +	}
    +}
    +
    +func (a *ajwernerBtree) Delete(item pieceRequestOrderItem) {
    +	mustValue(a.btree.Delete(item), item)
    +}
    +
    +func (a *ajwernerBtree) Add(item pieceRequestOrderItem) {
    +	_, overwrote := a.btree.Upsert(item)
    +	mustValue(!overwrote, item)
    +}
    +
    +func (a *ajwernerBtree) Scan(f func(pieceRequestOrderItem) bool) {
    +	it := a.btree.Iterator()
    +	it.First()
    +	for it.First(); it.Valid(); it.Next() {
    +		if !f(it.Cur()) {
    +			break
    +		}
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy/order.go b/deps/github.com/anacrolix/torrent/request-strategy/order.go
    new file mode 100644
    index 0000000..df656f6
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy/order.go
    @@ -0,0 +1,99 @@
    +package requestStrategy
    +
    +import (
    +	"bytes"
    +	"expvar"
    +
    +	g "github.com/anacrolix/generics"
    +	"github.com/anacrolix/multiless"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	"github.com/anacrolix/torrent/types"
    +)
    +
    +type (
    +	RequestIndex  uint32
    +	ChunkIndex    = RequestIndex
    +	Request       = types.Request
    +	pieceIndex    = types.PieceIndex
    +	piecePriority = types.PiecePriority
    +	// This can be made into a type-param later, will be great for testing.
    +	ChunkSpec = types.ChunkSpec
    +)
    +
    +func pieceOrderLess(i, j *pieceRequestOrderItem) multiless.Computation {
    +	return multiless.New().Int(
    +		int(j.state.Priority), int(i.state.Priority),
    +		// TODO: Should we match on complete here to prevent churn when availability changes?
    +	).Bool(
    +		j.state.Partial, i.state.Partial,
    +	).Int(
    +		// If this is done with relative availability, do we lose some determinism? If completeness
    +		// is used, would that push this far enough down?
    +		i.state.Availability, j.state.Availability,
    +	).Int(
    +		i.key.Index, j.key.Index,
    +	).Lazy(func() multiless.Computation {
    +		return multiless.New().Cmp(bytes.Compare(
    +			i.key.InfoHash[:],
    +			j.key.InfoHash[:],
    +		))
    +	})
    +}
    +
    +var packageExpvarMap = expvar.NewMap("request-strategy")
    +
    +// Calls f with requestable pieces in order.
    +func GetRequestablePieces(
    +	input Input, pro *PieceRequestOrder,
    +	f func(ih metainfo.Hash, pieceIndex int, orderState PieceRequestOrderState),
    +) {
    +	// Storage capacity left for this run, keyed by the storage capacity pointer on the storage
    +	// TorrentImpl. A nil value means no capacity limit.
    +	var storageLeft *int64
    +	if cap, ok := input.Capacity(); ok {
    +		storageLeft = &cap
    +	}
    +	var allTorrentsUnverifiedBytes int64
    +	var lastItem g.Option[pieceRequestOrderItem]
    +	pro.tree.Scan(func(_i pieceRequestOrderItem) bool {
    +		// Check that scan emits pieces in priority order.
    +		if lastItem.Ok {
    +			if _i.Less(&lastItem.Value) {
    +				panic("scan not in order")
    +			}
    +		}
    +		lastItem.Set(_i)
    +
    +		ih := _i.key.InfoHash
    +		t := input.Torrent(ih)
    +		pieceLength := t.PieceLength()
    +		if storageLeft != nil {
    +			if *storageLeft < pieceLength {
    +				return false
    +			}
    +			*storageLeft -= pieceLength
    +		}
    +		if t.IgnorePiece(_i.key.Index) {
    +			// TODO: Clarify exactly what is verified. Stuff that's being hashed should be
    +			// considered unverified and hold up further requests.
    +			return true
    +		}
    +		if input.MaxUnverifiedBytes() != 0 && allTorrentsUnverifiedBytes+pieceLength > input.MaxUnverifiedBytes() {
    +			return true
    +		}
    +		allTorrentsUnverifiedBytes += pieceLength
    +		f(ih, _i.key.Index, _i.state)
    +		return true
    +	})
    +	return
    +}
    +
    +type Input interface {
    +	Torrent(metainfo.Hash) Torrent
    +	// Storage capacity, shared among all Torrents with the same storage.TorrentCapacity pointer in
    +	// their storage.Torrent references.
    +	Capacity() (cap int64, capped bool)
    +	// Across all the Torrents. This might be partitioned by storage capacity key now.
    +	MaxUnverifiedBytes() int64
    +}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy/peer.go b/deps/github.com/anacrolix/torrent/request-strategy/peer.go
    new file mode 100644
    index 0000000..4176188
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy/peer.go
    @@ -0,0 +1,32 @@
    +package requestStrategy
    +
    +import (
    +	typedRoaring "github.com/anacrolix/torrent/typed-roaring"
    +)
    +
    +type PeerRequestState struct {
    +	Interested bool
    +	Requests   PeerRequests
    +	// Cancelled and waiting response
    +	Cancelled typedRoaring.Bitmap[RequestIndex]
    +}
    +
    +// A set of request indices iterable by order added.
    +type PeerRequests interface {
    +	// Can be more efficient than GetCardinality.
    +	IsEmpty() bool
    +	// See roaring.Bitmap.GetCardinality.
    +	GetCardinality() uint64
    +	Contains(RequestIndex) bool
    +	// Should not adjust iteration order if item already exists, although I don't think that usage
    +	// exists.
    +	Add(RequestIndex)
    +	// See roaring.Bitmap.Rank.
    +	Rank(RequestIndex) uint64
    +	// Must yield in order items were added.
    +	Iterate(func(RequestIndex) bool)
    +	// See roaring.Bitmap.CheckedRemove.
    +	CheckedRemove(RequestIndex) bool
    +	// Iterate a snapshot of the values. It is safe to mutate the underlying data structure.
    +	IterateSnapshot(func(RequestIndex) bool)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy/piece-request-order.go b/deps/github.com/anacrolix/torrent/request-strategy/piece-request-order.go
    new file mode 100644
    index 0000000..3056741
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy/piece-request-order.go
    @@ -0,0 +1,81 @@
    +package requestStrategy
    +
    +import "github.com/anacrolix/torrent/metainfo"
    +
    +type Btree interface {
    +	Delete(pieceRequestOrderItem)
    +	Add(pieceRequestOrderItem)
    +	Scan(func(pieceRequestOrderItem) bool)
    +}
    +
    +func NewPieceOrder(btree Btree, cap int) *PieceRequestOrder {
    +	return &PieceRequestOrder{
    +		tree: btree,
    +		keys: make(map[PieceRequestOrderKey]PieceRequestOrderState, cap),
    +	}
    +}
    +
    +type PieceRequestOrder struct {
    +	tree Btree
    +	keys map[PieceRequestOrderKey]PieceRequestOrderState
    +}
    +
    +type PieceRequestOrderKey struct {
    +	InfoHash metainfo.Hash
    +	Index    int
    +}
    +
    +type PieceRequestOrderState struct {
    +	Priority     piecePriority
    +	Partial      bool
    +	Availability int
    +}
    +
    +type pieceRequestOrderItem struct {
    +	key   PieceRequestOrderKey
    +	state PieceRequestOrderState
    +}
    +
    +func (me *pieceRequestOrderItem) Less(otherConcrete *pieceRequestOrderItem) bool {
    +	return pieceOrderLess(me, otherConcrete).Less()
    +}
    +
    +func (me *PieceRequestOrder) Add(key PieceRequestOrderKey, state PieceRequestOrderState) {
    +	if _, ok := me.keys[key]; ok {
    +		panic(key)
    +	}
    +	me.tree.Add(pieceRequestOrderItem{key, state})
    +	me.keys[key] = state
    +}
    +
    +func (me *PieceRequestOrder) Update(
    +	key PieceRequestOrderKey,
    +	state PieceRequestOrderState,
    +) {
    +	oldState, ok := me.keys[key]
    +	if !ok {
    +		panic("key should have been added already")
    +	}
    +	if state == oldState {
    +		return
    +	}
    +	me.tree.Delete(pieceRequestOrderItem{key, oldState})
    +	me.tree.Add(pieceRequestOrderItem{key, state})
    +	me.keys[key] = state
    +}
    +
    +func (me *PieceRequestOrder) existingItemForKey(key PieceRequestOrderKey) pieceRequestOrderItem {
    +	return pieceRequestOrderItem{
    +		key:   key,
    +		state: me.keys[key],
    +	}
    +}
    +
    +func (me *PieceRequestOrder) Delete(key PieceRequestOrderKey) {
    +	me.tree.Delete(pieceRequestOrderItem{key, me.keys[key]})
    +	delete(me.keys, key)
    +}
    +
    +func (me *PieceRequestOrder) Len() int {
    +	return len(me.keys)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy/piece-request-order_test.go b/deps/github.com/anacrolix/torrent/request-strategy/piece-request-order_test.go
    new file mode 100644
    index 0000000..ee5fb39
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy/piece-request-order_test.go
    @@ -0,0 +1,106 @@
    +package requestStrategy
    +
    +import (
    +	"testing"
    +
    +	"github.com/bradfitz/iter"
    +)
    +
    +func benchmarkPieceRequestOrder[B Btree](
    +	b *testing.B,
    +	// Initialize the next run, and return a Btree
    +	newBtree func() B,
    +	// Set any path hinting for the specified piece
    +	hintForPiece func(index int),
    +	numPieces int,
    +) {
    +	b.ResetTimer()
    +	b.ReportAllocs()
    +	for range iter.N(b.N) {
    +		pro := NewPieceOrder(newBtree(), numPieces)
    +		state := PieceRequestOrderState{}
    +		doPieces := func(m func(PieceRequestOrderKey)) {
    +			for i := range iter.N(numPieces) {
    +				key := PieceRequestOrderKey{
    +					Index: i,
    +				}
    +				hintForPiece(i)
    +				m(key)
    +			}
    +		}
    +		doPieces(func(key PieceRequestOrderKey) {
    +			pro.Add(key, state)
    +		})
    +		state.Availability++
    +		doPieces(func(key PieceRequestOrderKey) {
    +			pro.Update(key, state)
    +		})
    +		pro.tree.Scan(func(item pieceRequestOrderItem) bool {
    +			return true
    +		})
    +		doPieces(func(key PieceRequestOrderKey) {
    +			state.Priority = piecePriority(key.Index / 4)
    +			pro.Update(key, state)
    +		})
    +		pro.tree.Scan(func(item pieceRequestOrderItem) bool {
    +			return item.key.Index < 1000
    +		})
    +		state.Priority = 0
    +		state.Availability++
    +		doPieces(func(key PieceRequestOrderKey) {
    +			pro.Update(key, state)
    +		})
    +		pro.tree.Scan(func(item pieceRequestOrderItem) bool {
    +			return item.key.Index < 1000
    +		})
    +		state.Availability--
    +		doPieces(func(key PieceRequestOrderKey) {
    +			pro.Update(key, state)
    +		})
    +		doPieces(pro.Delete)
    +		if pro.Len() != 0 {
    +			b.FailNow()
    +		}
    +	}
    +}
    +
    +func zero[T any](t *T) {
    +	var zt T
    +	*t = zt
    +}
    +
    +func BenchmarkPieceRequestOrder(b *testing.B) {
    +	const numPieces = 2000
    +	b.Run("TidwallBtree", func(b *testing.B) {
    +		b.Run("NoPathHints", func(b *testing.B) {
    +			benchmarkPieceRequestOrder(b, NewTidwallBtree, func(int) {}, numPieces)
    +		})
    +		b.Run("SharedPathHint", func(b *testing.B) {
    +			var pathHint PieceRequestOrderPathHint
    +			var btree *tidwallBtree
    +			benchmarkPieceRequestOrder(
    +				b, func() *tidwallBtree {
    +					zero(&pathHint)
    +					btree = NewTidwallBtree()
    +					btree.PathHint = &pathHint
    +					return btree
    +				}, func(int) {}, numPieces,
    +			)
    +		})
    +		b.Run("PathHintPerPiece", func(b *testing.B) {
    +			pathHints := make([]PieceRequestOrderPathHint, numPieces)
    +			var btree *tidwallBtree
    +			benchmarkPieceRequestOrder(
    +				b, func() *tidwallBtree {
    +					btree = NewTidwallBtree()
    +					return btree
    +				}, func(index int) {
    +					btree.PathHint = &pathHints[index]
    +				}, numPieces,
    +			)
    +		})
    +	})
    +	b.Run("AjwernerBtree", func(b *testing.B) {
    +		benchmarkPieceRequestOrder(b, NewAjwernerBtree, func(index int) {}, numPieces)
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy/piece.go b/deps/github.com/anacrolix/torrent/request-strategy/piece.go
    new file mode 100644
    index 0000000..b858dff
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy/piece.go
    @@ -0,0 +1,12 @@
    +package requestStrategy
    +
    +type ChunksIterFunc func(func(ChunkIndex))
    +
    +type ChunksIter interface {
    +	Iter(func(ci ChunkIndex))
    +}
    +
    +type Piece interface {
    +	Request() bool
    +	NumPendingChunks() int
    +}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy/tidwall-btree.go b/deps/github.com/anacrolix/torrent/request-strategy/tidwall-btree.go
    new file mode 100644
    index 0000000..f7eabcd
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy/tidwall-btree.go
    @@ -0,0 +1,37 @@
    +package requestStrategy
    +
    +import (
    +	"github.com/tidwall/btree"
    +)
    +
    +type tidwallBtree struct {
    +	tree     *btree.BTreeG[pieceRequestOrderItem]
    +	PathHint *btree.PathHint
    +}
    +
    +func (me *tidwallBtree) Scan(f func(pieceRequestOrderItem) bool) {
    +	me.tree.Scan(f)
    +}
    +
    +func NewTidwallBtree() *tidwallBtree {
    +	return &tidwallBtree{
    +		tree: btree.NewBTreeGOptions(
    +			func(a, b pieceRequestOrderItem) bool {
    +				return a.Less(&b)
    +			},
    +			btree.Options{NoLocks: true, Degree: 64}),
    +	}
    +}
    +
    +func (me *tidwallBtree) Add(item pieceRequestOrderItem) {
    +	if _, ok := me.tree.SetHint(item, me.PathHint); ok {
    +		panic("shouldn't already have this")
    +	}
    +}
    +
    +type PieceRequestOrderPathHint = btree.PathHint
    +
    +func (me *tidwallBtree) Delete(item pieceRequestOrderItem) {
    +	_, deleted := me.tree.DeleteHint(item, me.PathHint)
    +	mustValue(deleted, item)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/request-strategy/torrent.go b/deps/github.com/anacrolix/torrent/request-strategy/torrent.go
    new file mode 100644
    index 0000000..5bc438e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/request-strategy/torrent.go
    @@ -0,0 +1,6 @@
    +package requestStrategy
    +
    +type Torrent interface {
    +	IgnorePiece(int) bool
    +	PieceLength() int64
    +}
    diff --git a/deps/github.com/anacrolix/torrent/requesting.go b/deps/github.com/anacrolix/torrent/requesting.go
    new file mode 100644
    index 0000000..b70f264
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/requesting.go
    @@ -0,0 +1,313 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"encoding/gob"
    +	"fmt"
    +	"reflect"
    +	"runtime/pprof"
    +	"time"
    +	"unsafe"
    +
    +	"github.com/anacrolix/generics/heap"
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/multiless"
    +
    +	requestStrategy "github.com/anacrolix/torrent/request-strategy"
    +	typedRoaring "github.com/anacrolix/torrent/typed-roaring"
    +)
    +
    +type (
    +	// Since we have to store all the requests in memory, we can't reasonably exceed what could be
    +	// indexed with the memory space available.
    +	maxRequests = int
    +)
    +
    +func (t *Torrent) requestStrategyPieceOrderState(i int) requestStrategy.PieceRequestOrderState {
    +	return requestStrategy.PieceRequestOrderState{
    +		Priority:     t.piece(i).purePriority(),
    +		Partial:      t.piecePartiallyDownloaded(i),
    +		Availability: t.piece(i).availability(),
    +	}
    +}
    +
    +func init() {
    +	gob.Register(peerId{})
    +}
    +
    +type peerId struct {
    +	*Peer
    +	ptr uintptr
    +}
    +
    +func (p peerId) Uintptr() uintptr {
    +	return p.ptr
    +}
    +
    +func (p peerId) GobEncode() (b []byte, _ error) {
    +	*(*reflect.SliceHeader)(unsafe.Pointer(&b)) = reflect.SliceHeader{
    +		Data: uintptr(unsafe.Pointer(&p.ptr)),
    +		Len:  int(unsafe.Sizeof(p.ptr)),
    +		Cap:  int(unsafe.Sizeof(p.ptr)),
    +	}
    +	return
    +}
    +
    +func (p *peerId) GobDecode(b []byte) error {
    +	if uintptr(len(b)) != unsafe.Sizeof(p.ptr) {
    +		panic(len(b))
    +	}
    +	ptr := unsafe.Pointer(&b[0])
    +	p.ptr = *(*uintptr)(ptr)
    +	log.Printf("%p", ptr)
    +	dst := reflect.SliceHeader{
    +		Data: uintptr(unsafe.Pointer(&p.Peer)),
    +		Len:  int(unsafe.Sizeof(p.Peer)),
    +		Cap:  int(unsafe.Sizeof(p.Peer)),
    +	}
    +	copy(*(*[]byte)(unsafe.Pointer(&dst)), b)
    +	return nil
    +}
    +
    +type (
    +	RequestIndex   = requestStrategy.RequestIndex
    +	chunkIndexType = requestStrategy.ChunkIndex
    +)
    +
    +type desiredPeerRequests struct {
    +	requestIndexes []RequestIndex
    +	peer           *Peer
    +	pieceStates    []requestStrategy.PieceRequestOrderState
    +}
    +
    +func (p *desiredPeerRequests) lessByValue(leftRequest, rightRequest RequestIndex) bool {
    +	t := p.peer.t
    +	leftPieceIndex := t.pieceIndexOfRequestIndex(leftRequest)
    +	rightPieceIndex := t.pieceIndexOfRequestIndex(rightRequest)
    +	ml := multiless.New()
    +	// Push requests that can't be served right now to the end. But we don't throw them away unless
    +	// there's a better alternative. This is for when we're using the fast extension and get choked
    +	// but our requests could still be good when we get unchoked.
    +	if p.peer.peerChoking {
    +		ml = ml.Bool(
    +			!p.peer.peerAllowedFast.Contains(leftPieceIndex),
    +			!p.peer.peerAllowedFast.Contains(rightPieceIndex),
    +		)
    +	}
    +	leftPiece := &p.pieceStates[leftPieceIndex]
    +	rightPiece := &p.pieceStates[rightPieceIndex]
    +	// Putting this first means we can steal requests from lesser-performing peers for our first few
    +	// new requests.
    +	priority := func() piecePriority {
    +		// Technically we would be happy with the cached priority here, except we don't actually
    +		// cache it anymore, and Torrent.piecePriority just does another lookup of *Piece to resolve
    +		// the priority through Piece.purePriority, which is probably slower.
    +		leftPriority := leftPiece.Priority
    +		rightPriority := rightPiece.Priority
    +		ml = ml.Int(
    +			-int(leftPriority),
    +			-int(rightPriority),
    +		)
    +		if !ml.Ok() {
    +			if leftPriority != rightPriority {
    +				panic("expected equal")
    +			}
    +		}
    +		return leftPriority
    +	}()
    +	if ml.Ok() {
    +		return ml.MustLess()
    +	}
    +	leftRequestState := t.requestState[leftRequest]
    +	rightRequestState := t.requestState[rightRequest]
    +	leftPeer := leftRequestState.peer
    +	rightPeer := rightRequestState.peer
    +	// Prefer chunks already requested from this peer.
    +	ml = ml.Bool(rightPeer == p.peer, leftPeer == p.peer)
    +	// Prefer unrequested chunks.
    +	ml = ml.Bool(rightPeer == nil, leftPeer == nil)
    +	if ml.Ok() {
    +		return ml.MustLess()
    +	}
    +	if leftPeer != nil {
    +		// The right peer should also be set, or we'd have resolved the computation by now.
    +		ml = ml.Uint64(
    +			rightPeer.requestState.Requests.GetCardinality(),
    +			leftPeer.requestState.Requests.GetCardinality(),
    +		)
    +		// Could either of the lastRequested be Zero? That's what checking an existing peer is for.
    +		leftLast := leftRequestState.when
    +		rightLast := rightRequestState.when
    +		if leftLast.IsZero() || rightLast.IsZero() {
    +			panic("expected non-zero last requested times")
    +		}
    +		// We want the most-recently requested on the left. Clients like Transmission serve requests
    +		// in received order, so the most recently-requested is the one that has the longest until
    +		// it will be served and therefore is the best candidate to cancel.
    +		ml = ml.CmpInt64(rightLast.Sub(leftLast).Nanoseconds())
    +	}
    +	ml = ml.Int(
    +		leftPiece.Availability,
    +		rightPiece.Availability)
    +	if priority == PiecePriorityReadahead {
    +		// TODO: For readahead in particular, it would be even better to consider distance from the
    +		// reader position so that reads earlier in a torrent don't starve reads later in the
    +		// torrent. This would probably require reconsideration of how readahead priority works.
    +		ml = ml.Int(leftPieceIndex, rightPieceIndex)
    +	} else {
    +		ml = ml.Int(t.pieceRequestOrder[leftPieceIndex], t.pieceRequestOrder[rightPieceIndex])
    +	}
    +	return ml.Less()
    +}
    +
    +type desiredRequestState struct {
    +	Requests   desiredPeerRequests
    +	Interested bool
    +}
    +
    +func (p *Peer) getDesiredRequestState() (desired desiredRequestState) {
    +	t := p.t
    +	if !t.haveInfo() {
    +		return
    +	}
    +	if t.closed.IsSet() {
    +		return
    +	}
    +	if t.dataDownloadDisallowed.Bool() {
    +		return
    +	}
    +	input := t.getRequestStrategyInput()
    +	requestHeap := desiredPeerRequests{
    +		peer:           p,
    +		pieceStates:    t.requestPieceStates,
    +		requestIndexes: t.requestIndexes,
    +	}
    +	// Caller-provided allocation for roaring bitmap iteration.
    +	var it typedRoaring.Iterator[RequestIndex]
    +	requestStrategy.GetRequestablePieces(
    +		input,
    +		t.getPieceRequestOrder(),
    +		func(ih InfoHash, pieceIndex int, pieceExtra requestStrategy.PieceRequestOrderState) {
    +			if ih != t.infoHash {
    +				return
    +			}
    +			if !p.peerHasPiece(pieceIndex) {
    +				return
    +			}
    +			requestHeap.pieceStates[pieceIndex] = pieceExtra
    +			allowedFast := p.peerAllowedFast.Contains(pieceIndex)
    +			t.iterUndirtiedRequestIndexesInPiece(&it, pieceIndex, func(r requestStrategy.RequestIndex) {
    +				if !allowedFast {
    +					// We must signal interest to request this. TODO: We could set interested if the
    +					// peers pieces (minus the allowed fast set) overlap with our missing pieces if
    +					// there are any readers, or any pending pieces.
    +					desired.Interested = true
    +					// We can make or will allow sustaining a request here if we're not choked, or
    +					// have made the request previously (presumably while unchoked), and haven't had
    +					// the peer respond yet (and the request was retained because we are using the
    +					// fast extension).
    +					if p.peerChoking && !p.requestState.Requests.Contains(r) {
    +						// We can't request this right now.
    +						return
    +					}
    +				}
    +				if p.requestState.Cancelled.Contains(r) {
    +					// Can't re-request while awaiting acknowledgement.
    +					return
    +				}
    +				requestHeap.requestIndexes = append(requestHeap.requestIndexes, r)
    +			})
    +		},
    +	)
    +	t.assertPendingRequests()
    +	desired.Requests = requestHeap
    +	return
    +}
    +
    +func (p *Peer) maybeUpdateActualRequestState() {
    +	if p.closed.IsSet() {
    +		return
    +	}
    +	if p.needRequestUpdate == "" {
    +		return
    +	}
    +	if p.needRequestUpdate == peerUpdateRequestsTimerReason {
    +		since := time.Since(p.lastRequestUpdate)
    +		if since < updateRequestsTimerDuration {
    +			panic(since)
    +		}
    +	}
    +	pprof.Do(
    +		context.Background(),
    +		pprof.Labels("update request", p.needRequestUpdate),
    +		func(_ context.Context) {
    +			next := p.getDesiredRequestState()
    +			p.applyRequestState(next)
    +			p.t.requestIndexes = next.Requests.requestIndexes[:0]
    +		},
    +	)
    +}
    +
    +// Transmit/action the request state to the peer.
    +func (p *Peer) applyRequestState(next desiredRequestState) {
    +	current := &p.requestState
    +	if !p.setInterested(next.Interested) {
    +		return
    +	}
    +	more := true
    +	requestHeap := heap.InterfaceForSlice(&next.Requests.requestIndexes, next.Requests.lessByValue)
    +	heap.Init(requestHeap)
    +
    +	t := p.t
    +	originalRequestCount := current.Requests.GetCardinality()
    +	for {
    +		if requestHeap.Len() == 0 {
    +			break
    +		}
    +		numPending := maxRequests(current.Requests.GetCardinality() + current.Cancelled.GetCardinality())
    +		if numPending >= p.nominalMaxRequests() {
    +			break
    +		}
    +		req := heap.Pop(requestHeap)
    +		existing := t.requestingPeer(req)
    +		if existing != nil && existing != p {
    +			// Don't steal from the poor.
    +			diff := int64(current.Requests.GetCardinality()) + 1 - (int64(existing.uncancelledRequests()) - 1)
    +			// Steal a request that leaves us with one more request than the existing peer
    +			// connection if the stealer more recently received a chunk.
    +			if diff > 1 || (diff == 1 && p.lastUsefulChunkReceived.Before(existing.lastUsefulChunkReceived)) {
    +				continue
    +			}
    +			t.cancelRequest(req)
    +		}
    +		more = p.mustRequest(req)
    +		if !more {
    +			break
    +		}
    +	}
    +	if !more {
    +		// This might fail if we incorrectly determine that we can fit up to the maximum allowed
    +		// requests into the available write buffer space. We don't want that to happen because it
    +		// makes our peak requests dependent on how much was already in the buffer.
    +		panic(fmt.Sprintf(
    +			"couldn't fill apply entire request state [newRequests=%v]",
    +			current.Requests.GetCardinality()-originalRequestCount))
    +	}
    +	newPeakRequests := maxRequests(current.Requests.GetCardinality() - originalRequestCount)
    +	// log.Printf(
    +	// 	"requests %v->%v (peak %v->%v) reason %q (peer %v)",
    +	// 	originalRequestCount, current.Requests.GetCardinality(), p.peakRequests, newPeakRequests, p.needRequestUpdate, p)
    +	p.peakRequests = newPeakRequests
    +	p.needRequestUpdate = ""
    +	p.lastRequestUpdate = time.Now()
    +	if enableUpdateRequestsTimer {
    +		p.updateRequestsTimer.Reset(updateRequestsTimerDuration)
    +	}
    +}
    +
    +// This could be set to 10s to match the unchoke/request update interval recommended by some
    +// specifications. I've set it shorter to trigger it more often for testing for now.
    +const (
    +	updateRequestsTimerDuration = 3 * time.Second
    +	enableUpdateRequestsTimer   = false
    +)
    diff --git a/deps/github.com/anacrolix/torrent/requesting_test.go b/deps/github.com/anacrolix/torrent/requesting_test.go
    new file mode 100644
    index 0000000..6c791a5
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/requesting_test.go
    @@ -0,0 +1,78 @@
    +package torrent
    +
    +import (
    +	"testing"
    +
    +	"github.com/bradfitz/iter"
    +	qt "github.com/frankban/quicktest"
    +
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +func keysAsSlice(m map[Request]struct{}) (sl []Request) {
    +	for k := range m {
    +		sl = append(sl, k)
    +	}
    +	return
    +}
    +
    +func makeTypicalRequests() map[Request]struct{} {
    +	m := make(map[Request]struct{})
    +	for p := pp.Integer(0); p < 4; p++ {
    +		for c := pp.Integer(0); c < 16; c++ {
    +			m[Request{p, ChunkSpec{c * defaultChunkSize, defaultChunkSize}}] = struct{}{}
    +		}
    +	}
    +	return m
    +}
    +
    +func TestLogExampleRequestMapOrdering(t *testing.T) {
    +	for k := range makeTypicalRequests() {
    +		t.Log(k)
    +	}
    +}
    +
    +func TestRequestMapOrderingPersistent(t *testing.T) {
    +	m := makeTypicalRequests()
    +	// Shows that map order is persistent across separate range statements.
    +	qt.Assert(t, keysAsSlice(m), qt.ContentEquals, keysAsSlice(m))
    +}
    +
    +func TestRequestMapOrderAcrossInstances(t *testing.T) {
    +	// This shows that different map instances with the same contents can have the same range order.
    +	qt.Assert(t, keysAsSlice(makeTypicalRequests()), qt.ContentEquals, keysAsSlice(makeTypicalRequests()))
    +}
    +
    +// Added for testing repeating loop iteration after shuffling in Peer.applyRequestState.
    +func TestForLoopRepeatItem(t *testing.T) {
    +	t.Run("ExplicitLoopVar", func(t *testing.T) {
    +		once := false
    +		var seen []int
    +		for i := 0; i < 4; i++ {
    +			seen = append(seen, i)
    +			if !once && i == 2 {
    +				once = true
    +				i--
    +				// Will i++ still run?
    +				continue
    +			}
    +		}
    +		// We can mutate i and it's observed by the loop. No special treatment of the loop var.
    +		qt.Assert(t, seen, qt.DeepEquals, []int{0, 1, 2, 2, 3})
    +	})
    +	t.Run("Range", func(t *testing.T) {
    +		once := false
    +		var seen []int
    +		for i := range iter.N(4) {
    +			seen = append(seen, i)
    +			if !once && i == 2 {
    +				once = true
    +				// Can we actually modify the next value of i produced by the range?
    +				i--
    +				continue
    +			}
    +		}
    +		// Range ignores any mutation to i.
    +		qt.Assert(t, seen, qt.DeepEquals, []int{0, 1, 2, 3})
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/reuse_test.go b/deps/github.com/anacrolix/torrent/reuse_test.go
    new file mode 100644
    index 0000000..5da4ab8
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/reuse_test.go
    @@ -0,0 +1,79 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"net"
    +	"sync/atomic"
    +	"syscall"
    +	"testing"
    +
    +	"github.com/anacrolix/log"
    +	qt "github.com/frankban/quicktest"
    +)
    +
    +// Show that multiple connections from the same local TCP port to the same remote port will fail.
    +func TestTcpPortReuseIsABadIdea(t *testing.T) {
    +	remote, err := net.Listen("tcp", "localhost:0")
    +	c := qt.New(t)
    +	c.Assert(err, qt.IsNil)
    +	defer remote.Close()
    +	dialer := net.Dialer{}
    +	// Show that we can't duplicate an existing connection even with various socket options.
    +	dialer.Control = func(network, address string, c syscall.RawConn) (err error) {
    +		return c.Control(func(fd uintptr) {
    +			err = setReusePortSockOpts(fd)
    +		})
    +	}
    +	// Tie up a local port to the remote.
    +	first, err := dialer.Dial("tcp", remote.Addr().String())
    +	c.Assert(err, qt.IsNil)
    +	defer first.Close()
    +	// Show that dialling the remote with the same local port fails.
    +	dialer.LocalAddr = first.LocalAddr()
    +	_, err = dialer.Dial("tcp", remote.Addr().String())
    +	c.Assert(err, qt.IsNotNil)
    +	// Show that not fixing the local port again allows connections to succeed.
    +	dialer.LocalAddr = nil
    +	second, err := dialer.Dial("tcp", remote.Addr().String())
    +	c.Assert(err, qt.IsNil)
    +	second.Close()
    +}
    +
    +// Show that multiple connections from the same local utp socket to the same remote port will
    +// succeed. This is necessary for ut_holepunch to work.
    +func TestUtpLocalPortIsReusable(t *testing.T) {
    +	const network = "udp"
    +	c := qt.New(t)
    +	remote, err := NewUtpSocket(network, "localhost:0", nil, log.Default)
    +	c.Assert(err, qt.IsNil)
    +	defer remote.Close()
    +	var remoteAccepts int32
    +	doneAccepting := make(chan struct{})
    +	go func() {
    +		defer close(doneAccepting)
    +		for {
    +			c, err := remote.Accept()
    +			if err != nil {
    +				if atomic.LoadInt32(&remoteAccepts) != 2 {
    +					t.Logf("error accepting on remote: %v", err)
    +				}
    +				break
    +			}
    +			// This is not a leak, bugger off.
    +			defer c.Close()
    +			atomic.AddInt32(&remoteAccepts, 1)
    +		}
    +	}()
    +	local, err := NewUtpSocket(network, "localhost:0", nil, log.Default)
    +	c.Assert(err, qt.IsNil)
    +	defer local.Close()
    +	first, err := local.DialContext(context.Background(), network, remote.Addr().String())
    +	c.Assert(err, qt.IsNil)
    +	defer first.Close()
    +	second, err := local.DialContext(context.Background(), network, remote.Addr().String())
    +	c.Assert(err, qt.IsNil)
    +	defer second.Close()
    +	remote.Close()
    +	<-doneAccepting
    +	c.Assert(atomic.LoadInt32(&remoteAccepts), qt.Equals, int32(2))
    +}
    diff --git a/deps/github.com/anacrolix/torrent/rlreader_test.go b/deps/github.com/anacrolix/torrent/rlreader_test.go
    new file mode 100644
    index 0000000..6bf25c4
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/rlreader_test.go
    @@ -0,0 +1,128 @@
    +package torrent
    +
    +import (
    +	"io"
    +	"log"
    +	"math/rand"
    +	"sync"
    +	"testing"
    +	"time"
    +
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +	"golang.org/x/time/rate"
    +)
    +
    +func writeN(ws []io.Writer, n int) error {
    +	b := make([]byte, n)
    +	for _, w := range ws[1:] {
    +		n1 := rand.Intn(n)
    +		wn, err := w.Write(b[:n1])
    +		if wn != n1 {
    +			if err == nil {
    +				panic(n1)
    +			}
    +			return err
    +		}
    +		n -= n1
    +	}
    +	wn, err := ws[0].Write(b[:n])
    +	if wn != n {
    +		if err == nil {
    +			panic(n)
    +		}
    +	}
    +	return err
    +}
    +
    +func TestRateLimitReaders(t *testing.T) {
    +	const (
    +		numReaders     = 2
    +		bytesPerSecond = 100
    +		burst          = 5
    +		readSize       = 6
    +		writeRounds    = 10
    +		bytesPerRound  = 12
    +	)
    +	control := rate.NewLimiter(bytesPerSecond, burst)
    +	shared := rate.NewLimiter(bytesPerSecond, burst)
    +	var (
    +		ws []io.Writer
    +		cs []io.Closer
    +	)
    +	wg := sync.WaitGroup{}
    +	type read struct {
    +		N int
    +		// When the read was allowed.
    +		At time.Time
    +	}
    +	reads := make(chan read)
    +	done := make(chan struct{})
    +	for i := 0; i < numReaders; i += 1 {
    +		r, w := io.Pipe()
    +		ws = append(ws, w)
    +		cs = append(cs, w)
    +		wg.Add(1)
    +		go func() {
    +			defer wg.Done()
    +			r := rateLimitedReader{
    +				l: shared,
    +				r: r,
    +			}
    +			b := make([]byte, readSize)
    +			for {
    +				n, err := r.Read(b)
    +				select {
    +				case reads <- read{n, r.lastRead}:
    +				case <-done:
    +					return
    +				}
    +				if err == io.EOF {
    +					return
    +				}
    +				if err != nil {
    +					panic(err)
    +				}
    +			}
    +		}()
    +	}
    +	closeAll := func() {
    +		for _, c := range cs {
    +			c.Close()
    +		}
    +	}
    +	defer func() {
    +		close(done)
    +		closeAll()
    +		wg.Wait()
    +	}()
    +	written := 0
    +	go func() {
    +		for i := 0; i < writeRounds; i += 1 {
    +			err := writeN(ws, bytesPerRound)
    +			if err != nil {
    +				log.Printf("error writing: %s", err)
    +				break
    +			}
    +			written += bytesPerRound
    +		}
    +		closeAll()
    +		wg.Wait()
    +		close(reads)
    +	}()
    +	totalBytesRead := 0
    +	started := time.Now()
    +	for r := range reads {
    +		totalBytesRead += r.N
    +		require.False(t, r.At.IsZero())
    +		// Copy what the reader should have done with its reservation.
    +		res := control.ReserveN(r.At, r.N)
    +		// If we don't have to wait with the control, the reader has gone too
    +		// fast.
    +		if res.Delay() > 0 {
    +			log.Printf("%d bytes not allowed at %s", r.N, time.Since(started))
    +			t.FailNow()
    +		}
    +	}
    +	assert.EqualValues(t, writeRounds*bytesPerRound, totalBytesRead)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/roaring.go b/deps/github.com/anacrolix/torrent/roaring.go
    new file mode 100644
    index 0000000..8e39416
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/roaring.go
    @@ -0,0 +1,16 @@
    +package torrent
    +
    +import (
    +	"github.com/anacrolix/torrent/typed-roaring"
    +)
    +
    +// Return the number of bits set in the range. To do this we need the rank of the item before the
    +// first, and the rank of the last item. An off-by-one minefield. Hopefully I haven't missed
    +// something in roaring's API that provides this.
    +func roaringBitmapRangeCardinality[T typedRoaring.BitConstraint](bm interface{ Rank(T) uint64 }, start, end T) (card uint64) {
    +	card = bm.Rank(end - 1)
    +	if start != 0 {
    +		card -= bm.Rank(start - 1)
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/segments/index.go b/deps/github.com/anacrolix/torrent/segments/index.go
    new file mode 100644
    index 0000000..6717dcb
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/segments/index.go
    @@ -0,0 +1,45 @@
    +package segments
    +
    +import (
    +	"sort"
    +)
    +
    +func NewIndex(segments LengthIter) (ret Index) {
    +	var start Length
    +	for l, ok := segments(); ok; l, ok = segments() {
    +		ret.segments = append(ret.segments, Extent{start, l})
    +		start += l
    +	}
    +	return
    +}
    +
    +type Index struct {
    +	segments []Extent
    +}
    +
    +func (me Index) iterSegments() func() (Length, bool) {
    +	return func() (Length, bool) {
    +		if len(me.segments) == 0 {
    +			return 0, false
    +		} else {
    +			l := me.segments[0].Length
    +			me.segments = me.segments[1:]
    +			return l, true
    +		}
    +	}
    +}
    +
    +func (me Index) Locate(e Extent, output Callback) bool {
    +	first := sort.Search(len(me.segments), func(i int) bool {
    +		_e := me.segments[i]
    +		return _e.End() > e.Start
    +	})
    +	if first == len(me.segments) {
    +		return false
    +	}
    +	e.Start -= me.segments[first].Start
    +	me.segments = me.segments[first:]
    +	return Scan(me.iterSegments(), e, func(i int, e Extent) bool {
    +		return output(i+first, e)
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/segments/segments.go b/deps/github.com/anacrolix/torrent/segments/segments.go
    new file mode 100644
    index 0000000..90e77ce
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/segments/segments.go
    @@ -0,0 +1,63 @@
    +package segments
    +
    +type Int = int64
    +
    +type Length = Int
    +
    +func min(i Int, rest ...Int) Int {
    +	ret := i
    +	for _, i := range rest {
    +		if i < ret {
    +			ret = i
    +		}
    +	}
    +	return ret
    +}
    +
    +type Extent struct {
    +	Start, Length Int
    +}
    +
    +func (e Extent) End() Int {
    +	return e.Start + e.Length
    +}
    +
    +type (
    +	Callback   = func(int, Extent) bool
    +	LengthIter = func() (Length, bool)
    +)
    +
    +func Scan(haystack LengthIter, needle Extent, callback Callback) bool {
    +	i := 0
    +	for needle.Length != 0 {
    +		l, ok := haystack()
    +		if !ok {
    +			return false
    +		}
    +		if needle.Start < l || needle.Start == l && l == 0 {
    +			e1 := Extent{
    +				Start:  needle.Start,
    +				Length: min(l, needle.End()) - needle.Start,
    +			}
    +			if e1.Length >= 0 {
    +				if !callback(i, e1) {
    +					return true
    +				}
    +				needle.Start = 0
    +				needle.Length -= e1.Length
    +			}
    +		} else {
    +			needle.Start -= l
    +		}
    +		i++
    +	}
    +	return true
    +}
    +
    +func LocaterFromLengthIter(li LengthIter) Locater {
    +	return func(e Extent, c Callback) bool {
    +		return Scan(li, e, c)
    +	}
    +}
    +
    +type Locater func(Extent, Callback) bool
    diff --git a/deps/github.com/anacrolix/torrent/segments/segments_test.go b/deps/github.com/anacrolix/torrent/segments/segments_test.go
    new file mode 100644
    index 0000000..9ce9164
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/segments/segments_test.go
    @@ -0,0 +1,92 @@
    +package segments
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +)
    +
    +func LengthIterFromSlice(ls []Length) LengthIter {
    +	return func() (Length, bool) {
    +		switch len(ls) {
    +		case 0:
    +			return -1, false
    +		default:
    +			l := ls[0]
    +			ls = ls[1:]
    +			return l, true
    +		}
    +	}
    +}
    +
    +type ScanCallbackValue struct {
    +	Index int
    +	Extent
    +}
    +
    +type collectExtents []ScanCallbackValue
    +
    +func (me *collectExtents) scanCallback(i int, e Extent) bool {
    +	*me = append(*me, ScanCallbackValue{
    +		Index:  i,
    +		Extent: e,
    +	})
    +	return true
    +}
    +
    +type newLocater func(LengthIter) Locater
    +
    +func assertLocate(t *testing.T, nl newLocater, ls []Length, needle Extent, firstExpectedIndex int, expectedExtents []Extent) {
    +	var actual collectExtents
    +	var expected collectExtents
    +	for i, e := range expectedExtents {
    +		expected.scanCallback(firstExpectedIndex+i, e)
    +	}
    +	nl(LengthIterFromSlice(ls))(needle, actual.scanCallback)
    +	assert.EqualValues(t, expected, actual)
    +}
    +
    +func testLocater(t *testing.T, newLocater newLocater) {
    +	assertLocate(t, newLocater,
    +		[]Length{1, 0, 2, 0, 3},
    +		Extent{2, 2},
    +		2,
    +		[]Extent{{1, 1}, {0, 0}, {0, 1}})
    +	assertLocate(t, newLocater,
    +		[]Length{1, 0, 2, 0, 3},
    +		Extent{6, 2},
    +		2,
    +		[]Extent{})
    +	assertLocate(t, newLocater,
    +		[]Length{1652, 1514, 1554, 1618, 1546, 129241752, 1537}, // 128737588
    +		Extent{0, 16384},
    +		0,
    +		[]Extent{
    +			{0, 1652},
    +			{0, 1514},
    +			{0, 1554},
    +			{0, 1618},
    +			{0, 1546},
    +			{0, 8500},
    +		})
    +	assertLocate(t, newLocater,
    +		[]Length{1652, 1514, 1554, 1618, 1546, 129241752, 1537, 1536, 1551}, // 128737588
    +		Extent{129236992, 16384},
    +		5,
    +		[]Extent{
    +			{129229108, 12644},
    +			{0, 1537},
    +			{0, 1536},
    +			{0, 667},
    +		})
    +}
    +
    +func TestScan(t *testing.T) {
    +	testLocater(t, LocaterFromLengthIter)
    +}
    +
    +func TestIndex(t *testing.T) {
    +	testLocater(t, func(li LengthIter) Locater {
    +		return NewIndex(li).Locate
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/smartban.go b/deps/github.com/anacrolix/torrent/smartban.go
    new file mode 100644
    index 0000000..034a702
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/smartban.go
    @@ -0,0 +1,56 @@
    +package torrent
    +
    +import (
    +	"bytes"
    +	"crypto/sha1"
    +	"net/netip"
    +
    +	g "github.com/anacrolix/generics"
    +
    +	"github.com/anacrolix/torrent/smartban"
    +)
    +
    +type bannableAddr = netip.Addr
    +
    +type smartBanCache = smartban.Cache[bannableAddr, RequestIndex, [sha1.Size]byte]
    +
    +type blockCheckingWriter struct {
    +	cache        *smartBanCache
    +	requestIndex RequestIndex
    +	// Peers that didn't match blocks written now.
    +	badPeers    map[bannableAddr]struct{}
    +	blockBuffer bytes.Buffer
    +	chunkSize   int
    +}
    +
    +func (me *blockCheckingWriter) checkBlock() {
    +	b := me.blockBuffer.Next(me.chunkSize)
    +	for _, peer := range me.cache.CheckBlock(me.requestIndex, b) {
    +		g.MakeMapIfNilAndSet(&me.badPeers, peer, struct{}{})
    +	}
    +	me.requestIndex++
    +}
    +
    +func (me *blockCheckingWriter) checkFullBlocks() {
    +	for me.blockBuffer.Len() >= me.chunkSize {
    +		me.checkBlock()
    +	}
    +}
    +
    +func (me *blockCheckingWriter) Write(b []byte) (int, error) {
    +	n, err := me.blockBuffer.Write(b)
    +	if err != nil {
    +		// bytes.Buffer.Write should never fail.
    +		panic(err)
    +	}
    +	me.checkFullBlocks()
    +	return n, err
    +}
    +
    +// Check any remaining block data. Terminal pieces or piece sizes that don't divide into the chunk
    +// size cleanly may leave fragments that should be checked.
    +func (me *blockCheckingWriter) Flush() {
    +	for me.blockBuffer.Len() != 0 {
    +		me.checkBlock()
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/smartban/smartban.go b/deps/github.com/anacrolix/torrent/smartban/smartban.go
    new file mode 100644
    index 0000000..96e9b75
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/smartban/smartban.go
    @@ -0,0 +1,51 @@
    +package smartban
    +
    +import (
    +	"sync"
    +)
    +
    +type Cache[Peer, BlockKey, Hash comparable] struct {
    +	Hash func([]byte) Hash
    +
    +	lock   sync.RWMutex
    +	blocks map[BlockKey]map[Peer]Hash
    +}
    +
    +type Block[Key any] struct {
    +	Key  Key
    +	Data []byte
    +}
    +
    +func (me *Cache[Peer, BlockKey, Hash]) Init() {
    +	me.blocks = make(map[BlockKey]map[Peer]Hash)
    +}
    +
    +func (me *Cache[Peer, BlockKey, Hash]) RecordBlock(peer Peer, key BlockKey, data []byte) {
    +	hash := me.Hash(data)
    +	me.lock.Lock()
    +	defer me.lock.Unlock()
    +	peers := me.blocks[key]
    +	if peers == nil {
    +		peers = make(map[Peer]Hash)
    +		me.blocks[key] = peers
    +	}
    +	peers[peer] = hash
    +}
    +
    +func (me *Cache[Peer, BlockKey, Hash]) CheckBlock(key BlockKey, data []byte) (bad []Peer) {
    +	correct := me.Hash(data)
    +	me.lock.RLock()
    +	defer me.lock.RUnlock()
    +	for peer, hash := range me.blocks[key] {
    +		if hash != correct {
    +			bad = append(bad, peer)
    +		}
    +	}
    +	return
    +}
    +
    +func (me *Cache[Peer, BlockKey, Hash]) ForgetBlock(key BlockKey) {
    +	me.lock.Lock()
    +	defer me.lock.Unlock()
    +	delete(me.blocks, key)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/socket.go b/deps/github.com/anacrolix/torrent/socket.go
    new file mode 100644
    index 0000000..2d4ea86
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/socket.go
    @@ -0,0 +1,184 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"net"
    +	"strconv"
    +	"syscall"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/perf"
    +	"github.com/anacrolix/missinggo/v2"
    +	"github.com/pkg/errors"
    +)
    +
    +type Listener interface {
    +	// Accept waits for and returns the next connection to the listener.
    +	Accept() (net.Conn, error)
    +
    +	// Addr returns the listener's network address.
    +	Addr() net.Addr
    +}
    +
    +type socket interface {
    +	Listener
    +	Dialer
    +	Close() error
    +}
    +
    +func listen(n network, addr string, f firewallCallback, logger log.Logger) (socket, error) {
    +	switch {
    +	case n.Tcp:
    +		return listenTcp(n.String(), addr)
    +	case n.Udp:
    +		return listenUtp(n.String(), addr, f, logger)
    +	default:
    +		panic(n)
    +	}
    +}
    +
    +// Dialing TCP from a local port limits us to a single outgoing TCP connection to each remote
    +// client. Instead, this should be a last resort if we need to use holepunching, and only then to
    +// connect to other clients that actually try to holepunch TCP.
    +const dialTcpFromListenPort = false
    +
    +var tcpListenConfig = net.ListenConfig{
    +	Control: func(network, address string, c syscall.RawConn) (err error) {
    +		controlErr := c.Control(func(fd uintptr) {
    +			if dialTcpFromListenPort {
    +				err = setReusePortSockOpts(fd)
    +			}
    +		})
    +		if err != nil {
    +			return
    +		}
    +		err = controlErr
    +		return
    +	},
    +	// BitTorrent connections manage their own keep-alives.
    +	KeepAlive: -1,
    +}
    +
    +func listenTcp(network, address string) (s socket, err error) {
    +	l, err := tcpListenConfig.Listen(context.Background(), network, address)
    +	if err != nil {
    +		return
    +	}
    +	netDialer := net.Dialer{
    +		// We don't want fallback, as we explicitly manage the IPv4/IPv6 distinction ourselves,
    +		// although it's probably not triggered as I think the network is already constrained to
    +		// tcp4 or tcp6 at this point.
    +		FallbackDelay: -1,
    +		// BitTorrent connections manage their own keepalives.
    +		KeepAlive: tcpListenConfig.KeepAlive,
    +		Control: func(network, address string, c syscall.RawConn) (err error) {
    +			controlErr := c.Control(func(fd uintptr) {
    +				err = setSockNoLinger(fd)
    +				if err != nil {
    +					// Failing to disable linger is undesirable, but not fatal.
    +					log.Levelf(log.Debug, "error setting linger socket option on tcp socket: %v", err)
    +					err = nil
    +				}
    +				// This is no longer required I think, see
    +				// https://github.com/anacrolix/torrent/discussions/856. I added this originally to
    +				// allow dialling out from the client's listen port, but that doesn't really work. I
    +				// think Linux older than ~2013 doesn't support SO_REUSEPORT.
    +				if dialTcpFromListenPort {
    +					err = setReusePortSockOpts(fd)
    +				}
    +			})
    +			if err == nil {
    +				err = controlErr
    +			}
    +			return
    +		},
    +	}
    +	if dialTcpFromListenPort {
    +		netDialer.LocalAddr = l.Addr()
    +	}
    +	s = tcpSocket{
    +		Listener: l,
    +		NetworkDialer: NetworkDialer{
    +			Network: network,
    +			Dialer:  &netDialer,
    +		},
    +	}
    +	return
    +}
    +
    +type tcpSocket struct {
    +	net.Listener
    +	NetworkDialer
    +}
    +
    +func listenAll(networks []network, getHost func(string) string, port int, f firewallCallback, logger log.Logger) ([]socket, error) {
    +	if len(networks) == 0 {
    +		return nil, nil
    +	}
    +	var nahs []networkAndHost
    +	for _, n := range networks {
    +		nahs = append(nahs, networkAndHost{n, getHost(n.String())})
    +	}
    +	for {
    +		ss, retry, err := listenAllRetry(nahs, port, f, logger)
    +		if !retry {
    +			return ss, err
    +		}
    +	}
    +}
    +
    +type networkAndHost struct {
    +	Network network
    +	Host    string
    +}
    +
    +func listenAllRetry(nahs []networkAndHost, port int, f firewallCallback, logger log.Logger) (ss []socket, retry bool, err error) {
    +	ss = make([]socket, 1, len(nahs))
    +	portStr := strconv.FormatInt(int64(port), 10)
    +	ss[0], err = listen(nahs[0].Network, net.JoinHostPort(nahs[0].Host, portStr), f, logger)
    +	if err != nil {
    +		return nil, false, errors.Wrap(err, "first listen")
    +	}
    +	defer func() {
    +		if err != nil || retry {
    +			for _, s := range ss {
    +				s.Close()
    +			}
    +			ss = nil
    +		}
    +	}()
    +	portStr = strconv.FormatInt(int64(missinggo.AddrPort(ss[0].Addr())), 10)
    +	for _, nah := range nahs[1:] {
    +		s, err := listen(nah.Network, net.JoinHostPort(nah.Host, portStr), f, logger)
    +		if err != nil {
    +			return ss,
    +				missinggo.IsAddrInUse(err) && port == 0,
    +				errors.Wrap(err, "subsequent listen")
    +		}
    +		ss = append(ss, s)
    +	}
    +	return
    +}
    +
    +// This isn't aliased from go-libutp since that assumes CGO.
    +type firewallCallback func(net.Addr) bool
    +
    +func listenUtp(network, addr string, fc firewallCallback, logger log.Logger) (socket, error) {
    +	us, err := NewUtpSocket(network, addr, fc, logger)
    +	return utpSocketSocket{us, network}, err
    +}
    +
    +// utpSocket wrapper, additionally wrapped for the torrent package's socket interface.
    +type utpSocketSocket struct {
    +	utpSocket
    +	network string
    +}
    +
    +func (me utpSocketSocket) DialerNetwork() string {
    +	return me.network
    +}
    +
    +func (me utpSocketSocket) Dial(ctx context.Context, addr string) (conn net.Conn, err error) {
    +	defer perf.ScopeTimerErr(&err)()
    +	return me.utpSocket.DialContext(ctx, me.network, addr)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/sockopts.go b/deps/github.com/anacrolix/torrent/sockopts.go
    new file mode 100644
    index 0000000..54f307d
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/sockopts.go
    @@ -0,0 +1,10 @@
    +//go:build !wasm
    +
    +package torrent
    +
    +import "syscall"
    +
    +var lingerOffVal = syscall.Linger{
    +	Onoff:  0,
    +	Linger: 0,
    +}
    diff --git a/deps/github.com/anacrolix/torrent/sockopts_unix.go b/deps/github.com/anacrolix/torrent/sockopts_unix.go
    new file mode 100644
    index 0000000..52ec9e8
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/sockopts_unix.go
    @@ -0,0 +1,29 @@
    +//go:build !windows && !wasm
    +
    +package torrent
    +
    +import (
    +	"syscall"
    +
    +	"golang.org/x/sys/unix"
    +)
    +
    +func setReusePortSockOpts(fd uintptr) (err error) {
    +	// I would use libp2p/go-reuseport to do this here, but no surprise it's
    +	// implemented incorrectly.
    +
    +	// Looks like we can get away with just REUSEPORT at least on Darwin, and probably by
    +	// extension BSDs and Linux.
    +	if false {
    +		err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
    +		if err != nil {
    +			return
    +		}
    +	}
    +	err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1)
    +	return
    +}
    +
    +func setSockNoLinger(fd uintptr) (err error) {
    +	return syscall.SetsockoptLinger(int(fd), syscall.SOL_SOCKET, syscall.SO_LINGER, &lingerOffVal)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/sockopts_wasm.go b/deps/github.com/anacrolix/torrent/sockopts_wasm.go
    new file mode 100644
    index 0000000..9705b91
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/sockopts_wasm.go
    @@ -0,0 +1,12 @@
    +package torrent
    +
    +// It's possible that we either need to use JS-specific way to allow port reuse, or to fall back to
    +// dialling TCP without forcing the local address to match the listener. If the fallback is
    +// implemented, then this should probably return an error to trigger it.
    +func setReusePortSockOpts(fd uintptr) error {
    +	return nil
    +}
    +
    +func setSockNoLinger(fd uintptr) error {
    +	return nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/sockopts_windows.go b/deps/github.com/anacrolix/torrent/sockopts_windows.go
    new file mode 100644
    index 0000000..c3c0ab0
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/sockopts_windows.go
    @@ -0,0 +1,15 @@
    +package torrent
    +
    +import (
    +	"syscall"
    +
    +	"golang.org/x/sys/windows"
    +)
    +
    +func setReusePortSockOpts(fd uintptr) (err error) {
    +	return windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
    +}
    +
    +func setSockNoLinger(fd uintptr) (err error) {
    +	return syscall.SetsockoptLinger(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_LINGER, &lingerOffVal)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/sources.go b/deps/github.com/anacrolix/torrent/sources.go
    new file mode 100644
    index 0000000..ed5ecbf
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/sources.go
    @@ -0,0 +1,80 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"net/http"
    +
    +	"github.com/anacrolix/log"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +// Add HTTP endpoints that serve the metainfo. They will be used if the torrent info isn't obtained
    +// yet. The Client HTTP client is used.
    +func (t *Torrent) UseSources(sources []string) {
    +	select {
    +	case <-t.Closed():
    +		return
    +	case <-t.GotInfo():
    +		return
    +	default:
    +	}
    +	for _, s := range sources {
    +		_, loaded := t.activeSources.LoadOrStore(s, struct{}{})
    +		if loaded {
    +			continue
    +		}
    +		s := s
    +		go func() {
    +			err := t.useActiveTorrentSource(s)
    +			_, loaded := t.activeSources.LoadAndDelete(s)
    +			if !loaded {
    +				panic(s)
    +			}
    +			level := log.Debug
    +			if err != nil && !errors.Is(err, context.Canceled) {
    +				level = log.Warning
    +			}
    +			t.logger.Levelf(level, "used torrent source %q [err=%v]", s, err)
    +		}()
    +	}
    +}
    +
    +func (t *Torrent) useActiveTorrentSource(source string) error {
    +	ctx, cancel := context.WithCancel(context.Background())
    +	defer cancel()
    +	go func() {
    +		select {
    +		case <-t.GotInfo():
    +		case <-t.Closed():
    +		case <-ctx.Done():
    +		}
    +		cancel()
    +	}()
    +	mi, err := getTorrentSource(ctx, source, t.cl.httpClient)
    +	if err != nil {
    +		return err
    +	}
    +	return t.MergeSpec(TorrentSpecFromMetaInfo(&mi))
    +}
    +
    +func getTorrentSource(ctx context.Context, source string, hc *http.Client) (mi metainfo.MetaInfo, err error) {
    +	var req *http.Request
    +	if req, err = http.NewRequestWithContext(ctx, http.MethodGet, source, nil); err != nil {
    +		return
    +	}
    +	var resp *http.Response
    +	if resp, err = hc.Do(req); err != nil {
    +		return
    +	}
    +	defer resp.Body.Close()
    +	if resp.StatusCode != http.StatusOK {
    +		err = fmt.Errorf("unexpected response status code: %v", resp.StatusCode)
    +		return
    +	}
    +	err = bencode.NewDecoder(resp.Body).Decode(&mi)
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/spec.go b/deps/github.com/anacrolix/torrent/spec.go
    new file mode 100644
    index 0000000..8cce3cb
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/spec.go
    @@ -0,0 +1,90 @@
    +package torrent
    +
    +import (
    +	"fmt"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +// Specifies a new torrent for adding to a client, or additions to an existing Torrent. There are
    +// constructor functions for magnet URIs and torrent metainfo files. TODO: This type should be
    +// dismantled into a new Torrent option type, and separate Torrent mutate method(s).
    +type TorrentSpec struct {
    +	// The tiered tracker URIs.
    +	Trackers [][]string
    +	// TODO: Move into a "new" Torrent opt type.
    +	InfoHash  metainfo.Hash
    +	InfoBytes []byte
    +	// The name to use if the Name field from the Info isn't available.
    +	DisplayName string
    +	// WebSeed URLs. For additional options add the URLs separately with Torrent.AddWebSeeds
    +	// instead.
    +	Webseeds  []string
    +	DhtNodes  []string
    +	PeerAddrs []string
    +	// The combination of the "xs" and "as" fields in magnet links, for now.
    +	Sources []string
    +
    +	// The chunk size to use for outbound requests. Defaults to 16KiB if not set. Can only be set
    +	// for new Torrents. TODO: Move into a "new" Torrent opt type.
    +	ChunkSize pp.Integer
    +	// TODO: Move into a "new" Torrent opt type.
    +	Storage storage.ClientImpl
    +
    +	DisableInitialPieceCheck bool
    +
    +	// Whether to allow data download or upload
    +	DisallowDataUpload   bool
    +	DisallowDataDownload bool
    +}
    +
    +func TorrentSpecFromMagnetUri(uri string) (spec *TorrentSpec, err error) {
    +	m, err := metainfo.ParseMagnetUri(uri)
    +	if err != nil {
    +		return
    +	}
    +	spec = &TorrentSpec{
    +		Trackers:    [][]string{m.Trackers},
    +		DisplayName: m.DisplayName,
    +		InfoHash:    m.InfoHash,
    +		Webseeds:    m.Params["ws"],
    +		Sources:     append(m.Params["xs"], m.Params["as"]...),
    +		PeerAddrs:   m.Params["x.pe"], // BEP 9
    +		// TODO: What's the parameter for DHT nodes?
    +	}
    +	return
    +}
    +
    +// The error will be from unmarshalling the info bytes. The TorrentSpec is still filled out as much
    +// as possible in this case.
    +func TorrentSpecFromMetaInfoErr(mi *metainfo.MetaInfo) (*TorrentSpec, error) {
    +	info, err := mi.UnmarshalInfo()
    +	if err != nil {
    +		err = fmt.Errorf("unmarshalling info: %w", err)
    +	}
    +	return &TorrentSpec{
    +		Trackers:    mi.UpvertedAnnounceList(),
    +		InfoHash:    mi.HashInfoBytes(),
    +		InfoBytes:   mi.InfoBytes,
    +		DisplayName: info.Name,
    +		Webseeds:    mi.UrlList,
    +		DhtNodes: func() (ret []string) {
    +			ret = make([]string, 0, len(mi.Nodes))
    +			for _, node := range mi.Nodes {
    +				ret = append(ret, string(node))
    +			}
    +			return
    +		}(),
    +	}, err
    +}
    +
    +// Panics if there was anything missing from the metainfo.
    +func TorrentSpecFromMetaInfo(mi *metainfo.MetaInfo) *TorrentSpec {
    +	ts, err := TorrentSpecFromMetaInfoErr(mi)
    +	if err != nil {
    +		panic(err)
    +	}
    +	return ts
    +}
    diff --git a/deps/github.com/anacrolix/torrent/stats.go b/deps/github.com/anacrolix/torrent/stats.go
    new file mode 100644
    index 0000000..90144bf
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/stats.go
    @@ -0,0 +1,12 @@
    +package torrent
    +
    +import (
    +	"io"
    +
    +	"github.com/davecgh/go-spew/spew"
    +)
    +
    +func dumpStats[T any](w io.Writer, stats T) {
    +	spew.NewDefaultConfig()
    +	spew.Fdump(w, stats)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/bolt-piece-completion.go b/deps/github.com/anacrolix/torrent/storage/bolt-piece-completion.go
    new file mode 100644
    index 0000000..442f57c
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/bolt-piece-completion.go
    @@ -0,0 +1,97 @@
    +//go:build !noboltdb && !wasm
    +// +build !noboltdb,!wasm
    +
    +package storage
    +
    +import (
    +	"encoding/binary"
    +	"os"
    +	"path/filepath"
    +	"time"
    +
    +	"go.etcd.io/bbolt"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +const (
    +	boltDbCompleteValue   = "c"
    +	boltDbIncompleteValue = "i"
    +)
    +
    +var completionBucketKey = []byte("completion")
    +
    +type boltPieceCompletion struct {
    +	db *bbolt.DB
    +}
    +
    +var _ PieceCompletion = (*boltPieceCompletion)(nil)
    +
    +func NewBoltPieceCompletion(dir string) (ret PieceCompletion, err error) {
    +	os.MkdirAll(dir, 0o750)
    +	p := filepath.Join(dir, ".torrent.bolt.db")
    +	db, err := bbolt.Open(p, 0o660, &bbolt.Options{
    +		Timeout: time.Second,
    +	})
    +	if err != nil {
    +		return
    +	}
    +	db.NoSync = true
    +	ret = &boltPieceCompletion{db}
    +	return
    +}
    +
    +func (me boltPieceCompletion) Get(pk metainfo.PieceKey) (cn Completion, err error) {
    +	err = me.db.View(func(tx *bbolt.Tx) error {
    +		cb := tx.Bucket(completionBucketKey)
    +		if cb == nil {
    +			return nil
    +		}
    +		ih := cb.Bucket(pk.InfoHash[:])
    +		if ih == nil {
    +			return nil
    +		}
    +		var key [4]byte
    +		binary.BigEndian.PutUint32(key[:], uint32(pk.Index))
    +		cn.Ok = true
    +		switch string(ih.Get(key[:])) {
    +		case boltDbCompleteValue:
    +			cn.Complete = true
    +		case boltDbIncompleteValue:
    +			cn.Complete = false
    +		default:
    +			cn.Ok = false
    +		}
    +		return nil
    +	})
    +	return
    +}
    +
    +func (me boltPieceCompletion) Set(pk metainfo.PieceKey, b bool) error {
    +	if c, err := me.Get(pk); err == nil && c.Ok && c.Complete == b {
    +		return nil
    +	}
    +	return me.db.Update(func(tx *bbolt.Tx) error {
    +		c, err := tx.CreateBucketIfNotExists(completionBucketKey)
    +		if err != nil {
    +			return err
    +		}
    +		ih, err := c.CreateBucketIfNotExists(pk.InfoHash[:])
    +		if err != nil {
    +			return err
    +		}
    +		var key [4]byte
    +		binary.BigEndian.PutUint32(key[:], uint32(pk.Index))
    +		return ih.Put(key[:], []byte(func() string {
    +			if b {
    +				return boltDbCompleteValue
    +			} else {
    +				return boltDbIncompleteValue
    +			}
    +		}()))
    +	})
    +}
    +
    +func (me *boltPieceCompletion) Close() error {
    +	return me.db.Close()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/bolt-piece-completion_test.go b/deps/github.com/anacrolix/torrent/storage/bolt-piece-completion_test.go
    new file mode 100644
    index 0000000..3a778a8
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/bolt-piece-completion_test.go
    @@ -0,0 +1,36 @@
    +package storage
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +func TestBoltPieceCompletion(t *testing.T) {
    +	td := t.TempDir()
    +
    +	pc, err := NewBoltPieceCompletion(td)
    +	require.NoError(t, err)
    +	defer pc.Close()
    +
    +	pk := metainfo.PieceKey{}
    +
    +	b, err := pc.Get(pk)
    +	require.NoError(t, err)
    +	assert.False(t, b.Ok)
    +
    +	require.NoError(t, pc.Set(pk, false))
    +
    +	b, err = pc.Get(pk)
    +	require.NoError(t, err)
    +	assert.Equal(t, Completion{Complete: false, Ok: true}, b)
    +
    +	require.NoError(t, pc.Set(pk, true))
    +
    +	b, err = pc.Get(pk)
    +	require.NoError(t, err)
    +	assert.Equal(t, Completion{Complete: true, Ok: true}, b)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/bolt-piece.go b/deps/github.com/anacrolix/torrent/storage/bolt-piece.go
    new file mode 100644
    index 0000000..67e03bd
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/bolt-piece.go
    @@ -0,0 +1,112 @@
    +//go:build !noboltdb && !wasm
    +// +build !noboltdb,!wasm
    +
    +package storage
    +
    +import (
    +	"encoding/binary"
    +	"io"
    +
    +	"go.etcd.io/bbolt"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type boltPiece struct {
    +	db  *bbolt.DB
    +	p   metainfo.Piece
    +	ih  metainfo.Hash
    +	key [24]byte
    +}
    +
    +var (
    +	_             PieceImpl = (*boltPiece)(nil)
    +	dataBucketKey           = []byte("data")
    +)
    +
    +func (me *boltPiece) pc() PieceCompletionGetSetter {
    +	return boltPieceCompletion{me.db}
    +}
    +
    +func (me *boltPiece) pk() metainfo.PieceKey {
    +	return metainfo.PieceKey{me.ih, me.p.Index()}
    +}
    +
    +func (me *boltPiece) Completion() Completion {
    +	c, err := me.pc().Get(me.pk())
    +	switch err {
    +	case bbolt.ErrDatabaseNotOpen:
    +		return Completion{}
    +	case nil:
    +	default:
    +		panic(err)
    +	}
    +	return c
    +}
    +
    +func (me *boltPiece) MarkComplete() error {
    +	return me.pc().Set(me.pk(), true)
    +}
    +
    +func (me *boltPiece) MarkNotComplete() error {
    +	return me.pc().Set(me.pk(), false)
    +}
    +
    +func (me *boltPiece) ReadAt(b []byte, off int64) (n int, err error) {
    +	err = me.db.View(func(tx *bbolt.Tx) error {
    +		db := tx.Bucket(dataBucketKey)
    +		if db == nil {
    +			return io.EOF
    +		}
    +		ci := off / chunkSize
    +		off %= chunkSize
    +		for len(b) != 0 {
    +			ck := me.chunkKey(int(ci))
    +			_b := db.Get(ck[:])
    +			// If the chunk is the wrong size, assume it's missing as we can't rely on the data.
    +			if len(_b) != chunkSize {
    +				return io.EOF
    +			}
    +			n1 := copy(b, _b[off:])
    +			off = 0
    +			ci++
    +			b = b[n1:]
    +			n += n1
    +		}
    +		return nil
    +	})
    +	return
    +}
    +
    +func (me *boltPiece) chunkKey(index int) (ret [26]byte) {
    +	copy(ret[:], me.key[:])
    +	binary.BigEndian.PutUint16(ret[24:], uint16(index))
    +	return
    +}
    +
    +func (me *boltPiece) WriteAt(b []byte, off int64) (n int, err error) {
    +	err = me.db.Update(func(tx *bbolt.Tx) error {
    +		db, err := tx.CreateBucketIfNotExists(dataBucketKey)
    +		if err != nil {
    +			return err
    +		}
    +		ci := off / chunkSize
    +		off %= chunkSize
    +		for len(b) != 0 {
    +			_b := make([]byte, chunkSize)
    +			ck := me.chunkKey(int(ci))
    +			copy(_b, db.Get(ck[:]))
    +			n1 := copy(_b[off:], b)
    +			db.Put(ck[:], _b)
    +			if n1 > len(b) {
    +				break
    +			}
    +			b = b[n1:]
    +			off = 0
    +			ci++
    +			n += n1
    +		}
    +		return nil
    +	})
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/bolt-piece_test.go b/deps/github.com/anacrolix/torrent/storage/bolt-piece_test.go
    new file mode 100644
    index 0000000..8c55848
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/bolt-piece_test.go
    @@ -0,0 +1,12 @@
    +package storage_test
    +
    +import (
    +	"testing"
    +
    +	"github.com/anacrolix/torrent/storage"
    +	"github.com/anacrolix/torrent/test"
    +)
    +
    +func TestBoltLeecherStorage(t *testing.T) {
    +	test.TestLeecherStorage(t, test.LeecherStorageTestCase{"Boltdb", storage.NewBoltDB, 0})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/bolt.go b/deps/github.com/anacrolix/torrent/storage/bolt.go
    new file mode 100644
    index 0000000..945b249
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/bolt.go
    @@ -0,0 +1,64 @@
    +//go:build !noboltdb && !wasm
    +// +build !noboltdb,!wasm
    +
    +package storage
    +
    +import (
    +	"encoding/binary"
    +	"path/filepath"
    +	"time"
    +
    +	"github.com/anacrolix/missinggo/expect"
    +	"go.etcd.io/bbolt"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +const (
    +	// Chosen to match the usual chunk size in a torrent client. This way, most chunk writes are to
    +	// exactly one full item in bbolt DB.
    +	chunkSize = 1 << 14
    +)
    +
    +type boltClient struct {
    +	db *bbolt.DB
    +}
    +
    +type boltTorrent struct {
    +	cl *boltClient
    +	ih metainfo.Hash
    +}
    +
    +func NewBoltDB(filePath string) ClientImplCloser {
    +	db, err := bbolt.Open(filepath.Join(filePath, "bolt.db"), 0o600, &bbolt.Options{
    +		Timeout: time.Second,
    +	})
    +	expect.Nil(err)
    +	db.NoSync = true
    +	return &boltClient{db}
    +}
    +
    +func (me *boltClient) Close() error {
    +	return me.db.Close()
    +}
    +
    +func (me *boltClient) OpenTorrent(_ *metainfo.Info, infoHash metainfo.Hash) (TorrentImpl, error) {
    +	t := &boltTorrent{me, infoHash}
    +	return TorrentImpl{
    +		Piece: t.Piece,
    +		Close: t.Close,
    +	}, nil
    +}
    +
    +func (me *boltTorrent) Piece(p metainfo.Piece) PieceImpl {
    +	ret := &boltPiece{
    +		p:  p,
    +		db: me.cl.db,
    +		ih: me.ih,
    +	}
    +	copy(ret.key[:], me.ih[:])
    +	binary.BigEndian.PutUint32(ret.key[20:], uint32(p.Index()))
    +	return ret
    +}
    +
    +func (boltTorrent) Close() error { return nil }
    diff --git a/deps/github.com/anacrolix/torrent/storage/default-dir-piece-completion-boltdb.go b/deps/github.com/anacrolix/torrent/storage/default-dir-piece-completion-boltdb.go
    new file mode 100644
    index 0000000..3ac6a77
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/default-dir-piece-completion-boltdb.go
    @@ -0,0 +1,11 @@
    +// Bolt piece completion is available, and sqlite is not.
    +//go:build !noboltdb && (!cgo || nosqlite) && !wasm
    +// +build !noboltdb
    +// +build !cgo nosqlite
    +// +build !wasm
    +
    +package storage
    +
    +func NewDefaultPieceCompletionForDir(dir string) (PieceCompletion, error) {
    +	return NewBoltPieceCompletion(dir)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/default-dir-piece-completion-other.go b/deps/github.com/anacrolix/torrent/storage/default-dir-piece-completion-other.go
    new file mode 100644
    index 0000000..3cd42fb
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/default-dir-piece-completion-other.go
    @@ -0,0 +1,14 @@
    +// Bolt piece completion is not available, and neither is sqlite.
    +//go:build (!cgo || nosqlite) && (noboltdb || wasm)
    +// +build !cgo nosqlite
    +// +build noboltdb wasm
    +
    +package storage
    +
    +import (
    +	"errors"
    +)
    +
    +func NewDefaultPieceCompletionForDir(dir string) (PieceCompletion, error) {
    +	return nil, errors.New("y ur OS no have features")
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/disabled/disabled.go b/deps/github.com/anacrolix/torrent/storage/disabled/disabled.go
    new file mode 100644
    index 0000000..f511222
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/disabled/disabled.go
    @@ -0,0 +1,52 @@
    +package disabled
    +
    +import (
    +	"errors"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +type Client struct{}
    +
    +func (c Client) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (storage.TorrentImpl, error) {
    +	capFunc := func() (int64, bool) {
    +		return 0, true
    +	}
    +	return storage.TorrentImpl{
    +		Piece: func(piece metainfo.Piece) storage.PieceImpl {
    +			return Piece{}
    +		},
    +		Close: func() error {
    +			return nil
    +		},
    +		Capacity: &capFunc,
    +	}, nil
    +}
    +
    +type Piece struct{}
    +
    +func (Piece) ReadAt(p []byte, off int64) (n int, err error) {
    +	err = errors.New("disabled")
    +	return
    +}
    +
    +func (Piece) WriteAt(p []byte, off int64) (n int, err error) {
    +	err = errors.New("disabled")
    +	return
    +}
    +
    +func (Piece) MarkComplete() error {
    +	return errors.New("disabled")
    +}
    +
    +func (Piece) MarkNotComplete() error {
    +	return errors.New("disabled")
    +}
    +
    +func (Piece) Completion() storage.Completion {
    +	return storage.Completion{
    +		Complete: false,
    +		Ok:       true,
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/doc.go b/deps/github.com/anacrolix/torrent/storage/doc.go
    new file mode 100644
    index 0000000..5ba6a0a
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/doc.go
    @@ -0,0 +1,2 @@
    +// Package storage implements storage backends for package torrent.
    +package storage
    diff --git a/deps/github.com/anacrolix/torrent/storage/file-deprecated.go b/deps/github.com/anacrolix/torrent/storage/file-deprecated.go
    new file mode 100644
    index 0000000..4560b9d
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/file-deprecated.go
    @@ -0,0 +1,34 @@
    +package storage
    +
    +import (
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +func NewFileWithCompletion(baseDir string, completion PieceCompletion) ClientImplCloser {
    +	return NewFileWithCustomPathMakerAndCompletion(baseDir, nil, completion)
    +}
    +
    +// File storage with data partitioned by infohash.
    +func NewFileByInfoHash(baseDir string) ClientImplCloser {
    +	return NewFileWithCustomPathMaker(baseDir, infoHashPathMaker)
    +}
    +
    +// Deprecated: Allows passing a function to determine the path for storing torrent data. The
    +// function is responsible for sanitizing the info if it uses some part of it (for example
    +// sanitizing info.Name).
    +func NewFileWithCustomPathMaker(baseDir string, pathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string) ClientImplCloser {
    +	return NewFileWithCustomPathMakerAndCompletion(baseDir, pathMaker, pieceCompletionForDir(baseDir))
    +}
    +
    +// Deprecated: Allows passing custom PieceCompletion
    +func NewFileWithCustomPathMakerAndCompletion(
    +	baseDir string,
    +	pathMaker TorrentDirFilePathMaker,
    +	completion PieceCompletion,
    +) ClientImplCloser {
    +	return NewFileOpts(NewFileClientOpts{
    +		ClientBaseDir:   baseDir,
    +		TorrentDirMaker: pathMaker,
    +		PieceCompletion: completion,
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/file-misc.go b/deps/github.com/anacrolix/torrent/storage/file-misc.go
    new file mode 100644
    index 0000000..8966ecb
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/file-misc.go
    @@ -0,0 +1,34 @@
    +package storage
    +
    +import "github.com/anacrolix/torrent/metainfo"
    +
    +type requiredLength struct {
    +	fileIndex int
    +	length    int64
    +}
    +
    +func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
    +	if n == 0 {
    +		return
    +	}
    +	for i, fi := range info.UpvertedFiles() {
    +		if off >= fi.Length {
    +			off -= fi.Length
    +			continue
    +		}
    +		n1 := n
    +		if off+n1 > fi.Length {
    +			n1 = fi.Length - off
    +		}
    +		ret = append(ret, requiredLength{
    +			fileIndex: i,
    +			length:    off + n1,
    +		})
    +		n -= n1
    +		if n == 0 {
    +			return
    +		}
    +		off = 0
    +	}
    +	panic("extent exceeds torrent bounds")
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/file-misc_test.go b/deps/github.com/anacrolix/torrent/storage/file-misc_test.go
    new file mode 100644
    index 0000000..f74196d
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/file-misc_test.go
    @@ -0,0 +1,37 @@
    +package storage
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +func TestExtentCompleteRequiredLengths(t *testing.T) {
    +	info := &metainfo.Info{
    +		Files: []metainfo.FileInfo{
    +			{Path: []string{"a"}, Length: 2},
    +			{Path: []string{"b"}, Length: 3},
    +		},
    +	}
    +	assert.Empty(t, extentCompleteRequiredLengths(info, 0, 0))
    +	assert.EqualValues(t, []requiredLength{
    +		{fileIndex: 0, length: 1},
    +	}, extentCompleteRequiredLengths(info, 0, 1))
    +	assert.EqualValues(t, []requiredLength{
    +		{fileIndex: 0, length: 2},
    +	}, extentCompleteRequiredLengths(info, 0, 2))
    +	assert.EqualValues(t, []requiredLength{
    +		{fileIndex: 0, length: 2},
    +		{fileIndex: 1, length: 1},
    +	}, extentCompleteRequiredLengths(info, 0, 3))
    +	assert.EqualValues(t, []requiredLength{
    +		{fileIndex: 1, length: 2},
    +	}, extentCompleteRequiredLengths(info, 2, 2))
    +	assert.EqualValues(t, []requiredLength{
    +		{fileIndex: 1, length: 3},
    +	}, extentCompleteRequiredLengths(info, 4, 1))
    +	assert.Len(t, extentCompleteRequiredLengths(info, 5, 0), 0)
    +	assert.Panics(t, func() { extentCompleteRequiredLengths(info, 6, 1) })
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/file-paths.go b/deps/github.com/anacrolix/torrent/storage/file-paths.go
    new file mode 100644
    index 0000000..8d338f8
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/file-paths.go
    @@ -0,0 +1,38 @@
    +package storage
    +
    +import (
    +	"os"
    +	"path/filepath"
    +	"strings"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +// Determines the filepath to be used for each file in a torrent.
    +type FilePathMaker func(opts FilePathMakerOpts) string
    +
    +// Determines the directory for a given torrent within a storage client.
    +type TorrentDirFilePathMaker func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string
    +
    +// Info passed to a FilePathMaker.
    +type FilePathMakerOpts struct {
    +	Info *metainfo.Info
    +	File *metainfo.FileInfo
    +}
    +
    +// defaultPathMaker just returns the storage client's base directory.
    +func defaultPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
    +	return baseDir
    +}
    +
    +func infoHashPathMaker(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
    +	return filepath.Join(baseDir, infoHash.HexString())
    +}
    +
    +func isSubFilepath(base, sub string) bool {
    +	rel, err := filepath.Rel(base, sub)
    +	if err != nil {
    +		return false
    +	}
    +	return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/file-piece.go b/deps/github.com/anacrolix/torrent/storage/file-piece.go
    new file mode 100644
    index 0000000..4777201
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/file-piece.go
    @@ -0,0 +1,59 @@
    +package storage
    +
    +import (
    +	"io"
    +	"log"
    +	"os"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type filePieceImpl struct {
    +	*fileTorrentImpl
    +	p metainfo.Piece
    +	io.WriterAt
    +	io.ReaderAt
    +}
    +
    +var _ PieceImpl = (*filePieceImpl)(nil)
    +
    +func (me *filePieceImpl) pieceKey() metainfo.PieceKey {
    +	return metainfo.PieceKey{me.infoHash, me.p.Index()}
    +}
    +
    +func (fs *filePieceImpl) Completion() Completion {
    +	c, err := fs.completion.Get(fs.pieceKey())
    +	if err != nil {
    +		log.Printf("error getting piece completion: %s", err)
    +		c.Ok = false
    +		return c
    +	}
    +
    +	verified := true
    +	if c.Complete {
    +		// If it's allegedly complete, check that its constituent files have the necessary length.
    +		for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
    +			s, err := os.Stat(fs.files[fi.fileIndex].path)
    +			if err != nil || s.Size() < fi.length {
    +				verified = false
    +				break
    +			}
    +		}
    +	}
    +
    +	if !verified {
    +		// The completion was wrong, fix it.
    +		c.Complete = false
    +		fs.completion.Set(fs.pieceKey(), false)
    +	}
    +
    +	return c
    +}
    +
    +func (fs *filePieceImpl) MarkComplete() error {
    +	return fs.completion.Set(fs.pieceKey(), true)
    +}
    +
    +func (fs *filePieceImpl) MarkNotComplete() error {
    +	return fs.completion.Set(fs.pieceKey(), false)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/file.go b/deps/github.com/anacrolix/torrent/storage/file.go
    new file mode 100644
    index 0000000..b873964
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/file.go
    @@ -0,0 +1,212 @@
    +package storage
    +
    +import (
    +	"fmt"
    +	"io"
    +	"os"
    +	"path/filepath"
    +
    +	"github.com/anacrolix/missinggo/v2"
    +
    +	"github.com/anacrolix/torrent/common"
    +	"github.com/anacrolix/torrent/metainfo"
    +	"github.com/anacrolix/torrent/segments"
    +)
    +
    +// File-based storage for torrents, that isn't yet bound to a particular torrent.
    +type fileClientImpl struct {
    +	opts NewFileClientOpts
    +}
    +
    +// All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
    +func NewFile(baseDir string) ClientImplCloser {
    +	return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
    +}
    +
    +type NewFileClientOpts struct {
    +	// The base directory for all downloads.
    +	ClientBaseDir   string
    +	FilePathMaker   FilePathMaker
    +	TorrentDirMaker TorrentDirFilePathMaker
    +	PieceCompletion PieceCompletion
    +}
    +
    +// NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
    +func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
    +	if opts.TorrentDirMaker == nil {
    +		opts.TorrentDirMaker = defaultPathMaker
    +	}
    +	if opts.FilePathMaker == nil {
    +		opts.FilePathMaker = func(opts FilePathMakerOpts) string {
    +			var parts []string
    +			if opts.Info.Name != metainfo.NoName {
    +				parts = append(parts, opts.Info.Name)
    +			}
    +			return filepath.Join(append(parts, opts.File.Path...)...)
    +		}
    +	}
    +	if opts.PieceCompletion == nil {
    +		opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
    +	}
    +	return fileClientImpl{opts}
    +}
    +
    +func (me fileClientImpl) Close() error {
    +	return me.opts.PieceCompletion.Close()
    +}
    +
    +func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
    +	dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
    +	upvertedFiles := info.UpvertedFiles()
    +	files := make([]file, 0, len(upvertedFiles))
    +	for i, fileInfo := range upvertedFiles {
    +		filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
    +			Info: info,
    +			File: &fileInfo,
    +		}))
    +		if !isSubFilepath(dir, filePath) {
    +			err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
    +			return
    +		}
    +		f := file{
    +			path:   filePath,
    +			length: fileInfo.Length,
    +		}
    +		if f.length == 0 {
    +			err = CreateNativeZeroLengthFile(f.path)
    +			if err != nil {
    +				err = fmt.Errorf("creating zero length file: %w", err)
    +				return
    +			}
    +		}
    +		files = append(files, f)
    +	}
    +	t := &fileTorrentImpl{
    +		files,
    +		segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
    +		infoHash,
    +		fs.opts.PieceCompletion,
    +	}
    +	return TorrentImpl{
    +		Piece: t.Piece,
    +		Close: t.Close,
    +	}, nil
    +}
    +
    +type file struct {
    +	// The safe, OS-local file path.
    +	path   string
    +	length int64
    +}
    +
    +type fileTorrentImpl struct {
    +	files          []file
    +	segmentLocater segments.Index
    +	infoHash       metainfo.Hash
    +	completion     PieceCompletion
    +}
    +
    +func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
    +	// Create a view onto the file-based torrent storage.
    +	_io := fileTorrentImplIO{fts}
    +	// Return the appropriate segments of this.
    +	return &filePieceImpl{
    +		fts,
    +		p,
    +		missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
    +		io.NewSectionReader(_io, p.Offset(), p.Length()),
    +	}
    +}
    +
    +func (fs *fileTorrentImpl) Close() error {
    +	return nil
    +}
    +
    +// A helper to create zero-length files which won't appear for file-orientated storage since no
    +// writes will ever occur to them (no torrent data is associated with a zero-length file). The
    +// caller should make sure the file name provided is safe/sanitized.
    +func CreateNativeZeroLengthFile(name string) error {
    +	os.MkdirAll(filepath.Dir(name), 0o777)
    +	var f io.Closer
    +	f, err := os.Create(name)
    +	if err != nil {
    +		return err
    +	}
    +	return f.Close()
    +}
    +
    +// Exposes file-based storage of a torrent, as one big ReadWriterAt.
    +type fileTorrentImplIO struct {
    +	fts *fileTorrentImpl
    +}
    +
    +// Returns EOF on short or missing file.
    +func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
    +	f, err := os.Open(file.path)
    +	if os.IsNotExist(err) {
    +		// File missing is treated the same as a short file.
    +		err = io.EOF
    +		return
    +	}
    +	if err != nil {
    +		return
    +	}
    +	defer f.Close()
    +	// Limit the read to within the expected bounds of this file.
    +	if int64(len(b)) > file.length-off {
    +		b = b[:file.length-off]
    +	}
    +	for off < file.length && len(b) != 0 {
    +		n1, err1 := f.ReadAt(b, off)
    +		b = b[n1:]
    +		n += n1
    +		off += int64(n1)
    +		if n1 == 0 {
    +			err = err1
    +			break
    +		}
    +	}
    +	return
    +}
    +
    +// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
    +func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
    +	fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
    +		n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
    +		n += n1
    +		b = b[n1:]
    +		err = err1
    +		return err == nil // && int64(n1) == e.Length
    +	})
    +	if len(b) != 0 && err == nil {
    +		err = io.EOF
    +	}
    +	return
    +}
    +
    +func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
    +	// log.Printf("write at %v: %v bytes", off, len(p))
    +	fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
    +		name := fst.fts.files[i].path
    +		os.MkdirAll(filepath.Dir(name), 0o777)
    +		var f *os.File
    +		f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
    +		if err != nil {
    +			return false
    +		}
    +		var n1 int
    +		n1, err = f.WriteAt(p[:e.Length], e.Start)
    +		// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
    +		closeErr := f.Close()
    +		n += n1
    +		p = p[n1:]
    +		if err == nil {
    +			err = closeErr
    +		}
    +		if err == nil && int64(n1) != e.Length {
    +			err = io.ErrShortWrite
    +		}
    +		return err == nil
    +	})
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/file_test.go b/deps/github.com/anacrolix/torrent/storage/file_test.go
    new file mode 100644
    index 0000000..a6c69fa
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/file_test.go
    @@ -0,0 +1,42 @@
    +package storage
    +
    +import (
    +	"bytes"
    +	"io"
    +	"os"
    +	"path/filepath"
    +	"testing"
    +
    +	"github.com/anacrolix/missinggo/v2"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +func TestShortFile(t *testing.T) {
    +	td := t.TempDir()
    +	s := NewFile(td)
    +	defer s.Close()
    +	info := &metainfo.Info{
    +		Name:        "a",
    +		Length:      2,
    +		PieceLength: missinggo.MiB,
    +	}
    +	ts, err := s.OpenTorrent(info, metainfo.Hash{})
    +	assert.NoError(t, err)
    +	f, err := os.Create(filepath.Join(td, "a"))
    +	require.NoError(t, err)
    +	err = f.Truncate(1)
    +	require.NoError(t, err)
    +	f.Close()
    +	var buf bytes.Buffer
    +	p := info.Piece(0)
    +	n, err := io.Copy(&buf, io.NewSectionReader(ts.Piece(p), 0, p.Length()))
    +	assert.EqualValues(t, 1, n)
    +	switch err {
    +	case nil, io.EOF:
    +	default:
    +		t.Errorf("expected nil or EOF error from truncated piece, got %v", err)
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/interface.go b/deps/github.com/anacrolix/torrent/storage/interface.go
    new file mode 100644
    index 0000000..9e8de06
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/interface.go
    @@ -0,0 +1,60 @@
    +package storage
    +
    +import (
    +	"io"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type ClientImplCloser interface {
    +	ClientImpl
    +	Close() error
    +}
    +
    +// Represents data storage for an unspecified torrent.
    +type ClientImpl interface {
    +	OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (TorrentImpl, error)
    +}
    +
    +type TorrentCapacity *func() (cap int64, capped bool)
    +
    +// Data storage bound to a torrent.
    +type TorrentImpl struct {
    +	Piece func(p metainfo.Piece) PieceImpl
    +	Close func() error
    +	Flush func() error
    +	// Storages that share the same space, will provide equal pointers. The function is called once
    +	// to determine the storage for torrents sharing the same function pointer, and mutated in
    +	// place.
    +	Capacity TorrentCapacity
    +}
    +
    +// Interacts with torrent piece data. Optional interfaces to implement include:
    +//
    +//	io.WriterTo, such as when a piece supports a more efficient way to write out incomplete chunks.
    +//	SelfHashing, such as when a piece supports a more efficient way to hash its contents.
    +type PieceImpl interface {
    +	// These interfaces are not as strict as normally required. They can
    +	// assume that the parameters are appropriate for the dimensions of the
    +	// piece.
    +	io.ReaderAt
    +	io.WriterAt
    +	// Called when the client believes the piece data will pass a hash check.
    +	// The storage can move or mark the piece data as read-only as it sees
    +	// fit.
    +	MarkComplete() error
    +	MarkNotComplete() error
    +	// Returns true if the piece is complete.
    +	Completion() Completion
    +}
    +
    +type Completion struct {
    +	Complete bool
    +	Ok       bool
    +	Err      error
    +}
    +
    +// Allows a storage backend to override hashing (i.e. if it can do it more efficiently than the torrent client can)
    +type SelfHashing interface {
    +	SelfHash() (metainfo.Hash, error)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/issue95_test.go b/deps/github.com/anacrolix/torrent/storage/issue95_test.go
    new file mode 100644
    index 0000000..9237079
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/issue95_test.go
    @@ -0,0 +1,51 @@
    +package storage
    +
    +import (
    +	"testing"
    +
    +	"github.com/anacrolix/missinggo/v2/resource"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +// Two different torrents opened from the same storage. Closing one should not
    +// break the piece completion on the other.
    +func testIssue95(t *testing.T, c ClientImpl) {
    +	i1 := &metainfo.Info{
    +		Files:  []metainfo.FileInfo{{Path: []string{"a"}}},
    +		Pieces: make([]byte, 20),
    +	}
    +	t1, err := c.OpenTorrent(i1, metainfo.HashBytes([]byte("a")))
    +	require.NoError(t, err)
    +	defer t1.Close()
    +	i2 := &metainfo.Info{
    +		Files:  []metainfo.FileInfo{{Path: []string{"a"}}},
    +		Pieces: make([]byte, 20),
    +	}
    +	t2, err := c.OpenTorrent(i2, metainfo.HashBytes([]byte("b")))
    +	require.NoError(t, err)
    +	defer t2.Close()
    +	t2p := t2.Piece(i2.Piece(0))
    +	assert.NoError(t, t1.Close())
    +	assert.NotPanics(t, func() { t2p.Completion() })
    +}
    +
    +func TestIssue95File(t *testing.T) {
    +	td := t.TempDir()
    +	cs := NewFile(td)
    +	defer cs.Close()
    +	testIssue95(t, cs)
    +}
    +
    +func TestIssue95MMap(t *testing.T) {
    +	td := t.TempDir()
    +	cs := NewMMap(td)
    +	defer cs.Close()
    +	testIssue95(t, cs)
    +}
    +
    +func TestIssue95ResourcePieces(t *testing.T) {
    +	testIssue95(t, NewResourcePieces(resource.OSFileProvider{}))
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/issue96_test.go b/deps/github.com/anacrolix/torrent/storage/issue96_test.go
    new file mode 100644
    index 0000000..726c11c
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/issue96_test.go
    @@ -0,0 +1,37 @@
    +package storage
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +func testMarkedCompleteMissingOnRead(t *testing.T, csf func(string) ClientImplCloser) {
    +	td := t.TempDir()
    +	cic := csf(td)
    +	defer cic.Close()
    +	cs := NewClient(cic)
    +	info := &metainfo.Info{
    +		PieceLength: 1,
    +		Files:       []metainfo.FileInfo{{Path: []string{"a"}, Length: 1}},
    +	}
    +	ts, err := cs.OpenTorrent(info, metainfo.Hash{})
    +	require.NoError(t, err)
    +	p := ts.Piece(info.Piece(0))
    +	require.NoError(t, p.MarkComplete())
    +	// require.False(t, p.GetIsComplete())
    +	n, err := p.ReadAt(make([]byte, 1), 0)
    +	require.Error(t, err)
    +	require.EqualValues(t, 0, n)
    +	require.False(t, p.Completion().Complete)
    +}
    +
    +func TestMarkedCompleteMissingOnReadFile(t *testing.T) {
    +	testMarkedCompleteMissingOnRead(t, NewFile)
    +}
    +
    +func TestMarkedCompleteMissingOnReadFileBoltDB(t *testing.T) {
    +	testMarkedCompleteMissingOnRead(t, NewBoltDB)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/map-piece-completion.go b/deps/github.com/anacrolix/torrent/storage/map-piece-completion.go
    new file mode 100644
    index 0000000..afb1e97
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/map-piece-completion.go
    @@ -0,0 +1,34 @@
    +package storage
    +
    +import (
    +	"sync"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type mapPieceCompletion struct {
    +	// TODO: Generics
    +	m sync.Map
    +}
    +
    +var _ PieceCompletion = (*mapPieceCompletion)(nil)
    +
    +func NewMapPieceCompletion() PieceCompletion {
    +	return &mapPieceCompletion{}
    +}
    +
    +func (*mapPieceCompletion) Close() error { return nil }
    +
    +func (me *mapPieceCompletion) Get(pk metainfo.PieceKey) (c Completion, err error) {
    +	v, ok := me.m.Load(pk)
    +	if ok {
    +		c.Complete = v.(bool)
    +	}
    +	c.Ok = ok
    +	return
    +}
    +
    +func (me *mapPieceCompletion) Set(pk metainfo.PieceKey, b bool) error {
    +	me.m.Store(pk, b)
    +	return nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/mark-complete_test.go b/deps/github.com/anacrolix/torrent/storage/mark-complete_test.go
    new file mode 100644
    index 0000000..7e50832
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/mark-complete_test.go
    @@ -0,0 +1,30 @@
    +package storage_test
    +
    +import (
    +	"testing"
    +
    +	"github.com/anacrolix/torrent/storage"
    +	test_storage "github.com/anacrolix/torrent/storage/test"
    +)
    +
    +func BenchmarkMarkComplete(b *testing.B) {
    +	bench := func(b *testing.B, ci storage.ClientImpl) {
    +		test_storage.BenchmarkPieceMarkComplete(
    +			b, ci, test_storage.DefaultPieceSize, test_storage.DefaultNumPieces, 0)
    +	}
    +	b.Run("File", func(b *testing.B) {
    +		ci := storage.NewFile(b.TempDir())
    +		b.Cleanup(func() { ci.Close() })
    +		bench(b, ci)
    +	})
    +	b.Run("Mmap", func(b *testing.B) {
    +		ci := storage.NewMMap(b.TempDir())
    +		b.Cleanup(func() { ci.Close() })
    +		bench(b, ci)
    +	})
    +	b.Run("BoltDb", func(b *testing.B) {
    +		ci := storage.NewBoltDB(b.TempDir())
    +		b.Cleanup(func() { ci.Close() })
    +		bench(b, ci)
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/mmap.go b/deps/github.com/anacrolix/torrent/storage/mmap.go
    new file mode 100644
    index 0000000..1851c32
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/mmap.go
    @@ -0,0 +1,230 @@
    +//go:build !wasm
    +// +build !wasm
    +
    +package storage
    +
    +import (
    +	"errors"
    +	"fmt"
    +	"io"
    +	"os"
    +	"path/filepath"
    +
    +	"github.com/anacrolix/missinggo/v2"
    +	"github.com/edsrzf/mmap-go"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	"github.com/anacrolix/torrent/mmap_span"
    +)
    +
    +type mmapClientImpl struct {
    +	baseDir string
    +	pc      PieceCompletion
    +}
    +
    +// TODO: Support all the same native filepath configuration that NewFileOpts provides.
    +func NewMMap(baseDir string) ClientImplCloser {
    +	return NewMMapWithCompletion(baseDir, pieceCompletionForDir(baseDir))
    +}
    +
    +func NewMMapWithCompletion(baseDir string, completion PieceCompletion) *mmapClientImpl {
    +	return &mmapClientImpl{
    +		baseDir: baseDir,
    +		pc:      completion,
    +	}
    +}
    +
    +func (s *mmapClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ TorrentImpl, err error) {
    +	span, err := mMapTorrent(info, s.baseDir)
    +	t := &mmapTorrentStorage{
    +		infoHash: infoHash,
    +		span:     span,
    +		pc:       s.pc,
    +	}
    +	return TorrentImpl{Piece: t.Piece, Close: t.Close, Flush: t.Flush}, err
    +}
    +
    +func (s *mmapClientImpl) Close() error {
    +	return s.pc.Close()
    +}
    +
    +type mmapTorrentStorage struct {
    +	infoHash metainfo.Hash
    +	span     *mmap_span.MMapSpan
    +	pc       PieceCompletionGetSetter
    +}
    +
    +func (ts *mmapTorrentStorage) Piece(p metainfo.Piece) PieceImpl {
    +	return mmapStoragePiece{
    +		pc:       ts.pc,
    +		p:        p,
    +		ih:       ts.infoHash,
    +		ReaderAt: io.NewSectionReader(ts.span, p.Offset(), p.Length()),
    +		WriterAt: missinggo.NewSectionWriter(ts.span, p.Offset(), p.Length()),
    +	}
    +}
    +
    +func (ts *mmapTorrentStorage) Close() error {
    +	errs := ts.span.Close()
    +	if len(errs) > 0 {
    +		return errs[0]
    +	}
    +	return nil
    +}
    +
    +func (ts *mmapTorrentStorage) Flush() error {
    +	errs := ts.span.Flush()
    +	if len(errs) > 0 {
    +		return errs[0]
    +	}
    +	return nil
    +}
    +
    +type mmapStoragePiece struct {
    +	pc PieceCompletionGetSetter
    +	p  metainfo.Piece
    +	ih metainfo.Hash
    +	io.ReaderAt
    +	io.WriterAt
    +}
    +
    +func (me mmapStoragePiece) pieceKey() metainfo.PieceKey {
    +	return metainfo.PieceKey{me.ih, me.p.Index()}
    +}
    +
    +func (sp mmapStoragePiece) Completion() Completion {
    +	c, err := sp.pc.Get(sp.pieceKey())
    +	if err != nil {
    +		panic(err)
    +	}
    +	return c
    +}
    +
    +func (sp mmapStoragePiece) MarkComplete() error {
    +	sp.pc.Set(sp.pieceKey(), true)
    +	return nil
    +}
    +
    +func (sp mmapStoragePiece) MarkNotComplete() error {
    +	sp.pc.Set(sp.pieceKey(), false)
    +	return nil
    +}
    +
    +func mMapTorrent(md *metainfo.Info, location string) (mms *mmap_span.MMapSpan, err error) {
    +	mms = &mmap_span.MMapSpan{}
    +	defer func() {
    +		if err != nil {
    +			mms.Close()
    +		}
    +	}()
    +	for _, miFile := range md.UpvertedFiles() {
    +		var safeName string
    +		safeName, err = ToSafeFilePath(append([]string{md.Name}, miFile.Path...)...)
    +		if err != nil {
    +			return
    +		}
    +		fileName := filepath.Join(location, safeName)
    +		var mm FileMapping
    +		mm, err = mmapFile(fileName, miFile.Length)
    +		if err != nil {
    +			err = fmt.Errorf("file %q: %s", miFile.DisplayPath(md), err)
    +			return
    +		}
    +		mms.Append(mm)
    +	}
    +	mms.InitIndex()
    +	return
    +}
    +
    +func mmapFile(name string, size int64) (_ FileMapping, err error) {
    +	dir := filepath.Dir(name)
    +	err = os.MkdirAll(dir, 0o750)
    +	if err != nil {
    +		err = fmt.Errorf("making directory %q: %s", dir, err)
    +		return
    +	}
    +	var file *os.File
    +	file, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o666)
    +	if err != nil {
    +		return
    +	}
    +	defer func() {
    +		if err != nil {
    +			file.Close()
    +		}
    +	}()
    +	var fi os.FileInfo
    +	fi, err = file.Stat()
    +	if err != nil {
    +		return
    +	}
    +	if fi.Size() < size {
    +		// I think this is necessary on HFS+. Maybe Linux will SIGBUS too if
    +		// you overmap a file but I'm not sure.
    +		err = file.Truncate(size)
    +		if err != nil {
    +			return
    +		}
    +	}
    +	return func() (ret mmapWithFile, err error) {
    +		ret.f = file
    +		if size == 0 {
    +			// Can't mmap() regions with length 0.
    +			return
    +		}
    +		intLen := int(size)
    +		if int64(intLen) != size {
    +			err = errors.New("size too large for system")
    +			return
    +		}
    +		ret.mmap, err = mmap.MapRegion(file, intLen, mmap.RDWR, 0, 0)
    +		if err != nil {
    +			err = fmt.Errorf("error mapping region: %s", err)
    +			return
    +		}
    +		if int64(len(ret.mmap)) != size {
    +			panic(len(ret.mmap))
    +		}
    +		return
    +	}()
    +}
    +
    +// Combines a mmapped region and file into a storage Mmap abstraction, which handles closing the
    +// mmap file handle.
    +func WrapFileMapping(region mmap.MMap, file *os.File) FileMapping {
    +	return mmapWithFile{
    +		f:    file,
    +		mmap: region,
    +	}
    +}
    +
    +type FileMapping = mmap_span.Mmap
    +
    +// Handles closing the mmap's file handle (needed for Windows). Could be implemented differently by
    +// OS.
    +type mmapWithFile struct {
    +	f    *os.File
    +	mmap mmap.MMap
    +}
    +
    +func (m mmapWithFile) Flush() error {
    +	return m.mmap.Flush()
    +}
    +
    +func (m mmapWithFile) Unmap() (err error) {
    +	if m.mmap != nil {
    +		err = m.mmap.Unmap()
    +	}
    +	fileErr := m.f.Close()
    +	if err == nil {
    +		err = fileErr
    +	}
    +	return
    +}
    +
    +func (m mmapWithFile) Bytes() []byte {
    +	if m.mmap == nil {
    +		return nil
    +	}
    +	return m.mmap
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/mmap_test.go b/deps/github.com/anacrolix/torrent/storage/mmap_test.go
    new file mode 100644
    index 0000000..54260ec
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/mmap_test.go
    @@ -0,0 +1,25 @@
    +package storage
    +
    +import (
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +
    +	"github.com/anacrolix/torrent/internal/testutil"
    +)
    +
    +func TestMmapWindows(t *testing.T) {
    +	c := qt.New(t)
    +	dir, mi := testutil.GreetingTestTorrent()
    +	cs := NewMMap(dir)
    +	defer func() {
    +		c.Check(cs.Close(), qt.IsNil)
    +	}()
    +	info, err := mi.UnmarshalInfo()
    +	c.Assert(err, qt.IsNil)
    +	ts, err := cs.OpenTorrent(&info, mi.HashInfoBytes())
    +	c.Assert(err, qt.IsNil)
    +	defer func() {
    +		c.Check(ts.Close(), qt.IsNil)
    +	}()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/piece-completion.go b/deps/github.com/anacrolix/torrent/storage/piece-completion.go
    new file mode 100644
    index 0000000..bc646bd
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/piece-completion.go
    @@ -0,0 +1,27 @@
    +package storage
    +
    +import (
    +	"github.com/anacrolix/log"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type PieceCompletionGetSetter interface {
    +	Get(metainfo.PieceKey) (Completion, error)
    +	Set(_ metainfo.PieceKey, complete bool) error
    +}
    +
    +// Implementations track the completion of pieces. It must be concurrent-safe.
    +type PieceCompletion interface {
    +	PieceCompletionGetSetter
    +	Close() error
    +}
    +
    +func pieceCompletionForDir(dir string) (ret PieceCompletion) {
    +	ret, err := NewDefaultPieceCompletionForDir(dir)
    +	if err != nil {
    +		log.Printf("couldn't open piece completion db in %q: %s", dir, err)
    +		ret = NewMapPieceCompletion()
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/piece-resource.go b/deps/github.com/anacrolix/torrent/storage/piece-resource.go
    new file mode 100644
    index 0000000..5327f31
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/piece-resource.go
    @@ -0,0 +1,279 @@
    +package storage
    +
    +import (
    +	"bytes"
    +	"fmt"
    +	"io"
    +	"path"
    +	"sort"
    +	"strconv"
    +	"sync"
    +
    +	"github.com/anacrolix/missinggo/v2/resource"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type piecePerResource struct {
    +	rp   PieceProvider
    +	opts ResourcePiecesOpts
    +}
    +
    +type ResourcePiecesOpts struct {
    +	// After marking a piece complete, don't bother deleting its incomplete blobs.
    +	LeaveIncompleteChunks bool
    +	// Sized puts require being able to stream from a statement executed on another connection.
    +	// Without them, we buffer the entire read and then put that.
    +	NoSizedPuts bool
    +	Capacity    *int64
    +}
    +
    +func NewResourcePieces(p PieceProvider) ClientImpl {
    +	return NewResourcePiecesOpts(p, ResourcePiecesOpts{})
    +}
    +
    +func NewResourcePiecesOpts(p PieceProvider, opts ResourcePiecesOpts) ClientImpl {
    +	return &piecePerResource{
    +		rp:   p,
    +		opts: opts,
    +	}
    +}
    +
    +type piecePerResourceTorrentImpl struct {
    +	piecePerResource
    +	locks []sync.RWMutex
    +}
    +
    +func (piecePerResourceTorrentImpl) Close() error {
    +	return nil
    +}
    +
    +func (s piecePerResource) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (TorrentImpl, error) {
    +	t := piecePerResourceTorrentImpl{
    +		s,
    +		make([]sync.RWMutex, info.NumPieces()),
    +	}
    +	return TorrentImpl{Piece: t.Piece, Close: t.Close}, nil
    +}
    +
    +func (s piecePerResourceTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
    +	return piecePerResourcePiece{
    +		mp:               p,
    +		piecePerResource: s.piecePerResource,
    +		mu:               &s.locks[p.Index()],
    +	}
    +}
    +
    +type PieceProvider interface {
    +	resource.Provider
    +}
    +
    +type ConsecutiveChunkReader interface {
    +	ReadConsecutiveChunks(prefix string) (io.ReadCloser, error)
    +}
    +
    +type piecePerResourcePiece struct {
    +	mp metainfo.Piece
    +	piecePerResource
    +	// This protects operations that move complete/incomplete pieces around, which can trigger read
    +	// errors that may cause callers to do more drastic things.
    +	mu *sync.RWMutex
    +}
    +
    +var _ io.WriterTo = piecePerResourcePiece{}
    +
    +func (s piecePerResourcePiece) WriteTo(w io.Writer) (int64, error) {
    +	s.mu.RLock()
    +	defer s.mu.RUnlock()
    +	if s.mustIsComplete() {
    +		r, err := s.completed().Get()
    +		if err != nil {
    +			return 0, fmt.Errorf("getting complete instance: %w", err)
    +		}
    +		defer r.Close()
    +		return io.Copy(w, r)
    +	}
    +	if ccr, ok := s.rp.(ConsecutiveChunkReader); ok {
    +		return s.writeConsecutiveIncompleteChunks(ccr, w)
    +	}
    +	return io.Copy(w, io.NewSectionReader(s, 0, s.mp.Length()))
    +}
    +
    +func (s piecePerResourcePiece) writeConsecutiveIncompleteChunks(ccw ConsecutiveChunkReader, w io.Writer) (int64, error) {
    +	r, err := ccw.ReadConsecutiveChunks(s.incompleteDirPath() + "/")
    +	if err != nil {
    +		return 0, err
    +	}
    +	defer r.Close()
    +	return io.Copy(w, r)
    +}
    +
    +// Returns if the piece is complete. Ok should be true, because we are the definitive source of
    +// truth here.
    +func (s piecePerResourcePiece) mustIsComplete() bool {
    +	completion := s.Completion()
    +	if !completion.Ok {
    +		panic("must know complete definitively")
    +	}
    +	return completion.Complete
    +}
    +
    +func (s piecePerResourcePiece) Completion() Completion {
    +	s.mu.RLock()
    +	defer s.mu.RUnlock()
    +	fi, err := s.completed().Stat()
    +	return Completion{
    +		Complete: err == nil && fi.Size() == s.mp.Length(),
    +		Ok:       true,
    +	}
    +}
    +
    +type SizedPutter interface {
    +	PutSized(io.Reader, int64) error
    +}
    +
    +func (s piecePerResourcePiece) MarkComplete() error {
    +	s.mu.Lock()
    +	defer s.mu.Unlock()
    +	incompleteChunks := s.getChunks()
    +	r, err := func() (io.ReadCloser, error) {
    +		if ccr, ok := s.rp.(ConsecutiveChunkReader); ok {
    +			return ccr.ReadConsecutiveChunks(s.incompleteDirPath() + "/")
    +		}
    +		return io.NopCloser(io.NewSectionReader(incompleteChunks, 0, s.mp.Length())), nil
    +	}()
    +	if err != nil {
    +		return fmt.Errorf("getting incomplete chunks reader: %w", err)
    +	}
    +	defer r.Close()
    +	completedInstance := s.completed()
    +	err = func() error {
    +		if sp, ok := completedInstance.(SizedPutter); ok && !s.opts.NoSizedPuts {
    +			return sp.PutSized(r, s.mp.Length())
    +		} else {
    +			return completedInstance.Put(r)
    +		}
    +	}()
    +	if err == nil && !s.opts.LeaveIncompleteChunks {
    +		// I think we do this synchronously here since we don't want callers to act on the completed
    +		// piece if we're concurrently still deleting chunks. The caller may decide to start
    +		// downloading chunks again and won't expect us to delete them. It seems to be much faster
    +		// to let the resource provider do this if possible.
    +		var wg sync.WaitGroup
    +		for _, c := range incompleteChunks {
    +			wg.Add(1)
    +			go func(c chunk) {
    +				defer wg.Done()
    +				c.instance.Delete()
    +			}(c)
    +		}
    +		wg.Wait()
    +	}
    +	return err
    +}
    +
    +func (s piecePerResourcePiece) MarkNotComplete() error {
    +	s.mu.Lock()
    +	defer s.mu.Unlock()
    +	return s.completed().Delete()
    +}
    +
    +func (s piecePerResourcePiece) ReadAt(b []byte, off int64) (int, error) {
    +	s.mu.RLock()
    +	defer s.mu.RUnlock()
    +	if s.mustIsComplete() {
    +		return s.completed().ReadAt(b, off)
    +	}
    +	return s.getChunks().ReadAt(b, off)
    +}
    +
    +func (s piecePerResourcePiece) WriteAt(b []byte, off int64) (n int, err error) {
    +	s.mu.RLock()
    +	defer s.mu.RUnlock()
    +	i, err := s.rp.NewInstance(path.Join(s.incompleteDirPath(), strconv.FormatInt(off, 10)))
    +	if err != nil {
    +		panic(err)
    +	}
    +	r := bytes.NewReader(b)
    +	if sp, ok := i.(SizedPutter); ok {
    +		err = sp.PutSized(r, r.Size())
    +	} else {
    +		err = i.Put(r)
    +	}
    +	n = len(b) - r.Len()
    +	return
    +}
    +
    +type chunk struct {
    +	offset   int64
    +	instance resource.Instance
    +}
    +
    +type chunks []chunk
    +
    +func (me chunks) ReadAt(b []byte, off int64) (int, error) {
    +	for {
    +		if len(me) == 0 {
    +			return 0, io.EOF
    +		}
    +		if me[0].offset <= off {
    +			break
    +		}
    +		me = me[1:]
    +	}
    +	n, err := me[0].instance.ReadAt(b, off-me[0].offset)
    +	if n == len(b) {
    +		return n, nil
    +	}
    +	if err == nil || err == io.EOF {
    +		n_, err := me[1:].ReadAt(b[n:], off+int64(n))
    +		return n + n_, err
    +	}
    +	return n, err
    +}
    +
    +func (s piecePerResourcePiece) getChunks() (chunks chunks) {
    +	names, err := s.incompleteDir().Readdirnames()
    +	if err != nil {
    +		return
    +	}
    +	for _, n := range names {
    +		offset, err := strconv.ParseInt(n, 10, 64)
    +		if err != nil {
    +			panic(err)
    +		}
    +		i, err := s.rp.NewInstance(path.Join(s.incompleteDirPath(), n))
    +		if err != nil {
    +			panic(err)
    +		}
    +		chunks = append(chunks, chunk{offset, i})
    +	}
    +	sort.Slice(chunks, func(i, j int) bool {
    +		return chunks[i].offset < chunks[j].offset
    +	})
    +	return
    +}
    +
    +func (s piecePerResourcePiece) completedInstancePath() string {
    +	return path.Join("completed", s.mp.Hash().HexString())
    +}
    +
    +func (s piecePerResourcePiece) completed() resource.Instance {
    +	i, err := s.rp.NewInstance(s.completedInstancePath())
    +	if err != nil {
    +		panic(err)
    +	}
    +	return i
    +}
    +
    +func (s piecePerResourcePiece) incompleteDirPath() string {
    +	return path.Join("incompleted", s.mp.Hash().HexString())
    +}
    +
    +func (s piecePerResourcePiece) incompleteDir() resource.DirInstance {
    +	i, err := s.rp.NewInstance(s.incompleteDirPath())
    +	if err != nil {
    +		panic(err)
    +	}
    +	return i.(resource.DirInstance)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/safe-path.go b/deps/github.com/anacrolix/torrent/storage/safe-path.go
    new file mode 100644
    index 0000000..9e50b7e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/safe-path.go
    @@ -0,0 +1,29 @@
    +package storage
    +
    +import (
    +	"errors"
    +	"path/filepath"
    +	"strings"
    +)
    +
    +// Get the first file path component. We can't use filepath.Split because that breaks off the last
    +// one. We could optimize this to avoid allocating a slice down the track.
    +func firstComponent(filePath string) string {
    +	return strings.SplitN(filePath, string(filepath.Separator), 2)[0]
    +}
    +
    +// Combines file info path components, ensuring the result won't escape into parent directories.
    +func ToSafeFilePath(fileInfoComponents ...string) (string, error) {
    +	safeComps := make([]string, 0, len(fileInfoComponents))
    +	for _, comp := range fileInfoComponents {
    +		safeComps = append(safeComps, filepath.Clean(comp))
    +	}
    +	safeFilePath := filepath.Join(safeComps...)
    +	fc := firstComponent(safeFilePath)
    +	switch fc {
    +	case "..":
    +		return "", errors.New("escapes root dir")
    +	default:
    +		return safeFilePath, nil
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/safe-path_test.go b/deps/github.com/anacrolix/torrent/storage/safe-path_test.go
    new file mode 100644
    index 0000000..452ab28
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/safe-path_test.go
    @@ -0,0 +1,71 @@
    +package storage
    +
    +import (
    +	"fmt"
    +	"log"
    +	"path/filepath"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +func init() {
    +	log.SetFlags(log.Flags() | log.Lshortfile)
    +}
    +
    +// I think these are mainly tests for bad metainfos that try to escape the client base directory.
    +var safeFilePathTests = []struct {
    +	input     []string
    +	expectErr bool
    +}{
    +	// We might want a test for invalid chars inside components, or file maker opt funcs returning
    +	// absolute paths (and thus presumably clobbering earlier "makers").
    +	{input: []string{"a", filepath.FromSlash(`b/..`)}, expectErr: false},
    +	{input: []string{"a", filepath.FromSlash(`b/../../..`)}, expectErr: true},
    +	{input: []string{"a", filepath.FromSlash(`b/../.././..`)}, expectErr: true},
    +	{
    +		input: []string{
    +			filepath.FromSlash(`NewSuperHeroMovie-2019-English-720p.avi /../../../../../Roaming/Microsoft/Windows/Start Menu/Programs/Startup/test3.exe`),
    +		},
    +		expectErr: true,
    +	},
    +}
    +
    +// Tests the ToSafeFilePath func.
    +func TestToSafeFilePath(t *testing.T) {
    +	for _, _case := range safeFilePathTests {
    +		actual, err := ToSafeFilePath(_case.input...)
    +		if _case.expectErr {
    +			if err != nil {
    +				continue
    +			}
    +			t.Errorf("%q: expected error, got output %q", _case.input, actual)
    +		}
    +	}
    +}
    +
    +// Check that safe file path handling still exists for the newer file-opt-maker variants.
    +func TestFileOptsSafeFilePathHandling(t *testing.T) {
    +	c := qt.New(t)
    +	for i, _case := range safeFilePathTests {
    +		c.Run(fmt.Sprintf("Case%v", i), func(c *qt.C) {
    +			info := metainfo.Info{
    +				Files: []metainfo.FileInfo{
    +					{Path: _case.input},
    +				},
    +			}
    +			client := NewFileOpts(NewFileClientOpts{
    +				ClientBaseDir: t.TempDir(),
    +			})
    +			defer func() { c.Check(client.Close(), qt.IsNil) }()
    +			torImpl, err := client.OpenTorrent(&info, metainfo.Hash{})
    +			if _case.expectErr {
    +				c.Check(err, qt.Not(qt.IsNil))
    +			} else {
    +				c.Check(torImpl.Close(), qt.IsNil)
    +			}
    +		})
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/sqlite-piece-completion.go b/deps/github.com/anacrolix/torrent/storage/sqlite-piece-completion.go
    new file mode 100644
    index 0000000..73407f3
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/sqlite-piece-completion.go
    @@ -0,0 +1,85 @@
    +// modernc.org/sqlite depends on modernc.org/libc which doesn't work for JS (and probably wasm but I
    +// think JS is the stronger signal).
    +
    +//go:build cgo && !nosqlite
    +// +build cgo,!nosqlite
    +
    +package storage
    +
    +import (
    +	"errors"
    +	"path/filepath"
    +	"sync"
    +
    +	"github.com/go-llsqlite/adapter"
    +	"github.com/go-llsqlite/adapter/sqlitex"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +// sqlite is always the default when available.
    +func NewDefaultPieceCompletionForDir(dir string) (PieceCompletion, error) {
    +	return NewSqlitePieceCompletion(dir)
    +}
    +
    +type sqlitePieceCompletion struct {
    +	mu     sync.Mutex
    +	closed bool
    +	db     *sqlite.Conn
    +}
    +
    +var _ PieceCompletion = (*sqlitePieceCompletion)(nil)
    +
    +func NewSqlitePieceCompletion(dir string) (ret *sqlitePieceCompletion, err error) {
    +	p := filepath.Join(dir, ".torrent.db")
    +	db, err := sqlite.OpenConn(p, 0)
    +	if err != nil {
    +		return
    +	}
    +	err = sqlitex.ExecScript(db, `create table if not exists piece_completion(infohash, "index", complete, unique(infohash, "index"))`)
    +	if err != nil {
    +		db.Close()
    +		return
    +	}
    +	ret = &sqlitePieceCompletion{db: db}
    +	return
    +}
    +
    +func (me *sqlitePieceCompletion) Get(pk metainfo.PieceKey) (c Completion, err error) {
    +	me.mu.Lock()
    +	defer me.mu.Unlock()
    +	err = sqlitex.Exec(
    +		me.db, `select complete from piece_completion where infohash=? and "index"=?`,
    +		func(stmt *sqlite.Stmt) error {
    +			c.Complete = stmt.ColumnInt(0) != 0
    +			c.Ok = true
    +			return nil
    +		},
    +		pk.InfoHash.HexString(), pk.Index)
    +	return
    +}
    +
    +func (me *sqlitePieceCompletion) Set(pk metainfo.PieceKey, b bool) error {
    +	me.mu.Lock()
    +	defer me.mu.Unlock()
    +	if me.closed {
    +		return errors.New("closed")
    +	}
    +	return sqlitex.Exec(
    +		me.db,
    +		`insert or replace into piece_completion(infohash, "index", complete) values(?, ?, ?)`,
    +		nil,
    +		pk.InfoHash.HexString(), pk.Index, b)
    +}
    +
    +func (me *sqlitePieceCompletion) Close() (err error) {
    +	me.mu.Lock()
    +	defer me.mu.Unlock()
    +	if me.closed {
    +		return
    +	}
    +	err = me.db.Close()
    +	me.db = nil
    +	me.closed = true
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/sqlite/deprecated.go b/deps/github.com/anacrolix/torrent/storage/sqlite/deprecated.go
    new file mode 100644
    index 0000000..47698ef
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/sqlite/deprecated.go
    @@ -0,0 +1,10 @@
    +//go:build cgo
    +// +build cgo
    +
    +package sqliteStorage
    +
    +import (
    +	"github.com/anacrolix/squirrel"
    +)
    +
    +type NewDirectStorageOpts = squirrel.NewCacheOpts
    diff --git a/deps/github.com/anacrolix/torrent/storage/sqlite/direct.go b/deps/github.com/anacrolix/torrent/storage/sqlite/direct.go
    new file mode 100644
    index 0000000..8e0a4a8
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/sqlite/direct.go
    @@ -0,0 +1,83 @@
    +//go:build cgo
    +// +build cgo
    +
    +package sqliteStorage
    +
    +import (
    +	"io"
    +
    +	"github.com/anacrolix/squirrel"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +// A convenience function that creates a connection pool, resource provider, and a pieces storage
    +// ClientImpl and returns them all with a Close attached.
    +func NewDirectStorage(opts NewDirectStorageOpts) (_ storage.ClientImplCloser, err error) {
    +	cache, err := squirrel.NewCache(opts)
    +	if err != nil {
    +		return
    +	}
    +	return &client{
    +		cache,
    +		cache.GetCapacity,
    +	}, nil
    +}
    +
    +func NewWrappingClient(cache *squirrel.Cache) storage.ClientImpl {
    +	return &client{
    +		cache,
    +		cache.GetCapacity,
    +	}
    +}
    +
    +type client struct {
    +	*squirrel.Cache
    +	capacity func() (int64, bool)
    +}
    +
    +func (c *client) OpenTorrent(*metainfo.Info, metainfo.Hash) (storage.TorrentImpl, error) {
    +	t := torrent{c.Cache}
    +	return storage.TorrentImpl{Piece: t.Piece, Close: t.Close, Capacity: &c.capacity}, nil
    +}
    +
    +type torrent struct {
    +	c *squirrel.Cache
    +}
    +
    +func (t torrent) Piece(p metainfo.Piece) storage.PieceImpl {
    +	ret := piece{
    +		sb: t.c.OpenWithLength(p.Hash().HexString(), p.Length()),
    +	}
    +	ret.ReaderAt = &ret.sb
    +	ret.WriterAt = &ret.sb
    +	return ret
    +}
    +
    +func (t torrent) Close() error {
    +	return nil
    +}
    +
    +type piece struct {
    +	sb squirrel.Blob
    +	io.ReaderAt
    +	io.WriterAt
    +}
    +
    +func (p piece) MarkComplete() error {
    +	return p.sb.SetTag("verified", true)
    +}
    +
    +func (p piece) MarkNotComplete() error {
    +	return p.sb.SetTag("verified", false)
    +}
    +
    +func (p piece) Completion() (ret storage.Completion) {
    +	err := p.sb.GetTag("verified", func(stmt squirrel.SqliteStmt) {
    +		ret.Complete = stmt.ColumnInt(0) != 0
    +	})
    +	ret.Ok = err == nil
    +	ret.Err = err
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/sqlite/dummy.go b/deps/github.com/anacrolix/torrent/storage/sqlite/dummy.go
    new file mode 100644
    index 0000000..ae48a77
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/sqlite/dummy.go
    @@ -0,0 +1 @@
    +package sqliteStorage
    diff --git a/deps/github.com/anacrolix/torrent/storage/sqlite/sqlite-storage_test.go b/deps/github.com/anacrolix/torrent/storage/sqlite/sqlite-storage_test.go
    new file mode 100644
    index 0000000..a566322
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/sqlite/sqlite-storage_test.go
    @@ -0,0 +1,106 @@
    +//go:build cgo
    +// +build cgo
    +
    +package sqliteStorage
    +
    +import (
    +	"errors"
    +	"fmt"
    +	"path/filepath"
    +	"testing"
    +
    +	_ "github.com/anacrolix/envpprof"
    +	"github.com/anacrolix/squirrel"
    +	"github.com/dustin/go-humanize"
    +	qt "github.com/frankban/quicktest"
    +
    +	"github.com/anacrolix/torrent/storage"
    +	test_storage "github.com/anacrolix/torrent/storage/test"
    +	"github.com/anacrolix/torrent/test"
    +)
    +
    +func TestLeecherStorage(t *testing.T) {
    +	test.TestLeecherStorage(t, test.LeecherStorageTestCase{
    +		"SqliteDirect",
    +		func(s string) storage.ClientImplCloser {
    +			path := filepath.Join(s, "sqlite3.db")
    +			var opts NewDirectStorageOpts
    +			opts.Path = path
    +			cl, err := NewDirectStorage(opts)
    +			if err != nil {
    +				panic(err)
    +			}
    +			return cl
    +		},
    +		0,
    +	})
    +}
    +
    +func BenchmarkMarkComplete(b *testing.B) {
    +	const pieceSize = test_storage.DefaultPieceSize
    +	const noTriggers = false
    +	var capacity int64 = test_storage.DefaultNumPieces * pieceSize / 2
    +	if noTriggers {
    +		// Since we won't push out old pieces, we have to mark them incomplete manually.
    +		capacity = 0
    +	}
    +	runBench := func(b *testing.B, ci storage.ClientImpl) {
    +		test_storage.BenchmarkPieceMarkComplete(b, ci, pieceSize, test_storage.DefaultNumPieces, capacity)
    +	}
    +	c := qt.New(b)
    +	b.Run("CustomDirect", func(b *testing.B) {
    +		var opts squirrel.NewCacheOpts
    +		opts.Capacity = capacity
    +		opts.NoTriggers = noTriggers
    +		benchOpts := func(b *testing.B) {
    +			opts.Path = filepath.Join(b.TempDir(), "storage.db")
    +			ci, err := NewDirectStorage(opts)
    +			c.Assert(err, qt.IsNil)
    +			defer ci.Close()
    +			runBench(b, ci)
    +		}
    +		b.Run("Default", benchOpts)
    +	})
    +	for _, memory := range []bool{false, true} {
    +		b.Run(fmt.Sprintf("Memory=%v", memory), func(b *testing.B) {
    +			b.Run("Direct", func(b *testing.B) {
    +				var opts NewDirectStorageOpts
    +				opts.Memory = memory
    +				opts.Capacity = capacity
    +				opts.NoTriggers = noTriggers
    +				directBench := func(b *testing.B) {
    +					opts.Path = filepath.Join(b.TempDir(), "storage.db")
    +					ci, err := NewDirectStorage(opts)
    +					var ujm squirrel.ErrUnexpectedJournalMode
    +					if errors.As(err, &ujm) {
    +						b.Skipf("setting journal mode %q: %v", opts.SetJournalMode, err)
    +					}
    +					c.Assert(err, qt.IsNil)
    +					defer ci.Close()
    +					runBench(b, ci)
    +				}
    +				for _, journalMode := range []string{"", "wal", "off", "truncate", "delete", "persist", "memory"} {
    +					opts.SetJournalMode = journalMode
    +					b.Run("JournalMode="+journalMode, func(b *testing.B) {
    +						for _, mmapSize := range []int64{-1} {
    +							if memory && mmapSize >= 0 {
    +								continue
    +							}
    +							b.Run(fmt.Sprintf("MmapSize=%s", func() string {
    +								if mmapSize < 0 {
    +									return "default"
    +								} else {
    +									return humanize.IBytes(uint64(mmapSize))
    +								}
    +							}()), func(b *testing.B) {
    +								opts.MmapSize = mmapSize
    +								opts.MmapSizeOk = true
    +								directBench(b)
    +							})
    +						}
    +					})
    +				}
    +			})
    +		})
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/storage_test.go b/deps/github.com/anacrolix/torrent/storage/storage_test.go
    new file mode 100644
    index 0000000..8eee160
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/storage_test.go
    @@ -0,0 +1,5 @@
    +package storage
    +
    +import (
    +	_ "github.com/anacrolix/envpprof"
    +)
    diff --git a/deps/github.com/anacrolix/torrent/storage/test/bench-piece-mark-complete.go b/deps/github.com/anacrolix/torrent/storage/test/bench-piece-mark-complete.go
    new file mode 100644
    index 0000000..43390e3
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/test/bench-piece-mark-complete.go
    @@ -0,0 +1,91 @@
    +package test_storage
    +
    +import (
    +	"bytes"
    +	"math/rand"
    +	"sync"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +const (
    +	ChunkSize        = 1 << 14
    +	DefaultPieceSize = 2 << 20
    +	DefaultNumPieces = 16
    +)
    +
    +// This writes chunks to the storage concurrently, and waits for them all to complete. This matches
    +// the behaviour from the peer connection read loop.
    +func BenchmarkPieceMarkComplete(
    +	b *testing.B, ci storage.ClientImpl,
    +	pieceSize int64, numPieces int,
    +	// This drives any special handling around capacity that may be configured into the storage
    +	// implementation.
    +	capacity int64,
    +) {
    +	c := qt.New(b)
    +	info := &metainfo.Info{
    +		Pieces:      make([]byte, numPieces*metainfo.HashSize),
    +		PieceLength: pieceSize,
    +		Length:      pieceSize * int64(numPieces),
    +		Name:        "TorrentName",
    +	}
    +	ti, err := ci.OpenTorrent(info, metainfo.Hash{})
    +	c.Assert(err, qt.IsNil)
    +	tw := storage.Torrent{ti}
    +	defer tw.Close()
    +	rand.Read(info.Pieces)
    +	data := make([]byte, pieceSize)
    +	readData := make([]byte, pieceSize)
    +	b.SetBytes(int64(numPieces) * pieceSize)
    +	oneIter := func() {
    +		for pieceIndex := 0; pieceIndex < numPieces; pieceIndex += 1 {
    +			pi := tw.Piece(info.Piece(pieceIndex))
    +			rand.Read(data)
    +			b.StartTimer()
    +			var wg sync.WaitGroup
    +			for off := int64(0); off < int64(len(data)); off += ChunkSize {
    +				wg.Add(1)
    +				go func(off int64) {
    +					defer wg.Done()
    +					n, err := pi.WriteAt(data[off:off+ChunkSize], off)
    +					if err != nil {
    +						panic(err)
    +					}
    +					if n != ChunkSize {
    +						panic(n)
    +					}
    +				}(off)
    +			}
    +			wg.Wait()
    +			if capacity == 0 {
    +				pi.MarkNotComplete()
    +			}
    +			// This might not apply if users of this benchmark don't cache with the expected capacity.
    +			c.Assert(pi.Completion(), qt.Equals, storage.Completion{Complete: false, Ok: true})
    +			c.Assert(pi.MarkComplete(), qt.IsNil)
    +			c.Assert(pi.Completion(), qt.Equals, storage.Completion{Complete: true, Ok: true})
    +			n, err := pi.WriteTo(bytes.NewBuffer(readData[:0]))
    +			b.StopTimer()
    +			c.Assert(err, qt.IsNil)
    +			c.Assert(n, qt.Equals, int64(len(data)))
    +			c.Assert(bytes.Equal(readData[:n], data), qt.IsTrue)
    +		}
    +	}
    +	// Fill the cache
    +	if capacity > 0 {
    +		iterN := int((capacity + info.TotalLength() - 1) / info.TotalLength())
    +		for i := 0; i < iterN; i += 1 {
    +			oneIter()
    +		}
    +	}
    +	b.StopTimer()
    +	b.ResetTimer()
    +	for i := 0; i < b.N; i += 1 {
    +		oneIter()
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/storage/wrappers.go b/deps/github.com/anacrolix/torrent/storage/wrappers.go
    new file mode 100644
    index 0000000..a3907e1
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/storage/wrappers.go
    @@ -0,0 +1,103 @@
    +package storage
    +
    +import (
    +	"io"
    +	"os"
    +
    +	"github.com/anacrolix/missinggo/v2"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type Client struct {
    +	ci ClientImpl
    +}
    +
    +func NewClient(cl ClientImpl) *Client {
    +	return &Client{cl}
    +}
    +
    +func (cl Client) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (*Torrent, error) {
    +	t, err := cl.ci.OpenTorrent(info, infoHash)
    +	if err != nil {
    +		return nil, err
    +	}
    +	return &Torrent{t}, nil
    +}
    +
    +type Torrent struct {
    +	TorrentImpl
    +}
    +
    +func (t Torrent) Piece(p metainfo.Piece) Piece {
    +	return Piece{t.TorrentImpl.Piece(p), p}
    +}
    +
    +type Piece struct {
    +	PieceImpl
    +	mip metainfo.Piece
    +}
    +
    +var _ io.WriterTo = Piece{}
    +
    +// Why do we have this wrapper? Well PieceImpl doesn't implement io.Reader, so we can't let io.Copy
    +// and friends check for io.WriterTo and fallback for us since they expect an io.Reader.
    +func (p Piece) WriteTo(w io.Writer) (int64, error) {
    +	if i, ok := p.PieceImpl.(io.WriterTo); ok {
    +		return i.WriteTo(w)
    +	}
    +	n := p.mip.Length()
    +	r := io.NewSectionReader(p, 0, n)
    +	return io.CopyN(w, r, n)
    +}
    +
    +func (p Piece) WriteAt(b []byte, off int64) (n int, err error) {
    +	// Callers should not be writing to completed pieces, but it's too
    +	// expensive to be checking this on every single write using uncached
    +	// completions.
    +
    +	// c := p.Completion()
    +	// if c.Ok && c.Complete {
    +	// 	err = errors.New("piece already completed")
    +	// 	return
    +	// }
    +	if off+int64(len(b)) > p.mip.Length() {
    +		panic("write overflows piece")
    +	}
    +	b = missinggo.LimitLen(b, p.mip.Length()-off)
    +	return p.PieceImpl.WriteAt(b, off)
    +}
    +
    +func (p Piece) ReadAt(b []byte, off int64) (n int, err error) {
    +	if off < 0 {
    +		err = os.ErrInvalid
    +		return
    +	}
    +	if off >= p.mip.Length() {
    +		err = io.EOF
    +		return
    +	}
    +	b = missinggo.LimitLen(b, p.mip.Length()-off)
    +	if len(b) == 0 {
    +		return
    +	}
    +	n, err = p.PieceImpl.ReadAt(b, off)
    +	if n > len(b) {
    +		panic(n)
    +	}
    +	if n == 0 && err == nil {
    +		panic("io.Copy will get stuck")
    +	}
    +	off += int64(n)
    +
    +	// Doing this here may be inaccurate. There's legitimate reasons we may fail to read while the
    +	// data is still there, such as too many open files. There should probably be a specific error
    +	// to return if the data has been lost.
    +	if off < p.mip.Length() {
    +		if err == io.EOF {
    +			p.MarkNotComplete()
    +		}
    +	}
    +
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/string-addr.go b/deps/github.com/anacrolix/torrent/string-addr.go
    new file mode 100644
    index 0000000..c124541
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/string-addr.go
    @@ -0,0 +1,11 @@
    +package torrent
    +
    +import "net"
    +
    +// This adds a net.Addr interface to a string address that has no presumed Network.
    +type StringAddr string
    +
    +var _ net.Addr = StringAddr("")
    +
    +func (StringAddr) Network() string   { return "" }
    +func (me StringAddr) String() string { return string(me) }
    diff --git a/deps/github.com/anacrolix/torrent/struct_test.go b/deps/github.com/anacrolix/torrent/struct_test.go
    new file mode 100644
    index 0000000..cee91e1
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/struct_test.go
    @@ -0,0 +1,12 @@
    +package torrent
    +
    +import (
    +	"testing"
    +	"unsafe"
    +)
    +
    +func TestStructSizes(t *testing.T) {
    +	t.Log("[]*File", unsafe.Sizeof([]*File(nil)))
    +	t.Log("Piece", unsafe.Sizeof(Piece{}))
    +	t.Log("map[*peer]struct{}", unsafe.Sizeof(map[*Peer]struct{}(nil)))
    +}
    diff --git a/deps/github.com/anacrolix/torrent/t.go b/deps/github.com/anacrolix/torrent/t.go
    new file mode 100644
    index 0000000..6a46070
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/t.go
    @@ -0,0 +1,286 @@
    +package torrent
    +
    +import (
    +	"strconv"
    +	"strings"
    +
    +	"github.com/anacrolix/chansync/events"
    +	"github.com/anacrolix/missinggo/v2/pubsub"
    +	"github.com/anacrolix/sync"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +// The Torrent's infohash. This is fixed and cannot change. It uniquely identifies a torrent.
    +func (t *Torrent) InfoHash() metainfo.Hash {
    +	return t.infoHash
    +}
    +
    +// Returns a channel that is closed when the info (.Info()) for the torrent has become available.
    +func (t *Torrent) GotInfo() events.Done {
    +	return t.gotMetainfoC
    +}
    +
    +// Returns the metainfo info dictionary, or nil if it's not yet available.
    +func (t *Torrent) Info() (info *metainfo.Info) {
    +	t.nameMu.RLock()
    +	info = t.info
    +	t.nameMu.RUnlock()
    +	return
    +}
    +
    +// Returns a Reader bound to the torrent's data. All read calls block until the data requested is
    +// actually available. Note that you probably want to ensure the Torrent Info is available first.
    +func (t *Torrent) NewReader() Reader {
    +	return t.newReader(0, t.length())
    +}
    +
    +func (t *Torrent) newReader(offset, length int64) Reader {
    +	r := reader{
    +		mu:     t.cl.locker(),
    +		t:      t,
    +		offset: offset,
    +		length: length,
    +	}
    +	r.readaheadFunc = defaultReadaheadFunc
    +	t.addReader(&r)
    +	return &r
    +}
    +
    +type PieceStateRuns []PieceStateRun
    +
    +func (me PieceStateRuns) String() (s string) {
    +	if len(me) > 0 {
    +		var sb strings.Builder
    +		sb.WriteString(me[0].String())
    +		for i := 1; i < len(me); i += 1 {
    +			sb.WriteByte(' ')
    +			sb.WriteString(me[i].String())
    +		}
    +		return sb.String()
    +	}
    +	return
    +}
    +
    +// Returns the state of pieces of the torrent. They are grouped into runs of same state. The sum of
    +// the state run-lengths is the number of pieces in the torrent.
    +func (t *Torrent) PieceStateRuns() (runs PieceStateRuns) {
    +	t.cl.rLock()
    +	runs = t.pieceStateRuns()
    +	t.cl.rUnlock()
    +	return
    +}
    +
    +func (t *Torrent) PieceState(piece pieceIndex) (ps PieceState) {
    +	t.cl.rLock()
    +	ps = t.pieceState(piece)
    +	t.cl.rUnlock()
    +	return
    +}
    +
    +// The number of pieces in the torrent. This requires that the info has been
    +// obtained first.
    +func (t *Torrent) NumPieces() pieceIndex {
    +	return t.numPieces()
    +}
    +
    +// Get missing bytes count for specific piece.
    +func (t *Torrent) PieceBytesMissing(piece int) int64 {
    +	t.cl.rLock()
    +	defer t.cl.rUnlock()
    +
    +	return int64(t.pieces[piece].bytesLeft())
    +}
    +
    +// Drop the torrent from the client, and close it. It's always safe to do
    +// this. No data corruption can, or should occur to either the torrent's data,
    +// or connected peers.
    +func (t *Torrent) Drop() {
    +	var wg sync.WaitGroup
    +	defer wg.Wait()
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	err := t.cl.dropTorrent(t.infoHash, &wg)
    +	if err != nil {
    +		panic(err)
    +	}
    +}
    +
    +// Number of bytes of the entire torrent we have completed. This is the sum of
    +// completed pieces, and dirtied chunks of incomplete pieces. Do not use this
    +// for download rate, as it can go down when pieces are lost or fail checks.
    +// Sample Torrent.Stats.DataBytesRead for actual file data download rate.
    +func (t *Torrent) BytesCompleted() int64 {
    +	t.cl.rLock()
    +	defer t.cl.rUnlock()
    +	return t.bytesCompleted()
    +}
    +
    +// The subscription emits as (int) the index of pieces as their state changes.
    +// A state change is when the PieceState for a piece alters in value.
    +func (t *Torrent) SubscribePieceStateChanges() *pubsub.Subscription[PieceStateChange] {
    +	return t.pieceStateChanges.Subscribe()
    +}
    +
    +// Returns true if the torrent is currently being seeded. This occurs when the
    +// client is willing to upload without wanting anything in return.
    +func (t *Torrent) Seeding() (ret bool) {
    +	t.cl.rLock()
    +	ret = t.seeding()
    +	t.cl.rUnlock()
    +	return
    +}
    +
    +// Clobbers the torrent display name if metainfo is unavailable.
    +// The display name is used as the torrent name while the metainfo is unavailable.
    +func (t *Torrent) SetDisplayName(dn string) {
    +	t.nameMu.Lock()
    +	if !t.haveInfo() {
    +		t.displayName = dn
    +	}
    +	t.nameMu.Unlock()
    +}
    +
    +// The current working name for the torrent. Either the name in the info dict,
    +// or a display name given such as by the dn value in a magnet link, or "".
    +func (t *Torrent) Name() string {
    +	return t.name()
    +}
    +
    +// The completed length of all the torrent data, in all its files. This is
    +// derived from the torrent info, when it is available.
    +func (t *Torrent) Length() int64 {
    +	return t._length.Value
    +}
    +
    +// Returns a run-time generated metainfo for the torrent that includes the
    +// info bytes and announce-list as currently known to the client.
    +func (t *Torrent) Metainfo() metainfo.MetaInfo {
    +	t.cl.rLock()
    +	defer t.cl.rUnlock()
    +	return t.newMetaInfo()
    +}
    +
    +func (t *Torrent) addReader(r *reader) {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	if t.readers == nil {
    +		t.readers = make(map[*reader]struct{})
    +	}
    +	t.readers[r] = struct{}{}
    +	r.posChanged()
    +}
    +
    +func (t *Torrent) deleteReader(r *reader) {
    +	delete(t.readers, r)
    +	t.readersChanged()
    +}
    +
    +// Raise the priorities of pieces in the range [begin, end) to at least Normal
    +// priority. Piece indexes are not the same as bytes. Requires that the info
    +// has been obtained, see Torrent.Info and Torrent.GotInfo.
    +func (t *Torrent) DownloadPieces(begin, end pieceIndex) {
    +	t.cl.lock()
    +	t.downloadPiecesLocked(begin, end)
    +	t.cl.unlock()
    +}
    +
    +func (t *Torrent) downloadPiecesLocked(begin, end pieceIndex) {
    +	for i := begin; i < end; i++ {
    +		if t.pieces[i].priority.Raise(PiecePriorityNormal) {
    +			t.updatePiecePriority(i, "Torrent.DownloadPieces")
    +		}
    +	}
    +}
    +
    +func (t *Torrent) CancelPieces(begin, end pieceIndex) {
    +	t.cl.lock()
    +	t.cancelPiecesLocked(begin, end, "Torrent.CancelPieces")
    +	t.cl.unlock()
    +}
    +
    +func (t *Torrent) cancelPiecesLocked(begin, end pieceIndex, reason string) {
    +	for i := begin; i < end; i++ {
    +		p := &t.pieces[i]
    +		if p.priority == PiecePriorityNone {
    +			continue
    +		}
    +		p.priority = PiecePriorityNone
    +		t.updatePiecePriority(i, reason)
    +	}
    +}
    +
    +func (t *Torrent) initFiles() {
    +	var offset int64
    +	t.files = new([]*File)
    +	for _, fi := range t.info.UpvertedFiles() {
    +		*t.files = append(*t.files, &File{
    +			t,
    +			strings.Join(append([]string{t.info.BestName()}, fi.BestPath()...), "/"),
    +			offset,
    +			fi.Length,
    +			fi,
    +			fi.DisplayPath(t.info),
    +			PiecePriorityNone,
    +		})
    +		offset += fi.Length
    +	}
    +}
    +
    +// Returns handles to the files in the torrent. This requires that the Info is
    +// available first.
    +func (t *Torrent) Files() []*File {
    +	return *t.files
    +}
    +
    +func (t *Torrent) AddPeers(pp []PeerInfo) (n int) {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	n = t.addPeers(pp)
    +	return
    +}
    +
    +// Marks the entire torrent for download. Requires the info first, see
    +// GotInfo. Sets piece priorities for historical reasons.
    +func (t *Torrent) DownloadAll() {
    +	t.DownloadPieces(0, t.numPieces())
    +}
    +
    +func (t *Torrent) String() string {
    +	s := t.name()
    +	if s == "" {
    +		return t.infoHash.HexString()
    +	} else {
    +		return strconv.Quote(s)
    +	}
    +}
    +
    +func (t *Torrent) AddTrackers(announceList [][]string) {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	t.addTrackers(announceList)
    +}
    +
    +func (t *Torrent) Piece(i pieceIndex) *Piece {
    +	return t.piece(i)
    +}
    +
    +func (t *Torrent) PeerConns() []*PeerConn {
    +	t.cl.rLock()
    +	defer t.cl.rUnlock()
    +	ret := make([]*PeerConn, 0, len(t.conns))
    +	for c := range t.conns {
    +		ret = append(ret, c)
    +	}
    +	return ret
    +}
    +
    +func (t *Torrent) WebseedPeerConns() []*Peer {
    +	t.cl.rLock()
    +	defer t.cl.rUnlock()
    +	ret := make([]*Peer, 0, len(t.conns))
    +	for _, c := range t.webSeeds {
    +		ret = append(ret, c)
    +	}
    +	return ret
    +}
    diff --git a/deps/github.com/anacrolix/torrent/test/init_test.go b/deps/github.com/anacrolix/torrent/test/init_test.go
    new file mode 100644
    index 0000000..b862d4b
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/test/init_test.go
    @@ -0,0 +1,11 @@
    +package test
    +
    +import (
    +	"log"
    +
    +	_ "github.com/anacrolix/envpprof"
    +)
    +
    +func init() {
    +	log.SetFlags(log.Flags() | log.Lshortfile)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/test/issue377_test.go b/deps/github.com/anacrolix/torrent/test/issue377_test.go
    new file mode 100644
    index 0000000..5b0e659
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/test/issue377_test.go
    @@ -0,0 +1,183 @@
    +package test
    +
    +import (
    +	"errors"
    +	"io"
    +	"os"
    +	"sync"
    +	"testing"
    +	"testing/iotest"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent"
    +	"github.com/anacrolix/torrent/internal/testutil"
    +	"github.com/anacrolix/torrent/metainfo"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +func justOneNetwork(cc *torrent.ClientConfig) {
    +	cc.DisableTCP = true
    +	cc.DisableIPv4 = true
    +}
    +
    +func TestReceiveChunkStorageFailureSeederFastExtensionDisabled(t *testing.T) {
    +	testReceiveChunkStorageFailure(t, false)
    +}
    +
    +func TestReceiveChunkStorageFailure(t *testing.T) {
    +	testReceiveChunkStorageFailure(t, true)
    +}
    +
    +func testReceiveChunkStorageFailure(t *testing.T, seederFast bool) {
    +	seederDataDir, metainfo := testutil.GreetingTestTorrent()
    +	defer os.RemoveAll(seederDataDir)
    +	seederClientConfig := torrent.TestingConfig(t)
    +	seederClientConfig.Debug = true
    +	justOneNetwork(seederClientConfig)
    +	seederClientStorage := storage.NewMMap(seederDataDir)
    +	defer seederClientStorage.Close()
    +	seederClientConfig.DefaultStorage = seederClientStorage
    +	seederClientConfig.Seed = true
    +	seederClientConfig.Debug = true
    +	seederClientConfig.Extensions.SetBit(pp.ExtensionBitFast, seederFast)
    +	seederClient, err := torrent.NewClient(seederClientConfig)
    +	require.NoError(t, err)
    +	defer seederClient.Close()
    +	defer testutil.ExportStatusWriter(seederClient, "s", t)()
    +	leecherClientConfig := torrent.TestingConfig(t)
    +	leecherClientConfig.Debug = true
    +	// Don't require fast extension, whether the seeder will provide it or not (so we can test mixed
    +	// cases).
    +	leecherClientConfig.MinPeerExtensions.SetBit(pp.ExtensionBitFast, false)
    +	justOneNetwork(leecherClientConfig)
    +	leecherClient, err := torrent.NewClient(leecherClientConfig)
    +	require.NoError(t, err)
    +	defer leecherClient.Close()
    +	defer testutil.ExportStatusWriter(leecherClient, "l", t)()
    +	info, err := metainfo.UnmarshalInfo()
    +	require.NoError(t, err)
    +	leecherStorage := diskFullStorage{
    +		pieces: make([]pieceState, info.NumPieces()),
    +		data:   make([]byte, info.TotalLength()),
    +	}
    +	defer leecherStorage.Close()
    +	leecherTorrent, new, err := leecherClient.AddTorrentSpec(&torrent.TorrentSpec{
    +		InfoHash: metainfo.HashInfoBytes(),
    +		Storage:  &leecherStorage,
    +	})
    +	leecherStorage.t = leecherTorrent
    +	require.NoError(t, err)
    +	assert.True(t, new)
    +	seederTorrent, err := seederClient.AddTorrent(metainfo)
    +	require.NoError(t, err)
    +	// Tell the seeder to find the leecher. Is it guaranteed seeders will always try to do this?
    +	seederTorrent.AddClientPeer(leecherClient)
    +	<-leecherTorrent.GotInfo()
    +	r := leecherTorrent.Files()[0].NewReader()
    +	defer r.Close()
    +	// We can't use assertReadAllGreeting here, because the default storage write error handler
    +	// disables data downloads, which now causes Readers to error when they're blocked.
    +	if false {
    +		assertReadAllGreeting(t, leecherTorrent.NewReader())
    +	} else {
    +		for func() bool {
    +			// We don't seem to need to seek, but that's probably just because the storage failure is
    +			// happening on the first read.
    +			r.Seek(0, io.SeekStart)
    +			if err := iotest.TestReader(r, []byte(testutil.GreetingFileContents)); err != nil {
    +				t.Logf("got error while reading: %v", err)
    +				return true
    +			}
    +			return false
    +		}() {
    +		}
    +	}
    +	// TODO: Check that PeerConns fastEnabled matches seederFast?
    +	// select {}
    +}
    +
    +type pieceState struct {
    +	complete bool
    +}
    +
    +type diskFullStorage struct {
    +	pieces                        []pieceState
    +	t                             *torrent.Torrent
    +	defaultHandledWriteChunkError bool
    +	data                          []byte
    +
    +	mu          sync.Mutex
    +	diskNotFull bool
    +}
    +
    +func (me *diskFullStorage) Piece(p metainfo.Piece) storage.PieceImpl {
    +	return pieceImpl{
    +		mip:             p,
    +		diskFullStorage: me,
    +	}
    +}
    +
    +func (me *diskFullStorage) Close() error {
    +	return nil
    +}
    +
    +func (d *diskFullStorage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (storage.TorrentImpl, error) {
    +	return storage.TorrentImpl{Piece: d.Piece, Close: d.Close}, nil
    +}
    +
    +type pieceImpl struct {
    +	mip metainfo.Piece
    +	*diskFullStorage
    +}
    +
    +func (me pieceImpl) state() *pieceState {
    +	return &me.diskFullStorage.pieces[me.mip.Index()]
    +}
    +
    +func (me pieceImpl) ReadAt(p []byte, off int64) (n int, err error) {
    +	off += me.mip.Offset()
    +	return copy(p, me.data[off:]), nil
    +}
    +
    +func (me pieceImpl) WriteAt(p []byte, off int64) (int, error) {
    +	off += me.mip.Offset()
    +	if !me.defaultHandledWriteChunkError {
    +		go func() {
    +			me.t.SetOnWriteChunkError(func(err error) {
    +				log.Printf("got write chunk error to custom handler: %v", err)
    +				me.mu.Lock()
    +				me.diskNotFull = true
    +				me.mu.Unlock()
    +				me.t.AllowDataDownload()
    +			})
    +			me.t.AllowDataDownload()
    +		}()
    +		me.defaultHandledWriteChunkError = true
    +	}
    +	me.mu.Lock()
    +	defer me.mu.Unlock()
    +	if me.diskNotFull {
    +		return copy(me.data[off:], p), nil
    +	}
    +	return copy(me.data[off:], p[:1]), errors.New("disk full")
    +}
    +
    +func (me pieceImpl) MarkComplete() error {
    +	me.state().complete = true
    +	return nil
    +}
    +
    +func (me pieceImpl) MarkNotComplete() error {
    +	panic("implement me")
    +}
    +
    +func (me pieceImpl) Completion() storage.Completion {
    +	return storage.Completion{
    +		Complete: me.state().complete,
    +		Ok:       true,
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/test/leecher-storage.go b/deps/github.com/anacrolix/torrent/test/leecher-storage.go
    new file mode 100644
    index 0000000..a60f077
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/test/leecher-storage.go
    @@ -0,0 +1,255 @@
    +package test
    +
    +import (
    +	"fmt"
    +	"io"
    +	"os"
    +	"runtime"
    +	"testing"
    +	"testing/iotest"
    +
    +	"github.com/anacrolix/missinggo/v2/bitmap"
    +	"github.com/frankban/quicktest"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +	"golang.org/x/time/rate"
    +
    +	"github.com/anacrolix/torrent"
    +	"github.com/anacrolix/torrent/internal/testutil"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +type LeecherStorageTestCase struct {
    +	Name       string
    +	Factory    StorageFactory
    +	GoMaxProcs int
    +}
    +
    +type StorageFactory func(string) storage.ClientImplCloser
    +
    +func TestLeecherStorage(t *testing.T, ls LeecherStorageTestCase) {
    +	// Seeder storage
    +	for _, ss := range []struct {
    +		name string
    +		f    StorageFactory
    +	}{
    +		{"File", storage.NewFile},
    +		{"Mmap", storage.NewMMap},
    +	} {
    +		t.Run(fmt.Sprintf("%sSeederStorage", ss.name), func(t *testing.T) {
    +			for _, responsive := range []bool{false, true} {
    +				t.Run(fmt.Sprintf("Responsive=%v", responsive), func(t *testing.T) {
    +					t.Run("NoReadahead", func(t *testing.T) {
    +						testClientTransfer(t, testClientTransferParams{
    +							Responsive:     responsive,
    +							SeederStorage:  ss.f,
    +							LeecherStorage: ls.Factory,
    +							GOMAXPROCS:     ls.GoMaxProcs,
    +						})
    +					})
    +					for _, readahead := range []int64{-1, 0, 1, 2, 9, 20} {
    +						t.Run(fmt.Sprintf("readahead=%v", readahead), func(t *testing.T) {
    +							testClientTransfer(t, testClientTransferParams{
    +								SeederStorage:  ss.f,
    +								Responsive:     responsive,
    +								SetReadahead:   true,
    +								Readahead:      readahead,
    +								LeecherStorage: ls.Factory,
    +								GOMAXPROCS:     ls.GoMaxProcs,
    +							})
    +						})
    +					}
    +				})
    +			}
    +		})
    +	}
    +}
    +
    +type ConfigureClient struct {
    +	Config func(cfg *torrent.ClientConfig)
    +	Client func(cl *torrent.Client)
    +}
    +
    +type testClientTransferParams struct {
    +	Responsive     bool
    +	Readahead      int64
    +	SetReadahead   bool
    +	LeecherStorage func(string) storage.ClientImplCloser
    +	// TODO: Use a generic option type. This is the capacity of the leecher storage for determining
    +	// whether it's possible for the leecher to be Complete. 0 currently means no limit.
    +	LeecherStorageCapacity     int64
    +	SeederStorage              func(string) storage.ClientImplCloser
    +	SeederUploadRateLimiter    *rate.Limiter
    +	LeecherDownloadRateLimiter *rate.Limiter
    +	ConfigureSeeder            ConfigureClient
    +	ConfigureLeecher           ConfigureClient
    +	GOMAXPROCS                 int
    +
    +	LeecherStartsWithoutMetadata bool
    +}
    +
    +// Creates a seeder and a leecher, and ensures the data transfers when a read
    +// is attempted on the leecher.
    +func testClientTransfer(t *testing.T, ps testClientTransferParams) {
    +	prevGOMAXPROCS := runtime.GOMAXPROCS(ps.GOMAXPROCS)
    +	newGOMAXPROCS := prevGOMAXPROCS
    +	if ps.GOMAXPROCS > 0 {
    +		newGOMAXPROCS = ps.GOMAXPROCS
    +	}
    +	defer func() {
    +		quicktest.Check(t, runtime.GOMAXPROCS(prevGOMAXPROCS), quicktest.ContentEquals, newGOMAXPROCS)
    +	}()
    +
    +	greetingTempDir, mi := testutil.GreetingTestTorrent()
    +	defer os.RemoveAll(greetingTempDir)
    +	// Create seeder and a Torrent.
    +	cfg := torrent.TestingConfig(t)
    +	// cfg.Debug = true
    +	cfg.Seed = true
    +	// Less than a piece, more than a single request.
    +	cfg.MaxAllocPeerRequestDataPerConn = 4
    +	// Some test instances don't like this being on, even when there's no cache involved.
    +	cfg.DropMutuallyCompletePeers = false
    +	if ps.SeederUploadRateLimiter != nil {
    +		cfg.UploadRateLimiter = ps.SeederUploadRateLimiter
    +	}
    +	// cfg.ListenAddr = "localhost:4000"
    +	if ps.SeederStorage != nil {
    +		storage := ps.SeederStorage(greetingTempDir)
    +		defer storage.Close()
    +		cfg.DefaultStorage = storage
    +	} else {
    +		cfg.DataDir = greetingTempDir
    +	}
    +	if ps.ConfigureSeeder.Config != nil {
    +		ps.ConfigureSeeder.Config(cfg)
    +	}
    +	seeder, err := torrent.NewClient(cfg)
    +	require.NoError(t, err)
    +	if ps.ConfigureSeeder.Client != nil {
    +		ps.ConfigureSeeder.Client(seeder)
    +	}
    +	defer testutil.ExportStatusWriter(seeder, "s", t)()
    +	seederTorrent, _, _ := seeder.AddTorrentSpec(torrent.TorrentSpecFromMetaInfo(mi))
    +	// Run a Stats right after Closing the Client. This will trigger the Stats
    +	// panic in #214 caused by RemoteAddr on Closed uTP sockets.
    +	defer seederTorrent.Stats()
    +	defer seeder.Close()
    +	// Adding a torrent and setting the info should trigger piece checks for everything
    +	// automatically. Wait until the seed Torrent agrees that everything is available.
    +	<-seederTorrent.Complete.On()
    +	// Create leecher and a Torrent.
    +	leecherDataDir := t.TempDir()
    +	cfg = torrent.TestingConfig(t)
    +	// See the seeder client config comment.
    +	cfg.DropMutuallyCompletePeers = false
    +	if ps.LeecherStorage == nil {
    +		cfg.DataDir = leecherDataDir
    +	} else {
    +		storage := ps.LeecherStorage(leecherDataDir)
    +		defer storage.Close()
    +		cfg.DefaultStorage = storage
    +	}
    +	if ps.LeecherDownloadRateLimiter != nil {
    +		cfg.DownloadRateLimiter = ps.LeecherDownloadRateLimiter
    +	}
    +	cfg.Seed = false
    +	// cfg.Debug = true
    +	if ps.ConfigureLeecher.Config != nil {
    +		ps.ConfigureLeecher.Config(cfg)
    +	}
    +	leecher, err := torrent.NewClient(cfg)
    +	require.NoError(t, err)
    +	defer leecher.Close()
    +	if ps.ConfigureLeecher.Client != nil {
    +		ps.ConfigureLeecher.Client(leecher)
    +	}
    +	defer testutil.ExportStatusWriter(leecher, "l", t)()
    +	leecherTorrent, new, err := leecher.AddTorrentSpec(func() (ret *torrent.TorrentSpec) {
    +		ret = torrent.TorrentSpecFromMetaInfo(mi)
    +		ret.ChunkSize = 2
    +		if ps.LeecherStartsWithoutMetadata {
    +			ret.InfoBytes = nil
    +		}
    +		return
    +	}())
    +	require.NoError(t, err)
    +	assert.False(t, leecherTorrent.Complete.Bool())
    +	assert.True(t, new)
    +
    +	//// This was used when observing coalescing of piece state changes.
    +	//logPieceStateChanges(leecherTorrent)
    +
    +	// Now do some things with leecher and seeder.
    +	added := leecherTorrent.AddClientPeer(seeder)
    +	assert.False(t, leecherTorrent.Seeding())
    +	// The leecher will use peers immediately if it doesn't have the metadata. Otherwise, they
    +	// should be sitting idle until we demand data.
    +	if !ps.LeecherStartsWithoutMetadata {
    +		assert.EqualValues(t, added, leecherTorrent.Stats().PendingPeers)
    +	}
    +	if ps.LeecherStartsWithoutMetadata {
    +		<-leecherTorrent.GotInfo()
    +	}
    +	r := leecherTorrent.NewReader()
    +	defer r.Close()
    +	go leecherTorrent.SetInfoBytes(mi.InfoBytes)
    +	if ps.Responsive {
    +		r.SetResponsive()
    +	}
    +	if ps.SetReadahead {
    +		r.SetReadahead(ps.Readahead)
    +	}
    +	assertReadAllGreeting(t, r)
    +	info, err := mi.UnmarshalInfo()
    +	require.NoError(t, err)
    +	canComplete := ps.LeecherStorageCapacity == 0 || ps.LeecherStorageCapacity >= info.TotalLength()
    +	if !canComplete {
    +		// Reading from a cache doesn't refresh older pieces until we fail to read those, so we need
    +		// to force a refresh since we just read the contents from start to finish.
    +		go leecherTorrent.VerifyData()
    +	}
    +	if canComplete {
    +		<-leecherTorrent.Complete.On()
    +	} else {
    +		<-leecherTorrent.Complete.Off()
    +	}
    +	assert.NotEmpty(t, seederTorrent.PeerConns())
    +	leecherPeerConns := leecherTorrent.PeerConns()
    +	if cfg.DropMutuallyCompletePeers {
    +		// I don't think we can assume it will be empty already, due to timing.
    +		// assert.Empty(t, leecherPeerConns)
    +	} else {
    +		assert.NotEmpty(t, leecherPeerConns)
    +	}
    +	foundSeeder := false
    +	for _, pc := range leecherPeerConns {
    +		completed := pc.PeerPieces().GetCardinality()
    +		t.Logf("peer conn %v has %v completed pieces", pc, completed)
    +		if completed == bitmap.BitRange(leecherTorrent.Info().NumPieces()) {
    +			foundSeeder = true
    +		}
    +	}
    +	if !foundSeeder {
    +		t.Errorf("didn't find seeder amongst leecher peer conns")
    +	}
    +
    +	seederStats := seederTorrent.Stats()
    +	assert.True(t, 13 <= seederStats.BytesWrittenData.Int64())
    +	assert.True(t, 8 <= seederStats.ChunksWritten.Int64())
    +
    +	leecherStats := leecherTorrent.Stats()
    +	assert.True(t, 13 <= leecherStats.BytesReadData.Int64())
    +	assert.True(t, 8 <= leecherStats.ChunksRead.Int64())
    +
    +	// Try reading through again for the cases where the torrent data size
    +	// exceeds the size of the cache.
    +	assertReadAllGreeting(t, r)
    +}
    +
    +func assertReadAllGreeting(t *testing.T, r io.ReadSeeker) {
    +	pos, err := r.Seek(0, io.SeekStart)
    +	assert.NoError(t, err)
    +	assert.EqualValues(t, 0, pos)
    +	quicktest.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), quicktest.IsNil)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/test/sqlite_test.go b/deps/github.com/anacrolix/torrent/test/sqlite_test.go
    new file mode 100644
    index 0000000..437499d
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/test/sqlite_test.go
    @@ -0,0 +1,68 @@
    +// This infernal language makes me copy conditional compilation expressions around. This test should
    +// run if sqlite storage is enabled, period.
    +
    +//go:build cgo
    +// +build cgo
    +
    +package test
    +
    +import (
    +	"net"
    +	"net/http"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +
    +	"github.com/anacrolix/torrent"
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/metainfo"
    +	sqliteStorage "github.com/anacrolix/torrent/storage/sqlite"
    +)
    +
    +func TestSqliteStorageClosed(t *testing.T) {
    +	c := qt.New(t)
    +	cfg := torrent.TestingConfig(t)
    +	storage, err := sqliteStorage.NewDirectStorage(sqliteStorage.NewDirectStorageOpts{})
    +	defer storage.Close()
    +	cfg.DefaultStorage = storage
    +	cfg.Debug = true
    +	c.Assert(err, qt.IsNil)
    +	cl, err := torrent.NewClient(cfg)
    +	c.Assert(err, qt.IsNil)
    +	defer cl.Close()
    +	l, err := net.Listen("tcp", "localhost:0")
    +	c.Assert(err, qt.IsNil)
    +	defer l.Close()
    +	// We need at least once piece to trigger a call to storage to determine completion state. We
    +	// need non-zero content length to trigger piece hashing.
    +	i := metainfo.Info{
    +		Pieces:      make([]byte, metainfo.HashSize),
    +		PieceLength: 1,
    +		Files: []metainfo.FileInfo{
    +			{Length: 1},
    +		},
    +	}
    +	mi := metainfo.MetaInfo{}
    +	mi.InfoBytes, err = bencode.Marshal(i)
    +	c.Assert(err, qt.IsNil)
    +	s := http.Server{
    +		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    +			mi.Write(w)
    +		}),
    +	}
    +	defer s.Close()
    +	go func() {
    +		err := s.Serve(l)
    +		if err != http.ErrServerClosed {
    +			panic(err)
    +		}
    +	}()
    +	// Close storage prematurely.
    +	storage.Close()
    +	tor, _, err := cl.AddTorrentSpec(&torrent.TorrentSpec{
    +		InfoHash: mi.HashInfoBytes(),
    +		Sources:  []string{"http://" + l.Addr().String()},
    +	})
    +	c.Assert(err, qt.IsNil)
    +	<-tor.GotInfo()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/test/transfer_test.go b/deps/github.com/anacrolix/torrent/test/transfer_test.go
    new file mode 100644
    index 0000000..501872f
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/test/transfer_test.go
    @@ -0,0 +1,226 @@
    +package test
    +
    +import (
    +	"io"
    +	"os"
    +	"sync"
    +	"testing"
    +	"testing/iotest"
    +	"time"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/v2/filecache"
    +	qt "github.com/frankban/quicktest"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +	"golang.org/x/time/rate"
    +
    +	"github.com/anacrolix/torrent"
    +	"github.com/anacrolix/torrent/internal/testutil"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +type fileCacheClientStorageFactoryParams struct {
    +	Capacity    int64
    +	SetCapacity bool
    +}
    +
    +func newFileCacheClientStorageFactory(ps fileCacheClientStorageFactoryParams) StorageFactory {
    +	return func(dataDir string) storage.ClientImplCloser {
    +		fc, err := filecache.NewCache(dataDir)
    +		if err != nil {
    +			panic(err)
    +		}
    +		var sharedCapacity *int64
    +		if ps.SetCapacity {
    +			sharedCapacity = &ps.Capacity
    +			fc.SetCapacity(ps.Capacity)
    +		}
    +		return struct {
    +			storage.ClientImpl
    +			io.Closer
    +		}{
    +			storage.NewResourcePiecesOpts(
    +				fc.AsResourceProvider(),
    +				storage.ResourcePiecesOpts{
    +					Capacity: sharedCapacity,
    +				}),
    +			io.NopCloser(nil),
    +		}
    +	}
    +}
    +
    +func TestClientTransferDefault(t *testing.T) {
    +	testClientTransfer(t, testClientTransferParams{
    +		LeecherStorage: newFileCacheClientStorageFactory(fileCacheClientStorageFactoryParams{}),
    +	})
    +}
    +
    +func TestClientTransferDefaultNoMetadata(t *testing.T) {
    +	testClientTransfer(t, testClientTransferParams{
    +		LeecherStorage:               newFileCacheClientStorageFactory(fileCacheClientStorageFactoryParams{}),
    +		LeecherStartsWithoutMetadata: true,
    +	})
    +}
    +
    +func TestClientTransferRateLimitedUpload(t *testing.T) {
    +	started := time.Now()
    +	testClientTransfer(t, testClientTransferParams{
    +		// We are uploading 13 bytes (the length of the greeting torrent). The
    +		// chunks are 2 bytes in length. Then the smallest burst we can run
    +		// with is 2. Time taken is (13-burst)/rate.
    +		SeederUploadRateLimiter: rate.NewLimiter(11, 2),
    +	})
    +	require.True(t, time.Since(started) > time.Second)
    +}
    +
    +func TestClientTransferRateLimitedDownload(t *testing.T) {
    +	testClientTransfer(t, testClientTransferParams{
    +		LeecherDownloadRateLimiter: rate.NewLimiter(512, 512),
    +		ConfigureSeeder: ConfigureClient{
    +			Config: func(cfg *torrent.ClientConfig) {
    +				// If we send too many keep alives, we consume all the leechers available download
    +				// rate. The default isn't exposed, but a minute is pretty reasonable.
    +				cfg.KeepAliveTimeout = time.Minute
    +			},
    +		},
    +	})
    +}
    +
    +func testClientTransferSmallCache(t *testing.T, setReadahead bool, readahead int64) {
    +	testClientTransfer(t, testClientTransferParams{
    +		LeecherStorage: newFileCacheClientStorageFactory(fileCacheClientStorageFactoryParams{
    +			SetCapacity: true,
    +			// Going below the piece length means it can't complete a piece so
    +			// that it can be hashed.
    +			Capacity: 5,
    +		}),
    +		LeecherStorageCapacity: 5,
    +		SetReadahead:           setReadahead,
    +		// Can't readahead too far or the cache will thrash and drop data we
    +		// thought we had.
    +		Readahead: readahead,
    +
    +		// These tests don't work well with more than 1 connection to the seeder.
    +		ConfigureLeecher: ConfigureClient{
    +			Config: func(cfg *torrent.ClientConfig) {
    +				cfg.DropDuplicatePeerIds = true
    +				// cfg.DisableIPv6 = true
    +				// cfg.DisableUTP = true
    +			},
    +		},
    +	})
    +}
    +
    +func TestClientTransferSmallCachePieceSizedReadahead(t *testing.T) {
    +	testClientTransferSmallCache(t, true, 5)
    +}
    +
    +func TestClientTransferSmallCacheLargeReadahead(t *testing.T) {
    +	testClientTransferSmallCache(t, true, 15)
    +}
    +
    +func TestClientTransferSmallCacheDefaultReadahead(t *testing.T) {
    +	testClientTransferSmallCache(t, false, -1)
    +}
    +
    +func TestFilecacheClientTransferVarious(t *testing.T) {
    +	TestLeecherStorage(t, LeecherStorageTestCase{
    +		"Filecache", newFileCacheClientStorageFactory(fileCacheClientStorageFactoryParams{}), 0,
    +	})
    +}
    +
    +// Check that after completing leeching, a leecher transitions to a seeding
    +// correctly. Connected in a chain like so: Seeder <-> Leecher <-> LeecherLeecher.
    +func testSeedAfterDownloading(t *testing.T, disableUtp bool) {
    +	greetingTempDir, mi := testutil.GreetingTestTorrent()
    +	defer os.RemoveAll(greetingTempDir)
    +
    +	cfg := torrent.TestingConfig(t)
    +	cfg.Seed = true
    +	cfg.MaxAllocPeerRequestDataPerConn = 4
    +	cfg.DataDir = greetingTempDir
    +	cfg.DisableUTP = disableUtp
    +	seeder, err := torrent.NewClient(cfg)
    +	require.NoError(t, err)
    +	defer seeder.Close()
    +	defer testutil.ExportStatusWriter(seeder, "s", t)()
    +	seederTorrent, ok, err := seeder.AddTorrentSpec(torrent.TorrentSpecFromMetaInfo(mi))
    +	require.NoError(t, err)
    +	assert.True(t, ok)
    +	seederTorrent.VerifyData()
    +
    +	cfg = torrent.TestingConfig(t)
    +	cfg.Seed = true
    +	cfg.DataDir = t.TempDir()
    +	cfg.DisableUTP = disableUtp
    +	// Make sure the leecher-leecher doesn't connect directly to the seeder. This is because I
    +	// wanted to see if having the higher chunk-sized leecher-leecher would cause the leecher to
    +	// error decoding. However it shouldn't because a client should only be receiving pieces sized
    +	// to the chunk size it expects.
    +	cfg.DisablePEX = true
    +	//cfg.Debug = true
    +	cfg.Logger = log.Default.WithContextText("leecher")
    +	leecher, err := torrent.NewClient(cfg)
    +	require.NoError(t, err)
    +	defer leecher.Close()
    +	defer testutil.ExportStatusWriter(leecher, "l", t)()
    +
    +	cfg = torrent.TestingConfig(t)
    +	cfg.DisableUTP = disableUtp
    +	cfg.Seed = false
    +	cfg.DataDir = t.TempDir()
    +	cfg.MaxAllocPeerRequestDataPerConn = 4
    +	cfg.Logger = log.Default.WithContextText("leecher-leecher")
    +	cfg.Debug = true
    +	leecherLeecher, _ := torrent.NewClient(cfg)
    +	require.NoError(t, err)
    +	defer leecherLeecher.Close()
    +	defer testutil.ExportStatusWriter(leecherLeecher, "ll", t)()
    +	leecherGreeting, ok, err := leecher.AddTorrentSpec(func() (ret *torrent.TorrentSpec) {
    +		ret = torrent.TorrentSpecFromMetaInfo(mi)
    +		ret.ChunkSize = 2
    +		return
    +	}())
    +	require.NoError(t, err)
    +	assert.True(t, ok)
    +	llg, ok, err := leecherLeecher.AddTorrentSpec(func() (ret *torrent.TorrentSpec) {
    +		ret = torrent.TorrentSpecFromMetaInfo(mi)
    +		ret.ChunkSize = 3
    +		return
    +	}())
    +	require.NoError(t, err)
    +	assert.True(t, ok)
    +	// Simultaneously DownloadAll in Leecher, and read the contents
    +	// consecutively in LeecherLeecher. This non-deterministically triggered a
    +	// case where the leecher wouldn't unchoke the LeecherLeecher.
    +	var wg sync.WaitGroup
    +	{
    +		// Prioritize a region, and ensure it's been hashed, so we want connections.
    +		r := llg.NewReader()
    +		llg.VerifyData()
    +		wg.Add(1)
    +		go func() {
    +			defer wg.Done()
    +			defer r.Close()
    +			qt.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), qt.IsNil)
    +		}()
    +	}
    +	go leecherGreeting.AddClientPeer(seeder)
    +	go leecherGreeting.AddClientPeer(leecherLeecher)
    +	wg.Add(1)
    +	go func() {
    +		defer wg.Done()
    +		leecherGreeting.DownloadAll()
    +		leecher.WaitAll()
    +	}()
    +	wg.Wait()
    +}
    +
    +func TestSeedAfterDownloadingDisableUtp(t *testing.T) {
    +	testSeedAfterDownloading(t, true)
    +}
    +
    +func TestSeedAfterDownloadingAllowUtp(t *testing.T) {
    +	testSeedAfterDownloading(t, false)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/test/unix_test.go b/deps/github.com/anacrolix/torrent/test/unix_test.go
    new file mode 100644
    index 0000000..e4ffa7e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/test/unix_test.go
    @@ -0,0 +1,42 @@
    +package test
    +
    +import (
    +	"io"
    +	"log"
    +	"net"
    +	"path/filepath"
    +	"testing"
    +
    +	"github.com/anacrolix/torrent"
    +	"github.com/anacrolix/torrent/dialer"
    +)
    +
    +func TestUnixConns(t *testing.T) {
    +	var closers []io.Closer
    +	defer func() {
    +		for _, c := range closers {
    +			c.Close()
    +		}
    +	}()
    +	configure := ConfigureClient{
    +		Config: func(cfg *torrent.ClientConfig) {
    +			cfg.DisableUTP = true
    +			cfg.DisableTCP = true
    +			cfg.Debug = true
    +		},
    +		Client: func(cl *torrent.Client) {
    +			cl.AddDialer(torrent.NetworkDialer{Network: "unix", Dialer: dialer.Default})
    +			l, err := net.Listen("unix", filepath.Join(t.TempDir(), "socket"))
    +			if err != nil {
    +				panic(err)
    +			}
    +			log.Printf("created listener %q", l)
    +			closers = append(closers, l)
    +			cl.AddListener(l)
    +		},
    +	}
    +	testClientTransfer(t, testClientTransferParams{
    +		ConfigureSeeder:  configure,
    +		ConfigureLeecher: configure,
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/test_test.go b/deps/github.com/anacrolix/torrent/test_test.go
    new file mode 100644
    index 0000000..6babc91
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/test_test.go
    @@ -0,0 +1,23 @@
    +package torrent
    +
    +// Helpers for testing
    +
    +import (
    +	"testing"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +func newTestingClient(t testing.TB) *Client {
    +	cl := new(Client)
    +	cl.init(TestingConfig(t))
    +	t.Cleanup(func() {
    +		cl.Close()
    +	})
    +	cl.initLogger()
    +	return cl
    +}
    +
    +func (cl *Client) newTorrentForTesting() *Torrent {
    +	return cl.newTorrent(metainfo.Hash{}, nil)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/testdata/The WIRED CD - Rip. Sample. Mash. Share.torrent b/deps/github.com/anacrolix/torrent/testdata/The WIRED CD - Rip. Sample. Mash. Share.torrent
    new file mode 100644
    index 0000000000000000000000000000000000000000..e5acba508544ddc46186a3e90eb4ebc0b608d2c7
    GIT binary patch
    literal 18593
    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
    TbS`OcFLZBma%FCGb1!9OZVJ-o
    
    literal 0
    HcmV?d00001
    
    diff --git a/deps/github.com/anacrolix/torrent/testdata/The-Fanimatrix-(DivX-5.1-HQ).avi.torrent b/deps/github.com/anacrolix/torrent/testdata/The-Fanimatrix-(DivX-5.1-HQ).avi.torrent
    new file mode 100644
    index 0000000000000000000000000000000000000000..06a105449fb4d82157f2dcb2053ab1c5f69480cf
    GIT binary patch
    literal 10500
    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
    G3uR?ZGdZ^a
    
    literal 0
    HcmV?d00001
    
    diff --git a/deps/github.com/anacrolix/torrent/testdata/bootstrap.dat.torrent b/deps/github.com/anacrolix/torrent/testdata/bootstrap.dat.torrent
    new file mode 100644
    index 0000000000000000000000000000000000000000..e5cdeb7ccd381902be284a23b3ce1806f1df3a16
    GIT binary patch
    literal 215716
    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
    aH+#0TX@b=Kt2a7ua%py9bY*EUWo2dQlJExr
    
    literal 0
    HcmV?d00001
    
    diff --git a/deps/github.com/anacrolix/torrent/testdata/debian-10.8.0-amd64-netinst.iso.torrent b/deps/github.com/anacrolix/torrent/testdata/debian-10.8.0-amd64-netinst.iso.torrent
    new file mode 100644
    index 0000000000000000000000000000000000000000..24e6728b4bf179f6a9d153e21fe0ab490c7600c0
    GIT binary patch
    literal 27426
    zcmce+QZQFcCXKY(%Y}>YN+qP}vjBVRS-h6+uRX
    zn3LPU*4EC&*2sj7ncLji*`Aw`(a_o1(ZI;c#F4?+#L&XPmch=^j2plO;9~qgxg6X^
    zb~ZL9w$7~V+$19ZLn0CuAu@Hevmr7vwy-fUGx@*lC1GaeHgYsEaJI0sB{DW}HnCs^
    zFtY%d0jwNsCS3n>s*{O{v6D438~guS{b%C;Df54p7#&TlO$?k&7@3(EI2o828J!$m
    z|HBMjEUbcj%z+7{V4o0kVxAOa41^>%fnRyhY
    z?EzmjS@JUZVPdzrfQ?qvxX@f=G9)9&tNy_q2UrKIU7=2#dO_+L1CiBVbaKHd8u|mY
    zKu{D-hL)Lytdfe5&3)y-%-JrJv!f<#{fF8MmiQPq?tL`PfXe)5)3Y5=^b$D9h|ht(
    z)wTn9_*zQb?`#44mY|IL-UMYt@0cmTzF{iv=kJwZ(5}0CUb=^GAbM^LR
    zV1S>mhgg``3gbqT@SVY$xm8P8vlWG&qEI`ifr2?_Ym#A8wVl{NzS)mS6_5JXe=K?#
    zAC!2v!|Ns)k=L1F1Y{ka_#j?h+z3=$6DRo>*&{KPtba!gTGgjmNMEQ||EG08kFPhD
    z&2ny(HQH2ovJiDS+OR5ioOkAvJ1YbNBM>gFk7OMiHa}*X@FoR`Z28$+Ee@ev=)(ns
    zv7D(er>#ibjN{dCP`j>WkVV;=w5)4(Pv_=)z6wnLXnGS+Fed35t(WD?kPlLlX0AGuyP`)Epj&RY%H?iD|Cs}U(cN(=vyil{{)boY#7=wEt_3{a;rTegz
    z{|W=6ATgB=Dv7XXJl98+B>g~NIoO0_YFP28@PS#^LxFiM|0!8E?%QGRECn%k|8icyYz&5>HlEQqj9G?Uu$HO6Qq^4KseARuCPAj?)CN#-``yIiZkza|
    zjT)!5iD*`9?~kL6w^WSRUGLfD!%dRS4=26Q`524RR6Bn9FR%b9rtBcu7Gtk2<(s#U
    z+<^Th$ma5KUCpy|K`gY9lGj0oj}|G5kXP0fgPMNvC0M%zWJy=umA)r?=~Nd^@J5S>
    zwmS6`VR}7>;U
    zvaWF-c__DuC|@%)h`;Yf@d6ZxJ7t_7>2t;~`K5d)NzQIr{M%jG+*=Su7)Ldeoygk*
    zB`aweJoz&o|A|g3OQd{@Q*jM-HGa#5dx<94*s6rfxu>DoQ1JduLsPb~#@brT1!y0f
    z-PvM!s0$_yWtOfR1tf--aTaGw?i-r}TELIA%2Xu(%?X_<7D)fovau(^0ZCT!`#mCo
    zNphGjy48nvPpcQ)teC3%4JWD~(5=tQMh@_I&?7UZNrFtl^Tb`W^!z+ieX@nbhn!o-
    zOR*wqn;az)^J$hI<}uYN7)iUCSFvd2v~UFgr7F*TygHzKJ8(p2
    zXTM5*pW`$w2a&g>g}ET^I1kR^`;+)%WqW7-X8+~jFq_#MP#W-smtQMF!6#P9V_@)CBW}Ag`@VNDqJEBVI7^
    zWwT6slo;uGFhd(fkb9vyMZ3ecik{7NoEuQhAvU|psX{#i0~Q4xO-<4DKwDMrsELh)
    z6fMs0nxVtw{!C!1=*ZdIf&%+yEz0{4Us4&Rnj4P4N6Jjnv9pfw2O_=k4qeNcKn6pI
    zZ0a60BW#ovp5k^|FN$#dswH;eBd?^M&hiuR!1)Y)aq#Zdb^@Hfr|z8YOw
    zlVQkx1MkGvT5GnG|9#vhdBCW3Gfn;jJ!%p91^45+ed*G@o)F>
    zdSD)`O$S+ys1Whbev^M7kj`Y^&>yRI_lY#bf`H%Y0w0egfW!ohRCS7R8!Enx5h62&
    z*LEFnEM)t2<2TS7Z{{aCFsEn_u6(f<-;MTwv$Ma>41zaaFpoROa!U>~3w1Mj8w3hW
    z=)=O{BG-y0Tz(hZBHzPq%FChbEDfAi2NR4S)-`OyD6Zw98m@G6MSYDH2>7ytHnBWv
    z)F({HF#oRAejFBqxc(htCY6f4U%mpC4h#_b=5o%ONq~NWOJFJ2jV0C?OHk8t#tc5z
    zSRu>vcWfal*aHyYrLgy-1t|>9gy(QJ$?A@gJAi8X>c`pwi#Lt@j}elr3`$=ZVu?lW
    z!drCLrhd|F1@u)>PW{?vg(%_zFhpdg`54f
    zDCR%fns>4EUcqMOx<4`A%2apZNYv&(jmh=fkE6^Qd8a%oEaxEBZ0GW4=h5hn8R`c7A
    zF~idEM{-FNL?LCdgoH_Npdl)ie#0W11keg5A|@eT|Mr}kWvZU-&62i?BhTRM9q*=`
    zCnPKKp5#0Up4o~?PMpP|f#Q!`?!TV2RS6t0m`BEP3SJEX8wkz)b6#+Zuo&>U`gUKj
    zW8R?idP_pM&kHVPwkq~1CX$^C%1v537CcvxrUdg_tR0HJoUKt&X6#IhW)pSSZpspW
    zS<#{pYY)QGP%t{($w6hp1BNu4G2roR&Dd9v-sV{cAnQUWhqRATH4#tx-1{&kM|8@i
    zXUCv4beB_Bjt&z1B^xO|gQ5!0854*J9Eb!(cxu<2KHcBb_H^eXrTM0L+fLye{u7Axvv4!in%llrvBqV6IgO#$
    zQ;&)Hn+PM|z5B1J-JAe6nd+AS-<*mAoDg2WTP0LkE3D@Z5452B!mmUOuuy$y-w8QG
    zJZExeCThK7?d~xU5RUs
    z9vUY+5uC5C?uSf)SYutShMkf5v?Ap2ZYw+{>G9)h2GWxJ>4tOIPP;DI-XRc
    z@wWR1s8dUdNpd4F@(ms{
    z;{tMPO+>E$3EP4Z;rt5HZvTm(DdS83zy?zrNCN|
    z+`&EEJSgA!XX{8ZSL)Mgetd;hN&&%pHv`}8pCYv-=AHWWb1sETDI--qf$~zM^xXOR
    zLy3htEX450%~{B}r1@^)OP?7S;zMPUAaMI4WXqwM?OIogt2&F%;4Y1%?Z3Dtg_A=Y
    ztY7E)VgJN#!1Qwv^c6zEuEy+uM0R|k=`|NGCA0JkW-;b`E&H_ilRwo6bF#nfwU_)+
    zv<*n9&tIrfNkxArbv$TBctZ-+nTMm>l3i8U48<9<%x?}Pu|%(m=ju`N$C-Q+)?2|EexiA
    zqBwey{e>gBoim@^coiL((#XE}#od_Nm71vO^{yuFmRh@fbm;vO^`c1{MN6Zfx&Hj^!Y$@4DT<)aaVKT)@nNFM35dk&(OPiYjmA
    z)NG7ERtKRyd1GkyvTN`@Q#cycvmYw8jJYn)3;z-|KaLwJtosgY24U~iS&*?v4RQw*
    z0YQUgJ2CrZX=w&T#HD(0*|hfTSLM)rFvFX)Di5^?ElLmfN+Dd>V##%vG`xKMa)W(x
    z;q$IDYMx`jtu2wQjt9eQ+Fo*kv0mDblpLlw+`@4qMKuF$)I~M{#?Lfe
    zvv+IeX&d9lL(pt&&iBcj=D4hmdHAaDETz1gF$bH;!1?x-bb>sc%yAU!`~)LOc?7{K
    zO8f|6(|D8^e1J}Jj5ISJ0a~nx(L-4Ar;@%$nkb=7o(tiW#=%u_jP@L)YYOhj`|#At
    zW#@gn8l}m;(MQ!O$rVr${>PAU!jhHQGQ~$|$;tgm_b~0f9jde<*mI#|%KRX;thFJ`
    z#cIq}e>`4kva3j$m2eEx`L+EMilLDihHSUNcUVE8-o|znx2)D9R9PYKOVtz&A1b(j
    zO>`mr2J}aIp1e5E)3zYVn&exB8rnKy>|^E>&l=7XGtln&?k@_;asgfip%AP&Kz4wLtXM)%nAU(=WZy}D()?jLg&ldt=V`V!RouCLOIon0a-Z)A4IidR;8`8WFYN5wR2B!r(h$CbU#Q#
    zDZ@sbovY=N6J>p^awXv@lT)FO5~mb
    za2y-pRt$B^J!Q_B)X)Y)p6Q&;O84z>iFke#b%@s^t29E7@}rC~ifNe|OM=UTPKbEn
    zq;j@ae)xQ-{yJVZn-Rt=Sw`%E<9WeU#M!6o)QU(xUDmPE(vbk0PQ=RD;>&2WUE*n%
    zqyHsnAl9s_Rk3bi6RTKk+IheOgMdhLxQaXYgsB0Pd?#Jgj32t=m#L-?WY~8HLs&UE
    z6L7Op$7wPEBm_Pd|NP3AUB>YDvw+)(gNIA2m5!iX&#(d*a-R!+OUEN6o=>q0$qGex%sP>(4v7U%S>u0yzcXRNnKjeD3HnhYacJwP9!^yPGt|07BQ#PK49Kc
    z-5~WAXFUg;5QM6^tzb*ZG&)s-!TX)ZYL5sjr&rp+tBNW#$a^UobI&{3J9RIB5*S>I
    zr!{QNF1sUfp)*z|oEzwSX3fmu{>b*ZZV=ak~H@nVaOcFJUY6k&?
    zdQQot>dAVti)8O3$*^dJKIa8>w=a`b%Qs(7=H90s+l7S6^&b}N=y<>BT{R>cRSMIC
    zKRr+it5othin=}>x~`?pgzR2p?j0Rlz~AUEqjU#BjoF|bWu20?J(p(P3B+_3q{=HN
    zT)bmZ3=RVf&08r{y9T~R_r&|=e{|p7@~AWASkf1R-vPJ>|Mr!}n(EXr{V5RHbP5O0
    zz58Xc=r|$zvh;&g4nN8pyv*y$z=R9l#Cr3H7v^5uEFvc?TZLWU(6O}W+bFj13Uj5g
    z!@}bA`GH_qRmF@5y+1>;FNX8mE2l7kGo{8}4ww~N?AgVrp(z#^(^6jg6@wSeqn5UI
    zhsEv-7{N!*&COIO(*z7x{GOh}l|c4-Wf;1v<3~;2JDcxHeM&S8fXSpYN#AnnYHH8~
    zL&0!HCbOQZvMyft{jbOM4FfTh1$K95iWagn|2b$q#-M=dRrHm!kK9R8q%unOLLhyL
    z2xRGKfb=0X#HCVV*O-0g?*(QrVV;>hZdIOMCLb%*}Z{G`PF0eXyFs9W?Ly$~j1tU(Q
    zU3$z_{xl4OJpAzz3NDn&TS*+VW^HmW>%P|t;4>AN+uLvE)!xXqDc3}47{gcA<=lyE2b@L}NH2GO$n
    zE5zF77?|I27t1xJ%;B8g5#l{Hy>K;kF?bt|1^M>V4V?1Lasq3^3ZGZ4IA+<^FpKmo
    zoIae}59CYqSMMNcS%ok32?>d>&sXUDbY%i!Qs$|@w%>V4)tM8vh#b2~y
    zJ$C1=(5^xnNBHN>3|-|naP72^5h0`Q8D(Y9D(6OA`10qH)JGL3ZbEo_oNzJKLM%1^
    z`>m!h*E;{O$LRs`yYqxZ4g@|_)Vwqe{NwnoyG
    zOGAfQD6W-=1fW|U(Gt78={D$H|loK0@XW=?d7NM=}5upHX%VHhwlj6}P$J*xsvn2O~);*Wq*MN*F@#X^iQk%Z$wn;E;C0h?OklMZ+Ej6=sT>xq6Y~#eZ
    zrhMjZek@)eW7?njFYGw2>_m4%;Fm|GzHt$>oI%GiFk
    zqfba;u?+6#D2T|7eUC@~0?`PpE2;Dy{OYcxGDK4^v707J{u?4p>H;+BXhgm`@+0;O
    zL!KOvS5D@vSKT?^$e4Af$*zxZ-%KQ3L?(wA`L61A+3PEEIs0YCyt$(vqr!3Pm_K@c
    zXr?LEonzN@zHl
    zStW|ke^YJkO_Q4+XgfxBOD$PBWXlC}S{r|6W+fS)w`+Vq6gv(I9-%?0WHCFH_?}>0
    z1EULaq+ygvj4us`yz91`#KEBY??b@TeCLsL#3gE8&^^>Lcn|;Mk5KDfscB300KLKz|#+tNf4rBV7r-xN@
    zYA{b%a3<;#&qNCRn4U~?uO@1SGpLZ-d5L)Pf;>CIYdLE_y|J#Iyd0VI>UFDV&Pte2
    z$(Ae-<=&7j_H0Fv;*Y?1w-b_b$;d(UMty++?bWYy6B1ukRkT%KxYO;jKI1W`4MovM
    zcm!4cKVkUWV_jPK=9q*GVSt%7%zoNa^TVkOAt!6qNG8PjLB!?-rK*)DT}sMRejHZB
    z1R1ou>d#)oF>TU^g}MRDJ@Ab(QExuOOp8dsCJbgU-*oQ7r8*&9Uvoqs1)3D(2xZnCBi3X!g3?B0I3nE?BI2~AqH9!UQTmI1
    z%Rmt_1jWBRg0@b|86|rQv}Q71Gz<{~xBTN#c^=C8=6-RD1NO7BY3b#wVG|QIkJAV=
    z3%%7A`aST_eciH=fPZgBV2O8nPJ1!x_Vxcn*gqIo-7)
    z5VLz#kP1zSWYmp8kS-rKQ%i$CGXIVscy%-QNTPGGoV$#z2JO`L4GzB(BM4E(aD%6m
    zbV*NoRsu?HCI9@*$(I2GHH2N7FZDD}Q5eBc5oe9h3~fldk?H%S-|py6o2b8Y;w+Be
    z`C#O89{e>+eaQoVz=W5po$m}T_(wAXUHz9pCA@vGnIeb{BDFoucIQQw?=)O?C2@K)
    zz$KGNmxZYRQdmOLvPB7X1cbm(hE*#mBJmu`&Rx9Xz92TikG5%_&Bvkig0_4Xc;TmfjW)J}(5J{pE}+
    z)i&l|G~`$z`{Y;P6E2>?YlH!Z4O*5j<*RRygwd0$5VRs<;DF&KJkPctb(}eX4T{}K
    zV3?JS;nE>Dc{g`7sTWtjOW$&{R}U=JRvu7J@pUfuF)FNq#a+-(wE`yUp5OEnbYBdF
    z3n7LS$_FRT&9XT_p?u7ct}Z+|xNSu1>ip=>%$euNLanm$v`Ptno|>utmXNK9WPD#A
    z*Tlz`FyOXia@}&E2Hs~bzXa>o$WtJ?zNDp^w4!rp$^6Oa9Wv#
    zXyx;dgfg|Q&ZO0Mw`z?ljPli#hy&ZkIe*2+JAb
    z7(<3qF6(rFda^HFF}uVj@Fwk1Os16kE!>yu3Oda*C!KwX_axu@Hv1}eBoPF4Vh97C
    zSCNak@7ihF^aI-ApXElrsh68g85@|vntpqee;P1M9%X=3KINHnU&k&ERnOSXXm8+6
    zfa4aWLG`8^U)+8XTb#OHEd@gof$zlm5E@n=;%3G~UJFx`w5b;tXf>E1)nOP!F#sSx
    z{L0>o;=j7g|8DYS5CBh$lV?1%y)iF5lJPJ$2kDgRB5=-f3O@kkm|H-e%=->!`?h|-
    zOd=-*HIM$vwUlH#&jyatw?+IL%0I8>F~hS)7y@OtJrl^nX%8j26|z;P041n}9MOp(#>Q4g
    zTj%3>irv6sts%^JED)Q0z{j*zO31V25hSr%_sA6XcM=R11F{gpqOJP4ENsiTb|s4@
    z#=3_h3tw)rs2CEVT?WF2tZ4_k3uJtsw76~NYWMYv3m7F?QK)QI8=0zM^tyI)C@umr
    zt5UZ})0%#L@wdnt&QL|~RPonDVm0=g9>L=<@!zRrW)dW)WeV`87@Ic^WX^Dk+rr!}
    znvghJE&+S-*kIyQi0zjJpgbT(RFCnzo@7g(R5w`A1f}cu=m7M%x9DkvQ5x?BFPx14
    zTGPBCc&MKF)*Ca!V%fQ7fl@P{y^~Mr%zTK+@yu0B!h{5sxk~WIbFs_4iz$kHEJ?jO
    zd-ZLX+m5TN0sObD-LHg{q0Gvkdt9W9+|79jRZIK&8@>k~WSz&^jdozKssFnbxS6(-
    zRS_eurfcbMX0$$2LW_51ZgsPf?-62{m43|1U|nl5j>d8sgrdBgfN(2TfEtuc&aA_G
    zj_#lSLQxhY0L)d1wwTg|6eL>hO!F{}OyhoxEK6%7s@Q
    zSB|Mh(rHoB5yzbE5hGS*tp8_2G~EXM^xA{db_hzd(A^3sgRZXvOldvA!W@+A7`;@A
    ztDOtht$OA#^>cVF0v<3w@w`60ruVUcqMeI(Wb>{BV;7-HW6
    z+JBD}`8ODhIXr8AO1yQFmDd2_o|n5rnP~)^8oMO91q7A2r$$3v(lZ$R4`(g=EauGKB>UQnsny(&
    zySu7f)N|#Np?bk7Aq+(s=;dXt%n2cKAMddk<<77Wl63_<>tf3TXF%^CVL(&utEH%i
    zDWX-Mmjdbw57AS?Y|=R=aujN-XY46Xl#ykYZ;LE>Q7Jc!z{8;*`nrCYO3Vye>xg06
    zXn*uGQh3gU;#Lyfz%h07U>8xWCJOqC1OzSn030u>QI>!Ngzhk{#nt4bfaHd`*1eQj
    zVo@J!&H}0}mGtRBv1Uox6z`tWcTah!ZkMIloc4UP
    zInLV8h{_74oCy41`m<>wPK>F45^F;Q#^eyiW)!2L<|x>~tj<;n<;qZ$J~o>U&$|Sj
    zy|CG=WIMt7$_5QLg`ac2Cl~=!o4~8Smf;+vvfJ!Z5stkw4ByTf-BY_4x88dUP<>gm(QbM?^yH
    z2NbwjkQni*IMH)VOzcJ-??f)v!dlEjM`TT
    zy{5GxV>zz9Fef1WN?OHbW|+v`vKc}<6kmD|SD}HQ_A4?RFzsO>x}Ee_tOTIC{rpY`
    z2hqa|52k|4ArE#c8U71bQp())yciPQbT3=6Qet(pQK(xt%(H%P^qAuVPDJnF_PrZi
    zeqEFI%smgp?l>O8#G9zzl5M;4-|g~vO?7Soxp3*W<8SKEuT}=rDe*9PS=xscnIHHL
    z+fovbO2o&S$|)=VaGbvFJ{KvNz-s33Eml*s8`{vCm7ui8i$y1h3hdu=jz!^LxNp0V
    zt)x>Jn_uY0kyJ`b?x&1bm;i5fBt0Ua?O(ymj@<$om4fA{9O6^+&bIn%X}k6yRC`N`
    z=S6*i7I{y=Mja1Ju6YdcR}yVKF3X_lNHqc;xVY)rgQ0vK`}Ff+F7ok|uU=$%AFKMi
    z&{)WDv+nIxx~^kTGPS$bZ-4U@flldCL&T#mr}8Ji#TUtfNE95D3n+x0M
    zf&#ZsX|jD!fjld{Xzi)Kd*=!{%%QC`uqk%IDo5Mxli6-0i+f#(A2b?8TJ99xiAaf|
    z8tZKdO&PydVoJ_2=+*2(km(sp1isVHzfx>V=K=JV3F0n4!I&dg$C)IFcfrV5JFc1g
    z86@P8XOn^7(-QGw6MySB4Af_RV=u)c4enePBV4T1XRs&zH8ozh@7*w)Ua{$fUuqW0
    z>hf9+ZvXUXhBjZSu((n_jpHw!XYaQ>o8x_5`r&QnZA?T&CxCj3Wm}eZrMg%c1vnXr
    z;H1%&Ds3Q+&#Eo3HFGMP~;+H1yT|-m#bh1zpG8l%+WN
    zttq1%uRM*gs|bLg($h5^FAoXJH(Ea{C-_3}apSte?}J+6ocrzAysHF8Dy
    znB&?O<&K~8{u^z`m>^mtY4I?R*&lG$3n!PTYGx9#;DwN_>D?o?Igk(7zFhYDb?h*{
    z;s#W~XV1+jYcV2o!NwXiOxNh$$tmE1tHwC-35^w)Gm_gacS-%EStKOeOpohAbOvJN<6YQ#HyDiyG=9-JtzYD$^GqpuS
    zoSmtDvchtWy2C$hCnS3WmOYP0zrsp+gZ_^mCwC}Q&XWGTZIG=77%(1!wnwLjMs_Qs
    znM~9xbl^x59l;h~;sa@y*_i}i9$a%~loR}Q1!9oNmQ>cGpA55kM}#aGq}yLk3Uo@N
    z@1J}XJ^|SG(Ly1VX72hQq4A?ftl)EsP)6z@$JZMJE~GGL608MUX`MjlwEUDGOGR#=
    z**|~ps*t_L%G6h*_RX90=xgnGvQK8##sn#Q&Bs|EM!&f1-e^n~8(g8#CjJiKE~M^r
    zaiwxzoj;D(0_8$?fvP~M8MO@TVEWD^;W7rcICn__hqQ;!kSn+>5G~#QJVzZ^%(dF@
    zL%eBSbgNF6`8c>7-IDo~wEc8V^vce1BaFJlqJVvb_MFclm$ufH)z;*majdG8lAG12
    zA0**#OG!YtPAZuH!7{oo{C>ml5{11>5KTbjKcd0`uh9%yqr)LFQtf%K(dz$$NqRy~
    z0`t#WsPXA)wNg-H%w;_+aboTd>hgQg+L9P0;Ibswo_1f=Uoh2BQv;t;)Fpv#u6TIa%CWYQF>OnK~q(t*Ke9&tcxj@Q$!d!ge@H96YIT(
    zB}b02{?nCqQj3A@QkNiA5x
    zD#3pORmorCrKp6fnxvoQoy*som9XIVwft9i(5M&D|BZ}gM%}GM_{87(U8ltC9KzKy
    z1lJ!}WAFrz%>5C=54lPOC?Bn4rKNXaNl?$fk@+C6F?IU&DA-y0>h@JR-=bc|VZ&N?
    zJ=f@EEO|<6UC(@Wr1v!|fVh|}ox0b?d$HHAMjKeUwnS6+!cJhoJ3x;SF@1oY2u|!O
    zTh#8YBuB4Oh0QE~Kcm#$pJPN$IH(+nWLf5cJJFr9W~h#PZfE~Io1%tj-fACya{2Op
    zi`$$LW==}UTT#3?_)5oqp3!ga0G7)Fhg*5+Ij-Q
    z8BLjhd#=E&nGJF>ZDMuyVibGu#4d;avNMhhmx5e-mL|1Gq{225!r^vl^Uil(kg(x%
    z^JZyud&kHe{%WUDfc$q3`jjFyLL4@*Q=zJB!c-VmYc-a?FW`W29VM*!q3hZ|fQxl>
    zr2)EYUvCkUlGd!vBoTQoVQ7enWjdgJVh6&iY91uElqK(__@P^lhV^3TRAL5=j1_XN
    zO<_>J(#r2M;%b>X!o^p9=n66FRca^A_dxZ%R6
    zK5_zqbNB!eDmz^QZA!)!-lQ+C4{j8OQ>3kO9ie@@uwRV;-5fA(A>N;JOou<-B%fw|
    z=DjOiTX$;ClI?(Hvz0nv%7J(Z0c+;nZ;jk}4F$li*>y2%1>|U%jl+GCXhc7v7dO
    z3&*R(c(gp
    zAm;Zf06A0b&3JmVCOV`r#_Va#LQ$h-9KsfTS5q1{S7G$1y%5)^36jk55$r`cM|Pbs
    zjzsT?T3Qp~P#t_bow=T#wTE%cDIpQ~2XSpWYudA~M*FMZ=KQ078d~6t<1Th7O4WYL
    zOqh^U>%pv@IJp%!|F~O3)u>uuJ?xSCBU}PNfWPl^YBdcuj6B}Tk!KZU_wrnz#!xx1
    z02lsMHNjb84ZPe^ID
    zr{0YUY^3MhdJmKu3lGQf4B*q&9t_daG%{ok;~YhnK0B93rO_l4pD*q@2kVC8NM-j#
    zS?WV>=%Yx8O06hv_<&XOU1IGnrX}_IV|6?t<3Vzo-R@~B*zAjPfxEYK+M3qEl_RQu
    zV$;bElymC+CMkGqqlSAlNvVF3YvNUMi!MS$xpy&8E=VjVu*&_neCpD5tepZ9h75iz
    zs#8tUPH;FdFnc`AqQ&@plhk7AbEwabHu#KBZ7#jr9}`*~k9KEE
    zw4({NIqhb?Ry`KFoFUJ3D%g!TnllRgIwkoUrURa|t`ctqTapxo@A9f&R!>zfRE`fL
    zJ1B?van8DG4v6w5IY1_5JZQfMq1a*q<)xMK(iJYv1%pG9fJ03rH;z29b%wfVa#Y`3
    zf?Dq_U^meWy9e6swj-a2Zl0Cw!ifZTUWflwpADn0d8cBa39;Zd51ZY3M_AwxzPQW{
    zW^T(QWcv5Xx>N||mS-b(Y3}ED;3s(H@pHJnK&~l-!f!O
    zVA_s1elp?~R`nKa*IT@c6x^(d)kum$j`yXGw1|cc`_9IPp|5o5ZyXSfq&je2X@}MR
    zi7b_{nApO?S|S!9%HD+erfPzCd>$wG9(%&;rF1wS(ga)*Fi5qvR<<$a@@kYccoB<2
    zSj`J9z7T2WBS$Q_DBfSvrt`&3(|yC1o?t}b!h%|jt<-N7a&>gftKuQTWU+l>~
    zVoqChZdFWMsp@CTo4Nq=*usaR&#Ax+HQKIK8_**Z>lDTihxR@)#4|ir4zw|>+28AI
    z6abh5>wBhgiXewH#A64HCJA&S*fwrq4rB>LMP5G;!Q55q>)plexNg6%oDRFz3M<$h
    z|Au&tNEp*@yraU;Kz$t`I3ka)nh1dt(5l`|&@}*poJ(8kMRWgX5i#=Z!8a}O-`IUv
    zEv9kxFP+KA5h9#ms~P7l+X=WxNP-VK7nOypVakT5F-&3;kafy6tCivrge>_hzCAQn
    z1$L)x>y5Y0qLYa~!KqUx<<8}{_VPH}3FIXyK`&OH7e1ykuTVunAmGfVF&>>{$RDc=&6&0=5Hi}&c^Ks4GWDA*5zINKO&azX2=LJVb;e5FT=54CmADElaf?_NOgpFvWf0uzvWeTFZQ_mNP-*xd(V$Sk5S#_ej0ufR-W=0xtY{y`p5!-v{Vu}!0(4I&j8_0dyNPH-0c-H-z+S6F7RtQJSe
    z6G=C^G;a%mWgY*hS5;LF?Zj)5w##fEiL(^as~#l%)$SsN{GdXM4|OsE{u=Z0#LSpS
    z&WJ~IKgr{9X5>5t1XTY*<%=;<9%;f$A5Ej(z|EzJc{0EQFO&i?T^$-x|=UQ
    zKzb6g??fvC=)!os6WL?3?CquqTGmvAa`DfI=&%hKYT6P%A|MBIuZ3u%
    zg8ME?C#*0@L9Q&;K(~`Yaq7)wqr-Pp%n2$=*><9PTsK#|lGIM-0bSYV**d6I$qF#o
    z(*ICX;SlOS11{sOM9bkejCIL!!c((QB0l+3z4+4cLK(K^mpps~z4MG{wi|QGl*t!z
    zPsrZ!TV~oIr&HV<@_*vG`p
    zP(EU8kDW6Apxn57NOQfJ)a$yrC5sF{z|8sRD*B&;lVrzwZ{4$;S^-T97cVhF?5PgdDd6p~}^uVR4Vt$|or8LE?+P-~jT*_;_(z_0tX6AAa7y
    z&wxiRMx2=Ho6UuAfSb(;5z5$e=2mUoVmHDZr
    zHb4H5lwOv6hD{aj>}}zFi#kq#0Tqnvi6q3}v5Ckj?yoi+LH5~MA2V=HNd0L59^seD
    zVn1ScSrp0;*}8_*Xd;xnfVD;zK%Aa+qML}Q^+MD^b03(^4Tq0UQaKYSoa&7BwBNh*
    z9o&n`cJI%gR7HP^vh!si4)3^C%FTU{9wFfL0^R|E1+@;d6Ylg$H*e^7z~on;LES?+
    z1Q%%B)Rfbh+vw2YWy=I)Cpm0^xFbVj`v&>%P=^3PeyS$^i@krU)$fb$;>(8X`(n2{
    z3$)DW96TeToXh1^XZC>CIeslfzS3#cynTjx*ggz|NF(6cJaz4SsQ5vB
    zQkRQACX(xZom`1|`9f^epUzt@x#7sYZ~(zf3}LNSeaBQTS2%$UxCBHB!TiwpFy#J&
    zE@-oH0t;;9GDdS0aOPMk4J_t2=|5pt^u6Ubp=L<+v19Zhr-CpD?Dx;{$^#&Wgf;|P
    z(R@BE@H>0q^Y9tVhqc*hguv#Lvxxb4p0ySx#sFQfWGVzhTU`vpm>MDmd#1
    z>4@0bW~ha)Z>deeGGqunX+eQ{0)nN<7uyfF7yBz0_@4WajG$H_j%pI&fISAQ_|PmH
    zWkIkqHkn-1GDLUh2AO18{Vp92``;dr+aaP@<*3QUUV|r8ZicD|oKw&dE{T4{TL#Pt
    zH(VM!M+(qpAfM3pOA#_5;YcUFoRIxhI6n%gfuxiLmHmN0q7`{xdTt*T=&?`Lple|R
    z%(F{pL6rrvTrP)cNzl=;ZRH8RaVGOEz99zT$v{m<-x&>R>D+81`-7pG&j76ZT4|s9
    zOvb85_XgrQ4bJRY45#Np_6nVLPm8dG!@i-Z0*JBb47%1efiTh&*j|7Nl54*q1K|q_
    zYG`d%H%%VNncEj!3?g$Cjf4EQ*EPxV0!+5Nr1Pn1YK={@u$FRLEb9iJ?ZRV>?&{ef
    z8~-d=kS*M~U`ocT9z7oxjQ~RKnA24K{$~EQeERRvspPyyH2dw2Mk9Ha)dNb%zA9X@
    zspbxaR#;Y*t%U}w1%~EK6fR$cz3MTkDER`+E&wfd#80S-5ST0WmR}5aazaai
    zf0*pKzDhCgC87O$a*w_PQKbHWGC5}7o%D>c4Q6P$1$g?j9KbCpK0M>zpNB(I{(==G
    zwrO7c>1^;$fag27h<*n0%CY(Q0|)o=lNSI$j(K-Y44`Hg4|cQY<2xS}*yl+Nn%Tcf
    z7<^UIjc)Tav)T-kPVY|We##L3YJX=>%8{g2+tGBI>0LxAcSDW>J2=t?DP7IXU2SP5
    z&Kb8@-C07nd)NX;N`X6{%hfObrn_3JP@lj&vboPsK8{gjAPJp8=a{jlk{0oo>$FHH9fV=1zh
    zcSOBOhzu3CR+jm;rw5GrN(jFzycYbFeC-TYC*n52isZ(^@WxvX{eRuJZ;v&>#Bd7i!I8$_c2+m2`A*1tt6g&lwhgBfZia|V
    zsxi2@H2&}hzjOO(4^-FeD$r8VW{~xZooMThdvLBaGrp&J`YTs{INeqoaS$rasr(aV78(xUZjTNImi+NrEZNQ7
    zxT24=mb^$r(*yktj{miFJU2Ywa1I98^51&S>Q-
    z$2v6OGh2Ok>nNJA5{jIy+jhW12(+0J?$iN-!xDWmGe}d;n)de0CBad
    zQ({AN=idSL*(dUv&LDLQ&F(qB<-o;(xNjUb^OzyDVMSnQTN4
    zW%u^)k3PVLby$}V7j*YoEg+DJ^KXFF17ZxU-zD^NMA~P8H4Y$zl>#{*cBw+X3h34p
    z1Q8_qOaK0|Kd5_JUZs@&)H<^=Qnw@GkpPXeIaS|cUA*>qa$>BXDV36#r(1!t+x?G<
    zG9h~Q&YE!!YU_{_mvC>ss4z96dC6MwqX-|CrQ)RJ*Ru2_sjxsLn&VeCd0MKSYm9O}
    zh>Dz>U6kf~L%QVgr8+YtHgXMyu*`iX@W^^@ldEQ1k~Tad%u9Wdx+R;nD`*4RF(~vt
    zsrF~`5xJtb9B;L=Hwb@>&eh`$FdeC}`v7)J?v4z!!8~QF680ur*7x@$uuGY#JZi_P
    zYe8T~dVWCtIcE&xPGYe(m0MCOQDar`$AtQO5KM&Fx8M
    zG!v^y0(&ya;fI!V(#2YwTXcowVKT^5fT>JEjH~~!^
    zGqvD2$ZcO*59Ne1{4Zkwh@7-en*+k~
    zzEk}SZ@YX64|i&6wYi9$plhOmeA7h&zTY4%Rz|YvlF6n20%7Ak-)#0&O)@Ln!I>Dv-&t#TLP#{Glz&15j!VNLPYsrF?{XovV7)Mcy;arA*}Z-FJbl;8J9Bzf|slVG*6mjTWsENy-agKgxD$
    zZbsri1`;JJ{48onis%cF7G$=ikDSP6dvj5JxQ2mvtHy;;7yNW)Z;}J7uin)ePI6%P
    zY1zyKZf47;eI_X*X(sZOo@B8FY3IDn!Ls}YhWO3$|-G3iFYwvdQU<
    z5Fqr2Y0uJG?^*Y^CJ_3vlQC@FwGn6?qW1zZDEu+pA)
    z3?lWY(UfUPkH<`4V&T_7`q4I1=&TD+2A#!Go<$#*thbKs$~IR4Dp8wi<$kqk|F8Cv
    z?e(fK-|axenX?meR9;`YfCC+mp5#xi3$|r{UONW;51$GfhVG&QPPTdCd-sZt@>sb*
    zF-HHSjFVxkZ@dfWMfZWQbaS7_HjWqUE@*_BIV7i~SM@B6^AwCU#lIuhbcY1@Fd3cT
    z+#tiGUOBGr-FVy56})ES79&l2RLu|Cwfxa=Vp~S?fQ5J*Skt+wW3|%e<3N!TfGXGM
    z<<@B|T`vJ_$B2*!-_FQH0o{SVRg?ZPp|k+hq=`zVr0ve|sIwC!YQZ-ovc^%C!f@Z#GE7*Ynu`9J|h
    zi-Q;`$cY|ltF|k(l6Wy!AJb>aR-~X()_XSW<3%}3ttO59Fm=%lOcrP2wlyW<>q80Ra$cVk|s%np2d<neyX`l%VCP_
    zo(0yjP{7Wido=%HF9dIB5?S3~U5``pik>hI)rfhP#lU!{f);-4>IoiU&lOCqS?2MN
    z>qySD=kspl<$!FWtj0!>n2Q)g+l=G)mua;}aUj_R&@W>59DaVdcH=B^GLBKNa?@Cb
    zE|~Xjk!l%P9nB%&b(D2gF?25!MWT)|womNbk3c6s*P{fKqLDC{p+NZ6b1086>WLZT
    zCl*4Wb>ON*5_#v7INl3v`*(lg9bg?n;7B!my*cQj8ymCa8EEM9JxS!P{iGA-?tp%x
    z^hyCD-4BIoCc1x-qw)`c1GLCsIn0i*N_^)B%TppQt;u_eYb}WZ-q@~B(rsh
    zzPKfW??0D;)hbj^f^;-^Q0}p`f;8|1L{`|Ot?1#I87VE)?j_h?jGjn#I$n^c68qPC
    ziykkt-HA*Mq=^6@PG!||K|Ks8^Kw$mcA;AsWuDkc_JN@S>bCg_dM}A8q}s~0H82wg
    zIUhxuB&ft!2pyMAEF3Mr@CxxOq@<{mw=QLeWA*n
    zD~Q^jY^h^li&0OHaW{H2zjODKTq3;A<{>8-0%5dK3v|i*PRp#v{plv@wau`A1!N!_
    z&Ph^E7=5r^q|_#1n&IPi2f)#k)Y^}n13z+Rm^TlM{R5r4N2#b%*THQLd17QiZfqgCoI<5_j
    z#a?tfD4g9u=0d`y!HdB&b1O~OwG(u5G-h@UYc>jyoxj66eK`(bLQ>1#SD?26Q4SX^
    z0--3%BKrLZ0H_87r9cvqR(%AbPXmjmMo}zU3s?2{anyjr8#2%|Yo7-&dHC=9&k@YX
    zl=&Ulz+_%0Msj+w%*ghYM9x^ut5u=9LYJu`(yF)b2G+Sc)R%XjzS&ks-oWib1Ci~W
    zDo$N7(;e3~nQ$c!+Bp$e0RT;jYk=V$m{Lv=QfJoslIgU|Se@a0J`nPNVPj+@JI}u*
    zqnd_yM?Y`MG7CJYvxp9EKp_ta^pCHnIZY|(r^;9&>s`i;Z!_a9TZ==ArYjnTuSDkd
    zCk0q(MuI)^6A-jwRR%9sN6^=PART7u!_rO>q#b<}mmQ&-l6=XY#GM$UEG=y-kuLuS
    z4e8Atm_13mJO9_f<~}veNTC{!RH0yQ)rfW9eUVb8D2nSQYuTIEB2@!GXJ8)GaT&lR
    zGn`_{4o&C7t#BVk0Yp04x)sSM8!YJ&>orrECpxwbeGJ2laP43OW%VV}S4_)Qb!UQp
    zZN&Cdilz-mNs5eI8fUm!u$aB5F|Mqg9D#Tv#HS20h?G$UFH
    z0HA>D0^o5;=u5)7ZrV}4m?)PS%x3bVO&};Wv-uOvZMB7CUv1t>7>dqM7B~*662k{B1xNAKwKl*8qsohcOlW6)8hfG
    zKWL1PxG=x*@wDm`8P|1{)*>)v~2Apy>P*qf`3U~G4M
    zmJm<>Y`7CM%wXdbX_8j~ol;p!T~|FwFu+0hU?5oE=utYG8aLgQ6JYLzUxDNwpLBZ!
    z6x5x+E-QqUKlo#=3&7~CT@6{F=*P-rvmw37Xnum5S$)2~XJt=RTTCc`XYs@7&55J}
    zG#;*|hbzPK+ltkhr#bK^i~Smd5+KX^5_o~!aJpCUthjjZS_z-#{8Pqj$pg21BpFe9
    z6p7RCW!%GUKmdi#kt2cEtFMWcn~Mdgu%424QDjz@v|#DP7LUNmSig1jv8UDaUWVZy
    z&&Dwz*ZVgp{{Ot;i|i07xUrT6!P;|`H+~&!UHFP(-XYjTtAa{F1;pYp($)LJ4Otr9
    z95uO+77uGe##CHUt#X9U@YJedj$>NCJk!-dm0O?>y4*(i(6^DyXdUFrVMLLgxZZ&a
    z6M^orexu@QZh->1$agbxyP-0a4S4ir6=gScq=W81%E9a`xV~DDOY3f_HRj>!3mk}Q
    zZ?*jQK|-?b@=tj{UR@%+tAEQl4leVs4zSF#OX>B5DzmYO?|4(Cx&WX2>{fan6%iBz
    z=)Yq;<9iBuCg%4zT+Z_V>x}eL>J>=w(MreCH?lPkh68gF{D_B=J3on3}
    z7r9kkvQUJ!eN&7JK0K!ZMGu`6o}}I=+>jD$Uc7?71=K!OBO%lTB)<(!bpxn#y%wQ(
    z%C}%>7g5Ykr#{?zT1sc64z7qV*tl>Hu)YtltFxdwe%wg^m7wK!7RvS%$!K%xp-Jh9
    zba>nryzrgsPgH6AaUBW*{3}K;qJRJj#YLn(;Z0HUt4%{l?GA?xDVO1-um0G_?B8cL
    zSJuQbf{x?WMI5`Z|6Eqeb2!qI2+ZE06Qzhmd4Q@76cla&@w@9qb09Ls%Ih|M!weSW
    zC9r+aN3}T2{AdMjpk|A&<1oO%&*FkSm7HvFRloz-<5tG%7}!b|_EBapbnNZ5
    zk*U{+XWCdsf|T$CQPkEDx^vEt_q|vI7tpcu!>bmr4I!T_Q3b=*0A$Cg-kR9vkoW~3)^)@p~g^|B(#e0mT8x|2D
    zaU{i+5pTH9Y(v|Ed1AuRF#UcJt0Lc=Ot!shnYBdm2*5X#lA05i%GB{KbDErd1
    zV=;tkDYN-A+1o@qL}R050WhSB$z*X^T8!9SR>wc;Sj?|Ax#tEB8w)Dbm+R|}CjjZ2
    zDN%NbQXT#+&Nw7!A)t-~r}tdS&u(x%*V3gH#HgL2<>1pXbgyUc9Jjr6H)W_CfV_Ix
    zP(x7c1Hm&n2rKR|{uIt}*BZEXobi&jT7HC^x03)EidcwerzTJUsrv(LqumLDunKX0
    z>S}l<#yoIY;v+pon-;^%m(%upxYTPb7}tRFG6E_T)*rCCi<2Ue)*|#WeqwDcLrU$i
    z^)}JFQs)=yK;5Rjt5olY?iR-8PNZRxDr)6CKwzp}n9{qSLje@%{q0N?$X2Srt@Och
    zP~MX>3*qJ!dGxf_Hq#KNU5QvrZv@?jm)tNnkNsn&Si*X1Q^+e!AR~Sz8l|>mmW@MI^K6?GL|i99C|MAvWbulRWuDkLiz#W~
    zCe?o3YN4M2*5FpN8c0gbJswnKtX9D-7zctv5f=mA5&hHfp&X+Nb+gbf;j;kH9hlYU
    z7#D!D7pFBDHRWD7&rn(>475d{Lm?=hLT%q(&*^{ubX1rL6y{kL4lM{XRWIxh%SF!k
    zA=tFG5Sy@HqL@l}1q)vw;CQx*&#NS^;lUFfEtTEJlaLN1_T7lqd*;^Nr=oZQ^LQRs
    zwFY1B2^P^eL==$tHFoCJ7w^UZBO5%uEa{m2c>~l%ysJr?6=MT*WRsQuB;VQxkzMEi
    z#!ZG&<8&tXv3LlE0cRJJ|yz$
    z+AdDTpKaf|QTpg%;nuC-7hi~}?fzM!y4>iU(F=dt-~NDl&aPv8TdeE7WV*|T`1y^N
    zg=^qav=ohH+6!k=R_&%f@j}C-X$}(s2ImE@4y#k41^X&oUEe5IDBFqwg-g;n8b*!*
    zWXD7K6E}TdrOX-{X5v8d7*d1IO2cT}Lmpaq>jSF!sHH;Q7-pK1=Q5n4LA>n|Qo9&q
    z=idQrRI80T1O%&2zQOs`&&$i&QV&K(%@-#@;yJ$$g&8?#RuZ+8!`#lwF$!IWR#Y!U
    z0|-o&)eFIhM50-`n%$l2Yz7w$7pts(_Vtf+&JwA8Hnf8xR1QI>`+E%O?IUr3dq6E9
    zb+%cPvo98MDXY{&^y=npTtt4mk?bCj;%=~L`$~>2q1^^igQ~2}ZTY8ax;0xhIncVX
    z^6Manli6~?5R6`TVR5-2lQC%gvm7Fr6uh+9=f&0YVnT)T@Hinr*A
    z3ws#auw{5(nK1bj1oTh*)Oy=KhY-n#nz$>FIaPVbMpdsbyR!E`pQ;C6_Gjj@`Wc0v
    zYQ1J-E{Vbm96U=O7^*-rJM1}se+Cv9TK=3%=6HC8-R642TDL`n&Bw#9Jg{uUyKXQA*YEs41nD%NI_&x97K1yj1ee*~mIeVdY@-llfm-0_awt
    zQRIo%+CGu|;Apfn=Wo8EMvT>sI}pFTW(R$tVfHNsb7sV=haq6myNhbtM9+)M
    z^(z9Q2TO2gGsNI}lczAykNKldM-C
    z91`h#c~Of9WJ-h|d0%dI$mgAlvbWjdwacotRJ?^GFuNmVU0`qyzTjMT1-XbnlG2%R
    zID;k2;76P95;C?2ZSAIyF3HT4LU`Pq3dBSajVI-9@cOv5OS
    z0D0oG$;!8too5s5ldm%hb`OkfzFnc590O8qxTZG+TW+$dov#2@fz;GHeCd^HYKfOH
    z7V}v@=@27%dWi=#N7avqj%Qy&)OK&HEZ{VZ$(O+e$Z2<`)H?&5DQF$f)j;NlhRZ7Y
    z;MO;;Flr6@R139q0C@XerCa>bjz=s+1Aetwn34Hu5xtEN4PL|wvUd+a3CWzOkdYj}W0
    zE7Mh*nfc8Bl_elHobAP&221e83zqg%cTF)HcLyLY5+V<5Yb=$3=di*`K(B`M$kVOG
    z@7v;Fc%-kaUrpJZQJm>oKzD2l>ew(#+B#PI#p=ytKH~yS`2CXf*5#mH5Z|ztP&?@G
    z>lm=Lim>*p-<^?Bh0~iIUw`$oEaJ;=tS^yh2Z$P-z~DsNNQ`yc6=AB4ApjwBso5z#
    zpRI4_ht!`sihriH@1G%nFqC>H?6=bTN#@|jBcp*Xj2+A!Qjyg6<|GT%&#NgUI53?@
    z92`-&i#H}aH`}6q^h%8d$40>{jX#$+$WL8r;HI0Blhu>=$pp5Z=k
    z;xu?~5lG1l@#aDQXMLb^9y73vpcEA8&iFhWrJMJ;pJj*g#3a8hMozq%hvA<
    zzCv*lKl*Fanc+niPva`$PHW9xDNKba3qhE5zzL~HBTDt<)@_04AadkdNXm4F1~?ld
    zVQDbIq*Ep*>L`NWY!?^pD(e%(Tfhm{rajW|up1z^<4(N-%SaqfkbWw$Ao?Gsk
    zg*iqOjy6UrG{qnmXg*zEcn3tGYu;F)M+0bkFA>G)tuq3RNQ5CehGw-ujuh%j~vqv_fVG#p|S0JAmA?@8L`7tNk)6qaZuWXXY50Z7Be^DmBXc;V%?nQ
    znz%N!;t=dZR@_S&*zViS%s9LZ0G
    zUFoPNn;aI@-iKdKMkIarpyzg!)5oNExZmhha!XC8oILtQa6`m&haC_h8Cq-x6>Mv$
    zu4x7`0ZX3_M}-BE2YA7WTudhXoh`qYac|6j>R_eOz`3yOG5aHLqo$}&ey5Y5|9Q&Z
    zABMP_T+JF*K_x|e6&&OyrcoksSbp7-nawrdH9-L$O5K0SC>U#lb6rvG8@rXci%6B~
    z{rv=ZZu~G04tzPm~0Kr^4{j!hk*tqrc8q4jsy>fTpsHpq;oVIlFjjjn_dGR
    zzMG(Lm;6AAdsZ1(IS+2qvVYu1YwQ;36Ub54$61h&_J!FgakWle+BVi
    zW=HE0cfL^)vc2j+1cIU;9=necDl_J^ZD||BGeL;uhJLWz^%!%&AJTOE#Fls{yBkDD
    z=Kgt!yXnb`7;eg&E3Z5Ck3o368y~|{{Ubc6xo>&TCle#a##~$^lJP14NURA?
    z)i9`2Nn+i!C5YBl;D}n@kw2Qn@-@E)&+5|stwVNonRpSp~cb;7##jf+fkHgPV
    zO6vm(G1h1-;6uNPXsJR|9RvLpZoYVmuWsu%Rg|OpiHSKw|8B@q^&$hiovnRY|4xSi
    zMa|aBwP_d0+JTM9>7t!iR#8>mvXRXLMRcnO8CVba>U(suUo?=Z>B_mG-hlz);hh6&
    zC0oBNqJxTXOgj=qp`6oQ0nkmEoVk)07c!u=d;c9J&z`Gr$qP__!4(!l0nCY@gtDho
    znW+5nW^T%`PRGs5^R4@Tux$=MRB5#L{7LimTds4&FQLu7Em~OSa{1Mgtc%%JlNSXE
    z^19%~8wdAAIfPzA%4+s}VrAVZqGpV8M-We`u1QQ6m%KJyo0mW4Aj@4B`OpsecyL8N
    z(PAAm+Rc^8D9UbCY5Ltar!sh&ioT_|dK(U@*u^9vrYvC>r{}RaCYT<-erN9?FT?eq
    z-}Z^zRG+a1gAMqKwv%RR^APP7snmdfmkkUsf|*vZhh{@!2Xt4jzihkaf94<|*{lO^
    z3lI)#GEv^ygc9uaKGld7a#Eiz2@yMtVH=(y$VGP7uMo9U*qf8!g}1}ihn^iIJEVL!
    zpa;mje`_}&1W2BSDuw%~F^vWk)6uUHoNlQ2dxLbKoPh(FMD*=GW3m_HyDnpDh$R-%HZRr8b@fPA=OM&ow9
    zV~x7*yfZQ!JFxyXZ)9RuGXQriBiMm{>N7F2t`L>EMc0I<1y|T-Ziz*GmnbBMq8A9N
    zTFU3C94_9=?Fb$+_pMi}Ry+-zwdJ98TVyLn1}1iznHkus-`$?b7dQ?xJ|$wN@5nKV
    zuIx8TSZ54ibh-JNsIV~{Ah5pbaM=7yOU<&esNAV!`sMM4;s8CEP#2-iUtt|;{rQhd
    za~*dKd`}i?uU(v)mY?-rBdpl%2}cea*XG8nQJoq;v%Vy!4Thh{Z|Q&>O4vQ#*?g
    z3K9|&HaGd3J0T54g0Ji!)(V}>kXk1Oo1G20nHfWM#t5pHVTm3;!;x!VU7-k89LSL_B;4>Py{@CIDHlsL9
    znG{0FxKtGgT1g(B&(JC$8&=%}3yQ>xGe+4vVZvHNBX~d9OR(5Pc^`raB2{@MfPd=m
    zQ3(jiUUOhWRYUESMl(TslHAZUS;DoRc1KjKVb9t0YNnBu%$t<&j;g=WDbba~(J35G
    z+~PF&jQ*|oPP?e~R?iZmEhM1?lJ3M52W5_!tPu+zK4|;1mw&Z??xDl{J219>~FcM7(e
    z=#p}&=Slc9LC-1t<3MH`41*=i&0S(TP;E00aD`n&tw0Q<)up+Jfvv>qKoY`N`lAid
    zBDQ6{!14q!#3Rz3((|yLR=}htD-3XTqdE^);~wz_839&?nzRpTp}5?>vEd-c%=s8}
    z`n^nipfjDs*v^d+Yw@1=X3
    zH0{b%IaOrv-DdOEC
    
    literal 0
    HcmV?d00001
    
    diff --git a/deps/github.com/anacrolix/torrent/testdata/debian-9.1.0-amd64-netinst.iso.torrent b/deps/github.com/anacrolix/torrent/testdata/debian-9.1.0-amd64-netinst.iso.torrent
    new file mode 100644
    index 0000000000000000000000000000000000000000..bb4b10255a7036a623b43c21251207a48ca97053
    GIT binary patch
    literal 23623
    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
    ZowQf$#}%}Y1)u3keXNdK>4+USWo37sYcT)-
    
    literal 0
    HcmV?d00001
    
    diff --git a/deps/github.com/anacrolix/torrent/testdata/sintel.torrent b/deps/github.com/anacrolix/torrent/testdata/sintel.torrent
    new file mode 100644
    index 0000000000000000000000000000000000000000..c3775deb9e2bba4d38805d99bf995fa1168505ca
    GIT binary patch
    literal 20792
    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
    aVsvkEa%FCGE@^KsbZ>HUWo~qHFJ)y`DD-Lo
    
    literal 0
    HcmV?d00001
    
    diff --git a/deps/github.com/anacrolix/torrent/testing.go b/deps/github.com/anacrolix/torrent/testing.go
    new file mode 100644
    index 0000000..6fb5411
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/testing.go
    @@ -0,0 +1,37 @@
    +package torrent
    +
    +import (
    +	"testing"
    +	"time"
    +
    +	"github.com/anacrolix/log"
    +
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +func TestingConfig(t testing.TB) *ClientConfig {
    +	cfg := NewDefaultClientConfig()
    +	cfg.ListenHost = LoopbackListenHost
    +	cfg.NoDHT = true
    +	cfg.DataDir = t.TempDir()
    +	cfg.DisableTrackers = true
    +	cfg.NoDefaultPortForwarding = true
    +	cfg.DisableAcceptRateLimiting = true
    +	cfg.ListenPort = 0
    +	cfg.KeepAliveTimeout = time.Millisecond
    +	cfg.MinPeerExtensions.SetBit(pp.ExtensionBitFast, true)
    +	cfg.Logger = log.Default.WithContextText(t.Name())
    +	// 2 would suffice for the greeting test, but 5 is needed for a few other tests. This should be
    +	// something slightly higher than the usual chunk size, so it gets tickled in some tests.
    +	cfg.MaxAllocPeerRequestDataPerConn = 5
    +	//cfg.Debug = true
    +	//cfg.Logger = cfg.Logger.WithText(func(m log.Msg) string {
    +	//	t := m.Text()
    +	//	m.Values(func(i interface{}) bool {
    +	//		t += fmt.Sprintf("\n%[1]T: %[1]v", i)
    +	//		return true
    +	//	})
    +	//	return t
    +	//})
    +	return cfg
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tests/issue-798/main.go b/deps/github.com/anacrolix/torrent/tests/issue-798/main.go
    new file mode 100644
    index 0000000..23f6be3
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tests/issue-798/main.go
    @@ -0,0 +1,19 @@
    +package main
    +
    +import (
    +	"fmt"
    +
    +	"github.com/anacrolix/torrent"
    +)
    +
    +func main() {
    +	config := torrent.NewDefaultClientConfig()
    +	config.DataDir = "./output"
    +	c, _ := torrent.NewClient(config)
    +	defer c.Close()
    +	t, _ := c.AddMagnet("magnet:?xt=urn:btih:99c82bb73505a3c0b453f9fa0e881d6e5a32a0c1&tr=https%3A%2F%2Ftorrent.ubuntu.com%2Fannounce&tr=https%3A%2F%2Fipv6.torrent.ubuntu.com%2Fannounce")
    +	<-t.GotInfo()
    +	fmt.Println("start downloading")
    +	t.DownloadAll()
    +	c.WaitAll()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/torrent-piece-request-order.go b/deps/github.com/anacrolix/torrent/torrent-piece-request-order.go
    new file mode 100644
    index 0000000..10623da
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/torrent-piece-request-order.go
    @@ -0,0 +1,65 @@
    +package torrent
    +
    +import (
    +	request_strategy "github.com/anacrolix/torrent/request-strategy"
    +)
    +
    +func (t *Torrent) updatePieceRequestOrder(pieceIndex int) {
    +	if t.storage == nil {
    +		return
    +	}
    +	if ro, ok := t.cl.pieceRequestOrder[t.clientPieceRequestOrderKey()]; ok {
    +		ro.Update(
    +			t.pieceRequestOrderKey(pieceIndex),
    +			t.requestStrategyPieceOrderState(pieceIndex))
    +	}
    +}
    +
    +func (t *Torrent) clientPieceRequestOrderKey() interface{} {
    +	if t.storage.Capacity == nil {
    +		return t
    +	}
    +	return t.storage.Capacity
    +}
    +
    +func (t *Torrent) deletePieceRequestOrder() {
    +	if t.storage == nil {
    +		return
    +	}
    +	cpro := t.cl.pieceRequestOrder
    +	key := t.clientPieceRequestOrderKey()
    +	pro := cpro[key]
    +	for i := 0; i < t.numPieces(); i++ {
    +		pro.Delete(t.pieceRequestOrderKey(i))
    +	}
    +	if pro.Len() == 0 {
    +		delete(cpro, key)
    +	}
    +}
    +
    +func (t *Torrent) initPieceRequestOrder() {
    +	if t.storage == nil {
    +		return
    +	}
    +	if t.cl.pieceRequestOrder == nil {
    +		t.cl.pieceRequestOrder = make(map[interface{}]*request_strategy.PieceRequestOrder)
    +	}
    +	key := t.clientPieceRequestOrderKey()
    +	cpro := t.cl.pieceRequestOrder
    +	if cpro[key] == nil {
    +		cpro[key] = request_strategy.NewPieceOrder(request_strategy.NewAjwernerBtree(), t.numPieces())
    +	}
    +}
    +
    +func (t *Torrent) addRequestOrderPiece(i int) {
    +	if t.storage == nil {
    +		return
    +	}
    +	t.cl.pieceRequestOrder[t.clientPieceRequestOrderKey()].Add(
    +		t.pieceRequestOrderKey(i),
    +		t.requestStrategyPieceOrderState(i))
    +}
    +
    +func (t *Torrent) getPieceRequestOrder() *request_strategy.PieceRequestOrder {
    +	return t.cl.pieceRequestOrder[t.clientPieceRequestOrderKey()]
    +}
    diff --git a/deps/github.com/anacrolix/torrent/torrent-stats.go b/deps/github.com/anacrolix/torrent/torrent-stats.go
    new file mode 100644
    index 0000000..0dd58ad
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/torrent-stats.go
    @@ -0,0 +1,17 @@
    +package torrent
    +
    +// Due to ConnStats, may require special alignment on some platforms. See
    +// https://github.com/anacrolix/torrent/issues/383.
    +type TorrentStats struct {
    +	// Aggregates stats over all connections past and present. Some values may not have much meaning
    +	// in the aggregate context.
    +	ConnStats
    +
    +	// Ordered by expected descending quantities (if all is well).
    +	TotalPeers       int
    +	PendingPeers     int
    +	ActivePeers      int
    +	ConnectedSeeders int
    +	HalfOpenPeers    int
    +	PiecesComplete   int
    +}
    diff --git a/deps/github.com/anacrolix/torrent/torrent.go b/deps/github.com/anacrolix/torrent/torrent.go
    new file mode 100644
    index 0000000..0b1baba
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/torrent.go
    @@ -0,0 +1,2919 @@
    +package torrent
    +
    +import (
    +	"bytes"
    +	"container/heap"
    +	"context"
    +	"crypto/sha1"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"math/rand"
    +	"net/netip"
    +	"net/url"
    +	"sort"
    +	"strings"
    +	"text/tabwriter"
    +	"time"
    +	"unsafe"
    +
    +	"github.com/RoaringBitmap/roaring"
    +	"github.com/anacrolix/chansync"
    +	"github.com/anacrolix/chansync/events"
    +	"github.com/anacrolix/dht/v2"
    +	. "github.com/anacrolix/generics"
    +	g "github.com/anacrolix/generics"
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/perf"
    +	"github.com/anacrolix/missinggo/slices"
    +	"github.com/anacrolix/missinggo/v2"
    +	"github.com/anacrolix/missinggo/v2/bitmap"
    +	"github.com/anacrolix/missinggo/v2/pubsub"
    +	"github.com/anacrolix/multiless"
    +	"github.com/anacrolix/sync"
    +	"github.com/pion/datachannel"
    +	"golang.org/x/exp/maps"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/common"
    +	"github.com/anacrolix/torrent/internal/check"
    +	"github.com/anacrolix/torrent/internal/nestedmaps"
    +	"github.com/anacrolix/torrent/metainfo"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +	utHolepunch "github.com/anacrolix/torrent/peer_protocol/ut-holepunch"
    +	request_strategy "github.com/anacrolix/torrent/request-strategy"
    +	"github.com/anacrolix/torrent/segments"
    +	"github.com/anacrolix/torrent/storage"
    +	"github.com/anacrolix/torrent/tracker"
    +	typedRoaring "github.com/anacrolix/torrent/typed-roaring"
    +	"github.com/anacrolix/torrent/webseed"
    +	"github.com/anacrolix/torrent/webtorrent"
    +)
    +
    +// Maintains state of torrent within a Client. Many methods should not be called before the info is
    +// available, see .Info and .GotInfo.
    +type Torrent struct {
    +	// Torrent-level aggregate statistics. First in struct to ensure 64-bit
    +	// alignment. See #262.
    +	stats  ConnStats
    +	cl     *Client
    +	logger log.Logger
    +
    +	networkingEnabled      chansync.Flag
    +	dataDownloadDisallowed chansync.Flag
    +	dataUploadDisallowed   bool
    +	userOnWriteChunkErr    func(error)
    +
    +	closed   chansync.SetOnce
    +	onClose  []func()
    +	infoHash metainfo.Hash
    +	pieces   []Piece
    +
    +	// The order pieces are requested if there's no stronger reason like availability or priority.
    +	pieceRequestOrder []int
    +	// Values are the piece indices that changed.
    +	pieceStateChanges pubsub.PubSub[PieceStateChange]
    +	// The size of chunks to request from peers over the wire. This is
    +	// normally 16KiB by convention these days.
    +	chunkSize pp.Integer
    +	chunkPool sync.Pool
    +	// Total length of the torrent in bytes. Stored because it's not O(1) to
    +	// get this from the info dict.
    +	_length Option[int64]
    +
    +	// The storage to open when the info dict becomes available.
    +	storageOpener *storage.Client
    +	// Storage for torrent data.
    +	storage *storage.Torrent
    +	// Read-locked for using storage, and write-locked for Closing.
    +	storageLock sync.RWMutex
    +
    +	// TODO: Only announce stuff is used?
    +	metainfo metainfo.MetaInfo
    +
    +	// The info dict. nil if we don't have it (yet).
    +	info      *metainfo.Info
    +	fileIndex segments.Index
    +	files     *[]*File
    +
    +	_chunksPerRegularPiece chunkIndexType
    +
    +	webSeeds map[string]*Peer
    +	// Active peer connections, running message stream loops. TODO: Make this
    +	// open (not-closed) connections only.
    +	conns               map[*PeerConn]struct{}
    +	maxEstablishedConns int
    +	// Set of addrs to which we're attempting to connect. Connections are
    +	// half-open until all handshakes are completed.
    +	halfOpen map[string]map[outgoingConnAttemptKey]*PeerInfo
    +
    +	// Reserve of peers to connect to. A peer can be both here and in the
    +	// active connections if were told about the peer after connecting with
    +	// them. That encourages us to reconnect to peers that are well known in
    +	// the swarm.
    +	peers prioritizedPeers
    +	// Whether we want to know more peers.
    +	wantPeersEvent missinggo.Event
    +	// An announcer for each tracker URL.
    +	trackerAnnouncers map[string]torrentTrackerAnnouncer
    +	// How many times we've initiated a DHT announce. TODO: Move into stats.
    +	numDHTAnnounces int
    +
    +	// Name used if the info name isn't available. Should be cleared when the
    +	// Info does become available.
    +	nameMu      sync.RWMutex
    +	displayName string
    +
    +	// The bencoded bytes of the info dict. This is actively manipulated if
    +	// the info bytes aren't initially available, and we try to fetch them
    +	// from peers.
    +	metadataBytes []byte
    +	// Each element corresponds to the 16KiB metadata pieces. If true, we have
    +	// received that piece.
    +	metadataCompletedChunks []bool
    +	metadataChanged         sync.Cond
    +
    +	// Closed when .Info is obtained.
    +	gotMetainfoC chan struct{}
    +
    +	readers                map[*reader]struct{}
    +	_readerNowPieces       bitmap.Bitmap
    +	_readerReadaheadPieces bitmap.Bitmap
    +
    +	// A cache of pieces we need to get. Calculated from various piece and
    +	// file priorities and completion states elsewhere.
    +	_pendingPieces roaring.Bitmap
    +	// A cache of completed piece indices.
    +	_completedPieces roaring.Bitmap
    +	// Pieces that need to be hashed.
    +	piecesQueuedForHash       bitmap.Bitmap
    +	activePieceHashes         int
    +	initialPieceCheckDisabled bool
    +
    +	connsWithAllPieces map[*Peer]struct{}
    +
    +	requestState map[RequestIndex]requestState
    +	// Chunks we've written to since the corresponding piece was last checked.
    +	dirtyChunks typedRoaring.Bitmap[RequestIndex]
    +
    +	pex pexState
    +
    +	// Is On when all pieces are complete.
    +	Complete chansync.Flag
    +
    +	// Torrent sources in use keyed by the source string.
    +	activeSources sync.Map
    +	sourcesLogger log.Logger
    +
    +	smartBanCache smartBanCache
    +
    +	// Large allocations reused between request state updates.
    +	requestPieceStates []request_strategy.PieceRequestOrderState
    +	requestIndexes     []RequestIndex
    +}
    +
    +type outgoingConnAttemptKey = *PeerInfo
    +
    +func (t *Torrent) length() int64 {
    +	return t._length.Value
    +}
    +
    +func (t *Torrent) selectivePieceAvailabilityFromPeers(i pieceIndex) (count int) {
    +	// This could be done with roaring.BitSliceIndexing.
    +	t.iterPeers(func(peer *Peer) {
    +		if _, ok := t.connsWithAllPieces[peer]; ok {
    +			return
    +		}
    +		if peer.peerHasPiece(i) {
    +			count++
    +		}
    +	})
    +	return
    +}
    +
    +func (t *Torrent) decPieceAvailability(i pieceIndex) {
    +	if !t.haveInfo() {
    +		return
    +	}
    +	p := t.piece(i)
    +	if p.relativeAvailability <= 0 {
    +		panic(p.relativeAvailability)
    +	}
    +	p.relativeAvailability--
    +	t.updatePieceRequestOrder(i)
    +}
    +
    +func (t *Torrent) incPieceAvailability(i pieceIndex) {
    +	// If we don't the info, this should be reconciled when we do.
    +	if t.haveInfo() {
    +		p := t.piece(i)
    +		p.relativeAvailability++
    +		t.updatePieceRequestOrder(i)
    +	}
    +}
    +
    +func (t *Torrent) readerNowPieces() bitmap.Bitmap {
    +	return t._readerNowPieces
    +}
    +
    +func (t *Torrent) readerReadaheadPieces() bitmap.Bitmap {
    +	return t._readerReadaheadPieces
    +}
    +
    +func (t *Torrent) ignorePieceForRequests(i pieceIndex) bool {
    +	return !t.wantPieceIndex(i)
    +}
    +
    +// Returns a channel that is closed when the Torrent is closed.
    +func (t *Torrent) Closed() events.Done {
    +	return t.closed.Done()
    +}
    +
    +// KnownSwarm returns the known subset of the peers in the Torrent's swarm, including active,
    +// pending, and half-open peers.
    +func (t *Torrent) KnownSwarm() (ks []PeerInfo) {
    +	// Add pending peers to the list
    +	t.peers.Each(func(peer PeerInfo) {
    +		ks = append(ks, peer)
    +	})
    +
    +	// Add half-open peers to the list
    +	for _, attempts := range t.halfOpen {
    +		for _, peer := range attempts {
    +			ks = append(ks, *peer)
    +		}
    +	}
    +
    +	// Add active peers to the list
    +	t.cl.rLock()
    +	defer t.cl.rUnlock()
    +	for conn := range t.conns {
    +		ks = append(ks, PeerInfo{
    +			Id:     conn.PeerID,
    +			Addr:   conn.RemoteAddr,
    +			Source: conn.Discovery,
    +			// > If the connection is encrypted, that's certainly enough to set SupportsEncryption.
    +			// > But if we're not connected to them with an encrypted connection, I couldn't say
    +			// > what's appropriate. We can carry forward the SupportsEncryption value as we
    +			// > received it from trackers/DHT/PEX, or just use the encryption state for the
    +			// > connection. It's probably easiest to do the latter for now.
    +			// https://github.com/anacrolix/torrent/pull/188
    +			SupportsEncryption: conn.headerEncrypted,
    +		})
    +	}
    +
    +	return
    +}
    +
    +func (t *Torrent) setChunkSize(size pp.Integer) {
    +	t.chunkSize = size
    +	t.chunkPool = sync.Pool{
    +		New: func() interface{} {
    +			b := make([]byte, size)
    +			return &b
    +		},
    +	}
    +}
    +
    +func (t *Torrent) pieceComplete(piece pieceIndex) bool {
    +	return t._completedPieces.Contains(bitmap.BitIndex(piece))
    +}
    +
    +func (t *Torrent) pieceCompleteUncached(piece pieceIndex) storage.Completion {
    +	if t.storage == nil {
    +		return storage.Completion{Complete: false, Ok: true}
    +	}
    +	return t.pieces[piece].Storage().Completion()
    +}
    +
    +// There's a connection to that address already.
    +func (t *Torrent) addrActive(addr string) bool {
    +	if _, ok := t.halfOpen[addr]; ok {
    +		return true
    +	}
    +	for c := range t.conns {
    +		ra := c.RemoteAddr
    +		if ra.String() == addr {
    +			return true
    +		}
    +	}
    +	return false
    +}
    +
    +func (t *Torrent) appendUnclosedConns(ret []*PeerConn) []*PeerConn {
    +	return t.appendConns(ret, func(conn *PeerConn) bool {
    +		return !conn.closed.IsSet()
    +	})
    +}
    +
    +func (t *Torrent) appendConns(ret []*PeerConn, f func(*PeerConn) bool) []*PeerConn {
    +	for c := range t.conns {
    +		if f(c) {
    +			ret = append(ret, c)
    +		}
    +	}
    +	return ret
    +}
    +
    +func (t *Torrent) addPeer(p PeerInfo) (added bool) {
    +	cl := t.cl
    +	torrent.Add(fmt.Sprintf("peers added by source %q", p.Source), 1)
    +	if t.closed.IsSet() {
    +		return false
    +	}
    +	if ipAddr, ok := tryIpPortFromNetAddr(p.Addr); ok {
    +		if cl.badPeerIPPort(ipAddr.IP, ipAddr.Port) {
    +			torrent.Add("peers not added because of bad addr", 1)
    +			// cl.logger.Printf("peers not added because of bad addr: %v", p)
    +			return false
    +		}
    +	}
    +	if replaced, ok := t.peers.AddReturningReplacedPeer(p); ok {
    +		torrent.Add("peers replaced", 1)
    +		if !replaced.equal(p) {
    +			t.logger.WithDefaultLevel(log.Debug).Printf("added %v replacing %v", p, replaced)
    +			added = true
    +		}
    +	} else {
    +		added = true
    +	}
    +	t.openNewConns()
    +	for t.peers.Len() > cl.config.TorrentPeersHighWater {
    +		_, ok := t.peers.DeleteMin()
    +		if ok {
    +			torrent.Add("excess reserve peers discarded", 1)
    +		}
    +	}
    +	return
    +}
    +
    +func (t *Torrent) invalidateMetadata() {
    +	for i := 0; i < len(t.metadataCompletedChunks); i++ {
    +		t.metadataCompletedChunks[i] = false
    +	}
    +	t.nameMu.Lock()
    +	t.gotMetainfoC = make(chan struct{})
    +	t.info = nil
    +	t.nameMu.Unlock()
    +}
    +
    +func (t *Torrent) saveMetadataPiece(index int, data []byte) {
    +	if t.haveInfo() {
    +		return
    +	}
    +	if index >= len(t.metadataCompletedChunks) {
    +		t.logger.Printf("%s: ignoring metadata piece %d", t, index)
    +		return
    +	}
    +	copy(t.metadataBytes[(1<<14)*index:], data)
    +	t.metadataCompletedChunks[index] = true
    +}
    +
    +func (t *Torrent) metadataPieceCount() int {
    +	return (len(t.metadataBytes) + (1 << 14) - 1) / (1 << 14)
    +}
    +
    +func (t *Torrent) haveMetadataPiece(piece int) bool {
    +	if t.haveInfo() {
    +		return (1<<14)*piece < len(t.metadataBytes)
    +	} else {
    +		return piece < len(t.metadataCompletedChunks) && t.metadataCompletedChunks[piece]
    +	}
    +}
    +
    +func (t *Torrent) metadataSize() int {
    +	return len(t.metadataBytes)
    +}
    +
    +func infoPieceHashes(info *metainfo.Info) (ret [][]byte) {
    +	for i := 0; i < len(info.Pieces); i += sha1.Size {
    +		ret = append(ret, info.Pieces[i:i+sha1.Size])
    +	}
    +	return
    +}
    +
    +func (t *Torrent) makePieces() {
    +	hashes := infoPieceHashes(t.info)
    +	t.pieces = make([]Piece, len(hashes))
    +	for i, hash := range hashes {
    +		piece := &t.pieces[i]
    +		piece.t = t
    +		piece.index = pieceIndex(i)
    +		piece.noPendingWrites.L = &piece.pendingWritesMutex
    +		piece.hash = (*metainfo.Hash)(unsafe.Pointer(&hash[0]))
    +		files := *t.files
    +		beginFile := pieceFirstFileIndex(piece.torrentBeginOffset(), files)
    +		endFile := pieceEndFileIndex(piece.torrentEndOffset(), files)
    +		piece.files = files[beginFile:endFile]
    +	}
    +}
    +
    +// Returns the index of the first file containing the piece. files must be
    +// ordered by offset.
    +func pieceFirstFileIndex(pieceOffset int64, files []*File) int {
    +	for i, f := range files {
    +		if f.offset+f.length > pieceOffset {
    +			return i
    +		}
    +	}
    +	return 0
    +}
    +
    +// Returns the index after the last file containing the piece. files must be
    +// ordered by offset.
    +func pieceEndFileIndex(pieceEndOffset int64, files []*File) int {
    +	for i, f := range files {
    +		if f.offset+f.length >= pieceEndOffset {
    +			return i + 1
    +		}
    +	}
    +	return 0
    +}
    +
    +func (t *Torrent) cacheLength() {
    +	var l int64
    +	for _, f := range t.info.UpvertedFiles() {
    +		l += f.Length
    +	}
    +	t._length = Some(l)
    +}
    +
    +// TODO: This shouldn't fail for storage reasons. Instead we should handle storage failure
    +// separately.
    +func (t *Torrent) setInfo(info *metainfo.Info) error {
    +	if err := validateInfo(info); err != nil {
    +		return fmt.Errorf("bad info: %s", err)
    +	}
    +	if t.storageOpener != nil {
    +		var err error
    +		t.storage, err = t.storageOpener.OpenTorrent(info, t.infoHash)
    +		if err != nil {
    +			return fmt.Errorf("error opening torrent storage: %s", err)
    +		}
    +	}
    +	t.nameMu.Lock()
    +	t.info = info
    +	t.nameMu.Unlock()
    +	t._chunksPerRegularPiece = chunkIndexType((pp.Integer(t.usualPieceSize()) + t.chunkSize - 1) / t.chunkSize)
    +	t.updateComplete()
    +	t.fileIndex = segments.NewIndex(common.LengthIterFromUpvertedFiles(info.UpvertedFiles()))
    +	t.displayName = "" // Save a few bytes lol.
    +	t.initFiles()
    +	t.cacheLength()
    +	t.makePieces()
    +	return nil
    +}
    +
    +func (t *Torrent) pieceRequestOrderKey(i int) request_strategy.PieceRequestOrderKey {
    +	return request_strategy.PieceRequestOrderKey{
    +		InfoHash: t.infoHash,
    +		Index:    i,
    +	}
    +}
    +
    +// This seems to be all the follow-up tasks after info is set, that can't fail.
    +func (t *Torrent) onSetInfo() {
    +	t.pieceRequestOrder = rand.Perm(t.numPieces())
    +	t.initPieceRequestOrder()
    +	MakeSliceWithLength(&t.requestPieceStates, t.numPieces())
    +	for i := range t.pieces {
    +		p := &t.pieces[i]
    +		// Need to add relativeAvailability before updating piece completion, as that may result in conns
    +		// being dropped.
    +		if p.relativeAvailability != 0 {
    +			panic(p.relativeAvailability)
    +		}
    +		p.relativeAvailability = t.selectivePieceAvailabilityFromPeers(i)
    +		t.addRequestOrderPiece(i)
    +		t.updatePieceCompletion(i)
    +		if !t.initialPieceCheckDisabled && !p.storageCompletionOk {
    +			// t.logger.Printf("piece %s completion unknown, queueing check", p)
    +			t.queuePieceCheck(i)
    +		}
    +	}
    +	t.cl.event.Broadcast()
    +	close(t.gotMetainfoC)
    +	t.updateWantPeersEvent()
    +	t.requestState = make(map[RequestIndex]requestState)
    +	t.tryCreateMorePieceHashers()
    +	t.iterPeers(func(p *Peer) {
    +		p.onGotInfo(t.info)
    +		p.updateRequests("onSetInfo")
    +	})
    +}
    +
    +// Called when metadata for a torrent becomes available.
    +func (t *Torrent) setInfoBytesLocked(b []byte) error {
    +	if metainfo.HashBytes(b) != t.infoHash {
    +		return errors.New("info bytes have wrong hash")
    +	}
    +	var info metainfo.Info
    +	if err := bencode.Unmarshal(b, &info); err != nil {
    +		return fmt.Errorf("error unmarshalling info bytes: %s", err)
    +	}
    +	t.metadataBytes = b
    +	t.metadataCompletedChunks = nil
    +	if t.info != nil {
    +		return nil
    +	}
    +	if err := t.setInfo(&info); err != nil {
    +		return err
    +	}
    +	t.onSetInfo()
    +	return nil
    +}
    +
    +func (t *Torrent) haveAllMetadataPieces() bool {
    +	if t.haveInfo() {
    +		return true
    +	}
    +	if t.metadataCompletedChunks == nil {
    +		return false
    +	}
    +	for _, have := range t.metadataCompletedChunks {
    +		if !have {
    +			return false
    +		}
    +	}
    +	return true
    +}
    +
    +// TODO: Propagate errors to disconnect peer.
    +func (t *Torrent) setMetadataSize(size int) (err error) {
    +	if t.haveInfo() {
    +		// We already know the correct metadata size.
    +		return
    +	}
    +	if uint32(size) > maxMetadataSize {
    +		return log.WithLevel(log.Warning, errors.New("bad size"))
    +	}
    +	if len(t.metadataBytes) == size {
    +		return
    +	}
    +	t.metadataBytes = make([]byte, size)
    +	t.metadataCompletedChunks = make([]bool, (size+(1<<14)-1)/(1<<14))
    +	t.metadataChanged.Broadcast()
    +	for c := range t.conns {
    +		c.requestPendingMetadata()
    +	}
    +	return
    +}
    +
    +// The current working name for the torrent. Either the name in the info dict,
    +// or a display name given such as by the dn value in a magnet link, or "".
    +func (t *Torrent) name() string {
    +	t.nameMu.RLock()
    +	defer t.nameMu.RUnlock()
    +	if t.haveInfo() {
    +		return t.info.BestName()
    +	}
    +	if t.displayName != "" {
    +		return t.displayName
    +	}
    +	return "infohash:" + t.infoHash.HexString()
    +}
    +
    +func (t *Torrent) pieceState(index pieceIndex) (ret PieceState) {
    +	p := &t.pieces[index]
    +	ret.Priority = t.piecePriority(index)
    +	ret.Completion = p.completion()
    +	ret.QueuedForHash = p.queuedForHash()
    +	ret.Hashing = p.hashing
    +	ret.Checking = ret.QueuedForHash || ret.Hashing
    +	ret.Marking = p.marking
    +	if !ret.Complete && t.piecePartiallyDownloaded(index) {
    +		ret.Partial = true
    +	}
    +	return
    +}
    +
    +func (t *Torrent) metadataPieceSize(piece int) int {
    +	return metadataPieceSize(len(t.metadataBytes), piece)
    +}
    +
    +func (t *Torrent) newMetadataExtensionMessage(c *PeerConn, msgType pp.ExtendedMetadataRequestMsgType, piece int, data []byte) pp.Message {
    +	return pp.Message{
    +		Type:       pp.Extended,
    +		ExtendedID: c.PeerExtensionIDs[pp.ExtensionNameMetadata],
    +		ExtendedPayload: append(bencode.MustMarshal(pp.ExtendedMetadataRequestMsg{
    +			Piece:     piece,
    +			TotalSize: len(t.metadataBytes),
    +			Type:      msgType,
    +		}), data...),
    +	}
    +}
    +
    +type pieceAvailabilityRun struct {
    +	Count        pieceIndex
    +	Availability int
    +}
    +
    +func (me pieceAvailabilityRun) String() string {
    +	return fmt.Sprintf("%v(%v)", me.Count, me.Availability)
    +}
    +
    +func (t *Torrent) pieceAvailabilityRuns() (ret []pieceAvailabilityRun) {
    +	rle := missinggo.NewRunLengthEncoder(func(el interface{}, count uint64) {
    +		ret = append(ret, pieceAvailabilityRun{Availability: el.(int), Count: int(count)})
    +	})
    +	for i := range t.pieces {
    +		rle.Append(t.pieces[i].availability(), 1)
    +	}
    +	rle.Flush()
    +	return
    +}
    +
    +func (t *Torrent) pieceAvailabilityFrequencies() (freqs []int) {
    +	freqs = make([]int, t.numActivePeers()+1)
    +	for i := range t.pieces {
    +		freqs[t.piece(i).availability()]++
    +	}
    +	return
    +}
    +
    +func (t *Torrent) pieceStateRuns() (ret PieceStateRuns) {
    +	rle := missinggo.NewRunLengthEncoder(func(el interface{}, count uint64) {
    +		ret = append(ret, PieceStateRun{
    +			PieceState: el.(PieceState),
    +			Length:     int(count),
    +		})
    +	})
    +	for index := range t.pieces {
    +		rle.Append(t.pieceState(pieceIndex(index)), 1)
    +	}
    +	rle.Flush()
    +	return
    +}
    +
    +// Produces a small string representing a PieceStateRun.
    +func (psr PieceStateRun) String() (ret string) {
    +	ret = fmt.Sprintf("%d", psr.Length)
    +	ret += func() string {
    +		switch psr.Priority {
    +		case PiecePriorityNext:
    +			return "N"
    +		case PiecePriorityNormal:
    +			return "."
    +		case PiecePriorityReadahead:
    +			return "R"
    +		case PiecePriorityNow:
    +			return "!"
    +		case PiecePriorityHigh:
    +			return "H"
    +		default:
    +			return ""
    +		}
    +	}()
    +	if psr.Hashing {
    +		ret += "H"
    +	}
    +	if psr.QueuedForHash {
    +		ret += "Q"
    +	}
    +	if psr.Marking {
    +		ret += "M"
    +	}
    +	if psr.Partial {
    +		ret += "P"
    +	}
    +	if psr.Complete {
    +		ret += "C"
    +	}
    +	if !psr.Ok {
    +		ret += "?"
    +	}
    +	return
    +}
    +
    +func (t *Torrent) writeStatus(w io.Writer) {
    +	fmt.Fprintf(w, "Infohash: %s\n", t.infoHash.HexString())
    +	fmt.Fprintf(w, "Metadata length: %d\n", t.metadataSize())
    +	if !t.haveInfo() {
    +		fmt.Fprintf(w, "Metadata have: ")
    +		for _, h := range t.metadataCompletedChunks {
    +			fmt.Fprintf(w, "%c", func() rune {
    +				if h {
    +					return 'H'
    +				} else {
    +					return '.'
    +				}
    +			}())
    +		}
    +		fmt.Fprintln(w)
    +	}
    +	fmt.Fprintf(w, "Piece length: %s\n",
    +		func() string {
    +			if t.haveInfo() {
    +				return fmt.Sprintf("%v (%v chunks)",
    +					t.usualPieceSize(),
    +					float64(t.usualPieceSize())/float64(t.chunkSize))
    +			} else {
    +				return "no info"
    +			}
    +		}(),
    +	)
    +	if t.info != nil {
    +		fmt.Fprintf(w, "Num Pieces: %d (%d completed)\n", t.numPieces(), t.numPiecesCompleted())
    +		fmt.Fprintf(w, "Piece States: %s\n", t.pieceStateRuns())
    +		// Generates a huge, unhelpful listing when piece availability is very scattered. Prefer
    +		// availability frequencies instead.
    +		if false {
    +			fmt.Fprintf(w, "Piece availability: %v\n", strings.Join(func() (ret []string) {
    +				for _, run := range t.pieceAvailabilityRuns() {
    +					ret = append(ret, run.String())
    +				}
    +				return
    +			}(), " "))
    +		}
    +		fmt.Fprintf(w, "Piece availability frequency: %v\n", strings.Join(
    +			func() (ret []string) {
    +				for avail, freq := range t.pieceAvailabilityFrequencies() {
    +					if freq == 0 {
    +						continue
    +					}
    +					ret = append(ret, fmt.Sprintf("%v: %v", avail, freq))
    +				}
    +				return
    +			}(),
    +			", "))
    +	}
    +	fmt.Fprintf(w, "Reader Pieces:")
    +	t.forReaderOffsetPieces(func(begin, end pieceIndex) (again bool) {
    +		fmt.Fprintf(w, " %d:%d", begin, end)
    +		return true
    +	})
    +	fmt.Fprintln(w)
    +
    +	fmt.Fprintf(w, "Enabled trackers:\n")
    +	func() {
    +		tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
    +		fmt.Fprintf(tw, "    URL\tExtra\n")
    +		for _, ta := range slices.Sort(slices.FromMapElems(t.trackerAnnouncers), func(l, r torrentTrackerAnnouncer) bool {
    +			lu := l.URL()
    +			ru := r.URL()
    +			var luns, runs url.URL = *lu, *ru
    +			luns.Scheme = ""
    +			runs.Scheme = ""
    +			var ml missinggo.MultiLess
    +			ml.StrictNext(luns.String() == runs.String(), luns.String() < runs.String())
    +			ml.StrictNext(lu.String() == ru.String(), lu.String() < ru.String())
    +			return ml.Less()
    +		}).([]torrentTrackerAnnouncer) {
    +			fmt.Fprintf(tw, "    %q\t%v\n", ta.URL(), ta.statusLine())
    +		}
    +		tw.Flush()
    +	}()
    +
    +	fmt.Fprintf(w, "DHT Announces: %d\n", t.numDHTAnnounces)
    +
    +	dumpStats(w, t.statsLocked())
    +
    +	fmt.Fprintf(w, "webseeds:\n")
    +	t.writePeerStatuses(w, maps.Values(t.webSeeds))
    +
    +	peerConns := maps.Keys(t.conns)
    +	// Peers without priorities first, then those with. I'm undecided about how to order peers
    +	// without priorities.
    +	sort.Slice(peerConns, func(li, ri int) bool {
    +		l := peerConns[li]
    +		r := peerConns[ri]
    +		ml := multiless.New()
    +		lpp := g.ResultFromTuple(l.peerPriority()).ToOption()
    +		rpp := g.ResultFromTuple(r.peerPriority()).ToOption()
    +		ml = ml.Bool(lpp.Ok, rpp.Ok)
    +		ml = ml.Uint32(rpp.Value, lpp.Value)
    +		return ml.Less()
    +	})
    +
    +	fmt.Fprintf(w, "%v peer conns:\n", len(peerConns))
    +	t.writePeerStatuses(w, g.SliceMap(peerConns, func(pc *PeerConn) *Peer {
    +		return &pc.Peer
    +	}))
    +}
    +
    +func (t *Torrent) writePeerStatuses(w io.Writer, peers []*Peer) {
    +	var buf bytes.Buffer
    +	for _, c := range peers {
    +		fmt.Fprintf(w, "- ")
    +		buf.Reset()
    +		c.writeStatus(&buf)
    +		w.Write(bytes.TrimRight(
    +			bytes.ReplaceAll(buf.Bytes(), []byte("\n"), []byte("\n  ")),
    +			" "))
    +	}
    +}
    +
    +func (t *Torrent) haveInfo() bool {
    +	return t.info != nil
    +}
    +
    +// Returns a run-time generated MetaInfo that includes the info bytes and
    +// announce-list as currently known to the client.
    +func (t *Torrent) newMetaInfo() metainfo.MetaInfo {
    +	return metainfo.MetaInfo{
    +		CreationDate: time.Now().Unix(),
    +		Comment:      "dynamic metainfo from client",
    +		CreatedBy:    "go.torrent",
    +		AnnounceList: t.metainfo.UpvertedAnnounceList().Clone(),
    +		InfoBytes: func() []byte {
    +			if t.haveInfo() {
    +				return t.metadataBytes
    +			} else {
    +				return nil
    +			}
    +		}(),
    +		UrlList: func() []string {
    +			ret := make([]string, 0, len(t.webSeeds))
    +			for url := range t.webSeeds {
    +				ret = append(ret, url)
    +			}
    +			return ret
    +		}(),
    +	}
    +}
    +
    +// Returns a count of bytes that are not complete in storage, and not pending being written to
    +// storage. This value is from the perspective of the download manager, and may not agree with the
    +// actual state in storage. If you want read data synchronously you should use a Reader. See
    +// https://github.com/anacrolix/torrent/issues/828.
    +func (t *Torrent) BytesMissing() (n int64) {
    +	t.cl.rLock()
    +	n = t.bytesMissingLocked()
    +	t.cl.rUnlock()
    +	return
    +}
    +
    +func (t *Torrent) bytesMissingLocked() int64 {
    +	return t.bytesLeft()
    +}
    +
    +func iterFlipped(b *roaring.Bitmap, end uint64, cb func(uint32) bool) {
    +	roaring.Flip(b, 0, end).Iterate(cb)
    +}
    +
    +func (t *Torrent) bytesLeft() (left int64) {
    +	iterFlipped(&t._completedPieces, uint64(t.numPieces()), func(x uint32) bool {
    +		p := t.piece(pieceIndex(x))
    +		left += int64(p.length() - p.numDirtyBytes())
    +		return true
    +	})
    +	return
    +}
    +
    +// Bytes left to give in tracker announces.
    +func (t *Torrent) bytesLeftAnnounce() int64 {
    +	if t.haveInfo() {
    +		return t.bytesLeft()
    +	} else {
    +		return -1
    +	}
    +}
    +
    +func (t *Torrent) piecePartiallyDownloaded(piece pieceIndex) bool {
    +	if t.pieceComplete(piece) {
    +		return false
    +	}
    +	if t.pieceAllDirty(piece) {
    +		return false
    +	}
    +	return t.pieces[piece].hasDirtyChunks()
    +}
    +
    +func (t *Torrent) usualPieceSize() int {
    +	return int(t.info.PieceLength)
    +}
    +
    +func (t *Torrent) numPieces() pieceIndex {
    +	return t.info.NumPieces()
    +}
    +
    +func (t *Torrent) numPiecesCompleted() (num pieceIndex) {
    +	return pieceIndex(t._completedPieces.GetCardinality())
    +}
    +
    +func (t *Torrent) close(wg *sync.WaitGroup) (err error) {
    +	if !t.closed.Set() {
    +		err = errors.New("already closed")
    +		return
    +	}
    +	for _, f := range t.onClose {
    +		f()
    +	}
    +	if t.storage != nil {
    +		wg.Add(1)
    +		go func() {
    +			defer wg.Done()
    +			t.storageLock.Lock()
    +			defer t.storageLock.Unlock()
    +			if f := t.storage.Close; f != nil {
    +				err1 := f()
    +				if err1 != nil {
    +					t.logger.WithDefaultLevel(log.Warning).Printf("error closing storage: %v", err1)
    +				}
    +			}
    +		}()
    +	}
    +	t.iterPeers(func(p *Peer) {
    +		p.close()
    +	})
    +	if t.storage != nil {
    +		t.deletePieceRequestOrder()
    +	}
    +	t.assertAllPiecesRelativeAvailabilityZero()
    +	t.pex.Reset()
    +	t.cl.event.Broadcast()
    +	t.pieceStateChanges.Close()
    +	t.updateWantPeersEvent()
    +	return
    +}
    +
    +func (t *Torrent) assertAllPiecesRelativeAvailabilityZero() {
    +	for i := range t.pieces {
    +		p := t.piece(i)
    +		if p.relativeAvailability != 0 {
    +			panic(fmt.Sprintf("piece %v has relative availability %v", i, p.relativeAvailability))
    +		}
    +	}
    +}
    +
    +func (t *Torrent) requestOffset(r Request) int64 {
    +	return torrentRequestOffset(t.length(), int64(t.usualPieceSize()), r)
    +}
    +
    +// Return the request that would include the given offset into the torrent data. Returns !ok if
    +// there is no such request.
    +func (t *Torrent) offsetRequest(off int64) (req Request, ok bool) {
    +	return torrentOffsetRequest(t.length(), t.info.PieceLength, int64(t.chunkSize), off)
    +}
    +
    +func (t *Torrent) writeChunk(piece int, begin int64, data []byte) (err error) {
    +	defer perf.ScopeTimerErr(&err)()
    +	n, err := t.pieces[piece].Storage().WriteAt(data, begin)
    +	if err == nil && n != len(data) {
    +		err = io.ErrShortWrite
    +	}
    +	return err
    +}
    +
    +func (t *Torrent) bitfield() (bf []bool) {
    +	bf = make([]bool, t.numPieces())
    +	t._completedPieces.Iterate(func(piece uint32) (again bool) {
    +		bf[piece] = true
    +		return true
    +	})
    +	return
    +}
    +
    +func (t *Torrent) pieceNumChunks(piece pieceIndex) chunkIndexType {
    +	return chunkIndexType((t.pieceLength(piece) + t.chunkSize - 1) / t.chunkSize)
    +}
    +
    +func (t *Torrent) chunksPerRegularPiece() chunkIndexType {
    +	return t._chunksPerRegularPiece
    +}
    +
    +func (t *Torrent) numChunks() RequestIndex {
    +	if t.numPieces() == 0 {
    +		return 0
    +	}
    +	return RequestIndex(t.numPieces()-1)*t.chunksPerRegularPiece() + t.pieceNumChunks(t.numPieces()-1)
    +}
    +
    +func (t *Torrent) pendAllChunkSpecs(pieceIndex pieceIndex) {
    +	t.dirtyChunks.RemoveRange(
    +		uint64(t.pieceRequestIndexOffset(pieceIndex)),
    +		uint64(t.pieceRequestIndexOffset(pieceIndex+1)))
    +}
    +
    +func (t *Torrent) pieceLength(piece pieceIndex) pp.Integer {
    +	if t.info.PieceLength == 0 {
    +		// There will be no variance amongst pieces. Only pain.
    +		return 0
    +	}
    +	if piece == t.numPieces()-1 {
    +		ret := pp.Integer(t.length() % t.info.PieceLength)
    +		if ret != 0 {
    +			return ret
    +		}
    +	}
    +	return pp.Integer(t.info.PieceLength)
    +}
    +
    +func (t *Torrent) smartBanBlockCheckingWriter(piece pieceIndex) *blockCheckingWriter {
    +	return &blockCheckingWriter{
    +		cache:        &t.smartBanCache,
    +		requestIndex: t.pieceRequestIndexOffset(piece),
    +		chunkSize:    t.chunkSize.Int(),
    +	}
    +}
    +
    +func (t *Torrent) hashPiece(piece pieceIndex) (
    +	ret metainfo.Hash,
    +	// These are peers that sent us blocks that differ from what we hash here.
    +	differingPeers map[bannableAddr]struct{},
    +	err error,
    +) {
    +	p := t.piece(piece)
    +	p.waitNoPendingWrites()
    +	storagePiece := t.pieces[piece].Storage()
    +
    +	// Does the backend want to do its own hashing?
    +	if i, ok := storagePiece.PieceImpl.(storage.SelfHashing); ok {
    +		var sum metainfo.Hash
    +		// log.Printf("A piece decided to self-hash: %d", piece)
    +		sum, err = i.SelfHash()
    +		missinggo.CopyExact(&ret, sum)
    +		return
    +	}
    +
    +	hash := pieceHash.New()
    +	const logPieceContents = false
    +	smartBanWriter := t.smartBanBlockCheckingWriter(piece)
    +	writers := []io.Writer{hash, smartBanWriter}
    +	var examineBuf bytes.Buffer
    +	if logPieceContents {
    +		writers = append(writers, &examineBuf)
    +	}
    +	_, err = storagePiece.WriteTo(io.MultiWriter(writers...))
    +	if logPieceContents {
    +		t.logger.WithDefaultLevel(log.Debug).Printf("hashed %q with copy err %v", examineBuf.Bytes(), err)
    +	}
    +	smartBanWriter.Flush()
    +	differingPeers = smartBanWriter.badPeers
    +	missinggo.CopyExact(&ret, hash.Sum(nil))
    +	return
    +}
    +
    +func (t *Torrent) haveAnyPieces() bool {
    +	return !t._completedPieces.IsEmpty()
    +}
    +
    +func (t *Torrent) haveAllPieces() bool {
    +	if !t.haveInfo() {
    +		return false
    +	}
    +	return t._completedPieces.GetCardinality() == bitmap.BitRange(t.numPieces())
    +}
    +
    +func (t *Torrent) havePiece(index pieceIndex) bool {
    +	return t.haveInfo() && t.pieceComplete(index)
    +}
    +
    +func (t *Torrent) maybeDropMutuallyCompletePeer(
    +	// I'm not sure about taking peer here, not all peer implementations actually drop. Maybe that's
    +	// okay?
    +	p *PeerConn,
    +) {
    +	if !t.cl.config.DropMutuallyCompletePeers {
    +		return
    +	}
    +	if !t.haveAllPieces() {
    +		return
    +	}
    +	if all, known := p.peerHasAllPieces(); !(known && all) {
    +		return
    +	}
    +	if p.useful() {
    +		return
    +	}
    +	p.logger.Levelf(log.Debug, "is mutually complete; dropping")
    +	p.drop()
    +}
    +
    +func (t *Torrent) haveChunk(r Request) (ret bool) {
    +	// defer func() {
    +	// 	log.Println("have chunk", r, ret)
    +	// }()
    +	if !t.haveInfo() {
    +		return false
    +	}
    +	if t.pieceComplete(pieceIndex(r.Index)) {
    +		return true
    +	}
    +	p := &t.pieces[r.Index]
    +	return !p.pendingChunk(r.ChunkSpec, t.chunkSize)
    +}
    +
    +func chunkIndexFromChunkSpec(cs ChunkSpec, chunkSize pp.Integer) chunkIndexType {
    +	return chunkIndexType(cs.Begin / chunkSize)
    +}
    +
    +func (t *Torrent) wantPieceIndex(index pieceIndex) bool {
    +	return t._pendingPieces.Contains(uint32(index))
    +}
    +
    +// A pool of []*PeerConn, to reduce allocations in functions that need to index or sort Torrent
    +// conns (which is a map).
    +var peerConnSlices sync.Pool
    +
    +func getPeerConnSlice(cap int) []*PeerConn {
    +	getInterface := peerConnSlices.Get()
    +	if getInterface == nil {
    +		return make([]*PeerConn, 0, cap)
    +	} else {
    +		return getInterface.([]*PeerConn)[:0]
    +	}
    +}
    +
    +// Calls the given function with a slice of unclosed conns. It uses a pool to reduce allocations as
    +// this is a frequent occurrence.
    +func (t *Torrent) withUnclosedConns(f func([]*PeerConn)) {
    +	sl := t.appendUnclosedConns(getPeerConnSlice(len(t.conns)))
    +	f(sl)
    +	peerConnSlices.Put(sl)
    +}
    +
    +func (t *Torrent) worstBadConnFromSlice(opts worseConnLensOpts, sl []*PeerConn) *PeerConn {
    +	wcs := worseConnSlice{conns: sl}
    +	wcs.initKeys(opts)
    +	heap.Init(&wcs)
    +	for wcs.Len() != 0 {
    +		c := heap.Pop(&wcs).(*PeerConn)
    +		if opts.incomingIsBad && !c.outgoing {
    +			return c
    +		}
    +		if opts.outgoingIsBad && c.outgoing {
    +			return c
    +		}
    +		if c._stats.ChunksReadWasted.Int64() >= 6 && c._stats.ChunksReadWasted.Int64() > c._stats.ChunksReadUseful.Int64() {
    +			return c
    +		}
    +		// If the connection is in the worst half of the established
    +		// connection quota and is older than a minute.
    +		if wcs.Len() >= (t.maxEstablishedConns+1)/2 {
    +			// Give connections 1 minute to prove themselves.
    +			if time.Since(c.completedHandshake) > time.Minute {
    +				return c
    +			}
    +		}
    +	}
    +	return nil
    +}
    +
    +// The worst connection is one that hasn't been sent, or sent anything useful for the longest. A bad
    +// connection is one that usually sends us unwanted pieces, or has been in the worse half of the
    +// established connections for more than a minute. This is O(n log n). If there was a way to not
    +// consider the position of a conn relative to the total number, it could be reduced to O(n).
    +func (t *Torrent) worstBadConn(opts worseConnLensOpts) (ret *PeerConn) {
    +	t.withUnclosedConns(func(ucs []*PeerConn) {
    +		ret = t.worstBadConnFromSlice(opts, ucs)
    +	})
    +	return
    +}
    +
    +type PieceStateChange struct {
    +	Index int
    +	PieceState
    +}
    +
    +func (t *Torrent) publishPieceChange(piece pieceIndex) {
    +	t.cl._mu.Defer(func() {
    +		cur := t.pieceState(piece)
    +		p := &t.pieces[piece]
    +		if cur != p.publicPieceState {
    +			p.publicPieceState = cur
    +			t.pieceStateChanges.Publish(PieceStateChange{
    +				int(piece),
    +				cur,
    +			})
    +		}
    +	})
    +}
    +
    +func (t *Torrent) pieceNumPendingChunks(piece pieceIndex) pp.Integer {
    +	if t.pieceComplete(piece) {
    +		return 0
    +	}
    +	return pp.Integer(t.pieceNumChunks(piece) - t.pieces[piece].numDirtyChunks())
    +}
    +
    +func (t *Torrent) pieceAllDirty(piece pieceIndex) bool {
    +	return t.pieces[piece].allChunksDirty()
    +}
    +
    +func (t *Torrent) readersChanged() {
    +	t.updateReaderPieces()
    +	t.updateAllPiecePriorities("Torrent.readersChanged")
    +}
    +
    +func (t *Torrent) updateReaderPieces() {
    +	t._readerNowPieces, t._readerReadaheadPieces = t.readerPiecePriorities()
    +}
    +
    +func (t *Torrent) readerPosChanged(from, to pieceRange) {
    +	if from == to {
    +		return
    +	}
    +	t.updateReaderPieces()
    +	// Order the ranges, high and low.
    +	l, h := from, to
    +	if l.begin > h.begin {
    +		l, h = h, l
    +	}
    +	if l.end < h.begin {
    +		// Two distinct ranges.
    +		t.updatePiecePriorities(l.begin, l.end, "Torrent.readerPosChanged")
    +		t.updatePiecePriorities(h.begin, h.end, "Torrent.readerPosChanged")
    +	} else {
    +		// Ranges overlap.
    +		end := l.end
    +		if h.end > end {
    +			end = h.end
    +		}
    +		t.updatePiecePriorities(l.begin, end, "Torrent.readerPosChanged")
    +	}
    +}
    +
    +func (t *Torrent) maybeNewConns() {
    +	// Tickle the accept routine.
    +	t.cl.event.Broadcast()
    +	t.openNewConns()
    +}
    +
    +func (t *Torrent) piecePriorityChanged(piece pieceIndex, reason string) {
    +	if t._pendingPieces.Contains(uint32(piece)) {
    +		t.iterPeers(func(c *Peer) {
    +			// if c.requestState.Interested {
    +			// 	return
    +			// }
    +			if !c.isLowOnRequests() {
    +				return
    +			}
    +			if !c.peerHasPiece(piece) {
    +				return
    +			}
    +			if c.requestState.Interested && c.peerChoking && !c.peerAllowedFast.Contains(piece) {
    +				return
    +			}
    +			c.updateRequests(reason)
    +		})
    +	}
    +	t.maybeNewConns()
    +	t.publishPieceChange(piece)
    +}
    +
    +func (t *Torrent) updatePiecePriority(piece pieceIndex, reason string) {
    +	if !t.closed.IsSet() {
    +		// It would be possible to filter on pure-priority changes here to avoid churning the piece
    +		// request order.
    +		t.updatePieceRequestOrder(piece)
    +	}
    +	p := &t.pieces[piece]
    +	newPrio := p.uncachedPriority()
    +	// t.logger.Printf("torrent %p: piece %d: uncached priority: %v", t, piece, newPrio)
    +	if newPrio == PiecePriorityNone {
    +		if !t._pendingPieces.CheckedRemove(uint32(piece)) {
    +			return
    +		}
    +	} else {
    +		if !t._pendingPieces.CheckedAdd(uint32(piece)) {
    +			return
    +		}
    +	}
    +	t.piecePriorityChanged(piece, reason)
    +}
    +
    +func (t *Torrent) updateAllPiecePriorities(reason string) {
    +	t.updatePiecePriorities(0, t.numPieces(), reason)
    +}
    +
    +// Update all piece priorities in one hit. This function should have the same
    +// output as updatePiecePriority, but across all pieces.
    +func (t *Torrent) updatePiecePriorities(begin, end pieceIndex, reason string) {
    +	for i := begin; i < end; i++ {
    +		t.updatePiecePriority(i, reason)
    +	}
    +}
    +
    +// Returns the range of pieces [begin, end) that contains the extent of bytes.
    +func (t *Torrent) byteRegionPieces(off, size int64) (begin, end pieceIndex) {
    +	if off >= t.length() {
    +		return
    +	}
    +	if off < 0 {
    +		size += off
    +		off = 0
    +	}
    +	if size <= 0 {
    +		return
    +	}
    +	begin = pieceIndex(off / t.info.PieceLength)
    +	end = pieceIndex((off + size + t.info.PieceLength - 1) / t.info.PieceLength)
    +	if end > pieceIndex(t.info.NumPieces()) {
    +		end = pieceIndex(t.info.NumPieces())
    +	}
    +	return
    +}
    +
    +// Returns true if all iterations complete without breaking. Returns the read regions for all
    +// readers. The reader regions should not be merged as some callers depend on this method to
    +// enumerate readers.
    +func (t *Torrent) forReaderOffsetPieces(f func(begin, end pieceIndex) (more bool)) (all bool) {
    +	for r := range t.readers {
    +		p := r.pieces
    +		if p.begin >= p.end {
    +			continue
    +		}
    +		if !f(p.begin, p.end) {
    +			return false
    +		}
    +	}
    +	return true
    +}
    +
    +func (t *Torrent) piecePriority(piece pieceIndex) piecePriority {
    +	return t.piece(piece).uncachedPriority()
    +}
    +
    +func (t *Torrent) pendRequest(req RequestIndex) {
    +	t.piece(t.pieceIndexOfRequestIndex(req)).pendChunkIndex(req % t.chunksPerRegularPiece())
    +}
    +
    +func (t *Torrent) pieceCompletionChanged(piece pieceIndex, reason string) {
    +	t.cl.event.Broadcast()
    +	if t.pieceComplete(piece) {
    +		t.onPieceCompleted(piece)
    +	} else {
    +		t.onIncompletePiece(piece)
    +	}
    +	t.updatePiecePriority(piece, reason)
    +}
    +
    +func (t *Torrent) numReceivedConns() (ret int) {
    +	for c := range t.conns {
    +		if c.Discovery == PeerSourceIncoming {
    +			ret++
    +		}
    +	}
    +	return
    +}
    +
    +func (t *Torrent) numOutgoingConns() (ret int) {
    +	for c := range t.conns {
    +		if c.outgoing {
    +			ret++
    +		}
    +	}
    +	return
    +}
    +
    +func (t *Torrent) maxHalfOpen() int {
    +	// Note that if we somehow exceed the maximum established conns, we want
    +	// the negative value to have an effect.
    +	establishedHeadroom := int64(t.maxEstablishedConns - len(t.conns))
    +	extraIncoming := int64(t.numReceivedConns() - t.maxEstablishedConns/2)
    +	// We want to allow some experimentation with new peers, and to try to
    +	// upset an oversupply of received connections.
    +	return int(min(
    +		max(5, extraIncoming)+establishedHeadroom,
    +		int64(t.cl.config.HalfOpenConnsPerTorrent),
    +	))
    +}
    +
    +func (t *Torrent) openNewConns() (initiated int) {
    +	defer t.updateWantPeersEvent()
    +	for t.peers.Len() != 0 {
    +		if !t.wantOutgoingConns() {
    +			return
    +		}
    +		if len(t.halfOpen) >= t.maxHalfOpen() {
    +			return
    +		}
    +		if len(t.cl.dialers) == 0 {
    +			return
    +		}
    +		if t.cl.numHalfOpen >= t.cl.config.TotalHalfOpenConns {
    +			return
    +		}
    +		p := t.peers.PopMax()
    +		opts := outgoingConnOpts{
    +			peerInfo:                 p,
    +			t:                        t,
    +			requireRendezvous:        false,
    +			skipHolepunchRendezvous:  false,
    +			receivedHolepunchConnect: false,
    +			HeaderObfuscationPolicy:  t.cl.config.HeaderObfuscationPolicy,
    +		}
    +		initiateConn(opts, false)
    +		initiated++
    +	}
    +	return
    +}
    +
    +func (t *Torrent) updatePieceCompletion(piece pieceIndex) bool {
    +	p := t.piece(piece)
    +	uncached := t.pieceCompleteUncached(piece)
    +	cached := p.completion()
    +	changed := cached != uncached
    +	complete := uncached.Complete
    +	p.storageCompletionOk = uncached.Ok
    +	x := uint32(piece)
    +	if complete {
    +		t._completedPieces.Add(x)
    +		t.openNewConns()
    +	} else {
    +		t._completedPieces.Remove(x)
    +	}
    +	p.t.updatePieceRequestOrder(piece)
    +	t.updateComplete()
    +	if complete && len(p.dirtiers) != 0 {
    +		t.logger.Printf("marked piece %v complete but still has dirtiers", piece)
    +	}
    +	if changed {
    +		log.Fstr("piece %d completion changed: %+v -> %+v", piece, cached, uncached).LogLevel(log.Debug, t.logger)
    +		t.pieceCompletionChanged(piece, "Torrent.updatePieceCompletion")
    +	}
    +	return changed
    +}
    +
    +// Non-blocking read. Client lock is not required.
    +func (t *Torrent) readAt(b []byte, off int64) (n int, err error) {
    +	for len(b) != 0 {
    +		p := &t.pieces[off/t.info.PieceLength]
    +		p.waitNoPendingWrites()
    +		var n1 int
    +		n1, err = p.Storage().ReadAt(b, off-p.Info().Offset())
    +		if n1 == 0 {
    +			break
    +		}
    +		off += int64(n1)
    +		n += n1
    +		b = b[n1:]
    +	}
    +	return
    +}
    +
    +// Returns an error if the metadata was completed, but couldn't be set for some reason. Blame it on
    +// the last peer to contribute. TODO: Actually we shouldn't blame peers for failure to open storage
    +// etc. Also we should probably cached metadata pieces per-Peer, to isolate failure appropriately.
    +func (t *Torrent) maybeCompleteMetadata() error {
    +	if t.haveInfo() {
    +		// Nothing to do.
    +		return nil
    +	}
    +	if !t.haveAllMetadataPieces() {
    +		// Don't have enough metadata pieces.
    +		return nil
    +	}
    +	err := t.setInfoBytesLocked(t.metadataBytes)
    +	if err != nil {
    +		t.invalidateMetadata()
    +		return fmt.Errorf("error setting info bytes: %s", err)
    +	}
    +	if t.cl.config.Debug {
    +		t.logger.Printf("%s: got metadata from peers", t)
    +	}
    +	return nil
    +}
    +
    +func (t *Torrent) readerPiecePriorities() (now, readahead bitmap.Bitmap) {
    +	t.forReaderOffsetPieces(func(begin, end pieceIndex) bool {
    +		if end > begin {
    +			now.Add(bitmap.BitIndex(begin))
    +			readahead.AddRange(bitmap.BitRange(begin)+1, bitmap.BitRange(end))
    +		}
    +		return true
    +	})
    +	return
    +}
    +
    +func (t *Torrent) needData() bool {
    +	if t.closed.IsSet() {
    +		return false
    +	}
    +	if !t.haveInfo() {
    +		return true
    +	}
    +	return !t._pendingPieces.IsEmpty()
    +}
    +
    +func appendMissingStrings(old, new []string) (ret []string) {
    +	ret = old
    +new:
    +	for _, n := range new {
    +		for _, o := range old {
    +			if o == n {
    +				continue new
    +			}
    +		}
    +		ret = append(ret, n)
    +	}
    +	return
    +}
    +
    +func appendMissingTrackerTiers(existing [][]string, minNumTiers int) (ret [][]string) {
    +	ret = existing
    +	for minNumTiers > len(ret) {
    +		ret = append(ret, nil)
    +	}
    +	return
    +}
    +
    +func (t *Torrent) addTrackers(announceList [][]string) {
    +	fullAnnounceList := &t.metainfo.AnnounceList
    +	t.metainfo.AnnounceList = appendMissingTrackerTiers(*fullAnnounceList, len(announceList))
    +	for tierIndex, trackerURLs := range announceList {
    +		(*fullAnnounceList)[tierIndex] = appendMissingStrings((*fullAnnounceList)[tierIndex], trackerURLs)
    +	}
    +	t.startMissingTrackerScrapers()
    +	t.updateWantPeersEvent()
    +}
    +
    +// Don't call this before the info is available.
    +func (t *Torrent) bytesCompleted() int64 {
    +	if !t.haveInfo() {
    +		return 0
    +	}
    +	return t.length() - t.bytesLeft()
    +}
    +
    +func (t *Torrent) SetInfoBytes(b []byte) (err error) {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	return t.setInfoBytesLocked(b)
    +}
    +
    +// Returns true if connection is removed from torrent.Conns.
    +func (t *Torrent) deletePeerConn(c *PeerConn) (ret bool) {
    +	if !c.closed.IsSet() {
    +		panic("connection is not closed")
    +		// There are behaviours prevented by the closed state that will fail
    +		// if the connection has been deleted.
    +	}
    +	_, ret = t.conns[c]
    +	delete(t.conns, c)
    +	// Avoid adding a drop event more than once. Probably we should track whether we've generated
    +	// the drop event against the PexConnState instead.
    +	if ret {
    +		if !t.cl.config.DisablePEX {
    +			t.pex.Drop(c)
    +		}
    +	}
    +	torrent.Add("deleted connections", 1)
    +	c.deleteAllRequests("Torrent.deletePeerConn")
    +	t.assertPendingRequests()
    +	if t.numActivePeers() == 0 && len(t.connsWithAllPieces) != 0 {
    +		panic(t.connsWithAllPieces)
    +	}
    +	return
    +}
    +
    +func (t *Torrent) decPeerPieceAvailability(p *Peer) {
    +	if t.deleteConnWithAllPieces(p) {
    +		return
    +	}
    +	if !t.haveInfo() {
    +		return
    +	}
    +	p.peerPieces().Iterate(func(i uint32) bool {
    +		p.t.decPieceAvailability(pieceIndex(i))
    +		return true
    +	})
    +}
    +
    +func (t *Torrent) assertPendingRequests() {
    +	if !check.Enabled {
    +		return
    +	}
    +	// var actual pendingRequests
    +	// if t.haveInfo() {
    +	// 	actual.m = make([]int, t.numChunks())
    +	// }
    +	// t.iterPeers(func(p *Peer) {
    +	// 	p.requestState.Requests.Iterate(func(x uint32) bool {
    +	// 		actual.Inc(x)
    +	// 		return true
    +	// 	})
    +	// })
    +	// diff := cmp.Diff(actual.m, t.pendingRequests.m)
    +	// if diff != "" {
    +	// 	panic(diff)
    +	// }
    +}
    +
    +func (t *Torrent) dropConnection(c *PeerConn) {
    +	t.cl.event.Broadcast()
    +	c.close()
    +	if t.deletePeerConn(c) {
    +		t.openNewConns()
    +	}
    +}
    +
    +// Peers as in contact information for dialing out.
    +func (t *Torrent) wantPeers() bool {
    +	if t.closed.IsSet() {
    +		return false
    +	}
    +	if t.peers.Len() > t.cl.config.TorrentPeersLowWater {
    +		return false
    +	}
    +	return t.wantOutgoingConns()
    +}
    +
    +func (t *Torrent) updateWantPeersEvent() {
    +	if t.wantPeers() {
    +		t.wantPeersEvent.Set()
    +	} else {
    +		t.wantPeersEvent.Clear()
    +	}
    +}
    +
    +// Returns whether the client should make effort to seed the torrent.
    +func (t *Torrent) seeding() bool {
    +	cl := t.cl
    +	if t.closed.IsSet() {
    +		return false
    +	}
    +	if t.dataUploadDisallowed {
    +		return false
    +	}
    +	if cl.config.NoUpload {
    +		return false
    +	}
    +	if !cl.config.Seed {
    +		return false
    +	}
    +	if cl.config.DisableAggressiveUpload && t.needData() {
    +		return false
    +	}
    +	return true
    +}
    +
    +func (t *Torrent) onWebRtcConn(
    +	c datachannel.ReadWriteCloser,
    +	dcc webtorrent.DataChannelContext,
    +) {
    +	defer c.Close()
    +	netConn := webrtcNetConn{
    +		ReadWriteCloser:    c,
    +		DataChannelContext: dcc,
    +	}
    +	peerRemoteAddr := netConn.RemoteAddr()
    +	//t.logger.Levelf(log.Critical, "onWebRtcConn remote addr: %v", peerRemoteAddr)
    +	if t.cl.badPeerAddr(peerRemoteAddr) {
    +		return
    +	}
    +	localAddrIpPort := missinggo.IpPortFromNetAddr(netConn.LocalAddr())
    +	pc, err := t.cl.initiateProtocolHandshakes(
    +		context.Background(),
    +		netConn,
    +		t,
    +		false,
    +		newConnectionOpts{
    +			outgoing:        dcc.LocalOffered,
    +			remoteAddr:      peerRemoteAddr,
    +			localPublicAddr: localAddrIpPort,
    +			network:         webrtcNetwork,
    +			connString:      fmt.Sprintf("webrtc offer_id %x: %v", dcc.OfferId, regularNetConnPeerConnConnString(netConn)),
    +		},
    +	)
    +	if err != nil {
    +		t.logger.WithDefaultLevel(log.Error).Printf("error in handshaking webrtc connection: %v", err)
    +		return
    +	}
    +	if dcc.LocalOffered {
    +		pc.Discovery = PeerSourceTracker
    +	} else {
    +		pc.Discovery = PeerSourceIncoming
    +	}
    +	pc.conn.SetWriteDeadline(time.Time{})
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	err = t.runHandshookConn(pc)
    +	if err != nil {
    +		t.logger.WithDefaultLevel(log.Debug).Printf("error running handshook webrtc conn: %v", err)
    +	}
    +}
    +
    +func (t *Torrent) logRunHandshookConn(pc *PeerConn, logAll bool, level log.Level) {
    +	err := t.runHandshookConn(pc)
    +	if err != nil || logAll {
    +		t.logger.WithDefaultLevel(level).Levelf(log.ErrorLevel(err), "error running handshook conn: %v", err)
    +	}
    +}
    +
    +func (t *Torrent) runHandshookConnLoggingErr(pc *PeerConn) {
    +	t.logRunHandshookConn(pc, false, log.Debug)
    +}
    +
    +func (t *Torrent) startWebsocketAnnouncer(u url.URL) torrentTrackerAnnouncer {
    +	wtc, release := t.cl.websocketTrackers.Get(u.String(), t.infoHash)
    +	// This needs to run before the Torrent is dropped from the Client, to prevent a new webtorrent.TrackerClient for
    +	// the same info hash before the old one is cleaned up.
    +	t.onClose = append(t.onClose, release)
    +	wst := websocketTrackerStatus{u, wtc}
    +	go func() {
    +		err := wtc.Announce(tracker.Started, t.infoHash)
    +		if err != nil {
    +			t.logger.WithDefaultLevel(log.Warning).Printf(
    +				"error in initial announce to %q: %v",
    +				u.String(), err,
    +			)
    +		}
    +	}()
    +	return wst
    +}
    +
    +func (t *Torrent) startScrapingTracker(_url string) {
    +	if _url == "" {
    +		return
    +	}
    +	u, err := url.Parse(_url)
    +	if err != nil {
    +		// URLs with a leading '*' appear to be a uTorrent convention to disable trackers.
    +		if _url[0] != '*' {
    +			t.logger.Levelf(log.Warning, "error parsing tracker url: %v", err)
    +		}
    +		return
    +	}
    +	if u.Scheme == "udp" {
    +		u.Scheme = "udp4"
    +		t.startScrapingTracker(u.String())
    +		u.Scheme = "udp6"
    +		t.startScrapingTracker(u.String())
    +		return
    +	}
    +	if _, ok := t.trackerAnnouncers[_url]; ok {
    +		return
    +	}
    +	sl := func() torrentTrackerAnnouncer {
    +		switch u.Scheme {
    +		case "ws", "wss":
    +			if t.cl.config.DisableWebtorrent {
    +				return nil
    +			}
    +			return t.startWebsocketAnnouncer(*u)
    +		case "udp4":
    +			if t.cl.config.DisableIPv4Peers || t.cl.config.DisableIPv4 {
    +				return nil
    +			}
    +		case "udp6":
    +			if t.cl.config.DisableIPv6 {
    +				return nil
    +			}
    +		}
    +		newAnnouncer := &trackerScraper{
    +			u:               *u,
    +			t:               t,
    +			lookupTrackerIp: t.cl.config.LookupTrackerIp,
    +		}
    +		go newAnnouncer.Run()
    +		return newAnnouncer
    +	}()
    +	if sl == nil {
    +		return
    +	}
    +	if t.trackerAnnouncers == nil {
    +		t.trackerAnnouncers = make(map[string]torrentTrackerAnnouncer)
    +	}
    +	t.trackerAnnouncers[_url] = sl
    +}
    +
    +// Adds and starts tracker scrapers for tracker URLs that aren't already
    +// running.
    +func (t *Torrent) startMissingTrackerScrapers() {
    +	if t.cl.config.DisableTrackers {
    +		return
    +	}
    +	t.startScrapingTracker(t.metainfo.Announce)
    +	for _, tier := range t.metainfo.AnnounceList {
    +		for _, url := range tier {
    +			t.startScrapingTracker(url)
    +		}
    +	}
    +}
    +
    +// Returns an AnnounceRequest with fields filled out to defaults and current
    +// values.
    +func (t *Torrent) announceRequest(event tracker.AnnounceEvent) tracker.AnnounceRequest {
    +	// Note that IPAddress is not set. It's set for UDP inside the tracker code, since it's
    +	// dependent on the network in use.
    +	return tracker.AnnounceRequest{
    +		Event: event,
    +		NumWant: func() int32 {
    +			if t.wantPeers() && len(t.cl.dialers) > 0 {
    +				return 200 // Win has UDP packet limit. See: https://github.com/anacrolix/torrent/issues/764
    +			} else {
    +				return 0
    +			}
    +		}(),
    +		Port:     uint16(t.cl.incomingPeerPort()),
    +		PeerId:   t.cl.peerID,
    +		InfoHash: t.infoHash,
    +		Key:      t.cl.announceKey(),
    +
    +		// The following are vaguely described in BEP 3.
    +
    +		Left:     t.bytesLeftAnnounce(),
    +		Uploaded: t.stats.BytesWrittenData.Int64(),
    +		// There's no mention of wasted or unwanted download in the BEP.
    +		Downloaded: t.stats.BytesReadUsefulData.Int64(),
    +	}
    +}
    +
    +// Adds peers revealed in an announce until the announce ends, or we have
    +// enough peers.
    +func (t *Torrent) consumeDhtAnnouncePeers(pvs <-chan dht.PeersValues) {
    +	cl := t.cl
    +	for v := range pvs {
    +		cl.lock()
    +		added := 0
    +		for _, cp := range v.Peers {
    +			if cp.Port == 0 {
    +				// Can't do anything with this.
    +				continue
    +			}
    +			if t.addPeer(PeerInfo{
    +				Addr:   ipPortAddr{cp.IP, cp.Port},
    +				Source: PeerSourceDhtGetPeers,
    +			}) {
    +				added++
    +			}
    +		}
    +		cl.unlock()
    +		// if added != 0 {
    +		// 	log.Printf("added %v peers from dht for %v", added, t.InfoHash().HexString())
    +		// }
    +	}
    +}
    +
    +// Announce using the provided DHT server. Peers are consumed automatically. done is closed when the
    +// announce ends. stop will force the announce to end.
    +func (t *Torrent) AnnounceToDht(s DhtServer) (done <-chan struct{}, stop func(), err error) {
    +	ps, err := s.Announce(t.infoHash, t.cl.incomingPeerPort(), true)
    +	if err != nil {
    +		return
    +	}
    +	_done := make(chan struct{})
    +	done = _done
    +	stop = ps.Close
    +	go func() {
    +		t.consumeDhtAnnouncePeers(ps.Peers())
    +		close(_done)
    +	}()
    +	return
    +}
    +
    +func (t *Torrent) timeboxedAnnounceToDht(s DhtServer) error {
    +	_, stop, err := t.AnnounceToDht(s)
    +	if err != nil {
    +		return err
    +	}
    +	select {
    +	case <-t.closed.Done():
    +	case <-time.After(5 * time.Minute):
    +	}
    +	stop()
    +	return nil
    +}
    +
    +func (t *Torrent) dhtAnnouncer(s DhtServer) {
    +	cl := t.cl
    +	cl.lock()
    +	defer cl.unlock()
    +	for {
    +		for {
    +			if t.closed.IsSet() {
    +				return
    +			}
    +			// We're also announcing ourselves as a listener, so we don't just want peer addresses.
    +			// TODO: We can include the announce_peer step depending on whether we can receive
    +			// inbound connections. We should probably only announce once every 15 mins too.
    +			if !t.wantAnyConns() {
    +				goto wait
    +			}
    +			// TODO: Determine if there's a listener on the port we're announcing.
    +			if len(cl.dialers) == 0 && len(cl.listeners) == 0 {
    +				goto wait
    +			}
    +			break
    +		wait:
    +			cl.event.Wait()
    +		}
    +		func() {
    +			t.numDHTAnnounces++
    +			cl.unlock()
    +			defer cl.lock()
    +			err := t.timeboxedAnnounceToDht(s)
    +			if err != nil {
    +				t.logger.WithDefaultLevel(log.Warning).Printf("error announcing %q to DHT: %s", t, err)
    +			}
    +		}()
    +	}
    +}
    +
    +func (t *Torrent) addPeers(peers []PeerInfo) (added int) {
    +	for _, p := range peers {
    +		if t.addPeer(p) {
    +			added++
    +		}
    +	}
    +	return
    +}
    +
    +// The returned TorrentStats may require alignment in memory. See
    +// https://github.com/anacrolix/torrent/issues/383.
    +func (t *Torrent) Stats() TorrentStats {
    +	t.cl.rLock()
    +	defer t.cl.rUnlock()
    +	return t.statsLocked()
    +}
    +
    +func (t *Torrent) statsLocked() (ret TorrentStats) {
    +	ret.ActivePeers = len(t.conns)
    +	ret.HalfOpenPeers = len(t.halfOpen)
    +	ret.PendingPeers = t.peers.Len()
    +	ret.TotalPeers = t.numTotalPeers()
    +	ret.ConnectedSeeders = 0
    +	for c := range t.conns {
    +		if all, ok := c.peerHasAllPieces(); all && ok {
    +			ret.ConnectedSeeders++
    +		}
    +	}
    +	ret.ConnStats = t.stats.Copy()
    +	ret.PiecesComplete = t.numPiecesCompleted()
    +	return
    +}
    +
    +// The total number of peers in the torrent.
    +func (t *Torrent) numTotalPeers() int {
    +	peers := make(map[string]struct{})
    +	for conn := range t.conns {
    +		ra := conn.conn.RemoteAddr()
    +		if ra == nil {
    +			// It's been closed and doesn't support RemoteAddr.
    +			continue
    +		}
    +		peers[ra.String()] = struct{}{}
    +	}
    +	for addr := range t.halfOpen {
    +		peers[addr] = struct{}{}
    +	}
    +	t.peers.Each(func(peer PeerInfo) {
    +		peers[peer.Addr.String()] = struct{}{}
    +	})
    +	return len(peers)
    +}
    +
    +// Reconcile bytes transferred before connection was associated with a
    +// torrent.
    +func (t *Torrent) reconcileHandshakeStats(c *PeerConn) {
    +	if c._stats != (ConnStats{
    +		// Handshakes should only increment these fields:
    +		BytesWritten: c._stats.BytesWritten,
    +		BytesRead:    c._stats.BytesRead,
    +	}) {
    +		panic("bad stats")
    +	}
    +	c.postHandshakeStats(func(cs *ConnStats) {
    +		cs.BytesRead.Add(c._stats.BytesRead.Int64())
    +		cs.BytesWritten.Add(c._stats.BytesWritten.Int64())
    +	})
    +	c.reconciledHandshakeStats = true
    +}
    +
    +// Returns true if the connection is added.
    +func (t *Torrent) addPeerConn(c *PeerConn) (err error) {
    +	defer func() {
    +		if err == nil {
    +			torrent.Add("added connections", 1)
    +		}
    +	}()
    +	if t.closed.IsSet() {
    +		return errors.New("torrent closed")
    +	}
    +	for c0 := range t.conns {
    +		if c.PeerID != c0.PeerID {
    +			continue
    +		}
    +		if !t.cl.config.DropDuplicatePeerIds {
    +			continue
    +		}
    +		if c.hasPreferredNetworkOver(c0) {
    +			c0.close()
    +			t.deletePeerConn(c0)
    +		} else {
    +			return errors.New("existing connection preferred")
    +		}
    +	}
    +	if len(t.conns) >= t.maxEstablishedConns {
    +		numOutgoing := t.numOutgoingConns()
    +		numIncoming := len(t.conns) - numOutgoing
    +		c := t.worstBadConn(worseConnLensOpts{
    +			// We've already established that we have too many connections at this point, so we just
    +			// need to match what kind we have too many of vs. what we're trying to add now.
    +			incomingIsBad: (numIncoming-numOutgoing > 1) && c.outgoing,
    +			outgoingIsBad: (numOutgoing-numIncoming > 1) && !c.outgoing,
    +		})
    +		if c == nil {
    +			return errors.New("don't want conn")
    +		}
    +		c.close()
    +		t.deletePeerConn(c)
    +	}
    +	if len(t.conns) >= t.maxEstablishedConns {
    +		panic(len(t.conns))
    +	}
    +	t.conns[c] = struct{}{}
    +	t.cl.event.Broadcast()
    +	// We'll never receive the "p" extended handshake parameter.
    +	if !t.cl.config.DisablePEX && !c.PeerExtensionBytes.SupportsExtended() {
    +		t.pex.Add(c)
    +	}
    +	return nil
    +}
    +
    +func (t *Torrent) newConnsAllowed() bool {
    +	if !t.networkingEnabled.Bool() {
    +		return false
    +	}
    +	if t.closed.IsSet() {
    +		return false
    +	}
    +	if !t.needData() && (!t.seeding() || !t.haveAnyPieces()) {
    +		return false
    +	}
    +	return true
    +}
    +
    +func (t *Torrent) wantAnyConns() bool {
    +	if !t.networkingEnabled.Bool() {
    +		return false
    +	}
    +	if t.closed.IsSet() {
    +		return false
    +	}
    +	if !t.needData() && (!t.seeding() || !t.haveAnyPieces()) {
    +		return false
    +	}
    +	return len(t.conns) < t.maxEstablishedConns
    +}
    +
    +func (t *Torrent) wantOutgoingConns() bool {
    +	if !t.newConnsAllowed() {
    +		return false
    +	}
    +	if len(t.conns) < t.maxEstablishedConns {
    +		return true
    +	}
    +	numIncomingConns := len(t.conns) - t.numOutgoingConns()
    +	return t.worstBadConn(worseConnLensOpts{
    +		incomingIsBad: numIncomingConns-t.numOutgoingConns() > 1,
    +		outgoingIsBad: false,
    +	}) != nil
    +}
    +
    +func (t *Torrent) wantIncomingConns() bool {
    +	if !t.newConnsAllowed() {
    +		return false
    +	}
    +	if len(t.conns) < t.maxEstablishedConns {
    +		return true
    +	}
    +	numIncomingConns := len(t.conns) - t.numOutgoingConns()
    +	return t.worstBadConn(worseConnLensOpts{
    +		incomingIsBad: false,
    +		outgoingIsBad: t.numOutgoingConns()-numIncomingConns > 1,
    +	}) != nil
    +}
    +
    +func (t *Torrent) SetMaxEstablishedConns(max int) (oldMax int) {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	oldMax = t.maxEstablishedConns
    +	t.maxEstablishedConns = max
    +	wcs := worseConnSlice{
    +		conns: t.appendConns(nil, func(*PeerConn) bool {
    +			return true
    +		}),
    +	}
    +	wcs.initKeys(worseConnLensOpts{})
    +	heap.Init(&wcs)
    +	for len(t.conns) > t.maxEstablishedConns && wcs.Len() > 0 {
    +		t.dropConnection(heap.Pop(&wcs).(*PeerConn))
    +	}
    +	t.openNewConns()
    +	return oldMax
    +}
    +
    +func (t *Torrent) pieceHashed(piece pieceIndex, passed bool, hashIoErr error) {
    +	t.logger.LazyLog(log.Debug, func() log.Msg {
    +		return log.Fstr("hashed piece %d (passed=%t)", piece, passed)
    +	})
    +	p := t.piece(piece)
    +	p.numVerifies++
    +	t.cl.event.Broadcast()
    +	if t.closed.IsSet() {
    +		return
    +	}
    +
    +	// Don't score the first time a piece is hashed, it could be an initial check.
    +	if p.storageCompletionOk {
    +		if passed {
    +			pieceHashedCorrect.Add(1)
    +		} else {
    +			log.Fmsg(
    +				"piece %d failed hash: %d connections contributed", piece, len(p.dirtiers),
    +			).AddValues(t, p).LogLevel(
    +
    +				log.Debug, t.logger)
    +
    +			pieceHashedNotCorrect.Add(1)
    +		}
    +	}
    +
    +	p.marking = true
    +	t.publishPieceChange(piece)
    +	defer func() {
    +		p.marking = false
    +		t.publishPieceChange(piece)
    +	}()
    +
    +	if passed {
    +		if len(p.dirtiers) != 0 {
    +			// Don't increment stats above connection-level for every involved connection.
    +			t.allStats((*ConnStats).incrementPiecesDirtiedGood)
    +		}
    +		for c := range p.dirtiers {
    +			c._stats.incrementPiecesDirtiedGood()
    +		}
    +		t.clearPieceTouchers(piece)
    +		hasDirty := p.hasDirtyChunks()
    +		t.cl.unlock()
    +		if hasDirty {
    +			p.Flush() // You can be synchronous here!
    +		}
    +		err := p.Storage().MarkComplete()
    +		if err != nil {
    +			t.logger.Levelf(log.Warning, "%T: error marking piece complete %d: %s", t.storage, piece, err)
    +		}
    +		t.cl.lock()
    +
    +		if t.closed.IsSet() {
    +			return
    +		}
    +		t.pendAllChunkSpecs(piece)
    +	} else {
    +		if len(p.dirtiers) != 0 && p.allChunksDirty() && hashIoErr == nil {
    +			// Peers contributed to all the data for this piece hash failure, and the failure was
    +			// not due to errors in the storage (such as data being dropped in a cache).
    +
    +			// Increment Torrent and above stats, and then specific connections.
    +			t.allStats((*ConnStats).incrementPiecesDirtiedBad)
    +			for c := range p.dirtiers {
    +				// Y u do dis peer?!
    +				c.stats().incrementPiecesDirtiedBad()
    +			}
    +
    +			bannableTouchers := make([]*Peer, 0, len(p.dirtiers))
    +			for c := range p.dirtiers {
    +				if !c.trusted {
    +					bannableTouchers = append(bannableTouchers, c)
    +				}
    +			}
    +			t.clearPieceTouchers(piece)
    +			slices.Sort(bannableTouchers, connLessTrusted)
    +
    +			if t.cl.config.Debug {
    +				t.logger.Printf(
    +					"bannable conns by trust for piece %d: %v",
    +					piece,
    +					func() (ret []connectionTrust) {
    +						for _, c := range bannableTouchers {
    +							ret = append(ret, c.trust())
    +						}
    +						return
    +					}(),
    +				)
    +			}
    +
    +			if len(bannableTouchers) >= 1 {
    +				c := bannableTouchers[0]
    +				if len(bannableTouchers) != 1 {
    +					t.logger.Levelf(log.Debug, "would have banned %v for touching piece %v after failed piece check", c.remoteIp(), piece)
    +				} else {
    +					// Turns out it's still useful to ban peers like this because if there's only a
    +					// single peer for a piece, and we never progress that piece to completion, we
    +					// will never smart-ban them. Discovered in
    +					// https://github.com/anacrolix/torrent/issues/715.
    +					t.logger.Levelf(log.Warning, "banning %v for being sole dirtier of piece %v after failed piece check", c, piece)
    +					c.ban()
    +				}
    +			}
    +		}
    +		t.onIncompletePiece(piece)
    +		p.Storage().MarkNotComplete()
    +	}
    +	t.updatePieceCompletion(piece)
    +}
    +
    +func (t *Torrent) cancelRequestsForPiece(piece pieceIndex) {
    +	start := t.pieceRequestIndexOffset(piece)
    +	end := start + t.pieceNumChunks(piece)
    +	for ri := start; ri < end; ri++ {
    +		t.cancelRequest(ri)
    +	}
    +}
    +
    +func (t *Torrent) onPieceCompleted(piece pieceIndex) {
    +	t.pendAllChunkSpecs(piece)
    +	t.cancelRequestsForPiece(piece)
    +	t.piece(piece).readerCond.Broadcast()
    +	for conn := range t.conns {
    +		conn.have(piece)
    +		t.maybeDropMutuallyCompletePeer(conn)
    +	}
    +}
    +
    +// Called when a piece is found to be not complete.
    +func (t *Torrent) onIncompletePiece(piece pieceIndex) {
    +	if t.pieceAllDirty(piece) {
    +		t.pendAllChunkSpecs(piece)
    +	}
    +	if !t.wantPieceIndex(piece) {
    +		// t.logger.Printf("piece %d incomplete and unwanted", piece)
    +		return
    +	}
    +	// We could drop any connections that we told we have a piece that we
    +	// don't here. But there's a test failure, and it seems clients don't care
    +	// if you request pieces that you already claim to have. Pruning bad
    +	// connections might just remove any connections that aren't treating us
    +	// favourably anyway.
    +
    +	// for c := range t.conns {
    +	// 	if c.sentHave(piece) {
    +	// 		c.drop()
    +	// 	}
    +	// }
    +	t.iterPeers(func(conn *Peer) {
    +		if conn.peerHasPiece(piece) {
    +			conn.updateRequests("piece incomplete")
    +		}
    +	})
    +}
    +
    +func (t *Torrent) tryCreateMorePieceHashers() {
    +	for !t.closed.IsSet() && t.activePieceHashes < t.cl.config.PieceHashersPerTorrent && t.tryCreatePieceHasher() {
    +	}
    +}
    +
    +func (t *Torrent) tryCreatePieceHasher() bool {
    +	if t.storage == nil {
    +		return false
    +	}
    +	pi, ok := t.getPieceToHash()
    +	if !ok {
    +		return false
    +	}
    +	p := t.piece(pi)
    +	t.piecesQueuedForHash.Remove(bitmap.BitIndex(pi))
    +	p.hashing = true
    +	t.publishPieceChange(pi)
    +	t.updatePiecePriority(pi, "Torrent.tryCreatePieceHasher")
    +	t.storageLock.RLock()
    +	t.activePieceHashes++
    +	go t.pieceHasher(pi)
    +	return true
    +}
    +
    +func (t *Torrent) getPieceToHash() (ret pieceIndex, ok bool) {
    +	t.piecesQueuedForHash.IterTyped(func(i pieceIndex) bool {
    +		if t.piece(i).hashing {
    +			return true
    +		}
    +		ret = i
    +		ok = true
    +		return false
    +	})
    +	return
    +}
    +
    +func (t *Torrent) dropBannedPeers() {
    +	t.iterPeers(func(p *Peer) {
    +		remoteIp := p.remoteIp()
    +		if remoteIp == nil {
    +			if p.bannableAddr.Ok {
    +				t.logger.WithDefaultLevel(log.Debug).Printf("can't get remote ip for peer %v", p)
    +			}
    +			return
    +		}
    +		netipAddr := netip.MustParseAddr(remoteIp.String())
    +		if Some(netipAddr) != p.bannableAddr {
    +			t.logger.WithDefaultLevel(log.Debug).Printf(
    +				"peer remote ip does not match its bannable addr [peer=%v, remote ip=%v, bannable addr=%v]",
    +				p, remoteIp, p.bannableAddr)
    +		}
    +		if _, ok := t.cl.badPeerIPs[netipAddr]; ok {
    +			// Should this be a close?
    +			p.drop()
    +			t.logger.WithDefaultLevel(log.Debug).Printf("dropped %v for banned remote IP %v", p, netipAddr)
    +		}
    +	})
    +}
    +
    +func (t *Torrent) pieceHasher(index pieceIndex) {
    +	p := t.piece(index)
    +	sum, failedPeers, copyErr := t.hashPiece(index)
    +	correct := sum == *p.hash
    +	switch copyErr {
    +	case nil, io.EOF:
    +	default:
    +		log.Fmsg("piece %v (%s) hash failure copy error: %v", p, p.hash.HexString(), copyErr).Log(t.logger)
    +	}
    +	t.storageLock.RUnlock()
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	if correct {
    +		for peer := range failedPeers {
    +			t.cl.banPeerIP(peer.AsSlice())
    +			t.logger.WithDefaultLevel(log.Debug).Printf("smart banned %v for piece %v", peer, index)
    +		}
    +		t.dropBannedPeers()
    +		for ri := t.pieceRequestIndexOffset(index); ri < t.pieceRequestIndexOffset(index+1); ri++ {
    +			t.smartBanCache.ForgetBlock(ri)
    +		}
    +	}
    +	p.hashing = false
    +	t.pieceHashed(index, correct, copyErr)
    +	t.updatePiecePriority(index, "Torrent.pieceHasher")
    +	t.activePieceHashes--
    +	t.tryCreateMorePieceHashers()
    +}
    +
    +// Return the connections that touched a piece, and clear the entries while doing it.
    +func (t *Torrent) clearPieceTouchers(pi pieceIndex) {
    +	p := t.piece(pi)
    +	for c := range p.dirtiers {
    +		delete(c.peerTouchedPieces, pi)
    +		delete(p.dirtiers, c)
    +	}
    +}
    +
    +func (t *Torrent) peersAsSlice() (ret []*Peer) {
    +	t.iterPeers(func(p *Peer) {
    +		ret = append(ret, p)
    +	})
    +	return
    +}
    +
    +func (t *Torrent) queuePieceCheck(pieceIndex pieceIndex) {
    +	piece := t.piece(pieceIndex)
    +	if piece.queuedForHash() {
    +		return
    +	}
    +	t.piecesQueuedForHash.Add(bitmap.BitIndex(pieceIndex))
    +	t.publishPieceChange(pieceIndex)
    +	t.updatePiecePriority(pieceIndex, "Torrent.queuePieceCheck")
    +	t.tryCreateMorePieceHashers()
    +}
    +
    +// Forces all the pieces to be re-hashed. See also Piece.VerifyData. This should not be called
    +// before the Info is available.
    +func (t *Torrent) VerifyData() {
    +	for i := pieceIndex(0); i < t.NumPieces(); i++ {
    +		t.Piece(i).VerifyData()
    +	}
    +}
    +
    +func (t *Torrent) connectingToPeerAddr(addrStr string) bool {
    +	return len(t.halfOpen[addrStr]) != 0
    +}
    +
    +func (t *Torrent) hasPeerConnForAddr(x PeerRemoteAddr) bool {
    +	addrStr := x.String()
    +	for c := range t.conns {
    +		ra := c.RemoteAddr
    +		if ra.String() == addrStr {
    +			return true
    +		}
    +	}
    +	return false
    +}
    +
    +func (t *Torrent) getHalfOpenPath(
    +	addrStr string,
    +	attemptKey outgoingConnAttemptKey,
    +) nestedmaps.Path[*PeerInfo] {
    +	return nestedmaps.Next(nestedmaps.Next(nestedmaps.Begin(&t.halfOpen), addrStr), attemptKey)
    +}
    +
    +func (t *Torrent) addHalfOpen(addrStr string, attemptKey *PeerInfo) {
    +	path := t.getHalfOpenPath(addrStr, attemptKey)
    +	if path.Exists() {
    +		panic("should be unique")
    +	}
    +	path.Set(attemptKey)
    +	t.cl.numHalfOpen++
    +}
    +
    +// Start the process of connecting to the given peer for the given torrent if appropriate. I'm not
    +// sure all the PeerInfo fields are being used.
    +func initiateConn(
    +	opts outgoingConnOpts,
    +	ignoreLimits bool,
    +) {
    +	t := opts.t
    +	peer := opts.peerInfo
    +	if peer.Id == t.cl.peerID {
    +		return
    +	}
    +	if t.cl.badPeerAddr(peer.Addr) && !peer.Trusted {
    +		return
    +	}
    +	addr := peer.Addr
    +	addrStr := addr.String()
    +	if !ignoreLimits {
    +		if t.connectingToPeerAddr(addrStr) {
    +			return
    +		}
    +	}
    +	if t.hasPeerConnForAddr(addr) {
    +		return
    +	}
    +	attemptKey := &peer
    +	t.addHalfOpen(addrStr, attemptKey)
    +	go t.cl.outgoingConnection(
    +		opts,
    +		attemptKey,
    +	)
    +}
    +
    +// Adds a trusted, pending peer for each of the given Client's addresses. Typically used in tests to
    +// quickly make one Client visible to the Torrent of another Client.
    +func (t *Torrent) AddClientPeer(cl *Client) int {
    +	return t.AddPeers(func() (ps []PeerInfo) {
    +		for _, la := range cl.ListenAddrs() {
    +			ps = append(ps, PeerInfo{
    +				Addr:    la,
    +				Trusted: true,
    +			})
    +		}
    +		return
    +	}())
    +}
    +
    +// All stats that include this Torrent. Useful when we want to increment ConnStats but not for every
    +// connection.
    +func (t *Torrent) allStats(f func(*ConnStats)) {
    +	f(&t.stats)
    +	f(&t.cl.connStats)
    +}
    +
    +func (t *Torrent) hashingPiece(i pieceIndex) bool {
    +	return t.pieces[i].hashing
    +}
    +
    +func (t *Torrent) pieceQueuedForHash(i pieceIndex) bool {
    +	return t.piecesQueuedForHash.Get(bitmap.BitIndex(i))
    +}
    +
    +func (t *Torrent) dialTimeout() time.Duration {
    +	return reducedDialTimeout(t.cl.config.MinDialTimeout, t.cl.config.NominalDialTimeout, t.cl.config.HalfOpenConnsPerTorrent, t.peers.Len())
    +}
    +
    +func (t *Torrent) piece(i int) *Piece {
    +	return &t.pieces[i]
    +}
    +
    +func (t *Torrent) onWriteChunkErr(err error) {
    +	if t.userOnWriteChunkErr != nil {
    +		go t.userOnWriteChunkErr(err)
    +		return
    +	}
    +	t.logger.WithDefaultLevel(log.Critical).Printf("default chunk write error handler: disabling data download")
    +	t.disallowDataDownloadLocked()
    +}
    +
    +func (t *Torrent) DisallowDataDownload() {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	t.disallowDataDownloadLocked()
    +}
    +
    +func (t *Torrent) disallowDataDownloadLocked() {
    +	t.dataDownloadDisallowed.Set()
    +	t.iterPeers(func(p *Peer) {
    +		// Could check if peer request state is empty/not interested?
    +		p.updateRequests("disallow data download")
    +		p.cancelAllRequests()
    +	})
    +}
    +
    +func (t *Torrent) AllowDataDownload() {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	t.dataDownloadDisallowed.Clear()
    +	t.iterPeers(func(p *Peer) {
    +		p.updateRequests("allow data download")
    +	})
    +}
    +
    +// Enables uploading data, if it was disabled.
    +func (t *Torrent) AllowDataUpload() {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	t.dataUploadDisallowed = false
    +	t.iterPeers(func(p *Peer) {
    +		p.updateRequests("allow data upload")
    +	})
    +}
    +
    +// Disables uploading data, if it was enabled.
    +func (t *Torrent) DisallowDataUpload() {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	t.dataUploadDisallowed = true
    +	for c := range t.conns {
    +		// TODO: This doesn't look right. Shouldn't we tickle writers to choke peers or something instead?
    +		c.updateRequests("disallow data upload")
    +	}
    +}
    +
    +// Sets a handler that is called if there's an error writing a chunk to local storage. By default,
    +// or if nil, a critical message is logged, and data download is disabled.
    +func (t *Torrent) SetOnWriteChunkError(f func(error)) {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	t.userOnWriteChunkErr = f
    +}
    +
    +func (t *Torrent) iterPeers(f func(p *Peer)) {
    +	for pc := range t.conns {
    +		f(&pc.Peer)
    +	}
    +	for _, ws := range t.webSeeds {
    +		f(ws)
    +	}
    +}
    +
    +func (t *Torrent) callbacks() *Callbacks {
    +	return &t.cl.config.Callbacks
    +}
    +
    +type AddWebSeedsOpt func(*webseed.Client)
    +
    +// Sets the WebSeed trailing path escaper for a webseed.Client.
    +func WebSeedPathEscaper(custom webseed.PathEscaper) AddWebSeedsOpt {
    +	return func(c *webseed.Client) {
    +		c.PathEscaper = custom
    +	}
    +}
    +
    +func (t *Torrent) AddWebSeeds(urls []string, opts ...AddWebSeedsOpt) {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	for _, u := range urls {
    +		t.addWebSeed(u, opts...)
    +	}
    +}
    +
    +func (t *Torrent) addWebSeed(url string, opts ...AddWebSeedsOpt) {
    +	if t.cl.config.DisableWebseeds {
    +		return
    +	}
    +	if _, ok := t.webSeeds[url]; ok {
    +		return
    +	}
    +	// I don't think Go http supports pipelining requests. However, we can have more ready to go
    +	// right away. This value should be some multiple of the number of connections to a host. I
    +	// would expect that double maxRequests plus a bit would be appropriate. This value is based on
    +	// downloading Sintel (08ada5a7a6183aae1e09d831df6748d566095a10) from
    +	// "https://webtorrent.io/torrents/".
    +	const maxRequests = 16
    +	ws := webseedPeer{
    +		peer: Peer{
    +			t:                        t,
    +			outgoing:                 true,
    +			Network:                  "http",
    +			reconciledHandshakeStats: true,
    +			// This should affect how often we have to recompute requests for this peer. Note that
    +			// because we can request more than 1 thing at a time over HTTP, we will hit the low
    +			// requests mark more often, so recomputation is probably sooner than with regular peer
    +			// conns. ~4x maxRequests would be about right.
    +			PeerMaxRequests: 128,
    +			// TODO: Set ban prefix?
    +			RemoteAddr: remoteAddrFromUrl(url),
    +			callbacks:  t.callbacks(),
    +		},
    +		client: webseed.Client{
    +			HttpClient: t.cl.httpClient,
    +			Url:        url,
    +			ResponseBodyWrapper: func(r io.Reader) io.Reader {
    +				return &rateLimitedReader{
    +					l: t.cl.config.DownloadRateLimiter,
    +					r: r,
    +				}
    +			},
    +		},
    +		activeRequests: make(map[Request]webseed.Request, maxRequests),
    +	}
    +	ws.peer.initRequestState()
    +	for _, opt := range opts {
    +		opt(&ws.client)
    +	}
    +	ws.peer.initUpdateRequestsTimer()
    +	ws.requesterCond.L = t.cl.locker()
    +	for i := 0; i < maxRequests; i += 1 {
    +		go ws.requester(i)
    +	}
    +	for _, f := range t.callbacks().NewPeer {
    +		f(&ws.peer)
    +	}
    +	ws.peer.logger = t.logger.WithContextValue(&ws)
    +	ws.peer.peerImpl = &ws
    +	if t.haveInfo() {
    +		ws.onGotInfo(t.info)
    +	}
    +	t.webSeeds[url] = &ws.peer
    +}
    +
    +func (t *Torrent) peerIsActive(p *Peer) (active bool) {
    +	t.iterPeers(func(p1 *Peer) {
    +		if p1 == p {
    +			active = true
    +		}
    +	})
    +	return
    +}
    +
    +func (t *Torrent) requestIndexToRequest(ri RequestIndex) Request {
    +	index := t.pieceIndexOfRequestIndex(ri)
    +	return Request{
    +		pp.Integer(index),
    +		t.piece(index).chunkIndexSpec(ri % t.chunksPerRegularPiece()),
    +	}
    +}
    +
    +func (t *Torrent) requestIndexFromRequest(r Request) RequestIndex {
    +	return t.pieceRequestIndexOffset(pieceIndex(r.Index)) + RequestIndex(r.Begin/t.chunkSize)
    +}
    +
    +func (t *Torrent) pieceRequestIndexOffset(piece pieceIndex) RequestIndex {
    +	return RequestIndex(piece) * t.chunksPerRegularPiece()
    +}
    +
    +func (t *Torrent) updateComplete() {
    +	t.Complete.SetBool(t.haveAllPieces())
    +}
    +
    +func (t *Torrent) cancelRequest(r RequestIndex) *Peer {
    +	p := t.requestingPeer(r)
    +	if p != nil {
    +		p.cancel(r)
    +	}
    +	// TODO: This is a check that an old invariant holds. It can be removed after some testing.
    +	//delete(t.pendingRequests, r)
    +	if _, ok := t.requestState[r]; ok {
    +		panic("expected request state to be gone")
    +	}
    +	return p
    +}
    +
    +func (t *Torrent) requestingPeer(r RequestIndex) *Peer {
    +	return t.requestState[r].peer
    +}
    +
    +func (t *Torrent) addConnWithAllPieces(p *Peer) {
    +	if t.connsWithAllPieces == nil {
    +		t.connsWithAllPieces = make(map[*Peer]struct{}, t.maxEstablishedConns)
    +	}
    +	t.connsWithAllPieces[p] = struct{}{}
    +}
    +
    +func (t *Torrent) deleteConnWithAllPieces(p *Peer) bool {
    +	_, ok := t.connsWithAllPieces[p]
    +	delete(t.connsWithAllPieces, p)
    +	return ok
    +}
    +
    +func (t *Torrent) numActivePeers() int {
    +	return len(t.conns) + len(t.webSeeds)
    +}
    +
    +func (t *Torrent) hasStorageCap() bool {
    +	f := t.storage.Capacity
    +	if f == nil {
    +		return false
    +	}
    +	_, ok := (*f)()
    +	return ok
    +}
    +
    +func (t *Torrent) pieceIndexOfRequestIndex(ri RequestIndex) pieceIndex {
    +	return pieceIndex(ri / t.chunksPerRegularPiece())
    +}
    +
    +func (t *Torrent) iterUndirtiedRequestIndexesInPiece(
    +	reuseIter *typedRoaring.Iterator[RequestIndex],
    +	piece pieceIndex,
    +	f func(RequestIndex),
    +) {
    +	reuseIter.Initialize(&t.dirtyChunks)
    +	pieceRequestIndexOffset := t.pieceRequestIndexOffset(piece)
    +	iterBitmapUnsetInRange(
    +		reuseIter,
    +		pieceRequestIndexOffset, pieceRequestIndexOffset+t.pieceNumChunks(piece),
    +		f,
    +	)
    +}
    +
    +type requestState struct {
    +	peer *Peer
    +	when time.Time
    +}
    +
    +// Returns an error if a received chunk is out of bounds in someway.
    +func (t *Torrent) checkValidReceiveChunk(r Request) error {
    +	if !t.haveInfo() {
    +		return errors.New("torrent missing info")
    +	}
    +	if int(r.Index) >= t.numPieces() {
    +		return fmt.Errorf("chunk index %v, torrent num pieces %v", r.Index, t.numPieces())
    +	}
    +	pieceLength := t.pieceLength(pieceIndex(r.Index))
    +	if r.Begin >= pieceLength {
    +		return fmt.Errorf("chunk begins beyond end of piece (%v >= %v)", r.Begin, pieceLength)
    +	}
    +	// We could check chunk lengths here, but chunk request size is not changed often, and tricky
    +	// for peers to manipulate as they need to send potentially large buffers to begin with. There
    +	// should be considerable checks elsewhere for this case due to the network overhead. We should
    +	// catch most of the overflow manipulation stuff by checking index and begin above.
    +	return nil
    +}
    +
    +func (t *Torrent) peerConnsWithDialAddrPort(target netip.AddrPort) (ret []*PeerConn) {
    +	for pc := range t.conns {
    +		dialAddr, err := pc.remoteDialAddrPort()
    +		if err != nil {
    +			continue
    +		}
    +		if dialAddr != target {
    +			continue
    +		}
    +		ret = append(ret, pc)
    +	}
    +	return
    +}
    +
    +func wrapUtHolepunchMsgForPeerConn(
    +	recipient *PeerConn,
    +	msg utHolepunch.Msg,
    +) pp.Message {
    +	extendedPayload, err := msg.MarshalBinary()
    +	if err != nil {
    +		panic(err)
    +	}
    +	return pp.Message{
    +		Type:            pp.Extended,
    +		ExtendedID:      MapMustGet(recipient.PeerExtensionIDs, utHolepunch.ExtensionName),
    +		ExtendedPayload: extendedPayload,
    +	}
    +}
    +
    +func sendUtHolepunchMsg(
    +	pc *PeerConn,
    +	msgType utHolepunch.MsgType,
    +	addrPort netip.AddrPort,
    +	errCode utHolepunch.ErrCode,
    +) {
    +	holepunchMsg := utHolepunch.Msg{
    +		MsgType:  msgType,
    +		AddrPort: addrPort,
    +		ErrCode:  errCode,
    +	}
    +	incHolepunchMessagesSent(holepunchMsg)
    +	ppMsg := wrapUtHolepunchMsgForPeerConn(pc, holepunchMsg)
    +	pc.write(ppMsg)
    +}
    +
    +func incHolepunchMessages(msg utHolepunch.Msg, verb string) {
    +	torrent.Add(
    +		fmt.Sprintf(
    +			"holepunch %v %v messages %v",
    +			msg.MsgType,
    +			addrPortProtocolStr(msg.AddrPort),
    +			verb,
    +		),
    +		1,
    +	)
    +}
    +
    +func incHolepunchMessagesReceived(msg utHolepunch.Msg) {
    +	incHolepunchMessages(msg, "received")
    +}
    +
    +func incHolepunchMessagesSent(msg utHolepunch.Msg) {
    +	incHolepunchMessages(msg, "sent")
    +}
    +
    +func (t *Torrent) handleReceivedUtHolepunchMsg(msg utHolepunch.Msg, sender *PeerConn) error {
    +	incHolepunchMessagesReceived(msg)
    +	switch msg.MsgType {
    +	case utHolepunch.Rendezvous:
    +		t.logger.Printf("got holepunch rendezvous request for %v from %p", msg.AddrPort, sender)
    +		sendMsg := sendUtHolepunchMsg
    +		senderAddrPort, err := sender.remoteDialAddrPort()
    +		if err != nil {
    +			sender.logger.Levelf(
    +				log.Warning,
    +				"error getting ut_holepunch rendezvous sender's dial address: %v",
    +				err,
    +			)
    +			// There's no better error code. The sender's address itself is invalid. I don't see
    +			// this error message being appropriate anywhere else anyway.
    +			sendMsg(sender, utHolepunch.Error, msg.AddrPort, utHolepunch.NoSuchPeer)
    +		}
    +		targets := t.peerConnsWithDialAddrPort(msg.AddrPort)
    +		if len(targets) == 0 {
    +			sendMsg(sender, utHolepunch.Error, msg.AddrPort, utHolepunch.NotConnected)
    +			return nil
    +		}
    +		for _, pc := range targets {
    +			if !pc.supportsExtension(utHolepunch.ExtensionName) {
    +				sendMsg(sender, utHolepunch.Error, msg.AddrPort, utHolepunch.NoSupport)
    +				continue
    +			}
    +			sendMsg(sender, utHolepunch.Connect, msg.AddrPort, 0)
    +			sendMsg(pc, utHolepunch.Connect, senderAddrPort, 0)
    +		}
    +		return nil
    +	case utHolepunch.Connect:
    +		holepunchAddr := msg.AddrPort
    +		t.logger.Printf("got holepunch connect request for %v from %p", holepunchAddr, sender)
    +		if g.MapContains(t.cl.undialableWithoutHolepunch, holepunchAddr) {
    +			setAdd(&t.cl.undialableWithoutHolepunchDialedAfterHolepunchConnect, holepunchAddr)
    +			if g.MapContains(t.cl.accepted, holepunchAddr) {
    +				setAdd(&t.cl.probablyOnlyConnectedDueToHolepunch, holepunchAddr)
    +			}
    +		}
    +		opts := outgoingConnOpts{
    +			peerInfo: PeerInfo{
    +				Addr:         msg.AddrPort,
    +				Source:       PeerSourceUtHolepunch,
    +				PexPeerFlags: sender.pex.remoteLiveConns[msg.AddrPort].UnwrapOrZeroValue(),
    +			},
    +			t: t,
    +			// Don't attempt to start our own rendezvous if we fail to connect.
    +			skipHolepunchRendezvous:  true,
    +			receivedHolepunchConnect: true,
    +			// Assume that the other end initiated the rendezvous, and will use our preferred
    +			// encryption. So we will act normally.
    +			HeaderObfuscationPolicy: t.cl.config.HeaderObfuscationPolicy,
    +		}
    +		initiateConn(opts, true)
    +		return nil
    +	case utHolepunch.Error:
    +		torrent.Add("holepunch error messages received", 1)
    +		t.logger.Levelf(log.Debug, "received ut_holepunch error message from %v: %v", sender, msg.ErrCode)
    +		return nil
    +	default:
    +		return fmt.Errorf("unhandled msg type %v", msg.MsgType)
    +	}
    +}
    +
    +func addrPortProtocolStr(addrPort netip.AddrPort) string {
    +	addr := addrPort.Addr()
    +	switch {
    +	case addr.Is4():
    +		return "ipv4"
    +	case addr.Is6():
    +		return "ipv6"
    +	default:
    +		panic(addrPort)
    +	}
    +}
    +
    +func (t *Torrent) trySendHolepunchRendezvous(addrPort netip.AddrPort) error {
    +	rzsSent := 0
    +	for pc := range t.conns {
    +		if !pc.supportsExtension(utHolepunch.ExtensionName) {
    +			continue
    +		}
    +		if pc.supportsExtension(pp.ExtensionNamePex) {
    +			if !g.MapContains(pc.pex.remoteLiveConns, addrPort) {
    +				continue
    +			}
    +		}
    +		t.logger.Levelf(log.Debug, "sent ut_holepunch rendezvous message to %v for %v", pc, addrPort)
    +		sendUtHolepunchMsg(pc, utHolepunch.Rendezvous, addrPort, 0)
    +		rzsSent++
    +	}
    +	if rzsSent == 0 {
    +		return errors.New("no eligible relays")
    +	}
    +	return nil
    +}
    +
    +func (t *Torrent) numHalfOpenAttempts() (num int) {
    +	for _, attempts := range t.halfOpen {
    +		num += len(attempts)
    +	}
    +	return
    +}
    +
    +func (t *Torrent) getDialTimeoutUnlocked() time.Duration {
    +	cl := t.cl
    +	cl.rLock()
    +	defer cl.rUnlock()
    +	return t.dialTimeout()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/torrent_mmap_test.go b/deps/github.com/anacrolix/torrent/torrent_mmap_test.go
    new file mode 100644
    index 0000000..8114309
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/torrent_mmap_test.go
    @@ -0,0 +1,18 @@
    +//go:build !wasm
    +// +build !wasm
    +
    +package torrent
    +
    +import (
    +	"testing"
    +
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +func TestEmptyFilesAndZeroPieceLengthWithMMapStorage(t *testing.T) {
    +	cfg := TestingConfig(t)
    +	ci := storage.NewMMap(cfg.DataDir)
    +	defer ci.Close()
    +	cfg.DefaultStorage = ci
    +	testEmptyFilesAndZeroPieceLength(t, cfg)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/torrent_test.go b/deps/github.com/anacrolix/torrent/torrent_test.go
    new file mode 100644
    index 0000000..808947e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/torrent_test.go
    @@ -0,0 +1,253 @@
    +package torrent
    +
    +import (
    +	"fmt"
    +	"io"
    +	"net"
    +	"os"
    +	"path/filepath"
    +	"sync"
    +	"testing"
    +
    +	g "github.com/anacrolix/generics"
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/v2"
    +	"github.com/anacrolix/missinggo/v2/bitmap"
    +	qt "github.com/frankban/quicktest"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/internal/testutil"
    +	"github.com/anacrolix/torrent/metainfo"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +	"github.com/anacrolix/torrent/storage"
    +)
    +
    +func r(i, b, l pp.Integer) Request {
    +	return Request{i, ChunkSpec{b, l}}
    +}
    +
    +// Check the given request is correct for various torrent offsets.
    +func TestTorrentRequest(t *testing.T) {
    +	const s = 472183431 // Length of torrent.
    +	for _, _case := range []struct {
    +		off int64   // An offset into the torrent.
    +		req Request // The expected request. The zero value means !ok.
    +	}{
    +		// Invalid offset.
    +		{-1, Request{}},
    +		{0, r(0, 0, 16384)},
    +		// One before the end of a piece.
    +		{1<<18 - 1, r(0, 1<<18-16384, 16384)},
    +		// Offset beyond torrent length.
    +		{472 * 1 << 20, Request{}},
    +		// One before the end of the torrent. Complicates the chunk length.
    +		{s - 1, r((s-1)/(1<<18), (s-1)%(1<<18)/(16384)*(16384), 12935)},
    +		{1, r(0, 0, 16384)},
    +		// One before end of chunk.
    +		{16383, r(0, 0, 16384)},
    +		// Second chunk.
    +		{16384, r(0, 16384, 16384)},
    +	} {
    +		req, ok := torrentOffsetRequest(472183431, 1<<18, 16384, _case.off)
    +		if (_case.req == Request{}) == ok {
    +			t.Fatalf("expected %v, got %v", _case.req, req)
    +		}
    +		if req != _case.req {
    +			t.Fatalf("expected %v, got %v", _case.req, req)
    +		}
    +	}
    +}
    +
    +func TestAppendToCopySlice(t *testing.T) {
    +	orig := []int{1, 2, 3}
    +	dupe := append([]int{}, orig...)
    +	dupe[0] = 4
    +	if orig[0] != 1 {
    +		t.FailNow()
    +	}
    +}
    +
    +func TestTorrentString(t *testing.T) {
    +	tor := &Torrent{}
    +	s := tor.InfoHash().HexString()
    +	if s != "0000000000000000000000000000000000000000" {
    +		t.FailNow()
    +	}
    +}
    +
    +// This benchmark is from the observation that a lot of overlapping Readers on
    +// a large torrent with small pieces had a lot of overhead in recalculating
    +// piece priorities everytime a reader (possibly in another Torrent) changed.
    +func BenchmarkUpdatePiecePriorities(b *testing.B) {
    +	const (
    +		numPieces   = 13410
    +		pieceLength = 256 << 10
    +	)
    +	cl := &Client{config: TestingConfig(b)}
    +	cl.initLogger()
    +	t := cl.newTorrent(metainfo.Hash{}, nil)
    +	require.NoError(b, t.setInfo(&metainfo.Info{
    +		Pieces:      make([]byte, metainfo.HashSize*numPieces),
    +		PieceLength: pieceLength,
    +		Length:      pieceLength * numPieces,
    +	}))
    +	t.onSetInfo()
    +	assert.EqualValues(b, 13410, t.numPieces())
    +	for i := 0; i < 7; i += 1 {
    +		r := t.NewReader()
    +		r.SetReadahead(32 << 20)
    +		r.Seek(3500000, io.SeekStart)
    +	}
    +	assert.Len(b, t.readers, 7)
    +	for i := 0; i < t.numPieces(); i += 3 {
    +		t._completedPieces.Add(bitmap.BitIndex(i))
    +	}
    +	t.DownloadPieces(0, t.numPieces())
    +	for i := 0; i < b.N; i += 1 {
    +		t.updateAllPiecePriorities("")
    +	}
    +}
    +
    +// Check that a torrent containing zero-length file(s) will start, and that
    +// they're created in the filesystem. The client storage is assumed to be
    +// file-based on the native filesystem based.
    +func testEmptyFilesAndZeroPieceLength(t *testing.T, cfg *ClientConfig) {
    +	cl, err := NewClient(cfg)
    +	require.NoError(t, err)
    +	defer cl.Close()
    +	ib, err := bencode.Marshal(metainfo.Info{
    +		Name:        "empty",
    +		Length:      0,
    +		PieceLength: 0,
    +	})
    +	require.NoError(t, err)
    +	fp := filepath.Join(cfg.DataDir, "empty")
    +	os.Remove(fp)
    +	assert.False(t, missinggo.FilePathExists(fp))
    +	tt, err := cl.AddTorrent(&metainfo.MetaInfo{
    +		InfoBytes: ib,
    +	})
    +	require.NoError(t, err)
    +	defer tt.Drop()
    +	tt.DownloadAll()
    +	require.True(t, cl.WaitAll())
    +	assert.True(t, tt.Complete.Bool())
    +	assert.True(t, missinggo.FilePathExists(fp))
    +}
    +
    +func TestEmptyFilesAndZeroPieceLengthWithFileStorage(t *testing.T) {
    +	cfg := TestingConfig(t)
    +	ci := storage.NewFile(cfg.DataDir)
    +	defer ci.Close()
    +	cfg.DefaultStorage = ci
    +	testEmptyFilesAndZeroPieceLength(t, cfg)
    +}
    +
    +func TestPieceHashFailed(t *testing.T) {
    +	mi := testutil.GreetingMetaInfo()
    +	cl := newTestingClient(t)
    +	tt := cl.newTorrent(mi.HashInfoBytes(), badStorage{})
    +	tt.setChunkSize(2)
    +	require.NoError(t, tt.setInfoBytesLocked(mi.InfoBytes))
    +	tt.cl.lock()
    +	tt.dirtyChunks.AddRange(
    +		uint64(tt.pieceRequestIndexOffset(1)),
    +		uint64(tt.pieceRequestIndexOffset(1)+3))
    +	require.True(t, tt.pieceAllDirty(1))
    +	tt.pieceHashed(1, false, nil)
    +	// Dirty chunks should be cleared so we can try again.
    +	require.False(t, tt.pieceAllDirty(1))
    +	tt.cl.unlock()
    +}
    +
    +// Check the behaviour of Torrent.Metainfo when metadata is not completed.
    +func TestTorrentMetainfoIncompleteMetadata(t *testing.T) {
    +	cfg := TestingConfig(t)
    +	cfg.Debug = true
    +	// Disable this just because we manually initiate a connection without it.
    +	cfg.MinPeerExtensions.SetBit(pp.ExtensionBitFast, false)
    +	cl, err := NewClient(cfg)
    +	require.NoError(t, err)
    +	defer cl.Close()
    +
    +	mi := testutil.GreetingMetaInfo()
    +	ih := mi.HashInfoBytes()
    +
    +	tt, _ := cl.AddTorrentInfoHash(ih)
    +	assert.Nil(t, tt.Metainfo().InfoBytes)
    +	assert.False(t, tt.haveAllMetadataPieces())
    +
    +	nc, err := net.Dial("tcp", fmt.Sprintf(":%d", cl.LocalPort()))
    +	require.NoError(t, err)
    +	defer nc.Close()
    +
    +	var pex PeerExtensionBits
    +	pex.SetBit(pp.ExtensionBitLtep, true)
    +	hr, err := pp.Handshake(nc, &ih, [20]byte{}, pex)
    +	require.NoError(t, err)
    +	assert.True(t, hr.PeerExtensionBits.GetBit(pp.ExtensionBitLtep))
    +	assert.EqualValues(t, cl.PeerID(), hr.PeerID)
    +	assert.EqualValues(t, ih, hr.Hash)
    +
    +	assert.EqualValues(t, 0, tt.metadataSize())
    +
    +	func() {
    +		cl.lock()
    +		defer cl.unlock()
    +		go func() {
    +			_, err = nc.Write(pp.Message{
    +				Type:       pp.Extended,
    +				ExtendedID: pp.HandshakeExtendedID,
    +				ExtendedPayload: func() []byte {
    +					d := map[string]interface{}{
    +						"metadata_size": len(mi.InfoBytes),
    +					}
    +					b, err := bencode.Marshal(d)
    +					if err != nil {
    +						panic(err)
    +					}
    +					return b
    +				}(),
    +			}.MustMarshalBinary())
    +			require.NoError(t, err)
    +		}()
    +		tt.metadataChanged.Wait()
    +	}()
    +	assert.Equal(t, make([]byte, len(mi.InfoBytes)), tt.metadataBytes)
    +	assert.False(t, tt.haveAllMetadataPieces())
    +	assert.Nil(t, tt.Metainfo().InfoBytes)
    +}
    +
    +func TestRelativeAvailabilityHaveNone(t *testing.T) {
    +	c := qt.New(t)
    +	var err error
    +	cl := Client{
    +		config: TestingConfig(t),
    +	}
    +	tt := Torrent{
    +		cl:           &cl,
    +		logger:       log.Default,
    +		gotMetainfoC: make(chan struct{}),
    +	}
    +	tt.setChunkSize(2)
    +	g.MakeMapIfNil(&tt.conns)
    +	pc := PeerConn{}
    +	pc.t = &tt
    +	pc.peerImpl = &pc
    +	pc.initRequestState()
    +	g.InitNew(&pc.callbacks)
    +	tt.conns[&pc] = struct{}{}
    +	err = pc.peerSentHave(0)
    +	c.Assert(err, qt.IsNil)
    +	info := testutil.Greeting.Info(5)
    +	err = tt.setInfo(&info)
    +	c.Assert(err, qt.IsNil)
    +	tt.onSetInfo()
    +	err = pc.peerSentHaveNone()
    +	c.Assert(err, qt.IsNil)
    +	var wg sync.WaitGroup
    +	tt.close(&wg)
    +	tt.assertAllPiecesRelativeAvailabilityZero()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/client.go b/deps/github.com/anacrolix/torrent/tracker/client.go
    new file mode 100644
    index 0000000..3b7e2ab
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/client.go
    @@ -0,0 +1,60 @@
    +package tracker
    +
    +import (
    +	"context"
    +	"net"
    +	"net/url"
    +
    +	"github.com/anacrolix/log"
    +
    +	trHttp "github.com/anacrolix/torrent/tracker/http"
    +	"github.com/anacrolix/torrent/tracker/udp"
    +	"github.com/anacrolix/torrent/types/infohash"
    +)
    +
    +type Client interface {
    +	Announce(context.Context, AnnounceRequest, AnnounceOpt) (AnnounceResponse, error)
    +	Scrape(ctx context.Context, ihs []infohash.T) (out udp.ScrapeResponse, err error)
    +	Close() error
    +}
    +
    +type AnnounceOpt = trHttp.AnnounceOpt
    +
    +type NewClientOpts struct {
    +	Http trHttp.NewClientOpts
    +	// Overrides the network in the scheme. Probably a legacy thing.
    +	UdpNetwork   string
    +	Logger       log.Logger
    +	ListenPacket func(network, addr string) (net.PacketConn, error)
    +}
    +
    +func NewClient(urlStr string, opts NewClientOpts) (Client, error) {
    +	_url, err := url.Parse(urlStr)
    +	if err != nil {
    +		return nil, err
    +	}
    +	switch _url.Scheme {
    +	case "http", "https":
    +		return trHttp.NewClient(_url, opts.Http), nil
    +	case "udp", "udp4", "udp6":
    +		network := _url.Scheme
    +		if opts.UdpNetwork != "" {
    +			network = opts.UdpNetwork
    +		}
    +		cc, err := udp.NewConnClient(udp.NewConnClientOpts{
    +			Network:      network,
    +			Host:         _url.Host,
    +			Logger:       opts.Logger,
    +			ListenPacket: opts.ListenPacket,
    +		})
    +		if err != nil {
    +			return nil, err
    +		}
    +		return &udpClient{
    +			cl:         cc,
    +			requestUri: _url.RequestURI(),
    +		}, nil
    +	default:
    +		return nil, ErrBadScheme
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/http/client.go b/deps/github.com/anacrolix/torrent/tracker/http/client.go
    new file mode 100644
    index 0000000..c6b06fc
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/http/client.go
    @@ -0,0 +1,49 @@
    +package httpTracker
    +
    +import (
    +	"context"
    +	"crypto/tls"
    +	"net"
    +	"net/http"
    +	"net/url"
    +)
    +
    +type Client struct {
    +	hc   *http.Client
    +	url_ *url.URL
    +}
    +
    +type (
    +	ProxyFunc       func(*http.Request) (*url.URL, error)
    +	DialContextFunc func(ctx context.Context, network, addr string) (net.Conn, error)
    +)
    +
    +type NewClientOpts struct {
    +	Proxy          ProxyFunc
    +	DialContext    DialContextFunc
    +	ServerName     string
    +	AllowKeepAlive bool
    +}
    +
    +func NewClient(url_ *url.URL, opts NewClientOpts) Client {
    +	return Client{
    +		url_: url_,
    +		hc: &http.Client{
    +			Transport: &http.Transport{
    +				DialContext: opts.DialContext,
    +				Proxy:       opts.Proxy,
    +				TLSClientConfig: &tls.Config{
    +					InsecureSkipVerify: true,
    +					ServerName:         opts.ServerName,
    +				},
    +				// This is for S3 trackers that hold connections open.
    +				DisableKeepAlives: !opts.AllowKeepAlive,
    +			},
    +		},
    +	}
    +}
    +
    +func (cl Client) Close() error {
    +	cl.hc.CloseIdleConnections()
    +	return nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/http/http.go b/deps/github.com/anacrolix/torrent/tracker/http/http.go
    new file mode 100644
    index 0000000..a7022a5
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/http/http.go
    @@ -0,0 +1,157 @@
    +package httpTracker
    +
    +import (
    +	"bytes"
    +	"context"
    +	"expvar"
    +	"fmt"
    +	"io"
    +	"math"
    +	"net"
    +	"net/http"
    +	"net/url"
    +	"strconv"
    +	"strings"
    +
    +	"github.com/anacrolix/missinggo/httptoo"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/tracker/shared"
    +	"github.com/anacrolix/torrent/tracker/udp"
    +	"github.com/anacrolix/torrent/version"
    +)
    +
    +var vars = expvar.NewMap("tracker/http")
    +
    +func setAnnounceParams(_url *url.URL, ar *AnnounceRequest, opts AnnounceOpt) {
    +	q := url.Values{}
    +
    +	q.Set("key", strconv.FormatInt(int64(ar.Key), 10))
    +	q.Set("info_hash", string(ar.InfoHash[:]))
    +	q.Set("peer_id", string(ar.PeerId[:]))
    +	// AFAICT, port is mandatory, and there's no implied port key.
    +	q.Set("port", fmt.Sprintf("%d", ar.Port))
    +	q.Set("uploaded", strconv.FormatInt(ar.Uploaded, 10))
    +	q.Set("downloaded", strconv.FormatInt(ar.Downloaded, 10))
    +
    +	// The AWS S3 tracker returns "400 Bad Request: left(-1) was not in the valid range 0 -
    +	// 9223372036854775807" if left is out of range, or "500 Internal Server Error: Internal Server
    +	// Error" if omitted entirely.
    +	left := ar.Left
    +	if left < 0 {
    +		left = math.MaxInt64
    +	}
    +	q.Set("left", strconv.FormatInt(left, 10))
    +
    +	if ar.Event != shared.None {
    +		q.Set("event", ar.Event.String())
    +	}
    +	// http://stackoverflow.com/questions/17418004/why-does-tracker-server-not-understand-my-request-bittorrent-protocol
    +	q.Set("compact", "1")
    +	// According to https://wiki.vuze.com/w/Message_Stream_Encryption. TODO:
    +	// Take EncryptionPolicy or something like it as a parameter.
    +	q.Set("supportcrypto", "1")
    +	doIp := func(versionKey string, ip net.IP) {
    +		if ip == nil {
    +			return
    +		}
    +		ipString := ip.String()
    +		q.Set(versionKey, ipString)
    +		// Let's try listing them. BEP 3 mentions having an "ip" param, and BEP 7 says we can list
    +		// addresses for other address-families, although it's not encouraged.
    +		q.Add("ip", ipString)
    +	}
    +	doIp("ipv4", opts.ClientIp4)
    +	doIp("ipv6", opts.ClientIp6)
    +	// We're operating purely on query-escaped strings, where + would have already been encoded to
    +	// %2B, and + has no other special meaning. See https://github.com/anacrolix/torrent/issues/534.
    +	qstr := strings.ReplaceAll(q.Encode(), "+", "%20")
    +
    +	// Some private trackers require the original query param to be in the first position.
    +	if _url.RawQuery != "" {
    +		_url.RawQuery += "&" + qstr
    +	} else {
    +		_url.RawQuery = qstr
    +	}
    +}
    +
    +type AnnounceOpt struct {
    +	UserAgent           string
    +	HostHeader          string
    +	ClientIp4           net.IP
    +	ClientIp6           net.IP
    +	HttpRequestDirector func(*http.Request) error
    +}
    +
    +type AnnounceRequest = udp.AnnounceRequest
    +
    +func (cl Client) Announce(ctx context.Context, ar AnnounceRequest, opt AnnounceOpt) (ret AnnounceResponse, err error) {
    +	_url := httptoo.CopyURL(cl.url_)
    +	setAnnounceParams(_url, &ar, opt)
    +	req, err := http.NewRequestWithContext(ctx, http.MethodGet, _url.String(), nil)
    +	userAgent := opt.UserAgent
    +	if userAgent == "" {
    +		userAgent = version.DefaultHttpUserAgent
    +	}
    +	if userAgent != "" {
    +		req.Header.Set("User-Agent", userAgent)
    +	}
    +
    +	if opt.HttpRequestDirector != nil {
    +		err = opt.HttpRequestDirector(req)
    +		if err != nil {
    +			err = fmt.Errorf("error modifying HTTP request: %w", err)
    +			return
    +		}
    +	}
    +
    +	req.Host = opt.HostHeader
    +	resp, err := cl.hc.Do(req)
    +	if err != nil {
    +		return
    +	}
    +	defer resp.Body.Close()
    +	var buf bytes.Buffer
    +	io.Copy(&buf, resp.Body)
    +	if resp.StatusCode != 200 {
    +		err = fmt.Errorf("response from tracker: %s: %q", resp.Status, buf.Bytes())
    +		return
    +	}
    +	var trackerResponse HttpResponse
    +	err = bencode.Unmarshal(buf.Bytes(), &trackerResponse)
    +	if _, ok := err.(bencode.ErrUnusedTrailingBytes); ok {
    +		err = nil
    +	} else if err != nil {
    +		err = fmt.Errorf("error decoding %q: %s", buf.Bytes(), err)
    +		return
    +	}
    +	if trackerResponse.FailureReason != "" {
    +		err = fmt.Errorf("tracker gave failure reason: %q", trackerResponse.FailureReason)
    +		return
    +	}
    +	vars.Add("successful http announces", 1)
    +	ret.Interval = trackerResponse.Interval
    +	ret.Leechers = trackerResponse.Incomplete
    +	ret.Seeders = trackerResponse.Complete
    +	if len(trackerResponse.Peers.List) != 0 {
    +		vars.Add("http responses with nonempty peers key", 1)
    +	}
    +	ret.Peers = trackerResponse.Peers.List
    +	if len(trackerResponse.Peers6) != 0 {
    +		vars.Add("http responses with nonempty peers6 key", 1)
    +	}
    +	for _, na := range trackerResponse.Peers6 {
    +		ret.Peers = append(ret.Peers, Peer{
    +			IP:   na.IP,
    +			Port: na.Port,
    +		})
    +	}
    +	return
    +}
    +
    +type AnnounceResponse struct {
    +	Interval int32 // Minimum seconds the local peer should wait before next announce.
    +	Leechers int32
    +	Seeders  int32
    +	Peers    []Peer
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/http/http_test.go b/deps/github.com/anacrolix/torrent/tracker/http/http_test.go
    new file mode 100644
    index 0000000..4e5efaf
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/http/http_test.go
    @@ -0,0 +1,76 @@
    +package httpTracker
    +
    +import (
    +	"net/url"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/tracker/udp"
    +)
    +
    +func TestUnmarshalHTTPResponsePeerDicts(t *testing.T) {
    +	var hr HttpResponse
    +	require.NoError(t, bencode.Unmarshal(
    +		[]byte("d5:peersl"+
    +			"d2:ip7:1.2.3.47:peer id20:thisisthe20bytepeeri4:porti9999ee"+
    +			"d2:ip39:2001:0db8:85a3:0000:0000:8a2e:0370:73347:peer id20:thisisthe20bytepeeri4:porti9998ee"+
    +			"e"+
    +			"6:peers618:123412341234123456"+
    +			"e"),
    +		&hr))
    +
    +	require.Len(t, hr.Peers.List, 2)
    +	assert.Equal(t, []byte("thisisthe20bytepeeri"), hr.Peers.List[0].ID)
    +	assert.EqualValues(t, 9999, hr.Peers.List[0].Port)
    +	assert.EqualValues(t, 9998, hr.Peers.List[1].Port)
    +	assert.NotNil(t, hr.Peers.List[0].IP)
    +	assert.NotNil(t, hr.Peers.List[1].IP)
    +
    +	assert.Len(t, hr.Peers6, 1)
    +	assert.EqualValues(t, "1234123412341234", hr.Peers6[0].IP)
    +	assert.EqualValues(t, 0x3536, hr.Peers6[0].Port)
    +}
    +
    +func TestUnmarshalHttpResponseNoPeers(t *testing.T) {
    +	var hr HttpResponse
    +	require.NoError(t, bencode.Unmarshal(
    +		[]byte("d6:peers618:123412341234123456e"),
    +		&hr,
    +	))
    +	require.Len(t, hr.Peers.List, 0)
    +	assert.Len(t, hr.Peers6, 1)
    +}
    +
    +func TestUnmarshalHttpResponsePeers6NotCompact(t *testing.T) {
    +	var hr HttpResponse
    +	require.Error(t, bencode.Unmarshal(
    +		[]byte("d6:peers6lee"),
    +		&hr,
    +	))
    +}
    +
    +// Checks that infohash bytes that correspond to spaces are escaped with %20 instead of +. See
    +// https://github.com/anacrolix/torrent/issues/534
    +func TestSetAnnounceInfohashParamWithSpaces(t *testing.T) {
    +	someUrl := &url.URL{}
    +	ihBytes := [20]uint8{
    +		0x2b, 0x76, 0xa, 0xa1, 0x78, 0x93, 0x20, 0x30, 0xc8, 0x47,
    +		0xdc, 0xdf, 0x8e, 0xae, 0xbf, 0x56, 0xa, 0x1b, 0xd1, 0x6c,
    +	}
    +	setAnnounceParams(
    +		someUrl,
    +		&udp.AnnounceRequest{
    +			InfoHash: ihBytes,
    +		},
    +		AnnounceOpt{})
    +	t.Logf("%q", someUrl)
    +	qt.Assert(t, someUrl.Query().Get("info_hash"), qt.Equals, string(ihBytes[:]))
    +	qt.Check(t,
    +		someUrl.String(),
    +		qt.Contains,
    +		"info_hash=%2Bv%0A%A1x%93%200%C8G%DC%DF%8E%AE%BFV%0A%1B%D1l")
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/http/peer.go b/deps/github.com/anacrolix/torrent/tracker/http/peer.go
    new file mode 100644
    index 0000000..b0deee0
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/http/peer.go
    @@ -0,0 +1,46 @@
    +package httpTracker
    +
    +import (
    +	"fmt"
    +	"net"
    +	"net/netip"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +)
    +
    +// TODO: Use netip.Addr and Option[[20]byte].
    +type Peer struct {
    +	IP   net.IP `bencode:"ip"`
    +	Port int    `bencode:"port"`
    +	ID   []byte `bencode:"peer id"`
    +}
    +
    +func (p Peer) ToNetipAddrPort() (addrPort netip.AddrPort, ok bool) {
    +	addr, ok := netip.AddrFromSlice(p.IP)
    +	addrPort = netip.AddrPortFrom(addr, uint16(p.Port))
    +	return
    +}
    +
    +func (p Peer) String() string {
    +	loc := net.JoinHostPort(p.IP.String(), fmt.Sprintf("%d", p.Port))
    +	if len(p.ID) != 0 {
    +		return fmt.Sprintf("%x at %s", p.ID, loc)
    +	} else {
    +		return loc
    +	}
    +}
    +
    +// Set from the non-compact form in BEP 3.
    +func (p *Peer) FromDictInterface(d map[string]interface{}) {
    +	p.IP = net.ParseIP(d["ip"].(string))
    +	if _, ok := d["peer id"]; ok {
    +		p.ID = []byte(d["peer id"].(string))
    +	}
    +	p.Port = int(d["port"].(int64))
    +}
    +
    +func (p Peer) FromNodeAddr(na krpc.NodeAddr) Peer {
    +	p.IP = na.IP
    +	p.Port = na.Port
    +	return p
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/http/protocol.go b/deps/github.com/anacrolix/torrent/tracker/http/protocol.go
    new file mode 100644
    index 0000000..11a90b2
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/http/protocol.go
    @@ -0,0 +1,83 @@
    +package httpTracker
    +
    +import (
    +	"fmt"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +)
    +
    +type HttpResponse struct {
    +	FailureReason string `bencode:"failure reason"`
    +	Interval      int32  `bencode:"interval"`
    +	TrackerId     string `bencode:"tracker id"`
    +	Complete      int32  `bencode:"complete"`
    +	Incomplete    int32  `bencode:"incomplete"`
    +	Peers         Peers  `bencode:"peers"`
    +	// BEP 7
    +	Peers6 krpc.CompactIPv6NodeAddrs `bencode:"peers6"`
    +}
    +
    +type Peers struct {
    +	List    []Peer
    +	Compact bool
    +}
    +
    +func (me Peers) MarshalBencode() ([]byte, error) {
    +	if me.Compact {
    +		cnas := make([]krpc.NodeAddr, 0, len(me.List))
    +		for _, peer := range me.List {
    +			cnas = append(cnas, krpc.NodeAddr{
    +				IP:   peer.IP,
    +				Port: peer.Port,
    +			})
    +		}
    +		return krpc.CompactIPv4NodeAddrs(cnas).MarshalBencode()
    +	} else {
    +		return bencode.Marshal(me.List)
    +	}
    +}
    +
    +var (
    +	_ bencode.Unmarshaler = (*Peers)(nil)
    +	_ bencode.Marshaler   = Peers{}
    +)
    +
    +func (me *Peers) UnmarshalBencode(b []byte) (err error) {
    +	var _v interface{}
    +	err = bencode.Unmarshal(b, &_v)
    +	if err != nil {
    +		return
    +	}
    +	switch v := _v.(type) {
    +	case string:
    +		vars.Add("http responses with string peers", 1)
    +		var cnas krpc.CompactIPv4NodeAddrs
    +		err = cnas.UnmarshalBinary([]byte(v))
    +		if err != nil {
    +			return
    +		}
    +		me.Compact = true
    +		for _, cp := range cnas {
    +			me.List = append(me.List, Peer{
    +				IP:   cp.IP[:],
    +				Port: cp.Port,
    +			})
    +		}
    +		return
    +	case []interface{}:
    +		vars.Add("http responses with list peers", 1)
    +		me.Compact = false
    +		for _, i := range v {
    +			var p Peer
    +			p.FromDictInterface(i.(map[string]interface{}))
    +			me.List = append(me.List, p)
    +		}
    +		return
    +	default:
    +		vars.Add("http responses with unhandled peers type", 1)
    +		err = fmt.Errorf("unsupported type: %T", _v)
    +		return
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/http/scrape.go b/deps/github.com/anacrolix/torrent/tracker/http/scrape.go
    new file mode 100644
    index 0000000..6940370
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/http/scrape.go
    @@ -0,0 +1,47 @@
    +package httpTracker
    +
    +import (
    +	"context"
    +	"log"
    +	"net/http"
    +	"net/url"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/tracker/udp"
    +	"github.com/anacrolix/torrent/types/infohash"
    +)
    +
    +type scrapeResponse struct {
    +	Files files `bencode:"files"`
    +}
    +
    +// Bencode should support bencode.Unmarshalers from a string in the dict key position.
    +type files = map[string]udp.ScrapeInfohashResult
    +
    +func (cl Client) Scrape(ctx context.Context, ihs []infohash.T) (out udp.ScrapeResponse, err error) {
    +	_url := cl.url_.JoinPath("..", "scrape")
    +	query, err := url.ParseQuery(_url.RawQuery)
    +	if err != nil {
    +		return
    +	}
    +	for _, ih := range ihs {
    +		query.Add("info_hash", ih.AsString())
    +	}
    +	_url.RawQuery = query.Encode()
    +	log.Printf("%q", _url.String())
    +	req, err := http.NewRequestWithContext(ctx, http.MethodGet, _url.String(), nil)
    +	if err != nil {
    +		return
    +	}
    +	resp, err := cl.hc.Do(req)
    +	if err != nil {
    +		return
    +	}
    +	defer resp.Body.Close()
    +	var decodedResp scrapeResponse
    +	err = bencode.NewDecoder(resp.Body).Decode(&decodedResp)
    +	for _, ih := range ihs {
    +		out = append(out, decodedResp.Files[ih.AsString()])
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/http/server/server.go b/deps/github.com/anacrolix/torrent/tracker/http/server/server.go
    new file mode 100644
    index 0000000..541ef3f
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/http/server/server.go
    @@ -0,0 +1,125 @@
    +package httpTrackerServer
    +
    +import (
    +	"fmt"
    +	"net"
    +	"net/http"
    +	"net/netip"
    +	"net/url"
    +	"strconv"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	"github.com/anacrolix/generics"
    +	"github.com/anacrolix/log"
    +
    +	"github.com/anacrolix/torrent/bencode"
    +	"github.com/anacrolix/torrent/tracker"
    +	httpTracker "github.com/anacrolix/torrent/tracker/http"
    +	trackerServer "github.com/anacrolix/torrent/tracker/server"
    +)
    +
    +type Handler struct {
    +	Announce *trackerServer.AnnounceHandler
    +	// Called to derive an announcer's IP if non-nil. If not specified, the Request.RemoteAddr is
    +	// used. Necessary for instances running behind reverse proxies for example.
    +	RequestHost func(r *http.Request) (netip.Addr, error)
    +}
    +
    +func unmarshalQueryKeyToArray(w http.ResponseWriter, key string, query url.Values) (ret [20]byte, ok bool) {
    +	str := query.Get(key)
    +	if len(str) != len(ret) {
    +		http.Error(w, fmt.Sprintf("%v has wrong length", key), http.StatusBadRequest)
    +		return
    +	}
    +	copy(ret[:], str)
    +	ok = true
    +	return
    +}
    +
    +// Returns false if there was an error and it was served.
    +func (me Handler) requestHostAddr(r *http.Request) (_ netip.Addr, err error) {
    +	if me.RequestHost != nil {
    +		return me.RequestHost(r)
    +	}
    +	host, _, err := net.SplitHostPort(r.RemoteAddr)
    +	if err != nil {
    +		return
    +	}
    +	return netip.ParseAddr(host)
    +}
    +
    +var requestHeadersLogger = log.Default.WithNames("request", "headers")
    +
    +func (me Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    +	vs := r.URL.Query()
    +	var event tracker.AnnounceEvent
    +	err := event.UnmarshalText([]byte(vs.Get("event")))
    +	if err != nil {
    +		http.Error(w, err.Error(), http.StatusBadRequest)
    +		return
    +	}
    +	infoHash, ok := unmarshalQueryKeyToArray(w, "info_hash", vs)
    +	if !ok {
    +		return
    +	}
    +	peerId, ok := unmarshalQueryKeyToArray(w, "peer_id", vs)
    +	if !ok {
    +		return
    +	}
    +	requestHeadersLogger.Levelf(log.Debug, "request RemoteAddr=%q, header=%q", r.RemoteAddr, r.Header)
    +	addr, err := me.requestHostAddr(r)
    +	if err != nil {
    +		log.Printf("error getting requester IP: %v", err)
    +		http.Error(w, "error determining your IP", http.StatusBadGateway)
    +		return
    +	}
    +	portU64, _ := strconv.ParseUint(vs.Get("port"), 0, 16)
    +	addrPort := netip.AddrPortFrom(addr, uint16(portU64))
    +	left, err := strconv.ParseInt(vs.Get("left"), 0, 64)
    +	if err != nil {
    +		left = -1
    +	}
    +	res := me.Announce.Serve(
    +		r.Context(),
    +		tracker.AnnounceRequest{
    +			InfoHash: infoHash,
    +			PeerId:   peerId,
    +			Event:    event,
    +			Port:     addrPort.Port(),
    +			NumWant:  -1,
    +			Left:     left,
    +		},
    +		addrPort,
    +		trackerServer.GetPeersOpts{
    +			MaxCount: generics.Some[uint](200),
    +		},
    +	)
    +	err = res.Err
    +	if err != nil {
    +		log.Printf("error serving announce: %v", err)
    +		http.Error(w, "error handling announce", http.StatusInternalServerError)
    +		return
    +	}
    +	var resp httpTracker.HttpResponse
    +	resp.Incomplete = res.Leechers.Value
    +	resp.Complete = res.Seeders.Value
    +	resp.Interval = res.Interval.UnwrapOr(5 * 60)
    +	resp.Peers.Compact = true
    +	for _, peer := range res.Peers {
    +		if peer.Addr().Is4() {
    +			resp.Peers.List = append(resp.Peers.List, tracker.Peer{
    +				IP:   peer.Addr().AsSlice(),
    +				Port: int(peer.Port()),
    +			})
    +		} else if peer.Addr().Is6() {
    +			resp.Peers6 = append(resp.Peers6, krpc.NodeAddr{
    +				IP:   peer.Addr().AsSlice(),
    +				Port: int(peer.Port()),
    +			})
    +		}
    +	}
    +	err = bencode.NewEncoder(w).Encode(resp)
    +	if err != nil {
    +		log.Printf("error encoding and writing response body: %v", err)
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/server/server.go b/deps/github.com/anacrolix/torrent/tracker/server/server.go
    new file mode 100644
    index 0000000..bedc027
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/server/server.go
    @@ -0,0 +1,324 @@
    +package trackerServer
    +
    +import (
    +	"context"
    +	"encoding/hex"
    +	"fmt"
    +	"net/netip"
    +	"sync"
    +	"time"
    +
    +	"github.com/anacrolix/generics"
    +	"github.com/anacrolix/log"
    +	"go.opentelemetry.io/otel"
    +	"go.opentelemetry.io/otel/attribute"
    +	"go.opentelemetry.io/otel/codes"
    +	"go.opentelemetry.io/otel/trace"
    +
    +	"github.com/anacrolix/torrent/tracker"
    +	"github.com/anacrolix/torrent/tracker/udp"
    +)
    +
    +// This is reserved for stuff like filtering by IP version, avoiding an announcer's IP or key,
    +// limiting return count, etc.
    +type GetPeersOpts struct {
    +	// Negative numbers are not allowed.
    +	MaxCount generics.Option[uint]
    +}
    +
    +type InfoHash = [20]byte
    +
    +type PeerInfo struct {
    +	AnnounceAddr
    +}
    +
    +type AnnounceAddr = netip.AddrPort
    +
    +type AnnounceTracker interface {
    +	TrackAnnounce(ctx context.Context, req udp.AnnounceRequest, addr AnnounceAddr) error
    +	Scrape(ctx context.Context, infoHashes []InfoHash) ([]udp.ScrapeInfohashResult, error)
    +	GetPeers(
    +		ctx context.Context,
    +		infoHash InfoHash,
    +		opts GetPeersOpts,
    +		remote AnnounceAddr,
    +	) ServerAnnounceResult
    +}
    +
    +type ServerAnnounceResult struct {
    +	Err      error
    +	Peers    []PeerInfo
    +	Interval generics.Option[int32]
    +	Leechers generics.Option[int32]
    +	Seeders  generics.Option[int32]
    +}
    +
    +type AnnounceHandler struct {
    +	AnnounceTracker AnnounceTracker
    +
    +	UpstreamTrackers       []Client
    +	UpstreamTrackerUrls    []string
    +	UpstreamAnnouncePeerId [20]byte
    +	UpstreamAnnounceGate   UpstreamAnnounceGater
    +
    +	mu sync.Mutex
    +	// Operations are only removed when all the upstream peers have been tracked.
    +	ongoingUpstreamAugmentations map[InfoHash]augmentationOperation
    +}
    +
    +type peerSet = map[PeerInfo]struct{}
    +
    +type augmentationOperation struct {
    +	// Closed when no more announce responses are pending. finalPeers will contain all the peers
    +	// seen.
    +	doneAnnouncing chan struct{}
    +	// This receives the latest peerSet until doneAnnouncing is closed.
    +	curPeers chan peerSet
    +	// This contains the final peerSet after doneAnnouncing is closed.
    +	finalPeers peerSet
    +}
    +
    +func (me augmentationOperation) getCurPeers() (ret peerSet) {
    +	ret, _ = me.getCurPeersAndDone()
    +	return
    +}
    +
    +func (me augmentationOperation) getCurPeersAndDone() (ret peerSet, done bool) {
    +	select {
    +	case ret = <-me.curPeers:
    +	case <-me.doneAnnouncing:
    +		ret = copyPeerSet(me.finalPeers)
    +		done = true
    +	}
    +	return
    +}
    +
    +// Adds peers from new that aren't in orig. Modifies both arguments.
    +func addMissing(orig []PeerInfo, new peerSet) {
    +	for _, peer := range orig {
    +		delete(new, peer)
    +	}
    +	for peer := range new {
    +		orig = append(orig, peer)
    +	}
    +}
    +
    +var tracer = otel.Tracer("torrent.tracker.udp")
    +
    +func (me *AnnounceHandler) Serve(
    +	ctx context.Context, req AnnounceRequest, addr AnnounceAddr, opts GetPeersOpts,
    +) (ret ServerAnnounceResult) {
    +	ctx, span := tracer.Start(
    +		ctx,
    +		"AnnounceHandler.Serve",
    +		trace.WithAttributes(
    +			attribute.Int64("announce.request.num_want", int64(req.NumWant)),
    +			attribute.Int("announce.request.port", int(req.Port)),
    +			attribute.String("announce.request.info_hash", hex.EncodeToString(req.InfoHash[:])),
    +			attribute.String("announce.request.event", req.Event.String()),
    +			attribute.Int64("announce.get_peers.opts.max_count_value", int64(opts.MaxCount.Value)),
    +			attribute.Bool("announce.get_peers.opts.max_count_ok", opts.MaxCount.Ok),
    +			attribute.String("announce.source.addr.ip", addr.Addr().String()),
    +			attribute.Int("announce.source.addr.port", int(addr.Port())),
    +		),
    +	)
    +	defer span.End()
    +	defer func() {
    +		span.SetAttributes(attribute.Int("announce.get_peers.len", len(ret.Peers)))
    +		if ret.Err != nil {
    +			span.SetStatus(codes.Error, ret.Err.Error())
    +		}
    +	}()
    +
    +	if req.Port != 0 {
    +		addr = netip.AddrPortFrom(addr.Addr(), req.Port)
    +	}
    +	ret.Err = me.AnnounceTracker.TrackAnnounce(ctx, req, addr)
    +	if ret.Err != nil {
    +		ret.Err = fmt.Errorf("tracking announce: %w", ret.Err)
    +		return
    +	}
    +	infoHash := req.InfoHash
    +	var op generics.Option[augmentationOperation]
    +	// Grab a handle to any augmentations that are already running.
    +	me.mu.Lock()
    +	op.Value, op.Ok = me.ongoingUpstreamAugmentations[infoHash]
    +	me.mu.Unlock()
    +	// Apply num_want limit to max count. I really can't tell if this is the right place to do it,
    +	// but it seems the most flexible.
    +	if req.NumWant != -1 {
    +		newCount := uint(req.NumWant)
    +		if opts.MaxCount.Ok {
    +			if newCount < opts.MaxCount.Value {
    +				opts.MaxCount.Value = newCount
    +			}
    +		} else {
    +			opts.MaxCount = generics.Some(newCount)
    +		}
    +	}
    +	ret = me.AnnounceTracker.GetPeers(ctx, infoHash, opts, addr)
    +	if ret.Err != nil {
    +		return
    +	}
    +	// Take whatever peers it has ready. If it's finished, it doesn't matter if we do this inside
    +	// the mutex or not.
    +	if op.Ok {
    +		curPeers, done := op.Value.getCurPeersAndDone()
    +		addMissing(ret.Peers, curPeers)
    +		if done {
    +			// It doesn't get any better with this operation. Forget it.
    +			op.Ok = false
    +		}
    +	}
    +	me.mu.Lock()
    +	// If we didn't have an operation, and don't have enough peers, start one. Allowing 1 is
    +	// assuming the announcing peer might be that one. Really we should record a value to prevent
    +	// duplicate announces. Also don't announce upstream if we got no peers because the caller asked
    +	// for none.
    +	if !op.Ok && len(ret.Peers) <= 1 && opts.MaxCount.UnwrapOr(1) > 0 {
    +		op.Value, op.Ok = me.ongoingUpstreamAugmentations[infoHash]
    +		if !op.Ok {
    +			op.Set(me.augmentPeersFromUpstream(req.InfoHash))
    +			generics.MakeMapIfNilAndSet(&me.ongoingUpstreamAugmentations, infoHash, op.Value)
    +		}
    +	}
    +	me.mu.Unlock()
    +	// Wait a while for the current operation.
    +	if op.Ok {
    +		// Force the augmentation to return with whatever it has if it hasn't completed in a
    +		// reasonable time.
    +		ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    +		select {
    +		case <-ctx.Done():
    +		case <-op.Value.doneAnnouncing:
    +		}
    +		cancel()
    +		addMissing(ret.Peers, op.Value.getCurPeers())
    +	}
    +	return
    +}
    +
    +func (me *AnnounceHandler) augmentPeersFromUpstream(infoHash [20]byte) augmentationOperation {
    +	const announceTimeout = time.Minute
    +	announceCtx, cancel := context.WithTimeout(context.Background(), announceTimeout)
    +	subReq := AnnounceRequest{
    +		InfoHash: infoHash,
    +		PeerId:   me.UpstreamAnnouncePeerId,
    +		Event:    tracker.None,
    +		Key:      0,
    +		NumWant:  -1,
    +		Port:     0,
    +	}
    +	peersChan := make(chan []Peer)
    +	var pendingUpstreams sync.WaitGroup
    +	for i := range me.UpstreamTrackers {
    +		client := me.UpstreamTrackers[i]
    +		url := me.UpstreamTrackerUrls[i]
    +		pendingUpstreams.Add(1)
    +		go func() {
    +			started, err := me.UpstreamAnnounceGate.Start(announceCtx, url, infoHash, announceTimeout)
    +			if err != nil {
    +				log.Printf("error reserving announce for %x to %v: %v", infoHash, url, err)
    +			}
    +			if err != nil || !started {
    +				peersChan <- nil
    +				return
    +			}
    +			log.Printf("announcing %x upstream to %v", infoHash, url)
    +			resp, err := client.Announce(announceCtx, subReq, tracker.AnnounceOpt{
    +				UserAgent: "aragorn",
    +			})
    +			interval := resp.Interval
    +			go func() {
    +				if interval < 5*60 {
    +					// This is as much to reduce load on upstream trackers in the event of errors,
    +					// as it is to reduce load on our peer store.
    +					interval = 5 * 60
    +				}
    +				err := me.UpstreamAnnounceGate.Completed(context.Background(), url, infoHash, interval)
    +				if err != nil {
    +					log.Printf("error recording completed announce for %x to %v: %v", infoHash, url, err)
    +				}
    +			}()
    +			peersChan <- resp.Peers
    +			if err != nil {
    +				log.Levelf(log.Warning, "error announcing to upstream %q: %v", url, err)
    +			}
    +		}()
    +	}
    +	peersToTrack := make(map[string]Peer)
    +	go func() {
    +		pendingUpstreams.Wait()
    +		cancel()
    +		close(peersChan)
    +		log.Levelf(log.Debug, "adding %v distinct peers from upstream trackers", len(peersToTrack))
    +		for _, peer := range peersToTrack {
    +			addrPort, ok := peer.ToNetipAddrPort()
    +			if !ok {
    +				continue
    +			}
    +			trackReq := AnnounceRequest{
    +				InfoHash: infoHash,
    +				Event:    tracker.Started,
    +				Port:     uint16(peer.Port),
    +				// Let's assume upstream peers are leechers without knowing better.
    +				Left: -1,
    +			}
    +			copy(trackReq.PeerId[:], peer.ID)
    +			// TODO: How do we know if these peers are leechers or seeders?
    +			err := me.AnnounceTracker.TrackAnnounce(context.TODO(), trackReq, addrPort)
    +			if err != nil {
    +				log.Levelf(log.Error, "error tracking upstream peer: %v", err)
    +			}
    +		}
    +		me.mu.Lock()
    +		delete(me.ongoingUpstreamAugmentations, infoHash)
    +		me.mu.Unlock()
    +	}()
    +	curPeersChan := make(chan map[PeerInfo]struct{})
    +	doneChan := make(chan struct{})
    +	retPeers := make(map[PeerInfo]struct{})
    +	go func() {
    +		defer close(doneChan)
    +		for {
    +			select {
    +			case peers, ok := <-peersChan:
    +				if !ok {
    +					return
    +				}
    +				voldemort(peers, peersToTrack, retPeers)
    +				pendingUpstreams.Done()
    +			case curPeersChan <- copyPeerSet(retPeers):
    +			}
    +		}
    +	}()
    +	// Take return references.
    +	return augmentationOperation{
    +		curPeers:       curPeersChan,
    +		finalPeers:     retPeers,
    +		doneAnnouncing: doneChan,
    +	}
    +}
    +
    +func copyPeerSet(orig peerSet) (ret peerSet) {
    +	ret = make(peerSet, len(orig))
    +	for k, v := range orig {
    +		ret[k] = v
    +	}
    +	return
    +}
    +
    +// Adds peers to trailing containers.
    +func voldemort(peers []Peer, toTrack map[string]Peer, sets ...map[PeerInfo]struct{}) {
    +	for _, protoPeer := range peers {
    +		toTrack[protoPeer.String()] = protoPeer
    +		addr, ok := netip.AddrFromSlice(protoPeer.IP)
    +		if !ok {
    +			continue
    +		}
    +		handlerPeer := PeerInfo{netip.AddrPortFrom(addr, uint16(protoPeer.Port))}
    +		for _, set := range sets {
    +			set[handlerPeer] = struct{}{}
    +		}
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/server/upstream-announcing.go b/deps/github.com/anacrolix/torrent/tracker/server/upstream-announcing.go
    new file mode 100644
    index 0000000..cfbf61c
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/server/upstream-announcing.go
    @@ -0,0 +1,18 @@
    +package trackerServer
    +
    +import (
    +	"context"
    +	"time"
    +)
    +
    +type UpstreamAnnounceGater interface {
    +	Start(ctx context.Context, tracker string, infoHash InfoHash,
    +		// How long the announce block remains before discarding it.
    +		timeout time.Duration,
    +	) (bool, error)
    +	Completed(
    +		ctx context.Context, tracker string, infoHash InfoHash,
    +		// Num of seconds reported by tracker, or some suitable value the caller has chosen.
    +		interval int32,
    +	) error
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/server/use.go b/deps/github.com/anacrolix/torrent/tracker/server/use.go
    new file mode 100644
    index 0000000..942321c
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/server/use.go
    @@ -0,0 +1,9 @@
    +package trackerServer
    +
    +import "github.com/anacrolix/torrent/tracker"
    +
    +type (
    +	AnnounceRequest = tracker.AnnounceRequest
    +	Client          = tracker.Client
    +	Peer            = tracker.Peer
    +)
    diff --git a/deps/github.com/anacrolix/torrent/tracker/shared/shared.go b/deps/github.com/anacrolix/torrent/tracker/shared/shared.go
    new file mode 100644
    index 0000000..7859ea9
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/shared/shared.go
    @@ -0,0 +1,10 @@
    +package shared
    +
    +import "github.com/anacrolix/torrent/tracker/udp"
    +
    +const (
    +	None      udp.AnnounceEvent = iota
    +	Completed                   // The local peer just completed the torrent.
    +	Started                     // The local peer has just resumed this torrent.
    +	Stopped                     // The local peer is leaving the swarm.
    +)
    diff --git a/deps/github.com/anacrolix/torrent/tracker/tracker.go b/deps/github.com/anacrolix/torrent/tracker/tracker.go
    new file mode 100644
    index 0000000..f3bc9c5
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/tracker.go
    @@ -0,0 +1,89 @@
    +package tracker
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"net"
    +	"net/http"
    +	"net/url"
    +	"time"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	"github.com/anacrolix/log"
    +
    +	trHttp "github.com/anacrolix/torrent/tracker/http"
    +	"github.com/anacrolix/torrent/tracker/shared"
    +	"github.com/anacrolix/torrent/tracker/udp"
    +)
    +
    +const (
    +	None      = shared.None
    +	Started   = shared.Started
    +	Stopped   = shared.Stopped
    +	Completed = shared.Completed
    +)
    +
    +type AnnounceRequest = udp.AnnounceRequest
    +
    +type AnnounceResponse = trHttp.AnnounceResponse
    +
    +type Peer = trHttp.Peer
    +
    +type AnnounceEvent = udp.AnnounceEvent
    +
    +var ErrBadScheme = errors.New("unknown scheme")
    +
    +type Announce struct {
    +	TrackerUrl          string
    +	Request             AnnounceRequest
    +	HostHeader          string
    +	HttpProxy           func(*http.Request) (*url.URL, error)
    +	HttpRequestDirector func(*http.Request) error
    +	DialContext         func(ctx context.Context, network, addr string) (net.Conn, error)
    +	ListenPacket        func(network, addr string) (net.PacketConn, error)
    +	ServerName          string
    +	UserAgent           string
    +	UdpNetwork          string
    +	// If the port is zero, it's assumed to be the same as the Request.SmtpPort.
    +	ClientIp4 krpc.NodeAddr
    +	// If the port is zero, it's assumed to be the same as the Request.SmtpPort.
    +	ClientIp6 krpc.NodeAddr
    +	Context   context.Context
    +	Logger    log.Logger
    +}
    +
    +// The code *is* the documentation.
    +const DefaultTrackerAnnounceTimeout = 15 * time.Second
    +
    +func (me Announce) Do() (res AnnounceResponse, err error) {
    +	cl, err := NewClient(me.TrackerUrl, NewClientOpts{
    +		Http: trHttp.NewClientOpts{
    +			Proxy:       me.HttpProxy,
    +			DialContext: me.DialContext,
    +			ServerName:  me.ServerName,
    +		},
    +		UdpNetwork:   me.UdpNetwork,
    +		Logger:       me.Logger.WithContextValue(fmt.Sprintf("tracker client for %q", me.TrackerUrl)),
    +		ListenPacket: me.ListenPacket,
    +	})
    +	if err != nil {
    +		return
    +	}
    +	defer cl.Close()
    +	if me.Context == nil {
    +		// This is just to maintain the old behaviour that should be a timeout of 15s. Users can
    +		// override it by providing their own Context. See comments elsewhere about longer timeouts
    +		// acting as rate limiting overloaded trackers.
    +		ctx, cancel := context.WithTimeout(context.Background(), DefaultTrackerAnnounceTimeout)
    +		defer cancel()
    +		me.Context = ctx
    +	}
    +	return cl.Announce(me.Context, me.Request, trHttp.AnnounceOpt{
    +		UserAgent:           me.UserAgent,
    +		HostHeader:          me.HostHeader,
    +		ClientIp4:           me.ClientIp4.IP,
    +		ClientIp6:           me.ClientIp6.IP,
    +		HttpRequestDirector: me.HttpRequestDirector,
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/tracker_test.go b/deps/github.com/anacrolix/torrent/tracker/tracker_test.go
    new file mode 100644
    index 0000000..998248d
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/tracker_test.go
    @@ -0,0 +1,13 @@
    +package tracker
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestUnsupportedTrackerScheme(t *testing.T) {
    +	t.Parallel()
    +	_, err := Announce{TrackerUrl: "lol://tracker.openbittorrent.com:80/announce"}.Do()
    +	require.Equal(t, ErrBadScheme, err)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp-server_test.go b/deps/github.com/anacrolix/torrent/tracker/udp-server_test.go
    new file mode 100644
    index 0000000..84408bf
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp-server_test.go
    @@ -0,0 +1,126 @@
    +package tracker
    +
    +import (
    +	"bytes"
    +	"encoding"
    +	"encoding/binary"
    +	"fmt"
    +	"math/rand"
    +	"net"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	"github.com/anacrolix/missinggo/v2"
    +
    +	"github.com/anacrolix/torrent/tracker/udp"
    +)
    +
    +type torrent struct {
    +	Leechers int32
    +	Seeders  int32
    +	Peers    []krpc.NodeAddr
    +}
    +
    +type server struct {
    +	pc    net.PacketConn
    +	conns map[udp.ConnectionId]struct{}
    +	t     map[[20]byte]torrent
    +}
    +
    +func marshal(parts ...interface{}) (ret []byte, err error) {
    +	var buf bytes.Buffer
    +	for _, p := range parts {
    +		err = binary.Write(&buf, binary.BigEndian, p)
    +		if err != nil {
    +			return
    +		}
    +	}
    +	ret = buf.Bytes()
    +	return
    +}
    +
    +func (s *server) respond(addr net.Addr, rh udp.ResponseHeader, parts ...interface{}) (err error) {
    +	b, err := marshal(append([]interface{}{rh}, parts...)...)
    +	if err != nil {
    +		return
    +	}
    +	_, err = s.pc.WriteTo(b, addr)
    +	return
    +}
    +
    +func (s *server) newConn() (ret udp.ConnectionId) {
    +	ret = rand.Uint64()
    +	if s.conns == nil {
    +		s.conns = make(map[udp.ConnectionId]struct{})
    +	}
    +	s.conns[ret] = struct{}{}
    +	return
    +}
    +
    +func (s *server) serveOne() (err error) {
    +	b := make([]byte, 0x10000)
    +	n, addr, err := s.pc.ReadFrom(b)
    +	if err != nil {
    +		return
    +	}
    +	r := bytes.NewReader(b[:n])
    +	var h udp.RequestHeader
    +	err = udp.Read(r, &h)
    +	if err != nil {
    +		return
    +	}
    +	switch h.Action {
    +	case udp.ActionConnect:
    +		if h.ConnectionId != udp.ConnectRequestConnectionId {
    +			return
    +		}
    +		connId := s.newConn()
    +		err = s.respond(addr, udp.ResponseHeader{
    +			udp.ActionConnect,
    +			h.TransactionId,
    +		}, udp.ConnectionResponse{
    +			connId,
    +		})
    +		return
    +	case udp.ActionAnnounce:
    +		if _, ok := s.conns[h.ConnectionId]; !ok {
    +			s.respond(addr, udp.ResponseHeader{
    +				TransactionId: h.TransactionId,
    +				Action:        udp.ActionError,
    +			}, []byte("not connected"))
    +			return
    +		}
    +		var ar AnnounceRequest
    +		err = udp.Read(r, &ar)
    +		if err != nil {
    +			return
    +		}
    +		t := s.t[ar.InfoHash]
    +		bm := func() encoding.BinaryMarshaler {
    +			ip := missinggo.AddrIP(addr)
    +			if ip.To4() != nil {
    +				return krpc.CompactIPv4NodeAddrs(t.Peers)
    +			}
    +			return krpc.CompactIPv6NodeAddrs(t.Peers)
    +		}()
    +		b, err = bm.MarshalBinary()
    +		if err != nil {
    +			panic(err)
    +		}
    +		err = s.respond(addr, udp.ResponseHeader{
    +			TransactionId: h.TransactionId,
    +			Action:        udp.ActionAnnounce,
    +		}, udp.AnnounceResponseHeader{
    +			Interval: 900,
    +			Leechers: t.Leechers,
    +			Seeders:  t.Seeders,
    +		}, b)
    +		return
    +	default:
    +		err = fmt.Errorf("unhandled action: %d", h.Action)
    +		s.respond(addr, udp.ResponseHeader{
    +			TransactionId: h.TransactionId,
    +			Action:        udp.ActionError,
    +		}, []byte("unhandled action"))
    +		return
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp.go b/deps/github.com/anacrolix/torrent/tracker/udp.go
    new file mode 100644
    index 0000000..cf68188
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp.go
    @@ -0,0 +1,51 @@
    +package tracker
    +
    +import (
    +	"context"
    +	"encoding/binary"
    +
    +	"github.com/anacrolix/generics"
    +
    +	trHttp "github.com/anacrolix/torrent/tracker/http"
    +	"github.com/anacrolix/torrent/tracker/udp"
    +	"github.com/anacrolix/torrent/types/infohash"
    +)
    +
    +type udpClient struct {
    +	cl         *udp.ConnClient
    +	requestUri string
    +}
    +
    +func (c *udpClient) Scrape(ctx context.Context, ihs []infohash.T) (out udp.ScrapeResponse, err error) {
    +	return c.cl.Client.Scrape(
    +		ctx,
    +		generics.SliceMap(ihs, func(from infohash.T) udp.InfoHash {
    +			return from
    +		}),
    +	)
    +}
    +
    +func (c *udpClient) Close() error {
    +	return c.cl.Close()
    +}
    +
    +func (c *udpClient) Announce(ctx context.Context, req AnnounceRequest, opts trHttp.AnnounceOpt) (res AnnounceResponse, err error) {
    +	if req.IPAddress == 0 && opts.ClientIp4 != nil {
    +		// I think we're taking bytes in big-endian order (all IPs), and writing it to a natively
    +		// ordered uint32. This will be correctly ordered when written back out by the UDP client
    +		// later. I'm ignoring the fact that IPv6 announces shouldn't have an IP address, we have a
    +		// perfectly good IPv4 address.
    +		req.IPAddress = binary.BigEndian.Uint32(opts.ClientIp4.To4())
    +	}
    +	h, nas, err := c.cl.Announce(ctx, req, udp.Options{RequestUri: c.requestUri})
    +	if err != nil {
    +		return
    +	}
    +	res.Interval = h.Interval
    +	res.Leechers = h.Leechers
    +	res.Seeders = h.Seeders
    +	for _, cp := range nas.NodeAddrs() {
    +		res.Peers = append(res.Peers, trHttp.Peer{}.FromNodeAddr(cp))
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/addr-family.go b/deps/github.com/anacrolix/torrent/tracker/udp/addr-family.go
    new file mode 100644
    index 0000000..ddecb4c
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/addr-family.go
    @@ -0,0 +1,26 @@
    +package udp
    +
    +import (
    +	"encoding"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +)
    +
    +// Discriminates behaviours based on address family in use.
    +type AddrFamily int
    +
    +const (
    +	AddrFamilyIpv4 = iota + 1
    +	AddrFamilyIpv6
    +)
    +
    +// Returns a marshaler for the given node addrs for the specified family.
    +func GetNodeAddrsCompactMarshaler(nas []krpc.NodeAddr, family AddrFamily) encoding.BinaryMarshaler {
    +	switch family {
    +	case AddrFamilyIpv4:
    +		return krpc.CompactIPv4NodeAddrs(nas)
    +	case AddrFamilyIpv6:
    +		return krpc.CompactIPv6NodeAddrs(nas)
    +	}
    +	return nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/announce.go b/deps/github.com/anacrolix/torrent/tracker/udp/announce.go
    new file mode 100644
    index 0000000..b5c9f8f
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/announce.go
    @@ -0,0 +1,53 @@
    +package udp
    +
    +import (
    +	"encoding"
    +	"fmt"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +)
    +
    +// Marshalled as binary by the UDP client, so be careful making changes.
    +type AnnounceRequest struct {
    +	InfoHash   [20]byte
    +	PeerId     [20]byte
    +	Downloaded int64
    +	Left       int64 // If less than 0, math.MaxInt64 will be used for HTTP trackers instead.
    +	Uploaded   int64
    +	// Apparently this is optional. None can be used for announces done at
    +	// regular intervals.
    +	Event     AnnounceEvent
    +	IPAddress uint32
    +	Key       int32
    +	NumWant   int32 // How many peer addresses are desired. -1 for default.
    +	Port      uint16
    +} // 82 bytes
    +
    +type AnnounceEvent int32
    +
    +func (me *AnnounceEvent) UnmarshalText(text []byte) error {
    +	for key, str := range announceEventStrings {
    +		if string(text) == str {
    +			*me = AnnounceEvent(key)
    +			return nil
    +		}
    +	}
    +	return fmt.Errorf("unknown event")
    +}
    +
    +var announceEventStrings = []string{"", "completed", "started", "stopped"}
    +
    +func (e AnnounceEvent) String() string {
    +	// See BEP 3, "event", and
    +	// https://github.com/anacrolix/torrent/issues/416#issuecomment-751427001. Return a safe default
    +	// in case event values are not sanitized.
    +	if e < 0 || int(e) >= len(announceEventStrings) {
    +		return ""
    +	}
    +	return announceEventStrings[e]
    +}
    +
    +type AnnounceResponsePeers interface {
    +	encoding.BinaryUnmarshaler
    +	NodeAddrs() []krpc.NodeAddr
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/client.go b/deps/github.com/anacrolix/torrent/tracker/udp/client.go
    new file mode 100644
    index 0000000..6b97ddc
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/client.go
    @@ -0,0 +1,225 @@
    +package udp
    +
    +import (
    +	"bytes"
    +	"context"
    +	"encoding/binary"
    +	"fmt"
    +	"io"
    +	"net"
    +	"sync"
    +	"time"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +)
    +
    +// Client interacts with UDP trackers via its Writer and Dispatcher. It has no knowledge of
    +// connection specifics.
    +type Client struct {
    +	mu           sync.Mutex
    +	connId       ConnectionId
    +	connIdIssued time.Time
    +
    +	shouldReconnectOverride func() bool
    +
    +	Dispatcher *Dispatcher
    +	Writer     io.Writer
    +}
    +
    +func (cl *Client) Announce(
    +	ctx context.Context, req AnnounceRequest, opts Options,
    +	// Decides whether the response body is IPv6 or IPv4, see BEP 15.
    +	ipv6 func(net.Addr) bool,
    +) (
    +	respHdr AnnounceResponseHeader,
    +	// A slice of krpc.NodeAddr, likely wrapped in an appropriate unmarshalling wrapper.
    +	peers AnnounceResponsePeers,
    +	err error,
    +) {
    +	respBody, addr, err := cl.request(ctx, ActionAnnounce, append(mustMarshal(req), opts.Encode()...))
    +	if err != nil {
    +		return
    +	}
    +	r := bytes.NewBuffer(respBody)
    +	err = Read(r, &respHdr)
    +	if err != nil {
    +		err = fmt.Errorf("reading response header: %w", err)
    +		return
    +	}
    +	if ipv6(addr) {
    +		peers = &krpc.CompactIPv6NodeAddrs{}
    +	} else {
    +		peers = &krpc.CompactIPv4NodeAddrs{}
    +	}
    +	err = peers.UnmarshalBinary(r.Bytes())
    +	if err != nil {
    +		err = fmt.Errorf("reading response peers: %w", err)
    +	}
    +	return
    +}
    +
    +// There's no way to pass options in a scrape, since we don't when the request body ends.
    +func (cl *Client) Scrape(
    +	ctx context.Context, ihs []InfoHash,
    +) (
    +	out ScrapeResponse, err error,
    +) {
    +	respBody, _, err := cl.request(ctx, ActionScrape, mustMarshal(ScrapeRequest(ihs)))
    +	if err != nil {
    +		return
    +	}
    +	r := bytes.NewBuffer(respBody)
    +	for r.Len() != 0 {
    +		var item ScrapeInfohashResult
    +		err = Read(r, &item)
    +		if err != nil {
    +			return
    +		}
    +		out = append(out, item)
    +	}
    +	if len(out) > len(ihs) {
    +		err = fmt.Errorf("got %v results but expected %v", len(out), len(ihs))
    +		return
    +	}
    +	return
    +}
    +
    +func (cl *Client) shouldReconnectDefault() bool {
    +	return cl.connIdIssued.IsZero() || time.Since(cl.connIdIssued) >= time.Minute
    +}
    +
    +func (cl *Client) shouldReconnect() bool {
    +	if cl.shouldReconnectOverride != nil {
    +		return cl.shouldReconnectOverride()
    +	}
    +	return cl.shouldReconnectDefault()
    +}
    +
    +func (cl *Client) connect(ctx context.Context) (err error) {
    +	if !cl.shouldReconnect() {
    +		return nil
    +	}
    +	return cl.doConnectRoundTrip(ctx)
    +}
    +
    +// This just does the connect request and updates local state if it succeeds.
    +func (cl *Client) doConnectRoundTrip(ctx context.Context) (err error) {
    +	respBody, _, err := cl.request(ctx, ActionConnect, nil)
    +	if err != nil {
    +		return err
    +	}
    +	var connResp ConnectionResponse
    +	err = binary.Read(bytes.NewReader(respBody), binary.BigEndian, &connResp)
    +	if err != nil {
    +		return
    +	}
    +	cl.connId = connResp.ConnectionId
    +	cl.connIdIssued = time.Now()
    +	//log.Printf("conn id set to %x", cl.connId)
    +	return
    +}
    +
    +func (cl *Client) connIdForRequest(ctx context.Context, action Action) (id ConnectionId, err error) {
    +	if action == ActionConnect {
    +		id = ConnectRequestConnectionId
    +		return
    +	}
    +	err = cl.connect(ctx)
    +	if err != nil {
    +		return
    +	}
    +	id = cl.connId
    +	return
    +}
    +
    +func (cl *Client) writeRequest(
    +	ctx context.Context, action Action, body []byte, tId TransactionId, buf *bytes.Buffer,
    +) (
    +	err error,
    +) {
    +	var connId ConnectionId
    +	if action == ActionConnect {
    +		connId = ConnectRequestConnectionId
    +	} else {
    +		// We lock here while establishing a connection ID, and then ensuring that the request is
    +		// written before allowing the connection ID to change again. This is to ensure the server
    +		// doesn't assign us another ID before we've sent this request. Note that this doesn't allow
    +		// for us to return if the context is cancelled while we wait to obtain a new ID.
    +		cl.mu.Lock()
    +		defer cl.mu.Unlock()
    +		connId, err = cl.connIdForRequest(ctx, action)
    +		if err != nil {
    +			return
    +		}
    +	}
    +	buf.Reset()
    +	err = Write(buf, RequestHeader{
    +		ConnectionId:  connId,
    +		Action:        action,
    +		TransactionId: tId,
    +	})
    +	if err != nil {
    +		panic(err)
    +	}
    +	buf.Write(body)
    +	_, err = cl.Writer.Write(buf.Bytes())
    +	//log.Printf("sent request with conn id %x", connId)
    +	return
    +}
    +
    +func (cl *Client) requestWriter(ctx context.Context, action Action, body []byte, tId TransactionId) (err error) {
    +	var buf bytes.Buffer
    +	for n := 0; ; n++ {
    +		err = cl.writeRequest(ctx, action, body, tId, &buf)
    +		if err != nil {
    +			return
    +		}
    +		select {
    +		case <-ctx.Done():
    +			return ctx.Err()
    +		case <-time.After(timeout(n)):
    +		}
    +	}
    +}
    +
    +const ConnectionIdMissmatchNul = "Connection ID missmatch.\x00"
    +
    +type ErrorResponse struct {
    +	Message string
    +}
    +
    +func (me ErrorResponse) Error() string {
    +	return fmt.Sprintf("error response: %#q", me.Message)
    +}
    +
    +func (cl *Client) request(ctx context.Context, action Action, body []byte) (respBody []byte, addr net.Addr, err error) {
    +	respChan := make(chan DispatchedResponse, 1)
    +	t := cl.Dispatcher.NewTransaction(func(dr DispatchedResponse) {
    +		respChan <- dr
    +	})
    +	defer t.End()
    +	ctx, cancel := context.WithCancel(ctx)
    +	defer cancel()
    +	writeErr := make(chan error, 1)
    +	go func() {
    +		writeErr <- cl.requestWriter(ctx, action, body, t.Id())
    +	}()
    +	select {
    +	case dr := <-respChan:
    +		if dr.Header.Action == action {
    +			respBody = dr.Body
    +			addr = dr.Addr
    +		} else if dr.Header.Action == ActionError {
    +			// udp://tracker.torrent.eu.org:451/announce frequently returns "Connection ID
    +			// missmatch.\x00"
    +			err = ErrorResponse{Message: string(dr.Body)}
    +		} else {
    +			err = fmt.Errorf("unexpected response action %v", dr.Header.Action)
    +		}
    +	case err = <-writeErr:
    +		err = fmt.Errorf("write error: %w", err)
    +	case <-ctx.Done():
    +		err = ctx.Err()
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/conn-client.go b/deps/github.com/anacrolix/torrent/tracker/udp/conn-client.go
    new file mode 100644
    index 0000000..da4d7c0
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/conn-client.go
    @@ -0,0 +1,133 @@
    +package udp
    +
    +import (
    +	"context"
    +	"net"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/v2"
    +)
    +
    +type listenPacketFunc func(network, addr string) (net.PacketConn, error)
    +
    +type NewConnClientOpts struct {
    +	// The network to operate to use, such as "udp4", "udp", "udp6".
    +	Network string
    +	// Tracker address
    +	Host string
    +	// If non-nil, forces either IPv4 or IPv6 in the UDP tracker wire protocol.
    +	Ipv6 *bool
    +	// Logger to use for internal errors.
    +	Logger log.Logger
    +	// Custom function to use as a substitute for net.ListenPacket
    +	ListenPacket listenPacketFunc
    +}
    +
    +// Manages a Client with a specific connection.
    +type ConnClient struct {
    +	Client  Client
    +	conn    net.PacketConn
    +	d       Dispatcher
    +	readErr error
    +	closed  bool
    +	newOpts NewConnClientOpts
    +}
    +
    +func (cc *ConnClient) reader() {
    +	b := make([]byte, 0x800)
    +	for {
    +		n, addr, err := cc.conn.ReadFrom(b)
    +		if err != nil {
    +			// TODO: Do bad things to the dispatcher, and incoming calls to the client if we have a
    +			// read error.
    +			cc.readErr = err
    +			if !cc.closed {
    +				// don't panic, just close the connection, fix https://github.com/anacrolix/torrent/issues/845
    +				cc.Close()
    +			}
    +			break
    +		}
    +		err = cc.d.Dispatch(b[:n], addr)
    +		if err != nil {
    +			cc.newOpts.Logger.Levelf(log.Debug, "dispatching packet received on %v: %v", cc.conn.LocalAddr(), err)
    +		}
    +	}
    +}
    +
    +func ipv6(opt *bool, network string, remoteAddr net.Addr) bool {
    +	if opt != nil {
    +		return *opt
    +	}
    +	switch network {
    +	case "udp4":
    +		return false
    +	case "udp6":
    +		return true
    +	}
    +	rip := missinggo.AddrIP(remoteAddr)
    +	return rip.To16() != nil && rip.To4() == nil
    +}
    +
    +// Allows a UDP Client to write packets to an endpoint without knowing about the network specifics.
    +type clientWriter struct {
    +	pc      net.PacketConn
    +	network string
    +	address string
    +}
    +
    +func (me clientWriter) Write(p []byte) (n int, err error) {
    +	addr, err := net.ResolveUDPAddr(me.network, me.address)
    +	if err != nil {
    +		return
    +	}
    +	return me.pc.WriteTo(p, addr)
    +}
    +
    +func NewConnClient(opts NewConnClientOpts) (cc *ConnClient, err error) {
    +	var conn net.PacketConn
    +	if opts.ListenPacket != nil {
    +		conn, err = opts.ListenPacket(opts.Network, ":0")
    +	} else {
    +		conn, err = net.ListenPacket(opts.Network, ":0")
    +	}
    +
    +	if err != nil {
    +		return
    +	}
    +	if opts.Logger.IsZero() {
    +		opts.Logger = log.Default
    +	}
    +	cc = &ConnClient{
    +		Client: Client{
    +			Writer: clientWriter{
    +				pc:      conn,
    +				network: opts.Network,
    +				address: opts.Host,
    +			},
    +		},
    +		conn:    conn,
    +		newOpts: opts,
    +	}
    +	cc.Client.Dispatcher = &cc.d
    +	go cc.reader()
    +	return
    +}
    +
    +func (cc *ConnClient) Close() error {
    +	cc.closed = true
    +	return cc.conn.Close()
    +}
    +
    +func (cc *ConnClient) Announce(
    +	ctx context.Context, req AnnounceRequest, opts Options,
    +) (
    +	h AnnounceResponseHeader, nas AnnounceResponsePeers, err error,
    +) {
    +	return cc.Client.Announce(ctx, req, opts, func(addr net.Addr) bool {
    +		return ipv6(cc.newOpts.Ipv6, cc.newOpts.Network, addr)
    +	})
    +}
    +
    +func (cc *ConnClient) LocalAddr() net.Addr {
    +	return cc.conn.LocalAddr()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/dispatcher.go b/deps/github.com/anacrolix/torrent/tracker/udp/dispatcher.go
    new file mode 100644
    index 0000000..5709bd5
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/dispatcher.go
    @@ -0,0 +1,71 @@
    +package udp
    +
    +import (
    +	"bytes"
    +	"fmt"
    +	"net"
    +	"sync"
    +)
    +
    +// Maintains a mapping of transaction IDs to handlers.
    +type Dispatcher struct {
    +	mu           sync.RWMutex
    +	transactions map[TransactionId]Transaction
    +}
    +
    +// The caller owns b.
    +func (me *Dispatcher) Dispatch(b []byte, addr net.Addr) error {
    +	buf := bytes.NewBuffer(b)
    +	var rh ResponseHeader
    +	err := Read(buf, &rh)
    +	if err != nil {
    +		return err
    +	}
    +	me.mu.RLock()
    +	defer me.mu.RUnlock()
    +	if t, ok := me.transactions[rh.TransactionId]; ok {
    +		t.h(DispatchedResponse{
    +			Header: rh,
    +			Body:   append([]byte(nil), buf.Bytes()...),
    +			Addr:   addr,
    +		})
    +		return nil
    +	} else {
    +		return fmt.Errorf("unknown transaction id %v", rh.TransactionId)
    +	}
    +}
    +
    +func (me *Dispatcher) forgetTransaction(id TransactionId) {
    +	me.mu.Lock()
    +	defer me.mu.Unlock()
    +	delete(me.transactions, id)
    +}
    +
    +func (me *Dispatcher) NewTransaction(h TransactionResponseHandler) Transaction {
    +	me.mu.Lock()
    +	defer me.mu.Unlock()
    +	for {
    +		id := RandomTransactionId()
    +		if _, ok := me.transactions[id]; ok {
    +			continue
    +		}
    +		t := Transaction{
    +			d:  me,
    +			h:  h,
    +			id: id,
    +		}
    +		if me.transactions == nil {
    +			me.transactions = make(map[TransactionId]Transaction)
    +		}
    +		me.transactions[id] = t
    +		return t
    +	}
    +}
    +
    +type DispatchedResponse struct {
    +	Header ResponseHeader
    +	// Response payload, after the header.
    +	Body []byte
    +	// Response source address
    +	Addr net.Addr
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/options.go b/deps/github.com/anacrolix/torrent/tracker/udp/options.go
    new file mode 100644
    index 0000000..a2c223d
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/options.go
    @@ -0,0 +1,24 @@
    +package udp
    +
    +import (
    +	"math"
    +)
    +
    +type Options struct {
    +	RequestUri string
    +}
    +
    +func (opts Options) Encode() (ret []byte) {
    +	for {
    +		l := len(opts.RequestUri)
    +		if l == 0 {
    +			break
    +		}
    +		if l > math.MaxUint8 {
    +			l = math.MaxUint8
    +		}
    +		ret = append(append(ret, optionTypeURLData, byte(l)), opts.RequestUri[:l]...)
    +		opts.RequestUri = opts.RequestUri[l:]
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/protocol.go b/deps/github.com/anacrolix/torrent/tracker/udp/protocol.go
    new file mode 100644
    index 0000000..653d013
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/protocol.go
    @@ -0,0 +1,82 @@
    +package udp
    +
    +import (
    +	"bytes"
    +	"encoding/binary"
    +	"io"
    +)
    +
    +type Action int32
    +
    +const (
    +	ActionConnect Action = iota
    +	ActionAnnounce
    +	ActionScrape
    +	ActionError
    +)
    +
    +const ConnectRequestConnectionId = 0x41727101980
    +
    +const (
    +	// BEP 41
    +	optionTypeEndOfOptions = 0
    +	optionTypeNOP          = 1
    +	optionTypeURLData      = 2
    +)
    +
    +type TransactionId = int32
    +
    +type ConnectionId = uint64
    +
    +type ConnectionRequest struct {
    +	ConnectionId  ConnectionId
    +	Action        Action
    +	TransactionId TransactionId
    +}
    +
    +type ConnectionResponse struct {
    +	ConnectionId ConnectionId
    +}
    +
    +type ResponseHeader struct {
    +	Action        Action
    +	TransactionId TransactionId
    +}
    +
    +type RequestHeader struct {
    +	ConnectionId  ConnectionId
    +	Action        Action
    +	TransactionId TransactionId
    +} // 16 bytes
    +
    +type AnnounceResponseHeader struct {
    +	Interval int32
    +	Leechers int32
    +	Seeders  int32
    +}
    +
    +type InfoHash = [20]byte
    +
    +func marshal(data interface{}) (b []byte, err error) {
    +	var buf bytes.Buffer
    +	err = Write(&buf, data)
    +	b = buf.Bytes()
    +	return
    +}
    +
    +func mustMarshal(data interface{}) []byte {
    +	b, err := marshal(data)
    +	if err != nil {
    +		panic(err)
    +	}
    +	return b
    +}
    +
    +// This is for fixed-size, builtin types only I think.
    +func Write(w io.Writer, data interface{}) error {
    +	return binary.Write(w, binary.BigEndian, data)
    +}
    +
    +func Read(r io.Reader, data interface{}) error {
    +	return binary.Read(r, binary.BigEndian, data)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/scrape.go b/deps/github.com/anacrolix/torrent/tracker/udp/scrape.go
    new file mode 100644
    index 0000000..13a69b9
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/scrape.go
    @@ -0,0 +1,13 @@
    +package udp
    +
    +type ScrapeRequest []InfoHash
    +
    +type ScrapeResponse []ScrapeInfohashResult
    +
    +type ScrapeInfohashResult struct {
    +	// I'm not sure why the fields are named differently for HTTP scrapes.
    +	// https://www.bittorrent.org/beps/bep_0048.html
    +	Seeders   int32 `bencode:"complete"`
    +	Completed int32 `bencode:"downloaded"`
    +	Leechers  int32 `bencode:"incomplete"`
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/server/server.go b/deps/github.com/anacrolix/torrent/tracker/udp/server/server.go
    new file mode 100644
    index 0000000..3568839
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/server/server.go
    @@ -0,0 +1,241 @@
    +package udpTrackerServer
    +
    +import (
    +	"bytes"
    +	"context"
    +	"crypto/rand"
    +	"encoding/binary"
    +	"fmt"
    +	"io"
    +	"net"
    +	"net/netip"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	"github.com/anacrolix/generics"
    +	"github.com/anacrolix/log"
    +	"go.opentelemetry.io/otel"
    +	"go.opentelemetry.io/otel/attribute"
    +	"go.opentelemetry.io/otel/codes"
    +	"go.opentelemetry.io/otel/trace"
    +
    +	trackerServer "github.com/anacrolix/torrent/tracker/server"
    +	"github.com/anacrolix/torrent/tracker/udp"
    +)
    +
    +type ConnectionTrackerAddr = string
    +
    +type ConnectionTracker interface {
    +	Add(ctx context.Context, addr ConnectionTrackerAddr, id udp.ConnectionId) error
    +	Check(ctx context.Context, addr ConnectionTrackerAddr, id udp.ConnectionId) (bool, error)
    +}
    +
    +type InfoHash = [20]byte
    +
    +type AnnounceTracker = trackerServer.AnnounceTracker
    +
    +type Server struct {
    +	ConnTracker  ConnectionTracker
    +	SendResponse func(ctx context.Context, data []byte, addr net.Addr) (int, error)
    +	Announce     *trackerServer.AnnounceHandler
    +}
    +
    +type RequestSourceAddr = net.Addr
    +
    +var tracer = otel.Tracer("torrent.tracker.udp")
    +
    +func (me *Server) HandleRequest(
    +	ctx context.Context,
    +	family udp.AddrFamily,
    +	source RequestSourceAddr,
    +	body []byte,
    +) (err error) {
    +	ctx, span := tracer.Start(ctx, "Server.HandleRequest",
    +		trace.WithAttributes(attribute.Int("payload.len", len(body))))
    +	defer span.End()
    +	defer func() {
    +		if err != nil {
    +			span.SetStatus(codes.Error, err.Error())
    +		}
    +	}()
    +	var h udp.RequestHeader
    +	var r bytes.Reader
    +	r.Reset(body)
    +	err = udp.Read(&r, &h)
    +	if err != nil {
    +		err = fmt.Errorf("reading request header: %w", err)
    +		return err
    +	}
    +	switch h.Action {
    +	case udp.ActionConnect:
    +		err = me.handleConnect(ctx, source, h.TransactionId)
    +	case udp.ActionAnnounce:
    +		err = me.handleAnnounce(ctx, family, source, h.ConnectionId, h.TransactionId, &r)
    +	default:
    +		err = fmt.Errorf("unimplemented")
    +	}
    +	if err != nil {
    +		err = fmt.Errorf("handling action %v: %w", h.Action, err)
    +	}
    +	return err
    +}
    +
    +func (me *Server) handleAnnounce(
    +	ctx context.Context,
    +	addrFamily udp.AddrFamily,
    +	source RequestSourceAddr,
    +	connId udp.ConnectionId,
    +	tid udp.TransactionId,
    +	r *bytes.Reader,
    +) error {
    +	// Should we set a timeout of 10s or something for the entire response, so that we give up if a
    +	// retry is imminent?
    +
    +	ok, err := me.ConnTracker.Check(ctx, source.String(), connId)
    +	if err != nil {
    +		err = fmt.Errorf("checking conn id: %w", err)
    +		return err
    +	}
    +	if !ok {
    +		return fmt.Errorf("incorrect connection id: %x", connId)
    +	}
    +	var req udp.AnnounceRequest
    +	err = udp.Read(r, &req)
    +	if err != nil {
    +		return err
    +	}
    +	// TODO: This should be done asynchronously to responding to the announce.
    +	announceAddr, err := netip.ParseAddrPort(source.String())
    +	if err != nil {
    +		err = fmt.Errorf("converting source net.Addr to AnnounceAddr: %w", err)
    +		return err
    +	}
    +	opts := trackerServer.GetPeersOpts{MaxCount: generics.Some[uint](50)}
    +	if addrFamily == udp.AddrFamilyIpv4 {
    +		opts.MaxCount = generics.Some[uint](150)
    +	}
    +	res := me.Announce.Serve(ctx, req, announceAddr, opts)
    +	if res.Err != nil {
    +		return res.Err
    +	}
    +	nodeAddrs := make([]krpc.NodeAddr, 0, len(res.Peers))
    +	for _, p := range res.Peers {
    +		var ip net.IP
    +		switch addrFamily {
    +		default:
    +			continue
    +		case udp.AddrFamilyIpv4:
    +			if !p.Addr().Unmap().Is4() {
    +				continue
    +			}
    +			ipBuf := p.Addr().As4()
    +			ip = ipBuf[:]
    +		case udp.AddrFamilyIpv6:
    +			ipBuf := p.Addr().As16()
    +			ip = ipBuf[:]
    +		}
    +		nodeAddrs = append(nodeAddrs, krpc.NodeAddr{
    +			IP:   ip[:],
    +			Port: int(p.Port()),
    +		})
    +	}
    +	var buf bytes.Buffer
    +	err = udp.Write(&buf, udp.ResponseHeader{
    +		Action:        udp.ActionAnnounce,
    +		TransactionId: tid,
    +	})
    +	if err != nil {
    +		return err
    +	}
    +	err = udp.Write(&buf, udp.AnnounceResponseHeader{
    +		Interval: res.Interval.UnwrapOr(5 * 60),
    +		Seeders:  res.Seeders.Value,
    +		Leechers: res.Leechers.Value,
    +	})
    +	if err != nil {
    +		return err
    +	}
    +	b, err := udp.GetNodeAddrsCompactMarshaler(nodeAddrs, addrFamily).MarshalBinary()
    +	if err != nil {
    +		err = fmt.Errorf("marshalling compact node addrs: %w", err)
    +		return err
    +	}
    +	buf.Write(b)
    +	n, err := me.SendResponse(ctx, buf.Bytes(), source)
    +	if err != nil {
    +		return err
    +	}
    +	if n < buf.Len() {
    +		err = io.ErrShortWrite
    +	}
    +	return err
    +}
    +
    +func (me *Server) handleConnect(ctx context.Context, source RequestSourceAddr, tid udp.TransactionId) error {
    +	connId := randomConnectionId()
    +	err := me.ConnTracker.Add(ctx, source.String(), connId)
    +	if err != nil {
    +		err = fmt.Errorf("recording conn id: %w", err)
    +		return err
    +	}
    +	var buf bytes.Buffer
    +	udp.Write(&buf, udp.ResponseHeader{
    +		Action:        udp.ActionConnect,
    +		TransactionId: tid,
    +	})
    +	udp.Write(&buf, udp.ConnectionResponse{connId})
    +	n, err := me.SendResponse(ctx, buf.Bytes(), source)
    +	if err != nil {
    +		return err
    +	}
    +	if n < buf.Len() {
    +		err = io.ErrShortWrite
    +	}
    +	return err
    +}
    +
    +func randomConnectionId() udp.ConnectionId {
    +	var b [8]byte
    +	_, err := rand.Read(b[:])
    +	if err != nil {
    +		panic(err)
    +	}
    +	return binary.BigEndian.Uint64(b[:])
    +}
    +
    +func RunSimple(ctx context.Context, s *Server, pc net.PacketConn, family udp.AddrFamily) error {
    +	ctx, cancel := context.WithCancel(ctx)
    +	defer cancel()
    +	var b [1500]byte
    +	// Limit concurrent handled requests.
    +	sem := make(chan struct{}, 1000)
    +	for {
    +		n, addr, err := pc.ReadFrom(b[:])
    +		ctx, span := tracer.Start(ctx, "handle udp packet")
    +		if err != nil {
    +			span.SetStatus(codes.Error, err.Error())
    +			span.End()
    +			return err
    +		}
    +		select {
    +		case <-ctx.Done():
    +			span.SetStatus(codes.Error, err.Error())
    +			span.End()
    +			return ctx.Err()
    +		default:
    +			span.SetStatus(codes.Error, "concurrency limit reached")
    +			span.End()
    +			log.Levelf(log.Debug, "dropping request from %v: concurrency limit reached", addr)
    +			continue
    +		case sem <- struct{}{}:
    +		}
    +		b := append([]byte(nil), b[:n]...)
    +		go func() {
    +			defer span.End()
    +			defer func() { <-sem }()
    +			err := s.HandleRequest(ctx, family, addr, b)
    +			if err != nil {
    +				log.Printf("error handling %v byte request from %v: %v", n, addr, err)
    +			}
    +		}()
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/timeout.go b/deps/github.com/anacrolix/torrent/tracker/udp/timeout.go
    new file mode 100644
    index 0000000..b5e1832
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/timeout.go
    @@ -0,0 +1,18 @@
    +package udp
    +
    +import (
    +	"time"
    +)
    +
    +const maxTimeout = 3840 * time.Second
    +
    +func timeout(contiguousTimeouts int) (d time.Duration) {
    +	if contiguousTimeouts > 8 {
    +		contiguousTimeouts = 8
    +	}
    +	d = 15 * time.Second
    +	for ; contiguousTimeouts > 0; contiguousTimeouts-- {
    +		d *= 2
    +	}
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/timeout_test.go b/deps/github.com/anacrolix/torrent/tracker/udp/timeout_test.go
    new file mode 100644
    index 0000000..4bb0dc8
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/timeout_test.go
    @@ -0,0 +1,15 @@
    +package udp
    +
    +import (
    +	"math"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +)
    +
    +func TestTimeoutMax(t *testing.T) {
    +	c := qt.New(t)
    +	c.Check(timeout(8), qt.Equals, maxTimeout)
    +	c.Check(timeout(9), qt.Equals, maxTimeout)
    +	c.Check(timeout(math.MaxInt32), qt.Equals, maxTimeout)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/transaction.go b/deps/github.com/anacrolix/torrent/tracker/udp/transaction.go
    new file mode 100644
    index 0000000..2018b35
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/transaction.go
    @@ -0,0 +1,23 @@
    +package udp
    +
    +import "math/rand"
    +
    +func RandomTransactionId() TransactionId {
    +	return TransactionId(rand.Uint32())
    +}
    +
    +type TransactionResponseHandler func(dr DispatchedResponse)
    +
    +type Transaction struct {
    +	id int32
    +	d  *Dispatcher
    +	h  TransactionResponseHandler
    +}
    +
    +func (t *Transaction) Id() TransactionId {
    +	return t.id
    +}
    +
    +func (t *Transaction) End() {
    +	t.d.forgetTransaction(t.id)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/udp_test.go b/deps/github.com/anacrolix/torrent/tracker/udp/udp_test.go
    new file mode 100644
    index 0000000..64aeb80
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/udp_test.go
    @@ -0,0 +1,139 @@
    +package udp
    +
    +import (
    +	"bytes"
    +	"context"
    +	"crypto/rand"
    +	"encoding/binary"
    +	"io"
    +	"net"
    +	"sync"
    +	"testing"
    +	"time"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	_ "github.com/anacrolix/envpprof"
    +	"github.com/anacrolix/missinggo/v2/iter"
    +	qt "github.com/frankban/quicktest"
    +	"github.com/stretchr/testify/require"
    +)
    +
    +// Ensure net.IPs are stored big-endian, to match the way they're read from
    +// the wire.
    +func TestNetIPv4Bytes(t *testing.T) {
    +	ip := net.IP([]byte{127, 0, 0, 1})
    +	if ip.String() != "127.0.0.1" {
    +		t.FailNow()
    +	}
    +	if string(ip) != "\x7f\x00\x00\x01" {
    +		t.Fatal([]byte(ip))
    +	}
    +}
    +
    +func TestMarshalAnnounceResponse(t *testing.T) {
    +	peers := krpc.CompactIPv4NodeAddrs{
    +		{[]byte{127, 0, 0, 1}, 2},
    +		{[]byte{255, 0, 0, 3}, 4},
    +	}
    +	b, err := peers.MarshalBinary()
    +	require.NoError(t, err)
    +	require.EqualValues(t,
    +		"\x7f\x00\x00\x01\x00\x02\xff\x00\x00\x03\x00\x04",
    +		b)
    +	require.EqualValues(t, 12, binary.Size(AnnounceResponseHeader{}))
    +}
    +
    +// Failure to write an entire packet to UDP is expected to given an error.
    +func TestLongWriteUDP(t *testing.T) {
    +	t.Parallel()
    +	l, err := net.ListenUDP("udp4", nil)
    +	require.NoError(t, err)
    +	defer l.Close()
    +	c, err := net.DialUDP("udp", nil, l.LocalAddr().(*net.UDPAddr))
    +	if err != nil {
    +		t.Fatal(err)
    +	}
    +	defer c.Close()
    +	for msgLen := 1; ; msgLen *= 2 {
    +		n, err := c.Write(make([]byte, msgLen))
    +		if err != nil {
    +			if isErrMessageTooLong(err) {
    +				return
    +			}
    +			t.Fatalf("expected message too long error: %v", err)
    +		}
    +		if n < msgLen {
    +			t.FailNow()
    +		}
    +	}
    +}
    +
    +func TestShortBinaryRead(t *testing.T) {
    +	var data ResponseHeader
    +	err := binary.Read(bytes.NewBufferString("\x00\x00\x00\x01"), binary.BigEndian, &data)
    +	if err != io.ErrUnexpectedEOF {
    +		t.FailNow()
    +	}
    +}
    +
    +func TestConvertInt16ToInt(t *testing.T) {
    +	i := 50000
    +	if int(uint16(int16(i))) != 50000 {
    +		t.FailNow()
    +	}
    +}
    +
    +func TestConnClientLogDispatchUnknownTransactionId(t *testing.T) {
    +	const network = "udp"
    +	cc, err := NewConnClient(NewConnClientOpts{
    +		Network: network,
    +	})
    +	c := qt.New(t)
    +	c.Assert(err, qt.IsNil)
    +	defer cc.Close()
    +	pc, err := net.ListenPacket(network, ":0")
    +	c.Assert(err, qt.IsNil)
    +	defer pc.Close()
    +	ccAddr := *cc.LocalAddr().(*net.UDPAddr)
    +	ccAddr.IP = net.IPv6loopback
    +	_, err = pc.WriteTo(make([]byte, 30), &ccAddr)
    +	c.Assert(err, qt.IsNil)
    +}
    +
    +func TestConnectionIdMismatch(t *testing.T) {
    +	t.Skip("Server host returns consistent connection ID in limited tests and so isn't effective.")
    +	cl, err := NewConnClient(NewConnClientOpts{
    +		// This host seems to return `Connection ID missmatch.\x00` every 2 minutes or so under
    +		// heavy use.
    +		Host: "tracker.torrent.eu.org:451",
    +		//Host:    "tracker.opentrackr.org:1337",
    +		Network: "udp",
    +	})
    +	c := qt.New(t)
    +	c.Assert(err, qt.IsNil)
    +	defer cl.Close()
    +	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    +	defer cancel()
    +	// Force every request to use a different connection ID. It's racey, but we want to get a
    +	// different ID issued before a request can be sent with an old ID.
    +	cl.Client.shouldReconnectOverride = func() bool { return true }
    +	started := time.Now()
    +	var wg sync.WaitGroup
    +	for range iter.N(2) {
    +		ar := AnnounceRequest{
    +			NumWant: -1,
    +			Event:   2,
    +		}
    +		rand.Read(ar.InfoHash[:])
    +		rand.Read(ar.PeerId[:])
    +		//spew.Dump(ar)
    +		wg.Add(1)
    +		go func() {
    +			defer wg.Done()
    +			_, _, err := cl.Announce(ctx, ar, Options{})
    +			// I'm looking for `error response: "Connection ID missmatch.\x00"`.
    +			t.Logf("announce error after %v: %v", time.Since(started), err)
    +		}()
    +	}
    +	wg.Wait()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/udp_unix.go b/deps/github.com/anacrolix/torrent/tracker/udp/udp_unix.go
    new file mode 100644
    index 0000000..6fcf9ed
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/udp_unix.go
    @@ -0,0 +1,14 @@
    +//go:build !windows
    +
    +package udp
    +
    +import (
    +	"strings"
    +)
    +
    +func isErrMessageTooLong(err error) bool {
    +	if err == nil {
    +		return false
    +	}
    +	return strings.Contains(err.Error(), "message too long")
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp/udp_windows.go b/deps/github.com/anacrolix/torrent/tracker/udp/udp_windows.go
    new file mode 100644
    index 0000000..a289e9e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp/udp_windows.go
    @@ -0,0 +1,11 @@
    +package udp
    +
    +import (
    +	"errors"
    +
    +	"golang.org/x/sys/windows"
    +)
    +
    +func isErrMessageTooLong(err error) bool {
    +	return errors.Is(err, windows.WSAEMSGSIZE)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker/udp_test.go b/deps/github.com/anacrolix/torrent/tracker/udp_test.go
    new file mode 100644
    index 0000000..232aeb1
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker/udp_test.go
    @@ -0,0 +1,194 @@
    +package tracker
    +
    +import (
    +	"bytes"
    +	"context"
    +	"crypto/rand"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"net"
    +	"net/url"
    +	"sync"
    +	"testing"
    +	"time"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	_ "github.com/anacrolix/envpprof"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +
    +	"github.com/anacrolix/torrent/tracker/udp"
    +)
    +
    +var trackers = []string{
    +	"udp://tracker.opentrackr.org:1337/announce",
    +	"udp://tracker.openbittorrent.com:6969/announce",
    +	"udp://localhost:42069",
    +}
    +
    +func TestAnnounceLocalhost(t *testing.T) {
    +	t.Parallel()
    +	srv := server{
    +		t: map[[20]byte]torrent{
    +			{0xa3, 0x56, 0x41, 0x43, 0x74, 0x23, 0xe6, 0x26, 0xd9, 0x38, 0x25, 0x4a, 0x6b, 0x80, 0x49, 0x10, 0xa6, 0x67, 0xa, 0xc1}: {
    +				Seeders:  1,
    +				Leechers: 2,
    +				Peers: krpc.CompactIPv4NodeAddrs{
    +					{[]byte{1, 2, 3, 4}, 5},
    +					{[]byte{6, 7, 8, 9}, 10},
    +				},
    +			},
    +		},
    +	}
    +	var err error
    +	srv.pc, err = net.ListenPacket("udp", "localhost:0")
    +	require.NoError(t, err)
    +	defer srv.pc.Close()
    +	go func() {
    +		require.NoError(t, srv.serveOne())
    +	}()
    +	req := AnnounceRequest{
    +		NumWant: -1,
    +		Event:   Started,
    +	}
    +	rand.Read(req.PeerId[:])
    +	copy(req.InfoHash[:], []uint8{0xa3, 0x56, 0x41, 0x43, 0x74, 0x23, 0xe6, 0x26, 0xd9, 0x38, 0x25, 0x4a, 0x6b, 0x80, 0x49, 0x10, 0xa6, 0x67, 0xa, 0xc1})
    +	go func() {
    +		require.NoError(t, srv.serveOne())
    +	}()
    +	ar, err := Announce{
    +		TrackerUrl: fmt.Sprintf("udp://%s/announce", srv.pc.LocalAddr().String()),
    +		Request:    req,
    +	}.Do()
    +	require.NoError(t, err)
    +	assert.EqualValues(t, 1, ar.Seeders)
    +	assert.EqualValues(t, 2, len(ar.Peers))
    +}
    +
    +func TestUDPTracker(t *testing.T) {
    +	t.Parallel()
    +	if testing.Short() {
    +		t.SkipNow()
    +	}
    +	req := AnnounceRequest{
    +		NumWant: -1,
    +	}
    +	rand.Read(req.PeerId[:])
    +	copy(req.InfoHash[:], []uint8{0xa3, 0x56, 0x41, 0x43, 0x74, 0x23, 0xe6, 0x26, 0xd9, 0x38, 0x25, 0x4a, 0x6b, 0x80, 0x49, 0x10, 0xa6, 0x67, 0xa, 0xc1})
    +	ctx, cancel := context.WithTimeout(context.Background(), DefaultTrackerAnnounceTimeout)
    +	defer cancel()
    +	if dl, ok := t.Deadline(); ok {
    +		var cancel func()
    +		ctx, cancel = context.WithDeadline(context.Background(), dl.Add(-time.Second))
    +		defer cancel()
    +	}
    +	ar, err := Announce{
    +		TrackerUrl: trackers[0],
    +		Request:    req,
    +		Context:    ctx,
    +	}.Do()
    +	// Skip any net errors as we don't control the server.
    +	var ne net.Error
    +	if errors.As(err, &ne) {
    +		t.Skip(err)
    +	}
    +	require.NoError(t, err)
    +	t.Logf("%+v", ar)
    +}
    +
    +func TestAnnounceRandomInfoHashThirdParty(t *testing.T) {
    +	t.Parallel()
    +	if testing.Short() {
    +		// This test involves contacting third party servers that may have
    +		// unpredictable results.
    +		t.SkipNow()
    +	}
    +	req := AnnounceRequest{
    +		Event: Stopped,
    +	}
    +	rand.Read(req.PeerId[:])
    +	rand.Read(req.InfoHash[:])
    +	wg := sync.WaitGroup{}
    +	ctx, cancel := context.WithTimeout(context.Background(), DefaultTrackerAnnounceTimeout)
    +	defer cancel()
    +	if dl, ok := t.Deadline(); ok {
    +		var cancel func()
    +		ctx, cancel = context.WithDeadline(ctx, dl.Add(-time.Second))
    +		defer cancel()
    +	}
    +	for _, url := range trackers {
    +		wg.Add(1)
    +		go func(url string) {
    +			defer wg.Done()
    +			resp, err := Announce{
    +				TrackerUrl: url,
    +				Request:    req,
    +				Context:    ctx,
    +			}.Do()
    +			if err != nil {
    +				t.Logf("error announcing to %s: %s", url, err)
    +				return
    +			}
    +			if resp.Leechers != 0 || resp.Seeders != 0 || len(resp.Peers) != 0 {
    +				// The info hash we generated was random in 2^160 space. If we
    +				// get a hit, something is weird.
    +				t.Fatal(resp)
    +			}
    +			t.Logf("announced to %s", url)
    +			cancel()
    +		}(url)
    +	}
    +	wg.Wait()
    +	cancel()
    +}
    +
    +// Check that URLPath option is done correctly.
    +func TestURLPathOption(t *testing.T) {
    +	conn, err := net.ListenPacket("udp", "localhost:0")
    +	if err != nil {
    +		panic(err)
    +	}
    +	defer conn.Close()
    +	announceErr := make(chan error)
    +	go func() {
    +		_, err := Announce{
    +			TrackerUrl: (&url.URL{
    +				Scheme: "udp",
    +				Host:   conn.LocalAddr().String(),
    +				Path:   "/announce",
    +			}).String(),
    +		}.Do()
    +		defer conn.Close()
    +		announceErr <- err
    +	}()
    +	var b [512]byte
    +	// conn.SetReadDeadline(time.Now().Add(time.Second))
    +	_, addr, _ := conn.ReadFrom(b[:])
    +	r := bytes.NewReader(b[:])
    +	var h udp.RequestHeader
    +	udp.Read(r, &h)
    +	w := &bytes.Buffer{}
    +	udp.Write(w, udp.ResponseHeader{
    +		Action:        udp.ActionConnect,
    +		TransactionId: h.TransactionId,
    +	})
    +	udp.Write(w, udp.ConnectionResponse{42})
    +	conn.WriteTo(w.Bytes(), addr)
    +	n, _, _ := conn.ReadFrom(b[:])
    +	r = bytes.NewReader(b[:n])
    +	udp.Read(r, &h)
    +	udp.Read(r, &AnnounceRequest{})
    +	all, _ := io.ReadAll(r)
    +	if string(all) != "\x02\x09/announce" {
    +		t.FailNow()
    +	}
    +	w = &bytes.Buffer{}
    +	udp.Write(w, udp.ResponseHeader{
    +		Action:        udp.ActionAnnounce,
    +		TransactionId: h.TransactionId,
    +	})
    +	udp.Write(w, udp.AnnounceResponseHeader{})
    +	conn.WriteTo(w.Bytes(), addr)
    +	require.NoError(t, <-announceErr)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/tracker_scraper.go b/deps/github.com/anacrolix/torrent/tracker_scraper.go
    new file mode 100644
    index 0000000..863838a
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/tracker_scraper.go
    @@ -0,0 +1,265 @@
    +package torrent
    +
    +import (
    +	"bytes"
    +	"context"
    +	"errors"
    +	"fmt"
    +	"net"
    +	"net/url"
    +	"time"
    +
    +	"github.com/anacrolix/dht/v2/krpc"
    +	"github.com/anacrolix/log"
    +
    +	"github.com/anacrolix/torrent/tracker"
    +)
    +
    +// Announces a torrent to a tracker at regular intervals, when peers are
    +// required.
    +type trackerScraper struct {
    +	u               url.URL
    +	t               *Torrent
    +	lastAnnounce    trackerAnnounceResult
    +	lookupTrackerIp func(*url.URL) ([]net.IP, error)
    +}
    +
    +type torrentTrackerAnnouncer interface {
    +	statusLine() string
    +	URL() *url.URL
    +}
    +
    +func (me trackerScraper) URL() *url.URL {
    +	return &me.u
    +}
    +
    +func (ts *trackerScraper) statusLine() string {
    +	var w bytes.Buffer
    +	fmt.Fprintf(&w, "next ann: %v, last ann: %v",
    +		func() string {
    +			na := time.Until(ts.lastAnnounce.Completed.Add(ts.lastAnnounce.Interval))
    +			if na > 0 {
    +				na /= time.Second
    +				na *= time.Second
    +				return na.String()
    +			} else {
    +				return "anytime"
    +			}
    +		}(),
    +		func() string {
    +			if ts.lastAnnounce.Err != nil {
    +				return ts.lastAnnounce.Err.Error()
    +			}
    +			if ts.lastAnnounce.Completed.IsZero() {
    +				return "never"
    +			}
    +			return fmt.Sprintf("%d peers", ts.lastAnnounce.NumPeers)
    +		}(),
    +	)
    +	return w.String()
    +}
    +
    +type trackerAnnounceResult struct {
    +	Err       error
    +	NumPeers  int
    +	Interval  time.Duration
    +	Completed time.Time
    +}
    +
    +func (me *trackerScraper) getIp() (ip net.IP, err error) {
    +	var ips []net.IP
    +	if me.lookupTrackerIp != nil {
    +		ips, err = me.lookupTrackerIp(&me.u)
    +	} else {
    +		// Do a regular dns lookup
    +		ips, err = net.LookupIP(me.u.Hostname())
    +	}
    +	if err != nil {
    +		return
    +	}
    +	if len(ips) == 0 {
    +		err = errors.New("no ips")
    +		return
    +	}
    +	me.t.cl.rLock()
    +	defer me.t.cl.rUnlock()
    +	if me.t.cl.closed.IsSet() {
    +		err = errors.New("client is closed")
    +		return
    +	}
    +	for _, ip = range ips {
    +		if me.t.cl.ipIsBlocked(ip) {
    +			continue
    +		}
    +		switch me.u.Scheme {
    +		case "udp4":
    +			if ip.To4() == nil {
    +				continue
    +			}
    +		case "udp6":
    +			if ip.To4() != nil {
    +				continue
    +			}
    +		}
    +		return
    +	}
    +	err = errors.New("no acceptable ips")
    +	return
    +}
    +
    +func (me *trackerScraper) trackerUrl(ip net.IP) string {
    +	u := me.u
    +	if u.Port() != "" {
    +		u.Host = net.JoinHostPort(ip.String(), u.Port())
    +	}
    +	return u.String()
    +}
    +
    +// Return how long to wait before trying again. For most errors, we return 5
    +// minutes, a relatively quick turn around for DNS changes.
    +func (me *trackerScraper) announce(ctx context.Context, event tracker.AnnounceEvent) (ret trackerAnnounceResult) {
    +	defer func() {
    +		ret.Completed = time.Now()
    +	}()
    +	ret.Interval = time.Minute
    +
    +	// Limit concurrent use of the same tracker URL by the Client.
    +	ref := me.t.cl.activeAnnounceLimiter.GetRef(me.u.String())
    +	defer ref.Drop()
    +	select {
    +	case <-ctx.Done():
    +		ret.Err = ctx.Err()
    +		return
    +	case ref.C() <- struct{}{}:
    +	}
    +	defer func() {
    +		select {
    +		case <-ref.C():
    +		default:
    +			panic("should return immediately")
    +		}
    +	}()
    +
    +	ip, err := me.getIp()
    +	if err != nil {
    +		ret.Err = fmt.Errorf("error getting ip: %s", err)
    +		return
    +	}
    +	me.t.cl.rLock()
    +	req := me.t.announceRequest(event)
    +	me.t.cl.rUnlock()
    +	// The default timeout works well as backpressure on concurrent access to the tracker. Since
    +	// we're passing our own Context now, we will include that timeout ourselves to maintain similar
    +	// behavior to previously, albeit with this context now being cancelled when the Torrent is
    +	// closed.
    +	ctx, cancel := context.WithTimeout(ctx, tracker.DefaultTrackerAnnounceTimeout)
    +	defer cancel()
    +	me.t.logger.WithDefaultLevel(log.Debug).Printf("announcing to %q: %#v", me.u.String(), req)
    +	res, err := tracker.Announce{
    +		Context:             ctx,
    +		HttpProxy:           me.t.cl.config.HTTPProxy,
    +		HttpRequestDirector: me.t.cl.config.HttpRequestDirector,
    +		DialContext:         me.t.cl.config.TrackerDialContext,
    +		ListenPacket:        me.t.cl.config.TrackerListenPacket,
    +		UserAgent:           me.t.cl.config.HTTPUserAgent,
    +		TrackerUrl:          me.trackerUrl(ip),
    +		Request:             req,
    +		HostHeader:          me.u.Host,
    +		ServerName:          me.u.Hostname(),
    +		UdpNetwork:          me.u.Scheme,
    +		ClientIp4:           krpc.NodeAddr{IP: me.t.cl.config.PublicIp4},
    +		ClientIp6:           krpc.NodeAddr{IP: me.t.cl.config.PublicIp6},
    +		Logger:              me.t.logger,
    +	}.Do()
    +	me.t.logger.WithDefaultLevel(log.Debug).Printf("announce to %q returned %#v: %v", me.u.String(), res, err)
    +	if err != nil {
    +		ret.Err = fmt.Errorf("announcing: %w", err)
    +		return
    +	}
    +	me.t.AddPeers(peerInfos(nil).AppendFromTracker(res.Peers))
    +	ret.NumPeers = len(res.Peers)
    +	ret.Interval = time.Duration(res.Interval) * time.Second
    +	return
    +}
    +
    +// Returns whether we can shorten the interval, and sets notify to a channel that receives when we
    +// might change our mind, or leaves it if we won't.
    +func (me *trackerScraper) canIgnoreInterval(notify *<-chan struct{}) bool {
    +	gotInfo := me.t.GotInfo()
    +	select {
    +	case <-gotInfo:
    +		// Private trackers really don't like us announcing more than they specify. They're also
    +		// tracking us very carefully, so it's best to comply.
    +		private := me.t.info.Private
    +		return private == nil || !*private
    +	default:
    +		*notify = gotInfo
    +		return false
    +	}
    +}
    +
    +func (me *trackerScraper) Run() {
    +	defer me.announceStopped()
    +
    +	ctx, cancel := context.WithCancel(context.Background())
    +	defer cancel()
    +	go func() {
    +		defer cancel()
    +		select {
    +		case <-ctx.Done():
    +		case <-me.t.Closed():
    +		}
    +	}()
    +
    +	// make sure first announce is a "started"
    +	e := tracker.Started
    +
    +	for {
    +		ar := me.announce(ctx, e)
    +		// after first announce, get back to regular "none"
    +		e = tracker.None
    +		me.t.cl.lock()
    +		me.lastAnnounce = ar
    +		me.t.cl.unlock()
    +
    +	recalculate:
    +		// Make sure we don't announce for at least a minute since the last one.
    +		interval := ar.Interval
    +		if interval < time.Minute {
    +			interval = time.Minute
    +		}
    +
    +		me.t.cl.lock()
    +		wantPeers := me.t.wantPeersEvent.C()
    +		me.t.cl.unlock()
    +
    +		// If we want peers, reduce the interval to the minimum if it's appropriate.
    +
    +		// A channel that receives when we should reconsider our interval. Starts as nil since that
    +		// never receives.
    +		var reconsider <-chan struct{}
    +		select {
    +		case <-wantPeers:
    +			if interval > time.Minute && me.canIgnoreInterval(&reconsider) {
    +				interval = time.Minute
    +			}
    +		default:
    +			reconsider = wantPeers
    +		}
    +
    +		select {
    +		case <-me.t.closed.Done():
    +			return
    +		case <-reconsider:
    +			// Recalculate the interval.
    +			goto recalculate
    +		case <-time.After(time.Until(ar.Completed.Add(interval))):
    +		}
    +	}
    +}
    +
    +func (me *trackerScraper) announceStopped() {
    +	ctx, cancel := context.WithTimeout(context.Background(), tracker.DefaultTrackerAnnounceTimeout)
    +	defer cancel()
    +	me.announce(ctx, tracker.Stopped)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/typed-roaring/bitmap.go b/deps/github.com/anacrolix/torrent/typed-roaring/bitmap.go
    new file mode 100644
    index 0000000..7f7b1a7
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/typed-roaring/bitmap.go
    @@ -0,0 +1,48 @@
    +package typedRoaring
    +
    +import (
    +	"github.com/RoaringBitmap/roaring"
    +)
    +
    +type Bitmap[T BitConstraint] struct {
    +	roaring.Bitmap
    +}
    +
    +func (me *Bitmap[T]) Contains(x T) bool {
    +	return me.Bitmap.Contains(uint32(x))
    +}
    +
    +func (me Bitmap[T]) Iterate(f func(x T) bool) {
    +	me.Bitmap.Iterate(func(x uint32) bool {
    +		return f(T(x))
    +	})
    +}
    +
    +func (me *Bitmap[T]) Add(x T) {
    +	me.Bitmap.Add(uint32(x))
    +}
    +
    +func (me *Bitmap[T]) Rank(x T) uint64 {
    +	return me.Bitmap.Rank(uint32(x))
    +}
    +
    +func (me *Bitmap[T]) CheckedRemove(x T) bool {
    +	return me.Bitmap.CheckedRemove(uint32(x))
    +}
    +
    +func (me *Bitmap[T]) Clone() Bitmap[T] {
    +	return Bitmap[T]{*me.Bitmap.Clone()}
    +}
    +
    +func (me *Bitmap[T]) CheckedAdd(x T) bool {
    +	return me.Bitmap.CheckedAdd(uint32(x))
    +}
    +
    +func (me *Bitmap[T]) Remove(x T) {
    +	me.Bitmap.Remove(uint32(x))
    +}
    +
    +// Returns an uninitialized iterator for the type of the receiver.
    +func (Bitmap[T]) IteratorType() Iterator[T] {
    +	return Iterator[T]{}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/typed-roaring/constraints.go b/deps/github.com/anacrolix/torrent/typed-roaring/constraints.go
    new file mode 100644
    index 0000000..d6e191f
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/typed-roaring/constraints.go
    @@ -0,0 +1,5 @@
    +package typedRoaring
    +
    +type BitConstraint interface {
    +	~int | ~uint32
    +}
    diff --git a/deps/github.com/anacrolix/torrent/typed-roaring/iterator.go b/deps/github.com/anacrolix/torrent/typed-roaring/iterator.go
    new file mode 100644
    index 0000000..8766db1
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/typed-roaring/iterator.go
    @@ -0,0 +1,21 @@
    +package typedRoaring
    +
    +import (
    +	"github.com/RoaringBitmap/roaring"
    +)
    +
    +type Iterator[T BitConstraint] struct {
    +	roaring.IntIterator
    +}
    +
    +func (t *Iterator[T]) Next() T {
    +	return T(t.IntIterator.Next())
    +}
    +
    +func (t *Iterator[T]) AdvanceIfNeeded(minVal T) {
    +	t.IntIterator.AdvanceIfNeeded(uint32(minVal))
    +}
    +
    +func (t *Iterator[T]) Initialize(a *Bitmap[T]) {
    +	t.IntIterator.Initialize(&a.Bitmap)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/types/infohash/infohash.go b/deps/github.com/anacrolix/torrent/types/infohash/infohash.go
    new file mode 100644
    index 0000000..0763b01
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/types/infohash/infohash.go
    @@ -0,0 +1,80 @@
    +package infohash
    +
    +import (
    +	"crypto/sha1"
    +	"encoding"
    +	"encoding/hex"
    +	"fmt"
    +)
    +
    +const Size = 20
    +
    +// 20-byte SHA1 hash used for info and pieces.
    +type T [Size]byte
    +
    +var _ fmt.Formatter = (*T)(nil)
    +
    +func (t T) Format(f fmt.State, c rune) {
    +	// TODO: I can't figure out a nice way to just override the 'x' rune, since it's meaningless
    +	// with the "default" 'v', or .String() already returning the hex.
    +	f.Write([]byte(t.HexString()))
    +}
    +
    +func (t T) Bytes() []byte {
    +	return t[:]
    +}
    +
    +func (t T) AsString() string {
    +	return string(t[:])
    +}
    +
    +func (t T) String() string {
    +	return t.HexString()
    +}
    +
    +func (t T) HexString() string {
    +	return fmt.Sprintf("%x", t[:])
    +}
    +
    +func (t *T) FromHexString(s string) (err error) {
    +	if len(s) != 2*Size {
    +		err = fmt.Errorf("hash hex string has bad length: %d", len(s))
    +		return
    +	}
    +	n, err := hex.Decode(t[:], []byte(s))
    +	if err != nil {
    +		return
    +	}
    +	if n != Size {
    +		panic(n)
    +	}
    +	return
    +}
    +
    +var (
    +	_ encoding.TextUnmarshaler = (*T)(nil)
    +	_ encoding.TextMarshaler   = T{}
    +)
    +
    +func (t *T) UnmarshalText(b []byte) error {
    +	return t.FromHexString(string(b))
    +}
    +
    +func (t T) MarshalText() (text []byte, err error) {
    +	return []byte(t.HexString()), nil
    +}
    +
    +func FromHexString(s string) (h T) {
    +	err := h.FromHexString(s)
    +	if err != nil {
    +		panic(err)
    +	}
    +	return
    +}
    +
    +func HashBytes(b []byte) (ret T) {
    +	hasher := sha1.New()
    +	hasher.Write(b)
    +	copy(ret[:], hasher.Sum(nil))
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/types/peerid.go b/deps/github.com/anacrolix/torrent/types/peerid.go
    new file mode 100644
    index 0000000..0e13473
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/types/peerid.go
    @@ -0,0 +1,14 @@
    +package types
    +
    +// Peer client ID.
    +type PeerID [20]byte
    +
    +// // Pretty prints the ID as hex, except parts that adher to the PeerInfo ID
    +// // Conventions of BEP 20.
    +// func (me PeerID) String() string {
    +// 	// if me[0] == '-' && me[7] == '-' {
    +// 	// 	return string(me[:8]) + hex.EncodeToString(me[8:])
    +// 	// }
    +// 	// return hex.EncodeToString(me[:])
    +// 	return fmt.Sprintf("%+q", me[:])
    +// }
    diff --git a/deps/github.com/anacrolix/torrent/types/types.go b/deps/github.com/anacrolix/torrent/types/types.go
    new file mode 100644
    index 0000000..a06f7e6
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/types/types.go
    @@ -0,0 +1,52 @@
    +package types
    +
    +import (
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +)
    +
    +type PieceIndex = int
    +
    +type ChunkSpec struct {
    +	Begin, Length pp.Integer
    +}
    +
    +type Request struct {
    +	Index pp.Integer
    +	ChunkSpec
    +}
    +
    +func (r Request) ToMsg(mt pp.MessageType) pp.Message {
    +	return pp.Message{
    +		Type:   mt,
    +		Index:  r.Index,
    +		Begin:  r.Begin,
    +		Length: r.Length,
    +	}
    +}
    +
    +// Describes the importance of obtaining a particular piece.
    +type PiecePriority byte
    +
    +func (pp *PiecePriority) Raise(maybe PiecePriority) bool {
    +	if maybe > *pp {
    +		*pp = maybe
    +		return true
    +	}
    +	return false
    +}
    +
    +// Priority for use in PriorityBitmap
    +func (me PiecePriority) BitmapPriority() int {
    +	return -int(me)
    +}
    +
    +const (
    +	PiecePriorityNone      PiecePriority = iota // Not wanted. Must be the zero value.
    +	PiecePriorityNormal                         // Wanted.
    +	PiecePriorityHigh                           // Wanted a lot.
    +	PiecePriorityReadahead                      // May be required soon.
    +	// Succeeds a piece where a read occurred. Currently the same as Now,
    +	// apparently due to issues with caching.
    +	PiecePriorityNext
    +	PiecePriorityNow // A Reader is reading in this piece. Highest urgency.
    +)
    diff --git a/deps/github.com/anacrolix/torrent/undirtied-chunks-iter.go b/deps/github.com/anacrolix/torrent/undirtied-chunks-iter.go
    new file mode 100644
    index 0000000..de0cce0
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/undirtied-chunks-iter.go
    @@ -0,0 +1,23 @@
    +package torrent
    +
    +import (
    +	"github.com/anacrolix/torrent/typed-roaring"
    +)
    +
    +func iterBitmapUnsetInRange[T typedRoaring.BitConstraint](it *typedRoaring.Iterator[T], start, end T, f func(T)) {
    +	it.AdvanceIfNeeded(start)
    +	lastDirty := start - 1
    +	for it.HasNext() {
    +		next := it.Next()
    +		if next >= end {
    +			break
    +		}
    +		for index := lastDirty + 1; index < next; index++ {
    +			f(index)
    +		}
    +		lastDirty = next
    +	}
    +	for index := lastDirty + 1; index < end; index++ {
    +		f(index)
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/undirtied-chunks-iter_test.go b/deps/github.com/anacrolix/torrent/undirtied-chunks-iter_test.go
    new file mode 100644
    index 0000000..9ee6ecf
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/undirtied-chunks-iter_test.go
    @@ -0,0 +1,19 @@
    +package torrent
    +
    +import (
    +	"testing"
    +
    +	typedRoaring "github.com/anacrolix/torrent/typed-roaring"
    +)
    +
    +func BenchmarkIterUndirtiedRequestIndexesInPiece(b *testing.B) {
    +	var bitmap typedRoaring.Bitmap[RequestIndex]
    +	it := bitmap.IteratorType()
    +	b.ReportAllocs()
    +	for i := 0; i < b.N; i++ {
    +		// This is the worst case, when Torrent.iterUndirtiedRequestIndexesInPiece can't find a
    +		// usable cached iterator. This should be the only allocation.
    +		it.Initialize(&bitmap)
    +		iterBitmapUnsetInRange(&it, 69, 420, func(RequestIndex) {})
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/url-net-addr.go b/deps/github.com/anacrolix/torrent/url-net-addr.go
    new file mode 100644
    index 0000000..6558e89
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/url-net-addr.go
    @@ -0,0 +1,26 @@
    +package torrent
    +
    +import (
    +	"net"
    +	"net/url"
    +)
    +
    +type urlNetAddr struct {
    +	u *url.URL
    +}
    +
    +func (me urlNetAddr) Network() string {
    +	return me.u.Scheme
    +}
    +
    +func (me urlNetAddr) String() string {
    +	return me.u.Host
    +}
    +
    +func remoteAddrFromUrl(urlStr string) net.Addr {
    +	u, err := url.Parse(urlStr)
    +	if err != nil {
    +		return nil
    +	}
    +	return urlNetAddr{u}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/ut-holepunching.go b/deps/github.com/anacrolix/torrent/ut-holepunching.go
    new file mode 100644
    index 0000000..10cbafc
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/ut-holepunching.go
    @@ -0,0 +1 @@
    +package torrent
    diff --git a/deps/github.com/anacrolix/torrent/ut-holepunching_test.go b/deps/github.com/anacrolix/torrent/ut-holepunching_test.go
    new file mode 100644
    index 0000000..ef7cda6
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/ut-holepunching_test.go
    @@ -0,0 +1,407 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"math/rand"
    +	"net"
    +	"os"
    +	"sync"
    +	"testing"
    +	"testing/iotest"
    +	"time"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/v2/iter"
    +	qt "github.com/frankban/quicktest"
    +	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
    +	"golang.org/x/time/rate"
    +
    +	"github.com/anacrolix/torrent/internal/testutil"
    +)
    +
    +// Check that after completing leeching, a leecher transitions to a seeding
    +// correctly. Connected in a chain like so: Seeder <-> Leecher <-> LeecherLeecher.
    +func TestHolepunchConnect(t *testing.T) {
    +	c := qt.New(t)
    +	greetingTempDir, mi := testutil.GreetingTestTorrent()
    +	defer os.RemoveAll(greetingTempDir)
    +
    +	cfg := TestingConfig(t)
    +	cfg.Seed = true
    +	cfg.MaxAllocPeerRequestDataPerConn = 4
    +	cfg.DataDir = greetingTempDir
    +	cfg.DisablePEX = true
    +	cfg.Debug = true
    +	cfg.AcceptPeerConnections = false
    +	// Listening, even without accepting, still means the leecher-leecher completes the dial to the
    +	// seeder, and so it won't attempt to holepunch.
    +	cfg.DisableTCP = true
    +	// Ensure that responding to holepunch connects don't wait around for the dial limit. We also
    +	// have to allow the initial connection to the leecher though, so it can rendezvous for us.
    +	cfg.DialRateLimiter = rate.NewLimiter(0, 1)
    +	cfg.Logger = cfg.Logger.WithContextText("seeder")
    +	seeder, err := NewClient(cfg)
    +	require.NoError(t, err)
    +	defer seeder.Close()
    +	defer testutil.ExportStatusWriter(seeder, "s", t)()
    +	seederTorrent, ok, err := seeder.AddTorrentSpec(TorrentSpecFromMetaInfo(mi))
    +	require.NoError(t, err)
    +	assert.True(t, ok)
    +	seederTorrent.VerifyData()
    +
    +	cfg = TestingConfig(t)
    +	cfg.Seed = true
    +	cfg.DataDir = t.TempDir()
    +	cfg.AlwaysWantConns = true
    +	cfg.Logger = cfg.Logger.WithContextText("leecher")
    +	// This way the leecher leecher will still try to use this peer as a relay, but won't be told
    +	// about the seeder via PEX.
    +	//cfg.DisablePEX = true
    +	cfg.Debug = true
    +	leecher, err := NewClient(cfg)
    +	require.NoError(t, err)
    +	defer leecher.Close()
    +	defer testutil.ExportStatusWriter(leecher, "l", t)()
    +
    +	cfg = TestingConfig(t)
    +	cfg.Seed = false
    +	cfg.DataDir = t.TempDir()
    +	cfg.MaxAllocPeerRequestDataPerConn = 4
    +	cfg.Debug = true
    +	cfg.NominalDialTimeout = time.Second
    +	cfg.Logger = cfg.Logger.WithContextText("leecher-leecher")
    +	//cfg.DisableUTP = true
    +	leecherLeecher, _ := NewClient(cfg)
    +	require.NoError(t, err)
    +	defer leecherLeecher.Close()
    +	defer testutil.ExportStatusWriter(leecherLeecher, "ll", t)()
    +	leecherGreeting, ok, err := leecher.AddTorrentSpec(func() (ret *TorrentSpec) {
    +		ret = TorrentSpecFromMetaInfo(mi)
    +		ret.ChunkSize = 2
    +		return
    +	}())
    +	_ = leecherGreeting
    +	require.NoError(t, err)
    +	assert.True(t, ok)
    +	llg, ok, err := leecherLeecher.AddTorrentSpec(func() (ret *TorrentSpec) {
    +		ret = TorrentSpecFromMetaInfo(mi)
    +		ret.ChunkSize = 3
    +		return
    +	}())
    +	require.NoError(t, err)
    +	assert.True(t, ok)
    +
    +	var wg sync.WaitGroup
    +	wg.Add(1)
    +	go func() {
    +		defer wg.Done()
    +		r := llg.NewReader()
    +		defer r.Close()
    +		qt.Check(t, iotest.TestReader(r, []byte(testutil.GreetingFileContents)), qt.IsNil)
    +	}()
    +	go seederTorrent.AddClientPeer(leecher)
    +	waitForConns(seederTorrent)
    +	go llg.AddClientPeer(leecher)
    +	waitForConns(llg)
    +	time.Sleep(time.Second)
    +	llg.cl.lock()
    +	targetAddr := seeder.ListenAddrs()[0]
    +	log.Printf("trying to initiate to %v", targetAddr)
    +	initiateConn(outgoingConnOpts{
    +		peerInfo: PeerInfo{
    +			Addr: targetAddr,
    +		},
    +		t:                       llg,
    +		requireRendezvous:       true,
    +		skipHolepunchRendezvous: false,
    +		HeaderObfuscationPolicy: llg.cl.config.HeaderObfuscationPolicy,
    +	}, true)
    +	llg.cl.unlock()
    +	wg.Wait()
    +
    +	c.Check(seeder.dialedSuccessfullyAfterHolepunchConnect, qt.Not(qt.HasLen), 0)
    +	c.Check(leecherLeecher.probablyOnlyConnectedDueToHolepunch, qt.Not(qt.HasLen), 0)
    +
    +	llClientStats := leecherLeecher.Stats()
    +	c.Check(llClientStats.NumPeersUndialableWithoutHolepunch, qt.Not(qt.Equals), 0)
    +	c.Check(llClientStats.NumPeersUndialableWithoutHolepunchDialedAfterHolepunchConnect, qt.Not(qt.Equals), 0)
    +	c.Check(llClientStats.NumPeersProbablyOnlyConnectedDueToHolepunch, qt.Not(qt.Equals), 0)
    +}
    +
    +func waitForConns(t *Torrent) {
    +	t.cl.lock()
    +	defer t.cl.unlock()
    +	for {
    +		for range t.conns {
    +			return
    +		}
    +		t.cl.event.Wait()
    +	}
    +}
    +
    +// Show that dialling TCP will complete before the other side accepts.
    +func TestDialTcpNotAccepting(t *testing.T) {
    +	l, err := net.Listen("tcp", "localhost:0")
    +	c := qt.New(t)
    +	c.Check(err, qt.IsNil)
    +	defer l.Close()
    +	dialedConn, err := net.Dial("tcp", l.Addr().String())
    +	c.Assert(err, qt.IsNil)
    +	dialedConn.Close()
    +}
    +
    +func TestTcpSimultaneousOpen(t *testing.T) {
    +	const network = "tcp"
    +	ctx := context.Background()
    +	makeDialer := func(localPort int, remoteAddr string) func() (net.Conn, error) {
    +		dialer := net.Dialer{
    +			LocalAddr: &net.TCPAddr{
    +				//IP:   net.IPv6loopback,
    +				Port: localPort,
    +			},
    +		}
    +		return func() (net.Conn, error) {
    +			return dialer.DialContext(ctx, network, remoteAddr)
    +		}
    +	}
    +	c := qt.New(t)
    +	// I really hate doing this in unit tests, but we would need to pick apart Dialer to get
    +	// perfectly synchronized simultaneous dials.
    +	for range iter.N(10) {
    +		first, second := randPortPair()
    +		t.Logf("ports are %v and %v", first, second)
    +		err := testSimultaneousOpen(
    +			c.Cleanup,
    +			makeDialer(first, fmt.Sprintf("localhost:%d", second)),
    +			makeDialer(second, fmt.Sprintf("localhost:%d", first)),
    +		)
    +		if err == nil {
    +			return
    +		}
    +		// This proves that the connections are not the same.
    +		if errors.Is(err, errMsgNotReceived) {
    +			t.Fatal(err)
    +		}
    +		// Could be a timing issue, so try again.
    +		t.Log(err)
    +	}
    +	// If we weren't able to get a simultaneous dial to occur, then we can't call it a failure.
    +	t.Skip("couldn't synchronize dials")
    +}
    +
    +func randIntInRange(low, high int) int {
    +	return rand.Intn(high-low+1) + low
    +}
    +
    +func randDynamicPort() int {
    +	return randIntInRange(49152, 65535)
    +}
    +
    +func randPortPair() (first int, second int) {
    +	first = randDynamicPort()
    +	for {
    +		second = randDynamicPort()
    +		if second != first {
    +			return
    +		}
    +	}
    +}
    +
    +func writeMsg(conn net.Conn) {
    +	conn.Write([]byte(defaultMsg))
    +	// Writing must be closed so the reader will get EOF and stop reading.
    +	conn.Close()
    +}
    +
    +func readMsg(conn net.Conn) error {
    +	msgBytes, err := io.ReadAll(conn)
    +	if err != nil {
    +		return err
    +	}
    +	msgStr := string(msgBytes)
    +	if msgStr != defaultMsg {
    +		return fmt.Errorf("read %q", msgStr)
    +	}
    +	return nil
    +}
    +
    +var errMsgNotReceived = errors.New("msg not received in time")
    +
    +// Runs two dialers simultaneously, then sends a message on one connection and check it reads from
    +// the other, thereby showing that both dials obtained endpoints to the same connection.
    +func testSimultaneousOpen(
    +	cleanup func(func()),
    +	firstDialer, secondDialer func() (net.Conn, error),
    +) error {
    +	errs := make(chan error)
    +	var dialsDone sync.WaitGroup
    +	const numDials = 2
    +	dialsDone.Add(numDials)
    +	signal := make(chan struct{})
    +	var dialersDone sync.WaitGroup
    +	dialersDone.Add(numDials)
    +	doDial := func(
    +		dialer func() (net.Conn, error),
    +		onSignal func(net.Conn),
    +	) {
    +		defer dialersDone.Done()
    +		conn, err := dialer()
    +		dialsDone.Done()
    +		errs <- err
    +		if err != nil {
    +			return
    +		}
    +		cleanup(func() {
    +			conn.Close()
    +		})
    +		<-signal
    +		onSignal(conn)
    +		//if err == nil {
    +		//	conn.Close()
    +		//}
    +	}
    +	go doDial(
    +		firstDialer,
    +		func(conn net.Conn) {
    +			writeMsg(conn)
    +			errs <- nil
    +		},
    +	)
    +	go doDial(
    +		secondDialer,
    +		func(conn net.Conn) {
    +			gotMsg := make(chan error, 1)
    +			go func() {
    +				gotMsg <- readMsg(conn)
    +			}()
    +			select {
    +			case err := <-gotMsg:
    +				errs <- err
    +			case <-time.After(time.Second):
    +				errs <- errMsgNotReceived
    +			}
    +		},
    +	)
    +	dialsDone.Wait()
    +	for range iter.N(numDials) {
    +		err := <-errs
    +		if err != nil {
    +			return err
    +		}
    +	}
    +	close(signal)
    +	for range iter.N(numDials) {
    +		err := <-errs
    +		if err != nil {
    +			return err
    +		}
    +	}
    +	dialersDone.Wait()
    +	return nil
    +}
    +
    +const defaultMsg = "hello"
    +
    +// Show that uTP doesn't implement simultaneous open. When two sockets dial each other, they both
    +// get separate connections. This means that holepunch connect may result in an accept (and dial)
    +// for one or both peers involved.
    +func TestUtpSimultaneousOpen(t *testing.T) {
    +	t.Parallel()
    +	c := qt.New(t)
    +	const network = "udp"
    +	ctx := context.Background()
    +	newUtpSocket := func(addr string) utpSocket {
    +		socket, err := NewUtpSocket(
    +			network,
    +			addr,
    +			func(net.Addr) bool {
    +				return false
    +			},
    +			log.Default,
    +		)
    +		c.Assert(err, qt.IsNil)
    +		return socket
    +	}
    +	first := newUtpSocket("localhost:0")
    +	defer first.Close()
    +	second := newUtpSocket("localhost:0")
    +	defer second.Close()
    +	getDial := func(sock utpSocket, addr string) func() (net.Conn, error) {
    +		return func() (net.Conn, error) {
    +			return sock.DialContext(ctx, network, addr)
    +		}
    +	}
    +	t.Logf("first addr is %v. second addr is %v", first.Addr().String(), second.Addr().String())
    +	for range iter.N(10) {
    +		err := testSimultaneousOpen(
    +			c.Cleanup,
    +			getDial(first, second.Addr().String()),
    +			getDial(second, first.Addr().String()),
    +		)
    +		if err == nil {
    +			t.Fatal("expected utp to fail simultaneous open")
    +		}
    +		if errors.Is(err, errMsgNotReceived) {
    +			return
    +		}
    +		skipGoUtpDialIssue(t, err)
    +		t.Log(err)
    +		time.Sleep(time.Second)
    +	}
    +	t.FailNow()
    +}
    +
    +func writeAndReadMsg(r, w net.Conn) error {
    +	go writeMsg(w)
    +	return readMsg(r)
    +}
    +
    +func skipGoUtpDialIssue(t *testing.T, err error) {
    +	if err.Error() == "timed out waiting for ack" {
    +		t.Skip("anacrolix go utp implementation has issues. Use anacrolix/go-libutp by enabling CGO.")
    +	}
    +}
    +
    +// Show that dialling one socket and accepting from the other results in them having ends of the
    +// same connection.
    +func TestUtpDirectDialMsg(t *testing.T) {
    +	t.Parallel()
    +	c := qt.New(t)
    +	const network = "udp4"
    +	ctx := context.Background()
    +	newUtpSocket := func(addr string) utpSocket {
    +		socket, err := NewUtpSocket(network, addr, func(net.Addr) bool {
    +			return false
    +		}, log.Default)
    +		c.Assert(err, qt.IsNil)
    +		return socket
    +	}
    +	for range iter.N(10) {
    +		err := func() error {
    +			first := newUtpSocket("localhost:0")
    +			defer first.Close()
    +			second := newUtpSocket("localhost:0")
    +			defer second.Close()
    +			writer, err := first.DialContext(ctx, network, second.Addr().String())
    +			if err != nil {
    +				return err
    +			}
    +			defer writer.Close()
    +			reader, err := second.Accept()
    +			defer reader.Close()
    +			c.Assert(err, qt.IsNil)
    +			return writeAndReadMsg(reader, writer)
    +		}()
    +		if err == nil {
    +			return
    +		}
    +		skipGoUtpDialIssue(t, err)
    +		t.Log(err)
    +		time.Sleep(time.Second)
    +	}
    +	t.FailNow()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/util/dirwatch/dirwatch.go b/deps/github.com/anacrolix/torrent/util/dirwatch/dirwatch.go
    new file mode 100644
    index 0000000..f617aee
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/util/dirwatch/dirwatch.go
    @@ -0,0 +1,215 @@
    +// Package dirwatch provides filesystem-notification based tracking of torrent
    +// info files and magnet URIs in a directory.
    +package dirwatch
    +
    +import (
    +	"bufio"
    +	"os"
    +	"path/filepath"
    +	"strings"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/v2"
    +	"github.com/fsnotify/fsnotify"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type Change uint
    +
    +const (
    +	Added Change = iota
    +	Removed
    +)
    +
    +type Event struct {
    +	MagnetURI string
    +	Change
    +	TorrentFilePath string
    +	InfoHash        metainfo.Hash
    +}
    +
    +type entity struct {
    +	metainfo.Hash
    +	MagnetURI       string
    +	TorrentFilePath string
    +}
    +
    +type Instance struct {
    +	w        *fsnotify.Watcher
    +	dirName  string
    +	Events   chan Event
    +	dirState map[metainfo.Hash]entity
    +	Logger   log.Logger
    +}
    +
    +func (i *Instance) Close() {
    +	i.w.Close()
    +}
    +
    +func (i *Instance) handleEvents() {
    +	defer close(i.Events)
    +	for e := range i.w.Events {
    +		i.Logger.WithDefaultLevel(log.Debug).Printf("event: %v", e)
    +		if e.Op == fsnotify.Write {
    +			// TODO: Special treatment as an existing torrent may have changed.
    +		} else {
    +			i.refresh()
    +		}
    +	}
    +}
    +
    +func (i *Instance) handleErrors() {
    +	for err := range i.w.Errors {
    +		log.Printf("error in torrent directory watcher: %s", err)
    +	}
    +}
    +
    +func torrentFileInfoHash(fileName string) (ih metainfo.Hash, ok bool) {
    +	mi, _ := metainfo.LoadFromFile(fileName)
    +	if mi == nil {
    +		return
    +	}
    +	ih = mi.HashInfoBytes()
    +	ok = true
    +	return
    +}
    +
    +func scanDir(dirName string) (ee map[metainfo.Hash]entity) {
    +	d, err := os.Open(dirName)
    +	if err != nil {
    +		log.Print(err)
    +		return
    +	}
    +	defer d.Close()
    +	names, err := d.Readdirnames(-1)
    +	if err != nil {
    +		log.Print(err)
    +		return
    +	}
    +	ee = make(map[metainfo.Hash]entity, len(names))
    +	addEntity := func(e entity) {
    +		e0, ok := ee[e.Hash]
    +		if ok {
    +			if e0.MagnetURI == "" || len(e.MagnetURI) < len(e0.MagnetURI) {
    +				return
    +			}
    +		}
    +		ee[e.Hash] = e
    +	}
    +	for _, n := range names {
    +		fullName := filepath.Join(dirName, n)
    +		switch filepath.Ext(n) {
    +		case ".torrent":
    +			ih, ok := torrentFileInfoHash(fullName)
    +			if !ok {
    +				break
    +			}
    +			e := entity{
    +				TorrentFilePath: fullName,
    +			}
    +			missinggo.CopyExact(&e.Hash, ih)
    +			addEntity(e)
    +		case ".magnet":
    +			uris, err := magnetFileURIs(fullName)
    +			if err != nil {
    +				log.Print(err)
    +				break
    +			}
    +			for _, uri := range uris {
    +				m, err := metainfo.ParseMagnetUri(uri)
    +				if err != nil {
    +					log.Printf("error parsing %q in file %q: %s", uri, fullName, err)
    +					continue
    +				}
    +				addEntity(entity{
    +					Hash:      m.InfoHash,
    +					MagnetURI: uri,
    +				})
    +			}
    +		}
    +	}
    +	return
    +}
    +
    +func magnetFileURIs(name string) (uris []string, err error) {
    +	f, err := os.Open(name)
    +	if err != nil {
    +		return
    +	}
    +	defer f.Close()
    +	scanner := bufio.NewScanner(f)
    +	scanner.Split(bufio.ScanWords)
    +	for scanner.Scan() {
    +		// Allow magnet URIs to be "commented" out.
    +		if strings.HasPrefix(scanner.Text(), "#") {
    +			continue
    +		}
    +		uris = append(uris, scanner.Text())
    +	}
    +	err = scanner.Err()
    +	return
    +}
    +
    +func (i *Instance) torrentRemoved(ih metainfo.Hash) {
    +	i.Events <- Event{
    +		InfoHash: ih,
    +		Change:   Removed,
    +	}
    +}
    +
    +func (i *Instance) torrentAdded(e entity) {
    +	i.Events <- Event{
    +		InfoHash:        e.Hash,
    +		Change:          Added,
    +		MagnetURI:       e.MagnetURI,
    +		TorrentFilePath: e.TorrentFilePath,
    +	}
    +}
    +
    +func (i *Instance) refresh() {
    +	_new := scanDir(i.dirName)
    +	old := i.dirState
    +	for ih := range old {
    +		_, ok := _new[ih]
    +		if !ok {
    +			i.torrentRemoved(ih)
    +		}
    +	}
    +	for ih, newE := range _new {
    +		oldE, ok := old[ih]
    +		if ok {
    +			if newE == oldE {
    +				continue
    +			}
    +			i.torrentRemoved(ih)
    +		}
    +		i.torrentAdded(newE)
    +	}
    +	i.dirState = _new
    +}
    +
    +func New(dirName string) (i *Instance, err error) {
    +	w, err := fsnotify.NewWatcher()
    +	if err != nil {
    +		return
    +	}
    +	err = w.Add(dirName)
    +	if err != nil {
    +		w.Close()
    +		return
    +	}
    +	i = &Instance{
    +		w:        w,
    +		dirName:  dirName,
    +		Events:   make(chan Event),
    +		dirState: make(map[metainfo.Hash]entity),
    +		Logger:   log.Default,
    +	}
    +	go func() {
    +		i.refresh()
    +		go i.handleEvents()
    +		go i.handleErrors()
    +	}()
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/util/dirwatch/dirwatch_test.go b/deps/github.com/anacrolix/torrent/util/dirwatch/dirwatch_test.go
    new file mode 100644
    index 0000000..0447599
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/util/dirwatch/dirwatch_test.go
    @@ -0,0 +1,15 @@
    +package dirwatch
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/require"
    +)
    +
    +func TestDirwatch(t *testing.T) {
    +	tempDirName := t.TempDir()
    +	t.Logf("tempdir: %q", tempDirName)
    +	dw, err := New(tempDirName)
    +	require.NoError(t, err)
    +	defer dw.Close()
    +}
    diff --git a/deps/github.com/anacrolix/torrent/utp.go b/deps/github.com/anacrolix/torrent/utp.go
    new file mode 100644
    index 0000000..3066ca0
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/utp.go
    @@ -0,0 +1,18 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"net"
    +)
    +
    +// Abstracts the utp Socket, so the implementation can be selected from
    +// different packages.
    +type utpSocket interface {
    +	net.PacketConn
    +	// net.Listener, but we can't have duplicate Close.
    +	Accept() (net.Conn, error)
    +	Addr() net.Addr
    +	// net.Dialer but there's no interface.
    +	DialContext(ctx context.Context, network, addr string) (net.Conn, error)
    +	// Dial(addr string) (net.Conn, error)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/utp_go.go b/deps/github.com/anacrolix/torrent/utp_go.go
    new file mode 100644
    index 0000000..1e60f82
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/utp_go.go
    @@ -0,0 +1,18 @@
    +//go:build !cgo || disable_libutp
    +// +build !cgo disable_libutp
    +
    +package torrent
    +
    +import (
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/utp"
    +)
    +
    +func NewUtpSocket(network, addr string, _ firewallCallback, _ log.Logger) (utpSocket, error) {
    +	s, err := utp.NewSocket(network, addr)
    +	if s == nil {
    +		return nil, err
    +	} else {
    +		return s, err
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/utp_libutp.go b/deps/github.com/anacrolix/torrent/utp_libutp.go
    new file mode 100644
    index 0000000..6da9402
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/utp_libutp.go
    @@ -0,0 +1,23 @@
    +//go:build cgo && !disable_libutp
    +// +build cgo,!disable_libutp
    +
    +package torrent
    +
    +import (
    +	utp "github.com/anacrolix/go-libutp"
    +	"github.com/anacrolix/log"
    +)
    +
    +func NewUtpSocket(network, addr string, fc firewallCallback, logger log.Logger) (utpSocket, error) {
    +	s, err := utp.NewSocket(network, addr, utp.WithLogger(logger))
    +	if s == nil {
    +		return nil, err
    +	}
    +	if err != nil {
    +		return s, err
    +	}
    +	if fc != nil {
    +		s.SetSyncFirewallCallback(utp.FirewallCallback(fc))
    +	}
    +	return s, err
    +}
    diff --git a/deps/github.com/anacrolix/torrent/utp_test.go b/deps/github.com/anacrolix/torrent/utp_test.go
    new file mode 100644
    index 0000000..18d62ca
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/utp_test.go
    @@ -0,0 +1,16 @@
    +package torrent
    +
    +import (
    +	"testing"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/stretchr/testify/assert"
    +)
    +
    +func TestNewUtpSocketErrorNilInterface(t *testing.T) {
    +	s, err := NewUtpSocket("fix", "your:language", nil, log.Default)
    +	assert.Error(t, err)
    +	if s != nil {
    +		t.Fatalf("expected nil, got %#v", s)
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/version/version.go b/deps/github.com/anacrolix/torrent/version/version.go
    new file mode 100644
    index 0000000..66483b6
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/version/version.go
    @@ -0,0 +1,61 @@
    +// Package version provides default versions, user-agents etc. for client identification.
    +package version
    +
    +import (
    +	"fmt"
    +	"reflect"
    +	"runtime/debug"
    +	"strings"
    +)
    +
    +var (
    +	DefaultExtendedHandshakeClientVersion string
    +	// This should be updated when client behaviour changes in a way that other peers could care
    +	// about.
    +	DefaultBep20Prefix   = "-GT0003-"
    +	DefaultHttpUserAgent string
    +	DefaultUpnpId        string
    +)
    +
    +func init() {
    +	const (
    +		longNamespace   = "anacrolix"
    +		longPackageName = "torrent"
    +	)
    +	type Newtype struct{}
    +	var newtype Newtype
    +	thisPkg := reflect.TypeOf(newtype).PkgPath()
    +	var (
    +		mainPath       = "unknown"
    +		mainVersion    = "unknown"
    +		torrentVersion = "unknown"
    +	)
    +	if buildInfo, ok := debug.ReadBuildInfo(); ok {
    +		mainPath = buildInfo.Main.Path
    +		mainVersion = buildInfo.Main.Version
    +		thisModule := ""
    +		// Note that if the main module is the same as this module, we get a version of "(devel)".
    +		for _, dep := range append(buildInfo.Deps, &buildInfo.Main) {
    +			if strings.HasPrefix(thisPkg, dep.Path) && len(dep.Path) >= len(thisModule) {
    +				thisModule = dep.Path
    +				torrentVersion = dep.Version
    +			}
    +		}
    +	}
    +	DefaultExtendedHandshakeClientVersion = fmt.Sprintf(
    +		"%v %v (%v/%v %v)",
    +		mainPath,
    +		mainVersion,
    +		longNamespace,
    +		longPackageName,
    +		torrentVersion,
    +	)
    +	DefaultUpnpId = fmt.Sprintf("%v %v", mainPath, mainVersion)
    +	// Per https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent#library_and_net_tool_ua_strings
    +	DefaultHttpUserAgent = fmt.Sprintf(
    +		"%v-%v/%v",
    +		longNamespace,
    +		longPackageName,
    +		torrentVersion,
    +	)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webrtc.go b/deps/github.com/anacrolix/torrent/webrtc.go
    new file mode 100644
    index 0000000..ca4f80f
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webrtc.go
    @@ -0,0 +1,85 @@
    +package torrent
    +
    +import (
    +	"net"
    +	"strconv"
    +	"time"
    +
    +	"github.com/pion/datachannel"
    +	"github.com/pion/webrtc/v3"
    +	"go.opentelemetry.io/otel"
    +	"go.opentelemetry.io/otel/attribute"
    +	"go.opentelemetry.io/otel/trace"
    +
    +	"github.com/anacrolix/torrent/webtorrent"
    +)
    +
    +const webrtcNetwork = "webrtc"
    +
    +type webrtcNetConn struct {
    +	datachannel.ReadWriteCloser
    +	webtorrent.DataChannelContext
    +}
    +
    +type webrtcNetAddr struct {
    +	*webrtc.ICECandidate
    +}
    +
    +var _ net.Addr = webrtcNetAddr{}
    +
    +func (webrtcNetAddr) Network() string {
    +	// Now that we have the ICE candidate, we can tell if it's over udp or tcp. But should we use
    +	// that for the network?
    +	return webrtcNetwork
    +}
    +
    +func (me webrtcNetAddr) String() string {
    +	return net.JoinHostPort(me.Address, strconv.FormatUint(uint64(me.Port), 10))
    +}
    +
    +func (me webrtcNetConn) LocalAddr() net.Addr {
    +	// I'm not sure if this evolves over time. It might also be unavailable if the PeerConnection is
    +	// closed or closes itself. The same concern applies to RemoteAddr.
    +	pair, err := me.DataChannelContext.GetSelectedIceCandidatePair()
    +	if err != nil {
    +		panic(err)
    +	}
    +	return webrtcNetAddr{pair.Local}
    +}
    +
    +func (me webrtcNetConn) RemoteAddr() net.Addr {
    +	// See comments on LocalAddr.
    +	pair, err := me.DataChannelContext.GetSelectedIceCandidatePair()
    +	if err != nil {
    +		panic(err)
    +	}
    +	return webrtcNetAddr{pair.Remote}
    +}
    +
    +// Do we need these for WebRTC connections exposed as net.Conns? Can we set them somewhere inside
    +// PeerConnection or on the channel or some transport?
    +
    +func (w webrtcNetConn) SetDeadline(t time.Time) error {
    +	w.Span.AddEvent("SetDeadline", trace.WithAttributes(attribute.String("time", t.String())))
    +	return nil
    +}
    +
    +func (w webrtcNetConn) SetReadDeadline(t time.Time) error {
    +	w.Span.AddEvent("SetReadDeadline", trace.WithAttributes(attribute.String("time", t.String())))
    +	return nil
    +}
    +
    +func (w webrtcNetConn) SetWriteDeadline(t time.Time) error {
    +	w.Span.AddEvent("SetWriteDeadline", trace.WithAttributes(attribute.String("time", t.String())))
    +	return nil
    +}
    +
    +func (w webrtcNetConn) Read(b []byte) (n int, err error) {
    +	_, span := otel.Tracer(tracerName).Start(w.Context, "Read")
    +	defer span.End()
    +	span.SetAttributes(attribute.Int("buf_len", len(b)))
    +	n, err = w.ReadWriteCloser.Read(b)
    +	span.RecordError(err)
    +	span.SetAttributes(attribute.Int("bytes_read", n))
    +	return
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webseed-peer.go b/deps/github.com/anacrolix/torrent/webseed-peer.go
    new file mode 100644
    index 0000000..5b6632b
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webseed-peer.go
    @@ -0,0 +1,222 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"errors"
    +	"fmt"
    +	"math/rand"
    +	"sync"
    +	"time"
    +
    +	"github.com/RoaringBitmap/roaring"
    +	"github.com/anacrolix/log"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +	pp "github.com/anacrolix/torrent/peer_protocol"
    +	"github.com/anacrolix/torrent/webseed"
    +)
    +
    +const (
    +	webseedPeerUnhandledErrorSleep   = 5 * time.Second
    +	webseedPeerCloseOnUnhandledError = false
    +)
    +
    +type webseedPeer struct {
    +	// First field for stats alignment.
    +	peer             Peer
    +	client           webseed.Client
    +	activeRequests   map[Request]webseed.Request
    +	requesterCond    sync.Cond
    +	lastUnhandledErr time.Time
    +}
    +
    +var _ peerImpl = (*webseedPeer)(nil)
    +
    +func (me *webseedPeer) peerImplStatusLines() []string {
    +	return []string{
    +		me.client.Url,
    +		fmt.Sprintf("last unhandled error: %v", eventAgeString(me.lastUnhandledErr)),
    +	}
    +}
    +
    +func (ws *webseedPeer) String() string {
    +	return fmt.Sprintf("webseed peer for %q", ws.client.Url)
    +}
    +
    +func (ws *webseedPeer) onGotInfo(info *metainfo.Info) {
    +	ws.client.SetInfo(info)
    +	// There should be probably be a callback in Client instead, so it can remove pieces at its whim
    +	// too.
    +	ws.client.Pieces.Iterate(func(x uint32) bool {
    +		ws.peer.t.incPieceAvailability(pieceIndex(x))
    +		return true
    +	})
    +}
    +
    +func (ws *webseedPeer) writeInterested(interested bool) bool {
    +	return true
    +}
    +
    +func (ws *webseedPeer) _cancel(r RequestIndex) bool {
    +	if active, ok := ws.activeRequests[ws.peer.t.requestIndexToRequest(r)]; ok {
    +		active.Cancel()
    +		// The requester is running and will handle the result.
    +		return true
    +	}
    +	// There should be no requester handling this, so no further events will occur.
    +	return false
    +}
    +
    +func (ws *webseedPeer) intoSpec(r Request) webseed.RequestSpec {
    +	return webseed.RequestSpec{ws.peer.t.requestOffset(r), int64(r.Length)}
    +}
    +
    +func (ws *webseedPeer) _request(r Request) bool {
    +	ws.requesterCond.Signal()
    +	return true
    +}
    +
    +func (ws *webseedPeer) doRequest(r Request) error {
    +	webseedRequest := ws.client.NewRequest(ws.intoSpec(r))
    +	ws.activeRequests[r] = webseedRequest
    +	err := func() error {
    +		ws.requesterCond.L.Unlock()
    +		defer ws.requesterCond.L.Lock()
    +		return ws.requestResultHandler(r, webseedRequest)
    +	}()
    +	delete(ws.activeRequests, r)
    +	return err
    +}
    +
    +func (ws *webseedPeer) requester(i int) {
    +	ws.requesterCond.L.Lock()
    +	defer ws.requesterCond.L.Unlock()
    +start:
    +	for !ws.peer.closed.IsSet() {
    +		// Restart is set if we don't need to wait for the requestCond before trying again.
    +		restart := false
    +		ws.peer.requestState.Requests.Iterate(func(x RequestIndex) bool {
    +			r := ws.peer.t.requestIndexToRequest(x)
    +			if _, ok := ws.activeRequests[r]; ok {
    +				return true
    +			}
    +			err := ws.doRequest(r)
    +			ws.requesterCond.L.Unlock()
    +			if err != nil && !errors.Is(err, context.Canceled) {
    +				log.Printf("requester %v: error doing webseed request %v: %v", i, r, err)
    +			}
    +			restart = true
    +			if errors.Is(err, webseed.ErrTooFast) {
    +				time.Sleep(time.Duration(rand.Int63n(int64(10 * time.Second))))
    +			}
    +			// Demeter is throwing a tantrum on Mount Olympus for this
    +			ws.peer.t.cl.locker().RLock()
    +			duration := time.Until(ws.lastUnhandledErr.Add(webseedPeerUnhandledErrorSleep))
    +			ws.peer.t.cl.locker().RUnlock()
    +			time.Sleep(duration)
    +			ws.requesterCond.L.Lock()
    +			return false
    +		})
    +		if restart {
    +			goto start
    +		}
    +		ws.requesterCond.Wait()
    +	}
    +}
    +
    +func (ws *webseedPeer) connectionFlags() string {
    +	return "WS"
    +}
    +
    +// Maybe this should drop all existing connections, or something like that.
    +func (ws *webseedPeer) drop() {}
    +
    +func (cn *webseedPeer) ban() {
    +	cn.peer.close()
    +}
    +
    +func (ws *webseedPeer) handleUpdateRequests() {
    +	// Because this is synchronous, webseed peers seem to get first dibs on newly prioritized
    +	// pieces.
    +	go func() {
    +		ws.peer.t.cl.lock()
    +		defer ws.peer.t.cl.unlock()
    +		ws.peer.maybeUpdateActualRequestState()
    +	}()
    +}
    +
    +func (ws *webseedPeer) onClose() {
    +	ws.peer.logger.Levelf(log.Debug, "closing")
    +	// Just deleting them means we would have to manually cancel active requests.
    +	ws.peer.cancelAllRequests()
    +	ws.peer.t.iterPeers(func(p *Peer) {
    +		if p.isLowOnRequests() {
    +			p.updateRequests("webseedPeer.onClose")
    +		}
    +	})
    +	ws.requesterCond.Broadcast()
    +}
    +
    +func (ws *webseedPeer) requestResultHandler(r Request, webseedRequest webseed.Request) error {
    +	result := <-webseedRequest.Result
    +	close(webseedRequest.Result) // one-shot
    +	// We do this here rather than inside receiveChunk, since we want to count errors too. I'm not
    +	// sure if we can divine which errors indicate cancellation on our end without hitting the
    +	// network though.
    +	if len(result.Bytes) != 0 || result.Err == nil {
    +		// Increment ChunksRead and friends
    +		ws.peer.doChunkReadStats(int64(len(result.Bytes)))
    +	}
    +	ws.peer.readBytes(int64(len(result.Bytes)))
    +	ws.peer.t.cl.lock()
    +	defer ws.peer.t.cl.unlock()
    +	if ws.peer.t.closed.IsSet() {
    +		return nil
    +	}
    +	err := result.Err
    +	if err != nil {
    +		switch {
    +		case errors.Is(err, context.Canceled):
    +		case errors.Is(err, webseed.ErrTooFast):
    +		case ws.peer.closed.IsSet():
    +		default:
    +			ws.peer.logger.Printf("Request %v rejected: %v", r, result.Err)
    +			// // Here lies my attempt to extract something concrete from Go's error system. RIP.
    +			// cfg := spew.NewDefaultConfig()
    +			// cfg.DisableMethods = true
    +			// cfg.Dump(result.Err)
    +
    +			if webseedPeerCloseOnUnhandledError {
    +				log.Printf("closing %v", ws)
    +				ws.peer.close()
    +			} else {
    +				ws.lastUnhandledErr = time.Now()
    +			}
    +		}
    +		if !ws.peer.remoteRejectedRequest(ws.peer.t.requestIndexFromRequest(r)) {
    +			panic("invalid reject")
    +		}
    +		return err
    +	}
    +	err = ws.peer.receiveChunk(&pp.Message{
    +		Type:  pp.Piece,
    +		Index: r.Index,
    +		Begin: r.Begin,
    +		Piece: result.Bytes,
    +	})
    +	if err != nil {
    +		panic(err)
    +	}
    +	return err
    +}
    +
    +func (me *webseedPeer) peerPieces() *roaring.Bitmap {
    +	return &me.client.Pieces
    +}
    +
    +func (cn *webseedPeer) peerHasAllPieces() (all, known bool) {
    +	if !cn.peer.t.haveInfo() {
    +		return true, false
    +	}
    +	return cn.client.Pieces.GetCardinality() == uint64(cn.peer.t.numPieces()), true
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webseed/client.go b/deps/github.com/anacrolix/torrent/webseed/client.go
    new file mode 100644
    index 0000000..ac42b8a
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webseed/client.go
    @@ -0,0 +1,206 @@
    +package webseed
    +
    +import (
    +	"bytes"
    +	"context"
    +	"errors"
    +	"fmt"
    +	"io"
    +	"log"
    +	"net/http"
    +	"strings"
    +
    +	"github.com/RoaringBitmap/roaring"
    +
    +	"github.com/anacrolix/torrent/common"
    +	"github.com/anacrolix/torrent/metainfo"
    +	"github.com/anacrolix/torrent/segments"
    +)
    +
    +type RequestSpec = segments.Extent
    +
    +type requestPartResult struct {
    +	resp *http.Response
    +	err  error
    +}
    +
    +type requestPart struct {
    +	req    *http.Request
    +	e      segments.Extent
    +	result chan requestPartResult
    +	start  func()
    +	// Wrap http response bodies for such things as download rate limiting.
    +	responseBodyWrapper ResponseBodyWrapper
    +}
    +
    +type Request struct {
    +	cancel func()
    +	Result chan RequestResult
    +}
    +
    +func (r Request) Cancel() {
    +	r.cancel()
    +}
    +
    +type Client struct {
    +	HttpClient *http.Client
    +	Url        string
    +	fileIndex  segments.Index
    +	info       *metainfo.Info
    +	// The pieces we can request with the Url. We're more likely to ban/block at the file-level
    +	// given that's how requests are mapped to webseeds, but the torrent.Client works at the piece
    +	// level. We can map our file-level adjustments to the pieces here. This probably need to be
    +	// private in the future, if Client ever starts removing pieces.
    +	Pieces              roaring.Bitmap
    +	ResponseBodyWrapper ResponseBodyWrapper
    +	PathEscaper         PathEscaper
    +}
    +
    +type ResponseBodyWrapper func(io.Reader) io.Reader
    +
    +func (me *Client) SetInfo(info *metainfo.Info) {
    +	if !strings.HasSuffix(me.Url, "/") && info.IsDir() {
    +		// In my experience, this is a non-conforming webseed. For example the
    +		// http://ia600500.us.archive.org/1/items URLs in archive.org torrents.
    +		return
    +	}
    +	me.fileIndex = segments.NewIndex(common.LengthIterFromUpvertedFiles(info.UpvertedFiles()))
    +	me.info = info
    +	me.Pieces.AddRange(0, uint64(info.NumPieces()))
    +}
    +
    +type RequestResult struct {
    +	Bytes []byte
    +	Err   error
    +}
    +
    +func (ws *Client) NewRequest(r RequestSpec) Request {
    +	ctx, cancel := context.WithCancel(context.Background())
    +	var requestParts []requestPart
    +	if !ws.fileIndex.Locate(r, func(i int, e segments.Extent) bool {
    +		req, err := newRequest(
    +			ws.Url, i, ws.info, e.Start, e.Length,
    +			ws.PathEscaper,
    +		)
    +		if err != nil {
    +			panic(err)
    +		}
    +		req = req.WithContext(ctx)
    +		part := requestPart{
    +			req:                 req,
    +			result:              make(chan requestPartResult, 1),
    +			e:                   e,
    +			responseBodyWrapper: ws.ResponseBodyWrapper,
    +		}
    +		part.start = func() {
    +			go func() {
    +				resp, err := ws.HttpClient.Do(req)
    +				part.result <- requestPartResult{
    +					resp: resp,
    +					err:  err,
    +				}
    +			}()
    +		}
    +		requestParts = append(requestParts, part)
    +		return true
    +	}) {
    +		panic("request out of file bounds")
    +	}
    +	req := Request{
    +		cancel: cancel,
    +		Result: make(chan RequestResult, 1),
    +	}
    +	go func() {
    +		b, err := readRequestPartResponses(ctx, requestParts)
    +		req.Result <- RequestResult{
    +			Bytes: b,
    +			Err:   err,
    +		}
    +	}()
    +	return req
    +}
    +
    +type ErrBadResponse struct {
    +	Msg      string
    +	Response *http.Response
    +}
    +
    +func (me ErrBadResponse) Error() string {
    +	return me.Msg
    +}
    +
    +func recvPartResult(ctx context.Context, buf io.Writer, part requestPart) error {
    +	result := <-part.result
    +	// Make sure there's no further results coming, it should be a one-shot channel.
    +	close(part.result)
    +	if result.err != nil {
    +		return result.err
    +	}
    +	defer result.resp.Body.Close()
    +	var body io.Reader = result.resp.Body
    +	if part.responseBodyWrapper != nil {
    +		body = part.responseBodyWrapper(body)
    +	}
    +	// Prevent further accidental use
    +	result.resp.Body = nil
    +	if ctx.Err() != nil {
    +		return ctx.Err()
    +	}
    +	switch result.resp.StatusCode {
    +	case http.StatusPartialContent:
    +		copied, err := io.Copy(buf, body)
    +		if err != nil {
    +			return err
    +		}
    +		if copied != part.e.Length {
    +			return fmt.Errorf("got %v bytes, expected %v", copied, part.e.Length)
    +		}
    +		return nil
    +	case http.StatusOK:
    +		// This number is based on
    +		// https://archive.org/download/BloodyPitOfHorror/BloodyPitOfHorror.asr.srt. It seems that
    +		// archive.org might be using a webserver implementation that refuses to do partial
    +		// responses to small files.
    +		if part.e.Start < 48<<10 {
    +			if part.e.Start != 0 {
    +				log.Printf("resp status ok but requested range [url=%q, range=%q]",
    +					part.req.URL,
    +					part.req.Header.Get("Range"))
    +			}
    +			// Instead of discarding, we could try receiving all the chunks present in the response
    +			// body. I don't know how one would handle multiple chunk requests resulting in an OK
    +			// response for the same file. The request algorithm might be need to be smarter for
    +			// that.
    +			discarded, _ := io.CopyN(io.Discard, body, part.e.Start)
    +			if discarded != 0 {
    +				log.Printf("discarded %v bytes in webseed request response part", discarded)
    +			}
    +			_, err := io.CopyN(buf, body, part.e.Length)
    +			return err
    +		} else {
    +			return ErrBadResponse{"resp status ok but requested range", result.resp}
    +		}
    +	case http.StatusServiceUnavailable:
    +		return ErrTooFast
    +	default:
    +		return ErrBadResponse{
    +			fmt.Sprintf("unhandled response status code (%v)", result.resp.StatusCode),
    +			result.resp,
    +		}
    +	}
    +}
    +
    +var ErrTooFast = errors.New("making requests too fast")
    +
    +func readRequestPartResponses(ctx context.Context, parts []requestPart) (_ []byte, err error) {
    +	var buf bytes.Buffer
    +	for _, part := range parts {
    +		part.start()
    +		err = recvPartResult(ctx, &buf, part)
    +		if err != nil {
    +			err = fmt.Errorf("reading %q at %q: %w", part.req.URL, part.req.Header.Get("Range"), err)
    +			break
    +		}
    +	}
    +	return buf.Bytes(), err
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webseed/request.go b/deps/github.com/anacrolix/torrent/webseed/request.go
    new file mode 100644
    index 0000000..53fe6db
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webseed/request.go
    @@ -0,0 +1,68 @@
    +package webseed
    +
    +import (
    +	"fmt"
    +	"net/http"
    +	"net/url"
    +	"strings"
    +
    +	"github.com/anacrolix/torrent/metainfo"
    +)
    +
    +type PathEscaper func(pathComps []string) string
    +
    +// Escapes path name components suitable for appending to a webseed URL. This works for converting
    +// S3 object keys to URLs too.
    +//
    +// Contrary to the name, this actually does a QueryEscape, rather than a PathEscape. This works
    +// better with most S3 providers.
    +func EscapePath(pathComps []string) string {
    +	return defaultPathEscaper(pathComps)
    +}
    +
    +func defaultPathEscaper(pathComps []string) string {
    +	var ret []string
    +	for _, comp := range pathComps {
    +		esc := url.PathEscape(comp)
    +		// S3 incorrectly escapes + in paths to spaces, so we add an extra encoding for that. This
    +		// seems to be handled correctly regardless of whether an endpoint uses query or path
    +		// escaping.
    +		esc = strings.ReplaceAll(esc, "+", "%2B")
    +		ret = append(ret, esc)
    +	}
    +	return strings.Join(ret, "/")
    +}
    +
    +func trailingPath(
    +	infoName string,
    +	fileComps []string,
    +	pathEscaper PathEscaper,
    +) string {
    +	if pathEscaper == nil {
    +		pathEscaper = defaultPathEscaper
    +	}
    +	return pathEscaper(append([]string{infoName}, fileComps...))
    +}
    +
    +// Creates a request per BEP 19.
    +func newRequest(
    +	url_ string, fileIndex int,
    +	info *metainfo.Info,
    +	offset, length int64,
    +	pathEscaper PathEscaper,
    +) (*http.Request, error) {
    +	fileInfo := info.UpvertedFiles()[fileIndex]
    +	if strings.HasSuffix(url_, "/") {
    +		// BEP specifies that we append the file path. We need to escape each component of the path
    +		// for things like spaces and '#'.
    +		url_ += trailingPath(info.Name, fileInfo.Path, pathEscaper)
    +	}
    +	req, err := http.NewRequest(http.MethodGet, url_, nil)
    +	if err != nil {
    +		return nil, err
    +	}
    +	if offset != 0 || length != fileInfo.Length {
    +		req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
    +	}
    +	return req, nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webseed/request_test.go b/deps/github.com/anacrolix/torrent/webseed/request_test.go
    new file mode 100644
    index 0000000..af3071f
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webseed/request_test.go
    @@ -0,0 +1,60 @@
    +package webseed
    +
    +import (
    +	"net/url"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +)
    +
    +func TestDefaultPathEscaper(t *testing.T) {
    +	c := qt.New(t)
    +	test := func(unescaped string, parts ...string) {
    +		assertPartsUnescape(c, unescaped, parts...)
    +	}
    +	for _, tc := range defaultPathEscapeTestCases {
    +		test(tc.escaped, tc.parts...)
    +	}
    +}
    +
    +// So we can manually check, and use these to seed fuzzing.
    +var defaultPathEscapeTestCases = []struct {
    +	escaped string
    +	parts   []string
    +}{
    +	{"/", []string{"", ""}},
    +	{"a_b-c/d + e.f", []string{"a_b-c", "d + e.f"}},
    +	{"a_1-b_c2/d 3. (e, f).g", []string{"a_1-b_c2", "d 3. (e, f).g"}},
    +	{"a_b-c/d + e.f", []string{"a_b-c", "d + e.f"}},
    +	{"a_1-b_c2/d 3. (e, f).g", []string{"a_1-b_c2", "d 3. (e, f).g"}},
    +	{"war/and/peace", []string{"war", "and", "peace"}},
    +	{"he//o#world/world", []string{"he//o#world", "world"}},
    +	{`ノ┬─┬ノ ︵ ( \o°o)\`, []string{`ノ┬─┬ノ ︵ ( \o°o)\`}},
    +	{
    +		`%aa + %bb/Parsi Tv - سرقت و باز کردن در ماشین در کم‌تر از ۳ ثانیه + فیلم.webm`,
    +		[]string{`%aa + %bb`, `Parsi Tv - سرقت و باز کردن در ماشین در کم‌تر از ۳ ثانیه + فیلم.webm`},
    +	},
    +}
    +
    +func assertPartsUnescape(c *qt.C, unescaped string, parts ...string) {
    +	escaped := defaultPathEscaper(parts)
    +	pathUnescaped, err := url.PathUnescape(escaped)
    +	c.Assert(err, qt.IsNil)
    +	c.Assert(pathUnescaped, qt.Equals, unescaped)
    +	queryUnescaped, err := url.QueryUnescape(escaped)
    +	c.Assert(err, qt.IsNil)
    +	c.Assert(queryUnescaped, qt.Equals, unescaped)
    +}
    +
    +func FuzzDefaultPathEscaper(f *testing.F) {
    +	for _, tc := range defaultPathEscapeTestCases {
    +		if len(tc.parts) == 2 {
    +			f.Add(tc.parts[0], tc.parts[1])
    +		}
    +	}
    +	// I think a single separator is enough to test special handling around /. Also fuzzing doesn't
    +	// let us take []string as an input.
    +	f.Fuzz(func(t *testing.T, first, second string) {
    +		assertPartsUnescape(qt.New(t), first+"/"+second, first, second)
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/LICENSE b/deps/github.com/anacrolix/torrent/webtorrent/LICENSE
    new file mode 100644
    index 0000000..99d4f26
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/LICENSE
    @@ -0,0 +1,21 @@
    +MIT License
    +
    +Copyright (c) 2019 Michiel De Backker
    +
    +Permission is hereby granted, free of charge, to any person obtaining a copy
    +of this software and associated documentation files (the "Software"), to deal
    +in the Software without restriction, including without limitation the rights
    +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    +copies of the Software, and to permit persons to whom the Software is
    +furnished to do so, subject to the following conditions:
    +
    +The above copyright notice and this permission notice shall be included in all
    +copies or substantial portions of the Software.
    +
    +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    +SOFTWARE.
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/fuzz_test.go b/deps/github.com/anacrolix/torrent/webtorrent/fuzz_test.go
    new file mode 100644
    index 0000000..14638fa
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/fuzz_test.go
    @@ -0,0 +1,31 @@
    +//go:build go1.18
    +// +build go1.18
    +
    +package webtorrent
    +
    +import (
    +	"encoding/json"
    +	"testing"
    +
    +	qt "github.com/frankban/quicktest"
    +)
    +
    +func FuzzJsonBinaryStrings(f *testing.F) {
    +	f.Fuzz(func(t *testing.T, in []byte) {
    +		jsonBytes, err := json.Marshal(binaryToJsonString(in))
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +		// t.Logf("%q", jsonBytes)
    +		var jsonStr string
    +		err = json.Unmarshal(jsonBytes, &jsonStr)
    +		if err != nil {
    +			t.Fatal(err)
    +		}
    +		// t.Logf("%q", jsonStr)
    +		c := qt.New(t)
    +		out, err := decodeJsonByteString(jsonStr, []byte{})
    +		c.Assert(err, qt.IsNil)
    +		c.Assert(out, qt.DeepEquals, in)
    +	})
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/otel.go b/deps/github.com/anacrolix/torrent/webtorrent/otel.go
    new file mode 100644
    index 0000000..2c09964
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/otel.go
    @@ -0,0 +1,6 @@
    +package webtorrent
    +
    +const (
    +	tracerName        = "anacrolix.torrent.webtorrent"
    +	webrtcConnTypeKey = "webtorrent.webrtc.conn.type"
    +)
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/setting-engine.go b/deps/github.com/anacrolix/torrent/webtorrent/setting-engine.go
    new file mode 100644
    index 0000000..a84ee02
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/setting-engine.go
    @@ -0,0 +1,24 @@
    +// These build constraints are copied from webrtc's settingengine.go.
    +//go:build !js
    +// +build !js
    +
    +package webtorrent
    +
    +import (
    +	"io"
    +
    +	"github.com/pion/logging"
    +	"github.com/pion/webrtc/v3"
    +)
    +
    +var s = webrtc.SettingEngine{
    +	// This could probably be done with better integration into anacrolix/log, but I'm not sure if
    +	// it's worth the effort.
    +	LoggerFactory: discardLoggerFactory{},
    +}
    +
    +type discardLoggerFactory struct{}
    +
    +func (discardLoggerFactory) NewLogger(scope string) logging.LeveledLogger {
    +	return logging.NewDefaultLeveledLoggerForScope(scope, logging.LogLevelInfo, io.Discard)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/setting-engine_js.go b/deps/github.com/anacrolix/torrent/webtorrent/setting-engine_js.go
    new file mode 100644
    index 0000000..ea42d11
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/setting-engine_js.go
    @@ -0,0 +1,13 @@
    +// These build constraints are copied from webrtc's settingengine_js.go.
    +//go:build js && wasm
    +// +build js,wasm
    +
    +package webtorrent
    +
    +import (
    +	"github.com/pion/webrtc/v3"
    +)
    +
    +// I'm not sure what to do for logging for JS. See
    +// https://gophers.slack.com/archives/CAK2124AG/p1649651943947579.
    +var s = webrtc.SettingEngine{}
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/195b11403204772a785dfc25a6f37ba920daf479f86bcfbbb880cd06cbb2ecf8 b/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/195b11403204772a785dfc25a6f37ba920daf479f86bcfbbb880cd06cbb2ecf8
    new file mode 100644
    index 0000000..9afa08b
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/195b11403204772a785dfc25a6f37ba920daf479f86bcfbbb880cd06cbb2ecf8
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("\x93")
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4 b/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4
    new file mode 100644
    index 0000000..a96f559
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("0")
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/caf81e9797b19c76c1fc4dbf537d4d81f389524539f402d13aa01f93a65ac7e9 b/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/caf81e9797b19c76c1fc4dbf537d4d81f389524539f402d13aa01f93a65ac7e9
    new file mode 100644
    index 0000000..67322c7
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/testdata/fuzz/FuzzJsonBinaryStrings/caf81e9797b19c76c1fc4dbf537d4d81f389524539f402d13aa01f93a65ac7e9
    @@ -0,0 +1,2 @@
    +go test fuzz v1
    +[]byte("")
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/tracker-client.go b/deps/github.com/anacrolix/torrent/webtorrent/tracker-client.go
    new file mode 100644
    index 0000000..bc9dab3
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/tracker-client.go
    @@ -0,0 +1,396 @@
    +package webtorrent
    +
    +import (
    +	"context"
    +	"crypto/rand"
    +	"encoding/json"
    +	"fmt"
    +	"net/http"
    +	"sync"
    +	"time"
    +
    +	g "github.com/anacrolix/generics"
    +	"github.com/anacrolix/log"
    +	"github.com/gorilla/websocket"
    +	"github.com/pion/datachannel"
    +	"github.com/pion/webrtc/v3"
    +	"go.opentelemetry.io/otel/trace"
    +
    +	"github.com/anacrolix/torrent/tracker"
    +)
    +
    +type TrackerClientStats struct {
    +	Dials                  int64
    +	ConvertedInboundConns  int64
    +	ConvertedOutboundConns int64
    +}
    +
    +// Client represents the webtorrent client
    +type TrackerClient struct {
    +	Url                string
    +	GetAnnounceRequest func(_ tracker.AnnounceEvent, infoHash [20]byte) (tracker.AnnounceRequest, error)
    +	PeerId             [20]byte
    +	OnConn             onDataChannelOpen
    +	Logger             log.Logger
    +	Dialer             *websocket.Dialer
    +
    +	mu             sync.Mutex
    +	cond           sync.Cond
    +	outboundOffers map[string]outboundOfferValue // OfferID to outboundOfferValue
    +	wsConn         *websocket.Conn
    +	closed         bool
    +	stats          TrackerClientStats
    +	pingTicker     *time.Ticker
    +
    +	WebsocketTrackerHttpHeader func() http.Header
    +	ICEServers                 []string
    +}
    +
    +func (me *TrackerClient) Stats() TrackerClientStats {
    +	me.mu.Lock()
    +	defer me.mu.Unlock()
    +	return me.stats
    +}
    +
    +func (me *TrackerClient) peerIdBinary() string {
    +	return binaryToJsonString(me.PeerId[:])
    +}
    +
    +type outboundOffer struct {
    +	offerId string
    +	outboundOfferValue
    +}
    +
    +// outboundOfferValue represents an outstanding offer.
    +type outboundOfferValue struct {
    +	originalOffer  webrtc.SessionDescription
    +	peerConnection *wrappedPeerConnection
    +	infoHash       [20]byte
    +	dataChannel    *webrtc.DataChannel
    +}
    +
    +type DataChannelContext struct {
    +	OfferId      string
    +	LocalOffered bool
    +	InfoHash     [20]byte
    +	// This is private as some methods might not be appropriate with data channel context.
    +	peerConnection *wrappedPeerConnection
    +	Span           trace.Span
    +	Context        context.Context
    +}
    +
    +func (me *DataChannelContext) GetSelectedIceCandidatePair() (*webrtc.ICECandidatePair, error) {
    +	return me.peerConnection.SCTP().Transport().ICETransport().GetSelectedCandidatePair()
    +}
    +
    +type onDataChannelOpen func(_ datachannel.ReadWriteCloser, dcc DataChannelContext)
    +
    +func (tc *TrackerClient) doWebsocket() error {
    +	metrics.Add("websocket dials", 1)
    +	tc.mu.Lock()
    +	tc.stats.Dials++
    +	tc.mu.Unlock()
    +
    +	var header http.Header
    +	if tc.WebsocketTrackerHttpHeader != nil {
    +		header = tc.WebsocketTrackerHttpHeader()
    +	}
    +
    +	c, _, err := tc.Dialer.Dial(tc.Url, header)
    +	if err != nil {
    +		return fmt.Errorf("dialing tracker: %w", err)
    +	}
    +	defer c.Close()
    +	tc.Logger.WithDefaultLevel(log.Info).Printf("connected")
    +	tc.mu.Lock()
    +	tc.wsConn = c
    +	tc.cond.Broadcast()
    +	tc.mu.Unlock()
    +	tc.announceOffers()
    +	closeChan := make(chan struct{})
    +	go func() {
    +		for {
    +			select {
    +			case <-tc.pingTicker.C:
    +				tc.mu.Lock()
    +				err := c.WriteMessage(websocket.PingMessage, []byte{})
    +				tc.mu.Unlock()
    +				if err != nil {
    +					return
    +				}
    +			case <-closeChan:
    +				return
    +
    +			}
    +		}
    +	}()
    +	err = tc.trackerReadLoop(tc.wsConn)
    +	close(closeChan)
    +	tc.mu.Lock()
    +	c.Close()
    +	tc.mu.Unlock()
    +	return err
    +}
    +
    +// Finishes initialization and spawns the run routine, calling onStop when it completes with the
    +// result. We don't let the caller just spawn the runner directly, since then we can race against
    +// .Close to finish initialization.
    +func (tc *TrackerClient) Start(onStop func(error)) {
    +	tc.pingTicker = time.NewTicker(60 * time.Second)
    +	tc.cond.L = &tc.mu
    +	go func() {
    +		onStop(tc.run())
    +	}()
    +}
    +
    +func (tc *TrackerClient) run() error {
    +	tc.mu.Lock()
    +	for !tc.closed {
    +		tc.mu.Unlock()
    +		err := tc.doWebsocket()
    +		level := log.Info
    +		tc.mu.Lock()
    +		if tc.closed {
    +			level = log.Debug
    +		}
    +		tc.mu.Unlock()
    +		tc.Logger.WithDefaultLevel(level).Printf("websocket instance ended: %v", err)
    +		time.Sleep(time.Minute)
    +		tc.mu.Lock()
    +	}
    +	tc.mu.Unlock()
    +	return nil
    +}
    +
    +func (tc *TrackerClient) Close() error {
    +	tc.mu.Lock()
    +	tc.closed = true
    +	if tc.wsConn != nil {
    +		tc.wsConn.Close()
    +	}
    +	tc.closeUnusedOffers()
    +	tc.pingTicker.Stop()
    +	tc.mu.Unlock()
    +	tc.cond.Broadcast()
    +	return nil
    +}
    +
    +func (tc *TrackerClient) announceOffers() {
    +	// tc.Announce grabs a lock on tc.outboundOffers. It also handles the case where outboundOffers
    +	// is nil. Take ownership of outboundOffers here.
    +	tc.mu.Lock()
    +	offers := tc.outboundOffers
    +	tc.outboundOffers = nil
    +	tc.mu.Unlock()
    +
    +	if offers == nil {
    +		return
    +	}
    +
    +	// Iterate over our locally-owned offers, close any existing "invalid" ones from before the
    +	// socket reconnected, reannounce the infohash, adding it back into the tc.outboundOffers.
    +	tc.Logger.WithDefaultLevel(log.Info).Printf("reannouncing %d infohashes after restart", len(offers))
    +	for _, offer := range offers {
    +		// TODO: Capture the errors? Are we even in a position to do anything with them?
    +		offer.peerConnection.Close()
    +		// Use goroutine here to allow read loop to start and ensure the buffer drains.
    +		go tc.Announce(tracker.Started, offer.infoHash)
    +	}
    +}
    +
    +func (tc *TrackerClient) closeUnusedOffers() {
    +	for _, offer := range tc.outboundOffers {
    +		offer.peerConnection.Close()
    +		offer.dataChannel.Close()
    +	}
    +	tc.outboundOffers = nil
    +}
    +
    +func (tc *TrackerClient) CloseOffersForInfohash(infoHash [20]byte) {
    +	tc.mu.Lock()
    +	defer tc.mu.Unlock()
    +	for key, offer := range tc.outboundOffers {
    +		if offer.infoHash == infoHash {
    +			offer.peerConnection.Close()
    +			delete(tc.outboundOffers, key)
    +		}
    +	}
    +}
    +
    +func (tc *TrackerClient) Announce(event tracker.AnnounceEvent, infoHash [20]byte) error {
    +	metrics.Add("outbound announces", 1)
    +	if event == tracker.Stopped {
    +		return tc.announce(event, infoHash, nil)
    +	}
    +	var randOfferId [20]byte
    +	_, err := rand.Read(randOfferId[:])
    +	if err != nil {
    +		return fmt.Errorf("generating offer_id bytes: %w", err)
    +	}
    +	offerIDBinary := binaryToJsonString(randOfferId[:])
    +
    +	pc, dc, offer, err := tc.newOffer(tc.Logger, offerIDBinary, infoHash)
    +	if err != nil {
    +		return fmt.Errorf("creating offer: %w", err)
    +	}
    +
    +	err = tc.announce(event, infoHash, []outboundOffer{
    +		{
    +			offerId: offerIDBinary,
    +			outboundOfferValue: outboundOfferValue{
    +				originalOffer:  offer,
    +				peerConnection: pc,
    +				infoHash:       infoHash,
    +				dataChannel:    dc,
    +			},
    +		},
    +	})
    +	if err != nil {
    +		dc.Close()
    +		pc.Close()
    +	}
    +	return err
    +}
    +
    +func (tc *TrackerClient) announce(event tracker.AnnounceEvent, infoHash [20]byte, offers []outboundOffer) error {
    +	request, err := tc.GetAnnounceRequest(event, infoHash)
    +	if err != nil {
    +		return fmt.Errorf("getting announce parameters: %w", err)
    +	}
    +
    +	req := AnnounceRequest{
    +		Numwant:    len(offers),
    +		Uploaded:   request.Uploaded,
    +		Downloaded: request.Downloaded,
    +		Left:       request.Left,
    +		Event:      request.Event.String(),
    +		Action:     "announce",
    +		InfoHash:   binaryToJsonString(infoHash[:]),
    +		PeerID:     tc.peerIdBinary(),
    +	}
    +	for _, offer := range offers {
    +		req.Offers = append(req.Offers, Offer{
    +			OfferID: offer.offerId,
    +			Offer:   offer.originalOffer,
    +		})
    +	}
    +
    +	data, err := json.Marshal(req)
    +	if err != nil {
    +		return fmt.Errorf("marshalling request: %w", err)
    +	}
    +
    +	tc.mu.Lock()
    +	defer tc.mu.Unlock()
    +	err = tc.writeMessage(data)
    +	if err != nil {
    +		return fmt.Errorf("write AnnounceRequest: %w", err)
    +	}
    +	for _, offer := range offers {
    +		g.MakeMapIfNilAndSet(&tc.outboundOffers, offer.offerId, offer.outboundOfferValue)
    +	}
    +	return nil
    +}
    +
    +func (tc *TrackerClient) writeMessage(data []byte) error {
    +	for tc.wsConn == nil {
    +		if tc.closed {
    +			return fmt.Errorf("%T closed", tc)
    +		}
    +		tc.cond.Wait()
    +	}
    +	return tc.wsConn.WriteMessage(websocket.TextMessage, data)
    +}
    +
    +func (tc *TrackerClient) trackerReadLoop(tracker *websocket.Conn) error {
    +	for {
    +		_, message, err := tracker.ReadMessage()
    +		if err != nil {
    +			return fmt.Errorf("read message error: %w", err)
    +		}
    +		// tc.Logger.WithDefaultLevel(log.Debug).Printf("received message from tracker: %q", message)
    +
    +		var ar AnnounceResponse
    +		if err := json.Unmarshal(message, &ar); err != nil {
    +			tc.Logger.WithDefaultLevel(log.Warning).Printf("error unmarshalling announce response: %v", err)
    +			continue
    +		}
    +		switch {
    +		case ar.Offer != nil:
    +			ih, err := jsonStringToInfoHash(ar.InfoHash)
    +			if err != nil {
    +				tc.Logger.WithDefaultLevel(log.Warning).Printf("error decoding info_hash in offer: %v", err)
    +				break
    +			}
    +			err = tc.handleOffer(offerContext{
    +				SessDesc: *ar.Offer,
    +				Id:       ar.OfferID,
    +				InfoHash: ih,
    +			}, ar.PeerID)
    +			if err != nil {
    +				tc.Logger.Levelf(log.Error, "handling offer for infohash %x: %v", ih, err)
    +			}
    +		case ar.Answer != nil:
    +			tc.handleAnswer(ar.OfferID, *ar.Answer)
    +		default:
    +			tc.Logger.Levelf(log.Warning, "unhandled announce response %q", message)
    +		}
    +	}
    +}
    +
    +type offerContext struct {
    +	SessDesc webrtc.SessionDescription
    +	Id       string
    +	InfoHash [20]byte
    +}
    +
    +func (tc *TrackerClient) handleOffer(
    +	offerContext offerContext,
    +	peerId string,
    +) error {
    +	peerConnection, answer, err := tc.newAnsweringPeerConnection(offerContext)
    +	if err != nil {
    +		return fmt.Errorf("creating answering peer connection: %w", err)
    +	}
    +	response := AnnounceResponse{
    +		Action:   "announce",
    +		InfoHash: binaryToJsonString(offerContext.InfoHash[:]),
    +		PeerID:   tc.peerIdBinary(),
    +		ToPeerID: peerId,
    +		Answer:   &answer,
    +		OfferID:  offerContext.Id,
    +	}
    +	data, err := json.Marshal(response)
    +	if err != nil {
    +		peerConnection.Close()
    +		return fmt.Errorf("marshalling response: %w", err)
    +	}
    +	tc.mu.Lock()
    +	defer tc.mu.Unlock()
    +	if err := tc.writeMessage(data); err != nil {
    +		peerConnection.Close()
    +		return fmt.Errorf("writing response: %w", err)
    +	}
    +	return nil
    +}
    +
    +func (tc *TrackerClient) handleAnswer(offerId string, answer webrtc.SessionDescription) {
    +	tc.mu.Lock()
    +	defer tc.mu.Unlock()
    +	offer, ok := tc.outboundOffers[offerId]
    +	if !ok {
    +		tc.Logger.WithDefaultLevel(log.Warning).Printf("could not find offer for id %+q", offerId)
    +		return
    +	}
    +	// tc.Logger.WithDefaultLevel(log.Debug).Printf("offer %q got answer %v", offerId, answer)
    +	metrics.Add("outbound offers answered", 1)
    +	err := offer.peerConnection.SetRemoteDescription(answer)
    +	if err != nil {
    +		err = fmt.Errorf("using outbound offer answer: %w", err)
    +		offer.peerConnection.span.RecordError(err)
    +		tc.Logger.LevelPrint(log.Error, err)
    +		return
    +	}
    +	delete(tc.outboundOffers, offerId)
    +	go tc.Announce(tracker.None, offer.infoHash)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/tracker-protocol.go b/deps/github.com/anacrolix/torrent/webtorrent/tracker-protocol.go
    new file mode 100644
    index 0000000..14be67e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/tracker-protocol.go
    @@ -0,0 +1,76 @@
    +package webtorrent
    +
    +import (
    +	"fmt"
    +	"math"
    +
    +	"github.com/pion/webrtc/v3"
    +)
    +
    +type AnnounceRequest struct {
    +	Numwant    int     `json:"numwant"`
    +	Uploaded   int64   `json:"uploaded"`
    +	Downloaded int64   `json:"downloaded"`
    +	Left       int64   `json:"left"`
    +	Event      string  `json:"event,omitempty"`
    +	Action     string  `json:"action"`
    +	InfoHash   string  `json:"info_hash"`
    +	PeerID     string  `json:"peer_id"`
    +	Offers     []Offer `json:"offers"`
    +}
    +
    +type Offer struct {
    +	OfferID string                    `json:"offer_id"`
    +	Offer   webrtc.SessionDescription `json:"offer"`
    +}
    +
    +type AnnounceResponse struct {
    +	InfoHash   string                     `json:"info_hash"`
    +	Action     string                     `json:"action"`
    +	Interval   *int                       `json:"interval,omitempty"`
    +	Complete   *int                       `json:"complete,omitempty"`
    +	Incomplete *int                       `json:"incomplete,omitempty"`
    +	PeerID     string                     `json:"peer_id,omitempty"`
    +	ToPeerID   string                     `json:"to_peer_id,omitempty"`
    +	Answer     *webrtc.SessionDescription `json:"answer,omitempty"`
    +	Offer      *webrtc.SessionDescription `json:"offer,omitempty"`
    +	OfferID    string                     `json:"offer_id,omitempty"`
    +}
    +
    +// I wonder if this is a defacto standard way to decode bytes to JSON for webtorrent. I don't really
    +// care.
    +func binaryToJsonString(b []byte) string {
    +	var seq []rune
    +	for _, v := range b {
    +		seq = append(seq, rune(v))
    +	}
    +	return string(seq)
    +}
    +
    +func jsonStringToInfoHash(s string) (ih [20]byte, err error) {
    +	b, err := decodeJsonByteString(s, ih[:0])
    +	if err != nil {
    +		return
    +	}
    +	if len(b) != len(ih) {
    +		err = fmt.Errorf("string decoded to %v bytes", len(b))
    +	}
    +	return
    +}
    +
    +func decodeJsonByteString(s string, b []byte) ([]byte, error) {
    +	defer func() {
    +		r := recover()
    +		if r == nil {
    +			return
    +		}
    +		panic(fmt.Sprintf("%q", s))
    +	}()
    +	for _, c := range []rune(s) {
    +		if c < 0 || c > math.MaxUint8 {
    +			return b, fmt.Errorf("rune out of bounds: %v", c)
    +		}
    +		b = append(b, byte(c))
    +	}
    +	return b, nil
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/transport.go b/deps/github.com/anacrolix/torrent/webtorrent/transport.go
    new file mode 100644
    index 0000000..8566258
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/transport.go
    @@ -0,0 +1,265 @@
    +package webtorrent
    +
    +import (
    +	"context"
    +	"expvar"
    +	"fmt"
    +	"io"
    +	"sync"
    +	"time"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/anacrolix/missinggo/v2/pproffd"
    +	"github.com/pion/datachannel"
    +	"github.com/pion/webrtc/v3"
    +	"go.opentelemetry.io/otel"
    +	"go.opentelemetry.io/otel/attribute"
    +	"go.opentelemetry.io/otel/codes"
    +	"go.opentelemetry.io/otel/trace"
    +)
    +
    +const (
    +	dataChannelLabel = "webrtc-datachannel"
    +)
    +
    +var (
    +	metrics = expvar.NewMap("webtorrent")
    +	api     = func() *webrtc.API {
    +		// Enable the detach API (since it's non-standard but more idiomatic).
    +		s.DetachDataChannels()
    +		return webrtc.NewAPI(webrtc.WithSettingEngine(s))
    +	}()
    +	newPeerConnectionMu sync.Mutex
    +)
    +
    +type wrappedPeerConnection struct {
    +	*webrtc.PeerConnection
    +	closeMu sync.Mutex
    +	pproffd.CloseWrapper
    +	span trace.Span
    +	ctx  context.Context
    +}
    +
    +func (me *wrappedPeerConnection) Close() error {
    +	me.closeMu.Lock()
    +	defer me.closeMu.Unlock()
    +	err := me.CloseWrapper.Close()
    +	me.span.End()
    +	return err
    +}
    +
    +func newPeerConnection(logger log.Logger, iceServers []string) (*wrappedPeerConnection, error) {
    +	newPeerConnectionMu.Lock()
    +	defer newPeerConnectionMu.Unlock()
    +	ctx, span := otel.Tracer(tracerName).Start(context.Background(), "PeerConnection")
    +
    +	pcConfig := webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: iceServers}}}
    +
    +	pc, err := api.NewPeerConnection(pcConfig)
    +	if err != nil {
    +		span.SetStatus(codes.Error, err.Error())
    +		span.RecordError(err)
    +		span.End()
    +		return nil, err
    +	}
    +	wpc := &wrappedPeerConnection{
    +		PeerConnection: pc,
    +		CloseWrapper:   pproffd.NewCloseWrapper(pc),
    +		ctx:            ctx,
    +		span:           span,
    +	}
    +	// If the state change handler intends to call Close, it should call it on the wrapper.
    +	wpc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
    +		logger.Levelf(log.Warning, "webrtc PeerConnection state changed to %v", state)
    +		span.AddEvent("connection state changed", trace.WithAttributes(attribute.String("state", state.String())))
    +	})
    +	return wpc, nil
    +}
    +
    +func setAndGatherLocalDescription(peerConnection *wrappedPeerConnection, sdp webrtc.SessionDescription) (_ webrtc.SessionDescription, err error) {
    +	gatherComplete := webrtc.GatheringCompletePromise(peerConnection.PeerConnection)
    +	peerConnection.span.AddEvent("setting local description")
    +	err = peerConnection.SetLocalDescription(sdp)
    +	if err != nil {
    +		err = fmt.Errorf("setting local description: %w", err)
    +		return
    +	}
    +	<-gatherComplete
    +	peerConnection.span.AddEvent("gathering complete")
    +	return *peerConnection.LocalDescription(), nil
    +}
    +
    +// newOffer creates a transport and returns a WebRTC offer to be announced. See
    +// https://github.com/pion/webrtc/blob/master/examples/data-channels/jsfiddle/main.go for what this is modelled on.
    +func (tc *TrackerClient) newOffer(
    +	logger log.Logger,
    +	offerId string,
    +	infoHash [20]byte,
    +) (
    +	peerConnection *wrappedPeerConnection,
    +	dataChannel *webrtc.DataChannel,
    +	offer webrtc.SessionDescription,
    +	err error,
    +) {
    +	peerConnection, err = newPeerConnection(logger, tc.ICEServers)
    +	if err != nil {
    +		return
    +	}
    +
    +	peerConnection.span.SetAttributes(attribute.String(webrtcConnTypeKey, "offer"))
    +
    +	dataChannel, err = peerConnection.CreateDataChannel(dataChannelLabel, nil)
    +	if err != nil {
    +		err = fmt.Errorf("creating data channel: %w", err)
    +		peerConnection.Close()
    +	}
    +	initDataChannel(dataChannel, peerConnection, func(dc datachannel.ReadWriteCloser, dcCtx context.Context, dcSpan trace.Span) {
    +		metrics.Add("outbound offers answered with datachannel", 1)
    +		tc.mu.Lock()
    +		tc.stats.ConvertedOutboundConns++
    +		tc.mu.Unlock()
    +		tc.OnConn(dc, DataChannelContext{
    +			OfferId:        offerId,
    +			LocalOffered:   true,
    +			InfoHash:       infoHash,
    +			peerConnection: peerConnection,
    +			Context:        dcCtx,
    +			Span:           dcSpan,
    +		})
    +	})
    +
    +	offer, err = peerConnection.CreateOffer(nil)
    +	if err != nil {
    +		dataChannel.Close()
    +		peerConnection.Close()
    +		return
    +	}
    +
    +	offer, err = setAndGatherLocalDescription(peerConnection, offer)
    +	if err != nil {
    +		dataChannel.Close()
    +		peerConnection.Close()
    +	}
    +	return
    +}
    +
    +type onDetachedDataChannelFunc func(detached datachannel.ReadWriteCloser, ctx context.Context, span trace.Span)
    +
    +func (tc *TrackerClient) initAnsweringPeerConnection(
    +	peerConn *wrappedPeerConnection,
    +	offerContext offerContext,
    +) (answer webrtc.SessionDescription, err error) {
    +	peerConn.span.SetAttributes(attribute.String(webrtcConnTypeKey, "answer"))
    +
    +	timer := time.AfterFunc(30*time.Second, func() {
    +		peerConn.span.SetStatus(codes.Error, "answer timeout")
    +		metrics.Add("answering peer connections timed out", 1)
    +		peerConn.Close()
    +	})
    +	peerConn.OnDataChannel(func(d *webrtc.DataChannel) {
    +		initDataChannel(d, peerConn, func(detached datachannel.ReadWriteCloser, ctx context.Context, span trace.Span) {
    +			timer.Stop()
    +			metrics.Add("answering peer connection conversions", 1)
    +			tc.mu.Lock()
    +			tc.stats.ConvertedInboundConns++
    +			tc.mu.Unlock()
    +			tc.OnConn(detached, DataChannelContext{
    +				OfferId:        offerContext.Id,
    +				LocalOffered:   false,
    +				InfoHash:       offerContext.InfoHash,
    +				peerConnection: peerConn,
    +				Context:        ctx,
    +				Span:           span,
    +			})
    +		})
    +	})
    +
    +	err = peerConn.SetRemoteDescription(offerContext.SessDesc)
    +	if err != nil {
    +		return
    +	}
    +	answer, err = peerConn.CreateAnswer(nil)
    +	if err != nil {
    +		return
    +	}
    +
    +	answer, err = setAndGatherLocalDescription(peerConn, answer)
    +	return
    +}
    +
    +// newAnsweringPeerConnection creates a transport from a WebRTC offer and returns a WebRTC answer to be announced.
    +func (tc *TrackerClient) newAnsweringPeerConnection(
    +	offerContext offerContext,
    +) (
    +	peerConn *wrappedPeerConnection, answer webrtc.SessionDescription, err error,
    +) {
    +	peerConn, err = newPeerConnection(tc.Logger, tc.ICEServers)
    +	if err != nil {
    +		err = fmt.Errorf("failed to create new connection: %w", err)
    +		return
    +	}
    +	answer, err = tc.initAnsweringPeerConnection(peerConn, offerContext)
    +	if err != nil {
    +		peerConn.span.RecordError(err)
    +		peerConn.Close()
    +	}
    +	return
    +}
    +
    +type datachannelReadWriter interface {
    +	datachannel.Reader
    +	datachannel.Writer
    +	io.Reader
    +	io.Writer
    +}
    +
    +type ioCloserFunc func() error
    +
    +func (me ioCloserFunc) Close() error {
    +	return me()
    +}
    +
    +func initDataChannel(
    +	dc *webrtc.DataChannel,
    +	pc *wrappedPeerConnection,
    +	onOpen onDetachedDataChannelFunc,
    +) {
    +	var span trace.Span
    +	dc.OnClose(func() {
    +		span.End()
    +	})
    +	dc.OnOpen(func() {
    +		pc.span.AddEvent("data channel opened")
    +		var ctx context.Context
    +		ctx, span = otel.Tracer(tracerName).Start(pc.ctx, "DataChannel")
    +		raw, err := dc.Detach()
    +		if err != nil {
    +			// This shouldn't happen if the API is configured correctly, and we call from OnOpen.
    +			panic(err)
    +		}
    +		onOpen(hookDataChannelCloser(raw, pc, span, dc), ctx, span)
    +	})
    +}
    +
    +// Hooks the datachannel's Close to Close the owning PeerConnection. The datachannel takes ownership
    +// and responsibility for the PeerConnection.
    +func hookDataChannelCloser(
    +	dcrwc datachannel.ReadWriteCloser,
    +	pc *wrappedPeerConnection,
    +	dataChannelSpan trace.Span,
    +	originalDataChannel *webrtc.DataChannel,
    +) datachannel.ReadWriteCloser {
    +	return struct {
    +		datachannelReadWriter
    +		io.Closer
    +	}{
    +		dcrwc,
    +		ioCloserFunc(func() error {
    +			dcrwc.Close()
    +			pc.Close()
    +			originalDataChannel.Close()
    +			dataChannelSpan.End()
    +			return nil
    +		}),
    +	}
    +}
    diff --git a/deps/github.com/anacrolix/torrent/webtorrent/transport_test.go b/deps/github.com/anacrolix/torrent/webtorrent/transport_test.go
    new file mode 100644
    index 0000000..c17328e
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/webtorrent/transport_test.go
    @@ -0,0 +1,35 @@
    +package webtorrent
    +
    +import (
    +	"testing"
    +
    +	"github.com/anacrolix/log"
    +	qt "github.com/frankban/quicktest"
    +	"github.com/pion/webrtc/v3"
    +)
    +
    +func TestClosingPeerConnectionDoesNotCloseUnopenedDataChannel(t *testing.T) {
    +	c := qt.New(t)
    +	var tc TrackerClient
    +	pc, dc, _, err := tc.newOffer(log.Default, "", [20]byte{})
    +	c.Assert(err, qt.IsNil)
    +	defer pc.Close()
    +	defer dc.Close()
    +	peerConnClosed := make(chan struct{})
    +	pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
    +		if state == webrtc.PeerConnectionStateClosed {
    +			close(peerConnClosed)
    +		}
    +	})
    +	dc.OnClose(func() {
    +		// This should not be called because the DataChannel is never opened.
    +		t.Fatal("DataChannel.OnClose handler called")
    +	})
    +	t.Logf("data channel ready state before close: %v", dc.ReadyState())
    +	dc.OnError(func(err error) {
    +		t.Logf("data channel error: %v", err)
    +	})
    +	pc.Close()
    +	c.Check(dc.ReadyState(), qt.Equals, webrtc.DataChannelStateClosed)
    +	<-peerConnClosed
    +}
    diff --git a/deps/github.com/anacrolix/torrent/worse-conns.go b/deps/github.com/anacrolix/torrent/worse-conns.go
    new file mode 100644
    index 0000000..ef33b97
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/worse-conns.go
    @@ -0,0 +1,118 @@
    +package torrent
    +
    +import (
    +	"container/heap"
    +	"fmt"
    +	"time"
    +	"unsafe"
    +
    +	"github.com/anacrolix/multiless"
    +	"github.com/anacrolix/sync"
    +)
    +
    +type worseConnInput struct {
    +	BadDirection        bool
    +	Useful              bool
    +	LastHelpful         time.Time
    +	CompletedHandshake  time.Time
    +	GetPeerPriority     func() (peerPriority, error)
    +	getPeerPriorityOnce sync.Once
    +	peerPriority        peerPriority
    +	peerPriorityErr     error
    +	Pointer             uintptr
    +}
    +
    +func (me *worseConnInput) doGetPeerPriority() {
    +	me.peerPriority, me.peerPriorityErr = me.GetPeerPriority()
    +}
    +
    +func (me *worseConnInput) doGetPeerPriorityOnce() {
    +	me.getPeerPriorityOnce.Do(me.doGetPeerPriority)
    +}
    +
    +type worseConnLensOpts struct {
    +	incomingIsBad, outgoingIsBad bool
    +}
    +
    +func worseConnInputFromPeer(p *PeerConn, opts worseConnLensOpts) worseConnInput {
    +	ret := worseConnInput{
    +		Useful:             p.useful(),
    +		LastHelpful:        p.lastHelpful(),
    +		CompletedHandshake: p.completedHandshake,
    +		Pointer:            uintptr(unsafe.Pointer(p)),
    +		GetPeerPriority:    p.peerPriority,
    +	}
    +	if opts.incomingIsBad && !p.outgoing {
    +		ret.BadDirection = true
    +	} else if opts.outgoingIsBad && p.outgoing {
    +		ret.BadDirection = true
    +	}
    +	return ret
    +}
    +
    +func (l *worseConnInput) Less(r *worseConnInput) bool {
    +	less, ok := multiless.New().Bool(
    +		r.BadDirection, l.BadDirection).Bool(
    +		l.Useful, r.Useful).CmpInt64(
    +		l.LastHelpful.Sub(r.LastHelpful).Nanoseconds()).CmpInt64(
    +		l.CompletedHandshake.Sub(r.CompletedHandshake).Nanoseconds()).LazySameLess(
    +		func() (same, less bool) {
    +			l.doGetPeerPriorityOnce()
    +			if l.peerPriorityErr != nil {
    +				same = true
    +				return
    +			}
    +			r.doGetPeerPriorityOnce()
    +			if r.peerPriorityErr != nil {
    +				same = true
    +				return
    +			}
    +			same = l.peerPriority == r.peerPriority
    +			less = l.peerPriority < r.peerPriority
    +			return
    +		}).Uintptr(
    +		l.Pointer, r.Pointer,
    +	).LessOk()
    +	if !ok {
    +		panic(fmt.Sprintf("cannot differentiate %#v and %#v", l, r))
    +	}
    +	return less
    +}
    +
    +type worseConnSlice struct {
    +	conns []*PeerConn
    +	keys  []worseConnInput
    +}
    +
    +func (me *worseConnSlice) initKeys(opts worseConnLensOpts) {
    +	me.keys = make([]worseConnInput, len(me.conns))
    +	for i, c := range me.conns {
    +		me.keys[i] = worseConnInputFromPeer(c, opts)
    +	}
    +}
    +
    +var _ heap.Interface = &worseConnSlice{}
    +
    +func (me worseConnSlice) Len() int {
    +	return len(me.conns)
    +}
    +
    +func (me worseConnSlice) Less(i, j int) bool {
    +	return me.keys[i].Less(&me.keys[j])
    +}
    +
    +func (me *worseConnSlice) Pop() interface{} {
    +	i := len(me.conns) - 1
    +	ret := me.conns[i]
    +	me.conns = me.conns[:i]
    +	return ret
    +}
    +
    +func (me *worseConnSlice) Push(x interface{}) {
    +	panic("not implemented")
    +}
    +
    +func (me worseConnSlice) Swap(i, j int) {
    +	me.conns[i], me.conns[j] = me.conns[j], me.conns[i]
    +	me.keys[i], me.keys[j] = me.keys[j], me.keys[i]
    +}
    diff --git a/deps/github.com/anacrolix/torrent/worse-conns_test.go b/deps/github.com/anacrolix/torrent/worse-conns_test.go
    new file mode 100644
    index 0000000..3865b64
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/worse-conns_test.go
    @@ -0,0 +1,44 @@
    +package torrent
    +
    +import (
    +	"testing"
    +	"time"
    +
    +	qt "github.com/frankban/quicktest"
    +)
    +
    +func TestWorseConnLastHelpful(t *testing.T) {
    +	c := qt.New(t)
    +	c.Check((&worseConnInput{}).Less(&worseConnInput{LastHelpful: time.Now()}), qt.IsTrue)
    +	c.Check((&worseConnInput{}).Less(&worseConnInput{CompletedHandshake: time.Now()}), qt.IsTrue)
    +	c.Check((&worseConnInput{LastHelpful: time.Now()}).Less(&worseConnInput{CompletedHandshake: time.Now()}), qt.IsFalse)
    +	c.Check((&worseConnInput{
    +		LastHelpful: time.Now(),
    +	}).Less(&worseConnInput{
    +		LastHelpful:        time.Now(),
    +		CompletedHandshake: time.Now(),
    +	}), qt.IsTrue)
    +	now := time.Now()
    +	c.Check((&worseConnInput{
    +		LastHelpful: now,
    +	}).Less(&worseConnInput{
    +		LastHelpful:        now.Add(-time.Nanosecond),
    +		CompletedHandshake: now,
    +	}), qt.IsFalse)
    +	readyPeerPriority := func() (peerPriority, error) {
    +		return 42, nil
    +	}
    +	c.Check((&worseConnInput{
    +		GetPeerPriority: readyPeerPriority,
    +	}).Less(&worseConnInput{
    +		GetPeerPriority: readyPeerPriority,
    +		Pointer:         1,
    +	}), qt.IsTrue)
    +	c.Check((&worseConnInput{
    +		GetPeerPriority: readyPeerPriority,
    +		Pointer:         2,
    +	}).Less(&worseConnInput{
    +		GetPeerPriority: readyPeerPriority,
    +		Pointer:         1,
    +	}), qt.IsFalse)
    +}
    diff --git a/deps/github.com/anacrolix/torrent/wstracker.go b/deps/github.com/anacrolix/torrent/wstracker.go
    new file mode 100644
    index 0000000..84af9cb
    --- /dev/null
    +++ b/deps/github.com/anacrolix/torrent/wstracker.go
    @@ -0,0 +1,92 @@
    +package torrent
    +
    +import (
    +	"context"
    +	"fmt"
    +	"net"
    +	netHttp "net/http"
    +	"net/url"
    +	"sync"
    +
    +	"github.com/anacrolix/log"
    +	"github.com/gorilla/websocket"
    +	"github.com/pion/datachannel"
    +
    +	"github.com/anacrolix/torrent/tracker"
    +	httpTracker "github.com/anacrolix/torrent/tracker/http"
    +	"github.com/anacrolix/torrent/webtorrent"
    +)
    +
    +type websocketTrackerStatus struct {
    +	url url.URL
    +	tc  *webtorrent.TrackerClient
    +}
    +
    +func (me websocketTrackerStatus) statusLine() string {
    +	return fmt.Sprintf("%+v", me.tc.Stats())
    +}
    +
    +func (me websocketTrackerStatus) URL() *url.URL {
    +	return &me.url
    +}
    +
    +type refCountedWebtorrentTrackerClient struct {
    +	webtorrent.TrackerClient
    +	refCount int
    +}
    +
    +type websocketTrackers struct {
    +	PeerId                     [20]byte
    +	Logger                     log.Logger
    +	GetAnnounceRequest         func(event tracker.AnnounceEvent, infoHash [20]byte) (tracker.AnnounceRequest, error)
    +	OnConn                     func(datachannel.ReadWriteCloser, webtorrent.DataChannelContext)
    +	mu                         sync.Mutex
    +	clients                    map[string]*refCountedWebtorrentTrackerClient
    +	Proxy                      httpTracker.ProxyFunc
    +	DialContext                func(ctx context.Context, network, addr string) (net.Conn, error)
    +	WebsocketTrackerHttpHeader func() netHttp.Header
    +	ICEServers                 []string
    +}
    +
    +func (me *websocketTrackers) Get(url string, infoHash [20]byte) (*webtorrent.TrackerClient, func()) {
    +	me.mu.Lock()
    +	defer me.mu.Unlock()
    +	value, ok := me.clients[url]
    +	if !ok {
    +		dialer := &websocket.Dialer{Proxy: me.Proxy, NetDialContext: me.DialContext, HandshakeTimeout: websocket.DefaultDialer.HandshakeTimeout}
    +		value = &refCountedWebtorrentTrackerClient{
    +			TrackerClient: webtorrent.TrackerClient{
    +				Dialer:             dialer,
    +				Url:                url,
    +				GetAnnounceRequest: me.GetAnnounceRequest,
    +				PeerId:             me.PeerId,
    +				OnConn:             me.OnConn,
    +				Logger: me.Logger.WithText(func(m log.Msg) string {
    +					return fmt.Sprintf("tracker client for %q: %v", url, m)
    +				}),
    +				WebsocketTrackerHttpHeader: me.WebsocketTrackerHttpHeader,
    +				ICEServers:                 me.ICEServers,
    +			},
    +		}
    +		value.TrackerClient.Start(func(err error) {
    +			if err != nil {
    +				me.Logger.Printf("error running tracker client for %q: %v", url, err)
    +			}
    +		})
    +		if me.clients == nil {
    +			me.clients = make(map[string]*refCountedWebtorrentTrackerClient)
    +		}
    +		me.clients[url] = value
    +	}
    +	value.refCount++
    +	return &value.TrackerClient, func() {
    +		me.mu.Lock()
    +		defer me.mu.Unlock()
    +		value.TrackerClient.CloseOffersForInfohash(infoHash)
    +		value.refCount--
    +		if value.refCount == 0 {
    +			value.TrackerClient.Close()
    +			delete(me.clients, url)
    +		}
    +	}
    +}
    diff --git a/deps/github.com/ledgerwatch/interfaces/.github/workflows/rust.yml b/deps/github.com/ledgerwatch/interfaces/.github/workflows/rust.yml
    new file mode 100644
    index 0000000..2ec3432
    --- /dev/null
    +++ b/deps/github.com/ledgerwatch/interfaces/.github/workflows/rust.yml
    @@ -0,0 +1,54 @@
    +on:
    +  pull_request:
    +  push:
    +    branches:
    +      - master
    +
    +name: Rust
    +
    +jobs:
    +  ci:
    +    runs-on: ubuntu-latest
    +
    +    steps:
    +      - uses: actions/checkout@v3
    +
    +      - uses: actions-rs/toolchain@v1
    +        with:
    +          profile: minimal
    +          toolchain: stable
    +          components: rustfmt, clippy
    +
    +      - uses: actions-rs/cargo@v1
    +        with:
    +          command: fmt
    +          args: --all -- --check
    +
    +      - uses: actions-rs/install@v0.1
    +        with:
    +          crate: cargo-hack
    +          version: latest
    +          use-tool-cache: true
    +
    +      - uses: actions-rs/cargo@v1
    +        with:
    +          command: hack
    +          args: check --workspace --ignore-private --each-feature --no-dev-deps
    +
    +      - uses: actions-rs/cargo@v1
    +        with:
    +          command: check
    +          args: --workspace --all-targets --all-features
    +
    +      - uses: actions-rs/cargo@v1
    +        with:
    +          command: test
    +
    +      - uses: actions-rs/clippy-check@v1
    +        with:
    +          token: ${{ secrets.GITHUB_TOKEN }}
    +          args: --all-features
    +
    +      - uses: actions-rs/audit-check@v1
    +        with:
    +          token: ${{ secrets.GITHUB_TOKEN }}
    diff --git a/deps/github.com/ledgerwatch/interfaces/.gitignore b/deps/github.com/ledgerwatch/interfaces/.gitignore
    new file mode 100644
    index 0000000..0d36de1
    --- /dev/null
    +++ b/deps/github.com/ledgerwatch/interfaces/.gitignore
    @@ -0,0 +1,5 @@
    +.idea/
    +/target
    +Cargo.lock
    +
    +go.work
    \ No newline at end of file
    diff --git a/deps/github.com/ledgerwatch/interfaces/Cargo.toml b/deps/github.com/ledgerwatch/interfaces/Cargo.toml
    new file mode 100644
    index 0000000..3a9dc30
    --- /dev/null
    +++ b/deps/github.com/ledgerwatch/interfaces/Cargo.toml
    @@ -0,0 +1,26 @@
    +[package]
    +name = "ethereum-interfaces"
    +version = "0.1.0"
    +authors = ["Artem Vorotnikov "]
    +edition = "2021"
    +license = "Apache-2.0"
    +
    +[features]
    +sentry = []
    +sentinel = []
    +remotekv = []
    +snapshotsync = []
    +txpool = []
    +web3 = []
    +
    +[dependencies]
    +arrayref = "0.3"
    +ethereum-types = { version = "0.14", default-features = false }
    +ethnum = { version = "1", default-features = false }
    +prost = "0.11"
    +tonic = "0.8"
    +
    +[build-dependencies]
    +protobuf-src = "1.1.0"
    +prost-build = "0.11"
    +tonic-build = "0.8"
    diff --git a/deps/github.com/ledgerwatch/interfaces/LICENSE b/deps/github.com/ledgerwatch/interfaces/LICENSE
    new file mode 100644
    index 0000000..261eeb9
    --- /dev/null
    +++ b/deps/github.com/ledgerwatch/interfaces/LICENSE
    @@ -0,0 +1,201 @@
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    +   APPENDIX: How to apply the Apache License to your work.
    +
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "[]"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
    +
    +   Copyright [yyyy] [name of copyright owner]
    +
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    diff --git a/deps/github.com/ledgerwatch/interfaces/README.md b/deps/github.com/ledgerwatch/interfaces/README.md
    new file mode 100644
    index 0000000..8e156f6
    --- /dev/null
    +++ b/deps/github.com/ledgerwatch/interfaces/README.md
    @@ -0,0 +1,28 @@
    +# Interfaces
    +Interfaces for Erigon components, compatible with Silkworm and Akula. Currently it is a collection of `.proto` files describing gRPC interfaces between components, but later documentation about each interface, its components, as well as required version of gRPC will be added
    +
    +
    +
    +See more info on the component and descriptions in [Components](./_docs/README.md)
    +
    +
    +# What's in this repo
    +- Protobuf definitions
    +- Wrappers:
    +  - Rust crate with autogenerated client and server based on [Tonic](https://github.com/hyperium/tonic)
    +
    +NOTE: You are free to ignore provided wrappers and use the .proto files directly
    +
    +# Suggested integration into other repositories
    +
    +Using a go module is the most effective way to include these definitions in consuming repos.
    +
    +``` 
    +go get github.com/ledgerwatch/interfaces
    +```
    +
    +This makes local development easier as go.mod redirect can be used, and saves on submodule/tree updates (which were the previous method of consumption).
    +
    +# Style guide 
    +
    +[https://developers.google.com/protocol-buffers/docs/style](https://developers.google.com/protocol-buffers/docs/style)
    diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/README.md b/deps/github.com/ledgerwatch/interfaces/_docs/README.md
    new file mode 100644
    index 0000000..62eba7e
    --- /dev/null
    +++ b/deps/github.com/ledgerwatch/interfaces/_docs/README.md
    @@ -0,0 +1,138 @@
    +# Erigon Architecture
    +
    +The architectural diagram
    +
    +![](../turbo-geth-architecture.png)
    +
    +# Loosely Coupled Architecture
    +
    +The node consists of loosely coupled components with well defined "edges" -- protocols that are used between these components.
    +
    +Its a reminiscence of [microservices architecture](https://en.wikipedia.org/wiki/Microservices), where each component has clearly defined reponsibilities and interface. Implementation might vary. In case of Erigon, we use gRPC/protobuf definitions, that allows the components to be written in different languages.
    +
    +In our experience, each p2p blockchain node has more or less these components, even when those aren't explicitly set up. In that case we have a highly coupled system of the same components but with more resistance to changes.
    +## Advantages of loosely coupled architecture
    +
    +* Less dependencies between components -- less side-effects of chaging one component is on another.
    +
    +* Team scalability -- with well specified components, its easy to make sub-teams that work on each component with less coordination overhead. Most cross-team communication is around the interface definition and interpretation.
    +
    +* Learning curve reduction -- it is not that easy to find a full-fledged blockchain node developer, but narrowing down the area of responsiblities, makes it easier to both find candidates and coach/mentor the right skillset for them.
    +
    +* Innovation and improvements of each layer independently -- for specialized teams for each sub-component, its easier to find some more improvements or optimizations or innovative approaches than in a team that has to keep everything about the node in the head.
    +
    +## Designing for upgradeabilty
    +
    +One important part of the design of a node is to make sure that we leave ourselves a room to upgrade it in a simple way.
    +
    +That means a couple of things:
    +- protocols for each components should be versioned, to make sure that we can't run inconsistent versions together. [semver](https://semver.org) is a better approach there because it allows to parse even future versions and figure out how compatible they are based on a simple convention;
    +
    +- trying to keep compatiblity as much as possible, unless there is a very good reason to break it, we will try to keep it. In practice that means:
    +    - adding new APIs is safe;
    +    - adding new parameters is safe, taking into account that we can always support them missing and revert to the old behaviour;
    +    - renaming parameters and methods considered harmful;
    +    - removing paramters and methods considered harmful;
    +    - radically changing the behaviour of the method w/o any changes to the protocol considered harmful;
    +
    +Tools for automatic checks about compabilitity are available for Protobuf: https://github.com/bufbuild/buf
    +## Implementation variants
    +
    +### Microservices
    +
    +Erigon uses gRPC-powered variant; each component implements gRPC interface, defined in the protobuf files. No language dependency across components.
    +
    +**Advantages**
    +- it is possible to run a single node spread on multiple machines (and specialize each machine to its job, like GPU/CPU for hash/proof calculations, memory-heavy TX pool, etc)
    +- it is possible to plug & play multiple variants of each component
    +- it is possible to write each component in its own language and use the power of each language to the most (perf-critical in Rust or C++, Go for networking, some parts in Python and JS for fast prototyping, etc)
    +- it is possible to replace components as better version in another language is written
    +
    +**Challenges**
    +- deployment process for average users could be clumsy
    +- managing multiple sub-projects
    +- testing interfaces, extensive integration testing is needed
    +
    +### Single binary
    +
    +That's when each module is in the same language and compiles to the same binary either as a static library or a dynamic library or just a subfolder in the code.
    +
    +**Advantages**
    +- simpler deployment process
    +- simpler component compatibility
    +
    +**Challenges**
    +- have to settle on a single language/framework for the whole project
    +- less flexibility with upgrades
    +
    +# Components
    +## 1. API Service (RPCDaemon, SilkRPC, etc)
    +
    +Each node exposes an API to plug it into other components. For Ethereum nodes, the example is JSON-RPC APIs or GraphQL APIs. It is an interface between DApps and the nodes.
    +
    +The API Service's responsibilities are to expose these APIs.
    +
    +The API design is not limited to JSON-RPC/http with `eth_call`s, it could be something completely else: gRPC, GraphQL or even some REST to power some webUIs.
    +
    +The API Service connects to the [Core].
    +
    +In Erigon, there are with two interfaces:
    +- [ETH Backend, proto](../remote/ethbackend.proto) -- blockchain events and core technical information (versions, etc)
    +- [KV, proto](../remote/kv.proto) -- database access
    +
    +## 2. Sentry
    +
    +Sentry is the component, connecting the node to the p2p network of the blockchain. In case of Erigon and Ethereum, it implements [`eth/65`, `eth/66`, etc](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#change-log) protocols via [devp2p](https://github.com/ethereum/devp2p).
    +
    +Sentry accepts connections from [Core] and [Transaction Pool] components.
    +
    +Erigon has the following interface for sentry:
    +- [P2Psentry, proto](../p2psentry/sentry.proto) -- sending/receiving messages, and peer penalization mechanism.
    +
    +Both the [transaction pool] and the [core] use the same interface.
    +
    +## 3. Transaction Pool
    +
    +Transaction pool contains valid transactions that are gossiped around the network but aren't mined yet. Transaction pool validates transactions that it gets from [Sentry] and, in case, the transaction is valid, adds it to its on in-memory storage. Please note that at the time of writing, Transaction Pool component
    +has not been split yet, but this should happen relatively soon.
    +
    +Miners use this component to get candidate transactions for the block.
    +
    +Separating tx pool in a separate components, makes forks like [mev-geth](https://github.com/flashbots/mev-geth) unnecessary, because it could be just a separate tx pool implementation.
    +
    +Transaction Pool connects to both Sentry and Core. Sentry provides new transactions to the tx pool, and Core either sends events to remove txs when a block with them is discovered, either from peers or through mining. Also, Core can re-add txs into the transaction pool in cases of chain splits.
    +
    +Erigon has the following interfaces for the transaction pool
    +- [txpool, proto](../txpool/txpool.proto)
    +- [txpool_control, proto](../txpool/txpool_control.proto)
    +- [mining, proto](../txpool/mining.proto)
    +
    +See more about the architecture: https://github.com/ledgerwatch/erigon/wiki/Transaction-Pool-Design
    +
    +## 4. Core
    +
    +Core is the passive part of the replicating state machine that is a blockchain. Core maintains its state and reacts to the protocol messages from the
    +outside, with the goal of synchronizing its state with other nodes in the network. This synchronization is achieved by applying or reverting state
    +transitions.
    +
    +Currently, Core is the largest and the most complex component, and it has its own internal structure. State transitions are split into stages,
    +and that gives rise to "[Staged Sync](./staged-sync.md)". In the staged sync, we consider two forward state transitions and reverts of previous state transitions
    +(also called "Unwind"). Forward state transitions are split into the invocation of functions in certain order. At the time of writing, there are
    +18 such functions, representing "stages". Reverts of previous state transitions are performed by invocation of another array of functions, also
    +in the specific order. See [Staged Sync Architecture](./staged-sync.md) for more information on Staged Sync.
    +
    +Core connects to [Sentry] and [Consensus Engine], and accepts connections from [Transaction Pool] and [API Service].
    +
    +## 5. Consensus Engine
    +
    +Consensus Engine is the component that abstracts away consensus mechanism like EtHash Proof Of Work, ProgPOW Proof of Work, Clique Proof Of Authority,
    +and in the future also AuRa Proof Of Authority and Proof Of Stake mechanism. Note that at the time of writing, Consensus Engine split has not been
    +done yet, but some [work has been done on the interface](https://github.com/ledgerwatch/erigon/wiki/Consensus-Engine-separation).
    +
    +Erigon has the following interface for the consensus engine:
    +- [consensus_engine, proto](../consensus_engine/consensus.proto)
    +
    +## 6. Downloader
    +
    +Downloader component abstracts away the functionality of deliverying some parts of the database using "out of band" protocols like BitTorrent,
    +IPFS, Swarm and others.
    diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/staged-sync.drawio b/deps/github.com/ledgerwatch/interfaces/_docs/staged-sync.drawio
    new file mode 100644
    index 0000000..d2a1824
    --- /dev/null
    +++ b/deps/github.com/ledgerwatch/interfaces/_docs/staged-sync.drawio
    @@ -0,0 +1 @@
    +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
    \ No newline at end of file
    diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/staged-sync.md b/deps/github.com/ledgerwatch/interfaces/_docs/staged-sync.md
    new file mode 100644
    index 0000000..88a9874
    --- /dev/null
    +++ b/deps/github.com/ledgerwatch/interfaces/_docs/staged-sync.md
    @@ -0,0 +1,103 @@
    +Staged Sync (Erigon)
    +---
    +
    +Staged Sync is a version of [Go-Ethereum](https://github.com/ethereum/go-ethereum)'s [[Full Sync]] that was rearchitected for better performance.
    +
    +## How The Sync Works
    +
    +Staged sync consists of independent stages, that are launched in special order one after another. This architecture allows for batch processing of data.
    +
    +After the last stage is finished, the process starts from the beginning, by looking for the new headers to download.
    +
    +If the app is restarted in between stages, it restarts from the first stage.
    +
    +![](./stages-overview.png)
    +
    +### Stage
    +
    +A Stage consists of: 
    +* stage ID;
    +* progress function;
    +* unwind function;
    +* prune function;
    +
    +Only ID and progress functions are required.
    +
    +Both progress and unwind functions can have side-effects. In practice, usually only progress do (downloader interaction).
    +
    +Each function (progress, unwind, prune) have **input** DB buckets and **output** DB buckets. That allows to build a dependency graph and run them in order.
    +
    +![](./stages-ordering.png)
    +
    +That is important because unwinds not always follow the reverse order of progress. A good example of that is tx pool update, that is always the final stage.
    +
    +Each stage saves its own progress. In Ethereum, at least a couple of stages' progress is "special", so we accord to that. Say, progress of the _execution stage_ is basis of many index-building stages.
    +
    +### Batch Processing
    +
    +![](./stages-batch-process.png)
    +
    +Each stage can work on a range of blocks. That is a huge performance improvement over sequential processing. 
    +
    +In Erigon genesis sync: 
    +- first stage downloads all headers
    +- then we download all bodies
    +- then we execute all blocks
    +- then we add a Merkle commitment (either from scratch or incremental)
    +- then we build all indices
    +- then we update the tx pool
    +
    +That allows to group similar operations together and optimize each stage for throughput. Also, some stages, like the commitment stage, require way less hashes computation on genesis sync.
    +
    +That also allows DB inserts optimisations, see next part.
    +
    +### ETL and optimial DB inserts
    +
    +![](./stages-etl.png)
    +
    +B-tree based DBs (lmdb, mdbx, etc) usually store data using pages. During random inserts, those pages get fragmented (remember Windows 98?) and often data needs to be moved between them to free up space in a certain page.
    +
    +That all is called **write amplification**. The more random stuff you insert into a DB, the more expensive it is to insert it.
    +
    +Luckily, if we insert keys in a sorted order, this effect is not there, we fill pages one by one.
    +
    +That is where our ETL framework comes to the rescue. When batch processing data, instead of wrting it directly to a database, we first extract it to a temp folder (could be in ram if fits). When extraction happens, we generate the keys for insertion. Then, we load data from these data files in a sorted manner using a heap. That way, the keys are always inserted sorted.
    +
    +This approach also allows us to avoid overwrites in certain scenarios, because we can specify the right strategy on loading data: do we want to keep only the latest data, convert it into a list or anything else.
    +
    +### RPC calls and indices
    +
    +![](./stages-rpc-methods.png)
    +
    +Some stages are building indices that serve the RPC API calls later on. That is why often we can introduce a new sync stage together with an API call that uses it. API module can always request state of any stage it needs to plan the execution accordingly.
    +
    +### Commitment As A Stage
    +
    +![](./stages-commitment.png)
    +
    +One more benefit of this approach, that the Merkle commitment (hex tries) in Erigon is its own stage with it own couple of buckets. Other stages are independent enough to either not be changed at all when/if the commitment mechanism changes or be changes minimaly.
    +
    +### What About Parallel Execution?
    +
    +Some parallel execution could be possible, in case stages aren't dependent on each other in any way. 
    +However, in practice, most stages are bound by their I/O performance, so making those parallel won't bring any performance benefits.
    +
    +There could be benefits in having parallelism **inside** stages. For Erigon, there is **senders recovery** stage that is very CPU intensive and could benefit from multicore execution. So it launches as many worker threads as there are CPU cores.
    +
    +### Adding/Removing stages
    +
    +Adding stages is usually a simple task. On the first launch the stage will launch like it was launched from genesis even though the node might be in a synced state.
    +
    +Removing or altering a sync stage could be more tricky because then the dependent stages should be considered.
    +
    +### Offline Capabilities
    +
    +Not every stage needs network to work. Therefore, it could be possible to run some stages, especially during genesis sync, no matter if the node has a connection or not. An example of that is indices building.
    +
    +### Risks & Tradeoffs
    +
    +* Malicious unwinds on genesis sync. Since we are checking commitments once per batch, that could be used to slow down the genesis sync significantly, if we sync/execute everything but get a wrong root hash in the end. After genesis sync is done, this is not an issue because even though we do batch processing, but in practice at the tip this architecture becomes block-by-block processor and is not worse than anything else. 
    +
    +* Batch processing doesn't allow most of the API calls on partial data during genesis sync. Basically, regular geth at 1/2 of the sync will respond to the RPC requests but Erigon requires the commitment stage to complete to allow these requests.
    +
    +Those tradeoffs are related to genesis sync, so in Erigon we are focusing on reducing the need for genesis sync (such as off-chain snapshot distribution) to minimize those issues.
    diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/stages-batch-process.png b/deps/github.com/ledgerwatch/interfaces/_docs/stages-batch-process.png
    new file mode 100644
    index 0000000000000000000000000000000000000000..7501442c0886c19c905e2c9d8b1ab5aa7dae7b13
    GIT binary patch
    literal 83515
    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

    ejbRd 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¬xV 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 literal 0 HcmV?d00001 diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/stages-commitment.png b/deps/github.com/ledgerwatch/interfaces/_docs/stages-commitment.png new file mode 100644 index 0000000000000000000000000000000000000000..d84708816ae3598a64cee0a0a5b680737eba2bbd GIT binary patch literal 61030 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 literal 0 HcmV?d00001 diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/stages-etl.png b/deps/github.com/ledgerwatch/interfaces/_docs/stages-etl.png new file mode 100644 index 0000000000000000000000000000000000000000..13d5e9dd3a807aac72784847cfbafa5bd7a73057 GIT binary patch literal 49802 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 literal 0 HcmV?d00001 diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/stages-ordering.png b/deps/github.com/ledgerwatch/interfaces/_docs/stages-ordering.png new file mode 100644 index 0000000000000000000000000000000000000000..c4fd8e869a692639b3050a1940bc572309cb3ad6 GIT binary patch literal 12190 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 Gqy8IwjIkO3 literal 0 HcmV?d00001 diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/stages-overview.png b/deps/github.com/ledgerwatch/interfaces/_docs/stages-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..629820b86e3d8a5e7910d3d74f32e6e58182b41e GIT binary patch literal 16305 zcmcJ02Q*yWyFL?1NFqv#AfgjQ?=40h45QbOiEi{Z7-bL=LV_p}UGy$dqo?bgXh9-6 zL-ZiSe^0*ecklOocir{>-MiMcmYFmA?6c24`|SO`&-=X3ex!|nk)NYGM?^$KuBNJ_ zOGI=k864M;o&ldD{nC5ji`Y{ara)BQdwm)FP_i*rv(?lj;s(d0M8x4JA`(Is@J|Q+ z!R1-RM5n+vF`+CA@~^X}lCw_z>zJs9P_dbBTak$9w6T}6v6r*2Ey~G;h+SCmk0W*= z0ee?ZFLq%ib|E1PH#a_Ol!cv#g|jE0i;Wj30_UAwtx>ip8|y#n2nh%Y@(PIY3W(?n zNU{sd3krY_5dl64VKLJ`>RZ^^xcu3mD4zgm;HIX970SiS)zh9`SONT2bMdlq0^gt* zd}`@|4`c8zD1a0cMT&EQLwPqhCmSOhOE?PjK}AeJluwXPP!ycF30KwE)L<7<1jkM& zM;q`3v$1k?C0t}^|w2_`bTv^Q+tGysHdHZ8W=Pc2P;oeT|Wg~Pf?7crl=qS zE#Zu^5U^0Ua`muNLs&R^i;I}3Sy|ZW86&h@J)PjTmQE7dR*I4;zJlIPMxf&=ilTZh z+QKMJcO7kO8!aypVP79L9bF|6F(+jYw1A1av89!+vZD&h(pm>@0qUz6TS&r8ob>cz zx>nXYR*oW07)vjduY!w`qm8etp1i${wUvO7i>j%(pCL?9!${d$&B)!uTfolKz)wH} zrlBvUfH76KaB+o!#@yB1FsAapf)1K)2KM@XVvY_NVS8V1b$yhvpCMYv*hs~~#}8rU z?Bd}qFJ`1_r{kh2sx4_?r!1tRErK$#aTXObR#kUVM|p{0jMS}!e6&@K1Wnu=jZxOF zVs>Z`YkdqHC1eP*HdaA9IC`ohM6{Ib9Ie#d-GmgiOiZ1e;V$~BdMYYH_Cj`&BGx8w zF+!(AO$1$Fj$Ya}rk+abDmKFGLJImiHfmn--bQwCP{BY)0wrND=8J(ldBBV$;bQU{ z8p`VK9v*NFS5rkW!BiwnJoG)BbQIADO(Q)KVS9v{zM>&aPgC2#6s~V-B4&lQP*qX3 z6gM<5HdWNq5)%>?Lt9#Th)9a4X&a)fT-B_=t5I+?(6%wQQ<6|3%v%*T3k^kAXAy08 zWhWFy-`-N&1EFZ5rmy3!f)Il%S;M?kt#q99FenctRdshgEm32XvaOnzj)tQ#e|K$MC`!roK+2c^dwx> z!AVsGv?2m#En?&(0k`zC(Ng#E@|3qwLD+l46r9m+u42w^0;(b|hT5(Upf*a?N5f9d zNW;|7*vClC#{^>nvvpSTwN;l_xASq7R~9!gwz1O}^3wLQQE>-1(zJF2pI}@S^qfWD z#wK7HDp=d97{EOJlx)3RjomeVS1^=RFm-nkws#fth50&&5k^Eq9q#OFY2z#=DyRdj zg^H)1ri8b*ytTH0IL5}_*4Eq0R!K>b3t9h49rwmuS$dXD-qTQ8WVl{2tADvECMu0kdl zNjGB$5q&sVfC?rkQwMD~PjC+W0H0`#h&VWMbn)^AzkK8g>s^>ICH}R>e=m6O`%fb( zten0ozD`6WS*NBXukTBYOD9XX@^K`1;{sFG#mLvMNDHs&T)m}#pNHq5SxrQ!Sdzb%{XYGM`b|gf zf4Knqmh`Lj)eG!o^8aiJ!QuSRB9e0|S@wv9f=nzoGGJ5rElH2ai>$34{oAwP>Vr$9 z#hGUlU5`Jg694Yh{lRbY^YAB%OsbI6K0=w81={7$WvOdbeYYzYGyFTB!ftSB$!sbO zbQznstd3SB30i$>7oQ2x^OF2^!E0!GyH{GvwkKsibM_L1vOOM(#>@2OmNYjv1i;IX z3*E_9Xn4#G*}3(`jb|sv`>i9Ly~>0!aOc!czsS##y0XQahx_?8lp?pZRHOIy(VF^4 z23f!A>)vA)iM^YrArDxzwK?_DUsv@?!K0QuX^8+x(#J<}{r4z5s!iY$OShe-!`feqzS&byP za_x3SUg4SJlskA-YEq?w-Hbv;+domyqX_g=*n6=M^;%o(+2cwE1;3 zbHc~p@-zFyzzZ5CPRrrurhN}`8Z9Ju{L|{-_-F&SWTUM7V6AbZMaXGP;9#Tz%RYlW zD%)8ef_;i+NWJ+kaDR%^F3tTR18#MyvChmNSL01m9eA`S2H*;-N3E-K`|IyM>htj*P7;&#Z*v1sL}tK-CJhGnK5uGnpQ z$;C8J>**#;?UqoW*hst7`}N8$zIbwaF{Rd7E{h)|-Tc>1pO^dfBh>CXyu#xB!|7n+ zQ%!9*{JVu&F^_gM+kQOWe)e;^NRM+P#j$*|yJ9ng`CGczpxWMRtG4J2_XVe>)$+#P zNym!jHH@%mcgdZ)a=WxfB|1NCy%sIm-2}}UiP#Kx(`G*N2H#JZo$gbq1QqM(mQP^TaT{kafgq5sx|5jabEt<7c8+ zNiW9+UyP>_u@5!kEYnOC%Sb-K;aeWk*?wNB%f6{!Q-{+Y=w&mxe! zM4VOJR4s~3a*&bTZHeV1&^yfgaEg6>*J2lz-xdq%2eGJSu(eGa*|incyaTOj8N3}- z9G|>gRkUHfQdJ>Uyt^{8{p^;>>^JLtS7Eb`OsIT%_eRSR-?y8^_A9~5sxPE`FL7wR z90d0sJo?UfvXqRfm}a(~s<=P(oB+ImTb9)^arv=4T1Yrh zOw(Iv7n}aXXRCwryEXR(g#psbUYZM;$2**;sfL=(=GrsDY_6lCbm53@evZ9dmkS~` zotKZ2nz{^%ccW}C50|fsv_cQmBHbzH%j%}p14ctNwsu{A&c$oZ4Z`_rf^6CJyq?4} zcI%r7vZfr5syB+QH>{R>YZgxD1T{SB@8WO0b(S%}jpg=P%3$awsr6uAPC3%6Y=5(j zk-zv3x(;eSg!p4F?hxkUj)^gntdRaRs=ryQyIambZMAB-d4HP6cG)OTR?X@DuTUs4 z+2!F5k?jx4=@L_GFq$i=MS0n&U!Q-&tY#H`SusvQBG-_W@yX>H1sQ(D&rzk>jogUL z+f(b*B^4@a7e(h+rC+XGTTx6&&g8en7ZmChRS@-ORi@OW2w8o)Ajn#j9gR!(nNDt3 zLDKx_mEMR*F6ObF%Aaymq`7;9;$-mvUhnb)!kZ9&(tsUEFG=Dv=eB(qF{0D(DEWB# z42tBpQyj~HjyZ?7Smt`;CM=&tkEegEXg&5j>MmyQ*&WwE8|HP2Ik21qnq^ONPFAIu zUT3X(2$8W&>D^mbZgpM8*DcSfd)+wkD&4A=iMjvs*>t`Vl)Zqo1BPx!!qeL?xIA9t zibLmUQ^gR%bp)MkTm8QL$`ocXUAAiIndZrJtLRNJmoH2Bb)VMl$Cn%nR#SQ%#ZPZ- z3pak55b@e(p1Re(_G1aDelVUTe@d%Bk`hdxca()inT~uqpZOa$p9`Lu#4k^HLw-B- zTn#zJUQ74H3q&ejRnqIE)Cr8ev)ytsMK_dA+uwaDoiXZZkTR2Y zKI-a3=*Vc|bB59n5+;_P&0Uh3TC%Obz6tT}T=AySOVg%8)0Maz<0>S%vd8p9=Szmj zyAhAY%3V;-X&Qpt6S($#glxZ2+|P{{Lj#>h8?8u}yv**$MJb_@R!H)cEq*9K zfe6CR0^A_1HKCBD{S1Hr&n7@-9#dl8O`WEoF zl}y3d4#-sCh~!)YM^%1Y3U2S? z+VRbk!_n3>PXU9yQx_%P@P!#lqN{ajqQ2kztY2al9!yWt&+jsPN|#`lNG}nOI)`I& zLYDk~Wytz>`foIIHoQ%bcA{qbDisnJZQXG3)WKh1*Y87M=GYlJy0!GzAN~!Kfs>(*y zEu9FFzbYCQ@Yh1Hm=%A3ge%X(lnep#1F@%!pO~(Zt8)ojmm;W!i zU$cG1ttUm4fIu+#-BFv5kyk2#|1oLytHGPqxtOEm$Y*6|E2M~^Sf0>}*JgtH z@j1~_>J8{s4&e45E73;jknX?*3*&5uNwbjIhB*lZlq4@1A7rW0>2PXm9O*11$MU|} zT^;TE@aWR~w{XU1<5hO>#olz>dylSXygah1C0~INfGP#KsT~vBj#t|3wIxe>E~o-a zF$Vx&(vX&%`N~KMjlbDa(wI0gk+m8TK8{@=8}^K7kG0YO{x(u6>-iol52xq%Pfvuf zzkF$>yb6J*SK({=p&+?Cte1zyA1ZT8bto`RkhGi}`(sv$3g-BO4x*a&r~N&^gTqgb z@p*`JsR2IU$iWv#UFf&VBwK{(d+J8r!$@!!{gDSRnVGankDk7Ymvo*Gsh^GH$pygZ zbI01)D=Sh;W)_lniucyP)PL?UYYC`-Oed=Dx3@-no)jK0T%t^4qar9w|xM7NDerSaAFRIJ|Q{3CTCr z=#%u66{en0ke-8l_eJ-q>|>JIT;w}rx%M zd-pEiM$`7ii?+-T5h6_5r-<>)u%pyHXek)*m6)jD)sz?w3_m~$Dl zRIJ%aEe6pU<+muInCpoKBPMita zU?8#(`7V%LypE4Y^t)9=Kfj3xQMN)>1W9%wGBP46mw_o5@0LYPNep=i<=C@o6ivQU z>-37Ey1{=ff{K-hjF?mc`n9;Eyt1(myu1-a4jP)9rUY%B3wx5vq^%Fp8Q@obpfno- z2At&XX;PvP9I3vYTx}|!{}HaP>mfP4_0J#QCfPrbkwTfSK_(b$kPasoJfW>&^?R|h z#U$C;$ZS)&1VP1)1x64qx?D*nXi*5j;QvpLJ!P2VtcV{DnjC})3;8Y zx$siY8bjn&&;69QoTM5#jUF|x&2$JN*ZZ*>)fs~&)9Yb&G$Rc&Y5kBFJ!^_sgPA#I zc!WQIN*z6Q6KacLcrO;kyw*>6LSC*Uz5Z-u z*7*b<0oT;)6}Y5oBew<|2xhn1R0BGl zpa1aV0k;B_l#Mh@vz3ixktq(W?x&@O=dS2L3~=EL9RYJsVY$wewGz*%%Df`4@Inq1 zWA}`3brz&cNdm*vLJIdl7nW&OcqhPx1La6{8eYPMXrZu9HZgH_7s=2Cr3 z$~GBeYRkKz+3-{~^LC(RSa7xi(-#sDLfCwJ_n^{cN@{tyD8A~!KoMez#;)Y-Tc+4J zsPPV5{hK5$Eo3aFU7!8*BdQSVBc0ohpKmQsH~T-1)Y4(5B!^76)|@n4@8{5i`zw1Ns)~D&H0L2U`3d>_jR&!GQFK|cltl8F$uX^xqdihuXYHwA#VhMZ!T z{F~1Q90dnuVZ3C}aR9A^Tb77@Kh&$_&hb6Ue6cK?;p9dWW_z|&F^yOH@N^(GN1(2O z;Z9Ul@#(jgP@Oa6M1c;~zy+but~^t2@!pB=Ke4^c#Wr;eT?_`~O$c=mF-3opJe-Qo9? zn~PPP128)Q;GIu!f1QbTs^?@BO83-}lg z*+pr3AI1S9UpK6 zlNhaHND@LsQp_2QEy|!U`i&WxM8ORm_|kZJ!5g}cIa#xliqgPk1?Xh+})C*gxiHfzIi~j}Si4sGd(Or!4Md>wx#V%>UB}uN9R?XE; zy70U%4f^?>aV|j*K{aKsEZ#a$^9TSJCLhRBlJi@S37Csn9Kbv{hrMK+G$uiR zP_?}{w>!ck#k(8HQy%-^JWVHXt8-&kF*O@UGJyKJR8jSFQSJoA@RC{aVMK=9a&El! zyD&1bw$1NtU7$C7GeIZK`0`d6JkB-fk^yDPI`#->w^6^4;z&*Hy)kXB5_5wWH&GLx zENs`AtsFJ?`g?Rdpqh;M>gTA(Us@=#ln1#1R;ddRQOZFBw9?+CWb~Wy_(_Q4a#|m+f)%?LCEDg3DAx2VdhFOS9i@T)pRFCkSU9CcCiDu_~Ac0_n@&T(i7GpM{DYzkc zth*e_rM)c7s~v4iqBnCei=XEY+U#!f>C@2Ljh5L`?b9&Wb!j=UtpMIU!~J$M4KiTh zljBHnqTO}LjW3>kyN`uYc>)`T)uVR}F;U*MrS1w2GTe2WS)(Q1`Tux(wwdwdXPa1akzRozcb>aaGq@!N zf3&}UmH+Nqx@WKC9#(E}+%6MG3;T>6Y3jawZ{rbX#t**c9o^LFHpZZ}Wb@$W&t-S~ zIWq$)_qV$E{Q=?HV0Tycdmv~$!`-(zioa#-4#0xEsnd0!i`5z#fLu*2cYiG_yFT_R zXB1GF$h3a!nURdpPVu40bLuUCrA>gNH~aaQl8SMxg?rcaTMWAT_I;^Ii}9xuU6~zd z38rNt`o2S73cn{k`2(lW=%1*5E%SH+{}|=|)%pa#pL9t}M(0N0j*hfPA!{O?msg(z zN>@jRSuy-#&EoN9OwbsW8pHUIa|6AIJyZA%ze0v5B8a4Gi(S#NAOtp^_FLtZwk#4~ zA^l1&{)H(MdsuX>Kz}-L-yAiR|HKP7F%pfP_z-_gd)8=g%!M%!Ri=!bi|tOskv_cZxU+w@W?8>aKJL zf+fD^`2HbzS!fY5TJG3)^>J}%(MCDJTo=X{4Jq%YNTo`zzs~u!SJ!$)xcDZFF|az| z_+SxRxwl=I6J42UOEN5lTDj-Y&s7D;*d*EB1?0$-yC?&B18x(TSf})9o33XjXH6J1 z$X9+w(+aC}FO}f$?jkich6N1@OhXZ^=3t_Yu>Axc=o&Yv}*9}43G0D9t z5tjM&9MQXPwB!!vLeAc}NuZvWo{iD9{Mv#`v!ptZ7v8OSq*i8HBSHt(Wnl?>0*uqiT`XQqzbg**$1gLdAWG%e?+60m&b(R$+~ zgE6`%7hyHDioGsJKa)J1ZYsJ|P_C!Xi3p9h29$TRrP#r%#_kOFPqda|m#EV#Q_`N@ znDPDoM9A_3X(ONRsKiP``<#TB{5C(vYpZFyH=Y2wb`uT}a03Nwpp=2FbH^T=K5;0F!7Zal&Wg2Z2WWu&2qnRm61%GZ?{pgyk?6YD=JFtN%GQA%7d#VS->sU(C;plz`W8e zZp*{Yqm}fA248~sT-sro=gC|(TD`3NWcbO0fV2Due#KT(POu@3=~7OErs72}T<$_1 z{W05FbMa!E&*Xt|ip0*uHA$ih>Ikl5gF-ia&Oy>CX@{`}v)Ec^m33y7y;Us<&o`r*S% zt~@ZKEL4a3?2hb};EX4Q-!cHtyZ;1qU)<%(>5u@>{Vn!(y!<7% zk64o0O|AYb&|}I72(h^|^@nnc8PX3=iSXjlHS36ERM7hH={P9U9mqrw z7@^d%mmk9aj&F&G_X%_q5~fP}cYw&57Y@I1b#JP8A3o!TBG~}{(y+kfl^ytn#H-5Q4+)?;1mE5& zeP+QLku0QqxUzB6>PtafGyA+1?0d+7@^a7dXCr}Dv!9pF@@ z<|nwMvwJyifzvN<1qx6sPM_y*(ms6LK?OO@kpN)%cr7NKP@v zJo5hzpr1|ewb-#Ps_Kzw{o7qy&k0eOc+5WHbV+JwjgesiX|KukfZLqeDMq(7w(4n) zo>IuJKL{RM9AwJWI<@^{=k2w#@qCo0ArsN&-aFZEBqoKjkbkVhEaX;6G6CyxUGiOe z5w4rjw`i@u-5IWSZQ~<0?A7O3$txjQIM_XeE%&vwuJktm&JRgR`8fk5TF=>=4v=3d z0co4#U-11}%nZuVJ@#7EQ|bL--5@fQ-X}1gFff>xydKc0BtaHJ4TACnqRJeTuhvqA zf`GY71St$@$D)f;OH7#c`o-GDzEX;(9GWTZFD>5JHa-JLEHglN8hhlN+6}zO&CoOC zo#JZiVEsg@65(}~UbErYLO>oNFM((3AmOR7Ofp}O*P^h7m`$}8DqlKNhzg;=bRI1H zTlc6&s7akPZM8jd<`q8pD_h{k@#|BKcI?ID-B6V$bnF2A7JFr`2NWX~o(o!Wr}KYE zx*_Y=&Q*}cVq}g^Of*>x1o^3u2dYwDD3U**+_`^2Ic)&t(DAB}!Xy#V*T>{yKMAfMS? z`5L>Pq^I52S2qE4NW6rQV0Pc85B@HJlm`rqn8 z9uH1$gRD`PMK}Y8mK?qsU(Om$$Oz68!2P%P;gg=rg9A}fS9#;UM@u92rhJgU6VL!q z?PbgbrOXiG1=#f-7e9|F^k@em#S`Iys~I(I)7R2ZO6nXgyCXghxIw360%24)?N_o? zu<&A48@#53n_d4ANz+#>u@he6ieX{%Q_fMZcw|=j<8Vrtrs62NbOE4Zxmg2IJ1PX+$X4s(F z$x2B@>5DD?)%~E8qnW+ct@*C9OWr#Ju&(|Wit_;Ehd*YNy9+{Nxnu#0`%cqMCd1$S zR?F(<2)=uN$v@dEWi3V2v2;71b0{xov{!cD3_!7kRM%$|w|_zb$@HRt=YgJ<>`3&j zm0rIU${^76UV1JzcWz^OOV?yXaF`}k3wQ=X?y~;)aCN@4s<(1}K*nnr4TK6}fm39g z1#Nenm+0L#o!Wgx=L~H^M|y;w#J+# zK_|zRSiBzwM15u7iyS$J$A`06{gUqb6$OBzy~Y!M11SQq?*=w9e<)v*etDv%1pV`u z|1Yg{g+`F8A9Dl=q}CuQoLzB~m-@iF2j^`js!`rMI-}T?oB0UgiT{N2Jpj(PgW9g? zh#ozLF92f?N9Wf%Pa3Wa7Xd80_Z)4ydqu1>j1X_bm<*80;9XZbDt`dEmswR1e;fPW z=GE-7W2=Iuq4!JY`a$0MgAv|Fub^thYY5n|L8<2}X2(D}Qs+oJB6tMeqcZGv$wU3+ zC4Ev&J%G%mYz#-AThj|YH5q-Sudi=>R&Pk0zp8ds8hu&FpnqThEaLHtYsk^wUmcv8 zV-+jUi`VE9!lkA_;GX3NN!~Hoi5g>U>E|D*V=o5|auZCjG1D2D%x5iA->nzknia$y zGGp|JWsOPjV5ng98Ka(W{>y zAs+Q-Wo8EMCCiqtXY#$%d}dP6&JnJUSys8dLbR%HM2^HB2*}*1&1hcd;8zyw z3-boASICluZ*AXzo~BTPslFl`Sz9UJs_()YK;XY(K{CS1pL;9@mwfr?0>U;r3>{e* z-P`ppDef(G(NzK~R|ue}BFCV6z|VcY3s^yr<>MqQ&!5iBOIm=jam)fdvUi&S`Xaho ztDy8y9(G}u-=KfFQmRbT$8jX9f_NZy&H8XZ9}esL7yg!()fkm1*4^BwB0JtdVh(D# zZ-X4h+9$b&3|(<~?eKnz<*mWhx+!m<>=-rosiD8JY^zA;b#{ExwROrRKWqO8Gvzg; z=^bu1WhUQMBt0tTG}b?@w~ORXF8TCLnq*dm2!9{@=uT=u6Yx`Jw zo7}NHpQ-zdtI(55sl_DH#A=5C{L@zk%XCTwLu6P2Hb2sMUp6TCa(MBsX2-IvG~oP; zt4*{t_m|M-(nYLw1~WYQ8=ATWCgMA47Xf;|5}5dCy|837=LQJb<>ymTfoIqOuD0!D zAMynhNmOm(Z_i*mAs0d843V#C&D!Gia<&9rn#lhheO zhrT$k5scNpm&HbN4pM!jt$#^D3c=Klt#;JdCq5XHST8L#&UK1Wd8rO!}{_`(EW0vt#IgN z(xnM1vExSyn!0C5U&G#@@v}Ds*|^z=Cy>mjxT_@UaOjciy&)U;9es~~aLGg=x&Xx9 zI&HJ{TU+s;c$@MPz|?|8_K_gh^w*Sd)@6Xf;qu8c|0J2i0LiSuonrY9tbOw|z~H*t z929?Rg$N?Z92`#@ADRFBA6Q#m9*A--Cv=?pTLL*4ssV(6pK`OgIR1{c|L?mW0!{Be z*k0^<6LKmKXzg?j4L>nnP#8H=J>_teF7A4l>yR5m$jjbGkXM<@w_1;Xdvq}bLj49y zSYQV}g@F3y(#t1gT5^F=sPvdu-;{}wyB1bVZr?bv=RbiecvNe5Um8r6rJx$zNBR93-AhoBAch3%;DEre5Nr4J8R>@QH>xezfY=>DyCW9a_E2o zc|AB^+VS93rIViOEcuRbY)AFOh(#&lohU-${UfmMH?NuuJ~{s(IcbtFBA1Rc`3Rhdgq^PA3=kzojY z`}Q$hIL2GDhM`d=<(l(h5j-5L*7Kn18!(P!$aH4+*C052XC9>5`UKj)$11?-Ne_vR zl>fTjC&#Z+nM;^g(0+aq$nS~K{s3f7X&k;ao#hlx+kIxZ0tl$_9{cFqhfCG&9?dWB zy@g-^ykEop3nY)`zNP>=2xj6PI4KYLQvz)+0WuKZpzI08Bx&8$r}(|Sfsm$wpmCfA z*`xb*qJVyBG>PZ<0wj0<5h=XgW|9Yn+nf*?ub$~3%;(n{pal=yWtbR9jJOkcXC7c` z(|-ZA+5f44nUn;WGi+HAi7R6&a`_Q zqd^r-TajrVom_kVXiOQh_3m#{8{pgdenpBz02j5UeZP~}*MQNKBQf#s6(Zp_bl`B7<&1$4c4BQ>MG%|0KX5(7XyjSTi5(C&n zKLFgf94jF zZG4ZBs~yeW;r~tAB!4#7-U8%uzFFmya4c%>1+vmv^q?)qRIW3Vlq(?)s*M9YPt?b& zU?JW#oGj)u$H5l(ui1gZl2dVgqPP#>DQ#KE(3K#PwrVYj#{i(qS#GdsO;9gNMbRAq z*tO$5=`8M_NvpGi#jhec2VZY~qL&p`fBtiwQD&!KF@sjL8tLdEG&G+Vy$lljzu07j z5AD-G1FCLRdaIokhe7KGa_W6g)#+9G^)~dZKZBIl%bAgm0Kl~4HWzwQ%c}4|T*p4E zGXDn?mHhe6Umglrqep)j6fF zAfcq5TYYkT#QB+jr30v3xZQaV-bma{%x|VE(4`ZxznAm;DSV))gbrvOSzMcUpZ)qq z5%h9K^JC<~Q_UX*FAw)~JK+T_DFcS6$=WhT&BsIbF+HTVvuQzJ+eSrGuw$;R_&K0w z*=>!aHK}Q9NZbNM^DU6HQZ@Sy(vt0ui4JXeICtQ-xCh1rmkWG#Zlp#RJ@d zqXEOTfdF0M7JqJj=P}pXp9lPMA|YC>wETE3`F3E#1v-&-7u#IKGI%zF4zAB4A}#r4 z(fQ9%oe5lnKn?5F;duj$UkK6RX?&bw)winzd%CylDU-PsV7gKs9B%@hwZ*@vb{g09 zGQE#LH}8JF(TPQ{!lm5%nI{hFwH2PTX;+ z{}^MqJj5tD%wXP#UbSVoU1gzVIpL7ku7>>8q<`B$#q?QA;8EoG7_7SiE-h6A#BGn; zr{~*Rm$IWuLyJ6;$wNPsjwA^8O62GQUb?xhwNI`M=uowVLFV@`Z^2OE0-?*3K&x_3 zuGaO+N4cEonDWV6ScHLxHjFhQpd>=)Wd$J_mlJc_l&|%dhxC&gpwSq~FVmy(a?X~r zPCkPcLMQ0g*8uIp(4~Uo#g9c&J%1> z_$B(Ms{+=ePQ{fxo*PO>~&& zc1w*6OeSa=K;X0blP(FO0-Sn5T!T*~gB%LzB|MU5f$}S6p7ieehiuP+e~UK06F**V zE-knL_o}&D`K$zZ5|3`dHtRq}@vzMQ1%o`npk`jiG(Qvj(F2d`^5yk;$N^XmbMV-sP>!#Wcv7bZr;X`m+|Ho;_fL-`7{_601F z*0z}DgFwuJT6U{48_9zF_>`aNkbTQQgP4IG$egMzDS50QqKsAXM zPL;CpVcwO(&eNmwB`kbMP|60`nPIzj{aG3ju41megA#I@H488Pa=G#}&*}fNK_plh z;MDY@bA;UbU!f``7U0@%6^iP>3H}w=zM%!>z51u`|FsYpCQ>7a8IfcbhO6N$mEsfgDS_y zS!4MGTF401SQDm7#PPER;0r}jBgF=2om0)Vzr{@?rl+khhLnOYJmi5CtY~p!RcUC07KxxEIf2sX#U^Os&-^J|*WSnEFbu z47v0Y2)7gJ3j=b{cH*Lk5_$@cKuN*RE^i zMPnZ!CUe*ZI4Q;O)Ofl_K4h;LH_L<68TtOul+}=eEIv&jW&w~#)aBI7yd;)N(4&Gq zJzRywIiN=yxmx)t5o){(H(TZ5oBxFUkeAP8_na)ZlF_@N6j=zIRtG>Vw_IBe@Q0TyKrNCIjyFGeIJ0&QEV8x<{tot@Gfud8cpZRi1UkOOi9_QQ^C zRL5pKGf6S~A2PpPm%v~K5&PJ*pyPw=JN?vPt%#G_2MuE}2lGkcgKzaCmuSQ%sCA)P z+#xqX?%!tCznZiVuYJ_EjjCQr90ZFAy_Yknnt%1TOA@I98#F+!GU7*41~rrkLQ>wa z!j0Kz-nRyv8xVai*S*kAyojeRUQrl(P8ku2eWZr|So?e|X3S9bw9j)wTiyosyTp5h z?NMMGO?wP>hqWHKsbUpCkT7X0K=x;XT4ex7DdfMmrY-$E@&aD>-9tq~%JyJr!Vdh* zM|SlH5Y#A`VIpp*K}5%6aA_KO{Xl9WZ8?|&qZohfo?`}J?$aLD%8ozwd>2ki{6^=opXyq$$wM z;$cTm_c&W!QF2l7p+&C=s#!XqT~q`rqzm#?yu+ZcFUoI+`6NJ@*I=JmlRGYau0Zl% z;zT3>f^TfycN{2B2%G!>78-0}s{@{DuPQs69!N4bzh6r8t`dxw49d8nZSk|qBY3Q!wFh!PEO%^ zU&qg-uPC}20ZRgUG+A-t8)+0~~aSNId)< jjG01695{f4eDmaNj{&c=?Xf-)_)}9xD3vSRyZ`?HM4^lv literal 0 HcmV?d00001 diff --git a/deps/github.com/ledgerwatch/interfaces/_docs/stages-rpc-methods.png b/deps/github.com/ledgerwatch/interfaces/_docs/stages-rpc-methods.png new file mode 100644 index 0000000000000000000000000000000000000000..e8f1e7967fb55b851ba58b4f4a3769a6c303c202 GIT binary patch literal 19284 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

    4?3C2<0CwcJRX;oNzh+43I!?cqEnn&I~NcwFs^ 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 prost_build::Config { + let mut config = prost_build::Config::new(); + config.bytes(&["."]); + config +} + +fn make_protos(protos: &[&str]) { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + tonic_build::configure() + .file_descriptor_set_path(out_dir.join("descriptor.bin")) + .compile_with_config(config(), protos, &["."]) + .unwrap(); +} + +fn main() { + std::env::set_var("PROTOC", protobuf_src::protoc()); + + let mut protos = vec!["types/types.proto"]; + + if cfg!(feature = "sentry") { + protos.push("p2psentry/sentry.proto"); + } + + if cfg!(feature = "sentinel") { + protos.push("p2psentinel/sentinel.proto"); + } + + if cfg!(feature = "remotekv") { + protos.push("remote/ethbackend.proto"); + protos.push("remote/kv.proto"); + } + + if cfg!(feature = "snapshotsync") { + protos.push("downloader/downloader.proto"); + } + + if cfg!(feature = "txpool") { + protos.push("txpool/mining.proto"); + protos.push("txpool/txpool.proto"); + } + + if cfg!(feature = "execution") { + protos.push("execution/execution.proto"); + } + + if cfg!(feature = "web3") { + protos.push("web3/common.proto"); + protos.push("web3/debug.proto"); + protos.push("web3/eth.proto"); + protos.push("web3/trace.proto"); + } + + make_protos(&protos); +} diff --git a/deps/github.com/ledgerwatch/interfaces/downloader/downloader.proto b/deps/github.com/ledgerwatch/interfaces/downloader/downloader.proto new file mode 100644 index 0000000..e2f0be0 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/downloader/downloader.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "types/types.proto"; + +option go_package = "./downloader;downloader"; + +package downloader; + +service Downloader { + rpc Download (DownloadRequest) returns (google.protobuf.Empty) {} + rpc Verify (VerifyRequest) returns (google.protobuf.Empty) {} + rpc Stats (StatsRequest) returns (StatsReply) {} +} + +// DownloadItem: +// - if Erigon created new snapshot and want seed it +// - if Erigon wnat download files - it fills only "torrent_hash" field +message DownloadItem { + string path = 1; + types.H160 torrent_hash = 2; // will be resolved as magnet link +} +message DownloadRequest { + repeated DownloadItem items = 1; // single hash will be resolved as magnet link +} + +message VerifyRequest { +} + + +message StatsRequest { +} + +message StatsReply { + // First step on startup - "resolve metadata": + // - understand total amount of data to download + // - ensure all pieces hashes available + // - validate files after crush + // - when all metadata ready - can start download/upload + int32 metadataReady = 1; + int32 filesTotal = 2; + + int32 peersUnique = 4; + uint64 connectionsTotal = 5; + + bool completed = 6; + float progress = 7; + + uint64 bytesCompleted = 8; + uint64 bytesTotal = 9; + uint64 uploadRate = 10; // bytes/sec + uint64 downloadRate = 11; // bytes/sec +} diff --git a/deps/github.com/ledgerwatch/interfaces/downloader/keep.go b/deps/github.com/ledgerwatch/interfaces/downloader/keep.go new file mode 100644 index 0000000..e518c75 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/downloader/keep.go @@ -0,0 +1 @@ +package downloader diff --git a/deps/github.com/ledgerwatch/interfaces/execution/execution.proto b/deps/github.com/ledgerwatch/interfaces/execution/execution.proto new file mode 100644 index 0000000..4184485 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/execution/execution.proto @@ -0,0 +1,105 @@ +syntax = "proto3"; + +package execution; + +import "types/types.proto"; + +option go_package = "./execution;execution"; + +enum ValidationStatus { + Success = 0; // State transition simulation is successful. + InvalidChain = 1; // State transition simulation is Unsuccessful. + TooFarAway = 2; // Chain hash is too far away from current chain head and unfeasible to validate. + MissingSegment = 3; // Chain segments are missing. +} + +message ForkChoiceReceipt { + bool success = 1; // Forkchoice is either successful or unsuccessful. + types.H256 latestValidHash = 2; // Return latest valid hash in case of halt of execution. +} + +// Result we receive after validation +message ValidationReceipt { + ValidationStatus validationStatus = 1; + types.H256 latestValidHash = 2; + optional types.H256 missingHash = 3; // The missing hash, in case we receive MissingSegment so that we can reverse download it. +}; + +message IsCanonicalResponse { + bool canonical = 1; // Whether hash is canonical or not. +} + +// Header is an header for execution +message Header { + types.H256 parentHash = 1; + types.H160 coinbase = 2; + types.H256 stateRoot = 3; + types.H256 receiptRoot = 4; + types.H2048 logsBloom = 5; + types.H256 mixDigest = 6; + uint64 blockNumber = 7; + uint64 gasLimit = 8; + uint64 gasUsed = 9; + uint64 timestamp = 10; + uint64 nonce = 11; + bytes extraData = 12; + types.H256 difficulty = 13; + types.H256 blockHash = 14; // We keep this so that we can validate it + types.H256 ommerHash = 15; + types.H256 transactionHash = 16; + optional types.H256 baseFeePerGas = 17; + optional types.H256 withdrawalHash = 18; +} + +// Body is a block body for execution +message BlockBody { + types.H256 blockHash = 1; + uint64 blockNumber = 2; + // Raw transactions in byte format. + repeated bytes transactions = 3; + repeated Header uncles = 4; + repeated types.Withdrawal withdrawals = 5; +} + +message GetHeaderResponse { + optional Header header = 1; +} + +message GetBodyResponse { + optional BlockBody body = 1; +} + +message GetHeaderHashNumberResponse { + optional uint64 blockNumber = 1; // null if not found. +} + +message GetSegmentRequest { + // Get headers/body by number or hash, invalid if none set. + optional uint64 blockNumber = 1; + optional types.H256 blockHash = 2; +} + +message InsertHeadersRequest { + repeated Header headers = 1; +} + +message InsertBodiesRequest { + repeated BlockBody bodies = 1; +} + +message EmptyMessage {} + +service Execution { + // Chain Putters. + rpc InsertHeaders(InsertHeadersRequest) returns(EmptyMessage); + rpc InsertBodies(InsertBodiesRequest) returns(EmptyMessage); + // Chain Validation and ForkChoice. + rpc ValidateChain(types.H256) returns(ValidationReceipt); + rpc UpdateForkChoice(types.H256) returns(ForkChoiceReceipt); + rpc AssembleBlock(EmptyMessage) returns(types.ExecutionPayload); // Builds on top of current head. + // Chain Getters. + rpc GetHeader(GetSegmentRequest) returns(GetHeaderResponse); + rpc GetBody(GetSegmentRequest) returns(GetBodyResponse); + rpc IsCanonicalHash(types.H256) returns(IsCanonicalResponse); + rpc GetHeaderHashNumber(types.H256) returns(GetHeaderHashNumberResponse); +} \ No newline at end of file diff --git a/deps/github.com/ledgerwatch/interfaces/execution/keep.go b/deps/github.com/ledgerwatch/interfaces/execution/keep.go new file mode 100644 index 0000000..956d739 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/execution/keep.go @@ -0,0 +1 @@ +package execution diff --git a/deps/github.com/ledgerwatch/interfaces/go.mod b/deps/github.com/ledgerwatch/interfaces/go.mod new file mode 100644 index 0000000..542a921 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/go.mod @@ -0,0 +1,3 @@ +module github.com/ledgerwatch/interfaces + +go 1.18 diff --git a/deps/github.com/ledgerwatch/interfaces/keep.go b/deps/github.com/ledgerwatch/interfaces/keep.go new file mode 100644 index 0000000..08badf2 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/keep.go @@ -0,0 +1 @@ +package interfaces diff --git a/deps/github.com/ledgerwatch/interfaces/p2psentinel/keep.go b/deps/github.com/ledgerwatch/interfaces/p2psentinel/keep.go new file mode 100644 index 0000000..40aaa2f --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/p2psentinel/keep.go @@ -0,0 +1 @@ +package p2psentinel diff --git a/deps/github.com/ledgerwatch/interfaces/p2psentinel/sentinel.proto b/deps/github.com/ledgerwatch/interfaces/p2psentinel/sentinel.proto new file mode 100644 index 0000000..3775e8e --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/p2psentinel/sentinel.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package sentinel; + +option go_package = "./sentinel;sentinel"; + +import "types/types.proto"; + +message EmptyMessage {} + +enum GossipType { + // Lightclient gossip + LightClientFinalityUpdateGossipType = 0; + LightClientOptimisticUpdateGossipType = 1; + // Legacy gossip + BeaconBlockGossipType = 2; + + // Global gossip topics. + AggregateAndProofGossipType = 3; + VoluntaryExitGossipType = 4; + ProposerSlashingGossipType = 5; + AttesterSlashingGossipType = 6; +} + +message GossipData { + bytes data = 1; // SSZ encoded data + GossipType type = 2; +} + +message Status { + uint32 fork_digest = 1; // 4 bytes can be repressented in uint32. + types.H256 finalized_root = 2; + uint64 finalized_epoch = 3; + types.H256 head_root = 4; + uint64 head_slot = 5; +} + +message PeerCount { + uint64 amount = 1; +} + +message RequestData { + bytes data = 1; // SSZ encoded data + string topic = 2; +} + +message ResponseData { + bytes data = 1; // prefix-stripped SSZ encoded data + bool error = 2; // did the peer encounter an error +} + +service Sentinel { + rpc SubscribeGossip(EmptyMessage) returns (stream GossipData); + rpc SendRequest(RequestData) returns (ResponseData); + rpc SetStatus(Status) returns(EmptyMessage); // Set status for peer filtering. + rpc GetPeers(EmptyMessage) returns (PeerCount); +} diff --git a/deps/github.com/ledgerwatch/interfaces/p2psentry/keep.go b/deps/github.com/ledgerwatch/interfaces/p2psentry/keep.go new file mode 100644 index 0000000..52deada --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/p2psentry/keep.go @@ -0,0 +1 @@ +package p2psentry diff --git a/deps/github.com/ledgerwatch/interfaces/p2psentry/sentry.proto b/deps/github.com/ledgerwatch/interfaces/p2psentry/sentry.proto new file mode 100644 index 0000000..5a5527d --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/p2psentry/sentry.proto @@ -0,0 +1,200 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "types/types.proto"; + +package sentry; + +option go_package = "./sentry;sentry"; + +enum MessageId { + // ======= eth 65 protocol =========== + + STATUS_65 = 0; + GET_BLOCK_HEADERS_65 = 1; + BLOCK_HEADERS_65 = 2; + BLOCK_HASHES_65 = 3; + GET_BLOCK_BODIES_65 = 4; + BLOCK_BODIES_65 = 5; + GET_NODE_DATA_65 = 6; + NODE_DATA_65 = 7; + GET_RECEIPTS_65 = 8; + RECEIPTS_65 = 9; + NEW_BLOCK_HASHES_65 = 10; + NEW_BLOCK_65 = 11; + TRANSACTIONS_65 = 12; + NEW_POOLED_TRANSACTION_HASHES_65 = 13; + GET_POOLED_TRANSACTIONS_65 = 14; + POOLED_TRANSACTIONS_65 = 15; + + + // ======= eth 66 protocol =========== + + // eth64 announcement messages (no id) + STATUS_66 = 17; + NEW_BLOCK_HASHES_66 = 18; + NEW_BLOCK_66 = 19; + TRANSACTIONS_66 = 20; + + // eth65 announcement messages (no id) + NEW_POOLED_TRANSACTION_HASHES_66 = 21; + + // eth66 messages with request-id + GET_BLOCK_HEADERS_66 = 22; + GET_BLOCK_BODIES_66 = 23; + GET_NODE_DATA_66 = 24; + GET_RECEIPTS_66 = 25; + GET_POOLED_TRANSACTIONS_66 = 26; + BLOCK_HEADERS_66 = 27; + BLOCK_BODIES_66 = 28; + NODE_DATA_66 = 29; + RECEIPTS_66 = 30; + POOLED_TRANSACTIONS_66 = 31; + + // ======= eth 67 protocol =========== + // Version 67 removed the GetNodeData and NodeData messages. + + // ======= eth 68 protocol =========== + NEW_POOLED_TRANSACTION_HASHES_68 = 32; +} + +message OutboundMessageData { + MessageId id = 1; + bytes data = 2; +} + +message SendMessageByMinBlockRequest { + OutboundMessageData data = 1; + uint64 min_block = 2; + uint64 max_peers = 3; +} + +message SendMessageByIdRequest { + OutboundMessageData data = 1; + types.H512 peer_id = 2; +} + +message SendMessageToRandomPeersRequest { + OutboundMessageData data = 1; + uint64 max_peers = 2; +} + +message SentPeers {repeated types.H512 peers = 1;} + +enum PenaltyKind {Kick = 0;} + +message PenalizePeerRequest { + types.H512 peer_id = 1; + PenaltyKind penalty = 2; +} + +message PeerMinBlockRequest { + types.H512 peer_id = 1; + uint64 min_block = 2; +} + +message PeerUselessRequest { + types.H512 peer_id = 1; +} + +message InboundMessage { + MessageId id = 1; + bytes data = 2; + types.H512 peer_id = 3; +} + +message Forks { + types.H256 genesis = 1; + repeated uint64 height_forks = 2; + repeated uint64 time_forks = 3; +} + +message StatusData { + uint64 network_id = 1; + types.H256 total_difficulty = 2; + types.H256 best_hash = 3; + Forks fork_data = 4; + uint64 max_block_height = 5; + uint64 max_block_time = 6; +} + +enum Protocol { + ETH65 = 0; + ETH66 = 1; + ETH67 = 2; + ETH68 = 3; +} + +message SetStatusReply {} + +message HandShakeReply { + Protocol protocol = 1; +} + +message MessagesRequest { + repeated MessageId ids = 1; +} + +message PeersReply { + repeated types.PeerInfo peers = 1; +} + +message PeerCountRequest {} + +message PeerCountPerProtocol { + Protocol protocol = 1; + uint64 count = 2; +} + +message PeerCountReply { + uint64 count = 1; + repeated PeerCountPerProtocol countsPerProtocol = 2; +} + +message PeerByIdRequest {types.H512 peer_id = 1;} + +message PeerByIdReply {optional types.PeerInfo peer = 1;} + +message PeerEventsRequest {} + +message PeerEvent { + enum PeerEventId { + // Happens after after a successful sub-protocol handshake. + Connect = 0; + Disconnect = 1; + } + types.H512 peer_id = 1; + PeerEventId event_id = 2; +} + +service Sentry { + // SetStatus - force new ETH client state of sentry - network_id, max_block, etc... + rpc SetStatus(StatusData) returns (SetStatusReply); + + rpc PenalizePeer(PenalizePeerRequest) returns (google.protobuf.Empty); + rpc PeerMinBlock(PeerMinBlockRequest) returns (google.protobuf.Empty); + rpc PeerUseless(PeerUselessRequest) returns (google.protobuf.Empty); + + // HandShake - pre-requirement for all Send* methods - returns list of ETH protocol versions, + // without knowledge of protocol - impossible encode correct P2P message + rpc HandShake(google.protobuf.Empty) returns (HandShakeReply); + rpc SendMessageByMinBlock(SendMessageByMinBlockRequest) returns (SentPeers); + rpc SendMessageById(SendMessageByIdRequest) returns (SentPeers); + rpc SendMessageToRandomPeers(SendMessageToRandomPeersRequest) + returns (SentPeers); + rpc SendMessageToAll(OutboundMessageData) returns (SentPeers); + + // Subscribe to receive messages. + // Calling multiple times with a different set of ids starts separate streams. + // It is possible to subscribe to the same set if ids more than once. + rpc Messages(MessagesRequest) returns (stream InboundMessage); + + rpc Peers(google.protobuf.Empty) returns (PeersReply); + rpc PeerCount(PeerCountRequest) returns (PeerCountReply); + rpc PeerById(PeerByIdRequest) returns (PeerByIdReply); + // Subscribe to notifications about connected or lost peers. + rpc PeerEvents(PeerEventsRequest) returns (stream PeerEvent); + + // NodeInfo returns a collection of metadata known about the host. + rpc NodeInfo(google.protobuf.Empty) returns(types.NodeInfoReply); +} diff --git a/deps/github.com/ledgerwatch/interfaces/remote/ethbackend.proto b/deps/github.com/ledgerwatch/interfaces/remote/ethbackend.proto new file mode 100644 index 0000000..1f40d86 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/remote/ethbackend.proto @@ -0,0 +1,227 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "types/types.proto"; + +package remote; + +option go_package = "./remote;remote"; + +service ETHBACKEND { + rpc Etherbase(EtherbaseRequest) returns (EtherbaseReply); + + rpc NetVersion(NetVersionRequest) returns (NetVersionReply); + + rpc NetPeerCount(NetPeerCountRequest) returns (NetPeerCountReply); + + // ------------------------------------------------------------------------ + // Engine API RPC requests natively implemented in the Erigon node backend + // See https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md + + // Validate and possibly execute the payload. + rpc EngineNewPayload(types.ExecutionPayload) returns (EnginePayloadStatus); + + // Update fork choice + rpc EngineForkChoiceUpdated(EngineForkChoiceUpdatedRequest) returns (EngineForkChoiceUpdatedResponse); + + // Fetch Execution Payload using its ID. + rpc EngineGetPayload(EngineGetPayloadRequest) returns (EngineGetPayloadResponse); + + rpc EngineGetPayloadBodiesByHashV1(EngineGetPayloadBodiesByHashV1Request) returns (EngineGetPayloadBodiesV1Response); + + rpc EngineGetPayloadBodiesByRangeV1(EngineGetPayloadBodiesByRangeV1Request) returns (EngineGetPayloadBodiesV1Response); + + // Fetch the blobs bundle using its ID. + rpc EngineGetBlobsBundleV1(EngineGetBlobsBundleRequest) returns (types.BlobsBundleV1); + + // End of Engine API requests + // ------------------------------------------------------------------------ + + // Version returns the service version number + rpc Version(google.protobuf.Empty) returns (types.VersionReply); + + // ProtocolVersion returns the Ethereum protocol version number (e.g. 66 for ETH66). + rpc ProtocolVersion(ProtocolVersionRequest) returns (ProtocolVersionReply); + + // ClientVersion returns the Ethereum client version string using node name convention (e.g. TurboGeth/v2021.03.2-alpha/Linux). + rpc ClientVersion(ClientVersionRequest) returns (ClientVersionReply); + + rpc Subscribe(SubscribeRequest) returns (stream SubscribeReply); + + // Only one subscription is needed to serve all the users, LogsFilterRequest allows to dynamically modifying the subscription + rpc SubscribeLogs(stream LogsFilterRequest) returns (stream SubscribeLogsReply); + + // High-level method - can read block from db, snapshots or apply any other logic + // it doesn't provide consistency + // Request fields are optional - it's ok to request block only by hash or only by number + rpc Block(BlockRequest) returns (BlockReply); + + // High-level method - can find block number by txn hash + // it doesn't provide consistency + rpc TxnLookup(TxnLookupRequest) returns (TxnLookupReply); + + // NodeInfo collects and returns NodeInfo from all running sentry instances. + rpc NodeInfo(NodesInfoRequest) returns (NodesInfoReply); + + // Peers collects and returns peers information from all running sentry instances. + rpc Peers(google.protobuf.Empty) returns (PeersReply); + + rpc PendingBlock(google.protobuf.Empty) returns (PendingBlockReply); +} + +enum Event { + HEADER = 0; + PENDING_LOGS = 1; + PENDING_BLOCK = 2; + // NEW_SNAPSHOT - one or many new snapshots (of snapshot sync) were created, + // client need to close old file descriptors and open new (on new segments), + // then server can remove old files + NEW_SNAPSHOT = 3; +} + +message EtherbaseRequest {} + +message EtherbaseReply { types.H160 address = 1; } + +message NetVersionRequest {} + +message NetVersionReply { uint64 id = 1; } + +message NetPeerCountRequest {} + +message NetPeerCountReply { uint64 count = 1; } + + +message EngineGetPayloadRequest { + uint64 payloadId = 1; +} + +message EngineGetBlobsBundleRequest { + uint64 payloadId = 1; +} + +enum EngineStatus { + VALID = 0; + INVALID = 1; + SYNCING = 2; + ACCEPTED = 3; + INVALID_BLOCK_HASH = 4; +} + +message EnginePayloadStatus { + EngineStatus status = 1; + types.H256 latestValidHash = 2; + string validationError = 3; +} + +message EnginePayloadAttributes { + uint32 version = 1; // v1 - no withdrawals, v2 - with withdrawals + uint64 timestamp = 2; + types.H256 prevRandao = 3; + types.H160 suggestedFeeRecipient = 4; + repeated types.Withdrawal withdrawals = 5; +} + +message EngineForkChoiceState { + types.H256 headBlockHash = 1; + types.H256 safeBlockHash = 2; + types.H256 finalizedBlockHash = 3; +} + +message EngineForkChoiceUpdatedRequest { + EngineForkChoiceState forkchoiceState = 1; + EnginePayloadAttributes payloadAttributes = 2; +} + +message EngineForkChoiceUpdatedResponse { + EnginePayloadStatus payloadStatus = 1; + uint64 payloadId = 2; +} + +message EngineGetPayloadResponse { + types.ExecutionPayload executionPayload = 1; + types.H256 blockValue = 2; +} + +message ProtocolVersionRequest {} + +message ProtocolVersionReply { uint64 id = 1; } + +message ClientVersionRequest {} + +message ClientVersionReply { string nodeName = 1; } + +message SubscribeRequest { + Event type = 1; +} + +message SubscribeReply { + Event type = 1; + bytes data = 2; // serialized data +} + +message LogsFilterRequest { + bool allAddresses = 1; + repeated types.H160 addresses = 2; + bool allTopics = 3; + repeated types.H256 topics = 4; +} + +message SubscribeLogsReply { + types.H160 address = 1; + types.H256 blockHash = 2; + uint64 blockNumber = 3; + bytes data = 4; + uint64 logIndex = 5; + repeated types.H256 topics = 6; + types.H256 transactionHash = 7; + uint64 transactionIndex = 8; + bool removed = 9; +} + +message BlockRequest { + uint64 blockHeight = 2; + types.H256 blockHash = 3; +} + +message BlockReply { + bytes blockRlp = 1; + bytes senders = 2; +} + +message TxnLookupRequest { + types.H256 txnHash = 1; +} + +message TxnLookupReply { + uint64 blockNumber = 1; +} + +message NodesInfoRequest { + uint32 limit = 1; +} + +message NodesInfoReply { + repeated types.NodeInfoReply nodesInfo = 1; +} + +message PeersReply { + repeated types.PeerInfo peers = 1; +} + +message PendingBlockReply { + bytes blockRlp = 1; +} + +message EngineGetPayloadBodiesByHashV1Request { + repeated types.H256 hashes = 1; +} + +message EngineGetPayloadBodiesByRangeV1Request { + uint64 start = 1; + uint64 count = 2; +} + +message EngineGetPayloadBodiesV1Response { + repeated types.ExecutionPayloadBodyV1 bodies = 1; +} diff --git a/deps/github.com/ledgerwatch/interfaces/remote/keep.go b/deps/github.com/ledgerwatch/interfaces/remote/keep.go new file mode 100644 index 0000000..fbe5b64 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/remote/keep.go @@ -0,0 +1 @@ +package remote diff --git a/deps/github.com/ledgerwatch/interfaces/remote/kv.proto b/deps/github.com/ledgerwatch/interfaces/remote/kv.proto new file mode 100644 index 0000000..ca7ff18 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/remote/kv.proto @@ -0,0 +1,251 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "types/types.proto"; + +package remote; + +option go_package = "./remote;remote"; + + +//Variables Naming: +// ts - TimeStamp +// tx - Database Transaction +// txn - Ethereum Transaction (and TxNum - is also number of Etherum Transaction) +// RoTx - Read-Only Database Transaction +// RwTx - Read-Write Database Transaction +// k - key +// v - value + +//Methods Naming: +// Get: exact match of criterias +// Range: [from, to) +// Each: [from, INF) +// Prefix: Has(k, prefix) +// Amount: [from, INF) AND maximum N records + +//Entity Naming: +// State: simple table in db +// InvertedIndex: supports range-scans +// History: can return value of key K as of given TimeStamp. Doesn't know about latest/current value of key K. Returns NIL if K not changed after TimeStamp. +// Domain: as History but also aware about latest/current value of key K. + +// Provides methods to access key-value data +service KV { + // Version returns the service version number + rpc Version(google.protobuf.Empty) returns (types.VersionReply); + + // Tx exposes read-only transactions for the key-value store + // + // When tx open, client must receive 1 message from server with txID + // When cursor open, client must receive 1 message from server with cursorID + // Then only client can initiate messages from server + rpc Tx(stream Cursor) returns (stream Pair); + + rpc StateChanges(StateChangeRequest) returns (stream StateChangeBatch); + + // Snapshots returns list of current snapshot files. Then client can just open all of them. + rpc Snapshots(SnapshotsRequest) returns (SnapshotsReply); + + + //Temporal methods + rpc DomainGet(DomainGetReq) returns (DomainGetReply); + rpc HistoryGet(HistoryGetReq) returns (HistoryGetReply); + + rpc IndexRange(IndexRangeReq) returns (IndexRangeReply); + // rpc IndexStream(IndexRangeReq) returns (stream IndexRangeReply); + + + // Range [from, to) + // Range(from, nil) means [from, EndOfTable) + // Range(nil, to) means [StartOfTable, to) + // If orderAscend=false server expecting `from`<`to`. Example: Range("B", "A") + rpc Range(RangeReq) returns (Pairs); + // rpc Stream(RangeReq) returns (stream Pairs); +} + +enum Op { + FIRST = 0; + FIRST_DUP = 1; + SEEK = 2; + SEEK_BOTH = 3; + CURRENT = 4; + LAST = 6; + LAST_DUP = 7; + NEXT = 8; + NEXT_DUP = 9; + NEXT_NO_DUP = 11; + PREV = 12; + PREV_DUP = 13; + PREV_NO_DUP = 14; + SEEK_EXACT = 15; + SEEK_BOTH_EXACT = 16; + + OPEN = 30; + CLOSE = 31; + OPEN_DUP_SORT = 32; + + COUNT = 33; +} + +message Cursor { + Op op = 1; + string bucketName = 2; + uint32 cursor = 3; + bytes k = 4; + bytes v = 5; +} + +message Pair { + bytes k = 1; + bytes v = 2; + uint32 cursorID = 3; // send once after new cursor open + uint64 viewID = 4; // return once after tx open. mdbx's tx.ViewID() - id of write transaction in db + uint64 txID = 5; // return once after tx open. internal identifier - use it in other methods - to achieve consistant DB view (to read data from same DB tx on server). +} + +enum Action { + STORAGE = 0; // Change only in the storage + UPSERT = 1; // Change of balance or nonce (and optionally storage) + CODE = 2; // Change of code (and optionally storage) + UPSERT_CODE = 3; // Change in (balance or nonce) and code (and optinally storage) + REMOVE = 4; // Account is deleted +} + +message StorageChange { + types.H256 location = 1; + bytes data = 2; +} + +message AccountChange { + types.H160 address = 1; + uint64 incarnation = 2; + Action action = 3; + bytes data = 4; // nil if there is no UPSERT in action + bytes code = 5; // nil if there is no CODE in action + repeated StorageChange storageChanges = 6; +} + +enum Direction { + FORWARD = 0; + UNWIND = 1; +} + +// StateChangeBatch - list of StateDiff done in one DB transaction +message StateChangeBatch { + uint64 stateVersionID = 1; // mdbx's tx.ID() - id of write transaction in db - where this changes happened + repeated StateChange changeBatch = 2; + uint64 pendingBlockBaseFee = 3; // BaseFee of the next block to be produced + uint64 blockGasLimit = 4; // GasLimit of the latest block - proxy for the gas limit of the next block to be produced +} + +// StateChange - changes done by 1 block or by 1 unwind +message StateChange { + Direction direction = 1; + uint64 blockHeight = 2; + types.H256 blockHash = 3; + repeated AccountChange changes = 4; + repeated bytes txs = 5; // enable by withTransactions=true +} + +message StateChangeRequest { + bool withStorage = 1; + bool withTransactions = 2; +} + +message SnapshotsRequest { +} + +message SnapshotsReply { + repeated string blocks_files = 1; + repeated string history_files = 2; +} + +message RangeReq { + uint64 tx_id = 1; // returned by .Tx() + + // It's ok to query wide/unlilmited range of data, server will use `pagination params` + // reply by limited batches/pages and client can decide: request next page or not + + // query params + string table = 2; + bytes from_prefix = 3; + bytes to_prefix = 4; + bool order_ascend = 5; + sint64 limit = 6; // <= 0 means no limit + + // pagination params + int32 page_size = 7; // <= 0 means server will choose + string page_token = 8; +} + + +//Temporal methods +message DomainGetReq { + uint64 tx_id = 1; // returned by .Tx() + + // query params + string table = 2; + bytes k = 3; + uint64 ts = 4; + bytes k2 = 5; +} + +message DomainGetReply{ + bytes v = 1; + bool ok = 2; +} + +message HistoryGetReq { + uint64 tx_id = 1; // returned by .Tx() + string table = 2; + bytes k = 3; + uint64 ts = 4; +} + +message HistoryGetReply{ + bytes v = 1; + bool ok = 2; +} +message IndexRangeReq { + uint64 tx_id = 1; // returned by .Tx() + + // query params + string table = 2; + bytes k = 3; + sint64 from_ts = 4; // -1 means Inf + sint64 to_ts = 5; // -1 means Inf + bool order_ascend = 6; + sint64 limit = 7; // <= 0 means no limit + + // pagination params + int32 page_size = 8; // <= 0 means server will choose + string page_token = 9; +} + +message IndexRangeReply { + repeated uint64 timestamps = 1; //TODO: it can be a bitmap + + string next_page_token = 2; +} + +message Pairs { + repeated bytes keys = 1; // TODO: replace by lengtsh+arena? Anyway on server we need copy (serialization happening outside tx) + repeated bytes values = 2; + + string next_page_token = 3; + // uint32 estimateTotal = 3; // send once after stream creation + + // repeated sint64 lengths = 1; //A length of -1 means that the field is NULL + // bytes keys = 2; + // bytes values = 3; +} + +message ParisPagination { + bytes next_key = 1; + sint64 limit = 2; +} +message IndexPagination { + sint64 next_time_stamp = 1; + sint64 limit = 2; +} \ No newline at end of file diff --git a/deps/github.com/ledgerwatch/interfaces/src/lib.rs b/deps/github.com/ledgerwatch/interfaces/src/lib.rs new file mode 100644 index 0000000..d5fbcd4 --- /dev/null +++ b/deps/github.com/ledgerwatch/interfaces/src/lib.rs @@ -0,0 +1,153 @@ +pub mod types { + use arrayref::array_ref; + + tonic::include_proto!("types"); + + macro_rules! U { + ($proto:ty, $h:ty, $u:ty) => { + impl From<$u> for $proto { + fn from(value: $u) -> Self { + Self::from(<$h>::from(<[u8; <$h>::len_bytes()]>::from(value))) + } + } + + impl From<$proto> for $u { + fn from(value: $proto) -> Self { + Self::from(<$h>::from(value).0) + } + } + }; + } + + // to PB + impl From for H128 { + fn from(value: ethereum_types::H128) -> Self { + Self { + hi: u64::from_be_bytes(*array_ref!(value, 0, 8)), + lo: u64::from_be_bytes(*array_ref!(value, 8, 8)), + } + } + } + + impl From for H160 { + fn from(value: ethereum_types::H160) -> Self { + Self { + hi: Some(ethereum_types::H128::from_slice(&value[..16]).into()), + lo: u32::from_be_bytes(*array_ref!(value, 16, 4)), + } + } + } + + impl From for H256 { + fn from(value: ethereum_types::H256) -> Self { + Self { + hi: Some(ethereum_types::H128::from_slice(&value[..16]).into()), + lo: Some(ethereum_types::H128::from_slice(&value[16..]).into()), + } + } + } + + impl From for H512 { + fn from(value: ethereum_types::H512) -> Self { + Self { + hi: Some(ethereum_types::H256::from_slice(&value[..32]).into()), + lo: Some(ethereum_types::H256::from_slice(&value[32..]).into()), + } + } + } + + // from PB + impl From for ethereum_types::H128 { + fn from(value: H128) -> Self { + let mut v = [0; Self::len_bytes()]; + v[..8].copy_from_slice(&value.hi.to_be_bytes()); + v[8..].copy_from_slice(&value.lo.to_be_bytes()); + + v.into() + } + } + + impl From for ethereum_types::H160 { + fn from(value: H160) -> Self { + type H = ethereum_types::H128; + + let mut v = [0; Self::len_bytes()]; + v[..H::len_bytes()] + .copy_from_slice(H::from(value.hi.unwrap_or_default()).as_fixed_bytes()); + v[H::len_bytes()..].copy_from_slice(&value.lo.to_be_bytes()); + + v.into() + } + } + + impl From for ethereum_types::H256 { + fn from(value: H256) -> Self { + type H = ethereum_types::H128; + + let mut v = [0; Self::len_bytes()]; + v[..H::len_bytes()] + .copy_from_slice(H::from(value.hi.unwrap_or_default()).as_fixed_bytes()); + v[H::len_bytes()..] + .copy_from_slice(H::from(value.lo.unwrap_or_default()).as_fixed_bytes()); + + v.into() + } + } + + impl From for ethereum_types::H512 { + fn from(value: H512) -> Self { + type H = ethereum_types::H256; + + let mut v = [0; Self::len_bytes()]; + v[..H::len_bytes()] + .copy_from_slice(H::from(value.hi.unwrap_or_default()).as_fixed_bytes()); + v[H::len_bytes()..] + .copy_from_slice(H::from(value.lo.unwrap_or_default()).as_fixed_bytes()); + + v.into() + } + } + + U!(H128, ethereum_types::H128, ethereum_types::U128); + U!(H256, ethereum_types::H256, ethereum_types::U256); + U!(H512, ethereum_types::H512, ethereum_types::U512); + + impl From for H256 { + fn from(v: ethnum::U256) -> Self { + ethereum_types::H256(v.to_be_bytes()).into() + } + } + + impl From for ethnum::U256 { + fn from(v: H256) -> Self { + ethnum::U256::from_be_bytes(ethereum_types::H256::from(v).0) + } + } +} + +#[cfg(feature = "sentry")] +pub mod sentry { + tonic::include_proto!("sentry"); +} + +#[cfg(feature = "remotekv")] +pub mod remotekv { + tonic::include_proto!("remote"); +} + +#[cfg(feature = "snapshotsync")] +pub mod snapshotsync { + tonic::include_proto!("downloader"); +} + +#[cfg(feature = "txpool")] +pub mod txpool { + tonic::include_proto!("txpool"); +} + +#[cfg(feature = "web3")] +pub mod web3 { + tonic::include_proto!("web3"); +} + +pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("descriptor"); diff --git a/deps/github.com/ledgerwatch/interfaces/turbo-geth-architecture.png b/deps/github.com/ledgerwatch/interfaces/turbo-geth-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..701d907e8dcdb0f87fb8ded64f5801d238574b10 GIT binary patch literal 103260 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