Browse Source

initial import

pull/2/head
Justine Alexandra Roberts Tunney 11 years ago
commit
71498109e7
31 changed files with 4706 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +68
    -0
      README.md
  3. +15
    -0
      dsp/dsp.go
  4. +70
    -0
      dsp/dsp_amd64.s
  5. +84
    -0
      dsp/dsp_test.go
  6. +356
    -0
      example/echo/echo_test.go
  7. +177
    -0
      example/echo2/echo2_test.go
  8. +108
    -0
      example/options/options_test.go
  9. +83
    -0
      example/rawsip/rawsip_test.go
  10. +93
    -0
      rtp/dtmf.go
  11. +157
    -0
      rtp/rtp.go
  12. +41
    -0
      sdp/codec.go
  13. +62
    -0
      sdp/codecs.go
  14. +45
    -0
      sdp/media.go
  15. +41
    -0
      sdp/origin.go
  16. +456
    -0
      sdp/sdp.go
  17. +457
    -0
      sdp/sdp_test.go
  18. +191
    -0
      sip/addr.go
  19. +164
    -0
      sip/addr_test.go
  20. +111
    -0
      sip/messages.go
  21. +378
    -0
      sip/msg.go
  22. +214
    -0
      sip/msg_test.go
  23. +96
    -0
      sip/phrases.go
  24. +62
    -0
      sip/prefs.go
  25. +291
    -0
      sip/transport.go
  26. +202
    -0
      sip/uri.go
  27. +194
    -0
      sip/uri_test.go
  28. +81
    -0
      sip/util.go
  29. +149
    -0
      sip/via.go
  30. +166
    -0
      util/escape.go
  31. +93
    -0
      util/util.go

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
*.test

+ 68
- 0
README.md View File

@ -0,0 +1,68 @@
# gosip
Version: 0.1
Copyright: Copyright (c) 2010-2014 Justine Tunney
License: MIT
## About
gosip (pronounced like the word "gossip") is a VoIP telephony library
written in Google's Go programming language.
gosip provides a barebones sip/sdp/rtp implementation suitable for use
over a trusted network. To talk to the outside world you should
deploy your gosip applications behind a session border controller
(like tube, FreeSWITCH, or OpenSER) which are more capable of dealing
with security, network quality issues and SIP interop.
I was originally going to write bindings for sofia-sip but ultimately
decided it'd be quicker, less buggy, and faster performing to write a
lightweight SIP stack from scratch.
## Installation
Once Go is installed, just run ``make`` to build/test/install.
## Learning
The following unit tests also serve as tutorials to help you
understand SIP and the abstractions provided by this library.
- sip/rawsip_test.go: How to do SIP the hard way
- sip/url_test.go: Shows you what the SIP URL data structures look like
- sip/addr_test.go: Addresses are pretty much URLs inside angle brackets
- sip/msg_test.go: How the data structure for SIP packets works
- sip/manualsip_test.go: Make SIP easier with parser/formatter objects
- sip/echo_test.go: Manually make a test call to an echo application
## Overview
This is what a sip stack looks like:
+-----------------------------------------------------------------------+
| 9. Application Layer (your code) |
+-----------------------------------------------------------------------+
| 8. Telephony API (tel/...) |
+---------------------------------------+-------------------------------+
| 6. SIP Transaction (sip/transact.go) | 6. Media Codecs (sip/dsp.go) |
+---------------------------------------+-------------------------------+
| 5. SIP Transport (sip/transport.go) | 5. RTP Transport (sip/rtp.go) |
+---------------------------------------+-------------------------------+
| 2/3/4. Network Transport Layer |
+-----------------------------------------------------------------------+
| 1. Tubes |
+-----------------------------------------------------------------------+
| 0. Electrons and Photons |
+-----------------------------------------------------------------------+
## RFCs
- [SIP (RFC 3261)](https://tools.ietf.org/html/rfc3261)
- [SDP (RFC 4566)](https://tools.ietf.org/html/rfc4566)
- [RTP (RFC 3550)](https://tools.ietf.org/html/rfc3550)
- [RTP DTMF (RFC 4733)](https://tools.ietf.org/html/rfc4733)

+ 15
- 0
dsp/dsp.go View File

@ -0,0 +1,15 @@
// Digital Signal Processing
package dsp
// Mixes together two audio frames containing 160 samples. Uses saturation
// adding so you don't hear clipping if ppl talk loud. Uses 128-bit SIMD
// instructions so we can add eight numbers at the same time.
func L16MixSat160(dst, src *int16)
// Compresses a PCM audio sample into a G.711 μ-Law sample. The BSR instruction
// is what makes this code fast.
func LinearToUlaw(linear int64) (ulaw int64)
// Turns a μ-Law byte back into an audio sample.
func UlawToLinear(ulaw int64) (linear int64)

+ 70
- 0
dsp/dsp_amd64.s View File

@ -0,0 +1,70 @@
#define ULAW_BIAS $0x84
// func L16MixSat160(dst, src *int16)
TEXT ·L16MixSat160(SB),4,$0-16
MOVQ dst+0(FP), AX
MOVQ src+8(FP), BX
MOVQ $19, CX
moar: MOVO 0(BX), X0
PADDSW 0(AX), X0
MOVO X0, 0(AX)
ADDQ $16, AX
ADDQ $16, BX
DECQ CX
CMPQ CX, $0
JGE moar
RET
// func LinearToUlaw(linear int64) (ulaw int64)
TEXT ·LinearToUlaw(SB),4,$0-16
MOVQ linear+0(FP), AX
CMPQ AX, $0 // if rax < 0: (Jump if Signed)
JS ifNeg // goto ifNeg
ifPos: ADDQ ULAW_BIAS, AX
MOVQ $0xFF, R13 // mask bits we'll apply later
JMP endIf
ifNeg: MOVQ ULAW_BIAS, BX // rax = ULAW_BIAS - rax
SUBQ AX, BX
MOVQ BX, AX
MOVQ $0x7F, R13 // mask bits we'll apply later
endIf: MOVQ AX, DX
ORQ $0xFF, DX
BSRQ DX, BX // get position of highest bit
SUBQ $7, BX
CMPQ BX, $8 // if rcx >= 8:
JGE loud // goto loud
MOVQ BX, CX
ADDQ $3, CX
SARQ CX, AX
ANDQ $0xF, AX
SHLQ $4, BX
ORQ BX, AX
XORQ R13, AX
MOVQ AX, ulaw+8(FP)
RET
loud: XORQ $0x7F, R13
MOVQ R13, ulaw+8(FP)
RET
// func UlawToLinear(ulaw int64) (linear int64)
TEXT ·UlawToLinear(SB),4,$0-16
MOVQ ulaw+0(FP), AX
NOTQ AX
MOVQ AX, CX
MOVQ AX, DX
ANDQ $0x0F, AX
SHLQ $3, AX
ADDQ $0x84, AX
ANDQ $0x70, CX
SARQ $4, CX
SHLQ CX, AX
ANDQ $0x80, DX
CMPQ DX, $0
JE death
MOVQ $0x84, BX
SUBQ AX, BX
MOVQ BX, linear+8(FP)
RET
death: SUBQ $0x84, AX
MOVQ AX, linear+8(FP)
RET

+ 84
- 0
dsp/dsp_test.go View File

@ -0,0 +1,84 @@
package dsp_test
import (
"github.com/jart/gosip/dsp"
"testing"
)
func TestL16MixSat160(t *testing.T) {
var x, y [160]int16
for n := 0; n < 160; n++ {
x[n] = int16(n)
y[n] = int16(666)
}
dsp.L16MixSat160(&x[0], &y[0])
for n := 0; n < 160; n++ {
want := int16(n + 666)
if x[n] != want {
t.Errorf("x[%v] = %v (wanted: %v)", n, x[n], want)
return
}
if y[n] != int16(666) {
t.Errorf("side effect y[%v] = %v", n, y[n])
return
}
}
}
var ulawTable = []int64{
-32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, -23932,
-22908, -21884, -20860, -19836, -18812, -17788, -16764, -15996, -15484,
-14972, -14460, -13948, -13436, -12924, -12412, -11900, -11388, -10876,
-10364, -9852, -9340, -8828, -8316, -7932, -7676, -7420, -7164,
-6908, -6652, -6396, -6140, -5884, -5628, -5372, -5116, -4860,
-4604, -4348, -4092, -3900, -3772, -3644, -3516, -3388, -3260,
-3132, -3004, -2876, -2748, -2620, -2492, -2364, -2236, -2108,
-1980, -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,
-1372, -1308, -1244, -1180, -1116, -1052, -988, -924, -876,
-844, -812, -780, -748, -716, -684, -652, -620, -588,
-556, -524, -492, -460, -428, -396, -372, -356, -340,
-324, -308, -292, -276, -260, -244, -228, -212, -196,
-180, -164, -148, -132, -120, -112, -104, -96, -88,
-80, -72, -64, -56, -48, -40, -32, -24, -16,
-8, 0, 32124, 31100, 30076, 29052, 28028, 27004, 25980,
24956, 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,
15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, 11900,
11388, 10876, 10364, 9852, 9340, 8828, 8316, 7932, 7676,
7420, 7164, 6908, 6652, 6396, 6140, 5884, 5628, 5372,
5116, 4860, 4604, 4348, 4092, 3900, 3772, 3644, 3516,
3388, 3260, 3132, 3004, 2876, 2748, 2620, 2492, 2364,
2236, 2108, 1980, 1884, 1820, 1756, 1692, 1628, 1564,
1500, 1436, 1372, 1308, 1244, 1180, 1116, 1052, 988,
924, 876, 844, 812, 780, 748, 716, 684, 652,
620, 588, 556, 524, 492, 460, 428, 396, 372,
356, 340, 324, 308, 292, 276, 260, 244, 228,
212, 196, 180, 164, 148, 132, 120, 112, 104,
96, 88, 80, 72, 64, 56, 48, 40, 32, 24, 16, 8, 0,
}
func TestLinearToUlaw(t *testing.T) {
if dsp.LinearToUlaw(0) != 255 {
t.Error("omg")
}
if dsp.LinearToUlaw(-100) != 114 {
t.Error("omg")
}
}
func TestUlawToLinear(t *testing.T) {
for n := 0; n <= 255; n++ {
if dsp.UlawToLinear(int64(n)) != ulawTable[n] {
t.Error("omg")
return
}
}
}
func TestLinearToUlawToLinear(t *testing.T) {
for n := 0; n <= 255; n++ {
if dsp.UlawToLinear(dsp.LinearToUlaw(ulawTable[n])) != ulawTable[n] {
t.Error("omg")
return
}
}
}

+ 356
- 0
example/echo/echo_test.go View File

@ -0,0 +1,356 @@
// Example demonstrating how to make a phone call (SIP/SDP/RTP/DTMF) to an Echo
// application without any higher level SIP abstractions.
//
// Example Trace:
//
// send 765 bytes to udp/[10.11.34.37]:5060 at 15:46:48.569658:
// ------------------------------------------------------------------------
// INVITE sip:10.11.34.37 SIP/2.0
// Via: SIP/2.0/UDP 10.11.34.37:59516;rport;branch=z9hG4bKS308QB9UUpNyD
// Max-Forwards: 70
// From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg
// To: <sip:10.11.34.37>
// Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf
// CSeq: 133097268 INVITE
// Contact: <sip:10.11.34.37:59516>
// User-Agent: tube/0.1
// Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, PRACK, MESSAGE, SUBSCRIBE, NOTIFY, REFER, UPDATE, INFO
// Supported: timer, 100rel
// Allow-Events: talk
// Content-Type: application/sdp
// Content-Disposition: session
// Content-Length: 218
//
// v=0
// o=- 2862054018559638081 6057228511765453924 IN IP4 10.11.34.37
// s=-
// c=IN IP4 10.11.34.37
// t=0 0
// m=audio 23448 RTP/AVP 0 101
// a=rtpmap:0 PCMU/8000
// a=rtpmap:101 telephone-event/8000
// a=fmtp:101 0-16
// a=ptime:20
// ------------------------------------------------------------------------
// recv 282 bytes from udp/[10.11.34.37]:5060 at 15:46:48.570890:
// ------------------------------------------------------------------------
// SIP/2.0 100 Trying
// Via: SIP/2.0/UDP 10.11.34.37:59516;rport=59516;branch=z9hG4bKS308QB9UUpNyD
// From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg
// To: <sip:10.11.34.37>
// Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf
// CSeq: 133097268 INVITE
// User-Agent: tube/0.1
// Content-Length: 0
//
// ------------------------------------------------------------------------
// recv 668 bytes from udp/[10.11.34.37]:5060 at 15:46:48.571844:
// ------------------------------------------------------------------------
// SIP/2.0 200 OK
// Via: SIP/2.0/UDP 10.11.34.37:59516;rport=59516;branch=z9hG4bKS308QB9UUpNyD
// From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg
// To: <sip:10.11.34.37>;tag=a1vFUD7vvK4ZN
// Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf
// CSeq: 133097268 INVITE
// Contact: <sip:10.11.34.37>
// User-Agent: tube/0.1
// Accept: application/sdp
// Allow: INVITE, ACK, BYE, CANCEL, OPTIONS
// Supported: timer
// Content-Type: application/sdp
// Content-Disposition: session
// Content-Length: 196
//
// v=0
// o=- 403551387931241779 5960509760717556241 IN IP4 10.11.34.37
// s=-
// c=IN IP4 10.11.34.37
// t=0 0
// m=audio 19858 RTP/AVP 0
// a=rtpmap:0 PCMU/8000
// a=rtmap:97 speex/16000
// a=rtmap:98 speex/8000
// ------------------------------------------------------------------------
// send 296 bytes to udp/[10.11.34.37]:5060 at 15:46:48.572320:
// ------------------------------------------------------------------------
// ACK sip:10.11.34.37 SIP/2.0
// Via: SIP/2.0/UDP 10.11.34.37:59516;rport;branch=z9hG4bKtct1S6SZrZBHS
// Max-Forwards: 70
// From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg
// To: <sip:10.11.34.37>;tag=a1vFUD7vvK4ZN
// Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf
// CSeq: 133097268 ACK
// Content-Length: 0
//
// ------------------------------------------------------------------------
// send 1017 bytes to udp/[10.11.34.37]:5060 at 15:46:53.048617:
// ------------------------------------------------------------------------
// BYE sip:10.11.34.37 SIP/2.0
// Via: SIP/2.0/UDP 10.11.34.37:59516;rport;branch=z9hG4bKUNKtU1a3N813m
// Max-Forwards: 70
// From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg
// To: <sip:10.11.34.37>;tag=a1vFUD7vvK4ZN
// Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf
// CSeq: 133097269 BYE
// User-Agent: tube/0.1
// Content-Type: text/plain
// Content-Length: 671
//
// .--. .--.
// .'(` / ..\
// __.>\ '. _.---,._,' ____.' _o/
// /.--. : |/' _.--.< '--. |.__
// _..-' `\ /' `' _.-' /--'
// >_.-``-. `Y /' _.---._____ _.--' /
// '` .-''. \|: \.' ___, .-'` ~'--....___.-'
// .'--._ `-: \/ /' \\
// /.'`\ :; /' `-.
// -` | |
// :.; : | thank you for flying tube
// |: | version o.1
// | |
// :. : | besiyata dishmaya
// .jgs ; telecommunications inc.
// /:::. `\
// ------------------------------------------------------------------------
// recv 353 bytes from udp/[10.11.34.37]:5060 at 15:46:53.049140:
// ------------------------------------------------------------------------
// SIP/2.0 200 OK
// Via: SIP/2.0/UDP 10.11.34.37:59516;rport=59516;branch=z9hG4bKUNKtU1a3N813m
// From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg
// To: <sip:10.11.34.37>;tag=a1vFUD7vvK4ZN
// Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf
// CSeq: 133097269 BYE
// User-Agent: tube/0.1
// Allow: INVITE, ACK, BYE, CANCEL, OPTIONS
// Supported: timer
// Content-Length: 0
//
// ------------------------------------------------------------------------
//
package echo_test
import (
"bytes"
"github.com/jart/gosip/rtp"
"github.com/jart/gosip/sdp"
"github.com/jart/gosip/sip"
"github.com/jart/gosip/util"
"log"
"math/rand"
"net"
"testing"
"time"
)
func TestCallToEchoApp(t *testing.T) {
// Connect to the remote SIP UDP endpoint.
conn, err := net.Dial("udp", "127.0.0.1:5060")
if err != nil {
t.Error("sip dial:", err)
return
}
defer conn.Close()
raddr := conn.RemoteAddr().(*net.UDPAddr)
laddr := conn.LocalAddr().(*net.UDPAddr)
// Create an RTP socket.
rtpsock, err := net.ListenPacket("udp", "108.61.60.146:0")
if err != nil {
t.Error("rtp listen:", err)
return
}
defer rtpsock.Close()
rtpaddr := rtpsock.LocalAddr().(*net.UDPAddr)
// Create an invite message and attach the SDP.
invite := &sip.Msg{
CallID: util.GenerateCallID(),
CSeq: util.GenerateCSeq(),
Method: "INVITE",
CSeqMethod: "INVITE",
Request: &sip.URI{
Scheme: "sip",
User: "echo",
Host: raddr.IP.String(),
Port: uint16(raddr.Port),
},
Via: &sip.Via{
Host: laddr.IP.String(),
Port: uint16(laddr.Port),
Params: sip.Params{"branch": util.GenerateBranch()},
},
From: &sip.Addr{
Display: "Echo Test",
Uri: &sip.URI{
Scheme: "sip",
Host: laddr.IP.String(),
Port: uint16(laddr.Port),
},
Params: sip.Params{"tag": util.GenerateTag()},
},
To: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: raddr.IP.String(),
Port: uint16(raddr.Port),
},
},
Contact: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: laddr.IP.String(),
Port: uint16(laddr.Port),
},
},
Headers: sip.Headers{
"Content-Type": "application/sdp",
"User-Agent": "gosip/1.o",
},
Payload: sdp.New(rtpaddr, sdp.ULAWCodec, sdp.DTMFCodec).String(),
}
// Turn invite message into a packet and send via UDP socket.
var b bytes.Buffer
invite.Append(&b)
log.Printf(">>> %s\n%s\n", raddr, b.String())
if amt, err := conn.Write(b.Bytes()); err != nil || amt != b.Len() {
t.Fatal(err)
}
// Receive provisional 100 Trying.
conn.SetDeadline(time.Now().Add(time.Second))
memory := make([]byte, 2048)
amt, err := conn.Read(memory)
if err != nil {
t.Fatal("read 100 trying:", err)
}
log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt]))
msg, err := sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal("parse 100 trying", err)
}
if !msg.IsResponse || msg.Status != 100 || msg.Phrase != "Trying" {
t.Fatal("didn't get 100 trying :[")
}
// Receive 200 OK.
conn.SetDeadline(time.Now().Add(5 * time.Second))
amt, err = conn.Read(memory)
if err != nil {
t.Fatal("read 200 ok:", err)
}
log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt]))
msg, err = sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal("parse 200 ok:", err)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {
t.Fatal("wanted 200 ok but got:", msg.Status, msg.Phrase)
}
if msg.Payload == "" || msg.Headers["Content-Type"] != "application/sdp" {
t.Fatal("200 ok didn't have sdp payload")
}
// Figure out where they want us to send RTP.
rsdp, err := sdp.Parse(msg.Payload)
if err != nil {
t.Fatal("failed to parse sdp", err)
}
rrtpaddr := &net.UDPAddr{IP: net.ParseIP(rsdp.Addr), Port: int(rsdp.Audio.Port)}
// Acknowledge the 200 OK to answer the call.
var ack sip.Msg
ack.Request = invite.Request
ack.From = msg.From
ack.To = msg.To
ack.CallID = msg.CallID
ack.Method = "ACK"
ack.CSeq = msg.CSeq
ack.CSeqMethod = "ACK"
ack.Via = msg.Via
b.Reset()
ack.Append(&b)
if amt, err := conn.Write(b.Bytes()); err != nil || amt != b.Len() {
t.Fatal(err)
}
// Send RTP packets containing junk until we get an echo response.
quit := make(chan bool)
go func() {
frameout := make([]byte, rtp.HeaderSize+160)
rtpHeader := rtp.Header{
PT: sdp.ULAWCodec.PT,
Seq: 666,
TS: 0,
Ssrc: rand.Uint32(),
}
for n := 0; n < 160; n++ {
frameout[rtp.HeaderSize+n] = byte(n)
}
ticker := time.NewTicker(20 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
rtpHeader.Write(frameout)
rtpHeader.TS += 160
rtpHeader.Seq++
amt, err = rtpsock.WriteTo(frameout, rrtpaddr)
if err != nil {
t.Fatal("rtp write", err)
}
case <-quit:
return
}
}
}()
defer func() { quit <- true }()
// We're talking to an echo application so they should send us back exactly
// the same audio.
rtpsock.SetDeadline(time.Now().Add(5 * time.Second))
amt, _, err = rtpsock.ReadFrom(memory)
if err != nil {
t.Fatal("rtp read", err)
}
if amt != rtp.HeaderSize+160 {
t.Fatal("rtp recv amt != 12+160")
}
var rtpHeader rtp.Header
err = rtpHeader.Read(memory)
if err != nil {
t.Fatal(err)
}
for n := 0; n < 160; n++ {
if memory[rtp.HeaderSize+n] != byte(n) {
t.Fatal("rtp response audio didnt match")
}
}
// Hangup (we'll be lazy and just change up the ack Msg)
ack.Method = "BYE"
ack.CSeqMethod = "BYE"
ack.CSeq++
b.Reset()
ack.Append(&b)
amt, err = conn.Write(b.Bytes())
if err != nil || amt != b.Len() {
t.Fatal(err)
}
// Wait for acknowledgment of hangup.
conn.SetDeadline(time.Now().Add(time.Second))
amt, err = conn.Read(memory)
if err != nil {
t.Fatal(err)
}
msg, err = sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal(err)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {
t.Fatal("wanted bye response 200 ok but got:", msg.Status, msg.Phrase)
}
}

+ 177
- 0
example/echo2/echo2_test.go View File

@ -0,0 +1,177 @@
// Echo test that uses slightly higher level APIs.
package echo2_test
import (
"bytes"
"github.com/jart/gosip/rtp"
"github.com/jart/gosip/sdp"
"github.com/jart/gosip/sip"
"log"
"math/rand"
"net"
"testing"
"time"
)
func TestCallToEchoApp(t *testing.T) {
to := &sip.Addr{Uri: &sip.URI{Host: "127.0.0.1", Port: 5060}}
from := &sip.Addr{Uri: &sip.URI{Host: "127.0.0.1"}}
// Create the SIP UDP transport layer.
tp, err := sip.NewUDPTransport(from)
if err != nil {
t.Fatal(err)
}
// Create an RTP socket.
rtpsock, err := net.ListenPacket("udp", "108.61.60.146:0")
if err != nil {
t.Fatal("rtp listen:", err)
}
defer rtpsock.Close()
rtpaddr := rtpsock.LocalAddr().(*net.UDPAddr)
// Send an INVITE message with an SDP.
invite := sip.NewRequest(tp, "INVITE", to, from)
sip.AttachSDP(invite, sdp.New(rtpaddr, sdp.ULAWCodec, sdp.DTMFCodec))
err = tp.Send(invite)
if err != nil {
t.Fatal(err)
}
// Receive provisional 100 Trying.
conn.SetDeadline(time.Now().Add(time.Second))
memory := make([]byte, 2048)
amt, err := conn.Read(memory)
if err != nil {
t.Fatal("read 100 trying:", err)
}
log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt]))
msg, err := sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal("parse 100 trying", err)
}
if !msg.IsResponse || msg.Status != 100 || msg.Phrase != "Trying" {
t.Fatal("didn't get 100 trying :[")
}
// Receive 200 OK.
conn.SetDeadline(time.Now().Add(5 * time.Second))
amt, err = conn.Read(memory)
if err != nil {
t.Fatal("read 200 ok:", err)
}
log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt]))
msg, err = sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal("parse 200 ok:", err)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {
t.Fatal("wanted 200 ok but got:", msg.Status, msg.Phrase)
}
if msg.Payload == "" || msg.Headers["Content-Type"] != "application/sdp" {
t.Fatal("200 ok didn't have sdp payload")
}
// Figure out where they want us to send RTP.
rsdp, err := sdp.Parse(msg.Payload)
if err != nil {
t.Fatal("failed to parse sdp", err)
}
rrtpaddr := &net.UDPAddr{IP: net.ParseIP(rsdp.Addr), Port: int(rsdp.Audio.Port)}
// Acknowledge the 200 OK to answer the call.
var ack sip.Msg
ack.Request = invite.Request
ack.From = msg.From
ack.To = msg.To
ack.CallID = msg.CallID
ack.Method = "ACK"
ack.CSeq = msg.CSeq
ack.CSeqMethod = "ACK"
ack.Via = msg.Via
b.Reset()
ack.Append(&b)
if amt, err := conn.Write(b.Bytes()); err != nil || amt != b.Len() {
t.Fatal(err)
}
// Send RTP packets containing junk until we get an echo response.
quit := make(chan bool)
go func() {
frameout := make([]byte, rtp.HeaderSize+160)
rtpHeader := rtp.Header{
PT: sdp.ULAWCodec.PT,
Seq: 666,
TS: 0,
Ssrc: rand.Uint32(),
}
for n := 0; n < 160; n++ {
frameout[rtp.HeaderSize+n] = byte(n)
}
ticker := time.NewTicker(20 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
rtpHeader.Write(frameout)
rtpHeader.TS += 160
rtpHeader.Seq++
amt, err = rtpsock.WriteTo(frameout, rrtpaddr)
if err != nil {
t.Fatal("rtp write", err)
}
case <-quit:
return
}
}
}()
defer func() { quit <- true }()
// We're talking to an echo application so they should send us back exactly
// the same audio.
rtpsock.SetDeadline(time.Now().Add(5 * time.Second))
amt, _, err = rtpsock.ReadFrom(memory)
if err != nil {
t.Fatal("rtp read", err)
}
if amt != rtp.HeaderSize+160 {
t.Fatal("rtp recv amt != 12+160")
}
var rtpHeader rtp.Header
err = rtpHeader.Read(memory)
if err != nil {
t.Fatal(err)
}
for n := 0; n < 160; n++ {
if memory[rtp.HeaderSize+n] != byte(n) {
t.Fatal("rtp response audio didnt match")
}
}
// Hangup (we'll be lazy and just change up the ack Msg)
ack.Method = "BYE"
ack.CSeqMethod = "BYE"
ack.CSeq++
b.Reset()
ack.Append(&b)
amt, err = conn.Write(b.Bytes())
if err != nil || amt != b.Len() {
t.Fatal(err)
}
// Wait for acknowledgment of hangup.
conn.SetDeadline(time.Now().Add(time.Second))
amt, err = conn.Read(memory)
if err != nil {
t.Fatal(err)
}
msg, err = sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal(err)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {
t.Fatal("wanted bye response 200 ok but got:", msg.Status, msg.Phrase)
}
}

+ 108
- 0
example/options/options_test.go View File

@ -0,0 +1,108 @@
// Example demonstrating how to ping a server with an OPTIONS message.
package options_test
import (
"bytes"
"github.com/jart/gosip/sip"
"github.com/jart/gosip/util"
"net"
"reflect"
"testing"
"time"
)
func TestOptions(t *testing.T) {
sock, err := net.Dial("udp", "127.0.0.1:5060")
if err != nil {
t.Error(err)
return
}
defer sock.Close()
raddr := sock.RemoteAddr().(*net.UDPAddr)
laddr := sock.LocalAddr().(*net.UDPAddr)
options := sip.Msg{
CSeq: util.GenerateCSeq(),
CallID: util.GenerateCallID(),
Method: "OPTIONS",
CSeqMethod: "OPTIONS",
Request: &sip.URI{
Scheme: "sip",
User: "echo",
Host: raddr.IP.String(),
Port: uint16(raddr.Port),
},
Via: &sip.Via{
Version: "2.0",
Proto: "UDP",
Host: laddr.IP.String(),
Port: uint16(laddr.Port),
Params: sip.Params{"branch": util.GenerateBranch()},
},
Contact: &sip.Addr{
Uri: &sip.URI{
Host: laddr.IP.String(),
Port: uint16(laddr.Port),
},
},
From: &sip.Addr{
Uri: &sip.URI{
User: "gosip",
Host: "justinetunney.com",
Port: 5060,
},
Params: sip.Params{"tag": util.GenerateTag()},
},
To: &sip.Addr{
Uri: &sip.URI{
Host: raddr.IP.String(),
Port: uint16(raddr.Port),
},
},
Headers: map[string]string{
"Accept": "application/sdp",
"User-Agent": "pokémon/1.o",
},
}
var b bytes.Buffer
if err := options.Append(&b); err != nil {
t.Fatal(err)
}
if amt, err := sock.Write(b.Bytes()); err != nil || amt != b.Len() {
t.Fatal(err)
}
memory := make([]byte, 2048)
sock.SetDeadline(time.Now().Add(time.Second))
amt, err := sock.Read(memory)
if err != nil {
t.Fatal(err)
}
msg, err := sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal(err)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {
t.Error("Not OK :[")
}
if options.CallID != msg.CallID {
t.Error("CallID didnt match")
}
if options.CSeq != msg.CSeq || options.CSeqMethod != msg.CSeqMethod {
t.Error("CSeq didnt match")
}
if !reflect.DeepEqual(options.From, msg.From) {
t.Error("From headers didn't match:\n%#v\n%#v", options.From, msg.From)
}
if _, ok := msg.To.Params["tag"]; !ok {
t.Error("Remote UA didnt tag To header")
}
msg.To.Params = nil
if !reflect.DeepEqual(options.To, msg.To) {
t.Error("To mismatch:\n%#v\n%#v", options.To, msg.To)
}
}

+ 83
- 0
example/rawsip/rawsip_test.go View File

@ -0,0 +1,83 @@
// This code demonstrates how SIP works without the zillion layers of
// sugar coating.
package rawsip_test
import (
"github.com/jart/gosip/util"
"net"
"strconv"
"strings"
"testing"
"time"
)
// An 'OPTIONS' message is used to:
//
// - Ping a server to see if it's alive.
// - Keep a connection alive in nat situations.
// - Ask a user agent what features they support.
//
func TestRawSIPOptions(t *testing.T) {
// create a new udp socket bound to a random port and "connect"
// the socket to the remote address
raddr := "127.0.0.1:5060"
conn, err := net.Dial("udp", raddr)
if err != nil {
t.Error(err)
return
}
// What local ip/port binding did the kernel choose for us?
laddr := conn.LocalAddr().String()
// Construct a SIP message.
cseq := util.GenerateCSeq()
fromtag := util.GenerateTag()
callid := util.GenerateCallID()
packet := "" +
"OPTIONS sip:echo@" + laddr + " SIP/2.0\r\n" +
"Via: SIP/2.0/UDP " + laddr + "\r\n" +
"Max-Forwards: 70\r\n" +
"To: <sip:" + raddr + ">\r\n" +
"From: <sip:" + laddr + ">;tag=" + fromtag + "\r\n" +
"Call-ID: " + callid + "\r\n" +
"CSeq: " + strconv.Itoa(cseq) + " OPTIONS\r\n" +
"Contact: <sip:" + laddr + ">\r\n" +
"User-Agent: pokémon/1.o\r\n" +
"Accept: application/sdp\r\n" +
"Content-Length: 0\r\n" +
"\r\n"
// Transmit our message.
bpacket := []uint8(packet)
amt, err := conn.Write(bpacket)
if err != nil || amt != len(bpacket) {
t.Error(err)
return
}
// Wait no longer than a second for them to get back to us.
err = conn.SetDeadline(time.Now().Add(time.Second))
if err != nil {
t.Error(err)
return
}
// Receive response.
buf := make([]byte, 2048)
amt, err = conn.Read(buf)
if err != nil {
t.Error(err)
return
}
response := buf[0:amt]
// Read response.
msg := string(response)
lines := strings.Split(msg, "\r\n")
if lines[0] != "SIP/2.0 200 OK" {
t.Errorf("not ok :[\n%s", msg)
return
}
}

+ 93
- 0
rtp/dtmf.go View File

@ -0,0 +1,93 @@
// RFC2833 RTP DTMF Telephone Events
package rtp
import (
"errors"
"fmt"
)
// Turns telephone event into ASCII character.
func DtmfToChar(event uint8) (byte, error) {
switch event {
case 0:
return '0', nil
case 1:
return '1', nil
case 2:
return '2', nil
case 3:
return '3', nil
case 4:
return '4', nil
case 5:
return '5', nil
case 6:
return '6', nil
case 7:
return '7', nil
case 8:
return '8', nil
case 9:
return '9', nil
case 10:
return '*', nil
case 11:
return '#', nil
case 12:
return 'A', nil
case 13:
return 'B', nil
case 14:
return 'C', nil
case 15:
return 'D', nil
case 16:
return '!', nil
default:
return '\x00', errors.New(fmt.Sprintf("bad tel event: %v", event))
}
}
// Turns ascii character into corresponding telephone event.
func CharToDtmf(ch byte) (event uint8, err error) {
switch ch {
case '0':
event = 0
case '1':
event = 1
case '2':
event = 2
case '3':
event = 3
case '4':
event = 4
case '5':
event = 5
case '6':
event = 6
case '7':
event = 7
case '8':
event = 8
case '9':
event = 9
case '*':
event = 10
case '#':
event = 11
case 'a', 'A':
event = 12
case 'b', 'B':
event = 13
case 'c', 'C':
event = 14
case 'd', 'D':
event = 15
case '!':
event = 16
default:
err = errors.New(fmt.Sprint("bad dtmf char:", string(ch)))
}
return
}

+ 157
- 0
rtp/rtp.go View File

@ -0,0 +1,157 @@
package rtp
import (
"errors"
)
const (
HeaderSize = 12
EventHeaderSize = 4
Version byte = 2
bit1 byte = (1 << 0)
bit2 byte = (1 << 1)
bit3 byte = (1 << 2)
bit4 byte = (1 << 3)
bit5 byte = (1 << 4)
bit6 byte = (1 << 5)
bit7 byte = (1 << 6)
bit8 byte = (1 << 7)
mask1 byte = (1 << 1) - 1
mask2 byte = (1 << 2) - 1
mask3 byte = (1 << 3) - 1
mask4 byte = (1 << 4) - 1
mask5 byte = (1 << 5) - 1
mask6 byte = (1 << 6) - 1
mask7 byte = (1 << 7) - 1
mask8 byte = (1 << 8) - 1
)
var (
ErrBadVersion = errors.New("bad rtp version header")
ErrExtendedHeadersNotSupported = errors.New("rtp extended headers not supported")
)
// Header is encoded at the beginning of a UDP audio packet.
type Header struct {
Pad bool // the padding flag is used for secure rtp
Mark bool // the marker flag is used for rfc2833
PT uint8 // payload type you got from sdp
Seq uint16 // sequence id useful for reordering packets
TS uint32 // timestamp measured in samples
Ssrc uint32 // random id used to identify an rtp session
}
// EventHeader stores things like DTMF and is encoded after Header.
type EventHeader struct {
// The event field is a number between 0 and 255 identifying a specific
// telephony event. An IANA registry of event codes for this field has been
// established (see IANA Considerations, Section 7). The initial content of
// this registry consists of the events defined in Section 3.
Event uint8
// If set to a value of one, the "end" bit indicates that this packet
// contains the end of the event. For long-lasting events that have to be
// split into segments (see below, Section 2.5.1.3), only the final packet
// for the final segment will have the E bit set.
E bool
// This field is reserved for future use. The sender MUST set it to zero, and
// the receiver MUST ignore it.
R bool
// For DTMF digits and other events representable as tones, this field
// describes the power level of the tone, expressed in dBm0 after dropping
// the sign. Power levels range from 0 to -63 dBm0. Thus, larger values
// denote lower volume. This value is defined only for events for which the
// documentation indicates that volume is applicable. For other events, the
// sender MUST set volume to zero and the receiver MUST ignore the value.
Volume uint8
// The duration field indicates the duration of the event or segment being
// reported, in timestamp units, expressed as an unsigned integer in network
// byte order. For a non-zero value, the event or segment began at the
// instant identified by the RTP timestamp and has so far lasted as long as
// indicated by this parameter. The event may or may not have ended. If the
// event duration exceeds the maximum representable by the duration field,
// the event is split into several contiguous segments as described below
// (Section 2.5.1.3).
//
// The special duration value of zero is reserved to indicate that the event
// lasts "forever", i.e., is a state and is considered to be effective until
// updated. A sender MUST NOT transmit a zero duration for events other than
// those defined as states. The receiver SHOULD ignore an event report with
// zero duration if the event is not a state.
//
// Events defined as states MAY contain a non-zero duration, indicating that
// the sender intends to refresh the state before the time duration has
// elapsed ("soft state").
//
// For a sampling rate of 8000 Hz, the duration field is sufficient to
// express event durations of up to approximately 8 seconds.
Duration uint16
}
// Writes an rtp header to a buffer. You need 12 bytes.
func (h *Header) Write(b []byte) {
b[0] = (Version & mask2) << 6
if h.Pad {
b[0] |= (1 & mask1) << 5
}
// if extend { b[0] |= (1 & mask1) << 4 }
// b[0] |= (csrcCount & mask4) << 0
b[1] = (h.PT & mask7) << 0
if h.Mark {
b[1] |= (1 & mask1) << 7
}
b[2] = byte(h.Seq >> 8)
b[3] = byte(h.Seq)
b[4] = byte(h.TS >> 24)
b[5] = byte(h.TS >> 16)
b[6] = byte(h.TS >> 8)
b[7] = byte(h.TS)
b[8] = byte(h.Ssrc >> 24)
b[9] = byte(h.Ssrc >> 16)
b[10] = byte(h.Ssrc >> 8)
b[11] = byte(h.Ssrc)
}
// Reads header bits from buffer.
func (h *Header) Read(b []byte) error {
if b[0]>>6 != Version {
return ErrBadVersion
} else if (b[0]>>4)&1 != 0 {
return ErrExtendedHeadersNotSupported
}
h.Pad = ((b[0]>>5)&mask1 == 1)
h.Mark = ((b[1]>>7)&mask1 == 1)
h.PT = uint8(b[1] & mask7)
h.Seq = (uint16(b[2]) << 8) & uint16(b[3])
h.TS = (uint32(b[4]) << 24) & (uint32(b[5]) << 16) & (uint32(b[6]) << 8) & uint32(b[7])
h.Ssrc = (uint32(b[8]) << 24) & (uint32(b[9]) << 16) & (uint32(b[10]) << 8) & uint32(b[11])
return nil
}
// Writes header bits to buffer.
func (h *EventHeader) Write(b []byte) {
b[0] = h.Event
b[1] = h.Volume & 63
if h.R {
b[1] |= 1 << 6
}
if h.E {
b[1] |= 1 << 7
}
b[2] = byte(h.Duration >> 8)
b[3] = byte(h.Duration)
}
// Reads header bits from buffer.
func (h *EventHeader) Read(b []byte) {
h.Event = b[0]
h.E = b[1]>>7 == 1
h.R = b[1]>>6&1 == 1
h.Volume = b[1] & 63
h.Duration = uint16(b[2])<<8 | uint16(b[3])
}

+ 41
- 0
sdp/codec.go View File

@ -0,0 +1,41 @@
package sdp
import (
"bytes"
"errors"
"strconv"
)
// Codec describes one of the codec lines in an SDP. This data will be
// magically filled in if the rtpmap wasn't provided (assuming it's a well
// known codec having a payload type less than 96.)
type Codec struct {
PT uint8 // 7-bit payload type we need to put in our RTP packets
Name string // e.g. PCMU, G729, telephone-event, etc.
Rate int // frequency in hertz. usually 8000
Param string // sometimes used to specify number of channels
Fmtp string // some extra info; i.e. dtmf might set as "0-16"
}
func (codec *Codec) Append(b *bytes.Buffer) error {
if codec.Name == "" {
return errors.New("Codec.Name is blank")
}
if codec.Rate < 8000 {
return errors.New("Codec.Rate < 8000")
}
b.WriteString("a=rtpmap:")
b.WriteString(strconv.Itoa(int(codec.PT)) + " ")
b.WriteString(codec.Name + "/")
b.WriteString(strconv.Itoa(codec.Rate))
if codec.Param != "" {
b.WriteString("/" + codec.Param)
}
b.WriteString("\r\n")
if codec.Fmtp != "" {
b.WriteString("a=fmtp:")
b.WriteString(strconv.Itoa(int(codec.PT)) + " ")
b.WriteString(codec.Fmtp + "\r\n")
}
return nil
}

+ 62
- 0
sdp/codecs.go View File

@ -0,0 +1,62 @@
// IANA Assigned VoIP Codec Payload Types
//
// The following codecs have been standardized by IANA thereby
// allowing their 'a=rtpmap' information to be omitted in SDP
// messages. they've been hard coded to make your life easier.
//
// Many of these codecs (G711, G722, G726, GSM, LPC) have been
// implemented by Steve Underwood in his excellent SpanDSP library.
//
// Newer codecs like silk, broadvoice, speex, etc. use a dynamic
// payload type.
//
// Reference Material:
//
// - IANA Payload Types: http://www.iana.org/assignments/rtp-parameters
// - Explains well-known ITU codecs: http://tools.ietf.org/html/rfc3551
//
package sdp
var (
ULAWCodec = &Codec{PT: 0, Name: "PCMU", Rate: 8000}
DTMFCodec = &Codec{PT: 101, Name: "telephone-event", Rate: 8000, Fmtp: "0-16"}
StandardCodecs = map[uint8]*Codec{
// G.711 μ-Law is the de-facto codec (SpanDSP g711.h)
0: ULAWCodec,
// Uncool codec asterisk ppl like (SpanDSP gsm0610.h)
3: &Codec{PT: 3, Name: "GSM", Rate: 8000},
4: &Codec{PT: 4, Name: "G723", Rate: 8000},
// adaptive pulse code modulation (SpanDSP ima_adpcm.h)
5: &Codec{PT: 5, Name: "DVI4", Rate: 8000},
6: &Codec{PT: 6, Name: "DVI4", Rate: 16000},
// chat with your friends ww2 field marshall style (SpanDSP lpc10.h)
7: &Codec{PT: 7, Name: "LPC", Rate: 8000},
// G.711 variant of μ-Law used in yurop (SpanDSP g711.h)
8: &Codec{PT: 8, Name: "PCMA", Rate: 8000},
// used for Polycom HD Voice; rate actually 16khz lol (SpanDSP g722.h)
9: &Codec{PT: 9, Name: "G722", Rate: 8000},
// 16-bit signed PCM stereo/mono (mind your MTU; adjust ptime)
10: &Codec{PT: 10, Name: "L16", Rate: 44100, Param: "2"},
11: &Codec{PT: 11, Name: "L16", Rate: 44100},
12: &Codec{PT: 12, Name: "QCELP", Rate: 8000},
// RFC3389 comfort noise
13: &Codec{PT: 13, Name: "CN", Rate: 8000},
14: &Codec{PT: 14, Name: "MPA", Rate: 90000},
15: &Codec{PT: 15, Name: "G728", Rate: 8000},
16: &Codec{PT: 16, Name: "DVI4", Rate: 11025},
17: &Codec{PT: 17, Name: "DVI4", Rate: 22050},
// best voice codec (if you got $10 bucks)
18: &Codec{PT: 18, Name: "G729", Rate: 8000},
25: &Codec{PT: 25, Name: "CelB", Rate: 90000},
26: &Codec{PT: 26, Name: "JPEG", Rate: 90000},
28: &Codec{PT: 28, Name: "nv", Rate: 90000},
// RFC4587 video
31: &Codec{PT: 31, Name: "H261", Rate: 90000},
32: &Codec{PT: 32, Name: "MPV", Rate: 90000},
33: &Codec{PT: 33, Name: "MP2T", Rate: 90000},
// $$$ video
34: &Codec{PT: 34, Name: "H263", Rate: 90000},
}
)

+ 45
- 0
sdp/media.go View File

@ -0,0 +1,45 @@
package sdp
import (
"bytes"
"errors"
"strconv"
)
// Media is a high level representation of the c=/m=/a= lines for describing a
// specific type of media. Only "audio" and "video" are supported at this time.
type Media struct {
Type string // "audio" or "video"
Proto string // RTP, SRTP, UDP, UDPTL, TCP, TLS, etc.
Port int // port number (0 - 2^16-1)
Codecs []Codec // never nil with at least one codec
}
func (media *Media) Append(b *bytes.Buffer) error {
if media.Type != "audio" && media.Type != "video" {
return errors.New("Media.Type not audio/video: " + media.Type)
}
if media.Codecs == nil || len(media.Codecs) == 0 {
return errors.New("Media.Codecs not set")
}
if media.Port == 0 {
return errors.New("Media.Port not set")
}
if media.Proto == "" {
media.Proto = "RTP/AVP"
}
b.WriteString("m=")
b.WriteString(media.Type + " ")
b.WriteString(strconv.Itoa(int(media.Port)) + " ")
b.WriteString(media.Proto)
for _, codec := range media.Codecs {
b.WriteString(" " + strconv.Itoa(int(codec.PT)))
}
b.WriteString("\r\n")
for _, codec := range media.Codecs {
if err := codec.Append(b); err != nil {
return err
}
}
return nil
}

+ 41
- 0
sdp/origin.go View File

@ -0,0 +1,41 @@
package sdp
import (
"bytes"
"errors"
"github.com/jart/gosip/util"
)
// Origin represents the session origin (o=) line of an SDP. Who knows what
// this is supposed to do.
type Origin struct {
User string // first value in o= line
ID string // second value in o= line
Version string // third value in o= line
Addr string // tracks ip of original user-agent
}
func (origin *Origin) Append(b *bytes.Buffer) error {
if origin.ID == "" {
return errors.New("sdp missing origin id")
}
if origin.Version == "" {
return errors.New("sdp missing origin version")
}
if origin.Addr == "" {
return errors.New("sdp missing origin address")
}
if origin.User == "" {
origin.User = "-"
}
b.WriteString("o=")
b.WriteString(origin.User + " ")
b.WriteString(origin.ID + " ")
b.WriteString(origin.Version)
if util.IsIPv6(origin.Addr) {
b.WriteString(" IN IP6 " + origin.Addr + "\r\n")
} else {
b.WriteString(" IN IP4 " + origin.Addr + "\r\n")
}
return nil
}

+ 456
- 0
sdp/sdp.go View File

@ -0,0 +1,456 @@
// Session Description Protocol Library
//
// This is the stuff people embed in SIP packets that tells you how to
// establish audio and/or video sessions.
//
// Here's a typical SDP for a phone call sent by Asterisk:
//
// v=0
// o=root 31589 31589 IN IP4 10.0.0.38
// s=session
// c=IN IP4 10.0.0.38 <-- ip we should connect to
// t=0 0
// m=audio 30126 RTP/AVP 0 101 <-- audio port number and codecs
// a=rtpmap:0 PCMU/8000 <-- use μ-Law codec at 8000 hz
// a=rtpmap:101 telephone-event/8000 <-- they support rfc2833 dtmf tones
// a=fmtp:101 0-16
// a=silenceSupp:off - - - - <-- they'll freak out if you use VAD
// a=ptime:20 <-- send packet every 20 milliseconds
// a=sendrecv <-- they wanna send and receive audio
//
// Here's an SDP response from MetaSwitch, meaning the exact same
// thing as above, but omitting fields we're smart enough to assume:
//
// v=0
// o=- 3366701332 3366701332 IN IP4 1.2.3.4
// s=-
// c=IN IP4 1.2.3.4
// t=0 0
// m=audio 32898 RTP/AVP 0 101
// a=rtpmap:101 telephone-event/8000
// a=ptime:20
//
// If you wanted to go where no woman or man has gone before in the
// voip world, like stream 44.1khz stereo MP3 audio over a IPv6 TCP
// socket for a Flash player to connect to, you could do something
// like:
//
// v=0
// o=- 3366701332 3366701332 IN IP6 dead:beef::666
// s=-
// c=IN IP6 dead:beef::666
// t=0 0
// m=audio 80 TCP/IP 111
// a=rtpmap:111 MP3/44100/2
// a=sendonly
//
// Reference Material:
//
// - SDP RFC: http://tools.ietf.org/html/rfc4566
// - SIP/SDP Handshake RFC: http://tools.ietf.org/html/rfc3264
//
package sdp
import (
"bytes"
"errors"
"github.com/jart/gosip/util"
"log"
"math/rand"
"net"
"strconv"
"strings"
)
const (
MaxLength = 1450
)
// SDP represents a Session Description Protocol SIP payload.
type SDP struct {
SendOnly bool // true if 'a=sendonly' was specified in sdp
RecvOnly bool // true if 'a=recvonly' was specified in sdp
Ptime int // transmit every X millisecs (0 means not set)
Addr string // connect to this ip; never blank (from c=)
Audio *Media // non nil if we can establish audio
Video *Media // non nil if we can establish video
Origin Origin // this must always be present
Session string // s= Session Name (defaults to "-")
Time string // t= Active Time (defaults to "0 0")
Attrs [][2]string // a= lines we don't recognize (never nil)
}
// Easy way to create a basic, everyday SDP for VoIP.
func New(addr *net.UDPAddr, codecs ...*Codec) *SDP {
sdp := new(SDP)
sdp.Addr = addr.IP.String()
sdp.Origin.ID = strconv.FormatInt(int64(rand.Uint32()), 10)
sdp.Origin.Version = sdp.Origin.ID
sdp.Origin.Addr = sdp.Addr
sdp.Audio = new(Media)
sdp.Audio.Type = "audio"
sdp.Audio.Proto = "RTP/AVP"
sdp.Audio.Port = addr.Port
sdp.Audio.Codecs = make([]Codec, len(codecs))
for i := 0; i < len(codecs); i++ {
sdp.Audio.Codecs[i] = *codecs[i]
}
sdp.Attrs = make([][2]string, 0, 8)
return sdp
}
// parses sdp message text into a happy data structure
func Parse(s string) (sdp *SDP, err error) {
sdp = new(SDP)
sdp.Session = "pokémon"
sdp.Time = "0 0"
// Eat version.
if !strings.HasPrefix(s, "v=0\r\n") {
return nil, errors.New("sdp must start with v=0\\r\\n")
}
s = s[5:]
// Turn into lines.
lines := strings.Split(s, "\r\n")
if lines == nil || len(lines) < 2 {
return nil, errors.New("too few lines in sdp")
}
// We abstract the structure of the media lines so we need a place to store
// them before assembling the audio/video data structures.
var audioinfo, videoinfo string
rtpmaps := make([]string, len(lines))
rtpmapcnt := 0
fmtps := make([]string, len(lines))
fmtpcnt := 0
sdp.Attrs = make([][2]string, 0, len(lines))
// Extract information from SDP.
var okOrigin, okConn bool
for _, line := range lines {
switch {
case line == "":
continue
case len(line) < 3 || line[1] != '=': // empty or invalid line
log.Println("Bad line in SDP:", line)
continue
case line[0] == 'm': // media line
line = line[2:]
if strings.HasPrefix(line, "audio ") {
audioinfo = line[6:]
} else if strings.HasPrefix(line, "video ") {
videoinfo = line[6:]
} else {
log.Println("Unsupported SDP media line:", line)
}
case line[0] == 's': // session line
sdp.Session = line[2:]
case line[0] == 't': // active time
sdp.Time = line[2:]
case line[0] == 'c': // connect to this ip address
if okConn {
log.Println("Dropping extra c= line in sdp:", line)
continue
}
sdp.Addr, err = parseConnLine(line)
if err != nil {
return nil, err
}
okConn = true
case line[0] == 'o': // origin line
err = parseOriginLine(&sdp.Origin, line)
if err != nil {
return nil, err
}
okOrigin = true
case line[0] == 'a': // attribute lines
line = line[2:]
switch {
case strings.HasPrefix(line, "rtpmap:"):
rtpmaps[rtpmapcnt] = line[7:]
rtpmapcnt++
case strings.HasPrefix(line, "fmtp:"):
fmtps[fmtpcnt] = line[5:]
fmtpcnt++
case strings.HasPrefix(line, "ptime:"):
ptimeS := line[6:]
if ptime, err := strconv.Atoi(ptimeS); err == nil && ptime > 0 {
sdp.Ptime = ptime
} else {
log.Println("Invalid SDP Ptime value", ptimeS)
}
case line == "sendrecv":
case line == "sendonly":
sdp.SendOnly = true
case line == "recvonly":
sdp.RecvOnly = true
default:
if n := strings.Index(line, ":"); n >= 0 {
if n == 0 {
log.Println("Evil SDP attribute:", line)
} else {
l := len(sdp.Attrs)
sdp.Attrs = sdp.Attrs[0 : l+1]
sdp.Attrs[l] = [2]string{line[0:n], line[n+1:]}
}
} else {
l := len(sdp.Attrs)
sdp.Attrs = sdp.Attrs[0 : l+1]
sdp.Attrs[l] = [2]string{line, ""}
}
}
}
}
rtpmaps = rtpmaps[0:rtpmapcnt]
fmtps = fmtps[0:fmtpcnt]
if !okConn || !okOrigin {
return nil, errors.New("sdp missing mandatory information")
}
// Assemble audio/video information.
var pts []uint8
if audioinfo != "" {
sdp.Audio = new(Media)
sdp.Audio.Type = "audio"
sdp.Audio.Port, sdp.Audio.Proto, pts, err = parseMediaInfo(audioinfo)
if err != nil {
return nil, err
}
err = populateCodecs(sdp.Audio, pts, rtpmaps, fmtps)
if err != nil {
return nil, err
}
} else {
sdp.Video = nil
}
if videoinfo != "" {
sdp.Video = new(Media)
sdp.Video.Type = "video"
sdp.Video.Port, sdp.Video.Proto, pts, err = parseMediaInfo(videoinfo)
if err != nil {
return nil, err
}
err = populateCodecs(sdp.Video, pts, rtpmaps, fmtps)
if err != nil {
return nil, err
}
} else {
sdp.Video = nil
}
if sdp.Audio == nil && sdp.Video == nil {
return nil, errors.New("sdp has no audio or video information")
}
return sdp, nil
}
func (sdp *SDP) String() string {
if sdp == nil {
return ""
}
var b bytes.Buffer
if err := sdp.Append(&b); err != nil {
log.Println("Bad SDP!", err)
return ""
}
return b.String()
}
func (sdp *SDP) Append(b *bytes.Buffer) error {
if sdp.Audio == nil && sdp.Video == nil {
return errors.New("sdp lonely no media :[")
}
if sdp.Session == "" {
sdp.Session = "pokémon"
}
if sdp.Time == "" {
sdp.Time = "0 0"
}
b.WriteString("v=0\r\n")
if err := sdp.Origin.Append(b); err != nil {
return err
}
b.WriteString("s=" + sdp.Session + "\r\n")
if util.IsIPv6(sdp.Addr) {
b.WriteString("c=IN IP6 " + sdp.Addr + "\r\n")
} else {
b.WriteString("c=IN IP4 " + sdp.Addr + "\r\n")
}
b.WriteString("t=" + sdp.Time + "\r\n")
if sdp.Audio != nil {
if err := sdp.Audio.Append(b); err != nil {
return err
}
}
if sdp.Video != nil {
if err := sdp.Video.Append(b); err != nil {
return err
}
}
if sdp.Attrs != nil {
for _, attr := range sdp.Attrs {
if attr[0] == "" {
return errors.New("SDP.Attrs key empty!")
}
if attr[1] == "" {
b.WriteString("a=" + attr[0] + "\r\n")
} else {
b.WriteString("a=" + attr[0] + ":" + attr[1] + "\r\n")
}
}
}
if sdp.Ptime > 0 {
b.WriteString("a=ptime:" + strconv.Itoa(sdp.Ptime) + "\r\n")
}
if sdp.SendOnly {
b.WriteString("a=sendonly\r\n")
} else if sdp.RecvOnly {
b.WriteString("a=recvonly\r\n")
} else {
b.WriteString("a=sendrecv\r\n")
}
return nil
}
// Here we take the list of payload types from the m= line (e.g. 9 18 0 101)
// and turn them into a list of codecs.
//
// If we couldn't find a matching rtpmap, iana standardized will be filled in
// like magic.
func populateCodecs(media *Media, pts []uint8, rtpmaps, fmtps []string) (err error) {
media.Codecs = make([]Codec, len(pts))
for n, pt := range pts {
codec := &media.Codecs[n]
codec.PT = pt
prefix := strconv.FormatInt(int64(pt), 10) + " "
for _, rtpmap := range rtpmaps {
if strings.HasPrefix(rtpmap, prefix) {
err = parseRtpmapInfo(codec, rtpmap[len(prefix):])
if err != nil {
return err
}
break
}
}
if codec.Name == "" {
if isDynamicPT(pt) {
return errors.New("dynamic codec missing rtpmap")
} else {
if v, ok := StandardCodecs[pt]; ok {
*codec = *v
} else {
return errors.New("unknown iana codec id: " +
strconv.Itoa(int(pt)))
}
}
}
for _, fmtp := range fmtps {
if strings.HasPrefix(fmtp, prefix) {
codec.Fmtp = fmtp[len(prefix):]
break
}
}
}
return nil
}
// Returns true if IANA says this payload type is dynamic.
func isDynamicPT(pt uint8) bool {
return (pt >= 96)
}
// Give me the part of the a=rtpmap line that looks like: "PCMU/8000" or
// "L16/16000/2".
func parseRtpmapInfo(codec *Codec, s string) (err error) {
toks := strings.Split(s, "/")
if toks != nil && len(toks) >= 2 {
codec.Name = toks[0]
codec.Rate, err = strconv.Atoi(toks[1])
if err != nil {
return errors.New("invalid rtpmap rate")
}
if len(toks) >= 3 {
codec.Param = toks[2]
}
} else {
return errors.New("invalid rtpmap")
}
return nil
}
// Give me the part of an "m=" line that looks like: "30126 RTP/AVP 0 101".
func parseMediaInfo(s string) (port int, proto string, pts []uint8, err error) {
toks := strings.Split(s, " ")
if toks == nil || len(toks) < 3 {
return 0, "", nil, errors.New("invalid m= line")
}
// We don't care if they say like "666/2" which is a weird way of saying hey!
// send ME rtcp too (I think).
portS := toks[0]
if n := strings.Index(portS, "/"); n > 0 {
portS = portS[0:n]
}
// Convert port to int and check range.
port, err = strconv.Atoi(portS)
if err != nil || !(0 <= port && port <= 65535) {
return 0, "", nil, errors.New("invalid m= port")
}
// Is it rtp? srtp? udp? tcp? etc. (must be 3+ chars)
proto = toks[1]
// The rest of these tokens are payload types sorted by preference.
pts = make([]uint8, len(toks)-2)
for n, pt := range toks[2:] {
pt, err := strconv.ParseUint(pt, 10, 8)
if err != nil {
return 0, "", nil, errors.New("invalid pt in m= line")
}
pts[n] = byte(pt)
}
return port, proto, pts, nil
}
// I want a string that looks like "c=IN IP4 10.0.0.38".
func parseConnLine(line string) (addr string, err error) {
toks := strings.Split(line[2:], " ")
if toks == nil || len(toks) != 3 {
return "", errors.New("invalid conn line")
}
if toks[0] != "IN" || (toks[1] != "IP4" && toks[1] != "IP6") {
return "", errors.New("unsupported conn net type")
}
addr = toks[2]
if n := strings.Index(addr, "/"); n >= 0 {
return "", errors.New("multicast address in c= line D:")
}
return addr, nil
}
// I want a string that looks like "o=root 31589 31589 IN IP4 10.0.0.38".
func parseOriginLine(origin *Origin, line string) error {
toks := strings.Split(line[2:], " ")
if toks == nil || len(toks) != 6 {
return errors.New("invalid origin line")
}
if toks[3] != "IN" || (toks[4] != "IP4" && toks[4] != "IP6") {
return errors.New("unsupported origin net type")
}
origin.User = toks[0]
origin.ID = toks[1]
origin.Version = toks[2]
origin.Addr = toks[5]
if n := strings.Index(origin.Addr, "/"); n >= 0 {
return errors.New("multicast address in o= line D:")
}
return nil
}

+ 457
- 0
sdp/sdp_test.go View File

@ -0,0 +1,457 @@
package sdp_test
import (
"fmt"
"github.com/jart/gosip/sdp"
"testing"
)
type sdpTest struct {
name string // arbitrary name for test
s string // raw sdp input to parse
s2 string // non-blank if sdp looks different when we format it
sdp *sdp.SDP // memory structure of 's' after parsing
err error
}
var sdpTests = []sdpTest{
sdpTest{
name: "Asterisk PCMU+DTMF",
s: ("v=0\r\n" +
"o=root 31589 31589 IN IP4 10.0.0.38\r\n" +
"s=session\r\n" +
"c=IN IP4 10.0.0.38\r\n" +
"t=0 0\r\n" +
"m=audio 30126 RTP/AVP 0 101\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:101 telephone-event/8000\r\n" +
"a=fmtp:101 0-16\r\n" +
"a=silenceSupp:off - - - -\r\n" +
"a=ptime:20\r\n" +
"a=sendrecv\r\n"),
sdp: &sdp.SDP{
Origin: sdp.Origin{
User: "root",
ID: "31589",
Version: "31589",
Addr: "10.0.0.38",
},
Session: "session",
Time: "0 0",
Addr: "10.0.0.38",
Audio: &sdp.Media{
Type: "audio",
Proto: "RTP/AVP",
Port: 30126,
Codecs: []sdp.Codec{
sdp.Codec{PT: 0, Name: "PCMU", Rate: 8000},
sdp.Codec{PT: 101, Name: "telephone-event", Rate: 8000, Fmtp: "0-16"},
},
},
Attrs: [][2]string{
[2]string{"silenceSupp", "off - - - -"},
},
Ptime: 20,
},
},
sdpTest{
name: "Audio+Video+Implicit+Fmtp",
s: "v=0\r\n" +
"o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" +
"c=IN IP4 1.2.3.4\r\n" +
"m=audio 32898 RTP/AVP 18\r\n" +
"m=video 32900 RTP/AVP 34\r\n" +
"a=fmtp:18 annexb=yes",
s2: "v=0\r\n" +
"o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" +
"s=pokémon\r\n" +
"c=IN IP4 1.2.3.4\r\n" +
"t=0 0\r\n" +
"m=audio 32898 RTP/AVP 18\r\n" +
"a=rtpmap:18 G729/8000\r\n" +
"a=fmtp:18 annexb=yes\r\n" +
"m=video 32900 RTP/AVP 34\r\n" +
"a=rtpmap:34 H263/90000\r\n" +
"a=sendrecv\r\n",
sdp: &sdp.SDP{
Origin: sdp.Origin{
User: "-",
ID: "3366701332",
Version: "3366701332",
Addr: "1.2.3.4",
},
Addr: "1.2.3.4",
Session: "pokémon",
Time: "0 0",
Audio: &sdp.Media{
Type: "audio",
Proto: "RTP/AVP",
Port: 32898,
Codecs: []sdp.Codec{
sdp.Codec{PT: 18, Name: "G729", Rate: 8000, Fmtp: "annexb=yes"},
},
},
Video: &sdp.Media{
Type: "video",
Proto: "RTP/AVP",
Port: 32900,
Codecs: []sdp.Codec{
sdp.Codec{PT: 34, Name: "H263", Rate: 90000},
},
},
Attrs: [][2]string{},
},
},
sdpTest{
name: "Implicit Codecs",
s: "v=0\r\n" +
"o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" +
"s=-\r\n" +
"c=IN IP4 1.2.3.4\r\n" +
"t=0 0\r\n" +
"m=audio 32898 RTP/AVP 9 18 0 101\r\n" +
"a=rtpmap:101 telephone-event/8000\r\n" +
"a=ptime:20\r\n",
s2: "v=0\r\n" +
"o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" +
"s=-\r\n" +
"c=IN IP4 1.2.3.4\r\n" +
"t=0 0\r\n" +
"m=audio 32898 RTP/AVP 9 18 0 101\r\n" +
"a=rtpmap:9 G722/8000\r\n" +
"a=rtpmap:18 G729/8000\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:101 telephone-event/8000\r\n" +
"a=ptime:20\r\n" +
"a=sendrecv\r\n",
sdp: &sdp.SDP{
Origin: sdp.Origin{
User: "-",
ID: "3366701332",
Version: "3366701332",
Addr: "1.2.3.4",
},
Session: "-",
Time: "0 0",
Addr: "1.2.3.4",
Audio: &sdp.Media{
Type: "audio",
Proto: "RTP/AVP",
Port: 32898,
Codecs: []sdp.Codec{
sdp.Codec{PT: 9, Name: "G722", Rate: 8000},
sdp.Codec{PT: 18, Name: "G729", Rate: 8000},
sdp.Codec{PT: 0, Name: "PCMU", Rate: 8000},
sdp.Codec{PT: 101, Name: "telephone-event", Rate: 8000},
},
},
Ptime: 20,
Attrs: [][2]string{},
},
},
sdpTest{
name: "IPv6",
s: "v=0\r\n" +
"o=- 3366701332 3366701332 IN IP6 dead:beef::666\r\n" +
"s=-\r\n" +
"c=IN IP6 dead:beef::666\r\n" +
"t=0 0\r\n" +
"m=audio 32898 RTP/AVP 9 18 0 101\r\n" +
"a=rtpmap:101 telephone-event/8000\r\n" +
"a=ptime:20\r\n",
s2: "v=0\r\n" +
"o=- 3366701332 3366701332 IN IP6 dead:beef::666\r\n" +
"s=-\r\n" +
"c=IN IP6 dead:beef::666\r\n" +
"t=0 0\r\n" +
"m=audio 32898 RTP/AVP 9 18 0 101\r\n" +
"a=rtpmap:9 G722/8000\r\n" +
"a=rtpmap:18 G729/8000\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:101 telephone-event/8000\r\n" +
"a=ptime:20\r\n" +
"a=sendrecv\r\n",
sdp: &sdp.SDP{
Origin: sdp.Origin{
User: "-",
ID: "3366701332",
Version: "3366701332",
Addr: "dead:beef::666",
},
Session: "-",
Time: "0 0",
Addr: "dead:beef::666",
Audio: &sdp.Media{
Type: "audio",
Proto: "RTP/AVP",
Port: 32898,
Codecs: []sdp.Codec{
sdp.Codec{PT: 9, Name: "G722", Rate: 8000},
sdp.Codec{PT: 18, Name: "G729", Rate: 8000},
sdp.Codec{PT: 0, Name: "PCMU", Rate: 8000},
sdp.Codec{PT: 101, Name: "telephone-event", Rate: 8000},
},
},
Ptime: 20,
Attrs: [][2]string{},
},
},
sdpTest{
name: "pjmedia long sdp is long",
s: ("v=0\r\n" +
"o=- 3457169218 3457169218 IN IP4 10.11.34.37\r\n" +
"s=pjmedia\r\n" +
"c=IN IP4 10.11.34.37\r\n" +
"t=0 0\r\n" +
"m=audio 4000 RTP/AVP 103 102 104 113 3 0 8 9 101\r\n" +
"a=rtpmap:103 speex/16000\r\n" +
"a=rtpmap:102 speex/8000\r\n" +
"a=rtpmap:104 speex/32000\r\n" +
"a=rtpmap:113 iLBC/8000\r\n" +
"a=fmtp:113 mode=30\r\n" +
"a=rtpmap:3 GSM/8000\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:8 PCMA/8000\r\n" +
"a=rtpmap:9 G722/8000\r\n" +
"a=rtpmap:101 telephone-event/8000\r\n" +
"a=fmtp:101 0-15\r\n" +
"a=rtcp:4001 IN IP4 10.11.34.37\r\n" +
"a=X-nat:0\r\n" +
"a=ptime:20\r\n" +
"a=sendrecv\r\n"),
sdp: &sdp.SDP{
Origin: sdp.Origin{
User: "-",
ID: "3457169218",
Version: "3457169218",
Addr: "10.11.34.37",
},
Session: "pjmedia",
Time: "0 0",
Addr: "10.11.34.37",
Audio: &sdp.Media{
Type: "audio",
Proto: "RTP/AVP",
Port: 4000,
Codecs: []sdp.Codec{
sdp.Codec{PT: 103, Name: "speex", Rate: 16000},
sdp.Codec{PT: 102, Name: "speex", Rate: 8000},
sdp.Codec{PT: 104, Name: "speex", Rate: 32000},
sdp.Codec{PT: 113, Name: "iLBC", Rate: 8000, Fmtp: "mode=30"},
sdp.Codec{PT: 3, Name: "GSM", Rate: 8000},
sdp.Codec{PT: 0, Name: "PCMU", Rate: 8000},
sdp.Codec{PT: 8, Name: "PCMA", Rate: 8000},
sdp.Codec{PT: 9, Name: "G722", Rate: 8000},
sdp.Codec{PT: 101, Name: "telephone-event", Rate: 8000, Fmtp: "0-15"},
},
},
Ptime: 20,
Attrs: [][2]string{
[2]string{"rtcp", "4001 IN IP4 10.11.34.37"},
[2]string{"X-nat", "0"},
},
},
},
sdpTest{
name: "mp3 tcp",
s: ("v=0\r\n" +
"o=- 3366701332 3366701334 IN IP4 10.11.34.37\r\n" +
"s=squigglies\r\n" +
"c=IN IP6 dead:beef::666\r\n" +
"t=0 0\r\n" +
"m=audio 80 TCP/IP 111\r\n" +
"a=rtpmap:111 MP3/44100/2\r\n" +
"a=sendonly\r\n"),
sdp: &sdp.SDP{
Origin: sdp.Origin{
User: "-",
ID: "3366701332",
Version: "3366701334",
Addr: "10.11.34.37",
},
Session: "squigglies",
Time: "0 0",
Addr: "dead:beef::666",
SendOnly: true,
Audio: &sdp.Media{
Type: "audio",
Proto: "TCP/IP",
Port: 80,
Codecs: []sdp.Codec{
sdp.Codec{PT: 111, Name: "MP3", Rate: 44100, Param: "2"},
},
},
Attrs: [][2]string{},
},
},
}
func sdpCompareCodec(t *testing.T, name string, correct, codec *sdp.Codec) {
if correct != nil && codec == nil {
t.Error(name, "not found")
}
if correct == nil && codec != nil {
t.Error(name, "DO NOT WANT", codec)
}
if codec == nil {
return
}
if correct.PT != codec.PT {
t.Error(name, "PT", correct.PT, "!=", codec.PT)
}
if correct.Name != codec.Name {
t.Error(name, "Name", correct.Name, "!=", codec.Name)
}
if correct.Rate != codec.Rate {
t.Error(name, "Rate", correct.Rate, "!=", codec.Rate)
}
if correct.Param != codec.Param {
t.Error(name, "Param", correct.Param, "!=", codec.Param)
}
if correct.Fmtp != codec.Fmtp {
t.Error(name, "Fmtp", correct.Fmtp, "!=", codec.Fmtp)
}
}
func sdpCompareCodecs(t *testing.T, name string, corrects, codecs []sdp.Codec) {
if corrects != nil && codecs == nil {
t.Error(name, "codecs not found")
}
if corrects == nil && codecs != nil {
t.Error(name, "codecs WUT", codecs)
}
if corrects == nil || codecs == nil {
return
}
if len(corrects) != len(codecs) {
t.Error(name, "len(Codecs)", len(corrects), "!=", len(codecs))
} else {
for i, _ := range corrects {
c1, c2 := &corrects[i], &codecs[i]
if c1 == nil || c2 == nil {
t.Error(name, "where my codecs at?")
} else {
sdpCompareCodec(t, name, c1, c2)
}
}
}
}
func sdpCompareMedia(t *testing.T, name string, correct, media *sdp.Media) {
if correct != nil && media == nil {
t.Error(name, "not found")
}
if correct == nil && media != nil {
t.Error(name, "DO NOT WANT", media)
}
if correct == nil || media == nil {
return
}
if correct.Type != media.Type {
t.Error(name, "Type", correct.Type, "!=", media.Type)
}
if correct.Proto != media.Proto {
t.Error(name, "Proto", correct.Proto, "!=", media.Proto)
}
if correct.Port != media.Port {
t.Error(name, "Port", correct.Port, "!=", media.Port)
}
if media.Codecs == nil || len(media.Codecs) < 1 {
t.Error(name, "Must have at least one codec")
}
sdpCompareCodecs(t, name, correct.Codecs, media.Codecs)
}
func TestParseSDP(t *testing.T) {
for _, test := range sdpTests {
sdp, err := sdp.ParseSDP(test.s)
if err != nil {
if test.err == nil {
t.Errorf("%v", err)
continue
} else { // test was supposed to fail
panic("todo")
}
}
if test.sdp.Addr != sdp.Addr {
t.Error(test.name, "Addr", test.sdp.Addr, "!=", sdp.Addr)
}
if test.sdp.Origin.User != sdp.Origin.User {
t.Error(test.name, "Origin.User", test.sdp.Origin.User, "!=",
sdp.Origin.User)
}
if test.sdp.Origin.ID != sdp.Origin.ID {
t.Error(test.name, "Origin.ID doesn't match")
}
if test.sdp.Origin.Version != sdp.Origin.Version {
t.Error(test.name, "Origin.Version doesn't match")
}
if test.sdp.Origin.Addr != sdp.Origin.Addr {
t.Error(test.name, "Origin.Addr doesn't match")
}
if test.sdp.Session != sdp.Session {
t.Error(test.name, "Session", test.sdp.Session, "!=", sdp.Session)
}
if test.sdp.Time != sdp.Time {
t.Error(test.name, "Time", test.sdp.Time, "!=", sdp.Time)
}
if test.sdp.Ptime != sdp.Ptime {
t.Error(test.name, "Ptime", test.sdp.Ptime, "!=", sdp.Ptime)
}
if test.sdp.RecvOnly != sdp.RecvOnly {
t.Error(test.name, "RecvOnly doesn't match")
}
if test.sdp.SendOnly != sdp.SendOnly {
t.Error(test.name, "SendOnly doesn't match")
}
if test.sdp.Attrs != nil {
if sdp.Attrs == nil {
t.Error(test.name, "Attrs weren't extracted")
} else if len(sdp.Attrs) != len(test.sdp.Attrs) {
t.Error(test.name, "Attrs length not same")
} else {
for i, _ := range sdp.Attrs {
p1, p2 := test.sdp.Attrs[i], sdp.Attrs[i]
if p1[0] != p2[0] || p1[1] != p2[1] {
t.Error(test.name, "attr", p1, "!=", p2)
break
}
}
}
} else {
if sdp.Attrs != nil {
t.Error(test.name, "unexpected attrs", sdp.Attrs)
}
}
sdpCompareMedia(t, "Audio", test.sdp.Audio, sdp.Audio)
sdpCompareMedia(t, "Video", test.sdp.Video, sdp.Video)
}
}
func TestFormatSDP(t *testing.T) {
for _, test := range sdpTests {
sdp := test.sdp.String()
s := test.s
if test.s2 != "" {
s = test.s2
}
if s != sdp {
t.Error("\n" + test.name + "\n\n" + s + "\nIS NOT\n\n" + sdp)
fmt.Printf("%s", sdp)
fmt.Printf("pokémon")
}
}
}

+ 191
- 0
sip/addr.go View File

@ -0,0 +1,191 @@
// SIP Address Library
//
// For example:
//
// "J.A. Roberts Tunney" <sip:jtunney@bsdtelecom.net;isup-oli=29>;tag=deadbeef
//
// Roughly equates to:
//
// {Display: "J.A. Roberts Tunney",
// Params: {"tag": "deadbeef"},
// Uri: {Scheme: "sip",
// User: "jtunney",
// Pass: "",
// Host: "bsdtelecom.net",
// Port: "",
// Params: {"isup-oli": "29"}}}
//
package sip
import (
"bytes"
"errors"
"github.com/jart/gosip/util"
"log"
"strings"
)
// Represents a SIP Address Linked List
type Addr struct {
Uri *URI // never nil
Display string // blank if not specified
Params Params // these look like ;key=lol;rport;key=wut
Next *Addr // for comma separated lists of addresses
}
// Parses a SIP address.
func ParseAddr(s string) (addr *Addr, err error) {
addr = new(Addr)
l := len(s)
if l == 0 {
return nil, errors.New("empty addr")
}
// Extract display.
switch n := strings.IndexAny(s, "\"<"); {
case n < 0:
return nil, errors.New("invalid address")
case s[n] == '<': // Display is not quoted.
addr.Display, s = strings.Trim(s[0:n], " "), s[n+1:]
case s[n] == '"': // We found an opening quote.
s = s[n+1:]
LOL:
for s != "" {
switch s[0] {
case '"': // Closing quote.
s = s[1:]
break LOL
case '\\': // Escape sequence.
if len(s) < 2 {
return nil, errors.New("evil quote escape")
}
switch s[1] {
case '"':
addr.Display += "\""
case '\\':
addr.Display += "\\"
}
s = s[2:]
default: // Generic character.
addr.Display += string(s[0])
s = s[1:]
}
}
if s == "" {
return nil, errors.New("no closing quote in display")
}
for s != "" {
c := s[0]
s = s[1:]
if c == '<' {
break
}
}
}
if n := strings.Index(s, ">"); n > 0 {
addr.Uri, err = ParseURI(s[0:n])
if err != nil {
return nil, err
}
s = s[n+1:]
} else {
addr.Uri, err = ParseURI(s)
if err != nil {
return nil, err
}
s = ""
}
// Extract semicolon delimited params.
if s != "" && s[0] == ';' {
addr.Params = parseParams(s[1:])
s = ""
}
// Is there another address?
s = strings.TrimLeft(s, " \t")
if s != "" && s[0] == ',' {
s = strings.TrimLeft(s[1:], " \t")
if s != "" {
addr.Next, err = ParseAddr(s)
if err != nil {
log.Println("[NOTICE]", "dropping invalid bonus addr:", s, err)
}
}
}
return addr, nil
}
func (addr *Addr) String() string {
if addr == nil {
return "<nil>"
}
var b bytes.Buffer
addr.Append(&b)
return b.String()
}
// Returns self if non-nil, otherwise other.
func (addr *Addr) Or(other *Addr) *Addr {
if addr == nil {
return other
}
return addr
}
// Sets newly generated tag ID and returns self.
func (addr *Addr) Tag() *Addr {
addr = addr.Copy()
addr.Params["tag"] = util.GenerateTag()
return addr
}
// Reassembles a SIP address into a buffer.
func (addr *Addr) Append(b *bytes.Buffer) {
if addr.Display != "" {
b.WriteString("\"")
b.WriteString(util.EscapeDisplay(addr.Display))
b.WriteString("\" ")
}
b.WriteString("<")
addr.Uri.Append(b)
b.WriteString(">")
addr.Params.Append(b)
if addr.Next != nil {
b.WriteString(", ")
addr.Next.Append(b)
}
}
// Deep copies a new Addr object.
func (addr *Addr) Copy() *Addr {
if addr == nil {
return nil
}
res := new(Addr)
res.Uri = addr.Uri.Copy()
res.Params = addr.Params.Copy()
res.Next = addr.Next.Copy()
return res
}
// Returns true if the host and port match. If a username is present in
// `addr`, then the username is `other` must also match.
func (addr *Addr) Compare(other *Addr) bool {
if addr != nil && other != nil {
return addr.Uri.Compare(other.Uri)
}
return false
}
// Returns pointer to last addr in linked list.
func (addr *Addr) Last() *Addr {
if addr != nil {
for ; addr.Next != nil; addr = addr.Next {
}
}
return addr
}

+ 164
- 0
sip/addr_test.go View File

@ -0,0 +1,164 @@
package sip_test
import (
"github.com/jart/gosip/sip"
"reflect"
"testing"
)
type addrTest struct {
s string
s2 string
addr sip.Addr
err error
}
var addrTests = []addrTest{
addrTest{
s: "<sip:pokémon.net>",
s2: "<sip:pok%c3%a9mon.net>",
addr: sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "pokémon.net",
Port: 5060,
},
},
},
addrTest{
s: "<sip:brave@toaster.net;isup-oli=29>;tag=deadbeef",
addr: sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
User: "brave",
Host: "toaster.net",
Port: 5060,
Params: sip.Params{
"isup-oli": "29",
},
},
Params: sip.Params{
"tag": "deadbeef",
},
},
},
addrTest{
s: `<sip:pokemon.com>, "Ditto" <sip:ditto@pokemon.com>`,
addr: sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "pokemon.com",
Port: 5060,
},
Next: &sip.Addr{
Display: "Ditto",
Uri: &sip.URI{
Scheme: "sip",
User: "ditto",
Host: "pokemon.com",
Port: 5060,
},
},
},
},
addrTest{
s: `<sip:1.2.3.4>, <sip:1.2.3.5>, <sip:[666::dead:beef]>`,
addr: sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "1.2.3.4",
Port: 5060,
},
Next: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "1.2.3.5",
Port: 5060,
},
Next: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "666::dead:beef",
Port: 5060,
},
},
},
},
},
addrTest{
s: " hello kitty <sip:jtunney@bsdtelecom.net;isup-oli=29>;tag=deadbeef",
s2: "\"hello kitty\" <sip:jtunney@bsdtelecom.net;isup-oli=29>;tag=deadbeef",
addr: sip.Addr{
Display: "hello kitty",
Uri: &sip.URI{
Scheme: "sip",
User: "jtunney",
Host: "bsdtelecom.net",
Port: 5060,
Params: sip.Params{
"isup-oli": "29",
},
},
Params: sip.Params{
"tag": "deadbeef",
},
},
},
addrTest{
s: "\"\\\"\\\"Justine \\\\Tunney \" " +
"<sip:jtunney@bsdtelecom.net;isup-oli=29>;tag=deadbeef",
addr: sip.Addr{
Display: "\"\"Justine \\Tunney ",
Uri: &sip.URI{
Scheme: "sip",
User: "jtunney",
Host: "bsdtelecom.net",
Port: 5060,
Params: sip.Params{
"isup-oli": "29",
},
},
Params: sip.Params{
"tag": "deadbeef",
},
},
},
}
func TestParseAddr(t *testing.T) {
for _, test := range addrTests {
addr, err := sip.ParseAddr(test.s)
if err != nil {
if test.err == nil {
t.Error(err)
continue
} else { // Test was supposed to fail.
panic("TODO(jart): Implement failing support.")
}
}
if !reflect.DeepEqual(&test.addr, addr) {
t.Errorf("%#v != %#v", &test.addr, addr)
}
}
}
func TestAddrString(t *testing.T) {
for _, test := range addrTests {
addr := test.addr.String()
var s string
if test.s2 != "" {
s = test.s2
} else {
s = test.s
}
if s != addr {
t.Error(s, "!=", addr)
}
}
}

+ 111
- 0
sip/messages.go View File

@ -0,0 +1,111 @@
package sip
import (
"github.com/jart/gosip/sdp"
"github.com/jart/gosip/util"
"log"
)
func NewRequest(tp Transport, method string, to, from *Addr) *Msg {
return &Msg{
Method: method,
Request: to.Uri.Copy(),
Via: tp.Via().Branch(),
From: from.Or(tp.Contact()).Tag(),
To: to.Copy(),
Contact: tp.Contact(),
CallID: util.GenerateCallID(),
CSeq: util.GenerateCSeq(),
CSeqMethod: method,
Headers: Headers{"User-Agent": "gosip/1.o"},
}
}
func NewResponse(msg *Msg, status int) *Msg {
return &Msg{
IsResponse: true,
Status: status,
Phrase: Phrases[status],
Via: msg.Via,
From: msg.From,
To: msg.To,
CallID: msg.CallID,
CSeq: msg.CSeq,
CSeqMethod: msg.CSeqMethod,
Headers: Headers{"User-Agent": "gosip/1.o"},
}
}
// http://tools.ietf.org/html/rfc3261#section-17.1.1.3
func NewAck(invite *Msg) *Msg {
return &Msg{
Method: "ACK",
Request: invite.Request,
Via: invite.Via,
From: invite.From,
To: invite.To,
CallID: invite.CallID,
CSeq: invite.CSeq,
CSeqMethod: "ACK",
Route: invite.Route,
Headers: Headers{"User-Agent": "gosip/1.o"},
}
}
func NewCancel(invite *Msg) *Msg {
if invite.IsResponse || invite.Method != "INVITE" {
log.Printf("Can't CANCEL anything non-INVITE:\n%s", invite)
}
return &Msg{
Method: "CANCEL",
Request: invite.Request,
Via: invite.Via,
From: invite.From,
To: invite.To,
CallID: invite.CallID,
CSeq: invite.CSeq,
CSeqMethod: "CANCEL",
Route: invite.Route,
Headers: Headers{"User-Agent": "gosip/1.o"},
}
}
func NewBye(last, invite, ok200 *Msg) *Msg {
return &Msg{
Request: ok200.Contact.Uri,
Via: invite.Via.Branch(),
From: last.From,
To: last.To,
CallID: last.CallID,
Method: "BYE",
CSeq: last.CSeq + 1,
CSeqMethod: "BYE",
Route: ok200.RecordRoute,
Headers: make(map[string]string),
}
}
// Returns true if `resp` can be considered an appropriate response to `msg`.
// Do not use for ACKs.
func ResponseMatch(msg, resp *Msg) bool {
return (resp.IsResponse &&
resp.CSeq == msg.CSeq &&
resp.CSeqMethod == msg.Method &&
resp.Via.Last().Compare(msg.Via))
}
// Returns true if `ack` can be considered an appropriate response to `msg`.
// we don't enforce a matching Via because some VoIP software will generate a
// new branch for ACKs.
func AckMatch(msg, ack *Msg) bool {
return (!ack.IsResponse &&
ack.Method == "ACK" &&
ack.CSeq == msg.CSeq &&
ack.CSeqMethod == "ACK" &&
ack.Via.Last().CompareAddr(msg.Via))
}
func AttachSDP(msg *Msg, sdp *sdp.SDP) {
msg.Headers["Content-Type"] = "application/sdp"
msg.Payload = sdp.String()
}

+ 378
- 0
sip/msg.go View File

@ -0,0 +1,378 @@
// SIP Message Library
package sip
import (
"bytes"
"errors"
"log"
"strconv"
"strings"
)
type Headers map[string]string
// Msg represents a SIP message. This can either be a request or a response.
// These fields are never nil unless otherwise specified.
type Msg struct {
magic int // Used for pseudo msgs or to mark direction
OutboundProxy string // Use Route instead if possible
IsResponse bool // This is a response (like 404 GO DIE)
Method string // Indicates type of request (if request)
Request *URI // dest URI (nil if response)
Status int // Indicates happiness of response (if response)
Phrase string // Explains happiness of response (if response)
Payload string // Stuff that comes after two line breaks
// Mandatory headers
Via *Via // Linked list of agents traversed (must have one)
Route *Addr // Used for goose routing and loose routing
RecordRoute *Addr // Used for loose routing
From *Addr // Logical sender of message
To *Addr // Logical destination of message
CallID string // Identifies call from invite to bye
CSeq int // Counter for network packet ordering
CSeqMethod string // Helps with matching to orig message
// Convenience headers.
MaxForwards int // 0 has context specific meaning
MinExpires int // Registrars need this when responding
Expires int // Seconds registration should expire
Paid *Addr // P-Asserted-Identity or nil (used for PSTN ANI)
Rpid *Addr // Remote-Party-Id or nil
Contact *Addr // Where we send response packets or nil
// All the other headers (never nil)
Headers Headers
}
func (msg *Msg) String() string {
if msg == nil {
return ""
}
var b bytes.Buffer
if err := msg.Append(&b); err != nil {
log.Println("Bad SIP message!", err)
return ""
}
return b.String()
}
// Parses a SIP message into a data structure. This takes ~70 µs on average.
func ParseMsg(packet string) (msg *Msg, err error) {
msg = new(Msg)
if packet == "" {
return nil, errors.New("empty msg")
}
if n := strings.Index(packet, "\r\n\r\n"); n > 0 {
packet, msg.Payload = packet[0:n], packet[n+4:]
}
lines := strings.Split(packet, "\r\n")
if lines == nil || len(lines) < 2 {
return nil, errors.New("too few lines")
}
var k, v string
var okVia, okTo, okFrom, okCallID, okComputer bool
err = msg.parseFirstLine(lines[0])
hdrs := lines[1:]
msg.Headers = make(map[string]string, len(hdrs))
msg.MaxForwards = 70
viap := &msg.Via
contactp := &msg.Contact
routep := &msg.Route
rroutep := &msg.RecordRoute
for _, hdr := range hdrs {
if hdr == "" {
continue
}
if hdr[0] == ' ' || hdr[0] == '\t' {
v = strings.Trim(hdr, "\t ") // Line continuation.
} else {
if i := strings.Index(hdr, ": "); i > 0 {
k, v = hdr[0:i], hdr[i+2:]
if k == "" || v == "" {
log.Println("[NOTICE]", "blank header found", hdr)
}
} else {
log.Println("[NOTICE]", "header missing delimiter", hdr)
continue
}
}
switch strings.ToLower(k) {
case "call-id":
okCallID = true
msg.CallID = v
case "via":
okVia = true
*viap, err = ParseVia(v)
if err != nil {
return nil, errors.New("Via header - " + err.Error())
}
viap = &(*viap).Next
case "to":
okTo = true
msg.To, err = ParseAddr(v)
if err != nil {
return nil, errors.New("To header - " + err.Error())
}
case "from":
okFrom = true
msg.From, err = ParseAddr(v)
if err != nil {
return nil, errors.New("From header - " + err.Error())
}
case "contact":
*contactp, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Contact header - " + err.Error())
}
contactp = &(*contactp).Next
case "cseq":
okComputer = false
if n := strings.Index(v, " "); n > 0 {
sseq, method := v[0:n], v[n+1:]
if seq, err := strconv.Atoi(sseq); err == nil {
msg.CSeq, msg.CSeqMethod = seq, method
okComputer = true
}
}
if !okComputer {
return nil, errors.New("Bad CSeq Header")
}
case "content-length":
if cl, err := strconv.Atoi(v); err == nil {
if cl > len(msg.Payload) {
msg.Payload = msg.Payload[0:cl]
log.Println("[DEBUG]", "discarding extra sip payload bytes")
} else if cl > len(msg.Payload) {
return nil, errors.New("content-length > len(payload)")
}
} else {
return nil, errors.New("Bad Content-Length header")
}
case "expires":
if cl, err := strconv.Atoi(v); err == nil && cl >= 0 {
msg.Expires = cl
} else {
return nil, errors.New("Bad Expires header")
}
case "min-expires":
if cl, err := strconv.Atoi(v); err == nil && cl > 0 {
msg.MinExpires = cl
} else {
return nil, errors.New("Bad Min-Expires header")
}
case "max-forwards":
if cl, err := strconv.Atoi(v); err == nil && cl > 0 {
msg.MaxForwards = cl
} else {
return nil, errors.New("Bad Max-Forwards header")
}
case "route":
*routep, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Bad Route header: " + err.Error())
}
routep = &(*routep).Next
case "record-route":
*rroutep, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Bad Record-Route header: " + err.Error())
}
rroutep = &(*rroutep).Next
case "p-asserted-identity":
msg.Paid, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Bad P-Asserted-Identity header: " + err.Error())
}
case "remote-party-id":
msg.Rpid, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Bad Remote-Party-ID header: " + err.Error())
}
default:
msg.Headers[k] = v
}
}
if !okVia || !okTo || !okFrom || !okCallID || !okComputer {
return nil, errors.New("Missing mandatory headers")
}
return msg, nil
}
func (msg *Msg) parseFirstLine(s string) (err error) {
toks := strings.Split(s, " ")
if toks != nil && len(toks) == 3 && toks[2] == "SIP/2.0" {
msg.Phrase = ""
msg.Status = 0
msg.Method = toks[0]
msg.Request = new(URI)
msg.Request, err = ParseURI(toks[1])
} else if toks != nil && len(toks) == 3 && toks[0] == "SIP/2.0" {
msg.IsResponse = true
msg.Method = ""
msg.Request = nil
msg.Phrase = toks[2]
msg.Status, err = strconv.Atoi(toks[1])
if err != nil {
return errors.New("Invalid status")
}
} else {
err = errors.New("Bad protocol or request line")
}
return err
}
func (msg *Msg) Copy() *Msg {
if msg == nil {
return nil
}
res := new(Msg)
*res = *msg
res.To = msg.To.Copy()
res.From = msg.From.Copy()
res.Via = msg.Via.Copy()
res.Paid = msg.Paid.Copy()
res.Rpid = msg.Rpid.Copy()
res.Route = msg.Route.Copy()
res.Request = msg.Request.Copy()
res.Contact = msg.Contact.Copy()
res.RecordRoute = msg.RecordRoute.Copy()
res.Headers = make(map[string]string, len(msg.Headers))
for k, v := range msg.Headers {
res.Headers[k] = v
}
return res
}
// i turn a sip message back into a packet
func (msg *Msg) Append(b *bytes.Buffer) error {
if !msg.IsResponse {
if msg.Method == "" {
return errors.New("Msg.Method not set")
}
if msg.Request == nil {
return errors.New("msg.Request not set")
}
b.WriteString(msg.Method)
b.WriteString(" ")
msg.Request.Append(b)
b.WriteString(" SIP/2.0\r\n")
} else {
if msg.Status < 100 {
return errors.New("Msg.Status < 100")
}
if msg.Status >= 700 {
return errors.New("Msg.Status >= 700")
}
if msg.Phrase == "" {
if v, ok := Phrases[msg.Status]; ok {
msg.Phrase = v
} else {
return errors.New("Msg.Phrase not set and Msg.Status is unknown")
}
}
b.WriteString("SIP/2.0 ")
b.WriteString(strconv.Itoa(msg.Status))
b.WriteString(" ")
b.WriteString(msg.Phrase)
b.WriteString("\r\n")
}
if msg.Via == nil {
return errors.New("Need moar Via headers")
}
for viap := msg.Via; viap != nil; viap = viap.Next {
b.WriteString("Via: ")
if err := viap.Append(b); err != nil {
return err
}
b.WriteString("\r\n")
}
if msg.MaxForwards < 0 {
return errors.New("MaxForwards is less than 0!!")
} else if msg.MaxForwards == 0 {
b.WriteString("Max-Forwards: 70\r\n")
} else {
b.WriteString("Max-Forwards: ")
b.WriteString(strconv.Itoa(msg.MaxForwards))
b.WriteString("\r\n")
}
b.WriteString("From: ")
msg.From.Append(b)
b.WriteString("\r\n")
b.WriteString("To: ")
msg.To.Append(b)
b.WriteString("\r\n")
if msg.CallID == "" {
return errors.New("CallID is blank")
}
b.WriteString("Call-ID: ")
b.WriteString(msg.CallID)
b.WriteString("\r\n")
if msg.CSeq < 0 || msg.CSeqMethod == "" {
return errors.New("Bad CSeq")
}
b.WriteString("CSeq: ")
b.WriteString(strconv.Itoa(msg.CSeq))
b.WriteString(" ")
b.WriteString(msg.CSeqMethod)
b.WriteString("\r\n")
if msg.Contact != nil {
b.WriteString("Contact: ")
msg.Contact.Append(b)
b.WriteString("\r\n")
}
// Expires is allowed to be 0 for for REGISTER stuff.
if msg.Expires > 0 ||
msg.Method == "REGISTER" || msg.CSeqMethod == "REGISTER" {
b.WriteString("Expires: ")
b.WriteString(strconv.Itoa(msg.Expires))
b.WriteString("\r\n")
}
if msg.MinExpires > 0 {
b.WriteString("Min-Expires: ")
b.WriteString(strconv.Itoa(msg.MinExpires))
b.WriteString("\r\n")
}
if msg.Headers != nil {
for k, v := range msg.Headers {
if k == "" || v == "" {
return errors.New("Header blank")
}
b.WriteString(k)
b.WriteString(": ")
b.WriteString(v)
b.WriteString("\r\n")
}
}
if msg.Paid != nil {
b.WriteString("P-Asserted-Identity: ")
msg.Paid.Append(b)
b.WriteString("\r\n")
}
if msg.Rpid != nil {
b.WriteString("Remote-Party-ID: ")
msg.Rpid.Append(b)
b.WriteString("\r\n")
}
b.WriteString("Content-Length: ")
b.WriteString(strconv.Itoa(len(msg.Payload)))
b.WriteString("\r\n\r\n")
b.WriteString(msg.Payload)
return nil
}

+ 214
- 0
sip/msg_test.go View File

@ -0,0 +1,214 @@
package sip_test
import (
"github.com/jart/gosip/sip"
"reflect"
"testing"
)
type msgTest struct {
name string
s string
msg sip.Msg
err error
}
var msgTests = []msgTest{
msgTest{
name: "OPTIONS",
s: "OPTIONS sip:10.11.34.37:42367 SIP/2.0\r\n" +
"Via: SIP/2.0/UDP 10.11.34.37:42367;rport;branch=9dc39c3c3e84\r\n" +
"Max-Forwards: 60\r\n" +
"To: <sip:10.11.34.37:5060>\r\n" +
"From: <sip:10.11.34.37:42367;laffo>;tag=11917cbc0513\r\n" +
"Call-ID: e71a163e-c440-474d-a4ec-5cd85a0309c6\r\n" +
"CSeq: 36612 OPTIONS\r\n" +
"Contact: <sip:10.11.34.37:42367>\r\n" +
"User-Agent: ghoul/0.1\r\n" +
"Accept: application/sdp\r\n" +
"Content-Length: 0\r\n" +
"\r\n",
msg: sip.Msg{
Method: "OPTIONS",
CSeqMethod: "OPTIONS",
MaxForwards: 60,
CallID: "e71a163e-c440-474d-a4ec-5cd85a0309c6",
CSeq: 36612,
Request: &sip.URI{
Scheme: "sip",
Host: "10.11.34.37",
Port: 42367,
},
Via: &sip.Via{
Version: "2.0",
Proto: "UDP",
Host: "10.11.34.37",
Port: 42367,
Params: sip.Params{"rport": "", "branch": "9dc39c3c3e84"},
},
To: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "10.11.34.37",
Port: 5060,
},
},
From: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "10.11.34.37",
Port: 42367,
Params: sip.Params{"laffo": ""},
},
Params: sip.Params{"tag": "11917cbc0513"},
},
Contact: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "10.11.34.37",
Port: 42367,
},
},
Headers: map[string]string{
"User-Agent": "ghoul/0.1",
"Accept": "application/sdp",
},
},
},
msgTest{
name: "INVITE",
s: "INVITE sip:10.11.34.37 SIP/2.0\r\n" +
"via: SIP/2.0/UDP 10.11.34.37:59516;rport;branch=z9hG4bKS308QB9UUpNyD\r\n" +
"Max-Forwards: 70\r\n" +
"From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg\r\n" +
"To: <sip:10.11.34.37>\r\n" +
"Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf\r\n" +
"CSeq: 133097268 INVITE\r\n" +
"Contact: <sip:10.11.34.37:59516>\r\n" +
" <sip:10.11.34.38:59516>\r\n" +
"User-Agent: tube/0.1\r\n" +
"Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, PRACK, MESSAGE, SUBSCRIBE, NOTIFY, REFER, UPDATE, INFO\r\n" +
"Supported: timer, 100rel\r\n" +
"Allow-Events: talk\r\n" +
"Content-Type: application/sdp\r\n" +
"Content-Disposition: session\r\n" +
"Content-Length: 218\r\n" +
"\r\n" +
"v=0\r\n" +
"o=- 2862054018559638081 6057228511765453924 IN IP4 10.11.34.37\r\n" +
"s=-\r\n" +
"c=IN IP4 10.11.34.37\r\n" +
"t=0 0\r\n" +
"m=audio 23448 RTP/AVP 0 101\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:101 telephone-event/8000\r\n" +
"a=fmtp:101 0-16\r\n" +
"a=ptime:20\r\n",
msg: sip.Msg{
Method: "INVITE",
CSeqMethod: "INVITE",
MaxForwards: 70,
CallID: "87704115-03b8-122e-08b5-001bfcce6bdf",
CSeq: 133097268,
Request: &sip.URI{
Scheme: "sip",
Host: "10.11.34.37",
Port: 5060,
},
Via: &sip.Via{
Version: "2.0",
Proto: "UDP",
Host: "10.11.34.37",
Port: 59516,
Params: sip.Params{"rport": "", "branch": "z9hG4bKS308QB9UUpNyD"},
},
To: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "10.11.34.37",
Port: 5060,
},
},
From: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "10.11.34.37",
Port: 59516,
},
Params: sip.Params{"tag": "S1jX7UtK5Zerg"},
},
Contact: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "10.11.34.37",
Port: 59516,
},
Next: &sip.Addr{
Uri: &sip.URI{
Scheme: "sip",
Host: "10.11.34.38",
Port: 59516,
},
},
},
Headers: map[string]string{
"User-Agent": "tube/0.1",
"Allow": "INVITE, ACK, BYE, CANCEL, OPTIONS, PRACK, MESSAGE, SUBSCRIBE, NOTIFY, REFER, UPDATE, INFO",
"Allow-Events": "talk",
"Content-Disposition": "session",
"Supported": "timer, 100rel",
"Content-Type": "application/sdp",
},
Payload: "v=0\r\n" +
"o=- 2862054018559638081 6057228511765453924 IN IP4 10.11.34.37\r\n" +
"s=-\r\n" +
"c=IN IP4 10.11.34.37\r\n" +
"t=0 0\r\n" +
"m=audio 23448 RTP/AVP 0 101\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:101 telephone-event/8000\r\n" +
"a=fmtp:101 0-16\r\n" +
"a=ptime:20\r\n",
},
},
}
func TestParseMsg(t *testing.T) {
for _, test := range msgTests {
msg, err := sip.ParseMsg(test.s)
if err != nil {
if test.err == nil {
t.Errorf("%v", err)
continue
} else { // test was supposed to fail
panic("TODO(jart): Implement failing support.")
}
}
if !reflect.DeepEqual(&test.msg, msg) {
t.Errorf("Message:\n%#v !=\n%#v", &test.msg, msg)
if !reflect.DeepEqual(test.msg.Payload, msg.Payload) {
t.Errorf("Payload:\n%#v !=\n%#v", test.msg.Payload, msg.Payload)
}
if !reflect.DeepEqual(test.msg.Headers, msg.Headers) {
t.Errorf("Headers:\n%#v !=\n%#v", test.msg.Headers, msg.Headers)
}
if !reflect.DeepEqual(test.msg.Via, msg.Via) {
t.Errorf("Via:\n%#v !=\n%#v", test.msg.Via, msg.Via)
}
if !reflect.DeepEqual(test.msg.Request, msg.Request) {
t.Errorf("Request:\n%#v !=\n%#v", test.msg.Request, msg.Request)
}
if !reflect.DeepEqual(test.msg.To, msg.To) {
t.Errorf("To:\n%#v !=\n%#v", test.msg.To, msg.To)
}
if !reflect.DeepEqual(test.msg.From, msg.From) {
t.Errorf("From:\n%#v !=\n%#v", test.msg.From, msg.From)
}
if !reflect.DeepEqual(test.msg.Contact, msg.Contact) {
t.Errorf("Contact:\n%#v !=\n%#v", test.msg.Contact, msg.Contact)
}
}
}
}

+ 96
- 0
sip/phrases.go View File

@ -0,0 +1,96 @@
// Standard SIP Protocol Messages
//
// http://www.iana.org/assignments/sip-parameters
package sip
var Phrases = map[int]string{
// 1xx: Provisional -- request received, continuing to process the
// request;
100: "Trying", // indicates server is not totally pwnd
180: "Ringing", // i promise the remote phone is ringing
181: "Call Is Being Forwarded",
182: "Queued",
183: "Session Progress", // establish early media (pstn ringback)
// 2xx: Success -- the action was successfully received, understood,
// and accepted;
200: "OK", // call is answered
202: "Accepted", // [RFC3265]
204: "No Notification", // [RFC5839]
// 3xx: Redirection -- further action needs to be taken in order to
// complete the request;
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily", // send your call there instead kthx
305: "Use Proxy", // you fool! send your call there instead
380: "Alternative Service",
// 4xx: Client Error -- the request contains bad syntax or cannot be
// fulfilled at this server;
400: "Bad Request", // missing headers, bad format, etc.
401: "Unauthorized", // resend request with auth header
402: "Payment Required", // i am greedy
403: "Forbidden", // gtfo
404: "Not Found", // wat?
405: "Method Not Allowed", // i don't support that type of request
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone", // shaniqua don't live here no more
411: "Length Required",
412: "Conditional Request Failed", // [RFC3903]
413: "Request Entity Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Unsupported URI Scheme",
417: "Unknown Resource-Priority",
420: "Bad Extension",
421: "Extension Required",
422: "Session Interval Too Small", // [RFC4028]
423: "Interval Too Brief",
428: "Use Identity Header", // [RFC4474]
429: "Provide Referrer Identity", // [RFC3892]
430: "Flow Failed", // [RFC5626]
433: "Anonymity Disallowed", // [RFC5079]
436: "Bad Identity-Info", // [RFC4474]
437: "Unsupported Certificate", // [RFC4474]
438: "Invalid Identity Header", // [RFC4474]
439: "First Hop Lacks Outbound Support", // [RFC5626]
440: "Max-Breadth Exceeded", // [RFC5393]
470: "Consent Needed", // [RFC5360]
480: "Temporarily Unavailable", // fast busy or soft fail
481: "Call/Transaction Does Not Exist", // bad news
482: "Loop Detected", // froot looping
483: "Too Many Hops", // froot looping
484: "Address Incomplete",
485: "Ambiguous",
486: "Busy Here",
487: "Request Terminated",
488: "Not Acceptable Here",
489: "Bad Event", // [RFC3265]
491: "Request Pending",
493: "Undecipherable",
494: "Security Agreement Required", // [RFC3329]
// 5xx: Server Error -- the server failed to fulfill an apparently
// valid request;
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Time-out",
505: "Version Not Supported",
513: "Message Too Large",
580: "Precondition Failure", // [RFC3312]
// 6xx: Global Failure -- the request cannot be fulfilled at any
// server.
600: "Busy Everywhere",
603: "Decline",
604: "Does Not Exist Anywhere",
606: "Not Acceptable",
687: "Dialog Terminated",
}

+ 62
- 0
sip/prefs.go View File

@ -0,0 +1,62 @@
// Global Settings. You can change these at startup to fine tune
// certain behaviors.
package gosip
import (
"os"
"time"
)
var (
// how often to check for shutdowns
DeathClock = 200 * time.Millisecond
// sip egress msg length must not exceed me. if you are brave and
// use jumbo frames you can increase this
MTU = 1450
// maximum number SRV/Redirects/Etc. to entertain. more
// accurately, this is the maximum number of time's we're allowed
// to enter the "calling" state.
MaxAttempts = 10
// this is how long to wait (in nanoseconds) for a 100 trying
// response before retransmitting the invite. multiply this by
// RetransmitAttempts and that's how long it'll be before we try
// another server. that might seem like a very long time but
// happy networks facilitate fast failover by sending hard errors
// (ICMP, 480, 500-599)
TryingTimeout = 500 * time.Millisecond
// how many times to attempt to retransmit before moving on
RetransmitAttempts = 2
// number of nanoseconds (across all attempts) before we give up
// trying to connect a call. this doesn't mean the call has to
// have been answered but rather has moved beyond the "calling"
// state and doesn't go back.
GiveupTimeout = 3 * time.Second
// ToDo: how long to wait before refreshing a call with a
// re-INVITE message.
RefreshTimeout = 15 * time.Minute
// how many times are proxies allowed to forward a message
MaxForwards int = 70
// aprox. how long the transaction engine remembers old Call-IDs
// to prevent a retransmit from accidentally opening a new dialog.
CallIDBanDuration = 35 * time.Second
// aprox. how often to sweep old transactions from ban list
CallIDBanSweepDuration = 5 * time.Second
// should the transport layer add timestamps to Via headers
// (µsi/µse = utc microseconds since unix epoch ingress/egress)
ViaTimestampTagging = true
// use this feature to print out raw sip messages the moment they
// are sent/received by the transport layer
TransportTrace = (os.Getenv("TPORT_LOG") == "true")
)

+ 291
- 0
sip/transport.go View File

@ -0,0 +1,291 @@
// SIP Transport Layer. Responsible for serializing messages to/from
// your network.
package sip
import (
"bytes"
"errors"
"flag"
"fmt"
"github.com/jart/gosip/util"
"log"
"net"
"strconv"
"strings"
"time"
)
var (
tracing = flag.Bool("tracing", false, "Enable SIP message tracing")
timestampTagging = flag.Bool("timestampTagging", false, "Add microsecond timestamps to Via tags")
)
// Transport defines any object capable of sending and receiving SIP messages.
// Such objects are responsible for their own reliability. This means checking
// network errors, raising alarms, rebinding sockets, etc.
type Transport interface {
// Sends a SIP message. Will not modify msg.
Send(msg *Msg) error
// Receives a SIP message. Must only be called by one goroutine. The received
// address is injected into the first Via header as the "received" param.
Recv() (msg *Msg, err error)
// Closes underlying resources. Please make sure all calls using this
// transport complete first.
Close() error
// When you send an outbound request (not a response) you have to set the via
// tag: ``msg.Via = tport.Via().SetBranch().SetNext(msg.Via)``. The details
// of the branch parameter... are tricky.
Via() *Via
// Returns a linked list of all canonical names and/or IP addresses that may
// be used to contact *this specific* transport.
Contact() *Addr
}
// Transport implementation that serializes messages to/from a UDP socket.
type udpTransport struct {
sock *net.UDPConn // thing returned by ListenUDP
addr *net.UDPAddr // handy for getting ip (contact might be host)
buf []byte // reusable memory for serialization
via *Via // who are we?
contact *Addr // uri that points to this specific transport
}
// Creates a new stateless network mechanism for transmitting and receiving SIP
// signalling messages.
//
// 'contact' is a SIP address, e.g. "<sip:1.2.3.4>", that tells how to bind
// sockets. This value is also used for contact headers which tell other
// user-agents where to send responses and hence should only contain an IP or
// canonical address.
func NewUDPTransport(contact *Addr) (tp Transport, err error) {
saddr := util.HostPortToString(contact.Uri.Host, contact.Uri.Port)
sock, err := net.ListenPacket("udp", saddr)
if err != nil {
return nil, err
}
addr := sock.LocalAddr().(*net.UDPAddr)
contact = contact.Copy()
contact.Uri.Port = uint16(addr.Port)
contact.Uri.Params["transport"] = addr.Network()
return &udpTransport{
sock: sock.(*net.UDPConn),
addr: addr,
buf: make([]byte, 2048),
contact: contact,
via: &Via{
Version: "2.0",
Proto: strings.ToUpper(addr.Network()),
Host: contact.Uri.Host,
Port: contact.Uri.Port,
},
}, nil
}
func (tp *udpTransport) Send(msg *Msg) error {
msg, saddr, err := tp.route(msg)
if err != nil {
return err
}
addr, err := net.ResolveUDPAddr("ip", saddr)
if err != nil {
return errors.New(fmt.Sprintf(
"udpTransport(%s) failed to resolve %s: %s", tp.addr, saddr, err))
}
msg.MaxForwards--
ts := time.Now()
addTimestamp(msg, ts)
var b bytes.Buffer
msg.Append(&b)
if *tracing {
tp.trace("send", b.String(), addr, ts)
}
_, err = tp.sock.WriteTo(b.Bytes(), addr)
if err != nil {
return errors.New(fmt.Sprintf(
"udpTransport(%s) write failed: %s", tp.addr, err))
}
return nil
}
func (tp *udpTransport) Recv() (msg *Msg, err error) {
for {
amt, addr, err := tp.sock.ReadFromUDP(tp.buf)
if err != nil {
return nil, errors.New(fmt.Sprintf(
"udpTransport(%s) read failed: %s", tp.addr, err))
}
ts := time.Now()
packet := string(tp.buf[0:amt])
if *tracing {
tp.trace("recv", packet, addr, ts)
}
// Validation: http://tools.ietf.org/html/rfc3261#section-16.3
msg, err = ParseMsg(packet)
if err != nil {
log.Printf("udpTransport(%s) got bad message from %s: %s\n%s", tp.addr, addr, err, packet)
continue
}
if msg.Via.Host != addr.IP.String() || int(msg.Via.Port) != addr.Port {
msg.Via.Params["received"] = addr.String()
}
addTimestamp(msg, ts)
if !tp.sanityCheck(msg) {
continue
}
tp.preprocess(msg)
break
}
return msg, nil
}
func (tp *udpTransport) Via() *Via {
return tp.via
}
func (tp *udpTransport) Contact() *Addr {
return tp.contact
}
func (tp *udpTransport) Close() error {
return tp.sock.Close()
}
func (tp *udpTransport) trace(dir, pkt string, addr net.Addr, t time.Time) {
size := len(pkt)
bar := strings.Repeat("-", 72)
suffix := "\n "
if pkt[len(pkt)-1] == '\n' {
suffix = ""
}
log.Printf(
"%s %d bytes to %s/%s at %s\n"+
"%s\n"+
"%s%s"+
"%s\n",
dir, size, addr.Network(), addr.String(),
t.Format(time.RFC3339Nano),
bar,
strings.Replace(pkt, "\n", "\n ", -1), suffix,
bar)
}
// Test if this message is acceptable.
func (tp *udpTransport) sanityCheck(msg *Msg) bool {
if msg.MaxForwards <= 0 {
log.Printf("udpTransport(%s) froot loop detected\n%s", tp.addr, msg)
go tp.Send(NewResponse(msg, 483))
return false
}
if msg.IsResponse {
if msg.Status >= 700 {
log.Printf("udpTransport(%s) msg has crazy status number\n%s", tp.addr, msg)
go tp.Send(NewResponse(msg, 400))
return false
}
} else {
if msg.CSeqMethod == "" || msg.CSeqMethod != msg.Method {
log.Printf("udpTransport(%s) bad cseq number\n%s", tp.addr, msg)
go tp.Send(NewResponse(msg, 400))
return false
}
}
return true
}
// Perform some ingress message mangling.
func (tp *udpTransport) preprocess(msg *Msg) {
if tp.contact.Compare(msg.Route) {
log.Printf("udpTransport(%s) removing our route header: %s", tp.addr, msg.Route)
msg.Route = msg.Route.Next
}
if _, ok := msg.Request.Params["lr"]; ok && msg.Route != nil && tp.contact.Uri.Compare(msg.Request) {
// RFC3261 16.4 Route Information Preprocessing
// RFC3261 16.12.1.2: Traversing a Strict-Routing Proxy
var oldReq, newReq *URI
if msg.Route.Next == nil {
oldReq, newReq = msg.Request, msg.Route.Uri
msg.Request = msg.Route.Uri
msg.Route = nil
} else {
seclast := msg.Route
for ; seclast.Next.Next != nil; seclast = seclast.Next {
}
oldReq, newReq = msg.Request, seclast.Next.Uri
msg.Request = seclast.Next.Uri
seclast.Next = nil
msg.Route.Last()
}
log.Printf("udpTransport(%s) fixing request uri after strict router traversal: %s -> %s",
tp.addr, oldReq, newReq)
}
}
func (tp *udpTransport) route(old *Msg) (msg *Msg, saddr string, err error) {
var host string
var port uint16
msg = new(Msg)
*msg = *old // Shallow copy is sufficient.
if msg.IsResponse {
msg.Via = old.Via.Copy()
if msg.Via.CompareAddr(tp.via) {
// In proxy scenarios we have to remove our own Via.
msg.Via = msg.Via.Next
}
if msg.Via == nil {
return nil, "", errors.New("Ran out of Via headers when forwarding Response!")
}
if msg.Via != nil {
if received, ok := msg.Via.Params["received"]; ok {
return msg, received, nil
} else {
host, port = msg.Via.Host, msg.Via.Port
}
} else {
return nil, "", errors.New("Message missing Via header")
}
} else {
if msg.Request == nil {
return nil, "", errors.New("Missing request URI")
}
if !msg.Via.CompareAddr(tp.via) {
return nil, "", errors.New("You forgot to say: msg.Via = tp.Via(msg.Via)")
}
if msg.Route != nil {
if msg.Method == "REGISTER" {
return nil, "", errors.New("Don't loose route register requests")
}
if _, ok := msg.Route.Uri.Params["lr"]; ok {
// RFC3261 16.12.1.1 Basic SIP Trapezoid
route := msg.Route
msg.Route = msg.Route.Next
host, port = route.Uri.Host, route.Uri.Port
} else {
// RFC3261 16.12.1.2: Traversing a Strict-Routing Proxy
msg.Route = old.Route.Copy()
msg.Route.Last().Next = &Addr{Uri: msg.Request}
msg.Request = msg.Route.Uri
msg.Route = msg.Route.Next
host, port = msg.Request.Host, msg.Request.Port
}
} else {
host, port = msg.Request.Host, msg.Request.Port
}
}
if msg.OutboundProxy != "" {
saddr = msg.OutboundProxy
} else {
saddr = util.HostPortToString(host, port)
}
return
}
func addTimestamp(msg *Msg, ts time.Time) {
if *timestampTagging {
msg.Via.Params["µsi"] = strconv.FormatInt(ts.UnixNano()/int64(time.Microsecond), 10)
}
}

+ 202
- 0
sip/uri.go View File

@ -0,0 +1,202 @@
// SIP URI Library
//
// We can't use net.URL because it doesn't support SIP URIs. This is because:
// a) it doesn't support semicolon parameters; b) it doesn't extract the user
// and host information when the "//" isn't present.
//
// For example:
//
// jtunney@bsdtelecom.net;isup-oli=29
//
// Roughly equates to:
//
// {Scheme: "sip",
// User: "jtunney",
// Pass: "",
// Host: "bsdtelecom.net",
// Port: "",
// Params: {"isup-oli": "29"}}
//
package sip
import (
"bytes"
"errors"
"github.com/jart/gosip/util"
"strconv"
"strings"
)
const (
delims = ":/@;?#<>"
)
var (
URIEmpty = errors.New("empty uri")
URISchemeNotFound = errors.New("scheme not found")
URIMissingHost = errors.New("host missing")
URIBadPort = errors.New("invalid port number")
)
type Params map[string]string
type URI struct {
Scheme string // sip, tel, etc. (never blank)
User string // sip:USER@host
Pass string // sip:user:PASS@host
Host string // example.com, 1.2.3.4, etc.
Port uint16 // 5060, 80, etc.
Params Params // semicolon delimited params after uris and addrs
}
// Parses a SIP URI.
func ParseURI(s string) (uri *URI, err error) {
uri = new(URI)
if s == "" {
return nil, URIEmpty
}
// Extract scheme.
n := strings.IndexAny(s, delims)
if n < 0 || s[n] != ':' {
return nil, URISchemeNotFound
}
uri.Scheme, s = s[0:n], s[n+1:]
// Extract user/pass.
n = strings.IndexAny(s, delims)
if n > 0 && s[n] == ':' { // sip:user:pass@host
// if next token isn't '@' then assume 'sip:host:port'
s2 := s[n+1:]
n2 := strings.IndexAny(s2, delims)
if n2 > 0 && s2[n2] == '@' {
uri.User = s[0:n]
s, n = s2, n2
if n < 0 || s[n] != '@' {
return nil, URIMissingHost
}
uri.Pass, s = s[0:n], s[n+1:]
}
} else if n > 0 && s[n] == '@' { // user@host
uri.User, s = s[0:n], s[n+1:]
}
// Extract host/port.
s, uri.Host, uri.Port, err = extractHostPort(s)
if err != nil {
return nil, err
}
// Extract semicolon delimited params.
if s != "" && s[0] == ';' {
uri.Params = parseParams(s[1:])
s = ""
}
// if s != "" {
// fmt.Fprintf(os.Stderr, "leftover data: %v\n", s)
// }
uri.User, err = util.URLUnescape(uri.User, false)
if err != nil {
return nil, err
}
uri.Pass, err = util.URLUnescape(uri.Pass, false)
if err != nil {
return nil, err
}
uri.Host, err = util.URLUnescape(uri.Host, false)
if err != nil {
return nil, err
}
return uri, nil
}
// Deep copies a URI object.
func (uri *URI) Copy() *URI {
if uri == nil {
return nil
}
res := new(URI)
*res = *uri
res.Params = uri.Params.Copy()
return res
}
func (uri *URI) String() string {
if uri == nil {
return "<nil>"
}
var b bytes.Buffer
uri.Append(&b)
return b.String()
}
func (uri *URI) Append(b *bytes.Buffer) {
if uri.Scheme == "" {
uri.Scheme = "sip"
}
b.WriteString(uri.Scheme)
b.WriteString(":")
if uri.User != "" {
if uri.Pass != "" {
b.WriteString(util.URLEscape(uri.User, false))
b.WriteString(":")
b.WriteString(util.URLEscape(uri.Pass, false))
} else {
b.WriteString(util.URLEscape(uri.User, false))
}
b.WriteString("@")
}
if util.IsIPv6(uri.Host) {
b.WriteString("[" + util.URLEscape(uri.Host, false) + "]")
} else {
b.WriteString(util.URLEscape(uri.Host, false))
}
if uri.Port > 0 && uri.Port != 5060 {
b.WriteString(":" + strconv.FormatInt(int64(uri.Port), 10))
}
uri.Params.Append(b)
}
// Returns true if scheme, host, and port match. if a username is present in
// `addr`, then the username is `other` must also match.
func (uri *URI) Compare(other *URI) bool {
if uri != nil && other != nil {
if uri.Scheme == other.Scheme &&
uri.Host == other.Host &&
uri.Port == other.Port {
if uri.User != "" {
if uri.User == other.User {
return true
}
} else {
return true
}
}
}
return false
}
func (params Params) Copy() Params {
res := make(Params, len(params))
for k, v := range params {
res[k] = v
}
return res
}
func (params Params) Append(b *bytes.Buffer) {
if params != nil && len(params) > 0 {
for k, v := range params {
b.WriteString(";")
b.WriteString(util.URLEscape(k, false))
if v != "" {
b.WriteString("=")
b.WriteString(util.URLEscape(v, false))
}
}
}
}

+ 194
- 0
sip/uri_test.go View File

@ -0,0 +1,194 @@
package sip_test
import (
"github.com/jart/gosip/sip"
"reflect"
"testing"
)
type uriTest struct {
s string // user input we want to convert
s2 string // non-blank if 's' changes after we parse/format it
uri sip.URI // what 's' should become after parsing
err error // if we expect parsing to fail
}
var uriTests = []uriTest{
uriTest{
s: "sip:bsdtelecom.net",
uri: sip.URI{
Scheme: "sip",
Host: "bsdtelecom.net",
Port: 5060,
},
},
uriTest{
s: "sip:bsdtelecom.net:666",
uri: sip.URI{
Scheme: "sip",
Host: "bsdtelecom.net",
Port: 666,
},
},
uriTest{
s: "sip:jtunney@bsdtelecom.net",
uri: sip.URI{
Scheme: "sip",
User: "jtunney",
Host: "bsdtelecom.net",
Port: 5060,
},
},
uriTest{
s: "sip:jtunney@bsdtelecom.net:5060",
s2: "sip:jtunney@bsdtelecom.net",
uri: sip.URI{
Scheme: "sip",
User: "jtunney",
Host: "bsdtelecom.net",
Port: 5060,
},
},
uriTest{
s: "sip:jtunney:lawl@bsdtelecom.net:5060",
s2: "sip:jtunney:lawl@bsdtelecom.net",
uri: sip.URI{
Scheme: "sip",
User: "jtunney",
Pass: "lawl",
Host: "bsdtelecom.net",
Port: 5060,
},
},
uriTest{
s: "sip:jtunney:lawl@bsdtelecom.net:5060;isup-oli=00;omg;lol=cat",
s2: "sip:jtunney:lawl@bsdtelecom.net;isup-oli=00;omg;lol=cat",
uri: sip.URI{
Scheme: "sip",
User: "jtunney",
Pass: "lawl",
Host: "bsdtelecom.net",
Port: 5060,
Params: sip.Params{
"isup-oli": "00",
"omg": "",
"lol": "cat",
},
},
},
uriTest{
s: "sip:jtunney@bsdtelecom.net;isup-oli=00;omg;lol=cat",
uri: sip.URI{
Scheme: "sip",
User: "jtunney",
Host: "bsdtelecom.net",
Port: 5060,
Params: sip.Params{
"isup-oli": "00",
"omg": "",
"lol": "cat",
},
},
},
uriTest{
s: "sip:[dead:beef::666]",
uri: sip.URI{
Scheme: "sip",
Host: "dead:beef::666",
Port: 5060,
},
},
uriTest{
s: "sip:[dead:beef::666]:5060",
s2: "sip:[dead:beef::666]",
uri: sip.URI{
Scheme: "sip",
Host: "dead:beef::666",
Port: 5060,
},
},
uriTest{
s: "sip:lol:cat@[dead:beef::666]:65535",
uri: sip.URI{
Scheme: "sip",
User: "lol",
Pass: "cat",
Host: "dead:beef::666",
Port: 65535,
},
},
uriTest{
s: "sip:lol:cat@[dead:beef::666]:65535;oh;my;goth",
uri: sip.URI{
Scheme: "sip",
User: "lol",
Pass: "cat",
Host: "dead:beef::666",
Port: 65535,
Params: sip.Params{
"oh": "",
"my": "",
"goth": "",
},
},
},
uriTest{
s: "sip:jtunney%3e:la%3ewl@bsdtelecom%3e.net:65535" +
";isup%3e-oli=00%3e;%3eomg;omg;lol=cat",
uri: sip.URI{
Scheme: "sip",
User: "jtunney>",
Pass: "la>wl",
Host: "bsdtelecom>.net",
Port: 65535,
Params: sip.Params{
"isup>-oli": "00>",
">omg": "",
"omg": "",
"lol": "cat",
},
},
},
}
func TestParse(t *testing.T) {
for _, test := range uriTests {
uri, err := sip.ParseURI(test.s)
if err != nil {
if test.err == nil {
t.Errorf("%v", err)
continue
} else { // test was supposed to fail
panic("TODO(jart): Implement failing support.")
}
}
if !reflect.DeepEqual(&test.uri, uri) {
t.Errorf("%#v != %#v", &test.uri, uri)
}
}
}
func TestFormat(t *testing.T) {
for _, test := range uriTests {
uri := test.uri.String()
s := test.s
if test.s2 != "" {
s = test.s2
}
if s != uri {
t.Error(test.s, "!=", uri)
}
}
}

+ 81
- 0
sip/util.go View File

@ -0,0 +1,81 @@
package sip
import (
"github.com/jart/gosip/util"
"strconv"
"strings"
)
func extractHostPort(s string) (s2, host string, port uint16, err error) {
port = 5060
if s == "" {
err = URIMissingHost
} else {
if s[0] == '[' { // quoted/ipv6: sip:[dead:beef::666]:5060
n := strings.Index(s, "]")
if n < 0 {
err = URIMissingHost
}
host, s = s[1:n], s[n+1:]
if s != "" && s[0] == ':' { // we has a port too
s = s[1:]
s, port, err = extractPort(s)
}
} else { // non-quoted host: sip:1.2.3.4:5060
switch n := strings.IndexAny(s, delims); {
case n < 0:
host, s = s, ""
case s[n] == ':': // host:port
host, s = s[0:n], s[n+1:]
s, port, err = extractPort(s)
default:
host, s = s[0:n], s[n:]
}
}
}
return s, host, port, err
}
func parseParams(s string) (res Params) {
items := strings.Split(s, ";")
if items == nil || len(items) == 0 || items[0] == "" {
return
}
res = make(Params, len(items))
for _, item := range items {
if item == "" {
continue
}
n := strings.Index(item, "=")
var k, v string
if n > 0 {
k, v = item[0:n], item[n+1:]
} else {
k, v = item, ""
}
k, kerr := util.URLUnescape(k, false)
v, verr := util.URLUnescape(v, false)
if kerr != nil || verr != nil {
continue
}
res[k] = v
}
return res
}
func extractPort(s string) (s2 string, port uint16, err error) {
if n := strings.IndexAny(s, delims); n > 0 {
port, err = parsePort(s[0:n])
s = s[n:]
} else {
port, err = parsePort(s)
s = ""
}
return s, port, err
}
func parsePort(s string) (port uint16, err error) {
i, err := strconv.ParseUint(s, 10, 16)
port = uint16(i)
return
}

+ 149
- 0
sip/via.go View File

@ -0,0 +1,149 @@
// SIP Via Address Library
package sip
import (
"bytes"
"errors"
"github.com/jart/gosip/util"
"strconv"
"strings"
)
var (
ViaBadHeader = errors.New("bad via header")
ViaProtoBlank = errors.New("via.Proto blank")
)
// Example: SIP/2.0/UDP 1.2.3.4:5060;branch=z9hG4bK556f77e6.
type Via struct {
Next *Via // pointer to next via header if any
Version string // protocol version e.g. "2.0"
Proto string // transport type "UDP"
Host string // name or ip of egress interface
Port uint16 // network port number
Params Params // params like branch, received, rport, etc.
}
// Parses a single SIP Via header, provided the part that comes after "Via: ".
//
// Via headers are goofy; they're like a URI without a scheme, user, and pass.
// They tell about the network interfaces a packet traversed to reach us.
//
// EBNF: http://sofia-sip.sourceforge.net/refdocs/sip/group__sip__via.html
func ParseVia(s string) (via *Via, err error) {
if s[0:4] == "SIP/" {
s = s[4:]
if n1 := strings.Index(s, "/"); n1 > 0 {
if n2 := strings.Index(s, " "); n2 >= n1+3 {
via = new(Via)
via.Version = s[0:n1]
via.Proto = s[n1+1 : n2]
s, via.Host, via.Port, err = extractHostPort(s[n2+1:])
if err != nil {
return nil, err
}
if s != "" && s[0] == ';' {
via.Params = parseParams(s[1:])
s = ""
}
return via, nil
}
}
}
return nil, ViaBadHeader
}
func (via *Via) Append(b *bytes.Buffer) error {
if via.Version == "" {
via.Version = "2.0"
}
if via.Proto == "" {
via.Proto = "UDP"
}
if via.Port == 0 {
via.Port = 5060
}
if via.Host == "" {
return ViaProtoBlank
}
b.WriteString("SIP/")
b.WriteString(via.Version)
b.WriteString("/")
b.WriteString(via.Proto)
b.WriteString(" ")
b.WriteString(via.Host)
if via.Port != 5060 {
b.WriteString(":")
b.WriteString(strconv.Itoa(int(via.Port)))
}
via.Params.Append(b)
return nil
}
// deep copies a new Via object
func (via *Via) Copy() *Via {
if via == nil {
return nil
}
res := new(Via)
res.Version = via.Version
res.Proto = via.Proto
res.Host = via.Host
res.Port = via.Port
res.Params = via.Params.Copy()
res.Next = via.Next.Copy()
return res
}
// Sets newly generated branch ID and returns self.
func (via *Via) Branch() *Via {
via = via.Copy()
via.Params["branch"] = util.GenerateBranch()
return via
}
// Sets Next field and returns self.
func (via *Via) SetNext(next *Via) *Via {
via.Next = next
return via
}
// returns pointer to last via in linked list.
func (via *Via) Last() *Via {
if via != nil {
for ; via.Next != nil; via = via.Next {
}
}
return via
}
// returns true if version, proto, ip, port, and branch match
func (via *Via) Compare(other *Via) bool {
return (via.CompareAddr(other) && via.CompareBranch(other))
}
func (via *Via) CompareAddr(other *Via) bool {
if via != nil && other != nil {
if via.Version == other.Version &&
via.Proto == other.Proto &&
via.Host == other.Host &&
via.Port == other.Port {
return true
}
}
return false
}
func (via *Via) CompareBranch(other *Via) bool {
if via != nil && other != nil {
if b1, ok := via.Params["branch"]; ok {
if b2, ok := other.Params["branch"]; ok {
if b1 == b2 {
return true
}
}
}
}
return false
}

+ 166
- 0
util/escape.go View File

@ -0,0 +1,166 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// this code adapted from go/src/pkg/http/url.go for gosip because the
// darn public interface won't let me turn off doPlus :'(
package util
import (
"strconv"
)
type URLEscapeError string
func (e URLEscapeError) Error() string {
return "invalid URL escape " + strconv.Quote(string(e))
}
// Escape quoted stuff in SIP addresses.
func EscapeDisplay(s string) (res string) {
for _, c := range s {
switch c {
case '"':
res += "\\\""
case '\\':
res += "\\\\"
default:
res += string(c)
}
}
return
}
// Return true if the specified character should be escaped when
// appearing in a URL string, according to RFC 2396.
func shouldEscape(c byte) bool {
if c <= ' ' || c >= 0x7F {
return true
}
switch c {
case '<', '>', '#', '%', '"', // RFC 2396 delims
'{', '}', '|', '\\', '^', '[', ']', '`', // RFC2396 unwise
'?', '&', '=', '+': // RFC 2396 reserved in path
return true
}
return false
}
// urlUnescape is like URLUnescape but can be told not to
// convert + into space. URLUnescape implements what is
// called "URL encoding" but that only applies to query strings.
// Elsewhere in the URL, + does not mean space.
func URLUnescape(s string, doPlus bool) (string, error) {
// Count %, check that they're well-formed.
n := 0
hasPlus := false
for i := 0; i < len(s); {
switch s[i] {
case '%':
n++
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
s = s[i:]
if len(s) > 3 {
s = s[0:3]
}
return "", URLEscapeError(s)
}
i += 3
case '+':
hasPlus = doPlus
i++
default:
i++
}
}
if n == 0 && !hasPlus {
return s, nil
}
t := make([]byte, len(s)-2*n)
j := 0
for i := 0; i < len(s); {
switch s[i] {
case '%':
t[j] = unhex(s[i+1])<<4 | unhex(s[i+2])
j++
i += 3
case '+':
if doPlus {
t[j] = ' '
} else {
t[j] = '+'
}
j++
i++
default:
t[j] = s[i]
j++
i++
}
}
return string(t), nil
}
func URLEscape(s string, doPlus bool) string {
spaceCount, hexCount := 0, 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c) {
if c == ' ' && doPlus {
spaceCount++
} else {
hexCount++
}
}
}
if spaceCount == 0 && hexCount == 0 {
return s
}
t := make([]byte, len(s)+2*hexCount)
j := 0
for i := 0; i < len(s); i++ {
switch c := s[i]; {
case c == ' ' && doPlus:
t[j] = '+'
j++
case shouldEscape(c):
t[j] = '%'
t[j+1] = "0123456789abcdef"[c>>4]
t[j+2] = "0123456789abcdef"[c&15]
j += 3
default:
t[j] = s[i]
j++
}
}
return string(t)
}
func ishex(c byte) bool {
switch {
case '0' <= c && c <= '9':
return true
case 'a' <= c && c <= 'f':
return true
case 'A' <= c && c <= 'F':
return true
}
return false
}
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}

+ 93
- 0
util/util.go View File

@ -0,0 +1,93 @@
package util
import (
"encoding/hex"
"math/rand"
"net"
"strconv"
"strings"
)
// Returns true if IP contains a colon.
func IsIPv6(ip string) bool {
n := strings.Index(ip, ":")
return n >= 0
}
// Returns true if IPv4 and is a LAN address as defined by RFC 1918.
func IsIPPrivate(ip net.IP) bool {
if ip != nil {
ip = ip.To4()
if ip != nil {
switch ip[0] {
case 10:
return true
case 172:
if ip[1] >= 16 && ip[1] <= 31 {
return true
}
case 192:
if ip[1] == 168 {
return true
}
}
}
}
return false
}
func HostPortToString(host string, port uint16) (saddr string) {
sport := strconv.FormatInt(int64(port), 10)
if IsIPv6(host) {
saddr = "[" + host + "]:" + sport
} else {
saddr = host + ":" + sport
}
return saddr
}
// Generates a secure random number between 0 and 50,000.
func GenerateCSeq() int {
return rand.Int() % 50000
}
// Generates a 48-bit secure random string like: 27c97271d363.
func GenerateTag() string {
return hex.EncodeToString(randomBytes(6))
}
// This is used in the Via tag. Probably not suitable for use by stateless
// proxies.
func GenerateBranch() string {
return "z9hG4bK-" + GenerateTag()
}
// Generates a secure UUID4, e.g.f47ac10b-58cc-4372-a567-0e02b2c3d479
func GenerateCallID() string {
lol := randomBytes(15)
digs := hex.EncodeToString(lol)
uuid4 := digs[0:8] + "-" + digs[8:12] + "-4" + digs[12:15] +
"-a" + digs[15:18] + "-" + digs[18:]
return uuid4
}
func randomBytes(l int) (b []byte) {
b = make([]byte, l)
for i := 0; i < l; i++ {
b[i] = byte(rand.Int())
}
return
}
func append(buf []byte, s string) []byte {
lenb, lens := len(buf), len(s)
if lenb+lens <= cap(buf) {
buf = buf[0 : lenb+lens]
} else {
panic("mtu exceeded D:")
}
for i := 0; i < lens; i++ {
buf[lenb+i] = byte(s[i])
}
return buf
}

Loading…
Cancel
Save