Smail/backend/config/sections.go

208 lines
5.6 KiB
Go

package config
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"sort"
"time"
)
type Section string
const (
SectionRoot Section = "root"
SectionSMTP Section = "smtp"
SectionEmail Section = "email"
SectionSSL Section = "ssl"
SectionPlugins Section = "plugins"
)
const CurrentSectionSchema uint32 = 1
type SectionRef struct {
File string `json:"file"`
Schema uint32 `json:"schema"`
Generation uint64 `json:"generation"`
SHA256 string `json:"sha256"`
}
type SectionMeta struct {
Schema uint32 `json:"schema"`
Generation uint64 `json:"generation"`
}
type DomainConfig struct {
Primary string `json:"primary,omitempty"`
Accepted []string `json:"accepted,omitempty"`
Web string `json:"web,omitempty"`
AccessHosts []string `json:"accessHosts,omitempty"`
}
type HTTPConfig struct {
Port int `json:"port,omitempty"`
HTTPSPort int `json:"httpsPort,omitempty"`
HTTPSEnabled bool `json:"httpsEnabled"`
TLSMode string `json:"tlsMode,omitempty"`
TrustedProxyCIDRs []string `json:"trustedProxyCidrs,omitempty"`
}
const (
HTTPSTLSModeDirect = "direct"
HTTPSTLSModeProxy = "proxy"
)
type SMTPSectionConfig struct {
Meta SectionMeta `json:"meta"`
Relay SMTPConfig `json:"relay"`
Listener SMTPListenerConfig `json:"listener,omitempty"`
}
type EmailSectionConfig struct {
Meta SectionMeta `json:"meta"`
Policy EmailConfig `json:"policy"`
IMAP IMAPListenerConfig `json:"imap,omitempty"`
POP3 POP3ListenerConfig `json:"pop3,omitempty"`
}
type SSLSectionConfig struct {
Meta SectionMeta `json:"meta"`
TLS SSLConfig `json:"tls"`
}
type PluginNetworkTarget struct {
Scheme string `json:"scheme"`
Host string `json:"host"`
Port int `json:"port"`
}
type PluginPermissions struct {
Capabilities []string `json:"capabilities,omitempty"`
Network []PluginNetworkTarget `json:"network,omitempty"`
}
type PluginInstance struct {
Enabled bool `json:"enabled"`
Executable string `json:"executable"`
Permissions PluginPermissions `json:"permissions"`
Secret StoredSecret `json:"secret,omitempty"`
Settings json.RawMessage `json:"settings,omitempty"`
}
type PluginsSectionConfig struct {
Meta SectionMeta `json:"meta"`
Plugins map[string]PluginInstance `json:"plugins"`
}
type RuntimeConfig struct {
Generation uint64
SetupState SetupState
Root RootConfig
SMTP SMTPSectionConfig
Email EmailSectionConfig
SSL SSLSectionConfig
Plugins PluginsSectionConfig
LoadedAt time.Time
}
type sectionDefinition struct {
File string
Schema uint32
}
var fixedSectionDefinitions = map[Section]sectionDefinition{
SectionRoot: {File: "config.json", Schema: CurrentConfigVersion},
SectionSMTP: {File: "smtp.json", Schema: CurrentSectionSchema},
SectionEmail: {File: "email.json", Schema: CurrentSectionSchema},
SectionSSL: {File: "ssl.json", Schema: CurrentSectionSchema},
SectionPlugins: {File: "plugins.json", Schema: CurrentSectionSchema},
}
var requiredPartitionSections = []Section{SectionSMTP, SectionEmail, SectionSSL, SectionPlugins}
var (
ErrInvalidSection = errors.New("invalid configuration section")
ErrInvalidSectionReference = errors.New("invalid configuration section reference")
ErrSectionGenerationMismatch = errors.New("configuration section generation mismatch")
)
func FixedSectionFile(section Section) (string, bool) {
definition, ok := fixedSectionDefinitions[section]
return definition.File, ok
}
func FixedSectionSchema(section Section) (uint32, bool) {
definition, ok := fixedSectionDefinitions[section]
return definition.Schema, ok
}
func RequiredPartitionSections() []Section {
result := append([]Section(nil), requiredPartitionSections...)
return result
}
func ValidateSectionReferences(references map[Section]SectionRef, generation uint64) error {
if generation == 0 || len(references) != len(requiredPartitionSections) {
return ErrInvalidSectionReference
}
for section := range references {
if section == SectionRoot {
return ErrInvalidSectionReference
}
if _, ok := fixedSectionDefinitions[section]; !ok {
return ErrInvalidSectionReference
}
}
for _, section := range requiredPartitionSections {
reference, ok := references[section]
if !ok {
return ErrInvalidSectionReference
}
definition := fixedSectionDefinitions[section]
if reference.File != definition.File || reference.Schema != definition.Schema {
return ErrInvalidSectionReference
}
if reference.Generation != generation {
return ErrSectionGenerationMismatch
}
if !validSHA256(reference.SHA256) {
return ErrInvalidSectionReference
}
}
return nil
}
func ValidateSectionMeta(section Section, meta SectionMeta, expectedGeneration uint64) error {
definition, ok := fixedSectionDefinitions[section]
if !ok || section == SectionRoot || meta.Schema != definition.Schema {
return ErrInvalidSection
}
if meta.Generation == 0 || meta.Generation != expectedGeneration {
return ErrSectionGenerationMismatch
}
return nil
}
func SectionDigest(data []byte) string {
digest := sha256.Sum256(data)
return hex.EncodeToString(digest[:])
}
func OrderedPartitionSections() []Section {
result := RequiredPartitionSections()
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
return result
}
func validSHA256(value string) bool {
if len(value) != sha256.Size*2 {
return false
}
decoded, err := hex.DecodeString(value)
if err != nil || len(decoded) != sha256.Size {
return false
}
return value == hex.EncodeToString(decoded)
}