diff --git a/tap/extensions/http/handlers.go b/tap/extensions/http/handlers.go index f2ea09c02..ffcbe8c7f 100644 --- a/tap/extensions/http/handlers.go +++ b/tap/extensions/http/handlers.go @@ -7,6 +7,7 @@ import ( "io" "io/ioutil" "net/http" + "strings" "github.com/up9inc/mizu/tap/api" ) @@ -34,12 +35,13 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi switch messageHTTP1 := messageHTTP1.(type) { case http.Request: ident := fmt.Sprintf( - "%s->%s %s->%s %d", + "%s->%s %s->%s %d %s", tcpID.SrcIP, tcpID.DstIP, tcpID.SrcPort, tcpID.DstPort, streamID, + "HTTP2", ) item = reqResMatcher.registerRequest(ident, &messageHTTP1, superTimer.CaptureTime) if item != nil { @@ -53,12 +55,13 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi } case http.Response: ident := fmt.Sprintf( - "%s->%s %s->%s %d", + "%s->%s %s->%s %d %s", tcpID.DstIP, tcpID.SrcIP, tcpID.DstPort, tcpID.SrcPort, streamID, + "HTTP2", ) item = reqResMatcher.registerResponse(ident, &messageHTTP1, superTimer.CaptureTime) if item != nil { @@ -84,23 +87,30 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi return nil } -func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) error { - req, err := http.ReadRequest(b) +func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) (switchingProtocolsHTTP2 bool, req *http.Request, err error) { + req, err = http.ReadRequest(b) if err != nil { - return err + return } counterPair.Request++ - body, err := ioutil.ReadAll(req.Body) + // Check HTTP2 upgrade - HTTP2 Over Cleartext (H2C) + if strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") && strings.ToLower(req.Header.Get("Upgrade")) == "h2c" { + switchingProtocolsHTTP2 = true + } + + var body []byte + body, err = ioutil.ReadAll(req.Body) req.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind ident := fmt.Sprintf( - "%s->%s %s->%s %d", + "%s->%s %s->%s %d %s", tcpID.SrcIP, tcpID.DstIP, tcpID.SrcPort, tcpID.DstPort, counterPair.Request, + "HTTP1", ) item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime) if item != nil { @@ -113,26 +123,34 @@ func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api } filterAndEmit(item, emitter, options) } - return nil + return } -func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) error { - res, err := http.ReadResponse(b, nil) +func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) (switchingProtocolsHTTP2 bool, err error) { + var res *http.Response + res, err = http.ReadResponse(b, nil) if err != nil { - return err + return } counterPair.Response++ - body, err := ioutil.ReadAll(res.Body) + // Check HTTP2 upgrade - HTTP2 Over Cleartext (H2C) + if res.StatusCode == 101 && strings.Contains(strings.ToLower(res.Header.Get("Connection")), "upgrade") && strings.ToLower(res.Header.Get("Upgrade")) == "h2c" { + switchingProtocolsHTTP2 = true + } + + var body []byte + body, err = ioutil.ReadAll(res.Body) res.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind ident := fmt.Sprintf( - "%s->%s %s->%s %d", + "%s->%s %s->%s %d %s", tcpID.DstIP, tcpID.SrcIP, tcpID.DstPort, tcpID.SrcPort, counterPair.Response, + "HTTP1", ) item := reqResMatcher.registerResponse(ident, res, superTimer.CaptureTime) if item != nil { @@ -145,5 +163,5 @@ func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api } filterAndEmit(item, emitter, options) } - return nil + return } diff --git a/tap/extensions/http/main.go b/tap/extensions/http/main.go index 531c030f0..fa3eea323 100644 --- a/tap/extensions/http/main.go +++ b/tap/extensions/http/main.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "log" + "net/http" "net/url" "time" @@ -85,7 +86,15 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co } dissected := false + switchingProtocolsHTTP2 := false for { + if switchingProtocolsHTTP2 { + switchingProtocolsHTTP2 = false + isHTTP2, err = checkIsHTTP2Connection(b, isClient) + prepareHTTP2Connection(b, isClient) + http2Assembler = createHTTP2Assembler(b) + } + if superIdentifier.Protocol != nil && superIdentifier.Protocol != &protocol { return errors.New("Identified by another protocol") } @@ -99,15 +108,39 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co } dissected = true } else if isClient { - err = handleHTTP1ClientStream(b, tcpID, counterPair, superTimer, emitter, options) + var req *http.Request + switchingProtocolsHTTP2, req, err = handleHTTP1ClientStream(b, tcpID, counterPair, superTimer, emitter, options) if err == io.EOF || err == io.ErrUnexpectedEOF { break } else if err != nil { continue } dissected = true + + // In case of an HTTP2 upgrade, duplicate the HTTP1 request into HTTP2 with stream ID 1 + if switchingProtocolsHTTP2 { + ident := fmt.Sprintf( + "%s->%s %s->%s 1 %s", + tcpID.SrcIP, + tcpID.DstIP, + tcpID.SrcPort, + tcpID.DstPort, + "HTTP2", + ) + item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime) + if item != nil { + item.ConnectionInfo = &api.ConnectionInfo{ + ClientIP: tcpID.SrcIP, + ClientPort: tcpID.SrcPort, + ServerIP: tcpID.DstIP, + ServerPort: tcpID.DstPort, + IsOutgoing: true, + } + filterAndEmit(item, emitter, options) + } + } } else { - err = handleHTTP1ServerStream(b, tcpID, counterPair, superTimer, emitter, options) + switchingProtocolsHTTP2, err = handleHTTP1ServerStream(b, tcpID, counterPair, superTimer, emitter, options) if err == io.EOF || err == io.ErrUnexpectedEOF { break } else if err != nil { @@ -132,6 +165,8 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string, reqDetails := request["details"].(map[string]interface{}) resDetails := response["details"].(map[string]interface{}) + isRequestUpgradedH2C := false + for _, header := range reqDetails["headers"].([]interface{}) { h := header.(map[string]interface{}) if h["name"] == "Host" { @@ -143,13 +178,19 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string, if h["name"] == ":path" { path = h["value"].(string) } + + if h["name"] == "Upgrade" { + if h["value"].(string) == "h2c" { + isRequestUpgradedH2C = true + } + } } if resDetails["bodySize"].(float64) < 0 { resDetails["bodySize"] = 0 } - if item.Protocol.Version == "2.0" { + if item.Protocol.Version == "2.0" && !isRequestUpgradedH2C { service = authority } else { service = host @@ -192,7 +233,7 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string, resDetails["statusText"] = grpcStatusCodes[statusCode] } - if item.Protocol.Version == "2.0" { + if item.Protocol.Version == "2.0" && !isRequestUpgradedH2C { reqDetails["url"] = path request["url"] = path } diff --git a/tap/extensions/http/matcher.go b/tap/extensions/http/matcher.go index 08a0b4e5c..01048fa21 100644 --- a/tap/extensions/http/matcher.go +++ b/tap/extensions/http/matcher.go @@ -92,6 +92,6 @@ func splitIdent(ident string) []string { } func genKey(split []string) string { - key := fmt.Sprintf("%s:%s->%s:%s,%s", split[0], split[2], split[1], split[3], split[4]) + key := fmt.Sprintf("%s:%s->%s:%s,%s%s", split[0], split[2], split[1], split[3], split[4], split[5]) return key }