mirror of
https://github.com/yuanyuanxiang/SimpleRemoter.git
synced 2026-01-21 23:13:08 +08:00
Server/go: authorization client automatically exit if verify succeed
This commit is contained in:
@@ -7,6 +7,8 @@
|
|||||||
```
|
```
|
||||||
server/go/
|
server/go/
|
||||||
├── go.mod # Go 模块定义
|
├── go.mod # Go 模块定义
|
||||||
|
├── auth/
|
||||||
|
│ └── auth.go # 授权验证模块 (TOKEN_AUTH + Heartbeat HMAC)
|
||||||
├── buffer/
|
├── buffer/
|
||||||
│ └── buffer.go # 线程安全的动态缓冲区
|
│ └── buffer.go # 线程安全的动态缓冲区
|
||||||
├── connection/
|
├── connection/
|
||||||
@@ -32,6 +34,7 @@ server/go/
|
|||||||
- **高并发**: 基于 Goroutine 池管理并发连接
|
- **高并发**: 基于 Goroutine 池管理并发连接
|
||||||
- **协议兼容**: 支持原有 C++ 客户端的多种协议标识 (Hell/Hello/Shine/Fuck)
|
- **协议兼容**: 支持原有 C++ 客户端的多种协议标识 (Hell/Hello/Shine/Fuck)
|
||||||
- **协议头解密**: 支持8种协议头加密方式 (V0-V6 + Default)
|
- **协议头解密**: 支持8种协议头加密方式 (V0-V6 + Default)
|
||||||
|
- **授权验证**: 支持 TOKEN_AUTH 和 Heartbeat HMAC-SHA256 双重授权验证
|
||||||
- **XOR编码**: 支持 XOREncoder16 数据编码/解码
|
- **XOR编码**: 支持 XOREncoder16 数据编码/解码
|
||||||
- **ZSTD 压缩**: 使用高效的 ZSTD 算法进行数据压缩
|
- **ZSTD 压缩**: 使用高效的 ZSTD 算法进行数据压缩
|
||||||
- **GBK编码**: 自动将 Windows 客户端的 GBK 编码转换为 UTF-8
|
- **GBK编码**: 自动将 Windows 客户端的 GBK 编码转换为 UTF-8
|
||||||
@@ -46,9 +49,10 @@ server/go/
|
|||||||
|
|
||||||
| 命令 | 值 | 说明 |
|
| 命令 | 值 | 说明 |
|
||||||
|------|-----|------|
|
|------|-----|------|
|
||||||
| TOKEN_AUTH | 100 | 授权请求 |
|
| TOKEN_AUTH | 100 | 授权请求 (验证 SN + Passcode + HMAC) |
|
||||||
| TOKEN_HEARTBEAT | 101 | 心跳包 |
|
| TOKEN_HEARTBEAT | 101 | 心跳包 (支持 HMAC 授权验证,返回 Authorized 状态) |
|
||||||
| TOKEN_LOGIN | 102 | 客户端登录 |
|
| TOKEN_LOGIN | 102 | 客户端登录 |
|
||||||
|
| CMD_HEARTBEAT_ACK | 216 | 心跳响应 (包含 Authorized 字段) |
|
||||||
|
|
||||||
其他命令会被记录为 Debug 日志,可按需扩展。
|
其他命令会被记录为 Debug 日志,可按需扩展。
|
||||||
|
|
||||||
@@ -75,6 +79,25 @@ go build -o simpleremoter-server ./cmd
|
|||||||
|
|
||||||
服务器默认监听 6543 端口,日志输出到 `logs/server.log`。
|
服务器默认监听 6543 端口,日志输出到 `logs/server.log`。
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
| 变量 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `YAMA_PWDHASH` | 密码的 SHA256 哈希值 (64位十六进制) | `61f04dd6...` |
|
||||||
|
| `YAMA_PWD` | 超级密码,用于 HMAC 签名验证 | `your_super_password` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
export YAMA_PWDHASH="61f04dd637a74ee34493fc1025de2c131022536da751c29e3ff4e9024d8eec43"
|
||||||
|
export YAMA_PWD="your_super_password"
|
||||||
|
./simpleremoter-server
|
||||||
|
|
||||||
|
# Windows PowerShell
|
||||||
|
$env:YAMA_PWDHASH="61f04dd637a74ee34493fc1025de2c131022536da751c29e3ff4e9024d8eec43"
|
||||||
|
$env:YAMA_PWD="your_super_password"
|
||||||
|
.\simpleremoter-server.exe
|
||||||
|
```
|
||||||
|
|
||||||
## 使用示例
|
## 使用示例
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -229,6 +252,46 @@ func main() {
|
|||||||
| szStartTime | 456 | 20 | 启动时间 |
|
| szStartTime | 456 | 20 | 启动时间 |
|
||||||
| szReserved | 476 | 512 | 扩展字段 (用`|`分隔) |
|
| szReserved | 476 | 512 | 扩展字段 (用`|`分隔) |
|
||||||
|
|
||||||
|
### Heartbeat 结构
|
||||||
|
|
||||||
|
客户端心跳包结构 (1024 字节):
|
||||||
|
|
||||||
|
| 字段 | 偏移 | 大小 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| Time | 0 | 8 | 时间戳 (uint64) |
|
||||||
|
| ActiveWnd | 8 | 512 | 当前活动窗口 |
|
||||||
|
| Ping | 520 | 4 | 延迟 (int) |
|
||||||
|
| HasSoftware | 524 | 4 | 软件标识 (int) |
|
||||||
|
| SN | 528 | 20 | 序列号 (用于授权验证) |
|
||||||
|
| Passcode | 548 | 44 | 授权码 (格式: v0-v1-v2-v3-v4-v5) |
|
||||||
|
| PwdHmac | 592 | 8 | HMAC 签名 (uint64) |
|
||||||
|
| Reserved | 600 | 424 | 保留字段 |
|
||||||
|
|
||||||
|
### HeartbeatACK 结构
|
||||||
|
|
||||||
|
服务端心跳响应结构 (32 字节):
|
||||||
|
|
||||||
|
| 字段 | 偏移 | 大小 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| Time | 0 | 8 | 原始时间戳 (uint64) |
|
||||||
|
| Authorized | 8 | 1 | 授权状态 (1=已授权, 0=未授权) |
|
||||||
|
| Reserved | 9 | 23 | 保留字段 |
|
||||||
|
|
||||||
|
### 授权验证流程
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端 Heartbeat 服务端
|
||||||
|
│ │
|
||||||
|
│ SN + Passcode + PwdHmac │
|
||||||
|
│ ────────────────────────────────► │
|
||||||
|
│ │ 1. 验证 Passcode 格式
|
||||||
|
│ │ 2. 验证 Passcode 哈希
|
||||||
|
│ │ 3. 验证 HMAC 签名
|
||||||
|
│ HeartbeatACK │
|
||||||
|
│ ◄──────────────────────────────── │
|
||||||
|
│ (Authorized=1 或 0) │
|
||||||
|
```
|
||||||
|
|
||||||
## API 参考
|
## API 参考
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|||||||
@@ -206,3 +206,102 @@ func GenHMAC(pwdHash, superPass string) string {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HeartbeatAuthResult contains the result of heartbeat authentication
|
||||||
|
type HeartbeatAuthResult struct {
|
||||||
|
Authorized bool
|
||||||
|
SN string
|
||||||
|
Passcode string
|
||||||
|
PwdHmac uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticateHeartbeat validates authorization info from a Heartbeat message
|
||||||
|
// Data format (after TOKEN_HEARTBEAT byte):
|
||||||
|
// - offset 0: Time (8 bytes, uint64)
|
||||||
|
// - offset 8: ActiveWnd (512 bytes)
|
||||||
|
// - offset 520: Ping (4 bytes, int)
|
||||||
|
// - offset 524: HasSoftware (4 bytes, int)
|
||||||
|
// - offset 528: SN (20 bytes)
|
||||||
|
// - offset 548: Passcode (44 bytes)
|
||||||
|
// - offset 592: PwdHmac (8 bytes, uint64)
|
||||||
|
func (a *Authenticator) AuthenticateHeartbeat(data []byte) *HeartbeatAuthResult {
|
||||||
|
result := &HeartbeatAuthResult{
|
||||||
|
Authorized: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimum length check: need at least SN + Passcode + PwdHmac
|
||||||
|
// Offset 528 + 20 (SN) + 44 (Passcode) + 8 (PwdHmac) = 600 bytes
|
||||||
|
if len(data) < 600 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract SN (offset 528, 20 bytes)
|
||||||
|
snBytes := data[528:548]
|
||||||
|
// Find null terminator
|
||||||
|
snEnd := bytes.IndexByte(snBytes, 0)
|
||||||
|
if snEnd == -1 {
|
||||||
|
snEnd = len(snBytes)
|
||||||
|
}
|
||||||
|
sn := string(snBytes[:snEnd])
|
||||||
|
result.SN = sn
|
||||||
|
|
||||||
|
// Extract Passcode (offset 548, 44 bytes)
|
||||||
|
passcodeBytes := data[548:592]
|
||||||
|
passcodeEnd := bytes.IndexByte(passcodeBytes, 0)
|
||||||
|
if passcodeEnd == -1 {
|
||||||
|
passcodeEnd = len(passcodeBytes)
|
||||||
|
}
|
||||||
|
passcode := string(passcodeBytes[:passcodeEnd])
|
||||||
|
result.Passcode = passcode
|
||||||
|
|
||||||
|
// Extract PwdHmac (offset 592, 8 bytes)
|
||||||
|
pwdHmac := binary.LittleEndian.Uint64(data[592:600])
|
||||||
|
result.PwdHmac = pwdHmac
|
||||||
|
|
||||||
|
// If SN, Passcode, or PwdHmac is empty/zero, not authorized
|
||||||
|
if sn == "" || passcode == "" || pwdHmac == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split passcode by '-'
|
||||||
|
parts := strings.Split(passcode, "-")
|
||||||
|
if len(parts) != 6 && len(parts) != 7 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get last 4 parts as subvector
|
||||||
|
subvector := parts[len(parts)-4:]
|
||||||
|
|
||||||
|
// Build password string: v[0] + " - " + v[1] + ": " + PwdHash + (optional: ": " + v[2])
|
||||||
|
password := parts[0] + " - " + parts[1] + ": " + a.config.PwdHash
|
||||||
|
if len(parts) == 7 {
|
||||||
|
password += ": " + parts[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive key from password and SN
|
||||||
|
finalKey := DeriveKey(password, sn)
|
||||||
|
|
||||||
|
// Get fixed length ID
|
||||||
|
hash256 := strings.Join(subvector, "-")
|
||||||
|
fixedKey := GetFixedLengthID(finalKey)
|
||||||
|
|
||||||
|
// Compare passcode
|
||||||
|
if hash256 != fixedKey {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passcode validation successful, now verify HMAC
|
||||||
|
superPass := os.Getenv("YAMA_PWD")
|
||||||
|
if superPass == "" {
|
||||||
|
superPass = a.config.SuperPass
|
||||||
|
}
|
||||||
|
|
||||||
|
if superPass != "" {
|
||||||
|
verified := VerifyMessage(superPass, []byte(passcode), pwdHmac)
|
||||||
|
if verified {
|
||||||
|
result.Authorized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -132,6 +132,19 @@ func (h *MyHandler) handleAuth(ctx *connection.Context, data []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleHeartbeat handles heartbeat from client (TOKEN_HEARTBEAT = 101)
|
// handleHeartbeat handles heartbeat from client (TOKEN_HEARTBEAT = 101)
|
||||||
|
// Heartbeat structure (after command byte):
|
||||||
|
// - offset 0: Time (8 bytes, uint64)
|
||||||
|
// - offset 8: ActiveWnd (512 bytes)
|
||||||
|
// - offset 520: Ping (4 bytes, int)
|
||||||
|
// - offset 524: HasSoftware (4 bytes, int)
|
||||||
|
// - offset 528: SN (20 bytes)
|
||||||
|
// - offset 548: Passcode (44 bytes)
|
||||||
|
// - offset 592: PwdHmac (8 bytes, uint64)
|
||||||
|
//
|
||||||
|
// HeartbeatACK structure:
|
||||||
|
// - offset 0: Time (8 bytes, uint64)
|
||||||
|
// - offset 8: Authorized (1 byte, char)
|
||||||
|
// - offset 9: Reserved (23 bytes)
|
||||||
func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) {
|
func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) {
|
||||||
|
|
||||||
// Parse Time from heartbeat request (offset 1, 8 bytes)
|
// Parse Time from heartbeat request (offset 1, 8 bytes)
|
||||||
@@ -141,6 +154,23 @@ func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) {
|
|||||||
uint64(data[5])<<32 | uint64(data[6])<<40 | uint64(data[7])<<48 | uint64(data[8])<<56
|
uint64(data[5])<<32 | uint64(data[6])<<40 | uint64(data[7])<<48 | uint64(data[8])<<56
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authenticate heartbeat if it contains authorization info
|
||||||
|
// data[1:] skips the command byte to get the raw Heartbeat structure
|
||||||
|
var authorized byte = 0
|
||||||
|
if len(data) > 1 {
|
||||||
|
authResult := h.auth.AuthenticateHeartbeat(data[1:])
|
||||||
|
if authResult.Authorized {
|
||||||
|
authorized = 1
|
||||||
|
// Log authorization success (only log once per connection to avoid spam)
|
||||||
|
if !ctx.IsAuthorized.Load() {
|
||||||
|
ctx.IsAuthorized.Store(true)
|
||||||
|
info := ctx.GetInfo()
|
||||||
|
h.log.Info("Heartbeat auth success: clientID=%s computer=%s ip=%s sn=%s passcode=%s pwdHmac=%d",
|
||||||
|
info.ClientID, info.ComputerName, ctx.GetPeerIP(), authResult.SN, authResult.Passcode, authResult.PwdHmac)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build HeartbeatACK response: CMD_HEARTBEAT_ACK(1) + HeartbeatACK(32)
|
// Build HeartbeatACK response: CMD_HEARTBEAT_ACK(1) + HeartbeatACK(32)
|
||||||
resp := make([]byte, 33)
|
resp := make([]byte, 33)
|
||||||
resp[0] = protocol.CommandHeartbeat // CMD_HEARTBEAT_ACK = 216
|
resp[0] = protocol.CommandHeartbeat // CMD_HEARTBEAT_ACK = 216
|
||||||
@@ -153,7 +183,9 @@ func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) {
|
|||||||
resp[6] = byte(hbTime >> 40)
|
resp[6] = byte(hbTime >> 40)
|
||||||
resp[7] = byte(hbTime >> 48)
|
resp[7] = byte(hbTime >> 48)
|
||||||
resp[8] = byte(hbTime >> 56)
|
resp[8] = byte(hbTime >> 56)
|
||||||
// Reserved[24] at offset 9 is already zero
|
// Authorized at offset 9 (1 byte)
|
||||||
|
resp[9] = authorized
|
||||||
|
// Reserved[23] at offset 10 is already zero
|
||||||
|
|
||||||
if err := h.srv.Send(ctx, resp); err != nil {
|
if err := h.srv.Send(ctx, resp); err != nil {
|
||||||
h.log.Error("Failed to send heartbeat ACK to client %d: %v", ctx.ID, err)
|
h.log.Error("Failed to send heartbeat ACK to client %d: %v", ctx.ID, err)
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ type Context struct {
|
|||||||
OutBuffer *buffer.Buffer // Decompressed data for processing
|
OutBuffer *buffer.Buffer // Decompressed data for processing
|
||||||
|
|
||||||
// Client info
|
// Client info
|
||||||
Info ClientInfo
|
Info ClientInfo
|
||||||
IsLoggedIn atomic.Bool
|
IsLoggedIn atomic.Bool
|
||||||
|
IsAuthorized atomic.Bool // Whether client is authorized via heartbeat
|
||||||
|
|
||||||
// Connection state
|
// Connection state
|
||||||
OnlineTime time.Time
|
OnlineTime time.Time
|
||||||
|
|||||||
@@ -260,7 +260,28 @@ func (s *Server) processData(ctx *connection.Context) {
|
|||||||
if err == protocol.ErrNeedMore {
|
if err == protocol.ErrNeedMore {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logger.Error("Parse error for connection %d: %v", ctx.ID, err)
|
// Log more details for unsupported protocol errors
|
||||||
|
if err == protocol.ErrUnsupported {
|
||||||
|
// Get first 32 bytes for debugging
|
||||||
|
peekLen := 32
|
||||||
|
if ctx.InBuffer.Len() < peekLen {
|
||||||
|
peekLen = ctx.InBuffer.Len()
|
||||||
|
}
|
||||||
|
rawData := ctx.InBuffer.Peek(peekLen)
|
||||||
|
// Sanitize for safe logging (replace non-printable chars)
|
||||||
|
ascii := make([]byte, len(rawData))
|
||||||
|
for i, b := range rawData {
|
||||||
|
if b >= 32 && b < 127 {
|
||||||
|
ascii[i] = b
|
||||||
|
} else {
|
||||||
|
ascii[i] = '.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.logger.Warn("Unsupported protocol from ip=%s conn=%d raw=%x ascii=%s",
|
||||||
|
ctx.GetPeerIP(), ctx.ID, rawData, string(ascii))
|
||||||
|
} else {
|
||||||
|
s.logger.Error("Parse error for connection %d: %v", ctx.ID, err)
|
||||||
|
}
|
||||||
_ = ctx.Close()
|
_ = ctx.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user