169 lines
3.9 KiB
Go
169 lines
3.9 KiB
Go
package agenthttp
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"zlh-agent/internal/provision"
|
|
"zlh-agent/internal/state"
|
|
)
|
|
|
|
/*
|
|
websocket.go
|
|
ZeroLagHub Agent — Real-time Log WebSocket
|
|
|
|
Endpoint:
|
|
GET /console/stream
|
|
*/
|
|
|
|
const maxInitialTail = 4096 // 4 KB
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Minimal WebSocket Upgrader (stdlib only)
|
|
----------------------------------------------------------------------------*/
|
|
|
|
type WebSocketConn struct {
|
|
Conn net.Conn
|
|
Rw *bufio.ReadWriter
|
|
}
|
|
|
|
func upgradeToWebSocket(w http.ResponseWriter, r *http.Request) (*WebSocketConn, error) {
|
|
if !strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") ||
|
|
strings.ToLower(r.Header.Get("Upgrade")) != "websocket" {
|
|
return nil, fmt.Errorf("invalid websocket upgrade request")
|
|
}
|
|
|
|
key := r.Header.Get("Sec-WebSocket-Key")
|
|
if key == "" {
|
|
return nil, fmt.Errorf("missing Sec-WebSocket-Key")
|
|
}
|
|
|
|
accept := computeAcceptKey(key)
|
|
|
|
h := w.Header()
|
|
h.Set("Upgrade", "websocket")
|
|
h.Set("Connection", "Upgrade")
|
|
h.Set("Sec-WebSocket-Accept", accept)
|
|
h.Set("Sec-WebSocket-Version", "13")
|
|
|
|
w.WriteHeader(http.StatusSwitchingProtocols)
|
|
|
|
hj, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
return nil, fmt.Errorf("websocket: hijacking not supported")
|
|
}
|
|
|
|
conn, rw, err := hj.Hijack()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("websocket hijack: %w", err)
|
|
}
|
|
|
|
return &WebSocketConn{
|
|
Conn: conn,
|
|
Rw: rw,
|
|
}, nil
|
|
}
|
|
|
|
func (c *WebSocketConn) WriteText(msg string) error {
|
|
payload := []byte(msg)
|
|
|
|
// FIN + opcode(1 = text)
|
|
header := []byte{0x81}
|
|
|
|
// Length encoding
|
|
if len(payload) < 126 {
|
|
header = append(header, byte(len(payload)))
|
|
} else {
|
|
header = append(header, 126, byte(len(payload)>>8), byte(len(payload)))
|
|
}
|
|
|
|
if _, err := c.Conn.Write(header); err != nil {
|
|
return err
|
|
}
|
|
_, err := c.Conn.Write(payload)
|
|
return err
|
|
}
|
|
|
|
func (c *WebSocketConn) Close() error {
|
|
return c.Conn.Close()
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
SHA-1 + Base64 for Sec-WebSocket-Accept
|
|
----------------------------------------------------------------------------*/
|
|
|
|
func computeAcceptKey(key string) string {
|
|
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
h := sha1.Sum([]byte(key + magic))
|
|
return base64.StdEncoding.EncodeToString(h[:])
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
MAIN HANDLER — /console/stream
|
|
----------------------------------------------------------------------------*/
|
|
|
|
func handleConsoleStream(w http.ResponseWriter, r *http.Request) {
|
|
cfg, err := state.LoadConfig()
|
|
if err != nil {
|
|
http.Error(w, "no config loaded", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ws, err := upgradeToWebSocket(w, r)
|
|
if err != nil {
|
|
log.Println("[ws] upgrade failed:", err)
|
|
http.Error(w, "websocket upgrade failed", http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer ws.Close()
|
|
|
|
dir := provision.ServerDir(*cfg)
|
|
logfile := filepath.Join(dir, "logs", "latest.log")
|
|
|
|
f, err := os.Open(logfile)
|
|
if err != nil {
|
|
_ = ws.WriteText(fmt.Sprintf("[error] cannot open log: %v", err))
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
// 1) Send last 4 KB (initial tail)
|
|
stat, _ := f.Stat()
|
|
sz := stat.Size()
|
|
if sz > maxInitialTail {
|
|
_, _ = f.Seek(sz-maxInitialTail, 0)
|
|
}
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
_ = ws.WriteText(scanner.Text())
|
|
}
|
|
|
|
// 2) Follow the file — stream new log lines live
|
|
for {
|
|
time.Sleep(500 * time.Millisecond)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
_ = ws.WriteText(line)
|
|
}
|
|
// on EOF, loop continues and scanner will pick up new lines
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------------------------------------
|
|
Register handler in NewMux()
|
|
----------------------------------------------------------------------------*/
|
|
|
|
func registerWebSocket(m *http.ServeMux) {
|
|
m.HandleFunc("/console/stream", handleConsoleStream)
|
|
}
|