346 lines
11 KiB
PHP
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)
|
|
];
|
|
}
|
|
}
|