Proxy chunking should handle arbitrarily large chunks

This commit is contained in:
Paul Bellamy committed 2015-08-04 17:47:51 +01:00
1 parent 1d1ebc103d
commit b85b90ef4e
4 files changed
+285 -196

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
// Based on net/http/internal
package proxy
import (
"bufio"
"bytes"
"errors"
"io"
"io/ioutil"
"strconv"
)
const maxLineLength = 4096 // assumed <= bufio.defaultBufSize
var (
ErrLineTooLong = errors.New("header line too long")
ErrInvalidChunkLength = errors.New("invalid byte in chunk length")
)
// Unlike net/http/internal.chunkedReader, this has an interface where we can
// handle individual chunks. The interface is based on database/sql.Rows.
func NewChunkedReader(r io.Reader) *ChunkedReader {
br, ok := r.(*bufio.Reader)
if !ok {
br = bufio.NewReader(r)
}
return &ChunkedReader{r: br}
}
type ChunkedReader struct {
r *bufio.Reader
chunk *io.LimitedReader
err error
buf [2]byte
}
// Next prepares the next chunk for reading. It returns true on success, or
// false if there is no next chunk or an error happened while preparing
// it. Err should be consulted to distinguish between the two cases.
//
// Every call to Chunk, even the first one, must be preceded by a call to Next.
//
// Calls to Next will discard any unread bytes in the current Chunk.
func (cr *ChunkedReader) Next() bool {
if cr.err != nil {
return false
}
// Check the termination of the previous chunk
if cr.chunk != nil {
// Make sure the remainder is drained, in case the user of this quit
// reading early.
if _, cr.err = io.Copy(ioutil.Discard, cr.chunk); cr.err != nil {
return false
}
// Check the next two bytes after the chunk are \r\n
if _, cr.err = io.ReadFull(cr.r, cr.buf[:2]); cr.err != nil {
return false
}
if cr.buf[0] != '\r' || cr.buf[1] != '\n' {
cr.err = errors.New("malformed chunked encoding")
return false
}
} else {
cr.chunk = &io.LimitedReader{R: cr.r}
}
// Setup the next chunk
if n := cr.beginChunk(); n > 0 {
cr.chunk.N = int64(n)
} else if cr.err == nil {
cr.err = io.EOF
}
return cr.err == nil
}
// Chunk returns the io.Reader of the current chunk. On each call, this returns
// the same io.Reader for a given chunk.
func (cr *ChunkedReader) Chunk() io.Reader {
return cr.chunk
}
// Err returns the error, if any, that was encountered during iteration.
func (cr *ChunkedReader) Err() error {
if cr.err == io.EOF {
return nil
}
return cr.err
}
func (cr *ChunkedReader) beginChunk() (n uint64) {
// chunk-size CRLF
var line []byte
line, cr.err = readLine(cr.r)
if cr.err != nil {
return
}
n, cr.err = strconv.ParseUint(string(line), 16, 64)
if cr.err != nil {
cr.err = ErrInvalidChunkLength
}
return
}
// Read a line of bytes (up to \n) from b.
// Give up if the line exceeds maxLineLength.
// The returned bytes are a pointer into storage in
// the bufio, so they are only valid until the next bufio read.
func readLine(b *bufio.Reader) (p []byte, err error) {
if p, err = b.ReadSlice('\n'); err != nil {
// We always know when EOF is coming.
// If the caller asked for a line, there should be a line.
if err == io.EOF {
err = io.ErrUnexpectedEOF
} else if err == bufio.ErrBufferFull {
err = ErrLineTooLong
}
return nil, err
}
if len(p) >= maxLineLength {
return nil, ErrLineTooLong
}
return bytes.TrimRight(p, " \t\n\r"), nil
}
+157
View File
@@ -0,0 +1,157 @@
// Based on net/http/internal
package proxy
import (
"bytes"
"io"
"io/ioutil"
"strconv"
"strings"
"testing"
)
func TestChunk(t *testing.T) {
r := NewChunkedReader(bytes.NewBufferString(
"7\r\nhello, \r\n17\r\nworld! 0123456789abcdef\r\n0\r\n",
))
assertNextChunk(t, r, "hello, ")
assertNextChunk(t, r, "world! 0123456789abcdef")
assertNoMoreChunks(t, r)
}
func TestIncompleteReadOfChunk(t *testing.T) {
r := NewChunkedReader(bytes.NewBufferString(
"7\r\nhello, \r\n17\r\nworld! 0123456789abcdef\r\n0\r\n",
))
// Incomplete read of first chunk
{
if !r.Next() {
t.Fatalf("Expected chunk, but ran out early: %v", r.Err())
}
if r.Err() != nil {
t.Fatalf("Error reading chunk: %q", r.Err())
}
// Read just 2 bytes
buf := make([]byte, 2)
if _, err := io.ReadFull(r.Chunk(), buf[:2]); err != nil {
t.Fatalf("Error reading first bytes of chunk: %q", err)
}
if buf[0] != 'h' || buf[1] != 'e' {
t.Fatalf("Unexpected first 2 bytes of chunk: %q", buf)
}
}
assertNextChunk(t, r, "world! 0123456789abcdef")
assertNoMoreChunks(t, r)
}
func TestMalformedChunks(t *testing.T) {
r := NewChunkedReader(bytes.NewBufferString(
"7\r\nhello, GARBAGEBYTES17\r\nworld! 0123456789abcdef\r\n0\r\n",
))
assertNextChunk(t, r, "hello, ")
assertError(t, r, "malformed chunked encoding")
}
type charReader byte
// Read an infinite sequence of some char
func (r *charReader) Read(p []byte) (int, error) {
b := byte(*r)
for i := range p {
p[i] = b
}
return len(p), nil
}
func TestLargeChunks(t *testing.T) {
var expected int64 = 1024 * 1024
chars := charReader('a')
r := NewChunkedReader(io.MultiReader(
strings.NewReader(strconv.FormatInt(expected, 16)+"\r\n"),
&io.LimitedReader{N: expected, R: &chars},
strings.NewReader("\r\n0\r\n"),
))
if !r.Next() {
t.Fatalf("Expected chunk, but ran out early: %v", r.Err())
}
if r.Err() != nil {
t.Fatalf("Error reading chunk: %q", r.Err())
}
n, err := io.Copy(ioutil.Discard, r.Chunk())
if n != expected {
t.Errorf("chunk reader read %q; want %q", n, expected)
}
if err != nil {
t.Fatalf("reading chunk: %v", err)
}
assertNoMoreChunks(t, r)
}
func TestInvalidChunkSize(t *testing.T) {
r := NewChunkedReader(bytes.NewBufferString(
"foobar\r\nhello, \r\n0\r\n",
))
assertError(t, r, "invalid byte in chunk length")
}
func TestBytesAfterLastChunkAreIgnored(t *testing.T) {
r := NewChunkedReader(bytes.NewBufferString(
"7\r\nhello, \r\n0\r\nGARBAGEBYTES",
))
assertNextChunk(t, r, "hello, ")
assertNoMoreChunks(t, r)
}
func assertNextChunk(t *testing.T, r *ChunkedReader, expected string) {
if !r.Next() {
t.Fatalf("Expected chunk, but ran out early: %v", r.Err())
}
if r.Err() != nil {
t.Fatalf("Error reading chunk: %q", r.Err())
}
data, err := ioutil.ReadAll(r.Chunk())
if string(data) != expected {
t.Errorf("chunk reader read %q; want %q", data, expected)
}
if err != nil {
t.Logf(`data: %q`, data)
t.Fatalf("reading chunk: %v", err)
}
}
func assertError(t *testing.T, r *ChunkedReader, e string) {
if r.Next() {
t.Errorf("Expected failure when reading chunks, but got one")
}
if r.Err() == nil || r.Err().Error() != e {
t.Errorf("chunk reader errored %q; want %q", r.Err(), e)
}
data, err := ioutil.ReadAll(r.Chunk())
if len(data) != 0 {
t.Errorf("chunk should have been empty. got %q", data)
}
if err != nil {
t.Logf(`data: %q`, data)
t.Errorf("reading chunk: %v", err)
}
if r.Next() {
t.Errorf("Expected no more chunks, but found too many")
}
}
func assertNoMoreChunks(t *testing.T, r *ChunkedReader) {
if r.Next() {
t.Errorf("Expected no more chunks, but found too many")
}
if r.Err() != nil {
t.Errorf("Expected no error, but found: %q", r.Err())
}
}
+3 -60
View File
@@ -2,32 +2,18 @@ package proxy
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"strconv"
"sync"
"github.com/fsouza/go-dockerclient"
. "github.com/weaveworks/weave/common"
)
const (
maxLineLength = 4096 // assumed <= bufio.defaultBufSize
maxChunkSize = bufio.MaxScanTokenSize
)
var (
ErrChunkTooLong = errors.New("chunk too long")
ErrInvalidChunkLength = errors.New("invalid byte in chunk length")
ErrLineTooLong = errors.New("header line too long")
ErrMalformedChunkEncoding = errors.New("malformed chunked encoding")
)
func (proxy *Proxy) Intercept(i interceptor, w http.ResponseWriter, r *http.Request) {
if err := i.InterceptRequest(r); err != nil {
switch err.(type) {
@@ -153,10 +139,9 @@ func doChunkedResponse(w http.ResponseWriter, resp *http.Response, client *httpu
defer up.Close()
var err error
chunks := bufio.NewScanner(io.MultiReader(remaining, up))
chunks.Split(splitChunks)
for chunks.Scan() && err == nil {
_, err = wf.Write(chunks.Bytes())
chunks := NewChunkedReader(io.MultiReader(remaining, up))
for chunks.Next() && err == nil {
_, err = io.Copy(wf, chunks.Chunk())
wf.Flush()
}
if err == nil {
@@ -167,48 +152,6 @@ func doChunkedResponse(w http.ResponseWriter, resp *http.Response, client *httpu
}
}
// a bufio.SplitFunc for http chunks
func splitChunks(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
i := bytes.IndexByte(data, '\n')
if i < 0 {
return 0, nil, nil
}
if i > maxLineLength {
return 0, nil, ErrLineTooLong
}
chunkSize64, err := strconv.ParseInt(
string(bytes.TrimRight(data[:i], " \t\r\n")),
16,
64,
)
switch {
case err != nil:
return 0, nil, ErrInvalidChunkLength
case chunkSize64 > maxChunkSize:
return 0, nil, ErrChunkTooLong
case chunkSize64 == 0:
return 0, nil, io.EOF
}
chunkSize := int(chunkSize64)
data = data[i+1:]
if len(data) < chunkSize+2 {
return 0, nil, nil
}
if data[chunkSize] != '\r' || data[chunkSize+1] != '\n' {
return 0, nil, ErrMalformedChunkEncoding
}
return i + chunkSize + 3, data[:chunkSize], nil
}
func hijack(w http.ResponseWriter, client *httputil.ClientConn) (down net.Conn, downBuf *bufio.ReadWriter, up net.Conn, remaining io.Reader, err error) {
hj, ok := w.(http.Hijacker)
if !ok {
-136
View File
@@ -1,136 +0,0 @@
// Based on net/http/internal
package proxy
import (
"bufio"
"bytes"
"strconv"
"strings"
"testing"
)
func TestChunk(t *testing.T) {
r := bufio.NewScanner(bytes.NewBufferString(
"7\r\nhello, \r\n17\r\nworld! 0123456789abcdef\r\n0\r\n",
))
r.Split(splitChunks)
assertNextChunk(t, r, "hello, ")
assertNextChunk(t, r, "world! 0123456789abcdef")
assertNoMoreChunks(t, r)
}
func TestMalformedChunks(t *testing.T) {
r := bufio.NewScanner(bytes.NewBufferString(
"7\r\nhello, GARBAGEBYTES17\r\nworld! 0123456789abcdef\r\n0\r\n",
))
r.Split(splitChunks)
// First chunk fails
{
if r.Scan() {
t.Errorf("Expected failure when reading chunks, but got one")
}
e := "malformed chunked encoding"
if r.Err() == nil || r.Err().Error() != e {
t.Errorf("chunk reader errored %q; want %q", r.Err(), e)
}
data := r.Bytes()
if len(data) != 0 {
t.Errorf("chunk should have been empty. got %q", data)
}
}
if r.Scan() {
t.Errorf("Expected no more chunks, but found too many")
}
}
func TestChunkTooLarge(t *testing.T) {
data := make([]byte, maxChunkSize+1)
r := bufio.NewScanner(bytes.NewBufferString(strings.Join(
[]string{
strconv.FormatInt(maxChunkSize+1, 16), string(data),
"0", "",
},
"\r\n",
)))
r.Split(splitChunks)
// First chunk fails
{
if r.Scan() {
t.Errorf("Expected failure when reading chunks, but got one")
}
e := "chunk too long"
if r.Err() == nil || r.Err().Error() != e {
t.Errorf("chunk reader errored %q; want %q", r.Err(), e)
}
data := r.Bytes()
if len(data) != 0 {
t.Errorf("chunk should have been empty. got %q", data)
}
}
if r.Scan() {
t.Errorf("Expected no more chunks, but found too many")
}
}
func TestInvalidChunkSize(t *testing.T) {
r := bufio.NewScanner(bytes.NewBufferString(
"foobar\r\nhello, \r\n0\r\n",
))
r.Split(splitChunks)
// First chunk fails
{
if r.Scan() {
t.Errorf("Expected failure when reading chunks, but got one")
}
e := "invalid byte in chunk length"
if r.Err() == nil || r.Err().Error() != e {
t.Errorf("chunk reader errored %q; want %q", r.Err(), e)
}
data := r.Bytes()
if len(data) != 0 {
t.Errorf("chunk should have been empty. got %q", data)
}
}
if r.Scan() {
t.Errorf("Expected no more chunks, but found too many")
}
}
func TestBytesAfterLastChunkAreIgnored(t *testing.T) {
r := bufio.NewScanner(bytes.NewBufferString(
"7\r\nhello, \r\n0\r\nGARBAGEBYTES",
))
r.Split(splitChunks)
assertNextChunk(t, r, "hello, ")
assertNoMoreChunks(t, r)
}
func assertNextChunk(t *testing.T, r *bufio.Scanner, expected string) {
if !r.Scan() {
t.Fatalf("Expected chunk, but ran out early: %v", r.Err())
}
if r.Err() != nil {
t.Fatalf("Error reading chunk: %q", r.Err())
}
data := r.Bytes()
if string(data) != expected {
t.Errorf("chunk reader read %q; want %q", data, expected)
}
}
func assertNoMoreChunks(t *testing.T, r *bufio.Scanner) {
if r.Scan() {
t.Errorf("Expected no more chunks, but found too many")
}
if r.Err() != nil {
t.Errorf("Expected no error, but found: %q", r.Err())
}
}