/** * Manages handshaking with the client. * * If seeders_stop_seeding config key is set to a number greater than 0, * we check if we have at least N seeders beyond ourselves for the requested * torrent and if so, stop seeding (to spare bandwith). * * @throws PHPTracker_Seeder_Error_CloseConnection In case when the reqeust is invalid or we don't want or cannot serve the requested torrent. * @param PHPTracker_Seeder_Client $client */ protected function shakeHand(PHPTracker_Seeder_Client $client) { $protocol_length = unpack('C', $client->socketRead(1)); $protocol_length = current($protocol_length); if (($protocol = $client->socketRead($protocol_length)) !== self::PROTOCOL_STRING) { $this->logger->logError("Client tries to connect with unsupported protocol: " . substr($protocol, 0, 100) . ". Closing connection."); throw new PHPTracker_Seeder_Error_CloseConnection('Unsupported protocol.'); } // 8 reserved void bytes. $client->socketRead(8); $info_hash = $client->socketRead(20); $client->peer_id = $client->socketRead(20); $info_hash_readable = unpack('H*', $info_hash); $info_hash_readable = reset($info_hash_readable); $torrent = $this->persistence->getTorrent($info_hash); if (!isset($torrent)) { throw new PHPTracker_Seeder_Error_CloseConnection('Unknown info hash.'); } $client->torrent = $torrent; // If we have X other seeders already, we stop seeding on our own. if (0 < ($seeders_stop_seeding = $this->config->get('seeders_stop_seeding', false, 0))) { $stats = $this->persistence->getPeerStats($info_hash, $this->peer_id); if ($stats['complete'] >= $seeders_stop_seeding) { $this->logger->logMessage("External seeder limit ({$seeders_stop_seeding}) reached for info hash {$info_hash_readable}, stopping seeding."); throw new PHPTracker_Seeder_Error_CloseConnection('Stop seeding, we have others to seed.'); } } // Our handshake signal. $client->socketWrite(pack('C', strlen(self::PROTOCOL_STRING)) . self::PROTOCOL_STRING . pack('a8', '') . $info_hash . pack('a20', $this->peer_id)); $this->logger->logMessage("Handshake completed with peer {$client->peer_id} with address {$client->address}:{$client->port}, info hash: {$info_hash_readable}."); }
/** * Announce a peer to be tracked and return message to the client. * * This methods needs 'interval' key to be set in the config of the class * (not i the GET!). This is a number representing seconds for the client to * wait for the next announcement. * * Optional config key 'load_balancing' (ON by defailt) adds 10% dispersion * to the interval value to avoid possible announce peeks. * * @param PHPTracker_Config_Interface $get Config-like representation of the CGI parameters (aka. GET) sent. * @return string */ public function announce(PHPTracker_Config_Interface $get) { try { try { list($info_hash, $peer_id, $port, $uploaded, $downloaded, $left) = $get->getMulti(array('info_hash', 'peer_id', 'port', 'uploaded', 'downloaded', 'left'), true); } catch (PHPTracker_Config_Error_Missing $e) { return $this->announceFailure("Invalid get parameters; " . $e->getMessage()); } // IP address might be set explicitly in the GET. $ip = $get->get('ip', false, $this->config->get('ip')); $event = $get->get('event', false, ''); if (20 != strlen($info_hash)) { return $this->announceFailure("Invalid length of info_hash."); } if (20 != strlen($peer_id)) { return $this->announceFailure("Invalid length of info_hash."); } if (!(is_numeric($port) && is_int($port = $port + 0) && 0 <= $port)) { return $this->announceFailure("Invalid port value."); } if (!(is_numeric($uploaded) && is_int($uploaded = $uploaded + 0) && 0 <= $uploaded)) { return $this->announceFailure("Invalid uploaded value."); } if (!(is_numeric($downloaded) && is_int($downloaded = $downloaded + 0) && 0 <= $downloaded)) { return $this->announceFailure("Invalid downloaded value."); } if (!(is_numeric($left) && is_int($left = $left + 0) && 0 <= $left)) { return $this->announceFailure("Invalid left value."); } $interval = intval($this->config->get('interval')); $this->persistence->saveAnnounce($info_hash, $peer_id, $ip, $port, $downloaded, $uploaded, $left, 'completed' == $event ? 'complete' : null, 'stopped' == $event ? 0 : $interval * 2); $peers = $this->persistence->getPeers($info_hash, $peer_id, $get->get('compact', false, false), $get->get('no_peer_id', false, false)); $peer_stats = $this->persistence->getPeerStats($info_hash, $peer_id); if (true === $this->config->get('load_balancing', false, true)) { // Load balancing for tracker announcements. $interval = $interval + mt_rand(round($interval / -10), round($interval / 10)); } $announce_response = array('interval' => $interval, 'complete' => intval($peer_stats['complete']), 'incomplete' => intval($peer_stats['incomplete']), 'peers' => $peers); return PHPTracker_Bencode_Builder::build($announce_response); } catch (Exception $e) { trigger_error('Failure while announcing: ' . $e->getMessage(), E_USER_WARNING); return $this->announceFailure("Failed to announce because of internal server error."); } }