diff --git a/api/pkg/tap/grpc_assembler.go b/api/pkg/tap/grpc_assembler.go index a63d93be0..b64344bcb 100644 --- a/api/pkg/tap/grpc_assembler.go +++ b/api/pkg/tap/grpc_assembler.go @@ -6,8 +6,11 @@ import ( "encoding/base64" "encoding/binary" "errors" + "io" "math" "net/http" + "net/url" + "strings" "golang.org/x/net/http2" "golang.org/x/net/http2/hpack" @@ -62,6 +65,7 @@ func (fbs *fragmentsByStream) pop(streamID uint32) ([]hpack.HeaderField, []byte) headers := (*fbs)[streamID].headers data := (*fbs)[streamID].data delete((*fbs), streamID) + return headers, data } @@ -100,9 +104,11 @@ func (ga *GrpcAssembler) readMessage() (uint32, interface{}, string, error) { headers, data := ga.fragmentsByStream.pop(streamID) + // Note: header keys are converted by http.Header.Set to canonical names, e.g. content-type -> Content-Type. + // By converting the keys we violate the HTTP/2 specification, which state that all headers must be lowercase. headersHTTP1 := make(http.Header) for _, header := range headers { - headersHTTP1[header.Name] = []string{header.Value} + headersHTTP1.Add(header.Name, header.Value) } dataString := base64.StdEncoding.EncodeToString(data) @@ -112,10 +118,14 @@ func (ga *GrpcAssembler) readMessage() (uint32, interface{}, string, error) { var messageHTTP1 interface{} if _, ok := headersHTTP1[":method"]; ok { messageHTTP1 = http.Request{ + URL: &url.URL{}, + Method: "POST", Header: headersHTTP1, Proto: protoHTTP2, ProtoMajor: protoMajorHTTP2, ProtoMinor: protoMinorHTTP2, + Body: io.NopCloser(strings.NewReader(dataString)), + ContentLength: int64(len(dataString)), } } else if _, ok := headersHTTP1[":status"]; ok { messageHTTP1 = http.Response{ @@ -123,6 +133,8 @@ func (ga *GrpcAssembler) readMessage() (uint32, interface{}, string, error) { Proto: protoHTTP2, ProtoMajor: protoMajorHTTP2, ProtoMinor: protoMinorHTTP2, + Body: io.NopCloser(strings.NewReader(dataString)), + ContentLength: int64(len(dataString)), } } else { return 0, nil, "", errors.New("Failed to assemble stream: neither a request nor a message") diff --git a/api/pkg/tap/har_writer.go b/api/pkg/tap/har_writer.go index 4620fcc82..2fabf94dd 100644 --- a/api/pkg/tap/har_writer.go +++ b/api/pkg/tap/har_writer.go @@ -7,6 +7,8 @@ import ( "net/http" "os" "path/filepath" + "strconv" + "strings" "time" "github.com/google/martian/har" @@ -40,26 +42,41 @@ type HarFile struct { } func NewEntry(request *http.Request, requestTime time.Time, response *http.Response, responseTime time.Time) (*har.Entry, error) { - // TODO: quick fix until TRA-3212 is implemented - if request.URL == nil || request.Method == "" { - return nil, errors.New("Invalid request") - } harRequest, err := har.NewRequest(request, true) if err != nil { SilentError("convert-request-to-har", "Failed converting request to HAR %s (%v,%+v)\n", err, err, err) return nil, errors.New("Failed converting request to HAR") } - // Martian copies http.Request.URL.String() to har.Request.URL. - // According to the spec, the URL field needs to be the absolute URL. - harRequest.URL = fmt.Sprintf("http://%s%s", request.Host, request.URL) - harResponse, err := har.NewResponse(response, true) if err != nil { SilentError("convert-response-to-har", "Failed converting response to HAR %s (%v,%+v)\n", err, err, err) return nil, errors.New("Failed converting response to HAR") } + if harRequest.PostData != nil && strings.HasPrefix(harRequest.PostData.MimeType, "application/grpc") { + // Force HTTP/2 gRPC into HAR template + + harRequest.URL = fmt.Sprintf("%s://%s%s", request.Header.Get(":scheme"), request.Header.Get(":authority"), request.Header.Get(":path")) + + status, err := strconv.Atoi(response.Header.Get(":status")) + if err != nil { + SilentError("convert-response-status-for-har", "Failed converting status to int %s (%v,%+v)\n", err, err, err) + return nil, errors.New("Failed converting response status to int for HAR") + } + harResponse.Status = status + } else { + // Martian copies http.Request.URL.String() to har.Request.URL, which usually contains the path. + // However, according to the HAR spec, the URL field needs to be the absolute URL. + var scheme string + if request.URL.Scheme != "" { + scheme = request.URL.Scheme + } else { + scheme = "http" + } + harRequest.URL = fmt.Sprintf("%s://%s%s", scheme, request.Host, request.URL) + } + totalTime := responseTime.Sub(requestTime).Round(time.Millisecond).Milliseconds() if totalTime < 1 { totalTime = 1