mirror of
https://github.com/HDInnovations/UNIT3D-Community-Edition.git
synced 2026-01-24 04:50:01 -06:00
This stops announce from sending peers to the users client that are from the same user. This does not stop a user from manually adding their other peer to the client to seed to themselves. With that being said if a users is found to be seeding to themselves that means they are activly trying to break the rules and should be banned.
520 lines
22 KiB
PHP
520 lines
22 KiB
PHP
<?php
|
|
/**
|
|
* NOTICE OF LICENSE.
|
|
*
|
|
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
|
|
* The details is bundled with this project in the file LICENSE.txt.
|
|
*
|
|
* @project UNIT3D Community Edition
|
|
*
|
|
* @author HDVinnie <hdinnovations@protonmail.com>
|
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
|
|
*/
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Helpers\Bencode;
|
|
use App\Models\FreeleechToken;
|
|
use App\Models\Group;
|
|
use App\Models\History;
|
|
use App\Models\Peer;
|
|
use App\Models\PersonalFreeleech;
|
|
use App\Models\Torrent;
|
|
use App\Models\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class AnnounceController extends Controller
|
|
{
|
|
// Torrent Moderation Codes
|
|
protected const PENDING = 0;
|
|
protected const APPROVED = 1;
|
|
protected const REJECTED = 2;
|
|
protected const POSTPONED = 3;
|
|
|
|
// Announce Intervals
|
|
private const MIN = 2_400;
|
|
private const MAX = 3_600;
|
|
|
|
/**
|
|
* Announce Code.
|
|
*
|
|
* @param \Illuminate\Http\Request $request
|
|
* @param \App\Models\User $passkey
|
|
*
|
|
* @throws \Exception
|
|
*
|
|
* @return Bencode response for the torrent client
|
|
*/
|
|
public function announce(Request $request, $passkey)
|
|
{
|
|
// For Performance Logging Only!
|
|
/*\DB::listen(function($sql) {
|
|
\Log::info($sql->sql);
|
|
\Log::info($sql->bindings);
|
|
\Log::info($sql->time);
|
|
});*/
|
|
|
|
// Check Announce Request Method
|
|
$method = $request->method();
|
|
if (! $request->isMethod('get')) {
|
|
info('Announce Request Method Was Not GET');
|
|
|
|
return response(Bencode::bencode(['failure reason' => 'Invalid Request Type: Client Request Was Not A HTTP GET.']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// Request Agent Information
|
|
$agent = $request->server('HTTP_USER_AGENT');
|
|
|
|
// Blacklist
|
|
if (config('client-blacklist.enabled') == true) {
|
|
// Check If Browser Is Blacklisted
|
|
$blockedBrowsers = config('client-blacklist.browsers');
|
|
if (in_array($agent, $blockedBrowsers)) {
|
|
abort(405, 'What Are You Trying To Do?');
|
|
die();
|
|
}
|
|
|
|
// Check If Client Is Blacklisted
|
|
$blockedClients = config('client-blacklist.clients');
|
|
if (in_array($agent, $blockedClients)) {
|
|
//info('Blacklist Client Attempted To Connect To Announce');
|
|
return response(Bencode::bencode(['failure reason' => 'The Client You Are Trying To Use Has Been Blacklisted']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
}
|
|
|
|
// If Passkey Is Not Provided Return Error to Client
|
|
if ($passkey == null) {
|
|
//info('Client Attempted To Connect To Announce Without A Passkey');
|
|
return response(Bencode::bencode(['failure reason' => 'Please Call Passkey']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If Infohash Is Not Provided Return Error to Client
|
|
if (! $request->has('info_hash')) {
|
|
//info('Client Attempted To Connect To Announce Without A Infohash');
|
|
return response(Bencode::bencode(['failure reason' => 'Missing info_hash']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If Peerid Is Not Provided Return Error to Client
|
|
if (! $request->has('peer_id')) {
|
|
//info('Client Attempted To Connect To Announce Without A Peerid');
|
|
return response(Bencode::bencode(['failure reason' => 'Missing peer_id']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If Port Is Not Provided Return Error to Client
|
|
if (! $request->has('port')) {
|
|
//info('Client Attempted To Connect To Announce Without A Specified Port');
|
|
return response(Bencode::bencode(['failure reason' => 'Missing port']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If "Left" Is Not Provided Return Error to Client
|
|
if (! $request->has('left')) {
|
|
//info('Client Attempted To Connect To Announce Without Supplying Any "Left" Information');
|
|
return response(Bencode::bencode(['failure reason' => 'Missing left']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If "Upload" Is Not Provided Return Error to Client
|
|
if (! $request->has('uploaded')) {
|
|
//info('Client Attempted To Connect To Announce Without Supplying Any "Upload" Information');
|
|
return response(Bencode::bencode(['failure reason' => 'Missing upload']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If "Download" Is Not Provided Return Error to Client
|
|
if (! $request->has('downloaded')) {
|
|
//info('Client Attempted To Connect To Announce Without Supplying Any "Download" Information');
|
|
return response(Bencode::bencode(['failure reason' => 'Missing download']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// Check Passkey Against Cache or Users Table
|
|
$user = Cache::get(sprintf('user.%s', $passkey)) ?? User::where('passkey', '=', $passkey)->first();
|
|
|
|
// If Passkey Doesn't Exist Return Error to Client
|
|
if ($user === null) {
|
|
//info('Client Attempted To Connect To Announce With A Invalid Passkey');
|
|
return response(Bencode::bencode(['failure reason' => 'Passkey is invalid']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// Caached System Required Groups
|
|
$banned_group = cache()->rememberForever('banned_group', fn () => Group::where('slug', '=', 'banned')->pluck('id'));
|
|
$validating_group = cache()->rememberForever('validating_group', fn () => Group::where('slug', '=', 'validating')->pluck('id'));
|
|
$disabled_group = cache()->rememberForever('disabled_group', fn () => Group::where('slug', '=', 'disabled')->pluck('id'));
|
|
|
|
// If User Is Banned Return Error to Client
|
|
if ($user->group_id == $banned_group[0]) {
|
|
//info('A Banned User (' . $user->username . ') Attempted To Announce');
|
|
return response(Bencode::bencode(['failure reason' => 'You are no longer welcome here']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If User Is Disabled Return Error to Client
|
|
if ($user->group_id == $disabled_group[0]) {
|
|
//info('A Disabled User (' . $user->username . ') Attempted To Announce');
|
|
return response(Bencode::bencode(['failure reason' => 'Your account is disabled. Please login.']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If User Account Is Unactivated Return Error to Client
|
|
if ($user->active == 0) {
|
|
//info('A Unactivated User (' . $user->username . ') Attempted To Announce');
|
|
return response(Bencode::bencode(['failure reason' => 'Your account is not activated']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If User Is Validating Return Error to Client
|
|
if ($user->group_id == $validating_group[0]) {
|
|
//info('A Validating User (' . $user->username . ') Attempted To Announce');
|
|
return response(Bencode::bencode(['failure reason' => 'Your account is still validating']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// Standard Information Fields
|
|
$event = $request->input('event');
|
|
$info_hash = bin2hex($request->input('info_hash'));
|
|
$peer_id = bin2hex($request->input('peer_id'));
|
|
$md5_peer_id = md5($peer_id);
|
|
$ip = $request->ip();
|
|
$port = (int) $request->input('port');
|
|
$left = (int) $request->input('left');
|
|
$uploaded = (int) $request->input('uploaded');
|
|
$real_uploaded = $uploaded;
|
|
$downloaded = (int) $request->input('downloaded');
|
|
$real_downloaded = $downloaded;
|
|
|
|
//Extra Information Fields
|
|
$tracker_id = $request->has('trackerid') ? bin2hex($request->input('tracker_id')) : null;
|
|
$compact = $request->input('compact') == 1;
|
|
$key = $request->has('key') ? bin2hex($request->input('key')) : null;
|
|
$corrupt = $request->has('corrupt') ? $request->input('corrupt') : null;
|
|
$ipv6 = $request->has('ipv6') ? bin2hex($request->input('ipv6')) : null;
|
|
$no_peer_id = $request->input('no_peer_id') == 1;
|
|
|
|
// If User Download Rights Are Disabled Return Error to Client
|
|
if ($user->can_download == 0 && $left != 0) {
|
|
//info('A User With Revoked Download Privileges Attempted To Announce');
|
|
return response(Bencode::bencode(['failure reason' => 'Your download privileges are Revoked']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If User Client Is Sending Negative Values Return Error to Client
|
|
if ($uploaded < 0 || $downloaded < 0 || $left < 0) {
|
|
//info('Client Attempted To Send Data With A Negative Value');
|
|
return response(Bencode::bencode(['failure reason' => 'Data from client is a negative value']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If User Client Does Not Support Compact Return Error to Client
|
|
if (! $compact) {
|
|
//info('Client Attempted To Connect To Announce But Doesn't Support Compact');
|
|
return response(Bencode::bencode(['failure reason' => "Your client doesn't support compact, please update your client"]))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// Check Info Hash Against Cache or Torrents Table
|
|
$torrent = Cache::get(sprintf('torrent.%s', $info_hash)) ?? Torrent::select(['id', 'status', 'free', 'doubleup', 'times_completed', 'seeders', 'leechers'])->withAnyStatus()->where('info_hash', '=', $info_hash)->first();
|
|
|
|
// If Torrent Doesnt Exsist Return Error to Client
|
|
if ($torrent === null) {
|
|
//info('Client Attempted To Connect To Announce But The Torrent Doesn't Exist Using Hash ' . $info_hash);
|
|
return response(Bencode::bencode(['failure reason' => 'Torrent not found']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If Torrent Is Pending Moderation Return Error to Client
|
|
if ($torrent->status == self::PENDING) {
|
|
//info('Client Attempted To Connect To Announce But The Torrent Is Pending Moderation');
|
|
return response(Bencode::bencode(['failure reason' => 'Torrent is still pending moderation']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If Torrent Is Rejected Return Error to Client
|
|
if ($torrent->status == self::REJECTED) {
|
|
//info('Client Attempted To Connect To Announce But The Torrent Is Rejected');
|
|
return response(Bencode::bencode(['failure reason' => 'Torrent has been rejected']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// If Torrent Is Postponed Return Error to Client
|
|
if ($torrent->status == self::POSTPONED) {
|
|
//info('Client Attempted To Connect To Announce But The Torrent Is Postponed');
|
|
return response(Bencode::bencode(['failure reason' => 'Torrent has been postponed']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// Get Torrents Peers
|
|
$peers = Peer::where('torrent_id', '=', $torrent->id)->where('user_id', '!=', $user->id)->take(50)->get()->toArray();
|
|
|
|
// Pull Count On Users Peers Per Torrent For Rate Limiting
|
|
$connections = Peer::where('torrent_id', '=', $torrent->id)->where('user_id', '=', $user->id)->count();
|
|
|
|
// If Users Peer Count On A Single Torrent Is Greater Than X Return Error to Client
|
|
if ($connections > config('announce.rate_limit')) {
|
|
//info('Client Attempted To Connect To Announce But Has Hit Rate Limits');
|
|
return response(Bencode::bencode(['failure reason' => 'You have reached the rate limit']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// Get The Current Peer
|
|
$peer = Peer::where('torrent_id', '=', $torrent->id)->where('md5_peer_id', $md5_peer_id)->where('user_id', '=', $user->id)->first();
|
|
|
|
// Flag is tripped if new session is created but client reports up/down > 0
|
|
$ghost = false;
|
|
if ($peer === null && $event == 'completed') {
|
|
return response(Bencode::bencode(['failure reason' => 'Torrent is complete but no record found.']))->withHeaders(['Content-Type' => 'text/plain']);
|
|
}
|
|
|
|
// Creates a new peer if not existing
|
|
if ($peer === null) {
|
|
if ($uploaded > 0 || $downloaded > 0) {
|
|
$ghost = true;
|
|
$event = 'started';
|
|
}
|
|
$peer = new Peer();
|
|
}
|
|
|
|
// Get history information
|
|
$history = History::where('info_hash', '=', $info_hash)->where('user_id', '=', $user->id)->first();
|
|
|
|
// If no History record found then create one
|
|
if ($history === null) {
|
|
$history = new History();
|
|
$history->user_id = $user->id;
|
|
$history->info_hash = $info_hash;
|
|
}
|
|
|
|
if ($ghost) {
|
|
$uploaded = ($real_uploaded >= $history->client_uploaded) ? ($real_uploaded - $history->client_uploaded) : 0;
|
|
$downloaded = ($real_downloaded >= $history->client_downloaded) ? ($real_downloaded - $history->client_downloaded) : 0;
|
|
} else {
|
|
$uploaded = ($real_uploaded >= $peer->uploaded) ? ($real_uploaded - $peer->uploaded) : 0;
|
|
$downloaded = ($real_downloaded >= $peer->downloaded) ? ($real_downloaded - $peer->downloaded) : 0;
|
|
}
|
|
|
|
$old_update = $peer->updated_at ? $peer->updated_at->timestamp : Carbon::now()->timestamp;
|
|
|
|
// Modification of Upload and Download
|
|
$personal_freeleech = PersonalFreeleech::where('user_id', '=', $user->id)->first();
|
|
$freeleech_token = FreeleechToken::where('user_id', '=', $user->id)->where('torrent_id', '=', $torrent->id)->first();
|
|
$group = Group::whereId($user->group_id)->first();
|
|
|
|
if (config('other.freeleech') == 1 || $torrent->free == 1 || $personal_freeleech || $group->is_freeleech == 1 || $freeleech_token) {
|
|
$mod_downloaded = 0;
|
|
} else {
|
|
$mod_downloaded = $downloaded;
|
|
}
|
|
|
|
if (config('other.doubleup') == 1 || $torrent->doubleup == 1 || $group->is_double_upload == 1) {
|
|
$mod_uploaded = $uploaded * 2;
|
|
} else {
|
|
$mod_uploaded = $uploaded;
|
|
}
|
|
|
|
if ($event == 'started') {
|
|
// Peer Update
|
|
$peer->peer_id = $peer_id;
|
|
$peer->md5_peer_id = $md5_peer_id;
|
|
$peer->info_hash = $info_hash;
|
|
$peer->ip = $request->ip();
|
|
$peer->port = $port;
|
|
$peer->agent = $agent;
|
|
$peer->uploaded = $real_uploaded;
|
|
$peer->downloaded = $real_downloaded;
|
|
$peer->seeder = $left == 0;
|
|
$peer->left = $left;
|
|
$peer->torrent_id = $torrent->id;
|
|
$peer->user_id = $user->id;
|
|
$peer->save();
|
|
// End Peer Update
|
|
|
|
// History Update
|
|
$history->agent = $agent;
|
|
$history->active = 1;
|
|
$history->seeder = $left == 0;
|
|
$history->immune = $user->group->is_immune == 1;
|
|
$history->uploaded += 0;
|
|
$history->actual_uploaded += 0;
|
|
$history->client_uploaded = $real_uploaded;
|
|
$history->downloaded += 0;
|
|
$history->actual_downloaded += 0;
|
|
$history->client_downloaded = $real_downloaded;
|
|
$history->save();
|
|
// End History Update
|
|
|
|
// Never push stats to users account on start event
|
|
} elseif ($event == 'completed') {
|
|
// Peer Update
|
|
$peer->peer_id = $peer_id;
|
|
$peer->md5_peer_id = $md5_peer_id;
|
|
$peer->info_hash = $info_hash;
|
|
$peer->ip = $request->ip();
|
|
$peer->port = $port;
|
|
$peer->agent = $agent;
|
|
$peer->uploaded = $real_uploaded;
|
|
$peer->downloaded = $real_downloaded;
|
|
$peer->seeder = 1;
|
|
$peer->left = 0;
|
|
$peer->torrent_id = $torrent->id;
|
|
$peer->user_id = $user->id;
|
|
$peer->save();
|
|
// End Peer Update
|
|
|
|
// History Update
|
|
$history->agent = $agent;
|
|
$history->active = 1;
|
|
$history->seeder = $left == 0;
|
|
$history->immune = $user->group->is_immune == 1;
|
|
$history->uploaded += $mod_uploaded;
|
|
$history->actual_uploaded += $uploaded;
|
|
$history->client_uploaded = $real_uploaded;
|
|
$history->downloaded += $mod_downloaded;
|
|
$history->actual_downloaded += $downloaded;
|
|
$history->client_downloaded = $real_downloaded;
|
|
$history->completed_at = Carbon::now();
|
|
// Seedtime Allocation
|
|
if ($left == 0) {
|
|
$new_update = $peer->updated_at->timestamp;
|
|
$diff = $new_update - $old_update;
|
|
$history->seedtime += $diff;
|
|
}
|
|
$history->save();
|
|
// End History Update
|
|
|
|
// User Update
|
|
$user->uploaded += $mod_uploaded;
|
|
$user->downloaded += $mod_downloaded;
|
|
$user->save();
|
|
// End User Update
|
|
|
|
// Torrent Completed Update
|
|
$torrent->times_completed++;
|
|
// End Torrent Completed Update
|
|
} elseif ($event == 'stopped') {
|
|
//Peer Update
|
|
$peer->peer_id = $peer_id;
|
|
$peer->md5_peer_id = $md5_peer_id;
|
|
$peer->info_hash = $info_hash;
|
|
$peer->ip = $request->ip();
|
|
$peer->port = $port;
|
|
$peer->agent = $agent;
|
|
$peer->uploaded = $real_uploaded;
|
|
$peer->downloaded = $real_downloaded;
|
|
$peer->seeder = $left == 0;
|
|
$peer->left = $left;
|
|
$peer->torrent_id = $torrent->id;
|
|
$peer->user_id = $user->id;
|
|
$peer->save();
|
|
//End Peer Update
|
|
|
|
// History Update
|
|
$history->agent = $agent;
|
|
$history->active = 0;
|
|
$history->seeder = $left == 0;
|
|
$history->immune = $user->group->is_immune == 1;
|
|
$history->uploaded += $mod_uploaded;
|
|
$history->actual_uploaded += $uploaded;
|
|
$history->client_uploaded = 0;
|
|
$history->downloaded += $mod_downloaded;
|
|
$history->actual_downloaded += $downloaded;
|
|
$history->client_downloaded = 0;
|
|
// Seedtime allocation
|
|
if ($left == 0) {
|
|
$new_update = $peer->updated_at->timestamp;
|
|
$diff = $new_update - $old_update;
|
|
$history->seedtime += $diff;
|
|
}
|
|
$history->save();
|
|
// End History Update
|
|
|
|
// Peer Delete (Now that history is updated)
|
|
$peer->delete();
|
|
// End Peer Delete
|
|
|
|
// User Update
|
|
$user->uploaded += $mod_uploaded;
|
|
$user->downloaded += $mod_downloaded;
|
|
$user->save();
|
|
// End User Update
|
|
} else {
|
|
// Peer Update
|
|
$peer->peer_id = $peer_id;
|
|
$peer->md5_peer_id = $md5_peer_id;
|
|
$peer->info_hash = $info_hash;
|
|
$peer->ip = $request->ip();
|
|
$peer->port = $port;
|
|
$peer->agent = $agent;
|
|
$peer->uploaded = $real_uploaded;
|
|
$peer->downloaded = $real_downloaded;
|
|
$peer->seeder = $left == 0;
|
|
$peer->left = $left;
|
|
$peer->torrent_id = $torrent->id;
|
|
$peer->user_id = $user->id;
|
|
$peer->save();
|
|
// End Peer Update
|
|
|
|
// History Update
|
|
$history->agent = $agent;
|
|
$history->active = 1;
|
|
$history->seeder = $left == 0;
|
|
$history->uploaded += $mod_uploaded;
|
|
$history->actual_uploaded += $uploaded;
|
|
$history->client_uploaded = $real_uploaded;
|
|
$history->downloaded += $mod_downloaded;
|
|
$history->actual_downloaded += $downloaded;
|
|
$history->client_downloaded = $real_downloaded;
|
|
// Seedtime allocation
|
|
if ($left == 0) {
|
|
$new_update = $peer->updated_at->timestamp;
|
|
$diff = $new_update - $old_update;
|
|
$history->seedtime += $diff;
|
|
}
|
|
$history->save();
|
|
// End History Update
|
|
|
|
// User Update
|
|
$user->uploaded += $mod_uploaded;
|
|
$user->downloaded += $mod_downloaded;
|
|
$user->save();
|
|
// End User Update
|
|
}
|
|
|
|
// Torrent Update
|
|
$torrent->seeders = Peer::where('torrent_id', '=', $torrent->id)->where('left', '=', '0')->count();
|
|
$torrent->leechers = Peer::where('torrent_id', '=', $torrent->id)->where('left', '>', '0')->count();
|
|
$torrent->save();
|
|
// End Torrent Update
|
|
|
|
// Build Response For Bittorrent Client
|
|
$res = [];
|
|
$res['interval'] = rand(self::MIN, self::MAX);
|
|
$res['min interval'] = self::MIN;
|
|
$res['tracker_id'] = $md5_peer_id; // A string that the client should send back on its next announcements.
|
|
$res['complete'] = $torrent->seeders;
|
|
$res['incomplete'] = $torrent->leechers;
|
|
$res['peers'] = $this->givePeers($peers, $compact, $no_peer_id, FILTER_FLAG_IPV4);
|
|
$res['peers6'] = $this->givePeers($peers, $compact, $no_peer_id, FILTER_FLAG_IPV6);
|
|
|
|
return response(Bencode::bencode($res))->withHeaders(['Content-Type' => 'text/plain']);
|
|
// End Build Response For Bittorrent Client
|
|
}
|
|
|
|
/**
|
|
* @param $peers
|
|
* @param $compact
|
|
* @param $no_peer_id
|
|
* @param $filter_flag
|
|
*
|
|
* @return string
|
|
*/
|
|
private function givePeers($peers, $compact, $no_peer_id, $filter_flag = FILTER_FLAG_IPV4)
|
|
{
|
|
if ($compact) {
|
|
$pcomp = '';
|
|
foreach ($peers as &$p) {
|
|
if (isset($p['ip']) && isset($p['port']) && filter_var($p['ip'], FILTER_VALIDATE_IP, $filter_flag)) {
|
|
$pcomp .= inet_pton($p['ip']);
|
|
$pcomp .= pack('n', (int) $p['port']);
|
|
}
|
|
}
|
|
|
|
return $pcomp;
|
|
}
|
|
if ($no_peer_id) {
|
|
foreach ($peers as &$p) {
|
|
unset($p['peer_id']);
|
|
}
|
|
|
|
return $peers;
|
|
}
|
|
|
|
return $peers;
|
|
}
|
|
}
|