Home  >  Article  >  Web Front-end  >  node.js implements web terminal operation for multiple users

node.js implements web terminal operation for multiple users

php中世界最好的语言
php中世界最好的语言Original
2018-04-14 13:35:391474browse

This time I will bring you node.js to implement multi-user web terminal operations. What are the precautions for node.js to implement multi-user web terminal operations. Here are actual cases, let’s take a look.

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

A terminal is similar to a command line tool in our understanding. In layman's terms, it is a process that can execute a 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 permission 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 (the server-side technology uses nodejs):

The node native module provides the repl module, which can be used to implement interactive input and execute output. It also provides tab completion functions, customized output styles and other functions. However, it can only execute node-related commands, so it cannot achieve what we want to execute. The purpose of the system shell The node native module child_porcess provides spawn, a uv_spawn function that encapsulates the underlying libuv. The underlying execution system calls fork and execvp to execute shell commands. However, it does not provide other features of the pseudo terminal, such as automatic tab completion, arrow keys display of historical commands, etc.

Therefore, it is impossible to implement a pseudo terminal using the native module of node on the server side. It is necessary to continue to explore the principle of pseudo 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 implemented through system call functions, 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 looks relatively abstract, but In fact, functionally speaking, it is responsible for the "processing" of input and output information, such as processing interrupt characters (ctrl) during the input process. c) and some backspace characters (backspace and delete), etc., while 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 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 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 to set the permissions of the slave device unlockpt to unlock the corresponding slave device to obtain the slave device name (similar to /dev/pts/123) to read and write the master (slave) device and perform operations

Therefore, a pty library with better encapsulation has emerged, which can achieve all the above functions 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-slave device of the pseudo-terminal, we manage the lifecycle and its resources of the pseudo-terminal in the parent process where the master device is located, and execute the shell in the child process where the slave device is located. During the execution Information and results are transmitted to the main device through a bidirectional pipe, and the process where the main device is located 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, through pty_forkpty (posix implementation of forkpty, compatible with sunOS and Unix and other systems) create a master-slave device, and then after setting the permissions in the child process (setuid, setgid), execute the system call pty_execvpe (encapsulation of execvpe), and then the input information of the master device will be executed here (the file executed by the child process For sh, stdin will be listened to);

The parent process exposes related objects to the node layer, such as the fd of the main device (through which the net.Socket object can be created for two-way data transmission), and at the same time registers libuv's message queue&baton->async, which is triggered when the child process exits&baton ->async message, execute pty_after_waitpid function;

Finally, the parent process creates 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), pty_waitpid function The wait4 function is encapsulated, and uv_async_send(&baton->async) is executed 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, master 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 creates the connect of the fd The socket implements input and output to the child process (slave device). The child process passes forkpty After creation, the login_tty operation is performed to reset the stdin, stderr and stderr of the child process, 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 the size setting of the pseudo terminal, so we can adjust the layout information of the pseudo terminal output information through parameters, so this also provides the function of adjusting the width and height of the command line on the web side, just set the pseudo terminal in the pty layer Just set the window size, which is measured in characters.

Web terminal security guarantee

There is no security guarantee when implementing the pseudo-terminal backend 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 related files of the host. 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) -- through volume For binary files such as "/usr/local/bin/docker", use the host's docker command 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 options:

Command ACL, implemented through command whitelist restricted bash chroot, creates a system user for each user, and restricts the user access scope

First of all, the command whitelist method should be eliminated. First of all, there is no 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:

Not sufficient to allow the 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 restricted bash shell, which is not easy to predict.

In the end, it seems 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 File. 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 the basic user is copied through the jailkit configuration file. Binary files and their dlls, such as basic shell instructions, git, vim, ruby, etc.; finally, additional processing is done for certain commands, and permissions are reset.

Some skills are still needed in the process of handling file mapping between the "new system" and the original system. 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; finally, mount --bind implementation, such as mount --bind /home/ttt/abc /usr/local/abc It shields the 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. /usr/ Access to local/abc will query the block of /home/ttt/abc through the memory mapping table, and then perform 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.

I believe you have mastered the method after reading the case in this article. For more exciting information, please pay attention to other related articles on the php Chinese website!

Recommended reading:

How to implement echart chart in angularjs

How to implement interlaced color change in js

The above is the detailed content of node.js implements web terminal operation for multiple users. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn