| @ -0,0 +1 @@ | |||
| *.test | |||
| @ -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) | |||
| @ -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) | |||
| @ -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 | |||
| @ -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 | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,356 @@ | |||
| // Example demonstrating how to make a phone call (SIP/SDP/RTP/DTMF) to an Echo | |||
| // application without any higher level SIP abstractions. | |||
| // | |||
| // Example Trace: | |||
| // | |||
| // send 765 bytes to udp/[10.11.34.37]:5060 at 15:46:48.569658: | |||
| // ------------------------------------------------------------------------ | |||
| // INVITE sip:10.11.34.37 SIP/2.0 | |||
| // Via: SIP/2.0/UDP 10.11.34.37:59516;rport;branch=z9hG4bKS308QB9UUpNyD | |||
| // Max-Forwards: 70 | |||
| // From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg | |||
| // To: <sip:10.11.34.37> | |||
| // Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf | |||
| // CSeq: 133097268 INVITE | |||
| // Contact: <sip:10.11.34.37:59516> | |||
| // User-Agent: tube/0.1 | |||
| // Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, PRACK, MESSAGE, SUBSCRIBE, NOTIFY, REFER, UPDATE, INFO | |||
| // Supported: timer, 100rel | |||
| // Allow-Events: talk | |||
| // Content-Type: application/sdp | |||
| // Content-Disposition: session | |||
| // Content-Length: 218 | |||
| // | |||
| // v=0 | |||
| // o=- 2862054018559638081 6057228511765453924 IN IP4 10.11.34.37 | |||
| // s=- | |||
| // c=IN IP4 10.11.34.37 | |||
| // t=0 0 | |||
| // m=audio 23448 RTP/AVP 0 101 | |||
| // a=rtpmap:0 PCMU/8000 | |||
| // a=rtpmap:101 telephone-event/8000 | |||
| // a=fmtp:101 0-16 | |||
| // a=ptime:20 | |||
| // ------------------------------------------------------------------------ | |||
| // recv 282 bytes from udp/[10.11.34.37]:5060 at 15:46:48.570890: | |||
| // ------------------------------------------------------------------------ | |||
| // SIP/2.0 100 Trying | |||
| // Via: SIP/2.0/UDP 10.11.34.37:59516;rport=59516;branch=z9hG4bKS308QB9UUpNyD | |||
| // From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg | |||
| // To: <sip:10.11.34.37> | |||
| // Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf | |||
| // CSeq: 133097268 INVITE | |||
| // User-Agent: tube/0.1 | |||
| // Content-Length: 0 | |||
| // | |||
| // ------------------------------------------------------------------------ | |||
| // recv 668 bytes from udp/[10.11.34.37]:5060 at 15:46:48.571844: | |||
| // ------------------------------------------------------------------------ | |||
| // SIP/2.0 200 OK | |||
| // Via: SIP/2.0/UDP 10.11.34.37:59516;rport=59516;branch=z9hG4bKS308QB9UUpNyD | |||
| // From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg | |||
| // To: <sip:10.11.34.37>;tag=a1vFUD7vvK4ZN | |||
| // Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf | |||
| // CSeq: 133097268 INVITE | |||
| // Contact: <sip:10.11.34.37> | |||
| // User-Agent: tube/0.1 | |||
| // Accept: application/sdp | |||
| // Allow: INVITE, ACK, BYE, CANCEL, OPTIONS | |||
| // Supported: timer | |||
| // Content-Type: application/sdp | |||
| // Content-Disposition: session | |||
| // Content-Length: 196 | |||
| // | |||
| // v=0 | |||
| // o=- 403551387931241779 5960509760717556241 IN IP4 10.11.34.37 | |||
| // s=- | |||
| // c=IN IP4 10.11.34.37 | |||
| // t=0 0 | |||
| // m=audio 19858 RTP/AVP 0 | |||
| // a=rtpmap:0 PCMU/8000 | |||
| // a=rtmap:97 speex/16000 | |||
| // a=rtmap:98 speex/8000 | |||
| // ------------------------------------------------------------------------ | |||
| // send 296 bytes to udp/[10.11.34.37]:5060 at 15:46:48.572320: | |||
| // ------------------------------------------------------------------------ | |||
| // ACK sip:10.11.34.37 SIP/2.0 | |||
| // Via: SIP/2.0/UDP 10.11.34.37:59516;rport;branch=z9hG4bKtct1S6SZrZBHS | |||
| // Max-Forwards: 70 | |||
| // From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg | |||
| // To: <sip:10.11.34.37>;tag=a1vFUD7vvK4ZN | |||
| // Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf | |||
| // CSeq: 133097268 ACK | |||
| // Content-Length: 0 | |||
| // | |||
| // ------------------------------------------------------------------------ | |||
| // send 1017 bytes to udp/[10.11.34.37]:5060 at 15:46:53.048617: | |||
| // ------------------------------------------------------------------------ | |||
| // BYE sip:10.11.34.37 SIP/2.0 | |||
| // Via: SIP/2.0/UDP 10.11.34.37:59516;rport;branch=z9hG4bKUNKtU1a3N813m | |||
| // Max-Forwards: 70 | |||
| // From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg | |||
| // To: <sip:10.11.34.37>;tag=a1vFUD7vvK4ZN | |||
| // Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf | |||
| // CSeq: 133097269 BYE | |||
| // User-Agent: tube/0.1 | |||
| // Content-Type: text/plain | |||
| // Content-Length: 671 | |||
| // | |||
| // .--. .--. | |||
| // .'(` / ..\ | |||
| // __.>\ '. _.---,._,' ____.' _o/ | |||
| // /.--. : |/' _.--.< '--. |.__ | |||
| // _..-' `\ /' `' _.-' /--' | |||
| // >_.-``-. `Y /' _.---._____ _.--' / | |||
| // '` .-''. \|: \.' ___, .-'` ~'--....___.-' | |||
| // .'--._ `-: \/ /' \\ | |||
| // /.'`\ :; /' `-. | |||
| // -` | | | |||
| // :.; : | thank you for flying tube | |||
| // |: | version o.1 | |||
| // | | | |||
| // :. : | besiyata dishmaya | |||
| // .jgs ; telecommunications inc. | |||
| // /:::. `\ | |||
| // ------------------------------------------------------------------------ | |||
| // recv 353 bytes from udp/[10.11.34.37]:5060 at 15:46:53.049140: | |||
| // ------------------------------------------------------------------------ | |||
| // SIP/2.0 200 OK | |||
| // Via: SIP/2.0/UDP 10.11.34.37:59516;rport=59516;branch=z9hG4bKUNKtU1a3N813m | |||
| // From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg | |||
| // To: <sip:10.11.34.37>;tag=a1vFUD7vvK4ZN | |||
| // Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf | |||
| // CSeq: 133097269 BYE | |||
| // User-Agent: tube/0.1 | |||
| // Allow: INVITE, ACK, BYE, CANCEL, OPTIONS | |||
| // Supported: timer | |||
| // Content-Length: 0 | |||
| // | |||
| // ------------------------------------------------------------------------ | |||
| // | |||
| package echo_test | |||
| import ( | |||
| "bytes" | |||
| "github.com/jart/gosip/rtp" | |||
| "github.com/jart/gosip/sdp" | |||
| "github.com/jart/gosip/sip" | |||
| "github.com/jart/gosip/util" | |||
| "log" | |||
| "math/rand" | |||
| "net" | |||
| "testing" | |||
| "time" | |||
| ) | |||
| func TestCallToEchoApp(t *testing.T) { | |||
| // Connect to the remote SIP UDP endpoint. | |||
| conn, err := net.Dial("udp", "127.0.0.1:5060") | |||
| if err != nil { | |||
| t.Error("sip dial:", err) | |||
| return | |||
| } | |||
| defer conn.Close() | |||
| raddr := conn.RemoteAddr().(*net.UDPAddr) | |||
| laddr := conn.LocalAddr().(*net.UDPAddr) | |||
| // Create an RTP socket. | |||
| rtpsock, err := net.ListenPacket("udp", "108.61.60.146:0") | |||
| if err != nil { | |||
| t.Error("rtp listen:", err) | |||
| return | |||
| } | |||
| defer rtpsock.Close() | |||
| rtpaddr := rtpsock.LocalAddr().(*net.UDPAddr) | |||
| // Create an invite message and attach the SDP. | |||
| invite := &sip.Msg{ | |||
| CallID: util.GenerateCallID(), | |||
| CSeq: util.GenerateCSeq(), | |||
| Method: "INVITE", | |||
| CSeqMethod: "INVITE", | |||
| Request: &sip.URI{ | |||
| Scheme: "sip", | |||
| User: "echo", | |||
| Host: raddr.IP.String(), | |||
| Port: uint16(raddr.Port), | |||
| }, | |||
| Via: &sip.Via{ | |||
| Host: laddr.IP.String(), | |||
| Port: uint16(laddr.Port), | |||
| Params: sip.Params{"branch": util.GenerateBranch()}, | |||
| }, | |||
| From: &sip.Addr{ | |||
| Display: "Echo Test", | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: laddr.IP.String(), | |||
| Port: uint16(laddr.Port), | |||
| }, | |||
| Params: sip.Params{"tag": util.GenerateTag()}, | |||
| }, | |||
| To: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: raddr.IP.String(), | |||
| Port: uint16(raddr.Port), | |||
| }, | |||
| }, | |||
| Contact: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: laddr.IP.String(), | |||
| Port: uint16(laddr.Port), | |||
| }, | |||
| }, | |||
| Headers: sip.Headers{ | |||
| "Content-Type": "application/sdp", | |||
| "User-Agent": "gosip/1.o", | |||
| }, | |||
| Payload: sdp.New(rtpaddr, sdp.ULAWCodec, sdp.DTMFCodec).String(), | |||
| } | |||
| // Turn invite message into a packet and send via UDP socket. | |||
| var b bytes.Buffer | |||
| invite.Append(&b) | |||
| log.Printf(">>> %s\n%s\n", raddr, b.String()) | |||
| if amt, err := conn.Write(b.Bytes()); err != nil || amt != b.Len() { | |||
| t.Fatal(err) | |||
| } | |||
| // Receive provisional 100 Trying. | |||
| conn.SetDeadline(time.Now().Add(time.Second)) | |||
| memory := make([]byte, 2048) | |||
| amt, err := conn.Read(memory) | |||
| if err != nil { | |||
| t.Fatal("read 100 trying:", err) | |||
| } | |||
| log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt])) | |||
| msg, err := sip.ParseMsg(string(memory[0:amt])) | |||
| if err != nil { | |||
| t.Fatal("parse 100 trying", err) | |||
| } | |||
| if !msg.IsResponse || msg.Status != 100 || msg.Phrase != "Trying" { | |||
| t.Fatal("didn't get 100 trying :[") | |||
| } | |||
| // Receive 200 OK. | |||
| conn.SetDeadline(time.Now().Add(5 * time.Second)) | |||
| amt, err = conn.Read(memory) | |||
| if err != nil { | |||
| t.Fatal("read 200 ok:", err) | |||
| } | |||
| log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt])) | |||
| msg, err = sip.ParseMsg(string(memory[0:amt])) | |||
| if err != nil { | |||
| t.Fatal("parse 200 ok:", err) | |||
| } | |||
| if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" { | |||
| t.Fatal("wanted 200 ok but got:", msg.Status, msg.Phrase) | |||
| } | |||
| if msg.Payload == "" || msg.Headers["Content-Type"] != "application/sdp" { | |||
| t.Fatal("200 ok didn't have sdp payload") | |||
| } | |||
| // Figure out where they want us to send RTP. | |||
| rsdp, err := sdp.Parse(msg.Payload) | |||
| if err != nil { | |||
| t.Fatal("failed to parse sdp", err) | |||
| } | |||
| rrtpaddr := &net.UDPAddr{IP: net.ParseIP(rsdp.Addr), Port: int(rsdp.Audio.Port)} | |||
| // Acknowledge the 200 OK to answer the call. | |||
| var ack sip.Msg | |||
| ack.Request = invite.Request | |||
| ack.From = msg.From | |||
| ack.To = msg.To | |||
| ack.CallID = msg.CallID | |||
| ack.Method = "ACK" | |||
| ack.CSeq = msg.CSeq | |||
| ack.CSeqMethod = "ACK" | |||
| ack.Via = msg.Via | |||
| b.Reset() | |||
| ack.Append(&b) | |||
| if amt, err := conn.Write(b.Bytes()); err != nil || amt != b.Len() { | |||
| t.Fatal(err) | |||
| } | |||
| // Send RTP packets containing junk until we get an echo response. | |||
| quit := make(chan bool) | |||
| go func() { | |||
| frameout := make([]byte, rtp.HeaderSize+160) | |||
| rtpHeader := rtp.Header{ | |||
| PT: sdp.ULAWCodec.PT, | |||
| Seq: 666, | |||
| TS: 0, | |||
| Ssrc: rand.Uint32(), | |||
| } | |||
| for n := 0; n < 160; n++ { | |||
| frameout[rtp.HeaderSize+n] = byte(n) | |||
| } | |||
| ticker := time.NewTicker(20 * time.Millisecond) | |||
| defer ticker.Stop() | |||
| for { | |||
| select { | |||
| case <-ticker.C: | |||
| rtpHeader.Write(frameout) | |||
| rtpHeader.TS += 160 | |||
| rtpHeader.Seq++ | |||
| amt, err = rtpsock.WriteTo(frameout, rrtpaddr) | |||
| if err != nil { | |||
| t.Fatal("rtp write", err) | |||
| } | |||
| case <-quit: | |||
| return | |||
| } | |||
| } | |||
| }() | |||
| defer func() { quit <- true }() | |||
| // We're talking to an echo application so they should send us back exactly | |||
| // the same audio. | |||
| rtpsock.SetDeadline(time.Now().Add(5 * time.Second)) | |||
| amt, _, err = rtpsock.ReadFrom(memory) | |||
| if err != nil { | |||
| t.Fatal("rtp read", err) | |||
| } | |||
| if amt != rtp.HeaderSize+160 { | |||
| t.Fatal("rtp recv amt != 12+160") | |||
| } | |||
| var rtpHeader rtp.Header | |||
| err = rtpHeader.Read(memory) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| for n := 0; n < 160; n++ { | |||
| if memory[rtp.HeaderSize+n] != byte(n) { | |||
| t.Fatal("rtp response audio didnt match") | |||
| } | |||
| } | |||
| // Hangup (we'll be lazy and just change up the ack Msg) | |||
| ack.Method = "BYE" | |||
| ack.CSeqMethod = "BYE" | |||
| ack.CSeq++ | |||
| b.Reset() | |||
| ack.Append(&b) | |||
| amt, err = conn.Write(b.Bytes()) | |||
| if err != nil || amt != b.Len() { | |||
| t.Fatal(err) | |||
| } | |||
| // Wait for acknowledgment of hangup. | |||
| conn.SetDeadline(time.Now().Add(time.Second)) | |||
| amt, err = conn.Read(memory) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| msg, err = sip.ParseMsg(string(memory[0:amt])) | |||
| if err != nil { | |||
| t.Fatal(err) | |||
| } | |||
| if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" { | |||
| t.Fatal("wanted bye response 200 ok but got:", msg.Status, msg.Phrase) | |||
| } | |||
| } | |||
| @ -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) | |||
| } | |||
| } | |||
| @ -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) | |||
| } | |||
| } | |||
| @ -0,0 +1,83 @@ | |||
| // This code demonstrates how SIP works without the zillion layers of | |||
| // sugar coating. | |||
| package rawsip_test | |||
| import ( | |||
| "github.com/jart/gosip/util" | |||
| "net" | |||
| "strconv" | |||
| "strings" | |||
| "testing" | |||
| "time" | |||
| ) | |||
| // An 'OPTIONS' message is used to: | |||
| // | |||
| // - Ping a server to see if it's alive. | |||
| // - Keep a connection alive in nat situations. | |||
| // - Ask a user agent what features they support. | |||
| // | |||
| func TestRawSIPOptions(t *testing.T) { | |||
| // create a new udp socket bound to a random port and "connect" | |||
| // the socket to the remote address | |||
| raddr := "127.0.0.1:5060" | |||
| conn, err := net.Dial("udp", raddr) | |||
| if err != nil { | |||
| t.Error(err) | |||
| return | |||
| } | |||
| // What local ip/port binding did the kernel choose for us? | |||
| laddr := conn.LocalAddr().String() | |||
| // Construct a SIP message. | |||
| cseq := util.GenerateCSeq() | |||
| fromtag := util.GenerateTag() | |||
| callid := util.GenerateCallID() | |||
| packet := "" + | |||
| "OPTIONS sip:echo@" + laddr + " SIP/2.0\r\n" + | |||
| "Via: SIP/2.0/UDP " + laddr + "\r\n" + | |||
| "Max-Forwards: 70\r\n" + | |||
| "To: <sip:" + raddr + ">\r\n" + | |||
| "From: <sip:" + laddr + ">;tag=" + fromtag + "\r\n" + | |||
| "Call-ID: " + callid + "\r\n" + | |||
| "CSeq: " + strconv.Itoa(cseq) + " OPTIONS\r\n" + | |||
| "Contact: <sip:" + laddr + ">\r\n" + | |||
| "User-Agent: pokémon/1.o\r\n" + | |||
| "Accept: application/sdp\r\n" + | |||
| "Content-Length: 0\r\n" + | |||
| "\r\n" | |||
| // Transmit our message. | |||
| bpacket := []uint8(packet) | |||
| amt, err := conn.Write(bpacket) | |||
| if err != nil || amt != len(bpacket) { | |||
| t.Error(err) | |||
| return | |||
| } | |||
| // Wait no longer than a second for them to get back to us. | |||
| err = conn.SetDeadline(time.Now().Add(time.Second)) | |||
| if err != nil { | |||
| t.Error(err) | |||
| return | |||
| } | |||
| // Receive response. | |||
| buf := make([]byte, 2048) | |||
| amt, err = conn.Read(buf) | |||
| if err != nil { | |||
| t.Error(err) | |||
| return | |||
| } | |||
| response := buf[0:amt] | |||
| // Read response. | |||
| msg := string(response) | |||
| lines := strings.Split(msg, "\r\n") | |||
| if lines[0] != "SIP/2.0 200 OK" { | |||
| t.Errorf("not ok :[\n%s", msg) | |||
| return | |||
| } | |||
| } | |||
| @ -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 | |||
| } | |||
| @ -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]) | |||
| } | |||
| @ -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 | |||
| } | |||
| @ -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}, | |||
| } | |||
| ) | |||
| @ -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 | |||
| } | |||
| @ -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 | |||
| } | |||
| @ -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 | |||
| } | |||
| @ -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") | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,191 @@ | |||
| // SIP Address Library | |||
| // | |||
| // For example: | |||
| // | |||
| // "J.A. Roberts Tunney" <sip:jtunney@bsdtelecom.net;isup-oli=29>;tag=deadbeef | |||
| // | |||
| // Roughly equates to: | |||
| // | |||
| // {Display: "J.A. Roberts Tunney", | |||
| // Params: {"tag": "deadbeef"}, | |||
| // Uri: {Scheme: "sip", | |||
| // User: "jtunney", | |||
| // Pass: "", | |||
| // Host: "bsdtelecom.net", | |||
| // Port: "", | |||
| // Params: {"isup-oli": "29"}}} | |||
| // | |||
| package sip | |||
| import ( | |||
| "bytes" | |||
| "errors" | |||
| "github.com/jart/gosip/util" | |||
| "log" | |||
| "strings" | |||
| ) | |||
| // Represents a SIP Address Linked List | |||
| type Addr struct { | |||
| Uri *URI // never nil | |||
| Display string // blank if not specified | |||
| Params Params // these look like ;key=lol;rport;key=wut | |||
| Next *Addr // for comma separated lists of addresses | |||
| } | |||
| // Parses a SIP address. | |||
| func ParseAddr(s string) (addr *Addr, err error) { | |||
| addr = new(Addr) | |||
| l := len(s) | |||
| if l == 0 { | |||
| return nil, errors.New("empty addr") | |||
| } | |||
| // Extract display. | |||
| switch n := strings.IndexAny(s, "\"<"); { | |||
| case n < 0: | |||
| return nil, errors.New("invalid address") | |||
| case s[n] == '<': // Display is not quoted. | |||
| addr.Display, s = strings.Trim(s[0:n], " "), s[n+1:] | |||
| case s[n] == '"': // We found an opening quote. | |||
| s = s[n+1:] | |||
| LOL: | |||
| for s != "" { | |||
| switch s[0] { | |||
| case '"': // Closing quote. | |||
| s = s[1:] | |||
| break LOL | |||
| case '\\': // Escape sequence. | |||
| if len(s) < 2 { | |||
| return nil, errors.New("evil quote escape") | |||
| } | |||
| switch s[1] { | |||
| case '"': | |||
| addr.Display += "\"" | |||
| case '\\': | |||
| addr.Display += "\\" | |||
| } | |||
| s = s[2:] | |||
| default: // Generic character. | |||
| addr.Display += string(s[0]) | |||
| s = s[1:] | |||
| } | |||
| } | |||
| if s == "" { | |||
| return nil, errors.New("no closing quote in display") | |||
| } | |||
| for s != "" { | |||
| c := s[0] | |||
| s = s[1:] | |||
| if c == '<' { | |||
| break | |||
| } | |||
| } | |||
| } | |||
| if n := strings.Index(s, ">"); n > 0 { | |||
| addr.Uri, err = ParseURI(s[0:n]) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| s = s[n+1:] | |||
| } else { | |||
| addr.Uri, err = ParseURI(s) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| s = "" | |||
| } | |||
| // Extract semicolon delimited params. | |||
| if s != "" && s[0] == ';' { | |||
| addr.Params = parseParams(s[1:]) | |||
| s = "" | |||
| } | |||
| // Is there another address? | |||
| s = strings.TrimLeft(s, " \t") | |||
| if s != "" && s[0] == ',' { | |||
| s = strings.TrimLeft(s[1:], " \t") | |||
| if s != "" { | |||
| addr.Next, err = ParseAddr(s) | |||
| if err != nil { | |||
| log.Println("[NOTICE]", "dropping invalid bonus addr:", s, err) | |||
| } | |||
| } | |||
| } | |||
| return addr, nil | |||
| } | |||
| func (addr *Addr) String() string { | |||
| if addr == nil { | |||
| return "<nil>" | |||
| } | |||
| var b bytes.Buffer | |||
| addr.Append(&b) | |||
| return b.String() | |||
| } | |||
| // Returns self if non-nil, otherwise other. | |||
| func (addr *Addr) Or(other *Addr) *Addr { | |||
| if addr == nil { | |||
| return other | |||
| } | |||
| return addr | |||
| } | |||
| // Sets newly generated tag ID and returns self. | |||
| func (addr *Addr) Tag() *Addr { | |||
| addr = addr.Copy() | |||
| addr.Params["tag"] = util.GenerateTag() | |||
| return addr | |||
| } | |||
| // Reassembles a SIP address into a buffer. | |||
| func (addr *Addr) Append(b *bytes.Buffer) { | |||
| if addr.Display != "" { | |||
| b.WriteString("\"") | |||
| b.WriteString(util.EscapeDisplay(addr.Display)) | |||
| b.WriteString("\" ") | |||
| } | |||
| b.WriteString("<") | |||
| addr.Uri.Append(b) | |||
| b.WriteString(">") | |||
| addr.Params.Append(b) | |||
| if addr.Next != nil { | |||
| b.WriteString(", ") | |||
| addr.Next.Append(b) | |||
| } | |||
| } | |||
| // Deep copies a new Addr object. | |||
| func (addr *Addr) Copy() *Addr { | |||
| if addr == nil { | |||
| return nil | |||
| } | |||
| res := new(Addr) | |||
| res.Uri = addr.Uri.Copy() | |||
| res.Params = addr.Params.Copy() | |||
| res.Next = addr.Next.Copy() | |||
| return res | |||
| } | |||
| // Returns true if the host and port match. If a username is present in | |||
| // `addr`, then the username is `other` must also match. | |||
| func (addr *Addr) Compare(other *Addr) bool { | |||
| if addr != nil && other != nil { | |||
| return addr.Uri.Compare(other.Uri) | |||
| } | |||
| return false | |||
| } | |||
| // Returns pointer to last addr in linked list. | |||
| func (addr *Addr) Last() *Addr { | |||
| if addr != nil { | |||
| for ; addr.Next != nil; addr = addr.Next { | |||
| } | |||
| } | |||
| return addr | |||
| } | |||
| @ -0,0 +1,164 @@ | |||
| package sip_test | |||
| import ( | |||
| "github.com/jart/gosip/sip" | |||
| "reflect" | |||
| "testing" | |||
| ) | |||
| type addrTest struct { | |||
| s string | |||
| s2 string | |||
| addr sip.Addr | |||
| err error | |||
| } | |||
| var addrTests = []addrTest{ | |||
| addrTest{ | |||
| s: "<sip:pokémon.net>", | |||
| s2: "<sip:pok%c3%a9mon.net>", | |||
| addr: sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "pokémon.net", | |||
| Port: 5060, | |||
| }, | |||
| }, | |||
| }, | |||
| addrTest{ | |||
| s: "<sip:brave@toaster.net;isup-oli=29>;tag=deadbeef", | |||
| addr: sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| User: "brave", | |||
| Host: "toaster.net", | |||
| Port: 5060, | |||
| Params: sip.Params{ | |||
| "isup-oli": "29", | |||
| }, | |||
| }, | |||
| Params: sip.Params{ | |||
| "tag": "deadbeef", | |||
| }, | |||
| }, | |||
| }, | |||
| addrTest{ | |||
| s: `<sip:pokemon.com>, "Ditto" <sip:ditto@pokemon.com>`, | |||
| addr: sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "pokemon.com", | |||
| Port: 5060, | |||
| }, | |||
| Next: &sip.Addr{ | |||
| Display: "Ditto", | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| User: "ditto", | |||
| Host: "pokemon.com", | |||
| Port: 5060, | |||
| }, | |||
| }, | |||
| }, | |||
| }, | |||
| addrTest{ | |||
| s: `<sip:1.2.3.4>, <sip:1.2.3.5>, <sip:[666::dead:beef]>`, | |||
| addr: sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "1.2.3.4", | |||
| Port: 5060, | |||
| }, | |||
| Next: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "1.2.3.5", | |||
| Port: 5060, | |||
| }, | |||
| Next: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "666::dead:beef", | |||
| Port: 5060, | |||
| }, | |||
| }, | |||
| }, | |||
| }, | |||
| }, | |||
| addrTest{ | |||
| s: " hello kitty <sip:jtunney@bsdtelecom.net;isup-oli=29>;tag=deadbeef", | |||
| s2: "\"hello kitty\" <sip:jtunney@bsdtelecom.net;isup-oli=29>;tag=deadbeef", | |||
| addr: sip.Addr{ | |||
| Display: "hello kitty", | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| User: "jtunney", | |||
| Host: "bsdtelecom.net", | |||
| Port: 5060, | |||
| Params: sip.Params{ | |||
| "isup-oli": "29", | |||
| }, | |||
| }, | |||
| Params: sip.Params{ | |||
| "tag": "deadbeef", | |||
| }, | |||
| }, | |||
| }, | |||
| addrTest{ | |||
| s: "\"\\\"\\\"Justine \\\\Tunney \" " + | |||
| "<sip:jtunney@bsdtelecom.net;isup-oli=29>;tag=deadbeef", | |||
| addr: sip.Addr{ | |||
| Display: "\"\"Justine \\Tunney ", | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| User: "jtunney", | |||
| Host: "bsdtelecom.net", | |||
| Port: 5060, | |||
| Params: sip.Params{ | |||
| "isup-oli": "29", | |||
| }, | |||
| }, | |||
| Params: sip.Params{ | |||
| "tag": "deadbeef", | |||
| }, | |||
| }, | |||
| }, | |||
| } | |||
| func TestParseAddr(t *testing.T) { | |||
| for _, test := range addrTests { | |||
| addr, err := sip.ParseAddr(test.s) | |||
| if err != nil { | |||
| if test.err == nil { | |||
| t.Error(err) | |||
| continue | |||
| } else { // Test was supposed to fail. | |||
| panic("TODO(jart): Implement failing support.") | |||
| } | |||
| } | |||
| if !reflect.DeepEqual(&test.addr, addr) { | |||
| t.Errorf("%#v != %#v", &test.addr, addr) | |||
| } | |||
| } | |||
| } | |||
| func TestAddrString(t *testing.T) { | |||
| for _, test := range addrTests { | |||
| addr := test.addr.String() | |||
| var s string | |||
| if test.s2 != "" { | |||
| s = test.s2 | |||
| } else { | |||
| s = test.s | |||
| } | |||
| if s != addr { | |||
| t.Error(s, "!=", addr) | |||
| } | |||
| } | |||
| } | |||
| @ -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() | |||
| } | |||
| @ -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 | |||
| } | |||
| @ -0,0 +1,214 @@ | |||
| package sip_test | |||
| import ( | |||
| "github.com/jart/gosip/sip" | |||
| "reflect" | |||
| "testing" | |||
| ) | |||
| type msgTest struct { | |||
| name string | |||
| s string | |||
| msg sip.Msg | |||
| err error | |||
| } | |||
| var msgTests = []msgTest{ | |||
| msgTest{ | |||
| name: "OPTIONS", | |||
| s: "OPTIONS sip:10.11.34.37:42367 SIP/2.0\r\n" + | |||
| "Via: SIP/2.0/UDP 10.11.34.37:42367;rport;branch=9dc39c3c3e84\r\n" + | |||
| "Max-Forwards: 60\r\n" + | |||
| "To: <sip:10.11.34.37:5060>\r\n" + | |||
| "From: <sip:10.11.34.37:42367;laffo>;tag=11917cbc0513\r\n" + | |||
| "Call-ID: e71a163e-c440-474d-a4ec-5cd85a0309c6\r\n" + | |||
| "CSeq: 36612 OPTIONS\r\n" + | |||
| "Contact: <sip:10.11.34.37:42367>\r\n" + | |||
| "User-Agent: ghoul/0.1\r\n" + | |||
| "Accept: application/sdp\r\n" + | |||
| "Content-Length: 0\r\n" + | |||
| "\r\n", | |||
| msg: sip.Msg{ | |||
| Method: "OPTIONS", | |||
| CSeqMethod: "OPTIONS", | |||
| MaxForwards: 60, | |||
| CallID: "e71a163e-c440-474d-a4ec-5cd85a0309c6", | |||
| CSeq: 36612, | |||
| Request: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.37", | |||
| Port: 42367, | |||
| }, | |||
| Via: &sip.Via{ | |||
| Version: "2.0", | |||
| Proto: "UDP", | |||
| Host: "10.11.34.37", | |||
| Port: 42367, | |||
| Params: sip.Params{"rport": "", "branch": "9dc39c3c3e84"}, | |||
| }, | |||
| To: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.37", | |||
| Port: 5060, | |||
| }, | |||
| }, | |||
| From: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.37", | |||
| Port: 42367, | |||
| Params: sip.Params{"laffo": ""}, | |||
| }, | |||
| Params: sip.Params{"tag": "11917cbc0513"}, | |||
| }, | |||
| Contact: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.37", | |||
| Port: 42367, | |||
| }, | |||
| }, | |||
| Headers: map[string]string{ | |||
| "User-Agent": "ghoul/0.1", | |||
| "Accept": "application/sdp", | |||
| }, | |||
| }, | |||
| }, | |||
| msgTest{ | |||
| name: "INVITE", | |||
| s: "INVITE sip:10.11.34.37 SIP/2.0\r\n" + | |||
| "via: SIP/2.0/UDP 10.11.34.37:59516;rport;branch=z9hG4bKS308QB9UUpNyD\r\n" + | |||
| "Max-Forwards: 70\r\n" + | |||
| "From: <sip:10.11.34.37:59516>;tag=S1jX7UtK5Zerg\r\n" + | |||
| "To: <sip:10.11.34.37>\r\n" + | |||
| "Call-ID: 87704115-03b8-122e-08b5-001bfcce6bdf\r\n" + | |||
| "CSeq: 133097268 INVITE\r\n" + | |||
| "Contact: <sip:10.11.34.37:59516>\r\n" + | |||
| " <sip:10.11.34.38:59516>\r\n" + | |||
| "User-Agent: tube/0.1\r\n" + | |||
| "Allow: INVITE, ACK, BYE, CANCEL, OPTIONS, PRACK, MESSAGE, SUBSCRIBE, NOTIFY, REFER, UPDATE, INFO\r\n" + | |||
| "Supported: timer, 100rel\r\n" + | |||
| "Allow-Events: talk\r\n" + | |||
| "Content-Type: application/sdp\r\n" + | |||
| "Content-Disposition: session\r\n" + | |||
| "Content-Length: 218\r\n" + | |||
| "\r\n" + | |||
| "v=0\r\n" + | |||
| "o=- 2862054018559638081 6057228511765453924 IN IP4 10.11.34.37\r\n" + | |||
| "s=-\r\n" + | |||
| "c=IN IP4 10.11.34.37\r\n" + | |||
| "t=0 0\r\n" + | |||
| "m=audio 23448 RTP/AVP 0 101\r\n" + | |||
| "a=rtpmap:0 PCMU/8000\r\n" + | |||
| "a=rtpmap:101 telephone-event/8000\r\n" + | |||
| "a=fmtp:101 0-16\r\n" + | |||
| "a=ptime:20\r\n", | |||
| msg: sip.Msg{ | |||
| Method: "INVITE", | |||
| CSeqMethod: "INVITE", | |||
| MaxForwards: 70, | |||
| CallID: "87704115-03b8-122e-08b5-001bfcce6bdf", | |||
| CSeq: 133097268, | |||
| Request: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.37", | |||
| Port: 5060, | |||
| }, | |||
| Via: &sip.Via{ | |||
| Version: "2.0", | |||
| Proto: "UDP", | |||
| Host: "10.11.34.37", | |||
| Port: 59516, | |||
| Params: sip.Params{"rport": "", "branch": "z9hG4bKS308QB9UUpNyD"}, | |||
| }, | |||
| To: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.37", | |||
| Port: 5060, | |||
| }, | |||
| }, | |||
| From: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.37", | |||
| Port: 59516, | |||
| }, | |||
| Params: sip.Params{"tag": "S1jX7UtK5Zerg"}, | |||
| }, | |||
| Contact: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.37", | |||
| Port: 59516, | |||
| }, | |||
| Next: &sip.Addr{ | |||
| Uri: &sip.URI{ | |||
| Scheme: "sip", | |||
| Host: "10.11.34.38", | |||
| Port: 59516, | |||
| }, | |||
| }, | |||
| }, | |||
| Headers: map[string]string{ | |||
| "User-Agent": "tube/0.1", | |||
| "Allow": "INVITE, ACK, BYE, CANCEL, OPTIONS, PRACK, MESSAGE, SUBSCRIBE, NOTIFY, REFER, UPDATE, INFO", | |||
| "Allow-Events": "talk", | |||
| "Content-Disposition": "session", | |||
| "Supported": "timer, 100rel", | |||
| "Content-Type": "application/sdp", | |||
| }, | |||
| Payload: "v=0\r\n" + | |||
| "o=- 2862054018559638081 6057228511765453924 IN IP4 10.11.34.37\r\n" + | |||
| "s=-\r\n" + | |||
| "c=IN IP4 10.11.34.37\r\n" + | |||
| "t=0 0\r\n" + | |||
| "m=audio 23448 RTP/AVP 0 101\r\n" + | |||
| "a=rtpmap:0 PCMU/8000\r\n" + | |||
| "a=rtpmap:101 telephone-event/8000\r\n" + | |||
| "a=fmtp:101 0-16\r\n" + | |||
| "a=ptime:20\r\n", | |||
| }, | |||
| }, | |||
| } | |||
| func TestParseMsg(t *testing.T) { | |||
| for _, test := range msgTests { | |||
| msg, err := sip.ParseMsg(test.s) | |||
| if err != nil { | |||
| if test.err == nil { | |||
| t.Errorf("%v", err) | |||
| continue | |||
| } else { // test was supposed to fail | |||
| panic("TODO(jart): Implement failing support.") | |||
| } | |||
| } | |||
| if !reflect.DeepEqual(&test.msg, msg) { | |||
| t.Errorf("Message:\n%#v !=\n%#v", &test.msg, msg) | |||
| if !reflect.DeepEqual(test.msg.Payload, msg.Payload) { | |||
| t.Errorf("Payload:\n%#v !=\n%#v", test.msg.Payload, msg.Payload) | |||
| } | |||
| if !reflect.DeepEqual(test.msg.Headers, msg.Headers) { | |||
| t.Errorf("Headers:\n%#v !=\n%#v", test.msg.Headers, msg.Headers) | |||
| } | |||
| if !reflect.DeepEqual(test.msg.Via, msg.Via) { | |||
| t.Errorf("Via:\n%#v !=\n%#v", test.msg.Via, msg.Via) | |||
| } | |||
| if !reflect.DeepEqual(test.msg.Request, msg.Request) { | |||
| t.Errorf("Request:\n%#v !=\n%#v", test.msg.Request, msg.Request) | |||
| } | |||
| if !reflect.DeepEqual(test.msg.To, msg.To) { | |||
| t.Errorf("To:\n%#v !=\n%#v", test.msg.To, msg.To) | |||
| } | |||
| if !reflect.DeepEqual(test.msg.From, msg.From) { | |||
| t.Errorf("From:\n%#v !=\n%#v", test.msg.From, msg.From) | |||
| } | |||
| if !reflect.DeepEqual(test.msg.Contact, msg.Contact) { | |||
| t.Errorf("Contact:\n%#v !=\n%#v", test.msg.Contact, msg.Contact) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @ -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", | |||
| } | |||
| @ -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") | |||
| ) | |||
| @ -0,0 +1,291 @@ | |||
| // SIP Transport Layer. Responsible for serializing messages to/from | |||
| // your network. | |||
| package sip | |||
| import ( | |||
| "bytes" | |||
| "errors" | |||
| "flag" | |||
| "fmt" | |||
| "github.com/jart/gosip/util" | |||
| "log" | |||
| "net" | |||
| "strconv" | |||
| "strings" | |||
| "time" | |||
| ) | |||
| var ( | |||
| tracing = flag.Bool("tracing", false, "Enable SIP message tracing") | |||
| timestampTagging = flag.Bool("timestampTagging", false, "Add microsecond timestamps to Via tags") | |||
| ) | |||
| // Transport defines any object capable of sending and receiving SIP messages. | |||
| // Such objects are responsible for their own reliability. This means checking | |||
| // network errors, raising alarms, rebinding sockets, etc. | |||
| type Transport interface { | |||
| // Sends a SIP message. Will not modify msg. | |||
| Send(msg *Msg) error | |||
| // Receives a SIP message. Must only be called by one goroutine. The received | |||
| // address is injected into the first Via header as the "received" param. | |||
| Recv() (msg *Msg, err error) | |||
| // Closes underlying resources. Please make sure all calls using this | |||
| // transport complete first. | |||
| Close() error | |||
| // When you send an outbound request (not a response) you have to set the via | |||
| // tag: ``msg.Via = tport.Via().SetBranch().SetNext(msg.Via)``. The details | |||
| // of the branch parameter... are tricky. | |||
| Via() *Via | |||
| // Returns a linked list of all canonical names and/or IP addresses that may | |||
| // be used to contact *this specific* transport. | |||
| Contact() *Addr | |||
| } | |||
| // Transport implementation that serializes messages to/from a UDP socket. | |||
| type udpTransport struct { | |||
| sock *net.UDPConn // thing returned by ListenUDP | |||
| addr *net.UDPAddr // handy for getting ip (contact might be host) | |||
| buf []byte // reusable memory for serialization | |||
| via *Via // who are we? | |||
| contact *Addr // uri that points to this specific transport | |||
| } | |||
| // Creates a new stateless network mechanism for transmitting and receiving SIP | |||
| // signalling messages. | |||
| // | |||
| // 'contact' is a SIP address, e.g. "<sip:1.2.3.4>", that tells how to bind | |||
| // sockets. This value is also used for contact headers which tell other | |||
| // user-agents where to send responses and hence should only contain an IP or | |||
| // canonical address. | |||
| func NewUDPTransport(contact *Addr) (tp Transport, err error) { | |||
| saddr := util.HostPortToString(contact.Uri.Host, contact.Uri.Port) | |||
| sock, err := net.ListenPacket("udp", saddr) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| addr := sock.LocalAddr().(*net.UDPAddr) | |||
| contact = contact.Copy() | |||
| contact.Uri.Port = uint16(addr.Port) | |||
| contact.Uri.Params["transport"] = addr.Network() | |||
| return &udpTransport{ | |||
| sock: sock.(*net.UDPConn), | |||
| addr: addr, | |||
| buf: make([]byte, 2048), | |||
| contact: contact, | |||
| via: &Via{ | |||
| Version: "2.0", | |||
| Proto: strings.ToUpper(addr.Network()), | |||
| Host: contact.Uri.Host, | |||
| Port: contact.Uri.Port, | |||
| }, | |||
| }, nil | |||
| } | |||
| func (tp *udpTransport) Send(msg *Msg) error { | |||
| msg, saddr, err := tp.route(msg) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| addr, err := net.ResolveUDPAddr("ip", saddr) | |||
| if err != nil { | |||
| return errors.New(fmt.Sprintf( | |||
| "udpTransport(%s) failed to resolve %s: %s", tp.addr, saddr, err)) | |||
| } | |||
| msg.MaxForwards-- | |||
| ts := time.Now() | |||
| addTimestamp(msg, ts) | |||
| var b bytes.Buffer | |||
| msg.Append(&b) | |||
| if *tracing { | |||
| tp.trace("send", b.String(), addr, ts) | |||
| } | |||
| _, err = tp.sock.WriteTo(b.Bytes(), addr) | |||
| if err != nil { | |||
| return errors.New(fmt.Sprintf( | |||
| "udpTransport(%s) write failed: %s", tp.addr, err)) | |||
| } | |||
| return nil | |||
| } | |||
| func (tp *udpTransport) Recv() (msg *Msg, err error) { | |||
| for { | |||
| amt, addr, err := tp.sock.ReadFromUDP(tp.buf) | |||
| if err != nil { | |||
| return nil, errors.New(fmt.Sprintf( | |||
| "udpTransport(%s) read failed: %s", tp.addr, err)) | |||
| } | |||
| ts := time.Now() | |||
| packet := string(tp.buf[0:amt]) | |||
| if *tracing { | |||
| tp.trace("recv", packet, addr, ts) | |||
| } | |||
| // Validation: http://tools.ietf.org/html/rfc3261#section-16.3 | |||
| msg, err = ParseMsg(packet) | |||
| if err != nil { | |||
| log.Printf("udpTransport(%s) got bad message from %s: %s\n%s", tp.addr, addr, err, packet) | |||
| continue | |||
| } | |||
| if msg.Via.Host != addr.IP.String() || int(msg.Via.Port) != addr.Port { | |||
| msg.Via.Params["received"] = addr.String() | |||
| } | |||
| addTimestamp(msg, ts) | |||
| if !tp.sanityCheck(msg) { | |||
| continue | |||
| } | |||
| tp.preprocess(msg) | |||
| break | |||
| } | |||
| return msg, nil | |||
| } | |||
| func (tp *udpTransport) Via() *Via { | |||
| return tp.via | |||
| } | |||
| func (tp *udpTransport) Contact() *Addr { | |||
| return tp.contact | |||
| } | |||
| func (tp *udpTransport) Close() error { | |||
| return tp.sock.Close() | |||
| } | |||
| func (tp *udpTransport) trace(dir, pkt string, addr net.Addr, t time.Time) { | |||
| size := len(pkt) | |||
| bar := strings.Repeat("-", 72) | |||
| suffix := "\n " | |||
| if pkt[len(pkt)-1] == '\n' { | |||
| suffix = "" | |||
| } | |||
| log.Printf( | |||
| "%s %d bytes to %s/%s at %s\n"+ | |||
| "%s\n"+ | |||
| "%s%s"+ | |||
| "%s\n", | |||
| dir, size, addr.Network(), addr.String(), | |||
| t.Format(time.RFC3339Nano), | |||
| bar, | |||
| strings.Replace(pkt, "\n", "\n ", -1), suffix, | |||
| bar) | |||
| } | |||
| // Test if this message is acceptable. | |||
| func (tp *udpTransport) sanityCheck(msg *Msg) bool { | |||
| if msg.MaxForwards <= 0 { | |||
| log.Printf("udpTransport(%s) froot loop detected\n%s", tp.addr, msg) | |||
| go tp.Send(NewResponse(msg, 483)) | |||
| return false | |||
| } | |||
| if msg.IsResponse { | |||
| if msg.Status >= 700 { | |||
| log.Printf("udpTransport(%s) msg has crazy status number\n%s", tp.addr, msg) | |||
| go tp.Send(NewResponse(msg, 400)) | |||
| return false | |||
| } | |||
| } else { | |||
| if msg.CSeqMethod == "" || msg.CSeqMethod != msg.Method { | |||
| log.Printf("udpTransport(%s) bad cseq number\n%s", tp.addr, msg) | |||
| go tp.Send(NewResponse(msg, 400)) | |||
| return false | |||
| } | |||
| } | |||
| return true | |||
| } | |||
| // Perform some ingress message mangling. | |||
| func (tp *udpTransport) preprocess(msg *Msg) { | |||
| if tp.contact.Compare(msg.Route) { | |||
| log.Printf("udpTransport(%s) removing our route header: %s", tp.addr, msg.Route) | |||
| msg.Route = msg.Route.Next | |||
| } | |||
| if _, ok := msg.Request.Params["lr"]; ok && msg.Route != nil && tp.contact.Uri.Compare(msg.Request) { | |||
| // RFC3261 16.4 Route Information Preprocessing | |||
| // RFC3261 16.12.1.2: Traversing a Strict-Routing Proxy | |||
| var oldReq, newReq *URI | |||
| if msg.Route.Next == nil { | |||
| oldReq, newReq = msg.Request, msg.Route.Uri | |||
| msg.Request = msg.Route.Uri | |||
| msg.Route = nil | |||
| } else { | |||
| seclast := msg.Route | |||
| for ; seclast.Next.Next != nil; seclast = seclast.Next { | |||
| } | |||
| oldReq, newReq = msg.Request, seclast.Next.Uri | |||
| msg.Request = seclast.Next.Uri | |||
| seclast.Next = nil | |||
| msg.Route.Last() | |||
| } | |||
| log.Printf("udpTransport(%s) fixing request uri after strict router traversal: %s -> %s", | |||
| tp.addr, oldReq, newReq) | |||
| } | |||
| } | |||
| func (tp *udpTransport) route(old *Msg) (msg *Msg, saddr string, err error) { | |||
| var host string | |||
| var port uint16 | |||
| msg = new(Msg) | |||
| *msg = *old // Shallow copy is sufficient. | |||
| if msg.IsResponse { | |||
| msg.Via = old.Via.Copy() | |||
| if msg.Via.CompareAddr(tp.via) { | |||
| // In proxy scenarios we have to remove our own Via. | |||
| msg.Via = msg.Via.Next | |||
| } | |||
| if msg.Via == nil { | |||
| return nil, "", errors.New("Ran out of Via headers when forwarding Response!") | |||
| } | |||
| if msg.Via != nil { | |||
| if received, ok := msg.Via.Params["received"]; ok { | |||
| return msg, received, nil | |||
| } else { | |||
| host, port = msg.Via.Host, msg.Via.Port | |||
| } | |||
| } else { | |||
| return nil, "", errors.New("Message missing Via header") | |||
| } | |||
| } else { | |||
| if msg.Request == nil { | |||
| return nil, "", errors.New("Missing request URI") | |||
| } | |||
| if !msg.Via.CompareAddr(tp.via) { | |||
| return nil, "", errors.New("You forgot to say: msg.Via = tp.Via(msg.Via)") | |||
| } | |||
| if msg.Route != nil { | |||
| if msg.Method == "REGISTER" { | |||
| return nil, "", errors.New("Don't loose route register requests") | |||
| } | |||
| if _, ok := msg.Route.Uri.Params["lr"]; ok { | |||
| // RFC3261 16.12.1.1 Basic SIP Trapezoid | |||
| route := msg.Route | |||
| msg.Route = msg.Route.Next | |||
| host, port = route.Uri.Host, route.Uri.Port | |||
| } else { | |||
| // RFC3261 16.12.1.2: Traversing a Strict-Routing Proxy | |||
| msg.Route = old.Route.Copy() | |||
| msg.Route.Last().Next = &Addr{Uri: msg.Request} | |||
| msg.Request = msg.Route.Uri | |||
| msg.Route = msg.Route.Next | |||
| host, port = msg.Request.Host, msg.Request.Port | |||
| } | |||
| } else { | |||
| host, port = msg.Request.Host, msg.Request.Port | |||
| } | |||
| } | |||
| if msg.OutboundProxy != "" { | |||
| saddr = msg.OutboundProxy | |||
| } else { | |||
| saddr = util.HostPortToString(host, port) | |||
| } | |||
| return | |||
| } | |||
| func addTimestamp(msg *Msg, ts time.Time) { | |||
| if *timestampTagging { | |||
| msg.Via.Params["µsi"] = strconv.FormatInt(ts.UnixNano()/int64(time.Microsecond), 10) | |||
| } | |||
| } | |||
| @ -0,0 +1,202 @@ | |||
| // SIP URI Library | |||
| // | |||
| // We can't use net.URL because it doesn't support SIP URIs. This is because: | |||
| // a) it doesn't support semicolon parameters; b) it doesn't extract the user | |||
| // and host information when the "//" isn't present. | |||
| // | |||
| // For example: | |||
| // | |||
| // jtunney@bsdtelecom.net;isup-oli=29 | |||
| // | |||
| // Roughly equates to: | |||
| // | |||
| // {Scheme: "sip", | |||
| // User: "jtunney", | |||
| // Pass: "", | |||
| // Host: "bsdtelecom.net", | |||
| // Port: "", | |||
| // Params: {"isup-oli": "29"}} | |||
| // | |||
| package sip | |||
| import ( | |||
| "bytes" | |||
| "errors" | |||
| "github.com/jart/gosip/util" | |||
| "strconv" | |||
| "strings" | |||
| ) | |||
| const ( | |||
| delims = ":/@;?#<>" | |||
| ) | |||
| var ( | |||
| URIEmpty = errors.New("empty uri") | |||
| URISchemeNotFound = errors.New("scheme not found") | |||
| URIMissingHost = errors.New("host missing") | |||
| URIBadPort = errors.New("invalid port number") | |||
| ) | |||
| type Params map[string]string | |||
| type URI struct { | |||
| Scheme string // sip, tel, etc. (never blank) | |||
| User string // sip:USER@host | |||
| Pass string // sip:user:PASS@host | |||
| Host string // example.com, 1.2.3.4, etc. | |||
| Port uint16 // 5060, 80, etc. | |||
| Params Params // semicolon delimited params after uris and addrs | |||
| } | |||
| // Parses a SIP URI. | |||
| func ParseURI(s string) (uri *URI, err error) { | |||
| uri = new(URI) | |||
| if s == "" { | |||
| return nil, URIEmpty | |||
| } | |||
| // Extract scheme. | |||
| n := strings.IndexAny(s, delims) | |||
| if n < 0 || s[n] != ':' { | |||
| return nil, URISchemeNotFound | |||
| } | |||
| uri.Scheme, s = s[0:n], s[n+1:] | |||
| // Extract user/pass. | |||
| n = strings.IndexAny(s, delims) | |||
| if n > 0 && s[n] == ':' { // sip:user:pass@host | |||
| // if next token isn't '@' then assume 'sip:host:port' | |||
| s2 := s[n+1:] | |||
| n2 := strings.IndexAny(s2, delims) | |||
| if n2 > 0 && s2[n2] == '@' { | |||
| uri.User = s[0:n] | |||
| s, n = s2, n2 | |||
| if n < 0 || s[n] != '@' { | |||
| return nil, URIMissingHost | |||
| } | |||
| uri.Pass, s = s[0:n], s[n+1:] | |||
| } | |||
| } else if n > 0 && s[n] == '@' { // user@host | |||
| uri.User, s = s[0:n], s[n+1:] | |||
| } | |||
| // Extract host/port. | |||
| s, uri.Host, uri.Port, err = extractHostPort(s) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| // Extract semicolon delimited params. | |||
| if s != "" && s[0] == ';' { | |||
| uri.Params = parseParams(s[1:]) | |||
| s = "" | |||
| } | |||
| // if s != "" { | |||
| // fmt.Fprintf(os.Stderr, "leftover data: %v\n", s) | |||
| // } | |||
| uri.User, err = util.URLUnescape(uri.User, false) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| uri.Pass, err = util.URLUnescape(uri.Pass, false) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| uri.Host, err = util.URLUnescape(uri.Host, false) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return uri, nil | |||
| } | |||
| // Deep copies a URI object. | |||
| func (uri *URI) Copy() *URI { | |||
| if uri == nil { | |||
| return nil | |||
| } | |||
| res := new(URI) | |||
| *res = *uri | |||
| res.Params = uri.Params.Copy() | |||
| return res | |||
| } | |||
| func (uri *URI) String() string { | |||
| if uri == nil { | |||
| return "<nil>" | |||
| } | |||
| var b bytes.Buffer | |||
| uri.Append(&b) | |||
| return b.String() | |||
| } | |||
| func (uri *URI) Append(b *bytes.Buffer) { | |||
| if uri.Scheme == "" { | |||
| uri.Scheme = "sip" | |||
| } | |||
| b.WriteString(uri.Scheme) | |||
| b.WriteString(":") | |||
| if uri.User != "" { | |||
| if uri.Pass != "" { | |||
| b.WriteString(util.URLEscape(uri.User, false)) | |||
| b.WriteString(":") | |||
| b.WriteString(util.URLEscape(uri.Pass, false)) | |||
| } else { | |||
| b.WriteString(util.URLEscape(uri.User, false)) | |||
| } | |||
| b.WriteString("@") | |||
| } | |||
| if util.IsIPv6(uri.Host) { | |||
| b.WriteString("[" + util.URLEscape(uri.Host, false) + "]") | |||
| } else { | |||
| b.WriteString(util.URLEscape(uri.Host, false)) | |||
| } | |||
| if uri.Port > 0 && uri.Port != 5060 { | |||
| b.WriteString(":" + strconv.FormatInt(int64(uri.Port), 10)) | |||
| } | |||
| uri.Params.Append(b) | |||
| } | |||
| // Returns true if scheme, host, and port match. if a username is present in | |||
| // `addr`, then the username is `other` must also match. | |||
| func (uri *URI) Compare(other *URI) bool { | |||
| if uri != nil && other != nil { | |||
| if uri.Scheme == other.Scheme && | |||
| uri.Host == other.Host && | |||
| uri.Port == other.Port { | |||
| if uri.User != "" { | |||
| if uri.User == other.User { | |||
| return true | |||
| } | |||
| } else { | |||
| return true | |||
| } | |||
| } | |||
| } | |||
| return false | |||
| } | |||
| func (params Params) Copy() Params { | |||
| res := make(Params, len(params)) | |||
| for k, v := range params { | |||
| res[k] = v | |||
| } | |||
| return res | |||
| } | |||
| func (params Params) Append(b *bytes.Buffer) { | |||
| if params != nil && len(params) > 0 { | |||
| for k, v := range params { | |||
| b.WriteString(";") | |||
| b.WriteString(util.URLEscape(k, false)) | |||
| if v != "" { | |||
| b.WriteString("=") | |||
| b.WriteString(util.URLEscape(v, false)) | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @ -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) | |||
| } | |||
| } | |||
| } | |||
| @ -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 | |||
| } | |||
| @ -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 | |||
| } | |||
| @ -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 | |||
| } | |||
| @ -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 | |||
| } | |||