mirror of https://github.com/calzoneman/sync.git
Merge branch '3.0' into uws
This commit is contained in:
commit
17cf6023db
|
@ -2,7 +2,7 @@
|
||||||
"author": "Calvin Montgomery",
|
"author": "Calvin Montgomery",
|
||||||
"name": "CyTube",
|
"name": "CyTube",
|
||||||
"description": "Online media synchronizer and chat",
|
"description": "Online media synchronizer and chat",
|
||||||
"version": "3.56.3",
|
"version": "3.56.5",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "http://github.com/calzoneman/sync"
|
"url": "http://github.com/calzoneman/sync"
|
||||||
},
|
},
|
||||||
|
|
|
@ -79,7 +79,9 @@ PollModule.prototype.onUserPostJoin = function (user) {
|
||||||
this.addUserToPollRoom(user);
|
this.addUserToPollRoom(user);
|
||||||
const self = this;
|
const self = this;
|
||||||
user.on("effectiveRankChange", () => {
|
user.on("effectiveRankChange", () => {
|
||||||
self.addUserToPollRoom(user);
|
if (self.channel && !self.channel.dead) {
|
||||||
|
self.addUserToPollRoom(user);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -386,6 +386,12 @@ function preprocessConfig(cfg) {
|
||||||
return contact.name !== 'calzoneman';
|
return contact.name !== 'calzoneman';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!cfg.io.throttle) {
|
||||||
|
cfg.io.throttle = {
|
||||||
|
'in-rate-limit': Infinity
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
118
src/ffmpeg.js
118
src/ffmpeg.js
|
@ -103,8 +103,14 @@ function translateStatusCode(statusCode) {
|
||||||
"the file to be downloaded.";
|
"the file to be downloaded.";
|
||||||
case 404:
|
case 404:
|
||||||
return "The requested link could not be found (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:
|
case 410:
|
||||||
return "The requested link does not exist (410 Gone).";
|
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 500:
|
||||||
case 503:
|
case 503:
|
||||||
return "The website hosting the audio/video link encountered an error " +
|
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) {
|
if (cookie) {
|
||||||
data.headers = { 'Cookie': cookie };
|
data.headers = { 'Cookie': cookie };
|
||||||
}
|
}
|
||||||
var req = transport.request(data, function (res) {
|
|
||||||
req.abort();
|
|
||||||
|
|
||||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
try {
|
||||||
if (redirCount > 2) {
|
var req = transport.request(data, function (res) {
|
||||||
return cb("The request for the audio/video file has been redirected " +
|
req.abort();
|
||||||
"more than twice. This could indicate a misconfiguration " +
|
|
||||||
"on the website hosting the link. For best results, use " +
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||||
"a direct link. See https://git.io/vrE75 for details.");
|
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) {
|
if (res.statusCode !== 200) {
|
||||||
return cb(translateStatusCode(res.statusCode));
|
return cb(translateStatusCode(res.statusCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^audio|^video/.test(res.headers["content-type"])) {
|
if (!/^audio|^video/.test(res.headers["content-type"])) {
|
||||||
return cb("Expected a content-type starting with 'audio' or 'video', but " +
|
return cb("Expected a content-type starting with 'audio' or 'video', but " +
|
||||||
"got '" + res.headers["content-type"] + "'. Only direct links " +
|
"got '" + res.headers["content-type"] + "'. Only direct links " +
|
||||||
"to video and audio files are accepted, and the website hosting " +
|
"to video and audio files are accepted, and the website hosting " +
|
||||||
"the file must be configured to send the correct MIME type. " +
|
"the file must be configured to send the correct MIME type. " +
|
||||||
"See https://git.io/vrE75 for details.");
|
"See https://git.io/vrE75 for details.");
|
||||||
}
|
}
|
||||||
|
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on("error", function (err) {
|
req.on("error", function (err) {
|
||||||
if (/hostname\/ip doesn't match/i.test(err.message)) {
|
if (/hostname\/ip doesn't match/i.test(err.message)) {
|
||||||
cb("The remote server provided an invalid SSL certificate. Details: "
|
cb("The remote server provided an invalid SSL certificate. Details: "
|
||||||
+ err.reason);
|
+ err.reason);
|
||||||
return;
|
return;
|
||||||
} else if (ECODE_MESSAGES.hasOwnProperty(err.code)) {
|
} else if (ECODE_MESSAGES.hasOwnProperty(err.code)) {
|
||||||
cb(`${ECODE_MESSAGES[err.code](err)} (error code: ${err.code})`);
|
cb(`${ECODE_MESSAGES[err.code](err)} (error code: ${err.code})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HPE_INVALID_CONSTANT comes from node's HTTP parser because
|
// HPE_INVALID_CONSTANT comes from node's HTTP parser because
|
||||||
// facebook's CDN violates RFC 2616 by sending a body even though
|
// facebook's CDN violates RFC 2616 by sending a body even though
|
||||||
// the request uses the HEAD method.
|
// the request uses the HEAD method.
|
||||||
// Avoid logging this because it's a known issue.
|
// Avoid logging this because it's a known issue.
|
||||||
if (!(err.code === 'HPE_INVALID_CONSTANT' && /fbcdn/.test(url))) {
|
if (!(err.code === 'HPE_INVALID_CONSTANT' && /fbcdn/.test(url))) {
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
"Error sending preflight request: %s (code=%s) (link: %s)",
|
"Error sending preflight request: %s (code=%s) (link: %s)",
|
||||||
err.message,
|
err.message,
|
||||||
err.code,
|
err.code,
|
||||||
url
|
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. " +
|
cb("An unexpected error occurred while trying to process the link. " +
|
||||||
"Try again, and contact support for further troubleshooting if the " +
|
"Try again, and contact support for further troubleshooting if the " +
|
||||||
"problem continues." + (err.code ? (" Error code: " + err.code) : ""));
|
"problem continues.");
|
||||||
});
|
}
|
||||||
|
|
||||||
req.end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readOldFormat(buf) {
|
function readOldFormat(buf) {
|
||||||
|
|
|
@ -233,6 +233,8 @@ class IOServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setRateLimiter(socket);
|
||||||
|
|
||||||
emitMetrics(socket);
|
emitMetrics(socket);
|
||||||
|
|
||||||
LOGGER.info('Accepted socket from %s', socket.context.ipAddress);
|
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() {
|
initSocketIO() {
|
||||||
patchSocketMetrics();
|
patchSocketMetrics();
|
||||||
patchTypecheckedFunctions();
|
patchTypecheckedFunctions();
|
||||||
|
@ -306,10 +327,12 @@ const outgoingPacketCount = new Counter({
|
||||||
function patchSocketMetrics() {
|
function patchSocketMetrics() {
|
||||||
const onevent = Socket.prototype.onevent;
|
const onevent = Socket.prototype.onevent;
|
||||||
const packet = Socket.prototype.packet;
|
const packet = Socket.prototype.packet;
|
||||||
|
const emit = require('events').EventEmitter.prototype.emit;
|
||||||
|
|
||||||
Socket.prototype.onevent = function patchedOnevent() {
|
Socket.prototype.onevent = function patchedOnevent() {
|
||||||
onevent.apply(this, arguments);
|
onevent.apply(this, arguments);
|
||||||
incomingEventCount.inc(1);
|
incomingEventCount.inc(1);
|
||||||
|
emit.call(this, 'cytube:count-event');
|
||||||
};
|
};
|
||||||
|
|
||||||
Socket.prototype.packet = function patchedPacket() {
|
Socket.prototype.packet = function patchedPacket() {
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
class TokenBucket {
|
class TokenBucket {
|
||||||
constructor(capacity, refillRate) {
|
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.capacity = capacity;
|
||||||
this.refillRate = refillRate;
|
this.refillRate = refillRate;
|
||||||
this.count = capacity;
|
this.count = capacity();
|
||||||
this.lastRefill = Date.now();
|
this.lastRefill = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
throttle() {
|
throttle() {
|
||||||
const now = Date.now();
|
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) {
|
if (delta > 0) {
|
||||||
this.count = Math.min(this.capacity, this.count + delta);
|
this.count = Math.min(this.capacity(), this.count + delta);
|
||||||
this.lastRefill = now;
|
this.lastRefill = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ var Config = require("../config");
|
||||||
var session = require("../session");
|
var session = require("../session");
|
||||||
var csrf = require("./csrf");
|
var csrf = require("./csrf");
|
||||||
const url = require("url");
|
const url = require("url");
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
const LOGGER = require('@calzoneman/jsli')('web/accounts');
|
const LOGGER = require('@calzoneman/jsli')('web/accounts');
|
||||||
|
|
||||||
|
@ -536,76 +537,91 @@ function handlePasswordReset(req, res) {
|
||||||
return;
|
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", {
|
sendPug(res, "account-passwordreset", {
|
||||||
reset: false,
|
reset: false,
|
||||||
resetEmail: "",
|
resetEmail: "",
|
||||||
resetErr: "Provided email does not match the email address on record for " + name
|
resetErr: "Provided email does not match the email address on record for " + name
|
||||||
});
|
});
|
||||||
return;
|
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));
|
crypto.randomBytes(20, (err, bytes) => {
|
||||||
// 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) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
|
LOGGER.error(
|
||||||
|
'Could not generate random bytes for password reset: %s',
|
||||||
|
err.stack
|
||||||
|
);
|
||||||
sendPug(res, "account-passwordreset", {
|
sendPug(res, "account-passwordreset", {
|
||||||
reset: false,
|
reset: false,
|
||||||
resetEmail: "",
|
resetEmail: email,
|
||||||
resetErr: err
|
resetErr: "Internal error when generating password reset"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.eventlog.log("[account] " + ip + " requested password recovery for " +
|
var hash = bytes.toString('hex');
|
||||||
name + " <" + email + ">");
|
// 24-hour expiration
|
||||||
|
var expire = Date.now() + 86400000;
|
||||||
|
var ip = req.realIP;
|
||||||
|
|
||||||
if (!emailConfig.getPasswordReset().isEnabled()) {
|
db.addPasswordReset({
|
||||||
sendPug(res, "account-passwordreset", {
|
ip: ip,
|
||||||
reset: false,
|
name: name,
|
||||||
resetEmail: email,
|
email: actualEmail,
|
||||||
resetErr: "This server does not have mail support enabled. Please " +
|
hash: hash,
|
||||||
"contact an administrator for assistance."
|
expire: expire
|
||||||
});
|
}, function (err, _dbres) {
|
||||||
return;
|
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({
|
if (!emailConfig.getPasswordReset().isEnabled()) {
|
||||||
username: name,
|
sendPug(res, "account-passwordreset", {
|
||||||
address: email,
|
reset: false,
|
||||||
url: `${baseUrl}/account/passwordrecover/${hash}`
|
resetEmail: email,
|
||||||
}).then(_result => {
|
resetErr: "This server does not have mail support enabled. Please " +
|
||||||
sendPug(res, "account-passwordreset", {
|
"contact an administrator for assistance."
|
||||||
reset: true,
|
});
|
||||||
resetEmail: email,
|
return;
|
||||||
resetErr: false
|
}
|
||||||
});
|
|
||||||
}).catch(error => {
|
const baseUrl = `${req.realProtocol}://${req.header("host")}`;
|
||||||
LOGGER.error("Sending password reset email failed: %s", error);
|
|
||||||
sendPug(res, "account-passwordreset", {
|
emailController.sendPasswordReset({
|
||||||
reset: false,
|
username: name,
|
||||||
resetEmail: email,
|
address: email,
|
||||||
resetErr: "Sending reset email failed. Please contact an " +
|
url: `${baseUrl}/account/passwordrecover/${hash}`
|
||||||
"administrator for assistance."
|
}).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."
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue