188 lines
3.8 KiB
Go
188 lines
3.8 KiB
Go
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)
|
|
} |