init
This commit is contained in:
194
internal/config/config.go
Normal file
194
internal/config/config.go
Normal file
@@ -0,0 +1,194 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user