8192)return false; // фактическая граница REG.RU, старая реализация падает if(self::$dropInbox&&str_contains($command,'/inbox-'))return false; if(self::$withoutZlib)$command=preg_replace('/^php /','php -d disable_functions=gzinflate ',$command); $process=proc_open($command,[0=>['pipe','r'],1=>['pipe','w'],2=>['pipe','w']],$pipes); fclose($pipes[0]);$output=stream_get_contents($pipes[1]);$error=stream_get_contents($pipes[2]);fclose($pipes[1]);fclose($pipes[2]);proc_close($process); if($error!=='')throw new \RuntimeException($error); return $output; } } class SFTP extends SSH2 { public static array $uploads=[]; public function put(string $path,string $bytes): bool {self::$uploads[]=$path;return file_put_contents($path,$bytes)!==false;} public function chmod(int $mode,string $path): bool {return chmod($path,$mode);} public function delete(string $path): bool {return !file_exists($path)||unlink($path);} } } namespace { require dirname(__DIR__).'/bootstrap.php'; use PrelandAdmin\{Store,Transport,Problem}; use phpseclib3\Net\{SSH2,SFTP}; function ensure(bool $condition,string $message): void {if(!$condition)throw new RuntimeException($message);echo 'PASS: '.$message."\n";} $fixture=sys_get_temp_dir().'/preland22-command-'.bin2hex(random_bytes(6));mkdir($fixture);mkdir($fixture.'/site'); $site=$fixture.'/site';file_put_contents($site.'/config.json','{"host":"initial.example","utm":{}}'); $store=new Store($fixture.'/state'); $target=['id'=>'limit','type'=>'sftp','host'=>'fixture','port'=>22,'user'=>'fixture','secret'=>'fixture','host_key'=>'ssh-ed25519 '.base64_encode('fixture-key'),'root'=>$site]; $transport=new Transport($store,$target); try { $transport->test();$before=$transport->readConfig(); ensure($before['config']->host==='initial.example','compressed helper reads under SSH 8KiB limit'); $transport->writeConfig($before['version'],'{"host":"small.example","utm":{}}'); ensure($transport->readConfig()['config']->host==='small.example','compressed RPC performs CAS write/read'); $large=json_encode(['host'=>'large.example','utm'=>(object)[],'padding'=>str_repeat('abcdefgh',3000)],JSON_THROW_ON_ERROR); $before=$transport->readConfig();$transport->writeConfig($before['version'],$large); ensure($transport->readConfig()['content']===$large,'large payload uses inbox and preserves bytes'); ensure(count(SFTP::$uploads)===1,'large payload uploaded through SFTP exactly once'); foreach(SFTP::$uploads as $path)ensure(!file_exists($path),'SFTP inbox removed after RPC'); $maximum=max(array_map('strlen',SSH2::$commands)); ensure($maximum<=7000,'all final shell commands stay within 7000 bytes (maximum '.$maximum.')'); $commandCount=count(SSH2::$commands);$uploads=count(SFTP::$uploads); // Inline test может оказаться длиннее inbox runner из-за base64 длинного // root, даже когда сам inbox помещается: этот preflight нельзя рекурсировать. $longRootTarget=$target;$longRootTarget['root']='/'.str_repeat('a',1800); try{(new Transport($store,$longRootTarget))->test();throw new RuntimeException('oversized test recursed');} catch(Problem $error){ensure(str_contains($error->getMessage(),'SSH bootstrap'),'oversized test RPC rejected before inbox/preflight recursion');} ensure(count(SSH2::$commands)===$commandCount&&count(SFTP::$uploads)===$uploads,'oversized test performs no exec/upload'); $bootstrap=new ReflectionMethod(Transport::class,'inboxCommand'); try{$bootstrap->invoke(null,'/*'.base64_encode(random_bytes(16000)).'*/',$site.'/inbox-test');throw new RuntimeException('oversized bootstrap accepted');} catch(Problem $error){ensure(str_contains($error->getMessage(),'bootstrap'),'oversized helper rejected before recursive preflight');} ensure(count(SSH2::$commands)===$commandCount&&count(SFTP::$uploads)===$uploads,'oversized bootstrap performs no remote call/upload'); $before=$transport->readConfig();SSH2::$dropInbox=true; try{$transport->writeConfig($before['version'],$large);throw new RuntimeException('dropped exec accepted');} catch(Problem $error){ensure(str_contains($error->getMessage(),'Неизвестный результат'),'dropped inbox exec remains unknown result');} SSH2::$dropInbox=false; foreach(SFTP::$uploads as $path)ensure(!file_exists($path),'inbox cleaned even when exec is interrupted'); SSH2::$withoutZlib=true; try{$transport->test();throw new RuntimeException('missing zlib accepted');}catch(Problem $error){ensure(str_contains($error->getMessage(),'zlib'),'missing gzinflate returns clear structured error');} SSH2::$withoutZlib=false; // Настоящие 90 изображений: два слоя base64 дают аргумент заметно больше PHP shell-limit 2 MiB. $sourceSet=dirname(__DIR__,2).'/1';$files=[]; foreach(array_keys(\PrelandAdmin\IndpicSetStorage::expectedDimensions()) as $filename){$bytes=file_get_contents($sourceSet.'/'.$filename);$files[$filename]=['data'=>base64_encode($bytes),'sha256'=>hash('sha256',$bytes)];} $hugePayload=['op'=>'set-write','number'=>3,'files'=>$files,'expected_manifest'=>null]; ensure(count($files)===90&&strlen(base64_encode(json_encode($hugePayload,JSON_THROW_ON_ERROR)))>2097152,'real 90-image set-write payload exceeds native 2 MiB shell argument limit'); $uploadsBeforeHuge=count(SFTP::$uploads);$configBeforeHuge=$transport->readConfig(); $transport->call($hugePayload); ensure(\PrelandAdmin\Remote::manifest($site.'/3')===\PrelandAdmin\Remote::manifest($sourceSet),'multi-MiB set-write activates all 90 real files with exact hashes'); ensure(count(SFTP::$uploads)===$uploadsBeforeHuge+1,'multi-MiB set-write uses exactly one SFTP inbox'); $hugeInbox=SFTP::$uploads[array_key_last(SFTP::$uploads)]; ensure(!file_exists($hugeInbox),'multi-MiB set-write inbox removed after actual RPC'); ensure(max(array_map('strlen',SSH2::$commands))<=7000,'multi-MiB set-write final exec stays below bootstrap limit'); ensure($transport->readConfig()['version']===$configBeforeHuge['version'],'image upload preserves config and the 2 MiB config contract'); // Обрыв до исполнения большой команды не должен трогать опубликованный набор; inbox удаляется. $hugePayload['expected_manifest']=\PrelandAdmin\Remote::manifest($site.'/3');$keptManifest=$hugePayload['expected_manifest'];SSH2::$dropInbox=true; try{$transport->call($hugePayload);throw new RuntimeException('dropped multi-MiB set-write accepted');} catch(Problem $error){ensure($error->status===504,'interrupted multi-MiB set-write remains unknown, not false success');} finally{SSH2::$dropInbox=false;} ensure(\PrelandAdmin\Remote::manifest($site.'/3')===$keptManifest,'interrupted multi-MiB exec preserves previously active set'); ensure(!file_exists(SFTP::$uploads[array_key_last(SFTP::$uploads)]),'interrupted multi-MiB set-write cleans private inbox'); } finally { $iterator=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fixture,FilesystemIterator::SKIP_DOTS),RecursiveIteratorIterator::CHILD_FIRST); foreach($iterator as $file)$file->isDir()?rmdir($file->getPathname()):unlink($file->getPathname());rmdir($fixture); } }