feat(admin): remember login for 90 days and support REG SSH limits
deploy-preland / deploy zoomstage.online (push) Successful in 57s
deploy-preland / deploy zoomstage.online (push) Successful in 57s
This commit is contained in:
1 parent
d04b40519e
commit
522aaf8554
13 files changed
+316
-27
No files matched your search
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/** #22: сервер разрывает SSH exec длиннее 8 KiB; helper и payload не теряются. */
|
||||
declare(strict_types=1);
|
||||
namespace phpseclib3\Net {
|
||||
class SSH2 {
|
||||
public static array $commands=[];
|
||||
public static bool $withoutZlib=false;
|
||||
public static bool $dropInbox=false;
|
||||
public function __construct(...$args) {}
|
||||
public function getServerPublicHostKey(): string {return 'ssh-ed25519 '.base64_encode('fixture-key');}
|
||||
public function login(...$args): bool {return true;}
|
||||
public function setTimeout($seconds): void {}
|
||||
public function isTimeout(): bool {return false;}
|
||||
public function exec(string $command): string|false {
|
||||
self::$commands[]=$command;
|
||||
if(strlen($command)>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;
|
||||
} 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