diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..2da4fe3 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,35 @@ +version: 2 +jobs: + build: + docker: + - image: summerwind/toolbox:latest + steps: + - checkout + - setup_remote_docker: + version: 19.03.12 + - run: + name: Build container + command: make build-container + release: + docker: + - image: summerwind/toolbox:latest + steps: + - checkout + - run: + name: Upload release files to GitHub + command: make github-release + +workflows: + version: 2 + main: + jobs: + - build + release: + jobs: + - release: + context: global + filters: + branches: + ignore: /.*/ + tags: + only: /.*/ diff --git a/.gitignore b/.gitignore index 9b175db..ecbc6d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store h2spec +h2specd glide.lock vendor/ release/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3ece8c8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: go -go: - - 1.7.x -sudo: false -install: - - go get github.com/Masterminds/glide -script: - - make build - - make test diff --git a/Dockerfile b/Dockerfile index 3fe0bca..2ea8f00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,26 @@ -FROM alpine:3.4 -MAINTAINER Moto Ishizawa "summerwind.jp" +FROM golang:1.18 as builder -COPY ./h2spec /usr/bin/h2spec +ARG VERSION +ARG COMMIT -ENTRYPOINT ["h2spec", "--help"] +ENV GO111MODULE=on \ + GOPROXY=https://proxy.golang.org + +WORKDIR /go/src/github.com/summerwind/h2spec +COPY go.mod go.sum . +RUN go mod download + +COPY . /workspace +WORKDIR /workspace + +RUN go vet ./... +RUN go test -v ./... +RUN CGO_ENABLED=0 go build -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=${COMMIT}" ./cmd/h2spec + +################### + +FROM ubuntu:22.04 + +COPY --from=builder /workspace/h2spec /usr/local/bin/h2spec + +ENTRYPOINT ["/usr/local/bin/h2spec"] diff --git a/Makefile b/Makefile index 626aa71..c231b0e 100755 --- a/Makefile +++ b/Makefile @@ -1,31 +1,27 @@ -VERSION=2.0.2 +VERSION=2.6.0 COMMIT=$(shell git rev-parse --verify HEAD) - -PACKAGES=$(shell go list ./... | grep -v /vendor/) BUILD_FLAGS=-ldflags "-X main.VERSION=$(VERSION) -X main.COMMIT=$(COMMIT)" -.PHONY: all all: build -.PHONY: build -build: vendor +build: go build $(BUILD_FLAGS) cmd/h2spec/h2spec.go -.PHONY: test test: - go test -v $(PACKAGES) - go vet $(PACKAGES) + go vet ./... + go test -v ./... -.PHONY: clean clean: - rm -rf h2spec - rm -rf release + rm -rf h2spec release -.PHONY: container -container: - GOARCH=amd64 GOOS=linux go build $(BUILD_FLAGS) cmd/h2spec/h2spec.go - docker build -t summerwind/h2spec:latest -t summerwind/h2spec:$(VERSION) . - rm -rf h2spec +build-container: + docker build --build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) -t summerwind/h2spec:latest -t summerwind/h2spec:$(VERSION) . + +push-container: + docker push summerwind/h2spec:latest + +push-release-container: + docker push summerwind/h2spec:$(VERSION) release: mkdir -p release @@ -42,5 +38,5 @@ release: tar -czf release/h2spec_linux_amd64.tar.gz h2spec rm -rf h2spec -vendor: - glide install +github-release: release + ghr v$(VERSION) release/ diff --git a/README.md b/README.md index e3f6c51..88bc250 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ This tool is compliant with [RFC 7540 (HTTP/2)](http://www.rfc-editor.org/rfc/rf Go to the [releases page](https://github.com/summerwind/h2spec/releases), find the version you want, and download the zip file or tarball file. The docker image is also available in [Docker Hub](https://hub.docker.com/r/summerwind/h2spec/). +## Your server + +Your server should respond on `GET /` or `POST /` requests with status 200 response with non-empty data. + ## Usage ``` @@ -16,12 +20,14 @@ Usage: h2spec [spec...] [flags] Flags: + -c, --ciphers string List of colon-separated TLS cipher names --dryrun Display only the title of test cases --help Display this help and exit -h, --host string Target host (default "127.0.0.1") -k, --insecure Don't verify server's certificate -j, --junit-report string Path for JUnit test report --max-header-length int Maximum length of HTTP header (default 4000) + -P, --path string Target path (default "/") -p, --port int Target port -S, --strict Run all test cases including strict test cases -o, --timeout int Time seconds to test timeout (default 2) @@ -86,7 +92,7 @@ $ h2spec --strict ## Build -First, you need to install Go, Glide and set GOPATH appropriately. +To build from source, you need to install [Go](https://golang.org) and export `GO111MODULE=on` first. To build: ``` diff --git a/client/1_starting_http2.go b/client/1_starting_http2.go new file mode 100644 index 0000000..5452333 --- /dev/null +++ b/client/1_starting_http2.go @@ -0,0 +1,21 @@ +package client + +import ( + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func StartingHTTP2() *spec.ClientTestGroup { + tg := NewTestGroup("1", "Starting HTTP/2") + + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a server connection preface", + Requirement: "The endpoint MUST accept server connection preface.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + return err + }, + }) + + return tg +} diff --git a/client/4_1_frame_format.go b/client/4_1_frame_format.go new file mode 100644 index 0000000..8d89a2d --- /dev/null +++ b/client/4_1_frame_format.go @@ -0,0 +1,79 @@ +package client + +import ( + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func FrameFormat() *spec.ClientTestGroup { + tg := NewTestGroup("4.1", "Frame Format") + + // Type: The 8-bit type of the frame. The frame type determines + // the format and semantics of the frame. Implementations MUST + // ignore and discard any frame that has a type that is unknown. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a frame with unknown type", + Requirement: "The endpoint MUST ignore and discard any frame that has a type that is unknown.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // UNKONWN Frame: + // Length: 8, Type: 255, Flags: 0, R: 0, StreamID: 0 + conn.Send([]byte("\x00\x00\x08\x16\x00\x00\x00\x00\x00")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")) + + data := [8]byte{} + conn.WritePing(false, data) + + return spec.VerifyPingFrameWithAck(conn, data) + }, + }) + + // Flags are assigned semantics specific to the indicated frame + // type. Flags that have no defined semantics for a particular + // frame type MUST be ignored and MUST be left unset (0x0) when + // sending. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a frame with undefined flag", + Requirement: "The endpoint MUST ignore any flags that is undefined.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // PING Frame: + // Length: 8, Type: 6, Flags: 255, R: 0, StreamID: 0 + conn.Send([]byte("\x00\x00\x08\x06\x16\x00\x00\x00\x00")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")) + + return spec.VerifyEventType(conn, spec.EventPingFrame) + }, + }) + + // R: A reserved 1-bit field. The semantics of this bit are + // undefined, and the bit MUST remain unset (0x0) when sending + // and MUST be ignored when receiving. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a frame with reserved field bit", + Requirement: "The endpoint MUST ignore the value of reserved field.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // PING Frame: + // Length: 8, Type: 6, Flags: 255, R: 1, StreamID: 0 + conn.Send([]byte("\x00\x00\x08\x06\x16\x80\x00\x00\x00")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")) + + return spec.VerifyEventType(conn, spec.EventPingFrame) + }, + }) + + return tg +} diff --git a/client/4_2_frame_size.go b/client/4_2_frame_size.go new file mode 100644 index 0000000..c800ccc --- /dev/null +++ b/client/4_2_frame_size.go @@ -0,0 +1,122 @@ +package client + +import ( + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" + "golang.org/x/net/http2" +) + +func FrameSize() *spec.ClientTestGroup { + tg := NewTestGroup("4.2", "Frame Size") + + // All implementations MUST be capable of receiving and minimally + // processing frames up to 2^14 octets in length, plus the 9-octet + // frame header (Section 4.1). + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a DATA frame with 2^14 octets in length", + Requirement: "The endpoint MUST be capable of receiving and minimally processing frames up to 2^14 octets in length.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + + conn.WriteHeaders(hp) + + data := spec.DummyString(conn.MaxFrameSize()) + conn.WriteData(req.StreamID, true, []byte(data)) + + pingData := [8]byte{} + conn.WritePing(false, pingData) + + return spec.VerifyPingFrameWithAck(conn, pingData) + }, + }) + + // An endpoint MUST send an error code of FRAME_SIZE_ERROR + // if a frame exceeds the size defined in SETTINGS_MAX_FRAME_SIZE, + // exceeds any limit defined for the frame type, or is too small + // to contain mandatory frame data. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a large size DATA frame that exceeds the SETTINGS_MAX_FRAME_SIZE", + Requirement: "The endpoint MUST send an error code of FRAME_SIZE_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + + conn.WriteHeaders(hp) + + data := spec.DummyString(conn.MaxFrameSize() + 1) + conn.WriteData(req.StreamID, true, []byte(data)) + + return spec.VerifyStreamError(conn, http2.ErrCodeFrameSize) + }, + }) + + // A frame size error in a frame that could alter the state of + // the entire connection MUST be treated as a connection error + // (Section 5.4.1); this includes any frame carrying a header block + // (Section 4.3) (that is, HEADERS, PUSH_PROMISE, and CONTINUATION), + // SETTINGS, and any frame with a stream identifier of 0. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a large size HEADERS frame that exceeds the SETTINGS_MAX_FRAME_SIZE", + Requirement: "The endpoint MUST respond with a connection error of type FRAME_SIZE_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + headers = append(headers, spec.DummyRespHeaders(c, 5)...) + + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + + conn.WriteHeaders(hp) + + return spec.VerifyConnectionError(conn, http2.ErrCodeFrameSize) + }, + }) + + return tg +} diff --git a/client/4_3_header_compression_and_decompression.go b/client/4_3_header_compression_and_decompression.go new file mode 100644 index 0000000..79c4ac2 --- /dev/null +++ b/client/4_3_header_compression_and_decompression.go @@ -0,0 +1,48 @@ +package client + +import ( + "bytes" + "encoding/binary" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" + "golang.org/x/net/http2" +) + +func HeaderCompressionAndDecompression() *spec.ClientTestGroup { + tg := NewTestGroup("4.3", "Header Compression and Decompression") + + // A decoding error in a header block MUST be treated as + // a connection error (Section 5.4.1) of type COMPRESSION_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends invalid header block fragment", + Requirement: "The endpoint MUST terminate the connection with a connection error of type COMPRESSION_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + // Literal Header Field with Incremental Indexing without + // Length and String segment. + data := new(bytes.Buffer) + data.Write([]byte("\x00\x00\x01\x01\x05")) + binary.Write(data, binary.LittleEndian, req.StreamID) + data.Write([]byte{0x40}) + + err = conn.Send(data.Bytes()) + if err != nil { + return err + } + + return spec.VerifyConnectionError(conn, http2.ErrCodeCompression) + }, + }) + + return tg +} diff --git a/client/4_http_frames.go b/client/4_http_frames.go new file mode 100644 index 0000000..99d4108 --- /dev/null +++ b/client/4_http_frames.go @@ -0,0 +1,13 @@ +package client + +import "github.com/summerwind/h2spec/spec" + +func HTTPFrames() *spec.ClientTestGroup { + tg := NewTestGroup("4", "HTTP Frames") + + tg.AddTestGroup(FrameFormat()) + tg.AddTestGroup(FrameSize()) + tg.AddTestGroup(HeaderCompressionAndDecompression()) + + return tg +} diff --git a/client/5_1_1_stream_identifiers.go b/client/5_1_1_stream_identifiers.go new file mode 100644 index 0000000..d8176a7 --- /dev/null +++ b/client/5_1_1_stream_identifiers.go @@ -0,0 +1,38 @@ +package client + +import ( + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" + "golang.org/x/net/http2" +) + +func StreamIdentifiers() *spec.ClientTestGroup { + tg := NewTestGroup("5.1.1", "Stream Identifiers") + + // An endpoint that receives an unexpected stream identifier + // MUST respond with a connection error (Section 5.4.1) of + // type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends an unexpected stream identifier", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: 101, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + return tg +} diff --git a/client/5_1_stream_states.go b/client/5_1_stream_states.go new file mode 100644 index 0000000..f6c75c2 --- /dev/null +++ b/client/5_1_stream_states.go @@ -0,0 +1,313 @@ +package client + +import ( + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" + "golang.org/x/net/http2" +) + +func StreamStates() *spec.ClientTestGroup { + tg := NewTestGroup("5.1", "Stream States") + + // idle: + // Receiving any frame other than HEADERS or PRIORITY on a stream + // in this state MUST be treated as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "idle: Sends a DATA frame", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + conn.WriteData(2, true, []byte("test")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // idle: + // Receiving any frame other than HEADERS or PRIORITY on a stream + // in this state MUST be treated as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "idle: Sends a RST_STREAM frame", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + conn.WriteRSTStream(2, http2.ErrCodeCancel) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // idle: + // Receiving any frame other than HEADERS or PRIORITY on a stream + // in this state MUST be treated as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "idle: Sends a WINDOW_UPDATE frame", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + conn.WriteWindowUpdate(2, 100) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // idle: + // Receiving any frame other than HEADERS or PRIORITY on a stream + // in this state MUST be treated as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "idle: Sends a CONTINUATION frame", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + blockFragment := conn.EncodeHeaders(headers) + conn.WriteContinuation(2, true, blockFragment) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // closed: + // An endpoint that receives any frame other than PRIORITY after + // receiving a RST_STREAM MUST treat that as a stream error + // (Section 5.4.2) of type STREAM_CLOSED. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "closed: Sends a DATA frame after sending RST_STREAM frame", + Requirement: "The endpoint MUST treat this as a stream error of type STREAM_CLOSED.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + conn.WriteRSTStream(req.StreamID, http2.ErrCodeCancel) + + conn.WriteData(req.StreamID, true, []byte("test")) + + return spec.VerifyStreamError(conn, http2.ErrCodeStreamClosed) + }, + }) + + // closed: + // An endpoint that receives any frame other than PRIORITY after + // receiving a RST_STREAM MUST treat that as a stream error + // (Section 5.4.2) of type STREAM_CLOSED. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "closed: Sends a HEADERS frame after sending RST_STREAM frame", + Requirement: "The endpoint MUST treat this as a stream error of type STREAM_CLOSED.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp1 := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp1) + + conn.WriteRSTStream(req.StreamID, http2.ErrCodeCancel) + + hp2 := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp2) + + return spec.VerifyStreamError(conn, http2.ErrCodeStreamClosed) + }, + }) + + // closed: + // An endpoint that receives any frame other than PRIORITY after + // receiving a RST_STREAM MUST treat that as a stream error + // (Section 5.4.2) of type STREAM_CLOSED. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "closed: Sends a CONTINUATION frame after sending RST_STREAM frame", + Requirement: "The endpoint MUST treat this as a stream error of type STREAM_CLOSED.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + conn.WriteRSTStream(req.StreamID, http2.ErrCodeCancel) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(req.StreamID, true, conn.EncodeHeaders(dummyHeaders)) + + codes := []http2.ErrCode{ + http2.ErrCodeStreamClosed, + http2.ErrCodeProtocol, + } + return spec.VerifyStreamError(conn, codes...) + }, + }) + + // closed: + // An endpoint that receives any frames after receiving a frame + // with the END_STREAM flag set MUST treat that as a connection + // error (Section 6.4.1) of type STREAM_CLOSED. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "closed: Sends a DATA frame", + Requirement: "The endpoint MUST treat this as a connection error of type STREAM_CLOSED.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + conn.WriteData(req.StreamID, true, []byte("test")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeStreamClosed) + }, + }) + + // closed: + // An endpoint that receives any frames after receiving a frame + // with the END_STREAM flag set MUST treat that as a connection + // error (Section 6.4.1) of type STREAM_CLOSED. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "closed: Sends a HEADERS frame", + Requirement: "The endpoint MUST treat this as a connection error of type STREAM_CLOSED.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + conn.WriteHeaders(hp) + + return spec.VerifyConnectionError(conn, http2.ErrCodeStreamClosed) + }, + }) + + // closed: + // An endpoint that receives any frames after receiving a frame + // with the END_STREAM flag set MUST treat that as a connection + // error (Section 6.4.1) of type STREAM_CLOSED. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "closed: Sends a CONTINUATION frame", + Requirement: "The endpoint MUST treat this as a connection error of type STREAM_CLOSED.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(req.StreamID, true, conn.EncodeHeaders(dummyHeaders)) + + codes := []http2.ErrCode{ + http2.ErrCodeStreamClosed, + http2.ErrCodeProtocol, + } + return spec.VerifyConnectionError(conn, codes...) + }, + }) + + tg.AddTestGroup(StreamIdentifiers()) + + return tg +} diff --git a/client/5_4_1_connection_error_handling.go b/client/5_4_1_connection_error_handling.go new file mode 100644 index 0000000..6a62e07 --- /dev/null +++ b/client/5_4_1_connection_error_handling.go @@ -0,0 +1,65 @@ +package client + +import ( + "fmt" + + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func ConnectionErrorHandling() *spec.ClientTestGroup { + tg := NewTestGroup("5.4.1", "Connection Error Handling") + + // After sending the GOAWAY frame for an error condition, + // the endpoint MUST close the TCP connection. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends an invalid PING frame for connection close", + Requirement: "The endpoint MUST close the TCP connection", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // PING frame with invalid stream ID + conn.Send([]byte("\x00\x00\x08\x06\x00\x00\x00\x00\x03")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")) + + return spec.VerifyConnectionClose(conn) + }, + }) + + // An endpoint that encounters a connection error SHOULD first send + // a GOAWAY frame (Section 6.8) with the stream identifier of the last + // stream that it successfully received from its peer. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends an invalid PING frame to receive GOAWAY frame", + Requirement: "An endpoint that encounters a connection error SHOULD first send a GOAWAY frame", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // PING frame with invalid stream ID + conn.Send([]byte("\x00\x00\x08\x06\x00\x00\x00\x00\x03")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")) + + actual, passed := conn.WaitEventByType(spec.EventGoAwayFrame) + if !passed { + return &spec.TestError{ + Expected: []string{ + fmt.Sprintf(spec.ExpectedGoAwayFrame, http2.ErrCodeProtocol), + }, + Actual: actual.String(), + } + } + + return nil + }, + }) + + return tg +} diff --git a/client/5_4_error_handling.go b/client/5_4_error_handling.go new file mode 100644 index 0000000..3b68f64 --- /dev/null +++ b/client/5_4_error_handling.go @@ -0,0 +1,11 @@ +package client + +import "github.com/summerwind/h2spec/spec" + +func ErrorHandling() *spec.ClientTestGroup { + tg := NewTestGroup("5.4", "Error Handling") + + tg.AddTestGroup(ConnectionErrorHandling()) + + return tg +} diff --git a/client/5_5_extending_http2.go b/client/5_5_extending_http2.go new file mode 100644 index 0000000..354c6a4 --- /dev/null +++ b/client/5_5_extending_http2.go @@ -0,0 +1,33 @@ +package client + +import ( + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func ExtendingHTTP2() *spec.ClientTestGroup { + tg := NewTestGroup("5.5", "Extending HTTP/2") + + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends an unknown extension frame", + Requirement: "The endpoint MUST ignore unknown or unsupported values in all extensible protocol elements.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // UNKONWN Frame: + // Length: 8, Type: 255, Flags: 0, R: 0, StreamID: 0 + conn.Send([]byte("\x00\x00\x08\x16\x00\x00\x00\x00\x00")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")) + + data := [8]byte{} + conn.WritePing(false, data) + + return spec.VerifyPingFrameWithAck(conn, data) + }, + }) + + return tg +} diff --git a/client/5_streams_and_multiplexing.go b/client/5_streams_and_multiplexing.go new file mode 100644 index 0000000..ce51cf2 --- /dev/null +++ b/client/5_streams_and_multiplexing.go @@ -0,0 +1,13 @@ +package client + +import "github.com/summerwind/h2spec/spec" + +func StreamsAndMultiplexing() *spec.ClientTestGroup { + tg := NewTestGroup("5", "Streams and Multiplexing") + + tg.AddTestGroup(StreamStates()) + tg.AddTestGroup(ErrorHandling()) + tg.AddTestGroup(ExtendingHTTP2()) + + return tg +} diff --git a/client/6_10_continuation.go b/client/6_10_continuation.go new file mode 100644 index 0000000..1f559a6 --- /dev/null +++ b/client/6_10_continuation.go @@ -0,0 +1,230 @@ +package client + +import ( + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func Continuation() *spec.ClientTestGroup { + tg := NewTestGroup("6.10", "CONTINUATION") + + // The CONTINUATION frame (type=0x9) is used to continue a sequence + // of header block fragments (Section 4.3). Any number of + // CONTINUATION frames can be sent, as long as the preceding frame + // is on the same stream and is a HEADERS, PUSH_PROMISE, + // or CONTINUATION frame without the END_HEADERS flag set. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends multiple CONTINUATION frames preceded by a HEADERS frame", + Requirement: "The endpoint must accept the frame.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: false, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(req.StreamID, false, conn.EncodeHeaders(dummyHeaders)) + conn.WriteContinuation(req.StreamID, true, conn.EncodeHeaders(dummyHeaders)) + + data := [8]byte{} + conn.WritePing(false, data) + + return spec.VerifyPingFrameWithAck(conn, data) + }, + }) + + // END_HEADERS (0x4): + // If the END_HEADERS bit is not set, this frame MUST be followed + // by another CONTINUATION frame. A receiver MUST treat the receipt + // of any other type of frame or a frame on a different stream as + // a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a CONTINUATION frame followed by any frame other than CONTINUATION", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: false, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(req.StreamID, false, conn.EncodeHeaders(dummyHeaders)) + conn.WriteData(req.StreamID, true, []byte("test")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // CONTINUATION frames MUST be associated with a stream. If a + // CONTINUATION frame is received whose stream identifier field is + // 0x0, the recipient MUST respond with a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a CONTINUATION frame with 0x0 stream identifier", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: false, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(0, true, conn.EncodeHeaders(dummyHeaders)) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // A CONTINUATION frame MUST be preceded by a HEADERS, PUSH_PROMISE + // or CONTINUATION frame without the END_HEADERS flag set. + // A recipient that observes violation of this rule MUST respond + // with a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a CONTINUATION frame preceded by a HEADERS frame with END_HEADERS flag", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(req.StreamID, true, conn.EncodeHeaders(dummyHeaders)) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // A CONTINUATION frame MUST be preceded by a HEADERS, PUSH_PROMISE + // or CONTINUATION frame without the END_HEADERS flag set. + // A recipient that observes violation of this rule MUST respond + // with a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a CONTINUATION frame preceded by a CONTINUATION frame with END_HEADERS flag", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: false, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(req.StreamID, true, conn.EncodeHeaders(dummyHeaders)) + conn.WriteContinuation(req.StreamID, true, conn.EncodeHeaders(dummyHeaders)) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // A CONTINUATION frame MUST be preceded by a HEADERS, PUSH_PROMISE + // or CONTINUATION frame without the END_HEADERS flag set. + // A recipient that observes violation of this rule MUST respond + // with a connection error (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a CONTINUATION frame preceded by a DATA frame", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: false, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + conn.WriteData(req.StreamID, true, []byte("test")) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(req.StreamID, false, conn.EncodeHeaders(dummyHeaders)) + conn.WriteContinuation(0, true, conn.EncodeHeaders(dummyHeaders)) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + return tg +} diff --git a/client/6_1_data.go b/client/6_1_data.go new file mode 100644 index 0000000..ff61541 --- /dev/null +++ b/client/6_1_data.go @@ -0,0 +1,109 @@ +package client + +import ( + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func Data() *spec.ClientTestGroup { + tg := NewTestGroup("6.1", "DATA") + + // DATA frames MUST be associated with a stream. If a DATA frame is + // received whose stream identifier field is 0x0, the recipient + // MUST respond with a connection error (Section 5.4.1) of type + // PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a DATA frame with 0x0 stream identifier", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + conn.WriteData(0, true, []byte("test")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // If a DATA frame is received whose stream is not in "open" or + // "half-closed (local)" state, the recipient MUST respond with + // a stream error (Section 5.4.2) of type STREAM_CLOSED. + // + // Note: This test case is duplicated with 5.1. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a DATA frame on the stream that is not in \"open\" or \"half-closed (local)\" state", + Requirement: "The endpoint MUST respond with a stream error of type STREAM_CLOSED.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + + conn.WriteHeaders(hp) + conn.WriteData(req.StreamID, true, []byte("test")) + + return spec.VerifyStreamError(conn, http2.ErrCodeStreamClosed) + }, + }) + + // If the length of the padding is the length of the frame payload + // or greater, the recipient MUST treat this as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a DATA frame with invalid pad length", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + headers = append(headers, spec.HeaderField("content-length", "4")) + + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + + conn.WriteHeaders(hp) + + // DATA frame: + // frame length: 5, pad length: 6 + payload := []byte("\x06\x54\x65\x73\x74") + var flags http2.Flags + flags |= http2.FlagHeadersEndStream + flags |= http2.FlagHeadersPadded + conn.WriteRawFrame(http2.FrameData, flags, req.StreamID, payload) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + return tg +} diff --git a/client/6_2_headers.go b/client/6_2_headers.go new file mode 100644 index 0000000..be43889 --- /dev/null +++ b/client/6_2_headers.go @@ -0,0 +1,118 @@ +package client + +import ( + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func Headers() *spec.ClientTestGroup { + tg := NewTestGroup("6.2", "HEADERS") + + // END_HEADERS (0x4): + // A HEADERS frame without the END_HEADERS flag set MUST be + // followed by a CONTINUATION frame for the same stream. + // A receiver MUST treat the receipt of any other type of frame + // or a frame on a different stream as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + // + // Note: This test case is duplicated with 4.3. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a HEADERS frame without the END_HEADERS flag, and a PRIORITY frame", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: false, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + pp := http2.PriorityParam{ + StreamDep: 0, + Exclusive: false, + Weight: 255, + } + conn.WritePriority(req.StreamID, pp) + + dummyHeaders := spec.DummyRespHeaders(c, 1) + conn.WriteContinuation(req.StreamID, true, conn.EncodeHeaders(dummyHeaders)) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // HEADERS frames MUST be associated with a stream. If a HEADERS + // frame is received whose stream identifier field is 0x0, the + // recipient MUST respond with a connection error (Section 5.4.1) + // of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a HEADERS frame with 0x0 stream identifier", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + + hp := http2.HeadersFrameParam{ + StreamID: 0, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + + conn.WriteHeaders(hp) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // The HEADERS frame can include padding. Padding fields and flags + // are identical to those defined for DATA frames (Section 6.1). + // Padding that exceeds the size remaining for the header block + // fragment MUST be treated as a PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a HEADERS frame with invalid pad length", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + + // HEADERS frame: + // frame length: 16, pad length: 17 + var flags http2.Flags + flags |= http2.FlagHeadersPadded + payload := append([]byte("\x11"), conn.EncodeHeaders(headers)...) + conn.WriteRawFrame(http2.FrameHeaders, flags, req.StreamID, payload) + + return spec.VerifyStreamError(conn, http2.ErrCodeProtocol) + }, + }) + + return tg +} diff --git a/client/6_3_priority.go b/client/6_3_priority.go new file mode 100644 index 0000000..416de70 --- /dev/null +++ b/client/6_3_priority.go @@ -0,0 +1,73 @@ +package client + +import ( + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func Priority() *spec.ClientTestGroup { + tg := NewTestGroup("6.3", "PRIORITY") + + // The PRIORITY frame always identifies a stream. If a PRIORITY + // frame is received with a stream identifier of 0x0, the recipient + // MUST respond with a connection error (Section 5.4.1) of type + // PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a PRIORITY frame with 0x0 stream identifier", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + pp := http2.PriorityParam{ + StreamDep: 0, + Exclusive: false, + Weight: 255, + } + conn.WritePriority(0, pp) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // A PRIORITY frame with a length other than 5 octets MUST be + // treated as a stream error (Section 5.4.2) of type + // FRAME_SIZE_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a PRIORITY frame with a length other than 5 octets", + Requirement: "The endpoint MUST respond with a stream error of type FRAME_SIZE_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + // PRIORITY frame: + // length: 4, flags: 0x0, stream_id: 0x01 + var flags http2.Flags + conn.WriteRawFrame(http2.FramePriority, flags, req.StreamID, []byte("\x80\x00\x00\x01")) + + return spec.VerifyStreamError(conn, http2.ErrCodeFrameSize) + }, + }) + + return tg +} diff --git a/client/6_4_rst_stream.go b/client/6_4_rst_stream.go new file mode 100644 index 0000000..301ad57 --- /dev/null +++ b/client/6_4_rst_stream.go @@ -0,0 +1,87 @@ +package client + +import ( + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func RSTStream() *spec.ClientTestGroup { + tg := NewTestGroup("6.4", "RST_STREAM") + + // RST_STREAM frames MUST be associated with a stream. If a + // RST_STREAM frame is received with a stream identifier of 0x0, + // the recipient MUST treat this as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a RST_STREAM frame with 0x0 stream identifier", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + conn.WriteRSTStream(0, http2.ErrCodeCancel) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // RST_STREAM frames MUST NOT be sent for a stream in the "idle" + // state. If a RST_STREAM frame identifying an idle stream is + // received, the recipient MUST treat this as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a RST_STREAM frame on a idle stream", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + conn.WriteRSTStream(2, http2.ErrCodeCancel) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // A RST_STREAM frame with a length other than 4 octets MUST be + // treated as a connection error (Section 5.4.1) of type + // FRAME_SIZE_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a RST_STREAM frame with a length other than 4 octets", + Requirement: "The endpoint MUST respond with a connection error of type FRAME_SIZE_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: true, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + // RST_STREAM frame: + // length: 3, flags: 0x0, stream_id: 0x01 + var flags http2.Flags + conn.WriteRawFrame(http2.FrameRSTStream, flags, req.StreamID, []byte("\x00\x00\x00")) + + return spec.VerifyStreamError(conn, http2.ErrCodeFrameSize) + }, + }) + + return tg +} diff --git a/client/6_5_2_defined_settings_parameters.go b/client/6_5_2_defined_settings_parameters.go new file mode 100644 index 0000000..0bb9707 --- /dev/null +++ b/client/6_5_2_defined_settings_parameters.go @@ -0,0 +1,132 @@ +package client + +import ( + "errors" + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func DefinedSETTINGSParameters() *spec.ClientTestGroup { + tg := NewTestGroup("6.5.2", "Defined SETTINGS Parameters") + + // SETTINGS_INITIAL_WINDOW_SIZE (0x4): + // Values above the maximum flow-control window size of 2^31-1 + // MUST be treated as a connection error (Section 5.4.1) of + // type FLOW_CONTROL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "SETTINGS_INITIAL_WINDOW_SIZE (0x4): Sends the value above the maximum flow control window size", + Requirement: "The endpoint MUST treat this as a connection error of type FLOW_CONTROL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + _, err := conn.ReadClientPreface() + if err != nil { + return err + } + + _, ok := conn.WaitEventByType(spec.EventSettingsFrame) + if !ok { + return errors.New("First frame from client must be SETTINGS") + } + + setting := http2.Setting{ + ID: http2.SettingInitialWindowSize, + Val: 2147483648, + } + conn.WriteSettings(setting) + + return spec.VerifyConnectionError(conn, http2.ErrCodeFlowControl) + }, + }) + + // SETTINGS_MAX_FRAME_SIZE (0x5): + // The initial value is 2^14 (16,384) octets. The value advertised + // by an endpoint MUST be between this initial value and the + // maximum allowed frame size (2^24-1 or 16,777,215 octets), + // inclusive. Values outside this range MUST be treated as a + // connection error (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "SETTINGS_MAX_FRAME_SIZE (0x5): Sends the value below the initial value", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + _, err := conn.ReadClientPreface() + if err != nil { + return err + } + + _, ok := conn.WaitEventByType(spec.EventSettingsFrame) + if !ok { + return errors.New("First frame from client must be SETTINGS") + } + + setting := http2.Setting{ + ID: http2.SettingMaxFrameSize, + Val: 16383, + } + conn.WriteSettings(setting) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // SETTINGS_MAX_FRAME_SIZE (0x5): + // The initial value is 2^14 (16,384) octets. The value advertised + // by an endpoint MUST be between this initial value and the + // maximum allowed frame size (2^24-1 or 16,777,215 octets), + // inclusive. Values outside this range MUST be treated as a + // connection error (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "SETTINGS_MAX_FRAME_SIZE (0x5): Sends the value above the maximum allowed frame size", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + _, err := conn.ReadClientPreface() + if err != nil { + return err + } + + _, ok := conn.WaitEventByType(spec.EventSettingsFrame) + if !ok { + return errors.New("First frame from client must be SETTINGS") + } + + setting := http2.Setting{ + ID: http2.SettingMaxFrameSize, + Val: 16777216, + } + conn.WriteSettings(setting) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // An endpoint that receives a SETTINGS frame with any unknown + // or unsupported identifier MUST ignore that setting. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a SETTINGS frame with unknown identifier", + Requirement: "The endpoint MUST ignore that setting.", + Run: func(c *config.Config, conn *spec.Conn) error { + _, err := conn.ReadClientPreface() + if err != nil { + return err + } + + _, ok := conn.WaitEventByType(spec.EventSettingsFrame) + if !ok { + return errors.New("First frame from client must be SETTINGS") + } + + setting := http2.Setting{ + ID: 0xFF, + Val: 1, + } + conn.WriteSettings(setting) + + data := [8]byte{} + conn.WritePing(false, data) + + return spec.VerifyPingFrameWithAck(conn, data) + }, + }) + + return tg +} diff --git a/client/6_5_3_settings_synchronization.go b/client/6_5_3_settings_synchronization.go new file mode 100644 index 0000000..a82beff --- /dev/null +++ b/client/6_5_3_settings_synchronization.go @@ -0,0 +1,41 @@ +package client + +import ( + "errors" + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func SettingsSynchronization() *spec.ClientTestGroup { + tg := NewTestGroup("6.5.3", "Settings Synchronization") + + // Once all values have been processed, the recipient MUST + // immediately emit a SETTINGS frame with the ACK flag set. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a SETTINGS frame without ACK flag", + Requirement: "The endpoint MUST immediately emit a SETTINGS frame with the ACK flag set.", + Run: func(c *config.Config, conn *spec.Conn) error { + _, err := conn.ReadClientPreface() + if err != nil { + return err + } + + _, ok := conn.WaitEventByType(spec.EventSettingsFrame) + if !ok { + return errors.New("First frame from client must be SETTINGS") + } + + setting := http2.Setting{ + ID: http2.SettingMaxConcurrentStreams, + Val: 100, + } + conn.WriteSettings(setting) + + return spec.VerifySettingsFrameWithAck(conn) + }, + }) + + return tg +} diff --git a/client/6_5_settings.go b/client/6_5_settings.go new file mode 100644 index 0000000..6261013 --- /dev/null +++ b/client/6_5_settings.go @@ -0,0 +1,111 @@ +package client + +import ( + "errors" + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func Settings() *spec.ClientTestGroup { + tg := NewTestGroup("6.5", "SETTINGS") + + // ACK (0x1): + // When set, bit 0 indicates that this frame acknowledges receipt + // and application of the peer's SETTINGS frame. When this bit is + // set, the payload of the SETTINGS frame MUST be empty. Receipt of + // a SETTINGS frame with the ACK flag set and a length field value + // other than 0 MUST be treated as a connection error (Section 5.4.1) + // of type FRAME_SIZE_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a SETTINGS frame with ACK flag and payload", + Requirement: "The endpoint MUST respond with a connection error of type FRAME_SIZE_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + _, err := conn.ReadClientPreface() + if err != nil { + return err + } + + _, ok := conn.WaitEventByType(spec.EventSettingsFrame) + if !ok { + return errors.New("First frame from client must be SETTINGS") + } + + // SETTINGS frame: + // length: 0, flags: 0x1, stream_id: 0x0 + conn.Send([]byte("\x00\x00\x01\x04\x01\x00\x00\x00\x00")) + conn.Send([]byte("\x00")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeFrameSize) + }, + }) + + // SETTINGS frames always apply to a connection, never a single + // stream. The stream identifier for a SETTINGS frame MUST be + // zero (0x0). If an endpoint receives a SETTINGS frame whose + // stream identifier field is anything other than 0x0, the + // endpoint MUST respond with a connection error (Section 5.4.1) + // of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a SETTINGS frame with a stream identifier other than 0x0", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + _, err := conn.ReadClientPreface() + if err != nil { + return err + } + + _, ok := conn.WaitEventByType(spec.EventSettingsFrame) + if !ok { + return errors.New("First frame from client must be SETTINGS") + } + + // SETTINGS frame: + // length: 6, flags: 0x0, stream_id: 0x1 + conn.Send([]byte("\x00\x00\x06\x04\x00\x00\x00\x00\x01")) + conn.Send([]byte("\x00\x03\x00\x00\x00\x64")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // The SETTINGS frame affects connection state. A badly formed or + // incomplete SETTINGS frame MUST be treated as a connection error + // (Section 5.4.1) of type PROTOCOL_ERROR. + // + // A SETTINGS frame with a length other than a multiple of 6 octets + // MUST be treated as a connection error (Section 5.4.1) of type + // FRAME_SIZE_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a SETTINGS frame with a length other than a multiple of 6 octets", + Requirement: "The endpoint MUST respond with a connection error of type FRAME_SIZE_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + _, err := conn.ReadClientPreface() + if err != nil { + return err + } + + _, ok := conn.WaitEventByType(spec.EventSettingsFrame) + if !ok { + return errors.New("First frame from client must be SETTINGS") + } + + // SETTINGS frame: + // length: 3, flags: 0x0, stream_id: 0x0 + conn.Send([]byte("\x00\x00\x03\x04\x00\x00\x00\x00\x00")) + conn.Send([]byte("\x00\x03\x00")) + + codes := []http2.ErrCode{ + http2.ErrCodeProtocol, + http2.ErrCodeFrameSize, + } + return spec.VerifyStreamError(conn, codes...) + }, + }) + + tg.AddTestGroup(DefinedSETTINGSParameters()) + tg.AddTestGroup(SettingsSynchronization()) + + return tg +} diff --git a/client/6_7_ping.go b/client/6_7_ping.go new file mode 100644 index 0000000..ea83ca3 --- /dev/null +++ b/client/6_7_ping.go @@ -0,0 +1,98 @@ +package client + +import ( + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func Ping() *spec.ClientTestGroup { + tg := NewTestGroup("6.7", "PING") + + // Receivers of a PING frame that does not include an ACK flag MUST + // send a PING frame with the ACK flag set in response, with an + // identical payload. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a PING frame", + Requirement: "The endpoint MUST sends a PING frame with ACK, with an identical payload.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + data := [8]byte{'h', '2', 's', 'p', 'e', 'c'} + conn.WritePing(false, data) + + return spec.VerifyPingFrameWithAck(conn, data) + }, + }) + + // ACK (0x1): + // When set, bit 0 indicates that this PING frame is a PING + // response. An endpoint MUST set this flag in PING responses. + // An endpoint MUST NOT respond to PING frames containing this + // flag. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a PING frame with ACK", + Requirement: "The endpoint MUST NOT respond to PING frames with ACK.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + unexpectedData := [8]byte{'i', 'n', 'v', 'a', 'l', 'i', 'd'} + expectedData := [8]byte{'h', '2', 's', 'p', 'e', 'c'} + conn.WritePing(true, unexpectedData) + conn.WritePing(false, expectedData) + + return spec.VerifyPingFrameWithAck(conn, expectedData) + }, + }) + + // If a PING frame is received with a stream identifier field value + // other than 0x0, the recipient MUST respond with a connection + // error (Section 5.4.1) of type PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a PING frame with a stream identifier field value other than 0x0", + Requirement: "The endpoint MUST respond with a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // PING frame: + // length: 8, flags: 0x0, stream_id: 1 + conn.Send([]byte("\x00\x00\x08\x06\x00\x00\x00\x00\x01")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // Receipt of a PING frame with a length field value other than 8 + // MUST be treated as a connection error (Section 5.4.1) of type + // FRAME_SIZE_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a PING frame with a length field value other than 8", + Requirement: "The endpoint MUST treat this as a connection error of type FRAME_SIZE_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // PING frame: + // length: 8, flags: 0x0, stream_id: 1 + conn.Send([]byte("\x00\x00\x06\x06\x00\x00\x00\x00\x00")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeFrameSize) + }, + }) + + return tg +} diff --git a/client/6_8_goaway.go b/client/6_8_goaway.go new file mode 100644 index 0000000..eba4e75 --- /dev/null +++ b/client/6_8_goaway.go @@ -0,0 +1,35 @@ +package client + +import ( + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func GoAway() *spec.ClientTestGroup { + tg := NewTestGroup("6.8", "GOAWAY") + + // An endpoint MUST treat a GOAWAY frame with a stream identifier + // other than 0x0 as a connection error (Section 5.4.1) of type + // PROTOCOL_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a GOAWAY frame with a stream identifier other than 0x0", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // GOAWAY frame: + // length: 8, flags: 0x0, stream_id: 1 + conn.Send([]byte("\x00\x00\x08\x07\x00\x00\x00\x00\x01")) + conn.Send([]byte("\x00\x00\x00\x00\x00\x00\x00\x00")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + return tg +} diff --git a/client/6_9_1_the_flow_control_window.go b/client/6_9_1_the_flow_control_window.go new file mode 100644 index 0000000..8d43c60 --- /dev/null +++ b/client/6_9_1_the_flow_control_window.go @@ -0,0 +1,116 @@ +package client + +import ( + "fmt" + + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func TheFlowControlWindow() *spec.ClientTestGroup { + tg := NewTestGroup("6.9.1", "The Flow-Control Window") + + // A sender MUST NOT allow a flow-control window to exceed 2^31-1 + // octets. If a sender receives a WINDOW_UPDATE that causes a + // flow-control window to exceed this maximum, it MUST terminate + // either the stream or the connection, as appropriate. + // For streams, the sender sends a RST_STREAM with an error code + // of FLOW_CONTROL_ERROR; for the connection, a GOAWAY frame with + // an error code of FLOW_CONTROL_ERROR is sent. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends multiple WINDOW_UPDATE frames increasing the flow control window to above 2^31-1", + Requirement: "The endpoint MUST sends a GOAWAY frame with a FLOW_CONTROL_ERROR code.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + conn.WriteWindowUpdate(0, 2147483647) + conn.WriteWindowUpdate(0, 2147483647) + + actual, passed := conn.WaitEventByType(spec.EventGoAwayFrame) + switch event := actual.(type) { + case spec.GoAwayFrameEvent: + passed = (event.ErrCode == http2.ErrCodeFlowControl) + default: + passed = false + } + + if !passed { + expected := []string{ + fmt.Sprintf("GOAWAY Frame (Error Code: %s)", http2.ErrCodeFlowControl), + } + + return &spec.TestError{ + Expected: expected, + Actual: actual.String(), + } + } + + return nil + }, + }) + + // A sender MUST NOT allow a flow-control window to exceed 2^31-1 + // octets. If a sender receives a WINDOW_UPDATE that causes a + // flow-control window to exceed this maximum, it MUST terminate + // either the stream or the connection, as appropriate. + // For streams, the sender sends a RST_STREAM with an error code + // of FLOW_CONTROL_ERROR; for the connection, a GOAWAY frame with + // an error code of FLOW_CONTROL_ERROR is sent. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends multiple WINDOW_UPDATE frames increasing the flow control window to above 2^31-1 on a stream", + Requirement: "The endpoint MUST sends a RST_STREAM frame with a FLOW_CONTROL_ERROR code.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + conn.WriteWindowUpdate(req.StreamID, 2147483647) + conn.WriteWindowUpdate(req.StreamID, 2147483647) + + actual, passed := conn.WaitEventByType(spec.EventRSTStreamFrame) + switch event := actual.(type) { + case spec.RSTStreamFrameEvent: + if event.Header().StreamID == req.StreamID { + passed = (event.ErrCode == http2.ErrCodeFlowControl) + } + default: + passed = false + } + + if !passed { + expected := []string{ + fmt.Sprintf("RST_STREAM Frame (Error Code: %s)", http2.ErrCodeFlowControl), + } + + return &spec.TestError{ + Expected: expected, + Actual: actual.String(), + } + } + + return nil + }, + }) + + return tg +} diff --git a/client/6_9_window_update.go b/client/6_9_window_update.go new file mode 100644 index 0000000..fc942a5 --- /dev/null +++ b/client/6_9_window_update.go @@ -0,0 +1,91 @@ +package client + +import ( + "golang.org/x/net/http2" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/spec" +) + +func WindowUpdate() *spec.ClientTestGroup { + tg := NewTestGroup("6.9", "WINDOW_UPDATE") + + // A receiver MUST treat the receipt of a WINDOW_UPDATE frame with + // an flow-control window increment of 0 as a stream error + // (Section 5.4.2) of type PROTOCOL_ERROR; errors on the connection + // flow-control window MUST be treated as a connection error + // (Section 5.4.1). + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a WINDOW_UPDATE frame with a flow control window increment of 0", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + conn.WriteWindowUpdate(0, 0) + + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + }, + }) + + // A receiver MUST treat the receipt of a WINDOW_UPDATE frame with + // an flow-control window increment of 0 as a stream error + // (Section 5.4.2) of type PROTOCOL_ERROR; errors on the connection + // flow-control window MUST be treated as a connection error + // (Section 5.4.1). + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a WINDOW_UPDATE frame with a flow control window increment of 0 on a stream", + Requirement: "The endpoint MUST treat this as a connection error of type PROTOCOL_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + req, err := conn.ReadRequest() + if err != nil { + return err + } + + headers := spec.CommonRespHeaders(c) + hp := http2.HeadersFrameParam{ + StreamID: req.StreamID, + EndStream: false, + EndHeaders: true, + BlockFragment: conn.EncodeHeaders(headers), + } + conn.WriteHeaders(hp) + + conn.WriteWindowUpdate(req.StreamID, 0) + + return spec.VerifyStreamError(conn, http2.ErrCodeProtocol) + }, + }) + + // A WINDOW_UPDATE frame with a length other than 4 octets MUST + // be treated as a connection error (Section 5.4.1) of type + // FRAME_SIZE_ERROR. + tg.AddTestCase(&spec.ClientTestCase{ + Desc: "Sends a WINDOW_UPDATE frame with a length other than 4 octets", + Requirement: "The endpoint MUST treat this as a connection error of type FRAME_SIZE_ERROR.", + Run: func(c *config.Config, conn *spec.Conn) error { + err := conn.Handshake() + if err != nil { + return err + } + + // WINDOW_UPDATE frame: + // length: 3, flags: 0x0, stream_id: 0 + conn.Send([]byte("\x00\x00\x03\x08\x00\x00\x00\x00\x00")) + conn.Send([]byte("\x00\x00\x01")) + + return spec.VerifyConnectionError(conn, http2.ErrCodeFrameSize) + }, + }) + + tg.AddTestGroup(TheFlowControlWindow()) + + return tg +} diff --git a/client/6_frame_definitions.go b/client/6_frame_definitions.go new file mode 100644 index 0000000..885c548 --- /dev/null +++ b/client/6_frame_definitions.go @@ -0,0 +1,19 @@ +package client + +import "github.com/summerwind/h2spec/spec" + +func FrameDefinitions() *spec.ClientTestGroup { + tg := NewTestGroup("6", "Frame Definitions") + + tg.AddTestGroup(Data()) + tg.AddTestGroup(Headers()) + tg.AddTestGroup(Priority()) + tg.AddTestGroup(RSTStream()) + tg.AddTestGroup(Settings()) + tg.AddTestGroup(Ping()) + tg.AddTestGroup(GoAway()) + tg.AddTestGroup(WindowUpdate()) + tg.AddTestGroup(Continuation()) + + return tg +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..fd41226 --- /dev/null +++ b/client/client.go @@ -0,0 +1,27 @@ +package client + +import "github.com/summerwind/h2spec/spec" + +var key = "client" + +func NewTestGroup(section string, name string) *spec.ClientTestGroup { + return &spec.ClientTestGroup{ + Key: key, + Section: section, + Name: name, + } +} + +func Spec() *spec.ClientTestGroup { + tg := &spec.ClientTestGroup{ + Key: key, + Name: "Generic tests for HTTP/2 client", + } + + tg.AddTestGroup(StartingHTTP2()) + tg.AddTestGroup(HTTPFrames()) + tg.AddTestGroup(StreamsAndMultiplexing()) + tg.AddTestGroup(FrameDefinitions()) + + return tg +} diff --git a/cmd/h2spec/h2spec.go b/cmd/h2spec/h2spec.go index 831369e..9fa01ba 100644 --- a/cmd/h2spec/h2spec.go +++ b/cmd/h2spec/h2spec.go @@ -29,12 +29,14 @@ func main() { flags := cmd.Flags() flags.StringP("host", "h", "127.0.0.1", "Target host") flags.IntP("port", "p", 0, "Target port") + flags.StringP("path", "P", "/", "Target path") flags.IntP("timeout", "o", 2, "Time seconds to test timeout") flags.Int("max-header-length", 4000, "Maximum length of HTTP header") flags.StringP("junit-report", "j", "", "Path for JUnit test report") flags.BoolP("strict", "S", false, "Run all test cases including strict test cases") flags.Bool("dryrun", false, "Display only the title of test cases") flags.BoolP("tls", "t", false, "Connect over TLS") + flags.StringP("ciphers", "c", "", "List of colon-separated TLS cipher names") flags.BoolP("insecure", "k", false, "Don't verify server's certificate") flags.BoolP("verbose", "v", false, "Output verbose log") flags.Bool("version", false, "Display version information and exit") @@ -70,6 +72,11 @@ func run(cmd *cobra.Command, args []string) error { return err } + path, err := flags.GetString("path") + if err != nil { + return err + } + timeout, err := flags.GetInt("timeout") if err != nil { return err @@ -100,6 +107,11 @@ func run(cmd *cobra.Command, args []string) error { return err } + ciphers, err := flags.GetString("ciphers") + if err != nil { + return err + } + insecure, err := flags.GetBool("insecure") if err != nil { return err @@ -121,18 +133,25 @@ func run(cmd *cobra.Command, args []string) error { c := &config.Config{ Host: host, Port: port, + Path: path, Timeout: time.Duration(timeout) * time.Second, MaxHeaderLen: maxHeaderLen, JUnitReport: junitReport, Strict: strict, DryRun: dryRun, TLS: tls, + Ciphers: ciphers, Insecure: insecure, Verbose: verbose, Sections: args, } - return h2spec.Run(c) + success, err := h2spec.Run(c) + if !success { + os.Exit(1) + } + + return err } func version() { diff --git a/cmd/h2specd/h2specd.go b/cmd/h2specd/h2specd.go new file mode 100644 index 0000000..6acfaf7 --- /dev/null +++ b/cmd/h2specd/h2specd.go @@ -0,0 +1,163 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + "github.com/summerwind/h2spec" + "github.com/summerwind/h2spec/config" +) + +var ( + VERSION string = "2.0.0" + COMMIT string = "(Unknown)" +) + +func main() { + var cmd = &cobra.Command{ + Use: "h2specd [spec...]", + Short: "Conformance testing tool for HTTP/2 client implementation", + Long: "Conformance testing tool for HTTP/2 client implementation.", + RunE: run, + } + + cmd.SilenceUsage = true + cmd.SilenceErrors = true + + flags := cmd.Flags() + flags.StringP("host", "h", "127.0.0.1", "Target host") + flags.IntP("port", "p", 0, "Target port") + flags.IntP("timeout", "o", 2, "Time seconds to test timeout") + flags.Int("max-header-length", 4000, "Maximum length of HTTP header") + flags.StringP("junit-report", "j", "", "Path for JUnit test report") + flags.BoolP("strict", "S", false, "Run all test cases including strict test cases") + flags.Bool("dryrun", false, "Display only the title of test cases") + flags.BoolP("tls", "t", false, "Connect over TLS") + flags.StringP("cert-file", "c", "server.crt", "Servr certificate file") + flags.StringP("cert-key-file", "k", "server.key", "Servr certificate key file") + + flags.IntP("from-port", "f", 30000, "The port starting from for client test cases") + flags.StringP("exec", "e", "", "Binary or command for http2 client") + + flags.BoolP("verbose", "v", false, "Output verbose log") + flags.Bool("version", false, "Display version information and exit") + flags.Bool("help", false, "Display this help and exit") + + err := cmd.Execute() + if err != nil { + fmt.Printf("Error: %s", err) + os.Exit(1) + } +} + +func run(cmd *cobra.Command, args []string) error { + flags := cmd.Flags() + + v, err := flags.GetBool("version") + if err != nil { + return err + } + + if v { + version() + return nil + } + + host, err := flags.GetString("host") + if err != nil { + return err + } + + port, err := flags.GetInt("port") + if err != nil { + return err + } + + timeout, err := flags.GetInt("timeout") + if err != nil { + return err + } + + maxHeaderLen, err := flags.GetInt("max-header-length") + if err != nil { + return err + } + + junitReport, err := flags.GetString("junit-report") + if err != nil { + return err + } + + strict, err := flags.GetBool("strict") + if err != nil { + return err + } + + dryRun, err := flags.GetBool("dryrun") + if err != nil { + return err + } + + tls, err := flags.GetBool("tls") + if err != nil { + return err + } + + certFile, err := flags.GetString("cert-file") + if err != nil { + return err + } + + certKeyFile, err := flags.GetString("cert-key-file") + if err != nil { + return err + } + + fromPort, err := flags.GetInt("from-port") + if err != nil { + return err + } + + exec, err := flags.GetString("exec") + if err != nil { + return err + } + + verbose, err := flags.GetBool("verbose") + if err != nil { + return err + } + + if port == 0 { + if tls { + port = 443 + } else { + port = 80 + } + } + + c := &config.Config{ + Host: host, + Port: port, + Timeout: time.Duration(timeout) * time.Second, + MaxHeaderLen: maxHeaderLen, + JUnitReport: junitReport, + Strict: strict, + DryRun: dryRun, + TLS: tls, + CertFile: certFile, + CertKeyFile: certKeyFile, + Verbose: verbose, + Sections: args, + FromPort: fromPort, + Exec: exec, + } + + return h2spec.RunClientSpec(c) +} + +func version() { + fmt.Printf("Version: %s (%s)\n", VERSION, COMMIT) +} diff --git a/config/config.go b/config/config.go index 4a77e36..b129156 100644 --- a/config/config.go +++ b/config/config.go @@ -17,38 +17,124 @@ const ( type Config struct { Host string Port int + Path string Timeout time.Duration MaxHeaderLen int JUnitReport string Strict bool DryRun bool TLS bool + Ciphers string Insecure bool Verbose bool Sections []string targetMap map[string]bool + CertFile string + CertKeyFile string + Exec string + FromPort int } -// Addr returns the string concatinated with hostname and port number. +// Addr returns the string concatenated with hostname and port number. func (c *Config) Addr() string { return fmt.Sprintf("%s:%d", c.Host, c.Port) } +func (c *Config) Scheme() string { + if c.TLS { + return "https" + } else { + return "http" + } +} + +func CiphersuiteByName(name string) uint16 { + switch name { + case "TLS_RSA_WITH_RC4_128_SHA": + return tls.TLS_RSA_WITH_RC4_128_SHA + case "TLS_RSA_WITH_3DES_EDE_CBC_SHA": + return tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA + case "TLS_RSA_WITH_AES_128_CBC_SHA": + return tls.TLS_RSA_WITH_AES_128_CBC_SHA + case "TLS_RSA_WITH_AES_128_CBC_SHA256": + return tls.TLS_RSA_WITH_AES_128_CBC_SHA256 + case "TLS_RSA_WITH_AES_256_GCM_SHA384": + return tls.TLS_RSA_WITH_AES_256_GCM_SHA384 + case "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": + return tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA + case "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": + return tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + case "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": + return tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + case "TLS_ECDHE_RSA_WITH_RC4_128_SHA": + return tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA + case "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": + return tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA + case "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": + return tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + case "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": + return tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + case "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": + return tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 + case "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": + return tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 + case "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": + return tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + case "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": + return tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + case "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": + return tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + case "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": + return tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + case "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": + return tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 + case "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": + return tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 + } + return 0 +} + +// Decode the user-defined list of allowed cipher suites from string +// representation. +// TODO: now Golang doesn't provide a way to convert ciphersuite name to ID, +// thus manual implementation is required. +func (c *Config) GetCiphersuites() []uint16 { + var ids []uint16 + + for _, name := range strings.Split(c.Ciphers, ":") { + id := CiphersuiteByName(name) + if id != 0 { + ids = append(ids, id) + } + } + + return ids +} + // TLSConfig returns a tls.Config based on the configuration of h2spec. -func (c *Config) TLSConfig() *tls.Config { +func (c *Config) TLSConfig() (*tls.Config, error) { if !c.TLS { - return nil + return nil, nil } config := tls.Config{ InsecureSkipVerify: c.Insecure, + CipherSuites: c.GetCiphersuites(), } if config.NextProtos == nil { config.NextProtos = append(config.NextProtos, "h2", "h2-16") } - return &config + if c.CertFile != "" && c.CertKeyFile != "" { + cert, err := tls.LoadX509KeyPair(c.CertFile, c.CertKeyFile) + if err != nil { + return nil, err + } + config.Certificates = []tls.Certificate{cert} + } + + return &config, nil } // RunMode returns a run mode of specified the section number. @@ -135,3 +221,7 @@ func (c *Config) buildTargetMap() { c.targetMap[section] = true } } + +func (c *Config) IsBrowserMode() bool { + return c.Exec == "" +} diff --git a/generic/2_streams_and_multiplexing.go b/generic/2_streams_and_multiplexing.go index 0919079..73795fe 100644 --- a/generic/2_streams_and_multiplexing.go +++ b/generic/2_streams_and_multiplexing.go @@ -54,7 +54,10 @@ func StreamsAndMultiplexing() *spec.TestGroup { // Set INITIAL_WINDOW_SIZE to zero to prevent the peer from // closing the stream. - settings := http2.Setting{http2.SettingInitialWindowSize, 0} + settings := http2.Setting{ + ID: http2.SettingInitialWindowSize, + Val: 0, + } conn.WriteSettings(settings) headers := spec.CommonHeaders(c) @@ -90,7 +93,10 @@ func StreamsAndMultiplexing() *spec.TestGroup { // Set INITIAL_WINDOW_SIZE to zero to prevent the peer from // closing the stream. - settings := http2.Setting{http2.SettingInitialWindowSize, 0} + settings := http2.Setting{ + ID: http2.SettingInitialWindowSize, + Val: 0, + } conn.WriteSettings(settings) headers := spec.CommonHeaders(c) @@ -145,7 +151,7 @@ func StreamsAndMultiplexing() *spec.TestGroup { data := [8]byte{} conn.WritePing(false, data) - return spec.VerifyPingFrameWithAck(conn, data) + return spec.VerifyPingFrameOrConnectionClose(conn, data) }, }) diff --git a/generic/3_4_rst_stream.go b/generic/3_4_rst_stream.go index 03021ac..c701b47 100644 --- a/generic/3_4_rst_stream.go +++ b/generic/3_4_rst_stream.go @@ -39,7 +39,7 @@ func RSTStream() *spec.TestGroup { data := [8]byte{} conn.WritePing(false, data) - return spec.VerifyPingFrameWithAck(conn, data) + return spec.VerifyPingFrameOrConnectionClose(conn, data) }, }) diff --git a/generic/3_5_settings.go b/generic/3_5_settings.go index 515bfb2..acf6a95 100644 --- a/generic/3_5_settings.go +++ b/generic/3_5_settings.go @@ -26,12 +26,30 @@ func Settings() *spec.TestGroup { } settings := []http2.Setting{ - http2.Setting{http2.SettingHeaderTableSize, 4096}, - http2.Setting{http2.SettingEnablePush, 1}, - http2.Setting{http2.SettingMaxConcurrentStreams, 100}, - http2.Setting{http2.SettingInitialWindowSize, 65535}, - http2.Setting{http2.SettingMaxFrameSize, 16384}, - http2.Setting{http2.SettingMaxHeaderListSize, 100}, + http2.Setting{ + ID: http2.SettingHeaderTableSize, + Val: 4096, + }, + http2.Setting{ + ID: http2.SettingEnablePush, + Val: 1, + }, + http2.Setting{ + ID: http2.SettingMaxConcurrentStreams, + Val: 100, + }, + http2.Setting{ + ID: http2.SettingInitialWindowSize, + Val: 65535, + }, + http2.Setting{ + ID: http2.SettingMaxFrameSize, + Val: 16384, + }, + http2.Setting{ + ID: http2.SettingMaxHeaderListSize, + Val: 100, + }, } conn.WriteSettings(settings...) diff --git a/generic/3_8_goaway.go b/generic/3_8_goaway.go index 64c190f..d43f60c 100644 --- a/generic/3_8_goaway.go +++ b/generic/3_8_goaway.go @@ -27,7 +27,10 @@ func GoAway() *spec.TestGroup { conn.WriteGoAway(0, http2.ErrCodeNo, []byte("h2spec")) - return spec.VerifyConnectionClose(conn) + data := [8]byte{'h', '2', 's', 'p', 'e', 'c'} + conn.WritePing(false, data) + + return spec.VerifyPingFrameOrConnectionClose(conn, data) }, }) diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index 0c08aff..0000000 --- a/glide.yaml +++ /dev/null @@ -1,15 +0,0 @@ -package: github.com/summerwind/h2spec -import: -- package: golang.org/x/net - varsion: 55a3084c9119aeb9ba2437d595b0a7e9cb635da9 - subpackages: - - http2 - - http2/hpack -- package: github.com/spf13/cobra - version: dc208f4211e7f6df7ec8cb62640f57d3e154910d -- package: github.com/spf13/pflag - version: a232f6d9f87afaaa08bafaff5da685f974b83313 -- package: github.com/spf13/viper - version: 5ed0fc31f7f453625df314d8e66b9791e8d13003 -- package: github.com/fatih/color - version: bf82308e8c8546dc2b945157173eb8a959ae9505 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2410c5d --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/summerwind/h2spec + +go 1.18 + +require ( + github.com/fatih/color v0.0.0-20161025120501-bf82308e8c85 + github.com/spf13/cobra v0.0.0-20170118185516-dc208f4211e7 + golang.org/x/net v0.0.0-20161104230106-55a3084c9119 +) + +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/mattn/go-colorable v0.1.0 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/spf13/pflag v1.0.3 // indirect + golang.org/x/sys v0.0.0-20190209173611-3b5209105503 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..af08b5c --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/fatih/color v0.0.0-20161025120501-bf82308e8c85 h1:g7ijd5QIEMWwZNVp/T/6kQ8RSh8rN+YNhghMcrET3qY= +github.com/fatih/color v0.0.0-20161025120501-bf82308e8c85/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o= +github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/spf13/cobra v0.0.0-20170118185516-dc208f4211e7 h1:/xfSxzUJXZPhOsBj5aCUvA3mOIc7ILcvvJpmvzhQk7w= +github.com/spf13/cobra v0.0.0-20170118185516-dc208f4211e7/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +golang.org/x/net v0.0.0-20161104230106-55a3084c9119 h1:T/FVHYSpR0pXqxZ6zNrRnmk4iHorxWyidE9VCMGJ5rQ= +golang.org/x/net v0.0.0-20161104230106-55a3084c9119/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503 h1:5SvYFrOM3W8Mexn9/oA44Ji7vhXAZQ9hiP+1Q/DMrWg= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/h2spec.go b/h2spec.go index cd4149e..f8c08d9 100644 --- a/h2spec.go +++ b/h2spec.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/summerwind/h2spec/client" "github.com/summerwind/h2spec/config" "github.com/summerwind/h2spec/generic" "github.com/summerwind/h2spec/hpack" @@ -13,9 +14,9 @@ import ( "github.com/summerwind/h2spec/spec" ) -func Run(c *config.Config) error { +func Run(c *config.Config) (bool, error) { total := 0 - failed := false + success := true specs := []*spec.TestGroup{ generic.Spec(), @@ -28,7 +29,7 @@ func Run(c *config.Config) error { s.Test(c) if s.FailedCount > 0 { - failed = true + success = false } total += s.FailedCount @@ -39,16 +40,16 @@ func Run(c *config.Config) error { d := end.Sub(start) if c.DryRun { - return nil + return true, nil } if total == 0 { log.SetIndentLevel(0) log.Println("No matched tests found.") - return nil + return true, nil } - if failed { + if !success { log.SetIndentLevel(0) reporter.FailedTests(specs) } @@ -60,9 +61,44 @@ func Run(c *config.Config) error { if c.JUnitReport != "" { err := reporter.JUnitReport(specs, c.JUnitReport) if err != nil { - return err + return false, err } } + return success, nil +} + +func RunClientSpec(c *config.Config) error { + s := client.Spec() + + server, err := spec.Listen(c, s) + if err != nil { + return err + } + + if !c.IsBrowserMode() { + start := time.Now() + s.Test(c) + end := time.Now() + d := end.Sub(start) + + if s.FailedCount > 0 { + log.SetIndentLevel(0) + reporter.PrintFailedClientTests(s) + } + + log.SetIndentLevel(0) + log.Println(fmt.Sprintf("Finished in %.4f seconds", d.Seconds())) + reporter.PrintSummaryForClient(s) + } else { + // Block running + log.Println("--exec is not defined, enable BROWSER mode") + + reportServer := reporter.NewWebReportServer(c, s) + log.Println(reportServer.RunForever()) + } + + defer server.Close() + return nil } diff --git a/hpack/2_3_3_index_address_space.go b/hpack/2_3_3_index_address_space.go index 43b5b4c..77be152 100644 --- a/hpack/2_3_3_index_address_space.go +++ b/hpack/2_3_3_index_address_space.go @@ -12,7 +12,7 @@ func IndexAddressSpace() *spec.TestGroup { // Indices strictly greater than the sum of the lengths of both // tables MUST be treated as a decoding error. tg.AddTestCase(&spec.TestCase{ - Desc: "Sends a header field representation with invalid index", + Desc: "Sends a indexed header field representation with invalid index", Requirement: "The endpoint MUST treat this as a decoding error.", Run: func(c *config.Config, conn *spec.Conn) error { var streamID uint32 = 1 @@ -41,5 +41,37 @@ func IndexAddressSpace() *spec.TestGroup { }, }) + // Indices strictly greater than the sum of the lengths of both + // tables MUST be treated as a decoding error. + tg.AddTestCase(&spec.TestCase{ + Desc: "Sends a literal header field representation with invalid index", + Requirement: "The endpoint MUST treat this as a decoding error.", + Run: func(c *config.Config, conn *spec.Conn) error { + var streamID uint32 = 1 + + err := conn.Handshake() + if err != nil { + return err + } + + // Literal Header Field with Incremental Indexing (index=70 & value=empty) + indexedRep := []byte("\x7F\x07\x00") + + headers := spec.CommonHeaders(c) + blockFragment := conn.EncodeHeaders(headers) + blockFragment = append(blockFragment, indexedRep...) + + hp := http2.HeadersFrameParam{ + StreamID: streamID, + EndStream: true, + EndHeaders: true, + BlockFragment: blockFragment, + } + conn.WriteHeaders(hp) + + return spec.VerifyConnectionError(conn, http2.ErrCodeCompression) + }, + }) + return tg } diff --git a/hpack/5_2_string_literal_representation.go b/hpack/5_2_string_literal_representation.go index bdb18db..e3a6f5c 100644 --- a/hpack/5_2_string_literal_representation.go +++ b/hpack/5_2_string_literal_representation.go @@ -121,9 +121,15 @@ func StringLiteralRepresentation() *spec.TestGroup { return err } - // Literal Header Field without Indexing - New Name (x-test: test) + // Literal Header Field without Indexing - New Name (x-test: tes{EOS}t) // This contains a EOS symbol in the middle: - rep := []byte("\x00\x85\xf2\xb2\x4a\x87\xff\xff\xff\xfd\x25\x42\x7f") + // + // Header: 0000 0000 + // Name Length: 1000 0101 + // Name String: 1111 0010 1011 0010 0100 1010 1000 0100 1111 1111 + // Value Length: 1000 0111 + // Value String: 0100 1001 0101 0001 1111 1111 1111 1111 1111 1111 1111 1010 0111 1111 + rep := []byte("\x00\x85\xf2\xb2\x4a\x84\xff\x87\x49\x51\xff\xff\xff\xfa\x7f") headers := spec.CommonHeaders(c) blockFragment := conn.EncodeHeaders(headers) diff --git a/http2/3_5_http2_connection_preface.go b/http2/3_5_http2_connection_preface.go index f55bfa9..a3f7978 100644 --- a/http2/3_5_http2_connection_preface.go +++ b/http2/3_5_http2_connection_preface.go @@ -60,9 +60,7 @@ func HTTP2ConnectionPreface() *spec.TestGroup { return err } - // Connection has not negotiated, so we verify connection close - // instead of connection error. - return spec.VerifyConnectionClose(conn) + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) }, }) diff --git a/http2/5_1_2_stream_concurrency.go b/http2/5_1_2_stream_concurrency.go index 28821bf..e36c9ce 100644 --- a/http2/5_1_2_stream_concurrency.go +++ b/http2/5_1_2_stream_concurrency.go @@ -33,7 +33,10 @@ func StreamConcurrency() *spec.TestGroup { // Set INITIAL_WINDOW_SIZE to zero to prevent the peer from // closing the stream. - settings := http2.Setting{http2.SettingInitialWindowSize, 0} + settings := http2.Setting{ + ID: http2.SettingInitialWindowSize, + Val: 0, + } conn.WriteSettings(settings) headers := spec.CommonHeaders(c) diff --git a/http2/5_1_stream_states.go b/http2/5_1_stream_states.go index f0eb93c..a6c48bb 100644 --- a/http2/5_1_stream_states.go +++ b/http2/5_1_stream_states.go @@ -24,7 +24,13 @@ func StreamStates() *spec.TestGroup { conn.WriteData(1, true, []byte("test")) - return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) + // This is an unclear part of the specification. Section 6.1 says + // to treat this as a stream error. + // -------- + // If a DATA frame is received whose stream is not in "open" or + // "half-closed (local)" state, the recipient MUST respond with + // a stream error (Section 5.4.2) of type STREAM_CLOSED. + return spec.VerifyStreamError(conn, http2.ErrCodeProtocol, http2.ErrCodeStreamClosed) }, }) @@ -327,7 +333,7 @@ func StreamStates() *spec.TestGroup { conn.WriteData(streamID, true, []byte("test")) - return spec.VerifyConnectionError(conn, http2.ErrCodeStreamClosed) + return spec.VerifyStreamError(conn, http2.ErrCodeStreamClosed) }, }) diff --git a/http2/5_3_1_stream_dependencies.go b/http2/5_3_1_stream_dependencies.go index f0cbe51..98d0761 100644 --- a/http2/5_3_1_stream_dependencies.go +++ b/http2/5_3_1_stream_dependencies.go @@ -12,7 +12,7 @@ func StreamDependencies() *spec.TestGroup { // A stream cannot depend on itself. An endpoint MUST treat this // as a stream error (Section 5.4.2) of type PROTOCOL_ERROR. tg.AddTestCase(&spec.TestCase{ - Desc: "Sends HEADERS frame that depend on itself", + Desc: "Sends HEADERS frame that depends on itself", Requirement: "The endpoint MUST treat this as a stream error of type PROTOCOL_ERROR.", Run: func(c *config.Config, conn *spec.Conn) error { var streamID uint32 = 1 diff --git a/http2/6_1_data.go b/http2/6_1_data.go index 491e3d3..4d98a17 100644 --- a/http2/6_1_data.go +++ b/http2/6_1_data.go @@ -94,7 +94,7 @@ func Data() *spec.TestGroup { conn.Send([]byte("\x00\x00\x05\x00\x09\x00\x00\x00\x01")) conn.Send([]byte("\x06\x54\x65\x73\x74")) - return spec.VerifyStreamError(conn, http2.ErrCodeProtocol) + return spec.VerifyConnectionError(conn, http2.ErrCodeProtocol) }, }) diff --git a/http2/6_2_headers.go b/http2/6_2_headers.go index 93219ba..acafcf0 100644 --- a/http2/6_2_headers.go +++ b/http2/6_2_headers.go @@ -138,12 +138,16 @@ func Headers() *spec.TestGroup { } headers := spec.CommonHeaders(c) + blockFragment := conn.EncodeHeaders(headers) + + fh := []byte("\x00\x00\x00\x01\x0d\x00\x00\x00\x01") + fh[2] = byte(len(blockFragment) + 1) // HEADERS frame: // frame length: 16, pad length: 17 - conn.Send([]byte("\x00\x00\x10\x01\x0d\x00\x00\x00\x01")) - conn.Send([]byte("\x11")) - conn.Send(conn.EncodeHeaders(headers)) + conn.Send(fh) + conn.Send([]byte{byte(len(blockFragment) + 2)}) + conn.Send(blockFragment) return spec.VerifyStreamError(conn, http2.ErrCodeProtocol) }, diff --git a/http2/6_7_ping.go b/http2/6_7_ping.go index 41e660b..2b6906b 100644 --- a/http2/6_7_ping.go +++ b/http2/6_7_ping.go @@ -86,8 +86,8 @@ func Ping() *spec.TestGroup { } // PING frame: - // length: 8, flags: 0x0, stream_id: 1 - conn.Send([]byte("\x00\x00\x06\x06\x00\x00\x00\x00\x01")) + // length: 8, flags: 0x0, stream_id: 0 + conn.Send([]byte("\x00\x00\x06\x06\x00\x00\x00\x00\x00")) conn.Send([]byte("\x00\x00\x00\x00\x00\x00")) return spec.VerifyConnectionError(conn, http2.ErrCodeFrameSize) diff --git a/http2/6_9_2_initial_flow_control_window_size.go b/http2/6_9_2_initial_flow_control_window_size.go index 565c7a6..d991ac9 100644 --- a/http2/6_9_2_initial_flow_control_window_size.go +++ b/http2/6_9_2_initial_flow_control_window_size.go @@ -72,11 +72,6 @@ func InitialFlowControlWindowSize() *spec.TestGroup { } conn.WriteSettings(settings2...) - err = spec.VerifySettingsFrameWithAck(conn) - if err != nil { - return err - } - // Wait for DATA frame... actual, passed := conn.WaitEventByType(spec.EventDataFrame) switch event := actual.(type) { diff --git a/http2/7_error_codes.go b/http2/7_error_codes.go index 9d54890..3a406f1 100644 --- a/http2/7_error_codes.go +++ b/http2/7_error_codes.go @@ -24,7 +24,10 @@ func ErrorCodes() *spec.TestGroup { conn.WriteGoAway(0, 0xff, []byte{}) - return spec.VerifyConnectionClose(conn) + data := [8]byte{'h', '2', 's', 'p', 'e', 'c'} + conn.WritePing(false, data) + + return spec.VerifyPingFrameOrConnectionClose(conn, data) }, }) @@ -58,7 +61,7 @@ func ErrorCodes() *spec.TestGroup { data := [8]byte{} conn.WritePing(false, data) - return spec.VerifyPingFrameWithAck(conn, data) + return spec.VerifyPingFrameOrConnectionClose(conn, data) }, }) diff --git a/http2/8_1_2_3_request_pseudo_header_fields.go b/http2/8_1_2_3_request_pseudo_header_fields.go index 389ee48..89031a1 100644 --- a/http2/8_1_2_3_request_pseudo_header_fields.go +++ b/http2/8_1_2_3_request_pseudo_header_fields.go @@ -212,7 +212,7 @@ func RequestPseudoHeaderFields() *spec.TestGroup { // the ":method", ":scheme", and ":path" pseudo-header fields, // unless it is a CONNECT request (Section 8.3). tg.AddTestCase(&spec.TestCase{ - Desc: "Sends a HEADERS frame with duplicated \":method\" pseudo-header field", + Desc: "Sends a HEADERS frame with duplicated \":path\" pseudo-header field", Requirement: "The endpoint MUST respond with a stream error of type PROTOCOL_ERROR.", Run: func(c *config.Config, conn *spec.Conn) error { var streamID uint32 = 1 @@ -223,7 +223,7 @@ func RequestPseudoHeaderFields() *spec.TestGroup { } headers := spec.CommonHeaders(c) - headers = append(headers, spec.HeaderField(":method", headers[2].Value)) + headers = append(headers, spec.HeaderField(":path", headers[2].Value)) hp := http2.HeadersFrameParam{ StreamID: streamID, diff --git a/reporter/client_reporter.go b/reporter/client_reporter.go new file mode 100644 index 0000000..895c8f5 --- /dev/null +++ b/reporter/client_reporter.go @@ -0,0 +1,64 @@ +package reporter + +import ( + "fmt" + + "github.com/summerwind/h2spec/log" + "github.com/summerwind/h2spec/spec" +) + +// SummaryForClient outputs the summary of test result that includes +// the number of passsed, skipped and failed. +func SummaryForClient(group *spec.ClientTestGroup) string { + passed := group.PassedCount + failed := group.FailedCount + skipped := group.SkippedCount + + total := passed + failed + skipped + tmp := "%d tests, %d passed, %d skipped, %d failed" + return fmt.Sprintf(tmp, total, passed, skipped, failed) +} + +func PrintSummaryForClient(group *spec.ClientTestGroup) { + log.Println(SummaryForClient(group)) +} + +// PrintFailedClientTests outputs the report of failed tests. +func PrintFailedClientTests(group *spec.ClientTestGroup) { + log.Print("Failures: \n\n") + + printClientFailed(group) +} + +func printClientFailed(tg *spec.ClientTestGroup) { + if tg.FailedCount == 0 { + return + } + + level := tg.Level() + + log.SetIndentLevel(level) + log.Println(tg.Title()) + log.SetIndentLevel(level + 1) + + failed := false + + for _, tc := range tg.Tests { + if tc.Result == nil { + continue + } + + if tc.Result.Failed { + tc.Result.Print() + failed = true + } + } + + if failed { + log.PrintBlankLine() + } + + for _, g := range tg.Groups { + printClientFailed(g) + } +} diff --git a/reporter/reporter.go b/reporter/reporter.go index fbeab1c..e90825d 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -25,7 +25,7 @@ func Summary(groups []*spec.TestGroup) { // FailedTests outputs the report of failed tests. func FailedTests(groups []*spec.TestGroup) { - log.Println("Failures: \n") + log.Print("Failures: \n\n") for _, tg := range groups { printFailed(tg) diff --git a/reporter/web_reporter.go b/reporter/web_reporter.go new file mode 100644 index 0000000..c6e8a94 --- /dev/null +++ b/reporter/web_reporter.go @@ -0,0 +1,165 @@ +package reporter + +import ( + "bytes" + "fmt" + "net/http" + "strings" + + "github.com/summerwind/h2spec/config" + "github.com/summerwind/h2spec/log" + "github.com/summerwind/h2spec/spec" +) + +const homeTemplate string = ` + +