diff --git a/tools/http2_interop/README.md b/tools/http2_interop/README.md new file mode 100644 index 00000000000..21688f09804 --- /dev/null +++ b/tools/http2_interop/README.md @@ -0,0 +1,9 @@ +HTTP/2 Interop Tests +==== + +This is a suite of tests that check a server to see if it plays nicely with other HTTP/2 clients. To run, just type: + +`go test -spec :1234` + +Where ":1234" is the ip:port of a running server. + diff --git a/tools/http2_interop/doc.go b/tools/http2_interop/doc.go new file mode 100644 index 00000000000..6c6b5cb1938 --- /dev/null +++ b/tools/http2_interop/doc.go @@ -0,0 +1,6 @@ +// http2interop project doc.go + +/* +http2interop document +*/ +package http2interop diff --git a/tools/http2_interop/frame.go b/tools/http2_interop/frame.go new file mode 100644 index 00000000000..12689e9b33d --- /dev/null +++ b/tools/http2_interop/frame.go @@ -0,0 +1,11 @@ +package http2interop + +import ( + "io" +) + +type Frame interface { + GetHeader() *FrameHeader + ParsePayload(io.Reader) error + MarshalBinary() ([]byte, error) +} diff --git a/tools/http2_interop/frameheader.go b/tools/http2_interop/frameheader.go new file mode 100644 index 00000000000..78fe4201f62 --- /dev/null +++ b/tools/http2_interop/frameheader.go @@ -0,0 +1,109 @@ +package http2interop + +import ( + "encoding/binary" + "fmt" + "io" +) + +type FrameHeader struct { + Length int + Type FrameType + Flags byte + Reserved Reserved + StreamID +} + +type Reserved bool + +func (r Reserved) String() string { + if r { + return "R" + } + return "" +} + +func (fh *FrameHeader) Parse(r io.Reader) error { + buf := make([]byte, 9) + if _, err := io.ReadFull(r, buf); err != nil { + return err + } + return fh.UnmarshalBinary(buf) +} + +func (fh *FrameHeader) UnmarshalBinary(b []byte) error { + if len(b) != 9 { + return fmt.Errorf("Invalid frame header length %d", len(b)) + } + *fh = FrameHeader{ + Length: int(b[0])<<16 | int(b[1])<<8 | int(b[2]), + Type: FrameType(b[3]), + Flags: b[4], + Reserved: Reserved(b[5]>>7 == 1), + StreamID: StreamID(binary.BigEndian.Uint32(b[5:9]) & 0x7fffffff), + } + return nil +} + +func (fh *FrameHeader) MarshalBinary() ([]byte, error) { + buf := make([]byte, 9, 9+fh.Length) + + if fh.Length > 0xFFFFFF || fh.Length < 0 { + return nil, fmt.Errorf("Invalid frame header length: %d", fh.Length) + } + if fh.StreamID < 0 { + return nil, fmt.Errorf("Invalid Stream ID: %v", fh.StreamID) + } + + buf[0], buf[1], buf[2] = byte(fh.Length>>16), byte(fh.Length>>8), byte(fh.Length) + buf[3] = byte(fh.Type) + buf[4] = fh.Flags + binary.BigEndian.PutUint32(buf[5:], uint32(fh.StreamID)) + + return buf, nil +} + +type StreamID int32 + +type FrameType byte + +func (ft FrameType) String() string { + switch ft { + case DataFrameType: + return "DATA" + case HeadersFrameType: + return "HEADERS" + case PriorityFrameType: + return "PRIORITY" + case ResetStreamFrameType: + return "RST_STREAM" + case SettingsFrameType: + return "SETTINGS" + case PushPromiseFrameType: + return "PUSH_PROMISE" + case PingFrameType: + return "PING" + case GoAwayFrameType: + return "GOAWAY" + case WindowUpdateFrameType: + return "WINDOW_UPDATE" + case ContinuationFrameType: + return "CONTINUATION" + default: + return fmt.Sprintf("UNKNOWN(%d)", byte(ft)) + } +} + +// Types +const ( + DataFrameType FrameType = 0 + HeadersFrameType FrameType = 1 + PriorityFrameType FrameType = 2 + ResetStreamFrameType FrameType = 3 + SettingsFrameType FrameType = 4 + PushPromiseFrameType FrameType = 5 + PingFrameType FrameType = 6 + GoAwayFrameType FrameType = 7 + WindowUpdateFrameType FrameType = 8 + ContinuationFrameType FrameType = 9 +) diff --git a/tools/http2_interop/http2interop.go b/tools/http2_interop/http2interop.go new file mode 100644 index 00000000000..f1bca7fe13d --- /dev/null +++ b/tools/http2_interop/http2interop.go @@ -0,0 +1,245 @@ +package http2interop + +import ( + "crypto/tls" + "fmt" + "io" + "log" +) + +const ( + Preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" +) + +func parseFrame(r io.Reader) (Frame, error) { + fh := FrameHeader{} + if err := fh.Parse(r); err != nil { + return nil, err + } + var f Frame + switch fh.Type { + case PingFrameType: + f = &PingFrame{ + Header: fh, + } + case SettingsFrameType: + f = &SettingsFrame{ + Header: fh, + } + default: + f = &UnknownFrame{ + Header: fh, + } + } + if err := f.ParsePayload(r); err != nil { + return nil, err + } + + return f, nil +} + +func streamFrame(w io.Writer, f Frame) error { + raw, err := f.MarshalBinary() + if err != nil { + return err + } + if _, err := w.Write(raw); err != nil { + return err + } + return nil +} + +func getHttp2Conn(addr string) (*tls.Conn, error) { + config := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"h2"}, + } + + conn, err := tls.Dial("tcp", addr, config) + if err != nil { + return nil, err + } + + return conn, nil +} + +func testClientShortSettings(addr string, length int) error { + c, err := getHttp2Conn(addr) + if err != nil { + return err + } + defer c.Close() + + if _, err := c.Write([]byte(Preface)); err != nil { + return err + } + + // Bad, settings, non multiple of 6 + sf := &UnknownFrame{ + Header: FrameHeader{ + Type: SettingsFrameType, + }, + Data: make([]byte, length), + } + if err := streamFrame(c, sf); err != nil { + return err + } + + for { + frame, err := parseFrame(c) + if err != nil { + return err + } + log.Println(frame) + } + + return nil +} + +func testClientPrefaceWithStreamId(addr string) error { + c, err := getHttp2Conn(addr) + if err != nil { + return err + } + defer c.Close() + + // Good so far + if _, err := c.Write([]byte(Preface)); err != nil { + return err + } + + // Bad, settings do not have ids + sf := &SettingsFrame{ + Header: FrameHeader{ + StreamID: 1, + }, + } + if err := streamFrame(c, sf); err != nil { + return err + } + + for { + frame, err := parseFrame(c) + if err != nil { + return err + } + log.Println(frame) + } + + return nil +} + +func testUnknownFrameType(addr string) error { + c, err := getHttp2Conn(addr) + if err != nil { + return err + } + defer c.Close() + + if _, err := c.Write([]byte(Preface)); err != nil { + return err + } + + // Send some settings, which are part of the client preface + sf := &SettingsFrame{} + if err := streamFrame(c, sf); err != nil { + return err + } + + // Write a bunch of invalid frame types. + for ft := ContinuationFrameType + 1; ft != 0; ft++ { + fh := &UnknownFrame{ + Header: FrameHeader{ + Type: ft, + }, + } + if err := streamFrame(c, fh); err != nil { + return err + } + } + + pf := &PingFrame{ + Data: []byte("01234567"), + } + if err := streamFrame(c, pf); err != nil { + return err + } + + for { + frame, err := parseFrame(c) + if err != nil { + return err + } + if npf, ok := frame.(*PingFrame); !ok { + continue + } else { + if string(npf.Data) != string(pf.Data) || npf.Header.Flags&PING_ACK == 0 { + return fmt.Errorf("Bad ping %+v", *npf) + } + return nil + } + } + + return nil +} + +func testShortPreface(addr string, prefacePrefix string) error { + c, err := getHttp2Conn(addr) + if err != nil { + return err + } + defer c.Close() + + if _, err := c.Write([]byte(prefacePrefix)); err != nil { + return err + } + + buf := make([]byte, 256) + for ; err == nil; _, err = c.Read(buf) { + } + // TODO: maybe check for a GOAWAY? + return err +} + +func testTLSMaxVersion(addr string, version uint16) error { + config := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"h2"}, + MaxVersion: version, + } + conn, err := tls.Dial("tcp", addr, config) + if err != nil { + return err + } + defer conn.Close() + + buf := make([]byte, 256) + if n, err := conn.Read(buf); err != nil { + if n != 0 { + return fmt.Errorf("Expected no bytes to be read, but was %d", n) + } + return err + } + return nil +} + +func testTLSApplicationProtocol(addr string) error { + config := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"h2c"}, + } + conn, err := tls.Dial("tcp", addr, config) + if err != nil { + return err + } + defer conn.Close() + + buf := make([]byte, 256) + if n, err := conn.Read(buf); err != nil { + if n != 0 { + return fmt.Errorf("Expected no bytes to be read, but was %d", n) + } + return err + } + return nil +} diff --git a/tools/http2_interop/http2interop_test.go b/tools/http2_interop/http2interop_test.go new file mode 100644 index 00000000000..3b687c035e8 --- /dev/null +++ b/tools/http2_interop/http2interop_test.go @@ -0,0 +1,50 @@ +package http2interop + +import ( + "crypto/tls" + "flag" + "io" + "os" + "testing" +) + +var ( + serverSpec = flag.String("spec", ":50051", "The server spec to test") +) + +func TestShortPreface(t *testing.T) { + for i := 0; i < len(Preface)-1; i++ { + if err := testShortPreface(*serverSpec, Preface[:i]+"X"); err != io.EOF { + t.Error("Expected an EOF but was", err) + } + } +} + +func TestUnknownFrameType(t *testing.T) { + if err := testUnknownFrameType(*serverSpec); err != nil { + t.Fatal(err) + } +} + +func TestTLSApplicationProtocol(t *testing.T) { + if err := testTLSApplicationProtocol(*serverSpec); err != io.EOF { + t.Fatal("Expected an EOF but was", err) + } +} + +func TestTLSMaxVersion(t *testing.T) { + if err := testTLSMaxVersion(*serverSpec, tls.VersionTLS11); err != io.EOF { + t.Fatal("Expected an EOF but was", err) + } +} + +func TestClientPrefaceWithStreamId(t *testing.T) { + if err := testClientPrefaceWithStreamId(*serverSpec); err != io.EOF { + t.Fatal("Expected an EOF but was", err) + } +} + +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} diff --git a/tools/http2_interop/ping.go b/tools/http2_interop/ping.go new file mode 100644 index 00000000000..6011eed4511 --- /dev/null +++ b/tools/http2_interop/ping.go @@ -0,0 +1,65 @@ +package http2interop + +import ( + "fmt" + "io" +) + +type PingFrame struct { + Header FrameHeader + Data []byte +} + +const ( + PING_ACK = 0x01 +) + +func (f *PingFrame) GetHeader() *FrameHeader { + return &f.Header +} + +func (f *PingFrame) ParsePayload(r io.Reader) error { + raw := make([]byte, f.Header.Length) + if _, err := io.ReadFull(r, raw); err != nil { + return err + } + return f.UnmarshalPayload(raw) +} + +func (f *PingFrame) UnmarshalPayload(raw []byte) error { + if f.Header.Length != len(raw) { + return fmt.Errorf("Invalid Payload length %d != %d", f.Header.Length, len(raw)) + } + if f.Header.Length != 8 { + return fmt.Errorf("Invalid Payload length %d", f.Header.Length) + } + + f.Data = []byte(string(raw)) + + return nil +} + +func (f *PingFrame) MarshalPayload() ([]byte, error) { + if len(f.Data) != 8 { + return nil, fmt.Errorf("Invalid Payload length %d", len(f.Data)) + } + return []byte(string(f.Data)), nil +} + +func (f *PingFrame) MarshalBinary() ([]byte, error) { + payload, err := f.MarshalPayload() + if err != nil { + return nil, err + } + + f.Header.Length = len(payload) + f.Header.Type = PingFrameType + header, err := f.Header.MarshalBinary() + if err != nil { + return nil, err + } + + header = append(header, payload...) + + return header, nil +} diff --git a/tools/http2_interop/settings.go b/tools/http2_interop/settings.go new file mode 100644 index 00000000000..5a2b1ada651 --- /dev/null +++ b/tools/http2_interop/settings.go @@ -0,0 +1,109 @@ +package http2interop + +import ( + "encoding/binary" + "fmt" + "io" +) + +const ( + SETTINGS_ACK = 1 +) + +type SettingsFrame struct { + Header FrameHeader + Params []SettingsParameter +} + +type SettingsIdentifier uint16 + +const ( + SettingsHeaderTableSize SettingsIdentifier = 1 + SettingsEnablePush SettingsIdentifier = 2 + SettingsMaxConcurrentStreams SettingsIdentifier = 3 + SettingsInitialWindowSize SettingsIdentifier = 4 + SettingsMaxFrameSize SettingsIdentifier = 5 + SettingsMaxHeaderListSize SettingsIdentifier = 6 +) + +func (si SettingsIdentifier) String() string { + switch si { + case SettingsHeaderTableSize: + return "HEADER_TABLE_SIZE" + case SettingsEnablePush: + return "ENABLE_PUSH" + case SettingsMaxConcurrentStreams: + return "MAX_CONCURRENT_STREAMS" + case SettingsInitialWindowSize: + return "INITIAL_WINDOW_SIZE" + case SettingsMaxFrameSize: + return "MAX_FRAME_SIZE" + case SettingsMaxHeaderListSize: + return "MAX_HEADER_LIST_SIZE" + default: + return fmt.Sprintf("UNKNOWN(%d)", uint16(si)) + } +} + +type SettingsParameter struct { + Identifier SettingsIdentifier + Value uint32 +} + +func (f *SettingsFrame) GetHeader() *FrameHeader { + return &f.Header +} + +func (f *SettingsFrame) ParsePayload(r io.Reader) error { + raw := make([]byte, f.Header.Length) + if _, err := io.ReadFull(r, raw); err != nil { + return err + } + return f.UnmarshalPayload(raw) +} + +func (f *SettingsFrame) UnmarshalPayload(raw []byte) error { + if f.Header.Length != len(raw) { + return fmt.Errorf("Invalid Payload length %d != %d", f.Header.Length, len(raw)) + } + + if f.Header.Length%6 != 0 { + return fmt.Errorf("Invalid Payload length %d", f.Header.Length) + } + + f.Params = make([]SettingsParameter, 0, f.Header.Length/6) + for i := 0; i < len(raw); i += 6 { + f.Params = append(f.Params, SettingsParameter{ + Identifier: SettingsIdentifier(binary.BigEndian.Uint16(raw[i : i+2])), + Value: binary.BigEndian.Uint32(raw[i+2 : i+6]), + }) + } + return nil +} + +func (f *SettingsFrame) MarshalPayload() ([]byte, error) { + raw := make([]byte, 0, len(f.Params)*6) + for i, p := range f.Params { + binary.BigEndian.PutUint16(raw[i*6:i*6+2], uint16(p.Identifier)) + binary.BigEndian.PutUint32(raw[i*6+2:i*6+6], p.Value) + } + return raw, nil +} + +func (f *SettingsFrame) MarshalBinary() ([]byte, error) { + payload, err := f.MarshalPayload() + if err != nil { + return nil, err + } + + f.Header.Length = len(payload) + f.Header.Type = SettingsFrameType + header, err := f.Header.MarshalBinary() + if err != nil { + return nil, err + } + + header = append(header, payload...) + + return header, nil +} diff --git a/tools/http2_interop/unknownframe.go b/tools/http2_interop/unknownframe.go new file mode 100644 index 00000000000..0450e7e976c --- /dev/null +++ b/tools/http2_interop/unknownframe.go @@ -0,0 +1,54 @@ +package http2interop + +import ( + "fmt" + "io" +) + +type UnknownFrame struct { + Header FrameHeader + Data []byte +} + +func (f *UnknownFrame) GetHeader() *FrameHeader { + return &f.Header +} + +func (f *UnknownFrame) ParsePayload(r io.Reader) error { + raw := make([]byte, f.Header.Length) + if _, err := io.ReadFull(r, raw); err != nil { + return err + } + return f.UnmarshalPayload(raw) +} + +func (f *UnknownFrame) UnmarshalPayload(raw []byte) error { + if f.Header.Length != len(raw) { + return fmt.Errorf("Invalid Payload length %d != %d", f.Header.Length, len(raw)) + } + + f.Data = []byte(string(raw)) + + return nil +} + +func (f *UnknownFrame) MarshalPayload() ([]byte, error) { + return []byte(string(f.Data)), nil +} + +func (f *UnknownFrame) MarshalBinary() ([]byte, error) { + f.Header.Length = len(f.Data) + buf, err := f.Header.MarshalBinary() + if err != nil { + return nil, err + } + + payload, err := f.MarshalPayload() + if err != nil { + return nil, err + } + + buf = append(buf, payload...) + + return buf, nil +}