github.com/livekit/protocol@v1.16.1-0.20240517185851-47e4c6bba773/sip/sip_test.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 "strconv" 20 "testing" 21 22 "github.com/stretchr/testify/require" 23 24 "github.com/livekit/protocol/livekit" 25 "github.com/livekit/protocol/rpc" 26 ) 27 28 const ( 29 sipNumber1 = "1111 1111" 30 sipNumber2 = "2222 2222" 31 sipNumber3 = "3333 3333" 32 sipTrunkID1 = "aaa" 33 sipTrunkID2 = "bbb" 34 ) 35 36 var trunkCases = []struct { 37 name string 38 trunks []*livekit.SIPTrunkInfo 39 exp int 40 expErr bool 41 invalid bool 42 }{ 43 { 44 name: "empty", 45 trunks: nil, 46 exp: -1, // no error; nil result 47 }, 48 { 49 name: "one wildcard", 50 trunks: []*livekit.SIPTrunkInfo{ 51 {SipTrunkId: "aaa"}, 52 }, 53 exp: 0, 54 }, 55 { 56 name: "matching", 57 trunks: []*livekit.SIPTrunkInfo{ 58 {SipTrunkId: "aaa", OutboundNumber: sipNumber2}, 59 }, 60 exp: 0, 61 }, 62 { 63 name: "matching inbound", 64 trunks: []*livekit.SIPTrunkInfo{ 65 {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1}}, 66 }, 67 exp: 0, 68 }, 69 { 70 name: "matching regexp", 71 trunks: []*livekit.SIPTrunkInfo{ 72 {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+ \d+$`}}, 73 }, 74 exp: 0, 75 }, 76 { 77 name: "not matching", 78 trunks: []*livekit.SIPTrunkInfo{ 79 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 80 }, 81 exp: -1, 82 }, 83 { 84 name: "not matching inbound", 85 trunks: []*livekit.SIPTrunkInfo{ 86 {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1 + "1"}}, 87 }, 88 exp: -1, 89 }, 90 { 91 name: "not matching regexp", 92 trunks: []*livekit.SIPTrunkInfo{ 93 {SipTrunkId: "aaa", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+$`}}, 94 }, 95 exp: -1, 96 }, 97 { 98 name: "one match", 99 trunks: []*livekit.SIPTrunkInfo{ 100 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 101 {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, 102 }, 103 exp: 1, 104 }, 105 { 106 name: "many matches", 107 trunks: []*livekit.SIPTrunkInfo{ 108 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 109 {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, 110 {SipTrunkId: "ccc", OutboundNumber: sipNumber2}, 111 }, 112 expErr: true, 113 invalid: true, 114 }, 115 { 116 name: "many matches default", 117 trunks: []*livekit.SIPTrunkInfo{ 118 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 119 {SipTrunkId: "bbb"}, 120 {SipTrunkId: "ccc", OutboundNumber: sipNumber2}, 121 {SipTrunkId: "ddd"}, 122 }, 123 exp: 2, 124 invalid: true, // it can successfully select "ccc", but the overall configuration is invalid 125 }, 126 { 127 name: "inbound", 128 trunks: []*livekit.SIPTrunkInfo{ 129 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 130 {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, 131 {SipTrunkId: "ccc", OutboundNumber: sipNumber2, InboundNumbers: []string{sipNumber1 + "1"}}, 132 }, 133 exp: 1, 134 }, 135 { 136 name: "regexp", 137 trunks: []*livekit.SIPTrunkInfo{ 138 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 139 {SipTrunkId: "bbb", OutboundNumber: sipNumber2}, 140 {SipTrunkId: "ccc", OutboundNumber: sipNumber2, InboundNumbersRegex: []string{`^\d+$`}}, 141 }, 142 exp: 1, 143 }, 144 { 145 name: "multiple defaults", 146 trunks: []*livekit.SIPTrunkInfo{ 147 {SipTrunkId: "aaa", OutboundNumber: sipNumber3}, 148 {SipTrunkId: "bbb"}, 149 {SipTrunkId: "ccc"}, 150 }, 151 expErr: true, 152 invalid: true, 153 }, 154 } 155 156 func TestSIPMatchTrunk(t *testing.T) { 157 for _, c := range trunkCases { 158 c := c 159 t.Run(c.name, func(t *testing.T) { 160 got, err := MatchTrunk(c.trunks, sipNumber1, sipNumber2) 161 if c.expErr { 162 require.Error(t, err) 163 require.Nil(t, got) 164 t.Log(err) 165 } else { 166 var exp *livekit.SIPTrunkInfo 167 if c.exp >= 0 { 168 exp = c.trunks[c.exp] 169 } 170 require.NoError(t, err) 171 require.Equal(t, exp, got) 172 } 173 }) 174 } 175 } 176 177 func TestSIPValidateTrunks(t *testing.T) { 178 for _, c := range trunkCases { 179 c := c 180 t.Run(c.name, func(t *testing.T) { 181 for i, r := range c.trunks { 182 if r.SipTrunkId == "" { 183 r.SipTrunkId = strconv.Itoa(i) 184 } 185 } 186 err := ValidateTrunks(c.trunks) 187 if c.invalid { 188 require.Error(t, err) 189 } else { 190 require.NoError(t, err) 191 } 192 }) 193 } 194 } 195 196 func newSIPTrunkDispatch() *livekit.SIPTrunkInfo { 197 return &livekit.SIPTrunkInfo{ 198 SipTrunkId: sipTrunkID1, 199 OutboundNumber: sipNumber2, 200 } 201 } 202 203 func newSIPReqDispatch(pin string, noPin bool) *rpc.EvaluateSIPDispatchRulesRequest { 204 return &rpc.EvaluateSIPDispatchRulesRequest{ 205 CallingNumber: sipNumber1, 206 CalledNumber: sipNumber2, 207 Pin: pin, 208 //NoPin: noPin, // TODO 209 } 210 } 211 212 func newDirectDispatch(room, pin string) *livekit.SIPDispatchRule { 213 return &livekit.SIPDispatchRule{ 214 Rule: &livekit.SIPDispatchRule_DispatchRuleDirect{ 215 DispatchRuleDirect: &livekit.SIPDispatchRuleDirect{ 216 RoomName: room, Pin: pin, 217 }, 218 }, 219 } 220 } 221 222 func newIndividualDispatch(roomPref, pin string) *livekit.SIPDispatchRule { 223 return &livekit.SIPDispatchRule{ 224 Rule: &livekit.SIPDispatchRule_DispatchRuleIndividual{ 225 DispatchRuleIndividual: &livekit.SIPDispatchRuleIndividual{ 226 RoomPrefix: roomPref, Pin: pin, 227 }, 228 }, 229 } 230 } 231 232 var dispatchCases = []struct { 233 name string 234 trunk *livekit.SIPTrunkInfo 235 rules []*livekit.SIPDispatchRuleInfo 236 reqPin string 237 noPin bool 238 exp int 239 expErr bool 240 invalid bool 241 }{ 242 // These cases just validate that no rules produce an error. 243 { 244 name: "empty", 245 trunk: nil, 246 rules: nil, 247 expErr: true, 248 }, 249 { 250 name: "only trunk", 251 trunk: newSIPTrunkDispatch(), 252 rules: nil, 253 expErr: true, 254 }, 255 // Default rules should work even if no trunk is defined. 256 { 257 name: "one rule/no trunk", 258 trunk: nil, 259 rules: []*livekit.SIPDispatchRuleInfo{ 260 {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, 261 }, 262 exp: 0, 263 }, 264 // Default rule should work with a trunk too. 265 { 266 name: "one rule/default trunk", 267 trunk: newSIPTrunkDispatch(), 268 rules: []*livekit.SIPDispatchRuleInfo{ 269 {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, 270 }, 271 exp: 0, 272 }, 273 // Rule matching the trunk should be selected. 274 { 275 name: "one rule/specific trunk", 276 trunk: newSIPTrunkDispatch(), 277 rules: []*livekit.SIPDispatchRuleInfo{ 278 {TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip", "")}, 279 }, 280 exp: 0, 281 }, 282 // Rule NOT matching the trunk should NOT be selected. 283 { 284 name: "one rule/wrong trunk", 285 trunk: newSIPTrunkDispatch(), 286 rules: []*livekit.SIPDispatchRuleInfo{ 287 {TrunkIds: []string{"zzz"}, Rule: newDirectDispatch("sip", "")}, 288 }, 289 expErr: true, 290 }, 291 // Direct rule with a pin should be selected, even if no pin is provided. 292 { 293 name: "direct pin/correct", 294 trunk: newSIPTrunkDispatch(), 295 rules: []*livekit.SIPDispatchRuleInfo{ 296 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")}, 297 {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")}, 298 }, 299 reqPin: "123", 300 exp: 0, 301 }, 302 // Direct rule with a pin should reject wrong pin. 303 { 304 name: "direct pin/wrong", 305 trunk: newSIPTrunkDispatch(), 306 rules: []*livekit.SIPDispatchRuleInfo{ 307 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip", "123")}, 308 {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip", "456")}, 309 }, 310 reqPin: "zzz", 311 expErr: true, 312 }, 313 // Multiple direct rules with the same pin should result in an error. 314 { 315 name: "direct pin/conflict", 316 trunk: newSIPTrunkDispatch(), 317 rules: []*livekit.SIPDispatchRuleInfo{ 318 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")}, 319 {TrunkIds: []string{sipTrunkID1, sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")}, 320 }, 321 reqPin: "123", 322 expErr: true, 323 invalid: true, 324 }, 325 // Multiple direct rules with the same pin on different trunks are ok. 326 { 327 name: "direct pin/no conflict on different trunk", 328 trunk: newSIPTrunkDispatch(), 329 rules: []*livekit.SIPDispatchRuleInfo{ 330 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip1", "123")}, 331 {TrunkIds: []string{sipTrunkID2}, Rule: newDirectDispatch("sip2", "123")}, 332 }, 333 reqPin: "123", 334 exp: 0, 335 }, 336 // Specific direct rules should take priority over default direct rules. 337 { 338 name: "direct pin/default and specific", 339 trunk: newSIPTrunkDispatch(), 340 rules: []*livekit.SIPDispatchRuleInfo{ 341 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, 342 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")}, 343 }, 344 reqPin: "123", 345 exp: 1, 346 }, 347 // Specific direct rules should take priority over default direct rules. No pin. 348 { 349 name: "direct/default and specific", 350 trunk: newSIPTrunkDispatch(), 351 rules: []*livekit.SIPDispatchRuleInfo{ 352 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, 353 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")}, 354 }, 355 exp: 1, 356 }, 357 // Specific direct rules should take priority over default direct rules. One with pin, other without. 358 { 359 name: "direct/default and specific/mixed 1", 360 trunk: newSIPTrunkDispatch(), 361 rules: []*livekit.SIPDispatchRuleInfo{ 362 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, 363 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "")}, 364 }, 365 exp: 1, 366 }, 367 { 368 name: "direct/default and specific/mixed 2", 369 trunk: newSIPTrunkDispatch(), 370 rules: []*livekit.SIPDispatchRuleInfo{ 371 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, 372 {TrunkIds: []string{sipTrunkID1}, Rule: newDirectDispatch("sip2", "123")}, 373 }, 374 exp: 1, 375 }, 376 // Multiple default direct rules are not allowed. 377 { 378 name: "direct/multiple defaults", 379 trunk: newSIPTrunkDispatch(), 380 rules: []*livekit.SIPDispatchRuleInfo{ 381 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, 382 {TrunkIds: nil, Rule: newDirectDispatch("sip2", "")}, 383 }, 384 expErr: true, 385 invalid: true, 386 }, 387 // Rules for specific numbers take priority. 388 { 389 name: "direct/number specific", 390 trunk: newSIPTrunkDispatch(), 391 rules: []*livekit.SIPDispatchRuleInfo{ 392 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "")}, 393 {TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1}}, 394 }, 395 exp: 1, 396 }, 397 { 398 name: "direct/number specific pin", 399 trunk: newSIPTrunkDispatch(), 400 rules: []*livekit.SIPDispatchRuleInfo{ 401 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, 402 {TrunkIds: nil, Rule: newDirectDispatch("sip2", "123"), InboundNumbers: []string{sipNumber1}}, 403 }, 404 exp: 1, 405 }, 406 { 407 name: "direct/number specific conflict", 408 trunk: newSIPTrunkDispatch(), 409 rules: []*livekit.SIPDispatchRuleInfo{ 410 {TrunkIds: nil, Rule: newDirectDispatch("sip1", ""), InboundNumbers: []string{sipNumber1}}, 411 {TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1, sipNumber2}}, 412 }, 413 expErr: true, 414 invalid: true, 415 }, 416 // Check the "personal room" use case. Rule that accepts a number without a pin and requires pin for everyone else. 417 { 418 name: "direct/open specific vs pin generic", 419 trunk: newSIPTrunkDispatch(), 420 rules: []*livekit.SIPDispatchRuleInfo{ 421 {TrunkIds: nil, Rule: newDirectDispatch("sip1", "123")}, 422 {TrunkIds: nil, Rule: newDirectDispatch("sip2", ""), InboundNumbers: []string{sipNumber1}}, 423 }, 424 exp: 1, 425 }, 426 // Cannot use both direct and individual rules with the same pin setup. 427 { 428 name: "direct vs individual/private", 429 trunk: newSIPTrunkDispatch(), 430 rules: []*livekit.SIPDispatchRuleInfo{ 431 {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123")}, 432 {TrunkIds: nil, Rule: newDirectDispatch("sip", "123")}, 433 }, 434 expErr: true, 435 invalid: true, 436 }, 437 { 438 name: "direct vs individual/open", 439 trunk: newSIPTrunkDispatch(), 440 rules: []*livekit.SIPDispatchRuleInfo{ 441 {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "")}, 442 {TrunkIds: nil, Rule: newDirectDispatch("sip", "")}, 443 }, 444 expErr: true, 445 invalid: true, 446 }, 447 // Direct rules take priority over individual rules. 448 { 449 name: "direct vs individual/priority", 450 trunk: newSIPTrunkDispatch(), 451 rules: []*livekit.SIPDispatchRuleInfo{ 452 {TrunkIds: nil, Rule: newIndividualDispatch("pref_", "123")}, 453 {TrunkIds: nil, Rule: newDirectDispatch("sip", "456")}, 454 }, 455 reqPin: "456", 456 exp: 1, 457 }, 458 } 459 460 func TestSIPMatchDispatchRule(t *testing.T) { 461 for _, c := range dispatchCases { 462 c := c 463 t.Run(c.name, func(t *testing.T) { 464 pins := []string{c.reqPin} 465 if !c.expErr && c.reqPin != "" { 466 // Should match the same rule, even if no pin is set (so that it can be requested). 467 pins = append(pins, "") 468 } 469 for i, r := range c.rules { 470 if r.SipDispatchRuleId == "" { 471 r.SipDispatchRuleId = fmt.Sprintf("rule_%d", i) 472 } 473 } 474 for _, pin := range pins { 475 pin := pin 476 name := pin 477 if name == "" { 478 name = "no pin" 479 } 480 t.Run(name, func(t *testing.T) { 481 got, err := MatchDispatchRule(c.trunk, c.rules, newSIPReqDispatch(pin, c.noPin)) 482 if c.expErr { 483 require.Error(t, err) 484 require.Nil(t, got) 485 t.Log(err) 486 } else { 487 var exp *livekit.SIPDispatchRuleInfo 488 if c.exp >= 0 { 489 exp = c.rules[c.exp] 490 } 491 require.NoError(t, err) 492 require.Equal(t, exp, got) 493 } 494 }) 495 } 496 }) 497 } 498 } 499 500 func TestSIPValidateDispatchRules(t *testing.T) { 501 for _, c := range dispatchCases { 502 c := c 503 t.Run(c.name, func(t *testing.T) { 504 for i, r := range c.rules { 505 if r.SipDispatchRuleId == "" { 506 r.SipDispatchRuleId = strconv.Itoa(i) 507 } 508 } 509 err := ValidateDispatchRules(c.rules) 510 if c.invalid { 511 require.Error(t, err) 512 } else { 513 require.NoError(t, err) 514 } 515 }) 516 } 517 }