2022-03-17 18:46:49 +08:00
< ? php
2022-03-19 14:55:43 +08:00
/**
* Handle announce and scrape
*
* @ link https :// github . com / HDInnovations / UNIT3D - Community - Edition / blob / master / app / Http / Controllers / AnnounceController . php
* @ link https :// github . com / Rhilip / RidPT / blob / master / application / Controllers / Tracker / AnnounceController . php
*/
2022-03-17 18:46:49 +08:00
namespace App\Repositories ;
use App\Exceptions\ClientNotAllowedException ;
use App\Models\Cheater ;
use App\Models\Peer ;
use App\Models\Setting ;
use App\Models\Snatch ;
use App\Models\Torrent ;
use App\Models\User ;
use Carbon\Carbon ;
use Illuminate\Database\Eloquent\Model ;
use Illuminate\Http\Request ;
use App\Exceptions\TrackerException ;
2022-03-18 15:44:04 +08:00
use Illuminate\Support\Arr ;
2022-03-19 14:55:43 +08:00
use Illuminate\Support\Facades\Cache ;
2022-03-17 18:46:49 +08:00
use Illuminate\Support\Facades\DB ;
use Illuminate\Support\Facades\Redis ;
use Rhilip\Bencode\Bencode ;
class TrackerRepository extends BaseRepository
{
2022-03-19 14:55:43 +08:00
const MIN_ANNOUNCE_WAIT_SECOND = 300 ;
2022-03-17 18:46:49 +08:00
const MAX_PEER_NUM_WANT = 50 ;
const MUST_BE_CHEATER_SPEED = 1024 * 1024 * 1024 ; //1024 MB/s
const MAY_BE_CHEATER_SPEED = 1024 * 1024 * 100 ; //100 MB/s
// Port Blacklist
protected const BLACK_PORTS = [
22 , // SSH Port
53 , // DNS queries
80 , 81 , 8080 , 8081 , // Hyper Text Transfer Protocol (HTTP) - port used for web traffic
411 , 412 , 413 , // Direct Connect Hub (unofficial)
443 , // HTTPS / SSL - encrypted web traffic, also used for VPN tunnels over HTTPS.
1214 , // Kazaa - peer-to-peer file sharing, some known vulnerabilities, and at least one worm (Benjamin) targeting it.
3389 , // IANA registered for Microsoft WBT Server, used for Windows Remote Desktop and Remote Assistance connections
4662 , // eDonkey 2000 P2P file sharing service. http://www.edonkey2000.com/
6346 , 6347 , // Gnutella (FrostWire, Limewire, Shareaza, etc.), BearShare file sharing app
6699 , // Port used by p2p software, such as WinMX, Napster.
];
2022-03-18 15:44:04 +08:00
private array $userUpdates = [];
2022-03-17 18:46:49 +08:00
public function announce ( Request $request ) : \Illuminate\Http\Response
{
2022-03-18 15:44:04 +08:00
do_log ( nexus_json_encode ( $_SERVER ));
2022-03-17 18:46:49 +08:00
try {
$withPeers = false ;
$queries = $this -> checkAnnounceFields ( $request );
2022-03-18 15:44:04 +08:00
do_log ( " [QUERIES] " . json_encode ( Arr :: only ( $queries , [ 'ip' , 'user_agent' , 'uploaded' , 'downloaded' , 'left' , 'event' ])), 'debug' );
2022-03-17 18:46:49 +08:00
$user = $this -> checkUser ( $request );
2022-03-18 15:44:04 +08:00
$clientAllow = $this -> checkClient ( $request );
2022-03-17 18:46:49 +08:00
$torrent = $this -> checkTorrent ( $queries , $user );
if ( $this -> isReAnnounce ( $queries ) === false ) {
$withPeers = true ;
$peerSelf = $this -> checkMinInterval ( $torrent , $queries , $user );
if ( ! $peerSelf ) {
$this -> checkPeer ( $torrent , $queries , $user );
$this -> checkPermission ( $torrent , $queries , $user );
$peerSelf = new Peer ([
'torrent' => $torrent -> id ,
'peer_id' => $queries [ 'peer_id' ],
'userid' => $user -> id ,
'passkey' => $user -> passkey ,
]);
} else {
$this -> checkCheater ( $torrent , $queries , $user , $peerSelf );
}
2022-03-18 15:44:04 +08:00
/**
* Note : Must get before update peer !
*/
$dataTraffic = $this -> getDataTraffic ( $torrent , $queries , $user , $peerSelf );
2022-03-17 18:46:49 +08:00
$this -> updatePeer ( $peerSelf , $queries );
2022-03-18 15:44:04 +08:00
$this -> updateSnatch ( $peerSelf , $queries , $dataTraffic );
2022-03-17 18:46:49 +08:00
$this -> updateTorrent ( $torrent , $queries );
2022-03-18 19:59:27 +08:00
if ( $dataTraffic [ 'uploaded_increment_for_user' ] > 0 ) {
$this -> userUpdates [ 'uploaded' ] = DB :: raw ( 'uploaded + ' . $dataTraffic [ 'uploaded_increment_for_user' ]);
}
if ( $dataTraffic [ 'downloaded_increment_for_user' ] > 0 ) {
$this -> userUpdates [ 'downloaded' ] = DB :: raw ( 'downloaded + ' . $dataTraffic [ 'downloaded_increment_for_user' ]);
}
if ( $user -> clientselect != $clientAllow -> id ) {
$this -> userUpdates [ 'clientselect' ] = $clientAllow -> id ;
}
if ( $user -> showclienterror == 'yes' ) {
$this -> userUpdates [ 'showclienterror' ] = 'no' ;
}
2022-03-17 18:46:49 +08:00
}
$repDict = $this -> generateSuccessAnnounceResponse ( $torrent , $queries , $user , $withPeers );
} catch ( ClientNotAllowedException $exception ) {
do_log ( " [ClientNotAllowedException] " . $exception -> getMessage ());
2022-03-18 19:59:27 +08:00
if ( isset ( $user ) && $user -> showclienterror == 'no' ) {
$this -> userUpdates [ 'showclienterror' ] = 'yes' ;
}
2022-03-18 15:44:04 +08:00
$repDict = $this -> generateFailedAnnounceResponse ( $exception -> getMessage ());
2022-03-17 18:46:49 +08:00
} catch ( TrackerException $exception ) {
2022-03-18 15:44:04 +08:00
$repDict = $this -> generateFailedAnnounceResponse ( $exception -> getMessage ());
} catch ( \Exception $exception ) {
//other system exception
do_log ( " [ " . get_class ( $exception ) . " ] " . $exception -> getMessage () . " \n " . $exception -> getTraceAsString (), 'error' );
$repDict = $this -> generateFailedAnnounceResponse ( " system error, report to sysop please, hint: " . REQUEST_ID );
2022-03-17 18:46:49 +08:00
} finally {
2022-03-18 15:44:04 +08:00
if ( isset ( $user ) && count ( $this -> userUpdates )) {
2022-03-17 18:46:49 +08:00
$user -> update ( $this -> userUpdates );
2022-03-18 15:44:04 +08:00
do_log ( last_query (), 'debug' );
2022-03-17 18:46:49 +08:00
}
return $this -> sendFinalAnnounceResponse ( $repDict );
}
}
/**
* @ param Request $request
* @ throws ClientNotAllowedException
* @ throws TrackerException
2022-03-18 19:59:27 +08:00
* @ refs
2022-03-17 18:46:49 +08:00
*/
protected function checkClient ( Request $request )
{
// Miss Header User-Agent is not allowed.
if ( ! $request -> header ( 'User-Agent' )) {
throw new TrackerException ( 'Invalid user-agent !' );
}
// Block Other Browser, Crawler (May Cheater or Faker Client) by check Requests headers
if ( $request -> header ( 'accept-language' ) || $request -> header ( 'referer' )
|| $request -> header ( 'accept-charset' )
/**
* This header check may block Non - bittorrent client `Aria2` to access tracker ,
* Because they always add this header which other clients don ' t have .
*
* @ see https :// blog . rhilip . info / archives / 1010 / ( in Chinese )
*/
|| $request -> header ( 'want-digest' )
) {
throw new TrackerException ( 'Abnormal access blocked !' );
}
$userAgent = $request -> header ( 'User-Agent' );
// Should also block User-Agent strings that are to long. (For Database reasons)
if ( \strlen (( string ) $userAgent ) > 64 ) {
throw new TrackerException ( 'The User-Agent of this client is too long!' );
}
// Block Browser by checking it's User-Agent
if ( \preg_match ( '/(Mozilla|Browser|Chrome|Safari|AppleWebKit|Opera|Links|Lynx|Bot|Unknown)/i' , ( string ) $userAgent )) {
throw new TrackerException ( 'Browser, Crawler or Cheater is not Allowed.' );
}
$agentAllowRep = new AgentAllowRepository ();
2022-03-18 15:44:04 +08:00
return $agentAllowRep -> checkClient ( $request -> peer_id , $userAgent , config ( 'app.debug' ));
2022-03-17 18:46:49 +08:00
}
protected function checkPasskey ( $passkey )
{
// If Passkey Lenght Is Wrong
if ( \strlen (( string ) $passkey ) !== 32 ) {
throw new TrackerException ( 'Invalid passkey ! the length of passkey must be 32' );
}
// If Passkey Format Is Wrong
if ( \strspn ( \strtolower ( $passkey ), 'abcdef0123456789' ) !== 32 ) { // MD5 char limit
throw new TrackerException ( " Invalid passkey ! The format of passkey is not correct " );
}
}
protected function checkAuthkey ( $authkey )
{
$arr = explode ( '|' , $authkey );
if ( count ( $arr ) != 3 ) {
throw new TrackerException ( 'Invalid authkey' );
}
$torrentId = $arr [ 0 ];
$uid = $arr [ 1 ];
$torrentRep = new TorrentRepository ();
try {
$decrypted = $torrentRep -> checkTrackerReportAuthKey ( $authkey );
} catch ( \Exception $exception ) {
throw new TrackerException ( $exception -> getMessage ());
}
if ( empty ( $decrypted )) {
throw new TrackerException ( 'Invalid authkey' );
}
return compact ( 'torrentId' , 'uid' );
}
2022-03-18 19:59:27 +08:00
/**
* @ param Request $request
* @ return array
* @ throws TrackerException
*/
2022-03-17 18:46:49 +08:00
protected function checkAnnounceFields ( Request $request ) : array
{
$queries = [];
// Part.1 check Announce **Need** Fields
foreach ([ 'info_hash' , 'peer_id' , 'port' , 'uploaded' , 'downloaded' , 'left' ] as $item ) {
$itemData = $request -> query -> get ( $item );
if ( ! \is_null ( $itemData )) {
$queries [ $item ] = $itemData ;
} else {
throw new TrackerException ( " key: $item is Missing ! " );
}
}
foreach ([ 'info_hash' , 'peer_id' ] as $item ) {
if ( \strlen (( string ) $queries [ $item ]) !== 20 ) {
throw new TrackerException ( " Invalid $item ! $item is not 20 bytes long " );
}
}
foreach ([ 'uploaded' , 'downloaded' , 'left' ] as $item ) {
$itemData = $queries [ $item ];
if ( ! \is_numeric ( $itemData ) || $itemData < 0 ) {
throw new TrackerException ( " Invalid $item ! $item Must be a number greater than or equal to 0 " );
}
}
// Part.2 check Announce **Option** Fields
foreach ([ 'event' => '' , 'no_peer_id' => 1 , 'compact' => 0 , 'numwant' => 50 , 'corrupt' => 0 , 'key' => '' ] as $item => $value ) {
$queries [ $item ] = $request -> query -> get ( $item , $value );
if ( $queries [ $item ] && $item == 'event' ) {
$queries [ $item ] = strtolower ( $queries [ $item ]);
}
}
foreach ([ 'numwant' , 'corrupt' , 'no_peer_id' , 'compact' ] as $item ) {
if ( ! \is_numeric ( $queries [ $item ]) || $queries [ $item ] < 0 ) {
throw new TrackerException ( " Invalid $item ! $item Must be a number greater than or equal to 0 " );
}
}
if ( ! \in_array ( \strtolower ( $queries [ 'event' ]), [ 'started' , 'completed' , 'stopped' , 'paused' , '' ])) {
throw new TrackerException ( " Unsupported Event type { $queries [ 'event' ] } . " );
}
// Part.3 check Port is Valid and Allowed
/**
* Normally , the port must in 1 - 65535 , that is ( $port > 0 && $port < 0xffff )
* However , in some case , When `&event=stopped` the port may set to 0.
*/
if ( $queries [ 'port' ] === 0 && \strtolower ( $queries [ 'event' ]) !== 'stopped' ) {
throw new TrackerException ( " Illegal port 0 under Event type { $queries [ 'event' ] } . " );
}
if ( ! \is_numeric ( $queries [ 'port' ]) || $queries [ 'port' ] < 0 || $queries [ 'port' ] > 0xFFFF || \in_array ( $queries [ 'port' ], self :: BLACK_PORTS ,
true )) {
throw new TrackerException ( " Illegal port { $queries [ 'port' ] } . Port should between 6881-64999 " );
}
// Part.4 Get User Ip Address
$queries [ 'ip' ] = $request -> getClientIp ();
// Part.5 Get Users Agent
$queries [ 'user_agent' ] = $request -> headers -> get ( 'user-agent' );
2022-03-18 15:44:04 +08:00
// Part.6 info_hash, binary
$queries [ 'info_hash' ] = $queries [ 'info_hash' ];
2022-03-17 18:46:49 +08:00
2022-03-18 15:44:04 +08:00
// Part.7
$queries [ 'peer_id' ] = $queries [ 'peer_id' ];
2022-03-17 18:46:49 +08:00
return $queries ;
}
protected function checkUser ( Request $request )
{
if ( $authkey = $request -> query -> get ( 'authkey' )) {
2022-03-18 15:44:04 +08:00
$checkResult = $this -> checkAuthkey ( $authkey );
2022-03-17 18:46:49 +08:00
$field = 'id' ;
2022-03-18 15:44:04 +08:00
$value = $checkResult [ 'uid' ];
2022-03-17 18:46:49 +08:00
} elseif ( $passkey = $request -> query -> get ( 'passkey' )) {
$this -> checkPasskey ( $passkey );
$field = 'passkey' ;
$value = $passkey ;
} else {
throw new TrackerException ( " Require authkey or passkey. " );
}
/**
* @ var $user User
*/
$user = User :: query () -> where ( $field , $value ) -> first ();
if ( ! $user ) {
throw new TrackerException ( " Invalid $field : $value . " );
}
$user -> checkIsNormal ();
if ( $user -> parked == 'yes' ) {
throw new TrackerException ( " Your account is parked! (Read the FAQ) " );
}
if ( $user -> downloadpos == 'no' ) {
throw new TrackerException ( " Your downloading privilege have been disabled! (Read the rules) " );
}
return $user ;
}
protected function checkTorrent ( $queries , User $user )
{
// Check Info Hash Against Torrents Table
2022-03-19 14:55:43 +08:00
$torrent = $this -> getTorrentByInfoHash ( $queries [ 'info_hash' ]);
2022-03-17 18:46:49 +08:00
// If Torrent Doesnt Exists Return Error to Client
if ( $torrent === null ) {
throw new TrackerException ( 'Torrent not registered with this tracker.' );
}
if ( $torrent -> banned == 'yes' && $user -> class < Setting :: get ( 'authority.seebanned' )) {
throw new TrackerException ( " torrent banned " );
}
return $torrent ;
}
protected function checkPeer ( Torrent $torrent , array $queries , User $user ) : void
{
if ( $queries [ 'event' ] === 'completed' ) {
throw new TrackerException ( " Torrent being announced as complete but no record found. " );
}
$counts = Peer :: query ()
-> where ( 'torrent' , '=' , $torrent -> id )
-> where ( 'userid' , $user -> id )
-> count ();
if ( $queries [ 'left' ] == 0 && $counts >= 3 ) {
throw new TrackerException ( " You cannot seed the same torrent from more than 3 locations. " );
}
if ( $queries [ 'left' ] > 0 && $counts >= 1 ) {
throw new TrackerException ( " You already are downloading the same torrent. You may only leech from one location at a time. " );
}
}
protected function checkPermission ( Torrent $torrent , $queries , User $user )
{
if ( $user -> class >= User :: CLASS_VIP ) {
return ;
}
$gigs = $user -> downloaded / ( 1024 * 1024 * 1024 );
if ( $gigs < 10 ) {
return ;
}
$ratio = ( $user -> downloaded > 0 ) ? ( $user -> uploaded / $user -> downloaded ) : 1 ;
$settingsMain = Setting :: get ( 'main' );
if ( $settingsMain [ 'waitsystem' ] == 'yes' ) {
2022-03-18 19:59:27 +08:00
$elapsed = Carbon :: now () -> diffInHours ( $torrent -> added );
2022-03-17 18:46:49 +08:00
if ( $ratio < 0.4 ) $wait = 24 ;
elseif ( $ratio < 0.5 ) $wait = 12 ;
elseif ( $ratio < 0.6 ) $wait = 6 ;
elseif ( $ratio < 0.8 ) $wait = 3 ;
else $wait = 0 ;
if ( $elapsed < $wait ) {
$msg = " Your ratio is too low! You need to wait " . mkprettytime ( $wait * 3600 - $elapsed ) . " to start " ;
throw new TrackerException ( $msg );
}
}
if ( $settingsMain [ 'maxdlsystem' ] == 'yes' ) {
if ( $ratio < 0.5 ) $max = 1 ;
elseif ( $ratio < 0.65 ) $max = 2 ;
elseif ( $ratio < 0.8 ) $max = 3 ;
elseif ( $ratio < 0.95 ) $max = 4 ;
else $max = 0 ;
if ( $max > 0 ) {
$counts = Peer :: query () -> where ( 'userid' , $user -> id ) -> where ( 'seeder' , 'no' ) -> count ();
if ( $counts > $max ) {
$msg = " Your slot limit is reached! You may at most download $max torrents at the same time " ;
throw new TrackerException ( $msg );
}
}
}
}
/**
* @ param Torrent $torrent
* @ param $queries
* @ param User $user
* @ return \Illuminate\Database\Eloquent\Builder | Model | object | null
* @ throws TrackerException
*/
protected function checkMinInterval ( Torrent $torrent , $queries , User $user )
{
$peer = Peer :: query ()
-> where ( 'torrent' , $torrent -> id )
-> where ( 'peer_id' , $queries [ 'peer_id' ])
-> first ();
2022-03-18 15:44:04 +08:00
if (
$peer
&& $queries [ 'event' ] == ''
&& $peer -> isValidDate ( 'prev_action' )
&& Carbon :: now () -> diffInSeconds ( $peer -> prev_action ) < self :: MIN_ANNOUNCE_WAIT_SECOND
) {
2022-03-17 18:46:49 +08:00
throw new TrackerException ( 'There is a minimum announce time of ' . self :: MIN_ANNOUNCE_WAIT_SECOND . ' seconds' );
}
return $peer ;
}
protected function checkCheater ( Torrent $torrent , $queries , User $user , Peer $peer )
{
$settingSecurity = Setting :: get ( 'security' );
$level = $settingSecurity [ 'cheaterdet' ];
if ( $level == 0 ) {
//don't do check
return ;
}
if ( $user -> class >= $settingSecurity [ 'nodetect' ]) {
//forever trust
return ;
}
if ( ! $peer -> isValidDate ( 'last_action' )) {
//no last action
return ;
}
$duration = Carbon :: now () -> diffInSeconds ( $peer -> last_action );
$upSpeed = $queries [ 'uploaded' ] > 0 ? ( $queries [ 'uploaded' ] / $duration ) : 0 ;
$oneGB = 1024 * 1024 * 1024 ;
$tenMB = 1024 * 1024 * 10 ;
$nowStr = Carbon :: now () -> toDateTimeString ();
$cheaterBaseData = [
'added' => $nowStr ,
'userid' => $user -> id ,
'torrentid' => $torrent -> id ,
'uploaded' => $queries [ 'uploaded' ],
'downloaded' => $queries [ 'downloaded' ],
'anctime' => $duration ,
'seeders' => $torrent -> seeders ,
'leechers' => $torrent -> leechers ,
];
if ( $queries [ 'uploaded' ] > $oneGB && ( $upSpeed > self :: MUST_BE_CHEATER_SPEED / $level )) {
//Uploaded more than 1 GB with uploading rate higher than 1024 MByte/S (For Consertive level). This is no doubt cheating.
$comment = " User account was automatically disabled by system " ;
$data = array_merge ( $cheaterBaseData , [ 'comment' => $comment ]);
Cheater :: query () -> insert ( $data );
$modComment = " We believe you're trying to cheat. And your account is disabled. " ;
$user -> updateWithModComment ([ 'enabled' => User :: ENABLED_NO ], $modComment );
throw new TrackerException ( $modComment );
}
if ( $queries [ 'uploaded' ] > $oneGB && ( $upSpeed > self :: MAY_BE_CHEATER_SPEED / $level )) {
//Uploaded more than 1 GB with uploading rate higher than 100 MByte/S (For Consertive level). This is likely cheating.
$comment = " Abnormally high uploading rate " ;
$data = array_merge ( $cheaterBaseData , [ 'comment' => $comment ]);
$this -> createOrUpdateCheater ( $torrent , $user , $data );
}
if ( $level > 1 ) {
if ( $queries [ 'uploaded' ] > $oneGB && ( $upSpeed > 1024 * 1024 ) && ( $queries [ 'leechers' ] < 2 * $level )) {
//Uploaded more than 1 GB with uploading rate higher than 1 MByte/S when there is less than 8 leechers (For Consertive level). This is likely cheating.
$comment = " User is uploading fast when there is few leechers " ;
$data = array_merge ( $cheaterBaseData , [ 'comment' => $comment ]);
$this -> createOrUpdateCheater ( $torrent , $user , $data );
}
if ( $queries [ 'uploaded' ] > $tenMB && ( $upSpeed > 1024 * 100 ) && ( $queries [ 'leechers' ] == 0 )) {
///Uploaded more than 10 MB with uploading speed faster than 100 KByte/S when there is no leecher. This is likely cheating.
$comment = " User is uploading when there is no leecher " ;
$data = array_merge ( $cheaterBaseData , [ 'comment' => $comment ]);
$this -> createOrUpdateCheater ( $torrent , $user , $data );
}
}
}
private function createOrUpdateCheater ( Torrent $torrent , User $user , array $createData )
{
$existsCheater = Cheater :: query ()
-> where ( 'torrentid' , $torrent -> id )
-> where ( 'userid' , $user -> id )
-> where ( 'added' , '>' , Carbon :: now () -> subHours ( 24 ))
-> first ();
if ( $existsCheater ) {
$existsCheater -> increment ( 'hit' );
} else {
$createData [ 'hit' ] = 1 ;
Cheater :: query () -> insert ( $createData );
}
}
protected function isReAnnounce ( array $queries ) : bool
{
unset ( $queries [ 'key' ]);
$lockKey = md5 ( http_build_query ( $queries ));
$redis = Redis :: connection () -> client ();
if ( ! $redis -> set ( $lockKey , NEXUS_START , [ 'nx' , 'ex' => self :: MIN_ANNOUNCE_WAIT_SECOND ])) {
do_log ( '[RE_ANNOUNCE]' );
return true ;
}
return false ;
}
private function generateSuccessAnnounceResponse ( $torrent , $queries , $user , $withPeers = true ) : array
{
// Build Response For Bittorrent Client
2022-03-19 14:55:43 +08:00
$minInterval = self :: MIN_ANNOUNCE_WAIT_SECOND ;
$interval = max ( $this -> getRealAnnounceInterval ( $torrent ), $minInterval );
2022-03-17 18:46:49 +08:00
$repDict = [
2022-03-19 14:55:43 +08:00
'interval' => $interval + random_int ( 10 , 100 ),
'min interval' => $minInterval + random_int ( 1 , 10 ),
2022-03-17 18:46:49 +08:00
'complete' => ( int ) $torrent -> seeders ,
'incomplete' => ( int ) $torrent -> leechers ,
'peers' => [],
'peers6' => [],
];
2022-03-18 15:44:04 +08:00
do_log ( " [REP_DICT_BASE] " . json_encode ( $repDict ));
2022-03-17 18:46:49 +08:00
/**
* For non `stopped` event only
* We query peers from database and send peer list , otherwise just quick return .
*/
2022-03-19 14:55:43 +08:00
if ( \strtolower ( $queries [ 'event' ]) !== 'stopped' && $withPeers ) {
2022-03-17 18:46:49 +08:00
$limit = ( $queries [ 'numwant' ] <= self :: MAX_PEER_NUM_WANT ? $queries [ 'numwant' ] : self :: MAX_PEER_NUM_WANT );
$baseQuery = Peer :: query ()
-> select ([ 'peer_id' , 'ip' , 'port' ])
-> where ( 'torrent' , $torrent -> id )
-> where ( 'userid' , '!=' , $user -> id )
-> limit ( $limit )
-> orderByRaw ( 'rand()' )
;
// Get Torrents Peers
if ( $queries [ 'left' ] == 0 ) {
// Only include leechers in a seeder's peerlist
$peers = $baseQuery -> where ( 'seeder' , 'no' ) -> get () -> toArray ();
} else {
$peers = $baseQuery -> get () -> toArray ();
}
2022-03-18 15:44:04 +08:00
do_log ( " [REP_DICT_PEER_QUERY] " . last_query ());
2022-03-17 18:46:49 +08:00
$repDict [ 'peers' ] = $this -> givePeers ( $peers , $queries [ 'compact' ], $queries [ 'no_peer_id' ]);
$repDict [ 'peers6' ] = $this -> givePeers ( $peers , $queries [ 'compact' ], $queries [ 'no_peer_id' ], FILTER_FLAG_IPV6 );
}
return $repDict ;
}
private function getRealAnnounceInterval ( Torrent $torrent )
{
$settingMain = Setting :: get ( 'main' );
$announce_wait = self :: MIN_ANNOUNCE_WAIT_SECOND ;
$real_annnounce_interval = $settingMain [ 'announce_interval' ];
$torrentSurvivalDays = Carbon :: now () -> diffInDays ( $torrent -> added );
if (
$settingMain [ 'anninterthreeage' ]
&& ( $settingMain [ 'anninterthree' ] > $announce_wait )
&& ( $torrentSurvivalDays >= $settingMain [ 'anninterthreeage' ])
) {
$real_annnounce_interval = $settingMain [ 'anninterthree' ];
} elseif (
$settingMain [ 'annintertwoage' ]
&& ( $settingMain [ 'annintertwo' ] > $announce_wait )
&& ( $torrentSurvivalDays >= $settingMain [ 'annintertwoage' ])
) {
$real_annnounce_interval = $settingMain [ 'annintertwo' ];
}
do_log ( sprintf (
'torrent: %s, survival days: %s, real_announce_interval: %s' ,
$torrent -> id , $torrentSurvivalDays , $real_annnounce_interval
), 'debug' );
return $real_annnounce_interval ;
}
2022-03-18 15:44:04 +08:00
private function getDataTraffic ( Torrent $torrent , $queries , User $user , Peer $peer ) : array
2022-03-17 18:46:49 +08:00
{
2022-03-18 15:44:04 +08:00
$log = sprintf (
" torrent: %s, user: %s, peer: %s, queriesUploaded: %s, queriesDownloaded: %s " ,
$torrent -> id , $user -> id , $peer -> id , $queries [ 'uploaded' ], $queries [ 'downloaded' ]
);
2022-03-17 18:46:49 +08:00
if ( $peer -> exists ) {
$realUploaded = max ( $queries [ 'uploaded' ] - $peer -> uploaded , 0 );
$realDownloaded = max ( $queries [ 'downloaded' ] - $peer -> downloaded , 0 );
2022-03-18 15:44:04 +08:00
$log .= " , [PEER_EXISTS], realUploaded: $realUploaded , realDownloaded: $realDownloaded " ;
2022-03-17 18:46:49 +08:00
} else {
$realUploaded = $queries [ 'uploaded' ];
$realDownloaded = $queries [ 'downloaded' ];
2022-03-18 15:44:04 +08:00
$log .= " , [PEER_NOT_EXISTS],, realUploaded: $realUploaded , realDownloaded: $realDownloaded " ;
2022-03-17 18:46:49 +08:00
}
$spStateReal = $torrent -> spStateReal ;
$uploaderRatio = Setting :: get ( 'torrent.uploaderdouble' );
2022-03-18 15:44:04 +08:00
$log .= " , spStateReal: $spStateReal , uploaderRatio: $uploaderRatio " ;
2022-03-17 18:46:49 +08:00
if ( $torrent -> owner == $user -> id ) {
//uploader, use the bigger one
$upRatio = max ( $uploaderRatio , Torrent :: $promotionTypes [ $spStateReal ][ 'up_multiplier' ]);
2022-03-18 15:44:04 +08:00
$log .= " , [IS_UPLOADER], upRatio: $upRatio " ;
2022-03-17 18:46:49 +08:00
} else {
$upRatio = Torrent :: $promotionTypes [ $spStateReal ][ 'up_multiplier' ];
2022-03-18 15:44:04 +08:00
$log .= " , [IS_NOT_UPLOADER], upRatio: $upRatio " ;
2022-03-17 18:46:49 +08:00
}
$downRatio = Torrent :: $promotionTypes [ $spStateReal ][ 'down_multiplier' ];
2022-03-18 15:44:04 +08:00
$log .= " , downRatio: $downRatio " ;
$result = [
'uploaded_increment' => $realUploaded ,
'uploaded_increment_for_user' => $realUploaded * $upRatio ,
'downloaded_increment' => $realDownloaded ,
'downloaded_increment_for_user' => $realDownloaded * $downRatio ,
2022-03-17 18:46:49 +08:00
];
2022-03-18 15:44:04 +08:00
do_log ( " $log , result: " . json_encode ( $result ));
return $result ;
2022-03-17 18:46:49 +08:00
}
private function givePeers ( $peers , $compact , $noPeerId , int $filterFlag = FILTER_FLAG_IPV4 ) : string | array
{
if ( $compact ) {
$pcomp = '' ;
foreach ( $peers as $p ) {
if ( isset ( $p [ 'ip' ], $p [ 'port' ]) && \filter_var ( $p [ 'ip' ], FILTER_VALIDATE_IP , $filterFlag )) {
$pcomp .= \inet_pton ( $p [ 'ip' ]);
$pcomp .= \pack ( 'n' , ( int ) $p [ 'port' ]);
}
}
return $pcomp ;
}
if ( $noPeerId ) {
foreach ( $peers as & $p ) {
unset ( $p [ 'peer_id' ]);
}
return $peers ;
}
return $peers ;
}
2022-03-18 15:44:04 +08:00
protected function generateFailedAnnounceResponse ( $reason ) : array
2022-03-17 18:46:49 +08:00
{
return [
2022-03-18 15:44:04 +08:00
'failure reason' => $reason ,
2022-03-17 18:46:49 +08:00
'min interval' => self :: MIN_ANNOUNCE_WAIT_SECOND ,
//'retry in' => self::MIN_ANNOUNCE_WAIT_SECOND
];
}
protected function sendFinalAnnounceResponse ( $repDict ) : \Illuminate\Http\Response
{
return \response ( Bencode :: encode ( $repDict ))
-> withHeaders ([ 'Content-Type' => 'text/plain; charset=utf-8' ])
-> withHeaders ([ 'Connection' => 'close' ])
-> withHeaders ([ 'Pragma' => 'no-cache' ]);
}
private function updateTorrent ( Torrent $torrent , $queries )
{
$torrent -> seeders = Peer :: query ()
-> where ( 'torrent' , $torrent -> id )
-> where ( 'to_go' , '=' , 0 )
-> count ();
$torrent -> leechers = Peer :: query ()
-> where ( 'torrent' , $torrent -> id )
-> where ( 'to_go' , '>' , 0 )
-> count ();
$torrent -> visible = Torrent :: VISIBLE_YES ;
$torrent -> last_action = Carbon :: now ();
if ( $queries [ 'event' ] == 'completed' ) {
$torrent -> times_completed = DB :: raw ( " times_completed + 1 " );
}
$torrent -> save ();
2022-03-18 15:44:04 +08:00
do_log ( last_query (), 'debug' );
2022-03-17 18:46:49 +08:00
}
private function updatePeer ( Peer $peer , $queries )
{
if ( $queries [ 'event' ] == 'stopped' ) {
2022-03-18 15:44:04 +08:00
$peer -> delete ();
do_log ( last_query (), 'debug' );
return ;
2022-03-17 18:46:49 +08:00
}
$nowStr = Carbon :: now () -> toDateTimeString ();
//torrent, userid, peer_id, ip, port, connectable, uploaded, downloaded, to_go, started, last_action, seeder, agent, downloadoffset, uploadoffset, passkey
$peer -> ip = $queries [ 'ip' ];
$peer -> port = $queries [ 'port' ];
$peer -> agent = $queries [ 'user_agent' ];
$peer -> updateConnectableStateIfNeeded ();
$peer -> to_go = $queries [ 'left' ];
$peer -> seeder = $queries [ 'left' ] == 0 ? 'yes' : 'no' ;
$peer -> last_action = $nowStr ;
$peer -> uploaded = $queries [ 'uploaded' ];
$peer -> downloaded = $queries [ 'downloaded' ];
2022-03-18 15:44:04 +08:00
if ( $peer -> exists ) {
$peer -> prev_action = $peer -> last_action ;
}
2022-03-17 18:46:49 +08:00
if ( $queries [ 'event' ] == 'started' ) {
$peer -> started = $nowStr ;
$peer -> uploadoffset = $queries [ 'uploaded' ];
$peer -> downloadoffset = $queries [ 'downloaded' ];
} elseif ( $queries [ 'event' ] == 'completed' ) {
2022-03-18 15:44:04 +08:00
$peer -> finishedat = time ();
2022-03-17 18:46:49 +08:00
}
$peer -> save ();
2022-03-18 15:44:04 +08:00
do_log ( last_query (), 'debug' );
2022-03-17 18:46:49 +08:00
}
2022-03-18 15:44:04 +08:00
/**
* Update snatch , uploaded & downloaded , use the increment value to do increment
*
* @ param Peer $peer
* @ param $queries
* @ param $dataTraffic
*/
private function updateSnatch ( Peer $peer , $queries , $dataTraffic )
2022-03-17 18:46:49 +08:00
{
$nowStr = Carbon :: now () -> toDateTimeString ();
$snatch = Snatch :: query ()
-> where ( 'torrentid' , $peer -> torrent )
-> where ( 'userid' , $peer -> userid )
-> first ();
//torrentid, userid, ip, port, uploaded, downloaded, to_go, ,seedtime, leechtime, last_action, startdat, completedat, finished
if ( ! $snatch ) {
$snatch = new Snatch ();
//initial
$snatch -> torrentid = $peer -> torrent ;
$snatch -> userid = $peer -> userid ;
2022-03-18 15:44:04 +08:00
$snatch -> uploaded = $dataTraffic [ 'uploaded_increment' ];
$snatch -> downloaded = $dataTraffic [ 'downloaded_increment' ];
$snatch -> startdat = $nowStr ;
2022-03-17 18:46:49 +08:00
} else {
2022-03-18 15:44:04 +08:00
//increase, use the increment value
$snatch -> uploaded = DB :: raw ( " uploaded + " . $dataTraffic [ 'uploaded_increment' ]);
$snatch -> downloaded = DB :: raw ( " downloaded + " . $dataTraffic [ 'downloaded_increment' ]);
2022-03-17 18:46:49 +08:00
$timeIncrease = Carbon :: now () -> diffInSeconds ( $peer -> last_action );
if ( $queries [ 'left' ] == 0 ) {
//seeder
$timeField = 'seedtime' ;
} else {
$timeField = 'leechtime' ;
}
$snatch -> { $timeField } = DB :: raw ( " $timeField + $timeIncrease " );
}
//always update
$snatch -> ip = $queries [ 'ip' ];
$snatch -> port = $queries [ 'port' ];
2022-03-18 15:44:04 +08:00
$snatch -> to_go = $queries [ 'left' ];
2022-03-17 18:46:49 +08:00
$snatch -> last_action = $nowStr ;
if ( $queries [ 'event' ] == 'completed' ) {
$snatch -> completedat = $nowStr ;
$snatch -> finished = 'yes' ;
}
$snatch -> save ();
2022-03-18 15:44:04 +08:00
do_log ( last_query (), 'debug' );
2022-03-17 18:46:49 +08:00
}
2022-03-19 14:55:43 +08:00
public function scrape ( Request $request ) : \Illuminate\Http\Response
{
do_log ( nexus_json_encode ( $_SERVER ));
try {
$infoHashArr = $this -> checkScrapeFields ( $request );
$user = $this -> checkUser ( $request );
$clientAllow = $this -> checkClient ( $request );
if ( $user -> clientselect != $clientAllow -> id ) {
$this -> userUpdates [ 'clientselect' ] = $clientAllow -> id ;
}
$repDict = $this -> generateScrapeResponse ( $infoHashArr );
} catch ( ClientNotAllowedException $exception ) {
do_log ( " [ClientNotAllowedException] " . $exception -> getMessage ());
if ( isset ( $user ) && $user -> showclienterror == 'no' ) {
$this -> userUpdates [ 'showclienterror' ] = 'yes' ;
}
$repDict = $this -> generateFailedAnnounceResponse ( $exception -> getMessage ());
} catch ( TrackerException $exception ) {
$repDict = $this -> generateFailedAnnounceResponse ( $exception -> getMessage ());
} catch ( \Exception $exception ) {
//other system exception
do_log ( " [ " . get_class ( $exception ) . " ] " . $exception -> getMessage () . " \n " . $exception -> getTraceAsString (), 'error' );
$repDict = $this -> generateFailedAnnounceResponse ( " system error, report to sysop please, hint: " . REQUEST_ID );
} finally {
if ( isset ( $user ) && count ( $this -> userUpdates )) {
$user -> update ( $this -> userUpdates );
do_log ( last_query (), 'debug' );
}
return $this -> sendFinalAnnounceResponse ( $repDict );
}
}
private function checkScrapeFields ( Request $request ) : array
{
preg_match_all ( '/info_hash=([^&]*)/i' , urldecode ( $request -> getQueryString ()), $info_hash_match );
$info_hash_array = $info_hash_match [ 1 ];
if ( count ( $info_hash_array ) < 1 ) {
throw new TrackerException ( " key: info_hash is Missing ! " );
} else {
foreach ( $info_hash_array as $item ) {
if ( strlen ( $item ) != 20 ) {
throw new TrackerException ( " Invalid info_hash ! :attribute is not 20 bytes long " );
}
}
}
return $info_hash_array ;
}
/**
* @ param $info_hash_array
* @ return array []
* @ see http :// www . bittorrent . org / beps / bep_0048 . html
*/
private function generateScrapeResponse ( $info_hash_array )
{
$torrent_details = [];
foreach ( $info_hash_array as $item ) {
$torrent = $this -> getTorrentByInfoHash ( $item );
if ( $torrent ) {
$torrent_details [ $item ] = [
'complete' => ( int ) $torrent -> seeders ,
'downloaded' => ( int ) $torrent -> times_completed ,
'incomplete' => ( int ) $torrent -> leechers ,
];
}
}
return [ 'files' => $torrent_details ];
}
private function getTorrentByInfoHash ( $infoHash )
{
return Cache :: remember ( bin2hex ( $infoHash ), 60 , function () use ( $infoHash ) {
$fieldRaw = 'id, owner, sp_state, seeders, leechers, added, banned, hr, visible, last_action, times_completed' ;
$torrent = Torrent :: query () -> where ( 'info_hash' , $infoHash ) -> selectRaw ( $fieldRaw ) -> first ();
do_log ( " cache miss, from database: " . last_query () . " , and get: " . $torrent -> id );
return $torrent ;
});
}
2022-03-17 18:46:49 +08:00
}