From c2ce6922611eb37cbd609f98333dd71eb8dff594 Mon Sep 17 00:00:00 2001 From: itismadness Date: Wed, 28 Mar 2018 02:02:31 +0700 Subject: [PATCH] initial commit --- .gitignore | 5 + LICENSE.md | 24 +++ README.md | 36 ++++ composer.json | 24 +++ phpunit.xml | 19 ++ src/BencodeTorrent.php | 392 +++++++++++++++++++++++++++++++++++ tests/BencodeTorrentTest.php | 82 ++++++++ tests/data/test_1.torrent | Bin 0 -> 16992 bytes 8 files changed, 582 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/BencodeTorrent.php create mode 100644 tests/BencodeTorrentTest.php create mode 100644 tests/data/test_1.torrent diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4296252 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +report/ +vendor/ +.DS_Store +composer.lock diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..00d2e13 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c4f818 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +BEncode Torrent +=============== + +PHP library for encoding and decoding BitTorrent BEncode data, focused around +[Gazelle](https://github.com/ApolloRIP/Gazelle). + +BEncode is the encoding used by BitTorrent to store and transmitting loosely structured data. It supports +* byte strings +* integers +* lists +* dictionaries (associative arrays, where keys are sorted alphabetically) + +You can see more information about how these types are supported at +[BitTorrentSpecification#Bencoding](https://wiki.theory.org/index.php/BitTorrentSpecification#Bencoding). + +In addition to the above, torrent files are expected to be BEncoded dictionaries that contain minimally the keys +__announce__ (byte string) and __info__ (dictionary). Within the __info__ dictionary, we then expect __piece length__ +(integer) and __pieces__ (byte string). If the torrent has only a single file, we then expect __name__ (byte string) +and __length__ (integer), whereas for a multi-file torrent, we'll have __name__ (byte string) and __files__ (list) +where each element is a dictionary that has the keys __length__ (integer) and __path__ (list of strings). + +As such, this library will make some checks when loading data that these mandatory fields exist or else an Exception is +raised. More information on these fields can be found at +[BitTorrentSpecification#Metainfo_File_Structure](https://wiki.theory.org/index.php/BitTorrentSpecification#Metainfo_File_Structure). + +Finally, this library is primarily aimed at being used within the [Gazelle](https://github.com/ApolloRIP/Gazelle) so +we have some utility functions within the library that make sense there to accomplish the following things: +* Ensuring torrent files are marked as 'private' +* Setting a 'source' on torrents (to ensure unique info hash) +* Cleaning out unnecessary fields that also reveal stuff about a user (like __announce list__ and __created by__) +* Generate string file lists as expected by Gazelle for display + +This is based (loosely) off the code in the two separate BEncode libraries within WCD's Gazelle +([bencodetorrent.class.php](https://github.com/WhatCD/Gazelle/blob/master/classes/bencodetorrent.class.php) and +[torrent.class.php](https://github.com/WhatCD/Gazelle/blob/master/classes/torrent.class.php)), but without the +necessary 32bit shims as well as make it a unified library used for both uploading and downloading the torrent files. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c0f0461 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "type": "library", + "name": "apollorip/bencode-torrent", + "description": "PHP Library for decoding and encoding BitTorrent BEncoded data, built for Gazelle", + "license": "Unlicense", + "authors": [ + { + "name": "itismadness", + "email": "itismadness@apollo.rip" + } + ], + "autoload": { + "psr-4": { + "ApolloRIP\\BencodeTorrent\\": "src/" + } + }, + "require": {}, + "require-dev": { + "phpunit/phpunit": "^6", + "squizlabs/php_codesniffer": "3.*", + "php": "^7", + "ext-mbstring": "^7" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..1a24ac7 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,19 @@ + + + + tests + + + + + src + + + + + + + \ No newline at end of file diff --git a/src/BencodeTorrent.php b/src/BencodeTorrent.php new file mode 100644 index 0000000..e1e95b3 --- /dev/null +++ b/src/BencodeTorrent.php @@ -0,0 +1,392 @@ +setDelim(); + } + + private function setDelim() { + if (BencodeTorrent::$utf8_filelist_delim === null) { + BencodeTorrent::$utf8_filelist_delim = utf8_encode(chr(BencodeTorrent::FILELIST_DELIM)); + } + } + + /** + * @param array $data + * @throws \Exception + */ + public function setData($data) { + $this->data = $data; + $this->validate(); + } + + /** + * @param string $data + * @throws \Exception + */ + public function decodeData(string $data) { + $this->data = $this->decode($data); + $this->validate(); + } + + /** + * @param string $path + * @throws \Exception + */ + public function decodeFile(string $path) { + $this->data = $this->decode(file_get_contents($path, FILE_BINARY)); + $this->validate(); + } + + /** + * @param string $data + * @param int $pos + * @return array|bool|float|string + */ + private function decode(string $data, int &$pos = 0) { + if ($data[$pos] === 'd') { + $pos++; + $return = []; + while ($data[$pos] !== 'e') { + $key = $this->decode($data, $pos); + $value = $this->decode($data, $pos); + if (empty($key) || empty($value)) { + break; + } + $return[$key] = $value; + } + $pos++; + } + elseif ($data[$pos] === 'l') { + $pos++; + $return = []; + while ($data[$pos] !== 'e') { + $value = $this->decode($data, $pos); + $return[] = $value; + } + $pos++; + } + elseif ($data[$pos] === 'i') { + $pos++; + $digits = strpos($data, 'e', $pos) - $pos; + $return = (int) substr($data, $pos, $digits); + $pos += $digits + 1; + } + else { + $digits = strpos($data, ':', $pos) - $pos; + $len = (int) substr($data, $pos, $digits); + $pos += ($digits + 1); + $return = substr($data, $pos, $len); + $pos += $len; + } + return $return; + } + + public function getData() { + return $this->data; + } + + /** + * @throws \Exception + */ + public function validate() { + if (empty($this->data['info'])) { + throw new \Exception("Torrent dictionary doesn't have info key"); + } + } + + /** + * @throws \RuntimeException + */ + private function hasData() { + if (empty($this->data) || !is_array($this->data)) { + throw new \RuntimeException('Must decode proper bencode string first'); + } + } + + /** + * @return string + */ + public function getEncode() { + $this->hasData(); + return $this->encodeVal($this->data); + } + + /** + * @param $data + * @return string + */ + private function encodeVal($data) { + if (is_array($data)) { + $return = ''; + $check = -1; + $list = true; + foreach ($data as $key => $value) { + if ($key !== ++$check) { + $list = false; + break; + } + + } + if ($list) { + $return .= 'l'; + foreach ($data as $value) { + $return .= $this->encodeVal($value); + } + } + else { + $return .= 'd'; + foreach ($data as $key => $value) { + $return .= $this->encodeVal(strval($key)); + $return .= $this->encodeVal($value); + } + } + $return .= 'e'; + } + elseif (is_integer($data)) { + $return = 'i'.$data.'e'; + } + else { + $return = strlen($data) . ':' . $data; + } + return $return; + } + + /** + * Utility function to clean out keys in the data and info dictionaries that we don't need in our torrent file + * when we go to store it in the DB or serve it up to the user (with the expectation that we'll be calling at + * least setAnnounceUrl(...) when a user asks for a valid torrent file). + * + * @return bool flag to indicate if we altered the info dictionary + */ + public function clean() { + $this->cleanDataDictionary(); + return $this->cleanInfoDictionary(); + } + + /** + * Clean out keys within the data dictionary that are not strictly necessary or will be overwritten dynamically + * on any downloaded torrent (like announce or comment), so that we store the smallest encoded string within the + * database and cuts down on potential waste. + */ + public function cleanDataDictionary() { + $allowed_keys = array('encoding', 'info'); + foreach ($this->data['info'] as $key => $value) { + if (!in_array($key, $allowed_keys)) { + unset($this->data['info'][$key]); + } + } + } + + /** + * Cleans out keys within the info dictionary (and would affect the info hash). + * @return bool + */ + public function cleanInfoDictionary() { + $cleaned = false; + $allowed_keys = array('files', 'name', 'piece length', 'pieces', 'private', 'length', 'name.utf8', 'name.utf-8', + 'md5sum', 'sha1', 'source', 'file-duration', 'file-media'); + foreach ($this->data['info'] as $key => $value) { + if (!in_array($key, $allowed_keys)) { + unset($this->data['info'][$key]); + $cleaned = true; + } + } + + return $cleaned; + } + + /** + * Returns a bool on whether the private flag set to 1 within the info dictionary. + * + * @return bool + */ + public function isPrivate() { + $this->hasData(); + return isset($this->data['info']['private']) && $this->data['info']['private'] === 1; + } + + /** + * Sets the private flag (if not already set) in the info dictionary. Setting this to 1 makes it so a client + * will only publish its presence in the swarm via the tracker in the announce URL, else it'll be discoverable + * via other means such as PEX peer exchange or dht, which is a negative for security and privacy of a private + * swarm. Returns a bool on whether or not the flag was changed so that an appropriate screen can be shown to the + * user. + * + * @return bool + */ + public function makePrivate() { + $this->hasData(); + if ($this->isPrivate()) { + return false; + } + $this->data['info']['private'] = 1; + ksort($this->data['info']); + return true; + } + + /** + * Set the source flag in the info dictionary equal to $source. This can be used to ensure a unique info hash + * across sites so long as all sites use the source flag. This isn't an 'official' flag (no accepted BEP on it), + * but it has become the defacto standard with more clients supporting it natively. Returns a boolean on whether + * or not the source was changed so that an appropriate screen can be shown to the user. + * + * @param $source + * + * @return bool true if the source was set/changed, false if no change + */ + public function setSource($source) { + $this->hasData(); + if (isset($this->data['info']['source']) && $this->data['info']['source'] === $source) { + return false; + } + $this->data['info']['source'] = $source; + ksort($this->data['info']); + return true; + } + + public function setAnnounceUrl($announce_url) { + $this->hasData(); + $this->data['announce'] = $announce_url; + ksort($this->data); + } + + public function setComment($comment) { + $this->hasData(); + $this->data['comment'] = $comment; + ksort($this->data); + } + + /** + * Get a sha1 encoding of the BEncoded info dictionary + * @return string + */ + public function getInfoHash() { + $this->hasData(); + return sha1($this->encodeVal($this->data['info'])); + } + + /** + * @return string + */ + public function getName() { + if (isset($this->data['info']['name.utf-8'])) { + return $this->data['info']['name.utf-8']; + } + return $this->data['info']['name']; + } + + public function getSize() { + $cur_size = 0; + if (!isset($this->data['info']['files'])) { + $cur_size = $this->data['info']['length']; + } + else { + foreach ($this->data['info']['files'] as $file) { + $cur_size += $file['length']; + } + } + return $cur_size; + } + + public function getFileList() { + $files = []; + if (!isset($this->data['info']['files'])) { + // Single-file torrent + $name = (isset($this->data['info']['name.utf-8']) ? $this->data['info']['name.utf-8'] : $this->data['info']['name']); + $size = $this->data['info']['length']; + $files[] = array('name' => $name, 'size' => $size); + } + else { + $path_key = isset($this->data['info']['files'][0]['path.utf-8']) ? 'path.utf-8' : 'path'; + foreach ($this->data['info']['files'] as $file) { + $tmp_path = array(); + foreach ($file[$path_key] as $sub_path) { + $tmp_path[] = $sub_path; + } + $files[] = array('name' => implode('/', $tmp_path), 'size' => $file['length']); + } + uasort($files, function($a, $b) { + return strnatcasecmp($a['name'], $b['name']); + }); + } + return $files; + } + + public function getGazelleFileList() { + $files = []; + foreach ($this->getFileList() as $file) { + $name = $file['name']; + $size = $file['length']; + $name = BencodeTorrent::makeUTF8(strtr($name, "\n\r\t", ' ')); + $ext_pos = strrpos($name, '.'); + // Should not be $ExtPos !== false. Extensionless files that start with a . should not get extensions + $ext = ($ext_pos ? trim(substr($name, $ext_pos + 1)) : ''); + $files[] = sprintf("%s s%ds %s %s", ".$ext", $size, $name, BencodeTorrent::$utf8_filelist_delim); + } + return $files; + } + + private static function makeUTF8($Str) { + if ($Str != '') { + if (BencodeTorrent::isUTF8($Str)) { + $Encoding = 'UTF-8'; + } + if (empty($Encoding)) { + $Encoding = mb_detect_encoding($Str, 'UTF-8, ISO-8859-1'); + } + if (empty($Encoding)) { + $Encoding = 'ISO-8859-1'; + } + if ($Encoding == 'UTF-8') { + return $Str; + } + else { + return @mb_convert_encoding($Str, 'UTF-8', $Encoding); + } + } + } + + private static function isUTF8($Str) { + return preg_match('%^(?: + [\x09\x0A\x0D\x20-\x7E] // ASCII + | [\xC2-\xDF][\x80-\xBF] // non-overlong 2-byte + | \xE0[\xA0-\xBF][\x80-\xBF] // excluding overlongs + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} // straight 3-byte + | \xED[\x80-\x9F][\x80-\xBF] // excluding surrogates + | \xF0[\x90-\xBF][\x80-\xBF]{2} // planes 1-3 + | [\xF1-\xF3][\x80-\xBF]{3} // planes 4-15 + | \xF4[\x80-\x8F][\x80-\xBF]{2} // plane 16 + )*$%xs', $Str + ); + } +} diff --git a/tests/BencodeTorrentTest.php b/tests/BencodeTorrentTest.php new file mode 100644 index 0000000..29e27ef --- /dev/null +++ b/tests/BencodeTorrentTest.php @@ -0,0 +1,82 @@ +decodeFile(__DIR__.'/data/test_1.torrent'); + } + catch (\Exception $exc) { + $this->fail('Decode should not have thrown exception'); + } + $data = $bencode->getData(); + $this->assertEquals('https://localhost:34000/4f9587fbcb06fe09165e4f84d35d0403/announce', $data['announce']); + $this->assertEquals('https://localhost:8080/torrents.php?id=2&torrentid=2', $data['comment']); + $this->assertEquals('uTorrent/3.4.2', $data['created by']); + $this->assertEquals(1425699508, $data['creation date']); + $this->assertEquals('UTF-8', $data['encoding']); + $this->assertArrayHasKey('info', $data); + $this->assertCount(11, $data['info']['files']); + $files = [ + [ + 'length' => 12310347, + 'path' => ['02 Should have known better.mp3'] + ], + [ + 'length' => 12197480, + 'path' => ['09 John My Beloved.mp3'] + ], + [ + 'length' => 11367829, + 'path' => ['07 The Only Thing.mp3'] + ], + [ + 'length' => 11360526, + 'path' => ['11 Blue Bucket of Gold.mp3'] + ], + [ + 'length' => 11175567, + 'path' => ['06 Fourth of July.mp3'] + ], + [ + 'length' => 9584196, + 'path' => ['01 Death with Dignity.mp3'] + ], + [ + 'length' => 8871591, + 'path' => ['03 All of me wants all of you.mp3'] + ], + [ + 'length' => 7942661, + 'path' => ['04 Drawn to the Blood.mp3'] + ], + [ + 'length' => 7789055, + 'path' => ['08 Carrie & Lowell.mp3'] + ], + [ + 'length' => 6438044, + 'path' => ['10 No shade in the shadow of the cross.mp3'] + ], + [ + 'length' => 5878964, + 'path' => ['05 Eugene.mp3'] + ] + ]; + $this->assertEquals($files, $data['info']['files']); + $this->assertEquals('Sufjan Stevens - Carrie & Lowell (2015) [MP3 320]', $data['info']['name']); + $this->assertEquals('Sufjan Stevens - Carrie & Lowell (2015) [MP3 320]', $bencode->getName()); + $this->assertEquals(16020, strlen($data['info']['pieces'])); + $this->assertEquals(1, $data['info']['private']); + $this->assertEquals('APL', $data['info']['source']); + $this->assertStringEqualsFile(__DIR__.'/data/test_1.torrent', $bencode->getEncode()); + } + + public function testSetData() { + + } +} \ No newline at end of file diff --git a/tests/data/test_1.torrent b/tests/data/test_1.torrent new file mode 100644 index 0000000000000000000000000000000000000000..67c6a5d5635d45f237dd9b66bc5da8cdf6e52596 GIT binary patch literal 16992 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 literal 0 HcmV?d00001