You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
doushio/server/web.js

582 lines
14 KiB

var _ = require('../lib/underscore'),
auth = require('./auth'),
caps = require('./caps'),
config = require('../config'),
formidable = require('formidable'),
hooks = require('../hooks'),
Stream = require('stream'),
url_parse = require('url').parse,
util = require('util'),
winston = require('winston');
var send;
if (config.SERVE_STATIC_FILES)
send = require('send');
var escape = require('../common').escape_html;
var routes = [];
var resources = [];
var server = require('http').createServer(function (req, resp) {
var ip = req.connection.remoteAddress;
var country;
if (config.TRUST_X_FORWARDED_FOR)
ip = parse_forwarded_for(req.headers['x-forwarded-for']) || ip;
if (config.CLOUDFLARE) {
ip = req.headers['cf-connecting-ip'] || ip;
country = req.headers['cf-ipcountry'];
}
if (!ip) {
resp.writeHead(500, {'Content-Type': 'text/plain'});
resp.end("Your IP could not be determined. "
+ "This server is misconfigured.");
return;
}
req.ident = caps.lookup_ident(ip, country);
if (req.ident.timeout)
return timeout(resp);
if (req.ident.ban)
return render_500(resp);
if (req.ident.slow)
return slow_request(req, resp);
handle_request(req, resp);
});
exports.server = server;
function handle_request(req, resp) {
var method = req.method.toLowerCase();
var parsed = url_parse(req.url, true);
req.url = parsed.pathname;
req.query = parsed.query;
req.cookies = parse_cookie(req.headers.cookie);
var numRoutes = routes.length;
for (var i = 0; i < numRoutes; i++) {
var route = routes[i];
if (method != route.method)
continue;
var m = req.url.match(route.pattern);
if (m) {
route.handler(req, resp, m);
if (config.DEBUG)
winston.verbose(route.method.toUpperCase() + ' ' + req.url);
return;
}
}
if (method == 'get' || method == 'head')
for (var i = 0; i < resources.length; i++)
if (handle_resource(req, resp, resources[i]))
return;
if (config.SERVE_IMAGES) {
if (require('../imager').serve_image(req, resp))
return;
}
if (config.SERVE_STATIC_FILES) {
send(req, req.url, {root: 'www/'}).pipe(resp);
return;
}
render_404(resp);
if (config.DEBUG)
winston.verbose('404 ' + req.url + ' fallthrough');
}
function handle_resource(req, resp, resource) {
var m = req.url.match(resource.pattern);
if (!m)
return false;
var args = [req];
if (resource.headParams)
args.push(m);
args.push(resource_second_handler.bind(null, req, resp, resource));
var cookie = auth.extract_login_cookie(req.cookies);
if (cookie) {
auth.check_cookie(cookie, function (err, ident) {
if (err && !resource.authPassthrough) {
if (config.DEBUG)
winston.verbose('DENY ' + req.url + ' (' + err + ')');
return forbidden(resp, 'No cookie.');
}
else if (!err)
_.extend(req.ident, ident);
resource.head.apply(null, args);
});
}
else if (!resource.authPassthrough) {
if (config.DEBUG)
winston.verbose('DENY ' + req.url);
render_404(resp);
}
else
resource.head.apply(null, args);
return true;
}
function resource_second_handler(req, resp, resource, err, act, arg) {
var method = req.method.toLowerCase();
var log = config.DEBUG;
if (err) {
if (err == 404) {
if (log)
winston.verbose('404 ' + req.url);
return render_404(resp);
}
else if (err != 500)
winston.error(err);
else if (log)
winston.verbose('500 ' + req.url);
return render_500(resp);
}
else if (act == 'ok') {
if (log)
winston.verbose(method.toUpperCase() + ' ' + req.url + ' 200');
if (method == 'head') {
var headers = (arg && arg.headers) || noCacheHeaders;
resp.writeHead(200, headers);
resp.end();
if (resource.tear_down)
resource.tear_down.call(arg);
}
else {
if (resource.tear_down) {
if (!arg)
arg = {};
arg.finished = function () {
resource.tear_down.call(arg);
};
}
resource.get.call(arg, req, resp);
}
}
else if (act == 304) {
resp.writeHead(304);
resp.end();
if (log)
winston.verbose('304 ' + req.url);
}
else if (act == 'redirect' || (act >= 300 && act < 400)) {
var headers = {Location: arg};
if (act == 'redirect')
act = 303;
if (log)
winston.verbose(act + ' ' + req.url + ' to ' + arg);
if (act == 303.1) {
act = 303;
headers['X-Robots-Tag'] = 'nofollow';
}
resp.writeHead(act, headers);
resp.end();
}
else if (act == 'redirect_js') {
if (log)
winston.verbose('303.js ' + req.url + ' to ' + arg);
if (method == 'head') {
resp.writeHead(303, {Location: arg});
resp.end();
}
else
redirect_js(resp, arg);
}
else
throw new Error("Unknown resource handler: " + act);
}
exports.route_get = function (pattern, handler) {
routes.push({method: 'get', pattern: pattern,
handler: auth_passthrough.bind(null, handler)});
};
exports.resource = function (pattern, head, get, tear_down) {
if (head === true)
head = function (req, cb) { cb(null, 'ok'); };
var res = {pattern: pattern, head: head, authPassthrough: true};
res.headParams = (head.length == 3);
if (get)
res.get = get;
if (tear_down)
res.tear_down = tear_down;
resources.push(res);
};
exports.resource_auth = function (pattern, head, get, finished) {
if (head === true)
head = function (req, cb) { cb(null, 'ok'); };
var res = {pattern: pattern, head: head, authPassthrough: false};
res.headParams = (head.length == 3);
if (get)
res.get = get;
if (finished)
res.finished = finished;
resources.push(res);
};
function parse_forwarded_for(ff) {
if (!ff)
return null;
var ips = ff.split(',');
if (!ips.length)
return null;
var last = ips[ips.length - 1].trim();
/* check that it looks like some kind of IPv4/v6 address */
if (!/^[\da-fA-F.:]{3,45}$/.test(last))
return null;
return last;
}
exports.parse_forwarded_for = parse_forwarded_for;
function auth_passthrough(handler, req, resp, params) {
var cookie = auth.extract_login_cookie(req.cookies);
if (!cookie) {
handler(req, resp, params);
return;
}
auth.check_cookie(cookie, function (err, ident) {
if (!err)
_.extend(req.ident, ident);
handler(req, resp, params);
});
}
exports.route_get_auth = function (pattern, handler) {
routes.push({method: 'get', pattern: pattern,
handler: auth_checker.bind(null, handler, false)});
};
function auth_checker(handler, is_post, req, resp, params) {
if (is_post) {
var form = new formidable.IncomingForm();
form.maxFieldsSize = 50 * 1024;
form.type = 'urlencoded';
try {
form.parse(req, function (err, fields) {
if (err) {
resp.writeHead(500, noCacheHeaders);
resp.end(preamble + escape(err));
return;
}
req.body = fields;
check_it();
});
}
catch (e) {
winston.error('formidable threw: ' + e);
return forbidden(resp, 'Bad request.');
}
}
else
check_it();
function check_it() {
cookie = auth.extract_login_cookie(req.cookies);
if (!cookie)
return forbidden(resp, 'No cookie.');
auth.check_cookie(cookie, ack);
}
function ack(err, session) {
if (err)
return forbidden(resp, err);
if (is_post && session.csrf != req.body.csrf)
return forbidden(resp, "Possible CSRF.");
_.extend(req.ident, session);
handler(req, resp, params);
}
}
function forbidden(resp, err) {
resp.writeHead(401, noCacheHeaders);
resp.end(preamble + escape(err));
}
exports.route_post = function (pattern, handler) {
// auth_passthrough conflicts with formidable
// (by the time the cookie check comes back, formidable can't
// catch the form data)
// We don't need the auth here anyway currently thanks to client_id
routes.push({method: 'post', pattern: pattern, handler: handler});
};
exports.route_post_auth = function (pattern, handler) {
routes.push({method: 'post', pattern: pattern,
handler: auth_checker.bind(null, handler, true)});
};
var noCacheHeaders = {'Content-Type': 'text/html; charset=UTF-8',
'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'Cache-Control': 'no-cache, no-store',
'X-Frame-Options': 'sameorigin',
'X-XSS-Protection': '1',
};
var preamble = '<!doctype html><meta charset=utf-8>';
exports.noCacheHeaders = noCacheHeaders;
exports.notFoundHtml = preamble + '<title>404</title>404';
exports.serverErrorHtml = preamble + '<title>500</title>Server error';
hooks.hook('reloadResources', function (res, cb) {
exports.notFoundHtml = res.notFoundHtml;
exports.serverErrorHtml = res.serverErrorHtml;
cb(null);
});
function render_404(resp) {
resp.writeHead(404, noCacheHeaders);
resp.end(exports.notFoundHtml);
};
exports.render_404 = render_404;
function render_500(resp) {
resp.writeHead(500, noCacheHeaders);
resp.end(exports.serverErrorHtml);
}
exports.render_500 = render_500;
function slow_request(req, resp) {
var n = Math.floor(1000 + Math.random() * 500);
if (Math.random() < 0.1)
n *= 10;
setTimeout(function () {
if (resp.finished)
return;
if (resp.socket && resp.socket.destroyed)
return resp.end();
handle_request(req, new Debuff(resp));
}, n);
}
function timeout(resp) {
var n = Math.random();
n = Math.round(9000 + n*n*50000);
setTimeout(function () {
if (resp.socket && !resp.socket.destroyed)
resp.socket.destroy();
resp.end();
}, n);
}
function redirect(resp, uri, code) {
var headers = {Location: uri};
for (var k in noCacheHeaders)
headers[k] = noCacheHeaders[k];
resp.writeHead(code || 303, headers);
resp.end(preamble + '<title>Redirect</title>'
+ '<a href="' + encodeURI(uri) + '">Proceed</a>.');
}
exports.redirect = redirect;
var redirectJsTmpl = require('fs').readFileSync('tmpl/redirect.html');
function redirect_js(resp, uri) {
resp.writeHead(200, noCacheHeaders);
resp.write(preamble + '<title>Redirecting...</title>');
resp.write('<script>var dest = "' + encodeURI(uri) + '";</script>');
resp.end(redirectJsTmpl);
}
exports.redirect_js = redirect_js;
exports.dump_server_error = function (resp, err) {
resp.writeHead(500, noCacheHeaders);
resp.write(preamble + '<title>Server error</title>\n<pre>');
resp.write(escape(util.inspect(err)));
resp.end('</pre>');
};
function parse_cookie(header) {
var chunks = {};
(header || '').split(';').forEach(function (part) {
var bits = part.match(/^([^=]*)=(.*)$/);
if (bits)
try {
chunks[bits[1].trim()] = decodeURIComponent(
bits[2].trim());
}
catch (e) {}
});
return chunks;
}
exports.parse_cookie = parse_cookie;
exports.prefers_json = function (accept) {
/* Not technically correct but whatever */
var mimes = (accept || '').split(',');
for (var i = 0; i < mimes.length; i++) {
if (/json/i.test(mimes[i]))
return true;
else if (/(html|xml|plain|image)/i.test(mimes[i]))
return false;
}
return false;
};
function Debuff(stream) {
Stream.call(this);
this.out = stream;
this.buf = [];
this.timer = 0;
this.writable = true;
this.destroyed = false;
this.closing = false;
this._flush = this._flush.bind(this);
this.on_close = this.destroy.bind(this);
this.on_error = this.on_error.bind(this);
stream.once('close', this.on_close);
stream.on('error', this.on_error);
this.timeout = setTimeout(this.destroy.bind(this), 120*1000);
}
util.inherits(Debuff, Stream);
var D = Debuff.prototype;
D.writeHead = function () {
if (!this._check())
return false;
this.buf.push({_head: [].slice.call(arguments)});
this._queue();
return true;
};
D.write = function (data, encoding) {
if (!this._check())
return false;
if (encoding)
this.buf.push({_enc: encoding, _data: data});
else
this.buf.push(data);
this._queue();
return true;
};
D.end = function (data, encoding) {
if (!this._check())
return;
if (encoding)
this.buf.push({_enc: encoding, _data: data});
else if (data)
this.buf.push(data);
this._queue();
this.closing = true;
this.cleanEnd = true;
};
D._check = function () {
if (!this.writable)
return false;
if (!this.out.writable) {
this.destroy();
return false;
}
if (this.out.sock && this.out.sock.destroyed) {
this.destroy();
return false;
}
return true;
};
D._queue = function () {
if (this.timer)
return;
if (Math.random() < 0.05)
return;
var wait = 500 + Math.floor(Math.random() * 5000);
if (Math.random() < 0.5)
wait *= 2;
this.timer = setTimeout(this._flush, wait);
};
D._flush = function () {
var limit = 500 + Math.floor(Math.random() * 1000);
if (Math.random() < 0.05)
limit *= 3;
var count = 0;
while (this.out.writable && this.buf.length && count < limit) {
var o = this.buf.shift();
if (o._head) {
this.out.writeHead.apply(this.out, o._head);
this.statusCode = this.out.statusCode;
continue;
}
var enc;
if (o._enc && o._data) {
enc = o.enc;
o = o._data;
}
if (!o.length)
continue;
var n = limit - count;
if (typeof o == 'string' && o.length > n) {
this.buf.unshift(o.slice(n));
o = o.slice(0, n);
}
count += o.length;
if (!this.out.write(o, enc))
break;
}
this.timer = 0;
if (this.out.writable && this.buf.length)
this._queue();
else if (this.closing) {
if (this.cleanEnd) {
this.out.end();
this._clean_up();
this.emit('close');
}
else {
this.destroy();
}
}
else
this.emit('drain');
};
D.destroy = function () {
if (this.destroyed)
return;
this._clean_up();
this.cleanEnd = false;
this.emit('close');
};
D.destroySoon = function () {
if (!this.timer)
return this.destroy();
this.writable = false;
this.closing = true;
};
D.on_error = function (err) {
if (!this.destroyed)
this._clean_up();
this.cleanEnd = false;
this.emit('error', err);
};
D._clean_up = function () {
this.writable = false;
this.destroyed = true;
this.closing = false;
this.out.removeListener('close', this.on_close);
this.out.removeListener('error', this.on_error);
if (this.timer) {
clearTimeout(this.timer);
this.timer = 0;
}
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = 0;
}
if (!this.out.finished) {
this.out.destroy();
}
};
D.getHeader = function (name) { return this.out.getHeader(name); };
D.setHeader = function (k, v) { this.out.setHeader(k, v); };
D.removeHeader = function (name) { return this.out.removeHeader(name); };
D.addTrailers = function (headers) { this.out.addTrailers(headers); };