commit 52f2be3f00f9c63b286a9b49366dca4622ddc923 from: Oliver Lowe date: Tue Mar 11 23:29:01 2025 UTC internal/sip: initial little testing 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 +From: Alice ;tag=1928301774 +Call-ID: a84b4c76e66710@pc33.atlanta.com +CSeq: 314159 INVITE +Contact: +Content-Type: application/sdp +Content-Length: 328 + +...` + + _, err := ReadRequest(strings.NewReader(raw)) + if err != nil { + t.Fatal("read request:", err) + } +}