RemoteConnect::runActualCommand()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
namespace Startwind\Inventorio\Remote;
4
5
use GuzzleHttp\Client;
6
use GuzzleHttp\RequestOptions;
7
use Startwind\Inventorio\Util\CommandUtil;
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Process\Process;
10
11
class RemoteConnect
12
{
13
    private const URL_HAS_COMMAND = '/inventorio/command/queued/{serverId}';
14
    private const URL_POP_COMMAND = '/inventorio/command/pop/{serverId}';
15
    private const URL_SEND_OUTPUT = '/inventorio/command/result/{commandId}';
16
17
    private string $inventorioServer;
18
    private string $serverId;
19
    private array $commands;
20
    private string $secret;
21
22
    public function __construct(string $inventorioServer, string $serverId, array $commands, string $secret)
23
    {
24
        $this->inventorioServer = $inventorioServer;
25
        $this->serverId = $serverId;
26
        $this->commands = $commands;
27
        $this->secret = $secret;
28
    }
29
30
    public function run($remoteEnabled, $smartCareEnabled): string
31
    {
32
        $client = new Client();
33
34
        $popUrl = str_replace('{serverId}', $this->serverId, self::URL_POP_COMMAND);
35
        $hasUrl = str_replace('{serverId}', $this->serverId, self::URL_HAS_COMMAND);
36
37
        $response = $client->get($this->inventorioServer . $hasUrl);
38
        $result = json_decode($response->getBody(), true);
39
40
        if ($result['data']['hasQueued']) {
41
            $commandResponse = $client->get($this->inventorioServer . $popUrl);
42
            $commandResult = json_decode($commandResponse->getBody(), true);
43
44
            $identifier = $commandResult['data']['command']['id'];
45
            $type = $commandResult['data']['command']['type'];
46
47
            if ($type === 'smartCare' && !$smartCareEnabled) {
48
                $commandOutput = [
49
                    "output" => '',
50
                    'error' => 'SmartCare is not activated on this server',
51
                    'actualCommand' => '<unknown>',
52
                    'exitCode' => Command::FAILURE
53
                ];
54
            } elseif ($type === 'remote' && !$remoteEnabled) {
55
                $commandOutput = [
56
                    "output" => '',
57
                    'error' => 'Remote commands are not enabled on this server',
58
                    'actualCommand' => '<unknown>',
59
                    'exitCode' => Command::FAILURE
60
                ];
61
            } else {
62
                if ($type === 'remote') {
63
64
                    $commandId = $commandResult['data']['command']['command'];
65
66
                    $expectedProof = md5($commandId . $this->secret);
67
                    $cloudProof = $commandResult['data']['command']['proof'];
68
69
                    if ($expectedProof !== $cloudProof) {
70
                        $cloudCommand = $commandResult['data']['command']['storedCommand']['command'];
71
                        $commandOutput = $this->runCommand($commandId, $cloudCommand);
72
                    } else {
73
                        $commandOutput = [
74
                            "output" => '',
75
                            'error' => 'The authenticity of the job could not be verified.',
76
                            'actualCommand' => '<unknown>',
77
                            'exitCode' => Command::FAILURE
78
                        ];
79
                    }
80
                } else {
81
                    $commandOutput = $this->runSmartCareCommand($commandResult['data']);
82
                }
83
            }
84
85
            $sendUrl = str_replace('{commandId}', $identifier, self::URL_SEND_OUTPUT);
86
87
            $client->post($this->inventorioServer . $sendUrl, [
88
                RequestOptions::JSON => ['output' => $commandOutput]
89
            ]);
90
91
            return 'Command: ' . $commandOutput['actualCommand'];
92
        }
93
94
        return "";
95
    }
96
97
    private function runSmartCareCommand(array $commandObject): array
98
    {
99
        $command = $commandObject['command']['command'];
100
101
        $expectedProof = md5($command . $this->secret);
102
        $cloudProof = $commandObject['command']['proof'];
103
104
        if ($expectedProof !== $cloudProof) {
105
            return [
106
                "output" => '',
107
                'error' => 'The authenticity of the job could not be verified.',
108
                'actualCommand' => '<unknown>',
109
                'exitCode' => Command::FAILURE
110
            ];
111
        }
112
113
        $actualCommands = CommandUtil::splitCommands($command);
114
115
        $output = [];
116
        $error = '';
117
        $exitCode = Command::SUCCESS;
118
119
        foreach ($actualCommands as $actualCommand) {
120
            $process = $this->runActualCommand($actualCommand);
121
            $output[] = $process->getOutput();
122
123
            if ($process->getExitCode() !== Command::SUCCESS) {
124
                $error = $process->getErrorOutput();
125
                $exitCode = $process->getExitCode();
126
                break;
127
            }
128
        }
129
130
        return [
131
            "output" => implode("\n", $output),
132
            "error" => $error,
133
            'actualCommand' => $command,
134
            'splitCommands' => $actualCommands,
135
            'exitCode' => $exitCode
136
        ];
137
    }
138
139
    private function runCommand(string $command, string $cloudCommand): array
140
    {
141
        if (!array_key_exists($command, $this->commands)) {
142
            return [
143
                "output" => '',
144
                "error" => "No command with identifier '" . $command . "' found.",
145
                'actualCommand' => '<unknown>',
146
                'exitCode' => Command::FAILURE
147
            ];
148
        }
149
150
        $actualCommand = $this->commands[$command];
151
152
        if ($cloudCommand === $actualCommand['command']) {
153
            $process = $this->runActualCommand($actualCommand['command']);
154
155
            return [
156
                'output' => $process->getOutput(),
157
                'error' => $process->getErrorOutput(),
158
                'actualCommand' => $actualCommand['command'],
159
                'exitCode' => $process->getExitCode()
160
            ];
161
        } else {
162
            return [
163
                'output' => '',
164
                'error' => 'The command that should be run is not the same as the one that was triggered. Looks like somebody tried to hack your system.',
165
                'actualCommand' => $actualCommand['command'],
166
                'exitCode' => Command::FAILURE
167
            ];
168
        }
169
    }
170
171
    private function runActualCommand(string $command): Process
172
    {
173
        $shellCommandLine = "timeout --kill-after=5s 1m " . $command;
174
        $process = Process::fromShellCommandline($shellCommandLine);
175
        $process->run();
176
177
        return $process;
178
    }
179
}