From 6665c981697f8d469ae03f06e6d818c450846e74 Mon Sep 17 00:00:00 2001 From: xiaomlove Date: Thu, 17 Mar 2022 18:46:49 +0800 Subject: [PATCH] refactor announce --- app/Console/Commands/Test.php | 10 +- app/Exceptions/ClientNotAllowedException.php | 8 + app/Exceptions/TrackerException.php | 8 + app/Http/Controllers/TrackerController.php | 97 +++ app/Models/Cheater.php | 12 + app/Models/NexusModel.php | 16 + app/Models/Peer.php | 29 + app/Models/Torrent.php | 5 +- app/Models/User.php | 2 +- app/Repositories/AgentAllowRepository.php | 29 +- app/Repositories/TrackerRepository.php | 712 +++++++++++++++++++ bootstrap/app.php | 2 +- composer.json | 5 +- composer.lock | 350 ++++++++- config/octane.php | 220 ++++++ public/viewpeerlist.php | 8 +- 16 files changed, 1486 insertions(+), 27 deletions(-) create mode 100644 app/Exceptions/ClientNotAllowedException.php create mode 100644 app/Exceptions/TrackerException.php create mode 100644 app/Http/Controllers/TrackerController.php create mode 100644 app/Models/Cheater.php create mode 100644 app/Repositories/TrackerRepository.php create mode 100644 config/octane.php diff --git a/app/Console/Commands/Test.php b/app/Console/Commands/Test.php index 55c5a74d..05430743 100644 --- a/app/Console/Commands/Test.php +++ b/app/Console/Commands/Test.php @@ -9,6 +9,7 @@ use App\Models\ExamProgress; use App\Models\ExamUser; use App\Models\HitAndRun; use App\Models\Medal; +use App\Models\Peer; use App\Models\SearchBox; use App\Models\Snatch; use App\Models\Tag; @@ -27,6 +28,7 @@ use Illuminate\Console\Command; use Illuminate\Encryption\Encrypter; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; use Rhilip\Bencode\Bencode; @@ -63,9 +65,11 @@ class Test extends Command */ public function handle() { - $rep = new AttendanceRepository(); - $r = $rep->migrateAttendance(); - dd($r); + + $peer = Peer::query()->first(); + echo $peer->prev_action->timestamp; } + + } diff --git a/app/Exceptions/ClientNotAllowedException.php b/app/Exceptions/ClientNotAllowedException.php new file mode 100644 index 00000000..8028ef7f --- /dev/null +++ b/app/Exceptions/ClientNotAllowedException.php @@ -0,0 +1,8 @@ +repository = $repository; + } + + private function getRules(): array + { + return [ + 'family_id' => 'required|numeric', + 'name' => 'required|string', + 'peer_id' => 'required|string', + 'agent' => 'required|string', + 'comment' => 'required|string', + + ]; + } + /** + * Display a listing of the resource. + * + * @return array + */ + public function index(Request $request) + { + $result = $this->repository->getList($request->all()); + $resource = AgentDenyResource::collection($result); + return $this->success($resource); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(Request $request) + { + $request->validate($this->getRules()); + $result = $this->repository->store($request->all()); + $resource = new AgentDenyResource($result); + return $this->success($resource); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return array + */ + public function show($id) + { + $result = AgentDeny::query()->findOrFail($id); + $resource = new AgentDenyResource($result); + return $this->success($resource); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return array + */ + public function update(Request $request, $id) + { + $request->validate($this->getRules()); + $result = $this->repository->update($request->all(), $id); + $resource = new AgentDenyResource($result); + return $this->success($resource); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return array + */ + public function destroy($id) + { + $result = $this->repository->delete($id); + return $this->success($result); + } +} diff --git a/app/Models/Cheater.php b/app/Models/Cheater.php new file mode 100644 index 00000000..6abf61ee --- /dev/null +++ b/app/Models/Cheater.php @@ -0,0 +1,12 @@ +format($this->dateFormat ?: 'Y-m-d H:i:s'); } + /** + * Check is valid date string + * + * @see https://stackoverflow.com/questions/19271381/correctly-determine-if-date-string-is-a-valid-date-in-that-format + * @param $name + * @param string $format + * @return bool + */ + public function isValidDate($name, $format = 'Y-m-d H:i:s'): bool + { + $date = $this->getRawOriginal($name); + $d = \DateTime::createFromFormat($format, $date); + // The Y ( 4 digits year ) returns TRUE for any integer with any number of digits so changing the comparison from == to === fixes the issue. + return $d && $d->format($format) === $date; + } + } diff --git a/app/Models/Peer.php b/app/Models/Peer.php index a46905dd..03853454 100644 --- a/app/Models/Peer.php +++ b/app/Models/Peer.php @@ -4,9 +4,15 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\Cache; class Peer extends NexusModel { + protected $fillable = [ + 'torrent', 'peer_id', 'ip', 'port', 'uploaded', 'downloaded', 'to_go', 'seeder', 'started', 'last_action', + 'prev_action', 'connectable', 'userid', 'agent', 'finishedat', 'downloadoffset', 'uploadedoffset', 'passkey', + ]; + const CONNECTABLE_YES = 'yes'; const CONNECTABLE_NO = 'no'; @@ -71,4 +77,27 @@ class Peer extends NexusModel { return $this->belongsTo(Torrent::class, 'torrent'); } + + /** + * + */ + public function updateConnectableStateIfNeeded() + { + $tmp_ip = $this->ip; + // IPv6 Check + if (filter_var($tmp_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + $tmp_ip = '['.$tmp_ip.']'; + } + $cacheKey = 'peers:connectable:'.$tmp_ip.'-'.$this->port.'-'.$this->agent; + if (!Cache::has($cacheKey)) { + $con = @fsockopen($tmp_ip, $this->port, $error_code, $error_message, 1); + if (is_resource($con)) { + $this->connectable = self::CONNECTABLE_YES; + fclose($con); + } else { + $this->connectable = self::CONNECTABLE_NO; + } + Cache::put($cacheKey, $this->connectable, 600); + } + } } diff --git a/app/Models/Torrent.php b/app/Models/Torrent.php index e29e0353..05f1e7e9 100644 --- a/app/Models/Torrent.php +++ b/app/Models/Torrent.php @@ -61,7 +61,7 @@ class Torrent extends NexusModel const PROMOTION_HALF_DOWN_TWO_TIMES_UP = 6; const PROMOTION_ONE_THIRD_DOWN = 7; - public static $promotionTypes = [ + public static array $promotionTypes = [ self::PROMOTION_NORMAL => ['text' => 'Normal', 'up_multiplier' => 1, 'down_multiplier' => 1], self::PROMOTION_FREE => ['text' => 'Free', 'up_multiplier' => 1, 'down_multiplier' => 0], self::PROMOTION_TWO_TIMES_UP => ['text' => '2X', 'up_multiplier' => 2, 'down_multiplier' => 1], @@ -79,6 +79,9 @@ class Torrent extends NexusModel public function getSpStateRealAttribute() { + if ($this->getRawOriginal('sp_state') === null) { + throw new \RuntimeException('no select sp_state field'); + } $spState = $this->sp_state; $global = self::getGlobalPromotionState(); $log = sprintf('torrent: %s sp_state: %s, global sp state: %s', $this->id, $spState, $global); diff --git a/app/Models/User.php b/app/Models/User.php index 4c9a8c0d..e16212aa 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -348,7 +348,7 @@ class User extends Authenticatable public function updateWithModComment(array $update, $modComment) { if (!$this->exists) { - throw new \RuntimeException('This mehtod only works when user exists!'); + throw new \RuntimeException('This method only works when user exists!'); } //@todo how to do prepare bindings here ? $modComment = addslashes($modComment); diff --git a/app/Repositories/AgentAllowRepository.php b/app/Repositories/AgentAllowRepository.php index 2fb0f50f..9af45d38 100644 --- a/app/Repositories/AgentAllowRepository.php +++ b/app/Repositories/AgentAllowRepository.php @@ -1,7 +1,7 @@ matchCount if ($matchNum > $matchCount && !IN_NEXUS) { - throw new NexusException("pattern: $pattern match start: $start got matches count: $matchCount, but require $matchNum."); + throw new ClientNotAllowedException("pattern: $pattern match start: $start got matches count: $matchCount, but require $matchNum."); } return array_slice($matches, 1, $matchNum); } + /** + * @param $peerId + * @param $agent + * @param false $debug + * @return \App\Models\NexusModel|mixed + * @throws ClientNotAllowedException + */ public function checkClient($peerId, $agent, $debug = false) { //check from high version to low version, if high version allow, stop! @@ -93,7 +100,7 @@ class AgentAllowRepository extends BaseRepository } } catch (\Exception $exception) { do_log("$logPrefix, check peer_id error: " . $exception->getMessage(), 'error'); - throw new NexusException("regular expression err for peer_id: " . $start . ", please ask sysop to fix this"); + throw new ClientNotAllowedException("regular expression err for peer_id: " . $start . ", please ask sysop to fix this"); } if ($peerIdResult == 1) { $isPeerIdAllowed = true; @@ -121,7 +128,7 @@ class AgentAllowRepository extends BaseRepository } } catch (\Exception $exception) { do_log("$logPrefix, check agent error: " . $exception->getMessage(), 'error'); - throw new NexusException("regular expression err for agent: " . $start . ", please ask sysop to fix this"); + throw new ClientNotAllowedException("regular expression err for agent: " . $start . ", please ask sysop to fix this"); } if ($agentResult == 1) { $isAgentAllowed = true; @@ -142,11 +149,11 @@ class AgentAllowRepository extends BaseRepository } if ($versionTooLowStr) { - throw new NexusException($versionTooLowStr); + throw new ClientNotAllowedException($versionTooLowStr); } if (!$agentAllowPassed) { - throw new NexusException("Banned Client, Please goto " . getSchemeAndHttpHost() . "/faq.php#id29 for a list of acceptable clients"); + throw new ClientNotAllowedException("Banned Client, Please goto " . getSchemeAndHttpHost() . "/faq.php#id29 for a list of acceptable clients"); } if ($debug) { @@ -160,14 +167,14 @@ class AgentAllowRepository extends BaseRepository if ($debug) { do_log("agentDeny: " . $agentDeny->toJson()); } - throw new NexusException(sprintf( + throw new ClientNotAllowedException(sprintf( "[%s-%s]Client: %s is banned due to: %s", $agentAllowPassed->id, $agentDeny->id, $agentDeny->name, $agentDeny->comment )); } } if (isHttps() && $agentAllowPassed->allowhttps != 'yes') { - throw new NexusException(sprintf( + throw new ClientNotAllowedException(sprintf( "[%s]This client does not support https well, Please goto %s/faq.php#id29 for a list of proper clients", $agentAllowPassed->id, getSchemeAndHttpHost() )); @@ -202,7 +209,7 @@ class AgentAllowRepository extends BaseRepository * @param bool $debug * @param string $logPrefix * @return int - * @throws NexusException + * @throws ClientNotAllowedException */ private function isAllowed($pattern, $start, $matchNum, $matchType, $value, $debug = false, $logPrefix = ''): int { @@ -234,7 +241,7 @@ class AgentAllowRepository extends BaseRepository $matchBench[$i] = hexdec($matchBench[$i]); $matchTarget[$i] = hexdec($matchTarget[$i]); } else { - throw new NexusException(sprintf("Invalid match type: %s", $matchType)); + throw new ClientNotAllowedException(sprintf("Invalid match type: %s", $matchType)); } if ($matchTarget[$i] > $matchBench[$i]) { //higher, pass directly diff --git a/app/Repositories/TrackerRepository.php b/app/Repositories/TrackerRepository.php new file mode 100644 index 00000000..162bfa12 --- /dev/null +++ b/app/Repositories/TrackerRepository.php @@ -0,0 +1,712 @@ +checkAnnounceFields($request); + $clientAllow = $this->checkClient($request); + $user = $this->checkUser($request); + $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); + } + + $this->updatePeer($peerSelf, $queries); + $this->updateSnatch($peerSelf, $queries); + $this->updateTorrent($torrent, $queries); + + $dataTraffic = $this->getRealUploadedDownloaded($torrent, $queries, $user, $peerSelf); + $this->userUpdates['uploaded'] = DB::raw('uploaded + ' . $dataTraffic['uploaded']); + $this->userUpdates['downloaded'] = DB::raw('downloaded + ' . $dataTraffic['downloaded']); + $this->userUpdates['clientselect'] = $clientAllow->id; + $this->userUpdates['showclienterror'] = 'no'; + } + $repDict = $this->generateSuccessAnnounceResponse($torrent, $queries, $user, $withPeers); + } catch (ClientNotAllowedException $exception) { + do_log("[ClientNotAllowedException] " . $exception->getMessage()); + $this->userUpdates['showclienterror'] = 'yes'; + $repDict = $this->generateFailedAnnounceResponse($exception); + } catch (TrackerException $exception) { + do_log("[TrackerException] " . $exception->getMessage()); + $repDict = $this->generateFailedAnnounceResponse($exception); + } finally { + if (isset($user)) { + $user->update($this->userUpdates); + } + return $this->sendFinalAnnounceResponse($repDict); + } + } + + /** + * @param Request $request + * @throws ClientNotAllowedException + * @throws TrackerException + */ + 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(); + $agentAllowRep->checkClient($request->peer_id, $userAgent); + + } + + 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'); + } + + 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'); + + // Part.6 bin2hex info_hash + $queries['info_hash'] = \bin2hex($queries['info_hash']); + + // Part.7 bin2hex peer_id + $queries['peer_id'] = \bin2hex($queries['peer_id']); + + return $queries; + } + + protected function checkUser(Request $request) + { + if ($authkey = $request->query->get('authkey')) { + list($torrentId, $uid) = $this->checkAuthkey($authkey); + $field = 'id'; + $value = $uid; + } 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 + $torrent = Torrent::query() + ->selectRaw('id, owner, sp_state, seeders, leechers, added, banned, hr, visible, last_action, times_completed') + ->where('info_hash', '=', $queries['info_hash']) + ->first(); + + // 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') { + $elapsed = Carbon::now()->diffInSeconds($torrent->added); + 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']) + ->where('userid', $user->id) + ->first(); + + if ($peer && $peer->isValidDate('prev_action') && Carbon::now()->diffInSeconds($peer->prev_action) < self::MIN_ANNOUNCE_WAIT_SECOND) { + 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 + $repDict = [ + 'interval' => $this->getRealAnnounceInterval($torrent), + 'min interval' => self::MIN_ANNOUNCE_WAIT_SECOND, + 'complete' => (int) $torrent->seeders, + 'incomplete' => (int) $torrent->leechers, + 'peers' => [], + 'peers6' => [], + ]; + if (!$withPeers) { + return $repDict; + } + + /** + * For non `stopped` event only + * We query peers from database and send peer list, otherwise just quick return. + */ + if (\strtolower($queries['event']) !== 'stopped') { + $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(); + } + + $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; + } + + private function getRealUploadedDownloaded(Torrent $torrent, $queries, User $user, Peer $peer): array + { + if ($peer->exists) { + $realUploaded = max($queries['uploaded'] - $peer->uploaded, 0); + $realDownloaded = max($queries['downloaded'] - $peer->downloaded, 0); + } else { + $realUploaded = $queries['uploaded']; + $realDownloaded = $queries['downloaded']; + } + $spStateReal = $torrent->spStateReal; + $uploaderRatio = Setting::get('torrent.uploaderdouble'); + if ($torrent->owner == $user->id) { + //uploader, use the bigger one + $upRatio = max($uploaderRatio, Torrent::$promotionTypes[$spStateReal]['up_multiplier']); + } else { + $upRatio = Torrent::$promotionTypes[$spStateReal]['up_multiplier']; + } + $downRatio = Torrent::$promotionTypes[$spStateReal]['down_multiplier']; + return [ + 'uploaded' => $realUploaded * $upRatio, + 'downloaded' => $realDownloaded * $downRatio + ]; + } + + 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; + } + + protected function generateFailedAnnounceResponse(\Exception $exception): array + { + return [ + 'failure reason' => $exception->getMessage(), + '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(); + } + + private function updatePeer(Peer $peer, $queries) + { + if ($queries['event'] == 'stopped') { + return $peer->delete(); + } + + $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->prev_action = DB::raw('last_action'); + $peer->last_action = $nowStr; + $peer->uploaded = $queries['uploaded']; + $peer->downloaded = $queries['downloaded']; + + if ($queries['event'] == 'started') { + $peer->started = $nowStr; + $peer->uploadoffset = $queries['uploaded']; + $peer->downloadoffset = $queries['downloaded']; + } elseif ($queries['event'] == 'completed') { + $peer->finishat = time(); + } + + $peer->save(); + } + + private function updateSnatch(Peer $peer, $queries) + { + $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; + $snatch->uploaded = $queries['uploaded']; + $snatch->downloaded = $queries['downloaded']; + $snatch->startat = $nowStr; + } else { + //increase + $snatch->uploaded = DB::raw("uploaded + " . $queries['uploaded']); + $snatch->downloaded = DB::raw("downloaded + " . $queries['downloaded']); + $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']; + $snatch->to_go = $queries['to_go']; + $snatch->last_action = $nowStr; + if ($queries['event'] == 'completed') { + $snatch->completedat = $nowStr; + $snatch->finished = 'yes'; + } + + $snatch->save(); + } + +} diff --git a/bootstrap/app.php b/bootstrap/app.php index b4bff56a..d1cd7b21 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,5 @@ =7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -6070,6 +6317,100 @@ ], "time": "2021-12-27T21:01:00+00:00" }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "22b37c8a3f6b5d94e9cdbd88e1270d96e2f97b34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/22b37c8a3f6b5d94e9cdbd88e1270d96e2f97b34", + "reference": "22b37c8a3f6b5d94e9cdbd88e1270d96e2f97b34", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0", + "symfony/http-foundation": "^4.4 || ^5.0 || ^6.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0", + "symfony/config": "^4.4 || ^5.0 || ^6.0", + "symfony/event-dispatcher": "^4.4 || ^5.0 || ^6.0", + "symfony/framework-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/http-kernel": "^4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.4@dev || ^6.0" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-main": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/symfony/psr-http-message-bridge/issues", + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-05T13:13:39+00:00" + }, { "name": "symfony/routing", "version": "v5.4.0", @@ -9438,13 +9779,14 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.3|^8.0", + "php": "^8.0", + "ext-bcmath": "*", "ext-gd": "*", "ext-json": "*", "ext-mbstring": "*", "ext-mysqli": "*", - "ext-xml": "*", - "ext-bcmath": "*" + "ext-redis": "*", + "ext-xml": "*" }, "platform-dev": [], "plugin-api-version": "2.2.0" diff --git a/config/octane.php b/config/octane.php new file mode 100644 index 00000000..62829166 --- /dev/null +++ b/config/octane.php @@ -0,0 +1,220 @@ + env('OCTANE_SERVER', 'swoole'), + + /* + |-------------------------------------------------------------------------- + | Force HTTPS + |-------------------------------------------------------------------------- + | + | When this configuration value is set to "true", Octane will inform the + | framework that all absolute links must be generated using the HTTPS + | protocol. Otherwise your links may be generated using plain HTTP. + | + */ + + 'https' => env('OCTANE_HTTPS', false), + + /* + |-------------------------------------------------------------------------- + | Octane Listeners + |-------------------------------------------------------------------------- + | + | All of the event listeners for Octane's events are defined below. These + | listeners are responsible for resetting your application's state for + | the next request. You may even add your own listeners to the list. + | + */ + + 'listeners' => [ + WorkerStarting::class => [ + EnsureUploadedFilesAreValid::class, + EnsureUploadedFilesCanBeMoved::class, + ], + + RequestReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + ...Octane::prepareApplicationForNextRequest(), + // + ], + + RequestHandled::class => [ + // + ], + + RequestTerminated::class => [ + // + ], + + TaskReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TaskTerminated::class => [ + // + ], + + TickReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TickTerminated::class => [ + // + ], + + OperationTerminated::class => [ + FlushTemporaryContainerInstances::class, + // DisconnectFromDatabases::class, + // CollectGarbage::class, + ], + + WorkerErrorOccurred::class => [ + ReportException::class, + StopWorkerIfNecessary::class, + ], + + WorkerStopping::class => [ + // + ], + ], + + /* + |-------------------------------------------------------------------------- + | Warm / Flush Bindings + |-------------------------------------------------------------------------- + | + | The bindings listed below will either be pre-warmed when a worker boots + | or they will be flushed before every new request. Flushing a binding + | will force the container to resolve that binding again when asked. + | + */ + + 'warm' => [ + ...Octane::defaultServicesToWarm(), + ], + + 'flush' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Octane Cache Table + |-------------------------------------------------------------------------- + | + | While using Swoole, you may leverage the Octane cache, which is powered + | by a Swoole table. You may set the maximum number of rows as well as + | the number of bytes per row using the configuration options below. + | + */ + + 'cache' => [ + 'rows' => 1000, + 'bytes' => 10000, + ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Tables + |-------------------------------------------------------------------------- + | + | While using Swoole, you may define additional tables as required by the + | application. These tables can be used to store data that needs to be + | quickly accessed by other workers on the particular Swoole server. + | + */ + + 'tables' => [ + 'example:1000' => [ + 'name' => 'string:1000', + 'votes' => 'int', + ], + ], + + /* + |-------------------------------------------------------------------------- + | File Watching + |-------------------------------------------------------------------------- + | + | The following list of files and directories will be watched when using + | the --watch option offered by Octane. If any of the directories and + | files are changed, Octane will automatically reload your workers. + | + */ + + 'watch' => [ + 'app', + 'bootstrap', + 'config', + 'database', + 'public/**/*.php', + 'resources/**/*.php', + 'routes', + 'composer.lock', + '.env', + ], + + /* + |-------------------------------------------------------------------------- + | Garbage Collection Threshold + |-------------------------------------------------------------------------- + | + | When executing long-lived PHP scripts such as Octane, memory can build + | up before being cleared by PHP. You can force Octane to run garbage + | collection if your application consumes this amount of megabytes. + | + */ + + 'garbage' => 50, + + /* + |-------------------------------------------------------------------------- + | Maximum Execution Time + |-------------------------------------------------------------------------- + | + | The following setting configures the maximum execution time for requests + | being handled by Octane. You may set this value to 0 to indicate that + | there isn't a specific time limit on Octane request execution time. + | + */ + + 'max_execution_time' => 30, + +]; diff --git a/public/viewpeerlist.php b/public/viewpeerlist.php index ab326b56..b501f08a 100644 --- a/public/viewpeerlist.php +++ b/public/viewpeerlist.php @@ -3,9 +3,9 @@ require "../include/bittorrent.php"; dbconn(); require_once(get_langfile_path()); //Send some headers to keep the user's browser from caching the response. -header("Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); -header("Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . "GMT" ); -header("Cache-Control: no-cache, must-revalidate" ); +header("Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); +header("Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . "GMT" ); +header("Cache-Control: no-cache, must-revalidate" ); header("Pragma: no-cache" ); header("Content-Type: text/xml; charset=utf-8"); @@ -51,7 +51,7 @@ function dltable($name, $arr, $torrent) else $s .= "" . get_username($e['userid']); - $secs = max(1, ($e["la"] - $e["st"])); + $secs = max(1, ($e["la"] - $e["st"])); if ($enablelocation_tweak == 'yes'){ list($loc_pub, $loc_mod) = get_ip_location($e["ip"]); $location = get_user_class() >= $userprofile_class ? "
" . $loc_pub . "
" : $loc_pub;