Commit cdf7cf5c authored by Mateusz Gajewski's avatar Mateusz Gajewski Committed by Matt Holt

HTTP/2 push support (golang 1.8) (#1215)

* WIP

* HTTP2/Push for golang 1.8

* Push plugin completed for review

* Correct build tag

* Move push plugin position

* Add build tags to tests

* Gofmt that code

* Add header/method validations

* Load push plugin

* Fixes for wrapping writers

* Push after delivering file

* Fixes, review changes

* Remove build tags, support new syntax

* Fix spelling

* gofmt -s -w .

* Gogland time

* Add interface guards

* gofmt

* After review fixes
parent 57900782
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
_ "github.com/mholt/caddy/caddyhttp/mime" _ "github.com/mholt/caddy/caddyhttp/mime"
_ "github.com/mholt/caddy/caddyhttp/pprof" _ "github.com/mholt/caddy/caddyhttp/pprof"
_ "github.com/mholt/caddy/caddyhttp/proxy" _ "github.com/mholt/caddy/caddyhttp/proxy"
_ "github.com/mholt/caddy/caddyhttp/push"
_ "github.com/mholt/caddy/caddyhttp/redirect" _ "github.com/mholt/caddy/caddyhttp/redirect"
_ "github.com/mholt/caddy/caddyhttp/rewrite" _ "github.com/mholt/caddy/caddyhttp/rewrite"
_ "github.com/mholt/caddy/caddyhttp/root" _ "github.com/mholt/caddy/caddyhttp/root"
......
...@@ -11,7 +11,7 @@ import ( ...@@ -11,7 +11,7 @@ import (
// ensure that the standard plugins are in fact plugged in // ensure that the standard plugins are in fact plugged in
// and registered properly; this is a quick/naive way to do it. // and registered properly; this is a quick/naive way to do it.
func TestStandardPlugins(t *testing.T) { func TestStandardPlugins(t *testing.T) {
numStandardPlugins := 29 // importing caddyhttp plugs in this many plugins numStandardPlugins := 30 // importing caddyhttp plugs in this many plugins
s := caddy.DescribePlugins() s := caddy.DescribePlugins()
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want { if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s) t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"errors"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
...@@ -161,3 +162,17 @@ func (w *gzipResponseWriter) CloseNotify() <-chan bool { ...@@ -161,3 +162,17 @@ func (w *gzipResponseWriter) CloseNotify() <-chan bool {
} }
panic(httpserver.NonCloseNotifierError{Underlying: w.ResponseWriter}) panic(httpserver.NonCloseNotifierError{Underlying: w.ResponseWriter})
} }
func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
if pusher, hasPusher := w.ResponseWriter.(http.Pusher); hasPusher {
return pusher.Push(target, opts)
}
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)")
}
// Interface guards
var _ http.Pusher = (*gzipResponseWriter)(nil)
var _ http.Flusher = (*gzipResponseWriter)(nil)
var _ http.CloseNotifier = (*gzipResponseWriter)(nil)
var _ http.Hijacker = (*gzipResponseWriter)(nil)
...@@ -9,6 +9,7 @@ import ( ...@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"errors"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
...@@ -23,7 +24,7 @@ type Headers struct { ...@@ -23,7 +24,7 @@ type Headers struct {
// setting headers on the response according to the configured rules. // setting headers on the response according to the configured rules.
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
replacer := httpserver.NewReplacer(r, nil, "") replacer := httpserver.NewReplacer(r, nil, "")
rww := &responseWriterWrapper{w: w} rww := &responseWriterWrapper{ResponseWriter: w}
for _, rule := range h.Rules { for _, rule := range h.Rules {
if httpserver.Path(r.URL.Path).Matches(rule.Path) { if httpserver.Path(r.URL.Path).Matches(rule.Path) {
for name := range rule.Headers { for name := range rule.Headers {
...@@ -62,20 +63,20 @@ type headerOperation func(http.Header) ...@@ -62,20 +63,20 @@ type headerOperation func(http.Header)
// responseWriterWrapper wraps the real ResponseWriter. // responseWriterWrapper wraps the real ResponseWriter.
// It defers header operations until writeHeader // It defers header operations until writeHeader
type responseWriterWrapper struct { type responseWriterWrapper struct {
w http.ResponseWriter http.ResponseWriter
ops []headerOperation ops []headerOperation
wroteHeader bool wroteHeader bool
} }
func (rww *responseWriterWrapper) Header() http.Header { func (rww *responseWriterWrapper) Header() http.Header {
return rww.w.Header() return rww.ResponseWriter.Header()
} }
func (rww *responseWriterWrapper) Write(d []byte) (int, error) { func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
if !rww.wroteHeader { if !rww.wroteHeader {
rww.WriteHeader(http.StatusOK) rww.WriteHeader(http.StatusOK)
} }
return rww.w.Write(d) return rww.ResponseWriter.Write(d)
} }
func (rww *responseWriterWrapper) WriteHeader(status int) { func (rww *responseWriterWrapper) WriteHeader(status int) {
...@@ -91,7 +92,7 @@ func (rww *responseWriterWrapper) WriteHeader(status int) { ...@@ -91,7 +92,7 @@ func (rww *responseWriterWrapper) WriteHeader(status int) {
op(h) op(h)
} }
rww.w.WriteHeader(status) rww.ResponseWriter.WriteHeader(status)
} }
// delHeader deletes the existing header according to the key // delHeader deletes the existing header according to the key
...@@ -109,19 +110,19 @@ func (rww *responseWriterWrapper) delHeader(key string) { ...@@ -109,19 +110,19 @@ func (rww *responseWriterWrapper) delHeader(key string) {
// Hijack implements http.Hijacker. It simply wraps the underlying // Hijack implements http.Hijacker. It simply wraps the underlying
// ResponseWriter's Hijack method if there is one, or returns an error. // ResponseWriter's Hijack method if there is one, or returns an error.
func (rww *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (rww *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := rww.w.(http.Hijacker); ok { if hj, ok := rww.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack() return hj.Hijack()
} }
return nil, nil, httpserver.NonHijackerError{Underlying: rww.w} return nil, nil, httpserver.NonHijackerError{Underlying: rww.ResponseWriter}
} }
// Flush implements http.Flusher. It simply wraps the underlying // Flush implements http.Flusher. It simply wraps the underlying
// ResponseWriter's Flush method if there is one, or panics. // ResponseWriter's Flush method if there is one, or panics.
func (rww *responseWriterWrapper) Flush() { func (rww *responseWriterWrapper) Flush() {
if f, ok := rww.w.(http.Flusher); ok { if f, ok := rww.ResponseWriter.(http.Flusher); ok {
f.Flush() f.Flush()
} else { } else {
panic(httpserver.NonFlusherError{Underlying: rww.w}) // should be recovered at the beginning of middleware stack panic(httpserver.NonFlusherError{Underlying: rww.ResponseWriter}) // should be recovered at the beginning of middleware stack
} }
} }
...@@ -129,8 +130,22 @@ func (rww *responseWriterWrapper) Flush() { ...@@ -129,8 +130,22 @@ func (rww *responseWriterWrapper) Flush() {
// It just inherits the underlying ResponseWriter's CloseNotify method. // It just inherits the underlying ResponseWriter's CloseNotify method.
// It panics if the underlying ResponseWriter is not a CloseNotifier. // It panics if the underlying ResponseWriter is not a CloseNotifier.
func (rww *responseWriterWrapper) CloseNotify() <-chan bool { func (rww *responseWriterWrapper) CloseNotify() <-chan bool {
if cn, ok := rww.w.(http.CloseNotifier); ok { if cn, ok := rww.ResponseWriter.(http.CloseNotifier); ok {
return cn.CloseNotify() return cn.CloseNotify()
} }
panic(httpserver.NonCloseNotifierError{Underlying: rww.w}) panic(httpserver.NonCloseNotifierError{Underlying: rww.ResponseWriter})
} }
func (rww *responseWriterWrapper) Push(target string, opts *http.PushOptions) error {
if pusher, hasPusher := rww.ResponseWriter.(http.Pusher); hasPusher {
return pusher.Push(target, opts)
}
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)")
}
// Interface guards
var _ http.Pusher = (*responseWriterWrapper)(nil)
var _ http.Flusher = (*responseWriterWrapper)(nil)
var _ http.CloseNotifier = (*responseWriterWrapper)(nil)
var _ http.Hijacker = (*responseWriterWrapper)(nil)
...@@ -459,6 +459,7 @@ var directives = []string{ ...@@ -459,6 +459,7 @@ var directives = []string{
"proxy", "proxy",
"fastcgi", "fastcgi",
"cgi", // github.com/jung-kurt/caddy-cgi "cgi", // github.com/jung-kurt/caddy-cgi
"push",
"websocket", "websocket",
"filemanager", // github.com/hacdias/caddy-filemanager "filemanager", // github.com/hacdias/caddy-filemanager
"markdown", "markdown",
......
...@@ -2,6 +2,7 @@ package httpserver ...@@ -2,6 +2,7 @@ package httpserver
import ( import (
"bufio" "bufio"
"errors"
"net" "net"
"net/http" "net/http"
"time" "time"
...@@ -95,3 +96,18 @@ func (r *ResponseRecorder) CloseNotify() <-chan bool { ...@@ -95,3 +96,18 @@ func (r *ResponseRecorder) CloseNotify() <-chan bool {
} }
panic(NonCloseNotifierError{Underlying: r.ResponseWriter}) panic(NonCloseNotifierError{Underlying: r.ResponseWriter})
} }
// Push resource to client
func (r *ResponseRecorder) Push(target string, opts *http.PushOptions) error {
if pusher, hasPusher := r.ResponseWriter.(http.Pusher); hasPusher {
return pusher.Push(target, opts)
}
return errors.New("push is unavailable (probably chained http.ResponseWriter does not implement http.Pusher)")
}
// Interface guards
var _ http.Pusher = (*ResponseRecorder)(nil)
var _ http.Flusher = (*ResponseRecorder)(nil)
var _ http.CloseNotifier = (*ResponseRecorder)(nil)
var _ http.Hijacker = (*ResponseRecorder)(nil)
...@@ -45,6 +45,7 @@ func NewServer(addr string, group []*SiteConfig) (*Server, error) { ...@@ -45,6 +45,7 @@ func NewServer(addr string, group []*SiteConfig) (*Server, error) {
sites: group, sites: group,
connTimeout: GracefulTimeout, connTimeout: GracefulTimeout,
} }
s.Server.Handler = s // this is weird, but whatever s.Server.Handler = s // this is weird, but whatever
// Disable HTTP/2 if desired // Disable HTTP/2 if desired
......
package push
import (
"net/http"
"strings"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func (h Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
pusher, hasPusher := w.(http.Pusher)
// No Pusher, no cry
if !hasPusher {
return h.Next.ServeHTTP(w, r)
}
// This is request for the pushed resource - it should not be recursive
if _, exists := r.Header[pushHeader]; exists {
return h.Next.ServeHTTP(w, r)
}
// Serve file first
code, err := h.Next.ServeHTTP(w, r)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
outer:
for _, rule := range h.Rules {
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
for _, resource := range rule.Resources {
pushErr := pusher.Push(resource.Path, &http.PushOptions{
Method: resource.Method,
Header: resource.Header,
})
if pushErr != nil {
// If we cannot push (either not supported or concurrent streams are full - break)
break outer
}
}
}
}
if links, exists := w.Header()["Link"]; exists {
h.pushLinks(pusher, links)
}
return code, err
}
func (h Middleware) pushLinks(pusher http.Pusher, links []string) {
outer:
for _, link := range links {
parts := strings.Split(link, ";")
if link == "" || strings.HasSuffix(link, "nopush") {
continue
}
target := strings.TrimSuffix(strings.TrimPrefix(parts[0], "<"), ">")
err := pusher.Push(target, &http.PushOptions{Method: http.MethodGet})
if err != nil {
break outer
}
}
}
package push
import (
"errors"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
type MockedPusher struct {
http.ResponseWriter
pushed map[string]*http.PushOptions
returnedError error
}
func (w *MockedPusher) Push(target string, options *http.PushOptions) error {
if w.pushed == nil {
w.pushed = make(map[string]*http.PushOptions)
}
w.pushed[target] = options
return w.returnedError
}
func TestMiddlewareWillPushResources(t *testing.T) {
// given
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
writer := httptest.NewRecorder()
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
middleware := Middleware{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}),
Rules: []Rule{
{Path: "/index.html", Resources: []Resource{
{Path: "/index.css", Method: http.MethodHead, Header: http.Header{"Test": []string{"Value"}}},
{Path: "/index2.css", Method: http.MethodGet},
}},
},
}
pushingWriter := &MockedPusher{ResponseWriter: writer}
// when
middleware.ServeHTTP(pushingWriter, request)
// then
expectedPushedResources := map[string]*http.PushOptions{
"/index.css": {
Method: http.MethodHead,
Header: http.Header{"Test": []string{"Value"}},
},
"/index2.css": {
Method: http.MethodGet,
Header: nil,
},
}
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
}
func TestMiddlewareShouldntDoRecursivePush(t *testing.T) {
// given
request, err := http.NewRequest(http.MethodGet, "/index.css", nil)
request.Header.Add(pushHeader, "")
writer := httptest.NewRecorder()
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
middleware := Middleware{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}),
Rules: []Rule{
{Path: "/", Resources: []Resource{
{Path: "/index.css", Method: http.MethodHead, Header: http.Header{"Test": []string{"Value"}}},
{Path: "/index2.css", Method: http.MethodGet},
}},
},
}
pushingWriter := &MockedPusher{ResponseWriter: writer}
// when
middleware.ServeHTTP(pushingWriter, request)
// then
if len(pushingWriter.pushed) > 0 {
t.Errorf("Expected 0 pushed resources, actual %d", len(pushingWriter.pushed))
}
}
func TestMiddlewareShouldStopPushingOnError(t *testing.T) {
// given
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
writer := httptest.NewRecorder()
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
middleware := Middleware{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}),
Rules: []Rule{
{Path: "/index.html", Resources: []Resource{
{Path: "/only.css", Method: http.MethodHead, Header: http.Header{"Test": []string{"Value"}}},
{Path: "/index2.css", Method: http.MethodGet},
{Path: "/index3.css", Method: http.MethodGet},
}},
},
}
pushingWriter := &MockedPusher{ResponseWriter: writer, returnedError: errors.New("Cannot push right now")}
// when
middleware.ServeHTTP(pushingWriter, request)
// then
expectedPushedResources := map[string]*http.PushOptions{
"/only.css": {
Method: http.MethodHead,
Header: http.Header{"Test": []string{"Value"}},
},
}
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
}
func TestMiddlewareWillNotPushResources(t *testing.T) {
// given
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
middleware := Middleware{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
}),
Rules: []Rule{
{Path: "/index.html", Resources: []Resource{
{Path: "/index.css", Method: http.MethodHead, Header: http.Header{"Test": []string{"Value"}}},
{Path: "/index2.css", Method: http.MethodGet},
}},
},
}
writer := httptest.NewRecorder()
// when
_, err2 := middleware.ServeHTTP(writer, request)
// then
if err2 != nil {
t.Errorf("Should not return error")
}
}
func TestMiddlewareShouldInterceptLinkHeader(t *testing.T) {
// given
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
writer := httptest.NewRecorder()
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
middleware := Middleware{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
w.Header().Add("Link", "</index.css>; rel=preload; as=stylesheet;")
w.Header().Add("Link", "</index2.css>; rel=preload; as=stylesheet;")
w.Header().Add("Link", "")
w.Header().Add("Link", "</index3.css>")
w.Header().Add("Link", "</index4.css>; rel=preload; nopush")
return 0, nil
}),
Rules: []Rule{},
}
pushingWriter := &MockedPusher{ResponseWriter: writer}
// when
_, err2 := middleware.ServeHTTP(pushingWriter, request)
// then
if err2 != nil {
t.Errorf("Should not return error")
}
expectedPushedResources := map[string]*http.PushOptions{
"/index.css": {
Method: http.MethodGet,
Header: nil,
},
"/index2.css": {
Method: http.MethodGet,
Header: nil,
},
"/index3.css": {
Method: http.MethodGet,
Header: nil,
},
}
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
}
func TestMiddlewareShouldInterceptLinkHeaderPusherError(t *testing.T) {
// given
request, err := http.NewRequest(http.MethodGet, "/index.html", nil)
writer := httptest.NewRecorder()
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
middleware := Middleware{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
w.Header().Add("Link", "</index.css>; rel=preload; as=stylesheet;")
w.Header().Add("Link", "</index2.css>; rel=preload; as=stylesheet;")
return 0, nil
}),
Rules: []Rule{},
}
pushingWriter := &MockedPusher{ResponseWriter: writer, returnedError: errors.New("Cannot push right now")}
// when
_, err2 := middleware.ServeHTTP(pushingWriter, request)
// then
if err2 != nil {
t.Errorf("Should not return error")
}
expectedPushedResources := map[string]*http.PushOptions{
"/index.css": {
Method: http.MethodGet,
Header: nil,
},
}
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
}
func comparePushedResources(t *testing.T, expected, actual map[string]*http.PushOptions) {
if len(expected) != len(actual) {
t.Errorf("Expected %d pushed resources, actual: %d", len(expected), len(actual))
}
for target, expectedTarget := range expected {
if actualTarget, exists := actual[target]; exists {
if expectedTarget.Method != actualTarget.Method {
t.Errorf("Expected %s resource method to be %s, actual: %s", target, expectedTarget.Method, actualTarget.Method)
}
if !reflect.DeepEqual(expectedTarget.Header, actualTarget.Header) {
t.Errorf("Expected %s resource push headers to be %v, actual: %v", target, expectedTarget.Header, actualTarget.Header)
}
} else {
t.Errorf("Expected %s to be pushed", target)
}
}
}
package push
import (
"net/http"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
type (
// Rule describes conditions on which resources will be pushed
Rule struct {
Path string
Resources []Resource
}
// Resource describes resource to be pushed
Resource struct {
Path string
Method string
Header http.Header
}
// Middleware supports pushing resources to clients
Middleware struct {
Next httpserver.Handler
Rules []Rule
}
ruleOp func([]Resource)
)
package push
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("push", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
var errInvalidHeader = errors.New("header directive requires [name] [value]")
var errHeaderStartsWithColon = errors.New("header cannot start with colon")
var errMethodNotSupported = errors.New("push supports only GET and HEAD methods")
const pushHeader = "X-Push"
var emptyRules = []Rule{}
// setup configures a new Push middleware
func setup(c *caddy.Controller) error {
rules, err := parsePushRules(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Middleware{Next: next, Rules: rules}
})
return nil
}
func parsePushRules(c *caddy.Controller) ([]Rule, error) {
var rules = make(map[string]*Rule)
for c.NextLine() {
if !c.NextArg() {
return emptyRules, c.ArgErr()
}
path := c.Val()
args := c.RemainingArgs()
var rule *Rule
var resources []Resource
var ops []ruleOp
if existingRule, ok := rules[path]; ok {
rule = existingRule
} else {
rule = new(Rule)
rule.Path = path
rules[rule.Path] = rule
}
for i := 0; i < len(args); i++ {
resources = append(resources, Resource{
Path: args[i],
Method: http.MethodGet,
Header: http.Header{pushHeader: []string{}},
})
}
for c.NextBlock() {
val := c.Val()
switch val {
case "method":
if !c.NextArg() {
return emptyRules, c.ArgErr()
}
method := c.Val()
if err := validateMethod(method); err != nil {
return emptyRules, errMethodNotSupported
}
ops = append(ops, setMethodOp(method))
case "header":
args := c.RemainingArgs()
if len(args) != 2 {
return emptyRules, errInvalidHeader
}
if err := validateHeader(args[0]); err != nil {
return emptyRules, err
}
ops = append(ops, setHeaderOp(args[0], args[1]))
default:
resources = append(resources, Resource{
Path: val,
Method: http.MethodGet,
Header: http.Header{pushHeader: []string{}},
})
}
}
for _, op := range ops {
op(resources)
}
rule.Resources = append(rule.Resources, resources...)
}
var returnRules []Rule
for path, rule := range rules {
if len(rule.Resources) == 0 {
return emptyRules, c.Errf("Rule %s has empty push resources list", path)
}
returnRules = append(returnRules, *rule)
}
return returnRules, nil
}
func setHeaderOp(key, value string) func(resources []Resource) {
return func(resources []Resource) {
for index := range resources {
resources[index].Header.Set(key, value)
}
}
}
func setMethodOp(method string) func(resources []Resource) {
return func(resources []Resource) {
for index := range resources {
resources[index].Method = method
}
}
}
func validateHeader(header string) error {
if strings.HasPrefix(header, ":") {
return errHeaderStartsWithColon
}
switch strings.ToLower(header) {
case "content-length", "content-encoding", "trailer", "te", "expect", "host":
return fmt.Errorf("push headers cannot include %s", header)
}
return nil
}
// rules based on https://go-review.googlesource.com/#/c/29439/4/http2/go18.go#94
func validateMethod(method string) error {
if method != http.MethodGet && method != http.MethodHead {
return errMethodNotSupported
}
return nil
}
package push
import (
"net/http"
"reflect"
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestPushAvailable(t *testing.T) {
err := setup(caddy.NewTestController("http", "push /index.html /available.css"))
if err != nil {
t.Fatalf("Error %s occurred, expected none", err)
}
}
func TestConfigParse(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
expected []Rule
}{
{
"ParseInvalidEmptyConfig", `push`, true, []Rule{},
},
{
"ParseInvalidConfig", `push /index.html`, true, []Rule{},
},
{
"ParseInvalidConfigBlock", `push /index.html /index.css {
method
}`, true, []Rule{},
},
{
"ParseInvalidHeaderFormat", `push /index.html /index.css {
header :invalid value
}`, true, []Rule{},
},
{
"ParseForbiddenHeader", `push /index.html /index.css {
header Content-Length 1000
}`, true, []Rule{},
},
{
"ParseInvalidMethod", `push /index.html /index.css {
method POST
}`, true, []Rule{},
},
{
"ParseInvalidHeaderBlock", `push /index.html /index.css {
header
}`, true, []Rule{},
},
{
"ParseInvalidHeaderBlock2", `push /index.html /index.css {
header name
}`, true, []Rule{},
},
{
"ParseProperConfig", `push /index.html /style.css /style2.css`, false, []Rule{
{
Path: "/index.html",
Resources: []Resource{
{
Path: "/style.css",
Method: http.MethodGet,
Header: http.Header{pushHeader: []string{}},
},
{
Path: "/style2.css",
Method: http.MethodGet,
Header: http.Header{pushHeader: []string{}},
},
},
},
},
},
{
"ParseSimpleInlinePush", `push /index.html {
/style.css
/style2.css
}`, false, []Rule{
{
Path: "/index.html",
Resources: []Resource{
{
Path: "/style.css",
Method: http.MethodGet,
Header: http.Header{pushHeader: []string{}},
},
{
Path: "/style2.css",
Method: http.MethodGet,
Header: http.Header{pushHeader: []string{}},
},
},
},
},
},
{
"ParseSimpleInlinePushWithOps", `push /index.html {
/style.css
/style2.css
header Test Value
}`, false, []Rule{
{
Path: "/index.html",
Resources: []Resource{
{
Path: "/style.css",
Method: http.MethodGet,
Header: http.Header{pushHeader: []string{}, "Test": []string{"Value"}},
},
{
Path: "/style2.css",
Method: http.MethodGet,
Header: http.Header{pushHeader: []string{}, "Test": []string{"Value"}},
},
},
},
},
},
{
"ParseProperConfigWithBlock", `push /index.html /style.css /style2.css {
method HEAD
header Own-Header Value
header Own-Header2 Value2
}`, false, []Rule{
{
Path: "/index.html",
Resources: []Resource{
{
Path: "/style.css",
Method: http.MethodHead,
Header: http.Header{
"Own-Header": []string{"Value"},
"Own-Header2": []string{"Value2"},
"X-Push": []string{},
},
},
{
Path: "/style2.css",
Method: http.MethodHead,
Header: http.Header{
"Own-Header": []string{"Value"},
"Own-Header2": []string{"Value2"},
"X-Push": []string{},
},
},
},
},
},
},
{
"ParseMergesRules", `push /index.html /index.css {
header name value
}
push /index.html /index2.css {
header name2 value2
method HEAD
}
`, false, []Rule{
{
Path: "/index.html",
Resources: []Resource{
{
Path: "/index.css",
Method: http.MethodGet,
Header: http.Header{
"Name": []string{"value"},
"X-Push": []string{},
},
},
{
Path: "/index2.css",
Method: http.MethodHead,
Header: http.Header{
"Name2": []string{"value2"},
"X-Push": []string{},
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t2 *testing.T) {
actual, err := parsePushRules(caddy.NewTestController("http", test.input))
if err == nil && test.shouldErr {
t2.Errorf("Test %s didn't error, but it should have", test.name)
} else if err != nil && !test.shouldErr {
t2.Errorf("Test %s errored, but it shouldn't have; got '%v'", test.name, err)
}
if len(actual) != len(test.expected) {
t2.Fatalf("Test %s expected %d rules, but got %d",
test.name, len(test.expected), len(actual))
}
for j, expectedRule := range test.expected {
actualRule := actual[j]
if actualRule.Path != expectedRule.Path {
t.Errorf("Test %s, rule %d: Expected path %s, but got %s",
test.name, j, expectedRule.Path, actualRule.Path)
}
if !reflect.DeepEqual(actualRule.Resources, expectedRule.Resources) {
t.Errorf("Test %s, rule %d: Expected resources %v, but got %v",
test.name, j, expectedRule.Resources, actualRule.Resources)
}
}
})
}
}
func TestSetupInstalledMiddleware(t *testing.T) {
// given
c := caddy.NewTestController("http", `push /index.html /test.js`)
// when
err := setup(c)
// then
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
middlewares := httpserver.GetConfig(c).Middleware()
if len(middlewares) != 1 {
t.Fatalf("Expected 1 middleware, had %d instead", len(middlewares))
}
handler := middlewares[0](httpserver.EmptyNext)
pushHandler, ok := handler.(Middleware)
if !ok {
t.Fatalf("Expected handler to be type Middleware, got: %#v", handler)
}
if !httpserver.SameNext(pushHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler Middleware was not set properly")
}
}
func TestSetupWithError(t *testing.T) {
// given
c := caddy.NewTestController("http", `push /index.html`)
// when
err := setup(c)
// then
if err == nil {
t.Error("Expected error but none occurred")
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment