mirror of
https://github.com/soypat/lneto.git
synced 2026-09-01 04:19:05 +00:00
first Multipart approach
This commit is contained in:
+30
-6
@@ -4,15 +4,17 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
methodGet = "GET"
|
||||
strHTTP11 = "HTTP/1.1"
|
||||
strCRLF = "\r\n"
|
||||
headerCookie = "Cookie"
|
||||
headerConnection = "Connection"
|
||||
strClose = "close"
|
||||
methodGet = "GET"
|
||||
strHTTP11 = "HTTP/1.1"
|
||||
strCRLF = "\r\n"
|
||||
headerCookie = "Cookie"
|
||||
headerConnection = "Connection"
|
||||
headerContentLength = "Content-Length"
|
||||
strClose = "close"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (h *Header) Add(key, value string) {
|
||||
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) {
|
||||
for _, test := range []struct {
|
||||
uri string
|
||||
|
||||
@@ -49,9 +49,12 @@ type MultipartHeader struct {
|
||||
Filename []byte
|
||||
}
|
||||
|
||||
// SetContentType sets the boundary parameter of a Content-Type field value,
|
||||
// i.e: "abc123" for "multipart/form-data; boundary=abc123". The leading "--" of
|
||||
// the wire delimiter is not included. Returns nil if there is no such parameter.
|
||||
// SetContentType sets [Multipart.Boundary] from the boundary parameter of a
|
||||
// Content-Type field value, i.e: "abc123" for
|
||||
// "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 {
|
||||
m.Boundary = ContentParam(contentType, "boundary")
|
||||
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) {
|
||||
return nil, ErrNeedMoreData
|
||||
} else if data[after] != '\n' {
|
||||
return nil, errInvalidName // Junk between delimiter and part.
|
||||
return nil, errBadDelimiter
|
||||
}
|
||||
after++
|
||||
end := bytes.Index(data[after:], []byte("\r\n\r\n"))
|
||||
@@ -155,6 +158,17 @@ func (m *Multipart) indexPartEnd(data []byte) int {
|
||||
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:
|
||||
// "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
|
||||
|
||||
@@ -28,17 +28,25 @@ func TestMultipartBoundary(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
contentType string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{contentType: "multipart/form-data; boundary=abc123", want: "abc123"},
|
||||
{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; BOUNDARY=xyz", want: "xyz"}, // Keys are case insensitive.
|
||||
{contentType: "multipart/form-data", want: ""}, // Absent.
|
||||
{contentType: "application/x-www-form-urlencoded", want: ""},
|
||||
{contentType: "multipart/form-data; BOUNDARY=xyz", want: "xyz"}, // Keys are case insensitive.
|
||||
{contentType: "multipart/form-data", wantErr: true}, // Absent, RFC 2046 5.1.1 requires it.
|
||||
{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))
|
||||
if err != nil {
|
||||
t.Skip("asdasd")
|
||||
if test.wantErr {
|
||||
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)
|
||||
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) {
|
||||
for _, test := range []struct {
|
||||
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.
|
||||
func TestNextPartHeaderEnd(t *testing.T) {
|
||||
m := Multipart{Boundary: []byte(multiBoundary)}
|
||||
|
||||
@@ -33,6 +33,9 @@ var (
|
||||
errCookiesParsed = errors.New("cookies already parsed, reset before parsing again")
|
||||
errBufferTooLarge = errors.New("httpraw: buffer exceeds max size (offsets are uint16)")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user