gotty-client/gotty-client.go

561 lines
12 KiB
Go
Raw Permalink Normal View History

2015-08-25 00:48:27 +08:00
package gottyclient
import (
2015-12-10 01:58:57 +08:00
"crypto/tls"
2015-08-26 15:29:52 +08:00
"encoding/base64"
2015-08-25 19:30:04 +08:00
"encoding/json"
2015-08-25 00:48:27 +08:00
"fmt"
2015-10-23 21:16:42 +08:00
"io"
2015-09-19 09:40:42 +08:00
"io/ioutil"
2015-08-25 00:48:27 +08:00
"net/http"
"net/url"
2015-08-25 04:53:35 +08:00
"os"
2015-09-19 09:40:42 +08:00
"regexp"
2015-08-25 00:48:27 +08:00
"strings"
2015-09-30 23:17:57 +08:00
"sync"
2015-09-19 09:40:42 +08:00
"time"
2015-08-25 00:48:27 +08:00
"github.com/containerd/console"
2016-01-04 19:27:45 +08:00
"github.com/creack/goselect"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
2015-08-25 00:48:27 +08:00
)
2018-04-10 19:59:05 +08:00
// message types for gotty
const (
OutputV1 = '0'
PongV1 = '1'
SetWindowTitleV1 = '2'
SetPreferencesV1 = '3'
SetReconnectV1 = '4'
InputV1 = '0'
PingV1 = '1'
ResizeTerminalV1 = '2'
)
// message types for gotty v2.0
const (
// Unknown message type, maybe set by a bug
UnknownOutput = '0'
// Normal output to the terminal
Output = '1'
// Pong to the browser
Pong = '2'
// Set window title of the terminal
SetWindowTitle = '3'
// Set terminal preference
SetPreferences = '4'
// Make terminal to reconnect
SetReconnect = '5'
// Unknown message type, maybe sent by a bug
UnknownInput = '0'
// User input typically from a keyboard
Input = '1'
// Ping to the server
Ping = '2'
// Notify that the browser size has been changed
ResizeTerminal = '3'
)
type gottyMessageType struct {
output byte
pong byte
setWindowTitle byte
setPreferences byte
setReconnect byte
input byte
ping byte
resizeTerminal byte
}
2015-09-19 09:40:42 +08:00
// GetAuthTokenURL transforms a GoTTY http URL to its AuthToken file URL
func GetAuthTokenURL(httpURL string) (*url.URL, *http.Header, error) {
header := http.Header{}
target, err := url.Parse(httpURL)
if err != nil {
return nil, nil, err
}
target.Path = strings.TrimLeft(target.Path+"auth_token.js", "/")
if target.User != nil {
header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(target.User.String())))
target.User = nil
}
return target, &header, nil
}
2015-09-30 23:17:57 +08:00
// GetURLQuery returns url.query
func GetURLQuery(rawurl string) (url.Values, error) {
target, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
return target.Query(), nil
}
2015-08-25 00:48:27 +08:00
// GetWebsocketURL transforms a GoTTY http URL to its WebSocket URL
2015-08-26 15:29:52 +08:00
func GetWebsocketURL(httpURL string) (*url.URL, *http.Header, error) {
header := http.Header{}
2015-08-25 00:48:27 +08:00
target, err := url.Parse(httpURL)
if err != nil {
2015-08-26 15:29:52 +08:00
return nil, nil, err
2015-08-25 00:48:27 +08:00
}
if target.Scheme == "https" {
target.Scheme = "wss"
} else {
target.Scheme = "ws"
}
target.Path = strings.TrimLeft(target.Path+"ws", "/")
2015-08-26 15:29:52 +08:00
if target.User != nil {
header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(target.User.String())))
target.User = nil
}
return target, &header, nil
2015-08-25 00:48:27 +08:00
}
type Client struct {
2016-02-24 16:53:10 +08:00
Dialer *websocket.Dialer
Conn *websocket.Conn
URL string
WriteMutex *sync.Mutex
Output io.Writer
poison chan bool
2016-02-24 16:53:10 +08:00
SkipTLSVerify bool
UseProxyFromEnv bool
Connected bool
2017-12-13 23:54:37 +08:00
EscapeKeys []byte
2018-04-10 19:59:05 +08:00
V2 bool
message *gottyMessageType
WSOrigin string
2015-09-30 23:17:57 +08:00
}
2015-10-13 00:07:50 +08:00
type querySingleType struct {
AuthToken string `json:"AuthToken"`
Arguments string `json:"Arguments"`
}
2015-09-30 23:17:57 +08:00
func (c *Client) write(data []byte) error {
c.WriteMutex.Lock()
defer c.WriteMutex.Unlock()
return c.Conn.WriteMessage(websocket.TextMessage, data)
2015-08-25 00:48:27 +08:00
}
2015-09-19 09:40:42 +08:00
// GetAuthToken retrieves an Auth Token from dynamic auth_token.js file
func (c *Client) GetAuthToken() (string, error) {
target, header, err := GetAuthTokenURL(c.URL)
if err != nil {
return "", err
}
2015-10-13 23:02:38 +08:00
logrus.Debugf("Fetching auth token auth-token: %q", target.String())
2015-09-19 09:40:42 +08:00
req, err := http.NewRequest("GET", target.String(), nil)
req.Header = *header
2016-02-24 16:53:10 +08:00
tr := &http.Transport{}
2015-12-10 01:58:57 +08:00
if c.SkipTLSVerify {
2016-02-24 16:53:10 +08:00
conf := &tls.Config{InsecureSkipVerify: true}
tr.TLSClientConfig = conf
2015-12-10 01:58:57 +08:00
}
2016-02-24 16:53:10 +08:00
if c.UseProxyFromEnv {
tr.Proxy = http.ProxyFromEnvironment
}
client := &http.Client{Transport: tr}
2015-09-19 09:40:42 +08:00
resp, err := client.Do(req)
if err != nil {
return "", err
}
2015-10-13 22:41:37 +08:00
switch resp.StatusCode {
case 200:
// Everything is OK
default:
return "", fmt.Errorf("unknown status code: %d (%s)", resp.StatusCode, http.StatusText(resp.StatusCode))
}
2015-09-19 09:40:42 +08:00
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
re := regexp.MustCompile("var gotty_auth_token = '(.*)'")
output := re.FindStringSubmatch(string(body))
if len(output) == 0 {
return "", fmt.Errorf("Cannot fetch GoTTY auth-token, please upgrade your GoTTY server.")
}
return output[1], nil
2015-09-19 09:40:42 +08:00
}
2015-08-25 00:48:27 +08:00
// Connect tries to dial a websocket server
func (c *Client) Connect() error {
2015-09-19 09:40:42 +08:00
// Retrieve AuthToken
authToken, err := c.GetAuthToken()
if err != nil {
return err
}
2015-10-13 23:02:38 +08:00
logrus.Debugf("Auth-token: %q", authToken)
2015-09-19 09:40:42 +08:00
// Open WebSocket connection
target, header, err := GetWebsocketURL(c.URL)
if err != nil {
return err
}
if c.WSOrigin != "" {
header.Add("Origin", c.WSOrigin)
}
2015-10-13 23:02:38 +08:00
logrus.Debugf("Connecting to websocket: %q", target.String())
2015-12-10 01:58:57 +08:00
if c.SkipTLSVerify {
c.Dialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
2016-02-24 16:53:10 +08:00
if c.UseProxyFromEnv {
c.Dialer.Proxy = http.ProxyFromEnvironment
}
2015-09-19 09:40:42 +08:00
conn, _, err := c.Dialer.Dial(target.String(), *header)
2015-08-25 00:48:27 +08:00
if err != nil {
return err
}
c.Conn = conn
2015-10-26 20:07:17 +08:00
c.Connected = true
2015-09-19 09:40:42 +08:00
2015-10-13 23:02:38 +08:00
// Pass arguments and auth-token
2015-09-30 23:17:57 +08:00
query, err := GetURLQuery(c.URL)
if err != nil {
return err
}
2015-10-22 23:35:05 +08:00
querySingle := querySingleType{
2015-10-13 00:07:50 +08:00
Arguments: "?" + query.Encode(),
AuthToken: authToken,
2015-09-30 23:17:57 +08:00
}
json, err := json.Marshal(querySingle)
if err != nil {
2015-10-13 00:07:50 +08:00
logrus.Errorf("Failed to parse init message %v", err)
2015-09-30 23:17:57 +08:00
return err
}
// Send Json
2015-10-13 23:02:38 +08:00
logrus.Debugf("Sending arguments and auth-token")
2015-09-30 23:17:57 +08:00
err = c.write(json)
2015-09-19 09:40:42 +08:00
if err != nil {
return err
}
// Initialize message types for gotty
c.initMessageType()
2015-09-19 09:40:42 +08:00
go c.pingLoop()
2015-08-25 00:48:27 +08:00
return nil
}
2018-04-10 19:59:05 +08:00
// initMessageType initialize message types for gotty
func (c *Client) initMessageType() {
if c.V2 {
c.message = &gottyMessageType{
output: Output,
pong: Pong,
setWindowTitle: SetWindowTitle,
setPreferences: SetPreferences,
setReconnect: SetReconnect,
input: Input,
ping: Ping,
resizeTerminal: ResizeTerminal,
}
} else {
c.message = &gottyMessageType{
output: OutputV1,
pong: PongV1,
setWindowTitle: SetWindowTitleV1,
setPreferences: SetPreferencesV1,
setReconnect: SetReconnectV1,
input: InputV1,
ping: PingV1,
resizeTerminal: ResizeTerminalV1,
}
}
}
2015-09-19 09:40:42 +08:00
func (c *Client) pingLoop() {
for {
2015-10-13 23:02:38 +08:00
logrus.Debugf("Sending ping")
2018-04-10 19:59:05 +08:00
c.write([]byte{c.message.ping})
2015-09-19 09:40:42 +08:00
time.Sleep(30 * time.Second)
}
}
2015-08-25 00:48:27 +08:00
// Close will nicely close the dialer
func (c *Client) Close() {
c.Conn.Close()
}
// ExitLoop will kill all goroutines launched by c.Loop()
2015-10-26 20:00:19 +08:00
// ExitLoop() -> wait Loop() -> Close()
func (c *Client) ExitLoop() {
fname := "ExitLoop"
openPoison(fname, c.poison)
2015-10-26 20:00:19 +08:00
}
2015-08-25 00:48:27 +08:00
// Loop will look indefinitely for new messages
func (c *Client) Loop() error {
2018-04-10 19:59:05 +08:00
2015-08-25 00:48:27 +08:00
if !c.Connected {
err := c.Connect()
if err != nil {
return err
}
}
term := console.Current()
err := term.SetRaw()
if err != nil {
return fmt.Errorf("Error setting raw terminal: %v", err)
}
defer term.Reset()
wg := &sync.WaitGroup{}
2015-10-23 21:16:42 +08:00
wg.Add(1)
go c.termsizeLoop(wg)
2015-10-23 21:16:42 +08:00
wg.Add(1)
go c.readLoop(wg)
2015-10-23 21:16:42 +08:00
wg.Add(1)
go c.writeLoop(wg)
/* Wait for all of the above goroutines to finish */
2015-10-23 21:16:42 +08:00
wg.Wait()
logrus.Debug("Client.Loop() exiting")
2015-08-25 04:53:35 +08:00
return nil
}
2015-08-25 19:30:04 +08:00
type winsize struct {
Rows uint16 `json:"rows"`
Columns uint16 `json:"columns"`
// unused
x uint16
y uint16
}
type posionReason int
const (
committedSuicide = iota
killed
)
func openPoison(fname string, poison chan bool) posionReason {
logrus.Debug(fname + " suicide")
/*
* The close() may raise panic if multiple goroutines commit suicide at the
* same time. Prevent that panic from bubbling up.
*/
defer func() {
if r := recover(); r != nil {
logrus.Debug("Prevented panic() of simultaneous suicides", r)
}
}()
/* Signal others to die */
close(poison)
return committedSuicide
}
func die(fname string, poison chan bool) posionReason {
logrus.Debug(fname + " died")
wasOpen := <-poison
if wasOpen {
logrus.Error("ERROR: The channel was open when it wasn't suppoed to be")
}
return killed
}
func (c *Client) termsizeLoop(wg *sync.WaitGroup) posionReason {
2015-10-23 21:16:42 +08:00
defer wg.Done()
fname := "termsizeLoop"
2015-08-25 19:30:04 +08:00
ch := make(chan os.Signal, 1)
2015-11-19 07:15:13 +08:00
notifySignalSIGWINCH(ch)
defer resetSignalSIGWINCH()
2015-08-25 19:30:04 +08:00
for {
2015-11-19 07:15:13 +08:00
if b, err := syscallTIOCGWINSZ(); err != nil {
logrus.Warn(err)
} else {
2018-04-10 19:59:05 +08:00
if err = c.write(append([]byte{c.message.resizeTerminal}, b...)); err != nil {
2015-11-19 07:15:13 +08:00
logrus.Warnf("ws.WriteMessage failed: %v", err)
}
2015-08-25 19:30:04 +08:00
}
2015-10-23 21:16:42 +08:00
select {
case <-c.poison:
/* Somebody poisoned the well; die */
return die(fname, c.poison)
2015-10-23 21:16:42 +08:00
case <-ch:
}
2015-08-25 19:30:04 +08:00
}
}
2015-10-23 21:16:42 +08:00
type exposeFd interface {
Fd() uintptr
}
func (c *Client) writeLoop(wg *sync.WaitGroup) posionReason {
2015-10-23 21:16:42 +08:00
defer wg.Done()
fname := "writeLoop"
2015-10-23 21:16:42 +08:00
buff := make([]byte, 128)
2015-08-25 18:11:21 +08:00
2015-10-23 21:16:42 +08:00
rdfs := &goselect.FDSet{}
2017-12-13 23:54:37 +08:00
reader := io.ReadCloser(os.Stdin)
2018-03-24 19:33:26 +08:00
pr := NewEscapeProxy(reader, c.EscapeKeys)
2017-12-13 23:54:37 +08:00
defer reader.Close()
2015-08-25 04:53:35 +08:00
for {
select {
case <-c.poison:
/* Somebody poisoned the well; die */
return die(fname, c.poison)
default:
}
2015-10-23 21:16:42 +08:00
rdfs.Zero()
rdfs.Set(reader.(exposeFd).Fd())
err := goselect.Select(1, rdfs, nil, nil, 50*time.Millisecond)
if err != nil {
return openPoison(fname, c.poison)
2015-08-25 04:53:35 +08:00
}
2015-10-23 21:16:42 +08:00
if rdfs.IsSet(reader.(exposeFd).Fd()) {
2017-12-13 23:54:37 +08:00
size, err := pr.Read(buff)
if err != nil {
if err == io.EOF {
// Send EOF to GoTTY
// Send 'Input' marker, as defined in GoTTY::client_context.go,
// followed by EOT (a translation of Ctrl-D for terminals)
2018-04-10 19:59:05 +08:00
err = c.write(append([]byte{c.message.input}, byte(4)))
if err != nil {
return openPoison(fname, c.poison)
}
continue
} else {
return openPoison(fname, c.poison)
}
}
if size <= 0 {
continue
2015-10-23 21:16:42 +08:00
}
2015-10-23 21:16:42 +08:00
data := buff[:size]
2018-04-10 19:59:05 +08:00
err = c.write(append([]byte{c.message.input}, data...))
2015-10-23 21:16:42 +08:00
if err != nil {
return openPoison(fname, c.poison)
2015-10-23 21:16:42 +08:00
}
}
2015-08-25 04:53:35 +08:00
}
2018-04-10 19:59:05 +08:00
2015-08-25 04:53:35 +08:00
}
func (c *Client) readLoop(wg *sync.WaitGroup) posionReason {
2015-10-23 21:16:42 +08:00
defer wg.Done()
fname := "readLoop"
2015-10-23 21:16:42 +08:00
type MessageNonBlocking struct {
Data []byte
Err error
}
msgChan := make(chan MessageNonBlocking)
2015-08-25 00:48:27 +08:00
for {
2015-10-23 21:16:42 +08:00
go func() {
_, data, err := c.Conn.ReadMessage()
msgChan <- MessageNonBlocking{Data: data, Err: err}
}()
select {
case <-c.poison:
/* Somebody poisoned the well; die */
return die(fname, c.poison)
2015-10-23 21:16:42 +08:00
case msg := <-msgChan:
if msg.Err != nil {
2015-10-23 22:23:46 +08:00
if _, ok := msg.Err.(*websocket.CloseError); !ok {
logrus.Warnf("c.Conn.ReadMessage: %v", msg.Err)
}
return openPoison(fname, c.poison)
2015-10-23 21:16:42 +08:00
}
if len(msg.Data) == 0 {
2015-10-23 21:16:42 +08:00
logrus.Warnf("An error has occured")
return openPoison(fname, c.poison)
2015-10-23 21:16:42 +08:00
}
switch msg.Data[0] {
2018-04-10 19:59:05 +08:00
case c.message.output: // data
2015-10-23 21:16:42 +08:00
buf, err := base64.StdEncoding.DecodeString(string(msg.Data[1:]))
if err != nil {
logrus.Warnf("Invalid base64 content: %q", msg.Data[1:])
2015-11-19 07:23:01 +08:00
break
2015-10-23 21:16:42 +08:00
}
c.Output.Write(buf)
2018-04-10 19:59:05 +08:00
case c.message.pong: // pong
case c.message.setWindowTitle: // new title
2015-10-23 21:16:42 +08:00
newTitle := string(msg.Data[1:])
2015-10-23 22:30:23 +08:00
fmt.Fprintf(c.Output, "\033]0;%s\007", newTitle)
2018-04-10 19:59:05 +08:00
case c.message.setPreferences: // json prefs
2015-10-23 21:16:42 +08:00
logrus.Debugf("Unhandled protocol message: json pref: %s", string(msg.Data[1:]))
2018-04-10 19:59:05 +08:00
case c.message.setReconnect: // autoreconnect
2015-10-23 21:16:42 +08:00
logrus.Debugf("Unhandled protocol message: autoreconnect: %s", string(msg.Data))
default:
logrus.Warnf("Unhandled protocol message: %s", string(msg.Data))
2015-09-19 09:40:42 +08:00
}
2015-08-25 00:48:27 +08:00
}
}
}
2015-10-23 22:30:23 +08:00
// SetOutput changes the output stream
func (c *Client) SetOutput(w io.Writer) {
c.Output = w
}
2015-11-02 02:31:46 +08:00
// ParseURL parses an URL which may be incomplete and tries to standardize it
func ParseURL(input string) (string, error) {
parsed, err := url.Parse(input)
if err != nil {
return "", err
}
switch parsed.Scheme {
case "http", "https":
// everything is ok
default:
return ParseURL(fmt.Sprintf("http://%s", input))
}
return parsed.String(), nil
}
2015-08-25 00:48:27 +08:00
// NewClient returns a GoTTY client object
2015-11-02 02:31:46 +08:00
func NewClient(inputURL string) (*Client, error) {
url, err := ParseURL(inputURL)
if err != nil {
return nil, err
}
2015-08-25 00:48:27 +08:00
return &Client{
2015-09-30 23:17:57 +08:00
Dialer: &websocket.Dialer{},
2015-11-02 02:31:46 +08:00
URL: url,
2015-09-30 23:17:57 +08:00
WriteMutex: &sync.Mutex{},
2015-10-23 22:30:23 +08:00
Output: os.Stdout,
poison: make(chan bool),
2015-08-25 00:48:27 +08:00
}, nil
}