Files
nexusphp/app/Repositories/SeedBoxRepository.php

388 lines
14 KiB
PHP
Raw Normal View History

<?php
namespace App\Repositories;
2025-05-11 02:33:22 +07:00
use App\Enums\SeedBoxRecord\IpAsnEnum;
use App\Enums\SeedBoxRecord\IsAllowedEnum;
use App\Enums\SeedBoxRecord\TypeEnum;
2022-07-23 15:05:32 +08:00
use App\Exceptions\InsufficientPermissionException;
use App\Models\Message;
use App\Models\Poll;
use App\Models\SeedBoxRecord;
use App\Models\User;
2025-05-11 02:33:22 +07:00
use GeoIp2\Database\Reader;
use Illuminate\Support\Arr;
2022-07-23 15:05:32 +08:00
use Illuminate\Support\Facades\Auth;
2025-05-11 02:33:22 +07:00
use MaxMind\Db\Reader\InvalidDatabaseException;
2022-07-23 15:05:32 +08:00
use Nexus\Database\NexusDB;
use PhpIP\IP;
use PhpIP\IPBlock;
class SeedBoxRepository extends BaseRepository
{
2025-05-11 02:33:22 +07:00
const IS_SEED_BOX_FROM_USER_RECORD_CACHE_PREFIX = "IS_SEED_BOX_FROM_USER_RECORD";
const APPROVAL_COUNT_CACHE_KEY = "SEED_BOX_RECORD_APPROVAL_NONE";
private static ?Reader $asnReader = null;
public function getList(array $params)
{
$query = Poll::query();
list($sortField, $sortType) = $this->getSortFieldAndType($params);
$query->orderBy($sortField, $sortType);
return $query->paginate();
}
2022-07-23 15:05:32 +08:00
/**
* @param array $params
* @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model
*/
public function store(array $params)
2022-07-23 15:05:32 +08:00
{
$params = $this->formatParams($params);
$seedBoxRecord = SeedBoxRecord::query()->create($params);
2025-05-11 02:33:22 +07:00
$this->clearApprovalCountCache();
2024-11-15 21:43:46 +08:00
publish_model_event("seed_box_record_created", $seedBoxRecord->id);
2022-07-23 15:05:32 +08:00
return $seedBoxRecord;
}
private function formatParams(array $params): array
{
2024-11-19 01:16:54 +08:00
$params = array_filter($params);
if (
!empty($params['ip'])
&& empty($params['ip_begin'])
&& empty($params['ip_end'])
&& empty($params['asn'])
) {
2022-07-23 15:05:32 +08:00
try {
$ipBlock = IPBlock::create($params['ip']);
$params['ip_begin_numeric'] = $ipBlock->getFirstIp()->numeric();
$params['ip_end_numeric'] = $ipBlock->getLastIp()->numeric();
2022-07-23 15:05:32 +08:00
$params['version'] = $ipBlock->getVersion();
} catch (\Exception $exception) {
do_log("[NOT_IP_BLOCK], {$params['ip']}" . $exception->getMessage());
}
2022-07-23 15:05:32 +08:00
if (empty($params['version'])) {
try {
$ip = IP::create($params['ip']);
$params['ip_begin_numeric'] = $ip->numeric();
$params['ip_end_numeric'] = $ip->numeric();
$params['version'] = $ip->getVersion();
} catch (\Exception $exception) {
do_log("[NOT_IP], {$params['ip']}" . $exception->getMessage());
}
}
if (empty($params['version'])) {
throw new \InvalidArgumentException("Invalid IPBlock or IP: " . $params['ip']);
}
2024-11-19 01:16:54 +08:00
} elseif (
empty($params['ip'])
&& empty($params['asn'])
&& !empty($params['ip_begin'])
&& !empty($params['ip_end'])
) {
$ipBegin = IP::create($params['ip_begin']);
$params['ip_begin_numeric'] = $ipBegin->numeric();
$ipEnd = IP::create($params['ip_end']);
$params['ip_end_numeric'] = $ipEnd->numeric();
2022-07-23 15:05:32 +08:00
if ($ipBegin->getVersion() != $ipEnd->getVersion()) {
throw new \InvalidArgumentException("ip_begin/ip_end must be the same version");
}
$params['version'] = $ipEnd->getVersion();
2024-11-19 01:16:54 +08:00
} elseif (
!empty($params['asn'])
&& empty($params['ip'])
&& empty($params['ip_begin'])
&& empty($params['ip_end'])
) {
do_log("only asn: " . $params['asn']);
} else {
2022-08-10 17:38:05 +08:00
throw new \InvalidArgumentException(nexus_trans('label.seed_box_record.ip_help'));
}
2022-07-23 15:05:32 +08:00
return $params;
}
public function update(array $params, $id)
{
2022-07-23 15:05:32 +08:00
$model = SeedBoxRecord::query()->findOrFail($id);
$params = $this->formatParams($params);
$model->update($params);
2025-05-11 02:33:22 +07:00
$this->clearApprovalCountCache();
2024-11-15 21:43:46 +08:00
publish_model_event("seed_box_record_updated", $id);
return $model;
}
public function getDetail($id)
{
$model = Poll::query()->findOrFail($id);
return $model;
}
public function delete($id, $uid)
{
2025-09-08 03:05:55 +07:00
$baseQuery = SeedBoxRecord::query()->whereIn('id', Arr::wrap($id))->where('uid', $uid);
$list = $baseQuery->clone()->get();
if ($list->isEmpty()) {
return false;
}
$baseQuery->delete();
2025-05-11 02:33:22 +07:00
$this->clearApprovalCountCache();
2025-09-08 03:05:55 +07:00
foreach ($list as $record) {
publish_model_event("seed_box_record_deleted", $record->id, $record->toJson());
}
return true;
}
2025-06-29 20:47:23 +07:00
public function updateStatus(SeedBoxRecord $seedBoxRecord, $status, $reason = '')
2022-07-23 15:05:32 +08:00
{
if (Auth::user()->class < User::CLASS_ADMINISTRATOR) {
throw new InsufficientPermissionException();
}
if (!isset(SeedBoxRecord::$status[$status])) {
throw new \InvalidArgumentException("Invalid status: $status");
}
if ($seedBoxRecord->status == $status) {
return true;
}
$message = [
'receiver' => $seedBoxRecord->uid,
'subject' => nexus_trans('seed-box.status_change_message.subject'),
'msg' => nexus_trans('seed-box.status_change_message.body', [
'id' => $seedBoxRecord->id,
'operator' => Auth::user()->username,
'old_status' => $seedBoxRecord->statusText,
'new_status' => nexus_trans('seed-box.status_text.' . $status),
'reason' => $reason,
2022-07-23 15:05:32 +08:00
]),
'added' => now()
];
return NexusDB::transaction(function () use ($seedBoxRecord, $status, $message) {
$seedBoxRecord->status = $status;
$seedBoxRecord->save();
2025-05-11 02:33:22 +07:00
$this->clearApprovalCountCache();
2022-07-23 15:05:32 +08:00
return Message::add($message);
});
}
2022-09-02 19:49:41 +08:00
public function renderIcon($ipArr, $uid): string
2022-07-23 15:05:32 +08:00
{
2022-09-02 19:49:41 +08:00
static $enableSeedBox;
if ($enableSeedBox === null) {
$enableSeedBox = get_setting('seed_box.enabled') == 'yes';
2022-07-23 15:05:32 +08:00
}
2022-09-02 19:49:41 +08:00
foreach (Arr::wrap($ipArr) as $ip) {
if ((isIPV4($ip) || isIPV6($ip)) && $enableSeedBox && isIPSeedBox($ip, $uid)) {
2022-09-12 20:00:07 +08:00
return $this->getSeedBoxIcon();
2022-09-02 19:49:41 +08:00
}
}
return '';
2022-07-23 15:05:32 +08:00
}
2022-09-12 20:00:07 +08:00
public function getSeedBoxIcon($isSeedBox = true): string
{
if (!$isSeedBox) {
return '';
}
return '<img src="pic/misc/seed-box.png" style="vertical-align: bottom; height: 16px; margin-left: 4px" title="SeedBox" />';
}
2025-05-11 02:33:22 +07:00
private function clearApprovalCountCache(): void
{
NexusDB::cache_del(self::APPROVAL_COUNT_CACHE_KEY);
}
public function updateUserCacheCronjob(IsAllowedEnum $isAllowed, IpAsnEnum $field): void
{
$size = 1000;
$page = 1;
$logPrefix = "isAllowed: $isAllowed->name, field: $field->name, page: $page, size: $size";
$selectRaw = sprintf("uid, group_concat(%s) as str", $field == IpAsnEnum::ASN ? 'asn' : 'ip');
while (true) {
$list = SeedBoxRecord::getValidQuery(TypeEnum::USER, $isAllowed, $field)
->selectRaw($selectRaw)
->groupBy('uid')
->forPage($page, $size)
->get();
if ($list->isEmpty()) {
do_log("$logPrefix, no more data ...");
break;
}
foreach ($list as $record) {
$uid = $record->uid;
$str = $record->str;
do_log("$logPrefix, handling user: $uid with $field->name: $str");
self::updateCache($record->uid, TypeEnum::USER, $isAllowed, $field, $str);
do_log("$logPrefix, handling user: $uid with $field->name: $str done!");
}
$page++;
}
do_log("$logPrefix, all done!");
}
public function updateAdminCacheCronjob(IsAllowedEnum $isAllowed, IpAsnEnum $field): void
{
$size = 1000;
$page = 1;
$logPrefix = "isAllowed: $isAllowed->name, field: $field->name, page: $page, size: $size";
$fieldName = $field == IpAsnEnum::ASN ? 'asn' : 'ip';
while (true) {
$list = SeedBoxRecord::getValidQuery(TypeEnum::ADMIN, $isAllowed, $field)
->selectRaw($fieldName)
->forPage($page, $size)
->get();
if ($list->isEmpty()) {
do_log("$logPrefix, no more data ...");
break;
}
self::updateCache(0, TypeEnum::ADMIN, $isAllowed, $field, $list->pluck($fieldName)->join(","));
$page++;
}
do_log("$logPrefix, all done!");
}
public static function updateCache(int $userId, TypeEnum $type, IsAllowedEnum $isAllowed, IpAsnEnum $field, string $ipOrAsnStr = null): void
{
if (!is_null($ipOrAsnStr)) {
$list = explode(',', $ipOrAsnStr);
} else {
$query = SeedBoxRecord::getValidQuery($type, $isAllowed, $field);
if ($userId > 0) {
$query->where('uid', $userId);
}
if ($field == IpAsnEnum::IP) {
$list = $query->pluck('ip')->toArray();
} else {
$list = $query->pluck('asn')->toArray();
}
}
$list = array_filter($list);
$key = self::getCacheKey($userId, $isAllowed, $field);
do_log("userId: $userId, type: $type->name, isAllowed: $isAllowed->name, ipOrAsn: $field->name, key: $key, list: " . json_encode($list));
NexusDB::cache_del($key);
if (!empty($list)) {
NexusDB::redis()->sadd($key, ...$list);
NexusDB::redis()->expireAt($key, time() + 86400 * 30);
}
}
public static function isSeedBoxFromUserRecords(int $userId, string $ip): array
{
$logPrefix = "userId: $userId, ip: $ip";
$redis = NexusDB::redis();
//first check from asn field
$asn = self::getAsnFromIp($ip);
$uidArr = [0, $userId];
if ($asn > 0) {
//check if allowed by asn
$logPrefix .= ", asn: $asn";
foreach($uidArr as $uid) {
$key = self::getCacheKey($uid, IsAllowedEnum::YES, IpAsnEnum::ASN);
if ($redis->sismember($key, $asn)) {
$desc = "$logPrefix, asn $asn in allowed $key, result: false";
return self::buildCheckResult(false, $desc);
}
}
foreach($uidArr as $uid) {
$key = self::getCacheKey($uid, IsAllowedEnum::NO, IpAsnEnum::ASN);
if ($redis->sismember($key, $asn)) {
$desc = ("$logPrefix, asn $asn in $key, result: true");
return self::buildCheckResult(true, $desc);
}
}
}
//then check from ip field
foreach($uidArr as $uid) {
$key = self::getCacheKey($uid, IsAllowedEnum::YES, IpAsnEnum::IP);
if ($redis->sismember($key, $ip)) {
$desc = ("$logPrefix, ip $ip in allowed $key, result: false");
return self::buildCheckResult(false, $desc);
}
}
foreach($uidArr as $uid) {
$key = self::getCacheKey($uid, IsAllowedEnum::NO, IpAsnEnum::IP);
if ($redis->sismember($key, $ip)) {
$desc = ("$logPrefix, ip $ip in $key, result: true");
return self::buildCheckResult(true, $desc);
}
}
return self::buildCheckResult(false, "not match any record, result: false");
}
private static function buildCheckResult(bool $isSeedBox, string $desc): array
{
$result = [
'result' => $isSeedBox,
'desc' => $desc,
];
do_log(json_encode($result));
return $result;
}
public static function getAsnFromIp(string $ip): int
{
//虽然 ip 对应的 asn 相对固定但不宜设置较大的缓存时间IP 地址较多,容易引起内存膨胀
return NexusDB::remember("IP_TO_ASN:$ip", 3600, function () use ($ip) {
$reader = self::getAsnReader();
if (is_null($reader)) {
return 0;
}
2025-09-08 03:05:55 +07:00
try {
$asnObj = $reader->asn($ip);
return $asnObj->autonomousSystemNumber ?? 0;
} catch (\Exception $e) {
2025-09-14 00:47:09 +07:00
do_log("ip: $ip, error: " . $e->getMessage());
2025-09-08 03:05:55 +07:00
return 0;
}
2025-05-11 02:33:22 +07:00
});
}
/**
* IS_SEED_BOX_FROM_USER_RECORD:IP:INCLUDES:USER:10001
* IS_SEED_BOX_FROM_USER_RECORD:IP:EXCLUDES:USER:10001
* IS_SEED_BOX_FROM_USER_RECORD:ASN:INCLUDES:USER:10001
* IS_SEED_BOX_FROM_USER_RECORD:ASN:EXCLUDES:USER:10001
* IS_SEED_BOX_FROM_USER_RECORD:ASN:EXCLUDES:ADMIN
*
* @param int $userId
* @param int $isAllowed
* @param string $field
* @return string
*/
private static function getCacheKey(int $userId, IsAllowedEnum $isAllowed, IpAsnEnum $field): string
2022-07-23 15:05:32 +08:00
{
2025-05-11 02:33:22 +07:00
$key = sprintf(
"%s:%s:%s",
self::IS_SEED_BOX_FROM_USER_RECORD_CACHE_PREFIX,
$field->name,
$isAllowed == IsAllowedEnum::YES ? "EXCLUDES" : "INCLUDES", //允许,要排队它不是 seedBox
);
if ($userId > 0) {
$key .= ":USER:$userId";
} else {
$key .= ":ADMIN";
}
return $key;
2022-07-23 15:05:32 +08:00
}
2025-05-11 02:33:22 +07:00
/**
* @throws InvalidDatabaseException
*/
private static function getAsnReader(): ?Reader
{
if (is_null(self::$asnReader)) {
$database = nexus_env('GEOIP2_ASN_DATABASE');
if (!file_exists($database) || !is_readable($database)) {
do_log("GEOIP2_ASN_DATABASE: $database not exists or not readable", "debug");
return null;
}
self::$asnReader = new Reader($database);
}
return self::$asnReader;
}
2022-07-23 15:05:32 +08:00
}