commit f62ba4580683b9231f5608a097aa281961f16c1b from: Oliver Lowe date: Fri Mar 21 11:03:26 2025 UTC internal/sip: correctly format and write requests 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 -From: Alice ;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 ;tag=a6c85cf +From: Alice ;tag=1928301774 +Call-ID: a84b4c76e66710@pc33.example.com CSeq: 314159 INVITE -Contact: +Contact: 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) }