Merge branch '3.0' into uws

This commit is contained in:
Calvin Montgomery 2018-07-26 21:02:01 -07:00
commit 17cf6023db
7 changed files with 181 additions and 109 deletions

View file

@ -2,7 +2,7 @@
"author": "Calvin Montgomery",
"name": "CyTube",
"description": "Online media synchronizer and chat",
"version": "3.56.3",
"version": "3.56.5",
"repository": {
"url": "http://github.com/calzoneman/sync"
},

View file

@ -79,7 +79,9 @@ PollModule.prototype.onUserPostJoin = function (user) {
this.addUserToPollRoom(user);
const self = this;
user.on("effectiveRankChange", () => {
self.addUserToPollRoom(user);
if (self.channel && !self.channel.dead) {
self.addUserToPollRoom(user);
}
});
};

View file

@ -386,6 +386,12 @@ function preprocessConfig(cfg) {
return contact.name !== 'calzoneman';
});
if (!cfg.io.throttle) {
cfg.io.throttle = {
'in-rate-limit': Infinity
};
}
return cfg;
}

View file

@ -103,8 +103,14 @@ function translateStatusCode(statusCode) {
"the file to be downloaded.";
case 404:
return "The requested link could not be found (404).";
case 405:
return "The website hosting the link does not support HEAD requests, " +
"so the link could not be retrieved.";
case 410:
return "The requested link does not exist (410 Gone).";
case 501:
return "The requested link could not be retrieved because the server " +
"hosting it does not support CyTube's request.";
case 500:
case 503:
return "The website hosting the audio/video link encountered an error " +
@ -143,68 +149,76 @@ function testUrl(url, cb, params = { redirCount: 0, cookie: '' }) {
if (cookie) {
data.headers = { 'Cookie': cookie };
}
var req = transport.request(data, function (res) {
req.abort();
if (res.statusCode === 301 || res.statusCode === 302) {
if (redirCount > 2) {
return cb("The request for the audio/video file has been redirected " +
"more than twice. This could indicate a misconfiguration " +
"on the website hosting the link. For best results, use " +
"a direct link. See https://git.io/vrE75 for details.");
try {
var req = transport.request(data, function (res) {
req.abort();
if (res.statusCode === 301 || res.statusCode === 302) {
if (redirCount > 2) {
return cb("The request for the audio/video file has been redirected " +
"more than twice. This could indicate a misconfiguration " +
"on the website hosting the link. For best results, use " +
"a direct link. See https://git.io/vrE75 for details.");
}
const nextParams = {
redirCount: redirCount + 1,
cookie: cookie + getCookie(res)
};
return testUrl(fixRedirectIfNeeded(data, res.headers["location"]), cb,
nextParams);
}
const nextParams = {
redirCount: redirCount + 1,
cookie: cookie + getCookie(res)
};
return testUrl(fixRedirectIfNeeded(data, res.headers["location"]), cb,
nextParams);
}
if (res.statusCode !== 200) {
return cb(translateStatusCode(res.statusCode));
}
if (res.statusCode !== 200) {
return cb(translateStatusCode(res.statusCode));
}
if (!/^audio|^video/.test(res.headers["content-type"])) {
return cb("Expected a content-type starting with 'audio' or 'video', but " +
"got '" + res.headers["content-type"] + "'. Only direct links " +
"to video and audio files are accepted, and the website hosting " +
"the file must be configured to send the correct MIME type. " +
"See https://git.io/vrE75 for details.");
}
if (!/^audio|^video/.test(res.headers["content-type"])) {
return cb("Expected a content-type starting with 'audio' or 'video', but " +
"got '" + res.headers["content-type"] + "'. Only direct links " +
"to video and audio files are accepted, and the website hosting " +
"the file must be configured to send the correct MIME type. " +
"See https://git.io/vrE75 for details.");
}
cb();
});
cb();
});
req.on("error", function (err) {
if (/hostname\/ip doesn't match/i.test(err.message)) {
cb("The remote server provided an invalid SSL certificate. Details: "
+ err.reason);
return;
} else if (ECODE_MESSAGES.hasOwnProperty(err.code)) {
cb(`${ECODE_MESSAGES[err.code](err)} (error code: ${err.code})`);
return;
}
req.on("error", function (err) {
if (/hostname\/ip doesn't match/i.test(err.message)) {
cb("The remote server provided an invalid SSL certificate. Details: "
+ err.reason);
return;
} else if (ECODE_MESSAGES.hasOwnProperty(err.code)) {
cb(`${ECODE_MESSAGES[err.code](err)} (error code: ${err.code})`);
return;
}
// HPE_INVALID_CONSTANT comes from node's HTTP parser because
// facebook's CDN violates RFC 2616 by sending a body even though
// the request uses the HEAD method.
// Avoid logging this because it's a known issue.
if (!(err.code === 'HPE_INVALID_CONSTANT' && /fbcdn/.test(url))) {
LOGGER.error(
"Error sending preflight request: %s (code=%s) (link: %s)",
err.message,
err.code,
url
);
}
// HPE_INVALID_CONSTANT comes from node's HTTP parser because
// facebook's CDN violates RFC 2616 by sending a body even though
// the request uses the HEAD method.
// Avoid logging this because it's a known issue.
if (!(err.code === 'HPE_INVALID_CONSTANT' && /fbcdn/.test(url))) {
LOGGER.error(
"Error sending preflight request: %s (code=%s) (link: %s)",
err.message,
err.code,
url
);
}
cb("An unexpected error occurred while trying to process the link. " +
"Try again, and contact support for further troubleshooting if the " +
"problem continues." + (err.code ? (" Error code: " + err.code) : ""));
});
req.end();
} catch (error) {
LOGGER.error('Unable to make raw file probe request: %s', error.stack);
cb("An unexpected error occurred while trying to process the link. " +
"Try again, and contact support for further troubleshooting if the " +
"problem continues." + (err.code ? (" Error code: " + err.code) : ""));
});
req.end();
"problem continues.");
}
}
function readOldFormat(buf) {

View file

@ -233,6 +233,8 @@ class IOServer {
return;
}
this.setRateLimiter(socket);
emitMetrics(socket);
LOGGER.info('Accepted socket from %s', socket.context.ipAddress);
@ -250,6 +252,25 @@ class IOServer {
}
}
setRateLimiter(socket) {
const thunk = () => Config.get('io.throttle.in-rate-limit');
socket._inRateLimit = new TokenBucket(thunk, thunk);
socket.on('cytube:count-event', () => {
if (socket._inRateLimit.throttle()) {
LOGGER.warn(
'Kicking client %s: exceeded in-rate-limit of %d',
socket.context.ipAddress,
thunk()
);
socket.emit('kick', { reason: 'Rate limit exceeded' });
socket.disconnect();
}
});
}
initSocketIO() {
patchSocketMetrics();
patchTypecheckedFunctions();
@ -306,10 +327,12 @@ const outgoingPacketCount = new Counter({
function patchSocketMetrics() {
const onevent = Socket.prototype.onevent;
const packet = Socket.prototype.packet;
const emit = require('events').EventEmitter.prototype.emit;
Socket.prototype.onevent = function patchedOnevent() {
onevent.apply(this, arguments);
incomingEventCount.inc(1);
emit.call(this, 'cytube:count-event');
};
Socket.prototype.packet = function patchedPacket() {

View file

@ -1,16 +1,27 @@
class TokenBucket {
constructor(capacity, refillRate) {
if (typeof refillRate !== 'function') {
const _refillRate = refillRate;
refillRate = () => _refillRate;
}
if (typeof capacity !== 'function') {
const _capacity = capacity;
capacity = () => _capacity;
}
this.capacity = capacity;
this.refillRate = refillRate;
this.count = capacity;
this.count = capacity();
this.lastRefill = Date.now();
}
throttle() {
const now = Date.now();
const delta = Math.floor((now - this.lastRefill) / 1000 * this.refillRate);
const delta = Math.floor(
(now - this.lastRefill) / 1000 * this.refillRate()
);
if (delta > 0) {
this.count = Math.min(this.capacity, this.count + delta);
this.count = Math.min(this.capacity(), this.count + delta);
this.lastRefill = now;
}

View file

@ -13,6 +13,7 @@ var Config = require("../config");
var session = require("../session");
var csrf = require("./csrf");
const url = require("url");
import crypto from 'crypto';
const LOGGER = require('@calzoneman/jsli')('web/accounts');
@ -536,76 +537,91 @@ function handlePasswordReset(req, res) {
return;
}
if (actualEmail !== email.trim()) {
if (actualEmail === '') {
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: "",
resetErr: `Username ${name} cannot be recovered because it ` +
"doesn't have an email address associated with it."
});
return;
} else if (actualEmail.toLowerCase() !== email.trim().toLowerCase()) {
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: "",
resetErr: "Provided email does not match the email address on record for " + name
});
return;
} else if (actualEmail === "") {
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: "",
resetErr: name + " doesn't have an email address on record. Please contact an " +
"administrator to manually reset your password."
});
return;
}
var hash = $util.sha1($util.randomSalt(64));
// 24-hour expiration
var expire = Date.now() + 86400000;
var ip = req.realIP;
db.addPasswordReset({
ip: ip,
name: name,
email: email,
hash: hash,
expire: expire
}, function (err, _dbres) {
crypto.randomBytes(20, (err, bytes) => {
if (err) {
LOGGER.error(
'Could not generate random bytes for password reset: %s',
err.stack
);
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: "",
resetErr: err
resetEmail: email,
resetErr: "Internal error when generating password reset"
});
return;
}
Logger.eventlog.log("[account] " + ip + " requested password recovery for " +
name + " <" + email + ">");
var hash = bytes.toString('hex');
// 24-hour expiration
var expire = Date.now() + 86400000;
var ip = req.realIP;
if (!emailConfig.getPasswordReset().isEnabled()) {
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: email,
resetErr: "This server does not have mail support enabled. Please " +
"contact an administrator for assistance."
});
return;
}
db.addPasswordReset({
ip: ip,
name: name,
email: actualEmail,
hash: hash,
expire: expire
}, function (err, _dbres) {
if (err) {
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: "",
resetErr: err
});
return;
}
const baseUrl = `${req.realProtocol}://${req.header("host")}`;
Logger.eventlog.log("[account] " + ip + " requested password recovery for " +
name + " <" + email + ">");
emailController.sendPasswordReset({
username: name,
address: email,
url: `${baseUrl}/account/passwordrecover/${hash}`
}).then(_result => {
sendPug(res, "account-passwordreset", {
reset: true,
resetEmail: email,
resetErr: false
});
}).catch(error => {
LOGGER.error("Sending password reset email failed: %s", error);
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: email,
resetErr: "Sending reset email failed. Please contact an " +
"administrator for assistance."
if (!emailConfig.getPasswordReset().isEnabled()) {
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: email,
resetErr: "This server does not have mail support enabled. Please " +
"contact an administrator for assistance."
});
return;
}
const baseUrl = `${req.realProtocol}://${req.header("host")}`;
emailController.sendPasswordReset({
username: name,
address: email,
url: `${baseUrl}/account/passwordrecover/${hash}`
}).then(_result => {
sendPug(res, "account-passwordreset", {
reset: true,
resetEmail: email,
resetErr: false
});
}).catch(error => {
LOGGER.error("Sending password reset email failed: %s", error);
sendPug(res, "account-passwordreset", {
reset: false,
resetEmail: email,
resetErr: "Sending reset email failed. Please contact an " +
"administrator for assistance."
});
});
});
});