package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "log" "net/http" "os" "strconv" "strings" "sync" "syscall" "time" "github.com/gorilla/websocket" ) // ValidateToken checks an HMAC-SHA256 signed token of the form "signature.timestamp". // It validates the timestamp is within ttl seconds (not too old) and within 5 seconds // of clock skew (not too far in the future), then verifies the HMAC signature. func ValidateToken(token, secret string, ttl int64) bool { parts := strings.SplitN(token, ".", 2) if len(parts) != 2 { return false } sig := parts[0] tsStr := parts[1] ts, err := strconv.ParseInt(tsStr, 10, 64) if err != nil { return false } now := time.Now().Unix() if now-ts > ttl { return false } if ts-now > 5 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(tsStr)) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(sig), []byte(expected)) } var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { host := r.Host if strings.Contains(host, ":") { host = strings.Split(host, ":")[0] } return host == "localhost" || host == "127.0.0.1" }, } // HandleWebSocket validates the token query parameter and upgrades the connection // to a WebSocket, then runs the main read loop for control and binary messages. func HandleWebSocket(pool *Pool, secret string, w http.ResponseWriter, r *http.Request) { token := r.URL.Query().Get("token") if !ValidateToken(token, secret, 30) { http.Error(w, "forbidden", http.StatusForbidden) return } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("ws upgrade: %v", err) return } defer conn.Close() var ( mu sync.Mutex activeSession string activePTY *os.File ptyCancel chan struct{} ) startRelay := func(sess *Session, name string) { cancel := make(chan struct{}) mu.Lock() ptyCancel = cancel mu.Unlock() go func() { buf := make([]byte, 32*1024) for { select { case <-cancel: return default: } n, err := sess.PTY.Read(buf) if n > 0 { if writeErr := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); writeErr != nil { return } } if err != nil { if err == io.EOF || strings.Contains(err.Error(), "input/output error") { conn.WriteMessage(websocket.TextMessage, MarshalExited(name)) } return } } }() } stopRelay := func() { mu.Lock() defer mu.Unlock() if ptyCancel != nil { close(ptyCancel) ptyCancel = nil } } defer stopRelay() for { msgType, data, err := conn.ReadMessage() if err != nil { return } switch msgType { case websocket.TextMessage: kind, msg := ParseControlMessage(data) switch kind { case "attach": attach := msg.(*AttachMsg) stopRelay() sess, err := pool.Attach(attach.Attach, 80, 24) if err != nil { conn.WriteMessage(websocket.TextMessage, MarshalError(err.Error())) continue } mu.Lock() activeSession = attach.Attach activePTY = sess.PTY mu.Unlock() sendSIGWINCH(sess) startRelay(sess, attach.Attach) conn.WriteMessage(websocket.TextMessage, MarshalAttached(attach.Attach)) case "resize": resize := msg.(*ResizeMsg) mu.Lock() name := activeSession mu.Unlock() if name != "" { pool.Resize(name, uint16(resize.Resize.Cols), uint16(resize.Resize.Rows)) } case "detach": stopRelay() mu.Lock() activeSession = "" activePTY = nil mu.Unlock() } case websocket.BinaryMessage: mu.Lock() pty := activePTY mu.Unlock() if pty != nil { pty.Write(data) } } } } // sendSIGWINCH sends SIGWINCH to the session's process to trigger tmux repaint. func sendSIGWINCH(s *Session) { if s == nil || s.Cmd == nil || s.Cmd.Process == nil { return } s.Cmd.Process.Signal(syscall.SIGWINCH) }