mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-12 16:15:31 +08:00
1841 lines
64 KiB
JavaScript
1841 lines
64 KiB
JavaScript
'use strict';
|
|
|
|
const dns = require('dns');
|
|
const crypto = require('crypto');
|
|
const EventEmitter = require('events');
|
|
const os = require('os');
|
|
const codes = require('./codes');
|
|
const db = require('../db');
|
|
const ObjectID = require('mongodb').ObjectID;
|
|
|
|
const PING_TIMEOUT = 10 * 120 * 1000;
|
|
const SOCKET_TIMEOUT = 5 * 60 * 1000;
|
|
|
|
class IRCConnection extends EventEmitter {
|
|
constructor(server, socket) {
|
|
super();
|
|
this.server = server;
|
|
this._socket = socket;
|
|
|
|
this._closed = false;
|
|
this._closing = false;
|
|
|
|
this._authenticating = false;
|
|
|
|
this.subscriptions = new Set();
|
|
|
|
this.remoteAddress = this._socket.remoteAddress;
|
|
this.id = crypto
|
|
.randomBytes(8)
|
|
.toString('base64')
|
|
.replace(/[=/+]+/g, '')
|
|
.toUpperCase();
|
|
|
|
this.connectionPass = false;
|
|
|
|
this.hostname = (server.options.hostname || os.hostname() || this._socket.localAddress || 'localhost').toLowerCase().trim();
|
|
|
|
this.processing = false;
|
|
this.queue = [];
|
|
this._remainder = '';
|
|
|
|
this.starting = false;
|
|
this.started = false;
|
|
this.capStarted = false;
|
|
this.capEnded = false;
|
|
this.capEnabled = new Set();
|
|
|
|
this.accumulateTimer = false;
|
|
this.accumulateStart = false;
|
|
this.fetching = false;
|
|
this.dofetch = false;
|
|
this.lastFetchedItem = new ObjectID();
|
|
|
|
this.subscriber = data => {
|
|
switch (data.action) {
|
|
case 'message': {
|
|
clearTimeout(this.accumulateTimer);
|
|
let time = Date.now();
|
|
if (this.accumulateStart && this.accumulateStart < time - 1000) {
|
|
this.accumulateStart = false;
|
|
return this.fetchMessages();
|
|
}
|
|
if (!this.accumulateStart) {
|
|
this.accumulateStart = time;
|
|
}
|
|
this.accumulateTimer = setTimeout(() => this.fetchMessages(), 80);
|
|
this.accumulateTimer.unref();
|
|
break;
|
|
}
|
|
|
|
case 'nick': {
|
|
if (data.session === this.session.id.toString()) {
|
|
// same session
|
|
break;
|
|
}
|
|
|
|
if (data.user === this.session.auth.id.toString()) {
|
|
let currentSource = this.getFormattedName();
|
|
this.session.nick = data.nick;
|
|
this.send({ source: currentSource, verb: 'NICK', target: false, params: this.session.nick });
|
|
} else {
|
|
this.send({ source: data.old, verb: 'NICK', target: false, params: data.nick });
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'join': {
|
|
if (data.session === this.session.id.toString()) {
|
|
// same session
|
|
break;
|
|
}
|
|
|
|
if (data.user === this.session.auth.id.toString()) {
|
|
let subscriptionKey = [this.session.ns, '#', data.channelId].join('.');
|
|
if (!this.subscriptions.has(subscriptionKey)) {
|
|
this.subscribe(subscriptionKey);
|
|
this.send({ source: this.getFormattedName(), verb: 'JOIN', target: false, message: data.channel });
|
|
}
|
|
} else {
|
|
this.send({ source: data.nick, verb: 'JOIN', target: false, message: data.channel });
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'part': {
|
|
if (data.session === this.session.id.toString()) {
|
|
// same session
|
|
break;
|
|
}
|
|
|
|
if (data.user === this.session.auth.id.toString()) {
|
|
let subscriptionKey = [this.session.ns, '#', data.channelId].join('.');
|
|
if (this.subscriptions.has(subscriptionKey)) {
|
|
this.unsubscribe(subscriptionKey);
|
|
this.send({ source: this.getFormattedName(), verb: 'PART', target: false, message: data.channel });
|
|
}
|
|
} else {
|
|
this.send({ source: data.nick, verb: 'PART', target: false, message: data.channel });
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case 'topic': {
|
|
this.send({ time: data.topicTime, source: data.topicAuthor, verb: 'TOPIC', target: data.channel, message: data.topic });
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
init() {
|
|
this._setListeners();
|
|
this._resetSession();
|
|
this.server.logger.info(
|
|
{
|
|
tnx: 'connection',
|
|
cid: this.id,
|
|
host: this.remoteAddress
|
|
},
|
|
'Connection from %s',
|
|
this.remoteAddress
|
|
);
|
|
this.send({
|
|
verb: 'NOTICE',
|
|
target: false,
|
|
params: 'Auth',
|
|
message: '*** Looking up your hostname...'
|
|
});
|
|
try {
|
|
dns.reverse(this.remoteAddress, (err, hostnames) => {
|
|
if (!err && hostnames && hostnames.length) {
|
|
this.session.clientHostname = hostnames[0];
|
|
this.send({
|
|
verb: 'NOTICE',
|
|
target: false,
|
|
params: 'Auth',
|
|
message: '*** Found your hostname'
|
|
});
|
|
} else {
|
|
this.session.clientHostname = this.remoteAddress;
|
|
this.send({
|
|
verb: 'NOTICE',
|
|
target: false,
|
|
params: 'Auth',
|
|
message: '*** Could not resolve your hostname; using your IP address (' + this.remoteAddress + ') instead'
|
|
});
|
|
}
|
|
});
|
|
} catch (E) {
|
|
this.session.clientHostname = this.remoteAddress;
|
|
this.send({
|
|
verb: 'NOTICE',
|
|
target: false,
|
|
params: '*',
|
|
message: '*** Could not resolve your hostname; using your IP address (' + this.remoteAddress + ') instead'
|
|
});
|
|
}
|
|
this.updatePinger();
|
|
}
|
|
|
|
write(payload) {
|
|
if (!this._socket || !this._socket.writable) {
|
|
return;
|
|
}
|
|
this._socket.write(payload);
|
|
}
|
|
|
|
fetchMessages(force) {
|
|
if (!force && this.fetching) {
|
|
this.dofetch = true;
|
|
return false;
|
|
}
|
|
this.fetching = true;
|
|
this.dofetch = false;
|
|
|
|
let query = {
|
|
_id: { $gt: this.lastFetchedItem },
|
|
rcpt: this.session.auth.id
|
|
};
|
|
|
|
let cursor = db.database.collection('chat').find(query);
|
|
|
|
let clear = () =>
|
|
cursor.close(() => {
|
|
db.redis.hset('irclast', this.session.auth.id.toString(), this.lastFetchedItem.toString(), () => {
|
|
if (this.dofetch) {
|
|
return setImmediate(() => this.fetchMessages(true));
|
|
} else {
|
|
this.fetching = false;
|
|
}
|
|
});
|
|
});
|
|
|
|
let processNext = () => {
|
|
cursor.next((err, message) => {
|
|
if (err) {
|
|
this.server.logger.error(
|
|
{
|
|
err,
|
|
tnx: 'chat',
|
|
cid: this.id
|
|
},
|
|
'Failed iterating db cursor. %s',
|
|
err.message
|
|
);
|
|
return;
|
|
}
|
|
if (!message) {
|
|
return clear();
|
|
}
|
|
this.lastFetchedItem = message._id;
|
|
|
|
if (message.session.toString() === this.session.id.toString() && !this.capEnabled.has('echo-message')) {
|
|
// ignore messages from self unless echo-message
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
let payload = {
|
|
time: message.time,
|
|
source: message.nick,
|
|
verb: 'PRIVMSG',
|
|
message: message.message
|
|
};
|
|
|
|
if (message.type === 'channel') {
|
|
payload.target = message.channel.name;
|
|
this.send(payload);
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
db.database.collection('nicks').findOne({
|
|
user: new ObjectID(message.target)
|
|
}, (err, nickData) => {
|
|
if (err) {
|
|
// ignore, not important
|
|
}
|
|
|
|
if (nickData) {
|
|
payload.target = nickData.nick;
|
|
} else {
|
|
payload.target = message.targetNick;
|
|
}
|
|
|
|
this.send(payload);
|
|
return setImmediate(processNext);
|
|
});
|
|
});
|
|
};
|
|
|
|
processNext();
|
|
}
|
|
|
|
send(payload) {
|
|
if (!this._socket || !this._socket.writable) {
|
|
return;
|
|
}
|
|
|
|
if (payload && typeof payload === 'object') {
|
|
let message = [];
|
|
|
|
let verb = (payload.verb || '')
|
|
.toString()
|
|
.toUpperCase()
|
|
.trim();
|
|
|
|
let tags = payload.tags;
|
|
if (tags && !Array.isArray(tags) && typeof tags === 'object') {
|
|
tags = Object.keys(tags || {}).forEach(key => ({
|
|
key,
|
|
value: tags[key]
|
|
}));
|
|
}
|
|
tags = [].concat(tags || []);
|
|
|
|
if (['PRIVMSG', 'NOTICE'].includes(verb.toUpperCase())) {
|
|
let time = payload.time ? (typeof payload.time !== 'object' ? new Date(payload.time) : payload.time) : new Date();
|
|
|
|
if (this.capEnabled.has('server-time')) {
|
|
tags.push({
|
|
key: 'time',
|
|
value: time.getISOString()
|
|
});
|
|
} else if (this.capEnabled.has('znc.in/server-time')) {
|
|
tags.push({
|
|
key: 't',
|
|
value: Math.round(time.getTime() / 1000)
|
|
});
|
|
}
|
|
}
|
|
|
|
if (tags.length) {
|
|
let tagStr = tags
|
|
.map(tag => {
|
|
if (typeof tag.value === 'boolean') {
|
|
if (tag.value === true) {
|
|
return tag.key;
|
|
}
|
|
return;
|
|
}
|
|
return (
|
|
tag.key +
|
|
'=' +
|
|
(tag.value || '').toString().replace(/[;\r\n\\ ]/g, c => {
|
|
switch (c) {
|
|
case ';':
|
|
return '\\:';
|
|
case '\r':
|
|
return '\\r';
|
|
case '\n':
|
|
return '\\n';
|
|
case ' ':
|
|
return '\\s';
|
|
}
|
|
})
|
|
);
|
|
})
|
|
.join(';');
|
|
if (tagStr.length) {
|
|
message.push('@' + tagStr);
|
|
}
|
|
}
|
|
|
|
payload.source = payload.source || this.hostname;
|
|
message.push(':' + payload.source);
|
|
|
|
if (verb) {
|
|
message.push(codes.has(verb) ? codes.get(verb) : verb);
|
|
}
|
|
|
|
if (payload.target) {
|
|
message.push(payload.target);
|
|
} else if (payload.target !== false) {
|
|
message.push(this.session.nick || this.id);
|
|
}
|
|
|
|
if (payload.params) {
|
|
message = message.concat(payload.params || []);
|
|
}
|
|
|
|
if (payload.message || typeof payload.message === 'string') {
|
|
message.push(':' + payload.message);
|
|
}
|
|
|
|
payload = message.join(' ');
|
|
}
|
|
|
|
this.server.logger.debug(
|
|
{
|
|
tnx: 'send',
|
|
cid: this.id,
|
|
host: this.remoteAddress
|
|
},
|
|
'S:',
|
|
(payload.length < 128 ? payload : payload.substr(0, 128) + '... +' + (payload.length - 128) + ' B').replace(/\r?\n/g, '\\n')
|
|
);
|
|
|
|
this.write(payload + '\r\n');
|
|
}
|
|
|
|
_setListeners() {
|
|
this._socket.on('close', () => this._onClose());
|
|
this._socket.on('error', err => this._onError(err));
|
|
this._socket.setTimeout(this.server.options.socketTimeout || SOCKET_TIMEOUT, () => this._onTimeout());
|
|
this._socket.on('readable', () => {
|
|
if (this.processing) {
|
|
return;
|
|
}
|
|
this.processing = true;
|
|
|
|
this.read();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fired when the socket is closed
|
|
* @event
|
|
*/
|
|
_onClose(/* hadError */) {
|
|
this.subscriptions.forEach(subscriptionKey => {
|
|
this.unsubscribe(subscriptionKey);
|
|
});
|
|
this.subscriptions.clear();
|
|
|
|
if (this._closed) {
|
|
return;
|
|
}
|
|
|
|
this.queue = [];
|
|
this.processing = false;
|
|
this._remainder = '';
|
|
|
|
this._closed = true;
|
|
this._closing = false;
|
|
|
|
this.server.logger.info(
|
|
{
|
|
tnx: 'close',
|
|
cid: this.id,
|
|
host: this.remoteAddress,
|
|
user: this.session.user
|
|
},
|
|
'Connection closed to %s',
|
|
this.remoteAddress
|
|
);
|
|
|
|
this.emit('close');
|
|
}
|
|
|
|
/**
|
|
* Fired when an error occurs with the socket
|
|
*
|
|
* @event
|
|
* @param {Error} err Error object
|
|
*/
|
|
_onError(err) {
|
|
if (['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(err.code)) {
|
|
this.close(); // mark connection as 'closing'
|
|
return;
|
|
}
|
|
|
|
this.server.logger.error(
|
|
{
|
|
err,
|
|
tnx: 'error',
|
|
user: this.session.user
|
|
},
|
|
'%s',
|
|
err.message
|
|
);
|
|
this.emit('error', err);
|
|
}
|
|
|
|
/**
|
|
* Fired when socket timeouts. Closes connection
|
|
*
|
|
* @event
|
|
*/
|
|
_onTimeout() {
|
|
// TODO: send timeout notification
|
|
this.close();
|
|
}
|
|
|
|
_resetSession() {
|
|
this.session = {
|
|
id: new ObjectID(),
|
|
remoteAddress: this.remoteAddress
|
|
};
|
|
}
|
|
|
|
close() {
|
|
clearTimeout(this.session.pingTimer);
|
|
if (!this._socket.destroyed && this._socket.writable) {
|
|
this._socket.end();
|
|
}
|
|
this._closing = true;
|
|
}
|
|
|
|
read() {
|
|
// update PING timeout
|
|
this.updatePinger();
|
|
|
|
let chunk;
|
|
let data = this._remainder;
|
|
while ((chunk = this._socket.read()) !== null) {
|
|
data += chunk.toString('binary');
|
|
if (data.indexOf('\n') >= 0) {
|
|
let lines = data.split(/\r?\n/).map(line => Buffer.from(line, 'binary').toString());
|
|
this._remainder = lines.pop();
|
|
|
|
if (lines.length) {
|
|
if (this.queue.length) {
|
|
this.queue = this.queue.concat(lines);
|
|
} else {
|
|
this.queue = lines;
|
|
}
|
|
}
|
|
|
|
return this.processQueue();
|
|
}
|
|
}
|
|
|
|
this.processing = false;
|
|
}
|
|
|
|
processQueue() {
|
|
if (!this.queue.length) {
|
|
this.read(); // see if there's anything left to read
|
|
return;
|
|
}
|
|
let line = this.queue.shift().trim();
|
|
if (!line) {
|
|
return this.processQueue();
|
|
}
|
|
|
|
let match = line.match(/^\s*(?:@([^\s]+)\s+)?(?::([^\s]+)\s+)?([^\s]+)\s*/);
|
|
if (!match) {
|
|
// TODO: send error message
|
|
// Can it even happen?
|
|
return this.processQueue();
|
|
}
|
|
|
|
let tags = !match[1] ? false : new Map();
|
|
(match[1] || '')
|
|
.toString()
|
|
.split(';')
|
|
.forEach(elm => {
|
|
if (!elm) {
|
|
return;
|
|
}
|
|
let eqPos = elm.indexOf('=');
|
|
if (eqPos < 0) {
|
|
tags.set(elm, true);
|
|
return;
|
|
}
|
|
|
|
let key = elm.substr(0, eqPos);
|
|
let value = elm.substr(eqPos + 1).replace(/\\(.)/g, (m, c) => {
|
|
switch (c) {
|
|
case ':':
|
|
return ';';
|
|
case 's':
|
|
return ' ';
|
|
case '\\':
|
|
return '\\';
|
|
case 'r':
|
|
return '\r';
|
|
case 'n':
|
|
return '\n';
|
|
default:
|
|
return c;
|
|
}
|
|
});
|
|
tags.set(key, value);
|
|
});
|
|
|
|
let prefix = (match[3] || '').toString() || false;
|
|
let verb = (match[3] || '').toString().toUpperCase();
|
|
let params = line.substr(match[0].length);
|
|
let data;
|
|
let separatorPos = params.indexOf(' :');
|
|
if (separatorPos >= 0) {
|
|
data = params.substr(separatorPos + 2);
|
|
params = params.substr(0, separatorPos);
|
|
}
|
|
params = params
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter(arg => arg);
|
|
if (data || typeof data === 'string') {
|
|
params.push(data);
|
|
}
|
|
|
|
let logLine = (line || '').toString();
|
|
|
|
this.server.logger.debug(
|
|
{
|
|
tnx: 'receive',
|
|
cid: this.id,
|
|
user: this.session.user
|
|
},
|
|
'C:',
|
|
logLine
|
|
);
|
|
|
|
if (typeof this['command_' + verb] === 'function') {
|
|
this['command_' + verb](tags, prefix, params, () => {
|
|
this.processQueue();
|
|
});
|
|
} else {
|
|
if (this.session.user) {
|
|
this.send({
|
|
verb: 'ERR_UNKNOWNCOMMAND',
|
|
target: this.session.nick,
|
|
params: verb,
|
|
message: 'Unknown command'
|
|
});
|
|
}
|
|
return this.processQueue();
|
|
}
|
|
}
|
|
|
|
printNickList(channelData, fresh, next) {
|
|
db.database
|
|
.collection('nicks')
|
|
.find({
|
|
user: { $in: channelData.members }
|
|
})
|
|
.project({
|
|
_id: true,
|
|
nick: true,
|
|
user: true
|
|
})
|
|
.toArray((err, nickList) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: channelData.channel, message: err.message });
|
|
return next();
|
|
}
|
|
|
|
if (!nickList) {
|
|
nickList = [];
|
|
}
|
|
|
|
if (fresh) {
|
|
nickList.unshift({
|
|
nick: this.session.nick
|
|
});
|
|
}
|
|
|
|
let lines = [];
|
|
let curLine = { members: [], length: 0 };
|
|
nickList.forEach(nickData => {
|
|
curLine.members.push(nickData.nick);
|
|
curLine.length += nickData.nick.length + 1;
|
|
if (curLine.length > 400) {
|
|
lines.push(curLine);
|
|
curLine = { members: [], length: 0 };
|
|
}
|
|
});
|
|
if (curLine.length) {
|
|
lines.push(curLine);
|
|
}
|
|
|
|
this.send({ source: this.getFormattedName(), verb: 'JOIN', target: false, message: channelData.channel });
|
|
|
|
if (channelData.topic) {
|
|
let topicTime = Math.round((channelData.topicTime || new Date()).getTime() / 1000);
|
|
this.send({ verb: 'RPL_TOPIC', params: channelData.channel, message: channelData.topic });
|
|
this.send({ verb: 'RPL_TOPICWHOTIME', params: [channelData.channel, channelData.topicAuthor, topicTime] });
|
|
}
|
|
|
|
lines.forEach(line => {
|
|
this.send({ verb: 'RPL_NAMREPLY', params: ['=', channelData.channel], message: line.members.join(' ') });
|
|
});
|
|
|
|
this.send({ verb: 'RPL_ENDOFNAMES', params: channelData.channel, message: 'End of /NAMES list' });
|
|
|
|
next();
|
|
});
|
|
}
|
|
|
|
getFormattedName(skipNick, nick) {
|
|
nick = nick || this.session.nick;
|
|
return (!skipNick && nick ? nick + '!' : '') + (this.session.user || 'unknown') + '@' + this.session.clientHostname;
|
|
}
|
|
|
|
checkSessionStart(next) {
|
|
if (this.starting || this.started) {
|
|
return setImmediate(next);
|
|
}
|
|
if (!this.session.user || !this.session.nick || (this.capStarted && !this.capEnded)) {
|
|
return setImmediate(next);
|
|
}
|
|
this.starting = true;
|
|
|
|
this.server.logger.info(
|
|
{
|
|
tnx: 'session',
|
|
cid: this.id,
|
|
host: this.remoteAddress,
|
|
user: this.session.user,
|
|
name: this.session.name
|
|
},
|
|
'Registered %s as %s',
|
|
this.session.user,
|
|
JSON.stringify(this.session.name)
|
|
);
|
|
|
|
this.send({ verb: 'NOTICE', target: 'Auth', message: 'Welcome to \x02' + this.server.name + '\x02!' });
|
|
|
|
this.send({ verb: 'RPL_WELCOME', message: 'Welcome to the ' + this.server.name + ' IRC Network ' + this.getFormattedName() });
|
|
|
|
this.send({ verb: 'RPL_YOURHOST', message: 'Your host is ' + this.hostname + ', running version ' + this.server.version });
|
|
|
|
this.send({ verb: 'RPL_CREATED', message: 'This server was created ' + this.server.startTimeFormatted });
|
|
|
|
// TODO: use real flags, <available user modes> <available channel modes> [<channel modes with a parameter>
|
|
this.send({ verb: 'RPL_MYINFO', params: [this.hostname, this.server.version, 'iosw', 'biklmnopstv', 'bklov'] });
|
|
|
|
this.send({ verb: 'RPL_ISUPPORT', params: this.server.supportedFormatted, message: 'are supported by this server' });
|
|
|
|
this.send({ verb: 'RPL_YOURID', params: this.id, message: 'your unique ID' });
|
|
|
|
this.send({ verb: 'RPL_MOTDSTART', message: this.hostname + ' message of the day' });
|
|
|
|
this.server.motd.split(/\r?\n/).forEach(line => this.send({ verb: 'RPL_MOTD', message: '- ' + line }));
|
|
|
|
this.send({ verb: 'RPL_ENDOFMOTD', message: 'End of message of the day' });
|
|
|
|
this.starting = false;
|
|
this.started = true;
|
|
|
|
if (!this.session.auth) {
|
|
this.send({
|
|
source: 'NickServ!NickServ@services.',
|
|
verb: 'NOTICE',
|
|
message: 'This server requires all users to be authenticated. Identify via /msg NickServ identify <password>'
|
|
});
|
|
return setImmediate(next);
|
|
} else {
|
|
this.initializeSubscriptions(next);
|
|
}
|
|
}
|
|
|
|
initializeSubscriptions(next) {
|
|
next = next || (() => false);
|
|
db.database
|
|
.collection('channels')
|
|
.find({
|
|
members: this.session.auth.id
|
|
})
|
|
.project({
|
|
_id: true,
|
|
channel: true,
|
|
members: true,
|
|
topic: true,
|
|
topicTime: true,
|
|
topicAuthor: true
|
|
})
|
|
.toArray((err, channels) => {
|
|
if (err) {
|
|
this.server.logger.error(
|
|
{
|
|
err,
|
|
tnx: 'setup',
|
|
cid: this.id,
|
|
user: this.session.auth.id
|
|
},
|
|
'Failed loading channels. %s',
|
|
err.message
|
|
);
|
|
return next();
|
|
}
|
|
if (Array.isArray(channels)) {
|
|
channels.forEach(channelData => {
|
|
this.server.logger.info(
|
|
{
|
|
tnx: 'setup',
|
|
cid: this.id,
|
|
channel: channelData._id
|
|
},
|
|
'Joining %s to channel %s',
|
|
this.session.auth.id,
|
|
channelData._id
|
|
);
|
|
|
|
this.subscribe([this.session.ns, '#', channelData._id].join('.'));
|
|
this.printNickList(channelData, false, next);
|
|
//this.send({ source: this.getFormattedName(), verb: 'JOIN', target: false, message: channelData.channel });
|
|
});
|
|
}
|
|
|
|
// private messages
|
|
this.subscribe([this.session.ns, '%', this.session.auth.id].join('.'));
|
|
|
|
// general notifications
|
|
this.subscribe([this.session.ns, '!', '*'].join('.'));
|
|
|
|
this.fetchMessages();
|
|
|
|
return next();
|
|
});
|
|
}
|
|
|
|
authenticate(user, password, next) {
|
|
this.server.userHandler.authenticate(
|
|
user.replace(/\+/, '@'),
|
|
password,
|
|
'irc',
|
|
{
|
|
protocol: 'IRC',
|
|
ip: this.remoteAddress
|
|
},
|
|
(err, result) => {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
if (!result) {
|
|
return next();
|
|
}
|
|
|
|
if (result.scope === 'master' && result.require2fa) {
|
|
// master password not allowed if 2fa is enabled!
|
|
return next();
|
|
}
|
|
|
|
//this.session.clientHostname = 'example.com';
|
|
this.session.user = result.username;
|
|
|
|
db.users.collection('users').findOne({ _id: result.user }, {
|
|
fields: {
|
|
_id: true,
|
|
username: true,
|
|
address: true,
|
|
name: true,
|
|
ns: true
|
|
}
|
|
}, (err, userData) => {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
let ns = userData.ns;
|
|
db.redis.hget('irclast', userData._id.toString(), (err, ircLast) => {
|
|
if (err) {
|
|
// ignore
|
|
}
|
|
|
|
if (ircLast) {
|
|
let lastFetchedItem = new ObjectID(ircLast);
|
|
let maxAge = Math.round(Date.now() / 1000 - 2 * 24 * 3600);
|
|
if (lastFetchedItem.getTimestamp().getTime() < maxAge * 1000) {
|
|
lastFetchedItem = new ObjectID(maxAge);
|
|
}
|
|
this.lastFetchedItem = lastFetchedItem;
|
|
}
|
|
|
|
if (userData.address) {
|
|
let parts = userData.address.split('@');
|
|
this.session.user = parts.shift();
|
|
this.session.clientHostname = parts.join('@');
|
|
if (!ns) {
|
|
ns = this.session.clientHostname;
|
|
}
|
|
}
|
|
|
|
this.session.nick = this.session.nick || userData.username;
|
|
this.session.ns = ns || 'root';
|
|
|
|
next(null, {
|
|
id: userData._id,
|
|
username: userData.username
|
|
});
|
|
});
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
getNick(nick, next) {
|
|
let ns = this.session.ns;
|
|
let nickview = nick.toLowerCase().replace(/\./g, '');
|
|
let user = this.session.auth.id;
|
|
|
|
let nickInfo = {
|
|
action: 'nick',
|
|
user,
|
|
changed: false,
|
|
nick,
|
|
session: this.session.id.toString()
|
|
};
|
|
|
|
db.database.collection('nicks').findOne({
|
|
ns,
|
|
nickview: { $ne: nickview },
|
|
user
|
|
}, (err, existingData) => {
|
|
if (err) {
|
|
// ignore, not important
|
|
}
|
|
|
|
if (existingData) {
|
|
nickInfo.old = this.getFormattedName(false, existingData.nick);
|
|
nickInfo.changed = true;
|
|
}
|
|
|
|
db.database.collection('nicks').findOne({
|
|
ns,
|
|
nickview
|
|
}, (err, nickData) => {
|
|
if (err) {
|
|
if (existingData && existingData.nick) {
|
|
err.existingNick = existingData.nick;
|
|
}
|
|
return next(err);
|
|
}
|
|
|
|
if (nickData) {
|
|
if (nickData.user.toString() === user.toString()) {
|
|
nickInfo.id = nickData._id;
|
|
return next(null, nickInfo);
|
|
}
|
|
|
|
err = new Error('Requested nick is already in use');
|
|
err.verb = 'ERR_NICKNAMEINUSE ';
|
|
if (existingData && existingData.nick) {
|
|
err.existingNick = existingData.nick;
|
|
}
|
|
return next(err);
|
|
}
|
|
|
|
db.database.collection('nicks').insertOne({
|
|
ns,
|
|
nickview,
|
|
nick,
|
|
user
|
|
}, (err, r) => {
|
|
if (err) {
|
|
if (err.code === 11000) {
|
|
err = new Error('Race condition in acquireing nick');
|
|
err.verb = 'ERR_NICKNAMEINUSE ';
|
|
if (existingData && existingData.nick) {
|
|
err.existingNick = existingData.nick;
|
|
}
|
|
return next(err);
|
|
}
|
|
return next(err);
|
|
}
|
|
|
|
let insertId = r && r.insertedId;
|
|
if (!insertId) {
|
|
let err = new Error('Failed to set up nick');
|
|
if (existingData && existingData.nick) {
|
|
err.existingNick = existingData.nick;
|
|
}
|
|
return next(err);
|
|
}
|
|
|
|
nickInfo.id = insertId;
|
|
|
|
if (existingData) {
|
|
// try to remove old nicks
|
|
db.database.collection('nicks').deleteOne({
|
|
_id: existingData._id
|
|
}, () => next(null, nickInfo));
|
|
} else {
|
|
next(null, nickInfo);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
verifyNickChange(currentSource, next) {
|
|
this.getNick(this.session.nick, (err, nickInfo) => {
|
|
if (err) {
|
|
currentSource = currentSource || this.getFormattedName();
|
|
this.send({ verb: err.verb || 'ERR_UNAVAILRESOURCE', params: this.session.nick, message: err.message });
|
|
this.session.nick = err.existingNick || 'user' + this.id;
|
|
|
|
if (currentSource && currentSource !== this.getFormattedName()) {
|
|
this.send({ source: currentSource, verb: 'NICK', target: false, params: this.session.nick });
|
|
}
|
|
|
|
return next();
|
|
}
|
|
|
|
if (nickInfo.changed) {
|
|
this.publish(this.session.ns + '.!.nick', nickInfo);
|
|
}
|
|
|
|
return next();
|
|
});
|
|
}
|
|
|
|
subscribe(subscriptionKey) {
|
|
this.subscriptions.add(subscriptionKey);
|
|
this.server.subscribe(this, subscriptionKey, this.subscriber);
|
|
}
|
|
|
|
unsubscribe(subscriptionKey) {
|
|
this.subscriptions.delete(subscriptionKey);
|
|
this.server.unsubscribe(this, subscriptionKey);
|
|
}
|
|
|
|
publish(subscriptionKey, data) {
|
|
this.server.publish(this, subscriptionKey, data);
|
|
}
|
|
|
|
checkAuth() {
|
|
if (!this.session.auth) {
|
|
this.send({ verb: 'ERR_NOTREGISTERED', message: 'Authentication required to chat in this server' });
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
updatePinger() {
|
|
clearTimeout(this.session.pingTimer);
|
|
this.session.pingTimer = setTimeout(() => {
|
|
this.send('PING :' + this.hostname);
|
|
this.session.pingTimer = setTimeout(() => {
|
|
this.send(
|
|
'ERROR :Closing link: (' +
|
|
(this.session.user || 'unknown') +
|
|
'@' +
|
|
this.session.clientHostname +
|
|
') [Ping timeout: ' +
|
|
Math.round(PING_TIMEOUT / 1000) +
|
|
' seconds]'
|
|
);
|
|
this.close();
|
|
}, PING_TIMEOUT);
|
|
}, PING_TIMEOUT);
|
|
}
|
|
|
|
command_QUIT() {
|
|
this.send('ERROR :Closing link: (' + this.getFormattedName(true) + ') [Client exited]');
|
|
this.close();
|
|
}
|
|
|
|
command_PING(tags, prefix, params, next) {
|
|
if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'PING', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
if (!this.session.user) {
|
|
this.send({ verb: 'ERR_NOTREGISTERED', message: 'You have not registered' });
|
|
return next();
|
|
}
|
|
let host = params[0] || this.session.clientHostname;
|
|
this.send({ verb: 'PONG', target: this.hostname, message: host });
|
|
return next();
|
|
}
|
|
|
|
command_PONG(tags, prefix, params, next) {
|
|
return next();
|
|
}
|
|
|
|
command_NICK(tags, prefix, params, next) {
|
|
let currentSource = this.getFormattedName();
|
|
|
|
if (params.length > 1) {
|
|
this.send({ verb: 'ERR_ERRONEUSNICKNAME', params, message: 'Erroneous Nickname' });
|
|
return next();
|
|
} else if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', params, message: 'Not enough parameters' });
|
|
return next();
|
|
} else if (this.server.disabledNicks.includes(params[0].trim().toLowerCase())) {
|
|
this.send({ verb: 'ERR_ERRONEUSNICKNAME', params, message: 'Erroneous Nickname' });
|
|
return next();
|
|
} else {
|
|
this.session.nick = params[0];
|
|
}
|
|
|
|
if (this.session.auth) {
|
|
this.verifyNickChange(currentSource, () => this.checkSessionStart(next));
|
|
} else {
|
|
this.checkSessionStart(next);
|
|
}
|
|
}
|
|
|
|
command_PASS(tags, prefix, params, next) {
|
|
if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'PASS', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
let pass = params.join(' ');
|
|
|
|
this.connectionPass = pass;
|
|
|
|
return next();
|
|
}
|
|
|
|
command_USER(tags, prefix, params, next) {
|
|
if (this.session.user) {
|
|
this.send({ verb: 'ERR_ALREADYREGISTERED', message: 'You may not reregister' });
|
|
return next();
|
|
}
|
|
|
|
if (params.length < 4) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'USER', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
this.session.user = params[0];
|
|
this.session.name = params.slice(3).join(' ');
|
|
this.session.time = new Date();
|
|
|
|
if (this.connectionPass) {
|
|
this.authenticate(this.session.user, this.connectionPass, (err, auth) => {
|
|
if (err || !auth) {
|
|
let message = err ? err.message : 'Authentication failed';
|
|
this.send('ERROR :Closing link: (' + this.getFormattedName(true) + ') [' + message + ']');
|
|
return this.close();
|
|
}
|
|
this.session.auth = auth;
|
|
return this.verifyNickChange(false, () => this.checkSessionStart(next));
|
|
});
|
|
} else {
|
|
return this.checkSessionStart(next);
|
|
}
|
|
}
|
|
|
|
command_JOIN(tags, prefix, params, next) {
|
|
if (!this.session.user || !this.session.nick) {
|
|
this.send({ verb: 'ERR_NOTREGISTERED', message: 'You have not registered' });
|
|
return next();
|
|
}
|
|
if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'JOIN', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
if (!this.checkAuth()) {
|
|
return next();
|
|
}
|
|
|
|
let channels = params[0]
|
|
.split(',')
|
|
.map(channel => channel.trim())
|
|
.filter(channel => channel);
|
|
|
|
if (channels.length === 1 && channels[0] === '0') {
|
|
// TODO: leave all channels
|
|
return next();
|
|
}
|
|
|
|
let channelPos = 0;
|
|
let processNext = () => {
|
|
if (channelPos >= channels.length) {
|
|
return next();
|
|
}
|
|
|
|
let channel = channels[channelPos++];
|
|
if (channel.length < 2 || !/^[#&]/.test(channel) || /[#&\s]/.test(channel.substr(1)) || /^[#&]\.+$/.test(channel)) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'Invalid channel name' });
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
let channelview = channel.toLowerCase().replace(/\./g, '');
|
|
|
|
let sendJoinMessages = (channelData, fresh, done) => {
|
|
let idString = this.session.auth.id.toString();
|
|
|
|
let eventData = {
|
|
action: 'join',
|
|
channel: channelData.channel,
|
|
session: this.session.id.toString(),
|
|
channelId: channelData._id.toString(),
|
|
user: idString,
|
|
nick: this.getFormattedName()
|
|
};
|
|
|
|
if (fresh) {
|
|
// notify channel members
|
|
this.publish([this.session.ns, '#', channelData._id].join('.'), eventData);
|
|
}
|
|
|
|
// notify other instances of self
|
|
this.publish([this.session.ns, '%', idString].join('.'), eventData);
|
|
this.printNickList(channelData, fresh, done);
|
|
};
|
|
|
|
let tryCount = 0;
|
|
let tryGetChannel = () => {
|
|
db.database.collection('channels').findOne({
|
|
ns: this.session.ns,
|
|
channelview
|
|
}, {
|
|
fields: {
|
|
_id: true,
|
|
channel: true,
|
|
topic: true,
|
|
topicTime: true,
|
|
topicAuthor: true
|
|
}
|
|
}, (err, channelData) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message });
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
if (channelData) {
|
|
return db.database.collection('channels').findOneAndUpdate({
|
|
_id: channelData._id
|
|
}, {
|
|
$addToSet: {
|
|
members: this.session.auth.id
|
|
}
|
|
}, {
|
|
returnOriginal: true
|
|
}, (err, result) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message });
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
if (!result || !result.value) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'Could not open channel' });
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
channelData = result.value;
|
|
|
|
this.subscribe([this.session.ns, '#', channelData._id].join('.'));
|
|
|
|
let idString = this.session.auth.id.toString();
|
|
if (!result.value.members.find(member => member.toString() === idString)) {
|
|
// new join!
|
|
return sendJoinMessages(channelData, true, processNext);
|
|
}
|
|
|
|
sendJoinMessages(channelData, false, processNext);
|
|
});
|
|
}
|
|
|
|
let time = new Date();
|
|
channelData = {
|
|
_id: new ObjectID(),
|
|
channel,
|
|
channelview,
|
|
ns: this.session.ns,
|
|
mode: [],
|
|
owner: this.session.auth.id,
|
|
members: [this.session.auth.id],
|
|
time,
|
|
topic: '',
|
|
topicTime: time,
|
|
topicAuthor: ''
|
|
};
|
|
|
|
db.database.collection('channels').insertOne(channelData, err => {
|
|
if (err) {
|
|
if (err.code === 11000 && tryCount++ < 5) {
|
|
return setTimeout(tryGetChannel, 100);
|
|
}
|
|
this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message });
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
this.subscribe([this.session.ns, '#', channelData._id].join('.'));
|
|
|
|
sendJoinMessages(channelData, false, processNext);
|
|
});
|
|
});
|
|
};
|
|
tryGetChannel();
|
|
};
|
|
processNext();
|
|
}
|
|
|
|
command_PART(tags, prefix, params, next) {
|
|
if (!this.session.user || !this.session.nick) {
|
|
this.send({ verb: 'ERR_NOTREGISTERED', message: 'You have not registered' });
|
|
return next();
|
|
}
|
|
if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'PART', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
if (!this.checkAuth()) {
|
|
return next();
|
|
}
|
|
|
|
let channels = params[0]
|
|
.split(',')
|
|
.map(channel => channel.trim())
|
|
.filter(channel => channel);
|
|
//let reason = params[1] || '';
|
|
|
|
if (channels.length === 1 && channels[0] === '0') {
|
|
// TODO: leave all channels
|
|
return next();
|
|
}
|
|
|
|
let channelPos = 0;
|
|
let processNext = () => {
|
|
if (channelPos >= channels.length) {
|
|
return next();
|
|
}
|
|
|
|
let channel = channels[channelPos++];
|
|
|
|
if (channel.length < 2 || !/^[#&]/.test(channel) || /[#&\s]/.test(channel.substr(1))) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' });
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
db.database.collection('channels').findOneAndUpdate({
|
|
ns: this.session.ns,
|
|
channelview: channel.toLowerCase().replace(/\./g, ''),
|
|
members: this.session.auth.id
|
|
}, {
|
|
$pull: {
|
|
members: this.session.auth.id
|
|
}
|
|
}, {
|
|
returnOriginal: false
|
|
}, (err, result) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message });
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
if (!result || !result.value) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' });
|
|
return setImmediate(processNext);
|
|
}
|
|
|
|
let channelData = result.value;
|
|
|
|
let eventData = {
|
|
action: 'part',
|
|
channel: channelData.channel,
|
|
session: this.session.id.toString(),
|
|
channelId: channelData._id.toString(),
|
|
user: this.session.auth.id.toString(),
|
|
nick: this.getFormattedName()
|
|
};
|
|
|
|
this.send({ source: this.getFormattedName(), verb: 'PART', target: false, message: channelData.channel });
|
|
|
|
let subscriptionKey = [this.session.ns, '#', channelData._id].join('.');
|
|
// notify channel members
|
|
this.publish(subscriptionKey, eventData);
|
|
this.unsubscribe(subscriptionKey);
|
|
|
|
return setImmediate(processNext);
|
|
});
|
|
};
|
|
processNext();
|
|
}
|
|
|
|
command_PRIVMSG(tags, prefix, params, next) {
|
|
if (!this.session.user || !this.session.nick) {
|
|
this.send({ verb: 'ERR_NOTREGISTERED', message: 'You have not registered' });
|
|
return next();
|
|
}
|
|
|
|
if (params.length < 2) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'PRIVMSG', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
let target = params[0].trim();
|
|
if (/[#&\s]/.test(target.substr(1))) {
|
|
this.send({ verb: 'ERR_NOSUCHNICK', params: target, message: 'No such nick/channel' });
|
|
return next();
|
|
}
|
|
|
|
if (target.trim().toLowerCase() === 'nickserv') {
|
|
return this.command_NICKSERV(
|
|
tags,
|
|
prefix,
|
|
params
|
|
.slice(1)
|
|
.join(' ')
|
|
.split(/\s+/)
|
|
.filter(arg => arg),
|
|
next
|
|
);
|
|
}
|
|
|
|
if (!this.checkAuth()) {
|
|
return next();
|
|
}
|
|
|
|
let resolveTarget = done => {
|
|
if (/^[#&\s]/.test(target)) {
|
|
// channel
|
|
db.database.collection('channels').findOne({
|
|
ns: this.session.ns,
|
|
channelview: target.toLowerCase().replace(/\./g, '')
|
|
}, (err, channelData) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: target, message: err.message });
|
|
return next();
|
|
}
|
|
|
|
if (!channelData) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: target, message: 'No such channel' });
|
|
return next();
|
|
}
|
|
|
|
done(false, {
|
|
type: 'channel',
|
|
channel: channelData,
|
|
target: channelData._id.toString(),
|
|
targets: channelData.members || []
|
|
});
|
|
});
|
|
} else {
|
|
// nick
|
|
// channel
|
|
db.database.collection('nicks').findOne({
|
|
ns: this.session.ns,
|
|
nickview: target.toLowerCase().replace(/\./g, '')
|
|
}, (err, nickData) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: target, message: err.message });
|
|
return next();
|
|
}
|
|
|
|
if (!nickData) {
|
|
this.send({ verb: 'ERR_NOSUCHNICK', params: target, message: 'No such nick/channel' });
|
|
return next();
|
|
}
|
|
|
|
done(false, {
|
|
type: 'nick',
|
|
nick: nickData,
|
|
target: nickData.user.toString(),
|
|
targets: [nickData.user].concat(nickData.user.toString() !== this.session.auth.id.toString() ? this.session.auth.id : [])
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
resolveTarget((err, targetData) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: target, message: err.message });
|
|
return next();
|
|
}
|
|
|
|
let msgId = new ObjectID();
|
|
let time = new Date();
|
|
let message = params.slice(1).join(' ');
|
|
let channel = (targetData.type === 'channel' && { id: targetData.channel._id, name: targetData.channel.channel }) || false;
|
|
let inserts = targetData.targets.map(user => {
|
|
let entry = {
|
|
insertOne: {
|
|
msgId,
|
|
channel,
|
|
targetNick: targetData.type === 'nick' ? targetData.nick.nick : false,
|
|
type: targetData.type,
|
|
target: targetData.target,
|
|
session: this.session.id,
|
|
ns: this.session.ns,
|
|
from: this.session.auth.id,
|
|
nick: this.getFormattedName(),
|
|
rcpt: user,
|
|
time,
|
|
message
|
|
}
|
|
};
|
|
return entry;
|
|
});
|
|
|
|
db.database.collection('chat').bulkWrite(inserts, { ordered: false }, err => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: target, message: err.message });
|
|
return next();
|
|
}
|
|
|
|
if (channel) {
|
|
this.publish([this.session.ns, '#', channel.id].join('.'), {
|
|
action: 'message',
|
|
msgId: msgId.toString()
|
|
});
|
|
} else {
|
|
targetData.targets.map(user => {
|
|
this.publish([this.session.ns, '%', user].join('.'), {
|
|
action: 'message',
|
|
msgId: msgId.toString()
|
|
});
|
|
});
|
|
}
|
|
return next();
|
|
});
|
|
});
|
|
}
|
|
|
|
command_CAP(tags, prefix, params, next) {
|
|
if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', target: this.session.nick || '*', params: 'CAP', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
let allowed = ['sasl', 'server-time', 'znc.in/server-time', 'echo-message'];
|
|
|
|
this.capStarted = true;
|
|
let subcommand = params
|
|
.shift()
|
|
.toUpperCase()
|
|
.trim();
|
|
|
|
switch (subcommand) {
|
|
case 'LS':
|
|
this.send({ verb: 'CAP', target: this.session.nick || '*', params: 'LS', message: allowed.join(' ') });
|
|
break;
|
|
|
|
case 'LIST':
|
|
this.send({ verb: 'CAP', target: this.session.nick || '*', params: 'LIST', message: Array.from(this.capEnabled).join(' ') });
|
|
break;
|
|
|
|
case 'REQ':
|
|
{
|
|
let ok = true;
|
|
let enable = [];
|
|
let disable = [];
|
|
params.forEach(arg => {
|
|
let argName = arg.trim().toLowerCase();
|
|
switch (argName.charAt(0)) {
|
|
case '-':
|
|
disable = true;
|
|
argName = argName.substr(1);
|
|
disable.push(argName);
|
|
break;
|
|
case '+':
|
|
argName = argName.substr(1);
|
|
enable.push(argName);
|
|
break;
|
|
default:
|
|
enable.push(argName);
|
|
}
|
|
if (!allowed.includes(argName)) {
|
|
// unknown extension
|
|
ok = false;
|
|
}
|
|
});
|
|
|
|
if (ok) {
|
|
// apply changes
|
|
enable.forEach(arg => this.capEnabled.add(arg));
|
|
disable.forEach(arg => this.capEnabled.delete(arg));
|
|
}
|
|
|
|
this.send({ verb: 'CAP', target: this.session.nick || '*', params: ok ? 'ACK' : 'NAK', message: params.join(' ') });
|
|
}
|
|
break;
|
|
|
|
case 'END':
|
|
this.capEnded = true;
|
|
if (this._authenticating) {
|
|
this._authenticating = false;
|
|
this.send({ verb: 'ERR_SASLABORTED', target: this.session.nick || '*', message: 'SASL authentication aborted' });
|
|
}
|
|
|
|
return this.checkSessionStart(next);
|
|
}
|
|
next();
|
|
}
|
|
|
|
command_NICKSERV(tags, prefix, params, next) {
|
|
if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', target: this.session.nick || '*', params: 'CAP', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
if (this.session.auth) {
|
|
this.send({
|
|
source: 'NickServ!NickServ@services.',
|
|
verb: 'NOTICE',
|
|
target: this.session.nick,
|
|
message: 'Already identified as ' + this.session.user
|
|
});
|
|
return next();
|
|
}
|
|
|
|
this.capStarted = true;
|
|
let subcommand = params
|
|
.shift()
|
|
.toUpperCase()
|
|
.trim();
|
|
|
|
switch (subcommand) {
|
|
case 'IDENTIFY': {
|
|
return this.authenticate(this.session.user, params.join(' '), (err, auth) => {
|
|
if (err) {
|
|
this.send('ERROR :Closing link: (' + this.getFormattedName(true) + ') [' + err.message + ']');
|
|
return this.close();
|
|
}
|
|
if (auth) {
|
|
this.session.auth = auth;
|
|
this.send({
|
|
source: 'NickServ!NickServ@services.',
|
|
verb: 'NOTICE',
|
|
target: this.session.nick,
|
|
message: 'You are now identified for ' + this.session.user
|
|
});
|
|
return this.verifyNickChange(false, () => this.initializeSubscriptions(next));
|
|
} else {
|
|
this.send({
|
|
source: 'NickServ!NickServ@services.',
|
|
verb: 'NOTICE',
|
|
target: this.session.nick,
|
|
message: 'Invalid password for ' + this.session.user
|
|
});
|
|
}
|
|
return next();
|
|
});
|
|
}
|
|
}
|
|
next();
|
|
}
|
|
|
|
command_AUTHENTICATE(tags, prefix, params, next) {
|
|
if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', target: this.session.nick || '*', params: 'AUTHENTICATE', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
if (!this.capEnabled.has('sasl')) {
|
|
// Authentication not enabled
|
|
return next();
|
|
}
|
|
|
|
if (this.session.auth) {
|
|
this.send({ verb: 'ERR_SASLALREADY', target: this.session.nick || '*', params: 'AUTHENTICATE', message: 'You have already authenticated' });
|
|
return next();
|
|
}
|
|
|
|
if (!this._authenticating) {
|
|
switch (params[0].trim().toUpperCase()) {
|
|
case 'PLAIN':
|
|
this.send('AUTHENTICATE +');
|
|
this._authenticating = true;
|
|
return next();
|
|
|
|
default:
|
|
this.send({ verb: 'RPL_SASLMECHS', target: this.session.nick || '*', params: 'PLAIN', message: 'are the available SASL mechanisms' });
|
|
this.send({ verb: 'ERR_SASLFAIL', target: this.session.nick || '*', message: 'SASL authentication failed' });
|
|
return next();
|
|
}
|
|
}
|
|
|
|
let auth = params[0].trim();
|
|
if (auth === '*') {
|
|
this._authenticating = false;
|
|
this.send({ verb: 'ERR_SASLABORTED', target: this.session.nick || '*', message: 'SASL authentication aborted' });
|
|
return next();
|
|
}
|
|
|
|
let parts = Buffer.from(auth, 'base64')
|
|
.toString()
|
|
.split('\x00');
|
|
|
|
//let nick = parts[0] || this.session.nick;
|
|
let user = parts[1] || this.session.nick;
|
|
let password = parts[2] || '';
|
|
|
|
this.authenticate(user, password, (err, auth) => {
|
|
this._authenticating = false;
|
|
if (err) {
|
|
this.send('ERROR :Closing link: (' + this.getFormattedName(true) + ') [' + err.message + ']');
|
|
return this.close();
|
|
}
|
|
if (auth) {
|
|
this.session.auth = auth;
|
|
this.send({
|
|
verb: 'RPL_LOGGEDIN',
|
|
target: this.session.nick || '*',
|
|
params: [this.getFormattedName(), this.session.user],
|
|
message: 'You are now logged in as ' + this.session.user
|
|
});
|
|
this.send({ verb: 'RPL_SASLSUCCESS', target: this.session.nick || '*', message: 'SASL authentication successful' });
|
|
return this.verifyNickChange(false, next);
|
|
} else {
|
|
this.send({ verb: 'ERR_SASLFAIL', target: this.session.nick || '*', message: 'SASL authentication failed' });
|
|
}
|
|
return next();
|
|
});
|
|
|
|
this._authenticating = false;
|
|
|
|
return next();
|
|
}
|
|
|
|
command_MODE(tags, prefix, params, next) {
|
|
if (!this.session.user || !this.session.nick) {
|
|
this.send({ verb: 'ERR_NOTREGISTERED', message: 'You have not registered' });
|
|
return next();
|
|
}
|
|
|
|
let channel = params[0].trim();
|
|
if (channel.length < 2 || !/^[#&]/.test(channel) || /[#&\s]/.test(channel.substr(1))) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' });
|
|
return next();
|
|
}
|
|
|
|
if (!this.checkAuth()) {
|
|
return next();
|
|
}
|
|
|
|
if (params.length > 1) {
|
|
this.send({ verb: 'ERR_CHANOPRIVSNEEDED', params: channel, message: 'You are not channel operator' });
|
|
return next();
|
|
}
|
|
|
|
db.database.collection('channels').findOne({
|
|
ns: this.session.ns,
|
|
channelview: channel.toLowerCase().replace(/\./g, '')
|
|
}, {
|
|
fields: {
|
|
_id: true,
|
|
mode: true,
|
|
time: true
|
|
}
|
|
}, (err, channelData) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message });
|
|
return next();
|
|
}
|
|
|
|
if (!channelData) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' });
|
|
return next();
|
|
}
|
|
|
|
let channelTime = Math.round((channelData.time || new Date()).getTime() / 1000);
|
|
|
|
this.send({ verb: 'RPL_CHANNELMODEIS', params: [channel, '+'] });
|
|
this.send({ verb: 'RPL_CREATIONTIME', params: [channel, channelTime] });
|
|
|
|
return next();
|
|
});
|
|
}
|
|
|
|
command_TOPIC(tags, prefix, params, next) {
|
|
if (!this.session.user || !this.session.nick) {
|
|
this.send({ verb: 'ERR_NOTREGISTERED', message: 'You have not registered' });
|
|
return next();
|
|
}
|
|
|
|
let channel = params[0].trim();
|
|
if (channel.length < 2 || !/^[#&]/.test(channel) || /[#&\s]/.test(channel.substr(1))) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' });
|
|
return next();
|
|
}
|
|
|
|
if (!this.checkAuth()) {
|
|
return next();
|
|
}
|
|
|
|
let newTopic = params
|
|
.slice(1)
|
|
.join(' ')
|
|
.trim();
|
|
|
|
if (params.length < 2) {
|
|
db.database.collection('channels').findOne({
|
|
ns: this.session.ns,
|
|
channelview: channel.toLowerCase().replace(/\./g, '')
|
|
}, {
|
|
fields: {
|
|
_id: true,
|
|
topic: true,
|
|
topicTime: true,
|
|
topicAuthor: true
|
|
}
|
|
}, (err, channelData) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message });
|
|
return next();
|
|
}
|
|
|
|
if (!channelData) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'No such channel' });
|
|
return next();
|
|
}
|
|
|
|
if (!channelData.topic) {
|
|
this.send({ verb: 'RPL_NOTOPIC', params: channel, message: 'No topic is set' });
|
|
return next();
|
|
}
|
|
|
|
let topicTime = Math.round((channelData.topicTime || new Date()).getTime() / 1000);
|
|
|
|
this.send({ verb: 'RPL_TOPIC', params: channel, message: channelData.topic });
|
|
this.send({ verb: 'RPL_TOPICWHOTIME', params: [channel, channelData.topicAuthor, topicTime] });
|
|
|
|
return next();
|
|
});
|
|
} else {
|
|
let topicTime = new Date();
|
|
let topicAuthor = this.getFormattedName();
|
|
|
|
return db.database.collection('channels').findOneAndUpdate({
|
|
ns: this.session.ns,
|
|
channelview: channel.toLowerCase().replace(/\./g, '')
|
|
}, {
|
|
$set: {
|
|
topic: newTopic,
|
|
topicAuthor,
|
|
topicTime
|
|
}
|
|
}, {
|
|
returnOriginal: false
|
|
}, (err, result) => {
|
|
if (err) {
|
|
this.send({ verb: 'ERR_FILEERROR', params: channel, message: err.message });
|
|
return next();
|
|
}
|
|
|
|
if (!result || !result.value) {
|
|
this.send({ verb: 'ERR_NOSUCHCHANNEL', params: channel, message: 'Could not open channel' });
|
|
return next();
|
|
}
|
|
|
|
let channelData = result.value;
|
|
|
|
this.publish([this.session.ns, '#', channelData._id].join('.'), {
|
|
action: 'topic',
|
|
channel: channelData.channel,
|
|
session: this.session.id.toString(),
|
|
channelId: channelData._id.toString(),
|
|
topic: newTopic,
|
|
topicAuthor,
|
|
topicTime
|
|
});
|
|
next();
|
|
});
|
|
}
|
|
}
|
|
|
|
command_OPER(tags, prefix, params, next) {
|
|
if (!this.session.user || !this.session.nick) {
|
|
this.send({ verb: 'ERR_NOTREGISTERED', message: 'You have not registered' });
|
|
return next();
|
|
}
|
|
|
|
if (!params.length) {
|
|
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'OPER', message: 'Not enough parameters' });
|
|
return next();
|
|
}
|
|
|
|
if (!this.checkAuth()) {
|
|
return next();
|
|
}
|
|
|
|
this.send({ verb: 'ERR_NOOPERHOST', message: 'No O-lines for your host' });
|
|
return next();
|
|
}
|
|
}
|
|
|
|
module.exports = IRCConnection;
|