Files
rogee b6d0af1a56
management-images / build-and-publish (push) Successful in 9m15s
docs: add one-click management deployment and image workflow
2026-09-16 17:57:05 +08:00

152 lines
4.3 KiB
Go

package management
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/url"
"regexp"
"sort"
"strings"
"time"
)
var idPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)
var requestPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$`)
var safePrefixPattern = regexp.MustCompile(`^[0-9]{0,32}$`)
func newID(prefix string) string {
var b [16]byte
if _, err := rand.Read(b[:]); err != nil {
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
return prefix + "-" + hex.EncodeToString(b[:])
}
func hashJSON(v any) (string, []byte, error) {
b, err := json.Marshal(v)
if err != nil {
return "", nil, err
}
h := sha256.Sum256(b)
return hex.EncodeToString(h[:]), b, nil
}
func hashBytes(b []byte) string {
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
func utcString(t time.Time) string { return t.UTC().Format(time.RFC3339Nano) }
func parseStoredTime(v string) (time.Time, error) { return time.Parse(time.RFC3339Nano, v) }
func validID(v string) bool { return idPattern.MatchString(v) }
func validRequestID(v string) bool { return requestPattern.MatchString(v) }
func maskNumber(v string) string {
if v == "" {
return ""
}
if len(v) <= 5 {
return strings.Repeat("*", len(v))
}
return v[:3] + strings.Repeat("*", len(v)-5) + v[len(v)-2:]
}
func containsString(values []string, value string) bool {
for _, item := range values {
if item == value {
return true
}
}
return false
}
func codecIntersection(a, b []string) []string {
var out []string
for _, v := range a {
if containsString(b, v) && !containsString(out, v) {
out = append(out, v)
}
}
return out
}
func sortedStrings(values []string) []string {
out := append([]string(nil), values...)
sort.Strings(out)
return out
}
func validateHost(host string) error {
if host == "" || len(host) > 253 || strings.ContainsAny(host, "\r\n/\\@ \t") {
return fmt.Errorf("host must be a hostname or IP address")
}
if net.ParseIP(host) != nil {
ip := net.ParseIP(host)
if ip.IsLoopback() || ip.IsUnspecified() || ip.IsLinkLocalUnicast() || ip.IsPrivate() {
return fmt.Errorf("private or local SIP host is not allowed")
}
return nil
}
if strings.EqualFold(host, "localhost") || strings.HasSuffix(strings.ToLower(host), ".localhost") {
return fmt.Errorf("local SIP host is not allowed")
}
for _, label := range strings.Split(host, ".") {
if label == "" || len(label) > 63 || strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
return fmt.Errorf("invalid SIP host")
}
}
return nil
}
func validateManagementURL(raw string, allowPrivate bool, allowlist map[string]bool) error {
if raw == "" {
return nil
}
u, err := url.Parse(raw)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return fmt.Errorf("management_url must be an https URL without credentials, query, or fragment")
}
host := u.Hostname()
if !allowPrivate {
if ip := net.ParseIP(host); ip != nil && (ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast()) {
return fmt.Errorf("management_url host is outside the allowed management network")
}
}
if len(allowlist) > 0 && !allowlist[strings.ToLower(host)] {
return fmt.Errorf("management_url host is not in the configured allowlist")
}
return nil
}
func safePrefix(prefix string) error {
if !safePrefixPattern.MatchString(prefix) {
return fmt.Errorf("dial_prefix must contain digits only")
}
return nil
}
func redactedConfig(cfg TrunkConfig) map[string]any {
return map[string]any{
"provider_id": cfg.ProviderID, "display_name": cfg.DisplayName, "enabled": cfg.Enabled,
"sip": map[string]any{"host": cfg.Sip.Host, "port": cfg.Sip.Port, "transport": cfg.Sip.Transport, "auth_mode": cfg.Sip.AuthMode, "register": cfg.Sip.Register, "credential_configured": cfg.Sip.CredentialRef != ""},
"codec_profile": cfg.CodecProfile, "caller_ids": cfg.CallerIDs, "dial_prefix": cfg.DialPrefix,
"egress_pool_id": cfg.EgressPoolID, "max_concurrency": cfg.MaxConcurrency, "max_cps": cfg.MaxCPS,
}
}
func parseCSV(v string) []string {
var out []string
for _, item := range strings.Split(v, ",") {
item = strings.TrimSpace(item)
if item != "" && !containsString(out, item) {
out = append(out, item)
}
}
return out
}