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