Commit Diff


commit - 52f2be3f00f9c63b286a9b49366dca4622ddc923
commit + f62ba4580683b9231f5608a097aa281961f16c1b
blob - 6e7dccc0c25d795b0587fd76fadd09f64ed3cd12
blob + 5eb9d57db83a496f81461b8a7b8ec0e94fc78360
--- internal/sip/sip.go
+++ internal/sip/sip.go
@@ -1,7 +1,9 @@
+// Package sip ... SIP protocol as specified in RFC 3261.
 package sip
 
 import (
 	"bufio"
+	"bytes"
 	"fmt"
 	"io"
 	"net/textproto"
@@ -25,17 +27,26 @@ type Request struct {
 	Method string
 	URI    string
 
-	Proto      string
-	ProtoMajor int
-	ProtoMinor int
-
 	Header        textproto.MIMEHeader
 	ContentLength int64
+	ContentType   string
 	Sequence      int
+	Via           Via
 
 	Body io.Reader
 }
 
+const magicViaCookie = "z9hG4bK"
+
+type Via struct {
+	Address string
+	Branch  string
+}
+
+func (v Via) String() string {
+	return fmt.Sprintf("SIP/2.0/UDP %s;branch=%s%s", v.Address, magicViaCookie, v.Branch)
+}
+
 func ReadRequest(r io.Reader) (*Request, error) {
 	msg, err := readMessage(r)
 	if err != nil {
@@ -51,8 +62,6 @@ func parseRequest(msg *message) (*Request, error) {
 	if msg.startLine[2] != version {
 		return &req, fmt.Errorf("unknown version %q", msg.startLine[2])
 	}
-	req.Proto = version
-	req.ProtoMajor = 2
 
 	req.Header = msg.header
 	if s := req.Header.Get("Content-Length"); s != "" {
@@ -66,6 +75,42 @@ func parseRequest(msg *message) (*Request, error) {
 	return &req, nil
 }
 
+func WriteRequest(w io.Writer, req *Request) (n int64, err error) {
+	// section 8.1.1. We can set Max-Forwards automatically.
+	required := []string{"To", "From", "CSeq", "Call-ID"}
+	for _, s := range required {
+		if req.Header.Get(s) == "" {
+			return 0, fmt.Errorf("missing field %s in header", s)
+		}
+	}
+	if req.Header.Get("Max-Forwards") == "" {
+		// TODO(otl): find section in RFC recommending 70.
+		// section x.x.x
+		req.Header.Set("Max-Forwards", strconv.Itoa(70))
+	}
+	if req.ContentLength > 0 {
+		req.Header.Set("Content-Length", strconv.Itoa(int(req.ContentLength)))
+	}
+	req.Header.Set("Via", req.Via.String())
+
+	buf := &bytes.Buffer{}
+	fmt.Fprintf(buf, "%s %s SIP/2.0\r\n", req.Method, req.URI)
+	for k := range req.Header {
+		for _, v := range req.Header.Values(k) {
+			fmt.Fprintf(buf, "%s: %s\r\n", k, v)
+		}
+	}
+	buf.WriteString("\r\n")
+	n, err = io.Copy(w, buf)
+	if err != nil {
+		return n, err
+	}
+
+	nn, err := io.Copy(w, req.Body)
+	n += nn
+	return n, err
+}
+
 type message struct {
 	startLine [3]string
 	header    textproto.MIMEHeader
@@ -118,7 +163,32 @@ type Response struct {
 
 	Header        textproto.MIMEHeader
 	ContentLength int64
-	Sequence      CommandSequence
+	// Sequence      CommandSequence
 
-	Body io.ReadCloser
+	Body io.Reader
 }
+
+func parseResponse(msg *message) (*Response, error) {
+	if msg.startLine[0] != version {
+		return nil, fmt.Errorf("unknown version %s", msg.startLine[0])
+	}
+
+	var resp Response
+	var err error
+	resp.StatusCode, err = strconv.Atoi(msg.startLine[1])
+	if err != nil {
+		return nil, fmt.Errorf("bad status code %q: %v", msg.startLine[1], err)
+	}
+	resp.Status = msg.startLine[2]
+
+	resp.Header = msg.header
+	if s := resp.Header.Get("Content-Length"); s != "" {
+		n, err := strconv.Atoi(s)
+		if err != nil {
+			return &resp, fmt.Errorf("parse content-length: %w", err)
+		}
+		resp.ContentLength = int64(n)
+	}
+	resp.Body = msg.body
+	return &resp, nil
+}
blob - d4a8c7941abe026fe56be17d1a89b3dd8309c349
blob + ec1f4354adb5e24810959469771fb2258027319f
--- internal/sip/sip_test.go
+++ internal/sip/sip_test.go
@@ -1,26 +1,48 @@
 package sip
 
 import (
+	"fmt"
+	"os"
 	"strings"
 	"testing"
 )
 
-func TestReadRequest(t *testing.T) {
-	raw := `INVITE sip:bob@biloxi.com SIP/2.0
-Via: SIP/2.0/UDP pc33.atlanta.com;branch=z9hG4bK776asdhds
-Max-Forwards: 70
-To: Bob <sip:bob@biloxi.com>
-From: Alice <sip:alice@atlanta.com>;tag=1928301774
-Call-ID: a84b4c76e66710@pc33.atlanta.com
+func TestRequest(t *testing.T) {
+	f, err := os.Open("testdata/invite")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+	_, err = ReadRequest(f)
+	if err != nil {
+		t.Fatal("read request:", err)
+	}
+}
+
+func TestResponse(t *testing.T) {
+	raw := `SIP/2.0 200 OK
+Via: SIP/2.0/UDP server10.example.com
+   ;branch=z9hG4bKnashds8;received=192.0.2.3
+Via: SIP/2.0/UDP bigbox3.site3.example.com
+   ;branch=z9hG4bK77ef4c2312983.1;received=192.0.2.2
+Via: SIP/2.0/UDP pc33.example.com
+   ;branch=z9hG4bK776asdhds ;received=192.0.2.1
+To: Bob <sip:bob@example.com>;tag=a6c85cf
+From: Alice <sip:alice@example.com>;tag=1928301774
+Call-ID: a84b4c76e66710@pc33.example.com
 CSeq: 314159 INVITE
-Contact: <sip:alice@pc33.atlanta.com>
+Contact: <sip:bob@192.0.2.4>
 Content-Type: application/sdp
-Content-Length: 328
+Content-Length: 131
 
 ...`
-
-	_, err := ReadRequest(strings.NewReader(raw))
+	msg, err := readMessage(strings.NewReader(raw))
 	if err != nil {
-		t.Fatal("read request:", err)
+		t.Fatal("read message:", err)
 	}
+	resp, err := parseResponse(msg)
+	if err != nil {
+		t.Fatalf("parse response: %v", err)
+	}
+	fmt.Printf("%+v\n", resp)
 }