Skip to content
This repository has been archived by the owner on Apr 6, 2019. It is now read-only.

Commit

Permalink
Work in progress, looks like we are going nowhere
Browse files Browse the repository at this point in the history
Unless we dont have a mechanism to detect that there is only one chunk that is being sent(for damn sure?), we can't implement this
  • Loading branch information
r0mflip committed Sep 16, 2018
1 parent eee7ecc commit a0c56d6
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 95 deletions.
30 changes: 6 additions & 24 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,35 +1,17 @@
{
"extends": "google",
"extends": "r0mflip",
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"max-len": [2, 100, {
"ignoreComments": true,
"max-len": [2, {
"code": 100,
"tabWidth": 2,
"ignoreUrls": true,
"tabWidth": 2
}],
"no-implicit-coercion": [2, {
"boolean": false,
"number": true,
"string": true
}],
"no-unused-expressions": [2, {
"allowShortCircuit": true,
"allowTernary": false
}],
"no-unused-vars": [2, {
"vars": "all",
"args": "after-used",
"argsIgnorePattern": "(^reject$|^_$)",
"varsIgnorePattern": "(^_$)"
}],
"quotes": [2, "single"],
"require-jsdoc": 0,
"valid-jsdoc": 0,
"arrow-parens": 0
"ignoreComments": true
}]
}
}
7 changes: 6 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
dist: trusty
language: node_js
sudo: false
node_js:
- "9"
- "8"
- "10"
script:
- npm run test
notification:
on_success: never
on_failure: always
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# taggart

[![Greenkeeper badge](https://badges.greenkeeper.io/ramlmn/taggart.svg)](https://greenkeeper.io/)

An ETag header middleware for express like apps. It is a zero dependency
middleware which generates `ETags` accurately.

**Note:** `express.static` uses `serve-static` which internally uses `send`.
`send` generates/supports only weak `ETags`. If you want strong etags, then you
have to write your own cache invalidation in your server, regardless of strong
or weak.

## Installation

``` bash
Expand All @@ -27,10 +30,10 @@ app.use(...);

# Working

`taggart` simply overrides the `write`, `end` and `send` methods of the `res`
`taggart` simply overrides the `write` and `end` methods of the `res`
(`ServerResponse` object) and tries to gather around the response content
being sent to the client, at the end, `taggart` adds the `ETag` header to the
response if it is't buffered or tansfered in chunks.
response if it is't buffered (or tansfered in chunks).

## License
[MIT](LICENSE)
117 changes: 77 additions & 40 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,92 @@

const crypto = require('crypto');

const noop = _ => {};

/**
* An ETag header middleware for express like apps
*
* @param {Object} req http IncomingMessage
* @param {Object} res http ServerReponse
* @param {Function} next next middleware to call
* @param {Object} opts options
* @param {Boolean} opts.weak weak tags?
* @return {Function}
*/
const addEtag = (req, res, next = _ => {}) => {
const write = res.write;
const end = res.end;
const send = res.send;

const onData = (...args) => {
if (!res.headersSent
&& !res.getHeader('Transfer-Encoding')
&& !res.getHeader('TE')
&& res.getHeader('Content-Length')) {
// we are in luck
const body = args[0];

// @TODO: allow weak etags

// genarate the etag
const etag = crypto.createHash('sha1')
.update(body)
.digest('hex');

res.setHeader('ETag', etag);
}
};
const addEtag = opts => {
const weak = (opts !== undefined) ? !!opts.weak : false;

// override the default methods and collect response buffer
return (req, res, next = noop) => {
const write = res.write;
const end = res.end;

res.write = (...args) => {
onData(...args);
write.apply(res, [...args]);
};
// sha1 ain't that bad
const hash = crypto.createHash('sha1');

res.end = (...args) => {
onData(...args);
end.apply(res, [...args]);
};
// keep track, for content-length
let length = 0;

res.send = (...args) => {
onData(...args);
send.apply(res, [...args]);
};
let onData = (chunk, encoding) => {
const TE = (res.getHeader('Transfer-Encoding') || res.getHeader('TE') || '').toLowerCase();

// if (chunk) {
// console.log(Buffer.byteLength(chunk, 'utf8'), res.headersSent, TE);
// } else {
// console.log(undefined, res.headersSent, TE);
// }

if (res.headersSent || TE === 'chunked') {
// bail
onData = noop;
return;
}

// convert chunk to buffer
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);

hash.update(chunk);
length += Buffer.byteLength(chunk, 'utf8');
};

const onEnd = (chunk, encoding) => {
onData(chunk, encoding);

next();
// generate tag
const l = length.toString(16);
const h = hash.digest('hex');

const tag = weak ? `W/${l}-${h}` : `${l}-${h}`;

// check if headers can be sent, ignore TE
if (!res.headersSent) {
res.setHeader('Content-Length', length);

if (res.getHeader('ETag')) {
res.removeHeader('ETag');
}

res.setHeader('ETag', tag);
}
};

// override the default methods
res.write = (...args) => {
onData(...args);
write.apply(res, [...args]);
};

res.end = (...args) => {
onEnd(...args);
end.apply(res, [...args]);
};

// non standard, express like
res.send = (...args) => {
onEnd(...args);
end.apply(res, [...args]);
};

if (typeof next === 'function') {
next();
}
};
};


Expand Down
33 changes: 18 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
{
"name": "@ramlmn/taggart",
"version": "0.1.0",
"author": "@r0mflip",
"description": "An ETag header middleware for express like apps",
"main": "index.js",
"scripts": {
"test": "eslint . && node test/test.js"
},
"engines": {
"node": ">=8.0.0"
},
"homepage": "https://github.com/ramlmn/taggart#readme",
"bugs": {
"url": "https://github.com/ramlmn/taggart/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ramlmn/taggart.git"
},
"license": "MIT",
"devDependencies": {
"eslint": "^5.6.0",
"eslint-config-r0mflip": "^0.1.3",
"express": "^4.16.3",
"node-fetch": "^2.2.0",
"tap-spec": "^5.0.0",
"tape": "^4.9.1"
},
"keywords": [
"ETag",
"e-tag",
"headers",
"express",
"middleware"
],
"author": "@mram",
"license": "MIT",
"bugs": {
"url": "https://github.com/ramlmn/taggart/issues"
},
"homepage": "https://github.com/ramlmn/taggart#readme",
"devDependencies": {
"eslint": "^5.2.0",
"eslint-config-google": "^0.9.1",
"express": "^4.16.3",
"node-fetch": "^2.1.2",
"tap-spec": "^5.0.0",
"tape": "^4.9.0"
}
]
}
5 changes: 5 additions & 0 deletions test/static/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Attributions

* [atari-adventure.jpg](atari-adventure.jpg) by Scott Canoni from [WikiMedia Commons](https://commons.wikimedia.org/wiki/File:Atari_Adventure_Easter_Egg_on_Atari_(Jakks_Pacific)_Port.jpg)
* Big buck bunny [trailer](trailer_400p.ogg) from [https://peach.blender.org/trailer-page/](https://peach.blender.org/trailer-page/)
* Lipsum text from [https://lipsum.com](https://lipsum.com)
Binary file added test/static/atari-adventure.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions test/static/small-text.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
values of β give rises to dom!
Binary file added test/static/trailer_400p.ogg
Binary file not shown.
65 changes: 54 additions & 11 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,44 @@ tape
.pipe(tapSpec())
.pipe(process.stdout);

tape('ETag test', async t => {
t.plan(2);

tape('ETags test in express', async t => {
t.plan(5);

const app = express();
app.set('etag', false);
app.use(taggart);
app.use(express.static(path.resolve(__dirname, 'public')));
app.use(taggart());
app.use(express.static(path.resolve(__dirname, 'static')));

const server = app.listen(0, async _ => {
const {port} = server.address();

try {
const res = await fetch(`http://localhost:${port}/garble.txt`);
const res = await fetch(`http://localhost:${port}/large-text.txt`);
const resETag = res.headers.get('ETag');
const testTag = 'W/"6281-165de617a21"';

if (resETag && testTag === resETag.toLowerCase()) {
t.pass(`Large text - got valid ETag ${resETag}`);
} else {
t.fail(`Large text - got invalid ETag ${resETag}`);
}
} catch (e) {
t.fail('Large text - Failed: ', e);
}

try {
const res = await fetch(`http://localhost:${port}/small-text.txt`);
const resETag = res.headers.get('ETag');
const testTag = '691544a391db46480b9a425ae3126fe2a0ec22fa';

if (resETag && testTag === resETag.toLowerCase()) {
t.pass(`Test 1: Got valid ETag: ${resETag}`);
t.pass(`Small text - got valid ETag ${resETag}`);
} else {
t.fail(`Test 1: Got invalid ETag: ${resETag}`);
t.fail(`Small text - got invalid ETag ${resETag}`);
}
} catch (e) {
t.fail('Test 1: Failed: ', e);
t.fail('Small text - Failed: ', e);
}

try {
Expand All @@ -45,12 +60,40 @@ tape('ETag test', async t => {
const testTag = 'b13764e8ffd613c4734f67fe51e47afd6bd903f2';

if (resETag && testTag === resETag.toLowerCase()) {
t.pass(`Test 2: Got valid ETag: ${resETag}`);
t.pass(`JSON request - got valid ETag ${resETag}`);
} else {
t.fail(`JSON request - got invalid ETag ${resETag}`);
}
} catch (e) {
t.fail('JSON request: Failed: ', e);
}

try {
const res = await fetch(`http://localhost:${port}/trailer_400p.ogg`);
const resETag = res.headers.get('ETag');
const testTag = 'b13764e8ffd613c4734f67fe51e47afd6bd903f2';

if (resETag && testTag === resETag.toLowerCase()) {
t.pass(`Video request - got valid ETag ${resETag}`);
} else {
t.fail(`Video request - got invalid ETag ${resETag}`);
}
} catch (e) {
t.fail('Video request: Failed: ', e);
}

try {
const res = await fetch(`http://localhost:${port}/atari-adventure.jpg`);
const resETag = res.headers.get('ETag');
const testTag = 'b13764e8ffd613c4734f67fe51e47afd6bd903f2';

if (resETag && testTag === resETag.toLowerCase()) {
t.pass(`Video request - got valid ETag ${resETag}`);
} else {
t.fail(`Test 2: Got invalid ETag: ${resETag}`);
t.fail(`Video request - got invalid ETag ${resETag}`);
}
} catch (e) {
t.fail('Test 2: Failed: ', e);
t.fail('Video request: Failed: ', e);
}

server.close();
Expand Down

0 comments on commit a0c56d6

Please sign in to comment.