diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 02:57:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 02:57:58 +0000 |
commit | be1c7e50e1e8809ea56f2c9d472eccd8ffd73a97 (patch) | |
tree | 9754ff1ca740f6346cf8483ec915d4054bc5da2d /web/server/h2o/libh2o/misc/cache-digest.js | |
parent | Initial commit. (diff) | |
download | netdata-be1c7e50e1e8809ea56f2c9d472eccd8ffd73a97.tar.xz netdata-be1c7e50e1e8809ea56f2c9d472eccd8ffd73a97.zip |
Adding upstream version 1.44.3.upstream/1.44.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'web/server/h2o/libh2o/misc/cache-digest.js')
5 files changed, 431 insertions, 0 deletions
diff --git a/web/server/h2o/libh2o/misc/cache-digest.js/.travis.yml b/web/server/h2o/libh2o/misc/cache-digest.js/.travis.yml new file mode 100644 index 00000000..7c774e43 --- /dev/null +++ b/web/server/h2o/libh2o/misc/cache-digest.js/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "6" +script: + - node test.js diff --git a/web/server/h2o/libh2o/misc/cache-digest.js/README.md b/web/server/h2o/libh2o/misc/cache-digest.js/README.md new file mode 100644 index 00000000..bb0649dd --- /dev/null +++ b/web/server/h2o/libh2o/misc/cache-digest.js/README.md @@ -0,0 +1,33 @@ +cache-digest.js +====== + +[![Build Status](https://travis-ci.org/h2o/cache-digest.js.svg?branch=master)](https://travis-ci.org/h2o/cache-digest.js) + +[Service Worker](https://developer.mozilla.org/docs/Web/API/Service_Worker_API) implementation of [Cache Digests for HTTP/2 (draft 01)](https://tools.ietf.org/html/draft-kazuho-h2-cache-digest-01) + +Warning +------ + +* WIP; the code is in early-beta stage +* only supports sending of _fresh_ digests without etag + +How to Use +------ + +1. install cache-digest.js into the root directory of the website +2. add `<script src="/cache-digest.js"></script>` to your web pages +3. adjust the web server configuration to send: + * `service-worker-allowed: /` response header + * `link: <push-URL>; rel="preload"` response header (see [spec](https://w3c.github.io/preload/)) + +Calculating Digests at Command Line +------ + +You can run cli.js to calculate cache digests manually. + +``` +% node cli.js -b https://example.com/style.css https://example.com/jquery.js https://example.com/shortcut.css +EdcLLJA +``` + +In the above example, `-b` option is used so that the digest would be encoded using [base64url](https://tools.ietf.org/html/rfc4648#section-5). Please refer to `-h` (help) option for more information. diff --git a/web/server/h2o/libh2o/misc/cache-digest.js/cache-digest.js b/web/server/h2o/libh2o/misc/cache-digest.js/cache-digest.js new file mode 100644 index 00000000..b972a6ca --- /dev/null +++ b/web/server/h2o/libh2o/misc/cache-digest.js/cache-digest.js @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2015,2016 Jxck, DeNA Co., Ltd., Kazuho Oku + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * + * Includes a minified SHA256 implementation taken from https://gist.github.com/kazuho/bb8aab1a2946bbf42127d8a6197ad18c, + * licensed under the following copyright: + * + * Copyright (c) 2015,2016 Chen Yi-Cyuan, Kazuho Oku + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +"use strict"; + +if (typeof self !== "undefined" && "ServiceWorkerGlobalScope" in self && + self instanceof ServiceWorkerGlobalScope) { + + /* ServiceWorker */ + self.addEventListener('fetch', function(evt) { + var req = evt.request.clone(); + if (req.method != "GET" || req.url.match(/\/cache-digests?\.js(?:\?|$)/)) { + logInfo(req, "skip"); + return; + } + evt.respondWith(caches.open("v1").then(function (cache) { + return cache.match(req).then(function (res) { + if (res && isFresh(res.headers.entries(), Date.now())) { + logInfo(req, "hit"); + return res; + } + var requestWithDigests = function (digests) { + if (digests != null) { + var err = null; + try { + req = new Request(req); + req.headers.append("cache-digest", digests); + if (req.headers.get("cache-digest") == null) + err = "append failed"; + } catch (e) { + err = e; + } + if (err) + logError(req, e); + } + return fetch(req).then(function (res) { + var cached = false; + if (res.status == 200 && isFresh(res.headers.entries(), Date.now())) { + cache.put(req, res.clone()); + cached = true; + } + logInfo(req, "fetched" + (cached ? " & cached" : "") + " with cache-digest:\"" + digests + "\""); + return res; + }); + }; + if (req.mode == "navigate") { + return generateCacheDigests(cache).then(requestWithDigests); + } else { + return requestWithDigests(null); + } + }); + })); + }); + +} else if (typeof navigator !== "undefined") { + + /* bootstrap, loaded via <script src=...> */ + navigator.serviceWorker.register("/cache-digest.js", {scope: "./"}).then(function(reg) { + console.log("registered cache-digest.js service worker"); + }).catch(function(e) { + console.log("failed to register cache-digest.js service worker:" + e); + }); + +} + +// returns a promise that returns the cache digest value +function generateCacheDigests(cache) { + var urls = []; + return cache.keys().then(function (reqs) { + // collect 31-bit hashes of fresh responses + return Promise.all(reqs.map(function (req) { + var now = Date.now(); + return cache.match(req).then(function (resp) { + if (resp && isFresh(resp.headers.entries(), now)) + urls.push(req.url); + }); + })).then(function () { + var dv = calcDigestValue(urls, 7); + return dv != null ? base64Encode(dv) + "; complete" : null; + }); + }); +} + +function calcDigestValue(urls, pbits) { + var nbits = Math.round(Math.log(Math.max(urls.length, 1)) * 1.4426950408889634); // round log2(urls.length) + if (nbits + pbits > 31) + return null; + var hashes = []; + for (var i = 0; i != urls.length; ++i) + hashes.push(sha256Truncated(urls[i], nbits + pbits)); + return (new BitCoder).addBits(nbits, 5).addBits(pbits, 5).gcsEncode(hashes, pbits).value; +} + +function isFresh(headers, now) { + var date = 0, maxAge = null; + var o; + while (!(o = headers.next()).done) { + var name = o.value[0], value = o.value[1]; + if (name.match(/^expires$/i) != null) { + var parsed = Date.parse(value); + if (parsed && parsed > now) + return true; + } else if (name.match(/^cache-control$/i) != null) { + var directives = value.split(/\s*,\s*/); + for (var i = 0; i != directives.length; ++i) { + var d = directives[i]; + if (d.match(/^\s*no-(?:cache|store)\s*$/) != null) { + return false; + } else if (d.match(/^\s*max-age\s*=\s*([0-9]+)/) != null) { + maxAge = Math.min(RegExp.$1, maxAge || Infinity); + } + } + } else if (name.match(/^date$/i) != null) { + date = Date.parse(value); + } + } + + if (maxAge != null) { + if (date + maxAge * 1000 > now) + return true; + } + + return false; +} + +function BitCoder() { + this.value = []; + this.leftBits = 0; +} + +BitCoder.prototype.addBit = function (b) { + if (this.leftBits == 0) { + this.value.push(0); + this.leftBits = 8; + } + --this.leftBits; + if (b) + this.value[this.value.length - 1] |= 1 << this.leftBits; + return this; +}; + +BitCoder.prototype.addBits = function (v, nbits) { + if (nbits != 0) { + do { + --nbits; + this.addBit(v & (1 << nbits)); + } while (nbits != 0); + } + return this; +}; + +BitCoder.prototype.gcsEncode = function (values, bits_fixed) { + values = values.sort(function (a, b) { return a - b; }); + var prev = -1; + for (var i = 0; i != values.length; ++i) { + if (prev == values[i]) + continue; + var v = values[i] - prev - 1; + for (var q = v >> bits_fixed; q != 0; --q) + this.addBit(0); + this.addBit(1); + this.addBits(v, bits_fixed); + prev = values[i]; + } + return this; +}; + +var base64Encode = function (buf) { + var TOKENS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + return function base64Encode(buf) { + var str = ''; + for (var pos = 0; pos < buf.length; pos += 3) { + var quad = buf[pos] << 16 | buf[pos + 1] << 8 | buf[pos + 2]; + str += TOKENS[(quad >> 18)] + TOKENS[(quad >> 12) & 63] + TOKENS[(quad >> 6) & 63] + TOKENS[quad & 63]; + } + str = str.substring(0, str.length - pos + buf.length); + return str; + }; +}(); + +function sha256Truncated(src, bits) { + // only supports bits <= 31 + return ((sha256(src)[0] >> 1) & 0x7fffffff) >> (31 - bits); +} + +var sha256=function(){var r=[-2147483648,8388608,32768,128],o=[24,16,8,0],a=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298];return function(n){var t,e,f,h,c,u,v,d,i,l,A,C,g,s=[],w=!0,b=!1,j=0,k=0,m=0,p=n.length,q=1779033703,x=3144134277,y=1013904242,z=2773480762,B=1359893119,D=2600822924,E=528734635,F=1541459225,G=0;do{for(s[0]=G,s[16]=s[1]=s[2]=s[3]=s[4]=s[5]=s[6]=s[7]=s[8]=s[9]=s[10]=s[11]=s[12]=s[13]=s[14]=s[15]=0,e=k;p>j&&64>e;++j)t=n.charCodeAt(j),128>t?s[e>>2]|=t<<o[3&e++]:2048>t?(s[e>>2]|=(192|t>>6)<<o[3&e++],s[e>>2]|=(128|63&t)<<o[3&e++]):55296>t||t>=57344?(s[e>>2]|=(224|t>>12)<<o[3&e++],s[e>>2]|=(128|t>>6&63)<<o[3&e++],s[e>>2]|=(128|63&t)<<o[3&e++]):(t=65536+((1023&t)<<10|1023&n.charCodeAt(++j)),s[e>>2]|=(240|t>>18)<<o[3&e++],s[e>>2]|=(128|t>>12&63)<<o[3&e++],s[e>>2]|=(128|t>>6&63)<<o[3&e++],s[e>>2]|=(128|63&t)<<o[3&e++]);m+=e-k,k=e-64,j==p&&(s[e>>2]|=r[3&e],++j),G=s[16],j>p&&56>e&&(s[15]=m<<3,b=!0);var H=q,I=x,J=y,K=z,L=B,M=D,N=E,O=F;for(f=16;64>f;++f)v=s[f-15],h=(v>>>7|v<<25)^(v>>>18|v<<14)^v>>>3,v=s[f-2],c=(v>>>17|v<<15)^(v>>>19|v<<13)^v>>>10,s[f]=s[f-16]+h+s[f-7]+c<<0;for(g=I&J,f=0;64>f;f+=4)w?(l=704751109,v=s[0]-210244248,O=v-1521486534<<0,K=v+143694565<<0,w=!1):(h=(H>>>2|H<<30)^(H>>>13|H<<19)^(H>>>22|H<<10),c=(L>>>6|L<<26)^(L>>>11|L<<21)^(L>>>25|L<<7),l=H&I,u=l^H&J^g,i=L&M^~L&N,v=O+c+i+a[f]+s[f],d=h+u,O=K+v<<0,K=v+d<<0),h=(K>>>2|K<<30)^(K>>>13|K<<19)^(K>>>22|K<<10),c=(O>>>6|O<<26)^(O>>>11|O<<21)^(O>>>25|O<<7),A=K&H,u=A^K&I^l,i=O&L^~O&M,v=N+c+i+a[f+1]+s[f+1],d=h+u,N=J+v<<0,J=v+d<<0,h=(J>>>2|J<<30)^(J>>>13|J<<19)^(J>>>22|J<<10),c=(N>>>6|N<<26)^(N>>>11|N<<21)^(N>>>25|N<<7),C=J&K,u=C^J&H^A,i=N&O^~N&L,v=M+c+i+a[f+2]+s[f+2],d=h+u,M=I+v<<0,I=v+d<<0,h=(I>>>2|I<<30)^(I>>>13|I<<19)^(I>>>22|I<<10),c=(M>>>6|M<<26)^(M>>>11|M<<21)^(M>>>25|M<<7),g=I&J,u=g^I&K^C,i=M&N^~M&O,v=L+c+i+a[f+3]+s[f+3],d=h+u,L=H+v<<0,H=v+d<<0;q=q+H<<0,x=x+I<<0,y=y+J<<0,z=z+K<<0,B=B+L<<0,D=D+M<<0,E=E+N<<0,F=F+O<<0}while(!b);return[q,x,y,z,B,D,E,F]}}(); + +function logRequest(req) { + var s = req.method + " " + req.url + "\n"; + var o; + for (var iter = req.headers.entries(); !(o = iter.next()).done;) + s += o.value[0] + ": " + o.value[1] + "\n"; + console.log(s); +} +function logError(req, msg) { + console.log(req.url + ":error:" + msg); +} +function logInfo(req, msg) { + console.log(req.url + ":info:" + msg); +} +function logDebug(req, msg) { + console.log(req.url + ":debug:" + msg); +} diff --git a/web/server/h2o/libh2o/misc/cache-digest.js/cli.js b/web/server/h2o/libh2o/misc/cache-digest.js/cli.js new file mode 100644 index 00000000..98f949a3 --- /dev/null +++ b/web/server/h2o/libh2o/misc/cache-digest.js/cli.js @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016 DeNA Co., Ltd., Kazuho Oku + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +"use strict"; + +var vm = require("vm"); +var fs = require("fs"); + +vm.runInThisContext(fs.readFileSync(__dirname + "/cache-digest.js", "ascii")); + +function main(argv) { + var pbits = 7; + var useBase64 = false; + + argv.shift(); + argv.shift(); + while (argv.length != 0 && argv[0].match(/^-/) != null) { + var opt = argv.shift(); + if (opt == "-") + break; + if (opt == "-h" || opt == "--help") { + console.log("Usage: node cmd.js [-b] [-p=pbits] URL1 URL2...") + return 0; + } else if (opt == "-b") { + useBase64 = 1; + } else if (opt.match(/^-p(?:(=(.*))|$)/) != null) { + if (RegExp.$1 != "") { + pbits = RegExp.$2 - 0; + } else if (argv.length == 0) { + console.error("argument value missing for option: -p"); + return 1; + } else { + pbits = argv.shift() - 0; + } + } else { + console.error("Unknown option: %s", opt); + return 1; + } + } + + var digest = calcDigestValue(argv, pbits); + if (digest == null) { + console.error("failed to calculate the digests"); + return 1; + } + + if (useBase64) { + console.log("%s", base64Encode(digest)); + } else { + fs.createWriteStream(null, {fd: 1, defaultEncoding: "binary"}).write(Buffer.from(digest)); + } + + return 0; +} + +process.exit(main(process.argv)); diff --git a/web/server/h2o/libh2o/misc/cache-digest.js/test.js b/web/server/h2o/libh2o/misc/cache-digest.js/test.js new file mode 100644 index 00000000..ff3f9b37 --- /dev/null +++ b/web/server/h2o/libh2o/misc/cache-digest.js/test.js @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016 DeNA Co., Ltd., Kazuho Oku + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +"use strict"; + +var vm = require("vm"); +var fs = require("fs"); + +var ntests = 0, failed = false; + +function ok(b, name) { + console.log((b ? "ok" : "not ok" ) + " " + ++ntests + " - " + name); + if (!b) + failed = true; +} + +function is(result, expected, name) { + if (Array.isArray(result)) { + if (result.length != expected.length) + return ok(false, name); + for (var i = 0; i != result.length; ++i) + if (result[i] !== expected[i]) + return ok(false, name); + return ok(true, name); + } else { + return ok(result === expected, name); + } +} + +vm.runInThisContext(fs.readFileSync("cache-digest.js", "ascii")); + +ok(isFresh([["Expires", "Mon, 27 Jun 2016 02:12:35 GMT"]][Symbol.iterator](), Date.parse("2016-06-27T02:12:00Z")), "expires-fresh"); +ok(!isFresh([["Expires", "Mon, 27 Jun 2016 02:12:35 GMT"]][Symbol.iterator](), Date.parse("2016-06-27T02:13:00Z")), "expires-stale"); +ok(!isFresh([["Cache-Control", "must-revalidate, max-age=600"]][Symbol.iterator](), Date.parse("2016-06-27T02:12:00Z")), "max-age-wo-date"); +ok(isFresh([["Cache-Control", "must-revalidate, max-age=600"], ["Date", "Mon, 27 Jun 2016 02:12:35 GMT"]][Symbol.iterator](), Date.parse("2016-06-27T02:22:00Z")), "max-age-fresh"); +ok(!isFresh([["Cache-Control", "must-revalidate, max-age=600"], ["Date", "Mon, 27 Jun 2016 02:12:35 GMT"]][Symbol.iterator](), Date.parse("2016-06-27T02:23:00Z")), "max-age-stale"); +is((new BitCoder).gcsEncode([], 2).value, []); +is((new BitCoder).gcsEncode([3, 10], 2).value, [0b11101100]); +is((new BitCoder).gcsEncode([1025], 8).value, [0b00001000, 0b00001000]); +is(base64Encode(["h", "e", "l"].map(function (c) { return c.charCodeAt(0) })), "aGVs"); +is(base64Encode(["h", "e", "l", "l"].map(function (c) { return c.charCodeAt(0) })), "aGVsbA"); +is(base64Encode(["h", "e", "l", "l", "o"].map(function (c) { return c.charCodeAt(0) })), "aGVsbG8"); +is(sha256(""), [0xe3b0c442, 0x98fc1c14, 0x9afbf4c8, 0x996fb924, 0x27ae41e4, 0x649b934c, 0xa495991b, 0x7852b855].map(function (v) { return v | 0; }), "sha256 empty string"); +is(sha256("hello world"), [0xb94d27b9, 0x934d3e08, 0xa52e52d7, 0xda7dabfa, 0xc484efe3, 0x7a5380ee, 0x9088f7ac, 0xe2efcde9].map(function (v) { return v | 0; }), "sha256 hello world"); +is(sha256Truncated("", 8), 0xe3); +is(sha256Truncated("", 5), 0x1c); +is(sha256Truncated("hello world", 11), 0x5ca); +is(calcDigestValue([], 7), [0x01, 0xc0]); +is(calcDigestValue(["https://example.com/style.css"], 7), [0x01, 0xf7, 0x40]); +is(calcDigestValue(["https://example.com/style.css", "https://example.com/jquery.js"], 7), [0x09, 0xd6, 0x50, 0xe0]); +is(calcDigestValue(["https://example.com/style.css", "https://example.com/jquery.js"], 4), [0x09, 0x16, 0x80]); +console.log("1.." + ntests); + +process.exit(failed ? 127 : 0); |