github.com/livekit/protocol@v1.16.1-0.20240517185851-47e4c6bba773/sip/sip.go (about)

     1  // Copyright 2023 LiveKit, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package sip
    16  
    17  import (
    18  	"fmt"
    19  	"math"
    20  	"regexp"
    21  	"sort"
    22  	"strconv"
    23  
    24  	"golang.org/x/exp/slices"
    25  
    26  	"github.com/livekit/protocol/livekit"
    27  	"github.com/livekit/protocol/logger"
    28  	"github.com/livekit/protocol/rpc"
    29  	"github.com/livekit/protocol/utils"
    30  )
    31  
    32  func NewCallID() string {
    33  	return utils.NewGuid(utils.SIPCallPrefix)
    34  }
    35  
    36  type ErrNoDispatchMatched struct {
    37  	NoRules      bool
    38  	NoTrunks     bool
    39  	CalledNumber string
    40  }
    41  
    42  func (e *ErrNoDispatchMatched) Error() string {
    43  	if e.NoRules {
    44  		return "No SIP Dispatch Rules defined"
    45  	}
    46  	if e.NoTrunks {
    47  		return fmt.Sprintf("No SIP Trunk or Dispatch Rules matched for %q", e.CalledNumber)
    48  	}
    49  	return fmt.Sprintf("No SIP Dispatch Rules matched for %q", e.CalledNumber)
    50  }
    51  
    52  // DispatchRulePriority returns sorting priority for dispatch rules. Lower value means higher priority.
    53  func DispatchRulePriority(info *livekit.SIPDispatchRuleInfo) int32 {
    54  	// In all these cases, prefer pin-protected rules and rules for specific calling number.
    55  	// Thus, the order will be the following:
    56  	// - 0: Direct or Pin (both pin-protected)
    57  	// - 1: Individual (pin-protected)
    58  	// - 100: Direct (open)
    59  	// - 101: Individual (open)
    60  	// Also, add 1K penalty for not specifying the calling number.
    61  	const (
    62  		last = math.MaxInt32
    63  	)
    64  	// TODO: Maybe allow setting specific priorities for dispatch rules?
    65  	priority := int32(0)
    66  	switch rule := info.GetRule().GetRule().(type) {
    67  	default:
    68  		return last
    69  	case *livekit.SIPDispatchRule_DispatchRuleDirect:
    70  		if rule.DispatchRuleDirect.GetPin() != "" {
    71  			priority = 0
    72  		} else {
    73  			priority = 100
    74  		}
    75  	case *livekit.SIPDispatchRule_DispatchRuleIndividual:
    76  		if rule.DispatchRuleIndividual.GetPin() != "" {
    77  			priority = 1
    78  		} else {
    79  			priority = 101
    80  		}
    81  	}
    82  	if len(info.InboundNumbers) == 0 {
    83  		priority += 1000
    84  	}
    85  	return priority
    86  }
    87  
    88  // SortDispatchRules predictably sorts dispatch rules by priority (first one is highest).
    89  func SortDispatchRules(rules []*livekit.SIPDispatchRuleInfo) {
    90  	sort.Slice(rules, func(i, j int) bool {
    91  		p1, p2 := DispatchRulePriority(rules[i]), DispatchRulePriority(rules[j])
    92  		if p1 < p2 {
    93  			return true
    94  		} else if p1 > p2 {
    95  			return false
    96  		}
    97  		// For predictable sorting order.
    98  		room1, _, _ := GetPinAndRoom(rules[i])
    99  		room2, _, _ := GetPinAndRoom(rules[j])
   100  		return room1 < room2
   101  	})
   102  }
   103  
   104  func printID(s string) string {
   105  	if s == "" {
   106  		return "<new>"
   107  	}
   108  	return s
   109  }
   110  
   111  // ValidateDispatchRules checks a set of dispatch rules for conflicts.
   112  func ValidateDispatchRules(rules []*livekit.SIPDispatchRuleInfo) error {
   113  	if len(rules) == 0 {
   114  		return nil
   115  	}
   116  	type ruleKey struct {
   117  		Pin    string
   118  		Trunk  string
   119  		Number string
   120  	}
   121  	byRuleKey := make(map[ruleKey]*livekit.SIPDispatchRuleInfo)
   122  	for _, r := range rules {
   123  		_, pin, err := GetPinAndRoom(r)
   124  		if err != nil {
   125  			return err
   126  		}
   127  		trunks := r.TrunkIds
   128  		if len(trunks) == 0 {
   129  			// This rule matches all trunks, but collides only with other default ones (specific rules take priority).
   130  			trunks = []string{""}
   131  		}
   132  		numbers := r.InboundNumbers
   133  		if len(numbers) == 0 {
   134  			// This rule matches all numbers, but collides only with other default ones (specific rules take priority).
   135  			numbers = []string{""}
   136  		}
   137  		for _, trunk := range trunks {
   138  			for _, number := range numbers {
   139  				key := ruleKey{Pin: pin, Trunk: trunk, Number: number}
   140  				r2 := byRuleKey[key]
   141  				if r2 != nil {
   142  					return fmt.Errorf("Conflicting SIP Dispatch Rules: same Trunk+Number+PIN combination for for %q and %q",
   143  						printID(r.SipDispatchRuleId), printID(r2.SipDispatchRuleId))
   144  				}
   145  				byRuleKey[key] = r
   146  			}
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  // SelectDispatchRule takes a list of dispatch rules, and takes the decision which one should be selected.
   153  // It returns an error if there are conflicting rules. Returns nil if no rules match.
   154  func SelectDispatchRule(rules []*livekit.SIPDispatchRuleInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) {
   155  	if len(rules) == 0 {
   156  		// Nil is fine here. We will report "no rules matched" later.
   157  		return nil, nil
   158  	}
   159  	if err := ValidateDispatchRules(rules); err != nil {
   160  		return nil, err
   161  	}
   162  	// Sorting will do the selection for us. We already filtered out irrelevant ones in MatchDispatchRule and above.
   163  	SortDispatchRules(rules)
   164  	return rules[0], nil
   165  }
   166  
   167  // GetPinAndRoom returns a room name/prefix and the pin for a dispatch rule. Just a convenience wrapper.
   168  func GetPinAndRoom(info *livekit.SIPDispatchRuleInfo) (room, pin string, err error) {
   169  	// TODO: Could probably add methods on SIPDispatchRuleInfo struct instead.
   170  	switch rule := info.GetRule().GetRule().(type) {
   171  	default:
   172  		return "", "", fmt.Errorf("Unsupported SIP Dispatch Rule: %T", rule)
   173  	case *livekit.SIPDispatchRule_DispatchRuleDirect:
   174  		pin = rule.DispatchRuleDirect.GetPin()
   175  		room = rule.DispatchRuleDirect.GetRoomName()
   176  	case *livekit.SIPDispatchRule_DispatchRuleIndividual:
   177  		pin = rule.DispatchRuleIndividual.GetPin()
   178  		room = rule.DispatchRuleIndividual.GetRoomPrefix()
   179  	}
   180  	return room, pin, nil
   181  }
   182  
   183  func printNumber(s string) string {
   184  	if s == "" {
   185  		return "<any>"
   186  	}
   187  	return strconv.Quote(s)
   188  }
   189  
   190  // ValidateTrunks checks a set of trunks for conflicts.
   191  func ValidateTrunks(trunks []*livekit.SIPTrunkInfo) error {
   192  	if len(trunks) == 0 {
   193  		return nil
   194  	}
   195  	byOutboundAndInbound := make(map[string]map[string]*livekit.SIPTrunkInfo)
   196  	for _, t := range trunks {
   197  		if len(t.InboundNumbersRegex) != 0 {
   198  			continue // can't effectively validate these
   199  		}
   200  		byInbound := byOutboundAndInbound[t.OutboundNumber]
   201  		if byInbound == nil {
   202  			byInbound = make(map[string]*livekit.SIPTrunkInfo)
   203  			byOutboundAndInbound[t.OutboundNumber] = byInbound
   204  		}
   205  		if len(t.InboundNumbers) == 0 {
   206  			if t2 := byInbound[""]; t2 != nil {
   207  				return fmt.Errorf("Conflicting SIP Trunks: %q and %q, using the same OutboundNumber %s without InboundNumbers set",
   208  					printID(t.SipTrunkId), printID(t2.SipTrunkId), printNumber(t.OutboundNumber))
   209  			}
   210  			byInbound[""] = t
   211  		} else {
   212  			for _, num := range t.InboundNumbers {
   213  				t2 := byInbound[num]
   214  				if t2 != nil {
   215  					return fmt.Errorf("Conflicting SIP Trunks: %q and %q, using the same OutboundNumber %s and InboundNumber %q",
   216  						printID(t.SipTrunkId), printID(t2.SipTrunkId), printNumber(t.OutboundNumber), num)
   217  				}
   218  				byInbound[num] = t
   219  			}
   220  		}
   221  	}
   222  	return nil
   223  }
   224  
   225  // MatchTrunk finds a SIP Trunk definition matching the request.
   226  // Returns nil if no rules matched or an error if there are conflicting definitions.
   227  func MatchTrunk(trunks []*livekit.SIPTrunkInfo, calling, called string) (*livekit.SIPTrunkInfo, error) {
   228  	var (
   229  		selectedTrunk   *livekit.SIPTrunkInfo
   230  		defaultTrunk    *livekit.SIPTrunkInfo
   231  		defaultTrunkCnt int // to error in case there are multiple ones
   232  	)
   233  	for _, tr := range trunks {
   234  		// Do not consider it if number doesn't match.
   235  		if len(tr.InboundNumbers) != 0 && !slices.Contains(tr.InboundNumbers, calling) {
   236  			continue
   237  		}
   238  		// Deprecated, but we still check it for backward compatibility.
   239  		matchesRe := len(tr.InboundNumbersRegex) == 0
   240  		for _, reStr := range tr.InboundNumbersRegex {
   241  			// TODO: we should cache it
   242  			re, err := regexp.Compile(reStr)
   243  			if err != nil {
   244  				logger.Errorw("cannot parse SIP trunk regexp", err, "trunkID", tr.SipTrunkId)
   245  				continue
   246  			}
   247  			if re.MatchString(calling) {
   248  				matchesRe = true
   249  				break
   250  			}
   251  		}
   252  		if !matchesRe {
   253  			continue
   254  		}
   255  		if tr.OutboundNumber == "" {
   256  			// Default/wildcard trunk.
   257  			defaultTrunk = tr
   258  			defaultTrunkCnt++
   259  		} else if tr.OutboundNumber == called {
   260  			// Trunk specific to the number.
   261  			if selectedTrunk != nil {
   262  				return nil, fmt.Errorf("Multiple SIP Trunks matched for %q", called)
   263  			}
   264  			selectedTrunk = tr
   265  			// Keep searching! We want to know if there are any conflicting Trunk definitions.
   266  		}
   267  	}
   268  	if selectedTrunk != nil {
   269  		return selectedTrunk, nil
   270  	}
   271  	if defaultTrunkCnt > 1 {
   272  		return nil, fmt.Errorf("Multiple default SIP Trunks matched for %q", called)
   273  	}
   274  	// Could still be nil here.
   275  	return defaultTrunk, nil
   276  }
   277  
   278  // MatchDispatchRule finds the best dispatch rule matching the request parameters. Returns an error if no rule matched.
   279  // Trunk parameter can be nil, in which case only wildcard dispatch rules will be effective (ones without Trunk IDs).
   280  func MatchDispatchRule(trunk *livekit.SIPTrunkInfo, rules []*livekit.SIPDispatchRuleInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) {
   281  	// Trunk can still be nil here in case none matched or were defined.
   282  	// This is still fine, but only in case we'll match exactly one wildcard dispatch rule.
   283  	if len(rules) == 0 {
   284  		return nil, &ErrNoDispatchMatched{NoRules: true, NoTrunks: trunk == nil, CalledNumber: req.CalledNumber}
   285  	}
   286  	// We split the matched dispatch rules into two sets in relation to Trunks: specific and default (aka wildcard).
   287  	// First, attempt to match any of the specific rules, where we did match the Trunk ID.
   288  	// If nothing matches there - fallback to default/wildcard rules, where no Trunk IDs were mentioned.
   289  	var (
   290  		specificRules []*livekit.SIPDispatchRuleInfo
   291  		defaultRules  []*livekit.SIPDispatchRuleInfo
   292  	)
   293  	noPin := req.NoPin
   294  	sentPin := req.GetPin()
   295  	for _, info := range rules {
   296  		if len(info.InboundNumbers) != 0 && !slices.Contains(info.InboundNumbers, req.CallingNumber) {
   297  			continue
   298  		}
   299  		_, rulePin, err := GetPinAndRoom(info)
   300  		if err != nil {
   301  			logger.Errorw("Invalid SIP Dispatch Rule", err, "dispatchRuleID", info.SipDispatchRuleId)
   302  			continue
   303  		}
   304  		// Filter heavily on the Pin, so that only relevant rules remain.
   305  		if noPin {
   306  			if rulePin != "" {
   307  				// Skip pin-protected rules if no pin mode requested.
   308  				continue
   309  			}
   310  		} else if sentPin != "" {
   311  			if rulePin == "" {
   312  				// Pin already sent, skip non-pin-protected rules.
   313  				continue
   314  			}
   315  			if sentPin != rulePin {
   316  				// Pin doesn't match. Don't return an error here, just wait for other rule to match (or none at all).
   317  				// Note that we will NOT match non-pin-protected rules, thus it will not fallback to open rules.
   318  				continue
   319  			}
   320  		}
   321  		if len(info.TrunkIds) == 0 {
   322  			// Default/wildcard dispatch rule.
   323  			defaultRules = append(defaultRules, info)
   324  			continue
   325  		}
   326  		// Specific dispatch rules. Require a Trunk associated with the number.
   327  		if trunk == nil {
   328  			continue
   329  		}
   330  		if !slices.Contains(info.TrunkIds, trunk.SipTrunkId) {
   331  			continue
   332  		}
   333  		specificRules = append(specificRules, info)
   334  	}
   335  	best, err := SelectDispatchRule(specificRules, req)
   336  	if err != nil {
   337  		return nil, err
   338  	} else if best != nil {
   339  		return best, nil
   340  	}
   341  	best, err = SelectDispatchRule(defaultRules, req)
   342  	if err != nil {
   343  		return nil, err
   344  	} else if best != nil {
   345  		return best, nil
   346  	}
   347  	return nil, &ErrNoDispatchMatched{NoRules: false, NoTrunks: trunk == nil, CalledNumber: req.CalledNumber}
   348  }
   349  
   350  // EvaluateDispatchRule checks a selected Dispatch Rule against the provided request.
   351  func EvaluateDispatchRule(rule *livekit.SIPDispatchRuleInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
   352  	sentPin := req.GetPin()
   353  
   354  	from := req.CallingNumber
   355  	if rule.HidePhoneNumber {
   356  		// TODO: Decide on the phone masking format.
   357  		//       Maybe keep regional code, but mask all but 4 last digits?
   358  		n := 4
   359  		if len(from) <= 4 {
   360  			n = 1
   361  		}
   362  		from = from[len(from)-n:]
   363  	}
   364  	fromID := "sip_" + from
   365  	fromName := "Phone " + from
   366  
   367  	room, rulePin, err := GetPinAndRoom(rule)
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  	if rulePin != "" {
   372  		if sentPin == "" {
   373  			return &rpc.EvaluateSIPDispatchRulesResponse{
   374  				SipDispatchRuleId: rule.SipDispatchRuleId,
   375  				Result:            rpc.SIPDispatchResult_REQUEST_PIN,
   376  				RequestPin:        true,
   377  			}, nil
   378  		}
   379  		if rulePin != sentPin {
   380  			// This should never happen in practice, because matchSIPDispatchRule should remove rules with the wrong pin.
   381  			return nil, fmt.Errorf("Incorrect PIN for SIP room")
   382  		}
   383  	} else {
   384  		// Pin was sent, but room doesn't require one. Assume user accidentally pressed phone button.
   385  	}
   386  	switch rule := rule.GetRule().GetRule().(type) {
   387  	case *livekit.SIPDispatchRule_DispatchRuleIndividual:
   388  		// TODO: Do we need to escape specific characters in the number?
   389  		// TODO: Include actual SIP call ID in the room name?
   390  		room = fmt.Sprintf("%s_%s_%s", rule.DispatchRuleIndividual.GetRoomPrefix(), from, utils.NewGuid(""))
   391  	}
   392  	return &rpc.EvaluateSIPDispatchRulesResponse{
   393  		SipDispatchRuleId:   rule.SipDispatchRuleId,
   394  		Result:              rpc.SIPDispatchResult_ACCEPT,
   395  		RoomName:            room,
   396  		ParticipantIdentity: fromID,
   397  		ParticipantName:     fromName,
   398  		ParticipantMetadata: rule.Metadata,
   399  	}, nil
   400  }