-
Notifications
You must be signed in to change notification settings - Fork 0
/
Initialization.php
409 lines (334 loc) · 14.7 KB
/
Initialization.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
<?php
//� 2019 Martin Peter Madsen
namespace MTM\Shells\Models\Shells\Bash;
class Initialization extends Processing
{
protected $_regEx=null;
protected $_commitChars=null;
protected $_useSudo=false;
protected $_spawnPid=null;
protected $_spawnName=null;
protected $_phpShPid=null;
protected $_phpShName=null;
protected $_basePipes=null;
public function setSudo($bool)
{
$this->_useSudo = $bool;
}
public function getRegEx()
{
if ($this->_regEx === null) {
$this->_regEx = "[" . uniqid("bash.", true) . "]";
}
return $this->_regEx;
}
protected function getCommit()
{
if ($this->_commitChars === null) {
$this->_commitChars = chr(13);
}
return $this->_commitChars;
}
public function initialize()
{
if ($this->_isInit === false) {
$this->_isInit = null;
try {
//set the prompt to a known value
$strCmd = "PS1=\"".$this->getRegEx()."\"";
$regEx = "(".preg_quote($this->getRegEx()).")";
$rTries = 10;
while (true) {
try {
$this->getCmd($strCmd, $regEx)->get();
break; //success
} catch (\Exception $e) {
switch ($e->getCode()) {
case 92987:
if ($rTries > 0) {
$rTries--;
//failed to write command to stdIn: Cannot add to a file, error opening for writing: /dev/shm/xxxxxxxxxx/stdIn
//system is a little busy and the pipe is not yet ready, just wait a little
usleep(100000);
break;
} else {
//fail
throw $e;
}
default:
throw $e;
}
}
}
//ssh connections will not inherit the terminal width of the parent.
$this->setTerminalSize(1000, 1000);
//dont record a history for this session
$strCmd = "unset HISTFILE";
$this->getCmd($strCmd)->get();
if ($this->getParent() === null) {
//if there is no parent then this is the initial shell
//get the PIDs back to init, we can use this to kill the shell if everything else fails.
$strCmd = "CURID=\$\$; while [[ \$CURID != 1 ]]; do echo $(cat /proc/\$CURID/status | grep \"Name:\" | awk '{ print $2 }'); echo \$CURID; CURID=\$(cat /proc/\$CURID/status | grep \"PPid:\" | awk '{ print $2 }'); done";
$data = $this->getCmd($strCmd)->get();
$procDatas = explode("\n", $data);
//index 1 is the bash prompt it self
//index 3 is the python spawn process
//index 5 depends on if we launched using sudo
// if $this->_useSudo === true then 5 is the sudo elevation of python
// if $this->_useSudo === false then 5 is the php shell execution
//index 7 only exists when $this->_useSudo === true, then it is the php shell execution
if ($this->_useSudo === false) {
if (count($procDatas) === 6) {
$this->_spawnName = trim($procDatas[2]);
$this->_spawnPid = trim($procDatas[3]);
$this->_phpShName = trim($procDatas[4]);
$this->_phpShPid = trim($procDatas[5]);
} elseif (count($procDatas) === 7) {
//seen on PHP 8 on raspberry pi
$this->_spawnName = trim($procDatas[2]);
$this->_spawnPid = trim($procDatas[3]);
$this->_phpShName = trim($procDatas[4]);
$this->_phpShPid = trim($procDatas[5]);
} else {
throw new \Exception("Failed to get process id");
}
} else {
if (count($procDatas) === 8) {
$this->_spawnName = trim($procDatas[2]);
$this->_spawnPid = trim($procDatas[3]);
$this->_phpShName = trim($procDatas[6]);
$this->_phpShPid = trim($procDatas[7]);
} else {
throw new \Exception("Failed to get process id");
}
}
//TODO: check for inotifywait availabillity and use instead of polling
//if a user is running sudo to get a root shell, we would not be able to kill the PID
//with the regular user if regular termination failed for some reason
//because the PID would belong to root
//we add a fail safe here, open a second process. If exit fails we kill the process
//since this process may outlive the php session
//its important that we check that the PID has not been taken over by another process
$osTool = \MTM\Utilities\Factories::getSoftware()->getOsTool();
$psPath = $osTool->getExecutablePath("ps");
$killPath = $osTool->getExecutablePath("kill");
$loopSleep = 2;
$procPid = getmypid();
//start a while loop. When lock is no longer, we clean up if needed
$strCmd = "(";
//must use nohup otherwise we cannot exit the shell until the process finishes
//nohup is POSIX compliant and will ignore any parent hangup signal
$strCmd .= " nohup sh -c '";
//currently the only senario that causes a zombie process is php execution timeout.
//when that happens register_shutdown_function is not called and the lock file is never removed.
$phpMax = ini_get("max_execution_time");
if ($phpMax > 0) {
//we have a timeout, likely running on a webserver
//set a max life time, since the PID is from the webserver
//and when our script ends, the PID likely lives on to serve
//other requests
$maxCount = ceil($phpMax / $loopSleep);
$strCmd .= " declare -i LOOPCOUNT=0;";
$strCmd .= " while";
$strCmd .= " [ -f \"" . $this->getPipes()->getLock()->getPathAsString() . "\" ] &&";
$strCmd .= " [ -n \"".$procPid."\" -a -e /proc/" . $procPid . " ] &&";
$strCmd .= " [ \"\$LOOPCOUNT\" -lt ".$maxCount . " ];";
$strCmd .= " do";
$strCmd .= " LOOPCOUNT+=1;";
$strCmd .= " sleep ".$loopSleep."s;";
$strCmd .= " done;";
} else {
//if we are in CLI mode, the PID will die with us
//if not stop setting the max_execution_time unlimited :)
$strCmd .= " while";
$strCmd .= " [ -f \"" . $this->getPipes()->getLock()->getPathAsString() . "\" ] &&";
$strCmd .= " [ -n \"".$procPid."\" -a -e /proc/" . $procPid . " ];";
$strCmd .= " do";
$strCmd .= " sleep ".$loopSleep."s;";
$strCmd .= " done;";
}
//sleep another cycle, that way processes have a chance to shutdown normally
//we dont know if the timer is on 0 when the lock file is removed
//the FS factory seems to destroy the lock before he shell is down
$strCmd .= " sleep ".$loopSleep."s ;";
//check the PHP sub process we spawned is is still alive
$strCmd .= " ".$killPath." -0 ".$this->_phpShPid." > /dev/null 2>&1;";
$strCmd .= " [ \$(echo \$?) == 0 ] &&";
$strCmd .= " PHPNAME=\$( ".$psPath." -p ".$this->_phpShPid." -o comm= ) &&"; //check the PHP sh pid process has the right name
$strCmd .= " [ \"\$PHPNAME\" == \"".$this->_phpShName."\" ] &&";
$strCmd .= " ".$killPath." -9 " . $this->_phpShPid . ";";//php spawn is still alive, kill it
//check the Python sub process we spawned is is still alive
$strCmd .= " ".$killPath." -0 ".$this->_spawnPid." > /dev/null 2>&1;";
$strCmd .= " [ \$(echo \$?) == 0 ] &&";
$strCmd .= " PYNAME=\$( ".$psPath." -p ".$this->_spawnPid." -o comm= ) &&"; //check the Python spawn pid process has the right name
$strCmd .= " [ \"\$PYNAME\" == \"".$this->_spawnName."\" ] &&";
$strCmd .= " ".$killPath." -9 " . $this->_spawnPid . ";";//process is alive and confirmed to be ours, -SIGKILL does not work on CentOS7, must be numerical for some reason
//check if the pipe dir is still here
$strCmd .= " [ -d \"".$this->getPipes()->getLock()->getDirectory()->getPathAsString()."\" ] &&";
$strCmd .= " rm -rf \"".$this->getPipes()->getLock()->getDirectory()->getPathAsString()."\"; ";
//debug log
// $strCmd .= " echo \"Ended bash shell: ".$this->getPipes()->getLock()->getDirectory()->getName()."\" >> ".MTM_FS_TEMP_PATH."mtm-shells.log";
//we dont want output
$strCmd .= " ' & ) > /dev/null 2>&1;";
$this->getCmd($strCmd)->get();
}
//reset the output so we have a clean beginning
$this->getPipes()->resetStdOut();
//fully initialized
$this->_isInit = true;
} catch (\Exception $e) {
$this->_isInit = false;
throw $e;
}
}
}
protected function getBasePipes()
{
if ($this->_basePipes === null) {
if ($this->getParent() === null) {
$osTool = \MTM\Utilities\Factories::getSoftware()->getOsTool();
if ($osTool->getType() == "linux") {
//get exe paths
$killPath = $osTool->getExecutablePath("kill");
$bashPath = $osTool->getExecutablePath("bash");
$pythonPath = $osTool->getExecutablePath("python3"); //prefer python3
if ($killPath === false) {
throw new \Exception("Missing Kill application");
} elseif ($bashPath === false) {
throw new \Exception("Missing Bash application");
} elseif ($pythonPath === false) {
//e.g. Centos8 does not ship with python
//dnf install python3 -y
//rm -rf /usr/bin/python; ln -s /usr/bin/python3 /usr/bin/python
$pythonPath = $osTool->getExecutablePath("python");
if ($pythonPath === false) {
throw new \Exception("Missing Python application");
}
}
if ($this->_useSudo === true) {
$sudoTool = \MTM\Utilities\Factories::getSoftware()->getSudoTool();
if ($sudoTool->isEnabled("python") === false) {
throw new \Exception("Cannot sudo python");
}
}
$fileFact = \MTM\FS\Factories::getFiles();
$dirFact = \MTM\FS\Factories::getDirectories();
$height = 1000;
$width = 1000;
//need non temp, since temp are torn down too quickly on destroy
//files are removed before we have finshed up the termination process
$dirObj = $dirFact->getNonTempDirectory();
$stdIn = $fileFact->getFile("stdIn", $dirObj);
$stdOut = $fileFact->getFile("stdOut", $dirObj);
$stdErr = $fileFact->getFile("stdErr", $dirObj);
$lock = $fileFact->getFile("procLock", $dirObj);
//will only be triggered on shutdown, so its ok if FS removes
//the file before we manage to terminate
$fileFact->setAsTempFile($lock);
//create files
$stdOut->create();
$stdErr->create();
$lock->create();
//on RHEL 7 the xterm TERM will show a duplicate PS1 command that cannot be removed,
//xterm-mono might work and help us with those pesky colors, need testing
$term = "vt100";
//create stdIn pipe
$strCmd = "mkfifo ".$stdIn->getPathAsString().";";
//segment off the entire command so we can return from exec() right away
$strCmd .= " (";
//stdIn must be bound to a process. We will be writing to it like a file
//so we use sleep to hold the pipe open
$strCmd .= " sleep 1000d > ".$stdIn->getPathAsString()." &";
//segment off the spawned process
$strCmd .= " (";
//setup the environment that the spawned python shell will inherit
$strCmd .= " export TERM=".$term.";";
//because the sleep process that is holding stdIn open is not bound
//we need a way to tear it down when we exit
$strCmd .= " SLEEP_PID=\$! ;";
//are we going to spawn a root shell using sudo?
if ($this->_useSudo === true) {
$strCmd .= " sudo";
}
//setup python to spawn a new bash shell
$strCmd .= " " . $pythonPath." -c";
$strCmd .= " \"";
//import the python os and pty packages
$strCmd .= "import pty, os;";
//set the height of the environment
$strCmd .= " os.environ['LINES'] = '".$height."';";
//set the width of the environment
$strCmd .= " os.environ['COLUMNS'] = '".$width."';";
//spawn bash as the new process
$strCmd .= " pty.spawn(['" . $bashPath . "']);";
$strCmd .= "\"";
//instruct the python shell to use our files and stdIn/stdOut/stdErr
$strCmd .= " < " . $stdIn->getPathAsString();
$strCmd .= " > " . $stdOut->getPathAsString();
$strCmd .= " 2> " . $stdErr->getPathAsString() . ";";
//when the python process exits wait a bit before cleaning up, maybe PHP wants one more read of stdOut
$strCmd .= " sleep 2s;";
//kill the sleep process holding stdIn open
$strCmd .= " \"".$killPath."\" -9 \$SLEEP_PID ;";
//remove the directory we are working in
$strCmd .= " rm -rf ".$dirObj->getPathAsString()." &";
//end of segment
$strCmd .= " ) &";
//end of segment
$strCmd .= " )";
//send all output to null
$strCmd .= " > /dev/null 2>&1";
//give me a shell!
exec($strCmd, $rData, $status);
if ($status != 0) {
throw new \Exception("Failed to excute shell setup: " . $status);
}
try {
//if the server is busy it could take a bit to setup the shell
$maxWait = 30;
$eTime = time() + $maxWait;
$stdInOk = false;
while ($eTime > time()) {
$stdErrData = $stdErr->getContent();
if ($stdErrData != "") {
$stdErrData = trim($stdErrData);
break;
}
//simply checking if the file exists is not enough
//many exceptions are caused by the stdin pipe not being ready to accept data
$stdInFp = @fopen($stdIn->getPathAsString(), "an");
if (is_resource($stdInFp) === true) {
fclose($stdInFp);
$stdInOk = true;
break;
} else {
usleep(10000);
}
}
if ($stdErrData != "") {
if (strpos("SyntaxError: Non-ASCII character", $stdErrData) !== false) {
//need to deal with the environment for python 2
//cannot figure out how to force utf-8, which is the standard on python3
throw new \Exception("Failed to create shell. Please consider using python3. Error: '".$stdErrData."'");
} else {
throw new \Exception("Failed to create shell. Error: '".$stdErrData."'");
}
} elseif ($stdInOk !== true) {
throw new \Exception("stdIn was never created");
}
$this->_basePipes = new \MTM\Shells\Models\Shells\ProcessPipe();
$this->_basePipes->setPipes($stdIn, $stdOut, $stdErr)->setLock($lock);
} catch (\Exception $e) {
$dirObj->delete();
throw $e;
}
} else {
throw new \Exception("Not handled");
}
} else {
throw new \Exception("Has parent, cannot be base");
}
}
return $this->_basePipes;
}
}