Skip to content

Commit dfc0dd4

Browse files
authored
Support specifying To/From SIP headers for outbound. (#702)
1 parent cbb720f commit dfc0dd4

3 files changed

Lines changed: 455 additions & 77 deletions

File tree

pkg/sip/client.go

Lines changed: 191 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@ package sip
1616

1717
import (
1818
"context"
19+
"errors"
20+
"fmt"
1921
"log/slog"
22+
"net"
2023
"net/netip"
24+
"strconv"
2125
"strings"
2226
"sync"
2327
"time"
2428

2529
"github.com/frostbyte73/core"
2630
"golang.org/x/exp/maps"
2731

32+
esip "github.com/emiago/sipgo/sip"
33+
2834
"github.com/livekit/protocol/livekit"
2935
"github.com/livekit/protocol/logger"
3036
"github.com/livekit/protocol/rpc"
@@ -175,32 +181,179 @@ func (c *Client) getActiveCall(tag LocalTag) *outboundCall {
175181
return c.activeCalls[tag]
176182
}
177183

184+
func setUriTransport(p *sip.Uri, tr livekit.SIPTransport) {
185+
if tr != livekit.SIPTransport_SIP_TRANSPORT_AUTO {
186+
p.UriParams.Add("transport", tr.Name())
187+
}
188+
}
189+
190+
func buildLegacyURI(user, addr string, tr livekit.SIPTransport) (*sip.Uri, error) {
191+
if user == "" {
192+
return nil, fmt.Errorf("number must be set")
193+
} else if strings.Contains(user, "@") {
194+
return nil, fmt.Errorf("should be a phone number or SIP user, not a full SIP URI")
195+
}
196+
if addr == "" {
197+
return nil, fmt.Errorf("address must be set")
198+
}
199+
if strings.HasPrefix(addr, "sip:") || strings.HasPrefix(addr, "sips:") {
200+
return nil, fmt.Errorf("address must be a hostname without 'sip:' prefix")
201+
} else if strings.Contains(addr, "transport=") {
202+
return nil, fmt.Errorf("legacy address must not contain parameters; use transport field")
203+
} else if strings.ContainsAny(addr, ";=") {
204+
return nil, fmt.Errorf("legacy address must not contain parameters")
205+
}
206+
p := &sip.Uri{Scheme: "sip"}
207+
setUriTransport(p, tr)
208+
209+
p.User = user
210+
if host, sport, err := net.SplitHostPort(addr); err == nil && sport != "" {
211+
p.Host = host
212+
p.Port, err = strconv.Atoi(sport)
213+
if err != nil {
214+
return nil, fmt.Errorf("invalid port in hostname: %q", sport)
215+
}
216+
} else {
217+
p.Host = addr
218+
}
219+
return p, nil
220+
}
221+
222+
func buildRawURI(raw string, tr livekit.SIPTransport) (*sip.Uri, error) {
223+
p := &sip.Uri{Scheme: "sip"}
224+
if n := len(raw); n != 0 && raw[0] == '<' && raw[n-1] == '>' {
225+
raw = raw[1 : n-1]
226+
}
227+
if err := esip.ParseUri(raw, p); err != nil {
228+
return nil, errors.New("invalid request URI")
229+
}
230+
setUriTransport(p, tr)
231+
return p, nil
232+
}
233+
234+
func buildValuesURI(u *livekit.SIPUri, tr livekit.SIPTransport) (*sip.Uri, error) {
235+
if tr != u.Transport {
236+
if u.Transport == livekit.SIPTransport_SIP_TRANSPORT_AUTO {
237+
//tr = tr
238+
} else if tr == livekit.SIPTransport_SIP_TRANSPORT_AUTO {
239+
tr = u.Transport
240+
} else {
241+
return nil, fmt.Errorf("different transports specified: %v vs %v", tr, u.Transport)
242+
}
243+
}
244+
p := &sip.Uri{Scheme: "sip"}
245+
setUriTransport(p, tr)
246+
if u.User == "" {
247+
return nil, fmt.Errorf("username or number must be set")
248+
}
249+
if u.Host == "" && u.Ip == "" {
250+
return nil, fmt.Errorf("host or ip must be set")
251+
}
252+
p.User = u.User
253+
p.Host = u.Host
254+
if p.Host == "" {
255+
p.Host = u.Ip
256+
}
257+
if _, sport, err := net.SplitHostPort(p.Host); err == nil && sport != "" {
258+
return nil, fmt.Errorf("host or ip must not contain port")
259+
}
260+
p.Port = int(u.Port)
261+
return p, nil
262+
}
263+
264+
func buildRequestURI(u *livekit.SIPRequestDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.Uri, error) {
265+
if u == nil {
266+
return buildLegacyURI(legacyUser, legacyAddr, tr)
267+
}
268+
switch u := u.Uri.(type) {
269+
default:
270+
case *livekit.SIPRequestDest_Raw:
271+
return buildRawURI(u.Raw, tr)
272+
case *livekit.SIPRequestDest_Values:
273+
return buildValuesURI(u.Values, tr)
274+
}
275+
return nil, fmt.Errorf("invalid request URI type")
276+
}
277+
278+
func buildFromToURI(u *livekit.SIPNamedDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.Uri, error) {
279+
if u == nil {
280+
return buildLegacyURI(legacyUser, legacyAddr, tr)
281+
}
282+
switch u := u.Uri.(type) {
283+
default:
284+
case *livekit.SIPNamedDest_Raw:
285+
return buildRawURI(u.Raw, tr)
286+
case *livekit.SIPNamedDest_Values:
287+
return buildValuesURI(u.Values, tr)
288+
}
289+
return nil, fmt.Errorf("invalid URI type")
290+
}
291+
292+
func buildFromHeader(u *livekit.SIPNamedDest, legacyName *string, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.FromHeader, error) {
293+
su, err := buildFromToURI(u, legacyUser, legacyAddr, tr)
294+
if err != nil {
295+
return nil, err
296+
}
297+
h := &sip.FromHeader{
298+
Address: *su,
299+
}
300+
if u != nil {
301+
h.DisplayName = u.DisplayName
302+
} else if legacyName != nil {
303+
h.DisplayName = *legacyName
304+
} else {
305+
// Nothing specified, preserve legacy behavior
306+
h.DisplayName = su.User
307+
}
308+
return h, nil
309+
}
310+
311+
func buildToHeader(u *livekit.SIPNamedDest, legacyUser, legacyAddr string, tr livekit.SIPTransport) (*sip.ToHeader, error) {
312+
if u != nil && legacyUser != "" {
313+
return nil, errors.New("cannot use both CallTo and SipToHeader")
314+
}
315+
su, err := buildFromToURI(u, legacyUser, legacyAddr, tr)
316+
if err != nil {
317+
return nil, err
318+
}
319+
h := &sip.ToHeader{
320+
Address: *su,
321+
}
322+
if u != nil {
323+
h.DisplayName = u.DisplayName
324+
}
325+
return h, nil
326+
}
327+
328+
func buildOutboundHeaders(req *rpc.InternalCreateSIPParticipantRequest, defaultHost string) (*sip.Uri, *sip.FromHeader, *sip.ToHeader, error) {
329+
uri, err := buildRequestURI(req.SipRequestUri, req.CallTo, req.Address, req.Transport)
330+
if err != nil {
331+
return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid request URI: %w", err))
332+
}
333+
to, err := buildToHeader(req.SipToHeader, req.CallTo, req.Address, req.Transport)
334+
if err != nil {
335+
return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid To header: %w", err))
336+
}
337+
fromHost := req.Hostname
338+
if fromHost == "" {
339+
fromHost = defaultHost
340+
}
341+
from, err := buildFromHeader(req.SipFromHeader, req.DisplayName, req.Number, fromHost, req.Transport)
342+
if err != nil {
343+
return nil, nil, nil, psrpc.NewError(psrpc.InvalidArgument, fmt.Errorf("invalid From header: %w", err))
344+
}
345+
return uri, from, to, nil
346+
}
347+
178348
func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCreateSIPParticipantRequest) (resp *rpc.InternalCreateSIPParticipantResponse, retErr error) {
179349
if c.mon.Health() != stats.HealthOK {
180350
return nil, siperrors.ErrUnavailable
181351
}
182352
req.Upgrade()
183-
if req.CallTo == "" {
184-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "call-to number must be set")
185-
} else if req.Address == "" {
186-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "trunk adresss must be set")
187-
} else if req.Number == "" {
188-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "trunk outbound number must be set")
189-
} else if req.RoomName == "" {
353+
if req.RoomName == "" {
190354
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "room name must be set")
191355
}
192-
if strings.Contains(req.CallTo, "@") {
193-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "call_to should be a phone number or SIP user, not a full SIP URI")
194-
}
195-
if strings.HasPrefix(req.Address, "sip:") || strings.HasPrefix(req.Address, "sips:") {
196-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must be a hostname without 'sip:' prefix")
197-
}
198-
if strings.Contains(req.Address, "transport=") {
199-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must not contain parameters; use transport field")
200-
}
201-
if strings.ContainsAny(req.Address, ";=") {
202-
return nil, psrpc.NewErrorf(psrpc.InvalidArgument, "address must not contain parameters")
203-
}
356+
defaultHost := c.ContactURI(TransportFrom(req.Transport)).GetHost()
204357
log := c.log
205358
if req.ProjectId != "" {
206359
log = log.WithValues("projectID", req.ProjectId)
@@ -212,22 +365,28 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
212365
if err != nil {
213366
return nil, err
214367
}
368+
uri, from, to, err := buildOutboundHeaders(req, defaultHost)
369+
if err != nil {
370+
return nil, err
371+
}
215372
tid := traceid.FromGUID(req.SipCallId)
216373
log = log.WithValues(
217374
"callID", req.SipCallId,
218375
"traceID", tid.String(),
219376
"room", req.RoomName,
220377
"participant", req.ParticipantIdentity,
221378
"participantName", req.ParticipantName,
222-
"fromHost", req.Hostname,
223-
"fromUser", req.Number,
224-
"toHost", req.Address,
225-
"toUser", req.CallTo,
379+
"fromHost", from.Address.Host,
380+
"fromUser", from.Address.User,
381+
"toHost", to.Address.Host,
382+
"toUser", to.Address.User,
383+
"reqHost", uri.Host,
384+
"reqUser", uri.User,
226385
"direction", "outbound",
227386
)
228387

229388
req.ParticipantAttributes = maps.Clone(req.ParticipantAttributes) // shallow clone - string/string map. Needed to avoid mutating psrpc req
230-
initial := c.createSIPCallInfo(req)
389+
initial := c.createSIPCallInfo(uri, from, to, req)
231390
state := NewCallState(c.getStateHandler(req.ProjectId, req.Observability, initial), initial)
232391

233392
defer func() {
@@ -255,11 +414,10 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
255414
},
256415
}
257416
sipConf := sipOutboundConfig{
258-
address: req.Address,
259417
transport: req.Transport,
260-
host: req.Hostname,
261-
from: req.Number,
262-
to: req.CallTo,
418+
uri: uri,
419+
from: from,
420+
to: to,
263421
user: req.Username,
264422
pass: req.Password,
265423
dtmf: req.Dtmf,
@@ -273,7 +431,6 @@ func (c *Client) createSIPParticipant(ctx context.Context, req *rpc.InternalCrea
273431
enabledFeatures: req.EnabledFeatures,
274432
featureFlags: req.FeatureFlags,
275433
mediaConfig: mconf,
276-
displayName: req.DisplayName,
277434
}
278435
log.Infow("Creating SIP participant")
279436
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
299456
return info, nil
300457
}
301458

302-
func (c *Client) createSIPCallInfo(req *rpc.InternalCreateSIPParticipantRequest) *livekit.SIPCallInfo {
303-
toUri := CreateURIFromUserAndAddress(req.CallTo, req.Address, TransportFrom(req.Transport))
304-
fromiUri := URI{
305-
User: req.Number,
306-
Host: req.Hostname,
307-
Addr: netip.AddrPortFrom(c.sconf.SignalingIP, uint16(c.conf.SIPPort)),
308-
}
459+
func (c *Client) createSIPCallInfo(uri *sip.Uri, from *sip.FromHeader, to *sip.ToHeader, req *rpc.InternalCreateSIPParticipantRequest) *livekit.SIPCallInfo {
460+
toUri := ConvertURI(&to.Address)
461+
fromUri := ConvertURI(&from.Address)
462+
fromUri.Addr = netip.AddrPortFrom(c.sconf.SignalingIP, uint16(c.conf.SIPPort))
309463

310464
callInfo := &livekit.SIPCallInfo{
311465
CallId: req.SipCallId,
@@ -316,7 +470,7 @@ func (c *Client) createSIPCallInfo(req *rpc.InternalCreateSIPParticipantRequest)
316470
ParticipantAttributes: req.ParticipantAttributes,
317471
CallDirection: livekit.SIPCallDirection_SCD_OUTBOUND,
318472
ToUri: toUri.ToSIPUri(),
319-
FromUri: fromiUri.ToSIPUri(),
473+
FromUri: fromUri.ToSIPUri(),
320474
CreatedAtNs: time.Now().UnixNano(),
321475
MediaEncryption: req.MediaEncryption.String(),
322476
EnabledFeatures: req.EnabledFeatures,

0 commit comments

Comments
 (0)