Files
license/internal/config/config.go

195 lines
4.9 KiB
Go
Raw Normal View History

2026-07-31 16:59:55 +08:00
package config
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
type Config struct {
ListenAddr string
DatabaseDSN string
SigningPrivateKeyFile string
SigningCertificateFile string
}
type fileConfig struct {
Server struct {
ListenAddr strictString `yaml:"listen_addr"`
} `yaml:"server"`
Database struct {
DSN strictString `yaml:"dsn"`
} `yaml:"database"`
Signing struct {
PrivateKeyFile strictString `yaml:"private_key_file"`
CertificateFile strictString `yaml:"certificate_file"`
} `yaml:"signing"`
}
type strictString string
func (value *strictString) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.ScalarNode || node.Tag != "!!str" {
return errors.New("必须是字符串")
}
*value = strictString(node.Value)
return nil
}
func LoadFile(filePath string) (Config, error) {
if filePath == "" {
return Config{}, errors.New("配置文件路径不能为空")
}
content, err := os.ReadFile(filePath)
if err != nil {
return Config{}, fmt.Errorf("读取配置文件 %q: %w", filePath, err)
}
if err := rejectYAMLMergeKeys(content); err != nil {
return Config{}, err
}
var raw fileConfig
decoder := yaml.NewDecoder(bytes.NewReader(content))
decoder.KnownFields(true)
if err := decoder.Decode(&raw); err != nil {
if errors.Is(err, io.EOF) {
return Config{}, errors.New("配置文件不能为空")
}
return Config{}, fmt.Errorf("解析配置文件: %w", err)
}
var extra any
if err := decoder.Decode(&extra); err == nil {
return Config{}, errors.New("配置文件只允许包含一个 YAML 文档")
} else if !errors.Is(err, io.EOF) {
return Config{}, fmt.Errorf("解析配置文件的尾部内容: %w", err)
}
return raw.build()
}
func rejectYAMLMergeKeys(content []byte) error {
var document yaml.Node
decoder := yaml.NewDecoder(bytes.NewReader(content))
if err := decoder.Decode(&document); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return fmt.Errorf("解析配置文件: %w", err)
}
return rejectYAMLMergeNode(&document, make(map[*yaml.Node]struct{}))
}
func rejectYAMLMergeNode(node *yaml.Node, visited map[*yaml.Node]struct{}) error {
if node == nil {
return nil
}
if _, exists := visited[node]; exists {
return nil
}
visited[node] = struct{}{}
if node.Kind == yaml.MappingNode {
for index := 0; index+1 < len(node.Content); index += 2 {
key := node.Content[index]
if key.Value == "<<" || key.Tag == "!!merge" || key.Tag == "tag:yaml.org,2002:merge" {
return fmt.Errorf("配置文件不允许使用 YAML merge key <<(第 %d 行,第 %d 列)", key.Line, key.Column)
}
if err := rejectYAMLMergeNode(key, visited); err != nil {
return err
}
if err := rejectYAMLMergeNode(node.Content[index+1], visited); err != nil {
return err
}
}
} else {
for _, child := range node.Content {
if err := rejectYAMLMergeNode(child, visited); err != nil {
return err
}
}
}
if node.Kind == yaml.AliasNode {
return rejectYAMLMergeNode(node.Alias, visited)
}
return nil
}
func (raw fileConfig) build() (Config, error) {
listenAddr, err := requireValue("server.listen_addr", raw.Server.ListenAddr)
if err != nil {
return Config{}, err
}
if err := validateLoopbackListenAddr(listenAddr); err != nil {
return Config{}, err
}
databaseDSN, err := requireValue("database.dsn", raw.Database.DSN)
if err != nil {
return Config{}, err
}
signingPrivateKeyFile, err := requireAbsolutePath("signing.private_key_file", raw.Signing.PrivateKeyFile)
if err != nil {
return Config{}, err
}
signingCertificateFile, err := requireAbsolutePath("signing.certificate_file", raw.Signing.CertificateFile)
if err != nil {
return Config{}, err
}
return Config{
ListenAddr: listenAddr,
DatabaseDSN: databaseDSN,
SigningPrivateKeyFile: signingPrivateKeyFile,
SigningCertificateFile: signingCertificateFile,
}, nil
}
func requireValue(name string, value strictString) (string, error) {
text := string(value)
if strings.TrimSpace(text) == "" {
return "", fmt.Errorf("配置项 %s 不能为空", name)
}
return text, nil
}
func requireAbsolutePath(name string, value strictString) (string, error) {
filePath, err := requireValue(name, value)
if err != nil {
return "", err
}
if !filepath.IsAbs(filePath) {
return "", fmt.Errorf("配置项 %s 必须是绝对路径", name)
}
return filePath, nil
}
func validateLoopbackListenAddr(listenAddr string) error {
host, port, err := net.SplitHostPort(listenAddr)
if err != nil {
return fmt.Errorf("配置项 server.listen_addr 必须是主机和端口: %w", err)
}
ip := net.ParseIP(host)
if ip == nil || !ip.IsLoopback() {
return errors.New("配置项 server.listen_addr 必须使用回环 IP 地址")
}
portNumber, err := strconv.ParseUint(port, 10, 16)
if err != nil || portNumber == 0 {
return errors.New("配置项 server.listen_addr 的端口无效")
}
return nil
}