package livekit import ( "errors" "fmt" "regexp" "slices" "strconv" "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/livekit/protocol/utils/xtwirp" "golang.org/x/text/language" ) var ( _ xtwirp.ErrorMeta = (*SIPStatus)(nil) _ error = (*SIPStatus)(nil) ) // SIPStatusFrom unwraps an error and returns associated SIP call status, if any. func SIPStatusFrom(err error) *SIPStatus { st, ok := status.FromError(err) if !ok { return nil } for _, d := range st.Details() { if e, ok := d.(*SIPStatus); ok { return e } } return nil } func (p SIPStatusCode) ShortName() string { return strings.TrimPrefix(p.String(), "SIP_STATUS_") } func (p *SIPStatus) Error() string { if p.Status != "" { return fmt.Sprintf("sip status: %d: %s", p.Code, p.Status) } return fmt.Sprintf("sip status: %d (%s)", p.Code, p.Code.ShortName()) } // Maps SIP response codes received from remote SIP servers to GRPC error codes. var sipCodeToGRPCCode = map[SIPStatusCode]codes.Code{ // 3xx - Redirection Responses SIPStatusCode_SIP_STATUS_MULTIPLE_CHOICES: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_MOVED_PERMANENTLY: codes.NotFound, SIPStatusCode_SIP_STATUS_MOVED_TEMPORARILY: codes.NotFound, SIPStatusCode_SIP_STATUS_USE_PROXY: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_ALTERNATIVE_SERVICE: codes.InvalidArgument, // 4xx - Client Failure Responses SIPStatusCode_SIP_STATUS_BAD_REQUEST: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_UNAUTHORIZED: codes.PermissionDenied, SIPStatusCode_SIP_STATUS_PAYMENT_REQUIRED: codes.PermissionDenied, SIPStatusCode_SIP_STATUS_FORBIDDEN: codes.PermissionDenied, SIPStatusCode_SIP_STATUS_NOTFOUND: codes.NotFound, SIPStatusCode_SIP_STATUS_METHOD_NOT_ALLOWED: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_NOT_ACCEPTABLE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_PROXY_AUTH_REQUIRED: codes.Unauthenticated, SIPStatusCode_SIP_STATUS_REQUEST_TIMEOUT: codes.DeadlineExceeded, SIPStatusCode_SIP_STATUS_CONFLICT: codes.FailedPrecondition, SIPStatusCode_SIP_STATUS_GONE: codes.NotFound, SIPStatusCode_SIP_STATUS_LENGTH_REQUIRED: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_CONDITIONAL_REQUEST_FAILED: codes.FailedPrecondition, SIPStatusCode_SIP_STATUS_REQUEST_ENTITY_TOO_LARGE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_REQUEST_URI_TOO_LONG: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_UNSUPPORTED_MEDIA_TYPE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE: codes.OutOfRange, SIPStatusCode_SIP_STATUS_UNKNOWN_RESOURCE_PRIORITY: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_BAD_EXTENSION: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_EXTENSION_REQUIRED: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_SESSION_INTERVAL_TOO_SMALL: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_INTERVAL_TOO_BRIEF: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_BAD_LOCATION_INFORMATION: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_BAD_ALERT_MESSAGE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_USE_IDENTITY_HEADER: codes.Unauthenticated, SIPStatusCode_SIP_STATUS_PROVIDE_REFERRER_IDENTITY: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_FLOW_FAILED: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_ANONYMITY_DISALLOWED: codes.PermissionDenied, SIPStatusCode_SIP_STATUS_BAD_IDENTITY_INFO: codes.Unauthenticated, SIPStatusCode_SIP_STATUS_UNSUPPORTED_CERTIFICATE: codes.Unauthenticated, SIPStatusCode_SIP_STATUS_INVALID_IDENTITY_HEADER: codes.Unauthenticated, SIPStatusCode_SIP_STATUS_FIRST_HOP_LACKS_OUTBOUND_SUPPORT: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_MAX_BREADTH_EXCEEDED: codes.ResourceExhausted, SIPStatusCode_SIP_STATUS_BAD_INFO_PACKAGE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_CONSENT_NEEDED: codes.PermissionDenied, SIPStatusCode_SIP_STATUS_TEMPORARILY_UNAVAILABLE: codes.ResourceExhausted, SIPStatusCode_SIP_STATUS_CALL_TRANSACTION_DOES_NOT_EXISTS: codes.NotFound, SIPStatusCode_SIP_STATUS_LOOP_DETECTED: codes.FailedPrecondition, SIPStatusCode_SIP_STATUS_TOO_MANY_HOPS: codes.FailedPrecondition, SIPStatusCode_SIP_STATUS_ADDRESS_INCOMPLETE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_AMBIGUOUS: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_BUSY_HERE: codes.ResourceExhausted, SIPStatusCode_SIP_STATUS_REQUEST_TERMINATED: codes.Aborted, SIPStatusCode_SIP_STATUS_NOT_ACCEPTABLE_HERE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_BAD_EVENT: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_REQUEST_PENDING: codes.Aborted, SIPStatusCode_SIP_STATUS_UNDECIPHERABLE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_SECURITY_AGREEMENT_REQUIRED: codes.Unauthenticated, // 5xx - Server Failure Responses SIPStatusCode_SIP_STATUS_INTERNAL_SERVER_ERROR: codes.FailedPrecondition, SIPStatusCode_SIP_STATUS_NOT_IMPLEMENTED: codes.FailedPrecondition, SIPStatusCode_SIP_STATUS_BAD_GATEWAY: codes.FailedPrecondition, SIPStatusCode_SIP_STATUS_SERVICE_UNAVAILABLE: codes.FailedPrecondition, SIPStatusCode_SIP_STATUS_GATEWAY_TIMEOUT: codes.DeadlineExceeded, SIPStatusCode_SIP_STATUS_VERSION_NOT_SUPPORTED: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_MESSAGE_TOO_LARGE: codes.InvalidArgument, // 6xx - Global Failure Responses SIPStatusCode_SIP_STATUS_GLOBAL_BUSY_EVERYWHERE: codes.ResourceExhausted, SIPStatusCode_SIP_STATUS_GLOBAL_DECLINE: codes.PermissionDenied, SIPStatusCode_SIP_STATUS_GLOBAL_DOES_NOT_EXIST_ANYWHERE: codes.NotFound, SIPStatusCode_SIP_STATUS_GLOBAL_NOT_ACCEPTABLE: codes.InvalidArgument, SIPStatusCode_SIP_STATUS_GLOBAL_UNWANTED: codes.PermissionDenied, SIPStatusCode_SIP_STATUS_GLOBAL_REJECTED: codes.PermissionDenied, } func (p *SIPStatus) GRPCStatus() *status.Status { code, ok := sipCodeToGRPCCode[p.Code] if !ok { code = codes.Unknown // 1xx and 2xx codes should never emit an error, something is wrong. if p.Code < 200 { code = codes.Unknown // 1xx are not final responses, something is wrong. } else if p.Code < 300 { return status.New(codes.OK, "OK") // Preserving previous behavior } else if code < 500 { code = codes.InvalidArgument } else if code < 600 { code = codes.FailedPrecondition // 5xx from remote server, per guideline (c) in gRPC docs } else if code < 700 { code = codes.InvalidArgument // Same as 4xx ,but authoritative } } msg := p.Status if msg == "" { msg = p.Code.ShortName() } st := status.New(code, fmt.Sprintf("sip status %d: %s", p.Code, msg)) if st2, err := st.WithDetails(p); err == nil { return st2 } return st } func (p *SIPStatus) TwirpErrorMeta() map[string]string { status := p.Status if status == "" { status = p.Code.String() } return map[string]string{ "sip_status_code": strconv.Itoa(int(p.Code)), "sip_status": status, } } // Name returns a lower-case short name for the transport. // It returns an empty string if transport is not specified. func (p SIPTransport) Name() string { switch p { case SIPTransport_SIP_TRANSPORT_AUTO: return "" case SIPTransport_SIP_TRANSPORT_UDP: return "udp" case SIPTransport_SIP_TRANSPORT_TCP: return "tcp" case SIPTransport_SIP_TRANSPORT_TLS: return "tls" default: return strings.TrimPrefix(p.String(), "SIP_TRANSPORT_") } } // ToProto implements DataPacket in Go SDK. func (p *SipDTMF) ToProto() *DataPacket { return &DataPacket{ Value: &DataPacket_SipDtmf{ SipDtmf: p, }, } } func (p *SIPTrunkInfo) ID() string { if p == nil { return "" } return p.SipTrunkId } func (p *SIPInboundTrunkInfo) ID() string { if p == nil { return "" } return p.SipTrunkId } func (p *SIPOutboundTrunkInfo) ID() string { if p == nil { return "" } return p.SipTrunkId } func (p *SIPDispatchRuleInfo) ID() string { if p == nil { return "" } return p.SipDispatchRuleId } // AsInbound converts legacy SIPTrunkInfo to SIPInboundTrunkInfo. func (p *SIPTrunkInfo) AsInbound() *SIPInboundTrunkInfo { if p == nil || p.Kind == SIPTrunkInfo_TRUNK_OUTBOUND { return nil } var nums []string if p.OutboundNumber != "" { nums = []string{p.OutboundNumber} } return &SIPInboundTrunkInfo{ SipTrunkId: p.SipTrunkId, Name: p.Name, Metadata: p.Metadata, Numbers: nums, AllowedAddresses: p.InboundAddresses, AllowedNumbers: p.InboundNumbers, AuthUsername: p.InboundUsername, AuthPassword: p.InboundPassword, } } // AsTrunkInfo converts SIPInboundTrunkInfo to legacy SIPTrunkInfo. func (p *SIPInboundTrunkInfo) AsTrunkInfo() *SIPTrunkInfo { if p == nil { return nil } var num string if len(p.Numbers) != 0 { num = p.Numbers[0] } return &SIPTrunkInfo{ SipTrunkId: p.SipTrunkId, Kind: SIPTrunkInfo_TRUNK_INBOUND, Name: p.Name, Metadata: p.Metadata, OutboundNumber: num, InboundAddresses: p.AllowedAddresses, InboundNumbers: p.AllowedNumbers, InboundUsername: p.AuthUsername, InboundPassword: p.AuthPassword, } } // AsOutbound converts legacy SIPTrunkInfo to SIPOutboundTrunkInfo. func (p *SIPTrunkInfo) AsOutbound() *SIPOutboundTrunkInfo { if p == nil || p.Kind == SIPTrunkInfo_TRUNK_INBOUND { return nil } var nums []string if p.OutboundNumber != "" { nums = []string{p.OutboundNumber} } return &SIPOutboundTrunkInfo{ SipTrunkId: p.SipTrunkId, Name: p.Name, Metadata: p.Metadata, Address: p.OutboundAddress, Transport: p.Transport, Numbers: nums, AuthUsername: p.OutboundUsername, AuthPassword: p.OutboundPassword, } } // AsTrunkInfo converts SIPOutboundTrunkInfo to legacy SIPTrunkInfo. func (p *SIPOutboundTrunkInfo) AsTrunkInfo() *SIPTrunkInfo { if p == nil { return nil } var num string if len(p.Numbers) != 0 { num = p.Numbers[0] } return &SIPTrunkInfo{ SipTrunkId: p.SipTrunkId, Kind: SIPTrunkInfo_TRUNK_OUTBOUND, Name: p.Name, Metadata: p.Metadata, OutboundAddress: p.Address, Transport: p.Transport, OutboundNumber: num, OutboundUsername: p.AuthUsername, OutboundPassword: p.AuthPassword, } } // validateHeaders makes sure header names/keys and values are per SIP specifications func validateHeaders(headers map[string]string) error { for headerName, headerValue := range headers { if err := ValidateHeaderName(headerName, true); err != nil { return fmt.Errorf("invalid header name: %w", err) } if err := ValidateHeaderValue(headerName, headerValue); err != nil { return fmt.Errorf("invalid header value for %s: %w", headerName, err) } } return nil } // validateHeaderNames Makes sure the values of the given map correspond to valid SIP header names func validateAttributesToHeaders(attributesToHeaders map[string]string) error { for _, headerName := range attributesToHeaders { if err := ValidateHeaderName(headerName, false); err != nil { return fmt.Errorf("invalid header name: %w", err) } } return nil } // validateHeaderToAttributes Makes sure the keys of the given map correspond to valid SIP header names func validateHeaderToAttributes(headerToAttributes map[string]string) error { for headerName := range headerToAttributes { if err := ValidateHeaderName(headerName, false); err != nil { return fmt.Errorf("invalid header name: %w", err) } } return nil } // validateHostnameFormat ensures value is a hostname or IP, not a SIP URI or containing transport parameter. // Returns nil if value is empty (for optional fields). // fieldName is used to construct error messages, e.g. "from_host" or "trunk hostname". func validateHostnameFormat(value, fieldName string) error { if strings.Contains(value, "transport=") { return errors.New(fieldName + " should not contain transport parameter") } if strings.ContainsAny(value, "@;") || strings.HasPrefix(value, "sip:") || strings.HasPrefix(value, "sips:") { return errors.New(fieldName + " should be a hostname or IP, not SIP URI") } return nil } func (p *SIPTrunkInfo) Validate() error { if len(p.InboundNumbersRegex) != 0 { return fmt.Errorf("trunks with InboundNumbersRegex are deprecated") } return nil } func (p *CreateSIPOutboundTrunkRequest) Validate() error { if p.Trunk == nil { return errors.New("missing trunk") } if p.Trunk.SipTrunkId != "" { return errors.New("trunk id must not be set") } if err := p.Trunk.Validate(); err != nil { return err } return nil } func (p *CreateSIPInboundTrunkRequest) Validate() error { if p.Trunk == nil { return errors.New("missing trunk") } if p.Trunk.SipTrunkId != "" { return errors.New("trunk id must not be set") } if err := p.Trunk.Validate(); err != nil { return err } return nil } func (p *UpdateSIPOutboundTrunkRequest) Validate() error { if p.SipTrunkId == "" { return errors.New("trunk id must be set") } if p.Action == nil { return errors.New("missing or unsupported update action") } switch a := p.Action.(type) { default: return nil case *UpdateSIPOutboundTrunkRequest_Replace: info := a.Replace if info == nil { return errors.New("missing trunk") } if info.SipTrunkId != "" && info.SipTrunkId != p.SipTrunkId { return errors.New("trunk id in the info must be empty or match the id in the update") } return info.Validate() case *UpdateSIPOutboundTrunkRequest_Update: diff := a.Update if diff == nil { return errors.New("missing trunk update") } return diff.Validate() } } func (p *UpdateSIPInboundTrunkRequest) Validate() error { if p.SipTrunkId == "" { return errors.New("trunk id must be set") } if p.Action == nil { return errors.New("missing or unsupported update action") } switch a := p.Action.(type) { default: return nil case *UpdateSIPInboundTrunkRequest_Replace: info := a.Replace if info == nil { return errors.New("missing trunk") } if info.SipTrunkId != "" && info.SipTrunkId != p.SipTrunkId { return errors.New("trunk id in the info must be empty or match the id in the update") } return info.Validate() case *UpdateSIPInboundTrunkRequest_Update: diff := a.Update if diff == nil { return errors.New("missing trunk update") } return diff.Validate() } } func (p *SIPInboundTrunkInfo) Validate() error { hasAuth := p.AuthUsername != "" || p.AuthPassword != "" hasCIDR := len(p.AllowedAddresses) != 0 hasNumbers := len(p.Numbers) != 0 // TODO: remove this condition, it doesn't really help with security if !hasAuth && !hasCIDR && !hasNumbers { return errors.New("for security, one of the fields must be set: AuthUsername+AuthPassword, AllowedAddresses or Numbers") } if err := validateHeaders(p.Headers); err != nil { return err } if err := validateAttributesToHeaders(p.AttributesToHeaders); err != nil { return err } if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil { return err } return nil } func (p *SIPInboundTrunkUpdate) Validate() error { if err := p.Numbers.Validate(); err != nil { return err } if err := p.AllowedAddresses.Validate(); err != nil { return err } if err := p.AllowedNumbers.Validate(); err != nil { return err } return nil } func (p *SIPInboundTrunkUpdate) Apply(info *SIPInboundTrunkInfo) error { if err := p.Validate(); err != nil { return err } applyListUpdate(&info.Numbers, p.Numbers) applyListUpdate(&info.AllowedAddresses, p.AllowedAddresses) applyListUpdate(&info.AllowedNumbers, p.AllowedNumbers) applyUpdate(&info.AuthUsername, p.AuthUsername) applyUpdate(&info.AuthPassword, p.AuthPassword) applyUpdate(&info.Name, p.Name) applyUpdate(&info.Metadata, p.Metadata) applyUpdate(&info.MediaEncryption, p.MediaEncryption) return info.Validate() } type UpdateSIPOutboundTrunkRequestAction interface { isUpdateSIPOutboundTrunkRequest_Action Apply(info *SIPOutboundTrunkInfo) (*SIPOutboundTrunkInfo, error) } var ( _ UpdateSIPOutboundTrunkRequestAction = (*UpdateSIPOutboundTrunkRequest_Replace)(nil) _ UpdateSIPOutboundTrunkRequestAction = (*UpdateSIPOutboundTrunkRequest_Update)(nil) ) func (p *UpdateSIPOutboundTrunkRequest_Replace) Apply(info *SIPOutboundTrunkInfo) (*SIPOutboundTrunkInfo, error) { val := cloneProto(p.Replace) if val == nil { return nil, errors.New("missing trunk") } if info.SipTrunkId != "" { val.SipTrunkId = info.SipTrunkId } if err := val.Validate(); err != nil { return nil, err } return val, nil } func (p *UpdateSIPOutboundTrunkRequest_Update) Apply(info *SIPOutboundTrunkInfo) (*SIPOutboundTrunkInfo, error) { diff := p.Update if diff == nil { return nil, errors.New("missing trunk update") } val := cloneProto(info) if err := diff.Apply(val); err != nil { return nil, err } return val, nil } func (p *SIPOutboundTrunkInfo) Validate() error { if len(p.Numbers) == 0 { return errors.New("no trunk numbers specified") } if p.Address == "" { return errors.New("no outbound address specified") } if err := validateHostnameFormat(p.Address, "trunk address"); err != nil { return err } if err := validateHostnameFormat(p.FromHost, "from_host"); err != nil { return err } if err := validateHeaders(p.Headers); err != nil { return err } if err := validateAttributesToHeaders(p.AttributesToHeaders); err != nil { return err } if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil { return err } return nil } func (p *SIPOutboundConfig) Validate() error { if p.Hostname == "" { return errors.New("no outbound hostname specified") } if err := validateHostnameFormat(p.Hostname, "trunk hostname"); err != nil { return err } if err := validateHostnameFormat(p.FromHost, "from_host"); err != nil { return err } if err := validateAttributesToHeaders(p.AttributesToHeaders); err != nil { return err } if err := validateHeaderToAttributes(p.HeadersToAttributes); err != nil { return err } return nil } func (p *SIPOutboundTrunkUpdate) Validate() error { if err := p.Numbers.Validate(); err != nil { return err } return nil } func (p *SIPOutboundTrunkUpdate) Apply(info *SIPOutboundTrunkInfo) error { if err := p.Validate(); err != nil { return err } applyUpdate(&info.Address, p.Address) applyUpdate(&info.Transport, p.Transport) applyUpdate(&info.DestinationCountry, p.DestinationCountry) applyListUpdate(&info.Numbers, p.Numbers) applyUpdate(&info.AuthUsername, p.AuthUsername) applyUpdate(&info.AuthPassword, p.AuthPassword) applyUpdate(&info.Name, p.Name) applyUpdate(&info.Metadata, p.Metadata) applyUpdate(&info.MediaEncryption, p.MediaEncryption) applyUpdate(&info.FromHost, p.FromHost) return info.Validate() } type UpdateSIPInboundTrunkRequestAction interface { isUpdateSIPInboundTrunkRequest_Action Apply(info *SIPInboundTrunkInfo) (*SIPInboundTrunkInfo, error) } var ( _ UpdateSIPInboundTrunkRequestAction = (*UpdateSIPInboundTrunkRequest_Replace)(nil) _ UpdateSIPInboundTrunkRequestAction = (*UpdateSIPInboundTrunkRequest_Update)(nil) ) func (p *UpdateSIPInboundTrunkRequest_Replace) Apply(info *SIPInboundTrunkInfo) (*SIPInboundTrunkInfo, error) { val := cloneProto(p.Replace) if val == nil { return nil, errors.New("missing trunk") } if info.SipTrunkId != "" { val.SipTrunkId = info.SipTrunkId } if err := val.Validate(); err != nil { return nil, err } return val, nil } func (p *UpdateSIPInboundTrunkRequest_Update) Apply(info *SIPInboundTrunkInfo) (*SIPInboundTrunkInfo, error) { diff := p.Update if diff == nil { return nil, errors.New("missing trunk update") } val := cloneProto(info) if err := diff.Apply(val); err != nil { return nil, err } return val, nil } func (p *CreateSIPDispatchRuleRequest) DispatchRuleInfo() *SIPDispatchRuleInfo { if p == nil { return nil } if p.DispatchRule != nil { return p.DispatchRule } return &SIPDispatchRuleInfo{ Rule: p.Rule, TrunkIds: p.TrunkIds, InboundNumbers: p.InboundNumbers, HidePhoneNumber: p.HidePhoneNumber, Name: p.Name, Metadata: p.Metadata, Attributes: p.Attributes, RoomPreset: p.RoomPreset, RoomConfig: p.RoomConfig, } } func (p *CreateSIPDispatchRuleRequest) Validate() error { if p.DispatchRule == nil { // legacy return p.DispatchRuleInfo().Validate() } if p.DispatchRule.SipDispatchRuleId != "" { return errors.New("rule id must not be set") } return p.DispatchRule.Validate() } func (p *UpdateSIPDispatchRuleRequest) Validate() error { if p.SipDispatchRuleId == "" { return errors.New("rule id must be set") } if p.Action == nil { return errors.New("missing or unsupported update action") } switch a := p.Action.(type) { default: return nil case *UpdateSIPDispatchRuleRequest_Replace: info := a.Replace if info == nil { return errors.New("missing dispatch rule") } if info.SipDispatchRuleId != "" && info.SipDispatchRuleId != p.SipDispatchRuleId { return errors.New("rule id in the info must be empty or match the id in the update") } return info.Validate() case *UpdateSIPDispatchRuleRequest_Update: diff := a.Update if diff == nil { return errors.New("missing dispatch rule update") } return diff.Validate() } } func (p *SIPDispatchRuleInfo) Validate() error { if p.Rule == nil { return errors.New("missing rule") } return nil } func (p *SIPDispatchRuleUpdate) Validate() error { if err := p.TrunkIds.Validate(); err != nil { return err } return nil } func (p *SIPDispatchRuleUpdate) Apply(info *SIPDispatchRuleInfo) error { if err := p.Validate(); err != nil { return err } applyListUpdate(&info.TrunkIds, p.TrunkIds) applyUpdatePtr(&info.Rule, p.Rule) applyUpdate(&info.Name, p.Name) applyUpdate(&info.Metadata, p.Metadata) applyUpdate(&info.MediaEncryption, p.MediaEncryption) applyMapDiff(&info.Attributes, p.Attributes) return info.Validate() } type UpdateSIPDispatchRuleRequestAction interface { isUpdateSIPDispatchRuleRequest_Action Apply(info *SIPDispatchRuleInfo) (*SIPDispatchRuleInfo, error) } var ( _ UpdateSIPDispatchRuleRequestAction = (*UpdateSIPDispatchRuleRequest_Replace)(nil) _ UpdateSIPDispatchRuleRequestAction = (*UpdateSIPDispatchRuleRequest_Update)(nil) ) func (p *UpdateSIPDispatchRuleRequest_Replace) Apply(info *SIPDispatchRuleInfo) (*SIPDispatchRuleInfo, error) { val := cloneProto(p.Replace) if val == nil { return nil, errors.New("missing dispatch rule") } if info.SipDispatchRuleId != "" { val.SipDispatchRuleId = info.SipDispatchRuleId } if err := val.Validate(); err != nil { return nil, err } return val, nil } func (p *UpdateSIPDispatchRuleRequest_Update) Apply(info *SIPDispatchRuleInfo) (*SIPDispatchRuleInfo, error) { diff := p.Update if diff == nil { return nil, errors.New("missing dispatch rule update") } val := cloneProto(info) if err := diff.Apply(val); err != nil { return nil, err } return val, nil } func (p *CreateSIPParticipantRequest) Validate() error { if p.SipTrunkId == "" && p.Trunk == nil { return errors.New("missing sip trunk id") } if p.Trunk != nil { if err := p.Trunk.Validate(); err != nil { return err } } if p.SipCallTo == "" { return errors.New("missing sip callee number") } else if strings.Contains(p.SipCallTo, "@") { return errors.New("SipCallTo should be a phone number or SIP user, not a full SIP URI") } if p.RoomName == "" { return errors.New("missing room name") } // Validate display_name if provided if p.DisplayName != nil { if len(*p.DisplayName) > 128 { return errors.New("DisplayName too long (max 128 characters)") } if _, err := strconv.Unquote("\"" + *p.DisplayName + "\""); err != nil { return fmt.Errorf("DisplayName must be a valid quoted string: %w", err) } } // Validate destination if provided if err := p.Destination.Validate(); err != nil { return err } if err := validateHeaders(p.Headers); err != nil { return err } return nil } func (d *Destination) Validate() error { if d == nil { return nil } // Rule 1: If city is specified, country must be specified if d.City != "" && d.Country == "" && d.Region == "" { return errors.New("if city is specified, country or region must also be specified") } // Rule 2: If country is specified, it must be a valid ISO 3166-1 alpha-2 code (2-letter only) if d.Country != "" { // First check: must be exactly 2 characters if len(d.Country) != 2 { return errors.New("country must be a valid ISO 3166-1 alpha-2 code (2-letter like 'US', 'IN', 'UK')") } // Use golang.org/x/text/language to validate 2-letter country codes region, err := language.ParseRegion(d.Country) if err != nil { return errors.New("country must be a valid ISO 3166-1 alpha-2 code (2-letter like 'US', 'IN', 'UK')") } // Check if the parsed region is actually a valid country // This is the most direct way to validate - region.IsCountry() returns true // only for actual valid countries, false for invalid codes like "XX" if !region.IsCountry() { return errors.New("country must be a valid ISO 3166-1 alpha-2 code (2-letter like 'US', 'IN', 'UK')") } // Additional check: ensure the parsed region matches our input // This prevents auto corrections by the library if region.String() != d.Country { return errors.New("country must be a valid ISO 3166-1 alpha-2 code (2-letter like 'US', 'IN', 'UK')") } } return nil } func (p *TransferSIPParticipantRequest) Validate() error { if p.RoomName == "" { return errors.New("missing room name") } if p.ParticipantIdentity == "" { return errors.New("missing participant identity") } if p.TransferTo == "" { return errors.New("missing transfer to") } // Validate TransferTo URI format and ensure RFC compliance var innerURI string if strings.HasPrefix(p.TransferTo, "<") && strings.HasSuffix(p.TransferTo, ">") { // Extract inner URI for validation innerURI = p.TransferTo[1 : len(p.TransferTo)-1] } else { innerURI = p.TransferTo } if !strings.HasPrefix(innerURI, "sip:") && !strings.HasPrefix(innerURI, "sips:") && !strings.HasPrefix(innerURI, "tel:") { // In theory the Refer-To header can receive the full name-addr. // This can make this check inaccurate, but we want to limit to just SIP and TEL URIs. return errors.New("transfer_to must be a valid SIP(s) or TEL URI (sip:, sips: or tel:)") } if strings.HasPrefix(innerURI, "sip:") || strings.HasPrefix(innerURI, "sips:") { // addr-spec = sip:... // name-addr = [ display-name ] // Both name-addr and addr-spec are allowed in RFC3515 (section-2.1). // However, name-addr is more premissive and widely-supported, so we convert. p.TransferTo = fmt.Sprintf("<%s>", innerURI) } else { // tel: URIs are not explicitly allowed in spec, but are generally supported. p.TransferTo = innerURI } if err := validateHeaders(p.Headers); err != nil { return err } return nil } func filterSlice[T any](arr []T, fnc func(v T) bool) []T { var out []T for _, v := range arr { if fnc(v) { out = append(out, v) } } return out } func filterIDs[T any, ID comparable](arr []T, ids []ID, get func(v T) ID) []T { if len(ids) == 0 { return arr } out := make([]T, len(ids)) for i, id := range ids { j := slices.IndexFunc(arr, func(v T) bool { return get(v) == id }) if j >= 0 { out[i] = arr[j] } } return out } func (p *ListSIPTrunkRequest) Filter(info *SIPTrunkInfo) bool { if info == nil { return true // for FilterSlice to work correctly with missing IDs } return true } func (p *ListSIPInboundTrunkRequest) Filter(info *SIPInboundTrunkInfo) bool { if info == nil { return true // for FilterSlice to work correctly with missing IDs } if len(p.TrunkIds) != 0 && !slices.Contains(p.TrunkIds, info.SipTrunkId) { return false } if len(p.Numbers) != 0 && len(info.Numbers) != 0 { ok := false for _, num := range info.Numbers { if slices.Contains(p.Numbers, num) { ok = true break } normalizedNum := NormalizeNumber(num) for _, reqNum := range p.Numbers { if NormalizeNumber(reqNum) == normalizedNum { ok = true break } } if ok { break } } if !ok { return false } } return true } func (p *ListSIPInboundTrunkRequest) FilterSlice(arr []*SIPInboundTrunkInfo) []*SIPInboundTrunkInfo { arr = filterIDs(arr, p.TrunkIds, func(v *SIPInboundTrunkInfo) string { return v.SipTrunkId }) return filterSlice(arr, p.Filter) } func (p *ListSIPOutboundTrunkRequest) Filter(info *SIPOutboundTrunkInfo) bool { if info == nil { return true // for FilterSlice to work correctly with missing IDs } if len(p.TrunkIds) != 0 && !slices.Contains(p.TrunkIds, info.SipTrunkId) { return false } if len(p.Numbers) != 0 && len(info.Numbers) != 0 { ok := false for _, num := range info.Numbers { if slices.Contains(p.Numbers, num) { ok = true break } } if !ok { return false } } return true } func (p *ListSIPOutboundTrunkRequest) FilterSlice(arr []*SIPOutboundTrunkInfo) []*SIPOutboundTrunkInfo { arr = filterIDs(arr, p.TrunkIds, func(v *SIPOutboundTrunkInfo) string { return v.SipTrunkId }) return filterSlice(arr, p.Filter) } func (p *ListSIPDispatchRuleRequest) Filter(info *SIPDispatchRuleInfo) bool { if info == nil { return true // for FilterSlice to work correctly with missing IDs } if len(p.DispatchRuleIds) != 0 && !slices.Contains(p.DispatchRuleIds, info.SipDispatchRuleId) { return false } if len(p.TrunkIds) != 0 && len(info.TrunkIds) != 0 { ok := false for _, id := range info.TrunkIds { if slices.Contains(p.TrunkIds, id) { ok = true break } } if !ok { return false } } return true } func (p *ListSIPDispatchRuleRequest) FilterSlice(arr []*SIPDispatchRuleInfo) []*SIPDispatchRuleInfo { arr = filterIDs(arr, p.DispatchRuleIds, func(v *SIPDispatchRuleInfo) string { return v.SipDispatchRuleId }) return filterSlice(arr, p.Filter) } // NormalizeNumber normalizes a phone number by removing formatting characters and ensuring it starts with a "+". // If the input is empty, it returns an empty string. // If the input doesn't match the expected number pattern, it returns the original input unchanged. func NormalizeNumber(num string) string { if num == "" { return "" } if !reNumber.MatchString(num) { return num } num = reNumberRepl.Replace(num) if !strings.HasPrefix(num, "+") { return "+" + num } return num } var ( reNumber = regexp.MustCompile(`^\+?[\d\- ()]+$`) reNumberRepl = strings.NewReplacer( " ", "", "-", "", "(", "", ")", "", ) )