Skip to content
Snippets Groups Projects
Commit 4aca796c authored by Carl Mastrangelo's avatar Carl Mastrangelo
Browse files

Add initial interop tests

parent d48c5a5b
No related branches found
No related tags found
No related merge requests found
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.
// http2interop project doc.go
/*
http2interop document
*/
package http2interop
package http2interop
import (
"io"
)
type Frame interface {
GetHeader() *FrameHeader
ParsePayload(io.Reader) error
MarshalBinary() ([]byte, error)
}
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
)
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
}
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())
}
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
}
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
}
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
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment