From 8aaa56566d0d93d4a22c86c5f35a3dba68ba2f90 Mon Sep 17 00:00:00 2001 From: "C. Torres" Date: Fri, 10 Jan 2025 22:09:57 -0300 Subject: [PATCH] Implement raw Stdin serving mode Implement serving mode whereby any byte received through the program's Stdin is sent in the websocket. This mode sets the terminal in pseudo raw mode, in the sense that the 'OPOST' Termios attribute is not set. Signed-off-by: C. Torres --- go.mod | 5 +++- go.sum | 2 ++ ws/common.go | 45 ++++++++++++++++++++++++++-- ws/raw_stdin.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 ws/raw_stdin.go diff --git a/go.mod b/go.mod index cd905c1..9686469 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module wssrv go 1.22.6 -require github.com/gorilla/websocket v1.5.3 +require ( + github.com/gorilla/websocket v1.5.3 + golang.org/x/sys v0.29.0 +) diff --git a/go.sum b/go.sum index 25a9fc4..478d5e3 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/ws/common.go b/ws/common.go index fe8a586..57770d9 100644 --- a/ws/common.go +++ b/ws/common.go @@ -3,16 +3,55 @@ package ws import ( "fmt" "os" + + "golang.org/x/sys/unix" ) -func checkFile(fileNa string) error { +func restore(fd int, t *unix.Termios) error { + return unix.IoctlSetTermios(fd, unix.TCSETS, t) +} + +// setupTerm sets the terminal with raw mode attributes for input, whereby +// 'OPOST' is not set. +func setupTerm() (int, *unix.Termios, error) { + var ( + err error + fd int + term *unix.Termios + + oldTerm = &unix.Termios{} + ) + + if fd, err = unix.Open("/dev/tty", unix.O_RDONLY, 0); err != nil { + return 0, nil, err + } + if term, err = unix.IoctlGetTermios(fd, unix.TCGETS); err != nil { + return 0, nil, err + } + *oldTerm = *term + + term.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | + unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + term.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + term.Cflag &^= unix.CSIZE | unix.PARENB + term.Cflag |= unix.CS8 + term.Cc[unix.VMIN] = 1 + term.Cc[unix.VTIME] = 0 + + if err = unix.IoctlSetTermios(fd, unix.TCSETS, term); err != nil { + return 0, nil, err + } + return fd, oldTerm, nil +} + +func checkFile(name string) error { var ( err error stat os.FileInfo ) - if stat, err = os.Stat(fileNa); err != nil { - return fmt.Errorf("unable to check metadata for file '%s': %v", fileNa, err) + if stat, err = os.Stat(name); err != nil { + return fmt.Errorf("unable to check metadata for file '%s': %v", name, err) } if !stat.Mode().IsRegular() { return fmt.Errorf("file '%s' is not regular") diff --git a/ws/raw_stdin.go b/ws/raw_stdin.go new file mode 100644 index 0000000..430fa1e --- /dev/null +++ b/ws/raw_stdin.go @@ -0,0 +1,80 @@ +package ws + +import ( + "context" + "fmt" + "wssrv/log" + + "github.com/gorilla/websocket" + "golang.org/x/sys/unix" +) + +const ( + bufSize = 8 + quitKey = 17 // Key ''. +) + +func sendRawStdin(ctx context.Context, cancel context.CancelCauseFunc, con *websocket.Conn, fd int) { + var ( + err error + n int + + buf, clean = make([]byte, bufSize), make([]byte, bufSize) + ) + + for { + select { + case <-ctx.Done(): + return + default: + buf = clean + if n, err = unix.Read(fd, buf); err != nil { + cancel(fmt.Errorf("unable to read from descriptor '%d': %v", fd, err)) + return + } + if n == 1 && buf[0] == quitKey { + cancel(nil) + return + } + fmt.Printf("> byte '%d'\n", buf[:n]) + if err = con.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil { + cancel(fmt.Errorf("unable write on websocket on '%s': %v", con.RemoteAddr(), err)) + return + } + } + } +} + +func sendRawStdinStream(con *websocket.Conn) error { + var ( + err error + fd int + term *unix.Termios + + ctx, cancel = context.WithCancelCause(context.Background()) + ) + + if fd, term, err = setupTerm(); err != nil { + return fmt.Errorf("unable to setup terminal for raw input: %v", err) + } + defer func(fd int, term *unix.Termios) { + var err error + if err = unix.IoctlSetTermios(fd, unix.TCSETS, term); err != nil { + log.Err.Printf("unable to restore terminal with descriptor '%d': %v", fd, err) + } + }(fd, term) + + go readMessage(ctx, cancel, con) + go sendRawStdin(ctx, cancel, con, fd) + + <-ctx.Done() + return context.Cause(ctx) +} + +func SendRawStdinStream(con *websocket.Conn) error { + switch { + case con == nil: + return fmt.Errorf("nil connection parameter") + } + return sendRawStdinStream(con) +}