BioTorrents.de’s version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

torrent.class.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <?php
  2. declare(strict_types=1);
  3. # todo: Rplace with https://github.com/OPSnet/bencode-torrent
  4. /*******************************************************************************
  5. |~~~~ Gazelle bencode parser ~~~~|
  6. --------------------------------------------------------------------------------
  7. Welcome to the Gazelle bencode parser. bencoding is the way of encoding data
  8. that bittorrent uses in torrent files. When we read the torrent files, we get
  9. one long string that must be parsed into a format we can easily edit - that's
  10. where this file comes into play.
  11. There are 4 data types in bencode:
  12. * String
  13. * Int
  14. * List - array without keys
  15. - like array('value', 'value 2', 'value 3', 'etc')
  16. * Dictionary - array with string keys
  17. - like array['key 1'] = 'value 1'; array['key 2'] = 'value 2';
  18. Before you go any further, we recommend reading the sections on bencoding and
  19. metainfo file structure here: http://wiki.theory.org/BitTorrentSpecification
  20. //----- How we store the data -----//
  21. * Strings
  22. - Stored as php strings. Not difficult to remember.
  23. * Integers
  24. - Stored as php ints
  25. - must be casted with (int)
  26. * Lists
  27. - Stored as a BENCODE_LIST object.
  28. - The actual list is in BENCODE_LIST::$Val, as an array with incrementing integer indices
  29. - The list in BENCODE_LIST::$Val is populated by the BENCODE_LIST::dec() function
  30. * Dictionaries
  31. - Stored as a BENCODE_DICT object.
  32. - The actual list is in BENCODE_DICT::$Val, as an array with string indices
  33. - The list in BENCODE_DICT::$Val is populated by the BENCODE_DICT::dec() function
  34. //----- BENCODE_* Objects -----//
  35. Lists and dictionaries are stored as objects. They each have the following
  36. functions:
  37. * decode(Type, $Key)
  38. - Decodes ANY bencoded element, given the type and the key
  39. - Gets the position and string from $this
  40. * encode($Val)
  41. - Encodes ANY non-bencoded element, given the value
  42. * dec()
  43. - Decodes either a dictionary or a list, depending on where it's called from
  44. - Uses the decode() function quite a bit
  45. * enc()
  46. - Encodes either a dictionary or a list, depending on where it's called from
  47. - Relies mostly on the encode() function
  48. Finally, as all torrents are just large dictionaries, the TORRENT class extends
  49. the BENCODE_DICT class.
  50. *******************************************************************************/
  51. class BENCODE2
  52. {
  53. public $Val; // Decoded array
  54. public $Pos = 1; // Pointer that indicates our position in the string
  55. public $Str = ''; // Torrent string
  56. public function __construct($Val, $IsParsed = false)
  57. {
  58. if (!$IsParsed) {
  59. $this->Str = $Val;
  60. $this->dec();
  61. } else {
  62. $this->Val = $Val;
  63. }
  64. }
  65. // Decode an element based on the type. The type is really just an indicator.
  66. public function decode($Type, $Key)
  67. {
  68. if (is_number($Type)) { // Element is a string
  69. // Get length of string
  70. $StrLen = $Type;
  71. while ($this->Str[$this->Pos + 1] !== ':') {
  72. $this->Pos++;
  73. $StrLen.=$this->Str[$this->Pos];
  74. }
  75. $this->Val[$Key] = substr($this->Str, $this->Pos + 2, $StrLen);
  76. $this->Pos += $StrLen;
  77. $this->Pos += 2;
  78. } elseif ($Type === 'i') { // Element is an int
  79. $this->Pos++;
  80. // Find end of integer (first occurance of 'e' after position)
  81. $End = strpos($this->Str, 'e', $this->Pos);
  82. // Get the integer, and - IMPORTANT - cast it as an int, so we know later that it's an int and not a string
  83. $this->Val[$Key] = (int)substr($this->Str, $this->Pos, $End-$this->Pos);
  84. $this->Pos = $End + 1;
  85. } elseif ($Type === 'l') { // Element is a list
  86. $this->Val[$Key] = new BENCODE_LIST(substr($this->Str, $this->Pos));
  87. $this->Pos += $this->Val[$Key]->Pos;
  88. } elseif ($Type === 'd') { // Element is a dictionary
  89. $this->Val[$Key] = new BENCODE_DICT(substr($this->Str, $this->Pos));
  90. $this->Pos += $this->Val[$Key]->Pos;
  91. // Sort by key to respect spec
  92. if (!empty($this->Val[$Key]->Val)) {
  93. ksort($this->Val[$Key]->Val);
  94. }
  95. } else {
  96. error('Invalid torrent file');
  97. }
  98. }
  99. public function encode($Val)
  100. {
  101. if (is_int($Val)) { // Integer
  102. return 'i'.$Val.'e';
  103. } elseif (is_string($Val)) {
  104. return strlen($Val).':'.$Val;
  105. } elseif (is_object($Val)) {
  106. return $Val->enc();
  107. } else {
  108. return 'fail';
  109. }
  110. }
  111. }
  112. class BENCODE_LIST extends BENCODE2
  113. {
  114. public function enc()
  115. {
  116. if (empty($this->Val)) {
  117. return 'le';
  118. }
  119. $Str = 'l';
  120. reset($this->Val);
  121. foreach ($this->Val as $Value) {
  122. $Str.=$this->encode($Value);
  123. }
  124. return $Str.'e';
  125. }
  126. // Decode a list
  127. public function dec()
  128. {
  129. $Key = 0; // Array index
  130. $Length = strlen($this->Str);
  131. while ($this->Pos < $Length) {
  132. $Type = $this->Str[$this->Pos];
  133. // $Type now indicates what type of element we're dealing with
  134. // It's either an integer (string), 'i' (an integer), 'l' (a list), 'd' (a dictionary), or 'e' (end of dictionary/list)
  135. if ($Type === 'e') { // End of list
  136. $this->Pos += 1;
  137. unset($this->Str); // Since we're finished parsing the string, we don't need to store it anymore. Benchmarked - this makes the parser run way faster
  138. return;
  139. }
  140. // Decode the bencoded element
  141. // This function changes $this->Pos and $this->Val, so you don't have to
  142. $this->decode($Type, $Key);
  143. ++$Key;
  144. }
  145. return true;
  146. }
  147. }
  148. class BENCODE_DICT extends BENCODE2
  149. {
  150. public function enc()
  151. {
  152. if (empty($this->Val)) {
  153. return 'de';
  154. }
  155. $Str = 'd';
  156. reset($this->Val);
  157. foreach ($this->Val as $Key => $Value) {
  158. $Str.=strlen($Key).':'.$Key.$this->encode($Value);
  159. }
  160. return $Str.'e';
  161. }
  162. // Decode a dictionary
  163. public function dec()
  164. {
  165. $Length = strlen($this->Str);
  166. while ($this->Pos<$Length) {
  167. if ($this->Str[$this->Pos] === 'e') { // End of dictionary
  168. $this->Pos += 1;
  169. unset($this->Str); // Since we're finished parsing the string, we don't need to store it anymore. Benchmarked - this makes the parser run way faster
  170. return;
  171. }
  172. // Get the dictionary key
  173. // Length of the key, in bytes
  174. $KeyLen = $this->Str[$this->Pos];
  175. // Allow for multi-digit lengths
  176. while ($this->Str[$this->Pos + 1] !== ':' && $this->Pos + 1 < $Length) {
  177. $this->Pos++;
  178. $KeyLen.=$this->Str[$this->Pos];
  179. }
  180. // $this->Pos is now on the last letter of the key length
  181. // Adding 2 brings it past that character and the ':' to the beginning of the string
  182. $this->Pos += 2;
  183. // Get the name of the key
  184. $Key = substr($this->Str, $this->Pos, $KeyLen);
  185. // Move the position past the key to the beginning of the element
  186. $this->Pos += $KeyLen;
  187. $Type = $this->Str[$this->Pos];
  188. // $Type now indicates what type of element we're dealing with
  189. // It's either an integer (string), 'i' (an integer), 'l' (a list), 'd' (a dictionary), or 'e' (end of dictionary/list)
  190. // Decode the bencoded element
  191. // This function changes $this->Pos and $this->Val, so you don't have to
  192. $this->decode($Type, $Key);
  193. }
  194. return true;
  195. }
  196. }
  197. class TORRENT extends BENCODE_DICT
  198. {
  199. public function dump()
  200. {
  201. // Convenience function used for testing and figuring out how we store the data
  202. print_r($this->Val);
  203. }
  204. public function dump_data()
  205. {
  206. // Function which serializes $this->Val for storage
  207. return base64_encode(serialize($this->Val));
  208. }
  209. public function set_announce_list($UrlsList)
  210. {
  211. $AnnounceList = new BENCODE_LIST([], true);
  212. foreach ($UrlsList as $Urls) {
  213. $SubList = new BENCODE_LIST($Urls, true);
  214. unset($SubList->Str);
  215. $AnnounceList->Val[] = $SubList;
  216. }
  217. $this->Val['announce-list'] = $AnnounceList;
  218. }
  219. public function set_announce_url($Announce)
  220. {
  221. $this->Val['announce'] = $Announce;
  222. ksort($this->Val);
  223. }
  224. // Returns an array of:
  225. // * the files in the torrent
  226. // * the total size of files described therein
  227. public function file_list()
  228. {
  229. $FileList = [];
  230. if (!isset($this->Val['info']->Val['files'])) { // Single file mode
  231. $TotalSize = $this->Val['info']->Val['length'];
  232. $FileList[] = array($TotalSize, $this->get_name());
  233. } else { // Multiple file mode
  234. $FileNames = [];
  235. $FileSizes = [];
  236. $TotalSize = 0;
  237. $Files = $this->Val['info']->Val['files']->Val;
  238. if (isset($Files[0]->Val['path.utf-8'])) {
  239. $PathKey = 'path.utf-8';
  240. } else {
  241. $PathKey = 'path';
  242. }
  243. foreach ($Files as $File) {
  244. $FileSize = $File->Val['length'];
  245. $TotalSize += $FileSize;
  246. $FileName = ltrim(implode('/', $File->Val[$PathKey]->Val), '/');
  247. $FileSizes[] = $FileSize;
  248. $FileNames[] = $FileName;
  249. }
  250. natcasesort($FileNames);
  251. foreach ($FileNames as $Index => $FileName) {
  252. $FileList[] = array($FileSizes[$Index], $FileName);
  253. }
  254. }
  255. return array($TotalSize, $FileList);
  256. }
  257. public function get_name()
  258. {
  259. if (isset($this->Val['info']->Val['name.utf-8'])) {
  260. return $this->Val['info']->Val['name.utf-8'];
  261. } else {
  262. return $this->Val['info']->Val['name'];
  263. }
  264. }
  265. public function make_private()
  266. {
  267. //----- The following properties do not affect the infohash:
  268. // anounce-list is an unofficial extension to the protocol
  269. // that allows for multiple trackers per torrent
  270. unset($this->Val['announce-list']);
  271. // Bitcomet & Azureus cache peers in here
  272. unset($this->Val['nodes']);
  273. // Azureus stores the dht_backup_enable flag here
  274. unset($this->Val['azureus_properties']);
  275. // Remove web-seeds
  276. unset($this->Val['url-list']);
  277. // Remove libtorrent resume info
  278. unset($this->Val['libtorrent_resume']);
  279. //----- End properties that do not affect the infohash
  280. if ($this->Val['info']->Val['private']) {
  281. return true; // Torrent is private
  282. } else {
  283. // Torrent is not private!
  284. // Add private tracker flag and sort info dictionary
  285. $this->Val['info']->Val['private'] = 1;
  286. ksort($this->Val['info']->Val);
  287. return false;
  288. }
  289. }
  290. }