From f30a29c03152bed75d69b46fb2b13a733ce28c18 Mon Sep 17 00:00:00 2001 From: Justine Alexandra Roberts Tunney Date: Tue, 21 Jul 2015 22:28:15 -0400 Subject: [PATCH] Made a SIP soft-phone in a few hours. --- fone/main.go | 218 +++++++++++++++++++++++++++++++++++------------- rtp/session.go | 62 +++++++++++++- sip/dialog.go | 68 +++++++++------ sip/messages.go | 2 +- sip/receiver.go | 4 +- sip/route.go | 4 +- sip/trace.go | 8 +- 7 files changed, 267 insertions(+), 99 deletions(-) diff --git a/fone/main.go b/fone/main.go index db04e4a..95fb6fd 100644 --- a/fone/main.go +++ b/fone/main.go @@ -1,7 +1,8 @@ package main -// #cgo pkg-config: libpulse-simple +// #cgo pkg-config: ncurses libpulse-simple // #include +// #include // #include // #include import "C" @@ -9,69 +10,84 @@ import "C" import ( "errors" "flag" + "fmt" "github.com/jart/gosip/dsp" "github.com/jart/gosip/rtp" "github.com/jart/gosip/sdp" "github.com/jart/gosip/sip" "github.com/jart/gosip/util" + "io/ioutil" + "log" "net" + "net/http" "os" + "os/signal" "time" "unsafe" ) const ( - hz = 8000 - chans = 1 - ptime = 20 - ssize = 2 - psamps = hz / (1000 / ptime) * chans - pbytes = psamps * ssize - filename = "/var/lib/asterisk/sounds/en/cc-yougotpranked.s16" + hz = 8000 + chans = 1 + ptime = 20 + ssize = 2 + psamps = hz / (1000 / ptime) * chans + pbytes = psamps * ssize ) var ( - address = flag.String("sipAddress", ":9020", "Listen address") - paServerFlag = flag.String("paServer", "", "Pulse Audio server name") - paSinkFlag = flag.String("paSink", "", "Pulse Audio device or sink name") + addressFlag = flag.String("address", "", "Public IP (or hostname) of the local machine. Defaults to asking an untrusted webserver.") + paServerFlag = flag.String("paServer", "", "PulseAudio server name") + paSinkFlag = flag.String("paSink", "", "PulseAudio device or sink name") + muteFlag = flag.Bool("mute", false, "Send comfort noise rather than microphone input") paName = C.CString("fone") ) func main() { - pa, err := makePulseAudio(C.PA_STREAM_PLAYBACK, filename) + log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile) + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s URI\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + if len(flag.Args()) != 1 { + flag.Usage() + os.Exit(1) + } + + // Whom Are We Calling? + requestURIString := flag.Args()[0] + requestURI, err := sip.ParseURI([]byte(requestURIString)) + if err != nil { + fmt.Fprintf(os.Stderr, "Bad Request URI: %s\n", err.Error()) + os.Exit(1) + } + + // Computer Speaker + speaker, err := makePulseAudio(C.PA_STREAM_PLAYBACK, requestURIString) if err != nil { panic(err) } - defer C.pa_simple_free(pa) - defer C.pa_simple_flush(pa, nil) + defer C.pa_simple_free(speaker) + defer C.pa_simple_flush(speaker, nil) - f, err := os.Open(filename) + // Computer Microphone + mic, err := makePulseAudio(C.PA_STREAM_RECORD, requestURIString) if err != nil { panic(err) } - defer f.Close() - - tick := time.NewTicker(ptime * time.Millisecond) - defer tick.Stop() - func() { - for { - var buf [pbytes]byte - select { - case <-tick.C: - got, _ := f.Read(buf[:]) - if got < pbytes { - return - } - var paerr C.int - if C.pa_simple_write(pa, unsafe.Pointer(&buf[0]), pbytes, &paerr) != 0 { - panic(C.GoString(C.pa_strerror(paerr))) - } - } + defer C.pa_simple_free(mic) + + // Get Public IP Address + publicIP := *addressFlag + if publicIP == "" { + publicIP, err = getPublicIP() + if err != nil { + panic(err) } - }() - os.Exit(0) + } - // Create RTP audio session. + // Create RTP Session rs, err := rtp.NewSession("") if err != nil { panic(err) @@ -79,11 +95,20 @@ func main() { defer rs.Close() rtpPort := uint16(rs.Sock.LocalAddr().(*net.UDPAddr).Port) + // Construct SIP INVITE invite := &sip.Msg{ Method: sip.MethodInvite, - Request: &sip.URI{User: "echo", Host: "127.0.0.1", Port: 5060}, + Request: requestURI, + Via: &sip.Via{Host: publicIP}, + To: &sip.Addr{Uri: requestURI}, + From: &sip.Addr{Uri: &sip.URI{Host: publicIP, User: os.Getenv("USER")}}, + Contact: &sip.Addr{Uri: &sip.URI{Host: publicIP}}, Payload: &sdp.SDP{ - Origin: sdp.Origin{ID: util.GenerateOriginID()}, + Addr: publicIP, + Origin: sdp.Origin{ + ID: util.GenerateOriginID(), + Addr: publicIP, + }, Audio: &sdp.Media{ Port: rtpPort, Codecs: []sdp.Codec{sdp.ULAWCodec, sdp.DTMFCodec}, @@ -91,49 +116,103 @@ func main() { }, } - // Create a SIP phone call. + // Create SIP Dialog State Machine dl, err := sip.NewDialog(invite) if err != nil { panic(err) } - // We're going to send white noise every 20ms. + // Send Audio Every 20ms var frame rtp.Frame awgn := dsp.NewAWGN(-45.0) ticker := time.NewTicker(20 * time.Millisecond) defer ticker.Stop() - // Hangup after 200ms. - death := time.After(200 * time.Millisecond) + // Ctrl+C or Kill Graceful Shutdown + death := make(chan os.Signal, 1) + signal.Notify(death, os.Interrupt, os.Kill) + + // DTMF Terminal Input + keyboard := make(chan byte) + keyboardStart := func() { + C.cbreak() + C.noecho() + go func() { + var buf [1]byte + for { + amt, err := os.Stdin.Read(buf[:]) + if err != nil || amt != 1 { + log.Printf("Keyboard: %s\r\n", err) + return + } + keyboard <- buf[0] + } + }() + } + + C.initscr() + defer C.endwin() // Let's GO! var answered bool + var paerr C.int for { select { + + // Send Audio case <-ticker.C: - for n := 0; n < 160; n++ { - frame[n] = awgn.Get() + if *muteFlag { + for n := 0; n < psamps; n++ { + frame[n] = awgn.Get() + } + } else { + if C.pa_simple_read(mic, unsafe.Pointer(&frame[0]), pbytes, &paerr) != 0 { + log.Printf("Microphone: %s\r\n", C.GoString(C.pa_strerror(paerr))) + break + } } if err := rs.Send(&frame); err != nil { - panic("RTP send failed: " + err.Error()) + log.Printf("RTP: %s\r\n", err.Error()) } - case err := <-dl.OnErr: - panic(err) + + // Send DTMF + case ch := <-keyboard: + if err := rs.SendDTMF(ch); err != nil { + log.Printf("DTMF: %s\r\n", err.Error()) + } + log.Printf("DTMF: %c\r\n", ch) + + // Receive Audio + case frame := <-rs.C: + if len(frame) != psamps { + log.Printf("RTP: Received undersized frame: %d != %d\r\n", len(frame), psamps) + } else { + if C.pa_simple_write(speaker, unsafe.Pointer(&frame[0]), pbytes, &paerr) != 0 { + log.Printf("Speaker: %s\r\n", C.GoString(C.pa_strerror(paerr))) + } + } + rs.R <- frame + + // Signalling + case rs.Peer = <-dl.OnPeer: case state := <-dl.OnState: switch state { case sip.DialogAnswered: answered = true + keyboardStart() case sip.DialogHangup: - if !answered { - panic("Call didn't get answered!") + if answered { + return + } else { + os.Exit(1) } - return } - case rs.Peer = <-dl.OnPeer: - case frame := <-rs.C: - rs.R <- frame + + // Errors and Interruptions + case err := <-dl.OnErr: + log.Fatalf("SIP: %s\r\n", err.Error()) case err := <-rs.E: - panic("RTP recv failed: " + err.Error()) + log.Printf("RTP: %s\r\n", err.Error()) rs.CloseAfterError() dl.Hangup <- true case <-death: @@ -149,11 +228,19 @@ func makePulseAudio(direction C.pa_stream_direction_t, streamName string) (*C.pa ss.channels = chans var ba C.pa_buffer_attr - ba.maxlength = pbytes * 4 - ba.tlength = pbytes - ba.prebuf = pbytes * 2 - ba.minreq = pbytes - ba.fragsize = 0xffffffff + if direction == C.PA_STREAM_PLAYBACK { + ba.maxlength = pbytes * 4 + ba.tlength = pbytes + ba.prebuf = pbytes * 2 + ba.minreq = pbytes + ba.fragsize = 0xffffffff + } else { + ba.maxlength = pbytes * 4 + ba.tlength = 0xffffffff + ba.prebuf = 0xffffffff + ba.minreq = 0xffffffff + ba.fragsize = pbytes + } var paServer *C.char if *paServerFlag != "" { @@ -177,3 +264,16 @@ func makePulseAudio(direction C.pa_stream_direction_t, streamName string) (*C.pa } return pa, nil } + +func getPublicIP() (string, error) { + resp, err := http.Get("http://api.ipify.org") + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(body), nil +} diff --git a/rtp/session.go b/rtp/session.go index fee65ea..aa4e11f 100644 --- a/rtp/session.go +++ b/rtp/session.go @@ -3,6 +3,7 @@ package rtp import ( + "errors" "github.com/jart/gosip/dsp" "github.com/jart/gosip/sdp" "log" @@ -18,6 +19,10 @@ const ( rtpBindPortMax = 32768 ) +var ( + dtmfCodes = map[byte]byte{'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '*': 10, '#': 11, 'a': 12, 'A': 12, 'b': 13, 'B': 13, 'c': 14, 'C': 14, 'd': 15, 'D': 15, '!': 16} +) + type Frame [160]int16 // Session allows sending and receiving slinear frames for a single SIP media @@ -46,7 +51,7 @@ type Session struct { // Creates a new RTP µLaw 20ptime session listening on host with a random port // selected from the range [16384,32768]. func NewSession(host string) (rs *Session, err error) { - conn, err := listenRTP(host) + conn, err := Listen(host) if err != nil { return nil, err } @@ -61,7 +66,6 @@ func NewSession(host string) (rs *Session, err error) { R: r, Sock: sock, Header: Header{ - PT: sdp.ULAWCodec.PT, Seq: 666, TS: 0, Ssrc: rand.Uint32(), @@ -74,6 +78,7 @@ func (rs *Session) Send(frame *Frame) (err error) { if rs == nil || rs.Sock == nil || rs.Peer == nil { return nil } + rs.Header.PT = sdp.ULAWCodec.PT rs.Header.Write(rs.obuf) rs.Header.TS += 160 rs.Header.Seq++ @@ -84,10 +89,11 @@ func (rs *Session) Send(frame *Frame) (err error) { return } -func (rs *Session) SendRaw(data []byte, samps uint32) (err error) { +func (rs *Session) SendRaw(pt uint8, data []byte, samps uint32) (err error) { if rs == nil || rs.Sock == nil || rs.Peer == nil { return nil } + rs.Header.PT = pt rs.Header.Write(rs.obuf) rs.Header.TS += samps rs.Header.Seq++ @@ -95,6 +101,51 @@ func (rs *Session) SendRaw(data []byte, samps uint32) (err error) { return } +func (rs *Session) SendDTMF(digit byte) error { + const volume = 6 + const duration = 1600 + const interval = 400 + code, ok := dtmfCodes[digit] + if !ok { + return errors.New("Invalid DTMF digit: " + string(digit)) + } + if rs == nil || rs.Sock == nil || rs.Peer == nil { + return nil + } + rs.Header.PT = sdp.DTMFCodec.PT + rs.Header.Mark = true + rs.obuf[HeaderSize+0] = code + rs.obuf[HeaderSize+1] = volume & 0x3f + dur := uint16(1) + for { + rs.obuf[HeaderSize+2] = byte(dur >> 8) + rs.obuf[HeaderSize+3] = byte(dur & 0xff) + rs.Header.Write(rs.obuf) + _, err := rs.Sock.WriteTo(rs.obuf[:HeaderSize+4], rs.Peer) + if err != nil { + return err + } + rs.Header.Seq++ + rs.Header.Mark = false + dur += interval + if dur >= duration { + break + } + } + rs.obuf[HeaderSize+1] |= 0x80 + rs.obuf[HeaderSize+2] = byte(duration >> 8) + rs.obuf[HeaderSize+3] = byte(duration & 0xff) + for n := 0; n < 3; n++ { + rs.Header.Write(rs.obuf) + _, err := rs.Sock.WriteTo(rs.obuf[:HeaderSize+4], rs.Peer) + if err != nil { + return err + } + rs.Header.Seq++ + } + return nil +} + func (rs *Session) Close() { if rs == nil || rs.Sock == nil { return @@ -152,12 +203,15 @@ func receiver(sock *net.UDPConn, c chan<- *Frame, e chan<- error, r <-chan *Fram close(e) } -func listenRTP(host string) (sock net.PacketConn, err error) { +func Listen(host string) (sock net.PacketConn, err error) { if strings.Contains(host, ":") { return net.ListenPacket("udp", host) } for i := 0; i < rtpBindMaxAttempts; i++ { port := rtpBindPortMin + rand.Int63()%(rtpBindPortMax-rtpBindPortMin+1) + if port%2 == 1 { + port-- + } saddr := net.JoinHostPort(host, strconv.FormatInt(port, 10)) sock, err = net.ListenPacket("udp", saddr) if err == nil || !strings.Contains(err.Error(), "address already in use") { diff --git a/sip/dialog.go b/sip/dialog.go index 00b37fc..18e166d 100755 --- a/sip/dialog.go +++ b/sip/dialog.go @@ -6,6 +6,7 @@ import ( "bytes" "errors" "flag" + "github.com/jart/gosip/rtp" "github.com/jart/gosip/sdp" "github.com/jart/gosip/util" "log" @@ -22,7 +23,7 @@ const ( var ( looseSignalling = flag.Bool("looseSignalling", true, "Permit SIP messages from servers other than the next hop.") - resendInterval = flag.Int("resendInterval", 200, "Milliseconds between SIP resends.") + resendInterval = flag.Int("resendInterval", 400, "Milliseconds between SIP resends.") maxResends = flag.Int("maxResends", 2, "Max SIP message retransmits.") ) @@ -95,7 +96,7 @@ func (dls *dialogState) run() { dls.sock.Close() dls.sock = nil if util.IsRefused(err) { - log.Printf("ICMP refusal: %s (%s)", dls.sock.RemoteAddr(), dls.dest) + log.Printf("ICMP refusal: %s (%s)\r\n", dls.sock.RemoteAddr(), dls.dest) if !dls.popRoute() { return } @@ -179,7 +180,7 @@ func (dls *dialogState) connect() bool { dls.cleanupSock() conn, err := net.Dial("udp", dls.addr) if err != nil { - log.Printf("net.Dial(udp, %s) failed: %s", dls.addr, err) + log.Printf("net.Dial(udp, %s) failed: %s\r\n", dls.addr, err) return false } dls.sock = conn.(*net.UDPConn) @@ -193,9 +194,9 @@ func (dls *dialogState) connect() bool { // SIP signalling paths can change depending on the environment, so we need // to be able to accept packets from anyone. if dls.csock == nil && *looseSignalling { - cconn, err := net.ListenPacket("udp", ":0") + cconn, err := rtp.Listen("") if err != nil { - log.Printf("net.ListenPacket(udp, :0) failed: %s", err) + log.Printf("Loose signalling not possible: %s\r\n", err) return false } dls.csock = cconn.(*net.UDPConn) @@ -213,28 +214,41 @@ func (dls *dialogState) populate(msg *Msg) { laddr := dls.sock.LocalAddr().(*net.UDPAddr) lhost := laddr.IP.String() lport := uint16(laddr.Port) - msg.Via = &Via{ - Host: lhost, - Port: lport, - Param: &Param{"branch", util.GenerateBranch(), nil}, + + if msg.Via == nil { + msg.Via = &Via{Host: lhost} + } + msg.Via.Port = lport + branch := msg.Via.Param.Get("branch") + if branch != nil { + branch.Value = util.GenerateBranch() + } else { + msg.Via.Param = &Param{"branch", util.GenerateBranch(), msg.Via.Param} } + if msg.Contact == nil { - if dls.csock != nil { - lport = uint16(dls.csock.LocalAddr().(*net.UDPAddr).Port) - } - msg.Contact = &Addr{ - Uri: &URI{ - Scheme: "sip", - Host: lhost, - Port: lport, - Param: &URIParam{"transport", "udp", nil}, - }, - } + msg.Contact = &Addr{Uri: &URI{Scheme: "sip", Host: lhost}} + } + if dls.csock != nil { + msg.Contact.Uri.Port = uint16(dls.csock.LocalAddr().(*net.UDPAddr).Port) + } else { + msg.Contact.Uri.Port = lport } + if msg.Contact.Uri.Param.Get("transport") == nil { + msg.Contact.Uri.Param = &URIParam{"transport", "udp", msg.Contact.Uri.Param} + } + if msg.Method == MethodInvite { if ms, ok := msg.Payload.(*sdp.SDP); ok { - ms.Addr = lhost - ms.Origin.Addr = lhost + if ms.Addr == "" { + ms.Addr = lhost + } + if ms.Origin.Addr == "" { + ms.Origin.Addr = lhost + } + if ms.Origin.ID == "" { + ms.Origin.ID = util.GenerateOriginID() + } } } PopulateMessage(nil, nil, msg) @@ -249,7 +263,7 @@ func (dls *dialogState) handleMessage(msg *Msg) bool { return false } if msg.CallID != dls.request.CallID { - log.Printf("Received message doesn't match dialog") + log.Printf("Received message doesn't match dialog\r\n") return dls.send(NewResponse(msg, StatusCallTransactionDoesNotExist)) } if msg.IsResponse() { @@ -261,7 +275,7 @@ func (dls *dialogState) handleMessage(msg *Msg) bool { func (dls *dialogState) handleResponse(msg *Msg) bool { if !ResponseMatch(dls.request, msg) { - log.Println("Received response doesn't match transaction") + log.Println("Received response doesn't match transaction\r\n") return true } if msg.Status >= StatusOK && dls.request.Method == MethodInvite { @@ -296,7 +310,7 @@ func (dls *dialogState) handleResponse(msg *Msg) bool { } case StatusServiceUnavailable: if dls.request == dls.invite { - log.Printf("Service unavailable: %s (%s)", dls.sock.RemoteAddr(), dls.dest) + log.Printf("Service unavailable: %s (%s)\r\n", dls.sock.RemoteAddr(), dls.dest) return dls.popRoute() } else { dls.errChan <- &ResponseError{Msg: msg} @@ -394,7 +408,7 @@ func (dls *dialogState) resendRequest() bool { dls.requestResends++ dls.requestTimer = time.After(duration(resendInterval)) } else { - log.Printf("Timeout: %s (%s)", dls.sock.RemoteAddr(), dls.dest) + log.Printf("Timeout: %s (%s)\r\n", dls.sock.RemoteAddr(), dls.dest) if !dls.popRoute() { return false } @@ -422,7 +436,7 @@ func (dls *dialogState) resendResponse() bool { dls.responseTimer = time.After(duration(resendInterval)) } else { // TODO(jart): If resending INVITE 200 OK, start sending BYE. - log.Printf("Timeout sending response: %s (%s)", dls.sock.RemoteAddr(), dls.dest) + log.Printf("Timeout sending response: %s (%s)\r\n", dls.sock.RemoteAddr(), dls.dest) if !dls.popRoute() { return false } diff --git a/sip/messages.go b/sip/messages.go index 5963d43..f821d16 100644 --- a/sip/messages.go +++ b/sip/messages.go @@ -60,7 +60,7 @@ func NewAck(msg, invite *Msg) *Msg { func NewCancel(invite *Msg) *Msg { if invite.IsResponse() || invite.Method != MethodInvite { - log.Printf("Can't CANCEL anything non-INVITE:\n%s", invite) + log.Printf("Can't CANCEL anything non-INVITE:\r\n%s", invite) } return &Msg{ Method: MethodCancel, diff --git a/sip/receiver.go b/sip/receiver.go index db86dc8..0656550 100644 --- a/sip/receiver.go +++ b/sip/receiver.go @@ -25,7 +25,7 @@ func ReceiveMessages(sock *net.UDPConn, c chan<- *Msg, e chan<- error) { } msg, err := ParseMsg(packet) if err != nil { - log.Println("Dropping SIP message:", err) + log.Printf("Dropping SIP message: %s\r\n", err) continue } addReceived(msg, addr) @@ -74,6 +74,6 @@ func fixMessagesFromStrictRouters(lhost string, lport uint16, msg *Msg) { seclast.Next = nil msg.Route.Last() } - log.Printf("Fixing request URI after strict router traversal: %s -> %s", oldReq, newReq) + log.Printf("Fixing request URI after strict router traversal: %s -> %s\r\n", oldReq, newReq) } } diff --git a/sip/route.go b/sip/route.go index 0edf83a..62259b8 100644 --- a/sip/route.go +++ b/sip/route.go @@ -105,10 +105,10 @@ func RouteAddress(host string, port uint16, wantSRV bool) (routes *AddressRoute, } s = " " + routes.Address + s } - log.Printf("%s routes to: %s", host, s) + log.Printf("%s routes to: %s\r\n", host, s) return routes, nil } - log.Printf("net.LookupSRV(sip, udp, %s) failed: %s", host, err) + log.Printf("net.LookupSRV(sip, udp, %s) failed: %s\r\n", host, err) } port = 5060 } diff --git a/sip/trace.go b/sip/trace.go index 894ea09..7219244 100644 --- a/sip/trace.go +++ b/sip/trace.go @@ -15,15 +15,15 @@ var ( func trace(dir string, pkt []byte, addr net.Addr) { size := len(pkt) bar := strings.Repeat("-", 72) - suffix := "\n" + suffix := "\r\n" if pkt != nil && len(pkt) > 0 && pkt[len(pkt)-1] == '\n' { suffix = "" } log.Printf( - "%s %d bytes from %s/%s\n"+ - "%s\n"+ + "%s %d bytes from %s/%s\r\n"+ + "%s\r\n"+ "%s%s"+ - "%s\n", + "%s\r\n", dir, size, addr.Network(), addr.String(), bar, pkt, suffix,