github.com/cilium/cilium@v1.16.2/pkg/hubble/filters/http_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Hubble 3 4 package filters 5 6 import ( 7 "context" 8 "strings" 9 "testing" 10 11 flowpb "github.com/cilium/cilium/api/v1/flow" 12 v1 "github.com/cilium/cilium/pkg/hubble/api/v1" 13 "github.com/cilium/cilium/pkg/monitor/api" 14 ) 15 16 func TestHTTPFilters(t *testing.T) { 17 httpFlow := func(http *flowpb.HTTP) *v1.Event { 18 return &v1.Event{ 19 Event: &flowpb.Flow{ 20 EventType: &flowpb.CiliumEventType{ 21 Type: api.MessageTypeAccessLog, 22 }, 23 L7: &flowpb.Layer7{ 24 Record: &flowpb.Layer7_Http{ 25 Http: http, 26 }, 27 }}, 28 } 29 } 30 31 type args struct { 32 f []*flowpb.FlowFilter 33 ev []*v1.Event 34 } 35 36 tests := []struct { 37 name string 38 args args 39 wantErr bool 40 wantErrContains string 41 want []bool 42 }{ 43 // status code filters 44 { 45 name: "status code full", 46 args: args{ 47 f: []*flowpb.FlowFilter{ 48 { 49 HttpStatusCode: []string{"200", "302"}, 50 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 51 }, 52 }, 53 ev: []*v1.Event{ 54 httpFlow(&flowpb.HTTP{Code: 200}), 55 httpFlow(&flowpb.HTTP{Code: 302}), 56 httpFlow(&flowpb.HTTP{Code: 404}), 57 httpFlow(&flowpb.HTTP{Code: 500}), 58 }, 59 }, 60 want: []bool{ 61 true, 62 true, 63 false, 64 false, 65 }, 66 wantErr: false, 67 }, 68 { 69 name: "status code prefix", 70 args: args{ 71 f: []*flowpb.FlowFilter{ 72 { 73 HttpStatusCode: []string{"40+", "5+"}, 74 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 75 }, 76 }, 77 ev: []*v1.Event{ 78 httpFlow(&flowpb.HTTP{Code: 302}), 79 httpFlow(&flowpb.HTTP{Code: 400}), 80 httpFlow(&flowpb.HTTP{Code: 404}), 81 httpFlow(&flowpb.HTTP{Code: 410}), 82 httpFlow(&flowpb.HTTP{Code: 004}), 83 httpFlow(&flowpb.HTTP{Code: 500}), 84 httpFlow(&flowpb.HTTP{Code: 501}), 85 httpFlow(&flowpb.HTTP{Code: 510}), 86 httpFlow(&flowpb.HTTP{Code: 050}), 87 }, 88 }, 89 want: []bool{ 90 false, 91 true, 92 true, 93 false, 94 false, 95 true, 96 true, 97 true, 98 false, 99 }, 100 wantErr: false, 101 }, 102 { 103 name: "invalid data", 104 args: args{ 105 f: []*flowpb.FlowFilter{ 106 { 107 HttpStatusCode: []string{"200"}, 108 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 109 }, 110 }, 111 ev: []*v1.Event{ 112 {Event: &flowpb.Flow{}}, 113 httpFlow(&flowpb.HTTP{}), 114 httpFlow(&flowpb.HTTP{Code: 777}), 115 }, 116 }, 117 want: []bool{ 118 false, 119 false, 120 false, 121 }, 122 wantErr: false, 123 }, 124 { 125 name: "invalid empty filter", 126 args: args{ 127 f: []*flowpb.FlowFilter{ 128 { 129 HttpStatusCode: []string{""}, 130 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 131 }, 132 }, 133 }, 134 wantErr: true, 135 }, 136 { 137 name: "invalid catch-all prefix", 138 args: args{ 139 f: []*flowpb.FlowFilter{ 140 { 141 HttpStatusCode: []string{"+"}, 142 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 143 }, 144 }, 145 }, 146 wantErr: true, 147 }, 148 { 149 name: "invalid status code", 150 args: args{ 151 f: []*flowpb.FlowFilter{ 152 { 153 HttpStatusCode: []string{"909"}, 154 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 155 }, 156 }, 157 }, 158 wantErr: true, 159 }, 160 { 161 name: "invalid status code text", 162 args: args{ 163 f: []*flowpb.FlowFilter{ 164 { 165 HttpStatusCode: []string{"HTTP 200 OK"}, 166 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 167 }, 168 }, 169 }, 170 wantErr: true, 171 }, 172 { 173 name: "invalid status code prefix", 174 args: args{ 175 f: []*flowpb.FlowFilter{ 176 { 177 HttpStatusCode: []string{"3++"}, 178 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 179 }, 180 }, 181 }, 182 wantErr: true, 183 }, 184 { 185 name: "invalid status code prefix", 186 args: args{ 187 f: []*flowpb.FlowFilter{ 188 { 189 HttpStatusCode: []string{"3+0"}, 190 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 191 }, 192 }, 193 }, 194 wantErr: true, 195 }, 196 { 197 name: "empty event type filter", 198 args: args{ 199 f: []*flowpb.FlowFilter{ 200 { 201 HttpStatusCode: []string{"200"}, 202 EventType: []*flowpb.EventTypeFilter{}, 203 }, 204 }, 205 ev: []*v1.Event{ 206 httpFlow(&flowpb.HTTP{Code: 200}), 207 }, 208 }, 209 want: []bool{ 210 true, 211 }, 212 wantErr: false, 213 }, 214 { 215 name: "compatible event type filter", 216 args: args{ 217 f: []*flowpb.FlowFilter{ 218 { 219 HttpStatusCode: []string{"200"}, 220 EventType: []*flowpb.EventTypeFilter{ 221 {Type: api.MessageTypeAccessLog}, 222 {Type: api.MessageTypeTrace}, 223 }, 224 }, 225 }, 226 ev: []*v1.Event{ 227 httpFlow(&flowpb.HTTP{Code: 200}), 228 }, 229 }, 230 want: []bool{ 231 true, 232 }, 233 wantErr: false, 234 }, 235 // method filters 236 { 237 name: "basic http method filter", 238 args: args{ 239 f: []*flowpb.FlowFilter{ 240 { 241 HttpMethod: []string{"GET"}, 242 EventType: []*flowpb.EventTypeFilter{ 243 {Type: api.MessageTypeAccessLog}, 244 {Type: api.MessageTypeTrace}, 245 }, 246 }, 247 { 248 HttpMethod: []string{"POST"}, 249 EventType: []*flowpb.EventTypeFilter{ 250 {Type: api.MessageTypeAccessLog}, 251 {Type: api.MessageTypeTrace}, 252 }, 253 }, 254 }, 255 ev: []*v1.Event{ 256 httpFlow(&flowpb.HTTP{Method: "gEt"}), 257 }, 258 }, 259 want: []bool{ 260 true, 261 false, 262 }, 263 wantErr: false, 264 }, 265 { 266 name: "http method wrong type", 267 args: args{ 268 f: []*flowpb.FlowFilter{ 269 { 270 HttpMethod: []string{"GET"}, 271 EventType: []*flowpb.EventTypeFilter{ 272 {Type: api.MessageTypeTrace}, 273 }, 274 }, 275 }, 276 ev: []*v1.Event{ 277 httpFlow(&flowpb.HTTP{Method: "gEt"}), 278 }, 279 }, 280 wantErr: true, 281 wantErrContains: "http method requires the event type filter", 282 }, 283 { 284 name: "http method wrong type", 285 args: args{ 286 f: []*flowpb.FlowFilter{ 287 { 288 HttpMethod: []string{"PUT"}, 289 EventType: []*flowpb.EventTypeFilter{ 290 {Type: api.MessageTypeAccessLog}, 291 {Type: api.MessageTypeTrace}, 292 }, 293 }, 294 { 295 HttpMethod: []string{"POST"}, 296 EventType: []*flowpb.EventTypeFilter{ 297 {Type: api.MessageTypeAccessLog}, 298 {Type: api.MessageTypeTrace}, 299 }, 300 }, 301 }, 302 ev: []*v1.Event{ 303 httpFlow(&flowpb.HTTP{Method: "DELETE"}), 304 }, 305 }, 306 want: []bool{ 307 false, 308 false, 309 }, 310 }, 311 // path filters 312 { 313 name: "path full", 314 args: args{ 315 f: []*flowpb.FlowFilter{ 316 { 317 HttpPath: []string{"/docs/[a-z]+", "/post/\\d+"}, 318 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 319 }, 320 }, 321 ev: []*v1.Event{ 322 httpFlow(&flowpb.HTTP{Url: "/docs/"}), 323 httpFlow(&flowpb.HTTP{Url: "/docs/tutorial/"}), 324 httpFlow(&flowpb.HTTP{Url: "/post/"}), 325 httpFlow(&flowpb.HTTP{Url: "/post/0"}), 326 httpFlow(&flowpb.HTTP{Url: "/post/slug"}), 327 httpFlow(&flowpb.HTTP{Url: "/post/123?key=value"}), 328 httpFlow(&flowpb.HTTP{Url: "/slug"}), 329 }, 330 }, 331 want: []bool{ 332 false, 333 true, 334 false, 335 true, 336 false, 337 true, 338 false, 339 }, 340 wantErr: false, 341 }, 342 // URL filters 343 { 344 name: "url simple", 345 args: args{ 346 f: []*flowpb.FlowFilter{ 347 { 348 HttpUrl: []string{"cilium.io"}, 349 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 350 }, 351 }, 352 ev: []*v1.Event{ 353 httpFlow(&flowpb.HTTP{Url: "http://example.com/"}), 354 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/docs/"}), 355 httpFlow(&flowpb.HTTP{Url: "https://cilium.io/"}), 356 httpFlow(&flowpb.HTTP{Url: "https://not.cilium.io/"}), 357 httpFlow(&flowpb.HTTP{Url: "https://cilium.example.com/"}), 358 }, 359 }, 360 want: []bool{ 361 false, 362 true, 363 true, 364 true, 365 false, 366 }, 367 wantErr: false, 368 }, 369 { 370 name: "url complete", 371 args: args{ 372 f: []*flowpb.FlowFilter{ 373 { 374 HttpUrl: []string{"^http://cilium.io/$"}, 375 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 376 }, 377 }, 378 ev: []*v1.Event{ 379 httpFlow(&flowpb.HTTP{Url: "http://example.com/"}), 380 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/docs/"}), 381 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/"}), 382 }, 383 }, 384 want: []bool{ 385 false, 386 false, 387 true, 388 }, 389 wantErr: false, 390 }, 391 { 392 name: "url full", 393 args: args{ 394 f: []*flowpb.FlowFilter{ 395 { 396 HttpUrl: []string{"^http://cilium.io/docs/[a-z]+$", "^http://example.com/post/\\d+"}, 397 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 398 }, 399 }, 400 ev: []*v1.Event{ 401 httpFlow(&flowpb.HTTP{Url: "http://example.com/post/12"}), 402 httpFlow(&flowpb.HTTP{Url: "http://example.com/post/125?key=value"}), 403 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/post/125?key=value"}), 404 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/docs/example"}), 405 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/docs/example/1243"}), 406 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/"}), 407 httpFlow(&flowpb.HTTP{Url: "http://example.com/docs/post"}), 408 }, 409 }, 410 want: []bool{ 411 true, 412 true, 413 false, 414 true, 415 false, 416 false, 417 false, 418 }, 419 wantErr: false, 420 }, 421 { 422 name: "url/path mix ", 423 args: args{ 424 f: []*flowpb.FlowFilter{ 425 { 426 HttpUrl: []string{"^http://cilium.io", "^http://example.com/post"}, 427 HttpPath: []string{"^/docs/[a-z]+", "^/post/\\d+"}, 428 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 429 }, 430 }, 431 ev: []*v1.Event{ 432 httpFlow(&flowpb.HTTP{Url: "http://example.com/post/12"}), 433 httpFlow(&flowpb.HTTP{Url: "http://example.com/post/125?key=value"}), 434 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/post/125?key=value"}), 435 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/docs/example"}), 436 httpFlow(&flowpb.HTTP{Url: "http://cilium.io/"}), 437 httpFlow(&flowpb.HTTP{Url: "http://example.com/docs/post"}), 438 httpFlow(&flowpb.HTTP{Url: "http://example.org/post/12"}), 439 }, 440 }, 441 want: []bool{ 442 true, 443 true, 444 true, 445 true, 446 false, 447 false, 448 false, 449 }, 450 wantErr: false, 451 }, 452 { 453 name: "invalid uri", 454 args: args{ 455 f: []*flowpb.FlowFilter{ 456 { 457 HttpPath: []string{"/post/\\d+"}, 458 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 459 }, 460 }, 461 ev: []*v1.Event{ 462 httpFlow(&flowpb.HTTP{Url: "/post/0"}), 463 httpFlow(&flowpb.HTTP{Url: "?/post/0"}), 464 }, 465 }, 466 want: []bool{ 467 true, 468 false, 469 }, 470 wantErr: false, 471 }, 472 { 473 name: "invalid path filter", 474 args: args{ 475 f: []*flowpb.FlowFilter{ 476 { 477 HttpPath: []string{"("}, 478 EventType: []*flowpb.EventTypeFilter{{Type: api.MessageTypeAccessLog}}, 479 }, 480 }, 481 }, 482 wantErr: true, 483 }, 484 // headers filters 485 { 486 name: "http headers match", 487 args: args{ 488 f: []*flowpb.FlowFilter{ 489 { 490 HttpHeader: []*flowpb.HTTPHeader{ 491 {Key: "Content", Value: "foo"}, 492 }, 493 EventType: []*flowpb.EventTypeFilter{ 494 {Type: api.MessageTypeAccessLog}, 495 {Type: api.MessageTypeTrace}, 496 }, 497 }, 498 }, 499 ev: []*v1.Event{ 500 httpFlow(&flowpb.HTTP{Headers: []*flowpb.HTTPHeader{{Key: "Content", Value: "foo"}}}), 501 }, 502 }, 503 want: []bool{ 504 true, 505 }, 506 }, 507 { 508 name: "http headers no match", 509 args: args{ 510 f: []*flowpb.FlowFilter{ 511 { 512 HttpHeader: []*flowpb.HTTPHeader{ 513 {Key: "Content", Value: "foo"}, 514 }, 515 EventType: []*flowpb.EventTypeFilter{ 516 {Type: api.MessageTypeAccessLog}, 517 {Type: api.MessageTypeTrace}, 518 }, 519 }, 520 }, 521 ev: []*v1.Event{ 522 httpFlow(&flowpb.HTTP{Headers: []*flowpb.HTTPHeader{{Key: "Content", Value: "bar"}}}), 523 }, 524 }, 525 want: []bool{ 526 false, 527 }, 528 }, 529 { 530 name: "http headers multiple with only one match", 531 args: args{ 532 f: []*flowpb.FlowFilter{ 533 { 534 HttpHeader: []*flowpb.HTTPHeader{ 535 {Key: "Cache-control", Value: "no-store"}, 536 }, 537 EventType: []*flowpb.EventTypeFilter{ 538 {Type: api.MessageTypeAccessLog}, 539 {Type: api.MessageTypeTrace}, 540 }, 541 }, 542 }, 543 ev: []*v1.Event{ 544 httpFlow(&flowpb.HTTP{Headers: []*flowpb.HTTPHeader{ 545 {Key: "Cache-control", Value: "no-cache"}, 546 {Key: "Cache-control", Value: "no-store"}, 547 }}), 548 }, 549 }, 550 want: []bool{ 551 true, 552 }, 553 }, 554 } 555 for _, tt := range tests { 556 t.Run(tt.name, func(t *testing.T) { 557 fl, err := BuildFilterList(context.Background(), tt.args.f, []OnBuildFilter{&HTTPFilter{}}) 558 if (err != nil) != tt.wantErr { 559 t.Errorf(`"%s" error = %v, wantErr %v`, tt.name, err, tt.wantErr) 560 return 561 } 562 if err != nil { 563 if tt.wantErrContains != "" { 564 if !strings.Contains(err.Error(), tt.wantErrContains) { 565 t.Errorf( 566 `"%s" error does not contain "%s"`, 567 err.Error(), tt.wantErrContains, 568 ) 569 } 570 } 571 return 572 } 573 for i, ev := range tt.args.ev { 574 if got := fl.MatchOne(ev); got != tt.want[i] { 575 t.Errorf("\"%s\" got %d = %v, want %v", tt.name, i, got, tt.want[i]) 576 } 577 } 578 }) 579 } 580 }