DPL operations snapshot

Source commit: 5c9157f5d7
This commit is contained in:
hellcat 2026-10-17 15:23:19 +08:00
parent b8d10fac96
commit 5eae52393d

94
panel-acceptance.php Normal file
View File

@ -0,0 +1,94 @@
<?php
/** On-demand batch acceptance lease. No bootstrap occurs when included by tests. */
namespace DplOnboarding;
require_once __DIR__.'/operations.php';
use App\Models\{User,Server,ServerGroup};
use App\Services\ServerService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use RuntimeException;
final class Acceptance {
public static function eligible(User $u,int $group): bool {
return !$u->banned && (int)$u->group_id===$group && (int)$u->u+(int)$u->d<(int)$u->transfer_enable && ($u->expired_at===null || (int)$u->expired_at>=time());
}
public static function prepare(string $dir,int $group,bool $apply): array {
return locked($dir,function()use($dir,$group,$apply){
if(!ServerGroup::find($group))throw new RuntimeException('Formal group missing');
$path=$dir.'/acceptance-user.json';
if(is_file($path) && readJson($path)['state']==='closed' && $apply){
$old=readJson($path);$prior=isset($old['id'])?User::find($old['id']):null;
if($old['owned'] && $prior && (!$prior->banned || $prior->email!==$old['email'] || $prior->uuid!==$old['uuid'] || $prior->token!==$old['token'] || $prior->is_admin || (int)$prior->group_id!==$old['group']))throw new RuntimeException('Prior lease not revoked');
writeJson($dir.'/acceptance-closed-'.($old['id']??'absent').'.json',$old);unlink($path);
}
if(is_file($path)){
$j=readJson($path);
if($j['group']!==$group || $j['state']==='closed')throw new RuntimeException('Lease closed or parameters changed; never reactivate');
$u=User::where('email',$j['email'])->first();
if(!$u && $j['owned'] && $j['state']==='prepared' && $apply){
$u=DB::transaction(fn()=>self::create($j),1);
}
if(!$u || $u->uuid!==$j['uuid'] || $u->token!==$j['token'] || !self::eligible($u,$group) || ($j['owned'] && $u->is_admin))throw new RuntimeException('Acceptance lease drifted');
if($j['owned'])self::limits($u,$j);
$j['id']=$u->id;$j['state']='active';if($apply)writeJson($path,$j);
return ['state'=>'ready','id'=>$u->id,'owned'=>$j['owned']];
}
$u=User::where('group_id',$group)->where('banned',0)->whereRaw('u+d<transfer_enable')->where(fn($q)=>$q->whereNull('expired_at')->orWhere('expired_at','>=',time()))->where('email','not like','node-qa-%')->where('email','not like','batch-acceptance-%')->orderBy('id')->first();
if(!$apply)return ['state'=>$u?'existing':'would_create'];
$j=['group'=>$group,'state'=>'prepared','owned'=>!$u,'email'=>$u?->email??'batch-acceptance-'.substr(hash('sha256',$dir),0,12).'-'.Str::lower(Str::random(8)).'@example.invalid','uuid'=>$u?->uuid??(string)Str::uuid(),'token'=>$u?->token??Str::random(32),'quota'=>104857600,'expires_at'=>time()+3600];
// Persist identity before commit. A lost response resumes the same row, never another user.
if(!$u && User::where('email',$j['email'])->exists())throw new RuntimeException('Unowned acceptance email conflict');
writeJson($path,$j);
if(!$u)$u=DB::transaction(fn()=>self::create($j),1);
$j['id']=$u->id;$j['state']='active';writeJson($path,$j);
return ['state'=>'ready','id'=>$u->id,'owned'=>$j['owned']];
});
}
private static function limits(User $u,array $j):void {
if((int)$u->transfer_enable!==$j['quota'] || (int)$u->expired_at!==$j['expires_at'] || (int)$u->device_limit!==1)throw new RuntimeException('Owned lease limits changed; inspect before mutation');
}
private static function create(array $j): User {
$u=new User(['email'=>$j['email'],'uuid'=>$j['uuid'],'token'=>$j['token'],'password'=>password_hash(Str::random(40),PASSWORD_BCRYPT),'group_id'=>$j['group'],'transfer_enable'=>$j['quota'],'expired_at'=>$j['expires_at'],'banned'=>0,'is_admin'=>0,'remind_expire'=>0,'remind_traffic'=>0,'device_limit'=>1]);$u->saveQuietly();return $u;
}
public static function source(string $dir,int $group,bool $inspect=false): array {
$j=readJson($dir.'/acceptance-user.json');$u=User::findOrFail($j['id']);
if($j['group']!==$group || (!$inspect && $j['state']!=='active') || $u->uuid!==$j['uuid'] || $u->token!==$j['token'] || (!$inspect && !self::eligible($u,$group)) || ($inspect && !$j['owned'] && !self::eligible($u,$group)) || (int)$u->group_id!==$group)throw new RuntimeException('Acceptance user unavailable');
if($j['owned'])self::limits($u,$j);
$nodes=[];
foreach(ServerService::getAvailableServers($u) as $s){
if(isset($s['ports']))throw new RuntimeException('Port-range nodes require an explicit verifier');
$model=Server::findOrFail($s['id']);
if(!$inspect && !ServerService::getAvailableUsers($model)->contains('id',$u->id))throw new RuntimeException('User excluded by effective node filter');
$nodes[]=['id'=>$s['id'],'type'=>$s['type'],'tag'=>$s['name'],'relay'=>$s['host'],'relay_port'=>(int)$s['port'],'sni'=>$s['protocol_settings']['tls']['server_name']??null,'uuid'=>$s['password']];
}
if($inspect){foreach($nodes as &$n)unset($n['uuid']);unset($n);return ['nodes'=>$nodes];}
return ['user_id'=>$u->id,'owned'=>$j['owned'],'token'=>$u->token,'nodes'=>$nodes];
}
public static function cleanup(string $dir,bool $apply): array {
return locked($dir,function()use($dir,$apply){
$p=$dir.'/acceptance-user.json';if(!is_file($p))return ['state'=>'absent'];$j=readJson($p);
$u=isset($j['id'])?User::find($j['id']):User::where('email',$j['email'])->first();
if($j['owned'] && $u){
if($u->email!==$j['email'] || $u->uuid!==$j['uuid'] || $u->token!==$j['token'] || $u->is_admin || (int)$u->group_id!==$j['group'])throw new RuntimeException('Cleanup ownership mismatch');
self::limits($u,$j);
if($apply){$u->banned=true;$u->saveQuietly();}
if($apply && !$u->fresh()->banned)throw new RuntimeException('Cleanup failed');
}
if($apply){$j['state']='closed';$j['id']=$u?->id;writeJson($p,$j);}
return ['state'=>$apply?'closed':'preview','owned'=>$j['owned'],'id'=>$u?->id];
});
}
}
if(realpath($_SERVER['SCRIPT_FILENAME']??'')===__FILE__){
$o=getopt('',['action:','dir:','group:','apply','help']);
if(isset($o['help'])){echo "--action=prepare|source|snapshot|cleanup --dir=PRIVATE_DIR --group=EXISTING_ID [--apply]\n";exit;}
try{
$dir=$o['dir']??'';$group=(int)($o['group']??0);
if(!preg_match('~^/[A-Za-z0-9_./-]+$~D',$dir)||str_contains($dir,'..')||$group<1)throw new RuntimeException('Explicit private directory and group required');
umask(0077);if(!is_dir($dir))mkdir($dir,0700,true);
if(is_link($dir)||(fileperms($dir)&0077))throw new RuntimeException('Private directory required');
boot();$apply=isset($o['apply']);
$r=match($o['action']??''){'prepare'=>Acceptance::prepare($dir,$group,$apply),'source'=>Acceptance::source($dir,$group),'snapshot'=>Acceptance::source($dir,$group,true),'cleanup'=>Acceptance::cleanup($dir,$apply),default=>throw new RuntimeException('Unknown action')};
echo json_encode($r,JSON_THROW_ON_ERROR).PHP_EOL;
}catch(\Throwable $e){fwrite(STDERR,json_encode(['state'=>'blocked','reason'=>'Inspect protected lease and effective authorization; no automatic overwrite']).PHP_EOL);exit(2);}
}