github.com/symfony-cli/symfony-cli@v0.0.0-20240514161054-ece2df437dfa/local/http/push.go (about) 1 /* 2 * Copyright (c) 2021-present Fabien Potencier <fabien@symfony.com> 3 * 4 * This file is part of Symfony CLI project 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU Affero General Public License as 8 * published by the Free Software Foundation, either version 3 of the 9 * License, or (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU Affero General Public License for more details. 15 * 16 * You should have received a copy of the GNU Affero General Public License 17 * along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package http 21 22 // most of the code from https://github.com/mholt/caddy 23 24 import ( 25 "net/http" 26 "strings" 27 28 "github.com/pkg/errors" 29 ) 30 31 type linkResource struct { 32 uri string 33 params map[string]string 34 } 35 36 // servePreloadLinks parses Link headers from backend and pushes resources found in them. 37 // If resource has 'nopush' attribute then it will be omitted. 38 func (s *Server) servePreloadLinks(w http.ResponseWriter, r *http.Request) ([]string, error) { 39 resources, exists := w.Header()["Link"] 40 if !exists { 41 return nil, nil 42 } 43 // check if this is a request for the pushed resource (avoid recursion) 44 if _, exists := r.Header["X-Push"]; exists { 45 return nil, nil 46 } 47 pusher, hasPusher := w.(http.Pusher) 48 // no push possible, carry on 49 if !hasPusher { 50 return nil, nil 51 } 52 headers := filterProxiedHeaders(r.Header) 53 rs := []string{} 54 for _, resource := range resources { 55 for _, resource := range parseLinkHeader(resource) { 56 if _, exists := resource.params["nopush"]; exists { 57 continue 58 } 59 if rel, exists := resource.params["rel"]; exists && rel != "preload" { 60 continue 61 } 62 if isRemoteResource(resource.uri) { 63 continue 64 } 65 if err := errors.WithStack(pusher.Push(resource.uri, &http.PushOptions{ 66 Method: http.MethodGet, 67 Header: headers, 68 })); err != nil { 69 return nil, err 70 } 71 rs = append(rs, resource.uri) 72 } 73 } 74 return rs, nil 75 } 76 77 func isRemoteResource(resource string) bool { 78 return strings.HasPrefix(resource, "//") || 79 strings.HasPrefix(resource, "http://") || 80 strings.HasPrefix(resource, "https://") 81 } 82 83 func filterProxiedHeaders(headers http.Header) http.Header { 84 filter := http.Header{} 85 for _, header := range []string{ 86 "Accept-Encoding", 87 "Accept-Language", 88 "Cache-Control", 89 "Host", 90 "User-Agent", 91 } { 92 if val, ok := headers[header]; ok { 93 filter[header] = val 94 } 95 } 96 return filter 97 } 98 99 // parseLinkHeader is responsible for parsing Link header and returning list of found resources. 100 // 101 // Accepted formats are: 102 // Link: </resource>; as=script 103 // Link: </resource>; as=script,</resource2>; as=style 104 // Link: </resource>;</resource2> 105 func parseLinkHeader(header string) []linkResource { 106 resources := []linkResource{} 107 108 if header == "" { 109 return resources 110 } 111 112 for _, link := range strings.Split(header, ",") { 113 l := linkResource{params: make(map[string]string)} 114 115 li, ri := strings.Index(link, "<"), strings.Index(link, ">") 116 117 if li == -1 || ri == -1 { 118 continue 119 } 120 121 l.uri = strings.TrimSpace(link[li+1 : ri]) 122 123 for _, param := range strings.Split(strings.TrimSpace(link[ri+1:]), ";") { 124 parts := strings.SplitN(strings.TrimSpace(param), "=", 2) 125 key := strings.TrimSpace(parts[0]) 126 127 if key == "" { 128 continue 129 } 130 131 if len(parts) == 1 { 132 l.params[key] = key 133 } 134 135 if len(parts) == 2 { 136 l.params[key] = strings.TrimSpace(parts[1]) 137 } 138 } 139 140 resources = append(resources, l) 141 } 142 143 return resources 144 }