mirror of
https://github.com/soypat/lneto.git
synced 2026-09-10 08:39:30 +00:00
first Multipart approach
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto/http/httpraw"
|
||||||
"github.com/soypat/lneto/internal"
|
"github.com/soypat/lneto/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,3 +96,34 @@ func BenchmarkHandle(b *testing.B) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// benchForm is package level so the Form's pair slice is reused across requests,
|
||||||
|
// as a real handler holding one per goroutine would.
|
||||||
|
var benchForm httpraw.Form
|
||||||
|
|
||||||
|
// BenchmarkRequestParseForm measures reading and parsing a urlencoded body into
|
||||||
|
// a buffer the caller owns. Nothing on the path may allocate.
|
||||||
|
func BenchmarkRequestParseForm(b *testing.B) {
|
||||||
|
const request = "POST /f HTTP/1.1\r\nHost: tinygo.org\r\n" +
|
||||||
|
"Content-Type: application/x-www-form-urlencoded\r\nContent-Length: 27\r\n\r\n" +
|
||||||
|
"user=gopher&msg=hello+world"
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
var mux MuxSlice
|
||||||
|
mux.Handle("POST /f", func(ex *Exchange) {
|
||||||
|
err := ex.RequestParseForm(&benchForm, buf, nopBackoff)
|
||||||
|
if err != nil || benchForm.Len() != 2 {
|
||||||
|
panic("invalid result")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
conn := &benchConn{request: request}
|
||||||
|
exch := benchExchange(b, conn)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.SetBytes(int64(len(request)))
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
conn.rewind()
|
||||||
|
exch.Release()
|
||||||
|
exch.Acquire(conn)
|
||||||
|
Handle(exch, &mux, nopBackoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -380,6 +380,122 @@ func (exch *Exchange) RequestParseCookie(dst *httpraw.Cookie, key string) error
|
|||||||
return dst.ParseBytes(value)
|
return dst.ParseBytes(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestContentType returns the request's Content-Type field value as it
|
||||||
|
// appears on the wire, parameters included, nil if absent. Test it with
|
||||||
|
// [httpraw.MediaTypeIs] and pick parameters out with [httpraw.ContentParam].
|
||||||
|
func (exch *Exchange) RequestContentType() []byte {
|
||||||
|
return exch.RequestHeader("Content-Type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestContentLength returns the body length declared by the request's
|
||||||
|
// Content-Length field. An absent field is not a client error: such a request
|
||||||
|
// has no body at all, RFC 9112 6.3. Check for the error to answer 411 instead.
|
||||||
|
// See [httpraw.Header.ContentLength].
|
||||||
|
func (exch *Exchange) RequestContentLength() (int64, error) {
|
||||||
|
return exch.RequestHeaderRaw().ContentLength()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestParseForm reads the request body into buf and parses it as
|
||||||
|
// "application/x-www-form-urlencoded" into dst. buf is the only storage used and
|
||||||
|
// the only limit: a body longer than buf is refused with [lneto.ErrBufferFull]
|
||||||
|
// before a single byte is read, leaving the caller free to answer 413. Pairs are
|
||||||
|
// left as they arrived, call [httpraw.Form.Decode] to decode them in place.
|
||||||
|
//
|
||||||
|
// Unlike http.Request.ParseForm the query string is not folded in, reach it with
|
||||||
|
// [Exchange.RequestQuery] or [Exchange.AppendQuery]. The body is consumed, so
|
||||||
|
// call this before [Exchange.ReadBody].
|
||||||
|
//
|
||||||
|
// A request with no Content-Length has no body, RFC 9112 6.3, and yields an
|
||||||
|
// empty form. Use [Exchange.RequestContentLength] to tell that apart from a body
|
||||||
|
// that arrived empty. backoff paces reads that return no data, as in [Handle].
|
||||||
|
func (exch *Exchange) RequestParseForm(dst *httpraw.Form, buf []byte, backoff lneto.BackoffStrategy) error {
|
||||||
|
if !httpraw.MediaTypeIs(exch.RequestContentType(), "application/x-www-form-urlencoded") {
|
||||||
|
return errNotFormEncoded
|
||||||
|
} else if exch.RequestHeader("Transfer-Encoding") != nil {
|
||||||
|
// Chunked bodies are framed, so reading Content-Length bytes off the
|
||||||
|
// wire would parse chunk sizes as form data. httpraw does not decode them.
|
||||||
|
return errUnsupportedTransferCoding
|
||||||
|
}
|
||||||
|
length, err := exch.RequestContentLength()
|
||||||
|
if err != nil {
|
||||||
|
dst.Reset(buf[:0])
|
||||||
|
return dst.Parse() // No length is no body, RFC 9112 6.3.
|
||||||
|
} else if length > int64(len(buf)) {
|
||||||
|
return lneto.ErrBufferFull // Refuse before reading, caller may answer 413.
|
||||||
|
}
|
||||||
|
buf = buf[:length]
|
||||||
|
var consecutiveBackoffs uint
|
||||||
|
for read := 0; read < len(buf); {
|
||||||
|
n, err := exch.ReadBody(buf[read:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if n == 0 {
|
||||||
|
backoff.Do(consecutiveBackoffs)
|
||||||
|
consecutiveBackoffs++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
consecutiveBackoffs = 0
|
||||||
|
read += n
|
||||||
|
}
|
||||||
|
dst.Reset(buf)
|
||||||
|
return dst.Parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestParseMultipart prepares dst from the boundary parameter of the
|
||||||
|
// request's Content-Type field. It reads no body: multipart parts declare no
|
||||||
|
// length, so the caller drives the loop with a buffer it owns and decides per
|
||||||
|
// part what to keep and when a part has grown too large.
|
||||||
|
//
|
||||||
|
// A part header and the bytes held back by [httpraw.Multipart.NextBody] both ask
|
||||||
|
// to be completed the same way: compact what is left to the front of the buffer
|
||||||
|
// and read more in behind it. A buffer that fills without completing either is
|
||||||
|
// the caller's cue that the part is too large to go on with.
|
||||||
|
//
|
||||||
|
// // refill compacts rest to the front of buf and reads more of the body in.
|
||||||
|
// refill := func(rest []byte) ([]byte, error) {
|
||||||
|
// n := copy(buf, rest)
|
||||||
|
// if n == len(buf) {
|
||||||
|
// return nil, lneto.ErrBufferFull
|
||||||
|
// }
|
||||||
|
// nr, err := exch.ReadBody(buf[n:])
|
||||||
|
// return buf[:n+nr], err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// err := exch.RequestParseMultipart(&mp)
|
||||||
|
// rest := buf[:0]
|
||||||
|
// for {
|
||||||
|
// next, err := mp.NextHeader(&hdr, rest)
|
||||||
|
// if err == io.EOF {
|
||||||
|
// break // Closing delimiter, body done.
|
||||||
|
// } else if err == httpraw.ErrNeedMoreData {
|
||||||
|
// rest, err = refill(rest)
|
||||||
|
// // ...handle err, then:
|
||||||
|
// continue
|
||||||
|
// } else if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// rest = next
|
||||||
|
// for {
|
||||||
|
// body, next, done := mp.NextBody(rest)
|
||||||
|
// // Consume body for hdr.Name, hdr.Filename.
|
||||||
|
// rest = next
|
||||||
|
// if done {
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// rest, err = refill(rest)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
func (exch *Exchange) RequestMultipart() (mp httpraw.Multipart, err error) {
|
||||||
|
contentType := exch.RequestContentType()
|
||||||
|
if !httpraw.MediaTypeIs(contentType, "multipart/form-data") {
|
||||||
|
return mp, errNotMultipart
|
||||||
|
}
|
||||||
|
return mp, mp.SetContentType(contentType)
|
||||||
|
}
|
||||||
|
|
||||||
// RequestHeader returns the value of the first request header field matching
|
// RequestHeader returns the value of the first request header field matching
|
||||||
// key, or nil if absent. Key matching is case sensitive.
|
// key, or nil if absent. Key matching is case sensitive.
|
||||||
func (exch *Exchange) RequestHeader(key string) []byte {
|
func (exch *Exchange) RequestHeader(key string) []byte {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"testing"
|
"testing"
|
||||||
@@ -611,3 +612,249 @@ func TestExchangeAppendQueryReusesBuffer(t *testing.T) {
|
|||||||
t.Errorf("AppendQuery allocated %v times into a buffer with capacity, want 0", allocs)
|
t.Errorf("AppendQuery allocated %v times into a buffer with capacity, want 0", allocs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formString renders a form as "key=value" pairs joined by '|', a pair with no
|
||||||
|
// value shown as the bare key.
|
||||||
|
func formString(f *httpraw.Form) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for i := 0; i < f.Len(); i++ {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteByte('|')
|
||||||
|
}
|
||||||
|
key, value := f.Pair(i)
|
||||||
|
sb.Write(key)
|
||||||
|
if value != nil {
|
||||||
|
sb.WriteByte('=')
|
||||||
|
sb.Write(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
const formType = "Content-Type: application/x-www-form-urlencoded\r\n"
|
||||||
|
|
||||||
|
func TestExchangeRequestParseForm(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
request string
|
||||||
|
bufSize int // Defaults to 64.
|
||||||
|
want string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "pairs",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\n" + formType + "Content-Length: 11\r\n\r\na=1&b=2&c=3",
|
||||||
|
want: "a=1|b=2|c=3",
|
||||||
|
}, {
|
||||||
|
// A flag and an empty value stay distinguishable, unlike http.FormValue.
|
||||||
|
name: "flag and empty",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\n" + formType + "Content-Length: 4\r\n\r\na&b=",
|
||||||
|
want: "a|b=",
|
||||||
|
}, {
|
||||||
|
name: "left encoded",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\n" + formType + "Content-Length: 7\r\n\r\nn=a%20b",
|
||||||
|
want: "n=a%20b",
|
||||||
|
}, {
|
||||||
|
name: "media type parameters",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\nContent-Type: application/x-www-form-urlencoded; charset=utf-8\r\nContent-Length: 3\r\n\r\na=1",
|
||||||
|
want: "a=1",
|
||||||
|
}, {
|
||||||
|
// Only the body is parsed: the query string is not folded in.
|
||||||
|
name: "query not folded",
|
||||||
|
request: "POST /f?q=go HTTP/1.1\r\nHost: h\r\n" + formType + "Content-Length: 3\r\n\r\na=1",
|
||||||
|
want: "a=1",
|
||||||
|
}, {
|
||||||
|
// No Content-Length means no body at all, RFC 9112 6.3.
|
||||||
|
name: "no content length",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\n" + formType + "\r\n",
|
||||||
|
want: "",
|
||||||
|
}, {
|
||||||
|
name: "wrong media type",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\nContent-Type: text/plain\r\nContent-Length: 3\r\n\r\na=1",
|
||||||
|
wantErr: errNotFormEncoded,
|
||||||
|
}, {
|
||||||
|
name: "no media type",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\nContent-Length: 3\r\n\r\na=1",
|
||||||
|
wantErr: errNotFormEncoded,
|
||||||
|
}, {
|
||||||
|
name: "chunked",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\n" + formType + "Transfer-Encoding: chunked\r\n\r\n3\r\na=1\r\n0\r\n\r\n",
|
||||||
|
wantErr: errUnsupportedTransferCoding,
|
||||||
|
}, {
|
||||||
|
name: "body larger than buffer",
|
||||||
|
request: "POST /f HTTP/1.1\r\nHost: h\r\n" + formType + "Content-Length: 11\r\n\r\na=1&b=2&c=3",
|
||||||
|
bufSize: 4,
|
||||||
|
wantErr: lneto.ErrBufferFull,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
bufSize := test.bufSize
|
||||||
|
if bufSize == 0 {
|
||||||
|
bufSize = 64
|
||||||
|
}
|
||||||
|
var form httpraw.Form
|
||||||
|
var gotErr error
|
||||||
|
var sm MuxSlice
|
||||||
|
sm.Reset(1)
|
||||||
|
sm.Handle("/f", func(exch *Exchange) {
|
||||||
|
gotErr = exch.RequestParseForm(&form, make([]byte, bufSize), nopBackoff)
|
||||||
|
})
|
||||||
|
serve(t, test.request, &sm)
|
||||||
|
if gotErr != test.wantErr {
|
||||||
|
t.Fatalf("want error %v, got %v", test.wantErr, gotErr)
|
||||||
|
}
|
||||||
|
if got := formString(&form); test.wantErr == nil && got != test.want {
|
||||||
|
t.Errorf("want %q, got %q", test.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A body arriving after the header, in its own segment, must still be parsed whole.
|
||||||
|
func TestExchangeRequestParseFormSplit(t *testing.T) {
|
||||||
|
conn := newConn("POST /f HTTP/1.1\r\nHost: h\r\n" + formType + "Content-Length: 11\r\n\r\na=1&")
|
||||||
|
conn.AddSegment("b=2&c=3")
|
||||||
|
conn.Hangup()
|
||||||
|
var form httpraw.Form
|
||||||
|
var gotErr error
|
||||||
|
var sm MuxSlice
|
||||||
|
sm.Reset(1)
|
||||||
|
sm.Handle("/f", func(exch *Exchange) {
|
||||||
|
gotErr = exch.RequestParseForm(&form, make([]byte, 64), nopBackoff)
|
||||||
|
})
|
||||||
|
exch := newExchange(t, conn, 1024, false)
|
||||||
|
if err := Handle(exch, &sm, nopBackoff); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if gotErr != nil {
|
||||||
|
t.Fatal(gotErr)
|
||||||
|
}
|
||||||
|
if got := formString(&form); got != "a=1|b=2|c=3" {
|
||||||
|
t.Errorf("want %q, got %q", "a=1|b=2|c=3", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode is the caller's call, and it must reach both keys and values.
|
||||||
|
func TestExchangeRequestParseFormDecode(t *testing.T) {
|
||||||
|
var form httpraw.Form
|
||||||
|
var sm MuxSlice
|
||||||
|
sm.Reset(1)
|
||||||
|
sm.Handle("/f", func(exch *Exchange) {
|
||||||
|
if err := exch.RequestParseForm(&form, make([]byte, 64), nopBackoff); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if err = form.Decode(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
serve(t, "POST /f HTTP/1.1\r\nHost: h\r\n"+formType+"Content-Length: 16\r\n\r\na+b=c%20d&e=f%2B", &sm)
|
||||||
|
if got := formString(&form); got != "a b=c d|e=f+" {
|
||||||
|
t.Errorf("want %q, got %q", "a b=c d|e=f+", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refill compacts the bytes the multipart parser held back to the front of buf
|
||||||
|
// and reads more of the body in behind them.
|
||||||
|
func refill(exch *Exchange, buf, rest []byte) ([]byte, error) {
|
||||||
|
n := copy(buf, rest)
|
||||||
|
if n == len(buf) {
|
||||||
|
return nil, lneto.ErrBufferFull // A delimiter or part header longer than buf.
|
||||||
|
}
|
||||||
|
nr, err := exch.ReadBody(buf[n:])
|
||||||
|
return buf[:n+nr], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole multipart loop as a handler writes it, over a body split so that a
|
||||||
|
// part straddles two reads and the caller must compact and refill.
|
||||||
|
func TestExchangeRequestParseMultipart(t *testing.T) {
|
||||||
|
const (
|
||||||
|
boundary = "--xyz"
|
||||||
|
head = "POST /f HTTP/1.1\r\nHost: h\r\nContent-Type: multipart/form-data; boundary=" + boundary + "\r\n\r\n"
|
||||||
|
part1 = "----xyz\r\nContent-Disposition: form-data; name=\"caption\"\r\n\r\nhi there\r\n"
|
||||||
|
part2 = "----xyz\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"beach.png\"\r\n\r\n\x89PNG\r\n\x00\r\n"
|
||||||
|
tail = "----xyz--\r\n"
|
||||||
|
)
|
||||||
|
conn := newConn(head + part1 + part2[:20])
|
||||||
|
conn.AddSegment(part2[20:] + tail)
|
||||||
|
conn.Hangup()
|
||||||
|
|
||||||
|
var got []string
|
||||||
|
var gotErr error
|
||||||
|
var sm MuxSlice
|
||||||
|
sm.Reset(1)
|
||||||
|
sm.Handle("/f", func(exch *Exchange) {
|
||||||
|
var mp httpraw.Multipart
|
||||||
|
if mp, gotErr = exch.RequestMultipart(); gotErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var hdr httpraw.MultipartHeader
|
||||||
|
buf := make([]byte, 128)
|
||||||
|
rest := buf[:0]
|
||||||
|
for {
|
||||||
|
next, err := mp.NextHeader(&hdr, rest)
|
||||||
|
if err == io.EOF {
|
||||||
|
return // Closing delimiter, body done.
|
||||||
|
} else if err == httpraw.ErrNeedMoreData {
|
||||||
|
if rest, gotErr = refill(exch, buf, rest); gotErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
gotErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name, total := string(hdr.Name), 0
|
||||||
|
rest = next
|
||||||
|
for {
|
||||||
|
body, next, done := mp.NextBody(rest)
|
||||||
|
total += len(body)
|
||||||
|
rest = next
|
||||||
|
if done {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if rest, gotErr = refill(exch, buf, rest); gotErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
got = append(got, name+":"+strconv.Itoa(total))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
exch := newExchange(t, conn, 1024, false)
|
||||||
|
if err := Handle(exch, &sm, nopBackoff); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if gotErr != nil {
|
||||||
|
t.Fatal(gotErr)
|
||||||
|
}
|
||||||
|
const want = "caption:8|photo:7"
|
||||||
|
if strings.Join(got, "|") != want {
|
||||||
|
t.Errorf("want %q, got %q", want, strings.Join(got, "|"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A request that is not multipart, or whose boundary is missing, must be refused.
|
||||||
|
func TestExchangeRequestParseMultipartRejects(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
contentType string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{contentType: "multipart/form-data; boundary=xyz"},
|
||||||
|
{contentType: "application/x-www-form-urlencoded", wantErr: true},
|
||||||
|
{contentType: "multipart/form-data", wantErr: true}, // Boundary is required.
|
||||||
|
{contentType: "", wantErr: true},
|
||||||
|
} {
|
||||||
|
var gotErr error
|
||||||
|
var sm MuxSlice
|
||||||
|
sm.Reset(1)
|
||||||
|
sm.Handle("/f", func(exch *Exchange) {
|
||||||
|
_, gotErr = exch.RequestMultipart()
|
||||||
|
})
|
||||||
|
request := "POST /f HTTP/1.1\r\nHost: h\r\n"
|
||||||
|
if test.contentType != "" {
|
||||||
|
request += "Content-Type: " + test.contentType + "\r\n"
|
||||||
|
}
|
||||||
|
serve(t, request+"\r\n", &sm)
|
||||||
|
if (gotErr != nil) != test.wantErr {
|
||||||
|
t.Errorf("%q: want error %v, got %v", test.contentType, test.wantErr, gotErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ var (
|
|||||||
errNoRequestProto = errors.New("httphi: request line with no HTTP version")
|
errNoRequestProto = errors.New("httphi: request line with no HTTP version")
|
||||||
errBusyExchanges = errors.New("httphi: exchanges still serving, cannot reuse their buffers")
|
errBusyExchanges = errors.New("httphi: exchanges still serving, cannot reuse their buffers")
|
||||||
errRouterTornDown = errors.New("httphi: router torn down, configure it before serving")
|
errRouterTornDown = errors.New("httphi: router torn down, configure it before serving")
|
||||||
|
|
||||||
|
errNotFormEncoded = errors.New("httphi: request body is not application/x-www-form-urlencoded")
|
||||||
|
errNotMultipart = errors.New("httphi: request body is not multipart/form-data")
|
||||||
|
errUnsupportedTransferCoding = errors.New("httphi: transfer coding not decoded, read the body directly")
|
||||||
)
|
)
|
||||||
|
|
||||||
type conn = io.ReadWriteCloser
|
type conn = io.ReadWriteCloser
|
||||||
|
|||||||
+30
-6
@@ -4,15 +4,17 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
methodGet = "GET"
|
methodGet = "GET"
|
||||||
strHTTP11 = "HTTP/1.1"
|
strHTTP11 = "HTTP/1.1"
|
||||||
strCRLF = "\r\n"
|
strCRLF = "\r\n"
|
||||||
headerCookie = "Cookie"
|
headerCookie = "Cookie"
|
||||||
headerConnection = "Connection"
|
headerConnection = "Connection"
|
||||||
strClose = "close"
|
headerContentLength = "Content-Length"
|
||||||
|
strClose = "close"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Flags is a bitset of signals gathered while parsing or building a header,
|
// Flags is a bitset of signals gathered while parsing or building a header,
|
||||||
@@ -343,6 +345,28 @@ func (h *Header) Get(key string) []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContentLength returns the body length declared by the Content-Length field.
|
||||||
|
// Fails with an error if the field is absent, which for a request means the
|
||||||
|
// message has no body at all unless a transfer coding applies, RFC 9112 6.3.
|
||||||
|
// The value must be digits only, so a negative or list-valued field is rejected
|
||||||
|
// rather than guessed at.
|
||||||
|
func (h *Header) ContentLength() (int64, error) {
|
||||||
|
kv := h.peekHeader(headerContentLength)
|
||||||
|
if !kv.isValid() {
|
||||||
|
return 0, errNoContentLength
|
||||||
|
}
|
||||||
|
value := trimOWS(h.hbuf.musttoken(kv.value))
|
||||||
|
if len(value) == 0 {
|
||||||
|
return 0, errBadContentLength
|
||||||
|
}
|
||||||
|
// Unsigned parse of 63 bits rejects a sign and anything past int64's range.
|
||||||
|
n, err := strconv.ParseUint(b2s(value), 10, 63)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errBadContentLength // strconv's error allocates and is not comparable.
|
||||||
|
}
|
||||||
|
return int64(n), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Add adds a new key-value pair to the HTTP header. Calling Add mangles the buffer.
|
// Add adds a new key-value pair to the HTTP header. Calling Add mangles the buffer.
|
||||||
func (h *Header) Add(key, value string) {
|
func (h *Header) Add(key, value string) {
|
||||||
h.appendHeader(key, value)
|
h.appendHeader(key, value)
|
||||||
|
|||||||
@@ -173,6 +173,39 @@ func TestHeaderRequestPath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHeaderContentLength(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
field string // Extra header lines, empty for an absent field.
|
||||||
|
want int64
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{field: "Content-Length: 0", want: 0},
|
||||||
|
{field: "Content-Length: 12", want: 12},
|
||||||
|
{field: "Content-Length: 12 ", want: 12}, // OWS around the value, RFC 9110 5.6.3.
|
||||||
|
{field: "Content-Length: 9223372036854775807", want: 9223372036854775807},
|
||||||
|
{field: "", wantErr: errNoContentLength}, // No body, RFC 9112 6.3.
|
||||||
|
{field: "Content-Length:", wantErr: errBadContentLength}, // Present but empty.
|
||||||
|
{field: "Content-Length: -1", wantErr: errBadContentLength}, // Digits only, RFC 9112 6.2.
|
||||||
|
{field: "Content-Length: 1 2", wantErr: errBadContentLength}, // Not a list.
|
||||||
|
{field: "Content-Length: 9223372036854775808", wantErr: errBadContentLength},
|
||||||
|
} {
|
||||||
|
var h Header
|
||||||
|
raw := "POST / HTTP/1.1\r\nHost: h\r\n"
|
||||||
|
if test.field != "" {
|
||||||
|
raw += test.field + "\r\n"
|
||||||
|
}
|
||||||
|
if err := h.ParseBytes(false, []byte(raw+"\r\n")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := h.ContentLength()
|
||||||
|
if err != test.wantErr {
|
||||||
|
t.Errorf("%q: want error %v, got %v", test.field, test.wantErr, err)
|
||||||
|
} else if err == nil && got != test.want {
|
||||||
|
t.Errorf("%q: want %d, got %d", test.field, test.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNextQueryPair(t *testing.T) {
|
func TestNextQueryPair(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
uri string
|
uri string
|
||||||
|
|||||||
@@ -49,9 +49,12 @@ type MultipartHeader struct {
|
|||||||
Filename []byte
|
Filename []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContentType sets the boundary parameter of a Content-Type field value,
|
// SetContentType sets [Multipart.Boundary] from the boundary parameter of a
|
||||||
// i.e: "abc123" for "multipart/form-data; boundary=abc123". The leading "--" of
|
// Content-Type field value, i.e: "abc123" for
|
||||||
// the wire delimiter is not included. Returns nil if there is no such parameter.
|
// "multipart/form-data; boundary=abc123". The leading "--" the delimiter carries
|
||||||
|
// on the wire is not included. Fails when the parameter is absent or is not
|
||||||
|
// 1 to 70 characters long, RFC 2046 5.1.1; a zero length boundary would match
|
||||||
|
// every "--" in the body.
|
||||||
func (m *Multipart) SetContentType(contentType []byte) error {
|
func (m *Multipart) SetContentType(contentType []byte) error {
|
||||||
m.Boundary = ContentParam(contentType, "boundary")
|
m.Boundary = ContentParam(contentType, "boundary")
|
||||||
if len(m.Boundary) == 0 || len(m.Boundary) > 70 {
|
if len(m.Boundary) == 0 || len(m.Boundary) > 70 {
|
||||||
@@ -86,7 +89,7 @@ func (m *Multipart) NextHeader(dst *MultipartHeader, data []byte) (rest []byte,
|
|||||||
if after >= len(data) {
|
if after >= len(data) {
|
||||||
return nil, ErrNeedMoreData
|
return nil, ErrNeedMoreData
|
||||||
} else if data[after] != '\n' {
|
} else if data[after] != '\n' {
|
||||||
return nil, errInvalidName // Junk between delimiter and part.
|
return nil, errBadDelimiter
|
||||||
}
|
}
|
||||||
after++
|
after++
|
||||||
end := bytes.Index(data[after:], []byte("\r\n\r\n"))
|
end := bytes.Index(data[after:], []byte("\r\n\r\n"))
|
||||||
@@ -155,6 +158,17 @@ func (m *Multipart) indexPartEnd(data []byte) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MediaTypeIs reports whether a Content-Type field value carries the given
|
||||||
|
// media type, ignoring case and any parameters that follow it, i.e: true for
|
||||||
|
// "text/plain; charset=utf-8" and media type "text/plain". mediaType must be
|
||||||
|
// ASCII lowercase. RFC 9110 8.3.1.
|
||||||
|
func MediaTypeIs(value []byte, mediaType string) bool {
|
||||||
|
if semi := bytes.IndexByte(value, ';'); semi >= 0 {
|
||||||
|
value = value[:semi]
|
||||||
|
}
|
||||||
|
return equalFold(trimOWS(value), mediaType)
|
||||||
|
}
|
||||||
|
|
||||||
// ContentParam returns the value of a parameter of a header field value, i.e:
|
// ContentParam returns the value of a parameter of a header field value, i.e:
|
||||||
// "utf-8" for key "charset" of "text/plain; charset=utf-8". Quoted values are
|
// "utf-8" for key "charset" of "text/plain; charset=utf-8". Quoted values are
|
||||||
// returned without their quotes and with escapes left as they appear on the
|
// returned without their quotes and with escapes left as they appear on the
|
||||||
|
|||||||
@@ -28,17 +28,25 @@ func TestMultipartBoundary(t *testing.T) {
|
|||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
contentType string
|
contentType string
|
||||||
want string
|
want string
|
||||||
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{contentType: "multipart/form-data; boundary=abc123", want: "abc123"},
|
{contentType: "multipart/form-data; boundary=abc123", want: "abc123"},
|
||||||
{contentType: "multipart/form-data; boundary=\"a b\"", want: "a b"},
|
{contentType: "multipart/form-data; boundary=\"a b\"", want: "a b"},
|
||||||
{contentType: "multipart/form-data; charset=utf-8; boundary=xyz", want: "xyz"},
|
{contentType: "multipart/form-data; charset=utf-8; boundary=xyz", want: "xyz"},
|
||||||
{contentType: "multipart/form-data; BOUNDARY=xyz", want: "xyz"}, // Keys are case insensitive.
|
{contentType: "multipart/form-data; BOUNDARY=xyz", want: "xyz"}, // Keys are case insensitive.
|
||||||
{contentType: "multipart/form-data", want: ""}, // Absent.
|
{contentType: "multipart/form-data", wantErr: true}, // Absent, RFC 2046 5.1.1 requires it.
|
||||||
{contentType: "application/x-www-form-urlencoded", want: ""},
|
{contentType: "application/x-www-form-urlencoded", wantErr: true}, // Not multipart at all.
|
||||||
|
{contentType: "multipart/form-data; boundary=", wantErr: true}, // Empty matches every "--".
|
||||||
} {
|
} {
|
||||||
err := mp.SetContentType([]byte(test.contentType))
|
err := mp.SetContentType([]byte(test.contentType))
|
||||||
if err != nil {
|
if test.wantErr {
|
||||||
t.Skip("asdasd")
|
if err == nil {
|
||||||
|
t.Errorf("%q: want error, got boundary %q", test.contentType, mp.Boundary)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
t.Errorf("%q: %s", test.contentType, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
got := string(mp.Boundary)
|
got := string(mp.Boundary)
|
||||||
if got != test.want {
|
if got != test.want {
|
||||||
@@ -47,6 +55,28 @@ func TestMultipartBoundary(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMediaTypeIs(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
value string
|
||||||
|
media string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{value: "text/plain", media: "text/plain", want: true},
|
||||||
|
{value: "text/plain; charset=utf-8", media: "text/plain", want: true},
|
||||||
|
{value: "text/plain;charset=utf-8", media: "text/plain", want: true},
|
||||||
|
{value: "Text/Plain", media: "text/plain", want: true}, // Case insensitive, RFC 9110 8.3.1.
|
||||||
|
{value: " text/plain ; x=1", media: "text/plain", want: true},
|
||||||
|
{value: "text/plain", media: "text/html"},
|
||||||
|
{value: "text/plainish", media: "text/plain"}, // Prefix must not match.
|
||||||
|
{value: "", media: "text/plain"},
|
||||||
|
{value: "multipart/form-data; boundary=abc", media: "multipart/form-data", want: true},
|
||||||
|
} {
|
||||||
|
if got := MediaTypeIs([]byte(test.value), test.media); got != test.want {
|
||||||
|
t.Errorf("%q is %q: want %v, got %v", test.value, test.media, test.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContentParam(t *testing.T) {
|
func TestContentParam(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
value string
|
value string
|
||||||
@@ -106,6 +136,16 @@ func TestNextPartHeaderNeedMore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Junk between the delimiter and the part header is a multipart framing error,
|
||||||
|
// not a header field name error.
|
||||||
|
func TestNextPartHeaderJunk(t *testing.T) {
|
||||||
|
m := Multipart{Boundary: []byte("abc")}
|
||||||
|
var hdr MultipartHeader
|
||||||
|
if _, err := m.NextHeader(&hdr, []byte("--abcX\r\nA: b\r\n\r\n")); err != errBadDelimiter {
|
||||||
|
t.Errorf("want errBadDelimiter, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The closing delimiter ends iteration.
|
// The closing delimiter ends iteration.
|
||||||
func TestNextPartHeaderEnd(t *testing.T) {
|
func TestNextPartHeaderEnd(t *testing.T) {
|
||||||
m := Multipart{Boundary: []byte(multiBoundary)}
|
m := Multipart{Boundary: []byte(multiBoundary)}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ var (
|
|||||||
errCookiesParsed = errors.New("cookies already parsed, reset before parsing again")
|
errCookiesParsed = errors.New("cookies already parsed, reset before parsing again")
|
||||||
errBufferTooLarge = errors.New("httpraw: buffer exceeds max size (offsets are uint16)")
|
errBufferTooLarge = errors.New("httpraw: buffer exceeds max size (offsets are uint16)")
|
||||||
errBadPercentEncode = errors.New("httpraw: invalid percent-encoding in URL")
|
errBadPercentEncode = errors.New("httpraw: invalid percent-encoding in URL")
|
||||||
|
errBadDelimiter = errors.New("httpraw: junk between multipart delimiter and part")
|
||||||
|
errNoContentLength = errors.New("httpraw: no Content-Length field")
|
||||||
|
errBadContentLength = errors.New("httpraw: invalid Content-Length value")
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxBufLen bounds the header buffer. Offsets/lengths are stored as uint16
|
// maxBufLen bounds the header buffer. Offsets/lengths are stored as uint16
|
||||||
|
|||||||
Reference in New Issue
Block a user