├── go.mod ├── README.md └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/arpitbbhayani/torrent-leecher 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | torrent leecher 2 | === 3 | 4 | This is an experiemental repository on my efforts to understand BitTorrent better. 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | ) 10 | 11 | // https://wiki.theory.org/BitTorrentSpecification 12 | type FileInfo struct { 13 | PieceLength int64 14 | Pieces [][]byte 15 | Length int64 16 | Name string 17 | } 18 | 19 | type Torrent struct { 20 | Info FileInfo 21 | Announce string 22 | } 23 | 24 | func BDecode(reader *bufio.Reader) (interface{}, error) { 25 | ch, err := reader.ReadByte() 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | switch ch { 31 | 32 | // integer 33 | case 'i': 34 | var buffer []byte 35 | for { 36 | ch, err = reader.ReadByte() 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // if we stumble upon `e`, dict complete, and we return 42 | if ch == 'e' { 43 | value, err := strconv.ParseInt(string(buffer), 10, 64) 44 | if err != nil { 45 | panic(err) 46 | } 47 | return value, nil 48 | } 49 | buffer = append(buffer, ch) 50 | } 51 | 52 | // list 53 | case 'l': 54 | var listHolder []interface{} 55 | for { 56 | ch, err = reader.ReadByte() 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | // if we stumble upon `e`, list complete, and we return 62 | if ch == 'e' { 63 | return listHolder, nil 64 | } 65 | 66 | // read the key 67 | reader.UnreadByte() 68 | data, err := BDecode(reader) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | // put key and value in dictionary 74 | listHolder = append(listHolder, data) 75 | } 76 | 77 | // dictioanry 78 | case 'd': 79 | dictHolder := map[string]interface{}{} 80 | for { 81 | ch, err = reader.ReadByte() 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | // if we stumble upon `e`, dict complete, and we return 87 | if ch == 'e' { 88 | return dictHolder, nil 89 | } 90 | 91 | // read the key 92 | reader.UnreadByte() 93 | data, err := BDecode(reader) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | // key has to be a string, if not then error 99 | key, ok := data.(string) 100 | if !ok { 101 | return nil, errors.New("key of the dictionary is not string") 102 | } 103 | 104 | // read the value 105 | value, err := BDecode(reader) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | // if key == "announce" || key == "announce-list" || key == "comment" || key == "created by" || key == "creation date" || key == "length" || key == "name" || key == "piece length" { 111 | // fmt.Println(value) 112 | // } 113 | 114 | // put key and value in dictionary 115 | dictHolder[key] = value 116 | } 117 | 118 | // string 119 | default: 120 | reader.UnreadByte() 121 | 122 | var lengthBuf []byte 123 | for { 124 | ch, err := reader.ReadByte() 125 | if err != nil { 126 | panic(err) 127 | } 128 | if ch == ':' { 129 | break 130 | } 131 | lengthBuf = append(lengthBuf, ch) 132 | } 133 | 134 | length, err := strconv.Atoi(string(lengthBuf)) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | var strBuf []byte 140 | for i := 0; i < length; i++ { 141 | ch, err := reader.ReadByte() 142 | if err != nil { 143 | panic(err) 144 | } 145 | strBuf = append(strBuf, ch) 146 | } 147 | 148 | return string(strBuf), nil 149 | } 150 | } 151 | 152 | func batch(data []byte, batch int) [][]byte { 153 | var result [][]byte 154 | for i := 0; i < len(data); i += batch { 155 | end := i + batch 156 | if end > len(data) { 157 | end = len(data) 158 | } 159 | result = append(result, data[i:end]) 160 | } 161 | return result 162 | } 163 | 164 | func ParseTorrent(reader *bufio.Reader) Torrent { 165 | data, err := BDecode(reader) 166 | if err != nil { 167 | panic(err) 168 | } 169 | 170 | tData, ok := data.(map[string]interface{}) 171 | if !ok { 172 | panic("not a valid torrent file") 173 | } 174 | tInfoData, ok := tData["info"].(map[string]interface{}) 175 | if !ok { 176 | panic("not a valid torrent file") 177 | } 178 | 179 | var torrentData Torrent 180 | torrentData.Announce = tData["announce"].(string) 181 | torrentData.Info = FileInfo{ 182 | PieceLength: tInfoData["piece length"].(int64), 183 | Length: tInfoData["length"].(int64), 184 | Name: tInfoData["name"].(string), 185 | Pieces: batch([]byte(tInfoData["pieces"].(string)), 20), 186 | } 187 | return torrentData 188 | } 189 | 190 | func main() { 191 | fp, err := os.Open("ubuntu-22.04-desktop-amd64.iso.torrent") 192 | if err != nil { 193 | panic(err) 194 | } 195 | defer fp.Close() 196 | breader := bufio.NewReader(fp) 197 | torrentData := ParseTorrent(breader) 198 | fmt.Println(torrentData.Info.Length) 199 | fmt.Println(torrentData.Info.PieceLength) 200 | fmt.Println(torrentData.Info.Length / torrentData.Info.PieceLength) 201 | fmt.Println(len(torrentData.Info.Pieces)) 202 | } 203 | --------------------------------------------------------------------------------