12 changed files with 27 additions and 725 deletions
@ -1,11 +1,11 @@ |
|||||||
module rtsp-server |
module rtsp-simple-server |
||||||
|
|
||||||
go 1.13 |
go 1.13 |
||||||
|
|
||||||
require ( |
require ( |
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect |
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect |
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect |
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect |
||||||
github.com/stretchr/testify v1.4.0 |
github.com/aler9/gortsplib v0.0.0-20200120091821-97304167de21 |
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 |
gopkg.in/alecthomas/kingpin.v2 v2.2.6 |
||||||
gortc.io/sdp v0.17.0 |
gortc.io/sdp v0.17.0 |
||||||
) |
) |
||||||
|
@ -1,90 +0,0 @@ |
|||||||
package rtsp |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/binary" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"net" |
|
||||||
) |
|
||||||
|
|
||||||
type Conn struct { |
|
||||||
c net.Conn |
|
||||||
writeBuf []byte |
|
||||||
} |
|
||||||
|
|
||||||
func NewConn(c net.Conn) *Conn { |
|
||||||
return &Conn{ |
|
||||||
c: c, |
|
||||||
writeBuf: make([]byte, 2048), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) Close() error { |
|
||||||
return c.c.Close() |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) RemoteAddr() net.Addr { |
|
||||||
return c.c.RemoteAddr() |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) ReadRequest() (*Request, error) { |
|
||||||
return requestDecode(c.c) |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) WriteRequest(req *Request) error { |
|
||||||
return requestEncode(c.c, req) |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) ReadResponse() (*Response, error) { |
|
||||||
return responseDecode(c.c) |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) WriteResponse(res *Response) error { |
|
||||||
return responseEncode(c.c, res) |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) ReadInterleavedFrame(frame []byte) (int, int, error) { |
|
||||||
var header [4]byte |
|
||||||
_, err := io.ReadFull(c.c, header[:]) |
|
||||||
if err != nil { |
|
||||||
return 0, 0, err |
|
||||||
} |
|
||||||
|
|
||||||
// connection terminated
|
|
||||||
if header[0] == 0x54 { |
|
||||||
return 0, 0, io.EOF |
|
||||||
} |
|
||||||
|
|
||||||
if header[0] != 0x24 { |
|
||||||
return 0, 0, fmt.Errorf("wrong magic byte (0x%.2x)", header[0]) |
|
||||||
} |
|
||||||
|
|
||||||
framelen := binary.BigEndian.Uint16(header[2:]) |
|
||||||
if framelen > 2048 { |
|
||||||
return 0, 0, fmt.Errorf("frame length greater than 2048") |
|
||||||
} |
|
||||||
|
|
||||||
_, err = io.ReadFull(c.c, frame[:framelen]) |
|
||||||
if err != nil { |
|
||||||
return 0, 0, err |
|
||||||
} |
|
||||||
|
|
||||||
return int(header[1]), int(framelen), nil |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) WriteInterleavedFrame(channel int, frame []byte) error { |
|
||||||
c.writeBuf[0] = 0x24 |
|
||||||
c.writeBuf[1] = byte(channel) |
|
||||||
binary.BigEndian.PutUint16(c.writeBuf[2:], uint16(len(frame))) |
|
||||||
n := copy(c.writeBuf[4:], frame) |
|
||||||
|
|
||||||
_, err := c.c.Write(c.writeBuf[:4+n]) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (c *Conn) Read(buf []byte) (int, error) { |
|
||||||
return c.c.Read(buf) |
|
||||||
} |
|
@ -1,88 +0,0 @@ |
|||||||
package rtsp |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
) |
|
||||||
|
|
||||||
type Request struct { |
|
||||||
Method string |
|
||||||
Url string |
|
||||||
Headers map[string]string |
|
||||||
Content []byte |
|
||||||
} |
|
||||||
|
|
||||||
func requestDecode(r io.Reader) (*Request, error) { |
|
||||||
rb := bufio.NewReader(r) |
|
||||||
|
|
||||||
req := &Request{} |
|
||||||
|
|
||||||
byts, err := readBytesLimited(rb, ' ', 255) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
req.Method = string(byts[:len(byts)-1]) |
|
||||||
|
|
||||||
if len(req.Method) == 0 { |
|
||||||
return nil, fmt.Errorf("empty method") |
|
||||||
} |
|
||||||
|
|
||||||
byts, err = readBytesLimited(rb, ' ', 255) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
req.Url = string(byts[:len(byts)-1]) |
|
||||||
|
|
||||||
if len(req.Url) == 0 { |
|
||||||
return nil, fmt.Errorf("empty path") |
|
||||||
} |
|
||||||
|
|
||||||
byts, err = readBytesLimited(rb, '\r', 255) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
proto := string(byts[:len(byts)-1]) |
|
||||||
|
|
||||||
if proto != _RTSP_PROTO { |
|
||||||
return nil, fmt.Errorf("expected '%s', got '%s'", _RTSP_PROTO, proto) |
|
||||||
} |
|
||||||
|
|
||||||
err = readByteEqual(rb, '\n') |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
req.Headers, err = readHeaders(rb) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
req.Content, err = readContent(rb, req.Headers) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return req, nil |
|
||||||
} |
|
||||||
|
|
||||||
func requestEncode(w io.Writer, req *Request) error { |
|
||||||
wb := bufio.NewWriter(w) |
|
||||||
|
|
||||||
_, err := wb.Write([]byte(req.Method + " " + req.Url + " " + _RTSP_PROTO + "\r\n")) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
err = writeHeaders(wb, req.Headers) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
err = writeContent(wb, req.Content) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return wb.Flush() |
|
||||||
} |
|
@ -1,134 +0,0 @@ |
|||||||
package rtsp |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
) |
|
||||||
|
|
||||||
var casesRequest = []struct { |
|
||||||
name string |
|
||||||
byts []byte |
|
||||||
req *Request |
|
||||||
}{ |
|
||||||
{ |
|
||||||
"options", |
|
||||||
[]byte("OPTIONS rtsp://example.com/media.mp4 RTSP/1.0\r\n" + |
|
||||||
"CSeq: 1\r\n" + |
|
||||||
"Proxy-Require: gzipped-messages\r\n" + |
|
||||||
"Require: implicit-play\r\n" + |
|
||||||
"\r\n"), |
|
||||||
&Request{ |
|
||||||
Method: "OPTIONS", |
|
||||||
Url: "rtsp://example.com/media.mp4", |
|
||||||
Headers: map[string]string{ |
|
||||||
"CSeq": "1", |
|
||||||
"Require": "implicit-play", |
|
||||||
"Proxy-Require": "gzipped-messages", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
"describe", |
|
||||||
[]byte("DESCRIBE rtsp://example.com/media.mp4 RTSP/1.0\r\n" + |
|
||||||
"CSeq: 2\r\n" + |
|
||||||
"\r\n"), |
|
||||||
&Request{ |
|
||||||
Method: "DESCRIBE", |
|
||||||
Url: "rtsp://example.com/media.mp4", |
|
||||||
Headers: map[string]string{ |
|
||||||
"CSeq": "2", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
"announce", |
|
||||||
[]byte("ANNOUNCE rtsp://example.com/media.mp4 RTSP/1.0\r\n" + |
|
||||||
"CSeq: 7\r\n" + |
|
||||||
"Content-Length: 306\r\n" + |
|
||||||
"Content-Type: application/sdp\r\n" + |
|
||||||
"Date: 23 Jan 1997 15:35:06 GMT\r\n" + |
|
||||||
"Session: 12345678\r\n" + |
|
||||||
"\r\n" + |
|
||||||
"v=0\n" + |
|
||||||
"o=mhandley 2890844526 2890845468 IN IP4 126.16.64.4\n" + |
|
||||||
"s=SDP Seminar\n" + |
|
||||||
"i=A Seminar on the session description protocol\n" + |
|
||||||
"u=http://www.cs.ucl.ac.uk/staff/M.Handley/sdp.03.ps\n" + |
|
||||||
"e=mjh@isi.edu (Mark Handley)\n" + |
|
||||||
"c=IN IP4 224.2.17.12/127\n" + |
|
||||||
"t=2873397496 2873404696\n" + |
|
||||||
"a=recvonly\n" + |
|
||||||
"m=audio 3456 RTP/AVP 0\n" + |
|
||||||
"m=video 2232 RTP/AVP 31\n"), |
|
||||||
&Request{ |
|
||||||
Method: "ANNOUNCE", |
|
||||||
Url: "rtsp://example.com/media.mp4", |
|
||||||
Headers: map[string]string{ |
|
||||||
"CSeq": "7", |
|
||||||
"Date": "23 Jan 1997 15:35:06 GMT", |
|
||||||
"Session": "12345678", |
|
||||||
"Content-Type": "application/sdp", |
|
||||||
"Content-Length": "306", |
|
||||||
}, |
|
||||||
Content: []byte("v=0\n" + |
|
||||||
"o=mhandley 2890844526 2890845468 IN IP4 126.16.64.4\n" + |
|
||||||
"s=SDP Seminar\n" + |
|
||||||
"i=A Seminar on the session description protocol\n" + |
|
||||||
"u=http://www.cs.ucl.ac.uk/staff/M.Handley/sdp.03.ps\n" + |
|
||||||
"e=mjh@isi.edu (Mark Handley)\n" + |
|
||||||
"c=IN IP4 224.2.17.12/127\n" + |
|
||||||
"t=2873397496 2873404696\n" + |
|
||||||
"a=recvonly\n" + |
|
||||||
"m=audio 3456 RTP/AVP 0\n" + |
|
||||||
"m=video 2232 RTP/AVP 31\n", |
|
||||||
), |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
"get_parameter", |
|
||||||
[]byte("GET_PARAMETER rtsp://example.com/media.mp4 RTSP/1.0\r\n" + |
|
||||||
"CSeq: 9\r\n" + |
|
||||||
"Content-Length: 24\r\n" + |
|
||||||
"Content-Type: text/parameters\r\n" + |
|
||||||
"Session: 12345678\r\n" + |
|
||||||
"\r\n" + |
|
||||||
"packets_received\n" + |
|
||||||
"jitter\n"), |
|
||||||
&Request{ |
|
||||||
Method: "GET_PARAMETER", |
|
||||||
Url: "rtsp://example.com/media.mp4", |
|
||||||
Headers: map[string]string{ |
|
||||||
"CSeq": "9", |
|
||||||
"Content-Type": "text/parameters", |
|
||||||
"Session": "12345678", |
|
||||||
"Content-Length": "24", |
|
||||||
}, |
|
||||||
Content: []byte("packets_received\n" + |
|
||||||
"jitter\n", |
|
||||||
), |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
func TestRequestDecode(t *testing.T) { |
|
||||||
for _, c := range casesRequest { |
|
||||||
t.Run(c.name, func(t *testing.T) { |
|
||||||
req, err := requestDecode(bytes.NewBuffer(c.byts)) |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, c.req, req) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestRequestEncode(t *testing.T) { |
|
||||||
for _, c := range casesRequest { |
|
||||||
t.Run(c.name, func(t *testing.T) { |
|
||||||
var buf bytes.Buffer |
|
||||||
err := requestEncode(&buf, c.req) |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, c.byts, buf.Bytes()) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
@ -1,95 +0,0 @@ |
|||||||
package rtsp |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"strconv" |
|
||||||
) |
|
||||||
|
|
||||||
type Response struct { |
|
||||||
StatusCode int |
|
||||||
Status string |
|
||||||
Headers map[string]string |
|
||||||
Content []byte |
|
||||||
} |
|
||||||
|
|
||||||
func responseDecode(r io.Reader) (*Response, error) { |
|
||||||
rb := bufio.NewReader(r) |
|
||||||
|
|
||||||
res := &Response{} |
|
||||||
|
|
||||||
byts, err := readBytesLimited(rb, ' ', 255) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
proto := string(byts[:len(byts)-1]) |
|
||||||
|
|
||||||
if proto != _RTSP_PROTO { |
|
||||||
return nil, fmt.Errorf("expected '%s', got '%s'", _RTSP_PROTO, proto) |
|
||||||
} |
|
||||||
|
|
||||||
byts, err = readBytesLimited(rb, ' ', 4) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
statusCodeStr := string(byts[:len(byts)-1]) |
|
||||||
|
|
||||||
statusCode64, err := strconv.ParseInt(statusCodeStr, 10, 32) |
|
||||||
res.StatusCode = int(statusCode64) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("unable to parse status code") |
|
||||||
} |
|
||||||
|
|
||||||
byts, err = readBytesLimited(rb, '\r', 255) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
res.Status = string(byts[:len(byts)-1]) |
|
||||||
|
|
||||||
if len(res.Status) == 0 { |
|
||||||
return nil, fmt.Errorf("empty status") |
|
||||||
} |
|
||||||
|
|
||||||
err = readByteEqual(rb, '\n') |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
res.Headers, err = readHeaders(rb) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
res.Content, err = readContent(rb, res.Headers) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return res, nil |
|
||||||
} |
|
||||||
|
|
||||||
func responseEncode(w io.Writer, res *Response) error { |
|
||||||
wb := bufio.NewWriter(w) |
|
||||||
|
|
||||||
_, err := wb.Write([]byte(_RTSP_PROTO + " " + strconv.FormatInt(int64(res.StatusCode), 10) + " " + res.Status + "\r\n")) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if len(res.Content) != 0 { |
|
||||||
res.Headers["Content-Length"] = strconv.FormatInt(int64(len(res.Content)), 10) |
|
||||||
} |
|
||||||
|
|
||||||
err = writeHeaders(wb, res.Headers) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
err = writeContent(wb, res.Content) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return wb.Flush() |
|
||||||
} |
|
@ -1,105 +0,0 @@ |
|||||||
package rtsp |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
) |
|
||||||
|
|
||||||
var casesResponse = []struct { |
|
||||||
name string |
|
||||||
byts []byte |
|
||||||
res *Response |
|
||||||
}{ |
|
||||||
{ |
|
||||||
"ok", |
|
||||||
[]byte("RTSP/1.0 200 OK\r\n" + |
|
||||||
"CSeq: 1\r\n" + |
|
||||||
"Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n" + |
|
||||||
"\r\n", |
|
||||||
), |
|
||||||
&Response{ |
|
||||||
StatusCode: 200, |
|
||||||
Status: "OK", |
|
||||||
Headers: map[string]string{ |
|
||||||
"CSeq": "1", |
|
||||||
"Public": "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
"ok with content", |
|
||||||
[]byte("RTSP/1.0 200 OK\r\n" + |
|
||||||
"CSeq: 2\r\n" + |
|
||||||
"Content-Base: rtsp://example.com/media.mp4\r\n" + |
|
||||||
"Content-Length: 444\r\n" + |
|
||||||
"Content-Type: application/sdp\r\n" + |
|
||||||
"\r\n" + |
|
||||||
"m=video 0 RTP/AVP 96\n" + |
|
||||||
"a=control:streamid=0\n" + |
|
||||||
"a=range:npt=0-7.741000\n" + |
|
||||||
"a=length:npt=7.741000\n" + |
|
||||||
"a=rtpmap:96 MP4V-ES/5544\n" + |
|
||||||
"a=mimetype:string;\"video/MP4V-ES\"\n" + |
|
||||||
"a=AvgBitRate:integer;304018\n" + |
|
||||||
"a=StreamName:string;\"hinted video track\"\n" + |
|
||||||
"m=audio 0 RTP/AVP 97\n" + |
|
||||||
"a=control:streamid=1\n" + |
|
||||||
"a=range:npt=0-7.712000\n" + |
|
||||||
"a=length:npt=7.712000\n" + |
|
||||||
"a=rtpmap:97 mpeg4-generic/32000/2\n" + |
|
||||||
"a=mimetype:string;\"audio/mpeg4-generic\"\n" + |
|
||||||
"a=AvgBitRate:integer;65790\n" + |
|
||||||
"a=StreamName:string;\"hinted audio track\"\n", |
|
||||||
), |
|
||||||
&Response{ |
|
||||||
StatusCode: 200, |
|
||||||
Status: "OK", |
|
||||||
Headers: map[string]string{ |
|
||||||
"Content-Base": "rtsp://example.com/media.mp4", |
|
||||||
"Content-Length": "444", |
|
||||||
"Content-Type": "application/sdp", |
|
||||||
"CSeq": "2", |
|
||||||
}, |
|
||||||
Content: []byte("m=video 0 RTP/AVP 96\n" + |
|
||||||
"a=control:streamid=0\n" + |
|
||||||
"a=range:npt=0-7.741000\n" + |
|
||||||
"a=length:npt=7.741000\n" + |
|
||||||
"a=rtpmap:96 MP4V-ES/5544\n" + |
|
||||||
"a=mimetype:string;\"video/MP4V-ES\"\n" + |
|
||||||
"a=AvgBitRate:integer;304018\n" + |
|
||||||
"a=StreamName:string;\"hinted video track\"\n" + |
|
||||||
"m=audio 0 RTP/AVP 97\n" + |
|
||||||
"a=control:streamid=1\n" + |
|
||||||
"a=range:npt=0-7.712000\n" + |
|
||||||
"a=length:npt=7.712000\n" + |
|
||||||
"a=rtpmap:97 mpeg4-generic/32000/2\n" + |
|
||||||
"a=mimetype:string;\"audio/mpeg4-generic\"\n" + |
|
||||||
"a=AvgBitRate:integer;65790\n" + |
|
||||||
"a=StreamName:string;\"hinted audio track\"\n", |
|
||||||
), |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
func TestResponseDecode(t *testing.T) { |
|
||||||
for _, c := range casesResponse { |
|
||||||
t.Run(c.name, func(t *testing.T) { |
|
||||||
res, err := responseDecode(bytes.NewBuffer(c.byts)) |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, c.res, res) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func TestResponseEncode(t *testing.T) { |
|
||||||
for _, c := range casesResponse { |
|
||||||
t.Run(c.name, func(t *testing.T) { |
|
||||||
var buf bytes.Buffer |
|
||||||
err := responseEncode(&buf, c.res) |
|
||||||
require.NoError(t, err) |
|
||||||
require.Equal(t, c.byts, buf.Bytes()) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
@ -1,161 +0,0 @@ |
|||||||
package rtsp |
|
||||||
|
|
||||||
import ( |
|
||||||
"bufio" |
|
||||||
"fmt" |
|
||||||
"io" |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
_RTSP_PROTO = "RTSP/1.0" |
|
||||||
_MAX_HEADER_COUNT = 255 |
|
||||||
_MAX_HEADER_KEY_LENGTH = 255 |
|
||||||
_MAX_HEADER_VALUE_LENGTH = 255 |
|
||||||
_MAX_CONTENT_LENGTH = 4096 |
|
||||||
) |
|
||||||
|
|
||||||
func readBytesLimited(rb *bufio.Reader, delim byte, n int) ([]byte, error) { |
|
||||||
for i := 1; i <= n; i++ { |
|
||||||
byts, err := rb.Peek(i) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
if byts[len(byts)-1] == delim { |
|
||||||
rb.Discard(len(byts)) |
|
||||||
return byts, nil |
|
||||||
} |
|
||||||
} |
|
||||||
return nil, fmt.Errorf("buffer length exceeds %d", n) |
|
||||||
} |
|
||||||
|
|
||||||
func readByteEqual(rb *bufio.Reader, cmp byte) error { |
|
||||||
byt, err := rb.ReadByte() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if byt != cmp { |
|
||||||
return fmt.Errorf("expected '%c', got '%c'", cmp, byt) |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func readHeaders(rb *bufio.Reader) (map[string]string, error) { |
|
||||||
ret := make(map[string]string) |
|
||||||
|
|
||||||
for { |
|
||||||
byt, err := rb.ReadByte() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
if byt == '\r' { |
|
||||||
err := readByteEqual(rb, '\n') |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
break |
|
||||||
} |
|
||||||
|
|
||||||
if len(ret) >= _MAX_HEADER_COUNT { |
|
||||||
return nil, fmt.Errorf("headers count exceeds %d", _MAX_HEADER_COUNT) |
|
||||||
} |
|
||||||
|
|
||||||
key := string([]byte{byt}) |
|
||||||
byts, err := readBytesLimited(rb, ':', _MAX_HEADER_KEY_LENGTH-1) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
key += string(byts[:len(byts)-1]) |
|
||||||
|
|
||||||
err = readByteEqual(rb, ' ') |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
byts, err = readBytesLimited(rb, '\r', _MAX_HEADER_VALUE_LENGTH) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
val := string(byts[:len(byts)-1]) |
|
||||||
|
|
||||||
if len(val) == 0 { |
|
||||||
return nil, fmt.Errorf("empty header value") |
|
||||||
} |
|
||||||
|
|
||||||
err = readByteEqual(rb, '\n') |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
ret[key] = val |
|
||||||
} |
|
||||||
|
|
||||||
return ret, nil |
|
||||||
} |
|
||||||
|
|
||||||
func writeHeaders(wb *bufio.Writer, headers map[string]string) error { |
|
||||||
// sort headers by key
|
|
||||||
// in order to obtain deterministic results
|
|
||||||
var keys []string |
|
||||||
for key := range headers { |
|
||||||
keys = append(keys, key) |
|
||||||
} |
|
||||||
sort.Strings(keys) |
|
||||||
|
|
||||||
for _, key := range keys { |
|
||||||
_, err := wb.Write([]byte(key + ": " + headers[key] + "\r\n")) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
_, err := wb.Write([]byte("\r\n")) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func readContent(rb *bufio.Reader, headers map[string]string) ([]byte, error) { |
|
||||||
cls, ok := headers["Content-Length"] |
|
||||||
if !ok { |
|
||||||
return nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
cl, err := strconv.ParseInt(cls, 10, 64) |
|
||||||
if err != nil { |
|
||||||
return nil, fmt.Errorf("invalid Content-Length") |
|
||||||
} |
|
||||||
|
|
||||||
if cl > _MAX_CONTENT_LENGTH { |
|
||||||
return nil, fmt.Errorf("Content-Length exceeds %d", _MAX_CONTENT_LENGTH) |
|
||||||
} |
|
||||||
|
|
||||||
ret := make([]byte, cl) |
|
||||||
n, err := io.ReadFull(rb, ret) |
|
||||||
if err != nil && n != len(ret) { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return ret, nil |
|
||||||
} |
|
||||||
|
|
||||||
func writeContent(wb *bufio.Writer, content []byte) error { |
|
||||||
if len(content) == 0 { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
_, err := wb.Write(content) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
Loading…
Reference in new issue