Files
go-sip/cmd/saas-server/startup.go
T

119 lines
3.6 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 == "" || !strings.HasPrefix(strings.TrimPrefix(address.Path, "/"), "saas-mock-") {
return c, errors.New("broker_url must use a dedicated saas-mock-* vhost")
}
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
}