demo/app/Modules/Tracker/Actions/AnnounceAction.php
2023-03-23 18:50:47 +01:00

346 lines
11 KiB
PHP

<?php
namespace LaraBB\Tracker\Actions;
use Illuminate\Database\Eloquent\Collection;
use LaraBB\Tracker\Traits\SetCurrentRequestTrait;
use Exception;
use JetBrains\PhpStorm\ArrayShape;
use LaraBB\Peer\Models\Peer;
use LaraBB\Peer\Tasks\DestroyTask as DestroyPeerTask;
use LaraBB\Peer\Tasks\FindTask as FindPeerTask;
use LaraBB\Peer\Tasks\StoreTask as StorePeerTask;
use LaraBB\Peer\Tasks\UpdateTask as UpdatePeerTask;
use LaraBB\Profile\Tasks\UpdateTask as UpdateProfileTask;
use LaraBB\Torrent\Models\Torrent;
use LaraBB\Torrent\Tasks\FindTask as FindTorrentTask;
use LaraBB\Torrent\Tasks\UpdateTask as UpdateTorrentTask;
use LaraBB\Tracker\UI\Web\Requests\Announce;
use LaraBB\User\Models\User;
use LaraBB\User\Tasks\FindTask as FindUserTask;
use Log;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Rhilip\Bencode\Bencode;
use stdClass;
/**
*
*/
class AnnounceAction
{
use SetCurrentRequestTrait;
/**
* @var User|null
*/
private ?User $user;
/**
* @var Peer|null
*/
private ?Peer $peer;
/**
* @var Torrent|null
*/
private ?Torrent $torrent;
/**
* @var stdClass
*/
private stdClass $clientRequest;
/**
* @var Announce
*/
private Announce $request;
/**
* @var string
*/
private string $browsers = 'Mozilla|Chrome|Opera|Lynx|Trident|Edge';
/**
* @param FindPeerTask $findPeerTask
* @param StorePeerTask $storePeerTask
* @param UpdatePeerTask $updatePeerTask
* @param DestroyPeerTask $destroyPeerTask
* @param FindUserTask $findUserTask
* @param UpdateProfileTask $updateProfileTask
* @param FindTorrentTask $findTorrentTask
* @param UpdateTorrentTask $updateTorrentTask
*/
public function __construct(
private readonly FindPeerTask $findPeerTask,
private readonly StorePeerTask $storePeerTask,
private readonly UpdatePeerTask $updatePeerTask,
private readonly DestroyPeerTask $destroyPeerTask,
private readonly FindUserTask $findUserTask,
private readonly UpdateProfileTask $updateProfileTask,
private readonly FindTorrentTask $findTorrentTask,
private readonly UpdateTorrentTask $updateTorrentTask
) {
}
/**
* @param Announce $request
* @return false|string
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function run(Announce $request): bool|string
{
$this->clientRequest = $this->setCurrentRequest($request);
$this->request = $request;
if (!$this->isTorrentClient()) {
return false;
}
$this->isFirewalled();
$this->user = $this->findUserTask->byPid($this->clientRequest->pid, ['profile']);
$this->torrent = $this->findTorrentTask->byInfoHash($this->clientRequest->infoHash);
$this->peer = $this->findPeerTask->byPeerIdAndTorrentUuid($this->clientRequest->peerId, $this->torrent->uuid);
if (is_null($this->peer)) {
$this->peer = $this->storePeerTask->run($this->preparePeerData());
}
switch ($this->clientRequest->event) {
case 'started':
$this->started();
break;
case 'stopped':
$this->stopped();
break;
case 'finished':
$this->finished();
break;
case empty($this->currentRequest->event):
$this->noEvent();
break;
}
return Bencode::encode([
'interval' => config('tracker.max_announce'),
'min interval' => config('tracker.max_announce'),
'complete' => $this->torrent->seeder,
'incomplete' => $this->torrent->leecher,
'peers' => (function($peerReturn) {
if($this->clientRequest->compact) {
return $peerReturn;
}
$peers = [];
$peerReturn->map(function(Peer $peer) use(&$peers) {
$peers[] = [
'peer id' => $peer->peer_id,
'ip' => $peer->ip,
'port' => $peer->port
];
});
return $peers;
})($this->sendRandomPeers())
]);
}
/**
* @return bool
*/
private function isTorrentClient(): bool
{
return !preg_match('/' . $this->browsers . '/', $this->clientRequest->agent);
}
/**
*
*/
public function isFirewalled(): void
{
$theError = '';
$fd = @fsockopen($this->clientRequest->ip, $this->clientRequest->port, $errno, $theError, 10);
$this->clientRequest->nat = !$fd;
if($fd) fclose($fd);
}
/**
* @return void
*/
private function started(): void
{
$this->updatePeerTask->run($this->peer, $this->preparePeerData('started'));
$this->updateTorrentTask->run($this->torrent, $this->prepareTorrentData('started'));
$this->updateProfileTask->run($this->user->profile, $this->prepareProfileData());
}
/**
* @return void
* @throws Exception
*/
private function stopped(): void
{
$this->destroyPeerTask->run($this->peer);
$this->updateTorrentTask->run($this->torrent, $this->prepareTorrentData('stopped'));
$this->updateProfileTask->run($this->user->profile, $this->prepareProfileData());
}
/**
* @return void
*/
private function finished(): void
{
$this->updatePeerTask->run($this->peer, $this->preparePeerData('finished'));
$this->updateTorrentTask->run($this->torrent, $this->prepareTorrentData('finished'));
$this->updateProfileTask->run($this->user->profile, $this->prepareProfileData());
}
/**
* @return void
*/
private function noEvent(): void
{
if ($this->peer->bytes != 0 && $this->clientRequest->left == 0) {
$this->updatePeerTask->run($this->peer, $this->preparePeerData('finished'));
$this->updateTorrentTask->run($this->torrent, $this->prepareTorrentData('finished'));
}
$this->collectBytes();
}
/**
* @return void
*/
private function collectBytes(): void
{
$diff = bcsub($this->peer->bytes, $this->clientRequest->left);
$this->updatePeerTask->run($this->peer, $this->preparePeerData('bytes', $diff));
}
/**
* @return string|null|Collection
*/
private function sendRandomPeers(): Collection|string|null
{
$peerCollection = $this->findPeerTask->byTorrentUuid($this->torrent->uuid);
if (!is_null($peerCollection)) {
if($this->clientRequest->compact) {
$compactStr = '';
$peerCollection->map(function (Peer $peer) use (&$compactStr) {
if($peer->created_uuid == $this->user->uuid) return;
$compactStr .= str_pad(pack('Nn', ip2long($peer->ip), $peer->port), 6);
});
return $compactStr;
}
return $peerCollection;
}
return null;
}
/**
* @param string $status
* @param float $diff
* @return array
*/
private function preparePeerData(string $status = '', float $diff = 0): array
{
$data = [
'created_uuid' => $this->user->uuid,
'updated_uuid' => $this->user->uuid,
'torrent_uuid' => $this->torrent->uuid,
'peer_id' => $this->clientRequest->peerId,
'ip' => $this->clientRequest->ip,
'port' => $this->clientRequest->port,
'status' => $this->clientRequest->left == 0 ? 1 : 2,
'natuser' => $this->clientRequest->nat,
'client' => $this->clientRequest->agent,
'dns' => $this->clientRequest->dns,
'downloaded' => $this->clientRequest->downloaded,
'uploaded' => $this->clientRequest->uploaded,
];
switch ($status) {
case 'started':
unset($data['peer_id'], $data['torrent_uuid'], $data['ip'], $data['port'], $data['natuser'], $data['client'], $data['dns']);
break;
case 'finished':
unset($data['peer_id'], $data['torrent_uuid'], $data['ip'], $data['port'], $data['natuser'], $data['client'], $data['dns'], $data['compact']);
$data['bytes'] = 0;
$data['status'] = 'seeder';
break;
case 'bytes':
unset($data['peer_id'], $data['torrent_uuid'], $data['ip'], $data['port'], $data['status'], $data['natuser'], $data['client'], $data['dns'], $data['compact']);
$data['bytes'] = empty($diff) ? $this->peer->bytes : $this->clientRequest->left;
break;
}
return $data;
}
/**
* @param string $status
* @return array
*/
private function prepareTorrentData(string $status = ''): array
{
switch ($status) {
case 'started':
$this->clientRequest->left > 0 ? $this->torrent->leecher++ : $this->torrent->seeder++;
return [
'leecher' => $this->torrent->leecher,
'seeder' => $this->torrent->seeder,
'finished' => $this->torrent->finished,
];
case 'finished':
return [
'leecher' => $this->torrent->leecher--,
'seeder' => $this->torrent->seeder++,
'finished' => $this->torrent->finished++,
];
case 'stopped':
$this->clientRequest->left > 0 ? $this->torrent->leecher-- : $this->torrent->seeder--;
return [
'leecher' => $this->torrent->leecher,
'seeder' => $this->torrent->seeder,
'finished' => $this->torrent->finished,
];
}
return [];
}
/**
* @return array
*/
#[ArrayShape([
'uploaded' => "mixed",
'downloaded' => "mixed",
'ratio' => "float|int"
])]
private function prepareProfileData(): array
{
$uploaded = $this->user->profile->uploaded + $this->clientRequest->uploaded;
$downloaded = $this->user->profile->downloaded + $this->clientRequest->downloaded;
return [
'uploaded' => $uploaded,
'downloaded' => $downloaded,
'ratio' => $uploaded == 0 ? 0 : ($downloaded / $uploaded)
];
}
}