Ferreteria/v0.6/clade/IO/Aspect/Connx/Stream/Runner/Remote/SSH2

From Woozle Writes Code
< Ferreteria‎ | v0.6‎ | clade‎ | IO‎ | Aspect‎ | Connx‎ | Stream‎ | Runner‎ | Remote
Jump to navigation Jump to search
clade: IO\Aspect\Connx\Stream\Runner\Remote\SSH2
Clade Family
Remote SSH2 (none)
Clade Aliases
Alias Clade
Base* [ca,i] IO\Aspect\Connx\Stream\Runner\Remote
CLineIface Sys\Data\Codec\aux\CmdLine
CommOp* [c,i] IO\Aspect\Connx\aux\itWent\CommOp
ConveyClass IO\Aspect\Connx\Stream\Buffer\Conveyer
CredsIface IO\Aspect\Creds
QRes* [c,i] Data\Mem\QVar\Res
Result* [c,i] IO\Aspect\Connx\aux\itWent\ProcOp
StreamClass IO\Aspect\Connx\Stream\Native\Generic
Tunnel* [c,i] IO\Aspect\Connx\aux\Tunnel
Subpages

About

  • Purpose: a secure shell (ssh) connection to a remote machine using PHP's ssh2 library

History

  • 2024-11-02 started
  • 2024-11-19 moved from [WFe]Config\Def\Connex\Shell -> [WFe]IO\Connex\Shell
  • 2024-11-26 moved from [WFe]IO\Connex\Shell\xSecure -> [WFe]IO\Connex\Aux\ssh\xShell
  • 2025-03-15 moved from [WFe]IO\Connx\Aux\ssh\xRemote -> [WFe]IO\Connx\Shell\Remote\xSSH
    • ...and re-parented from [WFe]IO\Connx\xShell ⇒ [WFe]IO\Connx\Shell\Remote
  • 2025-03-16 Removing the $oICreds parameter from NewTunnel(), because as far as I can tell it's just supposed to be the same creds stored in Creds()
    • Also not sure if the $nPortLocal parameter still makes sense... shouldn't that be in the Creds data as well?
  • 2025-05-27 more re-namespacing
  • 2026-01-20 see DoCommand()

Functions

Code

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 -- //
}