0
Fork 0
mirror of https://github.com/caddyserver/caddy.git synced 2024-12-23 22:27:38 -05:00

push: Allow pushing multiple resources via Link header (#1798)

* Allow pushing multiple resources via Link header

* Add nopush test case

* Extract Link header parsing to separate function

* Parser regexp-free

* Remove dead code, thx gometalinter

* Redundant condition - won't happen

* Reduce duplication
This commit is contained in:
Mateusz Gajewski 2017-08-29 03:38:29 +02:00 committed by Matt Holt
parent c0c7437fa5
commit 6d7462ac99
4 changed files with 210 additions and 14 deletions

View file

@ -32,6 +32,7 @@ outer:
if !matches {
_, matches = httpserver.IndexFile(h.Root, urlPath, staticfiles.IndexPages)
}
if matches {
for _, resource := range rule.Resources {
pushErr := pusher.Push(resource.Path, &http.PushOptions{
@ -57,27 +58,40 @@ outer:
return code, err
}
func (h Middleware) servePreloadLinks(pusher http.Pusher, headers http.Header, links []string) {
for _, link := range links {
parts := strings.Split(link, ";")
// servePreloadLinks parses Link headers from backend and pushes resources found in them.
// For accepted header formats check parseLinkHeader function.
//
// If resource has 'nopush' attribute then it will be omitted.
func (h Middleware) servePreloadLinks(pusher http.Pusher, headers http.Header, resources []string) {
outer:
for _, resource := range resources {
for _, resource := range parseLinkHeader(resource) {
if _, exists := resource.params["nopush"]; exists {
continue
}
if link == "" || strings.HasSuffix(link, "nopush") {
continue
}
if h.isRemoteResource(resource.uri) {
continue
}
target := strings.TrimSuffix(strings.TrimPrefix(parts[0], "<"), ">")
err := pusher.Push(resource.uri, &http.PushOptions{
Method: http.MethodGet,
Header: headers,
})
err := pusher.Push(target, &http.PushOptions{
Method: http.MethodGet,
Header: headers,
})
if err != nil {
break
if err != nil {
break outer
}
}
}
}
func (h Middleware) isRemoteResource(resource string) bool {
return strings.HasPrefix(resource, "//") ||
strings.HasPrefix(resource, "http://") ||
strings.HasPrefix(resource, "https://")
}
func (h Middleware) mergeHeaders(l, r http.Header) http.Header {
out := http.Header{}

View file

@ -269,6 +269,52 @@ func TestMiddlewareShouldInterceptLinkHeader(t *testing.T) {
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
}
func TestMiddlewareShouldInterceptLinkHeaderWithMultipleResources(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", "</assets/css/screen.css?v=5fc240c512>; rel=preload; as=style,</content/images/2016/06/Timeouts-001.png>; rel=preload; as=image,</content/images/2016/06/Timeouts-002.png>; rel=preload; as=image")
w.Header().Add("Link", "<//cdn.bizible.com/scripts/bizible.js>; rel=preload; as=script,</resource.png>; rel=preload; as=script; nopush")
return 0, nil
}),
Rules: []Rule{},
}
pushingWriter := &MockedPusher{ResponseWriter: writer}
// when
_, err2 := middleware.ServeHTTP(pushingWriter, request)
// then
if err2 != nil {
t.Error("Should not return error")
}
expectedPushedResources := map[string]*http.PushOptions{
"/assets/css/screen.css?v=5fc240c512": {
Method: http.MethodGet,
Header: http.Header{},
},
"/content/images/2016/06/Timeouts-001.png": {
Method: http.MethodGet,
Header: http.Header{},
},
"/content/images/2016/06/Timeouts-002.png": {
Method: http.MethodGet,
Header: http.Header{},
},
}
comparePushedResources(t, expectedPushedResources, pushingWriter.pushed)
}
func TestMiddlewareShouldInterceptLinkHeaderPusherError(t *testing.T) {
// given
expectedHeaders := http.Header{"Accept-Encoding": []string{"br"}}

View file

@ -0,0 +1,63 @@
package push
import (
"strings"
)
const (
commaSeparator = ","
semicolonSeparator = ";"
equalSeparator = "="
)
type linkResource struct {
uri string
params map[string]string
}
// parseLinkHeader is responsible for parsing Link header and returning list of found resources.
//
// Accepted formats are:
// Link: </resource>; as=script
// Link: </resource>; as=script,</resource2>; as=style
// Link: </resource>;</resource2>
func parseLinkHeader(header string) []linkResource {
resources := []linkResource{}
if header == "" {
return resources
}
for _, link := range strings.Split(header, commaSeparator) {
l := linkResource{params: make(map[string]string)}
li, ri := strings.Index(link, "<"), strings.Index(link, ">")
if li == -1 || ri == -1 {
continue
}
l.uri = strings.TrimSpace(link[li+1 : ri])
for _, param := range strings.Split(strings.TrimSpace(link[ri+1:]), semicolonSeparator) {
parts := strings.SplitN(strings.TrimSpace(param), equalSeparator, 2)
key := strings.TrimSpace(parts[0])
if key == "" {
continue
}
if len(parts) == 1 {
l.params[key] = key
}
if len(parts) == 2 {
l.params[key] = strings.TrimSpace(parts[1])
}
}
resources = append(resources, l)
}
return resources
}

View file

@ -0,0 +1,73 @@
package push
import (
"reflect"
"testing"
)
func TestDifferentParserInputs(t *testing.T) {
testCases := []struct {
header string
expectedResources []linkResource
}{
{
header: "</resource>; as=script",
expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"as": "script"}}},
},
{
header: "</resource>",
expectedResources: []linkResource{{uri: "/resource", params: map[string]string{}}},
},
{
header: "</resource>; nopush",
expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"nopush": "nopush"}}},
},
{
header: "</resource>;nopush;rel=next",
expectedResources: []linkResource{{uri: "/resource", params: map[string]string{"nopush": "nopush", "rel": "next"}}},
},
{
header: "</resource>;nopush;rel=next,</resource2>;nopush",
expectedResources: []linkResource{
{uri: "/resource", params: map[string]string{"nopush": "nopush", "rel": "next"}},
{uri: "/resource2", params: map[string]string{"nopush": "nopush"}},
},
},
{
header: "</resource>,</resource2>",
expectedResources: []linkResource{
{uri: "/resource", params: map[string]string{}},
{uri: "/resource2", params: map[string]string{}},
},
},
{
header: "malformed",
expectedResources: []linkResource{},
},
{
header: "<malformed",
expectedResources: []linkResource{},
},
{
header: ",",
expectedResources: []linkResource{},
},
{
header: ";",
expectedResources: []linkResource{},
},
{
header: "</resource> ; ",
expectedResources: []linkResource{{uri: "/resource", params: map[string]string{}}},
},
}
for i, test := range testCases {
actualResources := parseLinkHeader(test.header)
if !reflect.DeepEqual(actualResources, test.expectedResources) {
t.Errorf("Test %d (header: %s) - expected resources %v, got %v", i, test.header, test.expectedResources, actualResources)
}
}
}