From 71498109e748f8c633e360f9694f3afecd781c8e Mon Sep 17 00:00:00 2001 From: Justine Alexandra Roberts Tunney Date: Mon, 22 Dec 2014 13:06:54 -0500 Subject: [PATCH] initial import --- .gitignore | 1 + README.md | 68 +++++ dsp/dsp.go | 15 ++ dsp/dsp_amd64.s | 70 +++++ dsp/dsp_test.go | 84 ++++++ example/echo/echo_test.go | 356 +++++++++++++++++++++++++ example/echo2/echo2_test.go | 177 +++++++++++++ example/options/options_test.go | 108 ++++++++ example/rawsip/rawsip_test.go | 83 ++++++ rtp/dtmf.go | 93 +++++++ rtp/rtp.go | 157 +++++++++++ sdp/codec.go | 41 +++ sdp/codecs.go | 62 +++++ sdp/media.go | 45 ++++ sdp/origin.go | 41 +++ sdp/sdp.go | 456 +++++++++++++++++++++++++++++++ sdp/sdp_test.go | 457 ++++++++++++++++++++++++++++++++ sip/addr.go | 191 +++++++++++++ sip/addr_test.go | 164 ++++++++++++ sip/messages.go | 111 ++++++++ sip/msg.go | 378 ++++++++++++++++++++++++++ sip/msg_test.go | 214 +++++++++++++++ sip/phrases.go | 96 +++++++ sip/prefs.go | 62 +++++ sip/transport.go | 291 ++++++++++++++++++++ sip/uri.go | 202 ++++++++++++++ sip/uri_test.go | 194 ++++++++++++++ sip/util.go | 81 ++++++ sip/via.go | 149 +++++++++++ util/escape.go | 166 ++++++++++++ util/util.go | 93 +++++++ 31 files changed, 4706 insertions(+) create mode 100644 .gitignore create mode 100755 README.md create mode 100755 dsp/dsp.go create mode 100755 dsp/dsp_amd64.s create mode 100755 dsp/dsp_test.go create mode 100755 example/echo/echo_test.go create mode 100755 example/echo2/echo2_test.go create mode 100755 example/options/options_test.go create mode 100755 example/rawsip/rawsip_test.go create mode 100755 rtp/dtmf.go create mode 100755 rtp/rtp.go create mode 100644 sdp/codec.go create mode 100755 sdp/codecs.go create mode 100644 sdp/media.go create mode 100644 sdp/origin.go create mode 100755 sdp/sdp.go create mode 100755 sdp/sdp_test.go create mode 100755 sip/addr.go create mode 100755 sip/addr_test.go create mode 100644 sip/messages.go create mode 100755 sip/msg.go create mode 100755 sip/msg_test.go create mode 100755 sip/phrases.go create mode 100755 sip/prefs.go create mode 100755 sip/transport.go create mode 100755 sip/uri.go create mode 100755 sip/uri_test.go create mode 100644 sip/util.go create mode 100755 sip/via.go create mode 100755 util/escape.go create mode 100755 util/util.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ed3b07 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.test diff --git a/README.md b/README.md new file mode 100755 index 0000000..23e4b53 --- /dev/null +++ b/README.md @@ -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) diff --git a/dsp/dsp.go b/dsp/dsp.go new file mode 100755 index 0000000..d82bc8c --- /dev/null +++ b/dsp/dsp.go @@ -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) diff --git a/dsp/dsp_amd64.s b/dsp/dsp_amd64.s new file mode 100755 index 0000000..5ead1e5 --- /dev/null +++ b/dsp/dsp_amd64.s @@ -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 diff --git a/dsp/dsp_test.go b/dsp/dsp_test.go new file mode 100755 index 0000000..d537734 --- /dev/null +++ b/dsp/dsp_test.go @@ -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 + } + } +} diff --git a/example/echo/echo_test.go b/example/echo/echo_test.go new file mode 100755 index 0000000..06dcb02 --- /dev/null +++ b/example/echo/echo_test.go @@ -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: ;tag=S1jX7UtK5Zerg +// To: +// Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf +// CSeq: 133097268 INVITE +// Contact: +// 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: ;tag=S1jX7UtK5Zerg +// To: +// 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: ;tag=S1jX7UtK5Zerg +// To: ;tag=a1vFUD7vvK4ZN +// Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf +// CSeq: 133097268 INVITE +// Contact: +// 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: ;tag=S1jX7UtK5Zerg +// To: ;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: ;tag=S1jX7UtK5Zerg +// To: ;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: ;tag=S1jX7UtK5Zerg +// To: ;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) + } +} diff --git a/example/echo2/echo2_test.go b/example/echo2/echo2_test.go new file mode 100755 index 0000000..01fa40e --- /dev/null +++ b/example/echo2/echo2_test.go @@ -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) + } +} diff --git a/example/options/options_test.go b/example/options/options_test.go new file mode 100755 index 0000000..77217c0 --- /dev/null +++ b/example/options/options_test.go @@ -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) + } +} diff --git a/example/rawsip/rawsip_test.go b/example/rawsip/rawsip_test.go new file mode 100755 index 0000000..be703c3 --- /dev/null +++ b/example/rawsip/rawsip_test.go @@ -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: \r\n" + + "From: ;tag=" + fromtag + "\r\n" + + "Call-ID: " + callid + "\r\n" + + "CSeq: " + strconv.Itoa(cseq) + " OPTIONS\r\n" + + "Contact: \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 + } +} diff --git a/rtp/dtmf.go b/rtp/dtmf.go new file mode 100755 index 0000000..2af4fa7 --- /dev/null +++ b/rtp/dtmf.go @@ -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 +} diff --git a/rtp/rtp.go b/rtp/rtp.go new file mode 100755 index 0000000..803d144 --- /dev/null +++ b/rtp/rtp.go @@ -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]) +} diff --git a/sdp/codec.go b/sdp/codec.go new file mode 100644 index 0000000..27a065c --- /dev/null +++ b/sdp/codec.go @@ -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 +} diff --git a/sdp/codecs.go b/sdp/codecs.go new file mode 100755 index 0000000..e89a800 --- /dev/null +++ b/sdp/codecs.go @@ -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}, + } +) diff --git a/sdp/media.go b/sdp/media.go new file mode 100644 index 0000000..44e73f9 --- /dev/null +++ b/sdp/media.go @@ -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 +} diff --git a/sdp/origin.go b/sdp/origin.go new file mode 100644 index 0000000..0109165 --- /dev/null +++ b/sdp/origin.go @@ -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 +} diff --git a/sdp/sdp.go b/sdp/sdp.go new file mode 100755 index 0000000..2858039 --- /dev/null +++ b/sdp/sdp.go @@ -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 +} diff --git a/sdp/sdp_test.go b/sdp/sdp_test.go new file mode 100755 index 0000000..7dc5d5d --- /dev/null +++ b/sdp/sdp_test.go @@ -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") + } + } +} diff --git a/sip/addr.go b/sip/addr.go new file mode 100755 index 0000000..0df57ad --- /dev/null +++ b/sip/addr.go @@ -0,0 +1,191 @@ +// SIP Address Library +// +// For example: +// +// "J.A. Roberts Tunney" ;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 "" + } + 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 +} diff --git a/sip/addr_test.go b/sip/addr_test.go new file mode 100755 index 0000000..f236c36 --- /dev/null +++ b/sip/addr_test.go @@ -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: "", + s2: "", + addr: sip.Addr{ + Uri: &sip.URI{ + Scheme: "sip", + Host: "pokémon.net", + Port: 5060, + }, + }, + }, + + addrTest{ + s: ";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: `, "Ditto" `, + 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: `, , `, + 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 ;tag=deadbeef", + s2: "\"hello kitty\" ;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 \" " + + ";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) + } + } +} diff --git a/sip/messages.go b/sip/messages.go new file mode 100644 index 0000000..56c7d3b --- /dev/null +++ b/sip/messages.go @@ -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() +} diff --git a/sip/msg.go b/sip/msg.go new file mode 100755 index 0000000..1acbecb --- /dev/null +++ b/sip/msg.go @@ -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 +} diff --git a/sip/msg_test.go b/sip/msg_test.go new file mode 100755 index 0000000..6d5ac92 --- /dev/null +++ b/sip/msg_test.go @@ -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: \r\n" + + "From: ;tag=11917cbc0513\r\n" + + "Call-ID: e71a163e-c440-474d-a4ec-5cd85a0309c6\r\n" + + "CSeq: 36612 OPTIONS\r\n" + + "Contact: \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: ;tag=S1jX7UtK5Zerg\r\n" + + "To: \r\n" + + "Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf\r\n" + + "CSeq: 133097268 INVITE\r\n" + + "Contact: \r\n" + + " \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) + } + } + } +} diff --git a/sip/phrases.go b/sip/phrases.go new file mode 100755 index 0000000..39bd1a5 --- /dev/null +++ b/sip/phrases.go @@ -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", +} diff --git a/sip/prefs.go b/sip/prefs.go new file mode 100755 index 0000000..c7118ed --- /dev/null +++ b/sip/prefs.go @@ -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") +) diff --git a/sip/transport.go b/sip/transport.go new file mode 100755 index 0000000..09f320b --- /dev/null +++ b/sip/transport.go @@ -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. "", 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) + } +} diff --git a/sip/uri.go b/sip/uri.go new file mode 100755 index 0000000..3c024d9 --- /dev/null +++ b/sip/uri.go @@ -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 "" + } + 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)) + } + } + } +} diff --git a/sip/uri_test.go b/sip/uri_test.go new file mode 100755 index 0000000..25af202 --- /dev/null +++ b/sip/uri_test.go @@ -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) + } + } +} diff --git a/sip/util.go b/sip/util.go new file mode 100644 index 0000000..042ade7 --- /dev/null +++ b/sip/util.go @@ -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 +} diff --git a/sip/via.go b/sip/via.go new file mode 100755 index 0000000..a960778 --- /dev/null +++ b/sip/via.go @@ -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 +} diff --git a/util/escape.go b/util/escape.go new file mode 100755 index 0000000..7502adf --- /dev/null +++ b/util/escape.go @@ -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 +} diff --git a/util/util.go b/util/util.go new file mode 100755 index 0000000..81bcaa2 --- /dev/null +++ b/util/util.go @@ -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 +}