mirror of
https://github.com/1Panel-dev/KubePi.git
synced 2025-10-05 15:26:58 +08:00
263 lines
5.5 KiB
Go
263 lines
5.5 KiB
Go
package webtty
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"github.com/pkg/errors"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// WebTTY bridges a PTY slave and its PTY master.
|
|
// 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
|
|
permitWrite bool
|
|
columns int
|
|
rows int
|
|
reconnect int // in seconds
|
|
masterPrefs []byte
|
|
|
|
bufferSize int
|
|
writeMutex sync.Mutex
|
|
lastPingTime time.Time
|
|
}
|
|
|
|
const (
|
|
MaxBufferSize = 1024 * 1024 * 1
|
|
)
|
|
|
|
// 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,
|
|
columns: 0,
|
|
rows: 0,
|
|
|
|
bufferSize: MaxBufferSize,
|
|
lastPingTime: time.Now(),
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(wt)
|
|
}
|
|
|
|
return wt, nil
|
|
}
|
|
|
|
// Run starts the main process of the WebTTY.
|
|
// 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.
|
|
// If the connection to one end gets closed, returns ErrSlaveClosed or ErrMasterClosed.
|
|
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, 3)
|
|
|
|
slaveBuffer := make([]byte, wt.bufferSize)
|
|
go func() {
|
|
errs <- func() error {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
}
|
|
}()
|
|
for {
|
|
if slaveBuffer == nil {
|
|
return ErrSlaveClosed
|
|
}
|
|
n, err := wt.slave.Read(slaveBuffer)
|
|
if err != nil {
|
|
return ErrSlaveClosed
|
|
}
|
|
err = wt.handleSlaveReadEvent(slaveBuffer[:n])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}()
|
|
}()
|
|
masterBuffer := make([]byte, wt.bufferSize)
|
|
go func() {
|
|
errs <- func() error {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
}
|
|
}()
|
|
for {
|
|
if masterBuffer == nil {
|
|
return ErrMasterClosed
|
|
}
|
|
n, err := wt.masterConn.Read(masterBuffer)
|
|
if err != nil {
|
|
return ErrMasterClosed
|
|
}
|
|
err = wt.handleMasterReadEvent(masterBuffer[:n])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}()
|
|
}()
|
|
|
|
go func() {
|
|
errs <- func() error {
|
|
lostPingTimeout := time.Duration(180) * time.Second
|
|
seconds, _err := strconv.Atoi(os.Getenv("LOST_PING_TIMEOUT_SECONDS"))
|
|
if _err != nil && seconds > 30 {
|
|
lostPingTimeout = time.Duration(seconds) * time.Second
|
|
}
|
|
for {
|
|
time.Sleep(time.Duration(30) * time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if time.Now().After(wt.lastPingTime.Add(lostPingTimeout)) {
|
|
return ErrConnectionLostPing
|
|
}
|
|
}
|
|
}()
|
|
}()
|
|
|
|
defer func() {
|
|
slaveBuffer = nil
|
|
masterBuffer = nil
|
|
}()
|
|
|
|
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")
|
|
}
|
|
|
|
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()
|
|
|
|
_, err := wt.masterConn.Write(data)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to write to master")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (wt *WebTTY) handleMasterReadEvent(data []byte) error {
|
|
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
|
|
}
|
|
_, err := wt.slave.Write(data[1:])
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to write received data to slave")
|
|
}
|
|
|
|
case Ping:
|
|
err := wt.masterWrite([]byte{Pong})
|
|
wt.lastPingTime = time.Now()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to return Pong message to master")
|
|
}
|
|
|
|
case ResizeTerminal:
|
|
if wt.columns != 0 && wt.rows != 0 {
|
|
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")
|
|
}
|
|
rows := wt.rows
|
|
if rows == 0 {
|
|
rows = int(args.Rows)
|
|
}
|
|
|
|
columns := wt.columns
|
|
if columns == 0 {
|
|
columns = int(args.Columns)
|
|
}
|
|
|
|
return wt.slave.ResizeTerminal(columns, rows)
|
|
default:
|
|
return errors.Errorf("unknown message type `%c`", data[0])
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type argResizeTerminal struct {
|
|
Columns float64
|
|
Rows float64
|
|
}
|