module.misc.torrent.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <?php
  2. /////////////////////////////////////////////////////////////////
  3. /// getID3() by James Heinrich <info@getid3.org> //
  4. // available at https://github.com/JamesHeinrich/getID3 //
  5. // or https://www.getid3.org //
  6. // or http://getid3.sourceforge.net //
  7. // see readme.txt for more details //
  8. /////////////////////////////////////////////////////////////////
  9. // //
  10. // module.misc.torrent.php //
  11. // module for analyzing .torrent files //
  12. // dependencies: NONE //
  13. // ///
  14. /////////////////////////////////////////////////////////////////
  15. if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
  16. exit;
  17. }
  18. class getid3_torrent extends getid3_handler
  19. {
  20. /**
  21. * Assume all .torrent files are less than 1MB and just read entire thing into memory for easy processing.
  22. * Override this value if you need to process files larger than 1MB
  23. *
  24. * @var int
  25. */
  26. public $max_torrent_filesize = 1048576;
  27. /**
  28. * calculated InfoHash (SHA1 of the entire "info" Dictionary)
  29. *
  30. * @var string
  31. */
  32. private $infohash = '';
  33. const PIECE_HASHLENGTH = 20; // number of bytes the SHA1 hash is for each piece
  34. /**
  35. * @return bool
  36. */
  37. public function Analyze() {
  38. $info = &$this->getid3->info;
  39. $filesize = $info['avdataend'] - $info['avdataoffset'];
  40. if ($filesize > $this->max_torrent_filesize) { //
  41. $this->error('File larger ('.number_format($filesize).' bytes) than $max_torrent_filesize ('.number_format($this->max_torrent_filesize).' bytes), increase getid3_torrent->max_torrent_filesize if needed');
  42. return false;
  43. }
  44. $this->fseek($info['avdataoffset']);
  45. $TORRENT = $this->fread($filesize);
  46. $offset = 0;
  47. if (!preg_match('#^(d8\\:announce|d7\\:comment)#', $TORRENT)) {
  48. $this->error('Expecting "d8:announce" or "d7:comment" at '.$info['avdataoffset'].', found "'.substr($TORRENT, $offset, 12).'" instead.');
  49. return false;
  50. }
  51. $info['fileformat'] = 'torrent';
  52. $info['torrent'] = $this->NextEntity($TORRENT, $offset);
  53. if ($this->infohash) {
  54. $info['torrent']['infohash'] = $this->infohash;
  55. }
  56. if (empty($info['torrent']['info']['length']) && !empty($info['torrent']['info']['files'][0]['length'])) {
  57. $info['torrent']['info']['length'] = 0;
  58. foreach ($info['torrent']['info']['files'] as $key => $filedetails) {
  59. $info['torrent']['info']['length'] += $filedetails['length'];
  60. }
  61. }
  62. if (!empty($info['torrent']['info']['length']) && !empty($info['torrent']['info']['piece length']) && !empty($info['torrent']['info']['pieces'])) {
  63. $num_pieces_size = ceil($info['torrent']['info']['length'] / $info['torrent']['info']['piece length']);
  64. $num_pieces_hash = strlen($info['torrent']['info']['pieces']) / getid3_torrent::PIECE_HASHLENGTH; // should be concatenated 20-byte SHA1 hashes
  65. if ($num_pieces_hash == $num_pieces_size) {
  66. $info['torrent']['info']['piece_hash'] = array();
  67. for ($i = 0; $i < $num_pieces_size; $i++) {
  68. $info['torrent']['info']['piece_hash'][$i] = '';
  69. for ($j = 0; $j < getid3_torrent::PIECE_HASHLENGTH; $j++) {
  70. $info['torrent']['info']['piece_hash'][$i] .= sprintf('%02x', ord($info['torrent']['info']['pieces'][(($i * getid3_torrent::PIECE_HASHLENGTH) + $j)]));
  71. }
  72. }
  73. unset($info['torrent']['info']['pieces']);
  74. } else {
  75. $this->warning('found '.$num_pieces_size.' pieces based on file/chunk size; found '.$num_pieces_hash.' pieces in hash table');
  76. }
  77. }
  78. if (!empty($info['torrent']['info']['name']) && !empty($info['torrent']['info']['length']) && !isset($info['torrent']['info']['files'])) {
  79. // single-file torrent
  80. $info['torrent']['files'] = array($info['torrent']['info']['name'] => $info['torrent']['info']['length']);
  81. } elseif (!empty($info['torrent']['info']['files'])) {
  82. // multi-file torrent
  83. $info['torrent']['files'] = array();
  84. foreach ($info['torrent']['info']['files'] as $key => $filedetails) {
  85. $info['torrent']['files'][implode('/', $filedetails['path'])] = $filedetails['length'];
  86. }
  87. } else {
  88. $this->warning('no files found');
  89. }
  90. return true;
  91. }
  92. /**
  93. * @return string|array|int|bool
  94. */
  95. public function NextEntity(&$TORRENT, &$offset) {
  96. // https://fileformats.fandom.com/wiki/Torrent_file
  97. // https://en.wikipedia.org/wiki/Torrent_file
  98. // https://en.wikipedia.org/wiki/Bencode
  99. if ($offset >= strlen($TORRENT)) {
  100. $this->error('cannot read beyond end of file '.$offset);
  101. return false;
  102. }
  103. $type = $TORRENT[$offset++];
  104. if ($type == 'i') {
  105. // Integers are stored as i<integer>e:
  106. // i90e
  107. $value = $this->ReadSequentialDigits($TORRENT, $offset, true);
  108. if ($TORRENT[$offset++] == 'e') {
  109. //echo '<li>int: '.$value.'</li>';
  110. return (int) $value;
  111. }
  112. $this->error('unexpected('.__LINE__.') input "'.$value.'" at offset '.($offset - 1));
  113. return false;
  114. } elseif ($type == 'd') {
  115. // Dictionaries are stored as d[key1][value1][key2][value2][...]e. Keys and values appear alternately.
  116. // Keys must be strings and must be ordered alphabetically.
  117. // For example, {apple-red, lemon-yellow, violet-blue, banana-yellow} is stored as:
  118. // d5:apple3:red6:banana6:yellow5:lemon6:yellow6:violet4:bluee
  119. $values = array();
  120. //echo 'DICTIONARY @ '.$offset.'<ul>';
  121. $info_dictionary_start = null; // dummy declaration to prevent "Variable might not be defined" warnings
  122. while (true) {
  123. if ($TORRENT[$offset] === 'e') {
  124. break;
  125. }
  126. $thisentry = array();
  127. $key = $this->NextEntity($TORRENT, $offset);
  128. if ($key == 'info') {
  129. $info_dictionary_start = $offset;
  130. }
  131. if ($key === false) {
  132. $this->error('unexpected('.__LINE__.') input at offset '.$offset);
  133. return false;
  134. }
  135. $value = $this->NextEntity($TORRENT, $offset);
  136. if ($key == 'info') {
  137. $info_dictionary_end = $offset;
  138. $this->infohash = sha1(substr($TORRENT, $info_dictionary_start, $info_dictionary_end - $info_dictionary_start));
  139. }
  140. if ($value === false) {
  141. $this->error('unexpected('.__LINE__.') input at offset '.$offset);
  142. return false;
  143. }
  144. $values[$key] = $value;
  145. }
  146. if ($TORRENT[$offset++] == 'e') {
  147. //echo '</ul>';
  148. return $values;
  149. }
  150. $this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1));
  151. return false;
  152. } elseif ($type == 'l') {
  153. //echo 'LIST @ '.$offset.'<ul>';
  154. // Lists are stored as l[value 1][value2][value3][...]e. For example, {spam, eggs, cheeseburger} is stored as:
  155. // l4:spam4:eggs12:cheeseburgere
  156. $values = array();
  157. while (true) {
  158. if ($TORRENT[$offset] === 'e') {
  159. break;
  160. }
  161. $NextEntity = $this->NextEntity($TORRENT, $offset);
  162. if ($NextEntity === false) {
  163. $this->error('unexpected('.__LINE__.') input at offset '.($offset - 1));
  164. return false;
  165. }
  166. $values[] = $NextEntity;
  167. }
  168. if ($TORRENT[$offset++] == 'e') {
  169. //echo '</ul>';
  170. return $values;
  171. }
  172. $this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1));
  173. return false;
  174. } elseif (ctype_digit($type)) {
  175. // Strings are stored as <length of string>:<string>:
  176. // 4:wiki
  177. $length = $type;
  178. while (true) {
  179. $char = $TORRENT[$offset++];
  180. if ($char == ':') {
  181. break;
  182. } elseif (!ctype_digit($char)) {
  183. $this->error('unexpected('.__LINE__.') input "'.$char.'" at offset '.($offset - 1));
  184. return false;
  185. }
  186. $length .= $char;
  187. }
  188. if (($offset + $length) > strlen($TORRENT)) {
  189. $this->error('string at offset '.$offset.' claims to be '.$length.' bytes long but only '.(strlen($TORRENT) - $offset).' bytes of data left in file');
  190. return false;
  191. }
  192. $string = substr($TORRENT, $offset, $length);
  193. $offset += $length;
  194. //echo '<li>string: '.$string.'</li>';
  195. return (string) $string;
  196. } else {
  197. $this->error('unexpected('.__LINE__.') input "'.$type.'" at offset '.($offset - 1));
  198. return false;
  199. }
  200. }
  201. /**
  202. * @return string
  203. */
  204. public function ReadSequentialDigits(&$TORRENT, &$offset, $allow_negative=false) {
  205. $start_offset = $offset;
  206. $value = '';
  207. while (true) {
  208. $char = $TORRENT[$offset++];
  209. if (!ctype_digit($char)) {
  210. if ($allow_negative && ($char == '-') && (strlen($value) == 0)) {
  211. // allow negative-sign if first character and $allow_negative enabled
  212. } else {
  213. $offset--;
  214. break;
  215. }
  216. }
  217. $value .= $char;
  218. }
  219. if (($value[0] === '0') && ($value !== '0')) {
  220. $this->warning('illegal zero-padded number "'.$value.'" at offset '.$start_offset);
  221. }
  222. return $value;
  223. }
  224. }