DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
package protocol
import "time"
// AuthResponse 是认证成功后返回给客户端的登录令牌响应。
type AuthResponse struct {
Result string `json:"result"`
Authtoken string `json:"authtoken"`
ShardRole []ShardRole `json:"shardrole"`
}
// ShardRole 描述账号在单个分区里的已有角色。
type ShardRole struct {
Shard string `json:"shard"`
Name string `json:"name"`
LLT int64 `json:"llt"`
}
// ShardListResponse 是分区列表接口响应。
type ShardListResponse struct {
Shards []ShardDescriptor `json:"shards"`
}
// ShardDescriptor 描述客户端选区页展示的单个分区。
type ShardDescriptor struct {
Name string `json:"name"`
DisplayName string `json:"dn"`
ShowState int32 `json:"ss"`
Recommend int32 `json:"recmd"`
RecommendLang string `json:"recmd_lang"`
VGID int32 `json:"v_gid"`
MainlineLevel string `json:"mainline_level"`
}
// GetGateResponse 是网关寻址接口响应。
type GetGateResponse struct {
Result string `json:"result"`
IP string `json:"ip"`
LoginToken string `json:"logintoken"`
}
// NoticeResponse 是登录前公告和端点发现响应。
type NoticeResponse struct {
Endpoint map[string]string `json:"Endpoint"`
ServerTime string `json:"ServerTime"`
TimeoutTime string `json:"TimeoutTime"`
TokenTime string `json:"TokenTime"`
Publics []any `json:"Publics"`
Chatserver string `json:"Chatserver"`
ChatIp string `json:"chatIp"`
NoticeCustomer string `json:"NoticeCustomer"`
WinScanSwitch int `json:"WinScanSwitch"`
AndroidPrivacySwitch string `json:"AndroidPrivacySwitch"`
IosPrivacySwitch string `json:"IosPrivacySwitch"`
ClientExpireTime string `json:"clientExpireTime"`
}
// BuildAuthOK 构造认证成功响应。
func BuildAuthOK(authToken string) AuthResponse {
return AuthResponse{
Result: "ok",
Authtoken: authToken,
ShardRole: []ShardRole{},
}
}
// BuildShardList 构造分区列表响应并隔离调用方切片。
func BuildShardList(shards []ShardDescriptor) ShardListResponse {
return ShardListResponse{Shards: append([]ShardDescriptor(nil), shards...)}
}
// BuildGetGate 构造网关寻址响应。
func BuildGetGate(ip, loginToken string) GetGateResponse {
return GetGateResponse{
Result: "ok",
IP: ip,
LoginToken: loginToken,
}
}
// BuildNotice 构造公告响应并隔离端点表。
func BuildNotice(endpoints map[string]string, chatListen string, now time.Time) NoticeResponse {
var copiedEndpoints map[string]string
if endpoints != nil {
copiedEndpoints = make(map[string]string, len(endpoints))
for key, value := range endpoints {
copiedEndpoints[key] = value
}
}
return NoticeResponse{
Endpoint: copiedEndpoints,
ServerTime: formatUnix(now),
TimeoutTime: "30",
TokenTime: "12900",
Publics: []any{},
Chatserver: chatListen,
ChatIp: chatListen,
NoticeCustomer: "",
WinScanSwitch: 0,
AndroidPrivacySwitch: "false",
IosPrivacySwitch: "false",
ClientExpireTime: "0",
}
}
+5
View File
@@ -0,0 +1,5 @@
// Package protocol 定义 LongHeng 公开 gateway wire format。
//
// 这里包含 packet envelope、frame 编码、登录契约、校验和/加密辅助逻辑以及生成的玩法 schema。
// 任何变更都应视为 SDK 和向后兼容性决策。
package protocol
+194
View File
@@ -0,0 +1,194 @@
package protocol
import (
"errors"
"fmt"
"google.golang.org/protobuf/encoding/protowire"
)
// ErrorCode 是协议层向客户端暴露的稳定错误码。
type ErrorCode int32
// 协议错误码分组保留 HTTP 语义,并扩展框架内部错误。
const (
CodeOK ErrorCode = 0
CodeBadRequest ErrorCode = 400
CodeUnauthorized ErrorCode = 401
CodeNotFound ErrorCode = 404
CodeConflict ErrorCode = 409
CodeInternal ErrorCode = 500
CodeUnavailable ErrorCode = 503
CodeNotImplemented ErrorCode = 1001
)
// AppError 是服务内部携带错误码、公开消息和调试明细的错误。
type AppError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
}
// ErrorResponse 是发送给客户端的错误响应体。
type ErrorResponse struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Detail string `json:"detail,omitempty"`
ServerTime int64 `json:"server_time"`
}
// NewError 构造带协议错误码的应用错误。
func NewError(code ErrorCode, message string) *AppError {
return &AppError{Code: code, Message: message}
}
// Errorf 使用格式化消息构造应用错误。
func Errorf(code ErrorCode, format string, args ...any) *AppError {
return NewError(code, fmt.Sprintf(format, args...))
}
// WrapError 把底层错误明细保存在应用错误里。
func WrapError(code ErrorCode, message string, err error) *AppError {
if err == nil {
return NewError(code, message)
}
return &AppError{Code: code, Message: message, Detail: err.Error()}
}
// ErrorFrom 把任意 error 归一成 AppError。
func ErrorFrom(err error) AppError {
if err == nil {
return AppError{Code: CodeOK}
}
var appErr *AppError
if errors.As(err, &appErr) && appErr != nil {
return *appErr
}
return AppError{
Code: CodeInternal,
Message: "internal server error",
Detail: err.Error(),
}
}
// PublicErrorResponse 生成可安全返回给客户端的错误响应。
func PublicErrorResponse(err AppError, serverTime int64) ErrorResponse {
resp := ErrorResponse{
Code: err.Code,
Message: err.Message,
Detail: err.Detail,
ServerTime: serverTime,
}
if resp.Message == "" {
resp.Message = err.Code.String()
}
switch err.Code {
case CodeInternal:
resp.Message = "internal server error"
case CodeUnavailable:
resp.Message = "service unavailable"
case CodeUnauthorized:
resp.Message = "unauthorized"
}
switch err.Code {
case CodeBadRequest:
default:
resp.Detail = ""
}
return resp
}
// Error 返回应用错误的日志友好文本。
func (e *AppError) Error() string {
if e == nil {
return ""
}
if e.Detail == "" {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
return fmt.Sprintf("%s: %s: %s", e.Code, e.Message, e.Detail)
}
// String 返回错误码的协议文本。
func (c ErrorCode) String() string {
switch c {
case CodeOK:
return "OK"
case CodeBadRequest:
return "BAD_REQUEST"
case CodeUnauthorized:
return "UNAUTHORIZED"
case CodeNotFound:
return "NOT_FOUND"
case CodeConflict:
return "CONFLICT"
case CodeInternal:
return "INTERNAL"
case CodeUnavailable:
return "UNAVAILABLE"
case CodeNotImplemented:
return "NOT_IMPLEMENTED"
default:
return fmt.Sprintf("ERROR_%d", c)
}
}
// EncodeErrorResponse 把错误响应编码为轻量 protobuf wire body。
func EncodeErrorResponse(resp ErrorResponse) []byte {
body := make([]byte, 0, 96)
body = appendI32VarintField(body, 1, int32(resp.Code))
body = appendStringField(body, 2, resp.Message)
body = appendStringField(body, 3, resp.Detail)
body = appendInt64Varint(body, 4, resp.ServerTime)
return body
}
// DecodeErrorResponse 从 protobuf wire body 解码错误响应。
func DecodeErrorResponse(body []byte) ErrorResponse {
var resp ErrorResponse
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return resp
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.VarintType:
value, n := protowire.ConsumeVarint(body)
if n < 0 {
return resp
}
resp.Code = ErrorCode(int32FromVarint(value))
body = body[n:]
case fieldNumber == 2 && typ == protowire.BytesType:
value, n := protowire.ConsumeString(body)
if n < 0 {
return resp
}
resp.Message = value
body = body[n:]
case fieldNumber == 3 && typ == protowire.BytesType:
value, n := protowire.ConsumeString(body)
if n < 0 {
return resp
}
resp.Detail = value
body = body[n:]
case fieldNumber == 4 && typ == protowire.VarintType:
value, n := protowire.ConsumeVarint(body)
if n < 0 {
return resp
}
resp.ServerTime = int64FromVarint(value)
body = body[n:]
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return resp
}
body = body[n:]
}
}
return resp
}
+327
View File
@@ -0,0 +1,327 @@
package protocol
import (
"encoding/binary"
"errors"
"fmt"
"hash/crc32"
"io"
"sort"
"strings"
)
// 帧协议基础常量定义固定头、版本和默认 body 上限。
const (
FrameMagic uint16 = 0x5047 // 固定帧协议探测魔数:"GP",不是品牌缩写。
FrameVersion1 uint8 = 1
FrameHeaderSizeV1 = 32
MaxFrameBodySize uint32 = 4 * 1024 * 1024
)
// 标准帧 metadata 键名;链路字段兼容 W3C trace context。
const (
FrameMetadataTraceParent = "traceparent"
FrameMetadataTraceState = "tracestate"
FrameMetadataBaggage = "baggage"
// FrameMetadataIdempotencyKey 由客户端为一次业务请求生成,重试和重连时保持不变。
FrameMetadataIdempotencyKey = "idempotency-key"
frameMetadataMagic = "MD"
frameMetadataVersion = 1
frameMaxHeaderSize = 255
)
// 帧标志位描述 body 的压缩、加密和校验状态。
const (
FrameFlagCompressed uint16 = 1 << iota
FrameFlagEncrypted
FrameFlagChecksum
)
// 帧 codec 标识 body 的序列化格式。
const (
FrameCodecRaw uint16 = iota
FrameCodecProtobuf
FrameCodecJSON
)
// 帧解析和校验错误。
var (
ErrInvalidFrameMagic = errors.New("protocol frame magic is invalid")
ErrInvalidFrameVersion = errors.New("protocol frame version is invalid")
ErrInvalidFrameHeader = errors.New("protocol frame header is invalid")
ErrFrameBodyTooLarge = errors.New("protocol frame body is too large")
ErrFrameChecksumInvalid = errors.New("protocol frame checksum is invalid")
ErrFrameMetadataInvalid = errors.New("protocol frame metadata is invalid")
ErrFrameMetadataTooLarge = errors.New("protocol frame metadata is too large")
)
// Frame 是 gateway frame_v1 线协议的内存表示。
type Frame struct {
Version uint8
Flags uint16
Codec uint16
PacketID int32
MsgID uint32
Sequence uint64
Checksum uint32
Body []byte
Metadata map[string]string
}
// FrameFromPacket 把旧 packet 包装为 frame_v1。
func FrameFromPacket(pkt Packet, sequence uint64) Frame {
flags := uint16(0)
if pkt.Compressed {
flags |= FrameFlagCompressed
}
return Frame{
Version: FrameVersion1,
Flags: flags,
Codec: FrameCodecRaw,
PacketID: pkt.PacketID,
MsgID: pkt.MsgID,
Sequence: sequence,
Body: append([]byte(nil), pkt.Body...),
}
}
// Packet 把 frame 还原成旧 packet 视图并拷贝 body。
func (f Frame) Packet() Packet {
return Packet{
PacketID: f.PacketID,
MsgID: f.MsgID,
Compressed: f.Flags&FrameFlagCompressed != 0,
Body: append([]byte(nil), f.Body...),
}
}
func packetFromFrame(frame Frame) Packet {
return Packet{
PacketID: frame.PacketID,
MsgID: frame.MsgID,
Compressed: frame.Flags&FrameFlagCompressed != 0,
Body: frame.Body,
}
}
// ReadFrame 从 reader 读取并校验一个 frame_v1 帧。
func ReadFrame(r io.Reader, maxBodySize uint32) (Frame, error) {
maxBodySize = normMaxFrameBody(maxBodySize)
var header [FrameHeaderSizeV1]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return Frame{}, err
}
if binary.LittleEndian.Uint16(header[0:2]) != FrameMagic {
return Frame{}, ErrInvalidFrameMagic
}
version := header[2]
if version != FrameVersion1 {
return Frame{}, fmt.Errorf("%w: %d", ErrInvalidFrameVersion, version)
}
headerSize := int(header[3])
if headerSize < FrameHeaderSizeV1 {
return Frame{}, fmt.Errorf("%w: size=%d", ErrInvalidFrameHeader, headerSize)
}
var metadataHeader []byte
if headerSize > FrameHeaderSizeV1 {
metadataHeader = make([]byte, headerSize-FrameHeaderSizeV1)
if _, err := io.ReadFull(r, metadataHeader); err != nil {
return Frame{}, err
}
}
metadata, err := decodeFrameMeta(metadataHeader)
if err != nil {
return Frame{}, err
}
bodySize := binary.LittleEndian.Uint32(header[24:28])
if bodySize > maxBodySize {
return Frame{}, fmt.Errorf("%w: %d", ErrFrameBodyTooLarge, bodySize)
}
body := make([]byte, bodySize)
if bodySize > 0 {
if _, err := io.ReadFull(r, body); err != nil {
return Frame{}, err
}
}
frame := Frame{
Version: version,
Flags: binary.LittleEndian.Uint16(header[4:6]),
Codec: binary.LittleEndian.Uint16(header[6:8]),
PacketID: packetIDFromWire(binary.LittleEndian.Uint32(header[8:12])),
MsgID: binary.LittleEndian.Uint32(header[12:16]),
Sequence: binary.LittleEndian.Uint64(header[16:24]),
Checksum: binary.LittleEndian.Uint32(header[28:32]),
Body: body,
Metadata: metadata,
}
if frame.Flags&FrameFlagChecksum != 0 && crc32.ChecksumIEEE(body) != frame.Checksum {
return Frame{}, ErrFrameChecksumInvalid
}
return frame, nil
}
// WriteFrame 把 frame_v1 帧写入 writer。
func WriteFrame(w io.Writer, frame Frame) error {
version := frame.Version
if version == 0 {
version = FrameVersion1
}
if version != FrameVersion1 {
return fmt.Errorf("%w: %d", ErrInvalidFrameVersion, version)
}
bodySize := len(frame.Body)
if bodySize > int(MaxFrameBodySize) || uint64(bodySize) > uint64(^uint32(0)) {
return fmt.Errorf("%w: %d", ErrFrameBodyTooLarge, bodySize)
}
metadataHeader, err := encodeFrameMeta(frame.Metadata)
if err != nil {
return err
}
headerSize := FrameHeaderSizeV1 + len(metadataHeader)
if headerSize > frameMaxHeaderSize {
return fmt.Errorf("%w: header_size=%d", ErrFrameMetadataTooLarge, headerSize)
}
if frame.Flags&FrameFlagChecksum != 0 {
frame.Checksum = crc32.ChecksumIEEE(frame.Body)
} else {
frame.Checksum = 0
}
var header [FrameHeaderSizeV1]byte
binary.LittleEndian.PutUint16(header[0:2], FrameMagic)
header[2] = version
header[3] = uint8(headerSize) //nolint:gosec // G115:headerSize 已校验不超过 frameMaxHeaderSize。
binary.LittleEndian.PutUint16(header[4:6], frame.Flags)
binary.LittleEndian.PutUint16(header[6:8], frame.Codec)
binary.LittleEndian.PutUint32(header[8:12], packetIDToWire(frame.PacketID))
binary.LittleEndian.PutUint32(header[12:16], frame.MsgID)
binary.LittleEndian.PutUint64(header[16:24], frame.Sequence)
binary.LittleEndian.PutUint32(header[24:28], uint32(bodySize))
binary.LittleEndian.PutUint32(header[28:32], frame.Checksum)
if bodySize == 0 {
return writeFullBuffers(w, header[:], metadataHeader)
}
return writeFullBuffers(w, header[:], metadataHeader, frame.Body)
}
func normMaxFrameBody(maxBodySize uint32) uint32 {
if maxBodySize == 0 {
return MaxFrameBodySize
}
return maxBodySize
}
func packetIDFromWire(value uint32) int32 {
return int32(value) //nolint:gosec // G115:协议线格式用 uint32 保存 int32 的补码位模式。
}
func packetIDToWire(value int32) uint32 {
return uint32(value) //nolint:gosec // G115:协议线格式用 uint32 保存 int32 的补码位模式。
}
// TraceContextMetadata 构造 frame metadata 中的链路追踪字段。
func TraceContextMetadata(traceParent, traceState string) map[string]string {
out := make(map[string]string, 2)
if traceParent = strings.TrimSpace(traceParent); traceParent != "" {
out[FrameMetadataTraceParent] = traceParent
}
if traceState = strings.TrimSpace(traceState); traceState != "" {
out[FrameMetadataTraceState] = traceState
}
if len(out) == 0 {
return nil
}
return out
}
// CloneFrameMetadata 规范化并拷贝 frame metadata。
func CloneFrameMetadata(metadata map[string]string) map[string]string {
if len(metadata) == 0 {
return nil
}
out := make(map[string]string, len(metadata))
for key, value := range metadata {
key = strings.ToLower(strings.TrimSpace(key))
value = strings.TrimSpace(value)
if key == "" || value == "" {
continue
}
out[key] = value
}
if len(out) == 0 {
return nil
}
return out
}
func encodeFrameMeta(metadata map[string]string) ([]byte, error) {
metadata = CloneFrameMetadata(metadata)
if len(metadata) == 0 {
return nil, nil
}
if len(metadata) > 255 {
return nil, fmt.Errorf("%w: entries=%d", ErrFrameMetadataTooLarge, len(metadata))
}
keys := make([]string, 0, len(metadata))
for key := range metadata {
keys = append(keys, key)
}
sort.Strings(keys)
out := []byte{frameMetadataMagic[0], frameMetadataMagic[1], frameMetadataVersion, byte(len(keys))} //nolint:gosec // G115:metadata 条目数已限制在 255 以内。
for _, key := range keys {
value := metadata[key]
if len(key) > 255 || len(value) > 255 {
return nil, fmt.Errorf("%w: %s", ErrFrameMetadataTooLarge, key)
}
out = append(out, byte(len(key)), byte(len(value))) //nolint:gosec // G115:key/value 长度已限制在 255 以内。
out = append(out, key...)
out = append(out, value...)
if FrameHeaderSizeV1+len(out) > frameMaxHeaderSize {
return nil, fmt.Errorf("%w: header_size=%d", ErrFrameMetadataTooLarge, FrameHeaderSizeV1+len(out))
}
}
return out, nil
}
func decodeFrameMeta(extra []byte) (map[string]string, error) {
if len(extra) == 0 {
return nil, nil
}
if len(extra) < 4 || string(extra[:2]) != frameMetadataMagic {
return nil, ErrFrameMetadataInvalid
}
if extra[2] != frameMetadataVersion {
return nil, fmt.Errorf("%w: version=%d", ErrFrameMetadataInvalid, extra[2])
}
count := int(extra[3])
pos := 4
out := make(map[string]string, count)
for idx := 0; idx < count; idx++ {
if pos+2 > len(extra) {
return nil, ErrFrameMetadataInvalid
}
keyLen := int(extra[pos])
valueLen := int(extra[pos+1])
pos += 2
if keyLen == 0 || valueLen == 0 || pos+keyLen+valueLen > len(extra) {
return nil, ErrFrameMetadataInvalid
}
key := strings.ToLower(strings.TrimSpace(string(extra[pos : pos+keyLen])))
pos += keyLen
value := strings.TrimSpace(string(extra[pos : pos+valueLen]))
pos += valueLen
if key == "" || value == "" {
return nil, ErrFrameMetadataInvalid
}
out[key] = value
}
if pos != len(extra) {
return nil, ErrFrameMetadataInvalid
}
return out, nil
}
+341
View File
@@ -0,0 +1,341 @@
package protocol
import (
"crypto/aes"
"crypto/cipher"
"crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"sync"
)
// frame 加密参数使用 AES-GCM 推荐的 32 字节 key 和 12 字节 nonce。
const (
FrameSessionKeySize = 32
FrameNonceSize = 12
)
// frame 加密和重放保护错误。
var (
ErrFrameEncryptionKeyInvalid = errors.New("protocol frame encryption key is invalid")
ErrFrameEncryptedPayload = errors.New("protocol frame encrypted payload is invalid")
ErrFrameNonceReplay = errors.New("protocol frame nonce replay detected")
ErrFrameReplayGuardFull = errors.New("protocol frame replay guard is full")
)
// DeriveFrameSessionKey 从主密钥和会话 ID 派生 frame 会话密钥。
func DeriveFrameSessionKey(masterSecret, sessionID string) ([]byte, error) {
if masterSecret == "" || sessionID == "" {
return nil, ErrFrameEncryptionKeyInvalid
}
key, err := hkdf.Key(sha256.New, []byte(masterSecret), []byte(sessionID), "longheng:frame-session-key:v1", FrameSessionKeySize)
if err != nil {
return nil, err
}
return key, nil
}
// EncryptFrame 加密 frame body,并把 nonce 放入密文前缀。
func EncryptFrame(frame Frame, key []byte, random io.Reader) (Frame, error) {
if frame.Version == 0 {
// 加密 AAD 必须与最终写出的帧头一致;WriteFrame 会把 0 规范成 FrameVersion1。
frame.Version = FrameVersion1
}
if frame.Version != FrameVersion1 {
return Frame{}, fmt.Errorf("%w: %d", ErrInvalidFrameVersion, frame.Version)
}
aead, err := frameAEAD(key)
if err != nil {
return Frame{}, err
}
if random == nil {
random = rand.Reader
}
nonce := make([]byte, FrameNonceSize)
binary.BigEndian.PutUint64(nonce[:8], frame.Sequence)
if _, err := io.ReadFull(random, nonce[8:]); err != nil {
return Frame{}, err
}
out := frame
out.Flags |= FrameFlagEncrypted
aad, err := frameAAD(frame)
if err != nil {
return Frame{}, err
}
out.Body = aead.Seal(nonce, nonce, frame.Body, aad)
out.Checksum = 0
return out, nil
}
// WriteEncryptedFrame 加密后写出 frame_v1。
func WriteEncryptedFrame(w io.Writer, frame Frame, key []byte, random io.Reader) error {
encrypted, err := EncryptFrame(frame, key, random)
if err != nil {
return err
}
return WriteFrame(w, encrypted)
}
// DecryptFrame 解密 frame body 并执行可选 nonce 重放检查。
func DecryptFrame(frame Frame, key []byte, replay *FrameReplayGuard) (Frame, error) {
if frame.Flags&FrameFlagEncrypted == 0 {
return frame, nil
}
aead, err := frameAEAD(key)
if err != nil {
return Frame{}, err
}
if len(frame.Body) < FrameNonceSize+aead.Overhead() {
return Frame{}, ErrFrameEncryptedPayload
}
nonce := append([]byte(nil), frame.Body[:FrameNonceSize]...)
aad, err := frameAAD(frame)
if err != nil {
return Frame{}, err
}
plain, err := aead.Open(nil, nonce, frame.Body[FrameNonceSize:], aad)
if err != nil {
return Frame{}, fmt.Errorf("%w: %w", ErrFrameEncryptedPayload, err)
}
if replay != nil {
if err := replay.Seen(nonce); err != nil {
return Frame{}, err
}
}
out := frame
out.Flags &^= FrameFlagEncrypted
out.Body = plain
out.Checksum = 0
return out, nil
}
// ReadEncryptedFrame 读取 frame_v1 后立即解密。
func ReadEncryptedFrame(r io.Reader, maxBodySize uint32, key []byte, replay *FrameReplayGuard) (Frame, error) {
frame, err := ReadFrame(r, maxBodySize)
if err != nil {
return Frame{}, err
}
return DecryptFrame(frame, key, replay)
}
// EncryptEnvelope 把 envelope 按 frame_v1 规则加密。
func EncryptEnvelope(envelope Envelope, key []byte, random io.Reader) (Envelope, error) {
if NormalizeWireFormat(envelope.Format) != WireFormatFrameV1 {
return Envelope{}, ErrInvalidWireFormat
}
frame := Frame{
Version: envelope.Frame.Version,
Flags: envelope.Frame.Flags,
Codec: envelope.Frame.Codec,
PacketID: envelope.Packet.PacketID,
MsgID: envelope.Packet.MsgID,
Sequence: envelope.Frame.Sequence,
Body: append([]byte(nil), envelope.Packet.Body...),
Metadata: CloneFrameMetadata(envelope.Frame.Metadata),
}
if envelope.Packet.Compressed {
frame.Flags |= FrameFlagCompressed
}
encrypted, err := EncryptFrame(frame, key, random)
if err != nil {
return Envelope{}, err
}
return Envelope{
Format: WireFormatFrameV1,
Packet: packetFromFrame(encrypted),
Frame: encrypted,
}, nil
}
// DecryptEnvelope 解密 frame_v1 envelope,旧 packet envelope 直接透传。
func DecryptEnvelope(envelope Envelope, key []byte, replay *FrameReplayGuard) (Envelope, error) {
switch NormalizeWireFormat(envelope.Format) {
case WireFormatPacket:
return envelope, nil
case WireFormatFrameV1:
default:
return Envelope{}, ErrInvalidWireFormat
}
if envelope.Frame.Flags&FrameFlagEncrypted == 0 {
return envelope, nil
}
decrypted, err := DecryptFrame(envelope.Frame, key, replay)
if err != nil {
return Envelope{}, err
}
return Envelope{
Format: WireFormatFrameV1,
Packet: packetFromFrame(decrypted),
Frame: decrypted,
}, nil
}
// FrameReplayGuard 在有界序号窗口内精确记录 nonce,防止同一会话内加密帧重放。
//
// nonce 前 8 字节是大端 frame sequence,后 4 字节是随机后缀。同一 sequence
// 可能因响应和 push 编号空间重叠而出现不同 nonce,因此窗口内按完整 nonce
// 去重;一旦 sequence 滑出窗口,其任何 nonce 都永久按重放拒绝。
type FrameReplayGuard struct {
mu sync.Mutex
max int
initialized bool
highest uint64
total int
seen map[[FrameNonceSize]byte]struct{}
slots []frameReplaySlot
}
type frameReplaySlot struct {
sequence uint64
active bool
nonces [][FrameNonceSize]byte
}
// NewFrameReplayGuard 创建固定序号窗口和 nonce 容量的重放保护器。
func NewFrameReplayGuard(maxEntries int) *FrameReplayGuard {
if maxEntries <= 0 {
maxEntries = 4096
}
return &FrameReplayGuard{
max: maxEntries,
seen: make(map[[FrameNonceSize]byte]struct{}, maxEntries),
slots: make([]frameReplaySlot, maxEntries),
}
}
// Seen 记录 nonce;重复 nonce 或已滑出窗口的旧序号会返回重放错误。
func (g *FrameReplayGuard) Seen(nonce []byte) error {
if g == nil {
return ErrFrameEncryptedPayload
}
if len(nonce) != FrameNonceSize {
return ErrFrameEncryptedPayload
}
var key [FrameNonceSize]byte
copy(key[:], nonce)
sequence := binary.BigEndian.Uint64(key[:8])
g.mu.Lock()
defer g.mu.Unlock()
g.ensureReady()
window := replayWindow(g.max)
if g.initialized && sequence <= g.highest && g.highest-sequence >= window {
return ErrFrameNonceReplay
}
if !g.initialized {
g.initialized = true
g.highest = sequence
} else if sequence > g.highest {
g.advance(sequence)
}
if _, ok := g.seen[key]; ok {
return ErrFrameNonceReplay
}
if g.total >= g.max {
return ErrFrameReplayGuardFull
}
slot := &g.slots[sequence%window]
if slot.active && slot.sequence != sequence {
g.clearSlot(slot)
}
if !slot.active {
slot.sequence = sequence
slot.active = true
}
slot.nonces = append(slot.nonces, key)
g.seen[key] = struct{}{}
g.total++
return nil
}
func (g *FrameReplayGuard) ensureReady() {
if g.max <= 0 {
g.max = 4096
}
if g.seen == nil {
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
}
if len(g.slots) != g.max {
g.slots = make([]frameReplaySlot, g.max)
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
g.initialized = false
g.highest = 0
g.total = 0
}
}
func (g *FrameReplayGuard) advance(sequence uint64) {
delta := sequence - g.highest
window := replayWindow(g.max)
if delta >= window {
g.slots = make([]frameReplaySlot, g.max)
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
g.total = 0
g.highest = sequence
return
}
start := g.highest % window
for step := uint64(1); step <= delta; step++ {
g.clearSlot(&g.slots[(start+step)%window])
}
g.highest = sequence
}
func replayWindow(maxEntries int) uint64 {
if maxEntries <= 0 {
return 4096
}
// int 的正值范围始终可由 uint64 表示;这里集中转换,避免窗口运算散落窄化风险。
return uint64(maxEntries) // #nosec G115 -- maxEntries 已验证为正数,转换不会溢出。
}
func (g *FrameReplayGuard) clearSlot(slot *frameReplaySlot) {
if slot == nil || !slot.active {
return
}
for _, nonce := range slot.nonces {
delete(g.seen, nonce)
g.total--
}
if g.total < 0 {
g.total = 0
}
*slot = frameReplaySlot{}
}
func frameAEAD(key []byte) (cipher.AEAD, error) {
if len(key) != FrameSessionKeySize {
return nil, ErrFrameEncryptionKeyInvalid
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
func frameAAD(frame Frame) ([]byte, error) {
var aad [21]byte
aad[0] = frame.Version
binary.BigEndian.PutUint16(aad[1:3], frame.Flags&^FrameFlagEncrypted)
binary.BigEndian.PutUint16(aad[3:5], frame.Codec)
binary.BigEndian.PutUint32(aad[5:9], packetIDToWire(frame.PacketID))
binary.BigEndian.PutUint32(aad[9:13], frame.MsgID)
binary.BigEndian.PutUint64(aad[13:21], frame.Sequence)
metadata, err := encodeFrameMeta(frame.Metadata)
if err != nil {
return nil, err
}
if len(metadata) == 0 {
return aad[:], nil
}
out := make([]byte, 0, len(aad)+len(metadata))
out = append(out, aad[:]...)
out = append(out, metadata...)
return out, nil
}
+180
View File
@@ -0,0 +1,180 @@
package protocol
import (
"math"
"google.golang.org/protobuf/encoding/protowire"
)
// 示例游戏协议用于在通用蓝图中保留示例消息 ID 和响应流程,
// 避免继承旧项目生成的 protobuf 类型。真实游戏应替换 configs/protocol/game.json 并重新生成。
//go:generate go run ../../cmd/protogen -schema ../../configs/protocol/game.json -out game_schema_gen.go
// 示例 packet id 保留最小登录、请求响应、心跳和聊天流程。
const (
PacketIDContent = int32(0)
PacketIDReqResp = int32(1)
PacketIDPingPong = int32(2)
PacketIDChat = int32(5)
)
// OnConnectionRequest 是示例 on_connection 请求的轻量解码结果。
type OnConnectionRequest struct {
RawBody []byte
PassthroughID string
}
// OnConnectionResponse 是示例 on_connection 响应。
type OnConnectionResponse struct {
PassthroughID string
HasRole bool
ShardID int32
ServerTime int64
CurBundleVer string
}
// DecodeOnConnectionRequest 从示例请求体提取 passthrough id。
func DecodeOnConnectionRequest(body []byte) OnConnectionRequest {
passthroughID := extractPassID(body)
return OnConnectionRequest{
RawBody: append([]byte(nil), body...),
PassthroughID: passthroughID,
}
}
// EncodeOnConnectionResponse 编码示例 on_connection 响应。
func EncodeOnConnectionResponse(resp OnConnectionResponse) []byte {
respBody := make([]byte, 0, 64)
respBody = appendStringField(respBody, 1, resp.PassthroughID)
respBody = appendVarintField(respBody, 3, 0)
respBody = appendVarintField(respBody, 7, 0)
respBody = appendInt64Varint(respBody, 12, resp.ServerTime)
body := make([]byte, 0, 96)
body = appendBytesField(body, 1, respBody)
body = appendBoolField(body, 2, resp.HasRole)
body = appendI32VarintField(body, 3, resp.ShardID)
body = appendStringField(body, 4, resp.CurBundleVer)
return body
}
func appendTag(dst []byte, fieldNumber int, typ protowire.Type) []byte {
if fieldNumber <= 0 {
return dst
}
return protowire.AppendTag(dst, protowire.Number(fieldNumber), typ) //nolint:gosec // G115:协议字段号由 schema/generator 固定为正数。
}
func appendVarintField(dst []byte, fieldNumber int, value uint64) []byte {
dst = appendTag(dst, fieldNumber, protowire.VarintType)
return protowire.AppendVarint(dst, value)
}
func appendIntVarintField(dst []byte, fieldNumber int, value int) []byte {
return appendInt64Varint(dst, fieldNumber, int64(value))
}
func appendI32VarintField(dst []byte, fieldNumber int, value int32) []byte {
return appendInt64Varint(dst, fieldNumber, int64(value))
}
func appendInt64Varint(dst []byte, fieldNumber int, value int64) []byte {
// protobuf varint 对 signed int 兼容旧协议的补码写法,不能改成 zigzag。
return appendVarintField(dst, fieldNumber, uint64(value)) //nolint:gosec // G115:协议线格式要求保留 int64 的补码位模式。
}
func intFromVarint(value uint64) int {
signed := int64FromVarint(value)
if signed > int64(math.MaxInt) {
return math.MaxInt
}
if signed < int64(math.MinInt) {
return math.MinInt
}
return int(signed) //nolint:gosec // G115:前面已按当前平台 int 范围钳制。
}
func int32FromVarint(value uint64) int32 {
signed := int64FromVarint(value)
if signed > math.MaxInt32 {
return math.MaxInt32
}
if signed < math.MinInt32 {
return math.MinInt32
}
return int32(signed) //nolint:gosec // G115:前面已按 int32 范围钳制。
}
func int64FromVarint(value uint64) int64 {
return int64(value) //nolint:gosec // G115:协议线格式要求按补码恢复 int64。
}
func appendBoolField(dst []byte, fieldNumber int, value bool) []byte {
if value {
return appendVarintField(dst, fieldNumber, 1)
}
return appendVarintField(dst, fieldNumber, 0)
}
func appendStringField(dst []byte, fieldNumber int, value string) []byte {
dst = appendTag(dst, fieldNumber, protowire.BytesType)
return protowire.AppendString(dst, value)
}
func appendBytesField(dst []byte, fieldNumber int, value []byte) []byte {
dst = appendTag(dst, fieldNumber, protowire.BytesType)
return protowire.AppendBytes(dst, value)
}
func extractPassID(body []byte) string {
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return ""
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.BytesType:
reqBytes, n := protowire.ConsumeBytes(body)
if n < 0 {
return ""
}
return extractPassthroughID(reqBytes)
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return ""
}
body = body[n:]
}
}
return ""
}
func extractPassthroughID(body []byte) string {
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return ""
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.BytesType:
value, n := protowire.ConsumeString(body)
if n < 0 {
return ""
}
return value
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return ""
}
body = body[n:]
}
}
return ""
}
+486
View File
@@ -0,0 +1,486 @@
package protocol
// Code generated by protogen; DO NOT EDIT.
import "google.golang.org/protobuf/encoding/protowire"
const (
MessageOnConnection = uint32(1)
MessageEnterWorld = uint32(2)
MessageAddCurrency = uint32(10001)
MessageReadMail = uint32(10002)
MessagePushCreateProfile = uint32(30141)
MessageNoticePush = uint32(30142)
)
type MessageDescriptor struct {
MsgID uint32 `json:"msg_id"`
Name string `json:"name"`
Constant string `json:"constant"`
Description string `json:"description,omitempty"`
}
var genMsgDescs = []MessageDescriptor{
{MsgID: MessageOnConnection, Name: "OnConnection", Constant: "MessageOnConnection", Description: ""},
{MsgID: MessageEnterWorld, Name: "EnterWorld", Constant: "MessageEnterWorld", Description: ""},
{MsgID: MessageAddCurrency, Name: "AddCurrency", Constant: "MessageAddCurrency", Description: ""},
{MsgID: MessageReadMail, Name: "ReadMail", Constant: "MessageReadMail", Description: ""},
{MsgID: MessagePushCreateProfile, Name: "PushCreateProfile", Constant: "MessagePushCreateProfile", Description: ""},
{MsgID: MessageNoticePush, Name: "NoticePush", Constant: "MessageNoticePush", Description: ""},
}
func MessageDescriptors() []MessageDescriptor {
return append([]MessageDescriptor(nil), genMsgDescs...)
}
func MessageName(msgID uint32) (string, bool) {
switch msgID {
case MessageOnConnection:
return "OnConnection", true
case MessageEnterWorld:
return "EnterWorld", true
case MessageAddCurrency:
return "AddCurrency", true
case MessageReadMail:
return "ReadMail", true
case MessagePushCreateProfile:
return "PushCreateProfile", true
case MessageNoticePush:
return "NoticePush", true
default:
return "", false
}
}
type CreateProfilePush struct {
}
type NoticePush struct {
NoticeID string
DeliveryID string
Title string
Body string
AttachmentRef string
CreatedAt int64
}
type EnterWorldResponse struct {
AccountID string
RoleID string
Name string
Level int
ServerTime int64
Gold int64
Diamond int64
InventoryCount int
QuestCount int
UnreadMail int
}
type AddCurrencyRequest struct {
Currency string
Delta int64
}
type AddCurrencyResponse struct {
AccountID string
Currency string
Balance int64
ServerTime int64
}
type ReadMailRequest struct {
MailID string
}
type ReadMailResponse struct {
AccountID string
MailID string
Changed bool
UnreadMail int
ServerTime int64
}
func EncodeCreateProfilePush(value CreateProfilePush) []byte {
return []byte{}
}
func EncodeNoticePush(value NoticePush) []byte {
body := make([]byte, 0, 88)
body = appendStringField(body, 1, value.NoticeID)
body = appendStringField(body, 2, value.DeliveryID)
body = appendStringField(body, 3, value.Title)
body = appendStringField(body, 4, value.Body)
body = appendStringField(body, 5, value.AttachmentRef)
body = appendInt64Varint(body, 6, value.CreatedAt)
return body
}
func EncodeEnterWorldResponse(value EnterWorldResponse) []byte {
body := make([]byte, 0, 104)
body = appendStringField(body, 1, value.AccountID)
body = appendStringField(body, 2, value.RoleID)
body = appendStringField(body, 3, value.Name)
body = appendIntVarintField(body, 4, value.Level)
body = appendInt64Varint(body, 5, value.ServerTime)
body = appendInt64Varint(body, 6, value.Gold)
body = appendInt64Varint(body, 7, value.Diamond)
body = appendIntVarintField(body, 8, value.InventoryCount)
body = appendIntVarintField(body, 9, value.QuestCount)
body = appendIntVarintField(body, 10, value.UnreadMail)
return body
}
func EncodeAddCurrencyRequest(value AddCurrencyRequest) []byte {
body := make([]byte, 0, 24)
body = appendStringField(body, 1, value.Currency)
body = appendInt64Varint(body, 2, value.Delta)
return body
}
func EncodeAddCurrencyResponse(value AddCurrencyResponse) []byte {
body := make([]byte, 0, 48)
body = appendStringField(body, 1, value.AccountID)
body = appendStringField(body, 2, value.Currency)
body = appendInt64Varint(body, 3, value.Balance)
body = appendInt64Varint(body, 4, value.ServerTime)
return body
}
func EncodeReadMailRequest(value ReadMailRequest) []byte {
body := make([]byte, 0, 16)
body = appendStringField(body, 1, value.MailID)
return body
}
func EncodeReadMailResponse(value ReadMailResponse) []byte {
body := make([]byte, 0, 56)
body = appendStringField(body, 1, value.AccountID)
body = appendStringField(body, 2, value.MailID)
body = appendBoolField(body, 3, value.Changed)
body = appendIntVarintField(body, 4, value.UnreadMail)
body = appendInt64Varint(body, 5, value.ServerTime)
return body
}
func DecodeNoticePush(body []byte) NoticePush {
var value NoticePush
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return value
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.NoticeID = fieldValue
body = body[n:]
case fieldNumber == 2 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.DeliveryID = fieldValue
body = body[n:]
case fieldNumber == 3 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.Title = fieldValue
body = body[n:]
case fieldNumber == 4 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.Body = fieldValue
body = body[n:]
case fieldNumber == 5 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.AttachmentRef = fieldValue
body = body[n:]
case fieldNumber == 6 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.CreatedAt = int64FromVarint(fieldValue)
body = body[n:]
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return value
}
body = body[n:]
}
}
return value
}
func DecodeEnterWorldResponse(body []byte) EnterWorldResponse {
var value EnterWorldResponse
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return value
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.AccountID = fieldValue
body = body[n:]
case fieldNumber == 2 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.RoleID = fieldValue
body = body[n:]
case fieldNumber == 3 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.Name = fieldValue
body = body[n:]
case fieldNumber == 4 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.Level = intFromVarint(fieldValue)
body = body[n:]
case fieldNumber == 5 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.ServerTime = int64FromVarint(fieldValue)
body = body[n:]
case fieldNumber == 6 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.Gold = int64FromVarint(fieldValue)
body = body[n:]
case fieldNumber == 7 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.Diamond = int64FromVarint(fieldValue)
body = body[n:]
case fieldNumber == 8 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.InventoryCount = intFromVarint(fieldValue)
body = body[n:]
case fieldNumber == 9 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.QuestCount = intFromVarint(fieldValue)
body = body[n:]
case fieldNumber == 10 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.UnreadMail = intFromVarint(fieldValue)
body = body[n:]
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return value
}
body = body[n:]
}
}
return value
}
func DecodeAddCurrencyRequest(body []byte) AddCurrencyRequest {
var value AddCurrencyRequest
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return value
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.Currency = fieldValue
body = body[n:]
case fieldNumber == 2 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.Delta = int64FromVarint(fieldValue)
body = body[n:]
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return value
}
body = body[n:]
}
}
return value
}
func DecodeAddCurrencyResponse(body []byte) AddCurrencyResponse {
var value AddCurrencyResponse
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return value
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.AccountID = fieldValue
body = body[n:]
case fieldNumber == 2 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.Currency = fieldValue
body = body[n:]
case fieldNumber == 3 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.Balance = int64FromVarint(fieldValue)
body = body[n:]
case fieldNumber == 4 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.ServerTime = int64FromVarint(fieldValue)
body = body[n:]
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return value
}
body = body[n:]
}
}
return value
}
func DecodeReadMailRequest(body []byte) ReadMailRequest {
var value ReadMailRequest
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return value
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.MailID = fieldValue
body = body[n:]
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return value
}
body = body[n:]
}
}
return value
}
func DecodeReadMailResponse(body []byte) ReadMailResponse {
var value ReadMailResponse
for len(body) > 0 {
fieldNumber, typ, consumed := protowire.ConsumeTag(body)
if consumed < 0 {
return value
}
body = body[consumed:]
switch {
case fieldNumber == 1 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.AccountID = fieldValue
body = body[n:]
case fieldNumber == 2 && typ == protowire.BytesType:
fieldValue, n := protowire.ConsumeString(body)
if n < 0 {
return value
}
value.MailID = fieldValue
body = body[n:]
case fieldNumber == 3 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.Changed = fieldValue != 0
body = body[n:]
case fieldNumber == 4 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.UnreadMail = intFromVarint(fieldValue)
body = body[n:]
case fieldNumber == 5 && typ == protowire.VarintType:
fieldValue, n := protowire.ConsumeVarint(body)
if n < 0 {
return value
}
value.ServerTime = int64FromVarint(fieldValue)
body = body[n:]
default:
n := protowire.ConsumeFieldValue(fieldNumber, typ, body)
if n < 0 {
return value
}
body = body[n:]
}
}
return value
}
+172
View File
@@ -0,0 +1,172 @@
package protocol
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
)
// MaxPacketBodySize 是旧 packet 协议默认 body 上限。
const MaxPacketBodySize uint32 = 1024 * 1024
// 旧 packet 协议解析错误。
var (
ErrPacketBodyTooLarge = errors.New("protocol packet body is too large")
ErrInvalidPacketFlag = errors.New("protocol packet flag is invalid")
)
// Packet 是旧 gateway packet 线协议的内存表示。
type Packet struct {
PacketID int32
MsgID uint32
Compressed bool
Body []byte
}
// ReadPacket 从 reader 读取旧 packet。
func ReadPacket(r io.Reader, maxBodySize uint32) (Packet, error) {
maxBodySize = normMaxPacketBody(maxBodySize)
var header [13]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return Packet{}, err
}
bodySize := binary.LittleEndian.Uint32(header[0:4])
if bodySize > maxBodySize {
return Packet{}, fmt.Errorf("%w: %d", ErrPacketBodyTooLarge, bodySize)
}
body := make([]byte, bodySize)
if bodySize > 0 {
if _, err := io.ReadFull(r, body); err != nil {
return Packet{}, err
}
}
compressed, err := decodeCompressedFlag(header[12])
if err != nil {
return Packet{}, err
}
return Packet{
PacketID: packetIDFromWire(binary.LittleEndian.Uint32(header[4:8])),
MsgID: binary.LittleEndian.Uint32(header[8:12]),
Compressed: compressed,
Body: body,
}, nil
}
// WritePacket 把旧 packet 写入 writer。
func WritePacket(w io.Writer, pkt Packet) error {
bodySize := len(pkt.Body)
if bodySize > int(MaxPacketBodySize) || uint64(bodySize) > uint64(^uint32(0)) {
return fmt.Errorf("%w: %d", ErrPacketBodyTooLarge, bodySize)
}
var header [13]byte
binary.LittleEndian.PutUint32(header[0:4], uint32(bodySize))
binary.LittleEndian.PutUint32(header[4:8], packetIDToWire(pkt.PacketID))
binary.LittleEndian.PutUint32(header[8:12], pkt.MsgID)
if pkt.Compressed {
header[12] = 1
}
if bodySize == 0 {
return writeFull(w, header[:])
}
return writeFullBuffers(w, header[:], pkt.Body)
}
func normMaxPacketBody(maxBodySize uint32) uint32 {
if maxBodySize == 0 {
return MaxPacketBodySize
}
return maxBodySize
}
func decodeCompressedFlag(flag byte) (bool, error) {
switch flag {
case 0:
return false, nil
case 1:
return true, nil
default:
return false, fmt.Errorf("%w: compressed=%d", ErrInvalidPacketFlag, flag)
}
}
func writeFull(w io.Writer, data []byte) error {
for len(data) > 0 {
n, err := w.Write(data)
if err != nil {
return err
}
if n <= 0 {
return io.ErrShortWrite
}
data = data[n:]
}
return nil
}
func writeFullBuffers(w io.Writer, parts ...[]byte) error {
total := 0
var scratch [4][]byte
buffers := net.Buffers(scratch[:0])
for _, part := range parts {
if len(part) == 0 {
continue
}
total += len(part)
buffers = append(buffers, part)
}
if total == 0 {
return nil
}
if _, ok := w.(net.Conn); !ok {
return writeFullParts(w, parts...)
}
written, err := buffers.WriteTo(w)
if err != nil {
return err
}
if written == int64(total) {
return nil
}
if written < 0 || written > int64(total) {
return io.ErrShortWrite
}
return writeBufRemainder(w, int(written), parts...)
}
func writeFullParts(w io.Writer, parts ...[]byte) error {
for _, part := range parts {
if len(part) == 0 {
continue
}
if err := writeFull(w, part); err != nil {
return err
}
}
return nil
}
func writeBufRemainder(w io.Writer, written int, parts ...[]byte) error {
for _, part := range parts {
if len(part) == 0 {
continue
}
if written >= len(part) {
written -= len(part)
continue
}
if written > 0 {
part = part[written:]
written = 0
}
if err := writeFull(w, part); err != nil {
return err
}
}
return nil
}
+7
View File
@@ -0,0 +1,7 @@
package protocol
import "fmt"
func formatUnix(t interface{ Unix() int64 }) string {
return fmt.Sprintf("%d", t.Unix())
}
+105
View File
@@ -0,0 +1,105 @@
package protocol
import (
"bufio"
"encoding/binary"
"errors"
"io"
"strings"
)
// 支持的 gateway wire format 名称。
const (
WireFormatPacket = "packet"
WireFormatFrameV1 = "frame_v1"
)
// ErrInvalidWireFormat 表示 envelope 使用了不支持的线协议格式。
var ErrInvalidWireFormat = errors.New("protocol wire format is invalid")
// Envelope 是 packet 与 frame_v1 的统一传输容器。
type Envelope struct {
Format string
Packet Packet
Frame Frame
}
// ReadEnvelope 自动探测并读取 packet 或 frame_v1。
func ReadEnvelope(r *bufio.Reader, maxBodySize uint32) (Envelope, error) {
if r == nil {
return Envelope{}, io.ErrUnexpectedEOF
}
peek, err := r.Peek(4)
if err != nil {
return Envelope{}, err
}
// 读侧兼容两种线协议:先探测 frame_v1 magic,未命中则退回旧 packet 格式,方便客户端灰度升级。
if binary.LittleEndian.Uint16(peek[0:2]) == FrameMagic &&
peek[2] == FrameVersion1 &&
int(peek[3]) >= FrameHeaderSizeV1 {
frame, err := ReadFrame(r, maxBodySize)
if err != nil {
return Envelope{}, err
}
return Envelope{
Format: WireFormatFrameV1,
Packet: packetFromFrame(frame),
Frame: frame,
}, nil
}
packet, err := ReadPacket(r, maxBodySize)
if err != nil {
return Envelope{}, err
}
return Envelope{
Format: WireFormatPacket,
Packet: packet,
}, nil
}
// WriteEnvelope 按指定 wire format 写出 envelope。
func WriteEnvelope(w io.Writer, envelope Envelope) error {
format := strings.TrimSpace(envelope.Format)
if format == "" {
format = WireFormatPacket
} else {
format = NormalizeWireFormat(format)
}
switch format {
case WireFormatPacket:
return WritePacket(w, envelope.Packet)
case WireFormatFrameV1:
// frame_v1 的压缩标记以 Packet.Compressed 为准,避免调用方同时维护 Packet 和 Frame 两份状态时产生漂移。
flags := envelope.Frame.Flags &^ FrameFlagCompressed
if envelope.Packet.Compressed {
flags |= FrameFlagCompressed
}
frame := Frame{
Version: envelope.Frame.Version,
Flags: flags,
Codec: envelope.Frame.Codec,
PacketID: envelope.Packet.PacketID,
MsgID: envelope.Packet.MsgID,
Sequence: envelope.Frame.Sequence,
Body: envelope.Packet.Body,
Metadata: CloneFrameMetadata(envelope.Frame.Metadata),
}
return WriteFrame(w, frame)
default:
return ErrInvalidWireFormat
}
}
// NormalizeWireFormat 归一化 wire format 文本。
func NormalizeWireFormat(format string) string {
switch strings.ToLower(strings.TrimSpace(format)) {
case "":
return WireFormatPacket
case WireFormatPacket:
return WireFormatPacket
case WireFormatFrameV1:
return WireFormatFrameV1
default:
return ""
}
}