Home >Web Front-end >JS Tutorial >How to implement multi-user web terminal display using node.js
This article mainly introduces the node.js support for multi-user web terminal implementation and the solution to ensure the security of web terminals. Let’s study together for reference.
Terminal (command line), as a common function of local IDEs, has very powerful support for git operations and file operations of projects. For WebIDE, in the absence of a web pseudo-terminal, only providing an encapsulated command line interface is completely unable to satisfy developers. Therefore, in order to provide a better user experience, the development of web pseudo-terminals has been put on the agenda.
Research
The terminal is similar to the command line tool in our understanding. In layman's terms, it is a process that can execute the shell. Every time you enter a series of commands on the command line and press Enter, the terminal process will fork a child process to execute the entered command. The terminal process monitors the exit of the child process through the system call wait4(), and outputs it through the exposed stdout. Child process execution information.
If you implement a terminal function similar to localization on the web side, you may need to do more: network delay and reliability guarantee, shell user experience as close to localization as possible, web terminal UI width and height and Output information adaptation, security access control and authority management, etc. Before implementing the web terminal specifically, it is necessary to evaluate which of these functions are the most core. It is very clear: the functional implementation of the shell, user experience, and security (the web terminal is a function provided in the online server, so security must be guaranteed. of). Only under the premise of ensuring these two functions can the web pseudo terminal be officially launched.
Let’s first consider the technical implementation of these two functions (server-side technology uses nodejs):
The node native module provides the repl module, which can be used to implement interactive input and perform output , and also provides tab completion function, customized output style and other functions. However, it can only execute node-related commands, so it cannot achieve the purpose of executing the system shell. The node native module child_porcess provides spawn, which encapsulates the underlying libuv. The uv_spawn function uses the underlying system calls fork and execvp to execute shell commands. However, it does not provide other features of the pseudo terminal, such as tab automatic completion, arrow keys to display historical commands, etc.
Therefore, it is impossible to implement a pseudo terminal using the native module of node on the server side, and we need to continue to explore pseudo terminals. The principle of the terminal and the implementation direction of the node side.
Pseudo terminal
The pseudo terminal is not a real terminal, but a "service" provided by the kernel. Terminal services usually include three layers:
The top-level input and output interface provided to the character device; the middle-level line discipline; and the bottom-level hardware driver.
Among them, the top-level interface is often passed through System call function implementation, such as (read, write); while the underlying hardware driver is responsible for the master-slave device communication of the pseudo terminal, which is provided by the kernel; the line discipline seems relatively abstract, but in fact it is responsible for the function "Processing" of input and output information, such as processing interrupt characters (ctrl c) and some rollback characters (backspace and delete) during the input process, and converting the output newline character n to rn, etc.
A pseudo terminal is divided into two parts: the master device and the slave device. They are connected at the bottom through a bidirectional pipe (hardware driver) that implements the default line discipline. Any input from the pseudo terminal master is reflected on the slave and vice versa. The output information of the slave device is also sent to the master device through a pipe, so that the shell can be executed in the slave device of the pseudo terminal to complete the terminal function.
The slave device of the pseudo terminal can truly simulate the terminal's tab completion and other shell special commands. Therefore, under the premise that the node native module cannot meet the needs, we need to focus on the bottom layer and see See what functions the OS provides. Currently, the glibc library provides the posix_openpt interface, but the process is a bit cumbersome:
Use posix_openpt to open a pseudo-terminal master device grantpt set the permissions of the slave device unlockpt unlock the corresponding slave device obtain the slave device name (similar to /dev/pts /123) The master (slave) device reads, writes, and performs operations
Therefore, a pty library with better encapsulation has emerged, and all the above functions can be realized through just a forkpty function. By writing a node C extension module and using the pty library, a terminal that executes the command line from the device in a pseudo terminal is implemented.
Regarding the issue of pseudo-terminal security, we will discuss it at the end of the article.
Pseudo terminal implementation ideas
According to the characteristics of the master and slave devices of the pseudo terminal, we manage the pseudo terminal in the parent process where the master device is located The life cycle and its resources are executed in the sub-process of the slave device. The information and results during the execution are transmitted to the master device through a two-way pipe, and the process of the master device provides stdout to the outside.
Learn from the implementation ideas of pty.js here:
pid_t pid = pty_forkpty(&master, name, NULL, &winp); switch (pid) { case -1: return Nan::ThrowError("forkpty(3) failed."); case 0: if (strlen(cwd)) chdir(cwd); if (uid != -1 && gid != -1) { if (setgid(gid) == -1) { perror("setgid(2) failed."); _exit(1); } if (setuid(uid) == -1) { perror("setuid(2) failed."); _exit(1); } } pty_execvpe(argv[0], argv, env); perror("execvp(3) failed."); _exit(1); default: if (pty_nonblock(master) == -1) { return Nan::ThrowError("Could not set master fd to nonblocking."); } Local<Object> obj = Nan::New<Object>(); Nan::Set(obj, Nan::New<String>("fd").ToLocalChecked(), Nan::New<Number>(master)); Nan::Set(obj, Nan::New<String>("pid").ToLocalChecked(), Nan::New<Number>(pid)); Nan::Set(obj, Nan::New<String>("pty").ToLocalChecked(), Nan::New<String>(name).ToLocalChecked()); pty_baton *baton = new pty_baton(); baton->exit_code = 0; baton->signal_code = 0; baton->cb.Reset(Local<Function>::Cast(info[8])); baton->pid = pid; baton->async.data = baton; uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid); uv_thread_create(&baton->tid, pty_waitpid, static_cast<void*>(baton)); return info.GetReturnValue().Set(obj); }
First create the master-slave device through pty_forkpty (posix implementation of forkpty, compatible with systems such as sunOS and unix), then set the permissions (setuid, setgid) in the child process, execute the system call pty_execvpe (encapsulation of execvpe), and then The input information of the main device will be executed here (the file executed by the child process is sh and will listen to stdin);
The parent process exposes related objects to the node layer, such as the fd of the main device (through the fd You can create a net.Socket object for two-way data transmission), and register libuv's message queue &baton->async. When the child process exits, the &baton->async message is triggered, and the pty_after_waitpid function is executed;
Finally the parent process Create a child process by calling uv_thread_create to listen for the exit message of the previous child process (by executing the system call wait4, blocking the process listening for a specific pid, and the exit information is stored in the third parameter). The pty_waitpid function encapsulates wait4 function, and execute uv_async_send(&baton->async) at the end of the function to trigger the message.
After implementing the pty model at the bottom layer, some stdio operations need to be done at the node layer. Since the main device of the pseudo-terminal is created by executing a system call in the parent process, and the file descriptor of the main device is exposed to the node layer through fd, then the input and output of the pseudo-terminal are also read and written according to the fd to create the corresponding file type, such as PIPE, FILE to complete. In fact, at the OS level, the pseudo terminal master device is regarded as a PIPE, with two-way communication. Create a socket at the node layer through net.Socket(fd) to realize bidirectional IO of data flow. The slave device of the pseudo terminal also has the same input as the master device, so that the corresponding command is executed in the sub-process, and the output of the sub-process is also It will be reflected in the main device through PIPE, and then trigger the data event of the node layer Socket object.
The description of the input and output of the parent process, main device, child process, and slave device here is a bit confusing, so I’ll explain it here. The relationship between the parent process and the main device is: the parent process creates the main device through a system call (can be regarded as a PIPE), and obtains the fd of the main device. The parent process realizes input and output to the child process (slave device) by creating the connect socket of the fd. After the child process is created through forkpty, the login_tty operation is performed, and the stdin, stderr and stderr of the child process are reset, and all are copied to the fd of the slave device (the other end of the PIPE). Therefore, the input and output of the child process are associated with the fd of the slave device. The output data of the child process goes through PIPE, and the commands of the parent process are read from PIPE. For details, please see the reference forkpty implementation
In addition, the pty library provides pseudo-terminal size settings, so we can adjust the layout information of the pseudo-terminal output information through parameters, so this also provides adjustment commands on the web side. For the line width and height function, you only need to set the size of the pseudo terminal window on the pty layer. The window is in characters.
Web terminal security guarantee
There is no security guarantee when implementing the pseudo-terminal background based on the pty library provided by glibc. We want to directly operate a directory on the server through the web terminal, but we can directly obtain root permissions through the pseudo-terminal background. This is intolerable for the service because it directly affects the security of the server. All we need to implement is: A "system" in which users are online at the same time, each user's access rights can be configured, specific directories can be accessed, bash commands can be optionally configured, users are isolated from each other, users are unaware of the current environment, and the environment is simple and easy to deploy.
The most suitable technology selection is docker. As a kernel-level isolation, it can make full use of hardware resources and is very convenient for mapping host-related files. But docker is not omnipotent. If the program runs in a docker container, then allocating a container to each user will become much more complicated, and it is not under the control of the operation and maintenance personnel. This is the so-called DooD (docker out of docker). )--Use binary files such as volume "/usr/local/bin/docker" and use the docker command of the host to open the sibling image to run the build service. There are many shortcomings in using the docker-in-docker mode that is often discussed in the industry, especially at the file system level, which can be found in the references. Therefore, docker technology is not suitable for solving user access security issues for services already running in containers.
Next we need to consider solutions on a single machine. At present, the author only thinks of two solutions:
Command ACL, implement restricted bash chroot through command whitelist, create a system user for each user, and imprison the user access scope
First, the command The whitelist method should be excluded. First of all, it cannot guarantee that the bash of different releases of Linux is the same; secondly, it cannot effectively exhaust all commands; finally, due to the tab command completion function provided by the pseudo terminal and the existence of special characters such as delete , cannot effectively match the currently entered command. Therefore, the whitelist method has too many loopholes and should be abandoned.
restricted bash, triggered by /bin/bash -r, can restrict users from explicitly "cd directory", but it has many shortcomings:
is not sufficient to allow execution of completely untrusted software. When a command that is found to be a shell script is executed, rbash turns off any restrictions created in the shell to execute the script. When users run bash or dash from rbash, then they get an unlimited shell. There are many ways to break out of a restricted bash shell, which are not easily predictable.
In the end, it seems that there is only one solution, which is chroot. chroot modifies the user's root directory and runs the command in the specified root directory. You cannot jump out of the specified root directory, so you cannot access all directories of the original system; at the same time, chroot will create a system directory structure isolated from the original system, so various commands of the original system cannot be used in the "new system" because It is new and empty; finally, it is isolated and transparent when used by multiple users, which fully meets our needs.
Therefore, we finally chose chroot as the security solution for web terminals. However, using chroot requires a lot of extra processing, including not only the creation of new users, but also the initialization of commands. It is also mentioned above that the "new system" is empty, and there are no executable binary files, such as "ls, pmd", etc., so initializing the "new system" is necessary. However, many binary files are not only statically linked to many libraries, but also rely on dynamic link libraries (dlls) during runtime. For this reason, it is also necessary to find many dlls that each command depends on, which is extremely cumbersome. In order to help users get rid of this boring process, jailkit came into being.
jailkit, so easy to use
jailkit, as the name suggests, is used to jail users. The jailkit uses chroot internally to create the user root directory and provides a series of instructions to initialize and copy binary files and all their dlls. These functions can be operated through configuration files. Therefore, in actual development, jailkit is used with initialization shell scripts to achieve file system isolation.
The initialization shell here refers to the preprocessing script. Since chroot needs to set the root directory for each user, a corresponding user is created in the shell for each user with command line permissions, and passed The jailkit configuration file copies basic binary files and their DLLs, such as basic shell commands, git, vim, ruby, etc.; finally, it does additional processing for certain commands and resets permissions.
In the process of processing the file mapping between the "new system" and the original system, some skills are still needed. The author once mapped directories other than the user root directory set by chroot in the form of soft links. However, when accessing the soft links in the jail, an error was still reported and the file could not be found. This was due to the characteristics of chroot. , does not have permission to access the file system outside the root directory; if mapping is established through hard links, it is possible to modify the hard link files in the user root directory set by chroot, but operations involving deletion, creation, etc. cannot be performed correctly. It is mapped to the directory of the original system, and the hard link cannot connect to the directory, so the hard link does not meet the requirements; it is finally implemented through mount --bind, such as mount --bind /home/ttt/abc /usr/local/abc, which is shielded by Directory information (block) of the mounted directory (/usr/local/abc), and maintains the mapping relationship between the mounted directory and the mounted directory in the memory. Access to /usr/local/abc will pass through the memory The mapping table queries the block of /home/ttt/abc, and then performs operations to achieve directory mapping.
Finally, after initializing the "new system", you need to execute jail-related commands through the pseudo terminal:
sudo jk_chrootlaunch -j /usr/local/jailuser/${creater} -u $ {creater} -x /bin/bashr
After opening the bash program, communicate with the web terminal input (via websocket) received by the main device through PIPE.
The above is what I compiled for everyone. I hope it will be helpful to everyone in the future.
Related articles:
What should you pay attention to when using React.setState
How to use the common header of the page in vue Componentization (detailed tutorial)
About function throttling and function anti-shake in JS (detailed tutorial)
How to use three.js Implementing 3D Cinema
How to implement the side sliding menu component in Vue
The above is the detailed content of How to implement multi-user web terminal display using node.js. For more information, please follow other related articles on the PHP Chinese website!