From dd024571a1e4a9002e681bcb71855e8ca79986fc Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Thu, 30 Nov 2023 19:46:24 +0200 Subject: [PATCH] SIP: Move dispatch rule evaluation to protocol package. (#2279) --- go.mod | 2 +- go.sum | 4 +- pkg/service/ioservice_sip.go | 296 ++--------------------- pkg/service/ioservice_sip_test.go | 390 ------------------------------ 4 files changed, 21 insertions(+), 671 deletions(-) delete mode 100644 pkg/service/ioservice_sip_test.go diff --git a/go.mod b/go.mod index 558fd67d9..8a05272f4 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/jxskiss/base62 v1.1.0 github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 github.com/livekit/mediatransportutil v0.0.0-20231128042044-05525c8278cb - github.com/livekit/protocol v1.9.3-0.20231129173544-1c3f5fe919b0 + github.com/livekit/protocol v1.9.3-0.20231130173607-ec88d89da1d3 github.com/livekit/psrpc v0.5.2 github.com/mackerelio/go-osstat v0.2.4 github.com/magefile/mage v1.15.0 diff --git a/go.sum b/go.sum index a1b81d76c..5193e6f52 100644 --- a/go.sum +++ b/go.sum @@ -125,8 +125,8 @@ github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkD github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ= github.com/livekit/mediatransportutil v0.0.0-20231128042044-05525c8278cb h1:KiGg4k+kYQD9NjKixaSDMMeYOO2//XBM4IROTI1Itjo= github.com/livekit/mediatransportutil v0.0.0-20231128042044-05525c8278cb/go.mod h1:GBzn9xL+mivI1pW+tyExcKgbc0VOc29I9yJsNcAVaAc= -github.com/livekit/protocol v1.9.3-0.20231129173544-1c3f5fe919b0 h1:AhJlQejQ+Ma9Q+EPqCNt2S7h6ETJXDiO7qsQdTq9VvM= -github.com/livekit/protocol v1.9.3-0.20231129173544-1c3f5fe919b0/go.mod h1:8f342d5nvfNp9YAEfJokSR+zbNFpaivgU0h6vwaYhes= +github.com/livekit/protocol v1.9.3-0.20231130173607-ec88d89da1d3 h1:am72beYtXZM71MRr+12lkG3IyqKxzrCa6slsbKrwMe8= +github.com/livekit/protocol v1.9.3-0.20231130173607-ec88d89da1d3/go.mod h1:8f342d5nvfNp9YAEfJokSR+zbNFpaivgU0h6vwaYhes= github.com/livekit/psrpc v0.5.2 h1:+MvG8Otm/J6MTg2MP/uuMbrkxOWsrj2hDhu/I1VIU1U= github.com/livekit/psrpc v0.5.2/go.mod h1:cQjxg1oCxYHhxxv6KJH1gSvdtCHQoRZCHgPdm5N8v2g= github.com/mackerelio/go-osstat v0.2.4 h1:qxGbdPkFo65PXOb/F/nhDKpF2nGmGaCFDLXoZjJTtUs= diff --git a/pkg/service/ioservice_sip.go b/pkg/service/ioservice_sip.go index 3f0b34042..076bff512 100644 --- a/pkg/service/ioservice_sip.go +++ b/pkg/service/ioservice_sip.go @@ -1,252 +1,27 @@ +// Copyright 2023 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package service import ( "context" - "fmt" - "math" - "regexp" - "sort" "github.com/livekit/protocol/livekit" - "github.com/livekit/protocol/logger" "github.com/livekit/protocol/rpc" + "github.com/livekit/protocol/sip" ) -// sipRulePriority returns sorting priority for dispatch rules. Lower value means higher priority. -func sipRulePriority(info *livekit.SIPDispatchRuleInfo) int32 { - // In all these cases, prefer pin-protected rules. - // Thus, the order will be the following: - // - 0: Direct or Pin (both pin-protected) - // - 1: Individual (pin-protected) - // - 100: Direct (open) - // - 101: Individual (open) - const ( - last = math.MaxInt32 - ) - // TODO: Maybe allow setting specific priorities for dispatch rules? - switch rule := info.GetRule().GetRule().(type) { - default: - return last - case *livekit.SIPDispatchRule_DispatchRuleDirect: - if rule.DispatchRuleDirect.GetPin() != "" { - return 0 - } - return 100 - case *livekit.SIPDispatchRule_DispatchRuleIndividual: - if rule.DispatchRuleIndividual.GetPin() != "" { - return 1 - } - return 101 - } -} - -// sipSortRules predictably sorts dispatch rules by priority (first one is highest). -func sipSortRules(rules []*livekit.SIPDispatchRuleInfo) { - sort.Slice(rules, func(i, j int) bool { - p1, p2 := sipRulePriority(rules[i]), sipRulePriority(rules[j]) - if p1 < p2 { - return true - } else if p1 > p2 { - return false - } - // For predictable sorting order. - room1, _, _ := sipGetPinAndRoom(rules[i]) - room2, _, _ := sipGetPinAndRoom(rules[j]) - return room1 < room2 - }) -} - -// sipSelectDispatch takes a list of dispatch rules, and takes the decision which one should be selected. -// It returns an error if there are conflicting rules. Returns nil if no rules match. -func sipSelectDispatch(rules []*livekit.SIPDispatchRuleInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) { - if len(rules) == 0 { - return nil, nil - } - // Sorting will do the selection for us. We already filtered out irrelevant ones in matchSIPDispatchRule. - sipSortRules(rules) - byPin := make(map[string]*livekit.SIPDispatchRuleInfo) - var ( - pinRule *livekit.SIPDispatchRuleInfo - openRule *livekit.SIPDispatchRuleInfo - ) - openCnt := 0 - for _, r := range rules { - _, pin, err := sipGetPinAndRoom(r) - if err != nil { - return nil, err - } - if pin == "" { - openRule = r // last one - openCnt++ - } else if r2 := byPin[pin]; r2 != nil { - return nil, fmt.Errorf("Conflicting SIP Dispatch Rules: Same PIN for %q and %q", - r.SipDispatchRuleId, r2.SipDispatchRuleId) - } else { - byPin[pin] = r - // Pick the first one with a Pin. If Pin was provided in the request, we already filtered the right rules. - // If not, this rule will just be used to send RequestPin=true flag. - if pinRule == nil { - pinRule = r - } - } - } - if req.GetPin() != "" { - // If it's still nil that's fine. We will report "no rules matched" later. - return pinRule, nil - } - if pinRule != nil { - return pinRule, nil - } - if openCnt > 1 { - return nil, fmt.Errorf("Conflicting SIP Dispatch Rules: Matched %d open rules for %q", openCnt, req.CallingNumber) - } - return openRule, nil -} - -// sipGetPinAndRoom returns a room name/prefix and the pin for a dispatch rule. Just a convenience wrapper. -func sipGetPinAndRoom(info *livekit.SIPDispatchRuleInfo) (room, pin string, err error) { - // TODO: Could probably add methods on SIPDispatchRuleInfo struct instead. - switch rule := info.GetRule().GetRule().(type) { - default: - return "", "", fmt.Errorf("Unsupported SIP Dispatch Rule: %T", rule) - case *livekit.SIPDispatchRule_DispatchRuleDirect: - pin = rule.DispatchRuleDirect.GetPin() - room = rule.DispatchRuleDirect.GetRoomName() - case *livekit.SIPDispatchRule_DispatchRuleIndividual: - pin = rule.DispatchRuleIndividual.GetPin() - room = rule.DispatchRuleIndividual.GetRoomPrefix() - } - return room, pin, nil -} - -// sipMatchTrunk finds a SIP Trunk definition matching the request. -// Returns nil if no rules matched or an error if there are conflicting definitions. -func sipMatchTrunk(trunks []*livekit.SIPTrunkInfo, calling, called string) (*livekit.SIPTrunkInfo, error) { - var ( - selectedTrunk *livekit.SIPTrunkInfo - defaultTrunk *livekit.SIPTrunkInfo - defaultTrunkCnt int // to error in case there are multiple ones - ) - for _, tr := range trunks { - // Do not consider it if regexp doesn't match. - matches := len(tr.InboundNumbersRegex) == 0 - for _, reStr := range tr.InboundNumbersRegex { - // TODO: we should cache it - re, err := regexp.Compile(reStr) - if err != nil { - logger.Errorw("cannot parse SIP trunk regexp", err, "trunkID", tr.SipTrunkId) - continue - } - if re.MatchString(calling) { - matches = true - break - } - } - if !matches { - continue - } - if tr.OutboundNumber == "" { - // Default/wildcard trunk. - defaultTrunk = tr - defaultTrunkCnt++ - } else if tr.OutboundNumber == called { - // Trunk specific to the number. - if selectedTrunk != nil { - return nil, fmt.Errorf("Multiple SIP Trunks matched for %q", called) - } - selectedTrunk = tr - // Keep searching! We want to know if there are any conflicting Trunk definitions. - } - } - if selectedTrunk != nil { - return selectedTrunk, nil - } - if defaultTrunkCnt > 1 { - return nil, fmt.Errorf("Multiple default SIP Trunks matched for %q", called) - } - // Could still be nil here. - return defaultTrunk, nil -} - -// sipMatchDispatchRule finds the best dispatch rule matching the request parameters. Returns an error if no rule matched. -// Trunk parameter can be nil, in which case only wildcard dispatch rules will be effective (ones without Trunk IDs). -func sipMatchDispatchRule(trunk *livekit.SIPTrunkInfo, rules []*livekit.SIPDispatchRuleInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) { - // Trunk can still be nil here in case none matched or were defined. - // This is still fine, but only in case we'll match exactly one wildcard dispatch rule. - if len(rules) == 0 { - return nil, fmt.Errorf("No SIP Dispatch Rules defined") - } - // We split the matched dispatch rules into two sets: specific and default (aka wildcard). - // First, attempt to match any of the specific rules, where we did match the Trunk ID. - // If nothing matches there - fallback to default/wildcard rules, where no Trunk IDs were mentioned. - var ( - specificRules []*livekit.SIPDispatchRuleInfo - defaultRules []*livekit.SIPDispatchRuleInfo - ) - noPin := req.NoPin - sentPin := req.GetPin() - for _, info := range rules { - _, rulePin, err := sipGetPinAndRoom(info) - if err != nil { - logger.Errorw("Invalid SIP Dispatch Rule", err, "dispatchRuleID", info.SipDispatchRuleId) - continue - } - // Filter heavily on the Pin, so that only relevant rules remain. - if noPin { - if rulePin != "" { - // Skip pin-protected rules if no pin mode requested. - continue - } - } else if sentPin != "" { - if rulePin == "" { - // Pin already sent, skip non-pin-protected rules. - continue - } - if sentPin != rulePin { - // Pin doesn't match. Don't return an error here, just wait for other rule to match (or none at all). - // Note that we will NOT match non-pin-protected rules, thus it will not fallback to open rules. - continue - } - } - if len(info.TrunkIds) == 0 { - // Default/wildcard dispatch rule. - defaultRules = append(defaultRules, info) - continue - } - // Specific dispatch rules. Require a Trunk associated with the number. - if trunk == nil { - continue - } - matches := false - for _, id := range info.TrunkIds { - if id == trunk.SipTrunkId { - matches = true - break - } - } - if !matches { - continue - } - specificRules = append(specificRules, info) - } - best, err := sipSelectDispatch(specificRules, req) - if err != nil { - return nil, err - } else if best != nil { - return best, nil - } - best, err = sipSelectDispatch(defaultRules, req) - if err != nil { - return nil, err - } else if best != nil { - return best, nil - } - if trunk == nil { - return nil, fmt.Errorf("No SIP Trunk or Dispatch Rules matched for %q", req.CalledNumber) - } - return nil, fmt.Errorf("No SIP Dispatch Rules matched for %q", req.CalledNumber) -} - // matchSIPTrunk finds a SIP Trunk definition matching the request. // Returns nil if no rules matched or an error if there are conflicting definitions. func (s *IOInfoService) matchSIPTrunk(ctx context.Context, calling, called string) (*livekit.SIPTrunkInfo, error) { @@ -254,7 +29,7 @@ func (s *IOInfoService) matchSIPTrunk(ctx context.Context, calling, called strin if err != nil { return nil, err } - return sipMatchTrunk(trunks, calling, called) + return sip.MatchTrunk(trunks, calling, called) } // matchSIPDispatchRule finds the best dispatch rule matching the request parameters. Returns an error if no rule matched. @@ -266,7 +41,7 @@ func (s *IOInfoService) matchSIPDispatchRule(ctx context.Context, trunk *livekit if err != nil { return nil, err } - return sipMatchDispatchRule(trunk, rules, req) + return sip.MatchDispatchRule(trunk, rules, req) } func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) { @@ -278,42 +53,7 @@ func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.E if err != nil { return nil, err } - sentPin := req.GetPin() - - from := req.CallingNumber - if best.HidePhoneNumber { - // TODO: Decide on the phone masking format. - // Maybe keep regional code, but mask all but 4 last digits? - from = from[len(from)-4:] - } - fromName := "Phone " + from - - room, rulePin, err := sipGetPinAndRoom(best) - if err != nil { - return nil, err - } - if rulePin != "" { - if sentPin == "" { - return &rpc.EvaluateSIPDispatchRulesResponse{ - RequestPin: true, - }, nil - } - if rulePin != sentPin { - // This should never happen in practice, because matchSIPDispatchRule should remove rules with the wrong pin. - return nil, fmt.Errorf("Incorrect PIN for SIP room") - } - } else { - // Pin was sent, but room doesn't require one. Assume user accidentally pressed phone button. - } - switch rule := best.GetRule().GetRule().(type) { - case *livekit.SIPDispatchRule_DispatchRuleIndividual: - // TODO: Decide on the suffix. Do we need to escape specific characters? - room = rule.DispatchRuleIndividual.GetRoomPrefix() + from - } - return &rpc.EvaluateSIPDispatchRulesResponse{ - RoomName: room, - ParticipantIdentity: fromName, - }, nil + return sip.EvaluateDispatchRule(best, req) } func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc.GetSIPTrunkAuthenticationRequest) (*rpc.GetSIPTrunkAuthenticationResponse, error) { diff --git a/pkg/service/ioservice_sip_test.go b/pkg/service/ioservice_sip_test.go deleted file mode 100644 index b8b57fd07..000000000 --- a/pkg/service/ioservice_sip_test.go +++ /dev/null @@ -1,390 +0,0 @@ -package service - -import ( - "fmt" - "testing" - - "github.com/livekit/protocol/livekit" - "github.com/livekit/protocol/rpc" - "github.com/stretchr/testify/require" -) - -const ( - sipNumber1 = "1111 1111" - sipNumber2 = "2222 2222" - sipNumber3 = "3333 3333" - sipTrunkID1 = "aaa" - sipTrunkID2 = "bbb" -) - -func TestSIPMatchTrunk(t *testing.T) { - cases := []struct { - name string - trunks []*livekit.SIPTrunkInfo - exp int - expErr bool - }{ - { - name: "empty", - trunks: nil, - exp: -1, // no error; nil result - }, - { - name: "one wildcard", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa"}, - }, - exp: 0, - }, - { - name: "matching", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber2}, - }, - exp: 0, - }, - { - name: "matching regexp", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+ \d+$`}}, - }, - exp: 0, - }, - { - name: "not matching", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, - }, - exp: -1, - }, - { - name: "not matching regexp", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+$`}}, - }, - exp: -1, - }, - { - name: "one match", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, - {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, - }, - exp: 1, - }, - { - name: "many matches", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, - {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, - {SipTrunkId: "ccc", OutboundNumber: sipNumber2}, - }, - expErr: true, - }, - { - name: "many matches default", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, - {SipTrunkId: "bbb"}, - {SipTrunkId: "ccc", OutboundNumber: sipNumber2}, - {SipTrunkId: "ddd"}, - }, - exp: 2, - }, - { - name: "regexp", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, - {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, - {SipTrunkId: "ccc", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+$`}}, - }, - exp: 1, - }, - { - name: "multiple defaults", - trunks: []*livekit.SIPTrunkInfo{ - {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, - {SipTrunkId: "bbb"}, - {SipTrunkId: "ccc"}, - }, - expErr: true, - }, - } - for _, c := range cases { - c := c - t.Run(c.name, func(t *testing.T) { - got, err := sipMatchTrunk(c.trunks, sipNumber1, sipNumber2) - if c.expErr { - require.Error(t, err) - require.Nil(t, got) - t.Log(err) - } else { - var exp *livekit.SIPTrunkInfo - if c.exp >= 0 { - exp = c.trunks[c.exp] - } - require.NoError(t, err) - require.Equal(t, exp, got) - } - }) - } -} - -func newSIPTrunkDispatch() *livekit.SIPTrunkInfo { - return &livekit.SIPTrunkInfo{ - SipTrunkId: sipTrunkID1, - OutboundNumber: sipNumber2, - } -} - -func newSIPReqDispatch(pin string, noPin bool) *rpc.EvaluateSIPDispatchRulesRequest { - return &rpc.EvaluateSIPDispatchRulesRequest{ - CallingNumber: sipNumber1, - CalledNumber: sipNumber2, - Pin: pin, - //NoPin: noPin, // TODO - } -} - -func newDirectDispatch(room, pin string) *livekit.SIPDispatchRule { - return &livekit.SIPDispatchRule{ - Rule: &livekit.SIPDispatchRule_DispatchRuleDirect{ - DispatchRuleDirect: &livekit.SIPDispatchRuleDirect{ - RoomName: room, Pin: pin, - }, - }, - } -} - -func newIndividualDispatch(roomPref, pin string) *livekit.SIPDispatchRule { - return &livekit.SIPDispatchRule{ - Rule: &livekit.SIPDispatchRule_DispatchRuleIndividual{ - DispatchRuleIndividual: &livekit.SIPDispatchRuleIndividual{ - RoomPrefix: roomPref, Pin: pin, - }, - }, - } -} - -func TestSIPMatchDispatchRule(t *testing.T) { - cases := []struct { - name string - trunk *livekit.SIPTrunkInfo - rules []*livekit.SIPDispatchRuleInfo - reqPin string - noPin bool - exp int - expErr bool - }{ - // These cases just validate that no rules produce an error. - { - name: "empty", - trunk: nil, - rules: nil, - expErr: true, - }, - { - name: "only trunk", - trunk: newSIPTrunkDispatch(), - rules: nil, - expErr: true, - }, - // Default rules should work even if no trunk is defined. - { - name: "one rule/no trunk", - trunk: nil, - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, - }, - exp: 0, - }, - // Default rule should work with a trunk too. - { - name: "one rule/default trunk", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, - }, - exp: 0, - }, - // Rule matching the trunk should be selected. - { - name: "one rule/specific trunk", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip", "")}, - }, - exp: 0, - }, - // Rule NOT matching the trunk should NOT be selected. - { - name: "one rule/wrong trunk", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: []string{"zzz"}, Rule: newDirectDispatch("sip", "")}, - }, - expErr: true, - }, - // Direct rule with a pin should be selected, even if no pin is provided. - { - name: "direct pin/correct", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")}, - {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")}, - }, - reqPin: "123", - exp: 0, - }, - // Direct rule with a pin should reject wrong pin. - { - name: "direct pin/wrong", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")}, - {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")}, - }, - reqPin: "zzz", - expErr: true, - }, - // Multiple direct rules with the same pin should result in an error. - { - name: "direct pin/conflict", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")}, - {TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")}, - }, - reqPin: "123", - expErr: true, - }, - // Multiple direct rules with the same pin on different trunks are ok. - { - name: "direct pin/no conflict on different trunk", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")}, - {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")}, - }, - reqPin: "123", - exp: 0, - }, - // Specific direct rules should take priority over default direct rules. - { - name: "direct pin/default and specific", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, - {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")}, - }, - reqPin: "123", - exp: 1, - }, - // Specific direct rules should take priority over default direct rules. No pin. - { - name: "direct/default and specific", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, - {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")}, - }, - exp: 1, - }, - // Specific direct rules should take priority over default direct rules. One with pin, other without. - { - name: "direct/default and specific/mixed 1", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, - {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")}, - }, - exp: 1, - }, - { - name: "direct/default and specific/mixed 2", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, - {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")}, - }, - exp: 1, - }, - // Multiple default direct rules are not allowed. - { - name: "direct/multiple defaults", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, - {TrunkIds: nil, Rule: newDirectDispatch("sip2", "")}, - }, - expErr: true, - }, - // Cannot use both direct and individual rules with the same pin setup. - { - name: "direct vs individual/private", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123")}, - {TrunkIds: nil, Rule: newDirectDispatch("sip", "123")}, - }, - expErr: true, - }, - { - name: "direct vs individual/open", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "")}, - {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, - }, - expErr: true, - }, - // Direct rules take priority over individual rules. - { - name: "direct vs individual/priority", - trunk: newSIPTrunkDispatch(), - rules: []*livekit.SIPDispatchRuleInfo{ - {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123")}, - {TrunkIds: nil, Rule: newDirectDispatch("sip", "456")}, - }, - reqPin: "456", - exp: 1, - }, - } - for _, c := range cases { - c := c - t.Run(c.name, func(t *testing.T) { - pins := []string{c.reqPin} - if !c.expErr && c.reqPin != "" { - // Should match the same rule, even if no pin is set (so that it can be requested). - pins = append(pins, "") - } - for i, r := range c.rules { - if r.SipDispatchRuleId == "" { - r.SipDispatchRuleId = fmt.Sprintf("rule_%d", i) - } - } - for _, pin := range pins { - pin := pin - name := pin - if name == "" { - name = "no pin" - } - t.Run(name, func(t *testing.T) { - got, err := sipMatchDispatchRule(c.trunk, c.rules, newSIPReqDispatch(pin, c.noPin)) - if c.expErr { - require.Error(t, err) - require.Nil(t, got) - t.Log(err) - } else { - var exp *livekit.SIPDispatchRuleInfo - if c.exp >= 0 { - exp = c.rules[c.exp] - } - require.NoError(t, err) - require.Equal(t, exp, got) - } - }) - } - }) - } -}