Skip to content

Commit

Permalink
feat: support http2 and grpc, parse grpc content, partially
Browse files Browse the repository at this point in the history
  • Loading branch information
kevwan committed Jul 31, 2022
1 parent ba54fae commit 36c7fd7
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 184 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/fatih/color v1.13.0
github.com/olekukonko/tablewriter v0.0.5
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462
google.golang.org/protobuf v1.28.1
)

require (
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
Expand All @@ -18,3 +20,7 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdp
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
249 changes: 68 additions & 181 deletions protocol/grpc.go
Original file line number Diff line number Diff line change
@@ -1,208 +1,95 @@
package protocol

import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"strings"

"github.com/fatih/color"
"github.com/kevwan/tproxy/display"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
"google.golang.org/protobuf/encoding/protowire"
)

const (
http2HeaderLen = 9
http2Preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
http2SettingsPayloadLen = 6
)

type GrpcInterop struct{}

func (i *GrpcInterop) Dump(r io.Reader, source string, id int, quiet bool) {
i.readPreface(r, source, id)

data := make([]byte, bufferSize)
for {
n, err := r.Read(data)
if n > 0 && !quiet {
var buf strings.Builder
buf.WriteString(color.HiGreenString("from %s [%d]\n", source, id))

var index int
for index < n {
frameInfo, moreInfo, offset := i.explain(data[index:n])
buf.WriteString(fmt.Sprintf("%s%s%s\n",
color.HiBlueString("%s:(", grpcProtocol),
color.HiYellowString(frameInfo),
color.HiBlueString(")")))
end := index + offset
if end > n {
end = n
}
buf.WriteString(fmt.Sprint(hex.Dump(data[index:end])))
if len(moreInfo) > 0 {
buf.WriteString(fmt.Sprintf("\n%s\n\n", strings.TrimSpace(moreInfo)))
}
index += offset
}

display.PrintfWithTime("%s\n\n", strings.TrimSpace(buf.String()))
}
if err != nil && err != io.EOF {
fmt.Printf("unable to read data %v", err)
break
}
if n == 0 {
break
}
}
}
type grpcExplainer struct{}

func (i *GrpcInterop) explain(b []byte) (string, string, int) {
if len(b) < http2HeaderLen {
return "", "", len(b)
func (g *grpcExplainer) explain(b []byte) string {
if len(b) < grpcHeaderLen {
return ""
}

frame, err := http2.ReadFrameHeader(bytes.NewReader(b[:http2HeaderLen]))
if err != nil {
return "", "", len(b)
if int(b[0]) == 1 {
return ""
}

frameLen := http2HeaderLen + int(frame.Length)
switch frame.Type {
case http2.FrameSettings:
switch frame.Flags {
case http2.FlagSettingsAck:
return "http2:settings:ack", "", frameLen
default:
return i.explainSettings(b[http2HeaderLen:frameLen]), "", frameLen
}
case http2.FramePing:
id := hex.EncodeToString(b[http2HeaderLen:frameLen])
switch frame.Flags {
case http2.FlagPingAck:
return fmt.Sprintf("http2:ping:ack %s", id), "", frameLen
default:
return fmt.Sprintf("http2:ping %s", id), "", frameLen
}
case http2.FrameWindowUpdate:
increment := binary.BigEndian.Uint32(b[http2HeaderLen : http2HeaderLen+4])
return fmt.Sprintf("http2:window_update window_size_increment:%d", increment), "", frameLen
case http2.FrameHeaders:
info, headers := i.explainHeaders(frame, b[http2HeaderLen:frameLen])
var builder strings.Builder
for _, header := range headers {
builder.WriteString(fmt.Sprintf("%s: %s\n", header.Name, header.Value))
}
return info, builder.String(), frameLen
b = b[1:]
// 4 bytes as the pb message length
size := binary.BigEndian.Uint32(b)
b = b[4:]
if len(b) < int(size) {
return ""
}

if frame.StreamID > 0 {
if frame.Flags == http2.FlagDataEndStream {
return fmt.Sprintf("http2:%s stream:%d end_stream",
strings.ToLower(frame.Type.String()), frame.StreamID), "", frameLen
}

desc := fmt.Sprintf("http2:%s stream:%d", strings.ToLower(frame.Type.String()), frame.StreamID)
return desc, "", frameLen
}

return "http2:" + strings.ToLower(frame.Type.String()), "", frameLen
var builder strings.Builder
g.explainFields(b[:size], &builder, 0)
return builder.String()
}

func (i *GrpcInterop) explainHeaders(frame http2.FrameHeader, b []byte) (string, []hpack.HeaderField) {
var padded int
var weight int
if frame.Flags&http2.FlagHeadersPadded != 0 {
padded = int(b[0])
b = b[1 : len(b)-padded]
}
if frame.Flags&http2.FlagHeadersPriority != 0 {
b = b[4:]
weight = int(b[0])
b = b[1:]
}

var buf strings.Builder
buf.WriteString(fmt.Sprintf("http2:headers stream:%d", frame.StreamID))

switch {
case frame.Flags&http2.FlagHeadersEndStream != 0:
buf.WriteString(" end_stream")
case frame.Flags&http2.FlagHeadersEndHeaders != 0:
buf.WriteString(" end_headers")
case frame.Flags&http2.FlagHeadersPadded != 0:
buf.WriteString(" padded")
case frame.Flags&http2.FlagHeadersPriority != 0:
buf.WriteString(" priority")
}

if weight > 0 {
buf.WriteString(fmt.Sprintf(" weight:%d", weight))
}

if frame.Flags&http2.FlagHeadersEndStream != 0 || frame.Flags&http2.FlagHeadersEndHeaders != 0 {
headers, err := hpack.NewDecoder(0, nil).DecodeFull(b)
if err != nil {
return buf.String(), nil
func (g *grpcExplainer) explainFields(b []byte, builder *strings.Builder, depth int) bool {
for len(b) > 0 {
num, tp, n := protowire.ConsumeTag(b)
if n < 0 {
return false
}
b = b[n:]

return buf.String(), headers
}

return buf.String(), nil
}

func (i *GrpcInterop) explainSettings(b []byte) string {
var builder strings.Builder

builder.WriteString("http2:settings")
for i := 0; i < len(b)/http2SettingsPayloadLen; i++ {
start := i * http2SettingsPayloadLen
flag := binary.BigEndian.Uint16(b[start : start+2])
value := binary.BigEndian.Uint32(b[start+2 : start+http2SettingsPayloadLen])

switch http2.SettingID(flag) {
case http2.SettingHeaderTableSize:
builder.WriteString(fmt.Sprintf(" header_table_size:%d", value))
case http2.SettingEnablePush:
builder.WriteString(fmt.Sprintf(" enable_push:%d", value))
case http2.SettingMaxConcurrentStreams:
builder.WriteString(fmt.Sprintf(" max_concurrent_streams:%d", value))
case http2.SettingInitialWindowSize:
builder.WriteString(fmt.Sprintf(" initial_window_size:%d", value))
case http2.SettingMaxFrameSize:
builder.WriteString(fmt.Sprintf(" max_frame_size:%d", value))
case http2.SettingMaxHeaderListSize:
builder.WriteString(fmt.Sprintf(" max_header_list_size:%d", value))
switch tp {
case protowire.VarintType:
_, n = protowire.ConsumeVarint(b)
if n < 0 {
return false
}
b = b[n:]
write(builder, fmt.Sprintf("#%d: (varint)\n", num), depth)
case protowire.Fixed32Type:
_, n = protowire.ConsumeFixed32(b)
if n < 0 {
return false
}
b = b[n:]
write(builder, fmt.Sprintf("#%d: (fixed32)\n", num), depth)
case protowire.Fixed64Type:
_, n = protowire.ConsumeFixed64(b)
if n < 0 {
return false
}
b = b[n:]
write(builder, fmt.Sprintf("#%d: (fixed64)\n", num), depth)
case protowire.BytesType:
v, n := protowire.ConsumeBytes(b)
if n < 0 {
return false
}
var buf strings.Builder
if g.explainFields(b[1:n], &buf, depth+1) {
write(builder, fmt.Sprintf("#%d:\n", num), depth)
builder.WriteString(buf.String())
} else {
write(builder, fmt.Sprintf("#%d: %s\n", num, v), depth)
}
b = b[n:]
default:
_, _, n = protowire.ConsumeField(b)
if n < 0 {
return false
}
b = b[n:]
}
}

return builder.String()
return true
}

func (i *GrpcInterop) readPreface(r io.Reader, source string, id int) {
if source != ClientSide {
return
}

preface := make([]byte, len(http2Preface))
n, err := r.Read(preface)
if err != nil || n < len(http2Preface) {
return
func write(builder *strings.Builder, val string, depth int) {
for i := 0; i < depth; i++ {
builder.WriteString(" ")
}

fmt.Println()
var builder strings.Builder
builder.WriteString(color.HiGreenString("from %s [%d]\n", source, id))
builder.WriteString(fmt.Sprintf("%s%s%s\n",
color.HiBlueString("%s:(", grpcProtocol),
color.YellowString("http2:preface"),
color.HiBlueString(")")))
builder.WriteString(fmt.Sprint(hex.Dump(preface)))
display.PrintlnWithTime(builder.String())
builder.WriteString(val)
}
Loading

0 comments on commit 36c7fd7

Please sign in to comment.