github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4http/httpmatcher_test.go (about)

     1  package l4http
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"net"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/caddyserver/caddy/v2"
    13  	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
    14  	"go.uber.org/zap"
    15  
    16  	"github.com/mholt/caddy-l4/layer4"
    17  )
    18  
    19  func assertNoError(t *testing.T, err error) {
    20  	t.Helper()
    21  	if err != nil {
    22  		t.Fatalf("Unexpected error: %s\n", err)
    23  	}
    24  }
    25  
    26  // testHandler is a connection handler that will set a variable to let us know it was called.
    27  type testHandler struct {
    28  }
    29  
    30  // CaddyModule returns the Caddy module information.
    31  func (testHandler) CaddyModule() caddy.ModuleInfo {
    32  	return caddy.ModuleInfo{
    33  		ID:  "layer4.handlers.test_handler",
    34  		New: func() caddy.Module { return new(testHandler) },
    35  	}
    36  }
    37  
    38  // Handle handles the connections.
    39  func (h *testHandler) Handle(cx *layer4.Connection, next layer4.Handler) error {
    40  	cx.SetVar("test_handler_called", true)
    41  	return next.Handle(cx)
    42  }
    43  
    44  func init() {
    45  	caddy.RegisterModule(testHandler{})
    46  }
    47  
    48  func httpMatchTester(t *testing.T, matchers json.RawMessage, data []byte) (bool, error) {
    49  	in, out := net.Pipe()
    50  	defer func() { _ = in.Close() }()
    51  	defer func() { _ = out.Close() }()
    52  
    53  	cx := layer4.WrapConnection(in, make([]byte, 0), zap.NewNop())
    54  	go func() {
    55  		_, err := out.Write(data)
    56  		assertNoError(t, err)
    57  	}()
    58  
    59  	ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
    60  	defer cancel()
    61  
    62  	routes := layer4.RouteList{&layer4.Route{
    63  		MatcherSetsRaw: caddyhttp.RawMatcherSets{
    64  			caddy.ModuleMap{"http": matchers},
    65  		},
    66  		HandlersRaw: []json.RawMessage{json.RawMessage("{\"handler\":\"test_handler\"}")},
    67  	}}
    68  	err := routes.Provision(ctx)
    69  	assertNoError(t, err)
    70  
    71  	matched := false
    72  	compiledRoute := routes.Compile(zap.NewNop(), 10*time.Millisecond,
    73  		layer4.HandlerFunc(func(con *layer4.Connection) error {
    74  			matched = con.GetVar("test_handler_called") != nil
    75  			return nil
    76  		}))
    77  
    78  	err = compiledRoute.Handle(cx)
    79  	assertNoError(t, err)
    80  
    81  	return matched, err
    82  }
    83  
    84  func TestHttp1Matching(t *testing.T) {
    85  	http1RequestExample := []byte("GET /foo/bar?aaa=bbb HTTP/1.1\nHost: localhost:10443\nUser-Agent: curl/7.82.0\nAccept: */*\n\n")
    86  
    87  	for _, tc := range []struct {
    88  		name     string
    89  		matchers json.RawMessage
    90  		data     []byte
    91  	}{
    92  		{
    93  			name:     "match-by-host",
    94  			matchers: json.RawMessage("[{\"host\":[\"localhost\"]}]"),
    95  			data:     http1RequestExample,
    96  		},
    97  		{
    98  			name:     "match-by-method",
    99  			matchers: json.RawMessage("[{\"method\":[\"GET\"]}]"),
   100  			data:     http1RequestExample,
   101  		},
   102  		{
   103  			name:     "match-by-path",
   104  			matchers: json.RawMessage("[{\"path\":[\"/foo/bar\"]}]"),
   105  			data:     http1RequestExample,
   106  		},
   107  		{
   108  			name:     "match-by-query",
   109  			matchers: json.RawMessage("[{\"query\":{\"aaa\":[\"bbb\"]}}]"),
   110  			data:     http1RequestExample,
   111  		},
   112  		{
   113  			name:     "match-by-header",
   114  			matchers: json.RawMessage("[{\"header\":{\"user-agent\":[\"curl*\"]}}]"),
   115  			data:     http1RequestExample,
   116  		},
   117  		{
   118  			name:     "match-by-protocol",
   119  			matchers: json.RawMessage("[{\"protocol\":\"http\"}]"),
   120  			data:     http1RequestExample,
   121  		},
   122  	} {
   123  		t.Run(tc.name, func(t *testing.T) {
   124  			matched, err := httpMatchTester(t, tc.matchers, tc.data)
   125  			assertNoError(t, err)
   126  			if !matched {
   127  				t.Errorf("matcher did not match")
   128  			}
   129  		})
   130  	}
   131  }
   132  
   133  func TestHttp2Matching(t *testing.T) {
   134  	http2PriorKnowledgeRequestExample, err := base64.StdEncoding.DecodeString("UFJJICogSFRUUC8yLjANCg0KU00NCg0KAAASBAAAAAAAAAMAAABkAAQCAAAAAAIAAAAAAAAECAAAAAAAAf8AAQAALAEFAAAAAYIEjGJTnYjHZ/gxjgjjj4dBi6DkHROdCbgQNNM/eogltlDDq7wlwVMDKi8q")
   135  	assertNoError(t, err)
   136  
   137  	http2UpgradeRequestExample, err := base64.StdEncoding.DecodeString("R0VUIC9mb28vYmFyP2FhYT1iYmIgSFRUUC8xLjENCkhvc3Q6IGxvY2FsaG9zdDoxMDQ0Mw0KVXNlci1BZ2VudDogY3VybC83LjgyLjANCkFjY2VwdDogKi8qDQpDb25uZWN0aW9uOiBVcGdyYWRlLCBIVFRQMi1TZXR0aW5ncw0KVXBncmFkZTogaDJjDQpIVFRQMi1TZXR0aW5nczogQUFNQUFBQmtBQVFDQUFBQUFBSUFBQUFBDQoNCg==")
   138  	assertNoError(t, err)
   139  
   140  	for _, tc := range []struct {
   141  		name     string
   142  		matchers json.RawMessage
   143  		data     []byte
   144  	}{
   145  		{
   146  			name:     "match-by-host",
   147  			matchers: json.RawMessage("[{\"host\":[\"localhost\"]}]"),
   148  			data:     http2PriorKnowledgeRequestExample,
   149  		},
   150  		{
   151  			name:     "match-by-method",
   152  			matchers: json.RawMessage("[{\"method\":[\"GET\"]}]"),
   153  			data:     http2PriorKnowledgeRequestExample,
   154  		},
   155  		{
   156  			name:     "match-by-path",
   157  			matchers: json.RawMessage("[{\"path\":[\"/foo/bar\"]}]"),
   158  			data:     http2PriorKnowledgeRequestExample,
   159  		},
   160  		{
   161  			name:     "match-by-query",
   162  			matchers: json.RawMessage("[{\"query\":{\"aaa\":[\"bbb\"]}}]"),
   163  			data:     http2PriorKnowledgeRequestExample,
   164  		},
   165  		{
   166  			name:     "match-by-header",
   167  			matchers: json.RawMessage("[{\"header\":{\"user-agent\":[\"curl*\"]}}]"),
   168  			data:     http2PriorKnowledgeRequestExample,
   169  		},
   170  		{
   171  			name:     "match-by-protocol",
   172  			matchers: json.RawMessage("[{\"protocol\":\"http\"}]"),
   173  			data:     http2PriorKnowledgeRequestExample,
   174  		},
   175  
   176  		{
   177  			name:     "upgrade-match-by-host",
   178  			matchers: json.RawMessage("[{\"host\":[\"localhost\"]}]"),
   179  			data:     http2UpgradeRequestExample,
   180  		},
   181  		{
   182  			name:     "upgrade-match-by-method",
   183  			matchers: json.RawMessage("[{\"method\":[\"GET\"]}]"),
   184  			data:     http2UpgradeRequestExample,
   185  		},
   186  		{
   187  			name:     "upgrade-match-by-path",
   188  			matchers: json.RawMessage("[{\"path\":[\"/foo/bar\"]}]"),
   189  			data:     http2UpgradeRequestExample,
   190  		},
   191  		{
   192  			name:     "upgrade-match-by-query",
   193  			matchers: json.RawMessage("[{\"query\":{\"aaa\":[\"bbb\"]}}]"),
   194  			data:     http2UpgradeRequestExample,
   195  		},
   196  		{
   197  			name:     "upgrade-match-by-header",
   198  			matchers: json.RawMessage("[{\"header\":{\"user-agent\":[\"curl*\"]}}]"),
   199  			data:     http2UpgradeRequestExample,
   200  		},
   201  		{
   202  			name:     "upgrade-match-by-protocol",
   203  			matchers: json.RawMessage("[{\"protocol\":\"http\"}]"),
   204  			data:     http2UpgradeRequestExample,
   205  		},
   206  	} {
   207  		t.Run(tc.name, func(t *testing.T) {
   208  			matched, err := httpMatchTester(t, tc.matchers, tc.data)
   209  			assertNoError(t, err)
   210  			if !matched {
   211  				t.Errorf("matcher did not match")
   212  			}
   213  		})
   214  	}
   215  }
   216  
   217  func TestHttpMatchingByProtocolWithHttps(t *testing.T) {
   218  	ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
   219  	defer cancel()
   220  
   221  	routes := layer4.RouteList{&layer4.Route{
   222  		MatcherSetsRaw: caddyhttp.RawMatcherSets{
   223  			caddy.ModuleMap{"http": json.RawMessage("[{\"protocol\":\"https\"}]")},
   224  		},
   225  	}}
   226  
   227  	err := routes.Provision(ctx)
   228  	assertNoError(t, err)
   229  
   230  	handlerCalled := false
   231  	compiledRoute := routes.Compile(zap.NewNop(), 100*time.Millisecond,
   232  		layer4.HandlerFunc(func(con *layer4.Connection) error {
   233  			handlerCalled = true
   234  			return nil
   235  		}))
   236  
   237  	in, out := net.Pipe()
   238  	defer func() { _ = in.Close() }()
   239  	defer func() { _ = out.Close() }()
   240  
   241  	cx := layer4.WrapConnection(in, []byte{}, zap.NewNop())
   242  	go func() {
   243  		_, err := out.Write([]byte("GET /foo/bar?aaa=bbb HTTP/1.1\nHost: localhost:10443\n\n"))
   244  		assertNoError(t, err)
   245  	}()
   246  
   247  	// pretend the tls handler was executed before, not an ideal test setup but better then nothing
   248  	cx.SetVar("tls_connection_states", []*tls.ConnectionState{{ServerName: "localhost"}})
   249  
   250  	err = compiledRoute.Handle(cx)
   251  	assertNoError(t, err)
   252  	if !handlerCalled {
   253  		t.Fatalf("matcher did not match")
   254  	}
   255  }
   256  
   257  func TestHttpMatchingGarbage(t *testing.T) {
   258  	matchers := json.RawMessage("[{\"host\":[\"localhost\"]}]")
   259  
   260  	matched, err := httpMatchTester(t, matchers, []byte("not a valid http request"))
   261  	assertNoError(t, err)
   262  	if matched {
   263  		t.Fatalf("matcher did match")
   264  	}
   265  
   266  	validHttp2MagicWithoutHeadersFrame, err := base64.StdEncoding.DecodeString("UFJJICogSFRUUC8yLjANCg0KU00NCg0KAAASBAAAAAAAAAMAAABkAAQCAAAAAAIAAAAATm8gbG9uZ2VyIHZhbGlkIGh0dHAyIHJlcXVlc3QgZnJhbWVz")
   267  	assertNoError(t, err)
   268  	matched, err = httpMatchTester(t, matchers, validHttp2MagicWithoutHeadersFrame)
   269  	if matched {
   270  		t.Fatalf("matcher did match")
   271  	}
   272  }
   273  
   274  func TestMatchHTTP_isHttp(t *testing.T) {
   275  	for _, tc := range []struct {
   276  		name        string
   277  		data        []byte
   278  		shouldMatch bool
   279  	}{
   280  		{
   281  			name:        "http/1.1-only-lf",
   282  			data:        []byte("GET /foo/bar?aaa=bbb HTTP/1.1\nHost: localhost:10443\n\n"),
   283  			shouldMatch: true,
   284  		},
   285  		{
   286  			name:        "http/1.1-cr-lf",
   287  			data:        []byte("GET /foo/bar?aaa=bbb HTTP/1.1\r\nHost: localhost:10443\r\n\r\n"),
   288  			shouldMatch: true,
   289  		},
   290  		{
   291  			name:        "http/1.0-cr-lf",
   292  			data:        []byte("GET /foo/bar?aaa=bbb HTTP/1.0\r\nHost: localhost:10443\r\n\r\n"),
   293  			shouldMatch: true,
   294  		},
   295  		{
   296  			name:        "http/2.0-cr-lf",
   297  			data:        []byte("PRI * HTTP/2.0\r\n\r\n"),
   298  			shouldMatch: true,
   299  		},
   300  		{
   301  			name:        "dummy-short",
   302  			data:        []byte("dum\n"),
   303  			shouldMatch: false,
   304  		},
   305  		{
   306  			name:        "dummy-long",
   307  			data:        []byte("dummydummydummy\n"),
   308  			shouldMatch: false,
   309  		},
   310  		{
   311  			name:        "http/1.1-without-space-in-front",
   312  			data:        []byte("HTTP/1.1\n"),
   313  			shouldMatch: false,
   314  		},
   315  	} {
   316  		t.Run(tc.name, func(t *testing.T) {
   317  			_, matched := MatchHTTP{}.isHttp(tc.data)
   318  			if matched != tc.shouldMatch {
   319  				t.Fatalf("test %v | matched: %v != shouldMatch: %v", tc.name, matched, tc.shouldMatch)
   320  			}
   321  		})
   322  	}
   323  }