aboutsummaryrefslogtreecommitdiffhomepage
path: root/tools/http2_interop
diff options
context:
space:
mode:
authorGravatar Carl Mastrangelo <notcarl@google.com>2015-10-05 16:17:47 -0700
committerGravatar Carl Mastrangelo <notcarl@google.com>2015-10-05 16:17:47 -0700
commit4aca796ca291e7b0ef4fe2f4aa1c722fad642871 (patch)
tree2aa42b9c2523fd4362b2194cb24ae97a94c0996c /tools/http2_interop
parentd48c5a5b9feb9cfacde4741e92d6dfcdae632416 (diff)
Add initial interop tests
Diffstat (limited to 'tools/http2_interop')
-rw-r--r--tools/http2_interop/README.md9
-rw-r--r--tools/http2_interop/doc.go6
-rw-r--r--tools/http2_interop/frame.go11
-rw-r--r--tools/http2_interop/frameheader.go109
-rw-r--r--tools/http2_interop/http2interop.go245
-rw-r--r--tools/http2_interop/http2interop_test.go50
-rw-r--r--tools/http2_interop/ping.go65
-rw-r--r--tools/http2_interop/settings.go109
-rw-r--r--tools/http2_interop/unknownframe.go54
9 files changed, 658 insertions, 0 deletions
diff --git a/tools/http2_interop/README.md b/tools/http2_interop/README.md
new file mode 100644
index 0000000000..21688f0980
--- /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 0000000000..6c6b5cb193
--- /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 0000000000..12689e9b33
--- /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 0000000000..78fe4201f6
--- /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 0000000000..f1bca7fe13
--- /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 0000000000..3b687c035e
--- /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 0000000000..6011eed451
--- /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 0000000000..5a2b1ada65
--- /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 0000000000..0450e7e976
--- /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
+}