diff --git a/client/client.go b/client/client.go index b822e91..09a7931 100644 --- a/client/client.go +++ b/client/client.go @@ -124,6 +124,18 @@ func (c *client) WriteRequest(req *Request) error { return c.WriteBody(req.Body) } +// maxDuplicateStatusLines bounds how many repeated status lines we are willing +// to discard, so a misbehaving (or malicious) server streaming "HTTP/..." lines +// indefinitely cannot keep us looping. +const maxDuplicateStatusLines = 8 + +// isStatusLine reports whether line looks like an HTTP status line +// (e.g. "HTTP/1.0 200 OK"). It is used to skip duplicate status lines +// emitted by some non-compliant servers. +func isStatusLine(line []byte) bool { + return bytes.HasPrefix(line, []byte("HTTP/")) +} + // ReadResponse unmarshalls a HTTP response. func (c *client) ReadResponse(forceReadAll bool) (*Response, error) { version, code, msg, err := c.ReadStatusLine() @@ -131,6 +143,19 @@ func (c *client) ReadResponse(forceReadAll bool) (*Response, error) { if err != nil { return nil, fmt.Errorf("ReadStatusLine: %v", err) } + // Some non-compliant servers (e.g. certain embedded devices) repeat the + // status line one or more times before the actual headers. Skip any such + // duplicate status lines so that header parsing succeeds. + for i := 0; i < maxDuplicateStatusLines; i++ { + peeked, peekErr := c.Peek(5) + if peekErr != nil || !isStatusLine(peeked) { + break + } + // Discard the entire duplicate status line (read until newline). + if _, err := c.ReadBytes('\n'); err != nil { + break + } + } for { var key, value string var done bool diff --git a/client/reader_test.go b/client/reader_test.go index 77966f1..ed1dece 100644 --- a/client/reader_test.go +++ b/client/reader_test.go @@ -2,6 +2,7 @@ package client import ( "bufio" + "io" "strings" "testing" @@ -65,3 +66,30 @@ func TestReadVersion(t *testing.T) { }) } } + +// readWriterStub wraps a strings.Reader to satisfy io.ReadWriter (writes are no-ops). +type readWriterStub struct{ *strings.Reader } + +func (readWriterStub) Write(p []byte) (int, error) { return len(p), nil } + +func TestReadResponseDuplicateStatusLines(t *testing.T) { + // Some non-compliant embedded devices (e.g. Grandstream HT801) emit the + // HTTP status line twice before the headers. rawhttp must not error on this. + raw := "HTTP/1.0 200 OK\r\nHTTP/1.0 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\nhello" + c := NewClient(readWriterStub{strings.NewReader(raw)}) + resp, err := c.ReadResponse(false) + require.NoError(t, err, "ReadResponse must not fail on duplicate status lines") + require.Equal(t, 200, resp.Status.Code) + body, _ := io.ReadAll(resp.Body) + require.Equal(t, "hello", string(body)) +} + +func TestReadResponseTripleStatusLines(t *testing.T) { + // Also handle three consecutive status lines. + raw := "HTTP/1.0 200 OK\r\nHTTP/1.0 200 OK\r\nHTTP/1.0 200 OK\r\nX-Custom: test\r\n\r\n" + c := NewClient(readWriterStub{strings.NewReader(raw)}) + resp, err := c.ReadResponse(false) + require.NoError(t, err, "ReadResponse must not fail on triple status lines") + require.Equal(t, 200, resp.Status.Code) + require.Equal(t, "test", resp.Headers[0].Value) +}