1<?php 2 3namespace Shellbox\Command; 4 5use Shellbox\Shellbox; 6 7/** 8 * The abstract base class for commands. 9 */ 10abstract class Command { 11 /** @var string */ 12 private $command = ''; 13 /** @var int|float|null */ 14 private $cpuTimeLimit; 15 /** @var int|float|null */ 16 private $wallTimeLimit; 17 /** @var int|float|null */ 18 private $memoryLimit; 19 /** @var int|float|null */ 20 private $fileSizeLimit; 21 /** @var string[] */ 22 private $environment = []; 23 /** @var string */ 24 private $stdin = ''; 25 /** @var bool */ 26 private $passStdin; 27 /** @var bool */ 28 private $includeStderr; 29 /** @var bool */ 30 private $logStderr = false; 31 /** @var bool */ 32 private $forwardStderr = false; 33 /** @var bool */ 34 private $useLogPipe = false; 35 /** @var string|null */ 36 private $workingDirectory; 37 /** @var array */ 38 private $procOpenOptions = []; 39 /** @var bool */ 40 private $disableNetwork = false; 41 /** @var string[] */ 42 private $disabledSyscalls = []; 43 /** @var bool */ 44 private $firejailDefaultSeccomp = false; 45 /** @var bool */ 46 private $noNewPrivs = false; 47 /** @var bool */ 48 private $privateUserNamespace = false; 49 /** @var bool */ 50 private $privateDev = false; 51 /** @var string[] */ 52 private $allowedPaths = []; 53 /** @var string[] */ 54 private $disallowedPaths = []; 55 /** @var bool */ 56 private $disableSandbox = false; 57 58 /** 59 * Adds parameters to the command. All parameters are escaped via Shellbox::escape(). 60 * Null values are ignored. 61 * 62 * @param mixed|mixed[] ...$args 63 * @return $this 64 */ 65 public function params( ...$args ) { 66 if ( count( $args ) === 1 && is_array( $args[0] ) ) { 67 $args = $args[0]; 68 } 69 $command = Shellbox::escape( $args ); 70 if ( $this->command === '' ) { 71 $this->command = $command; 72 } else { 73 $this->command .= ' ' . $command; 74 } 75 return $this; 76 } 77 78 /** 79 * Adds unsafe parameters to the command. These parameters are NOT sanitized in any way. 80 * Null values are ignored. 81 * 82 * @param string|string[]|null ...$args 83 * @return $this 84 */ 85 public function unsafeParams( ...$args ) { 86 if ( count( $args ) === 1 && is_array( $args[0] ) ) { 87 $args = $args[0]; 88 } 89 foreach ( $args as $arg ) { 90 if ( $arg !== null ) { 91 if ( $this->command !== '' ) { 92 $this->command .= ' '; 93 } 94 $this->command .= $arg; 95 } 96 } 97 return $this; 98 } 99 100 /** 101 * Replace the whole command with the given set of arguments. 102 * 103 * @param string|string[] ...$args 104 * @return $this 105 */ 106 public function replaceParams( ...$args ) { 107 $this->command = ''; 108 $this->params( ...$args ); 109 return $this; 110 } 111 112 /** 113 * Replace the whole command string with something else. The command is not 114 * escaped or sanitized. 115 * 116 * @param string $command 117 * @return $this 118 */ 119 public function unsafeCommand( string $command ) { 120 $this->command = $command; 121 return $this; 122 } 123 124 /** 125 * Set the CPU time limit, that is, the amount of time the process spends 126 * in the running state. 127 * 128 * Whether this limit can be respected depends on the executor 129 * configuration. 130 * 131 * @param int|float $limit The limit in seconds 132 * @return $this 133 */ 134 public function cpuTimeLimit( $limit ) { 135 $this->cpuTimeLimit = $limit; 136 return $this; 137 } 138 139 /** 140 * Set the wall clock time limit, that is, the amount of real time the 141 * process may run for. 142 * 143 * Whether this limit can be respected depends on the executor 144 * configuration. 145 * 146 * @param int|float $limit The limit in seconds 147 * @return $this 148 */ 149 public function wallTimeLimit( $limit ) { 150 $this->wallTimeLimit = $limit; 151 return $this; 152 } 153 154 /** 155 * Set the memory limit in bytes. 156 * 157 * Whether this limit can be respected depends on the executor 158 * configuration. 159 * 160 * @param int|float $limit The limit in bytes 161 * @return $this 162 */ 163 public function memoryLimit( $limit ) { 164 $this->memoryLimit = $limit; 165 return $this; 166 } 167 168 /** 169 * Set the maximum file size that the command may create 170 * 171 * Whether this limit can be respected depends on the executor 172 * configuration. 173 * 174 * @param int|float $limit The limit in bytes 175 * @return $this 176 */ 177 public function fileSizeLimit( $limit ) { 178 $this->fileSizeLimit = $limit; 179 return $this; 180 } 181 182 /** 183 * Sets environment variables which should be added to the executed command 184 * environment. In CLI mode, the environment of the parent process will 185 * also be inherited. 186 * 187 * @param string[] $environment array of variable name => value 188 * @return $this 189 */ 190 public function environment( array $environment ) { 191 $this->environment = $environment; 192 return $this; 193 } 194 195 /** 196 * Sends the provided input to the command. Defaults to an empty string. 197 * If you want to pass stdin through to the command instead, use 198 * passStdin(). 199 * 200 * @param string $stdin 201 * @return $this 202 */ 203 public function stdin( string $stdin ) { 204 $this->stdin = $stdin; 205 return $this; 206 } 207 208 /** 209 * Controls whether stdin is passed through to the command, so that the 210 * user can interact with the command when it is run in CLI mode. If this 211 * is enabled: 212 * - The wall clock timeout will be disabled to avoid stopping the 213 * process with SIGTTIN/SIGTTOU (T206957). 214 * - The string specified with input() will be ignored. 215 * 216 * @param bool $yesno 217 * @return $this 218 */ 219 public function passStdin( bool $yesno = true ) { 220 $this->passStdin = $yesno; 221 return $this; 222 } 223 224 /** 225 * Controls whether stderr should be included in stdout, including errors 226 * from wrappers. Default: don't include. 227 * 228 * @param bool $includeStderr 229 * @return $this 230 */ 231 public function includeStderr( bool $includeStderr = true ) { 232 $this->includeStderr = $includeStderr; 233 return $this; 234 } 235 236 /** 237 * If this is set to true, text written to stderr by the command will be 238 * passed through to PHP's stderr. To avoid SIGTTIN/SIGTTOU, and to support 239 * Result::getStderr(), the file descriptor is not passed through, we just 240 * copy the data to stderr as we receive it. 241 * 242 * @param bool $yesno 243 * @return $this 244 */ 245 public function forwardStderr( bool $yesno = true ) { 246 $this->forwardStderr = $yesno; 247 return $this; 248 } 249 250 /** 251 * When enabled, text sent to stderr will be logged with a level of 'error'. 252 * 253 * @param bool $yesno 254 * @return $this 255 */ 256 public function logStderr( bool $yesno = true ) { 257 $this->logStderr = $yesno; 258 return $this; 259 } 260 261 /** 262 * Open FD 3 as a pipe and pass the write side to the command. Lines 263 * written to this pipe will be logged. This is used by some wrappers to 264 * provide log messages. 265 * 266 * @internal For Wrapper subclasses only 267 * @param bool $yesno 268 * @return $this 269 */ 270 public function useLogPipe( bool $yesno = true ) { 271 $this->useLogPipe = $yesno; 272 return $this; 273 } 274 275 /** 276 * Set the working directory under which the command will be run. 277 * 278 * @param string $path 279 * @return $this 280 */ 281 public function workingDirectory( string $path ) { 282 $this->workingDirectory = $path; 283 return $this; 284 } 285 286 /** 287 * Set special options to proc_open(). 288 * 289 * @internal For Wrapper subclasses only 290 * @param array $options 291 * @return $this 292 */ 293 public function procOpenOptions( array $options ) { 294 $this->procOpenOptions = $options; 295 return $this; 296 } 297 298 /** 299 * Disable networking, if possible. 300 * 301 * @param bool $yesno 302 * @return $this 303 */ 304 public function disableNetwork( bool $yesno = true ) { 305 $this->disableNetwork = $yesno; 306 return $this; 307 } 308 309 /** 310 * Specify the set of disabled syscalls. If the sandbox configuration 311 * permits, a seccomp filter will be set up to disallow them. 312 * 313 * @param string[] $syscalls 314 * @return $this 315 */ 316 public function disabledSyscalls( array $syscalls ) { 317 $this->disabledSyscalls = $syscalls; 318 return $this; 319 } 320 321 /** 322 * Enable/disable the default Firejail seccomp filter. This only works if 323 * Firejail is enabled. Firejail will also enable no_new_privs when this is 324 * enabled. 325 * 326 * @param bool $yesno 327 * @return $this 328 */ 329 public function firejailDefaultSeccomp( bool $yesno = true ) { 330 $this->firejailDefaultSeccomp = $yesno; 331 return $this; 332 } 333 334 /** 335 * Enable the no_new_privs attribute to prevent privilege escalation via 336 * setuid executables and similar. 337 * 338 * @param bool $yesno 339 * @return $this 340 */ 341 public function noNewPrivs( bool $yesno = true ) { 342 $this->noNewPrivs = $yesno; 343 return $this; 344 } 345 346 /** 347 * Use a private user namespace. 348 * 349 * @param bool $yesno 350 * @return $this 351 */ 352 public function privateUserNamespace( bool $yesno = true ) { 353 $this->privateUserNamespace = $yesno; 354 return $this; 355 } 356 357 /** 358 * Create a private /dev mount 359 * 360 * @param bool $yesno 361 * @return $this 362 */ 363 public function privateDev( bool $yesno = true ) { 364 $this->privateDev = $yesno; 365 return $this; 366 } 367 368 /** 369 * If called, the files/directories that are allowed will certainly be 370 * available to the shell command. 371 * 372 * Whether this can be respected depends on the configuration of the 373 * executor. 374 * 375 * @param string ...$paths 376 * 377 * @return $this 378 */ 379 public function allowPath( ...$paths ) { 380 $this->allowedPaths = array_merge( $this->allowedPaths, $paths ); 381 return $this; 382 } 383 384 /** 385 * Replace the list of allowed paths. 386 * 387 * @param string[] $paths 388 * @return $this 389 */ 390 public function allowedPaths( array $paths ) { 391 $this->allowedPaths = $paths; 392 return $this; 393 } 394 395 /** 396 * Disallow the specified paths so that the command cannot access them. 397 * 398 * Whether this can be respected depends on the configuration of the 399 * executor. 400 * 401 * @param string ...$paths 402 * @return $this 403 */ 404 public function disallowPath( ...$paths ) { 405 $this->disallowedPaths = array_merge( $this->disallowedPaths, $paths ); 406 return $this; 407 } 408 409 /** 410 * Replace the list of disallowed paths 411 * 412 * @param string[] $paths 413 * @return $this 414 */ 415 public function disallowedPaths( array $paths ) { 416 $this->disallowedPaths = $paths; 417 return $this; 418 } 419 420 /** 421 * Disable firejail and similar sandboxes 422 * 423 * @param bool $yesno 424 * @return $this 425 */ 426 public function disableSandbox( bool $yesno = true ) { 427 $this->disableSandbox = $yesno; 428 return $this; 429 } 430 431 /** 432 * Get command parameters for JSON serialization by the client. 433 * 434 * @internal 435 * @return array 436 */ 437 public function getClientData() { 438 return [ 439 'command' => $this->command, 440 'cpuLimit' => $this->cpuTimeLimit, 441 'wallTimeLimit' => $this->wallTimeLimit, 442 'memoryLimit' => $this->memoryLimit, 443 'fileSizeLimit' => $this->fileSizeLimit, 444 'environment' => $this->environment, 445 'includeStderr' => $this->includeStderr, 446 'logStderr' => $this->logStderr 447 ]; 448 } 449 450 /** 451 * Set command parameters using a data array created by getClientData() 452 * 453 * @internal 454 * @param array $data 455 */ 456 public function setClientData( $data ) { 457 foreach ( $data as $name => $value ) { 458 switch ( $name ) { 459 case 'command': 460 $this->command = $value; 461 break; 462 463 case 'cpuLimit': 464 $this->cpuTimeLimit = $value; 465 break; 466 467 case 'wallTimeLimit': 468 $this->wallTimeLimit = $value; 469 break; 470 471 case 'memoryLimit': 472 $this->memoryLimit = $value; 473 break; 474 475 case 'fileSizeLimit': 476 $this->fileSizeLimit = $value; 477 break; 478 479 case 'environment': 480 $this->environment = $value; 481 break; 482 483 case 'includeStderr': 484 $this->includeStderr = $value; 485 break; 486 487 case 'logStderr': 488 $this->logStderr = $value; 489 break; 490 } 491 } 492 } 493 494 /** 495 * Get the current command string 496 * 497 * @return string 498 */ 499 public function getCommandString() { 500 return $this->command; 501 } 502 503 /** 504 * Get the CPU limit 505 * 506 * @return int|float|null 507 */ 508 public function getCpuTimeLimit() { 509 return $this->cpuTimeLimit; 510 } 511 512 /** 513 * Get the wall clock time limit 514 * 515 * @return int|float|null 516 */ 517 public function getWallTimeLimit() { 518 return $this->wallTimeLimit; 519 } 520 521 /** 522 * Get the memory limit 523 * 524 * @return int|float|null 525 */ 526 public function getMemoryLimit() { 527 return $this->memoryLimit; 528 } 529 530 /** 531 * Get the file size limit 532 * 533 * @return int|float|null 534 */ 535 public function getFileSizeLimit() { 536 return $this->fileSizeLimit; 537 } 538 539 /** 540 * Get the environment 541 * 542 * @return string[] 543 */ 544 public function getEnvironment() { 545 return $this->environment; 546 } 547 548 /** 549 * Get the text to be passed to stdin 550 * 551 * @return string 552 */ 553 public function getStdin() { 554 return $this->stdin; 555 } 556 557 /** 558 * Get whether to pass through stdin 559 * 560 * @return bool 561 */ 562 public function getPassStdin() { 563 return $this->passStdin; 564 } 565 566 /** 567 * Get whether to duplicate stderr to stdout 568 * 569 * @return bool 570 */ 571 public function getIncludeStderr() { 572 return $this->includeStderr; 573 } 574 575 /** 576 * Get whether to log text seen on stderr 577 * 578 * @return bool 579 */ 580 public function getLogStderr() { 581 return $this->logStderr; 582 } 583 584 /** 585 * Get whether to forward the command's stderr to the parent's stderr 586 * 587 * @return bool 588 */ 589 public function getForwardStderr() { 590 return $this->forwardStderr; 591 } 592 593 /** 594 * Get whether to enable the log pipe 595 * 596 * @return bool 597 */ 598 public function getUseLogPipe() { 599 return $this->useLogPipe; 600 } 601 602 /** 603 * @return string|null 604 */ 605 public function getWorkingDirectory() { 606 return $this->workingDirectory; 607 } 608 609 /** 610 * Get the additional proc_open() options 611 * 612 * @return array 613 */ 614 public function getProcOpenOptions() { 615 return $this->procOpenOptions; 616 } 617 618 /** 619 * Get whether to disable external networking 620 * 621 * @return bool 622 */ 623 public function getDisableNetwork() { 624 return $this->disableNetwork; 625 } 626 627 /** 628 * Get the list of disabled syscalls 629 * 630 * @return string[] 631 */ 632 public function getDisabledSyscalls() { 633 return $this->disabledSyscalls; 634 } 635 636 /** 637 * Get whether to use firejail's default seccomp filter 638 * 639 * @return bool 640 */ 641 public function getFirejailDefaultSeccomp() { 642 return $this->firejailDefaultSeccomp; 643 } 644 645 /** 646 * Get whether to enable the no_new_privs process attribute 647 * 648 * @return bool 649 */ 650 public function getNoNewPrivs() { 651 return $this->noNewPrivs; 652 } 653 654 /** 655 * Get whether to use a private user namespace 656 * 657 * @return bool 658 */ 659 public function getPrivateUserNamespace() { 660 return $this->privateUserNamespace; 661 } 662 663 /** 664 * Get whether to mount a private /dev filesystem 665 * 666 * @return bool 667 */ 668 public function getPrivateDev() { 669 return $this->privateDev; 670 } 671 672 /** 673 * Get the allowed paths 674 * 675 * @return string[] 676 */ 677 public function getAllowedPaths() { 678 return $this->allowedPaths; 679 } 680 681 /** 682 * Get the disallowed paths 683 * 684 * @return string[] 685 */ 686 public function getDisallowedPaths() { 687 return $this->disallowedPaths; 688 } 689 690 /** 691 * Get whether to disable firejail and similar sandboxes 692 * 693 * @return bool 694 */ 695 public function getDisableSandbox() { 696 return $this->disableSandbox; 697 } 698} 699