Files
HomeAgent/internal/devicebridge/client/transport.go
JianFeeeee ba5785036a feat: 设备鉴权迁移至客户端 + 插件卸载保护
安全修复(客户端鉴权):
- remotedevice 服务端移除授权状态存储(authorized map/SetAuthorized/handleDeviceAuth)
- DeviceMeta.Authorized 改为设备 hello 自报,服务端仅透传展示
- device_ctl_* 工具移除服务端授权检查,无条件转发,设备端自行决定是否执行
- 共享设备桥库 Bridge 新增本地 authorized 状态,未授权收到 cmd 直接拒绝
- waiter: --device-authorized / device_authorized 配置控制本地授权
- GUI: 授权存 gui-prefs 本地文件;设备页仅本机可切换开关
- webui /device/auth 旧路径返回 410 Gone
- 根因:agent 可经 config_set 篡改服务端授权配置自行授权设备

插件管理强化:
- 内置插件禁止卸载(IsBuiltinPlugin + 409),外部插件卸载即时生效
- 卸载不存在插件返回 404;移除误导性 reload_required 提示
- webui 插件路由:名称白名单校验防路径穿越、保留字路径保护
2026-08-24 19:26:11 +08:00

361 lines
8.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package client
import (
"bufio"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"net"
"net/url"
"strings"
"time"
)
const (
wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
wsVersion = "13"
crlf = "\r\n"
)
// wsConn 封装一条 WebSocket 连接(客户端视角,帧带 mask
type wsConn struct {
conn net.Conn
br *bufio.Reader
bw *bufio.Writer
closed bool
}
// dialWS 发起 WS 客户端握手升级。
// 支持 ws:// 和 wss://wss 暂未实现,若需要需加 TLS dial
func dialWS(rawURL, token string, timeout time.Duration) (*wsConn, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("devicebridge: invalid ws url %q: %w", rawURL, err)
}
host := u.Host
if u.Port() == "" {
if u.Scheme == "wss" {
host = host + ":443"
} else {
host = host + ":80"
}
}
path := u.Path
if u.RawQuery != "" {
path = path + "?" + u.RawQuery
}
if path == "" {
path = "/"
}
// 默认路径
if token != "" && strings.Index(path, "token=") < 0 {
if strings.ContainsRune(path, '?') {
path = path + "&token=" + urlEscape(token)
} else {
path = path + "?token=" + urlEscape(token)
}
}
dialer := net.Dialer{Timeout: timeout}
conn, err := dialer.Dial("tcp", host)
if err != nil {
return nil, fmt.Errorf("devicebridge: dial %s: %w", host, err)
}
key := wsKey()
var sb strings.Builder
sb.WriteString("GET " + path + " HTTP/1.1" + crlf)
sb.WriteString("Host: " + host + crlf)
sb.WriteString("Upgrade: websocket" + crlf)
sb.WriteString("Connection: Upgrade" + crlf)
sb.WriteString("Sec-WebSocket-Key: " + key + crlf)
sb.WriteString("Sec-WebSocket-Version: " + wsVersion + crlf + crlf)
if _, err := conn.Write([]byte(sb.String())); err != nil {
conn.Close()
return nil, fmt.Errorf("devicebridge: write upgrade: %w", err)
}
br := bufio.NewReader(conn)
var headerBuf strings.Builder
for {
line, err := br.ReadString('\n')
if err != nil {
conn.Close()
return nil, fmt.Errorf("devicebridge: read upgrade resp: %w", err)
}
headerBuf.WriteString(line)
if strings.Contains(headerBuf.String(), crlf+crlf) {
break
}
}
if !strings.Contains(headerBuf.String(), " 101 ") {
conn.Close()
return nil, fmt.Errorf("devicebridge: upgrade failed: %s", firstLine(headerBuf.String()))
}
return &wsConn{conn: conn, br: br, bw: bufio.NewWriter(conn)}, nil
}
// writeText 发送 WS 文本帧0x1带 mask
func (w *wsConn) writeText(payload []byte) error {
return w.writeFrame(0x1, payload)
}
// writeBinary 发送 WS 二进制帧0x2带 mask
func (w *wsConn) writeBinary(payload []byte) error {
return w.writeFrame(0x2, payload)
}
// writeFrame 发送一个 WS 帧(客户端 mask 模式)。
func (w *wsConn) writeFrame(opcode byte, payload []byte) error {
if w.closed {
return fmt.Errorf("devicebridge: connection closed")
}
length := len(payload)
// 帧头
hdrLen := 2
switch {
case length < 126:
// 1 byte length
case length <= 0xffff:
hdrLen += 2
default:
hdrLen += 8
}
hdrLen += 4 // mask key
hdr := make([]byte, hdrLen)
hdr[0] = 0x80 | opcode
switch {
case length < 126:
hdr[1] = 0x80 | byte(length)
case length <= 0xffff:
hdr[1] = 0x80 | 126
binary.BigEndian.PutUint16(hdr[2:4], uint16(length))
default:
hdr[1] = 0x80 | 127
binary.BigEndian.PutUint64(hdr[2:10], uint64(length))
}
// mask key
var maskKey [4]byte
rand.Read(maskKey[:])
copy(hdr[hdrLen-4:], maskKey[:])
// mask payload
masked := make([]byte, length)
for i := 0; i < length; i++ {
masked[i] = payload[i] ^ maskKey[i&3]
}
if _, err := w.bw.Write(hdr); err != nil {
return err
}
if _, err := w.bw.Write(masked); err != nil {
return err
}
return w.bw.Flush()
}
// readFrame 读取一个 WS 帧。返回 (payload, isClose, opcode, error)。
// 客户端收到的帧不带 mask。
func (w *wsConn) readFrame() ([]byte, bool, byte, error) {
if w.closed {
return nil, true, 0, fmt.Errorf("devicebridge: connection closed")
}
b0, err := w.br.ReadByte()
if err != nil {
return nil, true, 0, err
}
opcode := b0 & 0x0f
b1, err := w.br.ReadByte()
if err != nil {
return nil, true, 0, err
}
length := uint64(b1 & 0x7f)
if length == 126 {
var ext [2]byte
if _, err := io.ReadFull(w.br, ext[:]); err != nil {
return nil, true, opcode, err
}
length = uint64(binary.BigEndian.Uint16(ext[:]))
} else if length == 127 {
var ext [8]byte
if _, err := io.ReadFull(w.br, ext[:]); err != nil {
return nil, true, opcode, err
}
length = binary.BigEndian.Uint64(ext[:])
}
// 二进制帧允许更大8MB文本帧 1MB
maxFrame := uint64(1 << 20)
if opcode == 0x2 {
maxFrame = 8 << 20
}
if length > maxFrame {
return nil, true, opcode, fmt.Errorf("devicebridge: frame too large (%d bytes)", length)
}
payload := make([]byte, length)
if _, err := io.ReadFull(w.br, payload); err != nil {
return nil, true, opcode, err
}
switch opcode {
case 0x1, 0x2:
return payload, false, opcode, nil
case 0x8:
return nil, true, opcode, nil
case 0x9: // ping
return nil, false, opcode, errPing
case 0xa: // pong
return nil, false, opcode, nil
default:
return nil, false, opcode, fmt.Errorf("devicebridge: unsupported opcode %x", opcode)
}
}
// writePong 发送 pong 帧。
func (w *wsConn) writePong() error {
return w.writeFrame(0xa, nil)
}
// close 发送关闭帧并关闭连接。
func (w *wsConn) close() error {
w.closed = true
_ = w.writeFrame(0x8, nil)
return w.conn.Close()
}
// ===== 辅助函数 =====
var errPing = fmt.Errorf("ping")
func wsKey() string {
var b [16]byte
rand.Read(b[:])
return base64.StdEncoding.EncodeToString(b[:])
}
func urlEscape(s string) string {
var sb strings.Builder
const hex = "0123456789ABCDEF"
for i := 0; i < len(s); i++ {
c := s[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' {
sb.WriteByte(c)
} else {
sb.WriteByte('%')
sb.WriteByte(hex[c>>4])
sb.WriteByte(hex[c&0xf])
}
}
return sb.String()
}
func firstLine(s string) string {
if i := strings.IndexByte(s, '\n'); i >= 0 {
return strings.TrimSpace(s[:i])
}
return strings.TrimSpace(s)
}
func wsAccept(key string) string {
h := sha256.Sum256([]byte(key + wsGUID))
return base64.StdEncoding.EncodeToString(h[:])
}
// readWSFrame 读取一个 WS 帧(从已有的 bufio.Reader兼容非 wsConn 场景)。
func readWSFrame(br *bufio.Reader) ([]byte, bool, byte, error) {
b0, err := br.ReadByte()
if err != nil {
return nil, true, 0, err
}
opcode := b0 & 0x0f
b1, err := br.ReadByte()
if err != nil {
return nil, true, 0, err
}
length := uint64(b1 & 0x7f)
if length == 126 {
var ext [2]byte
if _, err := io.ReadFull(br, ext[:]); err != nil {
return nil, true, opcode, err
}
length = uint64(binary.BigEndian.Uint16(ext[:]))
} else if length == 127 {
var ext [8]byte
if _, err := io.ReadFull(br, ext[:]); err != nil {
return nil, true, opcode, err
}
length = binary.BigEndian.Uint64(ext[:])
}
maxFrame := uint64(1 << 20)
if opcode == 0x2 {
maxFrame = 8 << 20
}
if length > maxFrame {
return nil, true, opcode, fmt.Errorf("frame too large")
}
payload := make([]byte, length)
if _, err := io.ReadFull(br, payload); err != nil {
return nil, true, opcode, err
}
switch opcode {
case 0x1, 0x2:
return payload, false, opcode, nil
case 0x8:
return nil, true, opcode, nil
default:
return nil, false, opcode, nil
}
}
// writeWSFrame 发送一个 WS 帧(非 mask 模式,服务端用)。
func writeWSFrame(w io.Writer, opcode byte, payload []byte) error {
length := len(payload)
hdr := []byte{0x80 | opcode}
switch {
case length < 126:
hdr = append(hdr, byte(length))
case length <= 0xffff:
hdr = append(hdr, 126, 0, 0)
binary.BigEndian.PutUint16(hdr[len(hdr)-2:], uint16(length))
default:
hdr = append(hdr, 127, 0, 0, 0, 0, 0, 0, 0, 0)
binary.BigEndian.PutUint64(hdr[len(hdr)-8:], uint64(length))
}
if _, err := w.Write(hdr); err != nil {
return err
}
if _, err := w.Write(payload); err != nil {
return err
}
return nil
}
// ensureTimeout 设置连接读写超时。
func (w *wsConn) setDeadline(t time.Time) {
if w.conn != nil {
w.conn.SetDeadline(t)
}
}
// LocalAddr 返回本地地址。
func (w *wsConn) LocalAddr() net.Addr {
if w.conn != nil {
return w.conn.LocalAddr()
}
return nil
}
// RemoteAddr 返回远程地址。
func (w *wsConn) RemoteAddr() net.Addr {
if w.conn != nil {
return w.conn.RemoteAddr()
}
return nil
}