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  }