package gottyclient import ( "bufio" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "os" "os/signal" "regexp" "strings" "sync" "syscall" "time" "unicode/utf8" "unsafe" "github.com/moul/gotty-client/vendor/github.com/Sirupsen/logrus" "github.com/moul/gotty-client/vendor/github.com/gorilla/websocket" "github.com/moul/gotty-client/vendor/golang.org/x/crypto/ssh/terminal" ) // 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 } // 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 } // 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 URL string Connected bool WriteMutex *sync.Mutex } func (c *Client) write(data []byte) error { c.WriteMutex.Lock() defer c.WriteMutex.Unlock() return c.Conn.WriteMessage(websocket.TextMessage, data) } // 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 } req, err := http.NewRequest("GET", target.String(), nil) req.Header = *header client := http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } re := regexp.MustCompile("var gotty_auth_token = '(.*)'") return re.FindStringSubmatch(string(body))[1], nil } // Connect tries to dial a websocket server func (c *Client) Connect() error { // Retrieve AuthToken authToken, err := c.GetAuthToken() if err != nil { return err } // Open WebSocket connection target, header, err := GetWebsocketURL(c.URL) if err != nil { return err } conn, _, err := c.Dialer.Dial(target.String(), *header) if err != nil { return err } c.Conn = conn query, err := GetURLQuery(c.URL) if err != nil { return err } querySingle := make(map[string]string) for key, value := range query { querySingle[key] = value[0] } querySingle["AuthToken"] = authToken json, err := json.Marshal(querySingle) if err != nil { log.Printf("Failed to parse init message %v", err) return err } // Send Json err = c.write(json) if err != nil { return err } go c.pingLoop() return nil } func (c *Client) pingLoop() { for { c.write([]byte("1")) time.Sleep(30 * time.Second) } } // 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.write(append([]byte("2"), 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.write(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 } if len(data) == 0 { done <- true logrus.Warnf("An error has occured") return } switch data[0] { case '0': // data buf, err := base64.StdEncoding.DecodeString(string(data[1:])) if err != nil { logrus.Warnf("Invalid base64 content: %q", data[1:]) } fmt.Print(string(buf)) case '1': // pong case '2': // new title newTitle := string(data[1:]) fmt.Printf("\033]0;%s\007", newTitle) case '3': // json prefs logrus.Debugf("Unhandled protocol message: json pref: %s", string(data)) case '4': // autoreconnect logrus.Debugf("Unhandled protocol message: autoreconnect: %s", string(data)) default: logrus.Warnf("Unhandled protocol message: %s", string(data)) } } } // NewClient returns a GoTTY client object func NewClient(httpURL string) (*Client, error) { return &Client{ Dialer: &websocket.Dialer{}, URL: httpURL, WriteMutex: &sync.Mutex{}, }, nil }