Some checks are pending
Security Checks / security (push) Waiting to run
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
255 lines
6.2 KiB
Go
255 lines
6.2 KiB
Go
package adminreset
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Smail/utils/password"
|
|
)
|
|
|
|
const (
|
|
ProtocolVersion = 1
|
|
maxControlMessageSize = 16 << 10
|
|
controlRequestTimeout = 15 * time.Second
|
|
defaultSessionCapacity = 128
|
|
)
|
|
|
|
var (
|
|
ErrUnauthorizedPeer = errors.New("unauthorized local peer")
|
|
ErrInvalidRequest = errors.New("invalid reset request")
|
|
ErrReplay = errors.New("reset request replayed")
|
|
ErrExpiredSession = errors.New("reset session expired")
|
|
ErrUnsupported = errors.New("local reset channel is unsupported")
|
|
ErrSessionCapacity = errors.New("reset session capacity exceeded")
|
|
)
|
|
|
|
type Request struct {
|
|
Version int `json:"version"`
|
|
Nonce string `json:"nonce"`
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
|
|
type Response struct {
|
|
OK bool `json:"ok"`
|
|
Code string `json:"code,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
Nonce string `json:"nonce,omitempty"`
|
|
}
|
|
|
|
type ResetHandler func(newPassword string) error
|
|
|
|
type SessionGuard struct {
|
|
mu sync.Mutex
|
|
pending map[string]time.Time
|
|
ttl time.Duration
|
|
capacity int
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewSessionGuard(ttl time.Duration) *SessionGuard {
|
|
return NewSessionGuardWithCapacity(ttl, defaultSessionCapacity)
|
|
}
|
|
|
|
// NewSessionGuardWithCapacity 创建带有并发会话上限的短时会话守卫。
|
|
func NewSessionGuardWithCapacity(ttl time.Duration, capacity int) *SessionGuard {
|
|
if ttl <= 0 {
|
|
ttl = 2 * time.Minute
|
|
}
|
|
if capacity <= 0 {
|
|
capacity = defaultSessionCapacity
|
|
}
|
|
return &SessionGuard{
|
|
pending: make(map[string]time.Time, capacity),
|
|
ttl: ttl,
|
|
capacity: capacity,
|
|
now: time.Now,
|
|
}
|
|
}
|
|
|
|
func (g *SessionGuard) Issue() (string, error) {
|
|
if g == nil {
|
|
return "", ErrInvalidRequest
|
|
}
|
|
now := g.now()
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
g.pruneLocked(now)
|
|
if len(g.pending) >= g.capacity {
|
|
return "", ErrSessionCapacity
|
|
}
|
|
for attempt := 0; attempt < 3; attempt++ {
|
|
nonce, err := NewNonce()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, exists := g.pending[nonce]; exists {
|
|
continue
|
|
}
|
|
g.pending[nonce] = now.Add(g.ttl)
|
|
return nonce, nil
|
|
}
|
|
return "", errors.New("reset nonce collision")
|
|
}
|
|
|
|
func (g *SessionGuard) Consume(nonce string) error {
|
|
if g == nil || strings.TrimSpace(nonce) == "" {
|
|
return ErrInvalidRequest
|
|
}
|
|
now := g.now()
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
expiry, exists := g.pending[nonce]
|
|
if !exists {
|
|
g.pruneLocked(now)
|
|
return ErrReplay
|
|
}
|
|
delete(g.pending, nonce)
|
|
if !now.Before(expiry) {
|
|
return ErrExpiredSession
|
|
}
|
|
g.pruneLocked(now)
|
|
return nil
|
|
}
|
|
|
|
func (g *SessionGuard) Discard(nonce string) {
|
|
if g == nil || nonce == "" {
|
|
return
|
|
}
|
|
g.mu.Lock()
|
|
delete(g.pending, nonce)
|
|
g.mu.Unlock()
|
|
}
|
|
|
|
func (g *SessionGuard) pruneLocked(now time.Time) {
|
|
for nonce, expiry := range g.pending {
|
|
if !now.Before(expiry) {
|
|
delete(g.pending, nonce)
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewNonce() (string, error) {
|
|
var raw [24]byte
|
|
if _, err := rand.Read(raw[:]); err != nil {
|
|
return "", fmt.Errorf("generate reset nonce: %w", err)
|
|
}
|
|
return hex.EncodeToString(raw[:]), nil
|
|
}
|
|
|
|
func DecodeRequest(r io.Reader) (Request, error) {
|
|
data, err := readControlFrame(r)
|
|
if err != nil {
|
|
return Request{}, ErrInvalidRequest
|
|
}
|
|
var request Request
|
|
if err := decodeControlJSON(data, &request); err != nil {
|
|
return Request{}, ErrInvalidRequest
|
|
}
|
|
if request.Version != ProtocolVersion || strings.TrimSpace(request.Nonce) == "" || password.ValidateUserPassword(request.NewPassword) != nil {
|
|
return Request{}, ErrInvalidRequest
|
|
}
|
|
return request, nil
|
|
}
|
|
|
|
func DecodeResponse(r io.Reader) (Response, error) {
|
|
data, err := readControlFrame(r)
|
|
if err != nil {
|
|
return Response{}, ErrInvalidRequest
|
|
}
|
|
var response Response
|
|
if err := decodeControlJSON(data, &response); err != nil {
|
|
return Response{}, ErrInvalidRequest
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func readControlFrame(r io.Reader) ([]byte, error) {
|
|
if r == nil {
|
|
return nil, ErrInvalidRequest
|
|
}
|
|
reader := bufio.NewReaderSize(r, maxControlMessageSize+1)
|
|
frame, err := reader.ReadSlice('\n')
|
|
if errors.Is(err, bufio.ErrBufferFull) || len(frame) > maxControlMessageSize {
|
|
return nil, ErrInvalidRequest
|
|
}
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return nil, ErrInvalidRequest
|
|
}
|
|
if len(bytes.TrimSpace(frame)) == 0 || reader.Buffered() != 0 {
|
|
return nil, ErrInvalidRequest
|
|
}
|
|
return frame, nil
|
|
}
|
|
|
|
func decodeControlJSON(data []byte, target any) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(target); err != nil {
|
|
return err
|
|
}
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
|
return ErrInvalidRequest
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func EncodeResponse(w io.Writer, response Response) error {
|
|
return json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
func handleResetConnection(conn io.ReadWriter, setDeadline func(time.Time) error, handler ResetHandler, guard *SessionGuard) {
|
|
if conn == nil || handler == nil || guard == nil {
|
|
return
|
|
}
|
|
if setDeadline != nil {
|
|
_ = setDeadline(time.Now().Add(controlRequestTimeout))
|
|
defer setDeadline(time.Time{})
|
|
}
|
|
nonce, err := guard.Issue()
|
|
if err != nil {
|
|
_ = EncodeResponse(conn, Response{Code: "CHANNEL_UNAVAILABLE", Message: "重置通道不可用"})
|
|
return
|
|
}
|
|
defer guard.Discard(nonce)
|
|
if err := EncodeResponse(conn, Response{OK: true, Code: "CHALLENGE", Nonce: nonce}); err != nil {
|
|
return
|
|
}
|
|
request, err := DecodeRequest(conn)
|
|
if err != nil || request.Nonce != nonce {
|
|
_ = EncodeResponse(conn, Response{Code: "INVALID_REQUEST", Message: "重置请求无效"})
|
|
return
|
|
}
|
|
if err := guard.Consume(request.Nonce); err != nil {
|
|
_ = EncodeResponse(conn, Response{Code: "REPLAY_OR_EXPIRED", Message: "重置会话已失效或已使用"})
|
|
return
|
|
}
|
|
password := request.NewPassword
|
|
defer clearPassword(&password)
|
|
if err := handler(password); err != nil {
|
|
_ = EncodeResponse(conn, Response{Code: "RESET_FAILED", Message: "超级管理员密码重置失败"})
|
|
return
|
|
}
|
|
_ = EncodeResponse(conn, Response{OK: true, Message: "超级管理员密码已重置"})
|
|
}
|
|
|
|
func clearPassword(password *string) {
|
|
if password == nil {
|
|
return
|
|
}
|
|
buffer := []byte(*password)
|
|
for i := range buffer {
|
|
buffer[i] = 0
|
|
}
|
|
*password = ""
|
|
}
|