package bamboo import ( "io" "log" "net" "strings" "sync" "time" "golang.org/x/net/websocket" ) // Terminal relay. When someone opens a terminal for this site in Hammerhead, Hammerhead lists a // session in the site's config (model.SiteConfig.Terminals). For each one the agent opens a // connection out to Hammerhead and joins it to its own SSH server, so Hammerhead can sign in over it. // Nothing listens for this, no port has to be open on an edge, and the SSH server still decides who // gets in: Hammerhead needs its public key in authorized_keys like any other client. const ( termSeenFor = time.Minute // a session id is answered once, even if the config is fetched again termDial = 10 * time.Second ) // sshTarget is the address to dial for the local SSH server. An address that listens on every // interface is reached on the loopback one. func sshTarget(listen string) string { if listen == "" { return "" } host, port, err := net.SplitHostPort(listen) if err != nil { return listen } if ip := net.ParseIP(host); host == "" || (ip != nil && ip.IsUnspecified()) { host = "127.0.0.1" } return net.JoinHostPort(host, port) } // openTerminals starts a relay for each session Hammerhead is waiting on that has not been answered yet. func (b *Bamboo) openTerminals(ids []string) { for _, id := range ids { b.mu.Lock() if b.terms == nil { b.terms = map[string]time.Time{} } for k, t := range b.terms { if time.Since(t) > termSeenFor { delete(b.terms, k) } } _, dup := b.terms[id] if !dup { b.terms[id] = time.Now() } b.mu.Unlock() if !dup { go b.relayTerminal(id) } } } func (b *Bamboo) relayTerminal(id string) { target := sshTarget(b.cfg.SSH) if target == "" { log.Printf("terminal: Hammerhead asked for a terminal but the SSH server is not running (start bamboo with -ssh)") return } wsURL := "ws" + strings.TrimPrefix(b.cfg.Hammerhead, "http") + "/site/terminal/" + id cfg, err := websocket.NewConfig(wsURL, b.cfg.Hammerhead) if err != nil { log.Printf("terminal: %v", err) return } cfg.Header.Set("Authorization", "Bearer "+b.cfg.Token) cfg.Dialer = &net.Dialer{Timeout: termDial} ws, err := websocket.DialConfig(cfg) if err != nil { log.Printf("terminal: could not reach Hammerhead: %v", err) return } ws.PayloadType = websocket.BinaryFrame defer ws.Close() local, err := net.DialTimeout("tcp", target, termDial) if err != nil { log.Printf("terminal: the SSH server at %s is not answering: %v", target, err) return // closing the connection tells Hammerhead at once } defer local.Close() var once sync.Once stop := func() { once.Do(func() { ws.Close(); local.Close() }) } done := make(chan struct{}, 2) go func() { io.Copy(local, ws); stop(); done <- struct{}{} }() go func() { io.Copy(ws, local); stop(); done <- struct{}{} }() <-done <-done }