1
0
mirror of https://github.com/hack-chat/main.git synced 2024-03-22 13:20:33 +08:00

Removed legacylayer, baked into modules instead

Originally I wanted the legacy layer to be a seperate module, allowing a server owner to remove legacy support by removing the module (for preformance). However, I didn't like the 39 thousand hooks that would be required and what this would do for latency. I dont like this alternative either though. /shrug
Added channel helper functions.
Added constants for warnings and errors. Started updating warning to have an id for i18l.
Added legacy helper functions
Moved the UAC module into the utility directory and renamed to _UAC.
This commit is contained in:
marzavec 2020-09-22 00:34:30 -05:00
parent 72324050f5
commit 5eb695bc53
23 changed files with 624 additions and 357 deletions

View File

@ -14,7 +14,7 @@ export async function run({ core, server, socket }) {
// attempt save, notify of failure
if (!core.configManager.save()) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Failed to save config, check logs.',
}, socket);
}

View File

@ -12,7 +12,7 @@ export async function run({
}) {
if (server.police.frisk(socket.address, 6)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are changing nicknames too fast. Wait a moment before trying again.',
}, socket);
}
@ -28,7 +28,7 @@ export async function run({
const newNick = payload.nick.trim();
if (!UAC.verifyNickname(newNick)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
}, socket);
}
@ -39,14 +39,14 @@ export async function run({
server.police.frisk(socket.address, 4);
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are not the admin, liar!',
}, socket);
}
if (newNick == previousNick) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You already have that name',
}, socket);
}
@ -63,7 +63,7 @@ export async function run({
if (userExists.length > 0) {
// That nickname is already in that channel
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Nickname taken',
}, socket);
}
@ -118,7 +118,7 @@ export function nickCheck({
// If there is no nickname target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help nick` for instructions on how to use this command.',
}, socket);

View File

@ -37,7 +37,7 @@ export async function run({
const score = text.length / 83 / 4;
if (server.police.frisk(socket.address, score)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
}, socket);
}
@ -46,6 +46,7 @@ export async function run({
const outgoingPayload = {
cmd: 'chat',
nick: socket.nick, /* @legacy */
uType: socket.uType, /* @legacy */
userid: socket.userid,
channel: socket.channel,
text,
@ -111,7 +112,7 @@ export function finalCmdCheck({ server, socket, payload }) {
}
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: `Unknown command: ${payload.text}`,
}, socket);

View File

@ -33,7 +33,7 @@ export async function run({ server, socket, payload }) {
const score = text.length / 83 / 4;
if (server.police.frisk(socket.address, score)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
}, socket);
}
@ -78,7 +78,7 @@ export function emoteCheck({
// If there is no emote target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help emote` for instructions on how to use this command.',
}, socket);

View File

@ -9,7 +9,7 @@ export async function run({
// check for spam
if (server.police.frisk(socket.address, 2)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
}, socket);
}

View File

@ -2,6 +2,17 @@
Description: Generates a semi-unique channel name then broadcasts it to each client
*/
import {
findUser,
} from '../utility/_Channels';
import {
Errors,
} from '../utility/_Constants';
import {
legacyInviteOut,
legacyInviteReply,
} from '../utility/_LegacyFunctions';
// module support functions
/**
* Returns the channel that should be invited to.
@ -22,34 +33,35 @@ export async function run({
// check for spam
if (server.police.frisk(socket.address, 2)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn',
text: 'You are sending invites too fast. Wait a moment before trying again.',
id: Errors.Invite.RATELIMIT,
}, socket);
}
// verify user input
if (typeof payload.userid !== 'number' || typeof payload.channel !== 'string') {
// if this is a legacy client add missing params to payload
if (socket.hcProtocol === 1) {
if (typeof socket.channel === 'undefined' || typeof payload.nick !== 'string') {
return true;
}
// why would you invite yourself?
if (payload.userid === socket.userid) {
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
} else if (typeof payload.userid !== 'number' || typeof payload.channel !== 'string') {
return true;
}
// @todo Verify this socket is part of payload.channel - multichannel patch
// find target user
let targetClient = server.findSockets({ channel: payload.channel, userid: payload.userid });
if (targetClient.length === 0) {
const targetUser = findUser(server, payload);
if (!targetUser) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn',
text: 'Could not find user in that channel',
id: Errors.Global.UNKNOWN_USER,
}, socket);
}
[targetClient] = targetClient;
// generate common channel
const channel = getChannel(payload.to);
@ -58,15 +70,23 @@ export async function run({
cmd: 'invite',
channel: socket.channel,
from: socket.userid,
to: payload.userid,
to: targetUser.userid,
inviteChannel: channel,
};
// send invite notice to target client
server.reply(outgoingPayload, targetClient);
if (targetUser.hcProtocol === 1) {
server.reply(legacyInviteOut(outgoingPayload, socket.nick), targetUser);
} else {
server.reply(outgoingPayload, targetUser);
}
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyInviteReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
// stats are fun
core.stats.increment('invites-sent');

View File

@ -1,139 +1,118 @@
/* eslint no-param-reassign: 0 */
/*
Description: Initial entry point, applies `channel` and `nick` to the calling socket
Description: Adds requested channel into the calling clients "subscribed channels"
*/
import * as UAC from '../utility/UAC/_info';
// module support functions
const crypto = require('crypto');
const hash = (password) => {
const sha = crypto.createHash('sha256');
sha.update(password);
return sha.digest('base64').substr(0, 6);
};
// exposed "login" function to allow hooks to verify user join events
// returns object containing user info or string if error
export function parseNickname(core, data) {
const userInfo = {
nick: data.nick,
uType: 'user', /* @legacy */
trip: null,
level: UAC.levels.default,
};
if (!UAC.verifyNickname(userInfo.nick)) {
// return error as string
// @todo Remove english and change to numeric id
return 'Nickname must consist of up to 24 letters, numbers, and underscores';
}
const password = data.pass || false;
if (hash(password + core.config.tripSalt) === core.config.adminTrip) {
userInfo.uType = 'admin'; /* @legacy */
userInfo.trip = 'Admin';
userInfo.level = UAC.levels.admin;
} else if (userInfo.nick.toLowerCase() === core.config.adminName.toLowerCase()) {
// they've got the main-admin name while not being an admin
// @todo Remove english and change to numeric id
return 'You are not the admin, liar!';
} else if (password) {
userInfo.trip = hash(password + core.config.tripSalt);
}
// @todo disallow moderator impersonation
// for (const mod of core.config.mods) {
core.config.mods.forEach((mod) => {
if (userInfo.trip === mod.trip) {
userInfo.uType = 'mod'; /* @legacy */
userInfo.level = UAC.levels.moderator;
}
});
return userInfo;
}
// import * as UAC from '../utility/UAC/_info';
import {
canJoinChannel,
} from '../utility/_Channels';
import {
Errors,
} from '../utility/_Constants';
import {
upgradeLegacyJoin,
legacyLevelToLabel,
} from '../utility/_LegacyFunctions';
import {
verifyNickname,
getUserPerms,
} from '../utility/_UAC';
// module main
export async function run({
core, server, socket, payload,
}) {
// check for spam
}) { // check for spam
if (server.police.frisk(socket.address, 3)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn',
text: 'You are joining channels too fast. Wait a moment and try again.',
id: Errors.Join.RATELIMIT,
}, socket);
}
// `join` is the legacy entry point, check if it needs to be upgraded
if (typeof socket.hcProtocol === 'undefined') {
payload = upgradeLegacyJoin(server, socket, payload);
}
// store payload values
const { channel, nick, pass } = payload;
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
}, socket);
}
// calling socket already in a channel
// @todo Multichannel update
// @todo multichannel update, will remove
if (typeof socket.channel !== 'undefined') {
return server.reply({
cmd: 'warn', // @todo Remove this
text: 'Joining more than one channel is not currently supported',
id: Errors.Join.ALREADY_JOINED,
}, socket);
}
// end todo
// check user input
if (typeof payload.channel !== 'string' || typeof payload.nick !== 'string') {
return true;
}
const channel = payload.channel.trim();
if (!channel) {
// must join a non-blank channel
return true;
}
const userInfo = this.parseNickname(core, payload);
if (typeof userInfo === 'string') {
// validates the user input for `nick`
const validName = verifyNickname(nick, socket);
if (validName !== true) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
text: userInfo,
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
id: Errors.Join.INVALID_NICK,
}, socket);
}
// get trip and level
const { trip, level } = getUserPerms(pass, core.config, channel);
// store the user values
const userInfo = {
nick,
trip,
uType: legacyLevelToLabel(level),
hash: socket.hash,
level,
userid: socket.userid,
channel,
};
// prevent admin impersonation
if (nick.toLowerCase() === core.config.adminName.toLowerCase()) {
if (userInfo.trip !== 'Admin') {
userInfo.nick = `Fake${userInfo.nick}`;
}
}
// check if the nickname already exists in the channel
const userExists = server.findSockets({
channel: payload.channel,
channel,
nick: (targetNick) => targetNick.toLowerCase() === userInfo.nick.toLowerCase(),
});
if (userExists.length > 0) {
// that nickname is already in that channel
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn',
text: 'Nickname taken',
id: Errors.Join.NAME_TAKEN,
}, socket);
}
// populate final userinfo fields
// @todo this could be move into parseNickname, changing the function name to match
userInfo.hash = server.getSocketHash(socket);
userInfo.userid = socket.userid;
// @todo place this within it's own function allowing import
// prepare to notify channel peers
const newPeerList = server.findSockets({ channel: payload.channel });
const newPeerList = server.findSockets({ channel });
const nicks = []; /* @legacy */
const users = [];
const joinAnnouncement = { ...{ cmd: 'onlineAdd' }, ...userInfo };
const joinAnnouncement = {
cmd: 'onlineAdd',
nick: userInfo.nick,
trip: userInfo.trip || 'null',
utype: userInfo.uType, /* @legacy */
hash: userInfo.hash,
level: userInfo.level,
userid: userInfo.userid,
channel: payload.channel,
};
// send join announcement and prep online set
// send join announcement and prep online set reply
for (let i = 0, l = newPeerList.length; i < l; i += 1) {
server.reply(joinAnnouncement, newPeerList[i]);
nicks.push(newPeerList[i].nick); /* @legacy */
@ -141,34 +120,26 @@ export async function run({
users.push({
nick: newPeerList[i].nick,
trip: newPeerList[i].trip,
utype: newPeerList[i].uType, /* @legacy */
uType: newPeerList[i].uType, /* @legacy */
hash: newPeerList[i].hash,
level: newPeerList[i].level,
userid: newPeerList[i].userid,
channel: payload.channel,
channel,
isme: false,
});
}
// store user info
socket.uType = userInfo.uType; /* @legacy */
socket.nick = userInfo.nick;
socket.trip = userInfo.trip;
socket.channel = payload.channel; /* @legacy */
socket.hash = userInfo.hash;
socket.level = userInfo.level;
socket.uType = userInfo.uType; /* @legacy */
socket.channel = channel; /* @legacy */
// @todo multi-channel patch
// socket.channels.push(channel);
nicks.push(socket.nick); /* @legacy */
users.push({
nick: socket.nick,
trip: socket.trip,
utype: socket.uType,
hash: socket.hash,
level: socket.level,
userid: socket.userid,
channel: payload.channel,
isme: true,
});
nicks.push(userInfo.nick); /* @legacy */
users.push({ ...{ isme: true }, ...userInfo });
// reply with channel peer list
server.reply({
@ -186,7 +157,7 @@ export async function run({
export const requiredData = []; // ['channel', 'nick'];
export const info = {
name: 'join',
description: 'Place calling socket into target channel with target nick & broadcast event to channel',
description: 'Join the target channel using the supplied nick and password',
usage: `
API: { cmd: 'join', nick: '<your nickname>', pass: '<optional password>', channel: '<target channel>' }`,
};

View File

@ -8,7 +8,7 @@ export async function run({ server, socket, payload }) {
// check for spam
if (server.police.frisk(socket.address, 6)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are changing channels too fast. Wait a moment before trying again.',
}, socket);
}
@ -20,7 +20,7 @@ export async function run({ server, socket, payload }) {
if (payload.channel === '') {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Cannot move to an empty channel.',
}, socket);
}
@ -109,7 +109,7 @@ export function moveCheck({
// If there is no channel target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help move` for instructions on how to use this command.',
}, socket);

View File

@ -53,6 +53,7 @@ export async function run({ server, socket }) {
socket.sessionID = createSessionID();
socket.hcProtocol = 2;
socket.userid = Math.floor(Math.random() * 9999999999999);
socket.hash = server.getSocketHash(socket);
// dispatch info
server.reply({

View File

@ -38,7 +38,7 @@ export async function run({ server, socket, payload }) {
const score = text.length / 83 / 4;
if (server.police.frisk(socket.address, score)) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
}, socket);
}
@ -53,7 +53,7 @@ export async function run({ server, socket, payload }) {
if (targetClient.length === 0) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Could not find user in channel',
}, socket);
}
@ -98,7 +98,7 @@ export function whisperCheck({
// If there is no nickname target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help whisper` for instructions on how to use this command.',
}, socket);
@ -126,7 +126,7 @@ export function whisperCheck({
if (payload.text.startsWith('/r ')) {
if (typeof socket.whisperReply === 'undefined') {
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Cannot reply to nobody',
}, socket);

View File

@ -1,166 +0,0 @@
/* eslint no-param-reassign: 0 */
/*
Description: This module adjusts outgoing data, making it compatible with legacy clients
Dear god this module is horrifying
*/
// import * as UAC from '../utility/UAC/_info';
// module main
export async function run({ server, socket }) {
return server.police.frisk(socket.address, 20);
}
// module hook functions
export function initHooks(server) {
server.registerHook('in', 'join', this.joinCheck.bind(this), 10);
server.registerHook('in', 'invite', this.inviteInCheck.bind(this), 10);
server.registerHook('out', 'invite', this.inviteOutCheck.bind(this), 10);
server.registerHook('in', 'ban', this.banCheck.bind(this), 10);
server.registerHook('in', 'dumb', this.dumbCheck.bind(this), 10);
server.registerHook('in', 'kick', this.kickCheck.bind(this), 10);
}
// hook incoming join events, if session was not invoked, default proto to 1
export function joinCheck({ socket, payload }) {
if (typeof socket.hcProtocol === 'undefined') {
socket.hcProtocol = 1;
const nickArray = payload.nick.split('#', 2);
payload.nick = nickArray[0].trim();
if (nickArray[1] && typeof payload.pass === 'undefined') {
payload.pass = nickArray[1]; // eslint-disable-line prefer-destructuring
}
// dunno how this happened on the legacy version
if (typeof payload.password !== 'undefined') {
payload.pass = payload.password;
}
if (typeof socket.userid === 'undefined') {
socket.userid = Math.floor(Math.random() * 9999999999999);
}
}
return payload;
}
// if legacy client sent an invite, downgrade request
export function inviteInCheck({ server, socket, payload }) {
if (socket.hcProtocol === 1) {
let targetClient = server.findSockets({ channel: socket.channel, nick: payload.nick });
if (targetClient.length === 0) {
server.reply({
cmd: 'warn',
text: 'Could not find user in that channel',
}, socket);
return false;
}
[targetClient] = targetClient;
payload.userid = targetClient.userid;
payload.channel = socket.channel;
}
return payload;
}
//
export function inviteOutCheck({ server, socket, payload }) {
if (socket.hcProtocol === 1) {
payload.cmd = 'info';
if (socket.userid === payload.from) {
let toClient = server.findSockets({ channel: socket.channel, userid: payload.from });
[toClient] = toClient;
payload.type = 'invite';
payload.from = toClient.nick;
payload.text = `You invited ${toClient.nick} to ?${payload.inviteChannel}`;
} else if (socket.userid === payload.to) {
let fromClient = server.findSockets({ channel: socket.channel, userid: payload.from });
[fromClient] = fromClient;
payload.type = 'invite';
payload.from = fromClient.nick;
payload.text = `${fromClient.nick} invited you to ?${payload.inviteChannel}`;
}
}
return payload;
}
export function banCheck({ server, socket, payload }) {
if (socket.hcProtocol === 1) {
let targetClient = server.findSockets({ channel: socket.channel, nick: payload.nick });
if (targetClient.length === 0) {
server.reply({
cmd: 'warn',
text: 'Could not find user in that channel',
}, socket);
return false;
}
[targetClient] = targetClient;
payload.userid = targetClient.userid;
payload.channel = socket.channel;
}
return payload;
}
export function dumbCheck({ server, socket, payload }) {
if (socket.hcProtocol === 1) {
let targetClient = server.findSockets({ channel: socket.channel, nick: payload.nick });
if (targetClient.length === 0) {
server.reply({
cmd: 'warn',
text: 'Could not find user in that channel',
}, socket);
return false;
}
[targetClient] = targetClient;
payload.userid = targetClient.userid;
payload.channel = socket.channel;
}
return payload;
}
export function kickCheck({ server, socket, payload }) {
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'number') {
if (typeof payload.nick !== 'object' && !Array.isArray(payload.nick)) {
return true;
}
}
const targetClient = server.findSockets({ channel: socket.channel, nick: payload.nick });
if (targetClient.length === 0) {
return false;
}
payload.userid = [];
for (let i = 0, j = targetClient.length; i < j; i += 1) {
payload.userid.push(targetClient[i].userid);
}
payload.channel = socket.channel;
}
return payload;
}
export const info = {
name: 'legacylayer',
description: 'This module adjusts outgoing data, making it compatible with legacy clients',
};

View File

@ -11,7 +11,7 @@ export async function run({ server, socket, payload }) {
// send warning to target socket
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: payload.text,
}, socket);

View File

@ -4,6 +4,12 @@
*/
import * as UAC from '../utility/UAC/_info';
import {
Errors,
} from '../utility/_Constants';
import {
findUser,
} from '../utility/_Channels';
// module main
export async function run({
@ -15,33 +21,38 @@ export async function run({
}
// check user input
if (typeof payload.userid !== 'number') {
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'string') {
return true;
}
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
} else if (typeof payload.userid !== 'number') {
return true;
}
// find target user
let badClient = server.findSockets({ channel: socket.channel, userid: payload.userid });
if (badClient.length === 0) {
const targetUser = findUser(server, payload);
if (!targetUser) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
text: 'Could not find user in channel',
cmd: 'warn',
text: 'Could not find user in that channel',
id: Errors.Global.UNKNOWN_USER,
}, socket);
}
[badClient] = badClient;
const targetNick = badClient.nick;
const targetNick = targetUser.nick;
// i guess banning mods or admins isn't the best idea?
if (badClient.level >= socket.level) {
if (targetUser.level >= socket.level) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn',
text: 'Cannot ban other users of the same level, how rude',
id: Errors.Global.PERMISSION,
}, socket);
}
// commit arrest record
server.police.arrest(badClient.address, badClient.hash);
server.police.arrest(targetUser.address, targetUser.hash);
console.log(`${socket.nick} [${socket.trip}] banned ${targetNick} in ${socket.channel}`);
@ -49,20 +60,20 @@ export async function run({
server.broadcast({
cmd: 'info',
text: `Banned ${targetNick}`,
user: UAC.getUserDetails(badClient),
user: UAC.getUserDetails(targetUser),
}, { channel: socket.channel, level: (level) => level < UAC.levels.moderator });
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} banned ${targetNick} in ${payload.channel}, userhash: ${badClient.hash}`,
text: `${socket.nick}#${socket.trip} banned ${targetNick} in ${payload.channel}, userhash: ${targetUser.hash}`,
channel: payload.channel,
user: UAC.getUserDetails(badClient),
user: UAC.getUserDetails(targetUser),
banner: UAC.getUserDetails(socket),
}, { level: UAC.isModerator });
// force connection closed
badClient.terminate();
targetUser.terminate();
// stats are fun
core.stats.increment('users-banned');

View File

@ -7,6 +7,12 @@
*/
import * as UAC from '../utility/UAC/_info';
import {
Errors,
} from '../utility/_Constants';
import {
findUser,
} from '../utility/_Channels';
// module constructor
export function init(core) {
@ -25,32 +31,37 @@ export async function run({
}
// check user input
if (typeof payload.userid !== 'number') {
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'string') {
return true;
}
payload.channel = socket.channel;
} else if (typeof payload.userid !== 'number') {
return true;
}
// find target user
let badClient = server.findSockets({ channel: payload.channel, userid: payload.userid });
if (badClient.length === 0) {
const targetUser = findUser(server, payload);
if (!targetUser) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
text: 'Could not find user in channel',
cmd: 'warn',
text: 'Could not find user in that channel',
id: Errors.Global.UNKNOWN_USER,
}, socket);
}
[badClient] = badClient;
// likely dont need this, muting mods and admins is fine
if (badClient.level >= socket.level) {
if (targetUser.level >= socket.level) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn',
text: 'This trick wont work on users of the same level',
id: Errors.Global.PERMISSION,
}, socket);
}
// store hash in mute list
const record = core.muzzledHashes[badClient.hash] = {
const record = core.muzzledHashes[targetUser.hash] = {
dumb: true,
};
@ -62,7 +73,7 @@ export async function run({
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} muzzled ${badClient.nick} in ${payload.channel}, userhash: ${badClient.hash}`,
text: `${socket.nick}#${socket.trip} muzzled ${targetUser.nick} in ${payload.channel}, userhash: ${targetUser.hash}`,
}, { level: UAC.isModerator });
return true;
@ -129,7 +140,7 @@ export function inviteCheck({ core, socket, payload }) {
/* const nickValid = Invite.checkNickname(payload.nick);
if (nickValid !== null) {
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: nickValid,
}, socket);
return false;

View File

@ -5,6 +5,12 @@
*/
import * as UAC from '../utility/UAC/_info';
import {
Errors,
} from '../utility/_Constants';
import {
findUsers,
} from '../utility/_Channels';
// module main
export async function run({
@ -16,27 +22,28 @@ export async function run({
}
// check user input
if (typeof payload.userid !== 'number') {
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'string') {
if (typeof payload.nick !== 'object' && !Array.isArray(payload.nick)) {
return true;
}
}
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
} else if (typeof payload.userid !== 'number') {
// @todo create multi-ban ui
if (typeof payload.userid !== 'object' && !Array.isArray(payload.userid)) {
return true;
}
}
let destChannel;
if (typeof payload.to === 'string' && !!payload.to.trim()) {
destChannel = payload.to;
} else {
destChannel = Math.random().toString(36).substr(2, 8);
}
// find target user(s)
const badClients = server.findSockets({ channel: payload.channel, userid: payload.userid });
const badClients = findUsers(server, payload);
if (badClients.length === 0) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
text: 'Could not find user(s) in channel',
cmd: 'warn',
text: 'Could not find user(s) in that channel',
id: Errors.Global.UNKNOWN_USER,
}, socket);
}
@ -45,8 +52,9 @@ export async function run({
for (let i = 0, j = badClients.length; i < j; i += 1) {
if (badClients[i].level >= socket.level) {
server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn',
text: 'Cannot kick other users with the same level, how rude',
id: Errors.Global.PERMISSION,
}, socket);
} else {
kicked.push(badClients[i]);
@ -57,6 +65,13 @@ export async function run({
return true;
}
let destChannel;
if (typeof payload.to === 'string' && !!payload.to.trim()) {
destChannel = payload.to;
} else {
destChannel = Math.random().toString(36).substr(2, 8);
}
// Announce the kicked clients arrival in destChannel and that they were kicked
// Before they arrive, so they don't see they got moved
for (let i = 0; i < kicked.length; i += 1) {
@ -64,12 +79,17 @@ export async function run({
cmd: 'onlineAdd',
nick: kicked[i].nick,
trip: kicked[i].trip || 'null',
uType: 'user',
hash: kicked[i].hash,
level: UAC.levels.default,
userid: kicked[i].userid,
channel: destChannel,
}, { channel: destChannel });
}
// Move all kicked clients to the new channel
for (let i = 0; i < kicked.length; i += 1) {
// @todo multi-channel update
kicked[i].channel = destChannel;
server.broadcast({
@ -84,6 +104,7 @@ export async function run({
for (let i = 0, j = kicked.length; i < j; i += 1) {
server.broadcast({
cmd: 'onlineRemove',
userid: kicked[i].userid,
nick: kicked[i].nick,
}, { channel: socket.channel });
}

View File

@ -1,5 +1,6 @@
/*
Description: Removes the target socket from the current channel and forces a join event in another
@deprecated This module will be removed or replaced
*/
import * as UAC from '../utility/UAC/_info';
@ -25,7 +26,7 @@ export async function run({ server, socket, payload }) {
if (badClients.length === 0) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Could not find user in channel',
}, socket);
}
@ -34,7 +35,7 @@ export async function run({ server, socket, payload }) {
if (badClient.level >= socket.level) {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Cannot move other users of the same level, how rude',
}, socket);
}

View File

@ -26,7 +26,7 @@ export async function run({
// check user input
if (typeof payload.ip !== 'string' && typeof payload.hash !== 'string') {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: "hash:'targethash' or ip:'1.2.3.4' is required",
}, socket);
}

View File

@ -18,7 +18,7 @@ export async function run({
// check user input
if (typeof payload.ip !== 'string' && typeof payload.hash !== 'string') {
return server.reply({
cmd: 'warn', // @todo Remove english and change to numeric id
cmd: 'warn', // @todo Add numeric error code as `id`
text: "hash:'targethash' or ip:'1.2.3.4' is required",
}, socket);
}

View File

@ -1,3 +1,4 @@
// NOTE: this has been moved into /utility/ as _UAC.js
/**
* User Account Control information containing level constants
* and simple helper functions related to users

View File

@ -0,0 +1,85 @@
import {
Errors,
} from './_Constants';
/**
* Checks if a client can join `channel`, returns numeric error code or true if
* able to join
* @public
* @param {string} channel Target channel
* @param {object} socket Target client to evaluate
* @return {boolean||error id}
*/
// eslint-disable-next-line no-unused-vars
export function canJoinChannel(channel, socket) {
if (typeof channel !== 'string') return Errors.Channel.INVALID_NAME;
if (channel === '') return Errors.Channel.INVALID_NAME;
if (channel.length > 120) return Errors.Channel.INVALID_LENGTH;
return true;
}
/**
* Returns an object containing info about the specified channel,
* including if it is owned, mods, permissions
* @public
* @param {string} config Server config object
* @param {string} channel Target channel
* @return {object}
*/
export function getChannelSettings(config, channel) {
if (typeof config.channels !== 'undefined') {
if (typeof config.channels[channel] !== 'undefined') {
return config.channels[channel];
}
}
return {
owned: false,
};
}
/**
* Returns an object containing info about the specified channel,
* including if it is owned, mods, permissions
* @public
* @param {MainServer} server Main server reference
* @param {object} payload Object containing `userid` or `nick`
* @param {number} limit Optional return limit
* @return {array}
*/
export function findUsers(server, payload, limit = 0) {
let targetClients;
if (typeof payload.userid !== 'undefined') {
targetClients = server.findSockets({
channel: payload.channel,
userid: payload.userid,
});
} else if (typeof payload.nick !== 'undefined') {
targetClients = server.findSockets({
channel: payload.channel,
nick: payload.nick,
});
} else {
return [];
}
if (limit !== 0 && targetClients.length > limit) {
return targetClients.splice(0, limit);
}
return targetClients;
}
/**
* Overload for `findUsers` when only 1 user is expected
* @public
* @param {MainServer} server Main server reference
* @param {object} payload Object containing `userid` or `nick`
* @param {number} limit Optional return limit
* @return {boolean||object}
*/
export function findUser(server, payload) {
return findUsers(server, payload, 1)[0] || false;
}

View File

@ -0,0 +1,34 @@
/* Base error ranges */
const GlobalErrors = 10;
const JoinErrors = 20;
const ChannelErrors = 30;
const InviteErrors = 40;
/**
* Holds the numeric id values for each error type
* @typedef {object} Errors
*/
exports.Errors = {
Global: {
RATELIMIT: GlobalErrors + 1,
UNKNOWN_USER: GlobalErrors + 2,
PERMISSION: GlobalErrors + 3,
},
Join: {
RATELIMIT: JoinErrors + 1,
INVALID_NICK: JoinErrors + 2,
ALREADY_JOINED: JoinErrors + 3,
NAME_TAKEN: JoinErrors + 4,
},
Channel: {
INVALID_NAME: ChannelErrors + 1,
INVALID_LENGTH: ChannelErrors + 2,
},
Invite: {
RATELIMIT: InviteErrors + 1,
INVALID_LENGTH: InviteErrors + 2,
},
};

View File

@ -0,0 +1,90 @@
/* eslint no-param-reassign: 0 */
import {
isAdmin,
isModerator,
} from './_UAC';
/**
* Marks the socket as using the legacy protocol and
* applies the missing `pass` property to the payload
* @param {MainServer} server Main server reference
* @param {WebSocket} socket Target client socket
* @param {object} payload The original `join` payload
* @returns {object}
*/
export function upgradeLegacyJoin(server, socket, payload) {
const newPayload = payload;
// `join` is the legacy entry point, so apply protocol version
socket.hcProtocol = 1;
// this would have been applied in the `session` module, apply it now
socket.hash = server.getSocketHash(socket);
// pull the password from the nick
const nickArray = payload.nick.split('#', 2);
newPayload.nick = nickArray[0].trim();
if (nickArray[1] && typeof payload.pass === 'undefined') {
newPayload.pass = nickArray[1]; // eslint-disable-line prefer-destructuring
}
// dunno how this happened on the legacy version
if (typeof payload.password !== 'undefined') {
newPayload.pass = payload.password;
}
// apply the missing `userid` prop
if (typeof socket.userid === 'undefined') {
socket.userid = Math.floor(Math.random() * 9999999999999);
}
return newPayload;
}
/**
* Return the correct `uType` label for the specific level
* @param {number} level Numeric level to find the label for
*/
export function legacyLevelToLabel(level) {
if (isAdmin(level)) return 'admin';
if (isModerator(level)) return 'mod';
return 'user';
}
/**
* Alter the outgoing payload to an `info` cmd and add/change missing props
* @param {object} payload Numeric level to find the label for
* @param {string} nick Numeric level to find the label for
* @return {object}
*/
export function legacyInviteOut(payload, nick) {
return {
...payload,
...{
cmd: 'info',
type: 'invite',
from: nick,
text: `${nick} invited you to ?${payload.inviteChannel}`,
},
};
}
/**
* Alter the outgoing payload to an `info` cmd and add/change missing props
* @param {object} payload Numeric level to find the label for
* @param {string} nick Numeric level to find the label for
* @return {object}
*/
export function legacyInviteReply(payload, nick) {
return {
...payload,
...{
cmd: 'info',
type: 'invite',
from: '',
text: `You invited ${nick} to ?${payload.inviteChannel}`,
},
};
}

View File

@ -0,0 +1,186 @@
/**
* User Account Control information containing level constants
* and simple helper functions related to users
* @property {Object} levels - Defines labels for default permission ranges
* @author MinusGix ( https://github.com/MinusGix )
* @version v1.0.0
* @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
*/
import {
getChannelSettings,
} from './_Channels';
const crypto = require('crypto');
/**
* Object defining labels for default permission ranges
* @typedef {Object} levels
* @property {number} admin Global administrator range
* @property {number} moderator Global moderator range
* @property {number} channelOwner Local administrator range
* @property {number} channelModerator Local moderator range
* @property {number} channelTrusted Local (non-public) channel trusted
* @property {number} trustedUser Public channel trusted
* @property {number} default Default user level
*/
export const levels = {
admin: 9999999,
moderator: 999999,
channelOwner: 99999,
channelModerator: 9999,
channelTrusted: 8999,
trustedUser: 500,
default: 100,
};
/**
* Returns true if target level is equal or greater than the global admin level
* @public
* @param {number} level Level to verify
* @return {boolean}
*/
export function isAdmin(level) {
return level >= levels.admin;
}
/**
* Returns true if target level is equal or greater than the global moderator level
* @public
* @param {number} level Level to verify
* @return {boolean}
*/
export function isModerator(level) {
return level >= levels.moderator;
}
/**
* Returns true if target level is equal or greater than the channel owner level
* @public
* @param {number} level Level to verify
* @return {boolean}
*/
export function isChannelOwner(level) {
return level >= levels.channelOwner;
}
/**
* Returns true if target level is equal or greater than the channel moderator level
* @public
* @param {number} level Level to verify
* @return {boolean}
*/
export function isChannelModerator(level) {
return level >= levels.channelModerator;
}
/**
* Returns true if target level is equal or greater than the channel trust level
* @public
* @param {number} level Level to verify
* @return {boolean}
*/
export function isChannelTrusted(level) {
return level >= levels.channelTrusted;
}
/**
* Returns true if target level is equal or greater than a trusted user
* @public
* @param {number} level Level to verify
* @return {boolean}
*/
export function isTrustedUser(level) {
return level >= levels.trustedUser;
}
/**
* Return an object containing public information about the socket
* @public
* @param {WebSocket} socket Target client
* @return {Object}
*/
export function getUserDetails(socket) {
return {
uType: socket.uType,
nick: socket.nick,
trip: socket.trip || 'null',
hash: socket.hash,
level: socket.level,
userid: socket.userid,
};
}
/**
* Returns true if the nickname is valid
* @public
* @param {string} nick Nickname to verify
* @return {boolean}
*/
export function verifyNickname(nick) {
if (typeof nick === 'undefined') return false;
return /^[a-zA-Z0-9_]{1,24}$/.test(nick);
}
/**
* Hashes a user's password, returning a trip code
* or a blank string
* @public
* @param {string} pass User's password
* @param {string} config Server config object
* @param {string} channel Channel-level permissions check
* @return {string}
*/
export function getUserPerms(pass, config, channel) {
if (!pass) {
return {
trip: '',
level: levels.default,
};
}
const sha = crypto.createHash('sha256');
sha.update(pass + config.tripSalt);
const trip = sha.digest('base64').substr(0, 6);
// check if user is global admin
if (trip === config.adminTrip) {
return {
trip: 'Admin',
level: levels.admin,
};
}
// check if user is global mod
config.mods.forEach((mod) => { // eslint-disable-line consistent-return
if (trip === mod.trip) {
return {
trip,
level: levels.moderator,
};
}
});
const channelSettings = getChannelSettings(config, channel);
if (channelSettings.owned) {
// check if user is channel owner
// @todo channel ownership patch
// check if user is channel mod
// @todo channel ownership patch
// check if user is channel trusted
// @todo channel ownership patch
}
// check if user is global trusted
// @todo channel ownership patch
return {
trip,
level: levels.default,
};
}