Merge branch 'optional-plugins' into develop
This commit is contained in:
Коммит
53239c789a
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
97
lib/proxy.js
97
lib/proxy.js
|
@ -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.
|
||||
|
|
Загрузка…
Ссылка в новой задаче