Skip to content

Commit

Permalink
fix: workaround buffered reader in http matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
ydylla committed Dec 1, 2022
1 parent 3fae9e6 commit 0f0cea0
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 6 deletions.
38 changes: 32 additions & 6 deletions modules/l4http/httpmatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ package l4http

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/mholt/caddy-l4/layer4"
"github.com/mholt/caddy-l4/modules/l4tls"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
"io"
"net/http"
"net/url"
)

func init() {
Expand Down Expand Up @@ -78,11 +80,18 @@ func (m MatchHTTP) Match(cx *layer4.Connection) (bool, error) {
req, ok := cx.GetVar("http_request").(*http.Request)
if !ok {
var err error
bufReader := bufio.NewReader(cx)

data, err := cx.MatchingBytes()
if !m.isHttp(data) {
return false, nil
}

// use bufio reader which exactly matches the size of prefetched data,
// to not trigger all bytes consumed error
bufReader := bufio.NewReaderSize(cx, len(data))
req, err = http.ReadRequest(bufReader)
if err != nil {
// TODO: find a way to distinguish actual errors from mismatches
return false, nil
return false, err
}

// check if req is a http2 request made with prior knowledge and if so parse it
Expand Down Expand Up @@ -113,6 +122,23 @@ func (m MatchHTTP) Match(cx *layer4.Connection) (bool, error) {
return m.matcherSets.AnyMatch(req), nil
}

func (m MatchHTTP) isHttp(data []byte) bool {
// try to find the end of a http request line, for example " HTTP/1.1\r\n"
i := bytes.IndexByte(data, 0x0a) // find first new line
if i < 10 {
return false
}
// assume only \n line ending
start := i - 9 // position of space in front of HTTP
end := i - 3 // cut off version number "1.1" or "2.0"
// if we got a correct \r\n line ending shift the calculated start & end to the left
if data[i-1] == 0x0d {
start -= 1
end -= 1
}
return bytes.Compare(data[start:end], []byte(" HTTP/")) == 0
}

// Parses information from a http2 request with prior knowledge (RFC 7540 Section 3.4)
func (m MatchHTTP) handleHttp2WithPriorKnowledge(reader io.Reader, req *http.Request) error {
// Does req contain a valid http2 magic?
Expand Down
51 changes: 51 additions & 0 deletions modules/l4http/httpmatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,54 @@ func TestHttpMatchingGarbage(t *testing.T) {
t.Fatalf("handler did not return an error or the wrong error -> %v", err)
}
}

func TestMatchHTTP_isHttp(t *testing.T) {
for _, tc := range []struct {
name string
data []byte
shouldMatch bool
}{
{
name: "http/1.1-only-lf",
data: []byte("GET /foo/bar?aaa=bbb HTTP/1.1\nHost: localhost:10443\n\n"),
shouldMatch: true,
},
{
name: "http/1.1-cr-lf",
data: []byte("GET /foo/bar?aaa=bbb HTTP/1.1\r\nHost: localhost:10443\r\n\r\n"),
shouldMatch: true,
},
{
name: "http/1.0-cr-lf",
data: []byte("GET /foo/bar?aaa=bbb HTTP/1.0\r\nHost: localhost:10443\r\n\r\n"),
shouldMatch: true,
},
{
name: "http/2.0-cr-lf",
data: []byte("PRI * HTTP/2.0\r\n\r\n"),
shouldMatch: true,
},
{
name: "dummy-short",
data: []byte("dum\n"),
shouldMatch: false,
},
{
name: "dummy-long",
data: []byte("dummydummydummy\n"),
shouldMatch: false,
},
{
name: "http/1.1-without-space-in-front",
data: []byte("HTTP/1.1\n"),
shouldMatch: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
matched := MatchHTTP{}.isHttp(tc.data)
if matched != tc.shouldMatch {
t.Fatalf("test %v | matched: %v != shouldMatch: %v", tc.name, matched, tc.shouldMatch)
}
})
}
}

0 comments on commit 0f0cea0

Please sign in to comment.