Initial SIP Dispatch Rules implementation (#2241)

This commit is contained in:
Denys Smirnov
2023-11-15 09:35:05 -05:00
committed by GitHub
parent 49ca1066cd
commit c6e467e903
3 changed files with 725 additions and 23 deletions
+2 -23
View File
@@ -17,15 +17,15 @@ package service
import (
"context"
"errors"
"fmt"
"google.golang.org/protobuf/types/known/emptypb"
"github.com/livekit/livekit-server/pkg/telemetry"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/psrpc"
"github.com/livekit/livekit-server/pkg/telemetry"
)
type IOInfoService struct {
@@ -215,27 +215,6 @@ func (s *IOInfoService) UpdateIngressState(ctx context.Context, req *rpc.UpdateI
return &emptypb.Empty{}, nil
}
func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
dispatchRules, err := s.ss.ListSIPDispatchRule(ctx)
if err != nil {
return nil, err
}
if len(dispatchRules) == 0 {
return nil, fmt.Errorf("No SIP Dispatch Rule Found")
}
directDispatchRule := dispatchRules[0].Rule.GetDispatchRuleDirect()
if directDispatchRule == nil {
return nil, fmt.Errorf("No SIP Direct Dispatch Rule Found")
}
return &rpc.EvaluateSIPDispatchRulesResponse{
RoomName: directDispatchRule.RoomName,
ParticipantIdentity: req.CallingNumber,
}, nil
}
func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc.GetSIPTrunkAuthenticationRequest) (*rpc.GetSIPTrunkAuthenticationResponse, error) {
return nil, nil
}
+329
View File
@@ -0,0 +1,329 @@
package service
import (
"context"
"fmt"
"math"
"regexp"
"sort"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
)
// 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_DispatchRulePin:
// TODO: If we assume that Pin is optional, this rule type is very similar to Direct. Could remove it?
return 0
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_DispatchRulePin:
pin = rule.DispatchRulePin.GetPin()
room = rule.DispatchRulePin.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, req *rpc.EvaluateSIPDispatchRulesRequest) (*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(req.CallingNumber) {
matches = true
break
}
}
if !matches {
continue
}
if tr.OutboundNumber == "" {
// Default/wildcard trunk.
defaultTrunk = tr
defaultTrunkCnt++
} else if tr.OutboundNumber == req.CalledNumber {
// Trunk specific to the number.
if selectedTrunk != nil {
return nil, fmt.Errorf("Multiple SIP Trunks matched for %q", req.CalledNumber)
}
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", req.CalledNumber)
}
// 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
)
// TODO: Apart from Pin, it would be nice to have a NoPin flag.
// The way it would work is that we will first list the rules and figure out if at least one has a Pin required.
// If it does, we will immediately respond with RequestPin=true. Now, on the SIP bridge side, we will run
// audio prompt asking for a Pin. The user will have an options to skip the pin (e.g. press #) and only try
// to match no-ping rooms. This will be very useful if only 1 number is available and has to route to both
// private and public rooms.
noPin := false
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, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPTrunkInfo, error) {
trunks, err := s.ss.ListSIPTrunk(ctx)
if err != nil {
return nil, err
}
return sipMatchTrunk(trunks, req)
}
// matchSIPDispatchRule 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 (s *IOInfoService) matchSIPDispatchRule(ctx context.Context, trunk *livekit.SIPTrunkInfo, 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.
rules, err := s.ss.ListSIPDispatchRule(ctx)
if err != nil {
return nil, err
}
return sipMatchDispatchRule(trunk, rules, req)
}
func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
trunk, err := s.matchSIPTrunk(ctx, req)
if err != nil {
return nil, err
}
best, err := s.matchSIPDispatchRule(ctx, trunk, req)
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
}
+394
View File
@@ -0,0 +1,394 @@
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) {
req := &rpc.EvaluateSIPDispatchRulesRequest{
CallingNumber: sipNumber1,
CalledNumber: sipNumber2,
}
got, err := sipMatchTrunk(c.trunks, req)
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)
}
})
}
})
}
}