interface iSSH2 extends BaseIface {
// OBJECTS
#function OCred() : CredsIface;
}
class cSSH2 extends BaseClass implements iSSH2 {
// ++ CONFIG ++ //
public function SType() : string { return 'ssh plug'; }
// -- CONFIG -- //
// ++ LIFECYCLE ++ //
protected function ActualOpen() : CommOpIface {
$oAct = $this->ItWentLike();
// [+STANDARD CODE]: get connection specs
$oEntry = $this->OEntry();
$oHost = $oEntry->OHost();
$oCred = $oEntry->OCred();
#$oSock = $this->OSock();
$sUser = $oCred->QSUser()->GetIt();
$sHost = $oHost->QSAddr()->GetIt();
$onPort = $oHost->QIPort();
// [-STANDARD CODE]
if ($onPort->HasIt()) {
$nPort = $onPort->GetIt();
$ftRem = "$sUser@$sHost:$nPort";
} else {
$ftRem = "$sUser@$sHost";
}
echo "Opening SSH connection to [$ftRem]...".CRLF;
try {
if ($onPort->HasIt()) {
$rSSH = @ssh2_connect($sHost, $nPort);
} else {
$rSSH = @ssh2_connect($sHost); // use ssh2_connect's default port
}
} catch (\Exception $e) {
$rSSH = FALSE;
echo $e;
$this->AmHere('TODO 2025-12-07: better diagnostics');
}
if ($rSSH === FALSE) {
$arErr = error_get_last();
$oScrn = self::Screen();
echo $oScrn->ErrorIt('Connection error').': '.$arErr['message'].CRLF;
die();
}
$this->QNative()->SetIt($rSSH);
$ftUser = self::Screen()->GreenIt($sUser);
echo "Authenticating remote user '$ftUser'...".CRLF;
@$rok = ssh2_auth_agent($rSSH,$sUser);
$ok = ($rok !== FALSE);
$oAct->SetOkay($ok);
if ($ok) {
$oAct->AddMsgString("User authenticated.");
} else {
$oAct->AddMsgString("SSH connection failed.");
}
return $oAct;
}
protected function ActualShut() : CommOpIface {
$rConn = $this->QNative()->GetIt();
$ok = ssh2_disconnect($rConn);
$oAct = $this->ItWentLike();
$oAct->SetOkay($ok);
return $oAct;
}
// -- LIFECYCLE -- //
// ++ I/O ++ //
public function HasBytes() : bool { return TRUE; } // 2026-02-09 not sure how to determine
// 2026-02-09 moved here from Native/File/Generic
public function PullBytes(int $nMax=NULL) : CommOpIface {
$bs = @stream_get_contents($this->QNative()->GetIt(),$nMax);
$oOp = new CommOpClass;
$oOp->SetOkay(is_string($bs));
if (is_string($bs)) {
if ($bs !== '') {
$oOp->QData()->SetIt($bs);
}
}
return $oOp;
}
// 2026-02-09 moved here from Native/File/Generic
public function PushBytes(string $s) : int|null {
/* 2026-01-02 buffering version -- buffering functionality has been moved to Stream\Buffer\Sender
$sRem = $s;
$ok = TRUE;
while ($ok && ($sRem !== '')) {
$sNow = substr($sRem,0,self::OBUFF_SIZE); // get burst to send
$ok = fwrite($this->Native(),$s); // try to send it
$n = strlen($s); // how much got sent?
$sRem = substr($sRem,$n); // remove bytes-sent from local buffer
}
*/
$ok = fwrite($this->QNative()->GetIt(),$s); // try to send it
// 2026-01-17 fwrite() supposedly works on non-file streams too.
return $ok;
} // 2025-12-25 This may not work for all stream-types.
// -- I/O -- //
// ++ UI ++ //
public function DescribeInline() : string { return 'SSH -> '.$this->TargetString(); }
protected function TargetString() : string {
#echo $this->ReflectThis()->Report();
$oEnt = $this->OEntry();
$oCred = $oEnt->OCred();
#$oSock = $this->OSock();
#$oHost = $oSock->OHost();
$oHost = $oEnt->OHost();
$sUser = $oCred->QSUser()->GetIt();
$sHost = $oHost->QSAddr()->GetIt();
$onPort = $oHost->QIPort();
if ($onPort->HasIt()) {
$nPort = $onPort->GetIt();
$ftRem = "$sUser@$sHost:$nPort";
} else {
$ftRem = "$sUser@$sHost";
}
if ($this->HasServer()) {
$oServer = $this->OServer();
$ftRem .= ' => '.$oServer->DescribeInline();
}
return $ftRem;
}
// -- UI -- //
// ++ OBJECTS ++ //
private $osNat = NULL; protected function QNative() : QResIface { return $this->osNat ?? ($this->osNat = QResClass::AsNew()); }
public function NewTunnel(CredsIface $oICreds, ?int $nPortLocal=NULL) : TunnelIface {
return new TunnelClass($this->Creds(),$oICreds,$nPortLocal);
}
// -- OBJECTS -- //
// ++ ACTION ++ //
// DOCS: https://wooz.dev/Ferreteria/v0.6/clade/IO/Aspect/Connx/Plug/Shell/Remote/SSH/@fx/DoCommand
public function DoCommand(CLineIface $oCmd) : ResultIface {
#$this->AmHere("COMMAND for ssh: ".$oCmd->AsString());
$rStrm = $this->QNative()->GetIt();
$this->AmHere("COMMAND @SSH2: ".$oCmd->AsString());
$rRecv = ssh2_exec($rStrm, $oCmd->AsString());
#$rErrs = ssh2_fetch_stream($rRecv, SSH2_STREAM_STDERR); // 2026/02/06 there are complications with this, apparently; see docs for ssh2_exec()
$this->QNative()->SetIt($rRecv);
$qoListen = $oCmd->QOListener();
if ($qoListen->HasIt()) {
$oListen = $qoListen->GetIt();
#echo $oListen->ReflectThis()->Report();
#echo ConveyClass::ReflectSelf()->Report(); die();
$oConvey = new ConveyClass;
$oConvey->Convey($this,$oListen);
} else {
stream_set_blocking($rRecv, TRUE); // wait for entire output when reading
$oCommOp = $this->PullBytes();
}
#echo $oStream->ReflectThis()->Report(); die();
#echo $oCommOp->VIEW_AsBlock();
$oProcOp = new ResultClass;
$oProcOp->CopyFrom($oCommOp);
return $oProcOp;
}
// -- ACTION -- //
}