mirror of
https://github.com/livekit/livekit.git
synced 2026-03-31 06:45:43 +00:00
SIP: Move dispatch rule evaluation to protocol package. (#2279)
This commit is contained in:
2
go.mod
2
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user