├── testdata └── sounds │ ├── README.md │ ├── demo-thanks.alaw │ ├── demo-thanks.g729 │ ├── demo-thanks.ulaw │ └── demo-thanks.wav ├── echome ├── README.md ├── echouas │ └── main.go └── echouac │ └── main.go ├── .gitignore ├── go.mod ├── LICENSE ├── utils.go ├── dialog_client.go ├── README.md ├── dialog_server.go ├── register_transaction.go ├── go.sum └── phone.go /testdata/sounds/README.md: -------------------------------------------------------------------------------- 1 | 2 | This files are download from asterisk public repo for testing purposes only. -------------------------------------------------------------------------------- /testdata/sounds/demo-thanks.alaw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emiago/sipgox/HEAD/testdata/sounds/demo-thanks.alaw -------------------------------------------------------------------------------- /testdata/sounds/demo-thanks.g729: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emiago/sipgox/HEAD/testdata/sounds/demo-thanks.g729 -------------------------------------------------------------------------------- /testdata/sounds/demo-thanks.ulaw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emiago/sipgox/HEAD/testdata/sounds/demo-thanks.ulaw -------------------------------------------------------------------------------- /testdata/sounds/demo-thanks.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emiago/sipgox/HEAD/testdata/sounds/demo-thanks.wav -------------------------------------------------------------------------------- /echome/README.md: -------------------------------------------------------------------------------- 1 | # echome 2 | 3 | This is echo tester that can be used with asterisk Echo. 4 | 5 | It sends RTP packet every 200ms and checks response -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /astscale 2 | /dialog 3 | /goproxy 4 | /go.work* 5 | /gophone 6 | /soundplayer 7 | /.vscode 8 | /g711 9 | /pbx 10 | /rtp_session* 11 | /rtp_playout_buffer* 12 | /MEDIA_QUALITY.md 13 | /asterisk-sounds-wav -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emiago/sipgox 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.5 6 | 7 | require ( 8 | github.com/emiago/media v0.1.1-0.20240619212740-bf8c5574162c 9 | github.com/emiago/sipgo v0.22.0 10 | github.com/icholy/digest v0.1.22 11 | github.com/pion/rtp v1.8.6 12 | github.com/rs/zerolog v1.32.0 13 | github.com/stretchr/testify v1.9.0 14 | ) 15 | 16 | require ( 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | 22 | require ( 23 | github.com/gobwas/httphead v0.1.0 // indirect 24 | github.com/gobwas/pool v0.2.1 // indirect 25 | github.com/gobwas/ws v1.3.2 // indirect 26 | github.com/google/uuid v1.6.0 // indirect 27 | github.com/mattn/go-colorable v0.1.13 // indirect 28 | github.com/mattn/go-isatty v0.0.20 // indirect 29 | github.com/pion/randutil v0.1.0 // indirect 30 | github.com/pion/rtcp v1.2.10 // indirect 31 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect 32 | golang.org/x/sys v0.19.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Emir Aganovic. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package sipgox 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net" 7 | "strings" 8 | 9 | "github.com/emiago/sipgo/sip" 10 | ) 11 | 12 | // We are lazy to write full sip uris 13 | func CheckLazySipUri(target string, destOverwrite string) string { 14 | if !strings.Contains(target, "@") { 15 | target = target + "@" + destOverwrite 16 | } 17 | 18 | if !strings.HasPrefix(target, "sip") { 19 | target = "sip:" + target 20 | } 21 | 22 | return target 23 | } 24 | 25 | func resolveHostIPWithTarget(network string, targetAddr string) (net.IP, error) { 26 | tip, _, _ := sip.ParseAddr(targetAddr) 27 | ip := net.ParseIP(tip) 28 | 29 | if network == "udp" { 30 | if ip != nil { 31 | if ip.IsLoopback() { 32 | // TO avoid UDP COnnected connection problem hitting different subnet 33 | return net.ParseIP("127.0.0.99"), nil 34 | } 35 | } 36 | } 37 | rip, _, err := sip.ResolveInterfacesIP("ip4", nil) 38 | return rip, err 39 | } 40 | 41 | func FindFreeInterfaceHostPort(network string, targetAddr string) (ip net.IP, port int, err error) { 42 | // Go with random 43 | // use empheral instead of this 44 | ip, err = resolveHostIPWithTarget(network, targetAddr) 45 | if err != nil { 46 | return ip, port, err 47 | } 48 | 49 | port, err = findFreePort(network, ip) 50 | if err != nil { 51 | return 52 | } 53 | 54 | if port == 0 { 55 | return ip, port, fmt.Errorf("failed to find free port") 56 | } 57 | 58 | return ip, port, err 59 | } 60 | 61 | func findFreePort(network string, ip net.IP) (int, error) { 62 | switch network { 63 | case "udp": 64 | var l *net.UDPConn 65 | l, err := net.ListenUDP("udp", &net.UDPAddr{IP: ip}) 66 | if err != nil { 67 | return 0, err 68 | } 69 | l.Close() 70 | // defer l.Close() 71 | port := l.LocalAddr().(*net.UDPAddr).Port 72 | return port, nil 73 | 74 | case "tcp", "ws", "tls", "wss": 75 | var l *net.TCPListener 76 | l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: ip}) 77 | if err != nil { 78 | return 0, err 79 | } 80 | defer l.Close() 81 | port := l.Addr().(*net.TCPAddr).Port 82 | return port, nil 83 | default: 84 | return rand.Intn(9999) + 6000, nil 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /dialog_client.go: -------------------------------------------------------------------------------- 1 | package sipgox 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/emiago/media" 8 | "github.com/emiago/sipgo" 9 | "github.com/emiago/sipgo/sip" 10 | ) 11 | 12 | type DialogClientSession struct { 13 | *media.MediaSession 14 | 15 | *sipgo.DialogClientSession 16 | 17 | subscriptions sync.Map 18 | 19 | // onClose used to cleanup internal logic 20 | onClose func() 21 | } 22 | 23 | func (d *DialogClientSession) Close() error { 24 | defer d.MediaSession.Close() 25 | 26 | if d.onClose != nil { 27 | d.onClose() 28 | } 29 | 30 | return d.DialogClientSession.Close() 31 | } 32 | 33 | // Hangup is alias for Bye 34 | func (d *DialogClientSession) Hangup(ctx context.Context) error { 35 | return d.Bye(ctx) 36 | } 37 | func (d *DialogClientSession) Bye(ctx context.Context) error { 38 | // defer close(d.done) 39 | // Let caller close media as it may delay 40 | // defer d.MediaSession.Close() 41 | return d.DialogClientSession.Bye(ctx) 42 | } 43 | 44 | // Refer tries todo refer (blind transfer) on call 45 | func (d *DialogClientSession) Refer(ctx context.Context, referTo sip.Uri) error { 46 | // TODO check state of call 47 | 48 | req := sip.NewRequest(sip.REFER, d.InviteRequest.Recipient) 49 | UACRequestBuild(req, d.InviteRequest, d.InviteResponse) 50 | 51 | req.AppendHeader(sip.NewHeader("Refer-to", referTo.String())) 52 | 53 | tx, err := d.TransactionRequest(ctx, req) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | select { 59 | case <-tx.Done(): 60 | return tx.Err() 61 | case res := <-tx.Responses(): 62 | if res.StatusCode != sip.StatusAccepted { 63 | return sipgo.ErrDialogResponse{ 64 | Res: res, 65 | } 66 | } 67 | 68 | case <-ctx.Done(): 69 | return tx.Cancel() 70 | } 71 | 72 | // There is now implicit subscription 73 | return nil 74 | } 75 | 76 | // func (d *DialogClientSession) readNotify(req *sip.Request, tx sip.ServerTransaction) error { 77 | // sub := d.subscriptions.Load(req.CallID().Value()) 78 | 79 | // select { 80 | // case <-d.Context().Done(): 81 | // return d.Context().Err() 82 | // case d.notifyChan <- req: 83 | // } 84 | // return nil 85 | // } 86 | 87 | // func (d *DialogClientSession) MediaStream(s MediaStreamer) error { 88 | // return s.MediaStream(d.MediaSession) 89 | // } 90 | -------------------------------------------------------------------------------- /echome/echouas/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "flag" 8 | "io" 9 | "os" 10 | "os/signal" 11 | "time" 12 | 13 | "github.com/emiago/media" 14 | "github.com/emiago/sipgo" 15 | "github.com/emiago/sipgox" 16 | "github.com/pion/rtp" 17 | "github.com/rs/zerolog" 18 | "github.com/rs/zerolog/log" 19 | ) 20 | 21 | func main() { 22 | addr := flag.String("l", "", "My listen ip") 23 | username := flag.String("u", "alice", "SIP Username") 24 | echoCount := flag.Int("echo", 1, "How many echos") 25 | // password := flag.String("p", "alice", "Password") 26 | flag.Parse() 27 | 28 | lev, err := zerolog.ParseLevel(os.Getenv("LOG_LEVEL")) 29 | if err != nil || lev == zerolog.NoLevel { 30 | lev = zerolog.InfoLevel 31 | } 32 | 33 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMicro 34 | log.Logger = zerolog.New(zerolog.ConsoleWriter{ 35 | Out: os.Stdout, 36 | TimeFormat: time.StampMicro, 37 | }).With().Timestamp().Logger().Level(lev) 38 | 39 | // Setup UAC 40 | ua, err := sipgo.NewUA( 41 | sipgo.WithUserAgent(*username), 42 | // sipgo.WithUserAgentIP(net.ParseIP(ip)), 43 | ) 44 | if err != nil { 45 | log.Fatal().Err(err).Msg("Fail to setup user agent") 46 | } 47 | 48 | phoneOpts := []sipgox.PhoneOption{} 49 | if *addr != "" { 50 | phoneOpts = append(phoneOpts, 51 | sipgox.WithPhoneListenAddr(sipgox.ListenAddr{ 52 | // Network: *tran, 53 | Network: "udp", 54 | Addr: *addr, 55 | }), 56 | ) 57 | } 58 | 59 | phone := sipgox.NewPhone(ua, phoneOpts...) 60 | ctx, _ := context.WithCancel(context.Background()) 61 | dialog, err := phone.Answer(ctx, sipgox.AnswerOptions{ 62 | Ringtime: 1 * time.Second, 63 | // SipHeaders: []sip.Header{sip.NewHeader("X-ECHO-ID", "sipgo")}, 64 | }) 65 | if err != nil { 66 | log.Fatal().Err(err).Msg("Fail to answer") 67 | } 68 | 69 | sig := make(chan os.Signal) 70 | signal.Notify(sig, os.Interrupt) 71 | // Start echo 72 | go func() { 73 | buf := make([]byte, media.RTPBufSize) 74 | for { 75 | p := rtp.Packet{} 76 | err := dialog.ReadRTP(buf, &p) 77 | if err != nil { 78 | if errors.Is(err, io.ErrClosedPipe) { 79 | return 80 | } 81 | log.Error().Err(err).Msg("Fail to read RTP") 82 | return 83 | } 84 | 85 | log.Info().Msg(p.String()) 86 | 87 | p.Payload = append(p.Payload, bytes.Repeat(p.Payload, *echoCount-1)...) 88 | 89 | if err := dialog.WriteRTP(&p); err != nil { 90 | log.Error().Err(err).Msg("Fail to echo RTP") 91 | return 92 | } 93 | } 94 | }() 95 | 96 | select { 97 | case <-sig: 98 | ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) 99 | dialog.Hangup(ctx) 100 | return 101 | 102 | case <-dialog.Done(): 103 | return 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sipgox 2 | 3 | is experimental/extra area to add more functionality on top [sipgo lib](https://github.com/emiago/sipgo), 4 | 5 | To find out more check [GO Documentation](https://pkg.go.dev/github.com/emiago/sipgox) 6 | 7 | --- 8 | **NOTE**: All media and phone development is moved to [diago lib](https://github.com/emiago/diago) project. Diago offers more correct way of building phones or servers. 9 | Please do not use this. This repo will keep focus only SIP extra things in future, and it will probably remove phone feature. 10 | 11 | --- 12 | 13 | If you find it useful, support [sipgo lib](https://github.com/emiago/sipgo), open issue etc... 14 | 15 | Checkout [echome](/echome/) example to see more. 16 | 17 | ## Phone (Deprecated, use diago) 18 | 19 | Features: 20 | - [x] Simple API for UA/phone build with dial answer register actions 21 | - [x] Minimal SDP package for audio 22 | - [x] RTP/RTCP receiving and logging 23 | - [x] Extendable MediaSession handling for RTP/RTCP handling (ex microphone,speaker) 24 | - [x] Hangup control on caller 25 | - [x] Timeouts handling 26 | - [x] Digest auth 27 | - [x] Transfers on phone answer, dial 28 | 29 | Phone is wrapper that allows you to build phone in couple of lines. 30 | Then you can quickly create/receive SIP call, handle RTP/RTCP, etc... 31 | It uses `sipgo.Dialog` and `media` package. 32 | 33 | *NOTE*: It has specific design for testing, and it can not be used for full softphone build. 34 | 35 | ### Dialer 36 | 37 | ```go 38 | ua, _ := sipgo.NewUA() 39 | defer ua.Close() 40 | 41 | // Create a phone 42 | phone := sipgox.NewPhone(ua) 43 | 44 | // Run dial 45 | ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) 46 | 47 | // Blocks until call is answered 48 | dialog, err := phone.Dial(ctx, sip.Uri{User:"bob", Host: "localhost", Port:5060}, sipgox.DialOptions{}) 49 | if err != nil { 50 | // handle error 51 | return 52 | } 53 | defer dialog.Close() // Close dialog for cleanup 54 | 55 | select { 56 | case <-dialog.Done(): 57 | return 58 | case <-time.After(5 *time.Second): 59 | dialog.Hangup(context.TODO()) 60 | } 61 | ``` 62 | 63 | ### Receiver 64 | 65 | ```go 66 | ctx, _ := context.WithCancel(context.Background()) 67 | 68 | ua, _ := sipgo.NewUA() 69 | defer ua.Close() 70 | 71 | // Create a phone 72 | phone := sipgox.NewPhone(ua) 73 | 74 | // Blocks until call is answered 75 | dialog, err := phone.Answer(ctx, sipgox.AnswerOptions{ 76 | Ringtime: 5* time.Second, 77 | }) 78 | if err != nil { 79 | //handle error 80 | return 81 | } 82 | defer dialog.Close() // Close dialog for cleanup 83 | 84 | select { 85 | case <-dialog.Done(): 86 | return 87 | case <-time.After(10 *time.Second): 88 | dialog.Hangup(context.TODO()) 89 | } 90 | ``` 91 | 92 | ### Reading/Writing RTP/RTCP on dialog 93 | 94 | After you Answer or Dial on phone, you receive dialog. 95 | 96 | **RTP** 97 | ```go 98 | buf := make([]byte, media.RTPBufSize) // Has MTU size 99 | pkt := rtp.Packet{} 100 | err := dialog.ReadRTP(buf, &pkt) 101 | 102 | err := dialog.WriteRTP(pkt) 103 | 104 | ``` 105 | 106 | similar is for RTCP 107 | -------------------------------------------------------------------------------- /echome/echouac/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "os" 8 | "os/signal" 9 | "time" 10 | 11 | "github.com/emiago/media" 12 | "github.com/emiago/sipgo" 13 | "github.com/emiago/sipgo/sip" 14 | "github.com/emiago/sipgox" 15 | "github.com/pion/rtp" 16 | "github.com/rs/zerolog" 17 | "github.com/rs/zerolog/log" 18 | ) 19 | 20 | func main() { 21 | // addr := flag.String("l", "127.0.0.100:5060", "My listen ip") 22 | // dst := flag.String("d", "127.0.0.10:5060", "Destination") 23 | username := flag.String("u", "alice", "SIP Username") 24 | echoCount := flag.Int("echo", 1, "How many echos to expect") 25 | 26 | // password := flag.String("p", "alice", "Password") 27 | flag.Parse() 28 | 29 | lev, err := zerolog.ParseLevel(os.Getenv("LOG_LEVEL")) 30 | if err != nil || lev == zerolog.NoLevel { 31 | lev = zerolog.InfoLevel 32 | } 33 | 34 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMicro 35 | log.Logger = zerolog.New(zerolog.ConsoleWriter{ 36 | Out: os.Stdout, 37 | TimeFormat: time.StampMicro, 38 | }).With().Timestamp().Logger().Level(lev) 39 | 40 | // Setup UAC 41 | // ip, _, _ := net.SplitHostPort(*addr) 42 | ua, err := sipgo.NewUA( 43 | sipgo.WithUserAgent(*username), 44 | // sipgo.WithUserAgentIP(net.ParseIP(ip)), 45 | ) 46 | if err != nil { 47 | log.Fatal().Err(err).Msg("Fail to setup user agent") 48 | } 49 | 50 | // udpAddr, err := net.ResolveUDPAddr("udp", *addr) 51 | // if err != nil { 52 | // log.Fatal().Err(err).Msg("Bad addr") 53 | // } 54 | 55 | phone := sipgox.NewPhone(ua) // sipgox.WithPhoneListenAddr(udpAddr), 56 | 57 | target := flag.Arg(0) 58 | 59 | recipient := sip.Uri{User: *username, Headers: sip.NewParams()} 60 | if err := sip.ParseUri(target, &recipient); err != nil { 61 | log.Fatal().Err(err).Msg("Target bad format") 62 | } 63 | 64 | ctx, _ := context.WithTimeout(context.Background(), 60*time.Second) 65 | dialog, err := phone.Dial(ctx, recipient, sipgox.DialOptions{}) 66 | if err != nil { 67 | log.Fatal().Err(err).Msg("Fail to dial") 68 | } 69 | defer dialog.Close() 70 | 71 | sequencer := rtp.NewFixedSequencer(1) 72 | sig := make(chan os.Signal) 73 | signal.Notify(sig, os.Interrupt) 74 | 75 | go func() { 76 | buf := make([]byte, media.RTPBufSize) 77 | for { 78 | select { 79 | case <-dialog.Done(): 80 | return 81 | case <-time.After(200 * time.Millisecond): 82 | } 83 | 84 | pkt := &rtp.Packet{ 85 | Header: rtp.Header{ 86 | Version: 2, 87 | Padding: false, 88 | Extension: false, 89 | Marker: false, 90 | PayloadType: 0, 91 | SequenceNumber: sequencer.NextSequenceNumber(), 92 | Timestamp: 20, // We do not care as we are not playing out 93 | SSRC: 111222, 94 | }, 95 | Payload: []byte("1234567890"), 96 | } 97 | 98 | log.Info().Msgf("Sent RTP\n%s", pkt.String()) 99 | 100 | if err := dialog.WriteRTP(pkt); err != nil { 101 | log.Error().Err(err).Msg("Fail to send RTP") 102 | return 103 | } 104 | 105 | p := rtp.Packet{} 106 | err := dialog.ReadRTP(buf, &p) 107 | if err != nil { 108 | log.Error().Err(err).Msg("Fail to read RTP") 109 | return 110 | } 111 | 112 | // use debug if you want to see what is received 113 | log.Debug().Msgf("Recv RTP\n%s", pkt.String()) 114 | if p.PayloadType != pkt.PayloadType { 115 | log.Error().Msg("RTP type mismatch") 116 | } 117 | 118 | // TODO: move this out of loop 119 | expectedPayload := make([]byte, 0, len(pkt.Payload)*(*echoCount)) 120 | for i := 0; i < *echoCount; i++ { 121 | expectedPayload = append(expectedPayload, pkt.Payload...) 122 | } 123 | 124 | if !bytes.Equal(p.Payload, expectedPayload) { 125 | log.Error().Str("recv", string(p.Payload)).Str("expected", string(expectedPayload)).Msg("RTP payload mismatch") 126 | } 127 | } 128 | }() 129 | 130 | for i := 0; ; i++ { 131 | select { 132 | case <-sig: 133 | ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) 134 | dialog.Hangup(ctx) 135 | return 136 | 137 | case <-dialog.Done(): 138 | return 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /dialog_server.go: -------------------------------------------------------------------------------- 1 | package sipgox 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/emiago/media" 9 | "github.com/emiago/sipgo" 10 | "github.com/emiago/sipgo/sip" 11 | ) 12 | 13 | type DialogServerSession struct { 14 | *media.MediaSession 15 | 16 | *sipgo.DialogServerSession 17 | 18 | waitNotify chan error 19 | 20 | // onClose used to cleanup internal logic 21 | onClose func() 22 | } 23 | 24 | func (d *DialogServerSession) Close() error { 25 | err := d.DialogServerSession.Close() 26 | 27 | if d.MediaSession != nil { 28 | d.MediaSession.Close() 29 | } 30 | 31 | if d.onClose != nil { 32 | d.onClose() 33 | } 34 | return err 35 | } 36 | 37 | // Hangup is alias for Bye 38 | func (d *DialogServerSession) Hangup(ctx context.Context) error { 39 | return d.Bye(ctx) 40 | } 41 | func (d *DialogServerSession) Bye(ctx context.Context) error { 42 | // defer close(d.done) 43 | // defer d.MediaSession.Close() 44 | return d.DialogServerSession.Bye(ctx) 45 | } 46 | 47 | // Refer tries todo refer (blind transfer) on call 48 | func (d *DialogServerSession) Refer(ctx context.Context, referTo sip.Uri) error { 49 | // TODO check state of call 50 | 51 | req := sip.NewRequest(sip.REFER, d.InviteRequest.Contact().Address) 52 | UASRequestBuild(req, d.InviteResponse) 53 | 54 | // Invite request tags must be preserved but switched 55 | req.AppendHeader(sip.NewHeader("Refer-to", referTo.String())) 56 | 57 | d.waitNotify = make(chan error) 58 | 59 | tx, err := d.TransactionRequest(ctx, req) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | select { 65 | case <-tx.Done(): 66 | return tx.Err() 67 | case res := <-tx.Responses(): 68 | if res.StatusCode != sip.StatusAccepted { 69 | return sipgo.ErrDialogResponse{ 70 | Res: res, 71 | } 72 | } 73 | 74 | case <-ctx.Done(): 75 | return tx.Cancel() 76 | } 77 | 78 | return d.Hangup(ctx) 79 | 80 | // There is now implicit subscription 81 | select { 82 | case e := <-d.waitNotify: 83 | return e 84 | case <-d.Context().Done(): 85 | return d.Context().Err() 86 | } 87 | } 88 | 89 | func (d *DialogServerSession) Notify(req *sip.Request) error { 90 | if req.CallID().Value() != d.InviteResponse.CallID().Value() { 91 | return sipgo.ErrDialogDoesNotExists 92 | } 93 | 94 | if req.Body() == nil { 95 | return fmt.Errorf("no body in notify") 96 | } 97 | 98 | if strings.HasPrefix("SIP/2.0 1", string(req.Body())) { 99 | return nil 100 | } 101 | 102 | var e error = nil 103 | if !strings.HasPrefix("SIP/2.0 200", string(req.Body())) { 104 | e = fmt.Errorf("not 200 response") 105 | } 106 | 107 | select { 108 | case d.waitNotify <- e: 109 | case <-d.Context().Done(): 110 | return d.Context().Err() 111 | } 112 | return nil 113 | } 114 | 115 | // func (d *DialogServerSession) MediaStream(s MediaStreamer) error { 116 | // return s.MediaStream(d.MediaSession) 117 | // } 118 | 119 | func UACRequestBuild(req *sip.Request, lastReq *sip.Request, lastResp *sip.Response) { 120 | from := lastReq.From() 121 | to := lastReq.To() 122 | callid := lastReq.CallID() 123 | if lastResp != nil { 124 | // To normally gets updated with tag 125 | to = lastResp.To() 126 | } 127 | 128 | req.AppendHeader(from) 129 | req.AppendHeader(to) 130 | req.AppendHeader(callid) 131 | 132 | if cont := lastReq.GetHeader("Contact"); cont != nil { 133 | req.AppendHeader(cont) 134 | } 135 | 136 | // This is not clear 137 | // hdrs := res.GetHeaders("Record-Route") 138 | // for i := len(hdrs) - 1; i >= 0; i-- { 139 | // recordRoute := hdrs[i] 140 | // req.AppendHeader(sip.NewHeader("Route", recordRoute.Value())) 141 | // } 142 | } 143 | 144 | func UASRequestBuild(req *sip.Request, lastResp *sip.Response) { 145 | // UAS building request from previous sent response has some work 146 | // From and To must be swapped 147 | // Callid and contact hdr is preserved 148 | // Record-Route hdrs become Route hdrs 149 | // 150 | // rest must be filled by client 151 | 152 | from := lastResp.From() 153 | to := lastResp.To() 154 | callid := lastResp.CallID() 155 | 156 | newFrom := &sip.FromHeader{ 157 | DisplayName: to.DisplayName, 158 | Address: to.Address, 159 | Params: to.Params, 160 | } 161 | 162 | newTo := &sip.ToHeader{ 163 | DisplayName: from.DisplayName, 164 | Address: from.Address, 165 | Params: from.Params, 166 | } 167 | 168 | req.AppendHeader(newFrom) 169 | req.AppendHeader(newTo) 170 | req.AppendHeader(callid) 171 | 172 | if cont := lastResp.GetHeader("Contact"); cont != nil { 173 | req.AppendHeader(cont) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /register_transaction.go: -------------------------------------------------------------------------------- 1 | package sipgox 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/emiago/sipgo" 11 | "github.com/emiago/sipgo/sip" 12 | "github.com/rs/zerolog" 13 | ) 14 | 15 | type RegisterTransaction struct { 16 | opts RegisterOptions 17 | Origin *sip.Request 18 | 19 | client *sipgo.Client 20 | log zerolog.Logger 21 | } 22 | 23 | func (t *RegisterTransaction) Terminate() error { 24 | return t.client.Close() 25 | } 26 | 27 | func NewRegisterTransaction(log zerolog.Logger, client *sipgo.Client, recipient sip.Uri, contact sip.ContactHeader, opts RegisterOptions) *RegisterTransaction { 28 | expiry, allowHDRS := opts.Expiry, opts.AllowHeaders 29 | // log := p.getLoggerCtx(ctx, "Register") 30 | req := sip.NewRequest(sip.REGISTER, recipient) 31 | req.AppendHeader(&contact) 32 | if expiry > 0 { 33 | expires := sip.ExpiresHeader(expiry) 34 | req.AppendHeader(&expires) 35 | } 36 | if allowHDRS != nil { 37 | req.AppendHeader(sip.NewHeader("Allow", strings.Join(allowHDRS, ", "))) 38 | } 39 | 40 | t := &RegisterTransaction{ 41 | Origin: req, // origin maybe updated after first register 42 | opts: opts, 43 | client: client, 44 | log: log, 45 | } 46 | 47 | return t 48 | } 49 | 50 | func (p *RegisterTransaction) Register(ctx context.Context) error { 51 | username, password, expiry := p.opts.Username, p.opts.Password, p.opts.Expiry 52 | client := p.client 53 | log := p.log 54 | req := p.Origin 55 | contact := *req.Contact().Clone() 56 | 57 | // Send request and parse response 58 | // req.SetDestination(*dst) 59 | log.Info().Str("uri", req.Recipient.String()).Int("expiry", int(expiry)).Msg("sending request") 60 | tx, err := client.TransactionRequest(ctx, req) 61 | if err != nil { 62 | return fmt.Errorf("fail to create transaction req=%q: %w", req.StartLine(), err) 63 | } 64 | defer tx.Terminate() 65 | 66 | res, err := getResponse(ctx, tx) 67 | if err != nil { 68 | return fmt.Errorf("fail to get response req=%q : %w", req.StartLine(), err) 69 | } 70 | 71 | via := res.Via() 72 | if via == nil { 73 | return fmt.Errorf("no Via header in response") 74 | } 75 | 76 | // https://datatracker.ietf.org/doc/html/rfc3581#section-9 77 | if rport, _ := via.Params.Get("rport"); rport != "" { 78 | if p, err := strconv.Atoi(rport); err == nil { 79 | contact.Address.Port = p 80 | } 81 | 82 | if received, _ := via.Params.Get("received"); received != "" { 83 | // TODO: consider parsing IP 84 | contact.Address.Host = received 85 | } 86 | 87 | // Update contact address of NAT 88 | req.ReplaceHeader(&contact) 89 | } 90 | 91 | log.Info().Int("status", int(res.StatusCode)).Msg("Received status") 92 | if res.StatusCode == sip.StatusUnauthorized || res.StatusCode == sip.StatusProxyAuthRequired { 93 | tx.Terminate() //Terminate previous 94 | 95 | log.Info().Msg("Unathorized. Doing digest auth") 96 | tx, err = client.DoDigestAuth(ctx, req, res, sipgo.DigestAuth{ 97 | Username: username, 98 | Password: password, 99 | }) 100 | if err != nil { 101 | return err 102 | } 103 | defer tx.Terminate() 104 | 105 | res, err = getResponse(ctx, tx) 106 | if err != nil { 107 | return fmt.Errorf("fail to get response req=%q : %w", req.StartLine(), err) 108 | } 109 | log.Info().Int("status", int(res.StatusCode)).Msg("Received status") 110 | } 111 | 112 | if res.StatusCode != 200 { 113 | return &RegisterResponseError{ 114 | RegisterReq: req, 115 | RegisterRes: res, 116 | Msg: res.StartLine(), 117 | } 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func (t *RegisterTransaction) QualifyLoop(ctx context.Context) error { 124 | 125 | // TODO: based on server response Expires header this must be adjusted 126 | expiry := t.opts.Expiry 127 | if expiry == 0 { 128 | expiry = 30 129 | } 130 | 131 | ticker := time.NewTicker(time.Duration(expiry) * time.Second) 132 | for { 133 | select { 134 | case <-ctx.Done(): 135 | return ctx.Err() 136 | case <-ticker.C: // TODO make configurable 137 | } 138 | err := t.qualify(ctx) 139 | if err != nil { 140 | return err 141 | } 142 | } 143 | } 144 | 145 | func (t *RegisterTransaction) Unregister(ctx context.Context) error { 146 | log := t.log 147 | req := t.Origin 148 | 149 | req.RemoveHeader("Expires") 150 | req.RemoveHeader("Contact") 151 | req.AppendHeader(sip.NewHeader("Contact", "*")) 152 | expires := sip.ExpiresHeader(0) 153 | req.AppendHeader(&expires) 154 | 155 | log.Info().Str("uri", req.Recipient.String()).Msg("UNREGISTER") 156 | return t.reregister(ctx, req) 157 | } 158 | 159 | func (t *RegisterTransaction) qualify(ctx context.Context) error { 160 | return t.reregister(ctx, t.Origin) 161 | } 162 | 163 | func (t *RegisterTransaction) reregister(ctx context.Context, req *sip.Request) error { 164 | // log := p.getLoggerCtx(ctx, "Register") 165 | log := t.log 166 | client := t.client 167 | username, password := t.opts.Username, t.opts.Password 168 | // Send request and parse response 169 | // req.SetDestination(*dst) 170 | req.RemoveHeader("Via") 171 | tx, err := client.TransactionRequest(ctx, req) 172 | if err != nil { 173 | return fmt.Errorf("fail to create transaction req=%q: %w", req.StartLine(), err) 174 | } 175 | defer tx.Terminate() 176 | 177 | res, err := getResponse(ctx, tx) 178 | if err != nil { 179 | return fmt.Errorf("fail to get response req=%q : %w", req.StartLine(), err) 180 | } 181 | 182 | log.Info().Int("status", int(res.StatusCode)).Msg("Received status") 183 | if res.StatusCode == sip.StatusUnauthorized || res.StatusCode == sip.StatusProxyAuthRequired { 184 | tx.Terminate() //Terminate previous 185 | log.Info().Msg("Unathorized. Doing digest auth") 186 | tx, err = client.DoDigestAuth(ctx, req, res, sipgo.DigestAuth{ 187 | Username: username, 188 | Password: password, 189 | }) 190 | if err != nil { 191 | return err 192 | } 193 | defer tx.Terminate() 194 | 195 | res, err = getResponse(ctx, tx) 196 | if err != nil { 197 | return fmt.Errorf("fail to get response req=%q : %w", req.StartLine(), err) 198 | } 199 | log.Info().Int("status", int(res.StatusCode)).Msg("Received status") 200 | } 201 | 202 | if res.StatusCode != 200 { 203 | return &RegisterResponseError{ 204 | RegisterReq: req, 205 | RegisterRes: res, 206 | Msg: res.StartLine(), 207 | } 208 | } 209 | 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/emiago/media v0.1.0 h1:w+TWK/lTfi2A/Jszl6eRXYF7WRY9Ac5R+Quh+Kpom2o= 6 | github.com/emiago/media v0.1.0/go.mod h1:ZR7F8OydXIBOSFXQwxkMs56fJD3I/KpN/Z+ZOhyaIiY= 7 | github.com/emiago/media v0.1.1-0.20240619203804-4165fb8ba225 h1:4oRmN/GPAx9UUw1EDbk3xNR4Z73WLDpsdf0AJ5RvGGY= 8 | github.com/emiago/media v0.1.1-0.20240619203804-4165fb8ba225/go.mod h1:phTj1EGDttAaMyKVvR/NG2CTdUVnbIDm5QzdH4nZUsU= 9 | github.com/emiago/media v0.1.1-0.20240619212740-bf8c5574162c h1:6+wLMyxGS+sz2WVILPlbW8vB3tyPDSxZ907glP2Q5Z8= 10 | github.com/emiago/media v0.1.1-0.20240619212740-bf8c5574162c/go.mod h1:phTj1EGDttAaMyKVvR/NG2CTdUVnbIDm5QzdH4nZUsU= 11 | github.com/emiago/sipgo v0.22.0 h1:GaQ51m26M9QnVBVY2aDJ/mXqq/BDfZ1A+nW7XgU/4Ts= 12 | github.com/emiago/sipgo v0.22.0/go.mod h1:a77FgPEEjJvfYWYfP3p53u+dNhWEMb/VGVS6guvBzx0= 13 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 14 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 15 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 16 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 17 | github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= 18 | github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= 19 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 20 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 21 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 22 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 23 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 24 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 | github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM= 26 | github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc= 27 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 28 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 32 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 33 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 34 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 35 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 36 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 37 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 38 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 39 | github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= 40 | github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= 41 | github.com/pion/rtp v1.8.6 h1:MTmn/b0aWWsAzux2AmP8WGllusBVw4NPYPVFFd7jUPw= 42 | github.com/pion/rtp v1.8.6/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= 43 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 44 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 45 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 48 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 49 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 50 | github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= 51 | github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 52 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= 53 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 54 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 55 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 56 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 57 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 58 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 60 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 61 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 67 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 69 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 70 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 72 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 73 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 75 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 76 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 77 | gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= 78 | gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 79 | -------------------------------------------------------------------------------- /phone.go: -------------------------------------------------------------------------------- 1 | package sipgox 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/emiago/media" 16 | "github.com/emiago/media/sdp" 17 | "github.com/emiago/sipgo" 18 | "github.com/emiago/sipgo/sip" 19 | "github.com/icholy/digest" 20 | "github.com/rs/zerolog" 21 | "github.com/rs/zerolog/log" 22 | ) 23 | 24 | // Phone is easy wrapper for creating phone like functionaliy 25 | // but actions are creating clients and servers on a fly so 26 | // it is not designed for long running apps 27 | 28 | var ( 29 | // Value must be zerolog.Logger 30 | ContextLoggerKey = "logger" 31 | ) 32 | 33 | type Phone struct { 34 | UA *sipgo.UserAgent 35 | // listenAddrs is map of transport:addr which will phone use to listen incoming requests 36 | listenAddrs []ListenAddr 37 | 38 | log zerolog.Logger 39 | 40 | // Custom client or server 41 | // By default they are created 42 | client *sipgo.Client 43 | server *sipgo.Server 44 | } 45 | 46 | type ListenAddr struct { 47 | Network string 48 | Addr string 49 | TLSConf *tls.Config 50 | } 51 | 52 | type Listener struct { 53 | ListenAddr 54 | io.Closer 55 | Listen func() error 56 | } 57 | 58 | type PhoneOption func(p *Phone) 59 | 60 | // WithPhoneListenAddrs 61 | // NOT TLS supported 62 | func WithPhoneListenAddr(addr ListenAddr) PhoneOption { 63 | return func(p *Phone) { 64 | p.listenAddrs = append(p.listenAddrs, addr) 65 | } 66 | } 67 | 68 | func WithPhoneLogger(l zerolog.Logger) PhoneOption { 69 | return func(p *Phone) { 70 | p.log = l 71 | } 72 | } 73 | 74 | // func WithPhoneClient(c *sipgo.Client) PhoneOption { 75 | // return func(p *Phone) { 76 | // p.client = c 77 | // } 78 | // } 79 | 80 | // func WithPhoneServer(s *sipgo.Server) PhoneOption { 81 | // return func(p *Phone) { 82 | // p.server = s 83 | // } 84 | // } 85 | 86 | func NewPhone(ua *sipgo.UserAgent, options ...PhoneOption) *Phone { 87 | p := &Phone{ 88 | UA: ua, 89 | // c: client, 90 | listenAddrs: []ListenAddr{}, 91 | log: log.Logger, 92 | } 93 | 94 | for _, o := range options { 95 | o(p) 96 | } 97 | 98 | if len(p.listenAddrs) == 0 { 99 | // WithPhoneListenAddr(ListenAddr{"udp", "127.0.0.1:5060"})(p) 100 | // WithPhoneListenAddr(ListenAddr{"tcp", "0.0.0.0:5060"})(p) 101 | } 102 | 103 | // In case ws we want to run http 104 | return p 105 | } 106 | 107 | func (p *Phone) Close() { 108 | } 109 | 110 | // func (p *Phone) getOrCreateClient(opts ...sipgo.ClientOption) (*sipgo.Client, error) { 111 | // if p.client != nil { 112 | // return p.client, nil 113 | // } 114 | 115 | // return sipgo.NewClient(p.ua, opts...) 116 | // } 117 | 118 | // func (p *Phone) getOrCreateServer(opts ...sipgo.ServerOption) (*sipgo.Server, error) { 119 | // if p.server != nil { 120 | // return p.server, nil 121 | // } 122 | 123 | // return sipgo.NewServer(p.ua, opts...) 124 | // } 125 | 126 | func (p *Phone) getLoggerCtx(ctx context.Context, caller string) zerolog.Logger { 127 | l := ctx.Value(ContextLoggerKey) 128 | if l != nil { 129 | log, ok := l.(zerolog.Logger) 130 | if ok { 131 | return log 132 | } 133 | } 134 | return p.log.With().Str("caller", caller).Logger() 135 | } 136 | 137 | func (p *Phone) getInterfaceAddr(network string, targetAddr string) (addr string, err error) { 138 | host, port, err := p.getInterfaceHostPort(network, targetAddr) 139 | if err != nil { 140 | return "", err 141 | } 142 | return net.JoinHostPort(host, strconv.Itoa(port)), nil 143 | } 144 | 145 | func (p *Phone) createServerListener(s *sipgo.Server, a ListenAddr) (*Listener, error) { 146 | 147 | network, addr := a.Network, a.Addr 148 | switch network { 149 | case "udp": 150 | // resolve local UDP endpoint 151 | laddr, err := net.ResolveUDPAddr("udp", addr) 152 | if err != nil { 153 | return nil, fmt.Errorf("fail to resolve address. err=%w", err) 154 | } 155 | udpConn, err := net.ListenUDP("udp", laddr) 156 | if err != nil { 157 | return nil, fmt.Errorf("listen udp error. err=%w", err) 158 | } 159 | 160 | // Port can be dynamic 161 | a.Addr = udpConn.LocalAddr().String() 162 | 163 | return &Listener{ 164 | a, 165 | udpConn, 166 | func() error { return s.ServeUDP(udpConn) }, 167 | }, nil 168 | 169 | case "ws", "tcp": 170 | laddr, err := net.ResolveTCPAddr("tcp", addr) 171 | if err != nil { 172 | return nil, fmt.Errorf("fail to resolve address. err=%w", err) 173 | } 174 | 175 | conn, err := net.ListenTCP("tcp", laddr) 176 | if err != nil { 177 | return nil, fmt.Errorf("listen tcp error. err=%w", err) 178 | } 179 | 180 | a.Addr = conn.Addr().String() 181 | // and uses listener to buffer 182 | if network == "ws" { 183 | return &Listener{ 184 | a, 185 | conn, 186 | func() error { return s.ServeWS(conn) }, 187 | }, nil 188 | } 189 | 190 | return &Listener{ 191 | a, 192 | conn, 193 | func() error { return s.ServeTCP(conn) }, 194 | }, nil 195 | } 196 | return nil, fmt.Errorf("unsuported protocol") 197 | } 198 | 199 | func (p *Phone) createServerListeners(s *sipgo.Server) (listeners []*Listener, e error) { 200 | newListener := func(a ListenAddr) error { 201 | l, err := p.createServerListener(s, a) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | listeners = append(listeners, l) 207 | return nil 208 | } 209 | 210 | if len(p.listenAddrs) == 0 { 211 | addr, err := p.getInterfaceAddr("udp", "") 212 | if err != nil { 213 | return listeners, err 214 | } 215 | err = newListener(ListenAddr{Network: "udp", Addr: addr}) 216 | // ip, err := resolveHostIPWithTarget("udp", "") 217 | // if err != nil { 218 | // return listeners, err 219 | // } 220 | // err = newListener(ListenAddr{Network: "udp", Addr: ip.String() + ":0"}) 221 | return listeners, err 222 | } 223 | 224 | for _, a := range p.listenAddrs { 225 | err := newListener(a) 226 | if err != nil { 227 | return nil, err 228 | } 229 | } 230 | return listeners, nil 231 | } 232 | 233 | func (p *Phone) getInterfaceHostPort(network string, targetAddr string) (host string, port int, err error) { 234 | for _, a := range p.listenAddrs { 235 | if a.Network == network { 236 | host, port, err = sip.ParseAddr(a.Addr) 237 | if err != nil { 238 | return 239 | } 240 | 241 | // What is with port 242 | // If user provides this 127.0.0.1:0 -> then this tell us to use random port 243 | // If user provides this non IP then port will stay empty 244 | if port != 0 { 245 | return 246 | } 247 | 248 | ip := net.ParseIP(host) 249 | if ip != nil { 250 | port, err = findFreePort(network, ip) 251 | return 252 | } 253 | 254 | // port = sip.DefaultPort(network) 255 | return 256 | } 257 | } 258 | 259 | ip, port, err := FindFreeInterfaceHostPort(network, targetAddr) 260 | if err != nil { 261 | return "", 0, err 262 | } 263 | return ip.String(), port, nil 264 | } 265 | 266 | var ( 267 | ErrRegisterFail = fmt.Errorf("register failed") 268 | ErrRegisterUnathorized = fmt.Errorf("register unathorized") 269 | ) 270 | 271 | type RegisterResponseError struct { 272 | RegisterReq *sip.Request 273 | RegisterRes *sip.Response 274 | 275 | Msg string 276 | } 277 | 278 | func (e *RegisterResponseError) StatusCode() sip.StatusCode { 279 | return e.RegisterRes.StatusCode 280 | } 281 | 282 | func (e RegisterResponseError) Error() string { 283 | return e.Msg 284 | } 285 | 286 | // Register the phone by sip uri. Pass username and password via opts 287 | // NOTE: this will block and keep periodic registration. Use context to cancel 288 | type RegisterOptions struct { 289 | Username string 290 | Password string 291 | 292 | Expiry int 293 | AllowHeaders []string 294 | UnregisterAll bool 295 | } 296 | 297 | func (p *Phone) Register(ctx context.Context, recipient sip.Uri, opts RegisterOptions) error { 298 | log := p.getLoggerCtx(ctx, "Register") 299 | // Make our client reuse address 300 | network := recipient.Headers["transport"] 301 | if network == "" { 302 | network = "udp" 303 | } 304 | lhost, lport, _ := p.getInterfaceHostPort(network, recipient.HostPort()) 305 | // addr := net.JoinHostPort(lhost, strconv.Itoa(lport)) 306 | 307 | // Run server on UA just to handle OPTIONS 308 | // We do not need to create listener as client will create underneath connections and point contact header 309 | server, err := sipgo.NewServer(p.UA) 310 | if err != nil { 311 | return err 312 | } 313 | defer server.Close() 314 | 315 | server.OnOptions(func(req *sip.Request, tx sip.ServerTransaction) { 316 | res := sip.NewResponseFromRequest(req, sip.StatusOK, "OK", nil) 317 | if err := tx.Respond(res); err != nil { 318 | log.Error().Err(err).Msg("OPTIONS 200 failed to respond") 319 | } 320 | }) 321 | 322 | client, err := sipgo.NewClient(p.UA, 323 | sipgo.WithClientHostname(lhost), 324 | sipgo.WithClientPort(lport), 325 | sipgo.WithClientNAT(), // add rport support 326 | ) 327 | defer client.Close() 328 | 329 | contactHdr := sip.ContactHeader{ 330 | Address: sip.Uri{ 331 | User: p.UA.Name(), 332 | Host: lhost, 333 | Port: lport, 334 | Headers: sip.HeaderParams{"transport": network}, 335 | UriParams: sip.NewParams(), 336 | }, 337 | Params: sip.NewParams(), 338 | } 339 | 340 | t, err := p.register(ctx, client, recipient, contactHdr, opts) 341 | if err != nil { 342 | return err 343 | } 344 | 345 | // Unregister 346 | defer func() { 347 | ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) 348 | err := t.Unregister(ctx) 349 | if err != nil { 350 | log.Error().Err(err).Msg("Fail to unregister") 351 | } 352 | }() 353 | 354 | return t.QualifyLoop(ctx) 355 | } 356 | 357 | func (p *Phone) register(ctx context.Context, client *sipgo.Client, recipient sip.Uri, contact sip.ContactHeader, opts RegisterOptions) (*RegisterTransaction, error) { 358 | t := NewRegisterTransaction(p.getLoggerCtx(ctx, "Register"), client, recipient, contact, opts) 359 | 360 | if opts.UnregisterAll { 361 | if err := t.Unregister(ctx); err != nil { 362 | return nil, ErrRegisterFail 363 | } 364 | } 365 | 366 | err := t.Register(ctx) 367 | if err != nil { 368 | return nil, err 369 | } 370 | 371 | return t, nil 372 | } 373 | 374 | type DialResponseError struct { 375 | InviteReq *sip.Request 376 | InviteResp *sip.Response 377 | 378 | Msg string 379 | } 380 | 381 | func (e *DialResponseError) StatusCode() sip.StatusCode { 382 | return e.InviteResp.StatusCode 383 | } 384 | 385 | func (e DialResponseError) Error() string { 386 | return e.Msg 387 | } 388 | 389 | type DialOptions struct { 390 | // Authentication via digest challenge 391 | Username string 392 | Password string 393 | 394 | // Custom headers passed on INVITE 395 | SipHeaders []sip.Header 396 | 397 | // SDP Formats to customize. NOTE: Only ulaw and alaw are fully supported 398 | Formats sdp.Formats 399 | 400 | // OnResponse is just callback called after INVITE is sent and all responses before final one 401 | // Useful for tracking call state 402 | OnResponse func(inviteResp *sip.Response) 403 | 404 | // OnRefer is called 2 times. 405 | // 1st with state NONE and dialog=nil. This is to have caller prepared 406 | // 2nd with state Established or Ended with dialog 407 | OnRefer func(state DialogReferState) 408 | 409 | // Experimental 410 | // 411 | // OnMedia handles INVITE updates and passes new MediaSession with new propertie 412 | OnMedia func(sess *media.MediaSession) 413 | } 414 | 415 | type DialogReferState struct { 416 | // Updates current transfer progress with state 417 | State sip.DialogState 418 | // Dialog present when state is answered (Confirmed dialog state) 419 | Dialog *DialogClientSession 420 | } 421 | 422 | // Dial creates dialog with recipient 423 | // 424 | // return DialResponseError in case non 200 responses 425 | func (p *Phone) Dial(dialCtx context.Context, recipient sip.Uri, o DialOptions) (*DialogClientSession, error) { 426 | log := p.getLoggerCtx(dialCtx, "Dial") 427 | ctx, _ := context.WithCancel(dialCtx) 428 | // defer cancel() 429 | 430 | network := "udp" 431 | if recipient.UriParams != nil { 432 | if t := recipient.UriParams["transport"]; t != "" { 433 | network = t 434 | } 435 | } 436 | // Remove password from uri. 437 | recipient.Password = "" 438 | 439 | server, err := sipgo.NewServer(p.UA) 440 | if err != nil { 441 | return nil, err 442 | } 443 | 444 | // We need to listen as long our answer context is running 445 | // Listener needs to be alive even after we have created dialog 446 | // listeners, err := p.createServerListeners(server) 447 | // if err != nil { 448 | // return nil, err 449 | // } 450 | // host, listenPort, _ := sip.ParseAddr(listeners[0].Addr) 451 | 452 | // NOTE: this can return empty port, in this case we probably have hostname 453 | host, port, err := p.getInterfaceHostPort(network, recipient.HostPort()) 454 | if err != nil { 455 | return nil, err 456 | } 457 | 458 | contactHDR := sip.ContactHeader{ 459 | Address: sip.Uri{User: p.UA.Name(), Host: host, Port: port}, 460 | Params: sip.HeaderParams{"transport": network}, 461 | } 462 | 463 | // We will force client to use same interface and port as defined for contact header 464 | // The problem could be if this is required to be different, but for now keeping phone simple 465 | client, err := sipgo.NewClient(p.UA, 466 | sipgo.WithClientHostname(host), 467 | sipgo.WithClientPort(port), 468 | ) 469 | if err != nil { 470 | return nil, err 471 | } 472 | 473 | // Setup dialog client 474 | dc := sipgo.NewDialogClient(client, contactHDR) 475 | 476 | server.OnBye(func(req *sip.Request, tx sip.ServerTransaction) { 477 | if err := dc.ReadBye(req, tx); err != nil { 478 | if errors.Is(err, sipgo.ErrDialogDoesNotExists) { 479 | log.Info().Msg("Received BYE but dialog was already closed") 480 | return 481 | } 482 | 483 | log.Error().Err(err).Msg("Dialog reading BYE failed") 484 | return 485 | } 486 | log.Debug().Msg("Received BYE") 487 | }) 488 | 489 | server.OnRefer(func(req *sip.Request, tx sip.ServerTransaction) { 490 | if o.OnRefer == nil { 491 | log.Warn().Str("req", req.StartLine()).Msg("Refer is not handled. Missing OnRefer") 492 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusMethodNotAllowed, "Method not allowed", nil)) 493 | return 494 | } 495 | 496 | var dialog *sipgo.DialogClientSession 497 | var newDialog *DialogClientSession 498 | var err error 499 | // TODO refactor this 500 | refer := func() error { 501 | referUri := sip.Uri{} 502 | dialog, err = dc.ReadRefer(req, tx, &referUri) 503 | if err != nil { 504 | return err 505 | } 506 | 507 | // Setup session 508 | rtpIp := p.UA.GetIP() 509 | if lip := net.ParseIP(host); lip != nil && !lip.IsUnspecified() { 510 | rtpIp = lip 511 | } 512 | msess, err := media.NewMediaSession(&net.UDPAddr{IP: rtpIp, Port: 0}) 513 | if err != nil { 514 | return err 515 | } 516 | 517 | notifyAccepted := true 518 | { 519 | // Do Notify 520 | log.Info().Msg("Sending NOTIFY") 521 | notify := sip.NewRequest(sip.NOTIFY, req.Contact().Address) 522 | notify.AppendHeader(sip.NewHeader("Content-Type", "message/sipfrag;version=2.0")) 523 | notify.SetBody([]byte("SIP/2.0 100 Trying")) 524 | cliTx, err := dialog.TransactionRequest(dialog.Context(), notify) 525 | if err != nil { 526 | return err 527 | } 528 | defer cliTx.Terminate() 529 | 530 | select { 531 | case <-cliTx.Done(): 532 | case res := <-cliTx.Responses(): 533 | notifyAccepted = res.StatusCode == sip.StatusOK 534 | } 535 | } 536 | 537 | invite := sip.NewRequest(sip.INVITE, referUri) 538 | invite.SetTransport(network) 539 | invite.AppendHeader(sip.NewHeader("Content-Type", "application/sdp")) 540 | invite.SetBody(msess.LocalSDP()) 541 | 542 | newDialog, err = p.dial(context.TODO(), dc, invite, msess, o) 543 | if err != nil { 544 | return err 545 | } 546 | 547 | if notifyAccepted { 548 | notify := sip.NewRequest(sip.NOTIFY, req.Contact().Address) 549 | notify.AppendHeader(sip.NewHeader("Content-Type", "message/sipfrag;version=2.0")) 550 | notify.SetBody([]byte("SIP/2.0 200 OK")) 551 | cliTx, err := dialog.TransactionRequest(dialog.Context(), notify) 552 | if err != nil { 553 | return err 554 | } 555 | defer cliTx.Terminate() 556 | } 557 | return nil 558 | } 559 | 560 | // Refer can happen and due to new dialog creation current one could be terminated. 561 | // Caller would not be able to get control of new dialog until it is answered 562 | // This way we say to caller to wait transfer completition, and current dialog can be terminated 563 | o.OnRefer(DialogReferState{State: 0}) 564 | if err := refer(); err != nil { 565 | log.Error().Err(err).Msg("Fail to dial REFER") 566 | // Handle better this errors 567 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusInternalServerError, err.Error(), nil)) 568 | o.OnRefer(DialogReferState{State: sip.DialogStateEnded}) 569 | return 570 | } 571 | 572 | // Let caller decide will it close current dialog or continue with transfer 573 | // defer dialog.Close() 574 | // defer dialog.Bye(context.TODO()) 575 | 576 | o.OnRefer(DialogReferState{State: sip.DialogStateConfirmed, Dialog: newDialog}) 577 | }) 578 | 579 | var dialogRef *DialogClientSession 580 | // waitingAck := atomic. 581 | 582 | server.OnInvite(func(req *sip.Request, tx sip.ServerTransaction) { 583 | id, err := sip.UACReadRequestDialogID(req) 584 | if err != nil { 585 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusBadRequest, "Bad Request", nil)) 586 | return 587 | } 588 | 589 | if dialogRef.ID != id { 590 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusNotFound, "Dialog does not exist", nil)) 591 | return 592 | } 593 | 594 | // Forking current dialog session and applying new SDP 595 | msess := dialogRef.MediaSession.Fork() 596 | 597 | if err := msess.RemoteSDP(req.Body()); err != nil { 598 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusBadRequest, "SDP applying failed", nil)) 599 | return 600 | } 601 | 602 | log.Info(). 603 | Str("formats", logFormats(msess.Formats)). 604 | Str("localAddr", msess.Laddr.String()). 605 | Str("remoteAddr", msess.Raddr.String()). 606 | Msg("Media/RTP session updated") 607 | 608 | if o.OnMedia != nil { 609 | o.OnMedia(msess) 610 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusOK, "OK", nil)) 611 | return 612 | } 613 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusMethodNotAllowed, "Method not allowed", nil)) 614 | }) 615 | 616 | server.OnAck(func(req *sip.Request, tx sip.ServerTransaction) { 617 | // This gets received when we send 200 on INVITE media update 618 | id, err := sip.UACReadRequestDialogID(req) 619 | if err != nil { 620 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusBadRequest, "Bad Request", nil)) 621 | return 622 | } 623 | 624 | if dialogRef.ID != id { 625 | tx.Respond(sip.NewResponseFromRequest(req, sip.StatusNotFound, "Dialog does not exist", nil)) 626 | return 627 | } 628 | }) 629 | 630 | // Start server 631 | // for _, l := range listeners { 632 | // log.Info().Str("network", l.Network).Str("addr", l.Addr).Msg("Listening on") 633 | // go l.Listen() 634 | // } 635 | 636 | // Setup session 637 | rtpIp := p.UA.GetIP() 638 | if lip := net.ParseIP(host); lip != nil && !lip.IsUnspecified() { 639 | rtpIp = lip 640 | } 641 | msess, err := media.NewMediaSession(&net.UDPAddr{IP: rtpIp, Port: 0}) 642 | if err != nil { 643 | return nil, err 644 | } 645 | 646 | // Create Generic SDP 647 | if len(o.Formats) > 0 { 648 | msess.Formats = o.Formats 649 | } 650 | sdpSend := msess.LocalSDP() 651 | 652 | // Creating INVITE 653 | req := sip.NewRequest(sip.INVITE, recipient) 654 | req.SetTransport(network) 655 | req.AppendHeader(sip.NewHeader("Content-Type", "application/sdp")) 656 | req.SetBody(sdpSend) 657 | 658 | // Add custom headers 659 | for _, h := range o.SipHeaders { 660 | log.Info().Str(h.Name(), h.Value()).Msg("Adding SIP header") 661 | req.AppendHeader(h) 662 | } 663 | 664 | dialog, err := p.dial(ctx, dc, req, msess, o) 665 | if err != nil { 666 | return nil, err 667 | } 668 | 669 | dialogRef = dialog 670 | 671 | return dialog, nil 672 | } 673 | 674 | func (p *Phone) dial(ctx context.Context, dc *sipgo.DialogClient, invite *sip.Request, msess *media.MediaSession, o DialOptions) (*DialogClientSession, error) { 675 | log := p.getLoggerCtx(ctx, "Dial") 676 | dialog, err := dc.WriteInvite(ctx, invite) 677 | if err != nil { 678 | return nil, err 679 | } 680 | p.logSipRequest(&log, invite) 681 | return p.dialWaitAnswer(ctx, dialog, msess, o) 682 | } 683 | 684 | func (p *Phone) dialWaitAnswer(ctx context.Context, dialog *sipgo.DialogClientSession, msess *media.MediaSession, o DialOptions) (*DialogClientSession, error) { 685 | log := p.getLoggerCtx(ctx, "Dial") 686 | invite := dialog.InviteRequest 687 | // Wait 200 688 | waitStart := time.Now() 689 | err := dialog.WaitAnswer(ctx, sipgo.AnswerOptions{ 690 | OnResponse: func(res *sip.Response) error { 691 | p.logSipResponse(&log, res) 692 | if o.OnResponse != nil { 693 | o.OnResponse(res) 694 | } 695 | return nil 696 | }, 697 | Username: o.Username, 698 | Password: o.Password, 699 | }) 700 | 701 | var rerr *sipgo.ErrDialogResponse 702 | if errors.As(err, &rerr) { 703 | return nil, &DialResponseError{ 704 | InviteReq: invite, 705 | InviteResp: rerr.Res, 706 | Msg: fmt.Sprintf("Call not answered: %s", rerr.Res.StartLine()), 707 | } 708 | } 709 | 710 | if err != nil { 711 | return nil, err 712 | } 713 | 714 | r := dialog.InviteResponse 715 | log.Info(). 716 | Int("code", int(r.StatusCode)). 717 | // Str("reason", r.Reason). 718 | Str("duration", time.Since(waitStart).String()). 719 | Msg("Call answered") 720 | 721 | // Setup media 722 | err = msess.RemoteSDP(r.Body()) 723 | // TODO handle bad SDP 724 | if err != nil { 725 | return nil, err 726 | } 727 | 728 | log.Info(). 729 | Str("formats", logFormats(msess.Formats)). 730 | Str("localAddr", msess.Laddr.String()). 731 | Str("remoteAddr", msess.Raddr.String()). 732 | Msg("Media/RTP session created") 733 | 734 | // Send ACK 735 | if err := dialog.Ack(ctx); err != nil { 736 | return nil, fmt.Errorf("fail to send ACK: %w", err) 737 | } 738 | 739 | return &DialogClientSession{ 740 | MediaSession: msess, 741 | DialogClientSession: dialog, 742 | }, nil 743 | } 744 | 745 | var ( 746 | // You can use this key with AnswerReadyCtxValue to get signal when 747 | // Answer is ready to receive traffic 748 | AnswerReadyCtxKey = "AnswerReadyCtxKey" 749 | ) 750 | 751 | type AnswerReadyCtxValue chan struct{} 752 | type AnswerOptions struct { 753 | Ringtime time.Duration 754 | SipHeaders []sip.Header 755 | 756 | // For authorizing INVITE unless RegisterAddr is defined 757 | Username string 758 | Password string 759 | Realm string //default sipgo 760 | 761 | RegisterAddr string //If defined it will keep registration in background 762 | 763 | // For SDP codec manipulating 764 | Formats sdp.Formats 765 | 766 | // OnCall is just INVITE request handler that you can use to notify about incoming call 767 | // After this dialog should be created and you can watch your changes with dialog.State 768 | // -1 == Cancel 769 | // 0 == continue 770 | // >0 different response 771 | OnCall func(inviteRequest *sip.Request) int 772 | 773 | // Default is 200 (answer a call) 774 | AnswerCode sip.StatusCode 775 | AnswerReason string 776 | } 777 | 778 | // Answer will answer call 779 | // Closing ansCtx will close listeners or it will be closed on BYE 780 | // TODO: reusing listener 781 | func (p *Phone) Answer(ansCtx context.Context, opts AnswerOptions) (*DialogServerSession, error) { 782 | 783 | dialog, err := p.answer(ansCtx, opts) 784 | if err != nil { 785 | return nil, err 786 | } 787 | log.Debug().Msg("Dialog answer created") 788 | if !dialog.InviteResponse.IsSuccess() { 789 | // Return closed/terminated dialog 790 | return dialog, dialog.Close() 791 | } 792 | 793 | return dialog, nil 794 | } 795 | 796 | func (p *Phone) answer(ansCtx context.Context, opts AnswerOptions) (*DialogServerSession, error) { 797 | log := p.getLoggerCtx(ansCtx, "Answer") 798 | ringtime := opts.Ringtime 799 | 800 | waitDialog := make(chan *DialogServerSession) 801 | var d *DialogServerSession 802 | 803 | // TODO reuse server and listener 804 | server, err := sipgo.NewServer(p.UA) 805 | if err != nil { 806 | return nil, err 807 | } 808 | 809 | // We need to listen as long our answer context is running 810 | // Listener needs to be alive even after we have created dialog 811 | listeners, err := p.createServerListeners(server) 812 | if err != nil { 813 | return nil, err 814 | } 815 | 816 | ctx, cancel := context.WithCancel(ansCtx) 817 | var exitErr error 818 | stopAnswer := sync.OnceFunc(func() { 819 | cancel() // Cancel context 820 | for _, l := range listeners { 821 | log.Debug().Str("addr", l.Addr).Msg("Closing listener") 822 | l.Close() 823 | } 824 | }) 825 | 826 | exitError := func(err error) { 827 | exitErr = err 828 | } 829 | 830 | lhost, lport, _ := sip.ParseAddr(listeners[0].Addr) 831 | contactHdr := sip.ContactHeader{ 832 | Address: sip.Uri{ 833 | User: p.UA.Name(), 834 | Host: lhost, 835 | Port: lport, 836 | Headers: sip.HeaderParams{"transport": listeners[0].Network}, 837 | UriParams: sip.NewParams(), 838 | }, 839 | Params: sip.NewParams(), 840 | } 841 | 842 | // Create client handle for responding 843 | client, err := sipgo.NewClient(p.UA, 844 | sipgo.WithClientNAT(), // needed for registration 845 | sipgo.WithClientHostname(lhost), 846 | // Do not use with ClientPort as we want always this to be a seperate connection 847 | ) 848 | if err != nil { 849 | return nil, err 850 | } 851 | 852 | if opts.RegisterAddr != "" { 853 | // We will use registration to resolve NAT 854 | // so WithClientNAT must be present 855 | 856 | // Keep registration 857 | rhost, rport, _ := sip.ParseAddr(opts.RegisterAddr) 858 | registerURI := sip.Uri{ 859 | Host: rhost, 860 | Port: rport, 861 | User: p.UA.Name(), 862 | } 863 | 864 | regTr, err := p.register(ctx, client, registerURI, contactHdr, RegisterOptions{ 865 | Username: opts.Username, 866 | Password: opts.Password, 867 | Expiry: 30, 868 | // UnregisterAll: true, 869 | // AllowHeaders: server.RegisteredMethods(), 870 | }) 871 | if err != nil { 872 | return nil, err 873 | } 874 | 875 | // In case our register changed contact due to NAT detection via rport, lets update 876 | contact := regTr.Origin.Contact() 877 | contactHdr = *contact.Clone() 878 | 879 | origStopAnswer := stopAnswer 880 | // Override stopAnswer with unregister 881 | stopAnswer = sync.OnceFunc(func() { 882 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) 883 | err := regTr.Unregister(ctx) 884 | if err != nil { 885 | log.Error().Err(err).Msg("Fail to unregister") 886 | } 887 | regTr = nil 888 | origStopAnswer() 889 | }) 890 | go func(ctx context.Context) { 891 | err := regTr.QualifyLoop(ctx) 892 | exitError(err) 893 | stopAnswer() 894 | }(ctx) 895 | } 896 | 897 | ds := sipgo.NewDialogServer(client, contactHdr) 898 | var chal *digest.Challenge 899 | server.OnInvite(func(req *sip.Request, tx sip.ServerTransaction) { 900 | if d != nil { 901 | didAnswered, _ := sip.MakeDialogIDFromResponse(d.InviteResponse) 902 | did, _ := sip.MakeDialogIDFromRequest(req) 903 | if did == didAnswered { 904 | 905 | // We received INVITE for update 906 | if err := d.MediaSession.RemoteSDP(req.Body()); err != nil { 907 | res := sip.NewResponseFromRequest(req, 400, err.Error(), nil) 908 | if err := tx.Respond(res); err != nil { 909 | log.Error().Err(err).Msg("Fail to send 400") 910 | return 911 | } 912 | return 913 | } 914 | 915 | res := sip.NewResponseFromRequest(req, 200, "OK", nil) 916 | if err := tx.Respond(res); err != nil { 917 | log.Error().Err(err).Msg("Fail to send 200") 918 | return 919 | } 920 | return 921 | } 922 | log.Error().Msg("Received second INVITE is not yet supported: 486 busy here") 923 | res := sip.NewResponseFromRequest(req, 486, "busy here", nil) 924 | if err := tx.Respond(res); err != nil { 925 | log.Error().Err(err).Msg("Fail to send 486") 926 | return 927 | } 928 | return 929 | } 930 | 931 | // We authorize request if password provided and no register addr defined 932 | // Use cases: 933 | // 1. INVITE auth like registrar before processing INVITE 934 | // 2. Auto answering client which keeps registration and accepts calls 935 | if opts.Password != "" && opts.RegisterAddr == "" { 936 | // https://www.rfc-editor.org/rfc/rfc2617#page-6 937 | h := req.GetHeader("Authorization") 938 | 939 | if h == nil { 940 | if chal != nil { 941 | // If challenge is created next is forbidden 942 | res := sip.NewResponseFromRequest(req, 403, "Forbidden", nil) 943 | tx.Respond(res) 944 | return 945 | } 946 | 947 | if opts.Realm == "" { 948 | opts.Realm = "sipgo" 949 | } 950 | 951 | chal = &digest.Challenge{ 952 | Realm: opts.Realm, 953 | Nonce: fmt.Sprintf("%d", time.Now().UnixMicro()), 954 | // Opaque: "sipgo", 955 | Algorithm: "MD5", 956 | } 957 | 958 | res := sip.NewResponseFromRequest(req, 401, "Unathorized", nil) 959 | res.AppendHeader(sip.NewHeader("WWW-Authenticate", chal.String())) 960 | tx.Respond(res) 961 | return 962 | } 963 | 964 | cred, err := digest.ParseCredentials(h.Value()) 965 | if err != nil { 966 | log.Error().Err(err).Msg("parsing creds failed") 967 | tx.Respond(sip.NewResponseFromRequest(req, 401, "Bad credentials", nil)) 968 | return 969 | } 970 | 971 | // Make digest and compare response 972 | digCred, err := digest.Digest(chal, digest.Options{ 973 | Method: "INVITE", 974 | URI: cred.URI, 975 | Username: opts.Username, 976 | Password: opts.Password, 977 | }) 978 | 979 | if err != nil { 980 | log.Error().Err(err).Msg("Calc digest failed") 981 | tx.Respond(sip.NewResponseFromRequest(req, 401, "Bad credentials", nil)) 982 | return 983 | } 984 | 985 | if cred.Response != digCred.Response { 986 | tx.Respond(sip.NewResponseFromRequest(req, 401, "Unathorized", nil)) 987 | return 988 | } 989 | log.Info().Str("username", cred.Username).Str("source", req.Source()).Msg("INVITE authorized") 990 | } 991 | p.logSipRequest(&log, req) 992 | 993 | dialog, err := ds.ReadInvite(req, tx) 994 | if err != nil { 995 | res := sip.NewResponseFromRequest(req, 400, err.Error(), nil) 996 | if err := tx.Respond(res); err != nil { 997 | log.Error().Err(err).Msg("Failed to send 400 response") 998 | } 999 | 1000 | exitError(err) 1001 | stopAnswer() 1002 | return 1003 | } 1004 | 1005 | err = func() error { 1006 | if opts.OnCall != nil { 1007 | // Handle OnCall handler 1008 | res := opts.OnCall(req) 1009 | switch { 1010 | case res < 0: 1011 | if err := dialog.Respond(sip.StatusBusyHere, "Busy", nil); err != nil { 1012 | d = nil 1013 | return fmt.Errorf("failed to respond oncall status code %d: %w", res, err) 1014 | } 1015 | case res > 0: 1016 | if err := dialog.Respond(sip.StatusCode(res), "", nil); err != nil { 1017 | d = nil 1018 | return fmt.Errorf("failed to respond oncall status code %d: %w", res, err) 1019 | } 1020 | } 1021 | } 1022 | 1023 | if opts.AnswerCode > 0 && opts.AnswerCode != sip.StatusOK { 1024 | log.Info().Int("code", int(opts.AnswerCode)).Msg("Answering call") 1025 | if opts.AnswerReason == "" { 1026 | // apply some default one 1027 | switch opts.AnswerCode { 1028 | case sip.StatusBusyHere: 1029 | opts.AnswerReason = "Busy" 1030 | case sip.StatusForbidden: 1031 | opts.AnswerReason = "Forbidden" 1032 | case sip.StatusUnauthorized: 1033 | opts.AnswerReason = "Unathorized" 1034 | } 1035 | } 1036 | 1037 | if err := dialog.Respond(opts.AnswerCode, opts.AnswerReason, nil); err != nil { 1038 | d = nil 1039 | return fmt.Errorf("failed to respond custom status code %d: %w", int(opts.AnswerCode), err) 1040 | } 1041 | p.logSipResponse(&log, dialog.InviteResponse) 1042 | 1043 | d = &DialogServerSession{ 1044 | DialogServerSession: dialog, 1045 | // done: make(chan struct{}), 1046 | } 1047 | select { 1048 | case <-tx.Done(): 1049 | return tx.Err() 1050 | case <-tx.Acks(): 1051 | log.Debug().Msg("ACK received. Returning dialog") 1052 | // Wait for ack 1053 | case <-ctx.Done(): 1054 | return ctx.Err() 1055 | } 1056 | 1057 | select { 1058 | case waitDialog <- d: 1059 | case <-ctx.Done(): 1060 | return ctx.Err() 1061 | } 1062 | return nil 1063 | } 1064 | 1065 | if err != nil { 1066 | return fmt.Errorf("fail to setup client handle: %w", err) 1067 | } 1068 | 1069 | // Now place a ring tone or do autoanswer 1070 | if ringtime > 0 { 1071 | res := sip.NewResponseFromRequest(req, 180, "Ringing", nil) 1072 | if err := dialog.WriteResponse(res); err != nil { 1073 | return fmt.Errorf("failed to send 180 response: %w", err) 1074 | } 1075 | p.logSipResponse(&log, res) 1076 | 1077 | select { 1078 | case <-tx.Cancels(): 1079 | return fmt.Errorf("received CANCEL") 1080 | case <-tx.Done(): 1081 | return fmt.Errorf("invite transaction finished while ringing") 1082 | case <-ctx.Done(): 1083 | return ctx.Err() 1084 | case <-time.After(ringtime): 1085 | // Ring time finished 1086 | } 1087 | } else { 1088 | // Send progress 1089 | res := sip.NewResponseFromRequest(req, 100, "Trying", nil) 1090 | if err := dialog.WriteResponse(res); err != nil { 1091 | return fmt.Errorf("failed to send 100 response: %w", err) 1092 | } 1093 | 1094 | p.logSipResponse(&log, res) 1095 | } 1096 | 1097 | contentType := req.ContentType() 1098 | if contentType == nil || contentType.Value() != "application/sdp" { 1099 | return fmt.Errorf("no SDP in INVITE provided") 1100 | } 1101 | 1102 | ip := p.UA.GetIP() 1103 | // rtpPort := rand.Intn(1000*2)/2 + 6000 1104 | if lip := net.ParseIP(lhost); lip != nil && !lip.IsUnspecified() { 1105 | ip = lip 1106 | } 1107 | 1108 | msess, err := media.NewMediaSession(&net.UDPAddr{IP: ip, Port: 0}) 1109 | if err != nil { 1110 | return err 1111 | } 1112 | // Set our custom formats in this negotiation 1113 | if len(opts.Formats) > 0 { 1114 | msess.Formats = opts.Formats 1115 | } 1116 | 1117 | err = msess.RemoteSDP(req.Body()) 1118 | if err != nil { 1119 | return err 1120 | } 1121 | 1122 | log.Info(). 1123 | Str("formats", logFormats(msess.Formats)). 1124 | Str("localAddr", msess.Laddr.String()). 1125 | Str("remoteAddr", msess.Raddr.String()). 1126 | Msg("Media/RTP session created") 1127 | 1128 | res := sip.NewSDPResponseFromRequest(req, msess.LocalSDP()) 1129 | 1130 | // via, _ := res.Via() 1131 | // via.Params["received"] = rhost 1132 | // via.Params["rport"] = strconv.Itoa(rport) 1133 | 1134 | // Add custom headers 1135 | for _, h := range opts.SipHeaders { 1136 | log.Info().Str(h.Name(), h.Value()).Msg("Adding SIP header") 1137 | res.AppendHeader(h) 1138 | } 1139 | 1140 | d = &DialogServerSession{ 1141 | DialogServerSession: dialog, 1142 | MediaSession: msess, 1143 | // done: make(chan struct{}), 1144 | } 1145 | 1146 | log.Info().Msg("Answering call") 1147 | if err := dialog.WriteResponse(res); err != nil { 1148 | d = nil 1149 | return fmt.Errorf("fail to send 200 response: %w", err) 1150 | } 1151 | p.logSipResponse(&log, res) 1152 | 1153 | select { 1154 | case <-tx.Done(): 1155 | // This can be as well TIMER L, which means we received ACK and no more retransmission of 200 will be done 1156 | case <-ctx.Done(): 1157 | // We have received BYE OR Cancel, so we will ignore transaction waiting. 1158 | } 1159 | 1160 | if err := tx.Err(); err != nil { 1161 | return fmt.Errorf("invite transaction ended with error: %w", err) 1162 | } 1163 | return nil 1164 | }() 1165 | 1166 | if err != nil { 1167 | dialog.Close() 1168 | exitError(err) 1169 | stopAnswer() 1170 | } 1171 | 1172 | }) 1173 | 1174 | server.OnAck(func(req *sip.Request, tx sip.ServerTransaction) { 1175 | // This on 2xx 1176 | if d == nil { 1177 | if chal != nil { 1178 | // Ack is for authorization 1179 | return 1180 | } 1181 | 1182 | exitError(fmt.Errorf("received ack but no dialog")) 1183 | stopAnswer() 1184 | } 1185 | 1186 | if err := ds.ReadAck(req, tx); err != nil { 1187 | exitError(fmt.Errorf("dialog ACK err: %w", err)) 1188 | stopAnswer() 1189 | return 1190 | } 1191 | 1192 | select { 1193 | case waitDialog <- d: 1194 | // Reset dialog for next receive 1195 | d = nil 1196 | case <-ctx.Done(): 1197 | } 1198 | 1199 | // Needs check for SDP is right? 1200 | }) 1201 | 1202 | server.OnBye(func(req *sip.Request, tx sip.ServerTransaction) { 1203 | if err := ds.ReadBye(req, tx); err != nil { 1204 | exitError(fmt.Errorf("dialog BYE err: %w", err)) 1205 | return 1206 | } 1207 | 1208 | stopAnswer() // This will close listener 1209 | 1210 | // // Close dialog as well 1211 | // if d != nil { 1212 | // close(d.done) 1213 | // d = nil 1214 | // } 1215 | }) 1216 | 1217 | server.OnOptions(func(req *sip.Request, tx sip.ServerTransaction) { 1218 | res := sip.NewResponseFromRequest(req, 200, "OK", nil) 1219 | tx.Respond(res) 1220 | }) 1221 | 1222 | for _, l := range listeners { 1223 | log.Info().Str("network", l.Network).Str("addr", l.Addr).Msg("Listening on") 1224 | go l.Listen() 1225 | } 1226 | 1227 | if v := ctx.Value(AnswerReadyCtxKey); v != nil { 1228 | close(v.(AnswerReadyCtxValue)) 1229 | } 1230 | 1231 | log.Info().Msg("Waiting for INVITE...") 1232 | select { 1233 | case d = <-waitDialog: 1234 | // Make sure we have cleanup after dialog stop 1235 | d.onClose = stopAnswer 1236 | return d, nil 1237 | case <-ctx.Done(): 1238 | // Check is this caller stopped answer 1239 | if ansCtx.Err() != nil { 1240 | stopAnswer() 1241 | return nil, ansCtx.Err() 1242 | } 1243 | 1244 | // This is when our processing of answer stopped 1245 | return nil, exitErr 1246 | } 1247 | } 1248 | 1249 | // AnswerWithCode will answer with custom code 1250 | // Dialog object is created but it is immediately closed 1251 | // Deprecated: Use Answer with options 1252 | func (p *Phone) AnswerWithCode(ansCtx context.Context, code sip.StatusCode, reason string, opts AnswerOptions) (*DialogServerSession, error) { 1253 | // TODO, do all options make sense? 1254 | opts.AnswerCode = code 1255 | opts.AnswerReason = reason 1256 | dialog, err := p.answer(ansCtx, opts) 1257 | if err != nil { 1258 | return nil, err 1259 | } 1260 | 1261 | if !dialog.InviteResponse.IsSuccess() { 1262 | return dialog, dialog.Close() 1263 | } 1264 | 1265 | // Return closed/terminated dialog 1266 | return dialog, nil 1267 | } 1268 | 1269 | // TODO allow this to be reformated outside 1270 | func (p *Phone) logSipRequest(log *zerolog.Logger, req *sip.Request) { 1271 | log.Info(). 1272 | Str("request", req.StartLine()). 1273 | Str("call_id", req.CallID().Value()). 1274 | Str("from", req.From().Value()). 1275 | Str("event", "SIP"). 1276 | Msg("Request") 1277 | } 1278 | 1279 | func (p *Phone) logSipResponse(log *zerolog.Logger, res *sip.Response) { 1280 | log.Info(). 1281 | Str("response", res.StartLine()). 1282 | Str("call_id", res.CallID().Value()). 1283 | Str("event", "SIP"). 1284 | Msg("Response") 1285 | } 1286 | 1287 | func getResponse(ctx context.Context, tx sip.ClientTransaction) (*sip.Response, error) { 1288 | select { 1289 | case <-tx.Done(): 1290 | return nil, fmt.Errorf("transaction died") 1291 | case res := <-tx.Responses(): 1292 | return res, nil 1293 | case <-ctx.Done(): 1294 | return nil, ctx.Err() 1295 | } 1296 | } 1297 | 1298 | func logFormats(f sdp.Formats) string { 1299 | out := make([]string, len(f)) 1300 | for i, v := range f { 1301 | switch v { 1302 | case "0": 1303 | out[i] = "0(ulaw)" 1304 | case "8": 1305 | out[i] = "8(alaw)" 1306 | default: 1307 | // Unknown then just use as number 1308 | out[i] = v 1309 | } 1310 | } 1311 | return strings.Join(out, ",") 1312 | } 1313 | --------------------------------------------------------------------------------