mirror of https://github.com/calzoneman/sync.git
494 lines
14 KiB
JavaScript
494 lines
14 KiB
JavaScript
var Server = require("./server");
|
|
var util = require("./utilities");
|
|
var db = require("./database");
|
|
var Config = require("./config");
|
|
var ACP = require("./acp");
|
|
var Account = require("./account");
|
|
var Flags = require("./flags");
|
|
import { EventEmitter } from 'events';
|
|
import Logger from './logger';
|
|
import net from 'net';
|
|
|
|
const LOGGER = require('@calzoneman/jsli')('user');
|
|
|
|
function User(socket, ip, loginInfo) {
|
|
this.flags = 0;
|
|
this.socket = socket;
|
|
// Expanding IPv6 addresses shouldn't really be necessary
|
|
// At some point, the IPv6 related stuff should be revisited
|
|
this.realip = net.isIPv6(ip) ? util.expandIPv6(ip) : ip;
|
|
this.displayip = util.cloakIP(this.realip);
|
|
this.channel = null;
|
|
this.queueLimiter = util.newRateLimiter();
|
|
this.chatLimiter = util.newRateLimiter();
|
|
this.reqPlaylistLimiter = util.newRateLimiter();
|
|
this.awaytimer = false;
|
|
|
|
if (loginInfo) {
|
|
this.account = new Account.Account(this.realip, loginInfo, socket.context.aliases);
|
|
this.registrationTime = new Date(this.account.user.time);
|
|
this.setFlag(Flags.U_REGISTERED | Flags.U_LOGGED_IN | Flags.U_READY);
|
|
socket.emit("login", {
|
|
success: true,
|
|
name: this.getName(),
|
|
guest: false
|
|
});
|
|
socket.emit("rank", this.account.effectiveRank);
|
|
if (this.account.globalRank >= 255) {
|
|
this.initAdminCallbacks();
|
|
}
|
|
this.emit("login", this.account);
|
|
LOGGER.info(ip + " logged in as " + this.getName());
|
|
} else {
|
|
this.account = new Account.Account(this.realip, null, socket.context.aliases);
|
|
socket.emit("rank", -1);
|
|
this.setFlag(Flags.U_READY);
|
|
this.once("login", account => {
|
|
if (account.globalRank >= 255) {
|
|
this.initAdminCallbacks();
|
|
}
|
|
});
|
|
}
|
|
|
|
socket.once("joinChannel", data => this.handleJoinChannel(data));
|
|
socket.once("initACP", () => this.handleInitACP());
|
|
socket.on("login", data => this.handleLogin(data));
|
|
}
|
|
|
|
User.prototype = Object.create(EventEmitter.prototype);
|
|
|
|
User.prototype.handleJoinChannel = function handleJoinChannel(data) {
|
|
if (typeof data !== "object" || typeof data.name !== "string") {
|
|
return;
|
|
}
|
|
|
|
if (this.inChannel()) {
|
|
return;
|
|
}
|
|
|
|
if (!util.isValidChannelName(data.name)) {
|
|
this.socket.emit("errorMsg", {
|
|
msg: "Invalid channel name. Channel names may consist of 1-30 " +
|
|
"characters in the set a-z, A-Z, 0-9, -, and _"
|
|
});
|
|
this.kick("Invalid channel name");
|
|
return;
|
|
}
|
|
|
|
data.name = data.name.toLowerCase();
|
|
if (data.name in Config.get("channel-blacklist")) {
|
|
this.kick("This channel is blacklisted.");
|
|
return;
|
|
}
|
|
|
|
this.waitFlag(Flags.U_READY, () => {
|
|
var chan;
|
|
try {
|
|
chan = Server.getServer().getChannel(data.name);
|
|
} catch (error) {
|
|
if (error.code === 'EWRONGPART') {
|
|
this.socket.emit("errorMsg", {
|
|
msg: "Channel '" + data.name + "' is hosted on another server. " +
|
|
"Try refreshing the page to update the connection URL."
|
|
});
|
|
} else {
|
|
LOGGER.error("Unexpected error from getChannel(): %s", error.stack);
|
|
this.socket.emit("errorMsg", {
|
|
msg: "Unable to join channel due to an internal error"
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!chan.is(Flags.C_READY)) {
|
|
chan.once("loadFail", reason => {
|
|
this.socket.emit("errorMsg", {
|
|
msg: reason,
|
|
alert: true
|
|
});
|
|
this.kick(`Channel could not be loaded: ${reason}`);
|
|
});
|
|
}
|
|
chan.joinUser(this, data);
|
|
});
|
|
};
|
|
|
|
User.prototype.handleInitACP = function handleInitACP() {
|
|
this.waitFlag(Flags.U_LOGGED_IN, () => {
|
|
if (this.account.globalRank >= 255) {
|
|
ACP.init(this);
|
|
} else {
|
|
this.kick("Attempted initACP from non privileged user. This incident " +
|
|
"will be reported.");
|
|
Logger.eventlog.log("[acp] Attempted initACP from socket client " +
|
|
this.getName() + "@" + this.realip);
|
|
}
|
|
});
|
|
};
|
|
|
|
User.prototype.handleLogin = function handleLogin(data) {
|
|
if (typeof data !== "object") {
|
|
this.socket.emit("errorMsg", {
|
|
msg: "Invalid login frame"
|
|
});
|
|
return;
|
|
}
|
|
|
|
var name = data.name;
|
|
if (typeof name !== "string") {
|
|
return;
|
|
}
|
|
|
|
var pw = data.pw || "";
|
|
if (typeof pw !== "string") {
|
|
pw = "";
|
|
}
|
|
|
|
if (this.is(Flags.U_LOGGING_IN) || this.is(Flags.U_LOGGED_IN)) {
|
|
return;
|
|
}
|
|
|
|
if (!pw) {
|
|
this.guestLogin(name);
|
|
} else {
|
|
this.login(name, pw);
|
|
}
|
|
};
|
|
|
|
User.prototype.die = function () {
|
|
for (const key in this.socket._events) {
|
|
delete this.socket._events[key];
|
|
}
|
|
|
|
delete this.socket.typecheckedOn;
|
|
delete this.socket.typecheckedOnce;
|
|
|
|
for (const key in this.__evHandlers) {
|
|
delete this.__evHandlers[key];
|
|
}
|
|
|
|
if (this.awaytimer) {
|
|
clearTimeout(this.awaytimer);
|
|
}
|
|
|
|
this.dead = true;
|
|
};
|
|
|
|
User.prototype.is = function (flag) {
|
|
return Boolean(this.flags & flag);
|
|
};
|
|
|
|
User.prototype.setFlag = function (flag) {
|
|
this.flags |= flag;
|
|
this.emit("setFlag", flag);
|
|
};
|
|
|
|
User.prototype.clearFlag = function (flag) {
|
|
this.flags &= ~flag;
|
|
this.emit("clearFlag", flag);
|
|
};
|
|
|
|
User.prototype.waitFlag = function (flag, cb) {
|
|
var self = this;
|
|
if (self.is(flag)) {
|
|
cb();
|
|
} else {
|
|
var wait = function (f) {
|
|
if (f === flag) {
|
|
self.removeListener("setFlag", wait);
|
|
cb();
|
|
}
|
|
};
|
|
self.on("setFlag", wait);
|
|
}
|
|
};
|
|
|
|
User.prototype.getName = function () {
|
|
return this.account.name;
|
|
};
|
|
|
|
User.prototype.getLowerName = function () {
|
|
return this.account.lowername;
|
|
};
|
|
|
|
User.prototype.inChannel = function () {
|
|
return this.channel != null && !this.channel.dead;
|
|
};
|
|
|
|
User.prototype.inRegisteredChannel = function () {
|
|
return this.inChannel() && this.channel.is(Flags.C_REGISTERED);
|
|
};
|
|
|
|
/* Called when a user's AFK status changes */
|
|
User.prototype.setAFK = function (afk) {
|
|
if (!this.inChannel()) {
|
|
return;
|
|
}
|
|
|
|
/* No change in AFK status, don't need to change anything */
|
|
if (this.is(Flags.U_AFK) === afk) {
|
|
this.autoAFK();
|
|
return;
|
|
}
|
|
|
|
if (afk) {
|
|
this.setFlag(Flags.U_AFK);
|
|
if (this.channel.modules.voteskip) {
|
|
this.channel.modules.voteskip.unvote(this.realip);
|
|
this.socket.emit("clearVoteskipVote");
|
|
}
|
|
} else {
|
|
this.clearFlag(Flags.U_AFK);
|
|
this.autoAFK();
|
|
}
|
|
|
|
if (!this.inChannel()) {
|
|
/*
|
|
* In unusual circumstances, the above emit("clearVoteskipVote")
|
|
* can cause the "disconnect" event to be fired synchronously,
|
|
* which results in this user no longer being in the channel.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
/* Number of AFK users changed, voteskip state changes */
|
|
if (this.channel.modules.voteskip) {
|
|
this.channel.modules.voteskip.update();
|
|
}
|
|
|
|
this.emit('afk', afk);
|
|
};
|
|
|
|
/* Automatically tag a user as AFK after a period of inactivity */
|
|
User.prototype.autoAFK = function () {
|
|
var self = this;
|
|
if (self.awaytimer) {
|
|
clearTimeout(self.awaytimer);
|
|
}
|
|
|
|
if (!self.inChannel() || !self.channel.modules.options) {
|
|
return;
|
|
}
|
|
|
|
/* Don't set a timer if the duration is invalid */
|
|
var timeout = parseFloat(self.channel.modules.options.get("afk_timeout"));
|
|
if (isNaN(timeout) || timeout <= 0) {
|
|
return;
|
|
}
|
|
|
|
self.awaytimer = setTimeout(function () {
|
|
self.setAFK(true);
|
|
}, timeout * 1000);
|
|
};
|
|
|
|
User.prototype.kick = function (reason) {
|
|
this.socket.emit("kick", { reason: reason });
|
|
this.socket.disconnect();
|
|
};
|
|
|
|
User.prototype.isAnonymous = function(){
|
|
var self = this;
|
|
return !self.is(Flags.U_LOGGED_IN);
|
|
};
|
|
|
|
User.prototype.initAdminCallbacks = function () {
|
|
var self = this;
|
|
self.socket.on("borrow-rank", function (rank) {
|
|
if (self.inChannel()) {
|
|
if (typeof rank !== "number") {
|
|
return;
|
|
}
|
|
|
|
if (rank > self.account.globalRank) {
|
|
return;
|
|
}
|
|
|
|
if (rank === 255 && self.account.globalRank > 255) {
|
|
rank = self.account.globalRank;
|
|
}
|
|
|
|
self.account.channelRank = rank;
|
|
self.account.effectiveRank = rank;
|
|
self.socket.emit("rank", rank);
|
|
self.channel.broadcastAll("setUserRank", {
|
|
name: self.getName(),
|
|
rank: rank
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
User.prototype.login = function (name, pw) {
|
|
var self = this;
|
|
self.setFlag(Flags.U_LOGGING_IN);
|
|
|
|
db.users.verifyLogin(name, pw, function (err, user) {
|
|
if (err) {
|
|
if (err === "Invalid username/password combination") {
|
|
Logger.eventlog.log("[loginfail] Login failed (bad password): " + name
|
|
+ "@" + self.realip);
|
|
}
|
|
|
|
self.socket.emit("login", {
|
|
success: false,
|
|
error: err
|
|
});
|
|
self.clearFlag(Flags.U_LOGGING_IN);
|
|
return;
|
|
}
|
|
|
|
const oldRank = self.account.effectiveRank;
|
|
self.account.user = user;
|
|
self.account.update();
|
|
self.socket.emit("rank", self.account.effectiveRank);
|
|
self.emit("effectiveRankChange", self.account.effectiveRank, oldRank);
|
|
self.registrationTime = new Date(user.time);
|
|
self.setFlag(Flags.U_REGISTERED);
|
|
self.socket.emit("login", {
|
|
success: true,
|
|
name: user.name
|
|
});
|
|
db.recordVisit(self.realip, self.getName());
|
|
LOGGER.info(self.realip + " logged in as " + user.name);
|
|
self.setFlag(Flags.U_LOGGED_IN);
|
|
self.clearFlag(Flags.U_LOGGING_IN);
|
|
self.emit("login", self.account);
|
|
});
|
|
};
|
|
|
|
var lastguestlogin = {};
|
|
User.prototype.guestLogin = function (name) {
|
|
var self = this;
|
|
|
|
if (self.realip in lastguestlogin) {
|
|
var diff = (Date.now() - lastguestlogin[self.realip]) / 1000;
|
|
if (diff < Config.get("guest-login-delay")) {
|
|
self.socket.emit("login", {
|
|
success: false,
|
|
error: "Guest logins are restricted to one per IP address per " +
|
|
Config.get("guest-login-delay") + " seconds."
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!util.isValidUserName(name)) {
|
|
self.socket.emit("login", {
|
|
success: false,
|
|
error: "Invalid username. Usernames must be 1-20 characters long and " +
|
|
"consist only of characters a-z, A-Z, 0-9, -, or _."
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (name.match(Config.get("reserved-names.usernames"))) {
|
|
LOGGER.warn(
|
|
'Rejecting attempt by %s to use reserved username "%s"',
|
|
self.realip,
|
|
name
|
|
);
|
|
self.socket.emit("login", {
|
|
success: false,
|
|
error: "That username is reserved."
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Prevent duplicate logins
|
|
self.setFlag(Flags.U_LOGGING_IN);
|
|
db.users.isUsernameTaken(name, function (err, taken) {
|
|
self.clearFlag(Flags.U_LOGGING_IN);
|
|
if (err) {
|
|
self.socket.emit("login", {
|
|
success: false,
|
|
error: err
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (taken) {
|
|
self.socket.emit("login", {
|
|
success: false,
|
|
error: "That username is registered."
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (self.inChannel()) {
|
|
var nameLower = name.toLowerCase();
|
|
for (var i = 0; i < self.channel.users.length; i++) {
|
|
if (self.channel.users[i].getLowerName() === nameLower) {
|
|
self.socket.emit("login", {
|
|
success: false,
|
|
error: "That name is already in use on this channel."
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Login succeeded
|
|
lastguestlogin[self.realip] = Date.now();
|
|
|
|
const oldRank = self.account.effectiveRank;
|
|
self.account.guestName = name;
|
|
self.account.update();
|
|
self.socket.emit("rank", self.account.effectiveRank);
|
|
self.emit("effectiveRankChange", self.account.effectiveRank, oldRank);
|
|
self.socket.emit("login", {
|
|
success: true,
|
|
name: name,
|
|
guest: true
|
|
});
|
|
db.recordVisit(self.realip, self.getName());
|
|
LOGGER.info(self.realip + " signed in as " + name);
|
|
self.setFlag(Flags.U_LOGGED_IN);
|
|
self.emit("login", self.account);
|
|
});
|
|
};
|
|
|
|
/* Clean out old login throttlers to save memory */
|
|
setInterval(function () {
|
|
var delay = Config.get("guest-login-delay");
|
|
for (var ip in lastguestlogin) {
|
|
var diff = (Date.now() - lastguestlogin[ip]) / 1000;
|
|
if (diff > delay) {
|
|
delete lastguestlogin[ip];
|
|
}
|
|
}
|
|
|
|
if (Config.get("aggressive-gc") && global && global.gc) {
|
|
global.gc();
|
|
}
|
|
}, 5 * 60 * 1000);
|
|
|
|
User.prototype.getFirstSeenTime = function getFirstSeenTime() {
|
|
if (this.registrationTime && this.socket.context.ipSessionFirstSeen) {
|
|
return Math.min(
|
|
this.registrationTime.getTime(),
|
|
this.socket.context.ipSessionFirstSeen.getTime()
|
|
);
|
|
} else if (this.registrationTime) {
|
|
return this.registrationTime.getTime();
|
|
} else if (this.socket.context.ipSessionFirstSeen) {
|
|
return this.socket.context.ipSessionFirstSeen.getTime();
|
|
} else {
|
|
LOGGER.error(`User "${this.getName()}" (IP: ${this.realip}) has neither ` +
|
|
"an IP session first seen time nor a registered account.");
|
|
return Date.now();
|
|
}
|
|
};
|
|
|
|
User.prototype.setChannelRank = function setRank(rank) {
|
|
const oldRank = this.account.effectiveRank;
|
|
const changed = oldRank !== rank;
|
|
this.account.channelRank = rank;
|
|
this.account.update();
|
|
this.socket.emit("rank", this.account.effectiveRank);
|
|
if (changed) {
|
|
this.emit("effectiveRankChange", this.account.effectiveRank, oldRank);
|
|
}
|
|
};
|
|
|
|
module.exports = User;
|