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 ;
2022-04-08 15:22:30 +08:00
use App\Models\HitAndRun ;
2022-03-17 18:46:49 +08:00
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
2022-04-20 15:29:12 +08:00
const ANNOUNCE_FIRST = 0 ;
const ANNOUNCE_DUAL = 1 ;
const ANNOUNCE_DUPLICATE = 2 ;
2022-03-17 18:46:49 +08:00
// 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.
];
public function announce ( Request $request ) : \Illuminate\Http\Response
{
2022-03-20 22:14:00 +08:00
do_log ( " queryString: " . $request -> getQueryString ());
2022-04-09 19:41:16 +08:00
/**
2022-04-21 17:54:26 +08:00
* Note : In Octane this class will be reused , use variable is better !!!
2022-04-09 19:41:16 +08:00
*/
2022-04-21 17:54:26 +08:00
$userUpdates = [];
2022-03-17 18:46:49 +08:00
try {
$withPeers = false ;
$queries = $this -> checkAnnounceFields ( $request );
$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 );
2022-04-20 15:29:12 +08:00
$isReAnnounce = $this -> isReAnnounce ( $request );
2022-04-21 16:14:08 +08:00
do_log ( " [IS_RE_ANNOUNCE] $isReAnnounce " );
2022-04-20 15:29:12 +08:00
if ( $isReAnnounce < self :: ANNOUNCE_DUPLICATE ) {
2022-04-18 19:07:35 +08:00
$isPeerExists = true ;
2022-04-23 01:14:01 +08:00
/** @var Peer $peerSelf */
$peerSelf = Peer :: query ()
-> where ( 'torrent' , $torrent -> id )
-> where ( 'peer_id' , $queries [ 'peer_id' ])
-> first ();
2022-03-17 18:46:49 +08:00
if ( ! $peerSelf ) {
2022-04-18 19:07:35 +08:00
$isPeerExists = false ;
2022-03-17 18:46:49 +08:00
$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 ,
]);
2022-04-23 01:14:01 +08:00
} elseif ( $isReAnnounce == self :: ANNOUNCE_FIRST ) {
$this -> checkMinInterval ( $peerSelf , $queries );
2022-03-17 18:46:49 +08:00
}
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
2022-04-08 15:22:30 +08:00
/**
* Note : Only check in old session
*/
if ( $peerSelf -> exists ) {
$this -> checkCheater ( $torrent , $dataTraffic , $user , $peerSelf );
}
2022-04-11 19:34:49 +08:00
/**
2022-04-18 19:07:35 +08:00
* Note : Must update peer first , otherwise updateTorrent () count peer not correct
2022-04-20 15:29:12 +08:00
* Update : Will not change $peerSelf any more
2022-04-11 19:34:49 +08:00
*/
2022-04-23 01:14:01 +08:00
if ( $isReAnnounce == self :: ANNOUNCE_FIRST || ( $isReAnnounce == self :: ANNOUNCE_DUAL && $queries [ 'event' ] !== 'stopped' )) {
2022-04-21 16:14:08 +08:00
$this -> updatePeer ( $peerSelf , $queries );
}
2022-03-17 18:46:49 +08:00
2022-04-20 15:29:12 +08:00
if ( $isReAnnounce === self :: ANNOUNCE_FIRST ) {
$withPeers = true ;
/**
* Note : Must update snatch first , otherwise peer `last_action` already change
*/
$snatch = $this -> updateSnatch ( $peerSelf , $queries , $dataTraffic );
if ( $queries [ 'event' ] == 'completed' ) {
$this -> handleHitAndRun ( $user , $torrent , $snatch );
}
$this -> updateTorrent ( $torrent , $queries , $isPeerExists );
if ( $dataTraffic [ 'uploaded_increment_for_user' ] > 0 ) {
2022-04-21 17:54:26 +08:00
$userUpdates [ 'uploaded' ] = DB :: raw ( 'uploaded + ' . $dataTraffic [ 'uploaded_increment_for_user' ]);
2022-04-20 15:29:12 +08:00
}
if ( $dataTraffic [ 'downloaded_increment_for_user' ] > 0 ) {
2022-04-21 17:54:26 +08:00
$userUpdates [ 'downloaded' ] = DB :: raw ( 'downloaded + ' . $dataTraffic [ 'downloaded_increment_for_user' ]);
2022-04-20 15:29:12 +08:00
}
if ( $user -> clientselect != $clientAllow -> id ) {
2022-04-21 17:54:26 +08:00
$userUpdates [ 'clientselect' ] = $clientAllow -> id ;
2022-04-20 15:29:12 +08:00
}
if ( $user -> showclienterror == 'yes' ) {
2022-04-21 17:54:26 +08:00
$userUpdates [ 'showclienterror' ] = 'no' ;
2022-04-20 15:29:12 +08:00
}
2022-03-18 19:59:27 +08:00
}
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' ) {
2022-04-21 17:54:26 +08:00
$userUpdates [ 'showclienterror' ] = 'yes' ;
2022-03-18 19:59:27 +08:00
}
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 ());
2022-03-31 16:28:08 +08:00
} catch ( \Throwable $exception ) {
2022-03-18 15:44:04 +08:00
//other system exception
do_log ( " [ " . get_class ( $exception ) . " ] " . $exception -> getMessage () . " \n " . $exception -> getTraceAsString (), 'error' );
2022-03-20 22:14:00 +08:00
$repDict = $this -> generateFailedAnnounceResponse ( " system error, report to sysop please, hint: " . nexus () -> getRequestId ());
2022-03-17 18:46:49 +08:00
} finally {
2022-04-20 15:29:12 +08:00
if ( isset ( $user )) {
2022-04-21 17:54:26 +08:00
$this -> updateUser ( $user , $userUpdates );
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 ) {
2022-03-20 22:14:00 +08:00
if (( $length = \strlen (( string ) $queries [ $item ])) !== 20 ) {
throw new TrackerException ( " Invalid $item ! $item is not 20 bytes long( $length ) " );
2022-03-17 18:46:49 +08:00
}
}
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
2022-04-21 16:14:08 +08:00
$ip = nexus () -> getRequestIp ();
$ipv4 = $ipv6 = '' ;
$ipv4Temp = $request -> query -> get ( 'ipv4' , '' );
$ipv6Temp = $request -> query -> get ( 'ipv6' , '' );
//use the real ip first, ip from parameter second
if ( isIPV4 ( $ip )) {
$ipv4 = $ip ;
} elseif ( isIPV4 ( $ipv4Temp )) {
$ipv4 = $ipv4Temp ;
}
if ( isIPV6 ( $ip )) {
$ipv6 = $ip ;
} elseif ( isIPV6 ( $ipv6Temp )) {
$ipv6 = $ipv6Temp ;
}
$queries [ 'ip' ] = $ip ;
$queries [ 'ipv4' ] = $ipv4 ;
$queries [ 'ipv6' ] = $ipv6 ;
2022-03-17 18:46:49 +08:00
// 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
*/
2022-04-21 18:18:17 +08:00
$user = User :: query () -> where ( $field , $value ) -> first ();
2022-03-17 18:46:49 +08:00
if ( ! $user ) {
2022-04-01 23:13:42 +08:00
throw new TrackerException ( " Invalid user $field : $value . " );
2022-03-17 18:46:49 +08:00
}
2022-04-07 01:16:17 +08:00
try {
$user -> checkIsNormal ();
} catch ( \Throwable $exception ) {
throw new TrackerException ( $exception -> getMessage ());
}
2022-03-17 18:46:49 +08:00
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. " );
}
2022-04-18 19:07:35 +08:00
$countResult = Peer :: query ()
2022-03-17 18:46:49 +08:00
-> where ( 'torrent' , '=' , $torrent -> id )
-> where ( 'userid' , $user -> id )
2022-04-18 19:07:35 +08:00
-> selectRaw ( 'count(distinct(peer_id)) as counts' )
-> first ()
;
$counts = $countResult ? $countResult -> counts : 0 ;
do_log ( " query: " . last_query () . " , counts: $counts " );
2022-03-17 18:46:49 +08:00
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 ) {
2022-04-18 19:07:35 +08:00
$countResult = Peer :: query ()
-> where ( 'userid' , $user -> id )
-> where ( 'seeder' , 'no' )
-> selectRaw ( 'count(distinct(peer_id)) as counts' )
-> first ();
$counts = $countResult ? $countResult -> counts : 0 ;
2022-03-17 18:46:49 +08:00
if ( $counts > $max ) {
$msg = " Your slot limit is reached! You may at most download $max torrents at the same time " ;
throw new TrackerException ( $msg );
}
}
}
}
2022-04-23 01:14:01 +08:00
2022-03-17 18:46:49 +08:00
/**
2022-04-23 01:14:01 +08:00
* @ param Peer $peer
2022-03-17 18:46:49 +08:00
* @ param $queries
* @ throws TrackerException
*/
2022-04-23 01:14:01 +08:00
protected function checkMinInterval ( Peer $peer , $queries )
2022-03-17 18:46:49 +08:00
{
2022-04-23 01:14:01 +08:00
$lastAction = $peer -> last_action ;
$isLastActionValidDate = $peer -> isValidDate ( 'last_action' );
$diffInSeconds = Carbon :: now () -> diffInSeconds ( $peer -> last_action );
$min = self :: MIN_ANNOUNCE_WAIT_SECOND ;
do_log ( sprintf (
'event: %s, last_action: %s, isLastActionValidDate: %s, diffInSeconds: %s' ,
$queries [ 'event' ], $lastAction , var_export ( $isLastActionValidDate , true ), $diffInSeconds
));
if ( $queries [ 'event' ] == '' && $isLastActionValidDate && $diffInSeconds < $min ) {
throw new TrackerException ( 'There is a minimum announce time of ' . $min . ' seconds' );
2022-03-17 18:46:49 +08:00
}
}
2022-04-08 15:22:30 +08:00
protected function checkCheater ( Torrent $torrent , $dataTraffic , User $user , Peer $peer )
2022-03-17 18:46:49 +08:00
{
$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 );
2022-04-08 15:22:30 +08:00
$upSpeed = $dataTraffic [ 'uploaded_increment' ] > 0 ? ( $dataTraffic [ 'uploaded_increment' ] / $duration ) : 0 ;
2022-04-08 16:28:00 +08:00
$peerInfo = Arr :: except ( $peer -> toArray (), [ 'peer_id' ]);
do_log ( " peerInfo: " . json_encode ( $peerInfo ) . " , upSpeed: $upSpeed , dataTraffic: " . json_encode ( $dataTraffic ));
2022-03-17 18:46:49 +08:00
$oneGB = 1024 * 1024 * 1024 ;
$tenMB = 1024 * 1024 * 10 ;
$nowStr = Carbon :: now () -> toDateTimeString ();
$cheaterBaseData = [
'added' => $nowStr ,
'userid' => $user -> id ,
'torrentid' => $torrent -> id ,
2022-04-08 15:22:30 +08:00
'uploaded' => $dataTraffic [ 'uploaded_increment' ],
'downloaded' => $dataTraffic [ 'downloaded_increment' ],
2022-03-17 18:46:49 +08:00
'anctime' => $duration ,
'seeders' => $torrent -> seeders ,
'leechers' => $torrent -> leechers ,
];
2022-04-08 15:22:30 +08:00
if ( $dataTraffic [ 'uploaded_increment' ] > $oneGB && ( $upSpeed > self :: MUST_BE_CHEATER_SPEED / $level )) {
2022-03-17 18:46:49 +08:00
//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 );
}
2022-04-08 15:22:30 +08:00
if ( $dataTraffic [ 'uploaded_increment' ] > $oneGB && ( $upSpeed > self :: MAY_BE_CHEATER_SPEED / $level )) {
2022-03-17 18:46:49 +08:00
//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 ) {
2022-04-08 15:22:30 +08:00
if ( $dataTraffic [ 'uploaded_increment' ] > $oneGB && ( $upSpeed > 1024 * 1024 ) && ( $torrent -> leechers < 2 * $level )) {
2022-03-17 18:46:49 +08:00
//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 );
}
2022-04-08 15:22:30 +08:00
if ( $dataTraffic [ 'uploaded_increment' ] > $tenMB && ( $upSpeed > 1024 * 100 ) && ( $torrent -> leechers == 0 )) {
2022-03-17 18:46:49 +08:00
///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 );
}
}
2022-04-20 15:29:12 +08:00
protected function isReAnnounce ( Request $request ) : int
2022-03-17 18:46:49 +08:00
{
2022-04-08 15:22:30 +08:00
$key = $request -> query -> get ( 'key' );
$queryString = $request -> getQueryString ();
2022-04-18 19:07:35 +08:00
$lockKeyOriginal = str_replace ( $key , '' , $queryString );
$lockKey = md5 ( $lockKeyOriginal );
2022-04-08 15:22:30 +08:00
$startTimestamp = nexus () -> getStartTimestamp ();
2022-03-17 18:46:49 +08:00
$redis = Redis :: connection () -> client ();
2022-04-20 15:29:12 +08:00
$cache = $redis -> get ( $lockKey );
2022-04-21 18:10:26 +08:00
do_log ( " key: $key , queryString: $queryString , lockKeyOriginal: $lockKeyOriginal , startTimestamp: $startTimestamp , cache: $cache " );
2022-04-20 15:29:12 +08:00
if ( $cache === false ) {
//new request
$redis -> set ( $lockKey , $startTimestamp , [ 'ex' => self :: MIN_ANNOUNCE_WAIT_SECOND ]);
return self :: ANNOUNCE_FIRST ;
} else {
if ( bcsub ( $startTimestamp , $cache , 3 ) < 0.5 ) {
do_log ( '[DUAL]' );
return self :: ANNOUNCE_DUAL ;
} else {
do_log ( '[RE_ANNOUNCE]' );
return self :: ANNOUNCE_DUPLICATE ;
}
2022-03-17 18:46:49 +08:00
}
}
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 ()
2022-04-21 16:14:08 +08:00
-> select ([ 'peer_id' , 'ip' , 'port' , 'ipv4' , 'ipv6' ])
2022-03-17 18:46:49 +08:00
-> 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 " ,
2022-04-09 14:15:29 +08:00
$torrent -> id , $user -> id , json_encode ( $peer -> only ([ 'uploaded' , 'downloaded' ])), $queries [ 'uploaded' ], $queries [ 'downloaded' ]
2022-03-18 15:44:04 +08:00
);
2022-03-17 18:46:49 +08:00
if ( $peer -> exists ) {
2022-04-08 15:22:30 +08:00
$realUploaded = max ( bcsub ( $queries [ 'uploaded' ], $peer -> uploaded ), 0 );
$realDownloaded = max ( bcsub ( $queries [ 'downloaded' ], $peer -> downloaded ), 0 );
2022-03-18 15:44:04 +08:00
$log .= " , [PEER_EXISTS], realUploaded: $realUploaded , realDownloaded: $realDownloaded " ;
2022-04-11 19:34:49 +08:00
$spStateReal = $torrent -> spStateReal ;
$uploaderRatio = Setting :: get ( 'torrent.uploaderdouble' );
$log .= " , spStateReal: $spStateReal , uploaderRatio: $uploaderRatio " ;
if ( $torrent -> owner == $user -> id ) {
//uploader, use the bigger one
$upRatio = max ( $uploaderRatio , Torrent :: $promotionTypes [ $spStateReal ][ 'up_multiplier' ]);
$log .= " , [IS_UPLOADER], upRatio: $upRatio " ;
} else {
$upRatio = Torrent :: $promotionTypes [ $spStateReal ][ 'up_multiplier' ];
$log .= " , [IS_NOT_UPLOADER], upRatio: $upRatio " ;
}
$downRatio = Torrent :: $promotionTypes [ $spStateReal ][ 'down_multiplier' ];
$log .= " , downRatio: $downRatio " ;
2022-03-17 18:46:49 +08:00
} else {
$realUploaded = $queries [ 'uploaded' ];
$realDownloaded = $queries [ 'downloaded' ];
2022-04-11 19:34:49 +08:00
/**
* If peer not exits , user increment = 0 ;
*/
$upRatio = 0 ;
$downRatio = 0 ;
$log .= " , [PEER_NOT_EXISTS], realUploaded: $realUploaded , realDownloaded: $realDownloaded , upRatio: $upRatio , downRatio: $downRatio " ;
2022-03-17 18:46:49 +08:00
}
2022-04-11 19:34:49 +08:00
2022-03-18 15:44:04 +08:00
$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-04-21 17:09:48 +08:00
do_log ( " $log , result: " . json_encode ( $result ), 'alert' );
2022-03-18 15:44:04 +08:00
return $result ;
2022-03-17 18:46:49 +08:00
}
2022-04-21 16:14:08 +08:00
private function givePeers ( $originalPeers , $compact , $noPeerId , int $filterFlag = FILTER_FLAG_IPV4 ) : string | array
2022-03-17 18:46:49 +08:00
{
2022-04-21 16:14:08 +08:00
$peers = [];
foreach ( $originalPeers as $value ) {
$ipKey = $filterFlag == FILTER_FLAG_IPV4 ? 'ipv4' : 'ipv6' ;
if ( ! empty ( $value [ $ipKey ]) && filter_var ( $value [ $ipKey ], FILTER_VALIDATE_IP , $filterFlag )) {
$peers [] = [
'peer_id' => $value [ 'peer_id' ],
'ip' => $value [ $ipKey ],
'port' => $value [ 'port' ]
];
}
}
2022-03-17 18:46:49 +08:00
if ( $compact ) {
$pcomp = '' ;
foreach ( $peers as $p ) {
2022-04-21 16:14:08 +08:00
$pcomp .= \inet_pton ( $p [ 'ip' ]);
$pcomp .= \pack ( 'n' , ( int ) $p [ 'port' ]);
2022-03-17 18:46:49 +08:00
}
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
{
2022-03-20 22:14:00 +08:00
do_log ( " [repDict] " . nexus_json_encode ( $repDict ));
2022-03-17 18:46:49 +08:00
return \response ( Bencode :: encode ( $repDict ))
-> withHeaders ([ 'Content-Type' => 'text/plain; charset=utf-8' ])
-> withHeaders ([ 'Connection' => 'close' ])
-> withHeaders ([ 'Pragma' => 'no-cache' ]);
}
2022-03-28 15:58:12 +08:00
/**
*
* @ param Torrent $torrent
* @ param $queries
2022-04-20 15:29:12 +08:00
* @ param bool $isPeerExists
2022-03-28 15:58:12 +08:00
*/
2022-04-18 19:07:35 +08:00
private function updateTorrent ( Torrent $torrent , $queries , bool $isPeerExists )
2022-03-17 18:46:49 +08:00
{
2022-04-22 13:23:54 +08:00
if ( ! empty ( $queries [ 'event' ])) {
$torrent -> seeders = Peer :: query ()
-> where ( 'torrent' , $torrent -> id )
-> where ( 'to_go' , '=' , 0 )
-> count ();
2022-04-22 01:32:17 +08:00
2022-04-22 13:23:54 +08:00
$torrent -> leechers = Peer :: query ()
-> where ( 'torrent' , $torrent -> id )
-> where ( 'to_go' , '>' , 0 )
-> count ();
}
2022-03-17 18:46:49 +08:00
$torrent -> visible = Torrent :: VISIBLE_YES ;
$torrent -> last_action = Carbon :: now ();
2022-04-18 19:07:35 +08:00
if ( $isPeerExists && $queries [ 'event' ] == 'completed' ) {
2022-03-17 18:46:49 +08:00
$torrent -> times_completed = DB :: raw ( " times_completed + 1 " );
}
$torrent -> save ();
2022-04-09 17:27:47 +08:00
do_log ( last_query ());
2022-03-17 18:46:49 +08:00
}
private function updatePeer ( Peer $peer , $queries )
{
if ( $queries [ 'event' ] == 'stopped' ) {
2022-04-20 01:28:17 +08:00
Peer :: query () -> where ( 'torrent' , $peer -> torrent ) -> where ( 'peer_id' , $queries [ 'peer_id' ]) -> delete ();
2022-04-09 17:44:55 +08:00
do_log ( last_query ());
2022-03-18 15:44:04 +08:00
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
2022-04-18 19:07:35 +08:00
$update = [
'torrent' => $peer -> torrent ,
'peer_id' => $queries [ 'peer_id' ],
'ip' => $queries [ 'ip' ],
'userid' => $peer -> userid ,
'passkey' => $peer -> passkey ,
'port' => $queries [ 'port' ],
'agent' => $queries [ 'user_agent' ],
'connectable' => $this -> getConnectable ( $queries [ 'ip' ], $queries [ 'port' ], $queries [ 'user_agent' ])
];
2022-04-21 16:14:08 +08:00
if ( ! empty ( $queries [ 'ipv4' ])) {
$update [ 'ipv4' ] = $queries [ 'ipv4' ];
}
if ( ! empty ( $queries [ 'ipv6' ])) {
$update [ 'ipv6' ] = $queries [ 'ipv6' ];
}
2022-03-17 18:46:49 +08:00
2022-04-08 22:47:38 +08:00
if ( $peer -> exists ) {
2022-04-18 19:07:35 +08:00
$update [ 'prev_action' ] = $peer -> last_action ;
2022-04-18 22:45:44 +08:00
$update [ 'started' ] = $peer -> started ;
2022-04-12 02:26:51 +08:00
if ( $queries [ 'event' ] == 'completed' ) {
2022-04-18 19:07:35 +08:00
$update [ 'finishedat' ] = time ();
2022-04-12 02:26:51 +08:00
}
} else {
2022-04-18 19:07:35 +08:00
$update [ 'started' ] = $nowStr ;
$update [ 'uploadoffset' ] = $queries [ 'uploaded' ];
$update [ 'downloadoffset' ] = $queries [ 'downloaded' ];
2022-04-08 22:47:38 +08:00
}
2022-04-18 19:07:35 +08:00
$update [ 'to_go' ] = $queries [ 'left' ];
$update [ 'seeder' ] = $queries [ 'left' ] == 0 ? 'yes' : 'no' ;
$update [ 'last_action' ] = $nowStr ;
$update [ 'uploaded' ] = $queries [ 'uploaded' ];
$update [ 'downloaded' ] = $queries [ 'downloaded' ];
2022-03-17 18:46:49 +08:00
2022-04-21 16:14:08 +08:00
$logData = json_encode ( Arr :: except ( $update , [ 'peer_id' ]));
2022-04-18 19:07:35 +08:00
if ( $peer -> exists ) {
2022-04-21 16:14:08 +08:00
$affected = Peer :: query () -> where ( 'torrent' , $peer -> torrent ) -> where ( 'peer_id' , $queries [ 'peer_id' ]) -> update ( $update );
do_log ( " [UPDATE], affected: $affected , data: $logData " );
2022-04-18 19:07:35 +08:00
} else {
Peer :: query () -> insert ( $update );
2022-04-21 16:14:08 +08:00
do_log ( " [INSERT], data: $logData " );
2022-04-18 19:07:35 +08:00
}
2022-04-21 16:14:08 +08:00
// $idArr = explode(',', $peer->ids);
// $ipArr = explode(',', $peer->ips);
// $logPrefix = "update: " . json_encode($update);
// $doUpdate = false;
// if ($peer->exists) {
// $logPrefix .= ", [EXISTS]";
// foreach ($idArr as $key => $id) {
// $ip = $ipArr[$key];
// if (isIPV4($ip) && isIPV4($queries['ip'])) {
// $update['ip'] = DB::raw("if(id = $id,'$ip', ip)");
// $doUpdate = true;
// $logPrefix .= ", v4, id = $id";
// } elseif (isIPV6($ip) && isIPV6($queries['ip'])) {
// $update['ip'] = DB::raw("if(id = $id,'$ip', ip)");
// $doUpdate = true;
// $logPrefix .= ", v6, id = $id";
// }
// }
// if ($doUpdate) {
// $affected = Peer::query()->where('torrent', $peer->torrent)->where('peer_id', $queries['peer_id'])->update($update);
// do_log("$logPrefix, [UPDATE], affected: $affected");
// } else {
// Peer::query()->insert($update);
// do_log("$logPrefix, [INSERT]");
// }
// } else {
// $logPrefix .= ", [NOT_EXISTS]";
// Peer::query()->insert($update);
// do_log("$logPrefix, [INSERT]");
// }
2022-04-18 19:07:35 +08:00
}
private function getConnectable ( $ip , $port , $agent )
{
$cacheKey = 'peers:connectable:' . $ip . '-' . $port . '-' . $agent ;
$log = " cacheKey: $cacheKey " ;
$connectable = Cache :: get ( $cacheKey );
if ( $connectable === null ) {
$con = @ fsockopen ( $ip , $port , $error_code , $error_message , 1 );
if ( is_resource ( $con )) {
$connectable = Peer :: CONNECTABLE_YES ;
fclose ( $con );
} else {
$connectable = Peer :: CONNECTABLE_NO ;
}
Cache :: put ( $cacheKey , $connectable , 3600 );
$log .= " , do check, connectable: " . $connectable ;
} else {
$log .= " , don't do check " ;
}
do_log ( $log );
return $connectable ;
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 ();
2022-04-11 19:34:49 +08:00
//initial, use report uploaded + downloaded
2022-03-17 18:46:49 +08:00
$snatch -> torrentid = $peer -> torrent ;
$snatch -> userid = $peer -> userid ;
2022-04-11 19:34:49 +08:00
$snatch -> uploaded = $queries [ 'uploaded' ];
$snatch -> downloaded = $queries [ 'downloaded' ];
2022-03-18 15:44:04 +08:00
$snatch -> startdat = $nowStr ;
2022-04-12 02:26:51 +08:00
} elseif ( $peer -> exists ) {
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 " );
2022-04-12 02:26:51 +08:00
if ( $queries [ 'event' ] == 'completed' ) {
$snatch -> completedat = $nowStr ;
$snatch -> finished = 'yes' ;
}
2022-03-17 18:46:49 +08:00
}
//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 ;
$snatch -> save ();
2022-04-21 16:14:08 +08:00
do_log ( last_query (), 'alert' );
2022-04-08 15:22:30 +08:00
return $snatch ;
2022-03-17 18:46:49 +08:00
}
2022-03-19 14:55:43 +08:00
public function scrape ( Request $request ) : \Illuminate\Http\Response
{
2022-03-20 22:14:00 +08:00
do_log ( " queryString: " . $request -> getQueryString ());
2022-04-09 19:41:16 +08:00
/**
2022-04-21 17:54:26 +08:00
* Note : In Octane this class will be reused , use variable is better !!!
2022-04-09 19:41:16 +08:00
*/
2022-04-21 17:54:26 +08:00
$userUpdates = [];
2022-03-19 14:55:43 +08:00
try {
$infoHashArr = $this -> checkScrapeFields ( $request );
$user = $this -> checkUser ( $request );
$clientAllow = $this -> checkClient ( $request );
if ( $user -> clientselect != $clientAllow -> id ) {
2022-04-21 17:54:26 +08:00
$userUpdates [ 'clientselect' ] = $clientAllow -> id ;
2022-03-19 14:55:43 +08:00
}
2022-03-20 22:14:00 +08:00
if ( $user -> showclienterror == 'yes' ) {
2022-04-21 17:54:26 +08:00
$userUpdates [ 'showclienterror' ] = 'no' ;
2022-03-20 22:14:00 +08:00
}
2022-03-19 14:55:43 +08:00
$repDict = $this -> generateScrapeResponse ( $infoHashArr );
} catch ( ClientNotAllowedException $exception ) {
do_log ( " [ClientNotAllowedException] " . $exception -> getMessage ());
if ( isset ( $user ) && $user -> showclienterror == 'no' ) {
2022-04-21 17:54:26 +08:00
$userUpdates [ 'showclienterror' ] = 'yes' ;
2022-03-19 14:55:43 +08:00
}
$repDict = $this -> generateFailedAnnounceResponse ( $exception -> getMessage ());
} catch ( TrackerException $exception ) {
$repDict = $this -> generateFailedAnnounceResponse ( $exception -> getMessage ());
2022-03-31 16:28:08 +08:00
} catch ( \Throwable $exception ) {
2022-03-19 14:55:43 +08:00
//other system exception
do_log ( " [ " . get_class ( $exception ) . " ] " . $exception -> getMessage () . " \n " . $exception -> getTraceAsString (), 'error' );
2022-03-20 22:14:00 +08:00
$repDict = $this -> generateFailedAnnounceResponse ( " system error, report to sysop please, hint: " . nexus () -> getRequestId ());
2022-03-19 14:55:43 +08:00
} finally {
2022-04-20 15:29:12 +08:00
if ( isset ( $user )) {
2022-04-21 17:54:26 +08:00
$this -> updateUser ( $user , $userUpdates );
2022-03-19 14:55:43 +08:00
}
return $this -> sendFinalAnnounceResponse ( $repDict );
}
}
private function checkScrapeFields ( Request $request ) : array
{
2022-04-07 15:44:43 +08:00
preg_match_all ( '/info_hash=([^&]*)/i' , $request -> getQueryString (), $info_hash_match );
2022-03-19 14:55:43 +08:00
$info_hash_array = $info_hash_match [ 1 ];
2022-04-07 15:44:43 +08:00
$info_hash_original = [];
2022-03-19 14:55:43 +08:00
if ( count ( $info_hash_array ) < 1 ) {
throw new TrackerException ( " key: info_hash is Missing ! " );
} else {
foreach ( $info_hash_array as $item ) {
2022-04-07 15:44:43 +08:00
$item = urldecode ( $item );
2022-04-07 01:16:17 +08:00
if (( $length = strlen ( $item )) != 20 ) {
throw new TrackerException ( " Invalid info_hash ! info_hash is not 20 bytes long( $length ) " );
2022-03-19 14:55:43 +08:00
}
2022-04-07 15:44:43 +08:00
$info_hash_original [] = $item ;
2022-03-19 14:55:43 +08:00
}
}
2022-04-07 15:44:43 +08:00
return $info_hash_original ;
2022-03-19 14:55:43 +08:00
}
/**
* @ 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 )
{
2022-04-01 23:13:42 +08:00
$cacheKey = __METHOD__ . bin2hex ( $infoHash );
return Cache :: remember ( $cacheKey , 60 , function () use ( $infoHash , $cacheKey ) {
2022-03-19 14:55:43 +08:00
$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 ();
2022-04-01 23:13:42 +08:00
do_log ( " [getTorrentByInfoHash] cache miss [ $cacheKey ], from database, and get: " . ( $torrent -> id ? ? '' ));
2022-03-19 14:55:43 +08:00
return $torrent ;
});
}
2022-04-08 15:22:30 +08:00
private function handleHitAndRun ( User $user , Torrent $torrent , Snatch $snatch )
{
$now = Carbon :: now ();
if ( $user -> class >= \App\Models\HitAndRun :: MINIMUM_IGNORE_USER_CLASS ) {
return ;
}
2022-04-19 19:18:52 +08:00
if ( $user -> isDonating ()) {
2022-04-08 15:22:30 +08:00
return ;
}
$hrMode = Setting :: get ( 'hr.mode' );
if ( $hrMode == HitAndRun :: MODE_DISABLED ) {
return ;
}
if ( $hrMode == HitAndRun :: MODE_MANUAL && $torrent -> hr != Torrent :: HR_YES ) {
return ;
}
$sql = sprintf (
" insert into `hit_and_runs` (`uid`, `torrent_id`, `snatched_id`) values(%d, %d, %d) on duplicate key update updated_at = '%s' " ,
$user -> id , $torrent -> id , $snatch -> id , $now -> toDateTimeString ()
);
2022-04-18 19:07:35 +08:00
DB :: insert ( $sql );
2022-04-08 15:22:30 +08:00
}
2022-04-21 17:54:26 +08:00
private function updateUser ( User $user , array $update )
2022-04-20 15:29:12 +08:00
{
2022-04-21 17:54:26 +08:00
$log = " update: " . json_encode ( $update );
if ( empty ( $update )) {
2022-04-21 16:14:08 +08:00
$log .= " , no update... " ;
} else {
2022-04-21 20:02:08 +08:00
$user -> update ( $update );
2022-04-21 16:14:08 +08:00
$log .= " , query: " . last_query ();
2022-04-20 15:29:12 +08:00
}
2022-04-21 16:14:08 +08:00
do_log ( $log , 'alert' );
2022-04-20 15:29:12 +08:00
}
2022-03-17 18:46:49 +08:00
}