Commit Diff


commit - c29a1e336e2176394997df419f8e06635db3eefe
commit + 52f2be3f00f9c63b286a9b49366dca4622ddc923
blob - /dev/null
blob + 6e7dccc0c25d795b0587fd76fadd09f64ed3cd12 (mode 644)
--- /dev/null
+++ internal/sip/sip.go
@@ -0,0 +1,124 @@
+package sip
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"net/textproto"
+	"strconv"
+	"strings"
+	"unicode"
+)
+
+const (
+	MethodRegister = "REGISTER"
+	MethodInvite   = "INVITE"
+	MethodAck      = "ACK"
+	MethodCancel   = "CANCEL"
+	MethodBye      = "BYE"
+	MethodOptions  = "OPTIONS"
+)
+
+const version = "SIP/2.0"
+
+type Request struct {
+	Method string
+	URI    string
+
+	Proto      string
+	ProtoMajor int
+	ProtoMinor int
+
+	Header        textproto.MIMEHeader
+	ContentLength int64
+	Sequence      int
+
+	Body io.Reader
+}
+
+func ReadRequest(r io.Reader) (*Request, error) {
+	msg, err := readMessage(r)
+	if err != nil {
+		return nil, err
+	}
+	return parseRequest(msg)
+}
+
+func parseRequest(msg *message) (*Request, error) {
+	var req Request
+	req.Method = msg.startLine[0]
+	req.URI = msg.startLine[1]
+	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 != "" {
+		n, err := strconv.Atoi(s)
+		if err != nil {
+			return &req, fmt.Errorf("parse content-length: %w", err)
+		}
+		req.ContentLength = int64(n)
+	}
+	req.Body = msg.body
+	return &req, nil
+}
+
+type message struct {
+	startLine [3]string
+	header    textproto.MIMEHeader
+	body      *bufio.Reader
+}
+
+func readMessage(rd io.Reader) (*message, error) {
+	r := textproto.NewReader(bufio.NewReader(rd))
+	line, err := r.ReadLine()
+	if err != nil {
+		return nil, fmt.Errorf("read start line: %w", err)
+	}
+	sline, err := parseStartLine(line)
+	if err != nil {
+		return nil, fmt.Errorf("parse start line: %w", err)
+	}
+
+	header, err := r.ReadMIMEHeader()
+	if err != nil {
+		return nil, fmt.Errorf("read header: %w", err)
+	}
+	return &message{startLine: sline, header: header, body: r.R}, nil
+}
+
+// Request-Line  =  Method SP Request-URI SP SIP-Version CRLF
+// Status-Line  =  SIP-Version SP Status-Code SP Reason-Phrase CRLF
+func parseStartLine(text string) (line [3]string, err error) {
+	fields := strings.Fields(text)
+	if len(fields) != 3 {
+		return line, fmt.Errorf("expected 3 fields, read %d", len(fields))
+	}
+	for i, s := range fields {
+		for _, r := range s {
+			if unicode.IsSpace(r) {
+				return line, fmt.Errorf("illegal space character in field %d", i)
+			}
+		}
+	}
+	return [3]string{fields[0], fields[1], fields[2]}, nil
+}
+
+type CommandSequence struct {
+	Number int
+	Method string
+}
+
+type Response struct {
+	Status     string
+	StatusCode int
+
+	Header        textproto.MIMEHeader
+	ContentLength int64
+	Sequence      CommandSequence
+
+	Body io.ReadCloser
+}
blob - /dev/null
blob + d4a8c7941abe026fe56be17d1a89b3dd8309c349 (mode 644)
--- /dev/null
+++ internal/sip/sip_test.go
@@ -0,0 +1,26 @@
+package sip
+
+import (
+	"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
+CSeq: 314159 INVITE
+Contact: <sip:alice@pc33.atlanta.com>
+Content-Type: application/sdp
+Content-Length: 328
+
+...`
+
+	_, err := ReadRequest(strings.NewReader(raw))
+	if err != nil {
+		t.Fatal("read request:", err)
+	}
+}