DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,791 @@
|
||||
package accountcenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TokenIssuer func(context.Context, Account, Shard, GateNode) (string, error)
|
||||
|
||||
type CenterOptions struct {
|
||||
Now func() time.Time
|
||||
TokenIssuer TokenIssuer
|
||||
Store Store
|
||||
}
|
||||
|
||||
type Center struct {
|
||||
mu sync.Mutex
|
||||
saveMu sync.Mutex
|
||||
now func() time.Time
|
||||
issuer TokenIssuer
|
||||
store Store
|
||||
version uint64
|
||||
nextNum int64
|
||||
accounts map[string]Account
|
||||
identity map[string]string
|
||||
bans map[string]BanRecord
|
||||
allows map[string]AllowRecord
|
||||
banIndex map[string]map[string]struct{}
|
||||
allowIndex map[string]map[string]struct{}
|
||||
shards map[string]Shard
|
||||
gates map[string]GateNode
|
||||
}
|
||||
|
||||
func NewMemoryCenter(options CenterOptions) *Center {
|
||||
center, err := NewCenter(context.Background(), options)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return center
|
||||
}
|
||||
|
||||
func NewCenter(ctx context.Context, options CenterOptions) (*Center, error) {
|
||||
now := options.Now
|
||||
if now == nil {
|
||||
now = func() time.Time { return time.Now().UTC() }
|
||||
}
|
||||
issuer := options.TokenIssuer
|
||||
if issuer == nil {
|
||||
issuer = randomTokenIssuer
|
||||
}
|
||||
center := &Center{
|
||||
now: now,
|
||||
issuer: issuer,
|
||||
store: options.Store,
|
||||
nextNum: 100000,
|
||||
accounts: make(map[string]Account),
|
||||
identity: make(map[string]string),
|
||||
bans: make(map[string]BanRecord),
|
||||
allows: make(map[string]AllowRecord),
|
||||
banIndex: make(map[string]map[string]struct{}),
|
||||
allowIndex: make(map[string]map[string]struct{}),
|
||||
shards: make(map[string]Shard),
|
||||
gates: make(map[string]GateNode),
|
||||
}
|
||||
if options.Store != nil {
|
||||
exported, ok, err := options.Store.Load(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: load account center state: %w", ErrStoreUnavailable, err)
|
||||
}
|
||||
if ok {
|
||||
center.importLocked(exported)
|
||||
}
|
||||
}
|
||||
return center, nil
|
||||
}
|
||||
|
||||
func (c *Center) Login(ctx context.Context, request LoginRequest) (LoginResult, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
identity, err := normalizeIdentity(request.Identity)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
account, created, err := c.findOrCreateAcct(identity)
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return LoginResult{}, err
|
||||
}
|
||||
shard, err := c.selectShardLocked(request.PreferredShardID)
|
||||
if err != nil {
|
||||
if created {
|
||||
c.rollbackCreated(account, identity)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return LoginResult{}, err
|
||||
}
|
||||
if err := c.checkAccessLocked(account.ID, shard.ID, request.RequireAllowList); err != nil {
|
||||
if created {
|
||||
c.rollbackCreated(account, identity)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return LoginResult{}, err
|
||||
}
|
||||
gate, err := c.selectGateLocked(account.ID, shard)
|
||||
if err != nil {
|
||||
if created {
|
||||
c.rollbackCreated(account, identity)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return LoginResult{}, err
|
||||
}
|
||||
if created {
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
if err := c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.rollbackCreated(account, identity)
|
||||
}); err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
} else {
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
}
|
||||
token, err := c.issuer(ctx, account, shard, gate)
|
||||
if err != nil {
|
||||
return LoginResult{}, fmt.Errorf("%w: %w", ErrLoginTokenUnavailable, err)
|
||||
}
|
||||
return LoginResult{
|
||||
Account: account,
|
||||
Shard: shard,
|
||||
Gate: GateSelection{
|
||||
Address: gate.Address,
|
||||
LoginToken: token,
|
||||
NodeID: gate.NodeID,
|
||||
},
|
||||
Created: created,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Center) Bind(ctx context.Context, request BindRequest) (Account, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
identity, err := normalizeIdentity(request.Identity)
|
||||
if err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
accountID := strings.TrimSpace(request.AccountID)
|
||||
if accountID == "" {
|
||||
return Account{}, ErrAccountNotFound
|
||||
}
|
||||
id := identityID(identity)
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
account, ok := c.accounts[accountID]
|
||||
if !ok {
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return Account{}, ErrAccountNotFound
|
||||
}
|
||||
if bound, ok := c.identity[id]; ok && bound != accountID {
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return Account{}, ErrIdentityConflict
|
||||
}
|
||||
if _, ok := c.identity[id]; !ok {
|
||||
previous := cloneAccount(account)
|
||||
account.Bindings = append(account.Bindings, identity)
|
||||
account.UpdatedAt = c.now().UTC()
|
||||
c.accounts[accountID] = cloneAccount(account)
|
||||
c.identity[id] = accountID
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
if err := c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.accounts[accountID] = previous
|
||||
delete(c.identity, id)
|
||||
}); err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
return cloneAccount(account), nil
|
||||
}
|
||||
out := cloneAccount(c.accounts[accountID])
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Center) SetBan(ctx context.Context, record BanRecord) error {
|
||||
subject := strings.TrimSpace(record.Subject)
|
||||
if subject == "" {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
record.Subject = subject
|
||||
record.Scope = normalizeScope(record.Scope)
|
||||
if record.CreatedAt.IsZero() {
|
||||
record.CreatedAt = c.now().UTC()
|
||||
}
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
key := accessKey(record.Subject, record.Scope)
|
||||
previous, hadPrevious := c.bans[key]
|
||||
if hadPrevious {
|
||||
c.unindexAccessLocked(c.banIndex, key, previous.Subject)
|
||||
}
|
||||
c.bans[accessKey(record.Subject, record.Scope)] = cloneBanRecord(record)
|
||||
c.indexAccessLocked(c.banIndex, key, record.Subject)
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
return c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.unindexAccessLocked(c.banIndex, key, record.Subject)
|
||||
if hadPrevious {
|
||||
c.bans[key] = previous
|
||||
c.indexAccessLocked(c.banIndex, key, previous.Subject)
|
||||
} else {
|
||||
delete(c.bans, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Center) SetAllow(ctx context.Context, record AllowRecord) error {
|
||||
subject := strings.TrimSpace(record.Subject)
|
||||
if subject == "" {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
record.Subject = subject
|
||||
record.Scope = normalizeScope(record.Scope)
|
||||
if record.CreatedAt.IsZero() {
|
||||
record.CreatedAt = c.now().UTC()
|
||||
}
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
key := accessKey(record.Subject, record.Scope)
|
||||
previous, hadPrevious := c.allows[key]
|
||||
if hadPrevious {
|
||||
c.unindexAccessLocked(c.allowIndex, key, previous.Subject)
|
||||
}
|
||||
c.allows[key] = cloneAllowRecord(record)
|
||||
c.indexAccessLocked(c.allowIndex, key, record.Subject)
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
return c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.unindexAccessLocked(c.allowIndex, key, record.Subject)
|
||||
if hadPrevious {
|
||||
c.allows[key] = previous
|
||||
c.indexAccessLocked(c.allowIndex, key, previous.Subject)
|
||||
} else {
|
||||
delete(c.allows, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Center) SetShards(ctx context.Context, shards []Shard) error {
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
previous := c.shards
|
||||
c.shards = make(map[string]Shard, len(shards))
|
||||
for _, shard := range shards {
|
||||
shard.ID = strings.TrimSpace(shard.ID)
|
||||
if shard.ID == "" {
|
||||
continue
|
||||
}
|
||||
shard.State = strings.TrimSpace(shard.State)
|
||||
if shard.State == "" {
|
||||
shard.State = ShardOpen
|
||||
}
|
||||
if shard.Weight <= 0 {
|
||||
shard.Weight = 1
|
||||
}
|
||||
c.shards[shard.ID] = shard
|
||||
}
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
return c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.shards = previous
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Center) SetGates(ctx context.Context, gates []GateNode) error {
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
previous := c.gates
|
||||
c.gates = make(map[string]GateNode, len(gates))
|
||||
for _, gate := range gates {
|
||||
gate.NodeID = strings.TrimSpace(gate.NodeID)
|
||||
gate.Address = strings.TrimSpace(gate.Address)
|
||||
if gate.NodeID == "" || gate.Address == "" {
|
||||
continue
|
||||
}
|
||||
if gate.Weight <= 0 {
|
||||
gate.Weight = 1
|
||||
}
|
||||
c.gates[gate.NodeID] = gate
|
||||
}
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
return c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.gates = previous
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Center) Snapshot() map[string]any {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return map[string]any{
|
||||
"accounts": len(c.accounts),
|
||||
"bindings": len(c.identity),
|
||||
"bans": len(c.bans),
|
||||
"allows": len(c.allows),
|
||||
"shards": len(c.shards),
|
||||
"gates": len(c.gates),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Center) GetAccount(ctx context.Context, accountID string) (Account, bool, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return Account{}, false, ErrAccountNotFound
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
account, ok := c.accounts[accountID]
|
||||
if !ok {
|
||||
return Account{}, false, nil
|
||||
}
|
||||
return cloneAccount(account), true, nil
|
||||
}
|
||||
|
||||
func (c *Center) FindAccountByIdentity(ctx context.Context, identity Identity) (Account, bool, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
normalized, err := normalizeIdentity(identity)
|
||||
if err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
accountID, ok := c.identity[identityID(normalized)]
|
||||
if !ok {
|
||||
return Account{}, false, nil
|
||||
}
|
||||
account, ok := c.accounts[accountID]
|
||||
if !ok {
|
||||
return Account{}, false, nil
|
||||
}
|
||||
return cloneAccount(account), true, nil
|
||||
}
|
||||
|
||||
func (c *Center) AccountAccess(ctx context.Context, accountID string) (AccountAccess, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return AccountAccess{}, err
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return AccountAccess{}, ErrAccountNotFound
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
access := AccountAccess{}
|
||||
for key := range c.banIndex[accountID] {
|
||||
if record, ok := c.bans[key]; ok {
|
||||
access.Bans = append(access.Bans, cloneBanRecord(record))
|
||||
}
|
||||
}
|
||||
for key := range c.allowIndex[accountID] {
|
||||
if record, ok := c.allows[key]; ok {
|
||||
access.Allows = append(access.Allows, cloneAllowRecord(record))
|
||||
}
|
||||
}
|
||||
sort.Slice(access.Bans, func(i, j int) bool {
|
||||
return accessKey(access.Bans[i].Subject, access.Bans[i].Scope) < accessKey(access.Bans[j].Subject, access.Bans[j].Scope)
|
||||
})
|
||||
sort.Slice(access.Allows, func(i, j int) bool {
|
||||
return accessKey(access.Allows[i].Subject, access.Allows[i].Scope) < accessKey(access.Allows[j].Subject, access.Allows[j].Scope)
|
||||
})
|
||||
return access, nil
|
||||
}
|
||||
|
||||
func (c *Center) Export() Export {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.exportLocked()
|
||||
}
|
||||
|
||||
func (c *Center) exportLocked() Export {
|
||||
out := Export{
|
||||
Accounts: make([]Account, 0, len(c.accounts)),
|
||||
Bans: make([]BanRecord, 0, len(c.bans)),
|
||||
Allows: make([]AllowRecord, 0, len(c.allows)),
|
||||
Shards: make([]Shard, 0, len(c.shards)),
|
||||
Gates: make([]GateNode, 0, len(c.gates)),
|
||||
}
|
||||
for _, account := range c.accounts {
|
||||
out.Accounts = append(out.Accounts, cloneAccount(account))
|
||||
}
|
||||
for _, ban := range c.bans {
|
||||
out.Bans = append(out.Bans, cloneBanRecord(ban))
|
||||
}
|
||||
for _, allow := range c.allows {
|
||||
out.Allows = append(out.Allows, cloneAllowRecord(allow))
|
||||
}
|
||||
for _, shard := range c.shards {
|
||||
out.Shards = append(out.Shards, shard)
|
||||
}
|
||||
for _, gate := range c.gates {
|
||||
out.Gates = append(out.Gates, gate)
|
||||
}
|
||||
sort.Slice(out.Accounts, func(i, j int) bool { return out.Accounts[i].ID < out.Accounts[j].ID })
|
||||
sort.Slice(out.Bans, func(i, j int) bool {
|
||||
return accessKey(out.Bans[i].Subject, out.Bans[i].Scope) < accessKey(out.Bans[j].Subject, out.Bans[j].Scope)
|
||||
})
|
||||
sort.Slice(out.Allows, func(i, j int) bool {
|
||||
return accessKey(out.Allows[i].Subject, out.Allows[i].Scope) < accessKey(out.Allows[j].Subject, out.Allows[j].Scope)
|
||||
})
|
||||
sort.Slice(out.Shards, func(i, j int) bool { return out.Shards[i].ID < out.Shards[j].ID })
|
||||
sort.Slice(out.Gates, func(i, j int) bool { return out.Gates[i].NodeID < out.Gates[j].NodeID })
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Center) importLocked(exported Export) {
|
||||
c.accounts = make(map[string]Account, len(exported.Accounts))
|
||||
c.identity = make(map[string]string)
|
||||
c.bans = make(map[string]BanRecord, len(exported.Bans))
|
||||
c.allows = make(map[string]AllowRecord, len(exported.Allows))
|
||||
c.banIndex = make(map[string]map[string]struct{})
|
||||
c.allowIndex = make(map[string]map[string]struct{})
|
||||
c.shards = make(map[string]Shard, len(exported.Shards))
|
||||
c.gates = make(map[string]GateNode, len(exported.Gates))
|
||||
c.nextNum = 100000
|
||||
for _, account := range exported.Accounts {
|
||||
account = cloneAccount(account)
|
||||
c.accounts[account.ID] = account
|
||||
if account.NumID > c.nextNum {
|
||||
c.nextNum = account.NumID
|
||||
}
|
||||
for _, identity := range account.Bindings {
|
||||
normalized, err := normalizeIdentity(identity)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
c.identity[identityID(normalized)] = account.ID
|
||||
}
|
||||
}
|
||||
for _, ban := range exported.Bans {
|
||||
ban.Subject = strings.TrimSpace(ban.Subject)
|
||||
if ban.Subject == "" {
|
||||
continue
|
||||
}
|
||||
ban.Scope = normalizeScope(ban.Scope)
|
||||
key := accessKey(ban.Subject, ban.Scope)
|
||||
c.bans[key] = cloneBanRecord(ban)
|
||||
c.indexAccessLocked(c.banIndex, key, ban.Subject)
|
||||
}
|
||||
for _, allow := range exported.Allows {
|
||||
allow.Subject = strings.TrimSpace(allow.Subject)
|
||||
if allow.Subject == "" {
|
||||
continue
|
||||
}
|
||||
allow.Scope = normalizeScope(allow.Scope)
|
||||
key := accessKey(allow.Subject, allow.Scope)
|
||||
c.allows[key] = cloneAllowRecord(allow)
|
||||
c.indexAccessLocked(c.allowIndex, key, allow.Subject)
|
||||
}
|
||||
for _, shard := range exported.Shards {
|
||||
shard.ID = strings.TrimSpace(shard.ID)
|
||||
if shard.ID == "" {
|
||||
continue
|
||||
}
|
||||
if shard.State == "" {
|
||||
shard.State = ShardOpen
|
||||
}
|
||||
c.shards[shard.ID] = shard
|
||||
}
|
||||
for _, gate := range exported.Gates {
|
||||
gate.NodeID = strings.TrimSpace(gate.NodeID)
|
||||
if gate.NodeID == "" {
|
||||
continue
|
||||
}
|
||||
c.gates[gate.NodeID] = gate
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Center) exportForSaveLocked() (Export, uint64) {
|
||||
c.version++
|
||||
return c.exportLocked(), c.version
|
||||
}
|
||||
|
||||
// saveMu 固定先于 mu 获取,避免落盘失败回滚覆盖后续已提交写入。
|
||||
func (c *Center) persistAfterUnlock(ctx context.Context, exported Export, version uint64, rollback func()) error {
|
||||
defer c.saveMu.Unlock()
|
||||
if c.store == nil {
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := c.store.Save(ctx, exported); err != nil {
|
||||
c.mu.Lock()
|
||||
if c.version == version && rollback != nil {
|
||||
rollback()
|
||||
c.version++
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("%w: save account center state: %w", ErrStoreUnavailable, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Center) Close() error {
|
||||
if c == nil || c.store == nil {
|
||||
return nil
|
||||
}
|
||||
c.saveMu.Lock()
|
||||
defer c.saveMu.Unlock()
|
||||
if closer, ok := c.store.(interface{ Close() error }); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Center) findOrCreateAcct(identity Identity) (Account, bool, error) {
|
||||
id := identityID(identity)
|
||||
if accountID, ok := c.identity[id]; ok {
|
||||
return cloneAccount(c.accounts[accountID]), false, nil
|
||||
}
|
||||
now := c.now().UTC()
|
||||
c.nextNum++
|
||||
account := Account{
|
||||
ID: fmt.Sprintf("acc-%d", c.nextNum),
|
||||
NumID: c.nextNum,
|
||||
Bindings: []Identity{identity},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
c.accounts[account.ID] = account
|
||||
c.identity[id] = account.ID
|
||||
return cloneAccount(account), true, nil
|
||||
}
|
||||
|
||||
func (c *Center) rollbackCreated(account Account, identity Identity) {
|
||||
delete(c.accounts, account.ID)
|
||||
delete(c.identity, identityID(identity))
|
||||
if account.NumID == c.nextNum {
|
||||
c.nextNum--
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Center) indexAccessLocked(index map[string]map[string]struct{}, key, subject string) {
|
||||
subject = strings.TrimSpace(subject)
|
||||
if subject == "" {
|
||||
return
|
||||
}
|
||||
items := index[subject]
|
||||
if items == nil {
|
||||
items = make(map[string]struct{})
|
||||
index[subject] = items
|
||||
}
|
||||
items[key] = struct{}{}
|
||||
}
|
||||
|
||||
func (c *Center) unindexAccessLocked(index map[string]map[string]struct{}, key, subject string) {
|
||||
subject = strings.TrimSpace(subject)
|
||||
if subject == "" {
|
||||
return
|
||||
}
|
||||
items := index[subject]
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
delete(items, key)
|
||||
if len(items) == 0 {
|
||||
delete(index, subject)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Center) selectShardLocked(preferred string) (Shard, error) {
|
||||
preferred = strings.TrimSpace(preferred)
|
||||
if preferred != "" {
|
||||
shard, ok := c.shards[preferred]
|
||||
if !ok {
|
||||
return Shard{}, ErrShardNotFound
|
||||
}
|
||||
if shard.State == ShardClosed {
|
||||
return Shard{}, ErrShardClosed
|
||||
}
|
||||
return shard, nil
|
||||
}
|
||||
shards := make([]Shard, 0, len(c.shards))
|
||||
for _, shard := range c.shards {
|
||||
if shard.State != ShardClosed {
|
||||
shards = append(shards, shard)
|
||||
}
|
||||
}
|
||||
if len(shards) == 0 {
|
||||
return Shard{}, ErrShardNotFound
|
||||
}
|
||||
sort.Slice(shards, func(i, j int) bool {
|
||||
if shards[i].Weight != shards[j].Weight {
|
||||
return shards[i].Weight > shards[j].Weight
|
||||
}
|
||||
return shards[i].ID < shards[j].ID
|
||||
})
|
||||
return shards[0], nil
|
||||
}
|
||||
|
||||
func (c *Center) selectGateLocked(accountID string, shard Shard) (GateNode, error) {
|
||||
candidates := make([]GateNode, 0, len(c.gates))
|
||||
for _, gate := range c.gates {
|
||||
if gate.Draining || gate.Address == "" {
|
||||
continue
|
||||
}
|
||||
if shard.GateGroupID != "" && gate.GateGroupID != "" && shard.GateGroupID != gate.GateGroupID {
|
||||
continue
|
||||
}
|
||||
if len(gate.ShardIDs) > 0 && !containsString(gate.ShardIDs, shard.ID) {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, gate)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return GateNode{}, ErrGateUnavailable
|
||||
}
|
||||
sort.Slice(candidates, func(i, j int) bool { return candidates[i].NodeID < candidates[j].NodeID })
|
||||
idx := int(hashString(accountID+"|"+shard.ID) % uint64(len(candidates))) //nolint:gosec // G115:取模结果严格小于 candidates 长度,可安全作为 slice 下标。
|
||||
return candidates[idx], nil
|
||||
}
|
||||
|
||||
func (c *Center) checkAccessLocked(accountID, shardID string, requireAllow bool) error {
|
||||
now := c.now().UTC()
|
||||
for _, scope := range []string{"global", "shard:" + shardID} {
|
||||
if ban, ok := c.bans[accessKey(accountID, scope)]; ok {
|
||||
if ban.Until.IsZero() || ban.Until.After(now) {
|
||||
return ErrAccountBanned
|
||||
}
|
||||
}
|
||||
}
|
||||
if !requireAllow {
|
||||
return nil
|
||||
}
|
||||
if _, ok := c.allows[accessKey(accountID, "global")]; ok {
|
||||
return nil
|
||||
}
|
||||
if _, ok := c.allows[accessKey(accountID, "shard:"+shardID)]; ok {
|
||||
return nil
|
||||
}
|
||||
return ErrAllowListRequired
|
||||
}
|
||||
|
||||
func normalizeIdentity(identity Identity) (Identity, error) {
|
||||
identity.Kind = strings.ToLower(strings.TrimSpace(identity.Kind))
|
||||
identity.Issuer = strings.TrimSpace(identity.Issuer)
|
||||
identity.Subject = strings.TrimSpace(identity.Subject)
|
||||
identity.Email = strings.ToLower(strings.TrimSpace(identity.Email))
|
||||
identity.Channel = strings.ToLower(strings.TrimSpace(identity.Channel))
|
||||
identity.DeviceID = strings.TrimSpace(identity.DeviceID)
|
||||
switch identity.Kind {
|
||||
case IdentityDevice:
|
||||
if identity.Subject == "" {
|
||||
identity.Subject = identity.DeviceID
|
||||
}
|
||||
case IdentityEmail:
|
||||
if identity.Subject == "" {
|
||||
identity.Subject = identity.Email
|
||||
}
|
||||
case IdentityOIDC:
|
||||
if identity.Issuer == "" || identity.Subject == "" {
|
||||
return Identity{}, ErrInvalidIdentity
|
||||
}
|
||||
case IdentityChannel:
|
||||
if identity.Issuer == "" {
|
||||
identity.Issuer = identity.Channel
|
||||
}
|
||||
}
|
||||
if identity.Kind == "" || identity.Subject == "" {
|
||||
return Identity{}, ErrInvalidIdentity
|
||||
}
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
func identityID(identity Identity) string {
|
||||
identity, _ = normalizeIdentity(identity)
|
||||
return "v1:" + encodeIdentityPart(identity.Kind) + ":" + encodeIdentityPart(identity.Issuer) + ":" + encodeIdentityPart(identity.Subject)
|
||||
}
|
||||
|
||||
func encodeIdentityPart(value string) string {
|
||||
if value == "" {
|
||||
return "_"
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(value))
|
||||
}
|
||||
|
||||
func normalizeScope(scope string) string {
|
||||
scope = strings.TrimSpace(scope)
|
||||
if scope == "" {
|
||||
return "global"
|
||||
}
|
||||
return scope
|
||||
}
|
||||
|
||||
func accessKey(subject, scope string) string {
|
||||
return "v1:" + encodeIdentityPart(strings.TrimSpace(subject)) + ":" + encodeIdentityPart(normalizeScope(scope))
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hashString(value string) uint64 {
|
||||
h := fnv.New64a()
|
||||
_, _ = h.Write([]byte(value))
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func randomTokenIssuer(_ context.Context, account Account, shard Shard, _ GateNode) (string, error) {
|
||||
var buf [16]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "lh-login-" + account.ID + "-" + shard.ID + "-" + hex.EncodeToString(buf[:]), nil
|
||||
}
|
||||
|
||||
func cloneAccount(account Account) Account {
|
||||
bindings := make([]Identity, 0, len(account.Bindings))
|
||||
for _, identity := range account.Bindings {
|
||||
bindings = append(bindings, cloneIdentity(identity))
|
||||
}
|
||||
account.Bindings = bindings
|
||||
account.Metadata = cloneStringMap(account.Metadata)
|
||||
return account
|
||||
}
|
||||
|
||||
func cloneIdentity(identity Identity) Identity {
|
||||
identity.Metadata = cloneStringMap(identity.Metadata)
|
||||
return identity
|
||||
}
|
||||
|
||||
func cloneBanRecord(record BanRecord) BanRecord {
|
||||
record.Metadata = cloneStringMap(record.Metadata)
|
||||
return record
|
||||
}
|
||||
|
||||
func cloneAllowRecord(record AllowRecord) AllowRecord {
|
||||
record.Metadata = cloneStringMap(record.Metadata)
|
||||
return record
|
||||
}
|
||||
|
||||
func cloneStringMap(in map[string]string) map[string]string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(in))
|
||||
for k, v := range in {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ctxErr(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user