package main import ( "encoding/json" "fmt" "io" "net" "net/http" "net/url" "os" "os/signal" "strconv" "strings" "sync" "sync/atomic" "syscall" "github.com/elisescu/tty-share/server" "github.com/gorilla/websocket" "github.com/hashicorp/yamux" "github.com/moby/term" log "github.com/sirupsen/logrus" ) type ttyShareClient struct { url string ttyWsConn *websocket.Conn tunnelWsConn *websocket.Conn tunnelAddresses *string detachKeys string wcChan chan os.Signal ioFlagAtomic uint32 // used with atomic winSizes struct { thisW uint16 thisH uint16 remoteW uint16 remoteH uint16 } winSizesMutex sync.Mutex tunnelMuxSession *yamux.Session } func newTtyShareClient(url string, detachKeys string, tunnelConfig *string) *ttyShareClient { return &ttyShareClient{ url: url, ttyWsConn: nil, detachKeys: detachKeys, wcChan: make(chan os.Signal, 1), ioFlagAtomic: 1, tunnelAddresses: tunnelConfig, } } func clearScreen() { fmt.Fprintf(os.Stdout, "\033[H\033[2J") } type keyListener struct { wrappedReader io.Reader ioFlagAtomicP *uint32 } func (kl *keyListener) Read(data []byte) (n int, err error) { n, err = kl.wrappedReader.Read(data) if _, ok := err.(term.EscapeError); ok { log.Debug("Escape code detected.") } // If we are not supposed to do any IO, then return 0 bytes read. This happens the local // window is smaller than the remote one if atomic.LoadUint32(kl.ioFlagAtomicP) == 0 { return 0, err } return } func (c *ttyShareClient) updateAndDecideStdoutMuted() { log.Infof("This window: %dx%d. Remote window: %dx%d", c.winSizes.thisW, c.winSizes.thisH, c.winSizes.remoteW, c.winSizes.remoteH) if c.winSizes.thisH < c.winSizes.remoteH || c.winSizes.thisW < c.winSizes.remoteW { atomic.StoreUint32(&c.ioFlagAtomic, 0) clearScreen() messageFormat := "\n\rYour window is smaller than the remote window. Please resize or press to detach.\n\r\tRemote window: %dx%d \n\r\tYour window: %dx%d \n\r" fmt.Printf(messageFormat, c.winSizes.remoteW, c.winSizes.remoteH, c.winSizes.thisW, c.winSizes.thisH) } else { if atomic.LoadUint32(&c.ioFlagAtomic) == 0 { // clear the screen when changing back to "write" // TODO: notify the remote side to "refresh" the content. clearScreen() } atomic.StoreUint32(&c.ioFlagAtomic, 1) } } func (c *ttyShareClient) updateThisWinSize() { size, err := term.GetWinsize(os.Stdin.Fd()) if err == nil { c.winSizesMutex.Lock() c.winSizes.thisW = size.Width c.winSizes.thisH = size.Height c.winSizesMutex.Unlock() } } func (c *ttyShareClient) Run() (err error) { log.Debugf("Connecting as a client to %s ..", c.url) resp, err := http.Get(c.url) if err != nil { return } // Get the path of the websockts route from the header ttyWsPath := resp.Header.Get("TTYSHARE-TTY-WSPATH") ttyWSProtocol := resp.Header.Get("TTYSHARE-VERSION") ttyTunnelPath := resp.Header.Get("TTYSHARE-TUNNEL-WSPATH") // Build the WS URL from the host part of the given http URL and the wsPath httpURL, err := url.Parse(c.url) if err != nil { return } wsScheme := "ws" if httpURL.Scheme == "https" { wsScheme = "wss" } ttyWsURL := wsScheme + "://" + httpURL.Host + ttyWsPath ttyTunnelURL := wsScheme + "://" + httpURL.Host + ttyTunnelPath log.Debugf("Built the WS URL from the headers: %s", ttyWsURL) c.ttyWsConn, _, err = websocket.DefaultDialer.Dial(ttyWsURL, nil) if err != nil { return } defer c.ttyWsConn.Close() tunnelFunc := func() { if *c.tunnelAddresses == "" { // Don't build a tunnel return } if ver, err := strconv.Atoi(ttyWSProtocol); err != nil || ver < 2 { log.Fatalf("Cannot create a tunnel. Server too old (protocol %d, required min. 2)", ver) } c.tunnelWsConn, _, err = websocket.DefaultDialer.Dial(ttyTunnelURL, nil) if err != nil { log.Errorf("Cannot create a tunnel connection with the server. Server needs to allow that") return } defer c.tunnelWsConn.Close() a := strings.Split(*c.tunnelAddresses, ":") tunnelRemoteAddress := fmt.Sprintf("%s:%s", a[1], a[2]) tunnelLocalAddress := fmt.Sprintf(":%s", a[0]) initMsg := server.TunInitMsg{ Address: tunnelRemoteAddress, } data, err := json.Marshal(initMsg) if err != nil { log.Errorf("Could not marshal the tunnel init message: %s", err.Error()) return } err = c.tunnelWsConn.WriteMessage(websocket.TextMessage, data) if err != nil { log.Errorf("Could not initiate the tunnel: %s", err.Error()) return } wsWRC := server.WSConnReadWriteCloser{ WsConn: c.tunnelWsConn, } localListener, err := net.Listen("tcp", tunnelLocalAddress) if err != nil { log.Errorf("Could not listen locally for the tunnel: %s", err.Error()) } c.tunnelMuxSession, err = yamux.Server(&wsWRC, nil) if err != nil { log.Errorf("Could not create mux server: %s", err.Error()) } for { localTunconn, err := localListener.Accept() if err != nil { log.Warnf("Cannot accept local tunnel connections: ", err.Error()) return } muxClient, err := c.tunnelMuxSession.Open() if err != nil { log.Warnf("Cannot create a muxer to the remote, over ws: ", err.Error()) return } go func() { io.Copy(muxClient, localTunconn) defer localTunconn.Close() defer muxClient.Close() }() go func() { io.Copy(localTunconn, muxClient) defer localTunconn.Close() defer muxClient.Close() }() } } detachBytes, err := term.ToBytes(c.detachKeys) if err != nil { log.Errorf("Invalid dettaching keys: %s", c.detachKeys) return } state, err := term.MakeRaw(os.Stdin.Fd()) defer term.RestoreTerminal(os.Stdin.Fd(), state) clearScreen() protoWS := server.NewTTYProtocolWSLocked(c.ttyWsConn) monitorWinChanges := func() { // start monitoring the size of the terminal signal.Notify(c.wcChan, syscall.SIGWINCH) for { select { case <-c.wcChan: c.updateThisWinSize() c.updateAndDecideStdoutMuted() protoWS.SetWinSize(int(c.winSizes.thisW), int(c.winSizes.thisH)) } } } readLoop := func() { var err error for { err = protoWS.ReadAndHandle( // onWrite func(data []byte) { if atomic.LoadUint32(&c.ioFlagAtomic) != 0 { os.Stdout.Write(data) } }, // onWindowSize func(cols, rows int) { c.winSizesMutex.Lock() c.winSizes.remoteW = uint16(cols) c.winSizes.remoteH = uint16(rows) c.winSizesMutex.Unlock() c.updateThisWinSize() c.updateAndDecideStdoutMuted() }, ) if err != nil { log.Errorf("Error parsing remote message: %s", err.Error()) if err == io.EOF { // Remote WS connection closed return } } } } writeLoop := func() { kl := &keyListener{ wrappedReader: term.NewEscapeProxy(os.Stdin, detachBytes), ioFlagAtomicP: &c.ioFlagAtomic, } _, err := io.Copy(protoWS, kl) if err != nil { log.Debugf("Connection closed: %s", err.Error()) c.Stop() return } } go monitorWinChanges() go writeLoop() go tunnelFunc() readLoop() clearScreen() return } func (c *ttyShareClient) Stop() { // if we had a tunnel, close it if c.tunnelMuxSession != nil { c.tunnelMuxSession.Close() c.tunnelWsConn.Close() } c.ttyWsConn.Close() signal.Stop(c.wcChan) }