fix(admin): route multi-megabyte sets to SFTP before shell escaping
deploy-preland / deploy zoomstage.online (push) Successful in 1m3s
deploy-preland / deploy zoomstage.online (push) Successful in 1m3s
This commit is contained in:
1 parent
6ec813bc99
commit
4fc4fd8162
2 files changed
+27
-3
No files matched your search
@@ -29,11 +29,15 @@ final class Transport
|
||||
$ssh=$this->connect();$code=file_get_contents(__DIR__.'/Remote.php');$code=preg_replace('/^<\?php\s*/','',$code);
|
||||
// Сжат только доверенный helper из поставки; payload остаётся JSON-данными.
|
||||
$entry="\necho json_encode(\\PrelandAdmin\\Remote::execute(json_decode(base64_decode(\$argv[1]),true,64,JSON_THROW_ON_ERROR)),JSON_THROW_ON_ERROR);";
|
||||
$command=self::inlineCommand($code,$entry,base64_encode(json_encode($payload,JSON_THROW_ON_ERROR)));
|
||||
$jsonPayload=json_encode($payload,JSON_THROW_ON_ERROR);
|
||||
// Размер base64 проверяется ДО inlineCommand/escapeshellarg: для ZIP PHP
|
||||
// завершает процесс при аргументе >2 МиБ раньше переключения на inbox.
|
||||
$argumentLength=4*intdiv(strlen($jsonPayload)+2,3);
|
||||
$command=$argumentLength>self::COMMAND_LIMIT?null:self::inlineCommand($code,$entry,base64_encode($jsonPayload));
|
||||
$inbox=null;$sftp=null;
|
||||
try {
|
||||
// Большие наборы доставляются SFTP в приватный inbox: argv/ARG_MAX не используется.
|
||||
if(strlen($command)>self::COMMAND_LIMIT) {
|
||||
if($command===null||strlen($command)>self::COMMAND_LIMIT) {
|
||||
if(($payload['op']??null)==='test')throw new Problem('SSH bootstrap превышает безопасный предел команды; сократите helper или путь подключения.');
|
||||
$parent=dirname($this->target['root']);$private=$parent.'/.preland-admin-'.substr(hash('sha256',$this->target['root']),0,16);
|
||||
$candidate=$private.'/inbox-'.bin2hex(random_bytes(16));
|
||||
@@ -41,7 +45,7 @@ final class Transport
|
||||
// не должен бесконечно пытаться доставить сам себя через inbox.
|
||||
$command=self::inboxCommand($code,$candidate);
|
||||
$this->call(['op'=>'test']);$sftp=$this->sftp();$inbox=$candidate;
|
||||
if(!$sftp->put($inbox,json_encode($payload,JSON_THROW_ON_ERROR))||!$sftp->chmod(0600,$inbox))throw new Problem('Не удалось доставить подготовленный набор.');
|
||||
if(!$sftp->put($inbox,$jsonPayload)||!$sftp->chmod(0600,$inbox))throw new Problem('Не удалось доставить подготовленный набор.');
|
||||
}
|
||||
$timeout=max(1,min(120,(int)(getenv('ADMIN_TRANSPORT_TIMEOUT')?:60)));$ssh->setTimeout($timeout);
|
||||
$output=$ssh->exec($command);$result=is_string($output)?json_decode($output,true):null;
|
||||
|
||||
@@ -70,6 +70,26 @@ namespace {
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user