📜 ⬆️ ⬇️

We write a simple web terminal emulator for PHP

I think a lot of people thought about making their terminal emulator in PHP, and usually stopped at solutions like the following:
<?php echo '<form><input name="cmd" /></form>'; if(isset($_GET['cmd'])) system($_GET['cmd']); 

Of course, such a solution causes a whole set of problems, the most insignificant of which is that the errors on the screen do not fall. There are much more significant things, for example, starting vi simply “hangs up” the execution of the command and you have to open a new console and write killall vi . And what certainly can’t be done is to execute ssh or sudo commands that require reading the password directly from the terminal. I will try to show a way with which you can eliminate most of the problems described above.

Writing a plain terminal emulator in PHP


For our terminal emulator you will need:

Perhaps, having seen the link to JSLinux, you have already begun to guess what we are going to do;).

The main idea of ​​implementation


The PHP documentation says that proc_open () is intended for two-way communication with processes, so we will open bash interactively using this function and will continue to work with it. Unfortunately, there is no support for pseudo terminals in PHP out of the box, so we will write the implementation of the required layer in C.

Any protection, as well as checks for errors and correct terminal completion in this example are not supposed (!) , You will have to think about it yourself;).
')

File shell.php



Getting user input

We must somehow receive input from the user, for example, through a FIFO file:

 <?php $temp_fifo_file = '/tmp/dolphin-pipe-'.uniqid('dolph'); if (!posix_mkfifo($temp_fifo_file, 0600)) { echo "Fatal error: Cannot create fifo file: something wrong with the system.\n"; exit(1); } function deleteTempFifo() { unlink($GLOBALS['temp_fifo']); } register_shutdown_function('deleteTempFifo'); $cmdfp = fopen($temp_fifo_file, 'r+'); stream_set_blocking($cmdfp, 0); 

Setting environment variables for the terminal

JSLinux works in vt100 emulation mode, we will also do the same :)

 putenv('TERM=vt100'); $cols = 80; //    ,  - $rows = 24; 

Command to run bash

Basically, we can just run " bash -i ", and this will work (even " sh -i " will work), but even better, if we can work through a pseudo-terminal, the programs will behave more "naturally" in this case . At the same time, we can use our bashrc, in which we will configure a color invitation :).

 chdir(dirname(__FILE__)); $cmd = "bash --rcfile ./bashrc -i 2>&1"; //         (pt.c,   ) //    ,       if (!file_exists('pt')) { system('cc -D__'.strtoupper(trim(`uname`)).'__ -o pt pt.c -lutil 2>&1', $retval); if ($retval) echo('<b>Warning:</b> Cannot compile pseudotty helper'); } clearstatcache(); if (file_exists('pt')) $cmd = "./pt $rows $cols $cmd"; $pp = proc_open($cmd, array(array('pipe','r'), array('pipe', 'w')), $pipes); stream_set_blocking($pipes[0], 0); stream_set_blocking($pipes[1], 0); ?> 

Sending commands from javascript

We will do one HTTP request per character (or more characters if the server “does not have time”). Yes, this may be an unjustified waste of resources in this case, and you can do everything through web sockets, but in terms of implementation, my scheme is much simpler :).

 <html><head><title>Terminal</title></head><body> <script> var pipeName = <?=json_encode($temp_fifo_file)?>, pending_str = '', processing = false; var sendCmdInterv = setInterval(function() { if (processing) return; if (pending_str.length) { processing = true; var previous_str = pending_str; pending_str = ''; var http = new XMLHttpRequest(); http.open("GET", "send-cmd.php?pipe=" + pipeName + "&cmd=" + encodeURIComponent(previous_str), true); http.onreadystatechange = function() { if (http.readyState == 4 && http.status == 200) { processing = false; pending_str = ''; } else { pending_str = previous_str + pending_str; } }; http.send(null); } }, 16); function send_cmd(val) { pending_str += val; } </script> 


Javascript terminal emulator

There are a lot of different useful parts in JSLinux, in this case it's a terminal emulator. The author forbids the distribution and modification of the term.js file without his knowledge, so in this example we will simply refer to its library :) and use it as is.

 <style> .term { font-family: monaco,courier,fixed,monospace,swiss,sans-serif; font-size: 13px; line-height: 16px; color: #f0f0f0; background: #000000; } tr { height: 16px; } .termReverse { color: #000000; background: #00ff00; } </style> <script src="http://bellard.org/jslinux/utils.js"></script> <script src="http://bellard.org/jslinux/term.js"></script> <script>var term = new Term(<?=$cols?>, <?=$rows?>, send_cmd); term.open();</script> 

Reading user input and executing commands

We read the user input from our FIFO file, the output of our team from the corresponding pipe, all in non-blocking mode, for simplicity. Also, as a small crutch, replace "\ n" with "\ r \ n", since in fact, term.js handles the output of the serial port driver, and not the "raw" output of the program;).

 <?php echo "<!-- ".str_repeat('-', 4096)." -->\n"; flush(); while (!feof($pipes[1])) { $ln = fgets($pipes[1], 4096); if ($ln !== false) { $ln = str_replace("\n", "\r\n", $ln); echo '<script>term.write('.json_encode($ln).');</script>'; flush(); continue; } $inp_ln = fgets($cmdfp, 4096); if ($inp_ln !== false) { // ensure that command is fully written by setting blocking to 1 stream_set_blocking($pipes[0], 1); fwrite($pipes[0], $inp_ln); stream_set_blocking($pipes[0], 0); } usleep(20000); } proc_close($pp); ?> 

We clear reception of commands after an exit

After the process is completed (for example, a person pressed Ctrl + D or entered “exit”), you need to stop sending user input to the server, because there is no one to accept it anyway :).
 <script>clearInterval(sendCmdInterv);</script> </body> </html> 


File send-cmd.php


The file to which we send commands is called send-cmd.php and consists of what (yes, no checks of input parameters :)):

 <?php $fp = fopen($_GET['pipe'], 'r+'); fwrite($fp, $_GET['cmd']); fclose($fp); 


Bashrc file


Here is what bashrc I suggest to use:

 export PS1='\[\033[01;33m\]\u\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' export PS2='> ' export PS4='+ ' export LANG=en_US.UTF-8 echo Welcome to simple terminal emulator'!' echo Scroll up and down using Ctrl-Up, Ctrl-Down, Ctrl-PageUp and Ctrl-PageDown. echo Output handling is based on JSLinux term.js library. Enjoy'!' 


Pt.c file


Utilities for working with pseudo-terminal already exist, for example, there is a good expect library that does almost what we need. Nevertheless, it seemed to me interesting to write my own utility, which will simply set the required terminal sizes and output everything to stdout and accept input to stdin:

 #include <unistd.h> #include <sys/select.h> #include <stdio.h> #ifdef __LINUX__ #include <pty.h> #else #include <util.h> #endif static void set_fds(fd_set *reads, int pttyno) { FD_ZERO(reads); FD_SET(0, reads); FD_SET(pttyno, reads); } int main(int argc, char *argv[]) { char buf[1024]; int pttyno, n = 0; int pid; struct winsize winsz; if (argc < 3) { fprintf(stderr, "Usage: %s <rows> <cols> <cmd> [args]\n", argv[0]); return 1; } winsz.ws_row = atoi(argv[1]); winsz.ws_col = atoi(argv[2]); winsz.ws_xpixel = winsz.ws_col * 14; winsz.ws_ypixel = winsz.ws_row * 14; pid = forkpty(&pttyno, NULL, NULL, &winsz); if (pid < 0) { perror("Cannot forkpty"); return 1; } else if (pid == 0) { execvp(argv[3], argv + 3); perror("Cannot exec bash"); } fd_set reads; set_fds(&reads, pttyno); while (select(pttyno + 1, &reads, NULL, NULL, NULL)) { if (FD_ISSET(0, &reads)) { n = read(0, buf, sizeof buf); if (n == 0) { return 0; } else if (n < 0) { perror("Could not read from stdin"); return 1; } write(pttyno, buf, n); } if (FD_ISSET(pttyno, &reads)) { n = read(pttyno, buf, sizeof buf); if (n == 0) { return 0; } else if (n < 0) { perror("Cannot read from ptty"); return 1; } write(1, buf, n); } set_fds(&reads, pttyno); } int statloc; wait(&statloc); return 0; } 


Demonstration of work


Unfortunately, I can’t give you a link to a working demo, since I don’t have a server that I don’t feel sorry for;). However, I recorded a short video that shows how it works:



Together


In theory, all source codes should be in this article. If you are lazy, I put the archive with the appropriate files on the people.

UPD:
If the pt.c program does not compile for FreeBSD, add the following header files to the beginning of the file (and #include <util.h> remove):
 #include <sys/types.h> #include <sys/ioctl.h> #include <termios.h> #include <libutil.h> 

Source: https://habr.com/ru/post/139878/


All Articles