TikiWiki/tiki-manager

View on GitHub
src/Libs/Host/SSH.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php
// Copyright (c) 2016, Avan.Tech, et. al.
// Copyright (c) 2008, Luis Argerich, Garland Foster, Eduardo Polidor, et. al.
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.

namespace TikiManager\Libs\Host;

use TikiManager\Config\App;

class SSH
{
    private static $sshKeyCheck = [];

    private $adapter;
    private $location;
    private $env = [];

    private $host;
    private $user;
    private $port;

    private $copy_id_port_in_host;

    protected $io;

    public function __construct($host, $user, $port, $adapter_class = null)
    {
        $this->host = $host ?: '';
        $this->user = $user ?: '';
        $this->port = $port ?: 22;
        $this->checkCopyId();
        $this->selectAdapter($adapter_class);
        $this->setenv('HTTP_ACCEPT_ENCODING', '');

        $this->io = App::get('io');

        $sshConnectionId = implode('_', [$this->user,$this->host, $this->port]);
        if (!isset(self::$sshKeyCheck[$sshConnectionId])) {
            self::$sshKeyCheck[$sshConnectionId] = $this->checkSshKey();
        }
    }

    public function chdir($location)
    {
        $this->location = $location;
        $this->adapter->setLocation($location);
    }

    public function checkCopyId()
    {
        $this->copy_id_port_in_host = true;
        $ph = popen('ssh-copy-id -h 2>&1', 'r');
        if (! is_resource($ph)) {
            $this->io->error('Required command (ssh-copy_id) not found.');
        } else {
            if (preg_match('/p port/', stream_get_contents($ph))) {
                $this->copy_id_port_in_host = false;
            }
            pclose($ph);
        }
    }

    public function setenv($var, $value)
    {
        $this->env[$var] = $value;
        $this->adapter->setEnv($this->env);
    }

    public function setupKey($publicKeyFile)
    {
        $this->adapter->unsetHandle();
        $file = escapeshellarg($publicKeyFile);

        if ($this->copy_id_port_in_host) {
            $host = escapeshellarg("-p {$this->port} {$this->user}@{$this->host}");
            `ssh-copy-id -i $file $host`;
        } else {
            $port = escapeshellarg($this->port);
            $host = escapeshellarg("{$this->user}@{$this->host}");
            `ssh-copy-id -i $file -p $port $host`;
        }
    }

    public function runCommand($command, $options = [])
    {
        return $this->adapter->runCommand($command, $options);
    }

    public function runCommands($commands, $output = false)
    {
        if (!is_array($commands)) {
            $commands = func_get_args();
            $output = end($commands) === true;
            $commands = array_filter($commands, 'is_string');
        }
        return $this->adapter->runCommands($commands, $output);
    }

    public function sendFile($localFile, $remoteFile)
    {
        $exitCode = $this->rsync([
            'src' => $localFile,
            'dest' => $remoteFile,
        ]);

        if ($exitCode == 0) {
            $this->runCommands(["chmod 0644 $remoteFile"]);
        }

        return $exitCode == 0;
    }

    public function receiveFile($remoteFile, $localFile)
    {
        $exitCode = $this->rsync([
            'src' => $remoteFile,
            'dest' => $localFile,
            'download' => true
        ]);

        return $exitCode == 0;
    }

    public function openShell($workingDir = '')
    {
        $key = $_ENV['SSH_KEY'];
        $port = null;
        if ($this->port != 22) {
            $port = " -p {$this->port} ";
        }
        if (strlen($workingDir) > 0) {
            $command = "ssh $port -i $key {$this->user}@{$this->host} " .
                "-t 'cd {$workingDir}; pwd; bash --login'";
        } else {
            $command = "ssh $port -i $key {$this->user}@{$this->host}";
        }

        if (! empty($_ENV['RUN_THROUGH_TIKI_WEB'])) {
            return $command;
        }

        passthru($command);
    }

    public function rsync($args = [])
    {
        $return_val = -1;
        if (empty($args['src']) || empty($args['dest'])) {
            return $return_val;
        }

        $exclude = [];
        if (!empty($args['exclude'])) {
            $exclude = is_array($args['exclude']) ? $args['exclude'] : [$args['exclude']];
            $exclude = array_map(function ($path) {
                return '--exclude=' . $path;
            }, $exclude);
        }

        $key = $_ENV['SSH_KEY'];
        $user = $this->user;
        $host = $this->host;
        $src = $args['src'];
        $dest = $args['dest'];
        $port = $this->port ;

        $localHost = new Local();

        // path may be escaped, in that case we un-escape, since we will escape after
        if (substr($src, 0, 1) === "'" && substr($src, -1) === "'"
            || substr($src, 0, 1) === '"' && substr($src, -1) === '"'
        ) {
            $src = substr($src, 1, -1);
        }

        // is it relative
        if ($src[0] !== '/' && ! empty($this->location)) {
            $src = $this->location . '/' . $src;
        }

        $rsyncParams = ['-a', '-L', '--delete'];
        $rsyncParams = array_merge($rsyncParams, $exclude);
        $rsyncParams[] = '-e';
        $rsyncParams[] = "ssh -p {$port} -i $key";
        if (isset($args['download']) && $args['download']) {
            $rsyncParams[] = "{$user}@{$host}:".escapeshellarg($src);
            $rsyncParams[] = $dest;
        } else {
            $rsyncParams[] = $src;
            $rsyncParams[] = "{$user}@{$host}:".escapeshellarg($dest);
        }

        $command = new Command('rsync', $rsyncParams);
        $localHost->runCommand($command);
        $return_var = $command->getReturn();

        if ($return_var != 0) {
            $this->io->error("RSYNC exit code: $return_var");
        }

        return $return_var;
    }

    private function selectAdapter($className)
    {
        // keep SSHSeclibAdapter as the default only for windows.
        $defaultClass = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? 'TikiManager\Libs\Host\SSHSeclibAdapter' : 'TikiManager\Libs\Host\SSHWrapperAdapter';
        $className = $className ?: $defaultClass;

        try {
            $this->adapter = new $className(
                $this->host,
                $this->user,
                $this->port
            );
        } catch (\Exception $e) {
            $this->adapter = new SSHWrapperAdapter(
                $this->host,
                $this->user,
                $this->port
            );
            debug("Unable to use $className, falling back to SSHWrapperAdapter");
        }
        return $this->adapter;
    }

    /**
     * Check if Private Key is within the authorized keys from remote server
     */
    private function checkSshKey()
    {
        $params = [
            '-i',
            $_ENV['SSH_KEY'],
            '-o',
            "IdentitiesOnly yes",
            '-o',
            "PreferredAuthentications publickey",
        ];

        if (! empty($_ENV['RUN_THROUGH_TIKI_WEB'])) {
            // Tiki web could use www-data user which fails host key checking
            $params[] = '-o';
            $params[] = "StrictHostKeyChecking no";
        }

        $params[] = "{$this->user}@{$this->host}";
        $params[] = "exit";

        if ($this->port !== '22') {
            array_unshift($params, '-p', $this->port);
        }

        $localHost = new Local();
        $command = new Command('ssh', $params);

        $localHost->runCommand($command);
        $returnVar = $command->getReturn();

        if ($returnVar != 0) {
            $message = "Your ssh keys are not properly set up. Please use 'tiki-manager instance:copysshkey' command.";
            if ($err = $command->getStderrContent()) {
                $message .= ' ' . $err;
            }
            $this->io->error($message);
            return false;
        }

        return true;
    }
}

// vi: expandtab shiftwidth=4 softtabstop=4 tabstop=4