119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type startupConfig struct {
|
|
Listen string `json:"listen"`
|
|
DispatcherID string `json:"dispatcher_id"`
|
|
DispatcherSecret string `json:"dispatcher_secret"`
|
|
BrokerURL string `json:"broker_url"`
|
|
Snapshots string `json:"snapshots"`
|
|
Feedback string `json:"feedback_dir"`
|
|
TLSCert string `json:"tls_cert,omitempty"`
|
|
TLSKey string `json:"tls_key,omitempty"`
|
|
}
|
|
|
|
func loadStartup(path string) (startupConfig, error) {
|
|
var c startupConfig
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return c, fmt.Errorf("read startup config: %w", err)
|
|
}
|
|
if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 {
|
|
return c, errors.New("startup config must be a private regular file")
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return c, fmt.Errorf("read startup config: %w", err)
|
|
}
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err = dec.Decode(&c); err != nil || !json.Valid(raw) {
|
|
return c, errors.New("invalid startup configuration")
|
|
}
|
|
if c.Listen == "" {
|
|
c.Listen = "0.0.0.0:8089"
|
|
}
|
|
if _, _, err = net.SplitHostPort(c.Listen); err != nil {
|
|
return c, errors.New("listen must specify host and port")
|
|
}
|
|
if c.DispatcherID == "" || c.DispatcherSecret == "" || strings.ContainsAny(c.DispatcherID+c.DispatcherSecret, "\r\n") || c.Snapshots == "" || c.Feedback == "" {
|
|
return c, errors.New("dispatcher identity, secret, snapshots and feedback_dir are required")
|
|
}
|
|
address, err := url.Parse(c.BrokerURL)
|
|
if err != nil || (address.Scheme != "amqp" && address.Scheme != "amqps") || address.Host == "" {
|
|
return c, errors.New("broker_url must specify a valid AMQP endpoint")
|
|
}
|
|
base, err := filepath.Abs(filepath.Dir(path))
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
resolve := func(p string) string {
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
if !filepath.IsAbs(p) {
|
|
p = filepath.Join(base, p)
|
|
}
|
|
return filepath.Clean(p)
|
|
}
|
|
c.Snapshots = resolve(c.Snapshots)
|
|
c.Feedback = resolve(c.Feedback)
|
|
c.TLSCert = resolve(c.TLSCert)
|
|
c.TLSKey = resolve(c.TLSKey)
|
|
// Resolve existing path components so aliases cannot place output in an input directory.
|
|
realPath := func(p string) (string, error) {
|
|
var suffix []string
|
|
for {
|
|
real, e := filepath.EvalSymlinks(p)
|
|
if e == nil {
|
|
for i := len(suffix) - 1; i >= 0; i-- {
|
|
real = filepath.Join(real, suffix[i])
|
|
}
|
|
return real, nil
|
|
}
|
|
if !os.IsNotExist(e) {
|
|
return "", e
|
|
}
|
|
parent := filepath.Dir(p)
|
|
if parent == p {
|
|
return "", e
|
|
}
|
|
suffix = append(suffix, filepath.Base(p))
|
|
p = parent
|
|
}
|
|
}
|
|
c.Snapshots, err = realPath(c.Snapshots)
|
|
if err != nil {
|
|
return c, fmt.Errorf("snapshot path: %w", err)
|
|
}
|
|
c.Feedback, err = realPath(c.Feedback)
|
|
if err != nil {
|
|
return c, fmt.Errorf("feedback path: %w", err)
|
|
}
|
|
overlaps := func(a, b string) bool {
|
|
rel, e := filepath.Rel(a, b)
|
|
return e == nil && (rel == "." || !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != "..")
|
|
}
|
|
if overlaps(c.Snapshots, c.Feedback) || overlaps(c.Feedback, c.Snapshots) || overlaps(c.Feedback, resolve(filepath.Base(path))) {
|
|
return c, errors.New("feedback directory must be separate from configuration inputs")
|
|
}
|
|
if (c.TLSCert == "") != (c.TLSKey == "") {
|
|
return c, errors.New("tls_cert and tls_key must be provided together")
|
|
}
|
|
if c.TLSCert != "" && (overlaps(c.Feedback, c.TLSCert) || overlaps(c.Feedback, c.TLSKey)) {
|
|
return c, errors.New("feedback directory overlaps TLS inputs")
|
|
}
|
|
return c, nil
|
|
}
|