5

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

jsmith
  • 151
  • 1
  • 8
  • At a very minimum, reassigning stdin, stdout, stderr of the process to point to the socket FD might work for some programs. Depending on the nature of the TUI, though, you might end up needing to create a pseudo terminal, forward data between the terminal controller and your socket, decode terminal size change events received via the telnet protocol and forward them to the terminal controller, etc. – Daniel Schepler Jan 17 '19 at 22:37
  • For the application I had in mind, I wasn't hellbent on worrying about terminal size changes. I'd imagine I'd have to interpret telnet bytecode, which I could do but would be a hassle. Perhaps it's a feature I should work on, but maybe not right away. Wouldn't creating a pseudo terminal for data forwarding create compatibility issues with some terminal emulations though? – jsmith Jan 18 '19 at 03:02

1 Answers1

5

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

lmars
  • 2,502
  • 1
  • 16
  • 9
  • Thank you so much! That works beautifully. I'm a bit new to networking in go, so I apologize for my newbiness. This is probably very simple to you. I've only had any experience really with piping ncurses over C networking, which is a bit archaic compared to this, and even still, it's usually just easier to pipe a connection to an application using something like socat. I wanted something bundled into a single application this time, however, since I thought it would be much neater to host on cloud platforms like Heroku, AWS, etc. So thank you so much! – jsmith Jan 18 '19 at 02:56