owpengram-server/internal/loadharness/types.go

140 lines
4.6 KiB
Go

// Package loadharness implements the real-MTProto capacity harness used by
// cmd/telesrv-load. It deliberately uses the published gotd fork instead of
// server-internal handlers or direct database fixtures.
package loadharness
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
const ManifestVersion = 1
// Endpoint is the immutable wire target shared by provisioning and runs.
type Endpoint struct {
Address string `json:"address"`
DC int `json:"dc"`
APIID int `json:"api_id"`
APIHash string `json:"api_hash"`
RSAKeyPath string `json:"rsa_key_path"`
Obfuscated bool `json:"obfuscated"`
PFS bool `json:"pfs"`
TempKeyTTL int `json:"temp_key_ttl_seconds,omitempty"`
}
// SessionRecord maps one physical MTProto session file to one logical account.
// It contains routing facts only; auth key material remains in encrypted files.
type SessionRecord struct {
Index int `json:"index"`
AccountIndex int `json:"account_index"`
DeviceIndex int `json:"device_index"`
Phone string `json:"phone"`
FirstName string `json:"first_name"`
SessionFile string `json:"session_file"`
UserID int64 `json:"user_id"`
AccessHash int64 `json:"access_hash"`
}
// Manifest never embeds session encryption keys, auth keys, phone-code hashes
// or raw server errors. It does contain generated test phone/user routing data,
// so it remains a controlled run artifact and is not copied into RunReport.
type Manifest struct {
Version int `json:"version"`
CreatedAt time.Time `json:"created_at"`
Endpoint Endpoint `json:"endpoint"`
Sessions []SessionRecord `json:"sessions"`
}
func (e Endpoint) Validate() error {
if strings.TrimSpace(e.Address) == "" {
return errors.New("endpoint address is required")
}
if e.DC == 0 {
return errors.New("endpoint DC must be non-zero")
}
if e.APIID <= 0 || strings.TrimSpace(e.APIHash) == "" {
return errors.New("endpoint api_id and api_hash are required")
}
if strings.TrimSpace(e.RSAKeyPath) == "" {
return errors.New("endpoint RSA key path is required")
}
return nil
}
func (m *Manifest) Validate() error {
if m == nil {
return errors.New("nil manifest")
}
if m.Version != ManifestVersion {
return fmt.Errorf("manifest version %d, want %d", m.Version, ManifestVersion)
}
if err := m.Endpoint.Validate(); err != nil {
return err
}
indices := make(map[int]struct{}, len(m.Sessions))
files := make(map[string]struct{}, len(m.Sessions))
for _, session := range m.Sessions {
if session.Index < 0 || session.AccountIndex < 0 || session.DeviceIndex < 0 {
return fmt.Errorf("session %d has a negative index", session.Index)
}
if _, ok := indices[session.Index]; ok {
return fmt.Errorf("duplicate session index %d", session.Index)
}
indices[session.Index] = struct{}{}
if strings.TrimSpace(session.Phone) == "" || strings.TrimSpace(session.SessionFile) == "" {
return fmt.Errorf("session %d is missing phone or session_file", session.Index)
}
clean := filepath.Clean(session.SessionFile)
if filepath.IsAbs(clean) || clean == "." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) || clean == ".." {
return fmt.Errorf("session %d has unsafe session_file %q", session.Index, session.SessionFile)
}
if _, ok := files[clean]; ok {
return fmt.Errorf("duplicate session file %q", clean)
}
files[clean] = struct{}{}
if session.UserID <= 0 {
return fmt.Errorf("session %d has no provisioned user_id", session.Index)
}
}
return nil
}
func LoadManifest(path string) (*Manifest, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read manifest: %w", err)
}
var manifest Manifest
decoder := json.NewDecoder(strings.NewReader(string(data)))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&manifest); err != nil {
return nil, fmt.Errorf("decode manifest: %w", err)
}
if err := manifest.Validate(); err != nil {
return nil, err
}
sort.Slice(manifest.Sessions, func(i, j int) bool { return manifest.Sessions[i].Index < manifest.Sessions[j].Index })
return &manifest, nil
}
func WriteManifest(path string, manifest *Manifest) error {
if err := manifest.Validate(); err != nil {
return err
}
data, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return fmt.Errorf("encode manifest: %w", err)
}
data = append(data, '\n')
return writeFileAtomic(path, data, 0o600)
}
func resolveSessionPath(manifestPath string, record SessionRecord) string {
return filepath.Join(filepath.Dir(manifestPath), filepath.FromSlash(record.SessionFile))
}