Search code examples
linuxsocketsgotelnettui

Sending a GUI/TUI Over a Socket Connection


Recently I have been trying to create a program in golang, which runs on a server, and accepts telnet connections. I would then like to open a TUI (text user interface) such as a curses menu (in the case of golang, something like: termui, gocui, etc) over that telnet connection. My question is, how exactly could I do this and/or would it even be possible? I have played around trying to start TUIs when a connection is accepted, but it just opens it on the server side, not on the telnet client side. From what I can tell, there is no easy way to just send a TUI over a telnet or any other socket IO connection for that matter.

Any help is appreciated in trying to figure this out. Thanks! :D


Solution

  • First, you should note that the example I give is completely insecure (don't expose it over the Internet!) and also doesn't provide for things like signal handling or resizing of the terminal (you may want to consider using SSH instead).

    But to answer your question, here is an example of running a TCP server and connecting remote clients to a termui program running in a local PTY (uses both the https://github.com/gizak/termui and https://github.com/kr/pty packages):

    package main
    
    import (
        "flag"
        "io"
        "log"
        "net"
        "os"
        "os/exec"
    
        ui "github.com/gizak/termui"
        "github.com/kr/pty"
    )
    
    var termuiFlag = flag.Bool("termui", false, "run a termui example")
    
    func main() {
        flag.Parse()
    
        var err error
        if *termuiFlag {
            err = runTermui()
        } else {
            err = runServer()
        }
        if err != nil {
            log.Fatal(err)
        }
    }
    
    // runTermui runs the termui "Hello World" example.
    func runTermui() error {
        if err := ui.Init(); err != nil {
            return err
        }
        defer ui.Close()
    
        p := ui.NewParagraph("Hello World!")
        p.Width = 25
        p.Height = 5
        ui.Render(p)
    
        for e := range ui.PollEvents() {
            if e.Type == ui.KeyboardEvent {
                break
            }
        }
    
        return nil
    }
    
    // runServer listens for TCP connections on a random port and connects
    // remote clients to a local PTY running the termui example.
    func runServer() error {
        ln, err := net.Listen("tcp", "127.0.0.1:0")
        if err != nil {
            return err
        }
        defer ln.Close()
        log.Printf("Listening for requests on %v", ln.Addr())
        for {
            conn, err := ln.Accept()
            if err != nil {
                return err
            }
            log.Printf("Connecting remote client %v to termui", conn.RemoteAddr())
            go connectTermui(conn)
        }
    }
    
    // connectTermui connects a client connection to a termui process running in a
    // PTY.
    func connectTermui(conn net.Conn) {
        defer func() {
            log.Printf("Closing remote client %v", conn.RemoteAddr())
            conn.Close()
        }()
    
        t, err := pty.StartWithSize(
            exec.Command(os.Args[0], "--termui"),
            &pty.Winsize{Cols: 80, Rows: 24},
        )
        if err != nil {
            log.Printf("Error starting termui: %v", err)
            return
        }
        defer t.Close()
    
        go io.Copy(t, conn)
        io.Copy(conn, t)
    }
    

    Example usage is to run this program in one window and connect to it using nc in another:

    $ go run server.go
    2019/01/18 01:39:37 Listening for requests on 127.0.0.1:56192
    
    $ nc 127.0.0.1 56192
    

    You should see the "Hello world" box (hit enter to disconnect).