2024-09-13 10:21:02 +00:00
< ? php
namespace App\Livewire\Project\Shared ;
2024-09-19 09:24:21 +00:00
use App\Helpers\SshMultiplexingHelper ;
2024-09-13 10:21:02 +00:00
use App\Models\Server ;
use Livewire\Attributes\On ;
use Livewire\Component ;
class Terminal extends Component
{
2025-01-23 10:51:01 +00:00
public bool $hasShell = true ;
2024-09-25 08:27:23 +00:00
public function getListeners ()
{
$teamId = auth () -> user () -> currentTeam () -> id ;
return [
" echo-private:team. { $teamId } ,ApplicationStatusChanged " => 'closeTerminal' ,
];
}
public function closeTerminal ()
{
$this -> dispatch ( 'reloadWindow' );
}
2025-01-23 10:51:01 +00:00
private function checkShellAvailability ( Server $server , string $container ) : bool
{
$escapedContainer = escapeshellarg ( $container );
try {
instant_remote_process ([
" docker exec { $escapedContainer } bash -c 'exit 0' 2>/dev/null || " .
" docker exec { $escapedContainer } sh -c 'exit 0' 2>/dev/null " ,
], $server );
return true ;
} catch ( \Throwable ) {
return false ;
}
}
2024-09-13 10:21:02 +00:00
#[On('send-terminal-command')]
public function sendTerminalCommand ( $isContainer , $identifier , $serverUuid )
{
2025-05-29 12:36:13 +00:00
$server = Server :: ownedByCurrentTeam () -> whereUuid ( $serverUuid ) -> firstOrFail ();
2025-06-14 11:56:48 +00:00
if ( ! $server -> isTerminalEnabled () || $server -> isForceDisabled ()) {
2025-06-17 07:28:07 +00:00
abort ( 403 , 'Terminal access is disabled on this server.' );
2025-05-29 12:36:13 +00:00
}
2024-09-13 10:21:02 +00:00
if ( $isContainer ) {
2024-12-04 11:43:41 +00:00
// Validate container identifier format (alphanumeric, dashes, and underscores only)
if ( ! preg_match ( '/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/' , $identifier )) {
2025-01-07 14:31:43 +00:00
throw new \InvalidArgumentException ( 'Invalid container identifier format' );
2024-12-04 11:43:41 +00:00
}
// Verify container exists and belongs to the user's team
2024-09-13 10:21:02 +00:00
$status = getContainerStatus ( $server , $identifier );
if ( $status !== 'running' ) {
2024-09-16 13:35:44 +00:00
return ;
2024-09-13 10:21:02 +00:00
}
2024-12-04 11:43:41 +00:00
2025-01-23 10:51:01 +00:00
// Check shell availability
$this -> hasShell = $this -> checkShellAvailability ( $server , $identifier );
if ( ! $this -> hasShell ) {
return ;
}
2024-12-04 11:43:41 +00:00
// Escape the identifier for shell usage
$escapedIdentifier = escapeshellarg ( $identifier );
$command = SshMultiplexingHelper :: generateSshCommand ( $server , " docker exec -it { $escapedIdentifier } sh -c 'PATH= \$ PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \" \$ SHELL \" ]; then exec \$ SHELL; else sh; fi' " );
2024-09-13 10:21:02 +00:00
} else {
2024-10-02 13:16:55 +00:00
$command = SshMultiplexingHelper :: generateSshCommand ( $server , 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi' );
2024-09-13 10:21:02 +00:00
}
// ssh command is sent back to frontend then to websocket
// this is done because the websocket connection is not available here
// a better solution would be to remove websocket on NodeJS and work with something like
// 1. Laravel Pusher/Echo connection (not possible without a sdk)
// 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
// 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
// 4. Follow-up discussions here:
// - https://github.com/coollabsio/coolify/issues/2298
// - https://github.com/coollabsio/coolify/discussions/3362
$this -> dispatch ( 'send-back-command' , $command );
}
public function render ()
{
return view ( 'livewire.project.shared.terminal' );
}
}