From 4aca796ca291e7b0ef4fe2f4aa1c722fad642871 Mon Sep 17 00:00:00 2001
From: Carl Mastrangelo <notcarl@google.com>
Date: Mon, 5 Oct 2015 16:17:47 -0700
Subject: [PATCH] Add initial interop tests

---
 tools/http2_interop/README.md            |   9 +
 tools/http2_interop/doc.go               |   6 +
 tools/http2_interop/frame.go             |  11 +
 tools/http2_interop/frameheader.go       | 109 ++++++++++
 tools/http2_interop/http2interop.go      | 245 +++++++++++++++++++++++
 tools/http2_interop/http2interop_test.go |  50 +++++
 tools/http2_interop/ping.go              |  65 ++++++
 tools/http2_interop/settings.go          | 109 ++++++++++
 tools/http2_interop/unknownframe.go      |  54 +++++
 9 files changed, 658 insertions(+)
 create mode 100644 tools/http2_interop/README.md
 create mode 100644 tools/http2_interop/doc.go
 create mode 100644 tools/http2_interop/frame.go
 create mode 100644 tools/http2_interop/frameheader.go
 create mode 100644 tools/http2_interop/http2interop.go
 create mode 100644 tools/http2_interop/http2interop_test.go
 create mode 100644 tools/http2_interop/ping.go
 create mode 100644 tools/http2_interop/settings.go
 create mode 100644 tools/http2_interop/unknownframe.go

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
+}
-- 
GitLab