Skip to content

Commit 3c04314

Browse files
committed
feat(http): add body regex filtering
1 parent 64a0f59 commit 3c04314

File tree

3 files changed

+144
-16
lines changed

3 files changed

+144
-16
lines changed

agent/protocol/http.go

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package protocol
22

33
import (
44
"bufio"
5+
"bytes"
56
"fmt"
67
"io"
78
"kyanos/agent/buffer"
@@ -289,6 +290,7 @@ type HttpFilter struct {
289290
TargetPathPrefix string
290291
TargetHostName string
291292
TargetMethods []string
293+
TargetBodyReg *regexp.Regexp
292294
needFilter *bool
293295
}
294296

@@ -304,24 +306,41 @@ func (filter HttpFilter) FilterByRequest() bool {
304306
filter.TargetPathReg != nil ||
305307
len(filter.TargetPathPrefix) > 0 ||
306308
len(filter.TargetMethods) > 0 ||
307-
len(filter.TargetHostName) > 0)
309+
len(filter.TargetHostName) > 0 ||
310+
filter.TargetBodyReg != nil)
308311
return *filter.needFilter
309312
}
310313

311314
func (filter HttpFilter) FilterByResponse() bool {
312-
return false
315+
return filter.TargetBodyReg != nil
316+
}
317+
318+
func extractHTTPBody(buf []byte) []byte {
319+
bodyStart := bytes.Index(buf, []byte(HTTP_BOUNDARY_MARKER))
320+
if bodyStart == -1 {
321+
return nil
322+
}
323+
return buf[bodyStart+len(HTTP_BOUNDARY_MARKER):]
313324
}
314325

315-
// Filter filters HTTP requests based on various criteria such as path, path prefix, path regex, method, and host name.
316-
// It returns true if the request matches all the specified criteria, otherwise it returns false.
317-
//
318-
// The filtering logic is as follows:
319-
// - If TargetPath is specified, the request path must exactly match TargetPath.
320-
// - If TargetPathPrefix is specified, the request path must start with TargetPathPrefix.
321-
// - If TargetPathReg is specified, the request path must match the regular expression TargetPathReg.
322-
// - If TargetMethods is specified, the request method must be one of the methods in TargetMethods.
323-
// - If TargetHostName is specified, the request host must exactly match TargetHostName.
324-
func (filter HttpFilter) Filter(parsedReq ParsedMessage, _ ParsedMessage) bool {
326+
func bodyMatches(msg ParsedMessage, bodyReg *regexp.Regexp) bool {
327+
if msg == nil || bodyReg == nil {
328+
return false
329+
}
330+
331+
switch typed := msg.(type) {
332+
case *ParsedHttpRequest:
333+
return bodyReg.Match(extractHTTPBody(typed.buf))
334+
case *ParsedHttpResponse:
335+
return bodyReg.Match(extractHTTPBody(typed.buf))
336+
default:
337+
return false
338+
}
339+
}
340+
341+
// Filter filters HTTP requests based on path, path prefix, path regex, method, host name,
342+
// and optionally request/response body regex content.
343+
func (filter HttpFilter) Filter(parsedReq ParsedMessage, parsedResp ParsedMessage) bool {
325344
req, ok := parsedReq.(*ParsedHttpRequest)
326345
if !ok {
327346
common.ProtocolParserLog.Warnf("[HttpFilter] cast to http.Request failed: %v\n", req)
@@ -349,6 +368,12 @@ func (filter HttpFilter) Filter(parsedReq ParsedMessage, _ ParsedMessage) bool {
349368
return false
350369
}
351370

371+
if filter.TargetBodyReg != nil &&
372+
!bodyMatches(parsedReq, filter.TargetBodyReg) &&
373+
!bodyMatches(parsedResp, filter.TargetBodyReg) {
374+
return false
375+
}
376+
352377
return true
353378
}
354379

agent/protocol/http_test.go

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,43 @@ func TestParseResponse(t *testing.T) {
164164
assert.Equal(t, uint64(20), message.Seq())
165165
}
166166

167+
func parseHTTPReqMessage(t *testing.T, raw string) protocol.ParsedMessage {
168+
t.Helper()
169+
170+
parser := protocol.HTTPStreamParser{}
171+
result := parser.ParseRequest(raw, protocol.Request, 10, 20)
172+
assert.Equal(t, protocol.Success, result.ParseState)
173+
assert.Len(t, result.ParsedMessages, 1)
174+
175+
return result.ParsedMessages[0]
176+
}
177+
178+
func parseHTTPRespMessage(t *testing.T, raw string) protocol.ParsedMessage {
179+
t.Helper()
180+
181+
streamBuffer := buffer.New(1000)
182+
streamBuffer.Add(10, []byte(raw), 10000)
183+
184+
parser := protocol.HTTPStreamParser{}
185+
result := parser.ParseResponse(raw, protocol.Response, 10, 20, streamBuffer)
186+
assert.Equal(t, protocol.Success, result.ParseState)
187+
assert.Len(t, result.ParsedMessages, 1)
188+
189+
return result.ParsedMessages[0]
190+
}
191+
167192
func TestHttpFilter_Filter(t *testing.T) {
168193
type fields struct {
169194
TargetPath string
170195
TargetPathReg *regexp.Regexp
171196
TargetPathPrefix string
172197
TargetHostName string
173198
TargetMethods []string
199+
TargetBodyReg *regexp.Regexp
174200
}
175201
type args struct {
176-
parsedReq protocol.ParsedMessage
202+
parsedReq protocol.ParsedMessage
203+
parsedResp protocol.ParsedMessage
177204
}
178205
tests := []struct {
179206
name string
@@ -197,6 +224,7 @@ func TestHttpFilter_Filter(t *testing.T) {
197224
parsedReq: &protocol.ParsedHttpRequest{
198225
Path: "/foo/bar",
199226
},
227+
parsedResp: nil,
200228
},
201229
want: true,
202230
},
@@ -209,6 +237,7 @@ func TestHttpFilter_Filter(t *testing.T) {
209237
parsedReq: &protocol.ParsedHttpRequest{
210238
Path: "/foo/bar/baz",
211239
},
240+
parsedResp: nil,
212241
},
213242
want: false,
214243
},
@@ -221,6 +250,7 @@ func TestHttpFilter_Filter(t *testing.T) {
221250
parsedReq: &protocol.ParsedHttpRequest{
222251
Path: "/foo/bar/baz",
223252
},
253+
parsedResp: nil,
224254
},
225255
want: true,
226256
},
@@ -233,6 +263,7 @@ func TestHttpFilter_Filter(t *testing.T) {
233263
parsedReq: &protocol.ParsedHttpRequest{
234264
Path: "/test",
235265
},
266+
parsedResp: nil,
236267
},
237268
want: false,
238269
},
@@ -245,6 +276,7 @@ func TestHttpFilter_Filter(t *testing.T) {
245276
parsedReq: &protocol.ParsedHttpRequest{
246277
Path: "/foo/bar/100/baz",
247278
},
279+
parsedResp: nil,
248280
},
249281
want: true,
250282
},
@@ -257,6 +289,7 @@ func TestHttpFilter_Filter(t *testing.T) {
257289
parsedReq: &protocol.ParsedHttpRequest{
258290
Path: "/test",
259291
},
292+
parsedResp: nil,
260293
},
261294
want: false,
262295
},
@@ -269,6 +302,7 @@ func TestHttpFilter_Filter(t *testing.T) {
269302
Host: "test.com",
270303
Method: "POST",
271304
},
305+
parsedResp: nil,
272306
},
273307
want: true,
274308
},
@@ -281,6 +315,7 @@ func TestHttpFilter_Filter(t *testing.T) {
281315
parsedReq: &protocol.ParsedHttpRequest{
282316
Method: "GET",
283317
},
318+
parsedResp: nil,
284319
},
285320
want: true,
286321
},
@@ -293,6 +328,7 @@ func TestHttpFilter_Filter(t *testing.T) {
293328
parsedReq: &protocol.ParsedHttpRequest{
294329
Method: "POST",
295330
},
331+
parsedResp: nil,
296332
},
297333
want: false,
298334
},
@@ -305,6 +341,7 @@ func TestHttpFilter_Filter(t *testing.T) {
305341
parsedReq: &protocol.ParsedHttpRequest{
306342
Host: "foo.bar",
307343
},
344+
parsedResp: nil,
308345
},
309346
want: true,
310347
},
@@ -317,6 +354,52 @@ func TestHttpFilter_Filter(t *testing.T) {
317354
parsedReq: &protocol.ParsedHttpRequest{
318355
Host: "foo.baz",
319356
},
357+
parsedResp: nil,
358+
},
359+
want: false,
360+
},
361+
{
362+
name: "filter_by_request_body_regex",
363+
fields: fields{
364+
TargetBodyReg: regexp.MustCompile(`reqId=123`),
365+
},
366+
args: args{
367+
parsedReq: parseHTTPReqMessage(t,
368+
"POST /foo HTTP/1.1\r\nHost: foo.bar\r\nContent-Length: 21\r\n\r\npayload=reqId=123&x=1",
369+
),
370+
parsedResp: parseHTTPRespMessage(t,
371+
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok",
372+
),
373+
},
374+
want: true,
375+
},
376+
{
377+
name: "filter_by_response_body_regex",
378+
fields: fields{
379+
TargetBodyReg: regexp.MustCompile(`status=ok`),
380+
},
381+
args: args{
382+
parsedReq: parseHTTPReqMessage(t,
383+
"POST /foo HTTP/1.1\r\nHost: foo.bar\r\nContent-Length: 6\r\n\r\nhello!",
384+
),
385+
parsedResp: parseHTTPRespMessage(t,
386+
"HTTP/1.1 200 OK\r\nContent-Length: 17\r\n\r\nresult=status=ok!",
387+
),
388+
},
389+
want: true,
390+
},
391+
{
392+
name: "not_filter_by_body_regex",
393+
fields: fields{
394+
TargetBodyReg: regexp.MustCompile(`reqId=123`),
395+
},
396+
args: args{
397+
parsedReq: parseHTTPReqMessage(t,
398+
"POST /foo HTTP/1.1\r\nHost: foo.bar\r\nContent-Length: 7\r\n\r\nnope123",
399+
),
400+
parsedResp: parseHTTPRespMessage(t,
401+
"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\nstillno",
402+
),
320403
},
321404
want: false,
322405
},
@@ -329,8 +412,18 @@ func TestHttpFilter_Filter(t *testing.T) {
329412
TargetPathPrefix: tt.fields.TargetPathPrefix,
330413
TargetHostName: tt.fields.TargetHostName,
331414
TargetMethods: tt.fields.TargetMethods,
415+
TargetBodyReg: tt.fields.TargetBodyReg,
332416
}
333-
assert.Equalf(t, tt.want, filter.Filter(tt.args.parsedReq, nil), "Filter(%v, %v)", tt.args.parsedReq, nil)
417+
assert.Equalf(t, tt.want, filter.Filter(tt.args.parsedReq, tt.args.parsedResp), "Filter(%v, %v)", tt.args.parsedReq, tt.args.parsedResp)
334418
})
335419
}
336420
}
421+
422+
func TestHttpFilter_BodyRegexRequiresResponseFiltering(t *testing.T) {
423+
filter := protocol.HttpFilter{
424+
TargetBodyReg: regexp.MustCompile(`reqId=123`),
425+
}
426+
427+
assert.True(t, filter.FilterByRequest())
428+
assert.True(t, filter.FilterByResponse())
429+
}

cmd/http.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import (
88
)
99

1010
var httpCmd = &cobra.Command{
11-
Use: "http [--method METHODS|--path PATH|--path-regex REGEX|--path-prefix PREFIX|--host HOSTNAME]",
11+
Use: "http [--method METHODS|--path PATH|--path-regex REGEX|--path-prefix PREFIX|--host HOSTNAME|--body REGEX]",
1212
Short: "watch HTTP message",
13-
Long: `Filter HTTP messages based on method, path (strict, regex, prefix), or host. Filter flags are combined with AND(&&).`,
13+
Long: `Filter HTTP messages based on method, path (strict, regex, prefix), host, or request/response body content. Filter flags are combined with AND(&&).`,
1414
Run: func(cmd *cobra.Command, args []string) {
1515
methods, err := cmd.Flags().GetStringSlice("method")
1616
if err != nil {
@@ -26,6 +26,7 @@ var httpCmd = &cobra.Command{
2626
}
2727
var (
2828
pathReg *regexp.Regexp
29+
bodyReg *regexp.Regexp
2930
)
3031
if pathRegStr, err := cmd.Flags().GetString("path-regex"); err != nil {
3132
logger.Fatalf("invalid path-regex: %v\n", err)
@@ -38,13 +39,21 @@ var httpCmd = &cobra.Command{
3839
if err != nil {
3940
logger.Fatalf("invalid path-prefix: %v\n", err)
4041
}
42+
if bodyRegStr, err := cmd.Flags().GetString("body"); err != nil {
43+
logger.Fatalf("invalid body: %v\n", err)
44+
} else if len(bodyRegStr) > 0 {
45+
if bodyReg, err = regexp.Compile(bodyRegStr); err != nil {
46+
logger.Fatalf("invalid body: %v\n", err)
47+
}
48+
}
4149

4250
options.MessageFilter = protocol.HttpFilter{
4351
TargetPath: path,
4452
TargetPathReg: pathReg,
4553
TargetPathPrefix: pathPrefix,
4654
TargetHostName: host,
4755
TargetMethods: methods,
56+
TargetBodyReg: bodyReg,
4857
}
4958
options.LatencyFilter = initLatencyFilter(cmd)
5059
options.SizeFilter = initSizeFilter(cmd)
@@ -58,6 +67,7 @@ func init() {
5867
httpCmd.Flags().String("path", "", "Specify the HTTP path to monitor, like: '/foo/bar'")
5968
httpCmd.Flags().String("path-regex", "", "Specify the regex for HTTP path to monitor, like: '\\/foo\\/bar\\/\\d+'")
6069
httpCmd.Flags().String("path-prefix", "", "Specify the prefix of HTTP path to monitor, like: '/foo'")
70+
httpCmd.Flags().String("body", "", "Specify a regex to match against HTTP request or response bodies, like: 'reqId=123'")
6171

6272
httpCmd.Flags().SortFlags = false
6373
httpCmd.PersistentFlags().SortFlags = false

0 commit comments

Comments
 (0)