gotty-client/gotty-client.go

179 lines
3.4 KiB
Go

package gottyclient
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"unicode/utf8"
"unsafe"
"github.com/Sirupsen/logrus"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh/terminal"
)
// GetWebsocketURL transforms a GoTTY http URL to its WebSocket URL
func GetWebsocketURL(httpURL string) (*url.URL, *http.Header, error) {
header := http.Header{}
target, err := url.Parse(httpURL)
if err != nil {
return nil, nil, err
}
if target.Scheme == "https" {
target.Scheme = "wss"
} else {
target.Scheme = "ws"
}
target.Path = strings.TrimLeft(target.Path+"ws", "/")
if target.User != nil {
header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(target.User.String())))
target.User = nil
}
return target, &header, nil
}
type Client struct {
Dialer *websocket.Dialer
Conn *websocket.Conn
Headers *http.Header
Target string
Connected bool
}
// Connect tries to dial a websocket server
func (c *Client) Connect() error {
conn, _, err := c.Dialer.Dial(c.Target, *c.Headers)
if err != nil {
return err
}
c.Conn = conn
return nil
}
// Close will nicely close the dialer
func (c *Client) Close() {
c.Conn.Close()
}
// Loop will look indefinitely for new messages
func (c *Client) Loop() error {
if !c.Connected {
err := c.Connect()
if err != nil {
return err
}
}
done := make(chan bool)
go c.readLoop(done)
go c.writeLoop(done)
go c.termsizeLoop(done)
<-done
return nil
}
type winsize struct {
Rows uint16 `json:"rows"`
Columns uint16 `json:"columns"`
// unused
x uint16
y uint16
}
func (c *Client) termsizeLoop(done chan bool) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGWINCH)
ws := winsize{}
for {
syscall.Syscall(syscall.SYS_IOCTL,
uintptr(0), uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(&ws)))
b, err := json.Marshal(ws)
if err != nil {
logrus.Warnf("json.Marshal error: %v", err)
}
err = c.Conn.WriteMessage(websocket.TextMessage, append([]byte("1"), b...))
if err != nil {
logrus.Warnf("ws.WriteMessage failed: %v", err)
}
<-ch
}
}
func (c *Client) writeLoop(done chan bool) {
oldState, err := terminal.MakeRaw(0)
if err == nil {
defer terminal.Restore(0, oldState)
}
reader := bufio.NewReader(os.Stdin)
for {
x, size, err := reader.ReadRune()
if size <= 0 || err != nil {
done <- true
return
}
p := make([]byte, size)
utf8.EncodeRune(p, x)
err = c.Conn.WriteMessage(websocket.TextMessage, append([]byte("0"), p...))
if err != nil {
done <- true
return
}
}
}
func (c *Client) readLoop(done chan bool) {
for {
_, data, err := c.Conn.ReadMessage()
if err != nil {
done <- true
logrus.Warnf("c.Conn.ReadMessage: %v", err)
return
}
switch data[0] {
case '0': // data
fmt.Print(string(data[1:]))
case '1': // new title
newTitle := string(data[1:])
fmt.Printf("\033]0;%s\007", newTitle)
case '2': // json prefs
logrus.Debugf("Unhandled protocol message: json pref: %s", string(data))
default:
logrus.Warnf("Unhandled protocol message: %s", string(data))
}
}
}
// NewClient returns a GoTTY client object
func NewClient(httpURL string) (*Client, error) {
target, header, err := GetWebsocketURL(httpURL)
if err != nil {
return nil, err
}
return &Client{
Dialer: &websocket.Dialer{},
Target: target.String(),
Headers: header,
}, nil
}