From e44e7cc98f423b74ccf6c05f6d3ad5a5c1666332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C4=83t=C4=83lin=20Mari=C8=99?= Date: Fri, 22 Dec 2017 04:16:14 -0800 Subject: [PATCH] New: Add rule to check if resources are compressed Fix #1 Fix #12 --- .sonarwhalrc | 1 + docs/user-guide/rules/http-compression.md | 568 ++++++++++++++ docs/user-guide/rules/index.md | 1 + package-lock.json | 13 + package.json | 1 + src/lib/connectors/utils/requester.ts | 1 + .../compression-check-options.ts | 5 + .../http-compression/http-compression.ts | 674 +++++++++++++++++ src/lib/utils/misc.ts | 6 + tests/lib/rules/http-compression/_tests.ts | 707 ++++++++++++++++++ .../http-compression/fixtures/favicon.br | Bin 0 -> 586 bytes .../http-compression/fixtures/favicon.gz | Bin 0 -> 648 bytes .../http-compression/fixtures/favicon.ico | Bin 0 -> 4286 bytes .../fixtures/favicon.zopfli.gz | Bin 0 -> 605 bytes .../rules/http-compression/fixtures/image.br | Bin 0 -> 1436 bytes .../rules/http-compression/fixtures/image.gz | Bin 0 -> 1465 bytes .../rules/http-compression/fixtures/image.png | Bin 0 -> 1432 bytes .../http-compression/fixtures/image.svgz | Bin 0 -> 1202 bytes .../http-compression/fixtures/image.zopfli.gz | Bin 0 -> 1492 bytes .../rules/http-compression/fixtures/page.br | Bin 0 -> 1149 bytes .../rules/http-compression/fixtures/page.gz | Bin 0 -> 1297 bytes .../rules/http-compression/fixtures/page.html | 55 ++ .../http-compression/fixtures/page.zopfli.gz | Bin 0 -> 1246 bytes .../http-compression/fixtures/script-small.br | 3 + .../http-compression/fixtures/script-small.gz | Bin 0 -> 85 bytes .../http-compression/fixtures/script-small.js | 2 + .../fixtures/script-small.zopfli.gz | Bin 0 -> 69 bytes .../rules/http-compression/fixtures/script.br | Bin 0 -> 1074 bytes .../rules/http-compression/fixtures/script.gz | Bin 0 -> 1134 bytes .../rules/http-compression/fixtures/script.js | 41 + .../fixtures/script.zopfli.gz | Bin 0 -> 1090 bytes .../lib/rules/http-compression/tests-http.ts | 50 ++ .../lib/rules/http-compression/tests-https.ts | 58 ++ 33 files changed, 2186 insertions(+) create mode 100644 docs/user-guide/rules/http-compression.md create mode 100644 src/lib/rules/http-compression/compression-check-options.ts create mode 100644 src/lib/rules/http-compression/http-compression.ts create mode 100644 tests/lib/rules/http-compression/_tests.ts create mode 100644 tests/lib/rules/http-compression/fixtures/favicon.br create mode 100644 tests/lib/rules/http-compression/fixtures/favicon.gz create mode 100644 tests/lib/rules/http-compression/fixtures/favicon.ico create mode 100644 tests/lib/rules/http-compression/fixtures/favicon.zopfli.gz create mode 100644 tests/lib/rules/http-compression/fixtures/image.br create mode 100644 tests/lib/rules/http-compression/fixtures/image.gz create mode 100644 tests/lib/rules/http-compression/fixtures/image.png create mode 100644 tests/lib/rules/http-compression/fixtures/image.svgz create mode 100644 tests/lib/rules/http-compression/fixtures/image.zopfli.gz create mode 100644 tests/lib/rules/http-compression/fixtures/page.br create mode 100644 tests/lib/rules/http-compression/fixtures/page.gz create mode 100644 tests/lib/rules/http-compression/fixtures/page.html create mode 100644 tests/lib/rules/http-compression/fixtures/page.zopfli.gz create mode 100644 tests/lib/rules/http-compression/fixtures/script-small.br create mode 100644 tests/lib/rules/http-compression/fixtures/script-small.gz create mode 100644 tests/lib/rules/http-compression/fixtures/script-small.js create mode 100644 tests/lib/rules/http-compression/fixtures/script-small.zopfli.gz create mode 100644 tests/lib/rules/http-compression/fixtures/script.br create mode 100644 tests/lib/rules/http-compression/fixtures/script.gz create mode 100644 tests/lib/rules/http-compression/fixtures/script.js create mode 100644 tests/lib/rules/http-compression/fixtures/script.zopfli.gz create mode 100644 tests/lib/rules/http-compression/tests-http.ts create mode 100644 tests/lib/rules/http-compression/tests-https.ts diff --git a/.sonarwhalrc b/.sonarwhalrc index 2aa92ed50af..48a4d92c099 100644 --- a/.sonarwhalrc +++ b/.sonarwhalrc @@ -17,6 +17,7 @@ "highest-available-document-mode": "warning", "html-checker": "warning", "http-cache": "warning", + "http-compression": "warning", "image-optimization-cloudinary": "warning", "manifest-app-name": "error", "manifest-exists": "warning", diff --git a/docs/user-guide/rules/http-compression.md b/docs/user-guide/rules/http-compression.md new file mode 100644 index 00000000000..218b68e4d86 --- /dev/null +++ b/docs/user-guide/rules/http-compression.md @@ -0,0 +1,568 @@ +# Require resources to be served compressed (`http-compression`) + +`http-compression` warns against not serving resources compressed when +requested as such using the most appropriate encoding. + +## Why is this important? + +One of the fastest and easiest ways one can improve the web site's/app's +performance is to reduce the amount of data that needs to get delivered +to the client by using HTTP compression. This not only [reduces the data +used by the user][wdmsc], but can also significallty cut down on the +server costs. + +However, there are a few things that need to be done right in order for +get the most out of compression: + +* Only compress resources for which the result of the compression + will be smaller than original size. + + In general text-based resources (HTML, CSS, JavaScript, SVGs, etc.) + compresss very well especially if the file is not very small. + The same goes for some other file formats (e.g.: ICO files, web fonts + such as EOT, OTF, and TTF, etc.) + + However, compressing resources that are already compressed (e.g.: + images, audio files, PDFs, etc.) not only waste CPU resources, but + usually result in little to no reduction, or in some cases even + a bigger file size. + + The same goes for resources that are very small because of the + overhead of compression file formats. + +* Use most efficien compression method. + + `gzip` is the most used encoding nowadays as it strikes a good + balance between compression ratio (as [high as 70%][gzip ratio] + especially for larger files) and encoding time, and is supported + pretty much everywhere. + + Better savings can be achieved using [`Zopfli`][zopfli] which + can reduce the size on average [3–8% more than `gzip`][zopfli + blog post]. Since `Zopfli` output (for the `gzip` option) is valid + `gzip` content, `Zopfli` works eveywere `gzip` works. The only + downsize is that encoding takes more time than with `gzip`, thus, + making `Zopfli` more suitable for static content (i.e. encoding + resources as part of build script, not on the fly). + + But, things can be improved even futher using [Brotli][brotli]. + This encoding allows to get [20–26% higher compression ratios][brotli + blog post] even over `Zopfli`. However, this encoding is not compatible + with `gzip`, limiting the support to modern browsers and its usage to + [only over HTTPS (as proxies misinterpreting unknown encodings)][brotli + over https]. + + So, in general, for best performance and interoperability resources + should be served compress with `Zopfli`, and `Brotli` over HTTPS with + a fallback to `Zopfli` if not supported HTTPS. + +* Avoid using deprecated or not widlly supported compression formats, + and `Content-Type` values. + + Avoid using deprecated `Content-Type` values such as `x-gzip`. Some + user agents may alias them to the correct, current equivalent value + (e.g.: alias `x-gzip` to `gzip`), but that is not always true. + + Also avoid using encoding that are not widely supported (e.g.: + `compress`, `bzip2`, [`SDCH`][unship sdch], etc.), and/or may not + be as efficient, or can create problems (e.g.: [`deflate`][deflate + issues]). In general these should be avoided, and one should just + stick to the encoding specified in the previous point. + +* Avoid potential caching related issues. + + When resources are served compressed, they should be served with + the `Vary` header containing the `Accept-Encoding` value (or with + something such as `Cache-Control: private` that prevents caching + in proxy caches and such altogether). + + This needs to be done in order to avoid problems such as an + intermediate proxy caching the compress version of the resource and + then sending it to all user agents regardless if they support that + particular encoding or not, or if they even want the compressed + version or not. + +* Resources should be served compressed only when requested as such, + appropriately encoded, and without relying on user agent sniffing. + + The `Accept-Encoding` request header specified should be respected. + Sending a content encoded with a different encoding than one of the + ones accepted can lead to problems. + +* Dealing with special cases. + + One such special case are `SVGZ` files that are just `SVG` files + compressed with `gzip`. Since they are already compressed, they + shouldn't be compressed again. However sending them without the + `Content-Encoding: gzip` header will create problems as user agents + will not know they need to decompress them before displaying them, + and thus, try to display them directly. + +## What does the rule check? + +The rule checks for the use cases previously specified, namely, it +checks that: + +* Only resources for which the result of the compression is smaller + than original size are served compressed. + +* The most efficient encodigs are used (by default the rule check if + `Zopfli` is used over HTTP and Brotli over `HTTPS`, however that can + be changed, see: [`Can the rule be configured?` + section](#can-the-rule-be-configured)). + +* Deprecated or not widely supported encodigs, and `Content-Type` + values are not used. + +* Potential caching related issues are avoided. + +* Resources are served compressed only when requested as such, are + appropriately encoded, and no user sniffing is done. + +* Special cases (such as `SVGZ`) are handled correctly. + +### Examples that **trigger** the rule + +Resource that should be compressed is not served compressed. + +e.g.: When the request for `https://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Type: text/javascript + + +``` + +Resource that should not be compressed is served compressed. + +e.g.: When the request for `https://example.com/example.png` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: br +Content-Type: image/png +Vary: Accept-Encoding + + +``` + +Resource that compressed results in a bigger or equal size to the +uncompressed size is still served compressed. + +e.g.: For `http://example.com/example.js` containing only `const x = 5;`, +using the defaults, the sizes may be as follows. + +```text +origina size: 13 bytes + +gzip size: 38 bytes +zopfli size: 33 bytes +brotli size: 17 bytes +``` + +When the request for `http://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: gzip +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +Resource that should be compressed is served compressed with deprecated +or disallowed compression method or `Content-Encoding` value. + +e.g.: When the request for `http://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate +``` + +response contains deprecated `x-gzip` value for `Content-Encoding` + +```text +HTTP/... 200 OK + +... +Content-Encoding: x-gzip +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +response is compressed with disallowed `compress` compression method + +```text +HTTP/... 200 OK + +... +Content-Encoding: compress +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +or response tries to use deprecated SDCH + +```text +HTTP/... 200 OK + +... +Content-Encoding: gzip, +Content-Type: text/javascript +Get-Dictionary: /dictionaries/search_dict, /dictionaries/help_dict +Vary: Accept-Encoding + + +``` + +Resource that should be compressed is not served compressed using +`Zopfli` over HTTP. + +e.g.: When the request for `http://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: gzip +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +Resource that should be compressed is served compressed using `Brotli` +over HTTP. + +e.g.: When the request for `http://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: br +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +Resource that should be compressed is not served compressed using +`Brotli` over HTTPS. + +e.g.: When the request for `https://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: gzip +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +Resource that is served compressed doesn't account for caching +(e.g: is not served with the `Vary` header with the `Accept-Encoding` +value included, or something such as `Cache-Control: private`). + +e.g.: When the request for `https://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: br +Content-Type: text/javascript + + +``` + +Resource is blindly served compressed using `gzip` no matter what the +user agent advertises as supporting. + +E.g.: When the request for `https://example.com/example.js` contains + +```text +... +Accept-Encoding: br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: gzip +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +Resource is served compressed only for certain user agents. + +E.g.: When the request for `https://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate, br +User-Agent: Mozilla/5.0 Gecko +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Type: text/javascript + + +``` + +however when requested with + +```text +... +Accept-Encoding: gzip, deflate, br +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0 +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: br +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +`SVGZ` resource is not served with `Content-Encoding: gzip` header: + +E.g.: When the request for `https://example.com/example.svgz` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Type: image/svg+xml + + +``` + +### Examples that **pass** the rule + +Resource that should be compressed is served compressed using `Zopfli` +over HTTP and with the `Vary: Accept-Encoding` header. + +e.g.: When the request for `http://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: gzip +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +Resource that should be compressed is served compressed using `Brotli` +over HTTPS and with the `Vary: Accept-Encoding` header. + +e.g.: When the request for `https://example.com/example.js` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: br +Content-Type: text/javascript +Vary: Accept-Encoding + + +``` + +Resource that should not be compressed is not served compressed. + +e.g.: When the request for `https://example.com/example.png` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Type: image/png + + +``` + +`SVGZ` resource is served with `Content-Encoding: gzip` header: + +e.g.: When the request for `https://example.com/example.svgz` contains + +```text +... +Accept-Encoding: gzip, deflate, br +``` + +response is + +```text +HTTP/... 200 OK + +... +Content-Encoding: gzip +Content-Type: image/svg+xml + + +``` + +## Can the rule be configured? + +You can override the defaults by specifying what type of compression +you don't want the rule to check for. This can be done for the `target` +(main page) and/or the `resources` the rule determines should be served +compressed, using the following format: + +```json +"http-compression": [ "warning", { + "resource": { + "": , + ... + }, + "target": { + "": , + ... + } +} +``` + +Where `` can be one of: `brotli`, `gzip`, or +`zopfli`. + +E.g. If you want the rule to check if only the page resources are +served compressed using `Brotli`, and not the page itself, you can +use the following configuration: + +```json +"http-compression": [ "warning", { + "target": { + "brotli": false + } +}] +``` + +Note: You can also use the [`ignoredUrls`](../index.md#rule-configuration) +property from the `.sonarwhalrc` file to exclude domains you don’t control +(e.g.: CDNs) from these checks. + + + +[brotli blog post]: https://opensource.googleblog.com/2015/09/introducing-brotli-new-compression.html +[brotli over https]: https://medium.com/@yoavweiss/well-the-technical-reason-for-brotli-being-https-only-is-that-otherwise-there-s-a-very-high-508f15f0ad95 +[brotli]: https://github.com/google/brotli +[deflate issues]: https://stackoverflow.com/questions/9170338/why-are-major-web-sites-using-GZIP/9186091#9186091 +[gzip is not enough]: https://www.youtube.com/watch?v=whGwm0Lky2s +[gzip ratio]: https://www.youtube.com/watch?v=Mjab_aZsdxw&t=24s +[unship sdch]: https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/nQl0ORHy7sw +[wdmsc]: https://whatdoesmysitecost.com/ +[zopfli blog post]: https://developers.googleblog.com/2013/02/compress-data-more-densely-with-zopfli.html +[zopfli]: https://github.com/google/zopfli diff --git a/docs/user-guide/rules/index.md b/docs/user-guide/rules/index.md index 4ecf5a791a1..6b168d8c084 100644 --- a/docs/user-guide/rules/index.md +++ b/docs/user-guide/rules/index.md @@ -23,6 +23,7 @@ * [`amp-validator`](amp-validator.md) * [`http-cache`](http-cache.md) +* [`http-compression`](http-compression.md) * [`image-optimization-cloudinary`](image-optimization-cloudinary.md) * [`no-html-only-headers`](no-html-only-headers.md) * [`no-http-redirects`](no-http-redirects.md) diff --git a/package-lock.json b/package-lock.json index 35c564122a2..e01fe3deeb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2118,6 +2118,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + }, "bcrypt-pbkdf": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", @@ -2267,6 +2272,14 @@ "repeat-element": "1.1.2" } }, + "brotli": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz", + "integrity": "sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y=", + "requires": { + "base64-js": "1.2.1" + } + }, "browserslist": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.0.tgz", diff --git a/package.json b/package.json index 693256fc866..ccc4fadb823 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "amphtml-validator": "^1.0.21", "axe-core": "^2.6.1", + "brotli": "^1.3.2", "browserslist": "^2.11.0", "caniuse-api": "^2.0.0", "canvas-prebuilt": "^1.6.0", diff --git a/src/lib/connectors/utils/requester.ts b/src/lib/connectors/utils/requester.ts index 27c3ff7c2f3..38e3ff687a1 100644 --- a/src/lib/connectors/utils/requester.ts +++ b/src/lib/connectors/utils/requester.ts @@ -48,6 +48,7 @@ export class Requester { public constructor(customOptions?: request.CoreOptions) { if (customOptions) { customOptions.followRedirect = false; + customOptions.rejectUnauthorized = false; this._maxRedirects = customOptions.maxRedirects || this._maxRedirects; } const options: request.CoreOptions = Object.assign({}, defaults, customOptions); diff --git a/src/lib/rules/http-compression/compression-check-options.ts b/src/lib/rules/http-compression/compression-check-options.ts new file mode 100644 index 00000000000..fbdebd206b0 --- /dev/null +++ b/src/lib/rules/http-compression/compression-check-options.ts @@ -0,0 +1,5 @@ +export type CompressionCheckOptions = { + brotli: boolean; + gzip: boolean; + zopfli: boolean; +}; diff --git a/src/lib/rules/http-compression/http-compression.ts b/src/lib/rules/http-compression/http-compression.ts new file mode 100644 index 00000000000..1257d2920bf --- /dev/null +++ b/src/lib/rules/http-compression/http-compression.ts @@ -0,0 +1,674 @@ +/** + * @fileoverview Check if resources are served compressed when requested + * as such using the most appropriate encoding. + */ + +/* + * ------------------------------------------------------------------------------ + * Requirements + * ------------------------------------------------------------------------------ + */ + +import * as decompressBrotli from 'brotli/decompress'; +import * as mimeDB from 'mime-db'; + +import { Category } from '../../enums/category'; +import { getFileExtension, isTextMediaType } from '../../utils/content-type'; +import { getHeaderValueNormalized, isHTTP, normalizeString } from '../../utils/misc'; +import { IAsyncHTMLElement, IResponse, IRule, IRuleBuilder, IFetchEnd } from '../../types'; +import { RuleContext } from '../../rule-context'; +import { CompressionCheckOptions } from './compression-check-options'; + +const uaString = 'Mozilla/5.0 Gecko'; + +/* + * ------------------------------------------------------------------------------ + * Public + * ------------------------------------------------------------------------------ + */ + +const rule: IRuleBuilder = { + create(context: RuleContext): IRule { + + const getRuleOptions = (property: string): CompressionCheckOptions => { + return Object.assign( + {}, + { + brotli: true, + gzip: true, + zopfli: true + }, + (context.ruleOptions && context.ruleOptions[property]) + ); + }; + + const resourceOptions: CompressionCheckOptions = getRuleOptions('resource'); + const targetOptions: CompressionCheckOptions = getRuleOptions('target'); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + const checkIfBytesMatch = (rawResponse: Buffer, magicNumbers) => { + return rawResponse && magicNumbers.every((b, i) => { + return rawResponse[i] === b; + }); + }; + + const getHeaderValues = (headers, headerName) => { + return (getHeaderValueNormalized(headers, headerName) || '').split(','); + }; + + const checkVaryHeader = async (resource, element, headers) => { + const varyHeaderValues = getHeaderValues(headers, 'vary'); + const cacheControlValues = getHeaderValues(headers, 'cache-control'); + + if (!cacheControlValues.includes('private') && + !varyHeaderValues.includes('accept-encoding')) { + await context.report(resource, element, `Should be served with the 'Vary' header containing 'Accept-Encoding' value.`); + } + }; + + const generateDisallowedCompressionMessage = (encoding: string) => { + return `Disallowed compression method: '${encoding}'.`; + }; + + const generateContentEncodingMessage = (encoding: string, notRequired?: boolean, suffix?: string) => { + return `Should${notRequired ? ' not' : ''} be served with the 'content-encoding${encoding ? `: ${encoding}`: ''}' header${suffix ? ` ${suffix}` : ''}.`; + }; + + const generateCompressionMessage = (encoding: string, notRequired?: boolean, suffix?: string) => { + return `Should${notRequired ? ' not' : ''} be served compressed${encoding ? ` with ${encoding}` : ''}${suffix ? ` ${suffix}` : ''}.`; + }; + + const generateSizeMessage = async (resource: string, element: IAsyncHTMLElement, encoding: string, sizeDifference) => { + await context.report(resource, element, `Should not be served compressed with ${encoding} as the compressed size is ${sizeDifference > 0 ? 'bigger than' : 'the same size as'} the uncompressed one.`); + }; + + const getNetworkData = async (resource: string, requestHeaders) => { + const networkData = await context.fetchContent(resource, requestHeaders); + + return { + contentEncodingHeaderValue: getHeaderValueNormalized(networkData.response.headers, 'content-encoding'), + rawContent: networkData.response.body.rawContent, + rawResponse: await networkData.response.body.rawResponse(), + response: networkData.response + }; + }; + + const isCompressedWithBrotli = (rawResponse: Buffer): boolean => { + + /* + * Brotli doesn't currently contain any magic numbers. + * https://github.com/google/brotli/issues/298#issuecomment-172549140 + */ + + try { + const decompressedContent = decompressBrotli(rawResponse); + + if (decompressedContent.byteLength === 0 && + rawResponse.byteLength !== 0) { + + return false; + } + } catch (e) { + return false; + } + + return true; + }; + + const isCompressedWithGzip = (rawContent: Buffer): boolean => { + // See: https://tools.ietf.org/html/rfc1952#page-5. + return checkIfBytesMatch(rawContent, [0x1f, 0x8b]); + }; + + const isNotCompressedWithZopfli = (rawResponse: Buffer): boolean => { + + /* + * Since the Zopfli output (for the gzip option) is valid + * gzip content, there doesn't seem to be a straightforward + * and foolproof way to identify files compressed with Zopfli. + * + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * + * From an email discussion with @lvandeve: + * + * " There is no way to tell for sure. Adding information + * to the output to indicate zopfli, would actually + * add bits to the output so such thing is not done :) + * Any compressor can set the FLG, MTIME, and so on + * to anything it wants, and users of zopfli can also + * change the MTIME bytes that zopfli had output to an + * actual time. + * + * One heuristic to tell that it was compressed with + * zopfli or another dense deflate compressor is to + * compress it with regular gzip -9 (which is fast), + * and compare that the size of the file to test is + * for example more than 3% smaller. " + * + * Using the above mentioned for every resource `sonarwhal` + * encounters can be expensive, plus, for the online scanner, + * it might also cause some security (?) problems. + * + * So, since this is not a foolproof way to identify files + * compressed with Zopfli, the following still not foolproof, + * but faster way is used. + * + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * + * 1. gzip + * + * A gzip member header has the following structure + * + * +---+---+---+---+---+---+---+---+---+---+ + * |ID1|ID2|CM |FLG| MTIME |XFL|OS | (more-->) + * +---+---+---+---+---+---+---+---+---+---+ + * + * where: + * + * ID1 = 1f and ID2 = 8b - these are the magic + * numbers that uniquely identify the content + * as being gzip. + * + * CM = 8 - this is a value customarily used by gzip + * + * FLG and MTIME are usually non-zero values. + * + * XFL will be either 0, 2, or 4: + * + * 0 - default, compressor used intermediate levels + * of compression (when any of the -2 ... -8 + * options are used). + * + * 2 - the compressor used maximum compression, + * slowest algorithm (when the -9 or --best + * option is used). + * + * 4 - the compressor used fastest algorithm (when + * the -1 or --fast option is used). + * + * 2. Zopfli + * + * One thing that Zopfli does is that it sets FLG and + * MTIME to 0, XFL to 2, and OS to 3 [1], so basically + * files compressed with Zopfli will most likely start + * with `1f8b 0800 0000 0000 0203`, unless things are + * changed by the user (which in general doesn't seem + * very likely to happen). + * + * Now, regular gzip output might also start with that, + * even thought the chance of doing so is smaller: + * + * * Most web servers (e.g.: Apache², NGINX³), by default, + * will not opt users into the best compression level, + * therefore, the output shouldn't have XFL set to 2. + * + * * Most utilities that output regular gzip will have + * non-zero values for MTIME and FLG. + * + * So, if a file does not start with: + * + * `1f8b 0800 0000 0000 0203` + * + * it's a good (not perfect) indication that Zopfli wasn't + * used, but it's a fast check compared to compressing + * files and comparing file sizes. However, if a file does + * start with that, it can be either Zopfli or gzip, and + * we cannot really make assumptions here. + * + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * + * Ref: + * + * ¹ https://github.com/google/zopfli/blob/6818a0859063b946094fb6f94732836404a0d89a/src/zopfli/gzip_container.c#L90-L101) + * ² https://httpd.apache.org/docs/current/mod/mod_deflate.html#DeflateCompressionLevel + * ³ https://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_comp_level + */ + + return !checkIfBytesMatch(rawResponse, [0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03]); + }; + + const checkBrotli = async (resource, element) => { + const { contentEncodingHeaderValue, rawResponse, response } = await getNetworkData(resource, { 'Accept-Encoding': 'br' }); + + const compressedWithBrotli = isCompressedWithBrotli(rawResponse); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check if compressed with Brotli over HTTP. + + if (isHTTP(resource)) { + if (compressedWithBrotli) { + await context.report(resource, element, generateCompressionMessage('Brotli', true, 'over HTTP')); + } + + return; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check compressed vs. uncompressed sizes. + + /* + * TODO: Remove the following once connectors + * support Brotli compression. + */ + const rawContent = compressedWithBrotli ? decompressBrotli(rawResponse) : response.body.rawContent; + + const itShouldNotBeCompressed = contentEncodingHeaderValue === 'br' && + rawContent.byteLength <= rawResponse.byteLength; + + if (compressedWithBrotli && itShouldNotBeCompressed) { + generateSizeMessage(resource, element, 'Brotli', rawResponse.byteLength - rawContent.byteLength); + + return; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check if compressed. + + if (!compressedWithBrotli) { + await context.report(resource, element, generateCompressionMessage('Brotli', false, 'over HTTPS')); + + return; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check related headers. + + await checkVaryHeader(resource, element, response.headers); + + if (contentEncodingHeaderValue !== 'br') { + await context.report(resource, element, generateContentEncodingMessage('br')); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check for user agent sniffing. + + const { rawResponse: uaRawResponse } = await getNetworkData(resource, { + 'Accept-Encoding': 'br', + 'User-Agent': uaString + }); + + if (!isCompressedWithBrotli(uaRawResponse)) { + await context.report(resource, element, generateCompressionMessage('Brotli', false, `over HTTPS regardless of the user agent`)); + } + }; + + const checkGzipZopfli = async (resource: string, element: IAsyncHTMLElement, shouldCheckIfCompressedWith: CompressionCheckOptions) => { + const { contentEncodingHeaderValue, rawContent, rawResponse, response } = await getNetworkData(resource, { 'Accept-Encoding': 'gzip' }); + + const compressedWithGzip = isCompressedWithGzip(rawResponse); + const notCompressedWithZopfli = isNotCompressedWithZopfli(rawResponse); + const itShouldNotBeCompressed = contentEncodingHeaderValue === 'gzip' && + rawContent.byteLength <= rawResponse.byteLength; + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check compressed vs. uncompressed sizes. + + if (compressedWithGzip && itShouldNotBeCompressed) { + generateSizeMessage(resource, element, notCompressedWithZopfli ? 'gzip' : 'Zopfli', rawResponse.byteLength - rawContent.byteLength); + + return; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check if compressed. + + if (!compressedWithGzip && shouldCheckIfCompressedWith.gzip) { + await context.report(resource, element, generateCompressionMessage('gzip')); + + return; + } + + if (notCompressedWithZopfli && shouldCheckIfCompressedWith.zopfli) { + await context.report(resource, element, generateCompressionMessage('Zopfli')); + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check related headers. + + if (shouldCheckIfCompressedWith.gzip || + shouldCheckIfCompressedWith.zopfli) { + await checkVaryHeader(resource, element, response.headers); + + if (contentEncodingHeaderValue !== 'gzip') { + await context.report(resource, element, generateContentEncodingMessage('gzip')); + } + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // Check for user agent sniffing. + + const { rawResponse: uaRawResponse } = await getNetworkData(resource, { + 'Accept-Encoding': 'gzip', + 'User-Agent': uaString + }); + + if (!isCompressedWithGzip(uaRawResponse) && + shouldCheckIfCompressedWith.gzip) { + await context.report(resource, element, generateCompressionMessage('gzip', false, `regardless of the user agent`)); + + return; + } + + if (isNotCompressedWithZopfli(uaRawResponse) && + !notCompressedWithZopfli && + shouldCheckIfCompressedWith.zopfli) { + await context.report(resource, element, generateCompressionMessage('Zopfli', false, `regardless of the user agent`)); + } + + }; + + const responseIsCompressed = (rawResponse: Buffer, contentEncodingHeaderValue: string): boolean => { + return isCompressedWithGzip(rawResponse) || + isCompressedWithBrotli(rawResponse) || + + /* + * Other compression methods may be used, but there is + * no way to check for all possible cases. So, if this + * point is reached, just consider 'content-encoding' + * header as a possible indication of the response being + * compressed. + */ + + (contentEncodingHeaderValue && + + /* + * Although `identity` should not be sent as + * a value for `content-encoding`, if sent, for + * the * scope of this function, just ignore it + * and consider no encoding was specified. + * + * From (now kinda obsolete) + * https://tools.ietf.org/html/rfc2616#page-24: + * + * " identity + * + * The default (identity) encoding; the use of no + * transformation whatsoever. This content-coding + * is used only in the Accept-Encoding header, and + * SHOULD NOT be used in the Content-Encoding header. " + * + * See also: http://httpwg.org/specs/rfc7231.html#content.coding.registration + */ + + (contentEncodingHeaderValue !== 'identity')); + }; + + const checkForDisallowedCompressionMethods = async (resource: string, element: IAsyncHTMLElement, response: IResponse) => { + + // See: https://www.iana.org/assignments/http-parameters/http-parameters.xml. + + const contentEncodingHeaderValue = getHeaderValueNormalized(response.headers, 'content-encoding'); + + if (!contentEncodingHeaderValue) { + return; + } + + const encodings = contentEncodingHeaderValue.split(','); + + for (const encoding of encodings) { + if (!['gzip', 'br'].includes(encoding)) { + + /* + * `x-gzip` is deprecated but usually user agents + * alias it to `gzip`, so if the content is actual + * `gzip`, don't trigger an error here as the gzip + * related check will show an error for the response + * not being served with `content-encoding: gzip`. + */ + + if (encoding === 'x-gzip' && isCompressedWithGzip(await response.body.rawResponse())) { + return; + } + + // For anything else just flag it as disallowed. + await context.report(resource, element, generateDisallowedCompressionMessage(encoding)); + } + } + + /* + * Special cases: + * + * * SDCH (Shared Dictionary Compression over HTTP) + * https://lists.w3.org/Archives/Public/ietf-http-wg/2008JulSep/att-0441/Shared_Dictionary_Compression_over_HTTP.pdf + * Theoretically this should only happen if the user + * agent advertises support for SDCH, but yet again, + * server might be misconfigured. + * + * For SDCH, the first response will not contain anything + * special regarding the `content-encoding` header, however, + * it will contain the `get-dictionary` header. + */ + + if (normalizeString(response.headers['get-dictionary'])) { + await context.report(resource, element, generateDisallowedCompressionMessage('sdch')); + } + }; + + const checkUncompressed = async (resource: string, element: IAsyncHTMLElement) => { + + /* + * From: http://httpwg.org/specs/rfc7231.html#header.accept-encoding + * + * " An "identity" token is used as a synonym for + * "no encoding" in order to communicate when no + * encoding is preferred. + * + * ... + * + * If no Accept-Encoding field is in the request, + * any content-coding is considered acceptable by + * the user agent. " + */ + + const { contentEncodingHeaderValue, rawResponse } = await getNetworkData(resource, { 'Accept-Encoding': 'identity' }); + + if (responseIsCompressed(rawResponse, contentEncodingHeaderValue)) { + await context.report(resource, element, generateCompressionMessage('', true, `for requests made with 'Accept-Encoding: identity'`)); + } + + if (contentEncodingHeaderValue) { + await context.report(resource, element, generateContentEncodingMessage('', true, `for requests made with 'Accept-Encoding: identity'`)); + } + }; + + const isCompressibleAccordingToMediaType = (mediaType: string): boolean => { + const COMMON_MEDIA_TYPES_THAT_SHOULD_BE_COMPRESSED = [ + 'image/x-icon', + 'image/bmp' + ]; + + const COMMON_MEDIA_TYPES_THAT_SHOULD_NOT_BE_COMPRESSED = [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'font/woff2', + 'font/woff', + 'font/otf', + 'font/ttf' + ]; + + if (!mediaType) { + return false; + } + + /* + * The reason for doing the following is because + * `mime-db` is quite big, so querying it for + * eveything is expensive. + */ + + /* + * Check if the media type is one of the common + * ones for which it is known the response should + * be compressed. + */ + + if (isTextMediaType(mediaType) || + COMMON_MEDIA_TYPES_THAT_SHOULD_BE_COMPRESSED.includes(mediaType)) { + return true; + } + + /* + * Check if the media type is one of the common + * ones for which it is known the response should + * not be compressed. + */ + + if (COMMON_MEDIA_TYPES_THAT_SHOULD_NOT_BE_COMPRESSED.includes(mediaType)) { + return false; + } + + /* + * If the media type is not included in any of the + * above, check `mime-db`. + */ + + const typeInfo = mimeDB[mediaType]; + + return typeInfo && typeInfo.compressible; + + }; + + const isSpecialCase = async (resource: string, element: IAsyncHTMLElement, response: IResponse): Promise => { + + /* + * Check for special cases: + * + * * Files that are by default compressed with gzip. + * + * SVGZ files are by default compressed with gzip, so + * by not sending them with the `Content-Encoding: gzip` + * header, browsers will not be able to display them + * correctly. + */ + + if ((response.mediaType === 'image/svg+xml' || getFileExtension(resource) === 'svgz') && + isCompressedWithGzip(await response.body.rawResponse())) { + + if (getHeaderValueNormalized(response.headers, 'content-encoding') !== 'gzip') { + await context.report(resource, element, generateContentEncodingMessage('gzip')); + } + + return true; + } + + return false; + }; + + const validate = (shouldCheckIfCompressedWith: CompressionCheckOptions) => { + return async (fetchEnd: IFetchEnd) => { + const { element, resource, response }: { element: IAsyncHTMLElement, resource: string, response: IResponse } = fetchEnd; + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + /* + * Check if this is a special case, and if it is, do the + * specific checks, but ignore all the checks that follow. + */ + + if (await isSpecialCase(resource, element, response)) { + return; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // If the resource should not be compressed: + if (!isCompressibleAccordingToMediaType(response.mediaType)) { + + const rawResponse = await response.body.rawResponse(); + const contentEncodingHeaderValue = getHeaderValueNormalized(response.headers, 'content-encoding'); + + // * Check if the resource is actually compressed. + if (responseIsCompressed(rawResponse, contentEncodingHeaderValue)) { + await context.report(resource, element, generateCompressionMessage('', true)); + } + + // * Check if resource is sent with the `Content-Encoding` header. + if (contentEncodingHeaderValue) { + await context.report(resource, element, `Should not be served with the 'content-encoding' header.`); + } + + return; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + /* + * If the resource should be compressed: + * + * * Check if the resource is sent compressed with an + * deprecated or not recommended compression method. + */ + + await checkForDisallowedCompressionMethods(resource, element, response); + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + /* + * * Check if it's actually compressed and served in + * the correct/required compressed format. + * + * Note: Checking if servers respect the qvalue + * is beyond of the scope for the time being, + * so the followings won't check that. + */ + + await checkUncompressed(resource, element); + + if (shouldCheckIfCompressedWith.gzip || + shouldCheckIfCompressedWith.zopfli) { + await checkGzipZopfli(resource, element, shouldCheckIfCompressedWith); + } + + if (shouldCheckIfCompressedWith.brotli) { + await checkBrotli(resource, element); + } + }; + }; + + return { + 'fetch::end': validate(resourceOptions), + 'manifestfetch::end': validate(resourceOptions), + 'targetfetch::end': validate(targetOptions) + }; + }, + meta: { + docs: { + category: Category.performance, + description: 'Require resources to be served compressed' + }, + recommended: true, + schema: [{ + additionalProperties: false, + definitions: { + options: { + additionalProperties: false, + minProperties: 1, + properties: { + brotli: { type: 'boolean' }, + gzip: { type: 'boolean' }, + zopfli: { type: 'boolean' } + } + } + }, + properties: { + resource: { $ref: '#/definitions/options' }, + target: { $ref: '#/definitions/options' } + }, + type: 'object' + }], + worksWithLocalFiles: false + } +}; + +export default rule; diff --git a/src/lib/utils/misc.ts b/src/lib/utils/misc.ts index fe450f0a3f4..39112767ace 100644 --- a/src/lib/utils/misc.ts +++ b/src/lib/utils/misc.ts @@ -94,6 +94,11 @@ const isHTMLDocument = (targetURL: string, responseHeaders: object): boolean => return mediaType === 'text/html'; }; +/** Convenience function to check if a resource is served over HTTP. */ +const isHTTP = (resource: string): boolean => { + return hasProtocol(resource, 'http:'); +}; + /** Convenience function to check if a resource is served over HTTPS. */ const isHTTPS = (resource: string): boolean => { return hasProtocol(resource, 'https:'); @@ -279,6 +284,7 @@ export { isDirectory, isFile, isHTMLDocument, + isHTTP, isHTTPS, isLocalFile, isRegularProtocol, diff --git a/tests/lib/rules/http-compression/_tests.ts b/tests/lib/rules/http-compression/_tests.ts new file mode 100644 index 00000000000..f27e34d6184 --- /dev/null +++ b/tests/lib/rules/http-compression/_tests.ts @@ -0,0 +1,707 @@ +import * as fs from 'fs'; + +import { IRuleTest } from '../../../helpers/rule-test-type'; + +const uaString = 'Mozilla/5.0 Gecko'; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// Error messages. + +const generateCompressionMessage = (encoding?: string, notRequired?: boolean, suffix?: string) => { + return `Should${notRequired ? ' not' : ''} be served compressed${encoding ? ` with ${encoding}` : ''}${suffix ? ` ${suffix}` : ''}.`; +}; + +const generateContentEncodingMessage = (encoding?: string, notRequired?: boolean, suffix?: string) => { + return `Should${notRequired ? ' not' : ''} be served with the 'content-encoding${encoding ? `: ${encoding}`: ''}' header${suffix ? ` ${suffix}` : ''}.`; +}; + +const generateDisallowedCompressionMessage = (encoding: string) => { + return `Disallowed compression method: '${encoding}'.`; +}; + +const generateSizeMessage = (encoding: string, differentSize: boolean) => { + return `Should not be served compressed with ${encoding} as the compressed size is ${differentSize ? 'bigger than' : 'the same size as'} the uncompressed one.`; +}; + +const generateUnneededContentEncodingMessage = (encoding?: string) => { + return `Should not be served with the 'content-encoding' header${encoding ? ` for requests made with 'Accept-Encoding: ${encoding}'` : ''}.`; +}; + +const varyMessage = `Should be served with the 'Vary' header containing 'Accept-Encoding' value.`; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// Files. + +/* eslint-disable no-sync */ +const faviconFile = { + brotli: fs.readFileSync(`${__dirname}/fixtures/favicon.br`), + gzip: fs.readFileSync(`${__dirname}/fixtures/favicon.gz`), + original: fs.readFileSync(`${__dirname}/fixtures/favicon.ico`), + zopfli: fs.readFileSync(`${__dirname}/fixtures/favicon.zopfli.gz`) +}; + +const htmlFile = { + brotli: fs.readFileSync(`${__dirname}/fixtures/page.br`), + gzip: fs.readFileSync(`${__dirname}/fixtures/page.gz`), + original: fs.readFileSync(`${__dirname}/fixtures/page.html`), + zopfli: fs.readFileSync(`${__dirname}/fixtures/page.zopfli.gz`) +}; + +const imageFile = { + brotli: fs.readFileSync(`${__dirname}/fixtures/image.br`), + gzip: fs.readFileSync(`${__dirname}/fixtures/image.gz`), + original: fs.readFileSync(`${__dirname}/fixtures/image.png`), + zopfli: fs.readFileSync(`${__dirname}/fixtures/image.zopfli.gz`) +}; + +const scriptFile = { + brotli: fs.readFileSync(`${__dirname}/fixtures/script.br`), + gzip: fs.readFileSync(`${__dirname}/fixtures/script.gz`), + original: fs.readFileSync(`${__dirname}/fixtures/script.js`), + zopfli: fs.readFileSync(`${__dirname}/fixtures/script.zopfli.gz`) +}; + +const scriptSmallFile = { + brotli: fs.readFileSync(`${__dirname}/fixtures/script-small.br`), + gzip: fs.readFileSync(`${__dirname}/fixtures/script-small.gz`), + original: fs.readFileSync(`${__dirname}/fixtures/script-small.js`), + zopfli: fs.readFileSync(`${__dirname}/fixtures/script-small.zopfli.gz`) +}; + +const svgzFile = fs.readFileSync(`${__dirname}/fixtures/image.svgz`); +/* eslint-enable no-sync */ + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// Server configs. + +const createConfig = ({ + faviconFileContent = faviconFile.zopfli, + faviconFileHeaders = { + 'Content-Encoding': 'gzip', + Vary: 'Accept-Encoding' + }, + htmlFileContent = htmlFile.zopfli, + htmlFileHeaders = { + 'Content-Encoding': 'gzip', + Vary: 'Accept-Encoding' + }, + imageFileContent = imageFile.original, + imageFileHeaders = {}, + request = { headers: { 'Accept-Encoding': 'gzip, deflate, br' }}, + scriptFileContent = scriptFile.zopfli, + scriptFileHeaders = { + 'Content-Encoding': 'gzip', + Vary: 'Accept-Encoding' + }, + svgzFileContent = svgzFile, + svgzFileHeaders = { 'Content-Encoding': 'gzip' } +} = {}) => { + return { + [JSON.stringify({ request })]: { + '/': { + content: htmlFileContent, + headers: Object.assign({ 'Content-Type': 'text/html; charset=utf-8' }, htmlFileHeaders) + }, + '/favicon.ico': { + content: faviconFileContent, + headers: Object.assign({ 'Content-Type': 'image/x-icon' }, faviconFileHeaders) + }, + '/image.png': { + content: imageFileContent, + headers: Object.assign({ 'Content-Type': 'image/png' }, imageFileHeaders) + }, + '/image.svgz': { + content: svgzFileContent, + headers: Object.assign({ 'Content-Type': 'image/svg+xml' }, svgzFileHeaders) + }, + '/script.js': { + content: scriptFileContent, + headers: Object.assign({ 'Content-Type': 'text/javascript; charset=utf-8' }, scriptFileHeaders) + } + } + }; +}; + +const brotliConfigs = { + faviconFileContent: faviconFile.brotli, + faviconFileHeaders: { + 'Content-Encoding': 'br', + Vary: 'Accept-Encoding' + }, + htmlFileContent: htmlFile.brotli, + htmlFileHeaders: { + 'Content-Encoding': 'br', + Vary: 'Accept-Encoding' + }, + request: { headers: { 'Accept-Encoding': 'br' } }, + scriptFileContent: scriptFile.brotli, + scriptFileHeaders: { + 'Content-Encoding': 'br', + Vary: 'Accept-Encoding' + } +}; + +const noCompressionConfigs = { + faviconFileContent: faviconFile.original, + faviconFileHeaders: { + 'Content-Encoding': null, + Vary: null + }, + htmlFileContent: htmlFile.original, + htmlFileHeaders: { + 'Content-Encoding': null, + Vary: null + }, + request: { headers: { 'Accept-Encoding': 'identity' } }, + scriptFileContent: scriptFile.original, + scriptFileHeaders: { + 'Content-Encoding': null, + Vary: null + } +}; + +const createGzipZopfliConfigs = (configs = {}) => { + return Object.assign( + // Accept-Encoding: gzip + createConfig(Object.assign({ request: { headers: { 'Accept-Encoding': 'gzip' } } }, configs)), + + // Accept-Encoding: gzip, deflate (jsdom) + createConfig(Object.assign({ request: { headers: { 'Accept-Encoding': 'gzip, deflate' } } }, configs)), + + // Accept-Encoding: gzip, deflate, br (chrome) + createConfig(configs) + ); +}; + +const createServerConfig = (configs = {}, https: boolean = false) => { + return Object.assign( + + // Accept-Encoding: identity + createConfig(noCompressionConfigs), + + /* + * Accept-Encoding: gzip + * Accept-Encoding: gzip, deflate (jsdom) + * Accept-Encoding: gzip, deflate, br (chrome) + */ + createGzipZopfliConfigs(configs), + + // Accept-Encoding: br + createConfig(Object.assign( + { request: { headers: { 'Accept-Encoding': 'br' } } }, + https ? Object.assign({}, brotliConfigs, configs) : configs + )) + ); +}; + +const createGzipZopfliServerConfig = (configs, https: boolean = false) => { + return Object.assign( + createServerConfig({}, https), + createGzipZopfliConfigs(configs) + ); +}; + +const createBrotliServerConfig = (configs) => { + return createGzipZopfliServerConfig( + Object.assign( + {}, + brotliConfigs, + configs + ), + true + ); +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +const testsForBrotli: Array = [ + { + name: `Resource is not served compressed with Brotli when Brotli compression is requested`, + reports: [{ message: generateCompressionMessage('Brotli', false, 'over HTTPS') }], + serverConfig: createBrotliServerConfig({ + scriptFileContent: scriptFile.original, + scriptFileHeaders: { 'Content-Encoding': null } + }) + }, + { + name: `Resource is served compressed with Brotli and without the 'Content-Encoding' header when Brotli compression is requested`, + reports: [{ message: generateContentEncodingMessage('br') }], + serverConfig: createBrotliServerConfig({ + scriptFileContent: scriptFile.brotli, + scriptFileHeaders: { + 'Content-Encoding': null, + vary: 'Accept-Encoding' + } + }) + } +]; + +const testsForBrotliOverHTTP: Array = [ + { + name: `Resource is served compressed with Brotli over HTTP`, + reports: [{ message: 'Should not be served compressed with Brotli over HTTP.' }], + serverConfig: createGzipZopfliServerConfig( + Object.assign( + { request: { headers: { 'Accept-Encoding': 'br' } } }, + { + scriptFileContent: scriptFile.brotli, + scriptFileHeaders: { + 'Content-Encoding': 'br', + vary: 'accept-encoding' + } + } + ) + ) + } +]; + +const testsForBrotliSmallSize: Array = [ + { + name: `Resource is served compressed with Brotli when Brotli compression is requested but uncompressed size is smaller the compressed size`, + reports: [{ message: generateSizeMessage('Brotli', true) }], + serverConfig: createBrotliServerConfig({ scriptFileContent: scriptSmallFile.brotli}) + } +]; + +const testsForBrotliUASniffing = (): Array => { + const headersConfig = { + request: { + headers: { + 'Accept-Encoding': 'br', + 'User-Agent': uaString + } + } + }; + + return [ + { + name: `Resource is not served compressed with Brotli when Brotli compression is requested with uncommon user agent string`, + reports: [{ message: generateCompressionMessage('Brotli', false, `over HTTPS regardless of the user agent`) }], + serverConfig: createBrotliServerConfig( + Object.assign( + headersConfig, + { + scriptFileContent: scriptFile.original, + scriptFileHeaders: { 'Content-Encoding': null} + } + ) + ) + } + ]; +}; + +const testsForDefaults = (https: boolean = false): Array => { + return [ + { + name: `Only resources that should be served compressed are served compressed`, + serverConfig: createServerConfig({}, https) + }, + + // Compression is applied to resources that should not be compressed.. + + { + name: `Resource that should not be served compressed is served compressed.`, + reports: [{ message: generateCompressionMessage('', true) }], + serverConfig: createGzipZopfliServerConfig( + { imageFileContent: imageFile.gzip }, + https + ) + }, + /* + * TODO: This breaks connectors. + * + * { + * name: `Resource that should not be served compressed is served with the 'Content-Encoding' header`, + * reports: [{ message: generateCompressionMessage('', true) }], + * serverConfig: createGzipZopfliServerConfig( + * { imageFileHeaders: { 'content-encoding': 'gzip' }, + * https + * ) + * }, + */ + { + name: `Resource that should not be served compressed is served compressed and with the 'Content-Encoding' header`, + reports: [ + { message: generateCompressionMessage('', true) }, + { message: generateContentEncodingMessage('', true) } + ], + serverConfig: createGzipZopfliServerConfig( + { + imageFileContent: imageFile.gzip, + imageFileHeaders: { 'Content-Encoding': 'gzip'} + }, + https + ) + } + ]; +}; + +const testsForDisallowedCompressionMethods = (https: boolean = false): Array => { + return [ + { + name: `Compressed resource is served with disallowed 'Content-Encoding: x-gzip' header`, + reports: [{ message: generateContentEncodingMessage('gzip') }], + serverConfig: createGzipZopfliServerConfig( + { + scriptFileHeaders: { + 'Content-Encoding': 'x-gzip', + vary: 'Accept-Encoding' + } + }, + https + ) + }, + { + name: `Compressed resource is served with disallowed 'Content-Encoding: x-compress' header`, + reports: [ + { message: generateDisallowedCompressionMessage('x-compress') }, + { message: generateCompressionMessage('gzip') } + ], + serverConfig: createGzipZopfliServerConfig( + { + scriptFileContent: scriptFile.original, + scriptFileHeaders: { + 'Content-Encoding': 'x-compress', + vary: 'Accept-Encoding' + } + }, + https + ) + }, + { + name: `Compressed resource is served with 'Get-Dictionary' header`, + reports: [{ message: generateDisallowedCompressionMessage('sdch') }], + serverConfig: createGzipZopfliServerConfig( + { + scriptFileHeaders: { + 'Content-Encoding': 'gzip', + 'geT-dictionary': '/dictionaries/search_dict, /dictionaries/help_dict', + vary: 'Accept-Encoding' + } + }, + https + ) + } + ]; +}; + +const testsForGzipZopfli = (https: boolean = false): Array => { + return [ + { + name: `Resource is not served compressed with gzip when gzip compression is requested`, + reports: [{ message: generateCompressionMessage('gzip') }], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileContent: scriptFile.original, + scriptFileHeaders: { 'Content-Encoding': null} + }, + https + ) + }, + { + name: `Resource is served compressed with gzip and without the 'Content-Encoding' header when gzip compression is requested`, + reports: [ + { message: generateCompressionMessage('Zopfli') }, + { message: generateContentEncodingMessage('gzip') } + ], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileContent: scriptFile.gzip, + scriptFileHeaders: { + 'Content-Encoding': null, + vary: 'Accept-Encoding' + } + }, + https + ) + }, + { + name: `Resource is not served compressed with Zopfli when gzip compression is requested`, + reports: [{ message: generateCompressionMessage('Zopfli') }], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileContent: scriptFile.gzip, + scriptFileHeaders: { + 'Content-Encoding': 'gzip', + vary: 'Accept-Encoding' + } + }, + https + ) + }, + { + name: `Resource is served compressed with Zopfli and without the 'Content-Encoding' header when gzip compression is requested`, + reports: [{ message: generateContentEncodingMessage('gzip') }], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileContent: scriptFile.zopfli, + scriptFileHeaders: { + 'Content-Encoding': null, + vary: 'Accept-Encoding' + } + }, + https + ) + } + ]; +}; + +const testsForGzipZopfliCaching = (https: boolean = false): Array => { + return [ + { + name: `Resource is served compressed with Zopfli and without the 'Vary' or 'Cache-Control' header when gzip compression is requested`, + reports: [{ message: varyMessage }], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileHeaders: { + 'Cache-control': null, + 'Content-Encoding': 'gzip', + vary: null + } + }, + https + ) + }, + { + name: `Resource is served compressed with Zopfli and with 'Cache-Control: private, max-age=0' header when gzip compression is requested`, + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileHeaders: { + 'Cache-Control': 'private, max-age=0', + 'Content-Encoding': 'gzip', + vary: null + } + }, + https + ) + }, + { + name: `Resource is served compressed with Zopfli and with 'Vary: user-agent' header when gzip compression is requested`, + reports: [{ message: varyMessage }], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileHeaders: { + 'Cache-Control': null, + 'Content-Encoding': 'gzip', + vary: 'user-agent' + } + }, + https + ) + }, + { + name: `Resource is served compressed with Zopfli and with 'Vary: user-agent, Accept-encoding' header when gzip compression is requested`, + reports: [{ message: varyMessage }], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileHeaders: { + 'Content-Encoding': 'gzip', + vary: 'user-agent, Accept-encoding' + } + }, + https + ) + } + ]; +}; + +const testsForGzipZopfliSmallSize = (https: boolean = false): Array => { + return [ + { + name: `Resource is served compressed with gzip when gzip compression is requested but uncompressed size is smaller the compressed size`, + reports: [{ message: generateSizeMessage('gzip', true) }], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileContent: scriptSmallFile.gzip + }, + https + ) + }, + { + name: `Resource is served compressed with Zopfli when gzip compression is requested but uncompressed size is smaller the compressed size`, + reports: [{ message: generateSizeMessage('Zopfli', true) }], + serverConfig: createGzipZopfliServerConfig( + { + request: { headers: { 'Accept-Encoding': 'gzip' }}, + scriptFileContent: scriptSmallFile.zopfli + }, + https + ) + } + ]; +}; + +const testsForGzipZopfliUASniffing = (https: boolean = false): Array => { + const headersConfig = { + request: { + headers: { + 'Accept-Encoding': 'gzip', + 'User-Agent': uaString + } + } + }; + + return [ + { + name: `Resource is not served compressed with gzip when gzip compression is requested with uncommon user agent string`, + reports: [{ message: generateCompressionMessage('gzip', false, `regardless of the user agent`) }], + serverConfig: createGzipZopfliServerConfig( + Object.assign( + headersConfig, + { + scriptFileContent: scriptFile.original, + scriptFileHeaders: { 'Content-Encoding': null} + } + ), + https + ) + }, + { + name: `Resource is not served compressed with Zopfli when Zopfli compression is requested with uncommon user agent string`, + reports: [{ message: generateCompressionMessage('Zopfli', false, `regardless of the user agent`) }], + serverConfig: createGzipZopfliServerConfig( + Object.assign( + headersConfig, + { + scriptFileContent: scriptFile.gzip, + scriptFileHeaders: { 'Content-Encoding': 'gzip'} + } + ), + https + ) + } + ]; +}; + +const testsForNoCompression = (https: boolean = false): Array => { + return [ + { + name: `Resource is served compressed when requested uncompressed`, + reports: [ + { message: `Should not be served compressed for requests made with 'Accept-Encoding: identity'.` }, + { message: `Should not be served with the 'content-encoding' header for requests made with 'Accept-Encoding: identity'.` } + ], + serverConfig: createGzipZopfliServerConfig( + { + faviconFileContent: faviconFile.original, + faviconFileHeaders: { 'Content-Encoding': null }, + htmlFileContent: htmlFile.original, + htmlFileHeaders: { 'Content-Encoding': null }, + request: { headers: { 'Accept-Encoding': 'identity' }} + }, + https + ) + }, + { + name: `Resource is served uncompressed and with the 'Content-Encoding: identity' header when requested uncompressed`, + + reports: [{ message: generateUnneededContentEncodingMessage('identity') }], + serverConfig: createGzipZopfliServerConfig( + { + faviconFileContent: faviconFile.original, + faviconFileHeaders: { 'Content-Encoding': null }, + htmlFileContent: htmlFile.original, + htmlFileHeaders: { 'Content-Encoding': null }, + request: { headers: { 'Accept-Encoding': 'identity' }}, + scriptFileContent: scriptFile.original, + scriptFileHeaders: { 'Content-Encoding': 'identity'} + }, + https + ) + }, + { + name: `Resources are served uncompressed when requested uncompressed`, + serverConfig: createGzipZopfliServerConfig( + { + faviconFileContent: faviconFile.original, + faviconFileHeaders: { 'Content-Encoding': null }, + htmlFileContent: htmlFile.original, + htmlFileHeaders: { 'Content-Encoding': null }, + request: { headers: { 'Accept-Encoding': 'identity' }}, + scriptFileContent: scriptFile.original, + scriptFileHeaders: { 'Content-Encoding': null } + }, + https + ) + } + ]; +}; + +const testsForSpecialCases = (https: boolean = false): Array => { + return [ + + // SVGZ. + + { + name: `SVGZ image is served without the 'Content-Encoding: gzip' header`, + reports: [{ message: generateContentEncodingMessage('gzip') }], + serverConfig: createServerConfig({ svgzFileHeaders: { 'Content-Encoding': null } }, https) + }, + { + name: `SVGZ image is served with the wrong 'Content-Encoding: br' header`, + reports: [{ message: generateContentEncodingMessage('gzip') }], + serverConfig: createServerConfig({ svgzFileHeaders: { 'Content-Encoding': 'x-gzip' } }, https) + } + ]; +}; + +const testsForUserConfigs = (encoding, isTarget: boolean = true, https: boolean = false): Array => { + const isBrotli = encoding === 'Brotli'; + const isGzip = encoding === 'gzip'; + + const configs = { request: { headers: { 'Accept-Encoding': isBrotli ? 'br' : 'gzip' }}}; + + if (!isBrotli) { + Object.assign(configs, { request: { headers: { vary: 'Accept-encoding' }}}); + } + + Object.assign( + configs, + isTarget ? + { + htmlFileContent: isGzip ? htmlFile.zopfli: htmlFile.gzip, + htmlFileHeaders: { 'Content-Encoding': null } + } : + { + scriptFileContent: isGzip ? scriptFile.zopfli: scriptFile.gzip, + scriptFileHeaders: { 'Content-Encoding': null } + } + ); + + return [ + { + name: `${isTarget ? 'Target' : 'Resource'} is not served compressed with ${encoding} when ${isBrotli ? 'Brotli': 'gzip'} compression is requested but the user configuration allows it`, + serverConfig: isBrotli && https ? createBrotliServerConfig(configs): createGzipZopfliServerConfig(configs, https) + } + ]; +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +export { + testsForBrotli, + testsForBrotliOverHTTP, + testsForBrotliSmallSize, + testsForBrotliUASniffing, + testsForDefaults, + testsForDisallowedCompressionMethods, + testsForGzipZopfli, + testsForGzipZopfliCaching, + testsForGzipZopfliSmallSize, + testsForGzipZopfliUASniffing, + testsForNoCompression, + testsForSpecialCases, + testsForUserConfigs +}; diff --git a/tests/lib/rules/http-compression/fixtures/favicon.br b/tests/lib/rules/http-compression/fixtures/favicon.br new file mode 100644 index 0000000000000000000000000000000000000000..dde54cd6e1cff0c6d3bb4bd5287e4b0d149b28e7 GIT binary patch literal 586 zcmV-Q0=4}cy$}E%1@4|E+%n{{7@)($epu(fo1L~xQ-+x5Y<{)k*nge<@SNho4w7i# z(J-DI=hH0CXM=P+S@OnhhTI{Pqo(%~vl%gAJ?})IqBXPW0IIk|Mn;~H4=CMLcpil& z66RfK=h-MW@`bQ36|fQS-=+<7_+Pxmx&DgXdE0HCrl);KNB zg1<4LikF||f6tiP^%1W9OuNoI=Q1oJ=HQ-tzl#mdH*+=oJyj3#of@cqO$b=1I@KpW zz?~P`en&jS9^CiFk#XT%Eq^kd>qu6}4)8!DY3_pf=l<=jVk>>im2P}6j*M&P9$RN( zjxE_=3Y%aoJO@it6EK&SRZZ9^glY#y@ zhU(qIf6zr?5ZA?`j_}(T!%fGA*qdXE^>1vj&X1*C2VO9E7W--~zP>`^ZQZ43R_>?? zGy*Rz27z@(Z}PMuAWG}s8at+~VvA##^VO#T`Nap!cssWC7~g?=4CviPP;Ozarn#1^ zJGjxuV$78o?7paMq+|+E;?sqPvlo)}Hg5o*4RTsq=35nd3j$g?ULlVURzy&W z)S)SK$W%d`iw6#cE)KzfW6(h;h=ZcyV2ewK3@sVjB|{e#6iztkU?O4<6EvsM^ZhRG z${Ej^`-4)*gek`u-y{`PiT2H1o5@jfi8OEXu z5*W{~d>O(shy`)`;XV8IqZNLHE6vC;%nD)1Gs@}4*G}kGDL3Zk=9jvKSM$Jm7dCk< zFDi)vS|MJ3W;Fy}Cz9}cUoMa@$*(DTtD=DsLJSK0yR8|yKKuUjRepKnJL~Yyv+tBNgGt5C>k=o6nkNT~_AetS% z86S|ImO@YQTZch3gC=s`^gT4c4f`GTvG}W~!iMQ~X*H%vG*~2RFAz2AMD;+7K}5Aq i)GCd_YOt44PRz=cIJKWeQ(}d5@QDR zGdyhgZJr!7=7=$7M9`EO6PRybde?bnrpHYmeGUGHGw`G7?`eJfv~TBqW&cjaJG8y_ z;aDe_=l%EE=3p;k!cLp(?b=A={^L`hPiH2DtS~DqMxem^#-o+7ZSBDq{7&i%yj#y- z9>q?`Rn}M+f|&5Njy>6L%g;`tTU;MlTfg^URC|fh6XJ62wr*9ioQdNDTH&uamwLeK z`cuU2pK?LI#7lnJi{2Fh4baoIm*zf;{mwk#6S>}c@#=Ep;qp!KltQkm#)b8XsXJe; zXP@ex=QO-_Pnmjg`QZ*@j>MKKhe)J6EP2$dBeY3wA!u7{yOd;k+-AYc#6m=ce?$5YFes ze>~)O{py+@{m~Ztz-ogocAk;>UhtD^%i*5GIQJp{J86<8ZuWTO(LH~0c+rPNSzXE+T?A`YLYBy)VYD5g! z@n63g{KdUD1Z`ilIachu>lX=~8(Cmr#1{mPB=;-{yT=qYLIFe$fPk7j5R zes9kHZGP(q+EWMrir@OtANzmz1yPqb5Cq0tXc=>&Y0QC|F+(6H=>G?MT@30u@Y)#C U_zN{XH-(mwp`avqLNki$Cm>qB?f?J) literal 0 HcmV?d00001 diff --git a/tests/lib/rules/http-compression/fixtures/favicon.zopfli.gz b/tests/lib/rules/http-compression/fixtures/favicon.zopfli.gz new file mode 100644 index 0000000000000000000000000000000000000000..9b079fe7c9fe7814bb6b218aca412dc902a0ba2a GIT binary patch literal 605 zcmV-j0;2sNiwFP!000021MQXrtXxqLhNrd}6hRrMwrvD;E3*yhK!-Da=%7Fiv=Q&hcs7yIzu*Ac^^} zE~W9so3w_z4>KDcKFz7W_b9(37{|Qn>-W^b(1e|TS~u?%|G8jw>9=JYw`O`U6PZOW zn2hqli9{2*k1JV6+=wv?F5v}vg560pnI@#0X_r_gC5jS78vG&KTWhF)TDeNh- zmCsTRc74e){N+8Xus4Y8gI#aAK)xiu7K!&miGeN%KJ z@6#6Lfgf58DA3yWdx$#YvC%g3KP3JSZR3V|Kz<}f!Pak$*aGAlG5R`gYDeT=#z(V$ zLD7F0Ont;3v4wqrIn6I^9Dgw$41MIT$lKR((;EAkgC64ZzAyd94Y%!ofq?j-HvI6RHFI9;r=NR|u89~Z z`!IHDZRs2PT-*;j@ozHvCi<7n{8sD_)_aNW@{N-CUZOT^!5Dfya)veND^j~Z&_@HQ zLDiVOZ_>H4^t7YsDgM`?ddv_Lxo^s=>Z{a||L=uKk^~=%tLJG7i-o9~k*LsLqHJG@ r(t*(o5v8&vHDh^7Al-ky^5?&dk=T&sC@eC6T}fF1ufVDSz7PNaOWZ=F literal 0 HcmV?d00001 diff --git a/tests/lib/rules/http-compression/fixtures/image.br b/tests/lib/rules/http-compression/fixtures/image.br new file mode 100644 index 0000000000000000000000000000000000000000..c36571a641bd79ab550f595153d10e89a2c78107 GIT binary patch literal 1436 zcmV;N1!MY)%YunePDc$28VUda01ZhGeDH)^D&sbzy;ei#+A(;DQf13v~r$aWjV)6*J6OVuW& zY}Hy1Piv4M7QatQAx@t7^{%8=&5GR^`!F*PZCL+uY1&5Q!|k6aX6_Ed@XFnq>@@*d zvUGW~QhL5~(fMf&5?I?w#Hm8QaBg~o1hK_{k8-(8|MUfU_Ic9A^nj?Zq|D&m0=ea` z_47xUTSb>ANHH@%Ht|JE+mmg?x%+CITTEmdKzYtiZNXE*2jn24ZSTRSM9 zy-fX=|D@YI8Thhs6gNhAVBlgVyFmks4f8qx=6@Se0#Gb^_r>nNFYMd-!%u!f*KPm! z&?CmgUXemQO)T0$#EKjmd=brI4xR}3=NtgEgGKut)tc6bTZ_=SK9S3(H1L8Iys9dE zgz(9hnFn_Q!&N~xZrNv>X3Sixb@09rV&3)O(WnQn`{&NjXIe@G#$5XudG8rSYJ$Hh z265FI*}UzWL{n4SCZ)_9L>!;&Le)|tZY}r@95M!E^9NtrrpDwuO4(i_GLw4FCW2K8 zfiZUkQ4yq}dCrSU+qWx15Y^cTy#4i1ZpILs$Aqi zsx3fQM>7%$3nD@^Tmo3YXal1)9OzoeM$Mob9=VZ}bRItP6XH5t<$#SK4p=B^$f zfP8hwQ{??{CXCJ}l%!h%k&@Fy6cN)6e(;!?G?+WL5q(!PxO620YXt7^N`j??SlEMz zDy0NrXef(g$1WhQ8MBfBY<3uQ<|Hoarel`|j_Rvzl>grgwAc@C8 z5d?I%xM;*9&n;Q{=mQ-qR`%?WN+qvtZH4frwZ$aD5R}bDfR8YD4%6&ZV*H6rgj|0= zt{(jbGB*tC^9Z6GP$-}n`75PT>2%5YOMN+i;huW+1o-RLqag4iCV& za?#L;6`_M0$kBlFV~reng{(weGNiB43_^!6A0p!cPY`SU*aHS?Futhvt!aW9*;{$h q$?Eu%Hb?j+)Kynqb=5V+`WMP%t4%w0cnJUi002ovPDHLkV1fh7y`+Hv literal 0 HcmV?d00001 diff --git a/tests/lib/rules/http-compression/fixtures/image.gz b/tests/lib/rules/http-compression/fixtures/image.gz new file mode 100644 index 0000000000000000000000000000000000000000..eab261afc814b8751a02893cdc0c9a72a087f404 GIT binary patch literal 1465 zcmV;q1xETGiwFp|Vli3(18HqxXJsyMZf5`im<4D0iBL{Q4GJ0x0000DNk~Le0000o z0000o2nGNE03JVxu>b%CUr9tkRA}Dqm`iL_RT#(r-*=vOX4=wr$^b>%Dkwz^P>Dnn zFwqS$CKxuFpbLY#Fp5;;gFu7`35kZd(hyfJkU&tw0%O4F$`vueC=v(?hKCrS^f8_H zz2~@?Qqaz{JfZ+@*y6URy_C>W^the_)9vmEONT-L=`}gm0 zQwhSgT&%Nmft@#RVQT;WJ$|YqB5S!=5R1oKl5LFurZ-5kZRS1Rd3QHzq^7B5gJ6Cb z6|>VCzid9zY_zH`y}X$=xs+eyT!LcVZrdV>V9#ek1; zxlI4`1$p*)(#G_FsIR2V;N1eb<*xPmhEG*%Lqq)Dt0x1S8z9r!)#$RrJw6K0=&wHy z4zGDedU3nW(1QUYkPm^OBbWQ!vX<6_1H_mJ5JeaB+2M8H9`ZN6jbH!PF4dOm=+@dk z!9p!nVXkY@>F;MZ=lht5pjulyD4)Gd{g?lw+dLWgvT+nQMtET0VkWym1B(swIsoQ> z8&LvKEPD6F?!PbW+xf#!enQu6|M<`&#>8HcLOo3^+CjvM92$HP&0!9n2>9n50JMWe z`yJJq)`(k+(78U5%cnH(f)%`~Dtv_S$(ET1cLKvzK{jsLXPaisT&s2Pz7S&G_2AK{ z2e13*&d+CBN(9DS`x$xf8ANJ=zbOWB)f(Bn?VChXQ`;t`%o{`;pX@@_QX+0G_zoO0 z24wRGU)rX|L+Qe%@$nJ_A z!h8I7^f0oSeyn_a4Q{5*N+F=6gc71`ICE{UZ7PAJ+Bz3Wp;lK*9U{W~1&^T=mLQ~t zALo8S3IVGXw2)wCOoc{4l(MU6f;rIK+75ug#ma|3W`^mXT>moUJ}fNC~^>jLB=9l{uI*LG}09!;haM* zSA;PNN-Lz#o9w2~xb;ncW{ct9X&L@z(+z&`n3*(~ zJGT*iS2MVDB?D^&?(a&1rG!}6gNQ1n1Yu|>i(|(wAg&p+k^yXX7CNh1Kl|$HShgUE$3YPUbho%@#3RowS^DS$9V=G$?2t+&uWfCG@TRrJ zB*GAs%|(EZFn12q>{Md>iA;oCe?P7s{RJ{N4D0gR4An?EtJ zV3y^N5SYsyi*9BhxCm6tkIoJcz`1hK(1;bGgB!@vfb(OG9C?MTL|ihYuhI-chcF)^ z;{i_)YyH>*25K(qA_$Aa;S6y}0HO2ZD%4DleJ9c;p00000 TNkvXXu0mjf{(EEpm<0d;YMrg~ literal 0 HcmV?d00001 diff --git a/tests/lib/rules/http-compression/fixtures/image.png b/tests/lib/rules/http-compression/fixtures/image.png new file mode 100644 index 0000000000000000000000000000000000000000..8065fad796584b520cdf2fdd81c38dd689373155 GIT binary patch literal 1432 zcmV;J1!ww+P)C`AlVi9{1H(G4*s7&e-q3xm2aid5r+K!gYhiH5k+ z5LYgcKv2U1W5DRj6*0jm5(o;0hZvyrF`f6l=eU?s(9X0xrX@}0znhyo=YGHMdCmdq zs;jQL>Zs!|=-8n(Q?JS+aC_ zvr>A#bJ6)}4H8(}NyMo_zHn}Og9Nd~fRA#yO#k!+dG>kI#`J)wucXZ2-2%DguJ!tc zPgQF}L;T*WCj*-sAk*2^=(58-J_^t1uRjkCuX#p#al6gXg8?Ft4}qa0m;2nZmezy= z#Fz*WMHlng;dS30@;AMWU;oxF)t2h$*4jS7LM>Hcu4~ch?`JpX`OJQ?`1aTGU3cwpdSCc8lciw*NS0Oo%iQ36mbdiTZdzc1|D`NL0sLf38o_|PN9 z#9om?JxwgyLBxt28hjDWVGf=M_~#q|w1Y+a9o3rFh+B)$xjvE0r!??_6}+k{e1!1H zmYD~40>f27Hg4Hxn`X>ht99_c5Mti-;L)fDulwiD&u3an1jbzZ8F}v+L~4S+DF$)X z8ri(_XL2B5p1C4jeKDWb+4K+NQ?jJ4)GJA~KVD&L)CY3V|_q z1W^&Bp?S`WO53+9LJ-yDm`_AdLcnSbEoJT7@3xP=dq`>9TP>aP*&#$OLZec*Y&a7^ z2m!?mDJ3K`h-!EDF$%K&-Cf$+#A`yx?us44d;E6vFtV9`tbBY8Zl=vjA)us$5~6H4 zb8W9}DuJZhIu}WyR#!_MBEtLykD(NnAf$#L=YBy70jm|XkYHv^g+@V?va4ulllyn|G@)P1vS=cIK`gAb@;z z$5Z6}a3+k-CzPaH0+Eu_L=+Lz4Sw*LnKYO?w-J3;Gq`jm18W5C?@EHDgjm>vh$^K7 zVQ46eW5+Hat{JnE0c>^{bmk;3>ZW6t29EExfhwL{bTGH+&FWr1`|9dgwjhbeK@kLW zx43A;BhM{a`sf24D^~XGkV+-5ZEc0{rnSW+!Vr|rMSzbmcMj9+RAT&zOoUv2Kdv7A z1u{1b>+=Yr98f5r82KxuQt5Qb`AdB{f8w(veBj&wXU9xe58u(~;v1LwElJDEJrGr> zA<@3CI)y47txAomsIR1Y7 zVO?MSaC>|E$HjguUVM6ce{erv?tUsg6laFXsH*%;i27li09Nx6f4K4{h!QxgFJH`cO|3*Td@m&>Hu+_rxAl9bWrVQM31w4{VIfTnD%e8; z7^9aLvPT8v7(_Ajrtnh@Et!Bb`~pd^Vb=YuxeUf>izk6J2T@uj0CIsq<0_1+Bulf3 z3p=xdU{Mvt{G*gNwi8WqS^^6c!w6+knCJn$3KMcTn9^t-E*5z^mbw_Bfn}Ss@{8`x znhV$mv2tByhn2LJLCK<%u!iTl=C16otWDNMq&m??sjwJ~j^tqo%L>Kuc!z*>e%cbb zJpl{btXB@36xDT%iYh0qMklyN9ZbHl-lPP+=z3$=H=G7#iHQtN%scRkO(h01oyltK z2CN?mHljK^=w$tkMM|@hPIoHZ6WOmvF70SOed~5Tcg8 zsgde~EX7^B{mAQjPR z;}b&~5`o4c7DOKccaS6ODE}x&(2x1X2nZFZMgdRMF-9te(ZPhwx5k+Y)!gU_3GHQ!gki^&X|1R?1caik(c9Hgt9|VQB|Ne7N-a=Prsw7Ma1_FU3?QE^x|EA&pB_{IMaTWzPK_KBkJ8QCM z)Vq~3hd56!h3?(=$u-Fl{L!R7kc~i7-A2N-&{|rbD`}vru)m^iPtGI#BG1n`mTWQc z!n_BgNSf{$OV`(s&=>Q4vIZ~JrNm%)`3iO44XS@VoqJH`J~DL2v`NKj_IYKc$y~KW*;%wcP(@hQnauY_H^xZvI+3<{|#-@ALMF?CJwYf@5}f%rdT% zh_Mgp$l`9_bBt0@(*fICj9IgH%;WSrkkM_B0x_Px?^li$=YFD zwJ~no2jLQX1~2#A#h&0?v+<*i<2_jPR$)`$2*IH59*^czreCkrszVjFW9(48EcP3(BZM1gPd$V2n9{a1|FS+Y;A zYo@W+doPuXCPQ9(k@v^*A}b>X{F)QjpCLou{rJGWD%!p|H#8jSV=vU@x2ecnQ?!m@ zJ=YL9Hm<~bvQla7;UQ3M96(!RYL|g=cVfKG1{`B36823rkl)lbZDP+0AR3-e8B%3= z!LkjKh1^h_dgf5Ez2LqUfE=GLbFp>>Uc^Z1gs%wJ?wx*?Y8x%^)v5l(A$R!~#95Mc7xVTS?#bZNsqv zhPZ<#80`$rY!3ghyj{IF%>he%wIzJIUgi>>lec#+gA4;V%icK?6ycalAz!%F9K|{` z@`ex3dePaJZyAy21@qR@|5CQc*n)>g-(O_}5onGVZoj+y`Ng}-t&ct?s;Go?Sr!^R z7MH2AdTG)@w;Y{TmWNtuPyE-Xjl=A1FcoPf+!+`dsi6ADT?DLaOPj7KerCZnE_G*Z1v+v7P*V)vm-OgGai*Ceyp<6rLhfQ_FA0b8erk{=YyS+7D=;hIDA%G zVzY6>Pkxieb z3XW1Q4(-;(w<+E}2aZcpNSIaDU2F=yEOKh?Ubbe9UxNPqg~7!Di_OJF`aOJMHyI_A zaQ{J62WeR6pFi_Ztd{rBfldehxbG^^@Ua%bp=yg^p~$Op7+$ zwy@y=iKTC14H#GTI)&d_jqVExy}upO4`qJ3O-lRl1{B%tNJG5m&VNE*{;gxz-*1NW zx=P^E)~QKbC9Ro~NAvI#0Yr#K>nF#_N(+OVL6>GuBg^`+#qkmN2V$NBr-$H(7Y~nf zM+#)?bI?89tZ$*E7-nvZwVxrUeN+O-*WXM040b`rZcjl)V)UO~{n;ALQsVhCxxI@U u=l4@LRQ~g#`6rI=>-;0%7uM`#l5|oW^cQwE&em-}5bJ;bdt?8Y1pojHa|Pi5 literal 0 HcmV?d00001 diff --git a/tests/lib/rules/http-compression/fixtures/page.br b/tests/lib/rules/http-compression/fixtures/page.br new file mode 100644 index 0000000000000000000000000000000000000000..39421825eb61e175e58bc5c43f2d1724c7be22cb GIT binary patch literal 1149 zcmV-@1cLh;1`GhK3hZ`fD4XZPEJDcKpT9#bW%m+_koQof z%ERGHt;$46RKOEZ73`qjhQUs>GU4loEdcf!lKmEkc}FybH*+=`aVBC8RnDp#q;&S9 zsFMK`O8%C$$gP?k6Yb@zlvvM`Tm$oETCkKxQ+PC5O2MWda~ZFH@9e^wM?yV2c8KZG zkxsDz)?|pd4o|_rQ!-5~Aly~etX7VIe_V>~o=8H*@(dbDK5TCmao2J?xrci=o}ZG@ zA@1qBb$8)PzDvG``6V`qeZ||f<>+>)(!m|E(5fqmg~1PVizB2rHvsy5L~NQkl7a%0cb zD4Go|N1FW{r-_BmL#CkWyP4T=HvEzV#GufM=n`~uD>dK-4>9E?H7!vFT~G)O0DE-&C}zt)V?BG{qpMeu+Dxgmn37Jg-TZ z!lhb(?V(Pa{SzKf=q@TF;H~VC1(GiC52RPSB;K}s3X{wVe~WxNn2IrZ%YK(#Q0g{G zG5jpXOFy?7NVpJ2>IvchSv@-Xc!+Qb6EhOX!-;frIL9=hvC$7+9}ce2_rH@S#*biy zN)VMC>^;o);!jQZchSTP8WlbW?r)d?bK^OZcWST-Z@N~2NB`e({?X@~B^@Jag6V%l z2=#sP9E%DNa{Jgdbkj2&=JHW1C|XD=#xGPdL-&SFVS*U}4`YAuqX`=;`4ID@dgyet zlo@Lxr4Th*l>b76!ew69-u=|)oCVlMG*aXwon2rdgq8<@RN;6DJw*F8i)-bqyBdR7 zHF;6fA4NWPwA-+3glV0=o9_Y7IuM5pPqIX+5U#-~)-mX7FTx-!tx#87t?|Ybv~v{x zNObe`r18aOuBuxF8z_@2LN@r1LlFJ33doT}>6*b&7}3K#u;i+keDLEEj+6xZYqv&t zsYuM0i6A8vr(-`Bq@CgNvl24w&?&Sys!r8^$oN^Yvx?Nl)_-dj@2COd`-|oO*Z*zI Po%!H<-wk_~1lAD)GTSb^ literal 0 HcmV?d00001 diff --git a/tests/lib/rules/http-compression/fixtures/page.gz b/tests/lib/rules/http-compression/fixtures/page.gz new file mode 100644 index 0000000000000000000000000000000000000000..b0edcf0bac48fd0caeb2d2c7dd6c5027e9cb2f96 GIT binary patch literal 1297 zcmV+s1@8JEiwFqz2Rd2+18`wyWiDuRZEOH-R!eUrH4wh%SC~0ZCgR3n5(({sgal|2 zg1g7AnW>fA-SMNB<=69-?Y28va!F4=%3sy@u+QJw*`%)vse78-%fngzDX0DI`9S{g z@}Tf{zLU1~vizJVX=Uy@L`u(xoUV^Q9CnWrCr2-dVmd$7>vx}eJ<;=FW%{zrAsv*N zeIk!iU)ZMmv!#^{J(eFw#hw#u_ZW?K^!)t>>1kj(UuOID<)JU~>l|oOyu>^yJG(ik z$Vp+%bd;qcnM5fEr7bTU4f|XFlpLp%`gGTkoXb&JHmO(9M#b3zMO3$-SGM}3{v->8 z%JGH5taLE>)QkO%3GgL56iDn1+w7B?bc|ZPW{zqOhSgPPgO}p5l*w2Q4=m5>f|B0m zY1EUo=!T*?s(i}?bb2}}${S2Uk1~k@zQe^y{o)0+qf+*cdVvC`J0EW(>CCHm1s-Dz` z9Q1`nf8>a-1D&_h0b~@rULg^$13vmG2|?EYt+I2vFA(mtH#zP`Hnh9yz=FqS#$0R} zum<3jLeNLFn}hJgw~T@zrvNTQl0zLUIh8O`^y!MHJjq~yE0rboPI2=E^$YPuY$Cf- zx>wsUciJiin*rWgwQ$&bph4Uq0DWy@-zJ0sHriT$8+*F7UolnR%F7_G7wps@;dAHM zfF+A>qjWy|W4B~6sFXJsHNR@2*P$SsC08tq^mD+jQ3bKgF;k#+Z-D?RN%&Q`FzX# zuJO7}6fqtp$#?u1D8Q}Q&v^T40=6@J#9>$qW0CVH|!2Q zJPQoW8|~wwF}91R_Zvu$s=nsiXn*6pyU16|j$aJk1-Ap|YgEecsa^CO8Tru!*S6}N zUU%n + + + + test + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing + elit. Phasellus dictum dolor ac sodales gravida. Sed in libero arcu. + Vestibulum tincidunt massa quis orci faucibus, in luctus odio bibendum. + Sed egestas ugue diam, eu dictum elit interdum in. In ac lectus hendrerit, + egestas diam at, aliquet massa. In vitae porttitor quam. Maecenas quis + risus quis leo molestie vestibulum. Fusce vestibulum mattis porttitor. + + Maecenas eu justo mi. Nam nec hendrerit elit, at semper urna. Aliquam + erat volutpat. Nam placerat in massa tristique aliquam. Proin laoreet + dolor vitae arcu luctus, ut blandit felis pellentesque. Curabitur eleif + end at eros sit amet ornare. Etiam fringilla blandit rhoncus. + + Phasellus varius, dui sed porta viverra, mauris ligula rutrum orci, + vel tempor lorem neque et tellus. Phasellus lobortis lectus ac ligula + blandit hendrerit. Nam faucibus dolor sit amet urna varius, et volutpat + dui interdum. Etiam at aliquam mauris. Proin a porta ligula. Sed at arcu + tristique, sollicitudin sem non, bibendum odio. Interdum et malesuada + fames ac ante ipsum primis in faucibus. Morbi interdum est eget molestie + mattis. In luctus sollicitudin viverra. Pellentesque scelerisque a lacus + a lacinia. Nam mattis venenatis ligula, eu elementum nulla tincidunt sed. + Curabitur porttitor viverra purus, quis vulputate urna finibus at. + Curabitur in auctor libero, vitae condimentum mauris. Cras tincidunt + justo quis dictum tristique. + + Quisque quis libero mattis, imperdiet turpis non, posuere turpis. + Ut cursus ex sit amet ultrices viverra. Sed mollis mi enim, id lacinia + magna vulputate eu. Sed molestie dolor velit, luctus blandit diam euismod + at. Donec sit amet nibh at nisl pretium varius quis non lectus. Duis + pharetra rutrum nisi quis varius. Fusce interdum risus lacus, nec + facilisis orci varius in. Duis nec purus turpis. Ut bibendum vitae elit + sed accumsan. Integer blandit risus id metus cursus hendrerit. Aliquam + faucibus bibendum pellentesque. Pellentesque habitant morbi tristique + senectus et netus et malesuada fames ac turpis egestas. + + Vestibulum varius erat quis sapien blandit iaculis eu eu orci. Aenean + eleifend sed lectus sit amet ultrices. Curabitur justo turpis, consequat + eleifend convallis sit amet, ullamcorper ac risus. Fusce non ligula nec + ligula viverra vulputate id sit amet justo. Aliquam quis neque dapibus, + interdum nibh ut, viverra risus. In vulputate diam lacinia condimentum + suscipit. Praesent pharetra fermentum orci sit amet consectetur. + + + + + + diff --git a/tests/lib/rules/http-compression/fixtures/page.zopfli.gz b/tests/lib/rules/http-compression/fixtures/page.zopfli.gz new file mode 100644 index 0000000000000000000000000000000000000000..d5aa51d5bfe9869c957e93f8d1dd677d89a57586 GIT binary patch literal 1246 zcmV<41R?t$iwFP!0000219eu%wi`JP-TNyrT9@_CtS0Hb$vJi2LnI`@hoMY>a=X92 zi!2INP8-GIC4d{q(=U8>`hF(3X$&u~PVEf>rt9;8(&6P50Kn-+=+}?^Ya~T*H!MQ> ze5iVP{p-W-Z)FYiq9l5H>YqPenlRGy;m-7K&P5O4a#BhQ)I0Wid-il^N3Z*r2{^Gb zhS%aS(DN@JB|U95ozB_6e|cq_{3jQRz_Zi{K8IXDSOFvH)V30GN?Hq`=b6QEy4q3- ztVj6jhC(4!0nbhi1szEC7)an+@Xj8O@GW`Z1cA>~GGKA_Xkb5(D4%Pn5tNf-Uz0*a z5rnsu1#)o=7p!i1NTv=`omPSDIm3B@Ouk0L>wD3aL{Xq#D}iT>6Hpyx_UeI?Qd!mk zrz89`wWAl8^8|jF8A}{q)8sKn0;V zQ-N9%9^sFHTd`ZS1r_c&RGm>*m^q;9KXV$)XkpPd1Hg6pB8QS2H<$~N8rrIfKAq0{ zAa8=Ia1NL}D_khF&L{**O0u9H;m@_;nVbG75MQ?Hl5ZVQ$zpniToRUL(OG4`jsc2&imge@qDVLEGr> z!5)tGdriZ)sGT?iH0mdi90EJm>N$b5&U2c~lHH)q9K&hdFM*_@-`c%kq&@~rN+XTt z!jT0yS=Wn`ayf5}0EsqNs+q?CSD!4~T2Ab%d=R@dyptP>dbCqziP;-s%fG+mL~g5Yq2LDC-X*|XNV&%0zjY!hr(y18?LPJA8y3=H zA(B|QFB!{6=9|XHG10|1K_a*FjN`yE&c?^?oYoE7qpOS_178MhrW~rEz{Y!zJ#?-{ z!L<7IO2sC#mcbq(X_dj)7EhlZAQn}&=RVp$a^9`U7n4Hl1~Y;;y}JooEkK@v*38RJ4_eY%g{s@>m4cf6iB zUO|d`K5)b<9p`jCynOLrxnFfEcb8TU4Y%XeZ0Mpk}NG;CE%q!7N$t+Gx%1Kqo r%hxTz>ut_4B=`>#d^|cHz=$Bqa=7;TCB;M*sZQz=DSQ4Pw^*Y+a&;f!{Px#Ew%h^_HlJOTY(W)njiIoBC2!*`upYU9jHHCU<*b=6>g+K8(%uV@P^!8MB zTUGePaus?P$n57wnYnxmp}S>;*!K2Tea`B`eK!e7wHbYAfylkS8qlRxOYZ{{P|qDj zer|38l^+#Wy}8G5g3k;u3U5X9p+bN(@~w`Eu4i>?yIz!yx_b3i-xu>Pxdy)40boc# zzIA@WafrvlsW9;)7ikK(R3OM53<;`E0p$6@di>Q&g` z0=yf2S65;DN zPD3};Zp@5eo!u*CHZ^XA%GaB$1$#KY9ye@FrdC$Jfn@h94603FZYR>s;hg7a zdE%D5-fT#xuW9r&`hr>zfu=OnMlbU<`M1U}ttaT8=ox(pK3*^Z=D~9%uha&x! zs^*Kcu6|xD=?Z!rribfP^Ht|1tXPS(V&3ZGQKyaEU$|oX#3C|GsbI~_bm0(TsrQ9J z4OrjFFR}h$3|7yjx6Of+LNtO^m^quEe4S{YxzrK<0o5@yQhZLD@r{B#Nn^2U&~#a2 zUA9~1EqgG#G44fmbrt#8QP$PddcuUS=Bt65_vnyeIJ>+{V-&M}Xcx8u6@X^ML&nr6 zreJcr=s)B5^V#{uVeYDr3f9|9%t03Q0RXL+BzmB5*Bn7L5l2Jo)}5el@Huldp%U0S sv^2vqgHwzJl=2i$uz|&c;r4T6+I8s^8f5N2RU?&?mSH$~RZi`#0hJ#l1ONa4 literal 0 HcmV?d00001 diff --git a/tests/lib/rules/http-compression/fixtures/script.gz b/tests/lib/rules/http-compression/fixtures/script.gz new file mode 100644 index 0000000000000000000000000000000000000000..a2d395d825c48d045a1ff3ff0c40a8b7dde6e309 GIT binary patch literal 1134 zcmV-!1d;n6iwFp|Vli3(19M|?X>fEdYI6W(R!y(uG!VS|SM+(6m;C{V1KI@%3D6=0 z=k0NNW{ey=&mSXxKGnA4nT5-HQ6_Fzbyt=B@dHWrE|j;;WxjZ!5Z`L3S+=(ar%WF{ z8XH3{R0My%r?0=nBs;ky*PXWLW1{Q|@h)W{{Zed^QWNp!4wtP9HzVGamA>3Li}y9t z=4_d(*;0-hd&zW5{BRqu^oMNZ0(p0l6p53qtN9E2yG!+0SX{7ftD(@&IWxUimnkOe z=*rdNqr9}`72Dv4%|*JPRM_gi8m%oi6yr?wRt0EyUr6c+OVMI^k%aH@u+q;#QF>|R z?x>R_xw4qEj8-59fA;RZ%7E3@JzQa-Bc@VZ0o(V=`%1sESP59E$W3y&_DAdyX^&p1 z3Oy#{m42$(zP`a0sFLg!-+(QMed2BefwfSU{g6a81%UlQvGZ;u z;g?7BwH#df^24)juhG=r@dC_6xpvCR=}U^*bA~*mm_FOiUgtN4u~02^@f_m zKof#)XH;A1Q%!tvsvq&#lZjcL12KyK6zACiG-!-r@HnTzgJ#FjeWf2`AZCtm zf&aXB7G2-4CM&}jmT8%0AQkg)MAJ7UT6tr01=>cF@$*Q9Be@+dK&_J<6d%*&37j-b z;GQzYZn~bEXNGUTK{W;iVlQ$bc*3KX?HMc?LpSZe3yyn(hff|7V2v_I+zgM!J8A(> zsH&f5u0SEHd1~?L-+_aUnshSFw2$hKS{VG>)UMcECkpqxl+bB_mx`#R+A_?6$lTmO zaSrcOf{7;>)4klb8WA;T2Z{e)JO7>^Juy3JgrNF+b1K!Ekj8zmN6b}{jCZT~vyiPx zGf4hybSM}^Vl#E~@2@bL8wzJ)s;7v63KFXp~ue{@@b0}p#lnf z+>GYV=cu7I(V(Swm2Pm^Lts)I+R**cnSBhX;vNKbsCVLDw z5iOdS(6)6Td!8+0H$4zOw;h46$>3}xp^?)xeY(okXxy;1bd%1P3^z}RQFF{W6kkknP0A{f` AuY6psggYq*ed}?<}6v%~51s;g=T_iqQlDd!4({gXDmb1a1ZI9B_r-DF7#k ze4&y7i?`MNMWTGMST`ys&%u(yh9U^>j0JM>3|BN?7s=AMn^zOafiqlcsT6EmU8_wu z5=8;K89`v&79e}TYG{FzQmN~K(+c0FhB8tw_fqQ=Dy)keWqJj`${!=YGa9h^zB?-t z?7672=2D^Jw!)7{o)St&?h1r zE(r^*@TIoKUrY5E$B46VdCAF})RsIwcgMo*{9p`1p*IdIz&n+K3v6hm^`CDR5sGQC zty^kwcZ#62)eR84_Y*bZq@ca@+1W8)YF(CxSO+RX1m6O$ir}93y*H3!Fe2!4u7s>Duq0r3>6aQ}=D(VMvb z&WW)#c;@6zi4y9RxCfphZIoK(lf{Uqxgr!+H@$eu@4x}Om8MJ&?cL&Tib{uSS593~ z)N_fZhWT1zOtoc%!{-$EKrzMpe?fVIaXriJs5v>OcGcp)TIb*YAGHVR1-RuqaI>nF zo#haA&tg<){I((Ex_T??yBG|;i(y9$Cf$I z#!TqrXNWu_+eauF%a(&{bLWek$Uo84mc9VCcZskUQnqd6qcQKyM(FC^HdXf*ETmhxqE!-t8MHZiNWJx7*tw>(bcbR5u@95#3~68_W~nxSsv>ZGO4d)oxl zk)s=x$^1B$18k%wLy6Ovn1gwaih0I)biS92mu5Rm3R@4}G>j8U$7pIz?I)h|3^UKe zFyo3_-MoLFeW!wYbX0I>N|O?!x5&~XX1#fp1tr4d`c$VBTEd22|Fzut8tCR9XvPCZ z?S{@m?Wa3-Q$C+VTADYX%TA?nDJ)a(Zj>?Ro5Mfn<&QDz{y#2ua3qelt IkkknP0JTFLeE { + ['gzip', 'zopfli', 'brotli'].forEach((encoding) => { + ruleRunner.testRule( + ruleName, + testsForUserConfigs(`${encoding}`, isTarget), + Object.assign( + {}, + testConfigs, + { ruleOptions: { [isTarget ? 'target' : 'resource']: { [encoding]: false } } } + ) + ); + }); +}); diff --git a/tests/lib/rules/http-compression/tests-https.ts b/tests/lib/rules/http-compression/tests-https.ts new file mode 100644 index 00000000000..826be792182 --- /dev/null +++ b/tests/lib/rules/http-compression/tests-https.ts @@ -0,0 +1,58 @@ +import { getRuleName } from '../../../../src/lib/utils/rule-helpers'; +import * as ruleRunner from '../../../helpers/rule-runner'; + +import { + testsForBrotli, + testsForBrotliSmallSize, + testsForBrotliUASniffing, + testsForDefaults, + testsForDisallowedCompressionMethods, + testsForGzipZopfli, + testsForGzipZopfliCaching, + testsForGzipZopfliSmallSize, + testsForGzipZopfliUASniffing, + testsForNoCompression, + testsForSpecialCases, + testsForUserConfigs +} from './_tests'; + +const ruleName = getRuleName(__dirname); + +/* + * TODO: Remove `ignoredConnectors` part once headless + * Chrome on Travis CI doesn't fail miserably. :( + */ +const testConfigs = { + https: true, + ignoredConnectors: ['chrome'] +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +ruleRunner.testRule(ruleName, testsForDefaults(true), testConfigs); +ruleRunner.testRule(ruleName, testsForSpecialCases(true), testConfigs); +ruleRunner.testRule(ruleName, testsForDisallowedCompressionMethods(true), testConfigs); +ruleRunner.testRule(ruleName, testsForNoCompression(true), testConfigs); +ruleRunner.testRule(ruleName, testsForGzipZopfli(true), testConfigs); +ruleRunner.testRule(ruleName, testsForGzipZopfliCaching(true), testConfigs); +ruleRunner.testRule(ruleName, testsForGzipZopfliSmallSize(true), testConfigs); +ruleRunner.testRule(ruleName, testsForGzipZopfliUASniffing(true), testConfigs); + +ruleRunner.testRule(ruleName, testsForBrotli, testConfigs); +ruleRunner.testRule(ruleName, testsForBrotliSmallSize, testConfigs); +ruleRunner.testRule(ruleName, testsForBrotliUASniffing(), testConfigs); + +// Tests for the user options. +[true, false].forEach((isTarget) => { + ['gzip', 'zopfli', 'brotli'].forEach((encoding) => { + ruleRunner.testRule( + ruleName, + testsForUserConfigs(`${encoding}`, isTarget, true), + Object.assign( + {}, + testConfigs, + { ruleOptions: { [isTarget ? 'target' : 'resource']: { [encoding]: false } } } + ) + ); + }); +});