gotty/webtty/webtty.go

257 lines
5.5 KiB
Go
Raw Normal View History

2017-02-25 22:37:07 +00:00
package webtty
import (
"context"
"encoding/base64"
"encoding/json"
2022-11-11 09:06:22 +00:00
"github.com/sorenisanerd/gotty/utils"
"log"
2017-02-25 22:37:07 +00:00
"sync"
"github.com/pkg/errors"
)
2017-08-24 05:40:28 +00:00
// WebTTY bridges a PTY slave and its PTY master.
2017-02-25 22:37:07 +00:00
// To support text-based streams and side channel commands such as
// terminal resizing, WebTTY uses an original protocol.
type WebTTY struct {
// PTY Master, which probably a connection to browser
masterConn Master
// PTY Slave
slave Slave
windowTitle []byte
arguments map[string][]string
2017-02-25 22:37:07 +00:00
permitWrite bool
writeLog bool
2017-08-24 05:40:28 +00:00
columns int
rows int
reconnect int // in seconds
2017-02-25 22:37:07 +00:00
masterPrefs []byte
decoder Decoder
2017-02-25 22:37:07 +00:00
bufferSize int
writeMutex sync.Mutex
}
// New creates a new instance of WebTTY.
// masterConn is a connection to the PTY master,
// typically it's a websocket connection to a client.
// slave is a PTY slave such as a local command with a PTY.
func New(masterConn Master, slave Slave, options ...Option) (*WebTTY, error) {
wt := &WebTTY{
masterConn: masterConn,
slave: slave,
permitWrite: false,
2017-08-24 05:40:28 +00:00
columns: 0,
rows: 0,
2017-02-25 22:37:07 +00:00
bufferSize: 1024,
decoder: &NullCodec{},
2017-02-25 22:37:07 +00:00
}
for _, option := range options {
option(wt)
}
return wt, nil
}
2017-08-24 05:40:28 +00:00
// Run starts the main process of the WebTTY.
2017-02-25 22:37:07 +00:00
// This method blocks until the context is canceled.
// Note that the master and slave are left intact even
// after the context is canceled. Closing them is caller's
// responsibility.
2017-08-24 05:40:28 +00:00
// If the connection to one end gets closed, returns ErrSlaveClosed or ErrMasterClosed.
2017-02-25 22:37:07 +00:00
func (wt *WebTTY) Run(ctx context.Context) error {
err := wt.sendInitializeMessage()
if err != nil {
return errors.Wrapf(err, "failed to send initializing message")
}
errs := make(chan error, 2)
go func() {
errs <- func() error {
buffer := make([]byte, wt.bufferSize)
for {
n, err := wt.slave.Read(buffer)
if err != nil {
return ErrSlaveClosed
}
err = wt.handleSlaveReadEvent(buffer[:n])
if err != nil {
return err
}
}
}()
}()
go func() {
errs <- func() error {
2017-08-24 05:40:28 +00:00
buffer := make([]byte, wt.bufferSize)
var line string
2017-02-25 22:37:07 +00:00
for {
2017-08-24 05:40:28 +00:00
n, err := wt.masterConn.Read(buffer)
2017-02-25 22:37:07 +00:00
if err != nil {
return ErrMasterClosed
}
err = wt.handleMasterReadEvent(buffer[:n], &line)
2017-02-25 22:37:07 +00:00
if err != nil {
return err
}
}
}()
}()
select {
case <-ctx.Done():
err = ctx.Err()
case err = <-errs:
}
return err
}
func (wt *WebTTY) sendInitializeMessage() error {
err := wt.masterWrite(append([]byte{SetWindowTitle}, wt.windowTitle...))
if err != nil {
return errors.Wrapf(err, "failed to send window title")
}
bufSizeMsg, _ := json.Marshal(wt.bufferSize)
err = wt.masterWrite(append([]byte{SetBufferSize}, bufSizeMsg...))
if err != nil {
return errors.Wrapf(err, "failed to send buffer size")
}
2017-02-25 22:37:07 +00:00
if wt.reconnect > 0 {
reconnect, _ := json.Marshal(wt.reconnect)
err := wt.masterWrite(append([]byte{SetReconnect}, reconnect...))
if err != nil {
return errors.Wrapf(err, "failed to set reconnect")
}
}
if wt.masterPrefs != nil {
err := wt.masterWrite(append([]byte{SetPreferences}, wt.masterPrefs...))
if err != nil {
return errors.Wrapf(err, "failed to set preferences")
}
}
return nil
}
func (wt *WebTTY) handleSlaveReadEvent(data []byte) error {
safeMessage := base64.StdEncoding.EncodeToString(data)
err := wt.masterWrite(append([]byte{Output}, []byte(safeMessage)...))
if err != nil {
return errors.Wrapf(err, "failed to send message to master")
}
return nil
}
func (wt *WebTTY) masterWrite(data []byte) error {
wt.writeMutex.Lock()
defer wt.writeMutex.Unlock()
2017-08-24 05:40:28 +00:00
_, err := wt.masterConn.Write(data)
2017-02-25 22:37:07 +00:00
if err != nil {
return errors.Wrapf(err, "failed to write to master")
}
return nil
}
func (wt *WebTTY) handleMasterReadEvent(data []byte, line *string) error {
2017-02-25 22:37:07 +00:00
if len(data) == 0 {
return errors.New("unexpected zero length read from master")
}
switch data[0] {
case Input:
if !wt.permitWrite {
return nil
}
if len(data) <= 1 {
return nil
}
var decodedBuffer = make([]byte, len(data))
n, err := wt.decoder.Decode(decodedBuffer, data[1:])
if err != nil {
return errors.Wrapf(err, "failed to decode received data")
}
if wt.writeLog {
utils.FormatWriteLog(decodedBuffer[:n], line)
// 13(ASCII) means carriage return(CR)
// it is the end of a line
if decodedBuffer[n-1] == 13 {
log.Printf("[write-log] %v %s\n", wt.arguments, *line)
*line = ""
}
2022-11-11 09:06:22 +00:00
}
_, err = wt.slave.Write(decodedBuffer[:n])
2017-02-25 22:37:07 +00:00
if err != nil {
return errors.Wrapf(err, "failed to write received data to slave")
}
case Ping:
err := wt.masterWrite([]byte{Pong})
if err != nil {
return errors.Wrapf(err, "failed to return Pong message to master")
}
case SetEncoding:
switch string(data[1:]) {
case "base64":
wt.decoder = base64.StdEncoding
case "null":
wt.decoder = NullCodec{}
}
2017-02-25 22:37:07 +00:00
case ResizeTerminal:
2017-08-24 05:40:28 +00:00
if wt.columns != 0 && wt.rows != 0 {
2017-02-25 22:37:07 +00:00
break
}
if len(data) <= 1 {
return errors.New("received malformed remote command for terminal resize: empty payload")
}
var args argResizeTerminal
err := json.Unmarshal(data[1:], &args)
if err != nil {
return errors.Wrapf(err, "received malformed data for terminal resize")
}
2017-08-24 05:40:28 +00:00
rows := wt.rows
2017-02-25 22:37:07 +00:00
if rows == 0 {
rows = int(args.Rows)
}
2017-08-24 05:40:28 +00:00
columns := wt.columns
2017-02-25 22:37:07 +00:00
if columns == 0 {
columns = int(args.Columns)
}
wt.slave.ResizeTerminal(columns, rows)
default:
return errors.Errorf("unknown message type `%c`", data[0])
}
return nil
}
type argResizeTerminal struct {
Columns float64
Rows float64
}