Files
livekit/pkg/service/ioservice_sip.go

185 lines
6.5 KiB
Go

// 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"
"errors"
"fmt"
"net/netip"
"github.com/dennwc/iters"
"github.com/twitchtv/twirp"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/sip"
)
// 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, trunkID string, call *rpc.SIPCall) (*livekit.SIPInboundTrunkInfo, error) {
if s.ss == nil {
return nil, ErrSIPNotConnected
}
if trunkID != "" {
// This is a best-effort optimization. Fallthrough to listing trunks if it doesn't work.
if tr, err := s.ss.LoadSIPInboundTrunk(ctx, trunkID); err == nil {
result, err := sip.MatchTrunkDetailed(iters.Slice([]*livekit.SIPInboundTrunkInfo{tr}), call)
if err == nil && result.MatchType != sip.TrunkMatchNone {
return result.Trunk, nil
}
}
}
it := s.ListAllSIPInboundTrunks(ctx, call.To.User)
result, err := sip.MatchTrunkDetailed(it, call)
if err != nil {
return nil, err
}
// If trunks exist but none matched, return a specific error
if result.MatchType == sip.TrunkMatchNone {
return nil, ErrSIPTrunkNotFound
}
// If no trunks were defined at all, return nil (this is different from TrunkMatchNone)
if result.MatchType == sip.TrunkMatchEmpty {
return nil, nil
}
return result.Trunk, nil
}
func (s *IOInfoService) SelectSIPInboundTrunk(ctx context.Context, called string) iters.Iter[*livekit.SIPInboundTrunkInfo] {
it := livekit.ListPageIter(s.ss.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{
Numbers: []string{called},
})
return iters.PagesAsIter(ctx, it)
}
func (s *IOInfoService) ListAllSIPInboundTrunks(ctx context.Context, called string) iters.Iter[*livekit.SIPInboundTrunkInfo] {
it := livekit.ListPageIter(s.ss.ListSIPInboundTrunk, &livekit.ListSIPInboundTrunkRequest{
Numbers: nil,
})
return iters.PagesAsIter(ctx, it)
}
// 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.SIPInboundTrunkInfo, req *rpc.EvaluateSIPDispatchRulesRequest) (*livekit.SIPDispatchRuleInfo, error) {
if s.ss == nil {
return nil, ErrSIPNotConnected
}
var trunkID string
if trunk != nil {
trunkID = trunk.SipTrunkId
}
// 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.
it := s.SelectSIPDispatchRule(ctx, trunkID)
return sip.MatchDispatchRuleIter(trunk, it, req)
}
func (s *IOInfoService) SelectSIPDispatchRule(ctx context.Context, trunkID string) iters.Iter[*livekit.SIPDispatchRuleInfo] {
var trunkIDs []string
if trunkID != "" {
trunkIDs = []string{trunkID}
}
it := livekit.ListPageIter(s.ss.ListSIPDispatchRule, &livekit.ListSIPDispatchRuleRequest{
TrunkIds: trunkIDs,
})
return iters.PagesAsIter(ctx, it)
}
func (s *IOInfoService) EvaluateSIPDispatchRules(ctx context.Context, req *rpc.EvaluateSIPDispatchRulesRequest) (*rpc.EvaluateSIPDispatchRulesResponse, error) {
call := req.SIPCall()
log := logger.GetLogger()
log = log.WithValues("toUser", call.To.User, "fromUser", call.From.User, "src", call.SourceIp)
if call.SourceIp == "" {
log.Warnw("source address is not set", nil)
// TODO: return error in the next release
}
_, err := netip.ParseAddr(call.SourceIp)
if call.SourceIp != "" && err != nil {
log.Errorw("cannot parse source IP", err)
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
trunk, err := s.matchSIPTrunk(ctx, req.SipTrunkId, call)
if err != nil {
return nil, err
}
trunkID := ""
if trunk != nil {
trunkID = trunk.SipTrunkId
}
log = log.WithValues("sipTrunk", trunkID)
if trunk != nil {
log.Debugw("SIP trunk matched")
} else {
log.Debugw("No SIP trunk matched")
}
best, err := s.matchSIPDispatchRule(ctx, trunk, req)
if err != nil {
if e := (*sip.ErrNoDispatchMatched)(nil); errors.As(err, &e) {
return &rpc.EvaluateSIPDispatchRulesResponse{
SipTrunkId: trunkID,
Result: rpc.SIPDispatchResult_DROP,
}, nil
}
return nil, err
}
log.Debugw("SIP dispatch rule matched", "sipRule", best.SipDispatchRuleId)
resp, err := sip.EvaluateDispatchRule("", trunk, best, req)
if err != nil {
return nil, err
}
resp.SipTrunkId = trunkID
return resp, err
}
func (s *IOInfoService) GetSIPTrunkAuthentication(ctx context.Context, req *rpc.GetSIPTrunkAuthenticationRequest) (*rpc.GetSIPTrunkAuthenticationResponse, error) {
call := req.SIPCall()
log := logger.GetLogger()
log = log.WithValues("toUser", call.To.User, "fromUser", call.From.User, "src", call.SourceIp)
if call.SourceIp == "" {
log.Warnw("source address is not set", nil)
// TODO: return error in the next release
}
_, err := netip.ParseAddr(call.SourceIp)
if call.SourceIp != "" && err != nil {
log.Errorw("cannot parse source IP", err)
return nil, twirp.WrapError(twirp.NewError(twirp.InvalidArgument, err.Error()), err)
}
trunk, err := s.matchSIPTrunk(ctx, "", call)
if err != nil {
// Check if this is the specific "no SIP trunk matched" error
if err == ErrSIPTrunkNotFound {
err := twirp.NotFoundError(fmt.Sprintf("sip trunk not found for destination %q", req.Call.To))
log.Errorw("No SIP trunk matched for auth", err, "sipTrunk", "", "to", req.Call.To)
return nil, err
}
return nil, err
}
if trunk == nil {
// This case is for TrunkMatchEmpty (no trunks defined at all)
// We don't return an error in this case
return nil, nil
}
log.Debugw("SIP trunk matched for auth", "sipTrunk", trunk.SipTrunkId)
return &rpc.GetSIPTrunkAuthenticationResponse{
SipTrunkId: trunk.SipTrunkId,
Username: trunk.AuthUsername,
Password: trunk.AuthPassword,
}, nil
}