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 }