Add http_response package

pull/297/head
Alex Kunin 3 months ago
parent 04f79a71d8
commit 8f35139a5e

@ -0,0 +1,139 @@
package http_response
import (
"bytes"
"regexp"
)
type HttpResponseHeader interface {
Name() []byte
Value() []byte
NormalizedValue() []byte
}
type HttpResponse interface {
Status() []byte
Headers() []HttpResponseHeader
Body() []byte
}
type httpResponseHeader struct {
name []byte
value []byte
normalizedValue []byte
}
func (h httpResponseHeader) Name() []byte {
return h.name
}
func (h httpResponseHeader) Value() []byte {
return h.value
}
func (h httpResponseHeader) NormalizedValue() []byte {
if h.normalizedValue == nil {
h.normalizedValue = normalizeHeaderValue(h.value)
}
return h.normalizedValue
}
var normalizeValueRe *regexp.Regexp
func normalizeHeaderValue(value []byte) []byte {
if normalizeValueRe == nil {
str := `\s*\n\s+`
normalizeValueRe = regexp.MustCompile(str)
}
return normalizeValueRe.ReplaceAllLiteral(bytes.TrimSpace(value), []byte{' '})
}
type FailedParsingHttpResponseError struct {
error string
}
func (e FailedParsingHttpResponseError) Error() string {
return e.error
}
var splitHttpResponseRe *regexp.Regexp
func splitHttpResponse(text []byte) (status []byte, headers []byte, body []byte, ok bool) {
if splitHttpResponseRe == nil {
str := `(?ms)\A(?P<status>(?:HTTP/\d\.\d \d{3} \w+|HTTP/2 [^\n]+))\n(?P<headers>.*?\n)\n(?P<body>.*)\z`
splitHttpResponseRe = regexp.MustCompile(str)
}
match := splitHttpResponseRe.FindSubmatch(text)
if match == nil {
return
}
status = match[1]
headers = match[2]
body = match[3]
ok = true
return
}
var splitHeadersRe *regexp.Regexp
func splitHeaders(text []byte) (headers []HttpResponseHeader, ok bool) {
if splitHeadersRe == nil {
str := `(?m)\A(?:^(?P<field>[^:\s]+):\s+(?P<value>.*$\n(?:^\s+.*$\n)*))`
splitHeadersRe = regexp.MustCompile(str)
}
for p := 0; p < len(text); {
match := splitHeadersRe.FindSubmatch(text[p:])
if match == nil {
return nil, false
}
name := match[1]
value := bytes.TrimRight(match[2], "\n")
headers = append(headers, httpResponseHeader{name, value, nil})
p += len(match[0])
}
ok = true
return
}
func normalizeEols(text []byte) []byte {
return bytes.ReplaceAll(text, []byte{'\r', '\n'}, []byte{'\n'})
}
type httpResponse struct {
status []byte
headers []HttpResponseHeader
body []byte
}
func NewHttpResponseFromText(text []byte) (HttpResponse, error) {
status, combinedHeaders, body, ok := splitHttpResponse(normalizeEols(text))
if !ok {
return nil, FailedParsingHttpResponseError{"failed extracting status, headers or body"}
}
headers, ok := splitHeaders(combinedHeaders)
if !ok {
return nil, FailedParsingHttpResponseError{"failed parsing headers"}
}
return httpResponse{status, headers, body}, nil
}
func (r httpResponse) Status() []byte {
return r.status
}
func (r httpResponse) Headers() []HttpResponseHeader {
return r.headers
}
func (r httpResponse) Body() []byte {
return r.body
}

@ -0,0 +1,64 @@
package http_response
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_normalizeHeaderValue(t *testing.T) {
require.Equal(t, "a b", string(normalizeHeaderValue([]byte("a b"))))
require.Equal(t, "a b", string(normalizeHeaderValue([]byte("a b "))))
require.Equal(t, "a b", string(normalizeHeaderValue([]byte(" a b"))))
require.Equal(t, "a b", string(normalizeHeaderValue([]byte(" a \n b "))))
}
func Test_splitHttpResponse(t *testing.T) {
_, _, _, ok := splitHttpResponse([]byte("whatever"))
require.False(t, ok)
_, _, _, ok = splitHttpResponse([]byte("HTTP/1.1 200 OK\n\nbody"))
require.False(t, ok)
status, header, body, ok := splitHttpResponse([]byte("HTTP/1.1 200 OK\nheader1: value\nheader2: value\n with\n continuation\n\nbody"))
require.True(t, ok)
require.Equal(t, "HTTP/1.1 200 OK", string(status))
require.Equal(t, "header1: value\nheader2: value\n with\n continuation\n", string(header))
require.Equal(t, "body", string(body))
status, header, body, ok = splitHttpResponse([]byte("HTTP/2 200 \ndate: Sun, 17 Mar 2024 21:58:03 GMT\n\n{}"))
require.True(t, ok)
require.Equal(t, "HTTP/2 200 ", string(status))
require.Equal(t, "date: Sun, 17 Mar 2024 21:58:03 GMT\n", string(header))
require.Equal(t, "{}", string(body))
}
func Test_splitHeaders(t *testing.T) {
_, ok := splitHeaders([]byte("whatever"))
require.False(t, ok)
_, ok = splitHeaders([]byte("a: b\nwhatever\n"))
require.False(t, ok)
res, ok := splitHeaders([]byte("a: b\n"))
require.True(t, ok)
require.Len(t, res, 1)
require.Equal(t, "a", string(res[0].Name()))
require.Equal(t, "b", string(res[0].Value()))
require.Equal(t, "b", string(res[0].NormalizedValue()))
res, ok = splitHeaders([]byte("a: b1\n b2\n b3\n"))
require.True(t, ok)
require.Len(t, res, 1)
require.Equal(t, "a", string(res[0].Name()))
require.Equal(t, "b1\n b2\n b3", string(res[0].Value()))
require.Equal(t, "b1 b2 b3", string(res[0].NormalizedValue()))
res, ok = splitHeaders([]byte("a: b\nc: d\n"))
require.True(t, ok)
require.Len(t, res, 2)
require.Equal(t, "a", string(res[0].Name()))
require.Equal(t, "b", string(res[0].Value()))
require.Equal(t, "c", string(res[1].Name()))
require.Equal(t, "d", string(res[1].Value()))
}
Loading…
Cancel
Save