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
+32
-9
@@ -6,6 +6,7 @@ use phpseclib3\Net\SFTP;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
final class Transport
|
||||
{
|
||||
private const COMMAND_LIMIT=7000;
|
||||
private ?SSH2 $ssh=null;
|
||||
public function __construct(private Store $store, public readonly array $target) {}
|
||||
private function connect(): SSH2 {
|
||||
@@ -26,23 +27,45 @@ final class Transport
|
||||
}
|
||||
else {
|
||||
$ssh=$this->connect();$code=file_get_contents(__DIR__.'/Remote.php');$code=preg_replace('/^<\?php\s*/','',$code);
|
||||
// Аргументы проходят shell quoting, обработчик не принимает команды от клиента.
|
||||
$runner=$code."\necho json_encode(\\PrelandAdmin\\Remote::execute(json_decode(base64_decode(\$argv[1]),true,64,JSON_THROW_ON_ERROR)),JSON_THROW_ON_ERROR);";
|
||||
$command='php -d display_errors=0 -r '.escapeshellarg($runner).' '.escapeshellarg(base64_encode(json_encode($payload,JSON_THROW_ON_ERROR)));
|
||||
// Сжат только доверенный 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)));
|
||||
$inbox=null;$sftp=null;
|
||||
try {
|
||||
// Большие наборы доставляются SFTP в приватный inbox: argv/ARG_MAX не используется.
|
||||
if(strlen($command)>60000) {
|
||||
$sftp=$this->sftp();$parent=dirname($this->target['root']);$private=$parent.'/.preland-admin-'.substr(hash('sha256',$this->target['root']),0,16);
|
||||
$this->call(['op'=>'test']);$inbox=$private.'/inbox-'.bin2hex(random_bytes(16));
|
||||
if(!$sftp->put($inbox,json_encode($payload,JSON_THROW_ON_ERROR)))throw new Problem('Не удалось доставить подготовленный набор.');$sftp->chmod(0600,$inbox);
|
||||
$runner=$code."\n\$inbox=\$argv[1];try{echo json_encode(\\PrelandAdmin\\Remote::execute(json_decode(file_get_contents(\$inbox),true,64,JSON_THROW_ON_ERROR)),JSON_THROW_ON_ERROR);}finally{unlink(\$inbox);}";
|
||||
$command='php -d display_errors=0 -r '.escapeshellarg($runner).' '.escapeshellarg($inbox);
|
||||
if(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));
|
||||
// Проверяем конечный bootstrap до рекурсивного preflight: большой helper
|
||||
// не должен бесконечно пытаться доставить сам себя через 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('Не удалось доставить подготовленный набор.');
|
||||
}
|
||||
$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;
|
||||
if($ssh->isTimeout()||!is_array($result)) { $this->ssh=null;throw new Problem('Неизвестный результат: соединение прервано. Перечитайте фактическое состояние.',504); }
|
||||
} finally {
|
||||
// Helper очищает inbox после чтения; резервная очистка также при обрыве
|
||||
// exec/ошибке загрузки, не подменяя исходную ошибку транспорта.
|
||||
if($inbox!==null&&$sftp!==null)try{$sftp->delete($inbox);}catch(\Throwable){}
|
||||
}
|
||||
}
|
||||
if(!($result['ok']??false)) {if(!empty($result['conflict']))throw new Conflict();throw new Problem($result['error']??'Ошибка транспорта.');}return $result;
|
||||
}
|
||||
private static function inlineCommand(string $code,string $entry,string $argument): string {
|
||||
if(!function_exists('gzdeflate'))throw new Problem('Для SSH-транспорта требуется расширение zlib.');
|
||||
$compressed=gzdeflate($code.$entry,9);if($compressed===false)throw new Problem('Не удалось подготовить SSH helper.');
|
||||
$runner='if(!function_exists("gzinflate")){echo json_encode(["ok"=>false,"error"=>"На удалённом PHP отсутствует zlib (gzinflate)."]);exit(1);}eval(gzinflate(base64_decode('.var_export(base64_encode($compressed),true).',true)));';
|
||||
return 'php -d display_errors=0 -r '.escapeshellarg($runner).' '.escapeshellarg($argument);
|
||||
}
|
||||
private static function inboxCommand(string $code,string $inbox): string {
|
||||
$entry="\n\$inbox=\$argv[1];try{echo json_encode(\\PrelandAdmin\\Remote::execute(json_decode(file_get_contents(\$inbox),true,64,JSON_THROW_ON_ERROR)),JSON_THROW_ON_ERROR);}finally{unlink(\$inbox);}";
|
||||
$command=self::inlineCommand($code,$entry,$inbox);
|
||||
if(strlen($command)>self::COMMAND_LIMIT)throw new Problem('SSH helper превышает безопасный предел команды; требуется уменьшить bootstrap.');
|
||||
return $command;
|
||||
}
|
||||
private function sftp(): SFTP {
|
||||
$t=$this->target;$sftp=new SFTP($t['host'],$t['port'],20);$key=$sftp->getServerPublicHostKey();$parts=explode(' ',(string)$key);$fp='SHA256:'.rtrim(base64_encode(hash('sha256',base64_decode($parts[1]??'',true)?:'',true)),'=');$expected=$t['host_key'];
|
||||
if(!is_string($key)||!hash_equals(rtrim($expected,'='),rtrim(str_starts_with($expected,'SHA256:')?$fp:$key,'=')))throw new Problem('Ключ SFTP хоста не совпадает.');
|
||||
|
||||
Reference in new issue
Block a user