📜 ⬆️ ⬇️

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;).

Ingredients


So, we will need:



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.

Work with websockets

 package main import ( "code.google.com/p/go.net/websocket" "http" "log" ) //  -  func PtyServer(ws *websocket.Conn) { // ws —     *websocket.Conn,  //   Read()  Write()  /   } func main() { http.Handle("/ws", websocket.Handler(PtyServer)) //    "/ws"   log.Fatal(http.ListenAndServe(":12345", nil)) //    12345 } 


Work with pseudo-terminal

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.

 package main /* #cgo LDFLAGS: -lutil #include <stdlib.h> #include <sys/ioctl.h> #...     ... int goForkpty(int *amaster, struct winsize *winp) { return forkpty(amaster, NULL, NULL, winp); } int goChangeWinsz(int fd, struct winsize *winp) { return ioctl(fd, TIOCSWINSZ, winp); } */ import "C" 


In the handler, we use it:

 func PtyServer(ws *websocket.Conn) { cols, rows := 80, 24 //    - int var winsz = new(C.struct_winsize) //    "var name = ..." —     winsz.ws_row = C.ushort(rows); //         winsz.ws_col = C.ushort(cols); winsz.ws_xpixel = C.ushort(cols * 9); winsz.ws_ypixel = C.ushort(rows * 16); cpttyno := C.int(-1) pid := int(C.goForkpty(&cpttyno, winsz)) pttyno := int(cpttyno) // ... } 


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:


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)

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


All Articles