Writing a web terminal emulator on Go using Websocket
What are we going to write
In the last article, we wrote a simple terminal emulator for PHP. I think now is the time to write something more serious, on webkets. What language should I use to work with web socket ..? Python..? Ruby ..? Javascript ..? Not! Since Go 1 was released, let's write on it;). I will try not to repeat myself and not to write the whole code here. I will cite only interesting, in my opinion, fragments.
Demo
Thanks to the user Aleks_ja for the opportunity to see the terminal emulator in action (you need a browser with the latest version of web sockets, for example, Firefox 11 or the latest Chrome). From the first time it may not connect, if the daemon does not have time to start in 100 ms - try to refresh the page first.
')
The source code of the web terminal is available on github . You need to compile a websok daemon yourself (with the go build ) - this is a small measure of protection against those who like to hack hosters;).
Websocket library ( go get code.google.com/p/go.net/websocket )
A browser that supports the latest Websocket specification (for example, the latest Firefox and Chrome)
Any web server with PHP (to start the daemon automatically)
We write a webcam daemon
A web server server and a pseudo-terminal emulator will be combined in our daemon. Although there is no native support for working with pseudo-terminals in Go, this language is easily integrated with C, so we will use the appropriate C calls to work directly with pseudo-terminals.
Let's write the binding for forkpty () and ioctl () (in ioctl () we’ll change the size of the terminal’s window): Go, though, integrates well with C, but doesn’t understand that pid_t and int are one and the same does not know how to work with a variable number of parameters in C functions.
Communication between the web socket and the pseudo terminal
Next we have to run, for example, bash and send output from the corresponding descriptor (pttyno) to the web socket, and vice versa, input from the web socket to send pttyno to the input is easy. A problem occurs when an incomplete UTF-8 sequence comes to us from a pseudo-terminal. We can read from the pseudo-terminal only in blocks (say, 2 Kb each) and the end of the block can “cut” the UTF-8 character into 2 parts - this “scrap” should not be sent to the browser, otherwise it will simply ignore this fragment. Here is a small piece of code that correctly handles this situation:
for end = buflen - 1; end >= 0; end-- { // Go if utf8.RuneStart(buf[end]) { // ... ch, width := utf8.DecodeRune(buf[end:buflen]) if ch != utf8.RuneError { end += width } break } }
We have to find at the end of the buffer (buf) bytes, which can serve as the beginning of a UTF-8 character (in Go terminology, rune), and then see if this character is intact. If everything is fine with the last character, then we return the “end” of the buffer back, otherwise we reduce the size of the buffer so that only whole characters are left there.
Display output from pseudo-terminal to browser
At first, I used JSLinux to display the output, but its author does not allow modifying and distributing the code of its libraries, so let's take the selectel / pyte library written by the companions from Selectk ... Wait, it’s python: (!) rewrite it in Javascript :)! The port from a python is not perfect, besides I am not a special python expert, but it does its work - Midnight Commander starts and runs without problems.
Accept user input
In order to accept user input, I still borrowed a certain amount of code from the author of JSLinux, the basic principles are described here . I also added the ability to insert some text into the input field at the bottom (for example, passwords) and added mappings for the F1 - F12 keys, as well as for Alt + (left / right arrow). As it turned out, the values ​​of the input characters for F-keys depend on the $ TERM environment variable and for vt100 are not defined at all, since in the VT100 they were not on the keyboard :). Since pyte is used for output, the $ TERM environment variable must be equal to linux, so we will use the mapping for these keys for this terminal.
We start the demon "on demand"
I implemented the web-daemon in such a way that it comes out a minute after the last connection, so it would be convenient if the script itself started the web-daemon when we open the terminal page. The PHP code for this is very simple:
<?php $PORT = 13923; // port terminal daemon will be run at system('exec nohup ./ws '.$PORT.' </dev/null >>ws.log 2>&1 &');
If you do not know what exec , I will explain: this is a special builtin-command in any UNIX shell that forces the shell to replace itself with the process being called. That is, we will not have the “extra” process of sh -c ./ws ... .
Moments that I haven't talked about
I did not talk about the following implementation details:
The client communication protocol was a bit complicated to support window resizing , but a bug appeared with the introduction of Russian letters
the web daemon is password protected, which is automatically generated at startup and used when connecting
use your bashrc to set the necessary terminal settings
since no rendering takes place on the server, but only bytes are sent, the daemon loads the server comparable to sshd (i.e., the CPU load is close to zero)
javascript's pyte implementation works extremely fast: there is no visible delay when starting Midnight Commander, the bandwidth is several thousand lines of text per second
when the browser window is closed, the session ends correctly
one demon can serve many clients at the same time without problems
due to the use of the <audio> tag and sounds from ubuntu, the terminal is able to “beep” :)
Project on github
For those interested, I repeat the links to the githab: github.com/YuriyNasretdinov/WebTerm - terminal emulator, which I described in the article github.com/YuriyNasretdinov/pyte is my implementation of the selectel / pyte library in Javascript (unfortunately not accepted by the developers)