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  }