Merge branch 'optional-plugins' into develop

This commit is contained in:
James Willcox 2014-06-24 12:57:11 -05:00
Родитель e6630709a1 cc11a815e0
Коммит 53239c789a
10 изменённых файлов: 162 добавлений и 65 удалений

Просмотреть файл

@ -40,6 +40,36 @@ pac:
# Plugin settings below.
#
# If a plugin has no config, the default looks like this:
#
# pluginName:
# enabled: false
# optional: false
#
# If 'enabled' is true, the plugin is loaded and used
# by default. If false, it is not. If 'optional' is
# true, it means the plugin can be enabled and disabled
# by request-specific headers. The initial state is
# still determined by the 'enabled' value.
# DOM plugin, provides its own plugin system for
# operating on a parsed HTML DOM
dom:
enabled: true
# ingress plugin, reads data from the forwarded response
ingress:
enabled: true
# egress plugin delivers the end result to the client
egress:
enabled: true
# gunzip plugin unzips gzip-formatted content, usually
# because we need to parse it for the DOM plugin
gunzip:
enabled: true
# Cache settings.
cache:
@ -77,15 +107,18 @@ imgcompression:
# Adblock settings.
adblock:
enabled: false
optional: true
# Gzip compression settings.
gzip:
enabled: true
# Compression level.
level: 9
# GIF to video transcoding settings.
gif2video:
enabled: false
optional: true
# Metrics reporting settings.
metrics:

Просмотреть файл

@ -2,6 +2,7 @@
var url = require('url');
var http = require('http');
var log = require('../log');
var NAME = exports.name = 'adblock';
@ -28,6 +29,8 @@ function fetchBlockList(list) {
// Use the hashtable as a set
list[hostname] = true;
}
log.debug('fetched adblock list');
});
});
}
@ -35,11 +38,11 @@ function fetchBlockList(list) {
var blockList = {};
fetchBlockList(blockList);
exports.handleRequest = function(request, response) {
exports.handleRequest = function(request, response, options, callback) {
emitter.signal('start');
var requestedUrl = url.parse(request.headers.path || request.url);
var hosts = requestedUrl.hostname.split('.');
var requestedUrl = url.parse(request.originalUrl);
var hosts = requestedUrl.host.split('.');
// We try to match all the subdomains
// e.g.: a.b.example.com => a.b.example.com, b.example.com, example.com
@ -48,15 +51,17 @@ exports.handleRequest = function(request, response) {
if (h in blockList) {
emitter.signal('count', 'hit');
request.log('BLOCKED', requestedUrl.href);
response.writeHead(403);
request.debug('blocked', requestedUrl.href);
response.writeHead(403, '', { 'content-type': 'text/plain' });
response.write('Blocked by adblock');
response.end();
emitter.signal('end');
return true;
callback(null, true);
return;
}
}
emitter.signal('count', 'miss');
emitter.signal('end');
return false;
callback(null, false);
};

Просмотреть файл

@ -41,7 +41,7 @@ function transcodeGIF(source, callback) {
exports.name = 'gif2video';
// Intercept GIF requests and serve transcoded to webm
exports.handleRequest = function(request) {
exports.handleRequest = function(request, response, options, callback) {
var gifUrl = pendingIntercepts[request.url];
if (gifUrl) {
request.log('replacing %s with %s', request.url, gifUrl);
@ -50,6 +50,8 @@ exports.handleRequest = function(request) {
request.originalUrl = request.url;
request.url = gifUrl;
}
callback(null, false);
};
// Replace <img src="foo.gif"> with <video autoplay loop src="foo.gif">

Просмотреть файл

@ -4,14 +4,16 @@ var CONFIG = require('config');
var util = require('../util');
var cheerio = require('cheerio');
var plugins = util.loadPluginsSync(__dirname, CONFIG);
var ALL_PLUGINS = util.loadPluginsSync(__dirname, CONFIG);
exports.name = 'dom';
// Runs DOM manipulations
exports.handleResponse = function(request, source, dest, options) {
var plugins = util.filterPlugins(ALL_PLUGINS.dom, options);
if (util.matchHeaders(source.headers, { 'content-type': /html/ })) {
request.log('intercepting for DOM manipulation: ' +
request.debug('intercepting for DOM manipulation: ' +
source.headers['content-type']);
var docdata = '';
@ -24,9 +26,9 @@ exports.handleResponse = function(request, source, dest, options) {
var i = 0;
function nextPlugin() {
if (i < plugins.dom.length) {
plugins.dom[i++].handleDOMResponse(request, source, $,
nextPlugin, options);
if (i < plugins.length) {
plugins[i++].handleDOMResponse(request, source, $,
nextPlugin, options);
} else {
// No more DOM plugins, write out the new (presumably changed) DOM
dest.write(new Buffer($.html()), function() {

Просмотреть файл

@ -20,7 +20,7 @@ exports.handleResponse = function(request, source, dest) {
source.on('end', function() {
var finalBuffer = Buffer.concat(bufs);
request.log('egress (accumulated) %d bytes', finalBuffer.length);
request.debug('egress (accumulated) %d bytes', finalBuffer.length);
dest.statusCode = source.statusCode;
dest.headers = source.headers;
@ -49,7 +49,7 @@ exports.handleResponse = function(request, source, dest) {
});
source.on('end', function() {
request.log('egress (streaming) %d bytes', count);
request.debug('egress (streaming) %d bytes', count);
dest.end();
emitter.signal('end');
});

Просмотреть файл

@ -1,5 +1,7 @@
'use strict';
var CONFIG = require('config');
var zlib = require('zlib');
var util = require('./util');
@ -9,7 +11,7 @@ var NAME = exports.name = 'gzip';
var emitter = require('../emit').get(NAME);
// Adds gzip compression for text/* if the agent accepts it
exports.handleResponse = function(request, source, dest, options) {
exports.handleResponse = function(request, source, dest) {
if (util.matchHeaders(request.headers, { 'accept-encoding': /gzip/ }) &&
util.matchHeaders(source.headers,
{ 'content-type': /(text\/|\/json)/, 'content-encoding': false })) {
@ -19,7 +21,7 @@ exports.handleResponse = function(request, source, dest, options) {
dest.accumulate = true;
dest.contentLengthChange = true;
source.pipe(zlib.createGzip({ options: options.gzip.level })).pipe(dest);
source.pipe(zlib.createGzip({ options: CONFIG.gzip.level })).pipe(dest);
} else {
// Do nothing
emitter.signal('count', 'miss');

Просмотреть файл

@ -4,19 +4,21 @@ var CONFIG = require('config');
var util = require('./util');
var plugins = util.loadPluginsSync(__dirname, CONFIG);
var ALL_PLUGINS = util.loadPluginsSync(__dirname, CONFIG);
exports.handleRequest = function(request, response, options, callback) {
var plugins = util.filterPlugins(ALL_PLUGINS.request, options);
// Handles request with given plugin (per id) and recursively calls the next
// plugin handler if unsuccessful.
function handleRequest(pluginIndex) {
if (pluginIndex >= plugins.request.length) {
if (pluginIndex >= plugins.length) {
// No plugin could successfully handle the request.
callback(null, false);
return;
}
var plugin = plugins.request[pluginIndex];
var plugin = plugins[pluginIndex];
plugin.handleRequest(request, response, options, function(err, handled) {
if (!err && handled) {
@ -36,8 +38,9 @@ exports.handleRequest = function(request, response, options, callback) {
exports.handleResponse = function(request, source, dest, options) {
var currentSource = source;
var currentDest = null;
plugins.response.forEach(function(plugin, i) {
currentDest = i === plugins.response.length - 1 ?
var plugins = util.filterPlugins(ALL_PLUGINS.response, options);
plugins.forEach(function(plugin, i) {
currentDest = i === plugins.length - 1 ?
dest :
new util.PipedResponse(currentSource);
plugin.handleResponse(request, currentSource, currentDest, options);

Просмотреть файл

@ -8,7 +8,7 @@ var emitter = require('../emit').get(NAME);
exports.handleResponse = function(request, source, dest) {
emitter.signal('start');
request.log('headers: ', source.headers);
request.debug('headers: ', source.headers);
var count = 0;
source.on('data', function(b) {
count += b.length;
@ -16,13 +16,13 @@ exports.handleResponse = function(request, source, dest) {
});
source.on('end', function() {
request.log('ingress %d bytes', count);
request.debug('ingress %d bytes', count);
dest.end();
emitter.signal('end');
});
source.on('error', function(err) {
request.log('error', err);
request.error(err);
dest.statusCode = 500;
dest.end();
emitter.signal('end');

Просмотреть файл

@ -7,6 +7,31 @@ var join = require('path').join;
var yaml = require('js-yaml');
var log = require('../log');
var CONFIG = require('config');
var shouldUsePlugin = exports.shouldUsePlugin = function(plugin, options) {
var pluginConfig = CONFIG[plugin.name];
if (!pluginConfig)
// Plugins disabled by default
return false;
}
if (pluginConfig.optional) {
var val = options.enabled.indexOf(plugin.name) >= 0;
return val;
} else if (pluginConfig.hasOwnProperty('enabled')) {
return pluginConfig.enabled;
} else {
return true;
}
};
exports.filterPlugins = function(plugins, options) {
return plugins.filter(function(plugin) {
return shouldUsePlugin(plugin, options);
});
};
exports.loadPluginsSync = function(dir, config) {
var manifest = yaml.safeLoad(fs.readFileSync(join(dir,
'plugins.yaml'), 'utf8'));
@ -17,10 +42,10 @@ exports.loadPluginsSync = function(dir, config) {
plugins[key] = [];
manifest[key].forEach(function(moduleName) {
var p = require(join(dir, moduleName));
if (config[p.name] &&
config[p.name].hasOwnProperty('enabled') &&
!config[p.name].enabled) {
log.debug('blocked plugin: ' + p.name);
if (!config[p.name] ||
(!config[p.name].enabled && !config[p.name].optional))
{
log.debug('disabled plugin: ' + p.name);
return;
}

Просмотреть файл

@ -38,6 +38,25 @@ var SpdyProxy = function(options) {
log.debug('%s listens on port %d', options.name, options.proxy.port);
}
function parseRequestOptions(request) {
var options = { enabled: [], disabled: [] };
var header = request.headers['x-gonzales-options'];
if (!header) {
return options;
}
var split = header.trim().split(' ');
split.forEach(function(token) {
if (token[0] === '+') {
options.enabled.push(token.substring(1));
} else if (token[0] === '-') {
options.disabled.push(token.substring(1));
}
});
return options;
}
// Handles GET and POST request.
function handleRequest(request, response) {
emitter.signal('start', 'request');
@ -63,51 +82,57 @@ var SpdyProxy = function(options) {
request.debug('HTTP/' + request.httpVersion + ' ' + request.method);
emitter.signal('start', 'request.plugin.request');
plugins.handleRequest(request, response, options, function(err, handled) {
emitter.signal('end', 'request.plugin.request');
var requestOptions = parseRequestOptions(request);
request.debug('request options', requestOptions);
if (handled) {
// Request was serviced by a plugin.
emitter.signal('end', 'request');
return;
}
plugins.handleRequest(request, response, requestOptions,
function(err, handled) {
emitter.signal('end', 'request.plugin.request');
var forwardRequest = http.request(httpOpts, function(forwardResponse) {
// Pass 300 responses straight through
//
// This is kind of terrible, and really just a hack to work around
// the fact that our plugins all expect a succesful response right now.
if (forwardResponse.statusCode >= 300 &&
forwardResponse.statusCode < 400) {
response.writeHead(forwardResponse.statusCode,
forwardResponse.headers);
forwardResponse.pipe(response);
if (handled) {
// Request was serviced by a plugin.
emitter.signal('end', 'request');
return;
}
forwardResponse.headers['proxy-agent'] = options.title;
var forwardRequest = http.request(httpOpts, function(forwardResponse) {
// Pass 300 responses straight through
//
// This is kind of terrible, and really just a hack to work around
// the fact that our plugins all expect a succesful
// response right now.
if (forwardResponse.statusCode >= 300 &&
forwardResponse.statusCode < 400) {
response.writeHead(forwardResponse.statusCode,
forwardResponse.headers);
forwardResponse.pipe(response);
return;
}
emitter.signal('start', 'request.plugin.response');
forwardResponse.headers['proxy-agent'] = options.title;
plugins.handleResponse(request, forwardResponse, response, options);
emitter.signal('start', 'request.plugin.response');
emitter.signal('end', 'request.plugin.response');
emitter.signal('end', 'request');
plugins.handleResponse(request, forwardResponse, response,
requestOptions);
emitter.signal('end', 'request.plugin.response');
emitter.signal('end', 'request');
});
forwardRequest.on('error', function(e) {
console.error('Client error: '.error + e.message);
response.writeHead(502, 'Proxy fetch failed');
response.end();
});
// Pipe POST data.
request.pipe(forwardRequest);
response.on('close', function() {
forwardRequest.abort();
});
});
forwardRequest.on('error', function(e) {
console.error('Client error: '.error + e.message);
response.writeHead(502, 'Proxy fetch failed');
response.end();
});
// Pipe POST data.
request.pipe(forwardRequest);
response.on('close', function() {
forwardRequest.abort();
});
});
}
// Handles CONNECT request.