From e36830442c2e991041c6ab00cb625b8e13cb06b3 Mon Sep 17 00:00:00 2001 From: ghost Date: Mon, 28 Aug 2023 15:17:11 +0300 Subject: [PATCH] implement peers online scrape --- README.md | 6 +- database/yggtracker.mwb | Bin 25872 -> 27023 bytes example/environment/crontab | 4 +- src/config/app.php.example | 4 + src/crontab/scrape.php | 144 ++++ src/library/database.php | 125 +++- src/library/scrapeer.php | 692 ++++++++++++++++++ .../assets/theme/default/css/framework.css | 4 + src/public/index.php | 141 +++- 9 files changed, 1084 insertions(+), 36 deletions(-) create mode 100644 src/crontab/scrape.php create mode 100644 src/library/scrapeer.php diff --git a/README.md b/README.md index b15d674..1dba59d 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,14 @@ git checkout -b my-pr-branch-name + [x] Sensitive + [ ] Comments + [ ] Features + + [x] Scrape trackers + + [x] Peers + + [x] Completed + + [x] Leechers + [x] Stars + [x] Downloads + [ ] Comments + [ ] Views - + [ ] Peers + [ ] Info page * [ ] User @@ -96,6 +99,7 @@ git checkout -b my-pr-branch-name #### Components [Icons](https://icons.getbootstrap.com) +[PHP Scrapper](https://github.com/medariox/scrapeer) #### Feedback diff --git a/database/yggtracker.mwb b/database/yggtracker.mwb index 55567148096f06968da59d815c624410d4fc3758..c80cb797c1f6140326414101b2bfc447ca54af7a 100644 GIT binary patch literal 27023 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 literal 25872 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 diff --git a/example/environment/crontab b/example/environment/crontab index 0ec8c1d..f392715 100644 --- a/example/environment/crontab +++ b/example/environment/crontab @@ -1,4 +1,6 @@ @reboot searchd @reboot indexer --all --rotate -* * * * * indexer magnet --rotate \ No newline at end of file +* * * * * indexer magnet --rotate + +* * * * * /usr/bin/php /YGGtracker/src/crontab/scrape.php \ No newline at end of file diff --git a/src/config/app.php.example b/src/config/app.php.example index 2239bd5..2659267 100644 --- a/src/config/app.php.example +++ b/src/config/app.php.example @@ -100,3 +100,7 @@ define('TRACKER_LINKS', (object) // Yggdrasil define('YGGDRASIL_URL_REGEX', '/^0{0,1}[2-3][a-f0-9]{0,2}:/'); // thanks to @ygguser (https://github.com/YGGverse/YGGo/issues/1#issuecomment-1498182228 ) + +// Crawler +define('CRAWLER_SCRAPE_QUEUE_LIMIT', 1); +define('CRAWLER_SCRAPE_TIME_OFFLINE_TIMEOUT', 60*60*24); \ No newline at end of file diff --git a/src/crontab/scrape.php b/src/crontab/scrape.php new file mode 100644 index 0000000..14b9bda --- /dev/null +++ b/src/crontab/scrape.php @@ -0,0 +1,144 @@ + [ + 'ISO8601' => date('c'), + 'total' => microtime(true), + ], +]; + +// Connect DB +try { + + $db = new Database(DB_HOST, DB_PORT, DB_NAME, DB_USERNAME, DB_PASSWORD); + +} catch(Exception $e) { + + var_dump($e); + + exit; +} + +// Init Scraper +try { + + $scraper = new Scrapeer\Scraper(); + +} catch(Exception $e) { + + var_dump($e); + + exit; +} + +// Begin +try { + + $db->beginTransaction(); + + // Reset time offline by timeout + $db->resetMagnetToAddressTrackerTimeOfflineByTimeout( + CRAWLER_SCRAPE_TIME_OFFLINE_TIMEOUT + ); + + foreach ($db->getMagnetToAddressTrackerScrapeQueue(CRAWLER_SCRAPE_QUEUE_LIMIT) as $queue) + { + if ($addressTracker = $db->getAddressTracker($queue->addressTrackerId)) + { + // Build url + $scheme = $db->getScheme($addressTracker->schemeId); + $host = $db->getHost($addressTracker->hostId); + $port = $db->getPort($addressTracker->portId); + $uri = $db->getUri($addressTracker->uriId); + + $url = $port->value ? sprintf('%s://%s:%s%s', $scheme->value, + $host->value, + $port->value, + $uri->value) : sprintf('%s://%s%s', $scheme->value, + $host->value, + $uri->value); + + $hash = str_replace('urn:btih:', false, $db->getMagnet($queue->magnetId)->xt); + + if ($scrape = $scraper->scrape([$hash], [$url], null, 1)) + { + $db->updateMagnetToAddressTrackerTimeOffline( + $queue->magnetToAddressTrackerId, + null + ); + + if (isset($scrape[$hash]['seeders'])) + { + $db->updateMagnetToAddressTrackerSeeders( + $queue->magnetToAddressTrackerId, + (int) $scrape[$hash]['seeders'], + time() + ); + } + + if (isset($scrape[$hash]['completed'])) + { + $db->updateMagnetToAddressTrackerCompleted( + $queue->magnetToAddressTrackerId, + (int) $scrape[$hash]['completed'], + time() + ); + } + + if (isset($scrape[$hash]['leechers'])) + { + $db->updateMagnetToAddressTrackerLeechers( + $queue->magnetToAddressTrackerId, + (int) $scrape[$hash]['leechers'], + time() + ); + } + } + else + { + $db->updateMagnetToAddressTrackerTimeOffline( + $queue->magnetToAddressTrackerId, + time() + ); + } + } + } + + $db->commit(); + +} catch (EXception $e) { + + $db->rollback(); + + var_dump($e); +} + +// Debug output +$debug['time']['total'] = microtime(true) - $debug['time']['total']; + +print_r( + array_merge($debug, [ + 'db' => [ + 'total' => [ + 'select' => $db->getDebug()->query->select->total, + 'insert' => $db->getDebug()->query->insert->total, + 'update' => $db->getDebug()->query->update->total, + 'delete' => $db->getDebug()->query->delete->total, + ] + ] + ]) +); \ No newline at end of file diff --git a/src/library/database.php b/src/library/database.php index 9ce7bb4..cea50bf 100644 --- a/src/library/database.php +++ b/src/library/database.php @@ -653,6 +653,50 @@ class Database { return $this->_db->lastInsertId(); } + public function updateMagnetToAddressTrackerSeeders(int $magnetToAddressTrackerId, int $seeders, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `seeders` = ?, `timeUpdated` = ? WHERE `magnetToAddressTrackerId` = ?'); + + $query->execute([$seeders, $timeUpdated, $magnetToAddressTrackerId]); + + return $query->rowCount(); + } + + public function updateMagnetToAddressTrackerCompleted(int $magnetToAddressTrackerId, int $completed, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `completed` = ?, `timeUpdated` = ? WHERE `magnetToAddressTrackerId` = ?'); + + $query->execute([$completed, $timeUpdated, $magnetToAddressTrackerId]); + + return $query->rowCount(); + } + + public function updateMagnetToAddressTrackerLeechers(int $magnetToAddressTrackerId, int $leechers, int $timeUpdated) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `leechers` = ?, `timeUpdated` = ? WHERE `magnetToAddressTrackerId` = ?'); + + $query->execute([$leechers, $timeUpdated, $magnetToAddressTrackerId]); + + return $query->rowCount(); + } + + public function updateMagnetToAddressTrackerTimeOffline(int $magnetToAddressTrackerId, int $timeOffline) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `timeOffline` = ? WHERE `magnetToAddressTrackerId` = ?'); + + $query->execute([$timeOffline, $magnetToAddressTrackerId]); + + return $query->rowCount(); + } + public function deleteMagnetToAddressTrackerByMagnetId(int $magnetId) : int { $this->_debug->query->delete->total++; @@ -686,6 +730,38 @@ class Database { return $query->fetchAll(); } + public function getMagnetToAddressTrackerScrapeQueue(int $limit) { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT * FROM `magnetToAddressTracker` + + WHERE `timeOffline` IS NULL + + ORDER BY `timeUpdated` ASC, RAND() + + LIMIT ' . (int) $limit); + + $query->execute(); + + return $query->fetchAll(); + } + + public function resetMagnetToAddressTrackerTimeOfflineByTimeout(int $timeOffline) : int { + + $this->_debug->query->update->total++; + + $query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `timeOffline` = NULL WHERE `timeOffline` < ?'); + + $query->execute( + [ + time() - $timeOffline + ] + ); + + return $query->rowCount(); + } + public function initMagnetToAddressTrackerId(int $magnetId, int $addressTrackerId) : int { if ($result = $this->findMagnetToAddressTracker($magnetId, $addressTrackerId)) { @@ -942,28 +1018,17 @@ class Database { return $this->_db->lastInsertId(); } - public function getMagnetDownloadsTotal(int $magnetId) : int { + public function getMagnetDownloadsTotalByUserId(int $magnetId) : int { $this->_debug->query->select->total++; - $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetDownload` WHERE `magnetId` = ?'); + $query = $this->_db->prepare('SELECT COUNT(DISTINCT `userId`) AS `result` FROM `magnetDownload` WHERE `magnetId` = ?'); $query->execute([$magnetId]); return $query->fetch()->result; } - public function deleteMagnetDownloadByUserId(int $magnetId, int $userId) : int { - - $this->_debug->query->delete->total++; - - $query = $this->_db->prepare('DELETE FROM `magnetDownload` WHERE `magnetId` = ? AND `userId` = ?'); - - $query->execute([$magnetId, $userId]); - - return $query->rowCount(); - } - public function findMagnetDownloadsTotalByUserId(int $magnetId, int $userId) : int { $this->_debug->query->select->total++; @@ -974,4 +1039,38 @@ class Database { return $query->fetch()->result; } + + // Magnet view + public function addMagnetView(int $magnetId, int $userId, int $timeAdded) : int { + + $this->_debug->query->insert->total++; + + $query = $this->_db->prepare('INSERT INTO `magnetView` SET `magnetId` = ?, `userId` = ?, `timeAdded` = ?'); + + $query->execute([$magnetId, $userId, $timeAdded]); + + return $this->_db->lastInsertId(); + } + + public function getMagnetViewsTotal(int $magnetId) : int { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetView` WHERE `magnetId` = ?'); + + $query->execute([$magnetId]); + + return $query->fetch()->result; + } + + public function findMagnetViewsTotalByUserId(int $magnetId, int $userId) : int { + + $this->_debug->query->select->total++; + + $query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetView` WHERE `magnetId` = ? AND `userId` = ?'); + + $query->execute([$magnetId, $userId]); + + return $query->fetch()->result; + } } \ No newline at end of file diff --git a/src/library/scrapeer.php b/src/library/scrapeer.php new file mode 100644 index 0000000..800c71d --- /dev/null +++ b/src/library/scrapeer.php @@ -0,0 +1,692 @@ +1) or string of infohash(es). + * @param array|string $trackers List (>1) or string of tracker(s). + * @param int|null $max_trackers Optional. Maximum number of trackers to be scraped, Default all. + * @param int $timeout Optional. Maximum time for each tracker scrape in seconds, Default 2. + * @param bool $announce Optional. Use announce instead of scrape, Default false. + * @return array List of results. + */ + public function scrape( $hashes, $trackers, $max_trackers = null, $timeout = 2, $announce = false ) { + $final_result = array(); + + if ( empty( $trackers ) ) { + $this->errors[] = 'No tracker specified, aborting.'; + return $final_result; + } else if ( ! is_array( $trackers ) ) { + $trackers = array( $trackers ); + } + + if ( is_int( $timeout ) ) { + $this->timeout = $timeout; + } else { + $this->timeout = 2; + $this->errors[] = 'Timeout must be an integer. Using default value.'; + } + + try { + $this->infohashes = $this->normalize_infohashes( $hashes ); + } catch ( \RangeException $e ) { + $this->errors[] = $e->getMessage(); + return $final_result; + } + + $max_iterations = is_int( $max_trackers ) ? $max_trackers : count( $trackers ); + foreach ( $trackers as $index => $tracker ) { + if ( ! empty( $this->infohashes ) && $index < $max_iterations ) { + $info = parse_url( $tracker ); + $protocol = $info['scheme']; + $host = $info['host']; + if ( empty( $protocol ) || empty( $host ) ) { + $this->errors[] = 'Skipping invalid tracker (' . $tracker . ').'; + continue; + } + + $port = isset( $info['port'] ) ? $info['port'] : null; + $path = isset( $info['path'] ) ? $info['path'] : null; + $passkey = $this->get_passkey( $path ); + $result = $this->try_scrape( $protocol, $host, $port, $passkey, $announce ); + $final_result = array_merge( $final_result, $result ); + continue; + } + break; + } + return $final_result; + } + + /** + * Normalizes the given hashes + * + * @throws \RangeException If amount of valid infohashes > 64 or < 1. + * + * @param array $infohashes List of infohash(es). + * @return array Normalized infohash(es). + */ + private function normalize_infohashes( $infohashes ) { + if ( ! is_array( $infohashes ) ) { + $infohashes = array( $infohashes ); + } + + foreach ( $infohashes as $index => $infohash ) { + if ( ! preg_match( '/^[a-f0-9]{40}$/i', $infohash ) ) { + $this->errors[] = 'Invalid infohash skipped (' . $infohash . ').'; + unset( $infohashes[ $index ] ); + } + } + + $total_infohashes = count( $infohashes ); + if ( $total_infohashes > 64 || $total_infohashes < 1 ) { + throw new \RangeException( 'Invalid amount of valid infohashes (' . $total_infohashes . ').' ); + } + + $infohashes = array_values( $infohashes ); + + return $infohashes; + } + + /** + * Returns the passkey found in the scrape request. + * + * @param string $path Path from the scrape request. + * @return string Passkey or empty string. + */ + private function get_passkey( $path ) { + if ( ! is_null( $path ) && preg_match( '/[a-z0-9]{32}/i', $path, $matches ) ) { + return '/' . $matches[0]; + } + + return ''; + } + + /** + * Tries to scrape with a single tracker. + * + * @throws \Exception In case of unsupported protocol. + * + * @param string $protocol Protocol of the tracker. + * @param string $host Domain or address of the tracker. + * @param int $port Optional. Port number of the tracker. + * @param string $passkey Optional. Passkey provided in the scrape request. + * @param bool $announce Optional. Use announce instead of scrape, Default false. + * @return array List of results. + */ + private function try_scrape( $protocol, $host, $port, $passkey, $announce ) { + $infohashes = $this->infohashes; + $this->infohashes = array(); + $results = array(); + try { + switch ( $protocol ) { + case 'udp': + $port = isset( $port ) ? $port : 80; + $results = $this->scrape_udp( $infohashes, $host, $port, $announce ); + break; + case 'http': + $port = isset( $port ) ? $port : 80; + $results = $this->scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce ); + break; + case 'https': + $port = isset( $port ) ? $port : 443; + $results = $this->scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce ); + break; + default: + throw new \Exception( 'Unsupported protocol (' . $protocol . '://' . $host . ').' ); + } + } catch ( \Exception $e ) { + $this->infohashes = $infohashes; + $this->errors[] = $e->getMessage(); + } + return $results; + } + + /** + * Initiates the HTTP(S) scraping + * + * @param array|string $infohashes List (>1) or string of infohash(es). + * @param string $protocol Protocol to use for the scraping. + * @param string $host Domain or IP address of the tracker. + * @param int $port Optional. Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS). + * @param string $passkey Optional. Passkey provided in the scrape request. + * @param bool $announce Optional. Use announce instead of scrape, Default false. + * @return array List of results. + */ + private function scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce ) { + if ( true === $announce ) { + $response = $this->http_announce( $infohashes, $protocol, $host, $port, $passkey ); + } else { + $query = $this->http_query( $infohashes, $protocol, $host, $port, $passkey ); + $response = $this->http_request( $query, $host, $port ); + } + $results = $this->http_data( $response, $infohashes, $host ); + + return $results; + } + + /** + * Builds the HTTP(S) query + * + * @param array|string $infohashes List (>1) or string of infohash(es). + * @param string $protocol Protocol to use for the scraping. + * @param string $host Domain or IP address of the tracker. + * @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS). + * @param string $passkey Optional. Passkey provided in the scrape request. + * @return string Request query. + */ + private function http_query( $infohashes, $protocol, $host, $port, $passkey ) { + $tracker_url = $protocol . '://' . $host . ':' . $port . $passkey; + $scrape_query = ''; + + foreach ( $infohashes as $index => $infohash ) { + if ( $index > 0 ) { + $scrape_query .= '&info_hash=' . urlencode( pack( 'H*', $infohash ) ); + } else { + $scrape_query .= '/scrape?info_hash=' . urlencode( pack( 'H*', $infohash ) ); + } + } + $request_query = $tracker_url . $scrape_query; + + return $request_query; + } + + /** + * Executes the query and returns the result + * + * @throws \Exception If the connection can't be established. + * @throws \Exception If the response isn't valid. + * + * @param string $query The query that will be executed. + * @param string $host Domain or IP address of the tracker. + * @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS). + * @return string Request response. + */ + private function http_request( $query, $host, $port ) { + $context = stream_context_create( array( + 'http' => array( + 'timeout' => $this->timeout, + ), + )); + + if ( false === ( $response = @file_get_contents( $query, false, $context ) ) ) { + throw new \Exception( 'Invalid scrape connection (' . $host . ':' . $port . ').' ); + } + + if ( substr( $response, 0, 12 ) !== 'd5:filesd20:' ) { + throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' ); + } + + return $response; + } + + /** + * Builds the query, sends the announce request and returns the data + * + * @throws \Exception If the connection can't be established. + * + * @param array|string $infohashes List (>1) or string of infohash(es). + * @param string $protocol Protocol to use for the scraping. + * @param string $host Domain or IP address of the tracker. + * @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS). + * @param string $passkey Optional. Passkey provided in the scrape request. + * @return string Request response. + */ + private function http_announce( $infohashes, $protocol, $host, $port, $passkey ) { + $tracker_url = $protocol . '://' . $host . ':' . $port . $passkey; + $context = stream_context_create( array( + 'http' => array( + 'timeout' => $this->timeout, + ), + )); + + $response_data = ''; + foreach ( $infohashes as $infohash ) { + $query = $tracker_url . '/announce?info_hash=' . urlencode( pack( 'H*', $infohash ) ); + if ( false === ( $response = @file_get_contents( $query, false, $context ) ) ) { + throw new \Exception( 'Invalid announce connection (' . $host . ':' . $port . ').' ); + } + + if ( substr( $response, 0, 12 ) !== 'd8:completei' || + substr( $response, 0, 46 ) === 'd8:completei0e10:downloadedi0e10:incompletei1e' ) { + continue; + } + + $ben_hash = '20:' . pack( 'H*', $infohash ) . 'd'; + $response_data .= $ben_hash . $response; + } + + return $response_data; + } + + /** + * Parses the response and returns the data + * + * @param string $response The response that will be parsed. + * @param array $infohashes List of infohash(es). + * @param string $host Domain or IP address of the tracker. + * @return array Parsed data. + */ + private function http_data( $response, $infohashes, $host ) { + $torrents_data = array(); + + foreach ( $infohashes as $infohash ) { + $ben_hash = '20:' . pack( 'H*', $infohash ) . 'd'; + $start_pos = strpos( $response, $ben_hash ); + if ( false !== $start_pos ) { + $start = $start_pos + 24; + $head = substr( $response, $start ); + $end = strpos( $head, 'ee' ) + 1; + $data = substr( $response, $start, $end ); + + $seeders = '8:completei'; + $torrent_info['seeders'] = $this->get_information( $data, $seeders, 'e' ); + + $completed = '10:downloadedi'; + $torrent_info['completed'] = $this->get_information( $data, $completed, 'e' ); + + $leechers = '10:incompletei'; + $torrent_info['leechers'] = $this->get_information( $data, $leechers, 'e' ); + + $torrents_data[ $infohash ] = $torrent_info; + } else { + $this->collect_infohash( $infohash ); + $this->errors[] = 'Invalid infohash (' . $infohash . ') for tracker: ' . $host . '.'; + } + } + + return $torrents_data; + } + + /** + * Parses a string and returns the data between $start and $end. + * + * @param string $data The data that will be parsed. + * @param string $start Beginning part of the data. + * @param string $end Ending part of the data. + * @return int Parsed information or 0. + */ + private function get_information( $data, $start, $end ) { + $start_pos = strpos( $data, $start ); + if ( false !== $start_pos ) { + $start = $start_pos + strlen( $start ); + $head = substr( $data, $start ); + $end = strpos( $head, $end ); + $information = substr( $data, $start, $end ); + + return (int) $information; + } + + return 0; + } + + /** + * Initiates the UDP scraping + * + * @param array|string $infohashes List (>1) or string of infohash(es). + * @param string $host Domain or IP address of the tracker. + * @param int $port Optional. Port number of the tracker, Default 80. + * @param bool $announce Optional. Use announce instead of scrape, Default false. + * @return array List of results. + */ + private function scrape_udp( $infohashes, $host, $port, $announce ) { + list( $socket, $transaction_id, $connection_id ) = $this->prepare_udp( $host, $port ); + + if ( true === $announce ) { + $response = $this->udp_announce( $socket, $infohashes, $connection_id ); + $keys = 'Nleechers/Nseeders'; + $start = 12; + $end = 16; + $offset = 20; + } else { + $response = $this->udp_scrape( $socket, $infohashes, $connection_id, $transaction_id, $host, $port ); + $keys = 'Nseeders/Ncompleted/Nleechers'; + $start = 8; + $end = $offset = 12; + } + $results = $this->udp_scrape_data( $response, $infohashes, $host, $keys, $start, $end, $offset ); + + return $results; + } + + /** + * Prepares the UDP connection + * + * @param string $host Domain or IP address of the tracker. + * @param int $port Optional. Port number of the tracker, Default 80. + * @return array Created socket, transaction ID and connection ID. + */ + private function prepare_udp( $host, $port ) { + $socket = $this->udp_create_connection( $host, $port ); + $transaction_id = $this->udp_connection_request( $socket ); + $connection_id = $this->udp_connection_response( $socket, $transaction_id, $host, $port ); + + return array( $socket, $transaction_id, $connection_id ); + } + + /** + * Creates the UDP socket and establishes the connection + * + * @throws \Exception If the socket couldn't be created or connected to. + * + * @param string $host Domain or IP address of the tracker. + * @param int $port Port number of the tracker, Default 80. + * @return resource $socket Created and connected socket. + */ + private function udp_create_connection( $host, $port ) { + if ( false === ( $socket = @socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ) ) ) { + throw new \Exception( "Couldn't create socket." ); + } + + $timeout = $this->timeout; + socket_set_option( $socket, SOL_SOCKET, SO_RCVTIMEO, array( 'sec' => $timeout, 'usec' => 0 ) ); + socket_set_option( $socket, SOL_SOCKET, SO_SNDTIMEO, array( 'sec' => $timeout, 'usec' => 0 ) ); + if ( false === @socket_connect( $socket, $host, $port ) ) { + throw new \Exception( "Couldn't connect to socket." ); + } + + return $socket; + } + + /** + * Writes to the connected socket and returns the transaction ID + * + * @throws \Exception If the socket couldn't be written to. + * + * @param resource $socket The socket resource. + * @return int The transaction ID. + */ + private function udp_connection_request( $socket ) { + $connection_id = "\x00\x00\x04\x17\x27\x10\x19\x80"; + $action = pack( 'N', 0 ); + $transaction_id = mt_rand( 0, 2147483647 ); + $buffer = $connection_id . $action . pack( 'N', $transaction_id ); + if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) { + socket_close( $socket ); + throw new \Exception( "Couldn't write to socket." ); + } + + return $transaction_id; + } + + /** + * Reads the connection response and returns the connection ID + * + * @throws \Exception If anything fails with the scraping. + * + * @param resource $socket The socket resource. + * @param int $transaction_id The transaction ID. + * @param string $host Domain or IP address of the tracker. + * @param int $port Port number of the tracker, Default 80. + * @return string The connection ID. + */ + private function udp_connection_response( $socket, $transaction_id, $host, $port ) { + if ( false === ( $response = @socket_read( $socket, 16 ) ) ) { + socket_close( $socket ); + throw new \Exception( 'Invalid scrape connection! (' . $host . ':' . $port . ').' ); + } + + if ( strlen( $response ) < 16 ) { + socket_close( $socket ); + throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' ); + } + + $result = unpack( 'Naction/Ntransaction_id', $response ); + if ( 0 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) { + socket_close( $socket ); + throw new \Exception( 'Invalid scrape result (' . $host . ':' . $port . ').' ); + } + + $connection_id = substr( $response, 8, 8 ); + + return $connection_id; + } + + /** + * Reads the socket response and returns the torrent data + * + * @throws \Exception If anything fails while reading the response. + * + * @param resource $socket The socket resource. + * @param array $hashes List (>1) or string of infohash(es). + * @param string $connection_id The connection ID. + * @param int $transaction_id The transaction ID. + * @param string $host Domain or IP address of the tracker. + * @param int $port Port number of the tracker, Default 80. + * @return string Response data. + */ + private function udp_scrape( $socket, $hashes, $connection_id, $transaction_id, $host, $port ) { + $this->udp_scrape_request( $socket, $hashes, $connection_id, $transaction_id ); + + $read_length = 8 + ( 12 * count( $hashes ) ); + if ( false === ( $response = @socket_read( $socket, $read_length ) ) ) { + socket_close( $socket ); + throw new \Exception( 'Invalid scrape connection (' . $host . ':' . $port . ').' ); + } + socket_close( $socket ); + + if ( strlen( $response ) < $read_length ) { + throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' ); + } + + $result = unpack( 'Naction/Ntransaction_id', $response ); + if ( 2 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) { + throw new \Exception( 'Invalid scrape result (' . $host . ':' . $port . ').' ); + } + + return $response; + } + + /** + * Writes to the connected socket + * + * @throws \Exception If the socket couldn't be written to. + * + * @param resource $socket The socket resource. + * @param array $hashes List (>1) or string of infohash(es). + * @param string $connection_id The connection ID. + * @param int $transaction_id The transaction ID. + */ + private function udp_scrape_request( $socket, $hashes, $connection_id, $transaction_id ) { + $action = pack( 'N', 2 ); + + $infohashes = ''; + foreach ( $hashes as $infohash ) { + $infohashes .= pack( 'H*', $infohash ); + } + + $buffer = $connection_id . $action . pack( 'N', $transaction_id ) . $infohashes; + if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) { + socket_close( $socket ); + throw new \Exception( "Couldn't write to socket." ); + } + } + + /** + * Writes the announce to the connected socket + * + * @throws \Exception If the socket couldn't be written to. + * + * @param resource $socket The socket resource. + * @param array $hashes List (>1) or string of infohash(es). + * @param string $connection_id The connection ID. + * @return string Torrent(s) data. + */ + private function udp_announce( $socket, $hashes, $connection_id ) { + $action = pack( 'N', 1 ); + $downloaded = $left = $uploaded = "\x30\x30\x30\x30\x30\x30\x30\x30"; + $peer_id = $this->random_peer_id(); + $event = pack( 'N', 3 ); + $ip_addr = pack( 'N', 0 ); + $key = pack( 'N', mt_rand( 0, 2147483647 ) ); + $num_want = -1; + $ann_port = pack( 'N', mt_rand( 0, 255 ) ); + + $response_data = ''; + foreach ( $hashes as $infohash ) { + $transaction_id = mt_rand( 0, 2147483647 ); + $buffer = $connection_id . $action . pack( 'N', $transaction_id ) . pack( 'H*', $infohash ) . + $peer_id . $downloaded . $left . $uploaded . $event . $ip_addr . $key . $num_want . $ann_port; + + if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) { + socket_close( $socket ); + throw new \Exception( "Couldn't write announce to socket." ); + } + + $response = $this->udp_verify_announce( $socket, $transaction_id ); + if ( false === $response ) { + continue; + } + + $response_data .= $response; + } + socket_close( $socket ); + + return $response_data; + } + + /** + * Generates a random peer ID + * + * @return string Generated peer ID. + */ + private function random_peer_id() { + $identifier = '-SP0054-'; + $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + $peer_id = $identifier . substr( str_shuffle( $chars ), 0, 12 ); + + return $peer_id; + } + + /** + * Verifies the correctness of the announce response + * + * @param resource $socket The socket resource. + * @param int $transaction_id The transaction ID. + * @return string Response data. + */ + private function udp_verify_announce( $socket, $transaction_id ) { + if ( false === ( $response = @socket_read( $socket, 20 ) ) ) { + return false; + } + + if ( strlen( $response ) < 20 ) { + return false; + } + + $result = unpack( 'Naction/Ntransaction_id', $response ); + if ( 1 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) { + return false; + } + + return $response; + } + + /** + * Reads the socket response and returns the torrent data + * + * @param string $response Data from the request response. + * @param array $hashes List (>1) or string of infohash(es). + * @param string $host Domain or IP address of the tracker. + * @param string $keys Keys for the unpacked information. + * @param int $start Start of the content we want to unpack. + * @param int $end End of the content we want to unpack. + * @param int $offset Offset to the next content part. + * @return array Scraped torrent data. + */ + private function udp_scrape_data( $response, $hashes, $host, $keys, $start, $end, $offset ) { + $torrents_data = array(); + + foreach ( $hashes as $infohash ) { + $byte_string = substr( $response, $start, $end ); + $data = unpack( 'N', $byte_string ); + $content = $data[1]; + if ( ! empty( $content ) ) { + $results = unpack( $keys, $byte_string ); + $torrents_data[ $infohash ] = $results; + } else { + $this->collect_infohash( $infohash ); + $this->errors[] = 'Invalid infohash (' . $infohash . ') for tracker: ' . $host . '.'; + } + $start += $offset; + } + + return $torrents_data; + } + + /** + * Collects info-hashes that couldn't be scraped. + * + * @param string $infohash Infohash that wasn't scraped. + */ + private function collect_infohash( $infohash ) { + $this->infohashes[] = $infohash; + } + + /** + * Checks if there are any errors + * + * @return bool True or false, depending if errors are present or not. + */ + public function has_errors() { + return ! empty( $this->errors ); + } + + /** + * Returns all the errors that were logged + * + * @return array All the logged errors. + */ + public function get_errors() { + return $this->errors; + } +} diff --git a/src/public/assets/theme/default/css/framework.css b/src/public/assets/theme/default/css/framework.css index 0146324..c48f7c6 100644 --- a/src/public/assets/theme/default/css/framework.css +++ b/src/public/assets/theme/default/css/framework.css @@ -129,6 +129,10 @@ padding: 4px; } +.padding-y-4 { + padding-top: 4px; + padding-bottom: 4px; +} .padding-x-4 { padding-left: 4px; padding-right: 4px; diff --git a/src/public/index.php b/src/public/index.php index f5f8d16..36a4f3d 100644 --- a/src/public/index.php +++ b/src/public/index.php @@ -88,6 +88,17 @@ else { if ($magnet = $db->getMagnet($result->magnetid)) { + // Get access info + $accessRead = ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved)); + $accessEdit = ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST)); + + // Update magnet viwed + if ($accessRead) + { + $db->addMagnetView($magnet->magnetId, $userId, time()); + } + + // Keywords $keywords = []; foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $keyword) @@ -95,6 +106,57 @@ else $keywords[] = $db->getKeywordTopic($keyword->keywordTopicId)->value; } + // Scrapes + $localScrape = (object) + [ + 'seeders' => 0, + 'completed' => 0, + 'leechers' => 0, + ]; + + $totalScrape = (object) + [ + 'seeders' => 0, + 'completed' => 0, + 'leechers' => 0, + ]; + + $trackers = []; + + foreach (TRACKER_LINKS as $tracker) + { + $trackers[] = $tracker->announce; + } + + foreach ($db->findAddressTrackerByMagnetId($magnet->magnetId) as $magnetToAddressTracker) + { + if ($addressTracker = $db->getAddressTracker($magnetToAddressTracker->addressTrackerId)) + { + $scheme = $db->getScheme($addressTracker->schemeId); + $host = $db->getHost($addressTracker->hostId); + $port = $db->getPort($addressTracker->portId); + $uri = $db->getUri($addressTracker->uriId); + + $url = $port->value ? sprintf('%s://%s:%s%s', $scheme->value, + $host->value, + $port->value, + $uri->value) : sprintf('%s://%s%s', $scheme->value, + $host->value, + $uri->value); + + if (in_array($url, $trackers)) + { + $localScrape->seeders += (int) $magnetToAddressTracker->seeders; + $localScrape->completed += (int) $magnetToAddressTracker->completed; + $localScrape->leechers += (int) $magnetToAddressTracker->leechers; + } + + $totalScrape->seeders += (int) $magnetToAddressTracker->seeders; + $totalScrape->completed += (int) $magnetToAddressTracker->completed; + $totalScrape->leechers += (int) $magnetToAddressTracker->leechers; + } + } + $response->magnets[] = (object) [ 'magnetId' => $magnet->magnetId, @@ -128,9 +190,14 @@ else ], 'access' => (object) [ - 'read' => ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved)), - 'edit' => ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST)), + 'read' => $accessRead, + 'edit' => $accessEdit, ], + 'scrape' => (object) + [ + 'local' => $localScrape, + 'total' => $totalScrape + ] ]; } } @@ -198,8 +265,23 @@ echo '' . PHP_EOL ?> public || !$magnet->approved ? 'opacity-06 opacity-hover-1' : false ?>">
-

metaTitle ?>

+

metaTitle ?>

+ public) { ?> + + + + + + + + approved) { ?> + + + + + + access->edit) { ?> @@ -229,24 +311,41 @@ echo '' . PHP_EOL ?>
-
-