123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- <?php
- /////////////////////////////////////////////////////////////////
- /// getID3() by James Heinrich <info@getid3.org> //
- // available at https://github.com/JamesHeinrich/getID3 //
- // or https://www.getid3.org //
- // or http://getid3.sourceforge.net //
- // see readme.txt for more details //
- /////////////////////////////////////////////////////////////////
- // //
- // module.misc.torrent.php //
- // module for analyzing .torrent files //
- // dependencies: NONE //
- // ///
- /////////////////////////////////////////////////////////////////
- if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
- exit;
- }
- class getid3_torrent extends getid3_handler
- {
- /**
- * Assume all .torrent files are less than 1MB and just read entire thing into memory for easy processing.
- * Override this value if you need to process files larger than 1MB
- *
- * @var int
- */
- public $max_torrent_filesize = 1048576;
- /**
- * calculated InfoHash (SHA1 of the entire "info" Dictionary)
- *
- * @var string
- */
- private $infohash = '';
- const PIECE_HASHLENGTH = 20; // number of bytes the SHA1 hash is for each piece
- /**
- * @return bool
- */
- public function Analyze() {
- $info = &$this->getid3->info;
- $filesize = $info['avdataend'] - $info['avdataoffset'];
- if ($filesize > $this->max_torrent_filesize) { //
- $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');
- return false;
- }
- $this->fseek($info['avdataoffset']);
- $TORRENT = $this->fread($filesize);
- $offset = 0;
- if (!preg_match('#^(d8\\:announce|d7\\:comment)#', $TORRENT)) {
- $this->error('Expecting "d8:announce" or "d7:comment" at '.$info['avdataoffset'].', found "'.substr($TORRENT, $offset, 12).'" instead.');
- return false;
- }
- $info['fileformat'] = 'torrent';
- $info['torrent'] = $this->NextEntity($TORRENT, $offset);
- if ($this->infohash) {
- $info['torrent']['infohash'] = $this->infohash;
- }
- if (empty($info['torrent']['info']['length']) && !empty($info['torrent']['info']['files'][0]['length'])) {
- $info['torrent']['info']['length'] = 0;
- foreach ($info['torrent']['info']['files'] as $key => $filedetails) {
- $info['torrent']['info']['length'] += $filedetails['length'];
- }
- }
- if (!empty($info['torrent']['info']['length']) && !empty($info['torrent']['info']['piece length']) && !empty($info['torrent']['info']['pieces'])) {
- $num_pieces_size = ceil($info['torrent']['info']['length'] / $info['torrent']['info']['piece length']);
- $num_pieces_hash = strlen($info['torrent']['info']['pieces']) / getid3_torrent::PIECE_HASHLENGTH; // should be concatenated 20-byte SHA1 hashes
- if ($num_pieces_hash == $num_pieces_size) {
- $info['torrent']['info']['piece_hash'] = array();
- for ($i = 0; $i < $num_pieces_size; $i++) {
- $info['torrent']['info']['piece_hash'][$i] = '';
- for ($j = 0; $j < getid3_torrent::PIECE_HASHLENGTH; $j++) {
- $info['torrent']['info']['piece_hash'][$i] .= sprintf('%02x', ord($info['torrent']['info']['pieces'][(($i * getid3_torrent::PIECE_HASHLENGTH) + $j)]));
- }
- }
- unset($info['torrent']['info']['pieces']);
- } else {
- $this->warning('found '.$num_pieces_size.' pieces based on file/chunk size; found '.$num_pieces_hash.' pieces in hash table');
- }
- }
- if (!empty($info['torrent']['info']['name']) && !empty($info['torrent']['info']['length']) && !isset($info['torrent']['info']['files'])) {
- // single-file torrent
- $info['torrent']['files'] = array($info['torrent']['info']['name'] => $info['torrent']['info']['length']);
- } elseif (!empty($info['torrent']['info']['files'])) {
- // multi-file torrent
- $info['torrent']['files'] = array();
- foreach ($info['torrent']['info']['files'] as $key => $filedetails) {
- $info['torrent']['files'][implode('/', $filedetails['path'])] = $filedetails['length'];
- }
- } else {
- $this->warning('no files found');
- }
- return true;
- }
- /**
- * @return string|array|int|bool
- */
- public function NextEntity(&$TORRENT, &$offset) {
- // https://fileformats.fandom.com/wiki/Torrent_file
- // https://en.wikipedia.org/wiki/Torrent_file
- // https://en.wikipedia.org/wiki/Bencode
- if ($offset >= strlen($TORRENT)) {
- $this->error('cannot read beyond end of file '.$offset);
- return false;
- }
- $type = $TORRENT[$offset++];
- if ($type == 'i') {
- // Integers are stored as i<integer>e:
- // i90e
- $value = $this->ReadSequentialDigits($TORRENT, $offset, true);
- if ($TORRENT[$offset++] == 'e') {
- //echo '<li>int: '.$value.'</li>';
- return (int) $value;
- }
- $this->error('unexpected('.__LINE__.') input "'.$value.'" at offset '.($offset - 1));
- return false;
- } elseif ($type == 'd') {
- // Dictionaries are stored as d[key1][value1][key2][value2][...]e. Keys and values appear alternately.
- // Keys must be strings and must be ordered alphabetically.
- // For example, {apple-red, lemon-yellow, violet-blue, banana-yellow} is stored as:
- // d5:apple3:red6:banana6:yellow5:lemon6:yellow6:violet4:bluee
- $values = array();
- //echo 'DICTIONARY @ '.$offset.'<ul>';
- $info_dictionary_start = null; // dummy declaration to prevent "Variable might not be defined" warnings
- while (true) {
- if ($TORRENT[$offset] === 'e') {
- break;
- }
- $thisentry = array();
- $key = $this->NextEntity($TORRENT, $offset);
- if ($key == 'info') {
- $info_dictionary_start = $offset;
- }
- if ($key === false) {
- $this->error('unexpected('.__LINE__.') input at offset '.$offset);
- return false;
- }
- $value = $this->NextEntity($TORRENT, $offset);
- if ($key == 'info') {
- $info_dictionary_end = $offset;
- $this->infohash = sha1(substr($TORRENT, $info_dictionary_start, $info_dictionary_end - $info_dictionary_start));
- }
- if ($value === false) {
- $this->error('unexpected('.__LINE__.') input at offset '.$offset);
- return false;
- }
- $values[$key] = $value;
- }
- if ($TORRENT[$offset++] == 'e') {
- //echo '</ul>';
- return $values;
- }
- $this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1));
- return false;
- } elseif ($type == 'l') {
- //echo 'LIST @ '.$offset.'<ul>';
- // Lists are stored as l[value 1][value2][value3][...]e. For example, {spam, eggs, cheeseburger} is stored as:
- // l4:spam4:eggs12:cheeseburgere
- $values = array();
- while (true) {
- if ($TORRENT[$offset] === 'e') {
- break;
- }
- $NextEntity = $this->NextEntity($TORRENT, $offset);
- if ($NextEntity === false) {
- $this->error('unexpected('.__LINE__.') input at offset '.($offset - 1));
- return false;
- }
- $values[] = $NextEntity;
- }
- if ($TORRENT[$offset++] == 'e') {
- //echo '</ul>';
- return $values;
- }
- $this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1));
- return false;
- } elseif (ctype_digit($type)) {
- // Strings are stored as <length of string>:<string>:
- // 4:wiki
- $length = $type;
- while (true) {
- $char = $TORRENT[$offset++];
- if ($char == ':') {
- break;
- } elseif (!ctype_digit($char)) {
- $this->error('unexpected('.__LINE__.') input "'.$char.'" at offset '.($offset - 1));
- return false;
- }
- $length .= $char;
- }
- if (($offset + $length) > strlen($TORRENT)) {
- $this->error('string at offset '.$offset.' claims to be '.$length.' bytes long but only '.(strlen($TORRENT) - $offset).' bytes of data left in file');
- return false;
- }
- $string = substr($TORRENT, $offset, $length);
- $offset += $length;
- //echo '<li>string: '.$string.'</li>';
- return (string) $string;
- } else {
- $this->error('unexpected('.__LINE__.') input "'.$type.'" at offset '.($offset - 1));
- return false;
- }
- }
- /**
- * @return string
- */
- public function ReadSequentialDigits(&$TORRENT, &$offset, $allow_negative=false) {
- $start_offset = $offset;
- $value = '';
- while (true) {
- $char = $TORRENT[$offset++];
- if (!ctype_digit($char)) {
- if ($allow_negative && ($char == '-') && (strlen($value) == 0)) {
- // allow negative-sign if first character and $allow_negative enabled
- } else {
- $offset--;
- break;
- }
- }
- $value .= $char;
- }
- if (($value[0] === '0') && ($value !== '0')) {
- $this->warning('illegal zero-padded number "'.$value.'" at offset '.$start_offset);
- }
- return $value;
- }
- }
|