├── .gitignore
├── .travis.yml
├── test
├── Makefile
├── README.md
├── cache-miss-1.js
├── cache-hit.js
├── cache-miss-2.js
├── filter.js
├── assurt.js
├── http.js
├── http-simple.js
├── cache-misc.js
├── http-public.js
├── http-only-if-cached.js
├── lib.js
└── helper.js
├── package.json
├── examples
├── simple.js
├── web-proxy.js
├── caching-proxy.js
└── reverse-proxy.js
├── LICENSE
├── lib
├── handler.js
├── client.js
├── mongodb.js
├── memcached.js
├── memory.js
├── helper.js
└── fishback.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.8
4 |
--------------------------------------------------------------------------------
/test/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | node helper.js
3 | node filter.js
4 | node cache-miss-1.js
5 | node cache-miss-2.js
6 | node cache-hit.js
7 | node cache-misc.js
8 | node http-simple.js
9 | node http-only-if-cached.js
10 | node http-public.js
11 |
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | Not very happy with this test code here--it's quite ugly and difficult to read.
2 | But I find that code to test async code is often more difficult to write than
3 | the async code in the first place.
4 |
5 | There's an interesting proxy test suite at
6 | that I will investigate at some point.
7 | Not free, but free for OS projects.
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fishback",
3 | "author": "Michael Stillwell (http://beebo.org/)",
4 | "description": "Simple RFC2616-compliant caching proxy server",
5 | "version": "0.3.1",
6 | "homepage": "https://github.com/ithinkihaveacat/node-fishback",
7 | "repository": "git://github.com/ithinkihaveacat/node-fishback.git",
8 | "bugs": "https://github.com/ithinkihaveacat/node-fishback/issues",
9 | "main": "./lib/fishback",
10 | "dependencies": {
11 | "memjs": ">=0.6.0"
12 | },
13 | "engines": {
14 | "node": ">=0.8 <0.10"
15 | },
16 | "scripts": {
17 | "test": "cd test && make"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/test/cache-miss-1.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var lib = require("./lib");
6 | var http = require("./http");
7 | var assurt = require("./assurt");
8 |
9 | lib.getCacheList(function (cache, next) {
10 |
11 | var req = new http.ServerRequest({
12 | url: "/",
13 | method: "GET"
14 | });
15 |
16 | var res = new http.ServerResponse();
17 |
18 | req.once('reject', assurt.calls(function () {
19 | cache.close();
20 | next();
21 | }));
22 |
23 | cache.request(req, res);
24 | req.fire();
25 |
26 | });
27 |
--------------------------------------------------------------------------------
/examples/simple.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var fishback = require("../lib/fishback");
6 | var http = require("http");
7 |
8 | var proxy = fishback.createProxy(new fishback.Client("localhost", 9000));
9 | proxy.on("newRequest", function (req) {
10 | console.log(req.method + " " + req.url);
11 | });
12 | proxy.on("newResponse", function (res) {
13 | res.setHeader("cache-control", "public, max-age=3600");
14 | });
15 |
16 | http.createServer(proxy.request.bind(proxy)).listen(8000);
17 |
18 | console.log("Listening on port 8000, and proxying to localhost:9000");
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2011 Michael Stillwell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/lib/handler.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | function Handler() {
6 | }
7 |
8 | require('util').inherits(Handler, require('events').EventEmitter);
9 |
10 | /**
11 | * Handles http.Server's 'request' event.
12 | *
13 | * @param {http.ServerRequest} serverRequest [description]
14 | * @param {http.ServerResponse} serverResponse [description]
15 | */
16 | Handler.prototype.request = function (serverRequest, serverResponse) {
17 | // jshint unused:false
18 | };
19 |
20 | /**
21 | * Adds an entry to the cache.
22 | *
23 | * @param {http.ClientResponse} res entry to add to the cache
24 | */
25 | Handler.prototype.response = function (clientResponse) {
26 | // jshint unused:false
27 | };
28 |
29 | /**
30 | * Closes the underlying memcached connection, etc.
31 | *
32 | * @param {callback} callback)
33 | */
34 | Handler.prototype.close = function (callback) {
35 | if (callback) {
36 | return callback.call();
37 | }
38 | };
39 |
40 | module.exports = Handler;
41 |
--------------------------------------------------------------------------------
/examples/web-proxy.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | // This can be used as a "standard" client-side proxy for browsers, etc.
6 | // Note that SSL is not supported!
7 |
8 | var fishback = require("../lib/fishback");
9 | var http = require("http");
10 |
11 | var PORT = 8080;
12 |
13 | var client = new fishback.Client();
14 |
15 | var proxy = fishback.createProxy(client);
16 | proxy.on('newResponse', function (res) {
17 | console.info(res.method + " " + res.url.slice(0, 75) + (res.url.length > 75 ? " ..." : ""));
18 | res.setHeader("cache-control", "public, max-age=3600"); // Example header adjustment
19 | });
20 |
21 | var server = new http.Server();
22 | server.on('request', proxy.request.bind(proxy));
23 | server.listen(PORT, function () {
24 | console.info("Listening on port " + PORT);
25 | console.info();
26 | console.info("Try:");
27 | console.info();
28 | console.info(" $ curl --proxy localhost:" + PORT + " http://www.bbc.co.uk/news/");
29 | console.info();
30 | });
31 |
--------------------------------------------------------------------------------
/test/cache-hit.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var lib = require("./lib");
6 | var http = require("./http");
7 | var assurt = require("./assurt");
8 |
9 | lib.getCacheList(function (cache, next) {
10 |
11 | var clientResponse = new http.ClientResponse({
12 | url: "/",
13 | method: "GET",
14 | statusCode: 200,
15 | headers: {
16 | "cache-control": "public, max-age=60"
17 | },
18 | data: [ "Hello, World!" ]
19 | });
20 |
21 | cache.response(clientResponse);
22 | clientResponse.fire();
23 |
24 | var req = new http.ServerRequest({
25 | url: "/",
26 | method: "GET"
27 | });
28 | req.on('reject', assurt.never(function q1() {
29 | cache.close();
30 | }));
31 |
32 | var res = new http.ServerResponse();
33 | res.once('end', assurt.calls(function () {
34 | assurt.response(res, { headers: {}, data: "Hello, World!" });
35 | cache.close();
36 | next();
37 | }));
38 |
39 | cache.request(req, res);
40 | req.fire();
41 |
42 | });
43 |
--------------------------------------------------------------------------------
/test/cache-miss-2.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var lib = require("./lib");
6 | var http = require("./http");
7 | var assert = require("assert");
8 | var assurt = require("./assurt");
9 |
10 | lib.getCacheList(function (cache, next) {
11 |
12 | var req, res;
13 |
14 | res = new http.ClientResponse({
15 | url: "/foo",
16 | method: "GET",
17 | statusCode: 200,
18 | headers: {
19 | "cache-control": "public, max-age=60"
20 | },
21 | data: [ "Hello, Foo!" ]
22 | });
23 |
24 | cache.response(res);
25 | res.fire();
26 |
27 | req = new http.ServerRequest({
28 | url: "/",
29 | method: "GET"
30 | });
31 |
32 | res = new http.ServerResponse();
33 | res.on('end', function () {
34 | assert.ok(false, "Response is not supposed to be returned!");
35 | cache.close();
36 | });
37 |
38 | req.on('reject', assurt.calls(function () {
39 | cache.close();
40 | next();
41 | }));
42 |
43 | cache.request(req, res);
44 | req.fire();
45 |
46 | });
47 |
--------------------------------------------------------------------------------
/examples/caching-proxy.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var fishback = require("../lib/fishback");
6 | var http = require("http");
7 |
8 | var PORT = 8080;
9 | var BACKEND_HOST = "www.bbc.co.uk";
10 | var BACKEND_PORT = "80";
11 |
12 | var cache = new fishback.Memory();
13 | cache.on('newRequest', function (req) {
14 | console.info("CACHE " + req.method + " " + req.url);
15 | });
16 |
17 | var client = new fishback.Client(BACKEND_HOST, BACKEND_PORT);
18 | client.on('newRequest', function (req) {
19 | console.info("CLIENT " + req.method + " " + req.url);
20 | });
21 |
22 | var proxy = fishback.createCachingProxy(cache, client);
23 | proxy.on('newRequest', function (req) {
24 | console.info("PROXY " + req.method + " " + req.url);
25 | });
26 |
27 | var server = new http.Server();
28 | server.on('request', proxy.request.bind(proxy));
29 | server.listen(PORT, function () {
30 | console.info("Listening on port " + PORT);
31 | console.info();
32 | console.info("Try:");
33 | console.info();
34 | console.info(" $ curl -s -i http://localhost:" + PORT + "/");
35 | });
36 |
--------------------------------------------------------------------------------
/examples/reverse-proxy.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var fishback = require("../lib/fishback");
6 | var http = require("http");
7 |
8 | var PORT = 8080;
9 | var BACKEND_HOST = "www.bbc.co.uk";
10 | var BACKEND_PORT = "80";
11 |
12 | var client = new fishback.Client(BACKEND_HOST, BACKEND_PORT);
13 | client.on('newRequest', function (req) {
14 | console.info("CLIENT.newRequest " + req.method + " " + req.url);
15 | });
16 | client.on('newResponse', function (res) {
17 | console.info("CLIENT.newResponse " + res.method + " " + res.backendUrl);
18 | });
19 |
20 | var proxy = fishback.createProxy(client);
21 | proxy.on('newRequest', function (req) {
22 | console.info("PROXY.newRequest " + req.method + " " + req.url);
23 | });
24 | proxy.on('newResponse', function (res) {
25 | console.info("PROXY.newResponse " + res.method + " " + res.url);
26 | });
27 |
28 | var server = new http.Server();
29 | server.on('request', proxy.request.bind(proxy));
30 | server.listen(PORT, function () {
31 | console.info("Listening on port " + PORT);
32 | console.info();
33 | console.info("Try:");
34 | console.info();
35 | console.info(" $ curl -s -i http://localhost:" + PORT + "/");
36 | });
37 |
--------------------------------------------------------------------------------
/test/filter.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var DELAY = 500;
6 |
7 | var lib = require("./lib");
8 | var http = require("./http");
9 | var assurt = require("./assurt");
10 | var fishback = require("../lib/fishback");
11 |
12 | var response = { statusCode: 200, headers: { "cache-control": "max-age=60, private" }, data: [ "Hello, World" ]};
13 | var expected = { headers: { "foo": "bar", "cache-control": "max-age=60, public" }, data: "Hello, World" };
14 |
15 | lib.getCacheList(function (cache) {
16 |
17 | var req = new http.ServerRequest({ url: "/", method: "GET" });
18 | var res = new http.ServerResponse();
19 |
20 | var client = new fishback.Client(null, null, {
21 | request: assurt.calls(function (options, callback) {
22 | var clientResponse = new http.ClientResponse(response);
23 | callback(clientResponse);
24 | clientResponse.fire();
25 | return new http.ClientRequest();
26 | })
27 | });
28 |
29 | var proxy = fishback.createCachingProxy(cache, client);
30 |
31 | proxy.on('newRequest', assurt.calls(function (req) {
32 | req.url = "/404";
33 | }));
34 |
35 | proxy.on('newResponse', assurt.calls(function (res) {
36 | res.setHeader('foo', 'bar');
37 | res.setHeader(
38 | 'cache-control',
39 | res.getHeader('cache-control').replace(/\bprivate\b/, "public")
40 | );
41 | }));
42 |
43 | res.on('end', assurt.calls(function () {
44 | assurt.response(res, expected);
45 | }));
46 |
47 | proxy.request(req, res);
48 | req.fire();
49 |
50 | setTimeout(cache.close.bind(cache), DELAY);
51 |
52 | });
53 |
--------------------------------------------------------------------------------
/test/assurt.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var assert = require('assert');
6 |
7 | /**
8 | * Convenience function for checking whether expected matches actual.
9 | * actual can contain headers not present in expected, but the reverse
10 | * is not true.
11 | *
12 | * @param {object} actual
13 | * @param {object} expected
14 | * @return {boolean}
15 | */
16 | function response(actual, expected) {
17 | Object.keys(expected.headers).forEach(function (k) {
18 | assert.equal(actual.headers[k], expected.headers[k]);
19 | });
20 | assert.equal(actual.data, expected.data);
21 | }
22 |
23 | function calls(callback, context) {
24 |
25 | if (calls.count === undefined) {
26 | calls.count = 0;
27 | process.on('exit', function () {
28 | var n_functions = calls.count === 1 ? "1 function" : (calls.count + " functions");
29 | assert.equal(calls.count, 0, "Failed to call " + n_functions);
30 | });
31 | }
32 |
33 | calls.count++;
34 |
35 | return function () {
36 | calls.count--;
37 | return callback.apply(context, arguments);
38 | };
39 | }
40 |
41 | function once(callback, context) {
42 | var count = 0;
43 | process.on('exit', function () {
44 | var name = callback.name ? callback.name : "unknown";
45 | assert.equal(count, 1, "Unexpectedly called function [" + name + "] " + count + " times");
46 | });
47 | return function () {
48 | count++;
49 | return callback.apply(context, arguments);
50 | };
51 | }
52 |
53 | function never(callback, context) {
54 | return function () {
55 | var name = callback.name ? callback.name : "unknown";
56 | assert.ok(false, "Unexpectedly called function [" + name + "]");
57 | return callback.apply(context, arguments);
58 | };
59 | }
60 |
61 | process.setMaxListeners(20);
62 |
63 | [response, calls, once, never].forEach(function (fn) {
64 | exports[fn.name] = fn;
65 | });
66 |
--------------------------------------------------------------------------------
/lib/client.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var fishback = require("./fishback");
6 | var Handler = require("./handler");
7 |
8 | function Client(backend_hostname, backend_port, http) {
9 | this.backend_hostname = backend_hostname;
10 | this.backend_port = backend_port;
11 | this.http = http || require('http');
12 | Handler.call(this);
13 | }
14 |
15 | require('util').inherits(Client, Handler);
16 |
17 | Client.prototype.request = function (serverRequest, serverResponse) {
18 |
19 | var emit = this.emit.bind(this);
20 |
21 | emit('newRequest', serverRequest);
22 |
23 | var req = {
24 | url: serverRequest.url,
25 | method: serverRequest.method,
26 | headers: { }
27 | };
28 | Object.keys(serverRequest.headers).forEach(function (k) {
29 | req.headers[k] = serverRequest.headers[k];
30 | });
31 |
32 | var tmp = require('url').parse(req.url);
33 |
34 | var options = {
35 | "host": tmp.hostname || this.backend_hostname,
36 | "port": tmp.port || this.backend_port || 80,
37 | "path": tmp.pathname + (tmp.search ? tmp.search : ''),
38 | "method": req.method,
39 | "headers": req.headers
40 | };
41 | options.headers.host = options.host;
42 |
43 | var clientRequest = this.http.request(options, function (clientResponse) {
44 | var port = options.port !== 80 ? ":" + options.port : "";
45 | var search = options.search ? "?" + options.search : "";
46 | clientResponse.backendUrl = "http://" + options.host + port + options.path + search;
47 | clientResponse.url = serverRequest.url;
48 | clientResponse.method = serverRequest.method;
49 | clientResponse.headers["x-cache"] = "MISS";
50 | fishback.proxyResponse(clientResponse, serverResponse);
51 | emit('newResponse', serverResponse);
52 | });
53 |
54 | fishback.proxyRequest(serverRequest, clientRequest);
55 |
56 | };
57 |
58 | module.exports = Client;
59 |
--------------------------------------------------------------------------------
/lib/mongodb.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var fishback = require('./fishback');
6 | var helper = require('./helper');
7 | var Handler = require('./handler');
8 |
9 | function MongoDb(client) {
10 | this.client = client;
11 | Handler.call(this);
12 | }
13 |
14 | require('util').inherits(MongoDb, Handler);
15 |
16 | MongoDb.prototype.request = function (req, res) {
17 |
18 | var emit = this.emit.bind(this);
19 |
20 | emit('newRequest', req);
21 |
22 | if (req.method !== 'GET' || !helper.wantsCache(req)) {
23 | req.emit('reject');
24 | return;
25 | }
26 |
27 | this.client.findOne({ url: req.url }, function (err, entry) {
28 |
29 | if (err) {
30 | return; // @TODO Handle the error
31 | }
32 |
33 | if (entry && helper.isVaryMatch(entry, req) && helper.isFreshEnough(entry, req)) {
34 | fishback.bufferToResponse(entry, res);
35 | emit('newResponse', res);
36 | return;
37 | }
38 |
39 | if (helper.onlyWantsCache(req)) {
40 | var buffer = {
41 | url: req.url,
42 | method: req.method,
43 | statusCode: 504,
44 | headers: { "x-cache": "MISS" },
45 | data: [ ]
46 | };
47 | fishback.bufferToResponse(buffer, res);
48 | emit('newResponse', res);
49 | return;
50 | }
51 |
52 | req.emit('reject');
53 | return;
54 |
55 | });
56 |
57 | };
58 |
59 | MongoDb.prototype.response = function (res) {
60 |
61 | if ((res.method !== 'GET') || (res.statusCode !== 200) || !helper.canCache(res)) {
62 | return;
63 | }
64 |
65 | var client = this.client;
66 |
67 | fishback.responseToBuffer(res, function (buffer) {
68 |
69 | // PREPARE ENTRY
70 |
71 | buffer.created = new Date().getTime();
72 | buffer.expires = helper.expiresAt(res);
73 | buffer.headers["x-cache"] = "HIT";
74 |
75 | client.update({ url: buffer.url }, buffer, { w: 0, upsert: true });
76 |
77 | });
78 |
79 | };
80 |
81 | MongoDb.prototype.close = function (callback) {
82 |
83 | this.client.db.close();
84 |
85 | if (callback) {
86 | return callback.call();
87 | }
88 |
89 | };
90 |
91 | module.exports = MongoDb;
92 |
--------------------------------------------------------------------------------
/lib/memcached.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var fishback = require('./fishback');
6 | var helper = require('./helper');
7 | var Handler = require('./handler');
8 |
9 | var memjs = require('memjs');
10 |
11 | function Memcached(client) {
12 | this.client = client || memjs.Client.create();
13 | Handler.call(this);
14 | }
15 |
16 | require('util').inherits(Memcached, Handler);
17 |
18 | Memcached.prototype.request = function (req, res) {
19 |
20 | var emit = this.emit.bind(this);
21 |
22 | emit('newRequest', req);
23 |
24 | if (req.method !== 'GET' || !helper.wantsCache(req)) {
25 | req.emit('reject');
26 | return;
27 | }
28 |
29 | var client = this.client;
30 |
31 | client.get(req.url, function (err, buffer) {
32 |
33 | if (err) {
34 | return; // @TODO Handle error
35 | }
36 |
37 | if (buffer && buffer.toString()) {
38 | // @TODO Handle malformed JSON
39 | var entry;
40 | try {
41 | entry = JSON.parse(buffer);
42 | } catch (e) {
43 | req.emit('reject');
44 | return;
45 | }
46 | if (helper.isVaryMatch(entry, req) && helper.isFreshEnough(entry, req)) {
47 | fishback.bufferToResponse(entry, res);
48 | emit('newResponse', res);
49 | return;
50 | }
51 | }
52 |
53 | if (helper.onlyWantsCache(req)) {
54 | fishback.bufferToResponse({
55 | url: req.url,
56 | method: req.method,
57 | statusCode: 504,
58 | headers: { "x-cache": "MISS" },
59 | data: [ ]
60 | }, res);
61 | emit('newResponse', res);
62 | return;
63 | }
64 |
65 | req.emit('reject');
66 | return;
67 |
68 | });
69 |
70 | };
71 |
72 | Memcached.prototype.response = function (res) {
73 |
74 | if ((res.method !== 'GET') || (res.statusCode !== 200) || !helper.canCache(res)) {
75 | return;
76 | }
77 |
78 | var client = this.client;
79 |
80 | fishback.responseToBuffer(res, function (entry) {
81 |
82 | // PREPARE ENTRY
83 |
84 | entry.created = new Date().getTime();
85 | entry.expires = helper.expiresAt(res);
86 | entry.headers["x-cache"] = "HIT";
87 |
88 | // INSERT ENTRY
89 |
90 | client.set(res.url, JSON.stringify(entry), function (err) {
91 | if (err) {
92 | console.log("error = ", err);
93 | }
94 | });
95 |
96 | });
97 |
98 | };
99 |
100 | Memcached.prototype.close = function () {
101 | return this.client.close();
102 | };
103 |
104 | module.exports = Memcached;
105 |
--------------------------------------------------------------------------------
/test/http.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var assert = require('assert');
6 | var util = require('util');
7 | var events = require('events');
8 |
9 | function ServerRequest(entry) {
10 | this.url = entry.url;
11 | this.method = entry.method || 'GET';
12 | this.headers = entry.headers || { };
13 | this.body = entry.body || [ ];
14 | }
15 |
16 | util.inherits(ServerRequest, events.EventEmitter);
17 |
18 | ServerRequest.prototype.fire = function () {
19 | var emit = this.emit.bind(this);
20 | this.body.forEach(function (chunk) {
21 | emit('data', chunk);
22 | });
23 | emit('end');
24 | };
25 |
26 | ServerRequest.prototype.noReject = function (s) {
27 | this.on('reject', function () {
28 | assert(false, "Unexpected reject event: " + s);
29 | });
30 | };
31 |
32 | function ServerResponse() {
33 | this.statusCode = 200;
34 | this.method = 'GET';
35 | this.headers = { };
36 | this.data = [ ];
37 | }
38 |
39 | util.inherits(ServerResponse, events.EventEmitter);
40 |
41 | ServerResponse.prototype.noEnd = function () {
42 | this.on('end', function () {
43 | assert(false, "Unexpected end event");
44 | });
45 | };
46 |
47 | ServerResponse.prototype.writeHead = function (statusCode, headers) {
48 | this.statusCode = statusCode;
49 | var h = this.headers;
50 | Object.keys(headers).forEach(function (k) {
51 | h[k] = headers[k];
52 | });
53 | };
54 |
55 | ServerResponse.prototype.setHeader = function (header, value) {
56 | this.headers[header] = value;
57 | };
58 |
59 | ServerResponse.prototype.getHeader = function (header) {
60 | return this.headers[header];
61 | };
62 |
63 | ServerResponse.prototype.write = function (chunk) {
64 | this.data.push(chunk);
65 | };
66 |
67 | ServerResponse.prototype.end = function () {
68 | };
69 |
70 | function ClientRequest() {
71 | }
72 |
73 | util.inherits(ClientRequest, events.EventEmitter);
74 |
75 | ClientRequest.prototype.end = function () {
76 | };
77 |
78 | function ClientResponse(entry) {
79 | this.url = entry.url;
80 | this.method = entry.method;
81 | this.statusCode = entry.statusCode || 200;
82 | var headers = { };
83 | Object.keys(entry.headers).forEach(function (k) {
84 | headers[k] = entry.headers[k];
85 | });
86 | this.headers = headers;
87 | this.data = entry.data || [ ];
88 | }
89 |
90 | util.inherits(ClientResponse, events.EventEmitter);
91 |
92 | ClientResponse.prototype.fire = function () {
93 | var emit = this.emit.bind(this);
94 | var data = this.data;
95 | data.forEach(function (chunk) {
96 | emit('data', chunk);
97 | });
98 | emit('end');
99 | };
100 |
101 | module.exports = {
102 | ServerRequest: ServerRequest,
103 | ServerResponse: ServerResponse,
104 | ClientRequest: ClientRequest,
105 | ClientResponse: ClientResponse
106 | };
107 |
--------------------------------------------------------------------------------
/test/http-simple.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var DELAY = 500;
6 |
7 | var lib = require("./lib");
8 | var http = require("./http");
9 | var assurt = require("./assurt");
10 | var fishback = require("../lib/fishback");
11 | var assert = require("assert");
12 |
13 | var response = {
14 | url: "/",
15 | method: "GET",
16 | statusCode: 200,
17 | headers: { "x-cache": "MISS", "cache-control": "public, max-age=60" },
18 | data: [ "Hello, World!\n" ]
19 | };
20 |
21 | lib.getCacheList(function (cache) {
22 |
23 | var client = new fishback.Client(null, null, {
24 | request: function (options, callback) {
25 | var clientResponse = new http.ClientResponse(response);
26 | callback(clientResponse);
27 | clientResponse.fire();
28 | return new http.ClientRequest();
29 | }
30 | });
31 |
32 | var proxy = fishback.createCachingProxy(cache, client);
33 |
34 | lib.step([
35 |
36 | function (next) {
37 | var req = new http.ServerRequest({ url: "/", method: "GET" });
38 | var res = new http.ServerResponse();
39 | res.on('end', assurt.calls(function () {
40 | assert.equal(res.headers["x-cache"], "MISS");
41 | setTimeout(next, DELAY);
42 | }));
43 | proxy.request(req, res);
44 | req.fire();
45 | },
46 |
47 | function (next) {
48 | var req = new http.ServerRequest({ url: "/", method: "GET" });
49 | var res = new http.ServerResponse();
50 | res.on('end', assurt.calls(function () {
51 | assert.equal(res.headers["x-cache"], "HIT");
52 | next.call();
53 | }));
54 | proxy.request(req, res);
55 | req.fire();
56 | },
57 |
58 | function (next) {
59 | var req = new http.ServerRequest({ url: "/", method: "GET" });
60 | var res = new http.ServerResponse();
61 | res.on('end', assurt.calls(function () {
62 | assert.equal(res.headers["x-cache"], "HIT");
63 | next.call();
64 | }));
65 | proxy.request(req, res);
66 | req.fire();
67 | },
68 |
69 | function (next) {
70 | var req = new http.ServerRequest({ url: "/", method: "GET" });
71 | var res = new http.ServerResponse();
72 | res.on('end', assurt.calls(function () {
73 | assert.equal(res.headers["x-cache"], "HIT");
74 | next.call();
75 | }));
76 | proxy.request(req, res);
77 | req.fire();
78 | },
79 |
80 | function (next) {
81 | var req = new http.ServerRequest({ url: "/", method: "GET" });
82 | var res = new http.ServerResponse();
83 | res.on('end', assurt.calls(function () {
84 | assert.equal(res.headers["x-cache"], "HIT");
85 | next.call();
86 | }));
87 | proxy.request(req, res);
88 | req.fire();
89 | },
90 |
91 | function () {
92 | cache.close();
93 | }
94 |
95 | ]);
96 |
97 | });
98 |
--------------------------------------------------------------------------------
/test/cache-misc.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var lib = require("./lib");
6 | var http = require("./http");
7 | var assurt = require("./assurt");
8 |
9 | var DELAY = 500;
10 |
11 | lib.getCacheList(function (cache, next) {
12 |
13 | // Add resource #1 to cache
14 |
15 | (function () {
16 |
17 | var res = new http.ClientResponse({
18 | url: "/foo",
19 | method: "GET",
20 | statusCode: 200,
21 | headers: {
22 | "cache-control": "public, max-age=60"
23 | },
24 | data: [ "Hello, Foo!" ]
25 | });
26 |
27 | cache.response(res);
28 | res.fire();
29 |
30 | })();
31 |
32 | // Add resource #2 to cache
33 |
34 | (function () {
35 |
36 | var res = new http.ClientResponse({
37 | url: "/bar",
38 | method: "GET",
39 | statusCode: 200,
40 | headers: {
41 | "cache-control": "public, max-age=60"
42 | },
43 | data: [ "Hello, Bar!" ]
44 | });
45 |
46 | cache.response(res);
47 | res.fire();
48 |
49 | })();
50 |
51 | (function () {
52 |
53 | var req = new http.ServerRequest({
54 | url: "/foo",
55 | method: "GET"
56 | });
57 | req.noReject("A");
58 |
59 | var res = new http.ServerResponse();
60 | res.once('end', assurt.once(function F1() {
61 | assurt.response(res, { headers: {}, data: "Hello, Foo!" });
62 | }));
63 |
64 | setTimeout(function () {
65 | cache.request(req, res);
66 | req.fire();
67 | }, DELAY);
68 |
69 | })();
70 |
71 | (function () {
72 |
73 | var req = new http.ServerRequest({
74 | url: "/bar",
75 | method: "GET"
76 | });
77 | req.noReject("B");
78 |
79 | var res = new http.ServerResponse();
80 | res.once('end', assurt.once(function F2() {
81 | assurt.response(res, { headers: {}, data: "Hello, Bar!" });
82 | }));
83 |
84 | setTimeout(function () {
85 | cache.request(req, res);
86 | req.fire();
87 | }, DELAY);
88 |
89 | })();
90 |
91 | (function () {
92 |
93 | var req = new http.ServerRequest({
94 | url: "/foo",
95 | method: "GET"
96 | });
97 | req.noReject("C");
98 |
99 | var res = new http.ServerResponse();
100 | res.once('end', assurt.once(function F3() {
101 | assurt.response(res, { headers: {}, data: "Hello, Foo!" });
102 | }));
103 |
104 | setTimeout(function () {
105 | cache.request(req, res);
106 | req.fire();
107 | }, DELAY);
108 |
109 | })();
110 |
111 | (function () {
112 |
113 | var req = new http.ServerRequest({
114 | url: "/quux",
115 | method: "GET"
116 | });
117 | req.once('reject', assurt.once(function F4() {
118 | cache.close();
119 | next();
120 | }));
121 |
122 | var res = new http.ServerResponse();
123 | res.noEnd();
124 |
125 | setTimeout(function () {
126 | cache.request(req, res);
127 | req.fire();
128 | }, 2 * DELAY);
129 |
130 | })();
131 |
132 | next();
133 |
134 | });
135 |
--------------------------------------------------------------------------------
/lib/memory.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var fishback = require('./fishback');
6 | var helper = require('./helper');
7 | var Handler = require('./handler');
8 |
9 | function Memory(maxSize) {
10 | this.data = {}; // hash of cache entries, keyed on URL for efficient lookup
11 | this.list = []; // array of cache entries, for efficient random access (useful for cache cleaning)
12 | this.maxSize = maxSize || 2000;
13 |
14 | Handler.call(this);
15 | }
16 |
17 | require('util').inherits(Memory, Handler);
18 |
19 | Memory.prototype.request = function (req, res) {
20 |
21 | var emit = this.emit.bind(this);
22 |
23 | var i, buffer;
24 |
25 | emit('newRequest', req);
26 |
27 | if (req.method !== 'GET') {
28 | req.emit('reject');
29 | return;
30 | }
31 |
32 | if (this.data[req.url] && helper.wantsCache(req)) {
33 | for (i = 0; i < this.data[req.url].length; i++) {
34 | buffer = this.data[req.url][i];
35 | if (helper.isVaryMatch(buffer, req) && helper.isFreshEnough(buffer, req)) {
36 | fishback.bufferToResponse(buffer, res);
37 | emit('newResponse', res);
38 | return;
39 | }
40 | }
41 | }
42 |
43 | if (helper.onlyWantsCache(req)) {
44 | fishback.bufferToResponse({
45 | url: req.url,
46 | method: req.method,
47 | statusCode: 504,
48 | headers: { "x-cache": "MISS" },
49 | data: [ ]
50 | }, res);
51 | emit('newResponse', res);
52 | return;
53 | } else {
54 | req.emit('reject');
55 | return;
56 | }
57 |
58 | };
59 |
60 | Memory.prototype.response = function (clientResponse) {
61 |
62 | if ((clientResponse.method !== 'GET') ||
63 | (clientResponse.statusCode !== 200) ||
64 | !helper.canCache(clientResponse)) {
65 | return;
66 | }
67 |
68 | fishback.responseToBuffer(clientResponse, (function (buffer) {
69 |
70 | // PREPARE ENTRY
71 |
72 | buffer.created = new Date().getTime();
73 | buffer.expires = helper.expiresAt(clientResponse);
74 | buffer.headers["x-cache"] = "HIT";
75 |
76 | // INSERT ENTRY
77 |
78 | // Clean before adding to the cache, mostly because it would be annoying
79 | // to have our newly-added cache entry cleaned right away.
80 | this.clean();
81 |
82 | if (!this.data[buffer.url]) {
83 | this.data[buffer.url] = [ buffer ];
84 | } else {
85 | this.data[buffer.url].push(buffer);
86 | }
87 |
88 | this.list.push(buffer);
89 |
90 | }).bind(this));
91 |
92 | };
93 |
94 | /**
95 | * Trims the cache to be less than or equal to this.maxSize entries.
96 | */
97 |
98 | Memory.prototype.clean = function () {
99 | if ((this.list.length === 0) || (this.list.length <= this.maxSize)) {
100 | return;
101 | }
102 |
103 | // Find the index of the LRU entry of three picked at random. The
104 | // random indices could be exactly the same, but this is unlikely
105 | // (outside of tests), and it's much easier to generate random
106 | // numbers with replacement. (And it won't lead to incorrect
107 | // results.)
108 | var index = [
109 | Math.floor(Math.random() * this.list.length),
110 | Math.floor(Math.random() * this.list.length),
111 | Math.floor(Math.random() * this.list.length)
112 | ].reduce((function (a, b) { return this.list[a].accessed < this.list[b].accessed ? a : b; }).bind(this));
113 |
114 | var entry = this.list.splice(index, 1)[0];
115 |
116 | for (var i = 0; i < this.data[entry.url].length; i++) {
117 | if (entry === this.data[entry.url][i]) {
118 | this.data[entry.url].splice(i, 1); // delete doesn't change the length of the array!
119 | if (this.data[entry.url].length === 0) {
120 | delete this.data[entry.url];
121 | }
122 | break;
123 | }
124 | }
125 | };
126 |
127 | module.exports = Memory;
128 |
--------------------------------------------------------------------------------
/lib/helper.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | /**
6 | * Returns a closure that, when called, returns the number of seconds
7 | * since the timer() function was itself called.
8 | */
9 |
10 | function timer(digits)
11 | {
12 | var t0 = new Date().getTime();
13 |
14 | return function () {
15 | var t1 = new Date().getTime();
16 | // linter doesn't like new Number(...)
17 | return Number.prototype.toFixed.call((t1 - t0) / 1000, digits || 3);
18 | };
19 | }
20 |
21 | function parseHeader(header)
22 | {
23 | return !header.trim() ? {} : header.trim().split(/\s*,\s*/).sort().reduce(function (p, c) {
24 | var t = c.split(/\s*=\s*/, 2);
25 | p[t[0].toLowerCase()] = t[1];
26 | return p;
27 | }, {});
28 | }
29 |
30 |
31 | function expiresAt(res)
32 | {
33 | if (!("expires" in res.headers) && !("cache-control" in res.headers)) {
34 | return new Date().getTime();
35 | }
36 |
37 | if (res.headers["cache-control"]) {
38 | var headers = parseHeader(res.headers["cache-control"]);
39 | if (headers["s-maxage"]) {
40 | return new Date().getTime() + (headers["s-maxage"] * 1000);
41 | } else if (headers["max-age"]) {
42 | return new Date().getTime() + (headers["max-age"] * 1000);
43 | }
44 | }
45 |
46 | if (res.headers.expires) {
47 | return Date.parse(res.headers.expires);
48 | }
49 |
50 | return null;
51 | }
52 |
53 | function canCache(res)
54 | {
55 | if (!("cache-control" in res.headers)) {
56 | return false;
57 | }
58 |
59 | var headers = parseHeader(res.headers["cache-control"]);
60 |
61 | return !("private" in headers) && !("no-store" in headers) && !("must-revalidate" in headers) &&
62 | (("public" in headers) || ("max-age" in headers) || ("s-maxage" in headers));
63 | }
64 |
65 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
66 |
67 | // true if the candidate reponse satisfies the request in terms
68 | // of freshness, otherwise false.
69 |
70 | function isFreshEnough(entry, req)
71 | {
72 | // If no cache-control header in request, then entry is fresh
73 | // if it hasn't expired.
74 | if (!("cache-control" in req.headers)) {
75 | return new Date().getTime() < entry.expires;
76 | }
77 |
78 | var headers = parseHeader(req.headers["cache-control"]);
79 |
80 | if ("must-revalidate" in headers) {
81 | return false;
82 | } else if ("max-stale" in headers) {
83 | // TODO Tell the client that the resource is stale, as RFC2616 requires
84 | return !headers["max-stale"] || ((headers["max-stale"] * 1000) > (new Date().getTime() - entry.expires)); // max-stale > "staleness"
85 | } else if ("max-age" in headers) {
86 | return (headers["max-age"] * 1000) > (new Date().getTime() - entry.created); // max-age > "age"
87 | } else if ("min-fresh" in headers) {
88 | return (headers["min-fresh"] * 1000) < (entry.expires - new Date().getTime()); // min-fresh < "time until expiry"
89 | } else {
90 | return new Date().getTime() < entry.expires;
91 | }
92 |
93 | }
94 |
95 | function wantsCache(req)
96 | {
97 | return !("cache-control" in req.headers) ||
98 | (req.headers["cache-control"].indexOf("no-cache") === -1) ||
99 | !("no-cache" in parseHeader(req.headers["cache-control"]));
100 | }
101 |
102 | // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
103 | function onlyWantsCache(req)
104 | {
105 | return ("cache-control" in req.headers) &&
106 | ("only-if-cached" in parseHeader(req.headers["cache-control"]));
107 | }
108 |
109 | function isVaryMatch(entry, req)
110 | {
111 | if (!("vary" in entry.headers)) {
112 | return true;
113 | }
114 |
115 | if (entry.headers.vary === "*") {
116 | return false;
117 | }
118 |
119 | return entry.headers.vary.split(/\s*,\s/).every(function (h) {
120 | return req.headers[h] === entry.headers[h];
121 | });
122 | }
123 |
124 | [parseHeader, expiresAt, canCache, isFreshEnough, wantsCache, onlyWantsCache, isVaryMatch].forEach(function (fn) {
125 | exports[fn.name] = fn;
126 | });
127 |
--------------------------------------------------------------------------------
/test/http-public.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var lib = require("./lib");
6 | var http = require("./http");
7 | var assurt = require("./assurt");
8 | var fishback = require("../lib/fishback");
9 |
10 | var NOW = 0;
11 | var DELAY = 500;
12 |
13 | var response = {
14 | url: '/',
15 | method: 'GET',
16 | statusCode: 200,
17 | headers: { "x-cache": "MISS", "cache-control": "max-age=60, public" },
18 | data: [ "Hello, World" ]
19 | };
20 |
21 | var expected_miss = [
22 | { headers: { "x-cache": "MISS", "cache-control": "max-age=60, public" }, data: "Hello, World" },
23 | { headers: { "x-cache": "HIT", "cache-control": "max-age=60, public" }, data: "Hello, World" },
24 | { headers: { "x-cache": "HIT", "cache-control": "max-age=60, public" }, data: "Hello, World" }
25 | ];
26 |
27 | var expected_hit = [
28 | { headers: { "x-cache": "HIT", "cache-control": "max-age=60, public" }, data: "Hello, World" },
29 | { headers: { "x-cache": "HIT", "cache-control": "max-age=60, public" }, data: "Hello, World" },
30 | { headers: { "x-cache": "HIT", "cache-control": "max-age=60, public" }, data: "Hello, World" }
31 | ];
32 |
33 | lib.getCacheList(function (cache) {
34 |
35 | Date.prototype.getTime = function() {
36 | return NOW;
37 | };
38 |
39 | var client = new fishback.Client(null, null, {
40 | request: function (options, callback) {
41 | var clientResponse = new http.ClientResponse(response);
42 | callback(clientResponse);
43 | clientResponse.fire();
44 | return new http.ClientRequest();
45 | }
46 | });
47 |
48 | var proxy = fishback.createCachingProxy(cache, client);
49 |
50 | lib.step([
51 |
52 | function (callback) {
53 |
54 | Date.prototype.getTime = function() {
55 | return NOW + 0;
56 | };
57 |
58 | lib.amap(
59 | [0, 1, 2],
60 | function (i, next) {
61 | var req = new http.ServerRequest({
62 | url: "/",
63 | method: "GET"
64 | });
65 | var res = new http.ServerResponse();
66 | res.on('end', assurt.calls(function () {
67 | assurt.response(res, expected_miss[i]);
68 | setTimeout(next, DELAY);
69 | }));
70 | proxy.request(req, res);
71 | req.fire();
72 | },
73 | callback
74 | );
75 |
76 | },
77 |
78 | // No cache misses
79 | function (callback) {
80 |
81 | Date.prototype.getTime = function() {
82 | return NOW + 30000;
83 | };
84 |
85 | lib.amap(
86 | [0, 1, 2],
87 | function (i, next) {
88 | var req = new http.ServerRequest({
89 | url: "/",
90 | method: "GET"
91 | });
92 | var res = new http.ServerResponse();
93 | res.on('end', assurt.calls(function () {
94 | assurt.response(res, expected_hit[i]);
95 | setTimeout(next, DELAY);
96 | }));
97 | proxy.request(req, res);
98 | req.fire();
99 | },
100 | callback
101 | );
102 |
103 | },
104 |
105 | // Should get a cache miss the first time, because we're 120 seconds
106 | // on.
107 | function (callback) {
108 |
109 | Date.prototype.getTime = function() {
110 | return NOW + 120000;
111 | };
112 |
113 | lib.amap(
114 | [0, 1, 2],
115 | function (i, next) {
116 | var req = new http.ServerRequest({
117 | url: "/",
118 | method: "GET"
119 | });
120 | var res = new http.ServerResponse();
121 | res.on('end', assurt.calls(function () {
122 | assurt.response(res, expected_miss[i]);
123 | setTimeout(next, DELAY);
124 | }));
125 | proxy.request(req, res);
126 | req.fire();
127 | },
128 | callback
129 | );
130 |
131 | },
132 |
133 | function () {
134 | cache.close();
135 | }
136 |
137 | ]);
138 |
139 | });
140 |
--------------------------------------------------------------------------------
/test/http-only-if-cached.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var assert = require('assert');
6 | var assurt = require("./assurt");
7 | var lib = require('./lib');
8 | var http = require("./http");
9 | var fishback = require("../lib/fishback");
10 |
11 | var NOW = 198025200000;
12 |
13 | lib.getCacheList(function (cache) {
14 |
15 | (function () {
16 |
17 | var client = new fishback.Client(null, null, {
18 | request: function () { assert.equal(false, true); }
19 | });
20 |
21 | var proxy = fishback.createCachingProxy(cache, client);
22 |
23 | var req = new http.ServerRequest({
24 | url: "/",
25 | method: "GET",
26 | headers: { "cache-control": "only-if-cached" }
27 | });
28 |
29 | var res = new http.ServerResponse();
30 | res.on('end', assurt.once(function F1() {
31 | assurt.response(res, {
32 | statusCode: 504,
33 | headers: { "x-cache": "MISS" },
34 | data: ""
35 | });
36 | cache.close();
37 | }));
38 |
39 | proxy.request(req, res);
40 | req.fire();
41 |
42 | })();
43 |
44 | });
45 |
46 | lib.getCacheList(function (cache, next) {
47 |
48 | (function () {
49 |
50 | var response = {
51 | url: "/",
52 | method: "GET",
53 | statusCode: 200,
54 | headers: { "x-cache": "MISS", "cache-control": "public, max-age=60" },
55 | body: [ "Hello, World!\n" ]
56 | };
57 |
58 | var client = new fishback.Client(null, null, {
59 | request: function (options, callback) {
60 | var clientResponse = new http.ClientResponse(response);
61 | callback(clientResponse);
62 | clientResponse.fire();
63 | return new http.ClientRequest();
64 | }
65 | });
66 |
67 | var proxy = fishback.createCachingProxy(cache, client);
68 |
69 | lib.step([
70 |
71 | function (callback) {
72 | Date.prototype.getTime = function() {
73 | return NOW;
74 | };
75 | var req = new http.ServerRequest({ url: "/", method: "GET" });
76 | var res = new http.ServerResponse();
77 | res.on('end', assurt.once(function F2() {
78 | assert.equal(res.statusCode, 200);
79 | assert.equal(res.headers["x-cache"], "MISS");
80 | callback();
81 | }));
82 | proxy.request(req, res);
83 | req.fire();
84 | },
85 | function (callback) {
86 | var req = new http.ServerRequest({ url: "/", method: "GET" });
87 | var res = new http.ServerResponse();
88 | res.on('end', assurt.once(function F3() {
89 | assert.equal(res.statusCode, 200);
90 | assert.equal(res.headers["x-cache"], "HIT");
91 | callback();
92 | }));
93 | proxy.request(req, res);
94 | req.fire();
95 | },
96 | function (callback) {
97 | var req = new http.ServerRequest({
98 | url: "/",
99 | method: "GET",
100 | headers: { "cache-control": "only-if-cached, max-age=60" }
101 | });
102 | var res = new http.ServerResponse();
103 | res.on('end', assurt.once(function F4() {
104 | assert.equal(res.statusCode, 200);
105 | assert.equal(res.headers["x-cache"], "HIT");
106 | callback();
107 | }));
108 | proxy.request(req, res);
109 | req.fire();
110 | },
111 | function (callback) {
112 | Date.prototype.getTime = function() {
113 | return NOW + 120000;
114 | };
115 | var req = new http.ServerRequest({
116 | url: "/",
117 | method: "GET",
118 | headers: { "cache-control": "only-if-cached, max-age=60" }
119 | });
120 | var res = new http.ServerResponse();
121 | res.on('end', assurt.once(function F5() {
122 | assert.equal(res.statusCode, 504);
123 | assert.equal(res.headers["x-cache"], "MISS");
124 | callback();
125 | cache.close();
126 | }));
127 | proxy.request(req, res);
128 | req.fire();
129 | }
130 |
131 | ]);
132 | })();
133 |
134 | next();
135 | });
136 |
--------------------------------------------------------------------------------
/lib/fishback.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var Handler = require("./handler");
6 |
7 | var Client = require("./client");
8 | var Memory = require("./memory");
9 | var MongoDb = require("./mongodb");
10 | var Memcached = require("./memcached");
11 |
12 | function Fishback(list) {
13 | this.list = list;
14 | Handler.call(this);
15 | }
16 |
17 | require('util').inherits(Fishback, Handler);
18 |
19 | Fishback.prototype.request = function (req, res) {
20 |
21 | var emit = this.emit.bind(this);
22 |
23 | emit('newRequest', req);
24 |
25 | res.on('endHead', function () {
26 | emit('newResponse', res, req);
27 | });
28 |
29 | // Call list[0].request(), if we get a "reject", call list[1].request(),
30 | // and so on.
31 | function process(list) {
32 | var head = list[0];
33 | var tail = list.slice(1);
34 | // @TODO Handle null head (all handlers emitted "reject")
35 | req.once("reject", function () {
36 | process(tail);
37 | });
38 | head.request(req, res);
39 | }
40 |
41 | process(this.list);
42 |
43 | };
44 |
45 | function proxyRequest(serverRequest, clientRequest) {
46 | // Because of inconsistencies in the http.request() API,
47 | // clientRequest.headers can't be written to; the caller needs to have
48 | // proxied these already.
49 | serverRequest.on('data', function (chunk) {
50 | clientRequest.write(chunk);
51 | });
52 | serverRequest.on('end', function () {
53 | clientRequest.end();
54 | });
55 | serverRequest.on('error', function () {
56 | clientRequest.end();
57 | });
58 | }
59 |
60 | function proxyResponse(clientResponse, serverResponse) {
61 | serverResponse.url = clientResponse.url;
62 | serverResponse.backendUrl = clientResponse.backendUrl;
63 | serverResponse.method = clientResponse.method;
64 | // Avoid writeHead() to give listeners a change to modify the serverResponse
65 | serverResponse.statusCode = clientResponse.statusCode;
66 | serverResponse.headers = { }; // read-only!
67 | Object.keys(clientResponse.headers).forEach(function (k) {
68 | serverResponse.setHeader(k, clientResponse.headers[k]);
69 | serverResponse.headers[k] = clientResponse.headers[k];
70 | });
71 | serverResponse.emit('endHead');
72 | clientResponse.on('data', function (chunk) {
73 | serverResponse.write(chunk);
74 | serverResponse.emit('data', chunk);
75 | });
76 | clientResponse.on('end', function () {
77 | serverResponse.end();
78 | serverResponse.emit('end');
79 | });
80 | }
81 |
82 | function bufferToResponse(buffer, serverResponse) {
83 | serverResponse.url = buffer.url;
84 | serverResponse.method = buffer.method;
85 | // Avoid writeHead() to give listeners a change to modify the serverResponse
86 | serverResponse.statusCode = buffer.statusCode;
87 | serverResponse.headers = { };
88 | Object.keys(buffer.headers).forEach(function (k) {
89 | serverResponse.setHeader(k, buffer.headers[k]);
90 | // Copy to "headers" property as a convenience--the "headers" property
91 | // is read-only. If trying to modify the response, use the setHeader()
92 | // method.
93 | serverResponse.headers[k] = buffer.headers[k];
94 | });
95 | serverResponse.emit('endHead');
96 | // nextTick so that anything listening for endHead has the chance to attach
97 | // listeners for the "data" and "end" events
98 | process.nextTick(function () {
99 | buffer.data.forEach(function (chunk) {
100 | serverResponse.write(chunk);
101 | serverResponse.emit('data', chunk);
102 | });
103 | serverResponse.end();
104 | serverResponse.emit('end');
105 | });
106 | }
107 |
108 | // clientResponse can be a ServerResponse or a ClientResponse (?)
109 | function responseToBuffer(clientResponse, callback) {
110 | var buffer = {
111 | url: clientResponse.url,
112 | method: clientResponse.method,
113 | statusCode: clientResponse.statusCode,
114 | headers: { },
115 | data: [ ]
116 | };
117 | Object.keys(clientResponse.headers).forEach(function (k) {
118 | buffer.headers[k] = clientResponse.headers[k];
119 | });
120 | clientResponse.on('data', function (chunk) {
121 | buffer.data.push(chunk);
122 | // @TODO If data gets too big, callback(null)
123 | });
124 | clientResponse.on('end', function () {
125 | // @TODO Dispose of clientResponse and remove listeners?
126 | callback(buffer);
127 | });
128 | clientResponse.on('close', function () {
129 | // @TODO Dispose of clientResponse and remove listeners?
130 | callback(null);
131 | });
132 | }
133 |
134 | function createCachingProxy(cache, client) {
135 | cache = cache || new Memory();
136 | client = client || new Client();
137 |
138 | // Save any new responses emitted by the client to the cache
139 | var fishback = new Fishback([cache, client]);
140 | client.on('newResponse', function (serverResponse) {
141 | cache.response(serverResponse);
142 | });
143 |
144 | return fishback;
145 | }
146 |
147 | function createProxy(client) {
148 | client = client || new Client();
149 |
150 | return new Fishback([client]);
151 | }
152 |
153 | [
154 | Client, Memory, Memcached, MongoDb, Fishback,
155 | createProxy, createCachingProxy,
156 | bufferToResponse, responseToBuffer,
157 | proxyRequest, proxyResponse
158 | ].forEach(function (fn) {
159 | exports[fn.name] = fn;
160 | });
161 |
--------------------------------------------------------------------------------
/test/lib.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var fishback = require("../lib/fishback");
6 |
7 | /**
8 | * Asynchronous map function. For each element of arr, fn(element, callback) is
9 | * called, where callback receives the result. Note that the individual "maps"
10 | * are performed sequentially; the result, however, is delivered to a callback,
11 | * instead of being returned, and the maps can be asynchronous.
12 | *
13 | * Example:
14 | *
15 | * function aadd(i, callback) {
16 | * callback(i + 1);
17 | * }
18 | *
19 | * amap([ 2, 3, 4 ], aadd, console.log);
20 | * // -> [ 3, 4, 5 ]
21 | *
22 | * @param arr the array over which
23 | * @param fn function of the form function(n, callback)
24 | * @param callback function of the form function(arr)
25 | */
26 |
27 | function amap(arr, fn, callback) {
28 |
29 | // https://gist.github.com/846521
30 |
31 | process.nextTick(function () {
32 | if (arr.length === 0) {
33 | callback([]);
34 | } else {
35 | fn(arr[0], function (v) {
36 | amap(arr.slice(1), fn, function (list) {
37 | callback([v].concat(list));
38 | });
39 | });
40 | }
41 | });
42 |
43 | }
44 |
45 | /**
46 | * Passed array of async functions (i.e. whose last argument is a callback), and calls
47 | * them in order.
48 | *
49 | * Example:
50 | *
51 | * step([
52 | * function (i, callback) {
53 | * console.log("got ", i);
54 | * callback(null, 7);
55 | * },
56 | * function (i, callback) {
57 | * console.log("got ", i);
58 | * callback();
59 | * }
60 | * ], null, 6);
61 | * // -> got 6
62 | * // -> got 7
63 | *
64 | * @param tasks array of functions with arguments ([arg]..., callback); callback has arguments (err, arg...)
65 | * @param [optional] errback called if any of the tasks returns an error
66 | * @param [optional] arg... arguments to the first task
67 | */
68 |
69 | // Libraries that do similar things:
70 | //
71 | // https://github.com/creationix/step
72 | // https://github.com/caolan/async
73 |
74 | function step(tasks, errback) {
75 |
76 | if (tasks && tasks[0]) {
77 | var args = Array.prototype.slice.call(arguments, 2);
78 | // Empty the event queue, to ensure isolation between steps
79 | process.nextTick(function () {
80 | tasks[0].apply(null, args.concat(function () { // Note: exception thrown if tasks[0] not a function
81 | var args = Array.prototype.slice.call(arguments);
82 | if (args[0] && errback) {
83 | errback(args[0]); // error returned, abort tasks (Note: exception thrown if errback not a function)
84 | } else {
85 | step.apply(null, [tasks.slice(1)].concat(errback, args.slice(1)));
86 | }
87 | }));
88 | });
89 | }
90 |
91 | }
92 |
93 | /**
94 | * Returns a function that, when called n times, in turn calls callback.
95 | *
96 | * @param {int} n
97 | * @param {Function} callback
98 | * @return {Function}
99 | */
100 | function knock(n, callback) {
101 | if (n <= 0) {
102 | callback();
103 | return function () { };
104 | } else {
105 | return function () {
106 | if (--n === 0) {
107 | callback();
108 | }
109 | };
110 | }
111 | }
112 |
113 | /**
114 | * @param {object} req
115 | * @param {Function} callback
116 | */
117 | function group(req, callback) {
118 | var res = { };
119 | Object.keys(req).forEach(function (k) {
120 | req[k](function () {
121 | var args = Array.prototype.slice.call(arguments);
122 | res[k] = args.length === 1 ? args[0] : args;
123 | if (Object.keys(res).length === Object.keys(req).length) {
124 | callback(res);
125 | }
126 | });
127 | });
128 | }
129 |
130 | function getCacheMemory(callback) {
131 | callback(new fishback.Memory());
132 | }
133 |
134 | function getCacheMemcached(callback) {
135 | var client = require('memjs').Client.create();
136 | client.flush(function () {
137 | callback(new fishback.Memcached(client));
138 | });
139 | }
140 |
141 | function getCacheMongoDb(callback) {
142 | var uri = process.env.MONGOLAB_URI ||
143 | process.env.MONGOHQ_URL ||
144 | 'mongodb://localhost:27017/fishback';
145 |
146 | var collname = "test" + (Math.random() + Math.pow(10, -9)).toString().substr(2, 8);
147 |
148 | require('mongodb').MongoClient.connect(uri, function (err, client) {
149 |
150 | if (err) { console.error(err); return; }
151 |
152 | function createCollection() {
153 | client.createCollection(collname, { capped: true, size: 10000 }, function (err, coll) {
154 | if (err) { console.error(err); return; }
155 | // @TODO Add index to url
156 | // http://mongodb.github.com/node-mongodb-native/api-generated/db.html#ensureindex
157 | callback(new fishback.MongoDb(coll));
158 | });
159 | }
160 |
161 | client.collectionNames(collname, function (err, coll) {
162 | if (err) { console.error(err); return; }
163 | if (coll.length) {
164 | client.dropCollection(collname, function (err) {
165 | if (err) { console.error(err); return; }
166 | createCollection();
167 | });
168 | } else {
169 | createCollection();
170 | }
171 | });
172 |
173 | });
174 | }
175 |
176 | function getCacheList(callback) {
177 |
178 | function _getCacheList(list) {
179 | var head = list[0];
180 | var tail = list.slice(1);
181 | if (head) {
182 | head(function (cache) {
183 | callback(cache, _getCacheList.bind(null, tail));
184 | });
185 | }
186 | }
187 |
188 | _getCacheList([ getCacheMemory, getCacheMemcached ]);
189 | }
190 |
191 | [knock, group, amap, step, getCacheList, getCacheMemory, getCacheMongoDb, getCacheMemcached].forEach(function (fn) {
192 | exports[fn.name] = fn;
193 | });
194 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/ithinkihaveacat/node-fishback)
2 |
3 | ## Overview
4 |
5 | Fishback is a simple NodeJS-powered caching HTTP proxy.
6 |
7 | As well as supporting different caching backends, the design lends itself to
8 | filtering and processing the headers of both requests and responses. (For
9 | example, changing `Cache-Control` headers.) It is not well-suited to
10 | transforming request or response *bodies*, though it can be integrated into
11 | systems that do provide this feature.
12 |
13 | Fishback tries hard to be RFC2616 compliant (and many of the slightly unusual
14 | features like `only-if-cached` and `max-stale` are supported), but there's
15 | probably some things it doesn't do completely correctly. (Though any variation
16 | from RFC2616 should be considered a bug.)
17 |
18 | ## Example
19 |
20 | ````js
21 | var fishback = require("../lib/fishback");
22 | var http = require("http");
23 |
24 | var proxy = fishback.createProxy(new fishback.Client("localhost", 9000));
25 | proxy.on("newRequest", function (req) {
26 | console.log(req.method + " " + req.url);
27 | });
28 | proxy.on("newResponse", function (res) {
29 | res.setHeader("cache-control", "public, max-age=3600");
30 | });
31 |
32 | http.createServer(proxy.request.bind(proxy)).listen(8000);
33 |
34 | console.log("Listening on port 8000, and proxying to localhost:9000");
35 | ````
36 |
37 | For more, see the [examples](examples) directory.
38 |
39 | ## Installation
40 |
41 | ````sh
42 | $ npm install fishback
43 | ````
44 |
45 | ## API
46 |
47 | Fishback is heavily event based, and it relies heavily on the four event
48 | emitters `http.ServerRequest`, `http.ServerResponse`, `http.ClientRequest` and
49 | `http.ClientResponse`.
50 |
51 | In contrast to most NodeJS "middleware" systems (including
52 | [Connect](http://www.senchalabs.org/connect/)), Fishback itself does not contain
53 | a web server. Instead, Fishback provides a handler for `http.Server's`
54 | ['request' event](http://nodejs.org/api/http.html#http_event_request).
55 |
56 | ### fishback.createProxy(client)
57 |
58 | * `fishback.Handler` `client` - probably a `fishback.Client`
59 |
60 | Convenience function for creating a simple proxy from a client.
61 |
62 | ### fishback.createCachingProxy(cache, client)
63 |
64 | * `fishback.Handler` `cache` - probably one of the cache backends
65 | * `fishback.Handler` `client` - probably a `fishback.Client`
66 |
67 | Convenience function for creating a proxy from a cache and client.
68 |
69 | ### Class: fishback.Handler(...)
70 |
71 | "Abstract" base class for all handlers. All the classes below (the HTTP client
72 | that does real requests, the various cache backends, and the Fishback class that
73 | ties them together) are derived from this class.
74 |
75 | #### Event: 'newRequest'
76 |
77 | `function (serverRequest) { }`
78 |
79 | * `http.ServerRequest` `serverRequest`
80 |
81 | Emitted when a new request has been received. (At the point the event is
82 | emitted, only headers are available, though you can of course arrange to listen
83 | to other events.)
84 |
85 | #### Event: 'newResponse'
86 |
87 | `function (serverResponse) { }`
88 |
89 | * `http.ServerResponse` `serverResponse`
90 |
91 | Emitted when a new response is being sent. (At the point the event is emitted,
92 | only headers are available. Because of limitations in the
93 | [http.ServerResponse](http://nodejs.org/api/http.html#http_class_http_serverresponse)
94 | API (`write()` does not fire any events), it is not possible to observe any
95 | "write" events.)
96 |
97 | #### cache.request(serverRequest, serverResponse)
98 |
99 | * `http.ServerRequest` `serverRequest`
100 | * `http.ServerResponse` `serverResponse`
101 |
102 | Processes a request/response pair.
103 |
104 | If unable to handle the request (e.g. resource is not cached), `serverRequest`
105 | will emit the `reject` event.
106 |
107 | If request is accepted (i.e. the handler is writing to `serverResponse`),
108 | `serverResponse` will emit the `endHead` event when headers have been set on the
109 | response.
110 |
111 | (If overriding this method, note that the handler must ensure that if a request
112 | is rejected, any handlers that may subsequently be invoked are actually able to
113 | fulfill the request! The most important implication of this constraint is that
114 | if the request method is not `GET`, it must be rejected immediately
115 | (synchronously). If the method is `GET`, the request can be rejected
116 | asynchronously since subsequent handlers do not need any information from `data`
117 | events they would otherwise have missed).)
118 |
119 | #### cache.response(clientResponse)
120 |
121 | * `http.ClientResponse` `clientResponse`
122 |
123 | Process a *client* response.
124 |
125 | This is really only useful for caching handlers--it allows them to populate
126 | their caches from responses to any real HTTP requests that are issued.
127 |
128 | (For example, `fishback.createCachingProxy()` arranges things so that if the
129 | cache handler fires a `reject` event, a "real" HTTP request to be issued; the
130 | response from this request is then passed to the cache.)
131 |
132 | ### Class: fishback.Fishback(list)
133 |
134 | Derived from `fishback.Handler`.
135 |
136 | * `list` an array of `fishback.Handler` objects
137 |
138 | The last object in list is assumed to be a real HTTP client that will never
139 | `reject` a request. The other objects can reject requests.
140 |
141 | ### Class: fishback.Client(backend_hostname, backend_port, http)
142 |
143 | Derived from `fishback.Handler`.
144 |
145 | * `backend_hostname` - e.g. 'localhost'
146 | * `backend_port` - e.g. 80
147 | * `http` - object with a `request()` method, such as `require('http')`
148 |
149 | Does a real HTTP request.
150 |
151 | ### Class: fishback.Memory()
152 |
153 | Derived from `fishback.Handler`.
154 |
155 | Caching backend.
156 |
157 | ### Class: fishback.MongoDb()
158 |
159 | Derived from `fishback.Handler`.
160 |
161 | ## Bugs
162 |
163 | * There's no HTTPS support.
164 | * If the proxy server is able to read from the origin faster than the client
165 | can receive data, content needs to be buffered, either by node or the
166 | kernel. (This can be fixed by backing off when `write()` returns false, and
167 | resuming only when the ["drain"
168 | event](http://nodejs.org/api/stream.html#stream_event_drain) is
169 | triggered. This is only likely to be a problem if you're streaming very
170 | large files through node.)
171 | * ETags (and `must-revalidate`) are not supported. (You don't get incorrect
172 | results; you just need retrieve the entire resource from the origin each
173 | time.)
174 |
175 | ## See Also
176 |
177 | If you're only after a proxy (rather than a caching proxy),
178 | [node-http-proxy](https://github.com/nodejitsu/node-http-proxy) may be more
179 | suitable.
180 |
181 | ## Author
182 |
183 | Michael Stillwell
184 |
185 |
--------------------------------------------------------------------------------
/test/helper.js:
--------------------------------------------------------------------------------
1 | /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, node:true, indent:4, maxerr:50, globalstrict:true */
2 |
3 | "use strict";
4 |
5 | var assert = require('assert');
6 | var fishback = require('../lib/helper');
7 | var lib = require('./lib');
8 |
9 | (function () {
10 |
11 | assert.doesNotThrow(function () {
12 | lib.step([]);
13 | });
14 |
15 | })();
16 |
17 | (function () {
18 |
19 | var a = [];
20 |
21 | lib.step([
22 | function (callback) {
23 | a.push(1);
24 | callback(null, "a");
25 | },
26 | function (s, callback) {
27 | a.push(s);
28 | a.push(2);
29 | callback(null, "b");
30 | },
31 | function (s, callback) {
32 | a.push(s);
33 | a.push(3);
34 | callback(null, "c");
35 | },
36 | function (s, callback) {
37 | assert.equal(s, "c");
38 | assert.deepEqual(a, [1, "a", 2, "b", 3]);
39 | callback();
40 | }
41 | ]);
42 |
43 | })();
44 |
45 | (function () {
46 |
47 | var a = [];
48 |
49 | lib.step(
50 | [
51 | function (callback) {
52 | a.push(1);
53 | callback("ooops", "a");
54 | },
55 | function (s, callback) {
56 | // This shouldn't be called, because previous function returned error
57 | a.push(s);
58 | a.push(2);
59 | callback();
60 | }
61 | ], function (err) {
62 | assert.equal(err, "ooops");
63 | assert.deepEqual(a, [ 1 ]);
64 | }
65 | );
66 |
67 | })();
68 |
69 | (function () {
70 |
71 | var data = [
72 | [ { }, false ],
73 | [ { "cache-control": "public" }, true ],
74 | [ { "cache-control": "s-maxage=7773, public, foo=bar" }, true ],
75 | [ { "cache-control": "no-cache, foo=bar" }, false ],
76 | [ { "cache-control": "no-store, foo=bar" }, false ],
77 | [ { "cache-control": "s-maxage=7773, private, foo=bar" }, false ],
78 | [ { "cache-control": "s-maxage=7773, qqq=public, foo=bar" }, true ],
79 | [ { "cache-control": "qqq=public, foo=bar" }, false ],
80 | [ { "expires": "Tue, 17 Jan 2012 00:49:02 GMT", "cache-control": "public, max-age=31536000" }, true ]
81 | ];
82 |
83 | data.forEach(function (d) {
84 | assert.equal(fishback.canCache({ headers: d[0] }), d[1], require('util').inspect(d));
85 | });
86 |
87 | })();
88 |
89 | (function () {
90 |
91 | var now = 1295222561275;
92 | Date.prototype.getTime = function () { return now; };
93 |
94 | var data = [
95 | [
96 | { created: (now - (180 * 1000)), expires: (now + (180 * 1000)) },
97 | { "cache-control": "max-age=120" },
98 | false
99 | ],
100 | [
101 | { created: now, expires: now + (180 * 1000) },
102 | { "cache-control": "max-age=120" },
103 | true
104 | ],
105 | [
106 | { created: 0, expires: now - (60 * 1000) },
107 | { "cache-control": "max-stale=120" },
108 | true
109 | ],
110 | [
111 | { created: 0, expires: now - (60 * 1000) },
112 | { "cache-control": "max-stale=120, max-age=0" },
113 | true
114 | ],
115 | [
116 | { created: 0, expires: now - (60 * 1000) },
117 | { "cache-control": "max-stale=30" },
118 | false
119 | ],
120 | [
121 | { created: 0, expires: now - (60 * 1000) },
122 | { "cache-control": "max-stale" },
123 | true
124 | ],
125 | [
126 | { created: 0, expires: now + (60 * 1000) },
127 | { "cache-control": "min-fresh=30" },
128 | true
129 | ],
130 | [
131 | { created: 0, expires: now + (60 * 1000) },
132 | { "cache-control": "min-fresh=120" },
133 | false
134 | ],
135 | [
136 | { created: 0, expires: now - (60 * 1000) },
137 | { },
138 | false
139 | ],
140 | [
141 | { created: 0, expires: now + (60 * 1000) },
142 | { "cache-control": "max-age=0" },
143 | false
144 | ],
145 | [
146 | { created: 0, expires: now + (60 * 1000) },
147 | { "cache-control": "must-revalidate" },
148 | false
149 | ],
150 | [
151 | { created: 0, expires: now - (60 * 1000) },
152 | { "cache-control": "jjj" },
153 | false
154 | ]
155 | ];
156 |
157 | data.forEach(function (d) {
158 | assert.equal(fishback.isFreshEnough(d[0], { headers: d[1] }), d[2], require('util').inspect(d));
159 | });
160 |
161 | })();
162 |
163 | (function () {
164 |
165 | var getTime = Date.prototype.getTime;
166 |
167 | var now = 1295222561275;
168 | Date.prototype.getTime = function () { return now; };
169 |
170 | var data = [
171 | [ { }, 1295222561275 ],
172 | [ { "expires": "Thu, 01 Dec 1994 16:00:00 GMT" }, 786297600000 ],
173 | [ { "cache-control": "max-age=60" }, 1295222621275 ]
174 | ];
175 |
176 | data.forEach(function (d) {
177 | assert.equal(fishback.expiresAt({ headers: d[0] }), d[1]);
178 | });
179 |
180 | Date.prototype.getTIme = getTime;
181 |
182 | })();
183 |
184 | (function () {
185 |
186 | var data = [
187 | [ { "cache-control": "jjjj", "foo": "no-cache" }, true ],
188 | [ { "cache-control": "no-cachejjj,foo=no-cache" }, true ],
189 | [ { "cache-control": "no-cachejjj,foo=no-cache, no-cache", "foo": "bar" }, false ]
190 | ];
191 |
192 | data.forEach(function (d) {
193 | assert.equal(fishback.wantsCache({ headers: d[0] }), d[1]);
194 |
195 | });
196 |
197 | })();
198 |
199 | (function () {
200 |
201 | var data = [
202 | [ { "cache-control": "jjjj", "foo": "no-cache" }, false ],
203 | [ { "cache-control": "no-cachejjj,only-if-cached" }, true ]
204 | ];
205 |
206 | data.forEach(function (d) {
207 | assert.equal(fishback.onlyWantsCache({ headers: d[0] }), d[1]);
208 |
209 | });
210 |
211 | })();
212 |
213 | (function () {
214 |
215 | var data = [
216 | [
217 | { },
218 | { },
219 | true
220 | ],
221 | [
222 | { "foo": "bar" },
223 | { },
224 | true
225 | ],
226 | [
227 | { "foo": "bar", "vary": "*" },
228 | { }, // doesn't have foo: bar
229 | false
230 | ],
231 | [
232 | { "foo": "bar", "vary": "quux" },
233 | { },
234 | true
235 | ],
236 | [
237 | { "foo": "bar", "vary": "foo" },
238 | { },
239 | false
240 | ],
241 | [
242 | { "foo": "bar", "vary": "foo" },
243 | { "foo": "bar" },
244 | true
245 | ]
246 |
247 | ];
248 |
249 | data.forEach(function (d) {
250 | assert.equal(fishback.isVaryMatch({ headers: d[0] }, { headers: d[1] }), d[2]);
251 | });
252 |
253 | })();
254 |
255 | (function () {
256 |
257 | var data = [
258 | [ "", {} ],
259 | [ " ", {} ],
260 | [ "foo=bar ", { "foo": "bar" } ],
261 | [ "foo=bar,baz", { "foo": "bar", "baz": undefined } ],
262 | [ " MAX-AGE=60, private", { "max-age": 60, "private": undefined } ]
263 | ];
264 |
265 | data.forEach(function (d) {
266 | assert.deepEqual(fishback.parseHeader(d[0]), d[1]);
267 | });
268 |
269 | })();
270 |
271 | (function () {
272 | var calledFoo = false;
273 | var calledBar = false;
274 | lib.group({
275 | foo: function (callback) {
276 | process.nextTick(function () {
277 | calledFoo = true;
278 | callback("qqqq");
279 | });
280 | },
281 | bar: function (callback) {
282 | process.nextTick(function () {
283 | calledBar = true;
284 | callback(1, 2, 3);
285 | });
286 | }
287 | }, function (group) {
288 | assert.equal(true, calledFoo);
289 | assert.equal(true, calledBar);
290 | assert.deepEqual({ foo: "qqqq", bar: [ 1, 2, 3 ]}, group);
291 | });
292 | assert.equal(false, calledFoo);
293 | assert.equal(false, calledBar);
294 | })();
295 |
296 | (function () {
297 | var called = false;
298 | var n = 0;
299 | var knock = lib.knock(n, function () {
300 | assert.equal(false, called);
301 | called = true;
302 | });
303 | assert.equal(true, called);
304 | knock();
305 | assert.equal(true, called);
306 | })();
307 |
308 | (function () {
309 | var called = false;
310 | var n = 3;
311 | var knock = lib.knock(n, function () {
312 | assert.equal(false, called);
313 | called = true;
314 | });
315 | assert.equal(false, called);
316 | knock();
317 | assert.equal(false, called);
318 | knock();
319 | assert.equal(false, called);
320 | knock();
321 | assert.equal(true, called);
322 | assert.equal(3, n);
323 | })();
324 |
--------------------------------------------------------------------------------