152 lines
4.3 KiB
Go
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
|
|
}
|