From dba2ea1a3f760aa7066a7184517cfe26eb9e9f32 Mon Sep 17 00:00:00 2001 From: Baptiste LARVOL-SIMON Date: Wed, 6 Feb 2019 17:31:27 +0100 Subject: [PATCH] Add Cookies support --- index.js | 6 ++- lib/cookies.js | 128 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/tasks.js | 58 ++++++++++++++++++++-- package.json | 13 ++++- 4 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 lib/cookies.js diff --git a/index.js b/index.js index 176e02b..b6f5385 100755 --- a/index.js +++ b/index.js @@ -25,6 +25,7 @@ app .option('-a, --analytics', 'checks for Google Analytics & Piwik') .option('-t, --tracking', 'checks for Social Media tracking & embeds') .option('-c, --cdn', 'checks for Content Delivery Networks') + .option('-k, --cookies [expiration delay, in month]', 'checks for cookies lifetime (< 13 month by defaut)', false) //.option('-r, --recursive', 'tries to follow links to check every internal site', false) .action((url, args) => { // Error Handling @@ -46,9 +47,10 @@ app if (args.parent.mute) ui.set('silent'); // initialize the task runner - const tasks = new Tasks(url, ui); + const tasks = new Tasks(url, ui, args); if (args.ssl) tasks.new('ssl'); + if (args.cookies) tasks.new('cookies'); if (args.fonts) tasks.new('fonts'); if (args.prefetching) tasks.new('prefetching'); if (args.analytics) tasks.new('analytics'); @@ -72,4 +74,4 @@ app app.parse(process.argv); -//if (!app.args.length) app.help(); \ No newline at end of file +//if (!app.args.length) app.help(); diff --git a/lib/cookies.js b/lib/cookies.js new file mode 100644 index 0000000..af96f49 --- /dev/null +++ b/lib/cookies.js @@ -0,0 +1,128 @@ +/*! + * cookies (adaptated from server-side cookies from https://github.com/pillarjs/cookies) + * Copyright(c) 2014 Jed Schmidt, http://jed.is/ + * Copyright(c) 2015-2016 Douglas Christopher Wilson + * Copyright(c) 2018 Baptiste LARVOL-SIMON, http://www.e-glop.net/ + * MIT Licensed + */ + +'use strict' + +var deprecate = require('depd')('cookies') +var Keygrip = require('keygrip') +var cache = {} + +/** + * RegExp to match field-content in RFC 7230 sec 3.2 + * + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * obs-text = %x80-FF + */ + +var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; + +/** + * RegExp to match Same-Site cookie attribute value. + */ + +var sameSiteRegExp = /^(?:lax|strict)$/i + +function Cookies(response){ + this.response = response; +} + +/** + * @return Array + **/ +Cookies.prototype.fetchAll = function() { + var header, cookies = []; + + header = this.response.headers["set-cookie"]; + if (!header) return []; + + header.forEach(cook => { + var name, + attrs = [], + data = cook.split(/\\{0}; /) + + name = data.shift().split('='); + data.forEach(attr => { + var dat = attr.split('='); + attrs[dat[0]] = dat[1]; + }); + + cookies.push(new Cookie(name[0], name[1], attrs)); + }); + + return cookies; +} + +function Cookie(name, value, attrs) { + if (!fieldContentRegExp.test(name)) { + throw new TypeError('argument name is invalid'); + } + + if (value && !fieldContentRegExp.test(value)) { + throw new TypeError('argument value is invalid'); + } + + value || (this.expires = new Date(0)) + + this.name = name + this.value = value || "" + + for (var name in attrs) { + switch ( name ) { + case 'secure': + this[name] = true; + break; + case 'expires': + this[name] = new Date(attrs[name]); + break; + default: + this[name] = attrs[name]; + } + } + + if (this.path && !fieldContentRegExp.test(this.path)) { + throw new TypeError('option path is invalid'); + } + + if (this.domain && !fieldContentRegExp.test(this.domain)) { + throw new TypeError('option domain is invalid'); + } + + if (this.sameSite && this.sameSite !== true && !sameSiteRegExp.test(this.sameSite)) { + throw new TypeError('option sameSite is invalid') + } +} + +Cookie.prototype.path = "/"; +Cookie.prototype.expires = undefined; +Cookie.prototype.domain = undefined; +Cookie.prototype.httpOnly = true; +Cookie.prototype.sameSite = false; +Cookie.prototype.secure = false; +Cookie.prototype.overwrite = false; + +// back-compat so maxage mirrors maxAge +Object.defineProperty(Cookie.prototype, 'maxage', { + configurable: true, + enumerable: true, + get: function () { return this.maxAge }, + set: function (val) { return this.maxAge = val } +}); +deprecate.property(Cookie.prototype, 'maxage', '"maxage"; use "maxAge" instead') + +function getPattern(name) { + if (cache[name]) return cache[name] + + return cache[name] = new RegExp( + "(?:^|;) *" + + name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") + + "=([^;]*)" + ) +} + +module.exports = Cookies diff --git a/lib/tasks.js b/lib/tasks.js index f559a29..9705a9d 100644 --- a/lib/tasks.js +++ b/lib/tasks.js @@ -8,9 +8,10 @@ const HTMLParser = require('./html-parser'); const FontsParser = require('./fonts-parser'); // const tools = require('./tools'); const UI = require('./ui'); +const Cookies = require('./cookies'); class Tasks { - constructor(url, uiInstance) { + constructor(url, uiInstance, args) { this.default_tasks = { normalize: { dependencies: [], @@ -29,6 +30,9 @@ class Tasks { dependencies: ['html', 'css', 'js'], mandatory: true }, + cookies: { + dependencies: ['html', 'css'] + }, ssl: { dependencies: ['html', 'css'] }, @@ -57,6 +61,7 @@ class Tasks { this.ui = (!uiInstance) ? new UI() : uiInstance; this.hp; + this.args = args; this.url = url; this.tasks = []; this.data = {}; @@ -69,13 +74,15 @@ class Tasks { /** * Adds a new Task to the list * @param {string} task + * @param {string} arg value * @class Tasks */ - new(task) { + new(task, value) { this.default_tasks[task].dependencies.forEach(dep => { if (this.tasks.indexOf(dep) === -1) this.tasks.push(dep); }); if (this.tasks.indexOf(task) === -1) this.tasks.push(task); + if ( value !== undefined ) this.value = value; } @@ -159,6 +166,7 @@ class Tasks { this.getPrefetchingInformation(); this.getAnalyticsInformation(); this.getCDNInformation(); + this.getCookiesInformation(); console.log(''); // console.log('Remaining: ', this.tasks); @@ -395,6 +403,50 @@ class Tasks { } + /** + * Gathers Cookies Information + * @class Tasks + */ + getCookiesInformation() { + if (!this.hasTask('cookies')) { + return; + } + + this.args.cookies = parseInt(this.args.cookies,10) ? parseInt(this.args.cookies,10) : 13; + + // go away if no cookie is given + if (typeof this.data.html.headers['set-cookie'] != 'object') { + return; + } + + var getter = new Cookies(this.data.html); + var cookies = getter.fetchAll(); + + if ( cookies.length == 0 ) { + this.ui.headline('No cookie set'); + this.remove('cookies'); + return; + } + + var strtotime = require('locutus/php/datetime/strtotime'); + + this.ui.headline('Cookies'); + for ( var i in cookies ) { + var t1 = cookies[i].expires.getTime(), + t2 = new Date(strtotime('+'+this.args.cookies+' month 1 hour')*1000).getTime(); + + if ( t1 > t2 ) { + this.ui.error('Cookie "'+cookies[i].name+'" expires in more than '+this.args.cookies+' month (expires on '+cookies[i].expires.toLocaleDateString()+')', true); + continue; + } + this.ui.listitem(cookies[i].name, 'Expires on '+cookies[i].expires.toLocaleDateString()+' and '+(cookies[i].secure ? 'secure' : 'unsecure')); + } + + this.remove('cookies'); + return; + } + + /** * Gathes General Information (meta data) of the Website * @class Tasks @@ -670,4 +722,4 @@ class Tasks { } } -module.exports = Tasks; \ No newline at end of file +module.exports = Tasks; diff --git a/package.json b/package.json index 85d1514..48987fd 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,22 @@ "cliui": "^4.1.0", "commander": "^2.15.1", "css": "^2.2.3", + "depd": "~1.1.2", "fs-extra": "^6.0.1", "get-ssl-certificate": "^2.1.2", "got": "^8.3.1", + "keygrip": "~1.0.3", + "locutus": "^2.0.10", "moment": "^2.22.1", "rootpath": "^0.1.2" }, "devDependencies": { - "ava": "^0.25.0" + "ava": "^0.25.0", + "eslint": "3.19.0", + "express": "4.16.4", + "istanbul": "0.4.5", + "mocha": "5.2.0", + "restify": "6.4.0", + "supertest": "3.3.0" } -} \ No newline at end of file +}