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 }