From 5eae52393d82f4877a6202bfe5ec36b4dfc3446d Mon Sep 17 00:00:00 2001 From: hellcat Date: Sat, 17 Oct 2026 15:23:19 +0800 Subject: [PATCH] DPL operations snapshot Source commit: 5c9157f5d7852976c92900c8a1113c547c2e2b97 --- panel-acceptance.php | 94 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 panel-acceptance.php diff --git a/panel-acceptance.php b/panel-acceptance.php new file mode 100644 index 0000000..b09b86f --- /dev/null +++ b/panel-acceptance.php @@ -0,0 +1,94 @@ +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+dwhere(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);} +}