From 493838874b8be74046f52ed5b8c567a85da61bfb Mon Sep 17 00:00:00 2001 From: Denys Smirnov Date: Tue, 26 May 2026 14:52:59 +0200 Subject: [PATCH] Support specifying To/From SIP headers for outbound. --- pkg/sip/client.go | 228 ++++++++++++++++++++++++++++++------ pkg/sip/outbound.go | 60 ++++------ pkg/sip/outbound_test.go | 244 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 455 insertions(+), 77 deletions(-) diff --git a/pkg/sip/client.go b/pkg/sip/client.go index fabc964a5..a92796136 100644 --- a/pkg/sip/client.go +++ b/pkg/sip/client.go @@ -16,8 +16,12 @@ package sip import ( "context" + "errors" + "fmt" "log/slog" + "net" "net/netip" + "strconv" "strings" "sync" "time" @@ -25,6 +29,8 @@ import ( "github.com/frostbyte73/core" "golang.org/x/exp/maps" + esip "github.com/emiago/sipgo/sip" + "github.com/livekit/protocol/livekit" "github.com/livekit/protocol/logger" "github.com/livekit/protocol/rpc" @@ -175,32 +181,179 @@ func (c *Client) getActiveCall(tag LocalTag) *outboundCall { return c.activeCalls[tag] } +func setUriTransport(p *sip.Uri, tr livekit.SIPTransport) { + if tr != livekit.SIPTransport_SIP_TRANSPORT_AUTO { + p.UriParams.Add("transport", tr.Name()) + } +} + +func buildLegacyURI(user, addr string, tr livekit.SIPTransport) (*sip.Uri, error) { + if user == "" { + return nil, fmt.Errorf("number must be set") + } else if strings.Contains(user, "@") { + return nil, fmt.Errorf("should be a phone number or SIP user, not a full SIP URI") + } + if addr == "" { + return nil, fmt.Errorf("address must be set") + } + if strings.HasPrefix(addr, "sip:") || strings.HasPrefix(addr, "sips:") { + return nil, fmt.Errorf("address must be a hostname without 'sip:' prefix") + } else if strings.Contains(addr, "transport=") { + return nil, fmt.Errorf("legacy address must not contain parameters; use transport field") + } else if strings.ContainsAny(addr, ";=") { + return nil, fmt.Errorf("legacy address must not contain parameters") + } + p := &sip.Uri{Scheme: "sip"} + setUriTransport(p, tr) + + p.User = user + if host, sport, err := net.SplitHostPort(addr); err == nil && sport != "" { + p.Host = host + p.Port, err = strconv.Atoi(sport) + if err != nil { + return nil, fmt.Errorf("invalid port in hostname: %q", sport) + } + } else { + p.Host = addr + } + return p, nil +} + +func buildRawURI(raw string, tr livekit.SIPTransport) (*sip.Uri, error) { + p := &sip.Uri{Scheme: "sip"} + if n := len(raw); n != 0 && raw[0] == '<' && raw[n-1] == '>' { + raw = raw[1 : n-1] + } + if err := esip.ParseUri(raw, p); err != nil { + return nil, errors.New("invalid request URI") + } + setUriTransport(p, tr) + return p, nil +} + +func buildValuesURI(u *livekit.SIPUri, tr livekit.SIPTransport) (*sip.Uri, error) { + if tr != u.Transport { + if u.Transport == livekit.SIPTransport_SIP_TRANSPORT_AUTO { + //tr = tr + } else if tr == livekit.SIPTransport_SIP_TRANSPORT_AUTO { + tr = u.Transport + } else { + return nil, fmt.Errorf("different transports specified: %v vs %v", tr, u.Transport) + } + } + p := &sip.Uri{Scheme: "sip"} + setUriTransport(p, tr) + if u.User == "" { + return nil, fmt.Errorf("username or number must be set") + } + if u.Host == "" && u.Ip == "" { + return nil, fmt.Errorf("host or ip must be set") + } + p.User = u.User + p.Host = u.Host + if p.Host == "" { + p.Host = u.Ip + } + if _, sport, err := net.SplitHostPort(p.Host); err == nil && sport != "" { + return nil, fmt.Errorf("host or ip must not contain port") + } + p.Port = int(u.Port) + return p, nil +} + +func buildRequestURI(u *livekit.SIPRequestDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.Uri, error) { + if u == nil { + return buildLegacyURI(legacyUser, legacyAddr, tr) + } + switch u := u.Uri.(type) { + default: + case *livekit.SIPRequestDest_Raw: + return buildRawURI(u.Raw, tr) + case *livekit.SIPRequestDest_Values: + return buildValuesURI(u.Values, tr) + } + return nil, fmt.Errorf("invalid request URI type") +} + +func buildFromToURI(u *livekit.SIPNamedDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.Uri, error) { + if u == nil { + return buildLegacyURI(legacyUser, legacyAddr, tr) + } + switch u := u.Uri.(type) { + default: + case *livekit.SIPNamedDest_Raw: + return buildRawURI(u.Raw, tr) + case *livekit.SIPNamedDest_Values: + return buildValuesURI(u.Values, tr) + } + return nil, fmt.Errorf("invalid URI type") +} + +func buildFromHeader(u *livekit.SIPNamedDest, legacyName *string, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.FromHeader, error) { + su, err := buildFromToURI(u, legacyUser, legacyAddr, tr) + if err != nil { + return nil, err + } + h := &sip.FromHeader{ + Address: *su, + } + if u != nil { + h.DisplayName = u.DisplayName + } else if legacyName != nil { + h.DisplayName = *legacyName + } else { + // Nothing specified, preserve legacy behavior + h.DisplayName = su.User + } + return h, nil +} + +func buildToHeader(u *livekit.SIPNamedDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.ToHeader, error) { + if u != nil && legacyUser != "" { + return nil, errors.New("cannot use both CallTo and SipToHeader") + } + su, err := buildFromToURI(u, legacyUser, legacyAddr, tr) + if err != nil { + return nil, err + } + h := &sip.ToHeader{ + Address: *su, + } + if u != nil { + h.DisplayName = u.DisplayName + } + return h, nil +} + +func buildOutboundHeaders(req *rpc.InternalCreateSIPParticipantRequest, defaultHost string) (*sip.Uri, *sip.FromHeader, *sip.ToHeader, error) { + uri, err := buildRequestURI(req.SipRequestUri, req.CallTo, req.Address, req.Transport) + if err != nil { + return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid request URI: %w", err)) + } + to, err := buildToHeader(req.SipToHeader, req.CallTo, req.Address, req.Transport) + if err != nil { + return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid To header: %w", err)) + } + fromHost := req.Hostname + if fromHost == "" { + fromHost = defaultHost + } + from, err := buildFromHeader(req.SipFromHeader, req.DisplayName, req.Number, fromHost, req.Transport) + if err != nil { + return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid From header: %w", err)) + } + return uri, from, to, nil +} + func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCreateSIPParticipantRequest) (resp *rpc.InternalCreateSIPParticipantResponse, retErr error) { if c.mon.Health() != stats.HealthOK { return nil, siperrors.ErrUnavailable } req.Upgrade() - if req.CallTo == "" { - return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "call-to number must be set") - } else if req.Address == "" { - return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "trunk adresss must be set") - } else if req.Number == "" { - return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "trunk outbound number must be set") - } else if req.RoomName == "" { + if req.RoomName == "" { return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "room name must be set") } - if strings.Contains(req.CallTo, "@") { - return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "call_to should be a phone number or SIP user, not a full SIP URI") - } - if strings.HasPrefix(req.Address, "sip:") || strings.HasPrefix(req.Address, "sips:") { - return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must be a hostname without 'sip:' prefix") - } - if strings.Contains(req.Address, "transport=") { - return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must not contain parameters; use transport field") - } - if strings.ContainsAny(req.Address, ";=") { - return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must not contain parameters") - } + defaultHost := c.ContactURI(TransportFrom(req.Transport)).GetHost() log := c.log if req.ProjectId != "" { log = log.WithValues("projectID", req.ProjectId) @@ -212,6 +365,10 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea if err != nil { return nil, err } + uri, from, to, err := buildOutboundHeaders(req, defaultHost) + if err != nil { + return nil, err + } tid := traceid.FromGUID(req.SipCallId) log = log.WithValues( "callID", req.SipCallId, @@ -219,15 +376,17 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea "room", req.RoomName, "participant", req.ParticipantIdentity, "participantName", req.ParticipantName, - "fromHost", req.Hostname, - "fromUser", req.Number, - "toHost", req.Address, - "toUser", req.CallTo, + "fromHost", from.Address.Host, + "fromUser", from.Address.User, + "toHost", to.Address.Host, + "toUser", to.Address.User, + "reqHost", uri.Host, + "reqUser", uri.User, "direction", "outbound", ) req.ParticipantAttributes = maps.Clone(req.ParticipantAttributes) // shallow clone - string/string map. Needed to avoid mutating psrpc req - initial := c.createSIPCallInfo(req) + initial := c.createSIPCallInfo(uri, from, to, req) state := NewCallState(c.getStateHandler(req.ProjectId, req.Observability, initial), initial) defer func() { @@ -255,11 +414,10 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea }, } sipConf := sipOutboundConfig{ - address: req.Address, transport: req.Transport, - host: req.Hostname, - from: req.Number, - to: req.CallTo, + uri: uri, + from: from, + to: to, user: req.Username, pass: req.Password, dtmf: req.Dtmf, @@ -273,7 +431,6 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea enabledFeatures: req.EnabledFeatures, featureFlags: req.FeatureFlags, mediaConfig: mconf, - displayName: req.DisplayName, } log.Infow("Creating SIP participant") call, err := c.newCall(ctx, tid, c.conf, log, LocalTag(req.SipCallId), roomConf, sipConf, state, req.ProjectId) @@ -299,13 +456,10 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea return info, nil } -func (c *Client) createSIPCallInfo(req *rpc.InternalCreateSIPParticipantRequest) *livekit.SIPCallInfo { - toUri := CreateURIFromUserAndAddress(req.CallTo, req.Address, TransportFrom(req.Transport)) - fromiUri := URI{ - User: req.Number, - Host: req.Hostname, - Addr: netip.AddrPortFrom(c.sconf.SignalingIP, uint16(c.conf.SIPPort)), - } +func (c *Client) createSIPCallInfo(uri *sip.Uri, from *sip.FromHeader, to *sip.ToHeader, req *rpc.InternalCreateSIPParticipantRequest) *livekit.SIPCallInfo { + toUri := ConvertURI(&to.Address) + fromUri := ConvertURI(&from.Address) + fromUri.Addr = netip.AddrPortFrom(c.sconf.SignalingIP, uint16(c.conf.SIPPort)) callInfo := &livekit.SIPCallInfo{ CallId: req.SipCallId, @@ -316,7 +470,7 @@ func (c *Client) createSIPCallInfo(req *rpc.InternalCreateSIPParticipantRequest) ParticipantAttributes: req.ParticipantAttributes, CallDirection: livekit.SIPCallDirection_SCD_OUTBOUND, ToUri: toUri.ToSIPUri(), - FromUri: fromiUri.ToSIPUri(), + FromUri: fromUri.ToSIPUri(), CreatedAtNs: time.Now().UnixNano(), MediaEncryption: req.MediaEncryption.String(), EnabledFeatures: req.EnabledFeatures, diff --git a/pkg/sip/outbound.go b/pkg/sip/outbound.go index 991e55dfe..d183dee91 100644 --- a/pkg/sip/outbound.go +++ b/pkg/sip/outbound.go @@ -46,11 +46,10 @@ import ( ) type sipOutboundConfig struct { - address string transport livekit.SIPTransport - host string - from string - to string + uri *sip.Uri + from *sip.FromHeader + to *sip.ToHeader user string pass string dtmf string @@ -64,7 +63,6 @@ type sipOutboundConfig struct { enabledFeatures []livekit.SIPFeature featureFlags map[string]string mediaConfig *sipMediaConfig - displayName *string } type outboundCall struct { @@ -104,15 +102,7 @@ func (c *Client) newCall(ctx context.Context, tid traceid.ID, conf *config.Confi tr := TransportFrom(sipConf.transport) contact := c.ContactURI(tr) - if sipConf.host == "" { - sipConf.host = contact.GetHost() - } - fromURI := URI{ - User: sipConf.from, - Host: sipConf.host, - Addr: contact.Addr, - Transport: tr, - } + now := time.Now() call := &outboundCall{ c: c, @@ -126,13 +116,13 @@ func (c *Client) newCall(ctx context.Context, tid traceid.ID, conf *config.Confi projectID: projectID, } call.stats.Update() - call.cc = c.newOutbound(log, id, fromURI, contact, sipConf.displayName, call.setAttrsToHeaders) + call.cc = c.newOutbound(log, id, sipConf.uri, sipConf.to, sipConf.from, contact, call.setAttrsToHeaders) call.log = call.log.WithValues("jitterBuf", call.jitterBuf, "sipCallID", call.cc.callID) if sipConf.featureFlags[outboundRouteHeadersFeatureFlag] == "true" { call.cc.routeHeaders = conf.OutboundRouteHeaders } - call.mon = c.mon.NewCall(stats.Outbound, sipConf.host, sipConf.address) + call.mon = c.mon.NewCall(stats.Outbound, sipConf.from.Address.Host, sipConf.to.Address.Host) var err error call.media, err = NewMediaPort(tid, call.log, call.mon, &MediaOptions{ @@ -602,10 +592,8 @@ func (c *outboundCall) sipSignal(ctx context.Context, tid traceid.ID) error { c.mon.InviteReq() c.sigTs.InviteTime = time.Now() - toUri := CreateURIFromUserAndAddress(c.sipConf.to, c.sipConf.address, TransportFrom(c.sipConf.transport)) - ringing := false - sdpResp, err := c.cc.Invite(ctx, toUri, c.sipConf.user, c.sipConf.pass, c.sipConf.headers, sdpOfferData, func(code sip.StatusCode, hdrs Headers) { + sdpResp, err := c.cc.Invite(ctx, c.sipConf.user, c.sipConf.pass, c.sipConf.headers, sdpOfferData, func(code sip.StatusCode, hdrs Headers) { if code == sip.StatusOK { return // is set separately } @@ -744,27 +732,19 @@ func (c *outboundCall) transferCall(ctx context.Context, transferTo string, head return nil } -func (c *Client) newOutbound(log logger.Logger, id LocalTag, from, contact URI, displayName *string, getHeaders setHeadersFunc) *sipOutbound { - from = from.Normalize() - if displayName == nil { // Nothing specified, preserve legacy behavior - displayName = &from.User - } - - fromHeader := &sip.FromHeader{ - DisplayName: *displayName, - Address: *from.GetURI(), - Params: sip.NewParams(), - } +func (c *Client) newOutbound(log logger.Logger, id LocalTag, uri *sip.Uri, to *sip.ToHeader, from *sip.FromHeader, contact URI, getHeaders setHeadersFunc) *sipOutbound { contactHeader := &sip.ContactHeader{ Address: *contact.GetContactURI(), } - fromHeader.Params.Add("tag", string(id)) + from.Params.Add("tag", string(id)) return &sipOutbound{ log: log, c: c, id: id, callID: guid.HashedID(string(id)), - from: fromHeader, + uri: uri, + to: to, + from: from, contact: contactHeader, referDone: make(chan error), // Do not buffer the channel to avoid reading a result for an old request nextCSeq: 1, @@ -776,7 +756,9 @@ type sipOutbound struct { log logger.Logger c *Client id LocalTag + uri *sip.Uri from *sip.FromHeader + to *sip.ToHeader contact *sip.ContactHeader routeHeaders []string @@ -786,7 +768,6 @@ type sipOutbound struct { invite *sip.Request inviteOk *sip.Response localSDP []byte // SDP Offer, constrained by the answer - to *sip.ToHeader nextCSeq uint32 getHeaders setHeadersFunc @@ -886,12 +867,11 @@ func (c *sipOutbound) RemoteHeaders() Headers { return c.inviteOk.Headers() } -func (c *sipOutbound) Invite(ctx context.Context, to URI, user, pass string, headers map[string]string, sdpOffer []byte, setState sipRespFunc) ([]byte, error) { +func (c *sipOutbound) Invite(ctx context.Context, user, pass string, headers map[string]string, sdpOffer []byte, setState sipRespFunc) ([]byte, error) { ctx, span := Tracer.Start(ctx, "sip.outbound.Invite") defer span.End() c.mu.Lock() defer c.mu.Unlock() - toHeader := &sip.ToHeader{Address: *to.GetURI()} var ( sipHeaders Headers @@ -912,7 +892,7 @@ authLoop: if try >= 5 { return nil, psrpc.NewError(psrpc.FailedPrecondition, ErrAuthMaxRetry) } - req, resp, err = c.attemptInvite(ctx, sip.CallIDHeader(c.callID), toHeader, sdpOffer, authHeaderRespName, authHeader, sipHeaders, setState) + req, resp, err = c.attemptInvite(ctx, sip.CallIDHeader(c.callID), sdpOffer, authHeaderRespName, authHeader, sipHeaders, setState) if err != nil { return nil, err } @@ -980,7 +960,7 @@ authLoop: } c.invite, c.inviteOk = req, resp - toHeader = resp.To() + toHeader := resp.To() if toHeader == nil { return nil, psrpc.NewErrorf(psrpc.Internal, "no To header in INVITE response") } @@ -1026,16 +1006,16 @@ func (c *sipOutbound) AckInviteOK(ctx context.Context) error { return c.c.sipCli.WriteRequest(sip.NewAckRequest(c.invite, c.inviteOk, nil)) } -func (c *sipOutbound) attemptInvite(ctx context.Context, callID sip.CallIDHeader, to *sip.ToHeader, offer []byte, authHeaderName, authHeader string, headers Headers, setState sipRespFunc) (*sip.Request, *sip.Response, error) { +func (c *sipOutbound) attemptInvite(ctx context.Context, callID sip.CallIDHeader, offer []byte, authHeaderName, authHeader string, headers Headers, setState sipRespFunc) (*sip.Request, *sip.Response, error) { ctx, span := Tracer.Start(ctx, "sip.outbound.attemptInvite") defer span.End() - req := sip.NewRequest(sip.INVITE, to.Address) + req := sip.NewRequest(sip.INVITE, *c.uri) c.setCSeq(req) req.RemoveHeader("Call-ID") req.AppendHeader(&callID) req.SetBody(offer) - req.AppendHeader(to) + req.AppendHeader(c.to) req.AppendHeader(c.from) req.AppendHeader(c.contact) diff --git a/pkg/sip/outbound_test.go b/pkg/sip/outbound_test.go index 9a4f67463..dbd6c25f2 100644 --- a/pkg/sip/outbound_test.go +++ b/pkg/sip/outbound_test.go @@ -22,6 +22,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/livekit/protocol/livekit" + "github.com/livekit/protocol/rpc" "github.com/livekit/sipgo/sip" ) @@ -124,3 +126,245 @@ func TestOutboundRouteHeaderWithRecordRoute(t *testing.T) { cancel() } + +func TestBuildOutboundHeaders(t *testing.T) { + newReq := func() *rpc.InternalCreateSIPParticipantRequest { + return &rpc.InternalCreateSIPParticipantRequest{} + } + check := func(t testing.TB, req *rpc.InternalCreateSIPParticipantRequest, defaultHost string, expURI, expFrom, expTo, expErr string) { + if defaultHost == "" { + defaultHost = "sip.default.test" + } + uri, from, to, err := buildOutboundHeaders(req, defaultHost) + if expErr != "" { + require.Error(t, err) + require.Equal(t, expErr, err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, expURI, uri.String()) + require.Equal(t, expFrom, from.String()) + require.Equal(t, expTo, to.String()) + } + expectErr := func(t testing.TB, req *rpc.InternalCreateSIPParticipantRequest, expErr string) { + check(t, req, "", "", "", "", expErr) + } + expect := func(t testing.TB, req *rpc.InternalCreateSIPParticipantRequest, expURI, expFrom, expTo string) { + check(t, req, "", expURI, expFrom, expTo, "") + } + uriVals := func(u *livekit.SIPUri) *livekit.SIPRequestDest { + return &livekit.SIPRequestDest{ + Uri: &livekit.SIPRequestDest_Values{ + Values: u, + }, + } + } + uriRaw := func(raw string) *livekit.SIPRequestDest { + return &livekit.SIPRequestDest{ + Uri: &livekit.SIPRequestDest_Raw{ + Raw: raw, + }, + } + } + namedVals := func(name string, u *livekit.SIPUri) *livekit.SIPNamedDest { + return &livekit.SIPNamedDest{ + DisplayName: name, + Uri: &livekit.SIPNamedDest_Values{ + Values: u, + }, + } + } + namedRaw := func(name string, raw string) *livekit.SIPNamedDest { + return &livekit.SIPNamedDest{ + DisplayName: name, + Uri: &livekit.SIPNamedDest_Raw{ + Raw: raw, + }, + } + } + t.Run("empty", func(t *testing.T) { + req := newReq() + expectErr(t, req, "invalid request URI: number must be set") + }) + t.Run("legacy", func(t *testing.T) { + req := newReq() + req.Address = "sip.test.com" + req.Number = "111" + req.CallTo = "222" + expect(t, req, + `sip:222@sip.test.com`, + `From: "111" `, + `To: `, + ) + }) + t.Run("legacy name", func(t *testing.T) { + req := newReq() + req.Address = "sip.test.com" + req.Number = "111" + req.CallTo = "222" + req.DisplayName = new("LK") + expect(t, req, + `sip:222@sip.test.com`, + `From: "LK" `, + `To: `, + ) + }) + t.Run("legacy and uri", func(t *testing.T) { + req := newReq() + req.Address = "sip.test.com" + req.Number = "111" + req.CallTo = "222" + req.SipRequestUri = uriVals(&livekit.SIPUri{ + User: "333", + Host: "sip.another.com", + }) + expect(t, req, + `sip:333@sip.another.com`, + `From: "111" `, + `To: `, + ) + }) + t.Run("legacy and uri raw", func(t *testing.T) { + req := newReq() + req.Address = "sip.test.com" + req.Number = "111" + req.CallTo = "222" + req.SipRequestUri = uriRaw(`sip:333@sip.another.com`) + expect(t, req, + `sip:333@sip.another.com`, + `From: "111" `, + `To: `, + ) + }) + t.Run("legacy and From", func(t *testing.T) { + req := newReq() + req.Address = "sip.test.com" + req.Number = "111" + req.CallTo = "222" + req.SipFromHeader = namedVals("LK", &livekit.SIPUri{ + User: "333", + Host: "sip.another.com", + }) + expect(t, req, + `sip:222@sip.test.com`, + `From: "LK" `, + `To: `, + ) + }) + t.Run("legacy and To both", func(t *testing.T) { + req := newReq() + req.Address = "sip.test.com" + req.Number = "111" + req.CallTo = "222" + req.SipToHeader = namedVals("User", &livekit.SIPUri{ + User: "333", + Host: "sip.another.com", + }) + expectErr(t, req, "invalid To header: cannot use both CallTo and SipToHeader") + }) + t.Run("legacy and To addr", func(t *testing.T) { + // Allow both Address and To. Address could be used as a network-level destination. + req := newReq() + req.Address = "1.2.3.4" + req.Number = "111" + req.SipToHeader = namedVals("User", &livekit.SIPUri{ + User: "333", + Host: "sip.another.com", + }) + // However, CallTo is needed for request URI, but it cannot be set because it conflicts with To header. + expectErr(t, req, "invalid request URI: number must be set") + }) + t.Run("all new", func(t *testing.T) { + req := newReq() + req.SipRequestUri = uriVals(&livekit.SIPUri{ + User: "222", + Host: "sip.test.com", + }) + req.SipFromHeader = namedVals("LK", &livekit.SIPUri{ + User: "111", + Host: "example.com", // OSS can override the hostname + }) + req.SipToHeader = namedVals("User", &livekit.SIPUri{ + User: "333", + Host: "sip.another.com", + }) + expect(t, req, + `sip:222@sip.test.com`, + `From: "LK" `, // OSS can override the hostname + `To: "User" `, + ) + }) + t.Run("all raw brackets", func(t *testing.T) { + req := newReq() + req.SipRequestUri = uriRaw(`sip:222@sip.test.com`) + req.SipFromHeader = namedRaw("LK", ``) + req.SipToHeader = namedRaw("User", ``) + expect(t, req, + `sip:222@sip.test.com`, + `From: "LK" `, + `To: "User" `, + ) + }) + t.Run("all raw no brackets", func(t *testing.T) { + req := newReq() + req.SipRequestUri = uriRaw(`sip:222@sip.test.com`) + req.SipFromHeader = namedRaw("LK", `sip:111@sip.livekit.test`) + req.SipToHeader = namedRaw("User", `sip:333@sip.another.com`) + expect(t, req, + `sip:222@sip.test.com`, + `From: "LK" `, + `To: "User" `, + ) + }) + t.Run("raw param override", func(t *testing.T) { + req := newReq() + req.SipRequestUri = uriRaw(`sip:222@sip.test.com`) + req.SipFromHeader = namedRaw("LK", `;tag=AAA`) + req.SipToHeader = namedRaw("User", `;tag=BBB`) + expectErr(t, req, "invalid To header: invalid request URI") + }) + t.Run("all raw transport", func(t *testing.T) { + req := newReq() + req.SipRequestUri = uriRaw(`sip:222@sip.test.com;transport=tcp`) + req.SipFromHeader = namedRaw("LK", `sip:111@sip.livekit.test;transport=tcp`) + req.SipToHeader = namedRaw("User", `sip:333@sip.another.com;transport=tcp`) + expect(t, req, + `sip:222@sip.test.com;transport=tcp`, + `From: "LK" `, + `To: "User" `, + ) + }) + t.Run("all raw req transport", func(t *testing.T) { + req := newReq() + req.Transport = livekit.SIPTransport_SIP_TRANSPORT_TLS + req.SipRequestUri = uriRaw(`sip:222@sip.test.com;transport=tcp`) + req.SipFromHeader = namedRaw("LK", `sip:111@example.com;transport=tcp`) + req.SipToHeader = namedRaw("User", `sip:333@sip.another.com;transport=tcp`) + expect(t, req, + `sip:222@sip.test.com;transport=tls`, + `From: "LK" `, + `To: "User" `, + ) + }) + t.Run("all new req transport", func(t *testing.T) { + req := newReq() + req.Transport = livekit.SIPTransport_SIP_TRANSPORT_TLS + req.SipRequestUri = uriVals(&livekit.SIPUri{ + User: "222", + Host: "sip.test.com", + }) + req.SipFromHeader = namedVals("LK", &livekit.SIPUri{ + User: "111", + Host: "example.com", + }) + req.SipToHeader = namedVals("User", &livekit.SIPUri{ + User: "333", + Host: "sip.another.com", + }) + expect(t, req, + `sip:222@sip.test.com;transport=tls`, + `From: "LK" `, + `To: "User" `, + ) + }) +}