['pipe','r'],1=>$log,2=>$log],$pipes,null,$env);if(!is_resource($p))throw new RuntimeException('server failed');fclose($pipes[0]);$rememberServers[$label]=[$p,$log]; $until=microtime(true)+5;do{$s=@fsockopen('127.0.0.1',$port);if($s){fclose($s);return $port;}usleep(20000);}while(microtime(true)<$until);throw new RuntimeException('server not ready'); } function rememberStop(string $label): void {global $rememberServers;if(!isset($rememberServers[$label]))return;[$p,$log]=$rememberServers[$label];proc_terminate($p);proc_close($p);fclose($log);unset($rememberServers[$label]);} function rememberRequest(int $port,string $path,string $cookie='',?array $body=null,?string $csrf=null): array { $h=['Content-Type: application/json'];if($cookie!=='')$h[]='Cookie: '.$cookie;if($csrf!==null)$h[]='X-CSRF-Token: '.$csrf; $ctx=stream_context_create(['http'=>['method'=>$body===null?'GET':'POST','header'=>implode("\r\n",$h),'content'=>$body===null?'':json_encode((object)$body,JSON_THROW_ON_ERROR),'ignore_errors'=>true,'timeout'=>10]]);$text=file_get_contents('http://127.0.0.1:'.$port.$path,false,$ctx);$headers=$http_response_header??[];preg_match('~HTTP/\S+ (\d+)~',$headers[0]??'',$m); $setCookie=null;foreach($headers as $header)if(str_starts_with(strtolower($header),'set-cookie: preland_admin=')){$setCookie=substr($header,12);$cookie=explode(';',$setCookie)[0];} clearstatcache();return [(int)($m[1]??0),json_decode((string)$text,true,64,JSON_THROW_ON_ERROR),$cookie,$setCookie]; } function rememberLogin(int $port,mixed $remember=false,string $cookie=''): array {return rememberRequest($port,'/api/login',$cookie,['username'=>'remember-fixture','password'=>'remember-fixture-password','remember'=>$remember]);} function rememberFile(string $cookie,bool $remember=true): string {global $rememberState;return $rememberState.'/'.($remember?'remember-sessions':'sessions').'/sess_'.substr($cookie,strlen('preland_admin='));} function rememberEdit(string $cookie,string $field,int $value): void {$file=rememberFile($cookie);$text=file_get_contents($file);$new=preg_replace('/'.preg_quote($field,'/').'\|i:\d+;/', $field.'|i:'.$value.';', $text,1,$count);if($count!==1)throw new RuntimeException('Missing fixture session field '.$field);file_put_contents($file,$new);} function rememberBacking(string $cookie): array { global $rememberState; $code='require $argv[1];$_COOKIE["preland_admin"]=$argv[2];PrelandAdmin\\Auth::session();echo json_encode(["bucket"=>basename(session_save_path()),"gc"=>(int)ini_get("session.gc_maxlifetime"),"cookie_lifetime"=>session_get_cookie_params()["lifetime"]]);session_write_close();'; $env=getenv();$env['ADMIN_STATE_DIR']=$rememberState;$env['ADMIN_COOKIE_SECURE']='0';$p=proc_open([PHP_BINARY,'-r',$code,dirname(__DIR__).'/bootstrap.php',substr($cookie,strlen('preland_admin='))],[0=>['pipe','r'],1=>['pipe','w'],2=>STDERR],$pipes,null,$env);fclose($pipes[0]);$out=stream_get_contents($pipes[1]);fclose($pipes[1]);if(proc_close($p)!==0)throw new RuntimeException('Backing inspection failed');return json_decode($out,true,64,JSON_THROW_ON_ERROR); } try{ $a=rememberServer('a'); [$status]=rememberRequest($a,'/api/state');rememberCheck($status===401&&!is_dir($rememberState.'/remember-sessions'),'anonymous request does not create long-lived backing'); [$status]=rememberRequest($a,'/api/login','',['username'=>'remember-fixture','password'=>'wrong','remember'=>true]);rememberCheck($status===401&&!is_dir($rememberState.'/remember-sessions'),'failed remembered login does not create long-lived backing'); foreach(['true',1,null,[],(object)['value'=>true]] as $bad){[$status]=rememberLogin($a,$bad);rememberCheck($status===422,'remember input requires strict boolean');} [$status,$regular,$regularCookie,$header]=rememberLogin($a,false);rememberCheck($status===200&&!str_contains(strtolower((string)$header),'expires=')&&!str_contains(strtolower((string)$header),'max-age='),'unchecked login issues browser-session cookie'); $normalFile=rememberFile($regularCookie,false);rememberCheck(is_file($normalFile)&&!is_dir($rememberState.'/remember-sessions'),'regular login remains in short-lived backing'); $backing=rememberBacking($regularCookie);rememberCheck($backing===['bucket'=>'sessions','gc'=>3600,'cookie_lifetime'=>0],'regular backing keeps GC one hour and browser-session lifetime'); $legacy='preland_admin='.bin2hex(random_bytes(16));$legacyCsrf=bin2hex(random_bytes(32));file_put_contents(rememberFile($legacy,false),'user|'.serialize('remember-fixture').'csrf|'.serialize($legacyCsrf).'last|'.serialize(time())); [$status,$oldState]=rememberRequest($a,'/api/state',$legacy);rememberCheck($status===200&&$oldState['csrf']===$legacyCsrf&&is_file(rememberFile($legacy,false)),'existing d04-style ordinary session continues without migration or deletion'); $before=time();[$status,$login,$cookie,$header]=rememberLogin($a,true,$regularCookie); rememberCheck($status===200&&preg_match('/expires=([^;]+)/i',(string)$header,$match)===1&&abs(strtotime($match[1])-($before+7776000))<3,'checked login issues native session cookie expiring 90 days from login'); rememberCheck(str_contains(strtolower($header),'httponly')&&str_contains(strtolower($header),'samesite=strict'),'remembered cookie preserves HttpOnly and SameSite Strict'); rememberCheck($cookie!==$regularCookie&&!is_file($normalFile)&&is_file(rememberFile($cookie)),'regular to remember login destroys old backing and creates fresh native SID'); $backing=rememberBacking($cookie);rememberCheck($backing===['bucket'=>'remember-sessions','gc'=>7776000,'cookie_lifetime'=>7776000],'only authenticated remember bucket uses ninety-day GC and cookie lifetime'); [$status]=rememberRequest($a,'/api/state',$regularCookie);rememberCheck($status===401,'old regular cookie cannot be reused after remembered login'); $text=file_get_contents(rememberFile($cookie));rememberCheck(!str_contains($text,'remember-fixture-password')&&!str_contains($text,'password'),'remembered server session stores no password'); preg_match('/remember_until\|i:(\d+);/',$text,$untilMatch);$absolute=(int)($untilMatch[1]??0);rememberCheck($absolute>=$before+7776000&&$absolute<=time()+7776000,'server stores absolute remember-until bound from login'); rememberEdit($cookie,'last',time()-7200);[$status,$state,$same,$refresh]=rememberRequest($a,'/api/state',$cookie); rememberCheck($status===200&&$state['csrf']===$login['csrf']&&$same===$cookie&&$refresh===null,'remembered session survives old idle timestamp without rotating cookie or CSRF'); preg_match('/remember_until\|i:(\d+);/',file_get_contents(rememberFile($cookie)),$after);rememberCheck((int)$after[1]===$absolute,'ordinary read does not slide absolute remember deadline'); rememberStop('a');$b=rememberServer('b');[$status,$body]=rememberRequest($b,'/api/state',$cookie);rememberCheck($status===200&&$body['csrf']===$login['csrf'],'remembered login survives fresh server and separate ephemeral session path'); $clients=[];$outputs=[];$code='$ctx=stream_context_create(["http"=>["header"=>"Cookie: ".$argv[2],"ignore_errors"=>true,"timeout"=>5]]);$body=file_get_contents($argv[1],false,$ctx);echo json_encode(["body"=>json_decode($body,true),"set_cookie"=>count(array_filter($http_response_header,fn($h)=>str_starts_with(strtolower($h),"set-cookie:")))]);'; for($i=0;$i<4;$i++){$path=$rememberRoot.'/parallel-'.$i.'.json';$out=fopen($path,'w');$p=proc_open([PHP_BINARY,'-r',$code,'http://127.0.0.1:'.$b.'/api/state',$cookie],[0=>['pipe','r'],1=>$out,2=>STDERR],$pipes);fclose($pipes[0]);$clients[]=[$p,$out];$outputs[]=$path;} foreach($clients as [$p,$out]){if(proc_close($p)!==0)throw new RuntimeException('Parallel request failed');fclose($out);}foreach($outputs as $path){$parallel=json_decode(file_get_contents($path),true,64,JSON_THROW_ON_ERROR);rememberCheck(($parallel['body']['user']??null)==='remember-fixture'&&($parallel['body']['csrf']??null)===$login['csrf']&&$parallel['set_cookie']===0,'parallel remembered requests preserve authentication, native cookie and CSRF');} $other=rememberServer('other',$rememberRoot.'/other-state');[$status]=rememberRequest($other,'/api/state',$cookie);rememberCheck($status===401,'remember cookie cannot authenticate against unrelated state'); // Абсолютный срок важнее свежего last; пограничный срок ещё до 90 дней допустим. rememberEdit($cookie,'remember_until',time()+5);rememberEdit($cookie,'last',time()-7200);[$status]=rememberRequest($b,'/api/state',$cookie);rememberCheck($status===200,'remembered session before absolute deadline ignores regular idle expiry'); rememberEdit($cookie,'remember_until',time()-1);rememberEdit($cookie,'last',time());[$status,$body,$ignored,$expiredHeader]=rememberRequest($b,'/api/state',$cookie);rememberCheck($status===401&&!is_file(rememberFile($cookie))&&str_contains(strtolower((string)$expiredHeader),'expires='),'absolute expiry destroys remembered backing and clears cookie despite fresh last'); [$status,$login,$cookie]=rememberLogin($b,true);$longFile=rememberFile($cookie);[$status,$newLogin,$newCookie,$header]=rememberLogin($b,false,$cookie);rememberCheck($status===200&&$newCookie!==$cookie&&!is_file($longFile)&&is_file(rememberFile($newCookie,false))&&!str_contains(strtolower((string)$header),'expires='),'remember to regular login destroys long-lived SID and restores session-only cookie'); [$status]=rememberRequest($b,'/api/state',$cookie);rememberCheck($status===401,'old remembered cookie cannot be reused after regular login'); $normal=rememberFile($newCookie,false);$raw=file_get_contents($normal);file_put_contents($normal,preg_replace('/last\|i:\d+;/','last|i:'.(time()-3601).';',$raw));[$status]=rememberRequest($b,'/api/state',$newCookie);rememberCheck($status===401,'unchecked login still enforces one-hour idle expiry'); [$status,$login,$cookie]=rememberLogin($b,true);$file=rememberFile($cookie);[$status]=rememberRequest($b,'/api/logout',$cookie,[],'invalid');rememberCheck($status===403&&is_file($file),'remembered logout requires existing CSRF'); [$status,$body,$ignored,$clear]=rememberRequest($b,'/api/logout',$cookie,[],$login['csrf']);rememberCheck($status===200&&!is_file($file)&&str_contains(strtolower((string)$clear),'expires=')&&str_contains(strtolower((string)$clear),'httponly')&&str_contains(strtolower((string)$clear),'samesite=strict'),'logout immediately destroys remembered SID and clears same cookie flags'); [$status]=rememberRequest($b,'/api/state',$cookie);rememberCheck($status===401,'captured pre-logout remembered cookie cannot be replayed'); $longCount=count(glob($rememberState.'/remember-sessions/sess_*')?:[]);for($i=0;$i<3;$i++)rememberRequest($b,'/api/state','preland_admin='.bin2hex(random_bytes(16)));rememberCheck(count(glob($rememberState.'/remember-sessions/sess_*')?:[])===$longCount,'arbitrary anonymous SIDs create no 90-day files'); rememberRequest($b,'/api/login','',['username'=>'remember-fixture','password'=>'wrong','remember'=>true]);rememberCheck(count(glob($rememberState.'/remember-sessions/sess_*')?:[])===$longCount,'failed remembered login creates no long-lived files in existing bucket'); rememberCheck((fileperms($rememberState.'/remember-sessions')&0777)===0700&&(fileperms($rememberState.'/sessions')&0777)===0700,'both backing directories keep private 0700 modes'); [$status,$normal,$normalCookie]=rememberLogin($b,false);$normalPath=rememberFile($normalCookie,false);$link=rememberFile($normalCookie,true);symlink($normalPath,$link);[$status]=rememberRequest($b,'/api/state',$normalCookie);rememberCheck($status===503&&is_file($normalPath),'remembered SID symlink is rejected without reading regular backing through it');unlink($link); [$status,$fresh,$freshCookie]=rememberLogin($b,true,$normalCookie);rememberCheck((fileperms(rememberFile($freshCookie))&0777)===0600,'authenticated remember file has private 0600 mode'); mkdir($rememberRoot.'/external',0700);mkdir($rememberRoot.'/unsafe',0700);symlink($rememberRoot.'/external',$rememberRoot.'/unsafe/remember-sessions');$unsafe=rememberServer('unsafe',$rememberRoot.'/unsafe');[$status]=rememberRequest($unsafe,'/api/state');rememberCheck($status===503&&glob($rememberRoot.'/external/*')===[],'remember-directory symlink fails closed without external writes'); echo "Remember auth contract complete\n"; }finally{foreach(array_keys($rememberServers) as $label)rememberStop($label);}