Files
bd2/go/internal/session/server.go
T
2026-09-21 09:53:46 +08:00

194 lines
6.0 KiB
Go

// Package session owns authentication, encryption, and request routing for a
// local account. It deliberately has no capture/fixture dependency.
package session
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
"bd2server/internal/cryptox"
"bd2server/internal/progress"
"bd2server/internal/protocol"
"bd2server/internal/transport"
"bd2server/internal/wire"
)
type LoginService interface {
Login(request, sessionKey []byte) ([]byte, error)
}
// Handler is implemented by domain services. ok=false means the endpoint is
// not owned by that service; unknown endpoints fail closed.
type Handler interface {
Handle(path string, request []byte) (packetCode int, response []byte, ok bool, err error)
}
// SessionAware handlers use a login-scoped opaque ID when protobuf request
// sequences participate in durable idempotency keys.
type SessionAware interface {
BeginSession(id string)
}
type Server struct {
mu sync.Mutex
key []byte
token string
loggedIn bool
login LoginService
handlers []Handler
progress *progress.Store
}
func NewServer(login LoginService, handlers ...Handler) (*Server, error) {
return NewServerWithProgress(login, progress.NewStore(), handlers...)
}
// NewServerWithProgress uses a caller-owned player progress store; the serve
// command supplies a file-backed store so changes survive server restarts.
func NewServerWithProgress(login LoginService, player *progress.Store, handlers ...Handler) (*Server, error) {
if login == nil {
return nil, errors.New("session login service is nil")
}
if player == nil {
return nil, errors.New("session progress store is nil")
}
key := make([]byte, 16)
if _, err := rand.Read(key); err != nil {
return nil, fmt.Errorf("create local session key: %w", err)
}
tokenBytes := make([]byte, 24)
if _, err := rand.Read(tokenBytes); err != nil {
return nil, fmt.Errorf("create local session token: %w", err)
}
return &Server{
key: []byte(hex.EncodeToString(key)), token: hex.EncodeToString(tokenBytes) + "|1",
login: login, handlers: append([]Handler(nil), handlers...), progress: player,
}, nil
}
func (s *Server) DispatchRaw(path string, body []byte, cookie string) (transport.RawReply, error) {
s.mu.Lock()
defer s.mu.Unlock()
if path == "/LoginUser" {
request, err := cryptox.DecryptBase64Payload(string(body), cryptox.Key())
if err != nil {
return transport.RawReply{}, fmt.Errorf("LoginUser decrypt: %w", err)
}
proto, err := s.login.Login(request, s.key)
if err != nil {
return transport.RawReply{}, fmt.Errorf("LoginUser: %w", err)
}
sessionBytes := make([]byte, 12)
if _, err := rand.Read(sessionBytes); err != nil {
return transport.RawReply{}, fmt.Errorf("create login request identity: %w", err)
}
sessionID := hex.EncodeToString(sessionBytes)
for _, handler := range s.handlers {
if aware, ok := handler.(SessionAware); ok {
aware.BeginSession(sessionID)
}
}
s.loggedIn = true
encoded, err := protocol.Encode(3, proto, cryptox.Key(), time.Now().UnixMilli())
return transport.RawReply{Body: encoded, Cookie: s.token}, err
}
if err := s.authorize(cookie); err != nil {
return transport.RawReply{}, err
}
if path == "/BatchRequest" {
return s.handleBatch(body)
}
request, err := cryptox.DecryptBase64Payload(string(body), s.key)
if err != nil {
return transport.RawReply{}, fmt.Errorf("%s decrypt: %w", path, err)
}
code, response, err := s.dispatch(path, request)
if err != nil {
return transport.RawReply{}, err
}
encoded, err := protocol.Encode(code, response, s.key, time.Now().UnixMilli())
return transport.RawReply{Body: encoded}, err
}
func (s *Server) handleBatch(body []byte) (transport.RawReply, error) {
requests, decoded, err := protocol.DecodeBatchRequest(body, s.key)
if err != nil {
return transport.RawReply{}, err
}
items := make([]protocol.BatchResponse, 0, len(requests))
for i, request := range requests {
code, response, err := s.dispatch(request.Path, decoded[i])
if err != nil {
return transport.RawReply{}, fmt.Errorf("batch %s: %w", request.Path, err)
}
raw, err := protocol.Encode(code, response, s.key, time.Now().UnixMilli())
if err != nil {
return transport.RawReply{}, err
}
var envelope protocol.Envelope
if err := json.Unmarshal(raw, &envelope); err != nil {
return transport.RawReply{}, err
}
items = append(items, protocol.BatchResponse{Path: request.Path, ResponseData: envelope})
}
encoded, err := json.Marshal(items)
return transport.RawReply{Body: encoded}, err
}
func (s *Server) dispatch(path string, request []byte) (int, []byte, error) {
if _, found, err := wire.Varint(request, 1); err != nil || !found {
return 0, nil, fmt.Errorf("%s has no request sequence", path)
}
switch path {
case "/SaveUserPosition":
if err := s.progress.SaveUserPosition(request); err != nil {
return 0, nil, fmt.Errorf("%s: %w", path, err)
}
return 7, nil, nil
case "/TutorialClear":
if err := s.progress.ClearTutorial(request); err != nil {
return 0, nil, fmt.Errorf("%s: %w", path, err)
}
return 102, nil, nil
case "/QuestUpdate":
questID, err := s.progress.UpdateQuest(request)
if err != nil {
return 0, nil, fmt.Errorf("%s: %w", path, err)
}
response := wire.AppendVarint(nil, 1, uint64(questID))
return 19, wire.AppendBytes(response, 2, nil), nil
}
for _, handler := range s.handlers {
code, response, ok, err := handler.Handle(path, request)
if err != nil {
return 0, nil, err
}
if ok {
return code, response, nil
}
}
return 0, nil, fmt.Errorf("%w: %s", transport.ErrNotImplemented, path)
}
func (s *Server) authorize(cookie string) error {
if !s.loggedIn {
return errors.New("session login required")
}
want := "s=" + s.token
for _, value := range strings.Split(cookie, ";") {
if strings.TrimSpace(value) == want {
return nil
}
}
return errors.New("invalid local session cookie")
}
func (s *Server) KeyForTest() []byte { return append([]byte(nil), s.key...) }
func (s *Server) ProgressForTest() *progress.Store { return s.progress }