Browse Source

Made a SIP soft-phone in a few hours.

pull/2/head
Justine Alexandra Roberts Tunney 11 years ago
parent
commit
f30a29c031
7 changed files with 267 additions and 99 deletions
  1. +159
    -59
      fone/main.go
  2. +58
    -4
      rtp/session.go
  3. +41
    -27
      sip/dialog.go
  4. +1
    -1
      sip/messages.go
  5. +2
    -2
      sip/receiver.go
  6. +2
    -2
      sip/route.go
  7. +4
    -4
      sip/trace.go

+ 159
- 59
fone/main.go View File

@ -1,7 +1,8 @@
package main
// #cgo pkg-config: libpulse-simple
// #cgo pkg-config: ncurses libpulse-simple
// #include <stdlib.h>
// #include <ncurses.h>
// #include <pulse/simple.h>
// #include <pulse/error.h>
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
}

+ 58
- 4
rtp/session.go View File

@ -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") {


+ 41
- 27
sip/dialog.go View File

@ -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
}


+ 1
- 1
sip/messages.go View File

@ -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,


+ 2
- 2
sip/receiver.go View File

@ -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)
}
}

+ 2
- 2
sip/route.go View File

@ -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
}


+ 4
- 4
sip/trace.go View File

@ -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,


Loading…
Cancel
Save