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

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

205 lines
6.6 KiB
Go
Raw Permalink 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 rpc
import (
"context"
"errors"
"fmt"
"strings"
"sync/atomic"
"longheng.io/server/pkg/contracts"
)
// RequireMode 描述对端没有签名时是否允许通过;签名"错误"始终拒绝。
//
// RequireAll 缺签名或签名错误都拒绝。生产推荐。
// RequirePresent 缺签名放行(灰度),签名错误仍拒绝。仅用于"双方还没全部接入鉴权"
// 的滚动升级窗口。完成滚动后应立刻切到 RequireAll。
type RequireMode string
const (
// RequireAll 表示缺签名和签名错误都拒绝,是生产推荐模式。
RequireAll RequireMode = "all"
// RequirePresent 表示缺签名暂时放行,但签名错误仍拒绝。
RequirePresent RequireMode = "present"
)
// AuthTransportMetrics 是 AuthenticatedTransport 的指标钩子。所有字段都是
// 64-bit counter,调用方可以原子读取作为 Prometheus / scope 输出。
// 调用方可以 nil,AuthenticatedTransport 自己持有一个匿名实例做内部统计。
type AuthTransportMetrics struct {
SignedRequests atomic.Uint64
SignedResponses atomic.Uint64
AcceptedRequests atomic.Uint64
AcceptedResponses atomic.Uint64
RejectedMissing atomic.Uint64
RejectedInvalid atomic.Uint64
RejectedSkew atomic.Uint64
RejectedAlgorithm atomic.Uint64
RejectedReplay atomic.Uint64
RejectedReplayCheck atomic.Uint64
GraceLetThrough atomic.Uint64
SignFailures atomic.Uint64
}
// AuthenticatedTransport 包装任意 Transport,发送侧自动签名,接收侧自动验签。
// 不引入新的拓扑或网络层,对调用方完全透明。
type AuthenticatedTransport struct {
inner Transport
auth *Authenticator
require RequireMode
metrics *AuthTransportMetrics
}
// AuthTransportOptions 是装配参数。Auth 必填;Inner 通常是 BusTransport
// 或 GRPCTransport。Require 缺省 RequireAll。
type AuthTransportOptions struct {
Inner Transport
Auth *Authenticator
Require RequireMode
Metrics *AuthTransportMetrics
}
// NewAuthenticatedTransport 构造一个签名 / 验签包装。inner 或 auth 为 nil 返回错误,
// 拒绝静默退化为"不鉴权"。
func NewAuthenticatedTransport(opts AuthTransportOptions) (*AuthenticatedTransport, error) {
if opts.Inner == nil {
return nil, errors.New("rpcauth: inner transport is required")
}
if opts.Auth == nil {
return nil, errors.New("rpcauth: authenticator is required")
}
require := opts.Require
switch require {
case "":
require = RequireAll
case RequireAll, RequirePresent:
default:
return nil, fmt.Errorf("rpcauth: unsupported require mode %q", require)
}
metrics := opts.Metrics
if metrics == nil {
metrics = &AuthTransportMetrics{}
}
return &AuthenticatedTransport{
inner: opts.Inner,
auth: opts.Auth,
require: require,
metrics: metrics,
}, nil
}
// Metrics 暴露内部计数器,便于 wiring 层挂到 metrics.Scope 上。
func (t *AuthenticatedTransport) Metrics() *AuthTransportMetrics {
if t == nil {
return nil
}
return t.metrics
}
// Require 暴露当前模式,主要用于自检 / 日志。
func (t *AuthenticatedTransport) Require() RequireMode {
if t == nil {
return ""
}
return t.require
}
// SubscribeRequests 订阅请求并在交给业务 handler 前完成验签。
func (t *AuthenticatedTransport) SubscribeRequests(ctx context.Context, nodeID string, handler RequestHandler) (TransportSubscription, error) {
if t == nil {
return nil, errors.New("rpcauth: transport is nil")
}
return t.inner.SubscribeRequests(ctx, nodeID, func(ctx context.Context, req contracts.RPCRequest) error {
if err := t.auth.VerifyRequestContext(ctx, req); err != nil {
t.recordVerifyFailure(err)
if t.shouldAcceptDespite(err) {
return handler(ctx, req)
}
return err
}
t.metrics.AcceptedRequests.Add(1)
return handler(ctx, req)
})
}
// SubscribeResponses 订阅响应并在交给等待方前完成验签。
func (t *AuthenticatedTransport) SubscribeResponses(ctx context.Context, nodeID string, handler ResponseHandler) (TransportSubscription, error) {
if t == nil {
return nil, errors.New("rpcauth: transport is nil")
}
return t.inner.SubscribeResponses(ctx, nodeID, func(ctx context.Context, resp contracts.RPCResponse) error {
if err := t.auth.VerifyResponseContext(ctx, resp); err != nil {
t.recordVerifyFailure(err)
if t.shouldAcceptDespite(err) {
return handler(ctx, resp)
}
return err
}
t.metrics.AcceptedResponses.Add(1)
return handler(ctx, resp)
})
}
// PublishRequest 对请求签名后交给底层 transport 发布。
func (t *AuthenticatedTransport) PublishRequest(ctx context.Context, targetNodeID string, request contracts.RPCRequest) error {
if t == nil {
return errors.New("rpcauth: transport is nil")
}
if err := t.auth.SignRequest(&request); err != nil {
t.metrics.SignFailures.Add(1)
return err
}
t.metrics.SignedRequests.Add(1)
return t.inner.PublishRequest(ctx, targetNodeID, request)
}
// PublishResponse 对响应签名后交给底层 transport 发布。
func (t *AuthenticatedTransport) PublishResponse(ctx context.Context, targetNodeID string, response contracts.RPCResponse) error {
if t == nil {
return errors.New("rpcauth: transport is nil")
}
if err := t.auth.SignResponse(&response); err != nil {
t.metrics.SignFailures.Add(1)
return err
}
t.metrics.SignedResponses.Add(1)
return t.inner.PublishResponse(ctx, targetNodeID, response)
}
// shouldAcceptDespite 区分"可灰度通过"和"必须拒绝"的失败。
// 只有 ErrAuthSignatureMissing 在 RequirePresent 模式下允许放行;
// invalid / skew / algorithm 一律拒绝,无论什么模式。
func (t *AuthenticatedTransport) shouldAcceptDespite(err error) bool {
if t.require != RequirePresent {
return false
}
if !errors.Is(err, ErrAuthSignatureMissing) {
return false
}
t.metrics.GraceLetThrough.Add(1)
return true
}
func (t *AuthenticatedTransport) recordVerifyFailure(err error) {
switch {
case errors.Is(err, ErrAuthSignatureMissing):
t.metrics.RejectedMissing.Add(1)
case errors.Is(err, ErrAuthSignatureInvalid):
t.metrics.RejectedInvalid.Add(1)
case errors.Is(err, ErrAuthTimestampSkew):
t.metrics.RejectedSkew.Add(1)
case errors.Is(err, ErrAuthAlgUnsup):
t.metrics.RejectedAlgorithm.Add(1)
case errors.Is(err, ErrAuthReplayDetected):
t.metrics.RejectedReplay.Add(1)
case errors.Is(err, ErrAuthReplayCheckFailed):
t.metrics.RejectedReplayCheck.Add(1)
default:
// 未知错误不分类,但仍然计入 invalid,避免悄悄漏统计。
t.metrics.RejectedInvalid.Add(1)
// 兜底,让调用方在日志里能看到具体类型。
_ = strings.TrimSpace(err.Error())
}
}