├── .editorconfig ├── .phpunit.result.cache ├── LICENSE.md ├── composer.json └── src ├── Middleware └── AddHttp2ServerPush.php ├── ServiceProvider.php └── config.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":1559:{a:2:{s:7:"defects";a:0:{}s:5:"times";a:13:{s:87:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_not_exceed_size_limit";d:0.038;s:88:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_not_add_excluded_asset";d:0.001;s:114:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_not_modify_a_response_with_no_server_push_assets";d:0.001;s:98:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_return_a_css_link_header_for_css";d:0.001;s:96:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_return_a_js_link_header_for_js";d:0;s:104:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_return_an_image_link_header_for_images";d:0.001;s:109:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_return_an_image_link_header_for_svg_objects";d:0.001;s:96:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_returns_well_formatted_link_headers";d:0.001;s:113:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_return_correct_push_headers_for_multiple_assets";d:0.001;s:104:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_not_return_a_push_header_for_inline_js";d:0.001;s:100:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_not_return_a_push_header_for_icons";d:0.001;s:93:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_return_limit_count_of_links";d:0.001;s:101:"JacobBennett\Http2ServerPush\Test\AddHttp2ServerPushTest::it_will_append_to_header_if_already_present";d:0.001;}}} -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jacob Bennett 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jacobbennett/laravel-http2serverpush", 3 | "description": "A HTTP2 Server Push Middleware for Laravel 5", 4 | "keywords": [ 5 | "laravel", 6 | "laravel-http2serverpush", 7 | "serverpush" 8 | ], 9 | "homepage": "https://github.com/jacobbennett/laravel-http2serverpush", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Jacob Bennett", 14 | "email": "me@jakebennett.net", 15 | "homepage": "https://jakebennett.net", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php" : "^7.0|^8.0", 21 | "illuminate/support": "~6.0|~7.0|~8.0", 22 | "illuminate/http": "~6.0|~7.0|~8.0", 23 | "symfony/dom-crawler": "^2.7|^3.0|^4.0|^5.0", 24 | "symfony/css-selector": "^2.7|^3.0|^4.0|^5.0" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^8.5", 28 | "scrutinizer/ocular": "^1.1" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "JacobBennett\\Http2ServerPush\\": "src" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "JacobBennett\\Http2ServerPush\\Test\\": "tests" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "phpunit" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Middleware/AddHttp2ServerPush.php: -------------------------------------------------------------------------------- 1 | isRedirection() || !$response instanceof Response || $request->isJson()) { 33 | return $response; 34 | } 35 | 36 | $this->generateAndAttachLinkHeaders($response, $limit, $sizeLimit, $excludeKeywords); 37 | 38 | return $response; 39 | } 40 | 41 | public function getConfig($key, $default=false) { 42 | if(!function_exists('config')) { // for tests.. 43 | return $default; 44 | } 45 | return config('http2serverpush.'.$key, $default); 46 | } 47 | 48 | /** 49 | * @param \Illuminate\Http\Response $response 50 | * 51 | * @return $this 52 | */ 53 | protected function generateAndAttachLinkHeaders(Response $response, $limit = null, $sizeLimit = null, $excludeKeywords=null) 54 | { 55 | $excludeKeywords = $excludeKeywords ?? $this->getConfig('exclude_keywords', []); 56 | $headers = $this->fetchLinkableNodes($response) 57 | ->flatten(1) 58 | ->map(function ($url) { 59 | return $this->buildLinkHeaderString($url); 60 | }) 61 | ->unique() 62 | ->filter(function($value, $key) use ($excludeKeywords){ 63 | if(!$value) return false; 64 | $exclude_keywords = collect($excludeKeywords)->map(function ($keyword) { 65 | return preg_quote($keyword); 66 | }); 67 | if($exclude_keywords->count() <= 0) { 68 | return true; 69 | } 70 | return !preg_match('%('.$exclude_keywords->implode('|').')%i', $value); 71 | }) 72 | ->take($limit); 73 | 74 | $sizeLimit = $sizeLimit ?? max(1, intval($this->getConfig('size_limit', 32*1024))); 75 | $headersText = trim($headers->implode(',')); 76 | while(strlen($headersText) > $sizeLimit) { 77 | $headers->pop(); 78 | $headersText = trim($headers->implode(',')); 79 | } 80 | 81 | if (!empty($headersText)) { 82 | $this->addLinkHeader($response, $headersText); 83 | } 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Get the DomCrawler instance. 90 | * 91 | * @param \Illuminate\Http\Response $response 92 | * 93 | * @return \Symfony\Component\DomCrawler\Crawler 94 | */ 95 | protected function getCrawler(Response $response) 96 | { 97 | if ($this->crawler) { 98 | return $this->crawler; 99 | } 100 | 101 | return $this->crawler = new Crawler($response->getContent()); 102 | } 103 | 104 | /** 105 | * Get all nodes we are interested in pushing. 106 | * 107 | * @param \Illuminate\Http\Response $response 108 | * 109 | * @return \Illuminate\Support\Collection 110 | */ 111 | protected function fetchLinkableNodes($response) 112 | { 113 | $crawler = $this->getCrawler($response); 114 | 115 | return collect($crawler->filter('link:not([rel*="icon"]), script[src], img[src], object[data]')->extract(['src', 'href', 'data'])); 116 | } 117 | 118 | /** 119 | * Build out header string based on asset extension. 120 | * 121 | * @param string $url 122 | * 123 | * @return string 124 | */ 125 | private function buildLinkHeaderString($url) 126 | { 127 | $linkTypeMap = [ 128 | '.CSS' => 'style', 129 | '.JS' => 'script', 130 | '.BMP' => 'image', 131 | '.GIF' => 'image', 132 | '.JPG' => 'image', 133 | '.JPEG' => 'image', 134 | '.PNG' => 'image', 135 | '.SVG' => 'image', 136 | '.TIFF' => 'image', 137 | ]; 138 | 139 | $type = collect($linkTypeMap)->first(function ($type, $extension) use ($url) { 140 | return Str::contains(strtoupper($url), $extension); 141 | }); 142 | 143 | if ($url && !$type) { 144 | $type = 'fetch'; 145 | } 146 | 147 | if(!preg_match('%^(https?:)?//%i', $url)) { 148 | $basePath = $this->getConfig('base_path', '/'); 149 | $url = $basePath . ltrim($url, $basePath); 150 | } 151 | 152 | return is_null($type) ? null : "<{$url}>; rel=preload; as={$type}"; 153 | } 154 | 155 | /** 156 | * Add Link Header 157 | * 158 | * @param \Illuminate\Http\Response $response 159 | * 160 | * @param $link 161 | */ 162 | private function addLinkHeader(Response $response, $link) 163 | { 164 | if ($response->headers->get('Link')) { 165 | $link = $response->headers->get('Link') . ',' . $link; 166 | } 167 | 168 | $response->header('Link', $link); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | __DIR__ . '/config.php' => config_path('http2serverpush.php'), 19 | ]); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | '6000', // in bytes 6 | 'base_path' => '/', 7 | 'exclude_keywords' => [] 8 | ]; 9 | --------------------------------------------------------------------------------