mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 16:40:32 +00:00
test: remove bt
This commit is contained in:
1 parent
bce32eb976
commit
4763ff129f
34 files changed
-2676
No files matched your search
@@ -1,57 +0,0 @@
|
||||
package bt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/metainfo"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/torrent"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
PeerID [20]byte
|
||||
|
||||
// torrents []torrent.Torrent
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
client := &Client{}
|
||||
|
||||
// 生成Peer ID,规则为前三位固定字母(-GP)+SemVer(xyz)+End(-),后面随机生成
|
||||
// 参考:https://wiki.theory.org/index.php/BitTorrentSpecification#peer_id
|
||||
peerID := [20]byte{'-', 'G', 'P', '0', '0', '1', '-'}
|
||||
_, err := rand.Read(peerID[7:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
client.PeerID = peerID
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (client *Client) AddTorrent(rawurl string) error {
|
||||
parse, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var metaInfo *metainfo.MetaInfo
|
||||
switch parse.Scheme {
|
||||
case "magnet":
|
||||
// TODO 磁力链接解析
|
||||
default:
|
||||
metaInfo, err = metainfo.ParseFromFile(rawurl)
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
torrent := torrent.Torrent{
|
||||
// client: client,
|
||||
MetaInfo: metaInfo,
|
||||
}
|
||||
// client.torrents = append(client.torrents, torrent)
|
||||
|
||||
torrent.Download("")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package bt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClient_AddTorrent(t *testing.T) {
|
||||
/*client := NewClient()
|
||||
metaInfo, err := metainfo.ParseFromFile("testdata/Game.of.Thrones.S08E05.720p.WEB.H264-MEMENTO.torrent")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
torrent := &
|
||||
torrent.MetaInfo.Announce = "udp://tracker.opentrackr.org:1337/announce"
|
||||
torrent.MetaInfo.AnnounceList = [][]string{}
|
||||
err = torrent.Tracker()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return
|
||||
}*/
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package dht
|
||||
|
||||
import (
|
||||
"github.com/marksamman/bencode"
|
||||
"net"
|
||||
)
|
||||
|
||||
func ping() string {
|
||||
data := map[string]interface{}{
|
||||
"t": "aa",
|
||||
"y": "q",
|
||||
"q": "ping",
|
||||
"a": map[string]interface{}{"id": "abcdefghij0123456789"},
|
||||
}
|
||||
req := bencode.Encode(data)
|
||||
// session.add_dht_router("router.utorrent.com", 6881)
|
||||
// session.add_dht_router("router.bittorrent.com", 6881)
|
||||
// session.add_dht_router("dht.transmissionbt.com", 6881)
|
||||
// session.add_dht_router("router.bitcomet.com", 6881)
|
||||
// session.add_dht_router("dht.aelitis.com", 6881)
|
||||
dial, err := net.Dial("udp", "router.utorrent.com:6881")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dial.Write(req)
|
||||
|
||||
reps := make([]byte, 65535)
|
||||
|
||||
n, err := dial.Read(reps)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(reps[:n])
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package dht
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_ping(t *testing.T) {
|
||||
println(ping())
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/marksamman/bencode"
|
||||
)
|
||||
|
||||
type MetaInfo struct {
|
||||
// tracker URL
|
||||
// http://bittorrent.org/beps/bep_0003.html#metainfo-files
|
||||
Announce string `json:"announce"`
|
||||
// tracker URL
|
||||
// http://bittorrent.org/beps/bep_0012.html
|
||||
AnnounceList [][]string `json:"announce-list"`
|
||||
Comment string `json:"comment"`
|
||||
CreatedBy string `json:"created by"`
|
||||
CreationDate int64 `json:"creation date"`
|
||||
Encoding string `json:"encoding"`
|
||||
// http://bittorrent.org/beps/bep_0019.html
|
||||
UrlList []string `json:"url-list"`
|
||||
Info *Info `json:"info"`
|
||||
|
||||
infoHash [20]byte
|
||||
totalSize uint64
|
||||
fileDetails []*FileDetail
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
PieceLength int `json:"piece length"`
|
||||
Pieces [][20]byte
|
||||
// 单个文件时,length为该文件的长度
|
||||
Length uint64 `json:"length"`
|
||||
// 多个文件
|
||||
Files []*File `json:"files"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Length uint64 `json:"length"`
|
||||
Path []string `json:"path"`
|
||||
}
|
||||
|
||||
type FileDetail struct {
|
||||
Length uint64
|
||||
Path []string
|
||||
Begin int64
|
||||
End int64
|
||||
}
|
||||
|
||||
type PieceMapping struct {
|
||||
file int
|
||||
begin int64
|
||||
end int64
|
||||
}
|
||||
|
||||
func ParseFromFile(path string) (*MetaInfo, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// http://bittorrent.org/beps/bep_0003.html#bencoding
|
||||
btDecode, err := bencode.Decode(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes, err := json.Marshal(btDecode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var metaInfo MetaInfo
|
||||
json.Unmarshal(bytes, &metaInfo)
|
||||
if metaInfo.Info != nil {
|
||||
// encode info hash
|
||||
info := btDecode["info"].(map[string]interface{})
|
||||
infoBtEncode := bencode.Encode(info)
|
||||
metaInfo.infoHash = sha1.Sum(infoBtEncode[:])
|
||||
// Split pieces hash
|
||||
pieces := info["pieces"].(string)
|
||||
if pieces != "" {
|
||||
metaInfo.Info.Pieces = make([][20]byte, len(pieces)/20)
|
||||
// pieces parse
|
||||
for i := range metaInfo.Info.Pieces {
|
||||
begin := i * 20
|
||||
metaInfo.Info.Pieces[i] = [20]byte{}
|
||||
copy(metaInfo.Info.Pieces[i][:], pieces[begin:begin+20])
|
||||
}
|
||||
}
|
||||
}
|
||||
calcFileSize(&metaInfo)
|
||||
return &metaInfo, nil
|
||||
}
|
||||
|
||||
// 获取某个分片的大小
|
||||
func (metaInfo *MetaInfo) GetPieceLength(index int) int {
|
||||
// 是否为最后一个分片
|
||||
if index == len(metaInfo.Info.Pieces)-1 {
|
||||
size := metaInfo.totalSize % uint64(metaInfo.Info.PieceLength)
|
||||
if size > 0 {
|
||||
return int(size)
|
||||
}
|
||||
}
|
||||
return metaInfo.Info.PieceLength
|
||||
}
|
||||
|
||||
// 获取所有文件的大小
|
||||
func (metaInfo *MetaInfo) GetTotalSize() uint64 {
|
||||
return metaInfo.totalSize
|
||||
}
|
||||
|
||||
// 获取info hash
|
||||
func (metaInfo *MetaInfo) GetInfoHash() [20]byte {
|
||||
return metaInfo.infoHash
|
||||
}
|
||||
|
||||
// 获取所有文件的开始偏移字节
|
||||
func (metaInfo *MetaInfo) GetFileDetails() []*FileDetail {
|
||||
return metaInfo.fileDetails
|
||||
}
|
||||
|
||||
// 计算所有文件的大小
|
||||
func calcFileSize(metaInfo *MetaInfo) {
|
||||
if metaInfo.Info != nil {
|
||||
fileCount := len(metaInfo.Info.Files)
|
||||
if fileCount > 0 {
|
||||
metaInfo.fileDetails = make([]*FileDetail, fileCount)
|
||||
var length uint64
|
||||
for i, f := range metaInfo.Info.Files {
|
||||
metaInfo.fileDetails[i] = &FileDetail{
|
||||
Length: f.Length,
|
||||
Path: f.Path,
|
||||
Begin: int64(length),
|
||||
End: int64(length + f.Length),
|
||||
}
|
||||
length += f.Length
|
||||
}
|
||||
metaInfo.totalSize = length
|
||||
} else {
|
||||
metaInfo.totalSize = metaInfo.Info.Length
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package metainfo
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseFromFile(t *testing.T) {
|
||||
// metaInfo, err := ParseFromFile("../testdata/Game.of.Thrones.S08E05.720p.WEB.H264-MEMENTO.torrent")
|
||||
metaInfo, err := ParseFromFile("E:\\bt\\win10.torrent")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("%+v\n", metaInfo.AnnounceList)
|
||||
for _, file := range metaInfo.Info.Files {
|
||||
fmt.Printf("%+v\n", file)
|
||||
}
|
||||
fmt.Printf("%+v\n", metaInfo.Info.PieceLength)
|
||||
fmt.Printf("%+v\n", len(metaInfo.Info.Pieces))
|
||||
fmt.Printf("%+v\n", hex.EncodeToString(metaInfo.infoHash[:]))
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
ProtocolIdentifier = "BitTorrent protocol"
|
||||
ProtocolIdentifierLength = 0x13
|
||||
)
|
||||
|
||||
// Handshake of Peer wire protocol
|
||||
// Per https://wiki.theory.org/index.php/BitTorrentSpecification#Handshake
|
||||
type Handshake struct {
|
||||
Pstrlen byte
|
||||
Pstr [19]byte
|
||||
Reserved [8]byte
|
||||
InfoHash [20]byte
|
||||
PeerID [20]byte
|
||||
}
|
||||
|
||||
func NewHandshake(reserved [8]byte, infoHash [20]byte, peerID [20]byte) *Handshake {
|
||||
var arr [ProtocolIdentifierLength]byte
|
||||
copy(arr[:], ProtocolIdentifier)
|
||||
return &Handshake{
|
||||
Pstrlen: ProtocolIdentifierLength,
|
||||
Pstr: arr,
|
||||
Reserved: reserved,
|
||||
InfoHash: infoHash,
|
||||
PeerID: peerID,
|
||||
}
|
||||
}
|
||||
|
||||
func (handshake *Handshake) Encode() []byte {
|
||||
writer := new(bytes.Buffer)
|
||||
err := binary.Write(writer, binary.BigEndian, handshake)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return writer.Bytes()
|
||||
}
|
||||
|
||||
func (handshake *Handshake) Decode(buf []byte) error {
|
||||
reader := bytes.NewReader(buf)
|
||||
err := binary.Read(reader, binary.BigEndian, handshake)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// message ID
|
||||
type ID byte
|
||||
|
||||
const (
|
||||
IdChoke ID = iota
|
||||
IdUnchoke
|
||||
IdInterested
|
||||
IdNotInterested
|
||||
IdHave
|
||||
IdBitfield
|
||||
IdRequest
|
||||
IdPiece
|
||||
IdCancel
|
||||
)
|
||||
|
||||
/*type Message struct {
|
||||
Length uint32
|
||||
ID MessageID
|
||||
Payload []byte
|
||||
}*/
|
||||
|
||||
type base struct {
|
||||
id ID
|
||||
}
|
||||
|
||||
func (base *base) ID() ID {
|
||||
return base.id
|
||||
}
|
||||
|
||||
func (base *base) Encode() []byte {
|
||||
return encode(base, nil)
|
||||
}
|
||||
|
||||
func (base *base) Decode(body []byte) {
|
||||
|
||||
}
|
||||
|
||||
type Message interface {
|
||||
ID() ID
|
||||
Encode() []byte
|
||||
Decode(body []byte)
|
||||
}
|
||||
|
||||
type TestMsg struct {
|
||||
*base
|
||||
}
|
||||
|
||||
func (base *TestMsg) Encode() []byte {
|
||||
return []byte{1, 2, 3}
|
||||
}
|
||||
|
||||
func (base *TestMsg) Decode(body []byte) {
|
||||
}
|
||||
|
||||
func encode(base *base, body []byte) []byte {
|
||||
buf := make([]byte, 5+len(body))
|
||||
binary.BigEndian.PutUint32(buf, uint32(len(buf)))
|
||||
buf[4] = byte(base.id)
|
||||
copy(buf[4:], body)
|
||||
return buf
|
||||
}
|
||||
|
||||
/*func Decode(buf []byte) Message {
|
||||
head := make([]byte, 5)
|
||||
body := message.Encode()
|
||||
binary.BigEndian.PutUint32(head, uint32(len(head)+len(body)))
|
||||
head[4] = byte(message.ID())
|
||||
return append(head, body...)
|
||||
}
|
||||
|
||||
func (msg *Message) ToBytes() []byte {
|
||||
buf := make([]byte, 5)
|
||||
binary.BigEndian.PutUint32(buf, msg.Length)
|
||||
buf[4] = byte(msg.ID)
|
||||
if msg.Payload != nil {
|
||||
return append(buf, msg.Payload.Encode()...)
|
||||
} else {
|
||||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *Message) FormBytes(buf []byte) {
|
||||
msg.Length = binary.BigEndian.Uint32(buf)
|
||||
msg.ID = ID(buf[4])
|
||||
if msg.Payload != nil {
|
||||
msg.Payload.Decode(buf[4:])
|
||||
}
|
||||
}*/
|
||||
|
||||
/*func NewMessage(id MessageID, Payload []byte) *Message {
|
||||
message := &Message{ID: id, Payload: Payload}
|
||||
if id == Keepalive {
|
||||
message.Length = 0
|
||||
} else {
|
||||
message.Length = uint32(len(Payload) + 1)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func (msg *Message) Encode() []byte {
|
||||
if msg.ID == Keepalive {
|
||||
return make([]byte, 4)
|
||||
} else {
|
||||
buf := make([]byte, 5+len(msg.Payload))
|
||||
binary.BigEndian.PutUint32(buf, msg.Length)
|
||||
buf[4] = byte(msg.ID)
|
||||
copy(buf[5:], msg.Payload[:])
|
||||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *Message) Decode(buf []byte) {
|
||||
msg.Length = binary.BigEndian.Uint32(buf)
|
||||
if msg.Length == 0 {
|
||||
msg.ID = -1
|
||||
} else {
|
||||
msg.ID = MessageID(buf[4])
|
||||
if msg.Length > 4 {
|
||||
msg.Payload = buf[5:]
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// Message protocol
|
||||
// Length prefix| message ID | Payload
|
||||
// 4-byte | 1-byte | remaining bytes
|
||||
// 100 | 5 | 99-bytes
|
||||
// https://wiki.theory.org/index.php/BitTorrentSpecification#Messages
|
||||
func SplitMessage(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if !atEOF && len(data) > 4 {
|
||||
length := int(binary.BigEndian.Uint32(data))
|
||||
if len(data)-4 >= length {
|
||||
return length + 4, data[:length+4], nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"github.com/RoaringBitmap/roaring"
|
||||
"math"
|
||||
)
|
||||
|
||||
// bitfield: <len=0001+X><id=5><bitfield>
|
||||
type Bitfield struct {
|
||||
*base
|
||||
pieceCount int
|
||||
bitmap *roaring.Bitmap
|
||||
}
|
||||
|
||||
func NewBitfield() *Bitfield {
|
||||
return &Bitfield{
|
||||
base: &base{
|
||||
id: IdBitfield,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildBitfield(pieceCount int, bitmap *roaring.Bitmap) *Bitfield {
|
||||
bitfield := NewBitfield()
|
||||
bitfield.pieceCount = pieceCount
|
||||
bitfield.bitmap = bitmap
|
||||
return bitfield
|
||||
}
|
||||
|
||||
func (b *Bitfield) Encode() []byte {
|
||||
buf := make([]byte, int(math.Ceil(float64(b.pieceCount)/8)))
|
||||
for i := 0; i < len(buf); i++ {
|
||||
temp := byte(0)
|
||||
for j := 0; j < 8; j++ {
|
||||
// 如果piece存在,位数标识为1
|
||||
if b.bitmap.ContainsInt(i*8 + j) {
|
||||
temp = temp | (1 << (7 - j))
|
||||
}
|
||||
}
|
||||
buf[i] = temp
|
||||
}
|
||||
return encode(b.base, buf)
|
||||
}
|
||||
|
||||
func (b *Bitfield) Decode(body []byte) {
|
||||
b.bitmap = roaring.New()
|
||||
// 初始化bitmap
|
||||
for i := 0; i < len(body); i++ {
|
||||
for j := 0; j < 8; j++ {
|
||||
if body[i]&(1<<(7-j)) > 0 {
|
||||
b.bitmap.AddInt(i*8 + j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 某个分片是否下载完成
|
||||
func (b *Bitfield) IsComplete(i int) bool {
|
||||
return b.bitmap.ContainsInt(i)
|
||||
}
|
||||
|
||||
// 给定一组分片下载状态,计算出当前peer能提供下载的分片下标,如果已经拥有则不用提供
|
||||
// has = 0b01111111,had = 0b10010000
|
||||
// has & ~had = 0b01111111 & 0b01101111 = 0b01101111
|
||||
func (b *Bitfield) Provide(had *roaring.Bitmap) []uint32 {
|
||||
xor := roaring.AndNot(b.bitmap, had)
|
||||
return xor.ToArray()
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"github.com/RoaringBitmap/roaring"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBitfield_IsComplete(t *testing.T) {
|
||||
type fields struct {
|
||||
payload *roaring.Bitmap
|
||||
}
|
||||
type args struct {
|
||||
i int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"case-1",
|
||||
fields{
|
||||
payload: roaring.BitmapOf(1, 2, 3, 4),
|
||||
},
|
||||
args{
|
||||
1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"case-2",
|
||||
fields{
|
||||
payload: roaring.BitmapOf(1, 2, 3, 4),
|
||||
},
|
||||
args{
|
||||
5,
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := &Bitfield{
|
||||
pieceCount: 100,
|
||||
bitmap: tt.fields.payload,
|
||||
}
|
||||
if got := b.IsComplete(tt.args.i); got != tt.want {
|
||||
t.Errorf("Bitfield.IsComplete() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitfield_Provide(t *testing.T) {
|
||||
type fields struct {
|
||||
payload *roaring.Bitmap
|
||||
}
|
||||
type args struct {
|
||||
had *roaring.Bitmap
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []uint32
|
||||
}{
|
||||
{
|
||||
"case-1",
|
||||
fields{
|
||||
payload: roaring.BitmapOf(0, 1, 2, 3, 4),
|
||||
},
|
||||
args{
|
||||
roaring.BitmapOf(0),
|
||||
},
|
||||
[]uint32{1, 2, 3, 4},
|
||||
},
|
||||
{
|
||||
"case-2",
|
||||
fields{
|
||||
payload: roaring.BitmapOf(0, 1, 2, 3, 4, 5, 6, 7, 8),
|
||||
},
|
||||
args{
|
||||
roaring.BitmapOf(0, 1, 3, 5, 6, 8),
|
||||
},
|
||||
[]uint32{2, 4, 7},
|
||||
},
|
||||
{
|
||||
"case-3",
|
||||
fields{
|
||||
payload: roaring.BitmapOf(),
|
||||
},
|
||||
args{
|
||||
roaring.BitmapOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
|
||||
},
|
||||
[]uint32{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := &Bitfield{
|
||||
pieceCount: 100,
|
||||
bitmap: tt.fields.payload,
|
||||
}
|
||||
if got := b.Provide(tt.args.had); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Bitfield.Provide() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package message
|
||||
|
||||
// choke: <len=0001><id=0>
|
||||
type Choke struct {
|
||||
*base
|
||||
}
|
||||
|
||||
func NewChoke() *Choke {
|
||||
return &Choke{
|
||||
base: &base{
|
||||
id: IdChoke,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package message
|
||||
|
||||
// choke: <len=0001><id=2>
|
||||
type Interested struct {
|
||||
*base
|
||||
}
|
||||
|
||||
func NewInterested() *Interested {
|
||||
return &Interested{
|
||||
base: &base{
|
||||
id: IdInterested,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package message
|
||||
|
||||
// keep-alive: <len=0000>
|
||||
type Keepalive struct {
|
||||
*base
|
||||
}
|
||||
|
||||
var data = make([]byte, 4)
|
||||
|
||||
func (k *Keepalive) Encode() []byte {
|
||||
return data
|
||||
}
|
||||
|
||||
func (k *Keepalive) Decode(body []byte) {
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package message
|
||||
|
||||
// choke: <len=0001><id=3>
|
||||
type NotInterested struct {
|
||||
*base
|
||||
}
|
||||
|
||||
func NewNotInterested() *NotInterested {
|
||||
return &NotInterested{
|
||||
base: &base{
|
||||
id: IdNotInterested,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package message
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// piece: <len=0009+X><id=7><index><begin><block>
|
||||
type Piece struct {
|
||||
*base
|
||||
Index uint32
|
||||
Begin uint32
|
||||
Block []byte
|
||||
}
|
||||
|
||||
func NewPiece() *Piece {
|
||||
return &Piece{
|
||||
base: &base{
|
||||
id: IdPiece,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildPiece(index uint32, begin uint32, block []byte) *Piece {
|
||||
piece := NewPiece()
|
||||
piece.Index = index
|
||||
piece.Begin = begin
|
||||
piece.Block = block
|
||||
return piece
|
||||
|
||||
}
|
||||
|
||||
func (p *Piece) Encode() []byte {
|
||||
buf := make([]byte, 8+len(p.Block))
|
||||
binary.BigEndian.PutUint32(buf[0:4], p.Index)
|
||||
binary.BigEndian.PutUint32(buf[4:8], p.Begin)
|
||||
copy(buf[8:], p.Block)
|
||||
return encode(p.base, buf)
|
||||
}
|
||||
|
||||
func (p *Piece) Decode(buf []byte) {
|
||||
p.Index = binary.BigEndian.Uint32(buf[0:4])
|
||||
p.Begin = binary.BigEndian.Uint32(buf[4:8])
|
||||
p.Block = buf[8:]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// request: <len=0013><id=6><index><begin><length>
|
||||
type Request struct {
|
||||
*base
|
||||
Index uint32
|
||||
Begin uint32
|
||||
Length uint32
|
||||
}
|
||||
|
||||
func NewRequest() *Request {
|
||||
return &Request{
|
||||
base: &base{
|
||||
id: IdRequest,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildRequest(index uint32, begin uint32, length uint32) *Request {
|
||||
request := NewRequest()
|
||||
request.Index = index
|
||||
request.Begin = begin
|
||||
request.Length = length
|
||||
return request
|
||||
}
|
||||
|
||||
func (r *Request) Encode() []byte {
|
||||
buf := make([]byte, 12)
|
||||
binary.BigEndian.PutUint32(buf[0:4], r.Index)
|
||||
binary.BigEndian.PutUint32(buf[4:8], r.Begin)
|
||||
binary.BigEndian.PutUint32(buf[8:12], r.Length)
|
||||
return encode(r.base, buf)
|
||||
}
|
||||
|
||||
func (r *Request) Decode(buf []byte) {
|
||||
r.Length = binary.BigEndian.Uint32(buf[0:4])
|
||||
r.Begin = binary.BigEndian.Uint32(buf[4:8])
|
||||
r.Index = binary.BigEndian.Uint32(buf[8:12])
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package message
|
||||
|
||||
// unchoke: <len=0001><id=1>
|
||||
type Unchoke struct {
|
||||
*base
|
||||
}
|
||||
|
||||
func NewUnchoke() *Unchoke {
|
||||
return &Unchoke{
|
||||
base: &base{
|
||||
id: IdUnchoke,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Peer struct {
|
||||
IP uint32
|
||||
Port uint16
|
||||
}
|
||||
|
||||
func (peer *Peer) Address() string {
|
||||
bts := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(bts, peer.IP)
|
||||
return net.IP(bts).String() + ":" + strconv.Itoa(int(peer.Port))
|
||||
}
|
||||
|
||||
// 生成Peer ID,规则为前三位固定字母(-GP)+SemVer(xyz)+End(-),后面随机生成
|
||||
// 参考:https://wiki.theory.org/index.php/BitTorrentSpecification#peer_id
|
||||
func GenPeerID() [20]byte {
|
||||
peerID := [20]byte{'-', 'G', 'P', '0', '0', '1', '-'}
|
||||
_, err := rand.Read(peerID[7:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return peerID
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,74 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/metainfo"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/peer"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/torrent"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
torrent := buildTorrent()
|
||||
torrent.Download("e:/testbt/download")
|
||||
}
|
||||
|
||||
func buildTorrent() *torrent.Torrent {
|
||||
// metaInfo, err := metainfo.ParseFromFile("../testdata/Game.of.Thrones.S08E05.720p.WEB.H264-MEMENTO.torrent")
|
||||
metaInfo, err := metainfo.ParseFromFile("../../testdata/office.torrent")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
metaInfo.AnnounceList[0] = append(metaInfo.AnnounceList[0], []string{
|
||||
"udp://tracker.coppersurfer.tk:6969/announce",
|
||||
"udp://tracker.leechers-paradise.org:6969/announce",
|
||||
"udp://tracker.opentrackr.org:1337/announce",
|
||||
"udp://p4p.arenabg.com:1337/announce",
|
||||
"udp://9.rarbg.to:2710/announce",
|
||||
"udp://9.rarbg.me:2710/announce",
|
||||
"udp://tracker.internetwarriors.net:1337/announce",
|
||||
"udp://exodus.desync.com:6969/announce",
|
||||
"udp://tracker.tiny-vps.com:6969/announce",
|
||||
"udp://tracker.sbsub.com:2710/announce",
|
||||
"udp://retracker.lanta-net.ru:2710/announce",
|
||||
"udp://open.stealth.si:80/announce",
|
||||
"udp://open.demonii.si:1337/announce",
|
||||
"udp://tracker.torrent.eu.org:451/announce",
|
||||
"udp://tracker.moeking.me:6969/announce",
|
||||
"udp://tracker.cyberia.is:6969/announce",
|
||||
"udp://denis.stalker.upeer.me:6969/announce",
|
||||
"udp://ipv4.tracker.harry.lu:80/announce",
|
||||
"udp://explodie.org:6969/announce",
|
||||
"udp://zephir.monocul.us:6969/announce",
|
||||
"udp://xxxtor.com:2710/announce",
|
||||
"udp://valakas.rollo.dnsabr.com:2710/announce",
|
||||
"udp://tracker.zerobytes.xyz:1337/announce",
|
||||
"udp://tracker.yoshi210.com:6969/announce",
|
||||
"udp://tracker.uw0.xyz:6969/announce",
|
||||
"udp://tracker.swateam.org.uk:2710/announce",
|
||||
"udp://tracker.nyaa.uk:6969/announce",
|
||||
"udp://tracker.filemail.com:6969/announce",
|
||||
"udp://tracker.ds.is:6969/announce",
|
||||
"udp://retracker.sevstar.net:2710/announce",
|
||||
"udp://retracker.netbynet.ru:2710/announce",
|
||||
"udp://retracker.akado-ural.ru:80/announce",
|
||||
"udp://opentracker.i2p.rocks:6969/announce",
|
||||
"udp://opentor.org:2710/announce",
|
||||
"udp://open.nyap2p.com:6969/announce",
|
||||
"udp://chihaya.toss.li:9696/announce",
|
||||
"udp://bt2.archive.org:6969/announce",
|
||||
"udp://bt1.archive.org:6969/announce",
|
||||
"udp://www.loushao.net:8080/announce",
|
||||
"udp://tracker4.itzmx.com:2710/announce",
|
||||
"udp://tracker3.itzmx.com:6961/announce",
|
||||
"udp://tracker2.itzmx.com:6961/announce",
|
||||
"udp://tracker.lelux.fi:6969/announce",
|
||||
"udp://tracker.kamigami.org:2710/announce",
|
||||
"udp://tracker.dler.org:6969/announce",
|
||||
"udp://tr.bangumi.moe:6969/announce",
|
||||
"udp://qg.lorzl.gq:2710/announce",
|
||||
"udp://bt2.54new.com:8080/announce",
|
||||
}...)
|
||||
return torrent.NewTorrent(peer.GenPeerID(), metaInfo)
|
||||
}
|
||||
@@ -1,464 +0,0 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/RoaringBitmap/roaring"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/mse"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/metainfo"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/peer"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/peer/message"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 现在主流客户端的block大小都是16KB
|
||||
const blockSize = 2 << 13
|
||||
const keepaliveTimeout = 60 * 2
|
||||
|
||||
var errPieceCheckFailed = errors.New("piece check failed")
|
||||
var keepaliveData = make([]byte, 4)
|
||||
|
||||
type peerConn struct {
|
||||
torrent *Torrent
|
||||
peer *peer.Peer
|
||||
conn net.Conn
|
||||
// this client is choking the peer
|
||||
amChoking bool
|
||||
// this client is interested in the peer
|
||||
amInterested bool
|
||||
// peer is choking this client
|
||||
peerChoking bool
|
||||
// peer is interested in this client
|
||||
peerInterested bool
|
||||
|
||||
// 上一次收到包的时间
|
||||
lastReciveTime int64
|
||||
|
||||
bitfield *message.Bitfield
|
||||
readyEnd bool
|
||||
|
||||
downloadedCh chan error
|
||||
disconnectCh chan error
|
||||
// block下载队列,官方推荐为5
|
||||
blockQueueCh chan interface{}
|
||||
}
|
||||
|
||||
func newPeerConn(torrent *Torrent, peer *peer.Peer) *peerConn {
|
||||
return &peerConn{
|
||||
torrent: torrent,
|
||||
peer: peer,
|
||||
}
|
||||
}
|
||||
|
||||
// 使用MSE加密来避免运营商对bt流量的封锁,基本上现在市面上BT客户端都默认开启了,不用MSE的话很多Peer拒绝连接
|
||||
// see http://wiki.vuze.com/w/Message_Stream_Encryption
|
||||
func (pc *peerConn) dialMse() error {
|
||||
conn, err := net.DialTimeout("tcp", pc.peer.Address(), time.Second*time.Duration(30))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mseConn := mse.WrapConn(conn)
|
||||
infoHash := pc.torrent.MetaInfo.GetInfoHash()
|
||||
_, err = mseConn.HandshakeOutgoing(infoHash[:], mse.PlainText, nil)
|
||||
if err != nil {
|
||||
mseConn.Close()
|
||||
return err
|
||||
}
|
||||
pc.conn = mseConn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handshake of Peer wire protocol
|
||||
// see https://wiki.theory.org/index.php/BitTorrentSpecification#Handshake
|
||||
func (pc *peerConn) handshake() (*peer.Handshake, error) {
|
||||
handshakeRes, err := func() (*peer.Handshake, error) {
|
||||
handshakeReq := peer.NewHandshake([8]byte{}, pc.torrent.MetaInfo.GetInfoHash(), pc.torrent.PeerID)
|
||||
_, err := pc.conn.Write(handshakeReq.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var read [68]byte
|
||||
_, err = io.ReadFull(pc.conn, read[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshakeRes := &peer.Handshake{}
|
||||
err = handshakeRes.Decode(read[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// InfoHash不匹配
|
||||
if handshakeRes.InfoHash != handshakeReq.InfoHash {
|
||||
return nil, fmt.Errorf("info_hash not currently serving")
|
||||
}
|
||||
return handshakeRes, nil
|
||||
}()
|
||||
if err != nil {
|
||||
pc.conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
// init state
|
||||
pc.amChoking = true
|
||||
pc.amInterested = false
|
||||
pc.peerChoking = true
|
||||
pc.peerInterested = false
|
||||
return handshakeRes, nil
|
||||
}
|
||||
|
||||
func (pc *peerConn) handleKeepalive() {
|
||||
pc.lastReciveTime = time.Now().Unix()
|
||||
for {
|
||||
time.Sleep(time.Minute * 2)
|
||||
// 如果超过两分钟没有响应,则断开连接
|
||||
if time.Now().Unix()-pc.lastReciveTime > keepaliveTimeout {
|
||||
pc.conn.Close()
|
||||
break
|
||||
}
|
||||
// 两分钟发送一次心跳包
|
||||
_, err := pc.conn.Write(keepaliveData)
|
||||
if err != nil {
|
||||
pc.conn.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 准备下载
|
||||
func (pc *peerConn) ready() error {
|
||||
if err := pc.dialMse(); err != nil {
|
||||
return fmt.Errorf("tcp dial error %w", err)
|
||||
}
|
||||
if _, err := pc.handshake(); err != nil {
|
||||
return fmt.Errorf("handshake error %w", err)
|
||||
}
|
||||
pc.readyEnd = false
|
||||
readyCh := make(chan bool)
|
||||
pc.disconnectCh = make(chan error)
|
||||
go pc.handleKeepalive()
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(pc.conn)
|
||||
scanner.Split(message.SplitMessage)
|
||||
for scanner.Scan() {
|
||||
pc.lastReciveTime = time.Now().Unix()
|
||||
buf := scanner.Bytes()
|
||||
length := binary.BigEndian.Uint32(buf[:4])
|
||||
if length == 0 {
|
||||
// keepalive
|
||||
} else {
|
||||
switch message.ID(buf[4]) {
|
||||
case message.IdChoke:
|
||||
break
|
||||
case message.IdUnchoke:
|
||||
pc.handleUnchoke(readyCh)
|
||||
break
|
||||
case message.IdInterested:
|
||||
break
|
||||
case message.IdNotInterested:
|
||||
break
|
||||
case message.IdHave:
|
||||
break
|
||||
case message.IdBitfield:
|
||||
pc.handleBitfield(buf)
|
||||
break
|
||||
case message.IdRequest:
|
||||
break
|
||||
case message.IdPiece:
|
||||
pc.handlePiece(buf)
|
||||
break
|
||||
case message.IdCancel:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 还未下载完成时连接断开
|
||||
err := scanner.Err()
|
||||
if err == nil {
|
||||
err = io.EOF
|
||||
}
|
||||
pc.disconnectCh <- err
|
||||
close(pc.disconnectCh)
|
||||
}()
|
||||
err := func() error {
|
||||
select {
|
||||
case status := <-readyCh:
|
||||
if status {
|
||||
return nil
|
||||
}
|
||||
return errors.New("ready fail")
|
||||
case <-time.After(time.Second * 30):
|
||||
// 30秒之后超时
|
||||
return errors.New("ready time out")
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
pc.conn.Close()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 下载指定piece
|
||||
func (pc *peerConn) downloadPiece(index int) (err error) {
|
||||
pieceLength := pc.torrent.MetaInfo.GetPieceLength(index)
|
||||
pc.downloadedCh = make(chan error)
|
||||
pc.blockQueueCh = make(chan interface{}, 5)
|
||||
defer close(pc.blockQueueCh)
|
||||
// 按块下载分片
|
||||
blockCount := pc.torrent.pieceStates.states[index].blockCount
|
||||
for i := 0; i < blockCount; i++ {
|
||||
offset := blockSize * i
|
||||
// 如果已经下载过就跳过
|
||||
if pc.torrent.pieceStates.isBlockDone(index, offset) {
|
||||
continue
|
||||
}
|
||||
|
||||
var blockLength uint32
|
||||
if i == blockCount-1 {
|
||||
// 最后一个block大小需要计算出来
|
||||
blockLength = uint32(pieceLength - offset)
|
||||
} else {
|
||||
blockLength = blockSize
|
||||
}
|
||||
// block下载排队
|
||||
select {
|
||||
case pc.blockQueueCh <- nil:
|
||||
break
|
||||
case err = <-pc.downloadedCh:
|
||||
break
|
||||
case err = <-pc.disconnectCh:
|
||||
break
|
||||
}
|
||||
// 如果连接出现问题或下载失败直接返回异常
|
||||
if err != nil {
|
||||
pc.conn.Close()
|
||||
return
|
||||
}
|
||||
// 发起request,对方会响应piece
|
||||
_, err = pc.conn.Write(message.BuildRequest(uint32(index), uint32(offset), blockLength).Encode())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
select {
|
||||
case err = <-pc.downloadedCh:
|
||||
break
|
||||
case err = <-pc.disconnectCh:
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
pc.conn.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (pc *peerConn) handleUnchoke(readyCh chan<- bool) {
|
||||
pc.peerChoking = false
|
||||
// 已经处理过Unchoke信号
|
||||
if pc.readyEnd {
|
||||
return
|
||||
}
|
||||
pc.readyEnd = true
|
||||
// 如果客户端对peer感兴趣并且peer没有choked客户端,就可以开始下载了
|
||||
if pc.amInterested {
|
||||
readyCh <- true
|
||||
} else {
|
||||
readyCh <- false
|
||||
}
|
||||
close(readyCh)
|
||||
}
|
||||
|
||||
func (pc *peerConn) handleBitfield(buf []byte) {
|
||||
pc.bitfield = message.NewBitfield()
|
||||
pc.bitfield.Decode(buf)
|
||||
have := pc.getHavePieces(pc.bitfield)
|
||||
if len(have) > 0 {
|
||||
// 表示对该peer感兴趣,并且不choked该peer
|
||||
pc.conn.Write(message.NewInterested().Encode())
|
||||
pc.amInterested = true
|
||||
|
||||
pc.conn.Write(message.NewUnchoke().Encode())
|
||||
pc.amChoking = false
|
||||
} else {
|
||||
pc.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理下载响应,每次接收到响应直接将block写入到对应文件中
|
||||
func (pc *peerConn) handlePiece(buf []byte) {
|
||||
piece := message.NewPiece()
|
||||
piece.Decode(buf)
|
||||
info := pc.torrent.MetaInfo.Info
|
||||
fds := pc.torrent.MetaInfo.GetFileDetails()
|
||||
blockLength := int64(len(piece.Block))
|
||||
pieceBegin := int64(piece.Index) * int64(info.PieceLength)
|
||||
blockBegin := pieceBegin + int64(piece.Begin)
|
||||
// 计算block对应要写到的文件偏移
|
||||
var fileBlocks []*fileBlock
|
||||
if len(info.Files) == 0 {
|
||||
// 单文件
|
||||
fileBlocks = append(fileBlocks, &fileBlock{
|
||||
filepath: info.Name,
|
||||
fileSeek: blockBegin,
|
||||
blockBegin: 0,
|
||||
blockEnd: blockLength,
|
||||
})
|
||||
} else {
|
||||
// 获取要写入的第一个文件和偏移
|
||||
writeIndex := getWriteFile(blockBegin, fds)
|
||||
// block对应种子的偏移
|
||||
blockFileBegin := blockBegin
|
||||
// block对应文件的偏移
|
||||
var blockSeek int64 = 0
|
||||
for _, f := range fds[writeIndex:] {
|
||||
// 计算文件可剩余写入字节数
|
||||
fileWritable := f.End - blockFileBegin
|
||||
// 计算block剩余写入字节数
|
||||
blockWritable := blockLength - blockSeek
|
||||
fb := &fileBlock{
|
||||
filepath: filepath.Join(f.Path...),
|
||||
fileSeek: blockFileBegin - f.Begin,
|
||||
blockBegin: blockSeek,
|
||||
}
|
||||
if fileWritable >= blockWritable {
|
||||
// 若够block写入直接跳出循环
|
||||
fb.blockEnd = blockSeek + blockWritable
|
||||
fileBlocks = append(fileBlocks, fb)
|
||||
break
|
||||
} else {
|
||||
// 否则计算剩余可写字节数,写入到下一个文件中
|
||||
fb.blockEnd = blockSeek + fileWritable
|
||||
fileBlocks = append(fileBlocks, fb)
|
||||
blockFileBegin = f.End
|
||||
blockSeek += fileWritable
|
||||
}
|
||||
}
|
||||
}
|
||||
// 开始写入文件
|
||||
for _, f := range fileBlocks {
|
||||
err := func() error {
|
||||
name := filepath.Join(pc.torrent.Path, f.filepath)
|
||||
file, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.WriteAt(piece.Block[f.blockBegin:f.blockEnd], f.fileSeek)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
pc.downloadedCh <- errPieceCheckFailed
|
||||
pc.conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
pc.torrent.pieceStates.setBlockDone(int(piece.Index), int(piece.Begin))
|
||||
// 出队
|
||||
<-pc.blockQueueCh
|
||||
|
||||
log.Debugf("piece:%d block:%d", piece.Index, piece.Begin)
|
||||
|
||||
// piece全部下载完
|
||||
if pc.torrent.pieceStates.isPieceDone(int(piece.Index)) {
|
||||
// 计算piece对应的文件偏移
|
||||
sha1 := sha1.New()
|
||||
pieceLength := pc.torrent.MetaInfo.GetPieceLength(int(piece.Index))
|
||||
writeIndex := getWriteFile(pieceBegin, fds)
|
||||
fileBegin := pieceBegin - fds[writeIndex].Begin
|
||||
for _, fd := range fds[writeIndex:] {
|
||||
func() {
|
||||
file, err := os.Open(path.Join(pc.torrent.Path, fd.Path[0]))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.Seek(fileBegin, 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
written, err := io.CopyN(sha1, file, int64(pieceLength))
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
pieceLength -= int(written)
|
||||
}()
|
||||
if pieceLength > 0 {
|
||||
// 继续读下个文件
|
||||
fileBegin = 0
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
// 断开连接 TODO 用连接池进行复用
|
||||
pc.conn.Close()
|
||||
|
||||
// 校验piece SHA-1 hash
|
||||
downHash := [20]byte{}
|
||||
copy(downHash[:], sha1.Sum(nil))
|
||||
if downHash == pc.torrent.MetaInfo.Info.Pieces[piece.Index] {
|
||||
// piece下载完成
|
||||
pc.downloadedCh <- nil
|
||||
} else {
|
||||
pc.downloadedCh <- errPieceCheckFailed
|
||||
}
|
||||
close(pc.downloadedCh)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取piece对应要写入的文件
|
||||
// TODO 使用sort.Search二分查找法检索
|
||||
func getWriteFile(pieceBegin int64, fds []*metainfo.FileDetail) int {
|
||||
for i, f := range fds {
|
||||
if f.Begin <= pieceBegin && f.End > pieceBegin {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
type fileBlock struct {
|
||||
filepath string
|
||||
fileSeek int64
|
||||
blockBegin int64
|
||||
blockEnd int64
|
||||
}
|
||||
|
||||
// 获取peer能提供需要下载的文件分片
|
||||
func (pc *peerConn) getHavePieces(bitfield *message.Bitfield) []uint32 {
|
||||
had := roaring.New()
|
||||
for i := 0; i < pc.torrent.pieceStates.size(); i++ {
|
||||
if pc.torrent.pieceStates.getState(i) == stateFinish {
|
||||
had.AddInt(i)
|
||||
}
|
||||
}
|
||||
return bitfield.Provide(had)
|
||||
}
|
||||
|
||||
// 获取要写入到的文件
|
||||
/*func (ps *peerConn) getWriteFile(request *message.Request) string {
|
||||
info := ps.torrent.MetaInfo.Info
|
||||
// 单文件
|
||||
if len(info.Files) == 0 {
|
||||
return info.Name
|
||||
} else {
|
||||
request.Index * info.PieceLength + be
|
||||
for i := 0; i < len(info.Files); i++ {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1,122 +0,0 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/peer"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/tracker"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_peerState_ready(t *testing.T) {
|
||||
|
||||
ps := getUsablePeer()
|
||||
fmt.Println("get a peer")
|
||||
|
||||
// 下载前准备
|
||||
err := ps.ready()
|
||||
fmt.Println("ready end")
|
||||
if err != nil {
|
||||
fmt.Println(ps.peer.Address())
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_peerState_downloadPiece(t *testing.T) {
|
||||
|
||||
ps := getUsablePeer()
|
||||
fmt.Println("get a peer")
|
||||
|
||||
// 下载前准备
|
||||
err := ps.ready()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
have := ps.getHavePieces(ps.bitfield)
|
||||
fmt.Printf("download is ready,have %d\n", len(have))
|
||||
if len(have) > 0 {
|
||||
// 获取分片的长度
|
||||
for _, index := range have {
|
||||
fmt.Printf("download index %d\n", index)
|
||||
ps.downloadPiece(int(index))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取一个能连接上的peer
|
||||
func getUsablePeer() *peerConn {
|
||||
ch := getUsablePeerMore()
|
||||
ps := <-ch
|
||||
return ps
|
||||
}
|
||||
|
||||
// 获取多个能连接上的peer
|
||||
func getUsablePeerMore() chan *peerConn {
|
||||
torrent := buildTorrent()
|
||||
tracker := &tracker.Tracker{
|
||||
PeerID: torrent.PeerID,
|
||||
MetaInfo: torrent.MetaInfo,
|
||||
}
|
||||
peers := <-tracker.Tracker()
|
||||
ch := make(chan *peerConn)
|
||||
for i := range peers {
|
||||
go peerTest(torrent, peers[i], ch)
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func peerTest(torrent *Torrent, peer *peer.Peer, ch chan *peerConn) {
|
||||
ps := &peerConn{
|
||||
torrent: torrent,
|
||||
peer: peer,
|
||||
}
|
||||
// 连接至peer
|
||||
err := ps.ready()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ps.conn.Close()
|
||||
ch <- ps
|
||||
}
|
||||
|
||||
func TestSome(t *testing.T) {
|
||||
ch := make(chan interface{})
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
select {
|
||||
case ch <- nil:
|
||||
fmt.Println("write")
|
||||
case <-time.After(time.Second * 3):
|
||||
fmt.Println("timeout")
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
r := <-ch
|
||||
fmt.Println(r)
|
||||
fmt.Println("111")
|
||||
|
||||
}
|
||||
|
||||
func TestSome2(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(10)
|
||||
file, err := os.OpenFile("e:/testbt/test.data", os.O_RDWR|os.O_CREATE, 0644)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func(i int) {
|
||||
for j := 0; j < 100; j++ {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_, err = file.WriteAt([]byte{byte(i)}, int64(i*8))
|
||||
}
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/peer"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/tracker"
|
||||
)
|
||||
|
||||
type peerPool struct {
|
||||
torrent *Torrent
|
||||
|
||||
states map[*peer.Peer]*peerState
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
type peerState struct {
|
||||
// 是否在被使用
|
||||
using bool
|
||||
// 下载失败次数
|
||||
errors int
|
||||
}
|
||||
|
||||
func newPeerPool(torrent *Torrent) *peerPool {
|
||||
return &peerPool{
|
||||
torrent: torrent,
|
||||
lock: &sync.Mutex{},
|
||||
states: map[*peer.Peer]*peerState{},
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *peerPool) fetch() {
|
||||
tracker := &tracker.Tracker{
|
||||
PeerID: pp.torrent.PeerID,
|
||||
MetaInfo: pp.torrent.MetaInfo,
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// 当peer数量少于200个时重新发起一次tracker
|
||||
if len(pp.states) < 200 {
|
||||
go func() {
|
||||
for peers := range tracker.Tracker() {
|
||||
pp.put(peers)
|
||||
}
|
||||
}()
|
||||
}
|
||||
// 每2分钟检测一次
|
||||
time.Sleep(time.Minute * 2)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (pp *peerPool) put(peers []*peer.Peer) {
|
||||
pp.lock.Lock()
|
||||
defer pp.lock.Unlock()
|
||||
for _, peer := range peers {
|
||||
if _, ok := pp.states[peer]; !ok {
|
||||
pp.states[peer] = &peerState{
|
||||
using: false,
|
||||
errors: 0,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (pp *peerPool) get() *peer.Peer {
|
||||
pp.lock.Lock()
|
||||
defer pp.lock.Unlock()
|
||||
for peer, state := range pp.states {
|
||||
if !state.using {
|
||||
state.using = true
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 将peer重新放回池中,等待使用
|
||||
func (pp *peerPool) release(peer *peer.Peer) {
|
||||
pp.lock.Lock()
|
||||
defer pp.lock.Unlock()
|
||||
pp.states[peer].using = false
|
||||
pp.states[peer].errors = 0
|
||||
}
|
||||
|
||||
// 标记peer为不可用,超过3次则剔除该peer
|
||||
func (pp *peerPool) unavailable(peer *peer.Peer) {
|
||||
pp.lock.Lock()
|
||||
defer pp.lock.Unlock()
|
||||
pp.states[peer].using = false
|
||||
pp.states[peer].errors++
|
||||
if pp.states[peer].errors > 3 {
|
||||
delete(pp.states, peer)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/metainfo"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"github.com/RoaringBitmap/roaring"
|
||||
)
|
||||
|
||||
type state int
|
||||
|
||||
const (
|
||||
stateReady = iota
|
||||
stateDownloading
|
||||
stateFinish
|
||||
)
|
||||
|
||||
// 所有piece的下载状态
|
||||
type pieceStates struct {
|
||||
lock *sync.RWMutex
|
||||
// 所有piece的block下载进度
|
||||
states map[int]*pieceState
|
||||
}
|
||||
|
||||
type pieceState struct {
|
||||
state state
|
||||
// piece中block的个数
|
||||
blockCount int
|
||||
// piece中block的下载记录
|
||||
blockRecord *roaring.Bitmap
|
||||
}
|
||||
|
||||
func newPiecesState(meta *metainfo.MetaInfo) *pieceStates {
|
||||
pieceCount := len(meta.Info.Pieces)
|
||||
ps := &pieceStates{
|
||||
lock: &sync.RWMutex{},
|
||||
states: make(map[int]*pieceState, pieceCount),
|
||||
}
|
||||
for i := 0; i < pieceCount; i++ {
|
||||
ps.states[i] = &pieceState{
|
||||
state: stateReady,
|
||||
blockCount: int(math.Ceil(float64(meta.GetPieceLength(i)) / blockSize)),
|
||||
blockRecord: roaring.New(),
|
||||
}
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
func (ps *pieceStates) getState(index int) state {
|
||||
ps.lock.RLock()
|
||||
defer ps.lock.RUnlock()
|
||||
return ps.states[index].state
|
||||
}
|
||||
|
||||
func (ps *pieceStates) setState(index int, state state) {
|
||||
ps.lock.Lock()
|
||||
defer ps.lock.Unlock()
|
||||
ps.states[index].state = state
|
||||
}
|
||||
|
||||
func (ps *pieceStates) size() int {
|
||||
return len(ps.states)
|
||||
}
|
||||
|
||||
func (ps *pieceStates) getLeft() int {
|
||||
ps.lock.RLock()
|
||||
defer ps.lock.RUnlock()
|
||||
count := 0
|
||||
for _, v := range ps.states {
|
||||
if v.state == stateReady || v.state == stateDownloading {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (ps *pieceStates) isDone() bool {
|
||||
ps.lock.RLock()
|
||||
defer ps.lock.RUnlock()
|
||||
for _, v := range ps.states {
|
||||
if v.state == stateReady || v.state == stateDownloading {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ps *pieceStates) isPieceDone(index int) bool {
|
||||
ps.lock.RLock()
|
||||
defer ps.lock.RUnlock()
|
||||
state := ps.states[index]
|
||||
return uint64(state.blockCount) == state.blockRecord.GetCardinality()
|
||||
}
|
||||
|
||||
// 指定piece下的某个block是否下载完成
|
||||
func (ps *pieceStates) isBlockDone(pieceIndex int, blockIndex int) bool {
|
||||
return ps.states[pieceIndex].blockRecord.ContainsInt(blockIndex)
|
||||
}
|
||||
|
||||
// 指定piece下的某个block下载完成
|
||||
func (ps *pieceStates) setBlockDone(pieceIndex int, blockIndex int) {
|
||||
ps.states[pieceIndex].blockRecord.AddInt(blockIndex)
|
||||
}
|
||||
|
||||
// 清空指定piece下所有的block下载记录
|
||||
func (ps *pieceStates) clearBlocks(index int) {
|
||||
ps.states[index].blockRecord.Clear()
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/metainfo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Torrent struct {
|
||||
PeerID [20]byte
|
||||
MetaInfo *metainfo.MetaInfo
|
||||
|
||||
pieceStates *pieceStates
|
||||
peerPool *peerPool
|
||||
Path string
|
||||
}
|
||||
|
||||
func NewTorrent(peerID [20]byte, metaInfo *metainfo.MetaInfo) *Torrent {
|
||||
torrent := &Torrent{
|
||||
PeerID: peerID,
|
||||
MetaInfo: metaInfo,
|
||||
pieceStates: newPiecesState(metaInfo),
|
||||
}
|
||||
return torrent
|
||||
}
|
||||
|
||||
func (t *Torrent) Download(path string) {
|
||||
t.Path = path
|
||||
t.peerPool = newPeerPool(t)
|
||||
t.peerPool.fetch()
|
||||
|
||||
pieceQueueCh := make(chan interface{}, 128)
|
||||
defer close(pieceQueueCh)
|
||||
downloadedCh := make(chan interface{})
|
||||
defer close(downloadedCh)
|
||||
|
||||
for i := 0; i < len(t.MetaInfo.Info.Pieces); i++ {
|
||||
// 加入下载队列
|
||||
pieceQueueCh <- nil
|
||||
go func(index int) {
|
||||
t.pieceStates.setState(index, stateDownloading)
|
||||
// 一直下载到成功
|
||||
for {
|
||||
peer := t.peerPool.get()
|
||||
if peer == nil {
|
||||
time.Sleep(time.Second * 10)
|
||||
continue
|
||||
}
|
||||
pc := newPeerConn(t, peer)
|
||||
err := pc.ready()
|
||||
if err != nil {
|
||||
t.peerPool.unavailable(peer)
|
||||
} else {
|
||||
err = pc.downloadPiece(index)
|
||||
if err != nil {
|
||||
log.Debugf("piece %d download fail:%s", index, err.Error())
|
||||
// 下载失败
|
||||
t.peerPool.unavailable(peer)
|
||||
if errors.Is(err, errPieceCheckFailed) {
|
||||
// piece校验失败,需要重新下载过一遍,清空block记录
|
||||
t.pieceStates.clearBlocks(index)
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
// 下载完成
|
||||
t.pieceStates.setState(index, stateFinish)
|
||||
t.peerPool.release(peer)
|
||||
log.Debugf("piece %d download success,total:%d,left:%d", index, t.pieceStates.size(), t.pieceStates.getLeft())
|
||||
if t.pieceStates.isDone() {
|
||||
downloadedCh <- nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
<-pieceQueueCh
|
||||
}(i)
|
||||
}
|
||||
<-downloadedCh
|
||||
log.Debug("download end")
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/metainfo"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/peer"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
func TestTorrent_Download(t *testing.T) {
|
||||
torrent := buildTorrent()
|
||||
torrent.Download("e:/testbt/download")
|
||||
}
|
||||
|
||||
func buildTorrent() *Torrent {
|
||||
// metaInfo, err := metainfo.ParseFromFile("../testdata/Game.of.Thrones.S08E05.720p.WEB.H264-MEMENTO.torrent")
|
||||
metaInfo, err := metainfo.ParseFromFile("../testdata/office.torrent")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
metaInfo.AnnounceList[0] = append(metaInfo.AnnounceList[0], []string{
|
||||
"udp://tracker.coppersurfer.tk:6969/announce",
|
||||
"udp://tracker.leechers-paradise.org:6969/announce",
|
||||
"udp://tracker.opentrackr.org:1337/announce",
|
||||
"udp://p4p.arenabg.com:1337/announce",
|
||||
"udp://9.rarbg.to:2710/announce",
|
||||
"udp://9.rarbg.me:2710/announce",
|
||||
"udp://tracker.internetwarriors.net:1337/announce",
|
||||
"udp://exodus.desync.com:6969/announce",
|
||||
"udp://tracker.tiny-vps.com:6969/announce",
|
||||
"udp://tracker.sbsub.com:2710/announce",
|
||||
"udp://retracker.lanta-net.ru:2710/announce",
|
||||
"udp://open.stealth.si:80/announce",
|
||||
"udp://open.demonii.si:1337/announce",
|
||||
"udp://tracker.torrent.eu.org:451/announce",
|
||||
"udp://tracker.moeking.me:6969/announce",
|
||||
"udp://tracker.cyberia.is:6969/announce",
|
||||
"udp://denis.stalker.upeer.me:6969/announce",
|
||||
"udp://ipv4.tracker.harry.lu:80/announce",
|
||||
"udp://explodie.org:6969/announce",
|
||||
"udp://zephir.monocul.us:6969/announce",
|
||||
"udp://xxxtor.com:2710/announce",
|
||||
"udp://valakas.rollo.dnsabr.com:2710/announce",
|
||||
"udp://tracker.zerobytes.xyz:1337/announce",
|
||||
"udp://tracker.yoshi210.com:6969/announce",
|
||||
"udp://tracker.uw0.xyz:6969/announce",
|
||||
"udp://tracker.swateam.org.uk:2710/announce",
|
||||
"udp://tracker.nyaa.uk:6969/announce",
|
||||
"udp://tracker.filemail.com:6969/announce",
|
||||
"udp://tracker.ds.is:6969/announce",
|
||||
"udp://retracker.sevstar.net:2710/announce",
|
||||
"udp://retracker.netbynet.ru:2710/announce",
|
||||
"udp://retracker.akado-ural.ru:80/announce",
|
||||
"udp://opentracker.i2p.rocks:6969/announce",
|
||||
"udp://opentor.org:2710/announce",
|
||||
"udp://open.nyap2p.com:6969/announce",
|
||||
"udp://chihaya.toss.li:9696/announce",
|
||||
"udp://bt2.archive.org:6969/announce",
|
||||
"udp://bt1.archive.org:6969/announce",
|
||||
"udp://www.loushao.net:8080/announce",
|
||||
"udp://tracker4.itzmx.com:2710/announce",
|
||||
"udp://tracker3.itzmx.com:6961/announce",
|
||||
"udp://tracker2.itzmx.com:6961/announce",
|
||||
"udp://tracker.lelux.fi:6969/announce",
|
||||
"udp://tracker.kamigami.org:2710/announce",
|
||||
"udp://tracker.dler.org:6969/announce",
|
||||
"udp://tr.bangumi.moe:6969/announce",
|
||||
"udp://qg.lorzl.gq:2710/announce",
|
||||
"udp://bt2.54new.com:8080/announce",
|
||||
}...)
|
||||
return NewTorrent(peer.GenPeerID(), metaInfo)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ErrorCode int
|
||||
|
||||
const (
|
||||
ErrTimeout ErrorCode = -1
|
||||
ErrResponse ErrorCode = -2
|
||||
)
|
||||
|
||||
type BtError interface {
|
||||
error
|
||||
}
|
||||
|
||||
type TrackerError struct {
|
||||
Code ErrorCode
|
||||
Err error
|
||||
}
|
||||
|
||||
func (te *TrackerError) Error() string {
|
||||
return fmt.Sprintf("%s:%d", te.Err.Error(), te.Code)
|
||||
}
|
||||
|
||||
func NewTrackerError(code ErrorCode, err error) *TrackerError {
|
||||
return &TrackerError{code, err}
|
||||
}
|
||||
@@ -1,412 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/metainfo"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/peer"
|
||||
)
|
||||
|
||||
const (
|
||||
udpConnectRequestMagic = 0x41727101980
|
||||
udpConnectTimeout = 15
|
||||
udpConnectRetries = 3
|
||||
udpActionConnect = 0
|
||||
udpActionAnnounce = 1
|
||||
udpActionScrape = 2
|
||||
udpActionError = 3
|
||||
)
|
||||
|
||||
/*
|
||||
Offset size Name Value
|
||||
0 64-bit integer protocol_id 0x41727101980 // magic constant
|
||||
8 32-bit integer action 0 // connect
|
||||
12 32-bit integer transaction_id
|
||||
16
|
||||
Per https://www.libtorrent.org/udp_tracker_protocol.html#connecting
|
||||
*/
|
||||
type udpConnectRequest struct {
|
||||
protocolId uint64
|
||||
action uint32
|
||||
transactionId uint32
|
||||
}
|
||||
|
||||
func newUdpConnectRequest() *udpConnectRequest {
|
||||
return &udpConnectRequest{
|
||||
protocolId: udpConnectRequestMagic,
|
||||
action: udpActionConnect,
|
||||
transactionId: rand.Uint32(),
|
||||
}
|
||||
}
|
||||
|
||||
func (req *udpConnectRequest) encode() []byte {
|
||||
buf := make([]byte, 16)
|
||||
binary.BigEndian.PutUint64(buf[:8], req.protocolId)
|
||||
binary.BigEndian.PutUint32(buf[8:12], req.action)
|
||||
binary.BigEndian.PutUint32(buf[12:], req.transactionId)
|
||||
return buf
|
||||
}
|
||||
|
||||
/*
|
||||
Offset size Name Value
|
||||
0 32-bit integer action 0 // connect
|
||||
4 32-bit integer transaction_id
|
||||
8 64-bit integer connection_id
|
||||
16
|
||||
*/
|
||||
type udpConnectResponse struct {
|
||||
action uint32
|
||||
transactionId uint32
|
||||
connectionId uint64
|
||||
}
|
||||
|
||||
func newUdpConnectResponse(buf []byte) *udpConnectResponse {
|
||||
return &udpConnectResponse{
|
||||
action: binary.BigEndian.Uint32(buf[:4]),
|
||||
transactionId: binary.BigEndian.Uint32(buf[4:8]),
|
||||
connectionId: binary.BigEndian.Uint64(buf[8:]),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Offset size Name Value
|
||||
0 64-bit integer connection_id
|
||||
8 32-bit integer action 1 // announce
|
||||
12 32-bit integer transaction_id
|
||||
16 20-byte string info_hash
|
||||
36 20-byte string peer_id
|
||||
56 64-bit integer downloaded
|
||||
64 64-bit integer left
|
||||
72 64-bit integer uploaded
|
||||
80 32-bit integer event 0 // 0: none; 1: completed; 2: started; 3: stopped
|
||||
84 32-bit integer ip address 0 // default
|
||||
88 32-bit integer key
|
||||
92 32-bit integer num_want -1 // default
|
||||
96 16-bit integer port
|
||||
98
|
||||
Per https://www.libtorrent.org/udp_tracker_protocol.html#announcing
|
||||
*/
|
||||
type udpAnnounceRequest struct {
|
||||
connectionId uint64
|
||||
action uint32
|
||||
transactionId uint32
|
||||
infoHash [20]byte
|
||||
peerID [20]byte
|
||||
downloaded uint64
|
||||
left uint64
|
||||
uploaded uint64
|
||||
event uint32
|
||||
ip uint32
|
||||
key uint32
|
||||
numWant uint32
|
||||
port uint16
|
||||
}
|
||||
|
||||
func newUdpAnnounceRequest(connectionId uint64) *udpAnnounceRequest {
|
||||
return &udpAnnounceRequest{
|
||||
connectionId: connectionId,
|
||||
action: udpActionAnnounce,
|
||||
transactionId: rand.Uint32(),
|
||||
}
|
||||
}
|
||||
|
||||
func (req *udpAnnounceRequest) encode() []byte {
|
||||
buf := make([]byte, 98, 98)
|
||||
binary.BigEndian.PutUint64(buf[:8], req.connectionId)
|
||||
binary.BigEndian.PutUint32(buf[8:12], req.action)
|
||||
binary.BigEndian.PutUint32(buf[12:16], req.transactionId)
|
||||
copy(buf[16:36], req.infoHash[:])
|
||||
copy(buf[36:56], req.peerID[:])
|
||||
binary.BigEndian.PutUint64(buf[56:64], req.downloaded)
|
||||
binary.BigEndian.PutUint64(buf[64:72], req.left)
|
||||
binary.BigEndian.PutUint64(buf[72:80], req.uploaded)
|
||||
binary.BigEndian.PutUint32(buf[80:84], req.event)
|
||||
binary.BigEndian.PutUint32(buf[84:88], req.ip)
|
||||
binary.BigEndian.PutUint32(buf[88:92], req.key)
|
||||
binary.BigEndian.PutUint32(buf[92:96], req.numWant)
|
||||
binary.BigEndian.PutUint16(buf[96:98], req.port)
|
||||
return buf
|
||||
}
|
||||
|
||||
/*
|
||||
Offset size Name Value
|
||||
0 32-bit integer action 1 // announce
|
||||
4 32-bit integer transaction_id
|
||||
8 32-bit integer interval
|
||||
12 32-bit integer leechers
|
||||
16 32-bit integer seeders
|
||||
20 + 6 * n 32-bit integer ip address
|
||||
24 + 6 * n 16-bit integer TCP port
|
||||
20 + 6 * N
|
||||
Per https://www.libtorrent.org/udp_tracker_protocol.html#announcing
|
||||
*/
|
||||
type udpAnnounceResponse struct {
|
||||
action uint32
|
||||
transactionId uint32
|
||||
interval uint32
|
||||
leechers uint32
|
||||
seeders uint32
|
||||
peers []*peer.Peer
|
||||
}
|
||||
|
||||
func newUdpAnnounceResponse(buf []byte) *udpAnnounceResponse {
|
||||
response := &udpAnnounceResponse{
|
||||
action: binary.BigEndian.Uint32(buf[:4]),
|
||||
transactionId: binary.BigEndian.Uint32(buf[4:8]),
|
||||
interval: binary.BigEndian.Uint32(buf[8:12]),
|
||||
leechers: binary.BigEndian.Uint32(buf[12:16]),
|
||||
seeders: binary.BigEndian.Uint32(buf[16:20]),
|
||||
}
|
||||
count := (len(buf) - 20) / 6
|
||||
response.peers = make([]*peer.Peer, count, count)
|
||||
for i := 0; i < count; i++ {
|
||||
ipBegin := 20 + 6*i
|
||||
portBegin := ipBegin + 4
|
||||
response.peers[i] = &peer.Peer{
|
||||
IP: binary.BigEndian.Uint32(buf[ipBegin:portBegin]),
|
||||
Port: binary.BigEndian.Uint16(buf[portBegin : portBegin+2]),
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
type Tracker struct {
|
||||
PeerID [20]byte
|
||||
MetaInfo *metainfo.MetaInfo
|
||||
}
|
||||
|
||||
func (tracker *Tracker) connect(conn net.Conn, timeout int64) (response *udpConnectResponse, err error) {
|
||||
request := newUdpConnectRequest()
|
||||
buf := request.encode()
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(timeout)))
|
||||
conn.Write(buf)
|
||||
len, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len != 16 {
|
||||
err = NewTrackerError(ErrResponse, errors.New("invalid response"))
|
||||
return
|
||||
}
|
||||
response = newUdpConnectResponse(buf)
|
||||
return
|
||||
}
|
||||
|
||||
func (tracker *Tracker) announce(conn net.Conn, timeout int64, connectionId uint64) (response *udpAnnounceResponse, err error) {
|
||||
request := newUdpAnnounceRequest(connectionId)
|
||||
request.infoHash = tracker.MetaInfo.GetInfoHash()
|
||||
request.peerID = tracker.PeerID
|
||||
request.downloaded = 0
|
||||
request.uploaded = 0
|
||||
request.left = tracker.MetaInfo.GetTotalSize() - request.downloaded
|
||||
request.event = 0
|
||||
request.ip = 0
|
||||
request.key = 0
|
||||
request.numWant = 200
|
||||
request.port = 6882
|
||||
encode := request.encode()
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(timeout)))
|
||||
conn.Write(encode)
|
||||
buf := make([]byte, 20+request.numWant*6)
|
||||
len, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len < 20 {
|
||||
err = errors.New("announce fail")
|
||||
return
|
||||
}
|
||||
response = newUdpAnnounceResponse(buf[:len])
|
||||
return
|
||||
}
|
||||
|
||||
// 通过tracker服务器获取peer信息
|
||||
func (tracker *Tracker) Tracker() <-chan []*peer.Peer {
|
||||
peersCh := make(chan []*peer.Peer)
|
||||
metaInfo := tracker.MetaInfo
|
||||
|
||||
checkAnnounceList := func(announceList [][]string) bool {
|
||||
if len(announceList) > 0 {
|
||||
for _, l := range announceList {
|
||||
if len(l) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 只要获取到了peers就直接返回
|
||||
if checkAnnounceList(metaInfo.AnnounceList) {
|
||||
wg := sync.WaitGroup{}
|
||||
for _, announceArr := range metaInfo.AnnounceList {
|
||||
if len(announceArr) > 0 {
|
||||
for _, announce := range announceArr {
|
||||
wg.Add(1)
|
||||
go func(announce string) {
|
||||
peers, err := tracker.DoTracker(announce)
|
||||
if err == nil {
|
||||
peersCh <- peers
|
||||
}
|
||||
wg.Done()
|
||||
}(announce)
|
||||
}
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(peersCh)
|
||||
}()
|
||||
} else {
|
||||
go func() {
|
||||
peers, err := tracker.DoTracker(metaInfo.Announce)
|
||||
if err == nil {
|
||||
peersCh <- peers
|
||||
}
|
||||
close(peersCh)
|
||||
}()
|
||||
}
|
||||
return peersCh
|
||||
}
|
||||
|
||||
func (tracker *Tracker) DoTracker(announce string) (peers []*peer.Peer, err error) {
|
||||
if announce != "" {
|
||||
url, _ := url.Parse(announce)
|
||||
switch url.Scheme {
|
||||
case "http", "https":
|
||||
return tracker.httpTracker(url)
|
||||
case "udp", "udp4", "udp6":
|
||||
return tracker.udpTracker(url)
|
||||
default:
|
||||
return nil, errors.New("unsupported protocol")
|
||||
}
|
||||
}
|
||||
return nil, errors.New("empty announce")
|
||||
}
|
||||
|
||||
// http://bittorrent.org/beps/bep_0003.html#trackers
|
||||
func (tracker *Tracker) httpTracker(url *url.URL) (peers []*peer.Peer, err error) {
|
||||
metaInfo := tracker.MetaInfo
|
||||
peerID := tracker.PeerID
|
||||
|
||||
query := url.Query()
|
||||
infoHash := metaInfo.GetInfoHash()
|
||||
query.Add("info_hash", string(infoHash[:]))
|
||||
query.Add("peer_id", string(peerID[:]))
|
||||
query.Add("port", "6882")
|
||||
query.Add("uploaded", "0")
|
||||
query.Add("downloaded", "0")
|
||||
query.Add("left", strconv.FormatInt(int64(metaInfo.GetTotalSize()), 10))
|
||||
url.RawQuery = query.Encode()
|
||||
request, err := http.NewRequest("GET", url.String(), nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var httpClient = &http.Client{
|
||||
Timeout: time.Second * time.Duration(15),
|
||||
}
|
||||
request.Header.Set("User-Agent", "gopeed/0.0.1")
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
resp, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return
|
||||
}
|
||||
}
|
||||
// TODO Parse HTTP tracker response
|
||||
fmt.Println(resp)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// http://bittorrent.org/beps/bep_0015.html
|
||||
func (tracker *Tracker) udpTracker(url *url.URL) (peers []*peer.Peer, err error) {
|
||||
conn, err := dial(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
announceResponse, err := func() (announceResponse *udpAnnounceResponse, err error) {
|
||||
var connectResponse *udpConnectResponse
|
||||
// 0:connect 1:announce
|
||||
state := udpActionConnect
|
||||
// 访问超时最多重试8次,当有请求成功则将重试次数重置为0
|
||||
for n := 0; n < udpConnectRetries; n++ {
|
||||
// 15 * 2 ^ n seconds
|
||||
timeout := int64(udpConnectTimeout * math.Pow(2, float64(n)))
|
||||
switch state {
|
||||
case udpActionConnect:
|
||||
connectResponse, err = tracker.connect(conn, timeout)
|
||||
if err != nil {
|
||||
if oe, ok := err.(*net.OpError); ok && oe.Timeout() {
|
||||
break
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
n = -1
|
||||
state = udpActionAnnounce
|
||||
break
|
||||
case udpActionAnnounce:
|
||||
announceResponse, err = tracker.announce(conn, timeout, connectResponse.connectionId)
|
||||
if err != nil {
|
||||
if oe, ok := err.(*net.OpError); ok && oe.Timeout() {
|
||||
break
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return announceResponse.peers, nil
|
||||
}
|
||||
|
||||
func dial(trackerUrl *url.URL) (net.Conn, error) {
|
||||
return net.Dial(trackerUrl.Scheme, trackerUrl.Host)
|
||||
}
|
||||
|
||||
func lookupIP(host string, scheme string) (ip net.IP, err error) {
|
||||
ips, _ := net.LookupIP(host)
|
||||
if len(ips) == 0 {
|
||||
err = errors.New("no ips")
|
||||
return
|
||||
}
|
||||
for _, ip = range ips {
|
||||
switch scheme {
|
||||
case "udp", "udp4":
|
||||
if ip.To4() != nil {
|
||||
return
|
||||
}
|
||||
case "udp6":
|
||||
if ip.To4() == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
err = errors.New("no available ips ")
|
||||
return
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/monkeyWie/gopeed-core/pkg/protocol/bt/metainfo"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTracker_Tracker(t *testing.T) {
|
||||
metaInfo, err := metainfo.ParseFromFile("../testdata/Game.of.Thrones.S08E05.720p.WEB.H264-MEMENTO.torrent")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tracker := &Tracker{
|
||||
PeerID: [20]byte{},
|
||||
MetaInfo: metaInfo,
|
||||
}
|
||||
tracker.MetaInfo.Announce = "udp://tracker.opentrackr.org:1337/announce"
|
||||
tracker.MetaInfo.AnnounceList = [][]string{}
|
||||
peers := <-tracker.Tracker()
|
||||
fmt.Println(len(peers))
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Resolve return the file response to be downloaded
|
||||
func Resolve(request *Request) (*Response, error) {
|
||||
httpRequest, err := BuildHTTPRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Use "Range" header to resolve
|
||||
httpRequest.Header.Add("Range", "bytes=0-0")
|
||||
httpClient := BuildHTTPClient()
|
||||
response, err := httpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != 200 && response.StatusCode != 206 {
|
||||
return nil, fmt.Errorf("response status error:%d", response.StatusCode)
|
||||
}
|
||||
ret := &Response{}
|
||||
// Get file name by "Content-Disposition"
|
||||
if contentDisposition := response.Header.Get("Content-Disposition"); contentDisposition != "" {
|
||||
_, params, _ := mime.ParseMediaType(contentDisposition)
|
||||
if filename := params["filename"]; filename != "" {
|
||||
ret.Name = filename
|
||||
}
|
||||
}
|
||||
// Get file name by URL
|
||||
if ret.Name == "" {
|
||||
ret.Name = path.Base(httpRequest.URL.Path)
|
||||
}
|
||||
// Unknown file name
|
||||
if ret.Name == "" {
|
||||
ret.Name = "unknown"
|
||||
}
|
||||
// Is support range
|
||||
ret.Range = response.StatusCode == 206
|
||||
// Get file size
|
||||
if ret.Range {
|
||||
contentRange := response.Header.Get("Content-Range")
|
||||
if contentRange != "" {
|
||||
// e.g. bytes 0-1000/1001 => 1001
|
||||
total := subLastSlash(contentRange)
|
||||
if total != "" && total != "*" {
|
||||
parse, err := strconv.ParseInt(total, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.Size = parse
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contentLength := response.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
ret.Size, _ = strconv.ParseInt(contentLength, 10, 64)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Down
|
||||
func Down(request *Request) error {
|
||||
response, err := Resolve(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// allocate file
|
||||
file, err := os.Create(response.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if err := file.Truncate(response.Size); err != nil {
|
||||
return err
|
||||
}
|
||||
// support range
|
||||
if response.Range {
|
||||
cons := 16
|
||||
chunkSize := response.Size / int64(cons)
|
||||
waitGroup := &sync.WaitGroup{}
|
||||
waitGroup.Add(cons)
|
||||
for i := 0; i < cons; i++ {
|
||||
start := int64(i) * chunkSize
|
||||
end := start + chunkSize
|
||||
if i == cons-1 {
|
||||
end = response.Size
|
||||
}
|
||||
go downChunk(request, file, start, end-1, waitGroup)
|
||||
}
|
||||
waitGroup.Wait()
|
||||
} else {
|
||||
downChunk(request, file, 0, response.Size, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func subLastSlash(str string) string {
|
||||
index := strings.LastIndex(str, "/")
|
||||
if index != -1 {
|
||||
return str[index+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func BuildHTTPRequest(request *Request) (*http.Request, error) {
|
||||
// Build request
|
||||
httpRequest, err := http.NewRequest(strings.ToUpper(request.Method), request.URL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range request.Header {
|
||||
httpRequest.Header.Add(k, v)
|
||||
}
|
||||
return httpRequest, nil
|
||||
}
|
||||
|
||||
func BuildHTTPClient() *http.Client {
|
||||
// Cookie handle
|
||||
jar, _ := cookiejar.New(nil)
|
||||
|
||||
return &http.Client{Jar: jar}
|
||||
}
|
||||
|
||||
func downChunk(request *Request, file *os.File, start int64, end int64, waitGroup *sync.WaitGroup) {
|
||||
if waitGroup != nil {
|
||||
defer waitGroup.Done()
|
||||
}
|
||||
httpRequest, _ := BuildHTTPRequest(request)
|
||||
httpRequest.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", start, end))
|
||||
fmt.Printf("down %d-%d\n", start, end)
|
||||
httpClient := BuildHTTPClient()
|
||||
httpResponse, err := httpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
buf := make([]byte, 8192)
|
||||
writeIndex := start
|
||||
for {
|
||||
n, err := httpResponse.Body.Read(buf)
|
||||
if n > 0 {
|
||||
writeSize, err := file.WriteAt(buf[0:n], writeIndex)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
writeIndex += int64(writeSize)
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDown(t *testing.T) {
|
||||
type args struct {
|
||||
request *Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
"proxyee-down.ico",
|
||||
args{
|
||||
&Request{
|
||||
"get",
|
||||
"https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/master/front/public/favicon.ico",
|
||||
map[string]string{
|
||||
"Host": "github.com",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
||||
"Referer": "http://github.com/proxyee-down-org/proxyee-down/releases",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
os.Remove("favicon.ico")
|
||||
err := Down(tt.args.request)
|
||||
if err != nil {
|
||||
t.Errorf("error down= %v", err)
|
||||
return
|
||||
}
|
||||
downMd5 := fileMd5("favicon.ico")
|
||||
os.Remove("favicon.ico")
|
||||
if "8de7a6a2e786861013d61b77b2394012" != downMd5 {
|
||||
t.Errorf("error md5= %v", downMd5)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolve(t *testing.T) {
|
||||
// os.Setenv("HTTP_PROXY", "http://127.0.0.1:8888")
|
||||
type args struct {
|
||||
request *Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Response
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"proxyee-down.ico",
|
||||
args{
|
||||
&Request{
|
||||
"get",
|
||||
"https://raw.githubusercontent.com/proxyee-down-org/proxyee-down/master/front/public/favicon.ico",
|
||||
map[string]string{
|
||||
"Host": "github.com",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
|
||||
"Referer": "http://github.com/proxyee-down-org/proxyee-down/releases",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
},
|
||||
nil,
|
||||
},
|
||||
},
|
||||
&Response{"favicon.ico", 116093, true},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Resolve(tt.args.request)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Resolve() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Resolve() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemp(t *testing.T) {
|
||||
f, err := os.Create("e:/testbt/test.data")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := f.Truncate(1024 * 1024 * 512); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
bts := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
s := time.Now().UnixNano()
|
||||
f.Write(bts)
|
||||
fmt.Printf("write from 0 use: %d\n", time.Now().UnixNano()-s)
|
||||
s = time.Now().UnixNano()
|
||||
f.Seek(1024*1024*128, 0)
|
||||
f.Write(bts)
|
||||
fmt.Printf("write from 128M use %d\n", time.Now().UnixNano()-s)
|
||||
s = time.Now().UnixNano()
|
||||
f.Write(bts)
|
||||
fmt.Printf("write from 128M+9b use %d\n", time.Now().UnixNano()-s)
|
||||
s = time.Now().UnixNano()
|
||||
f.Seek(0, 0)
|
||||
f.Write(bts)
|
||||
fmt.Printf("write from 0 agein use %d\n", time.Now().UnixNano()-s)
|
||||
s = time.Now().UnixNano()
|
||||
f.Seek(1024*1024*64, 0)
|
||||
f.Write(bts)
|
||||
fmt.Printf("write from agein 64M use %d\n", time.Now().UnixNano()-s)
|
||||
}
|
||||
|
||||
func fileMd5(filePath string) string {
|
||||
file, _ := os.Open(filePath)
|
||||
|
||||
// Tell the program to call the following function when the current function returns
|
||||
defer file.Close()
|
||||
|
||||
// Open a new hash interface to write to
|
||||
hash := md5.New()
|
||||
|
||||
// Copy the file in the hash interface and check for any error
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func TestTemp2(t *testing.T) {
|
||||
fmt.Println("123456789"[2:3])
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package http
|
||||
|
||||
type Request struct {
|
||||
Method string
|
||||
URL string
|
||||
Header map[string]string
|
||||
Content []byte
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package http
|
||||
|
||||
type Response struct {
|
||||
Name string
|
||||
Size int64
|
||||
Range bool
|
||||
}
|
||||
Reference in new issue
Block a user