mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-01-27 10:18:25 +08:00
Added experimental IRC server component
This commit is contained in:
parent
019ae9cef9
commit
9b54586e2a
14 changed files with 1830 additions and 37 deletions
|
@ -66,6 +66,9 @@ bugsnagCode=""
|
|||
[api]
|
||||
# @include "api.toml"
|
||||
|
||||
[irc]
|
||||
# @include "irc.toml"
|
||||
|
||||
[sender]
|
||||
# @include "sender.toml"
|
||||
|
||||
|
|
25
config/irc.toml
Normal file
25
config/irc.toml
Normal file
|
@ -0,0 +1,25 @@
|
|||
# If enabled then Wild Duck exposes an IRC interface for (authenticated) chat
|
||||
enabled=false # IRC server is not enabled by default
|
||||
port=6667
|
||||
# by default bind to localhost only
|
||||
host="127.0.0.1"
|
||||
|
||||
# Use `true` for port 6697 and `false` for 6667. Try to always use `true` on production
|
||||
secure=false
|
||||
|
||||
[motd]
|
||||
source="message" # "message" or "file"
|
||||
#file="/path/to/motd"
|
||||
message="Set MOTD message with the server config option \u0002irc.motd\u0002"
|
||||
|
||||
[tls]
|
||||
# If certificate path is not defined, use global or built-in self-signed certs
|
||||
#key="/path/to/server/key.pem"
|
||||
#cert="/path/to/server/cert.pem"
|
||||
|
||||
[setup]
|
||||
# Public configuration for IRC
|
||||
hostname="localhost"
|
||||
secure=true
|
||||
# port defaults to irc.port
|
||||
#port=6697
|
101
irc.js
Normal file
101
irc.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('wild-config');
|
||||
const log = require('npmlog');
|
||||
const IRCServer = require('./lib/irc/server');
|
||||
const UserHandler = require('./lib/user-handler');
|
||||
const MessageHandler = require('./lib/message-handler');
|
||||
const db = require('./lib/db');
|
||||
const fs = require('fs');
|
||||
const certs = require('./lib/certs').get('irc');
|
||||
|
||||
const serverOptions = {
|
||||
port: config.irc.port,
|
||||
host: config.irc.host,
|
||||
secure: config.irc.secure,
|
||||
|
||||
// log to console
|
||||
logger: {
|
||||
info(...args) {
|
||||
args.shift();
|
||||
log.info('IRC', ...args);
|
||||
},
|
||||
debug(...args) {
|
||||
args.shift();
|
||||
log.silly('IRC', ...args);
|
||||
},
|
||||
error(...args) {
|
||||
args.shift();
|
||||
log.error('IRC', ...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (certs) {
|
||||
serverOptions.key = certs.key;
|
||||
if (certs.ca) {
|
||||
serverOptions.ca = certs.ca;
|
||||
}
|
||||
serverOptions.cert = certs.cert;
|
||||
}
|
||||
|
||||
const server = new IRCServer(serverOptions);
|
||||
|
||||
config.on('reload', () => {
|
||||
// update message of the day
|
||||
updateMotd();
|
||||
});
|
||||
|
||||
updateMotd();
|
||||
|
||||
module.exports = done => {
|
||||
if (!config.irc.enabled) {
|
||||
return setImmediate(() => done(null, false));
|
||||
}
|
||||
|
||||
let started = false;
|
||||
|
||||
server.messageHandler = new MessageHandler({
|
||||
database: db.database,
|
||||
redis: db.redis,
|
||||
gridfs: db.gridfs,
|
||||
attachments: config.attachments
|
||||
});
|
||||
|
||||
server.userHandler = new UserHandler({
|
||||
database: db.database,
|
||||
users: db.users,
|
||||
redis: db.redis,
|
||||
authlogExpireDays: config.log.authlogExpireDays
|
||||
});
|
||||
|
||||
server.on('error', err => {
|
||||
if (!started) {
|
||||
started = true;
|
||||
return done(err);
|
||||
}
|
||||
log.error('IRC', err);
|
||||
});
|
||||
|
||||
server.listen(config.irc.port, config.irc.host, () => {
|
||||
if (started) {
|
||||
return server.close();
|
||||
}
|
||||
started = true;
|
||||
done(null, server);
|
||||
});
|
||||
};
|
||||
|
||||
function updateMotd() {
|
||||
if (config.irc.motd.source === 'message') {
|
||||
server.motd = config.irc.motd.message;
|
||||
} else if (config.irc.motd.source === 'file') {
|
||||
fs.readFile(config.irc.motd.file, 'utf-8', (err, motd) => {
|
||||
if (err) {
|
||||
log.error('IRC', 'Failed to realod MOTD. %s', err.message);
|
||||
return;
|
||||
}
|
||||
server.motd = motd;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,7 +11,11 @@ module.exports = (db, server, userHandler) => {
|
|||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string().hex().lowercase().length(24).required()
|
||||
user: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required()
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
|
@ -86,10 +90,25 @@ module.exports = (db, server, userHandler) => {
|
|||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string().hex().lowercase().length(24).required(),
|
||||
description: Joi.string().trim().max(255).required(),
|
||||
scopes: Joi.array().items(Joi.string().valid('imap', 'pop3', 'smtp', '*').required()).unique(),
|
||||
generateMobileconfig: Joi.boolean().truthy(['Y', 'true', 'yes', 1]).default(false),
|
||||
user: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
description: Joi.string()
|
||||
.trim()
|
||||
.max(255)
|
||||
.required(),
|
||||
scopes: Joi.array()
|
||||
.items(
|
||||
Joi.string()
|
||||
.valid('imap', 'pop3', 'smtp', 'irc', '*')
|
||||
.required()
|
||||
)
|
||||
.unique(),
|
||||
generateMobileconfig: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 1])
|
||||
.default(false),
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
cidr: 'forbidden'
|
||||
|
@ -97,7 +116,10 @@ module.exports = (db, server, userHandler) => {
|
|||
});
|
||||
|
||||
if (typeof req.params.scopes === 'string') {
|
||||
req.params.scopes = req.params.scopes.split(',').map(scope => scope.trim()).filter(scope => scope);
|
||||
req.params.scopes = req.params.scopes
|
||||
.split(',')
|
||||
.map(scope => scope.trim())
|
||||
.filter(scope => scope);
|
||||
}
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
|
@ -169,7 +191,10 @@ module.exports = (db, server, userHandler) => {
|
|||
|
||||
let profileOpts = {};
|
||||
Object.keys(config.api.mobileconfig || {}).forEach(key => {
|
||||
profileOpts[key] = (config.api.mobileconfig[key] || '').toString().replace(/\{email\}/g, userData.address).trim();
|
||||
profileOpts[key] = (config.api.mobileconfig[key] || '')
|
||||
.toString()
|
||||
.replace(/\{email\}/g, userData.address)
|
||||
.trim();
|
||||
});
|
||||
|
||||
let options = {
|
||||
|
@ -220,8 +245,16 @@ module.exports = (db, server, userHandler) => {
|
|||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string().hex().lowercase().length(24).required(),
|
||||
asp: Joi.string().hex().lowercase().length(24).required(),
|
||||
user: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
asp: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
cidr: 'forbidden'
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
const GridFSBucket = require('mongodb').GridFSBucket;
|
||||
const libbase64 = require('libbase64');
|
||||
|
||||
const FEATURE_DECODE_ATTACHMENTS = false;
|
||||
|
||||
class GridstoreStorage {
|
||||
constructor(options) {
|
||||
this.bucketName = (options.options && options.options.bucket) || 'attachments';
|
||||
|
@ -53,7 +55,7 @@ class GridstoreStorage {
|
|||
}
|
||||
});
|
||||
|
||||
if (attachment.transferEncoding === 'base64' && this.decodeBase64) {
|
||||
if (FEATURE_DECODE_ATTACHMENTS && attachment.transferEncoding === 'base64' && this.decodeBase64) {
|
||||
let lineLen = 0;
|
||||
let expectBr = false;
|
||||
//find out the length of first line
|
||||
|
|
|
@ -6,7 +6,7 @@ const fs = require('fs');
|
|||
const certs = new Map();
|
||||
|
||||
// load certificate files
|
||||
[false, 'imap', 'lmtp', 'pop3', 'api', 'api.mobileconfig'].forEach(type => {
|
||||
[false, 'imap', 'lmtp', 'pop3', 'api', 'irc', 'api.mobileconfig'].forEach(type => {
|
||||
let tlsconf = config.tls;
|
||||
|
||||
if (type) {
|
||||
|
|
493
lib/irc/codes.js
Normal file
493
lib/irc/codes.js
Normal file
|
@ -0,0 +1,493 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = new Map([
|
||||
['RPL_WELCOME', '001'],
|
||||
['RPL_YOURHOST', '002'],
|
||||
['RPL_CREATED', '003'],
|
||||
['RPL_MYINFO', '004'],
|
||||
['RPL_BOUNCE', '005'],
|
||||
['RPL_ISUPPORT', '005'],
|
||||
['RPL_MAP', '006'],
|
||||
['RPL_MAPEND', '007'],
|
||||
['RPL_SNOMASK', '008'],
|
||||
['RPL_STATMEMTOT', '009'],
|
||||
['RPL_BOUNCE', '010'],
|
||||
['RPL_STATMEM', '010'],
|
||||
['RPL_YOURCOOKIE', '014'],
|
||||
['RPL_MAP', '015'],
|
||||
['RPL_MAPMORE', '016'],
|
||||
['RPL_MAPEND', '017'],
|
||||
['RPL_YOURID', '042'],
|
||||
['RPL_SAVENICK', '043'],
|
||||
['RPL_ATTEMPTINGJUNC', '050'],
|
||||
['RPL_ATTEMPTINGREROUTE', '051'],
|
||||
['RPL_TRACELINK', '200'],
|
||||
['RPL_TRACECONNECTING', '201'],
|
||||
['RPL_TRACEHANDSHAKE', '202'],
|
||||
['RPL_TRACEUNKNOWN', '203'],
|
||||
['RPL_TRACEOPERATOR', '204'],
|
||||
['RPL_TRACEUSER', '205'],
|
||||
['RPL_TRACESERVER', '206'],
|
||||
['RPL_TRACESERVICE', '207'],
|
||||
['RPL_TRACENEWTYPE', '208'],
|
||||
['RPL_TRACECLASS', '209'],
|
||||
['RPL_TRACERECONNECT', '210'],
|
||||
['RPL_STATS', '210'],
|
||||
['RPL_STATSLINKINFO', '211'],
|
||||
['RPL_STATSCOMMANDS', '212'],
|
||||
['RPL_STATSCLINE', '213'],
|
||||
['RPL_STATSNLINE', '214'],
|
||||
['RPL_STATSILINE', '215'],
|
||||
['RPL_STATSKLINE', '216'],
|
||||
['RPL_STATSQLINE', '217'],
|
||||
['RPL_STATSPLINE', '217'],
|
||||
['RPL_STATSYLINE', '218'],
|
||||
['RPL_ENDOFSTATS', '219'],
|
||||
['RPL_STATSPLINE', '220'],
|
||||
['RPL_STATSBLINE', '220'],
|
||||
['RPL_UMODEIS', '221'],
|
||||
['RPL_MODLIST', '222'],
|
||||
['RPL_STATSBLINE', '222'],
|
||||
['RPL_STATSELINE', '223'],
|
||||
['RPL_STATSGLINE', '223'],
|
||||
['RPL_STATSFLINE', '224'],
|
||||
['RPL_STATSTLINE', '224'],
|
||||
['RPL_STATSDLINE', '225'],
|
||||
['RPL_STATSZLINE', '225'],
|
||||
['RPL_STATSELINE', '225'],
|
||||
['RPL_STATSCOUNT', '226'],
|
||||
['RPL_STATSNLINE', '226'],
|
||||
['RPL_STATSGLINE', '227'],
|
||||
['RPL_STATSVLINE', '227'],
|
||||
['RPL_STATSQLINE', '228'],
|
||||
['RPL_SERVICEINFO', '231'],
|
||||
['RPL_ENDOFSERVICES', '232'],
|
||||
['RPL_RULES', '232'],
|
||||
['RPL_SERVICE', '233'],
|
||||
['RPL_SERVLIST', '234'],
|
||||
['RPL_SERVLISTEND', '235'],
|
||||
['RPL_STATSVERBOSE', '236'],
|
||||
['RPL_STATSENGINE', '237'],
|
||||
['RPL_STATSFLINE', '238'],
|
||||
['RPL_STATSIAUTH', '239'],
|
||||
['RPL_STATSVLINE', '240'],
|
||||
['RPL_STATSXLINE', '240'],
|
||||
['RPL_STATSLLINE', '241'],
|
||||
['RPL_STATSUPTIME', '242'],
|
||||
['RPL_STATSOLINE', '243'],
|
||||
['RPL_STATSHLINE', '244'],
|
||||
['RPL_STATSSLINE', '245'],
|
||||
['RPL_STATSPING', '246'],
|
||||
['RPL_STATSTLINE', '246'],
|
||||
['RPL_STATSULINE', '246'],
|
||||
['RPL_STATSBLINE', '247'],
|
||||
['RPL_STATSXLINE', '247'],
|
||||
['RPL_STATSGLINE', '247'],
|
||||
['RPL_STATSULINE', '248'],
|
||||
['RPL_STATSDEFINE', '248'],
|
||||
['RPL_STATSULINE', '249'],
|
||||
['RPL_STATSDEBUG', '249'],
|
||||
['RPL_STATSDLINE', '250'],
|
||||
['RPL_STATSCONN', '250'],
|
||||
['RPL_LUSERCLIENT', '251'],
|
||||
['RPL_LUSEROP', '252'],
|
||||
['RPL_LUSERUNKNOWN', '253'],
|
||||
['RPL_LUSERCHANNELS', '254'],
|
||||
['RPL_LUSERME', '255'],
|
||||
['RPL_ADMINME', '256'],
|
||||
['RPL_ADMINLOC1', '257'],
|
||||
['RPL_ADMINLOC2', '258'],
|
||||
['RPL_ADMINEMAIL', '259'],
|
||||
['RPL_TRACELOG', '261'],
|
||||
['RPL_TRACEPING', '262'],
|
||||
['RPL_TRACEEND', '262'],
|
||||
['RPL_TRYAGAIN', '263'],
|
||||
['RPL_LOCALUSERS', '265'],
|
||||
['RPL_GLOBALUSERS', '266'],
|
||||
['RPL_START_NETSTAT', '267'],
|
||||
['RPL_NETSTAT', '268'],
|
||||
['RPL_END_NETSTAT', '269'],
|
||||
['RPL_PRIVS', '270'],
|
||||
['RPL_SILELIST', '271'],
|
||||
['RPL_ENDOFSILELIST', '272'],
|
||||
['RPL_NOTIFY', '273'],
|
||||
['RPL_ENDNOTIFY', '274'],
|
||||
['RPL_STATSDELTA', '274'],
|
||||
['RPL_STATSDLINE', '275'],
|
||||
['RPL_VCHANEXIST', '276'],
|
||||
['RPL_VCHANHELP', '278'],
|
||||
['RPL_ENDOFGLIST', '281'],
|
||||
['RPL_ACCEPTLIST', '281'],
|
||||
['RPL_JUPELIST', '282'],
|
||||
['RPL_ALIST', '283'],
|
||||
['RPL_ENDOFALIST', '284'],
|
||||
['RPL_GLIST_HASH', '285'],
|
||||
['RPL_NEWHOSTIS', '285'],
|
||||
['RPL_CHANINFO_USERS', '286'],
|
||||
['RPL_CHKHEAD', '286'],
|
||||
['RPL_CHANINFO_CHOPS', '287'],
|
||||
['RPL_CHANUSER', '287'],
|
||||
['RPL_CHANINFO_VOICES', '288'],
|
||||
['RPL_PATCHHEAD', '288'],
|
||||
['RPL_CHANINFO_AWAY', '289'],
|
||||
['RPL_PATCHCON', '289'],
|
||||
['RPL_CHANINFO_OPERS', '290'],
|
||||
['RPL_HELPHDR', '290'],
|
||||
['RPL_DATASTR', '290'],
|
||||
['RPL_CHANINFO_BANNED', '291'],
|
||||
['RPL_HELPOP', '291'],
|
||||
['RPL_ENDOFCHECK', '291'],
|
||||
['RPL_CHANINFO_BANS', '292'],
|
||||
['RPL_HELPTLR', '292'],
|
||||
['RPL_CHANINFO_INVITE', '293'],
|
||||
['RPL_HELPHLP', '293'],
|
||||
['RPL_CHANINFO_INVITES', '294'],
|
||||
['RPL_HELPFWD', '294'],
|
||||
['RPL_CHANINFO_KICK', '295'],
|
||||
['RPL_HELPIGN', '295'],
|
||||
['RPL_CHANINFO_KICKS', '296'],
|
||||
['RPL_END_CHANINFO', '299'],
|
||||
['RPL_NONE', '300'],
|
||||
['RPL_AWAY', '301'],
|
||||
['RPL_AWAY', '301'],
|
||||
['RPL_USERHOST', '302'],
|
||||
['RPL_ISON', '303'],
|
||||
['RPL_TEXT', '304'],
|
||||
['RPL_NOWAWAY', '306'],
|
||||
['RPL_USERIP', '307'],
|
||||
['RPL_SUSERHOST', '307'],
|
||||
['RPL_NOTIFYACTION', '308'],
|
||||
['RPL_WHOISADMIN', '308'],
|
||||
['RPL_RULESSTART', '308'],
|
||||
['RPL_NICKTRACE', '309'],
|
||||
['RPL_WHOISSADMIN', '309'],
|
||||
['RPL_ENDOFRULES', '309'],
|
||||
['RPL_WHOISHELPER', '309'],
|
||||
['RPL_WHOISSVCMSG', '310'],
|
||||
['RPL_WHOISHELPOP', '310'],
|
||||
['RPL_WHOISSERVICE', '310'],
|
||||
['RPL_WHOISUSER', '311'],
|
||||
['RPL_WHOISSERVER', '312'],
|
||||
['RPL_WHOISOPERATOR', '313'],
|
||||
['RPL_WHOWASUSER', '314'],
|
||||
['RPL_ENDOFWHO', '315'],
|
||||
['RPL_WHOISCHANOP', '316'],
|
||||
['RPL_WHOISIDLE', '317'],
|
||||
['RPL_ENDOFWHOIS', '318'],
|
||||
['RPL_WHOISCHANNELS', '319'],
|
||||
['RPL_WHOISVIRT', '320'],
|
||||
['RPL_WHOIS_HIDDEN', '320'],
|
||||
['RPL_WHOISSPECIAL', '320'],
|
||||
['RPL_LISTSTART', '321'],
|
||||
['RPL_LIST', '322'],
|
||||
['RPL_LISTEND', '323'],
|
||||
['RPL_CHANNELMODEIS', '324'],
|
||||
['RPL_UNIQOPIS', '325'],
|
||||
['RPL_CHANNELPASSIS', '325'],
|
||||
['RPL_CHPASSUNKNOWN', '327'],
|
||||
['RPL_CREATIONTIME', '329'],
|
||||
['RPL_WHOWAS_TIME', '330'],
|
||||
['RPL_NOTOPIC', '331'],
|
||||
['RPL_TOPIC', '332'],
|
||||
['RPL_TOPICWHOTIME', '333'],
|
||||
['RPL_LISTUSAGE', '334'],
|
||||
['RPL_COMMANDSYNTAX', '334'],
|
||||
['RPL_LISTSYNTAX', '334'],
|
||||
['RPL_WHOISBOT', '335'],
|
||||
['RPL_CHANPASSOK', '338'],
|
||||
['RPL_BADCHANPASS', '339'],
|
||||
['RPL_INVITING', '341'],
|
||||
['RPL_SUMMONING', '342'],
|
||||
['RPL_INVITED', '345'],
|
||||
['RPL_INVITELIST', '346'],
|
||||
['RPL_ENDOFINVITELIST', '347'],
|
||||
['RPL_EXCEPTLIST', '348'],
|
||||
['RPL_ENDOFEXCEPTLIST', '349'],
|
||||
['RPL_VERSION', '351'],
|
||||
['RPL_WHOREPLY', '352'],
|
||||
['RPL_NAMREPLY', '353'],
|
||||
['RPL_WHOSPCRPL', '354'],
|
||||
['RPL_NAMREPLY_', '355'],
|
||||
['RPL_MAP', '357'],
|
||||
['RPL_MAPMORE', '358'],
|
||||
['RPL_MAPEND', '359'],
|
||||
['RPL_KILLDONE', '361'],
|
||||
['RPL_CLOSING', '362'],
|
||||
['RPL_CLOSEEND', '363'],
|
||||
['RPL_LINKS', '364'],
|
||||
['RPL_ENDOFLINKS', '365'],
|
||||
['RPL_ENDOFNAMES', '366'],
|
||||
['RPL_BANLIST', '367'],
|
||||
['RPL_ENDOFBANLIST', '368'],
|
||||
['RPL_ENDOFWHOWAS', '369'],
|
||||
['RPL_INFO', '371'],
|
||||
['RPL_MOTD', '372'],
|
||||
['RPL_INFOSTART', '373'],
|
||||
['RPL_ENDOFINFO', '374'],
|
||||
['RPL_MOTDSTART', '375'],
|
||||
['RPL_ENDOFMOTD', '376'],
|
||||
['RPL_KICKEXPIRED', '377'],
|
||||
['RPL_SPAM', '377'],
|
||||
['RPL_BANEXPIRED', '378'],
|
||||
['RPL_WHOISHOST', '378'],
|
||||
['RPL_KICKLINKED', '379'],
|
||||
['RPL_WHOISMODES', '379'],
|
||||
['RPL_BANLINKED', '380'],
|
||||
['RPL_YOURHELPER', '380'],
|
||||
['RPL_YOUREOPER', '381'],
|
||||
['RPL_REHASHING', '382'],
|
||||
['RPL_YOURESERVICE', '383'],
|
||||
['RPL_MYPORTIS', '384'],
|
||||
['RPL_NOTOPERANYMORE', '385'],
|
||||
['RPL_QLIST', '386'],
|
||||
['RPL_IRCOPS', '386'],
|
||||
['RPL_ENDOFQLIST', '387'],
|
||||
['RPL_ENDOFIRCOPS', '387'],
|
||||
['RPL_ALIST', '388'],
|
||||
['RPL_ENDOFALIST', '389'],
|
||||
['RPL_TIME', '391'],
|
||||
['RPL_TIME', '391'],
|
||||
['RPL_TIME', '391'],
|
||||
['RPL_TIME', '391'],
|
||||
['RPL_USERSSTART', '392'],
|
||||
['RPL_USERS', '393'],
|
||||
['RPL_ENDOFUSERS', '394'],
|
||||
['RPL_NOUSERS', '395'],
|
||||
['RPL_HOSTHIDDEN', '396'],
|
||||
['ERR_UNKNOWNERROR', '400'],
|
||||
['ERR_NOSUCHNICK', '401'],
|
||||
['ERR_NOSUCHSERVER', '402'],
|
||||
['ERR_NOSUCHCHANNEL', '403'],
|
||||
['ERR_CANNOTSENDTOCHAN', '404'],
|
||||
['ERR_TOOMANYCHANNELS', '405'],
|
||||
['ERR_WASNOSUCHNICK', '406'],
|
||||
['ERR_TOOMANYTARGETS', '407'],
|
||||
['ERR_NOSUCHSERVICE', '408'],
|
||||
['ERR_NOCOLORSONCHAN', '408'],
|
||||
['ERR_NOORIGIN', '409'],
|
||||
['ERR_NORECIPIENT', '411'],
|
||||
['ERR_NOTEXTTOSEND', '412'],
|
||||
['ERR_NOTOPLEVEL', '413'],
|
||||
['ERR_WILDTOPLEVEL', '414'],
|
||||
['ERR_BADMASK', '415'],
|
||||
['ERR_TOOMANYMATCHES', '416'],
|
||||
['ERR_QUERYTOOLONG', '416'],
|
||||
['ERR_LENGTHTRUNCATED', '419'],
|
||||
['ERR_UNKNOWNCOMMAND', '421'],
|
||||
['ERR_NOMOTD', '422'],
|
||||
['ERR_NOADMININFO', '423'],
|
||||
['ERR_FILEERROR', '424'],
|
||||
['ERR_NOOPERMOTD', '425'],
|
||||
['ERR_TOOMANYAWAY', '429'],
|
||||
['ERR_EVENTNICKCHANGE', '430'],
|
||||
['ERR_NONICKNAMEGIVEN', '431'],
|
||||
['ERR_ERRONEUSNICKNAME', '432'],
|
||||
['ERR_NICKNAMEINUSE', '433'],
|
||||
['ERR_SERVICENAMEINUSE', '434'],
|
||||
['ERR_NORULES', '434'],
|
||||
['ERR_SERVICECONFUSED', '435'],
|
||||
['ERR_BANONCHAN', '435'],
|
||||
['ERR_NICKCOLLISION', '436'],
|
||||
['ERR_UNAVAILRESOURCE', '437'],
|
||||
['ERR_BANNICKCHANGE', '437'],
|
||||
['ERR_NICKTOOFAST', '438'],
|
||||
['ERR_DEAD', '438'],
|
||||
['ERR_TARGETTOOFAST', '439'],
|
||||
['ERR_SERVICESDOWN', '440'],
|
||||
['ERR_USERNOTINCHANNEL', '441'],
|
||||
['ERR_NOTONCHANNEL', '442'],
|
||||
['ERR_USERONCHANNEL', '443'],
|
||||
['ERR_NOLOGIN', '444'],
|
||||
['ERR_SUMMONDISABLED', '445'],
|
||||
['ERR_USERSDISABLED', '446'],
|
||||
['ERR_NONICKCHANGE', '447'],
|
||||
['ERR_NOTIMPLEMENTED', '449'],
|
||||
['ERR_NOTREGISTERED', '451'],
|
||||
['ERR_IDCOLLISION', '452'],
|
||||
['ERR_HOSTILENAME', '455'],
|
||||
['ERR_ACCEPTFULL', '456'],
|
||||
['ERR_ACCEPTNOT', '458'],
|
||||
['ERR_NOTFORHALFOPS', '460'],
|
||||
['ERR_NEEDMOREPARAMS', '461'],
|
||||
['ERR_ALREADYREGISTERED', '462'],
|
||||
['ERR_NOPERMFORHOST', '463'],
|
||||
['ERR_PASSWDMISMATCH', '464'],
|
||||
['ERR_YOUREBANNEDCREEP', '465'],
|
||||
['ERR_YOUWILLBEBANNED', '466'],
|
||||
['ERR_KEYSET', '467'],
|
||||
['ERR_INVALIDUSERNAME', '468'],
|
||||
['ERR_ONLYSERVERSCANCHANGE', '468'],
|
||||
['ERR_LINKSET', '469'],
|
||||
['ERR_LINKCHANNEL', '470'],
|
||||
['ERR_KICKEDFROMCHAN', '470'],
|
||||
['ERR_CHANNELISFULL', '471'],
|
||||
['ERR_UNKNOWNMODE', '472'],
|
||||
['ERR_INVITEONLYCHAN', '473'],
|
||||
['ERR_BANNEDFROMCHAN', '474'],
|
||||
['ERR_BADCHANNELKEY', '475'],
|
||||
['ERR_BADCHANMASK', '476'],
|
||||
['ERR_NOCHANMODES', '477'],
|
||||
['ERR_NEEDREGGEDNICK', '477'],
|
||||
['ERR_BANLISTFULL', '478'],
|
||||
['ERR_BADCHANNAME', '479'],
|
||||
['ERR_LINKFAIL', '479'],
|
||||
['ERR_NOULINE', '480'],
|
||||
['ERR_CANNOTKNOCK', '480'],
|
||||
['ERR_NOPRIVILEGES', '481'],
|
||||
['ERR_CHANOPRIVSNEEDED', '482'],
|
||||
['ERR_CANTKILLSERVER', '483'],
|
||||
['ERR_RESTRICTED', '484'],
|
||||
['ERR_ISCHANSERVICE', '484'],
|
||||
['ERR_DESYNC', '484'],
|
||||
['ERR_ATTACKDENY', '484'],
|
||||
['ERR_UNIQOPRIVSNEEDED', '485'],
|
||||
['ERR_KILLDENY', '485'],
|
||||
['ERR_CANTKICKADMIN', '485'],
|
||||
['ERR_ISREALSERVICE', '485'],
|
||||
['ERR_NONONREG', '486'],
|
||||
['ERR_ACCOUNTONLY', '486'],
|
||||
['ERR_CHANTOORECENT', '487'],
|
||||
['ERR_MSGSERVICES', '487'],
|
||||
['ERR_TSLESSCHAN', '488'],
|
||||
['ERR_VOICENEEDED', '489'],
|
||||
['ERR_SECUREONLYCHAN', '489'],
|
||||
['ERR_NOOPERHOST', '491'],
|
||||
['ERR_NOSERVICEHOST', '492'],
|
||||
['ERR_NOFEATURE', '493'],
|
||||
['ERR_BADFEATURE', '494'],
|
||||
['ERR_BADLOGTYPE', '495'],
|
||||
['ERR_BADLOGSYS', '496'],
|
||||
['ERR_BADLOGVALUE', '497'],
|
||||
['ERR_ISOPERLCHAN', '498'],
|
||||
['ERR_CHANOWNPRIVNEEDED', '499'],
|
||||
['ERR_UMODEUNKNOWNFLAG', '501'],
|
||||
['ERR_USERSDONTMATCH', '502'],
|
||||
['ERR_GHOSTEDCLIENT', '503'],
|
||||
['ERR_VWORLDWARN', '503'],
|
||||
['ERR_USERNOTONSERV', '504'],
|
||||
['ERR_TOOMANYWATCH', '512'],
|
||||
['ERR_BADPING', '513'],
|
||||
['ERR_INVALID_ERROR', '514'],
|
||||
['ERR_TOOMANYDCC', '514'],
|
||||
['ERR_BADEXPIRE', '515'],
|
||||
['ERR_DONTCHEAT', '516'],
|
||||
['ERR_DISABLED', '517'],
|
||||
['ERR_NOINVITE', '518'],
|
||||
['ERR_LONGMASK', '518'],
|
||||
['ERR_ADMONLY', '519'],
|
||||
['ERR_TOOMANYUSERS', '519'],
|
||||
['ERR_OPERONLY', '520'],
|
||||
['ERR_MASKTOOWIDE', '520'],
|
||||
['ERR_WHOTRUNC', '520'],
|
||||
['ERR_LISTSYNTAX', '521'],
|
||||
['ERR_WHOSYNTAX', '522'],
|
||||
['ERR_WHOLIMEXCEED', '523'],
|
||||
['ERR_QUARANTINED', '524'],
|
||||
['ERR_OPERSPVERIFY', '524'],
|
||||
['ERR_REMOTEPFX', '525'],
|
||||
['ERR_PFXUNROUTABLE', '526'],
|
||||
['ERR_BADHOSTMASK', '550'],
|
||||
['ERR_HOSTUNAVAIL', '551'],
|
||||
['ERR_USINGSLINE', '552'],
|
||||
['ERR_STATSSLINE', '553'],
|
||||
['RPL_LOGON', '600'],
|
||||
['RPL_LOGOFF', '601'],
|
||||
['RPL_WATCHOFF', '602'],
|
||||
['RPL_WATCHSTAT', '603'],
|
||||
['RPL_NOWON', '604'],
|
||||
['RPL_NOWOFF', '605'],
|
||||
['RPL_WATCHLIST', '606'],
|
||||
['RPL_ENDOFWATCHLIST', '607'],
|
||||
['RPL_WATCHCLEAR', '608'],
|
||||
['RPL_MAPMORE', '610'],
|
||||
['RPL_ISOPER', '610'],
|
||||
['RPL_ISLOCOP', '611'],
|
||||
['RPL_ISNOTOPER', '612'],
|
||||
['RPL_ENDOFISOPER', '613'],
|
||||
['RPL_MAPMORE', '615'],
|
||||
['RPL_WHOISMODES', '615'],
|
||||
['RPL_WHOISHOST', '616'],
|
||||
['RPL_DCCSTATUS', '617'],
|
||||
['RPL_WHOISBOT', '617'],
|
||||
['RPL_DCCLIST', '618'],
|
||||
['RPL_ENDOFDCCLIST', '619'],
|
||||
['RPL_WHOWASHOST', '619'],
|
||||
['RPL_DCCINFO', '620'],
|
||||
['RPL_RULESSTART', '620'],
|
||||
['RPL_RULES', '621'],
|
||||
['RPL_ENDOFRULES', '622'],
|
||||
['RPL_MAPMORE', '623'],
|
||||
['RPL_OMOTDSTART', '624'],
|
||||
['RPL_OMOTD', '625'],
|
||||
['RPL_ENDOFO', '626'],
|
||||
['RPL_SETTINGS', '630'],
|
||||
['RPL_ENDOFSETTINGS', '631'],
|
||||
['RPL_DUMPING', '640'],
|
||||
['RPL_DUMPRPL', '641'],
|
||||
['RPL_EODUMP', '642'],
|
||||
['RPL_TRACEROUTE_HOP', '660'],
|
||||
['RPL_TRACEROUTE_START', '661'],
|
||||
['RPL_MODECHANGEWARN', '662'],
|
||||
['RPL_CHANREDIR', '663'],
|
||||
['RPL_SERVMODEIS', '664'],
|
||||
['RPL_OTHERUMODEIS', '665'],
|
||||
['RPL_ENDOF_GENERIC', '666'],
|
||||
['RPL_WHOWASDETAILS', '670'],
|
||||
['RPL_WHOISSECURE', '671'],
|
||||
['RPL_UNKNOWNMODES', '672'],
|
||||
['RPL_CANNOTSETMODES', '673'],
|
||||
['RPL_LUSERSTAFF', '678'],
|
||||
['RPL_TIMEONSERVERIS', '679'],
|
||||
['RPL_NETWORKS', '682'],
|
||||
['RPL_YOURLANGUAGEIS', '687'],
|
||||
['RPL_LANGUAGE', '688'],
|
||||
['RPL_WHOISSTAFF', '689'],
|
||||
['RPL_WHOISLANGUAGE', '690'],
|
||||
['RPL_MODLIST', '702'],
|
||||
['RPL_ENDOFMODLIST', '703'],
|
||||
['RPL_HELPSTART', '704'],
|
||||
['RPL_HELPTXT', '705'],
|
||||
['RPL_ENDOFHELP', '706'],
|
||||
['RPL_ETRACEFULL', '708'],
|
||||
['RPL_ETRACE', '709'],
|
||||
['RPL_KNOCK', '710'],
|
||||
['RPL_KNOCKDLVR', '711'],
|
||||
['ERR_TOOMANYKNOCK', '712'],
|
||||
['ERR_CHANOPEN', '713'],
|
||||
['ERR_KNOCKONCHAN', '714'],
|
||||
['ERR_KNOCKDISABLED', '715'],
|
||||
['RPL_TARGUMODEG', '716'],
|
||||
['RPL_TARGNOTIFY', '717'],
|
||||
['RPL_UMODEGMSG', '718'],
|
||||
['RPL_OMOTDSTART', '720'],
|
||||
['RPL_OMOTD', '721'],
|
||||
['RPL_ENDOFOMOTD', '722'],
|
||||
['ERR_NOPRIVS', '723'],
|
||||
['RPL_TESTMARK', '724'],
|
||||
['RPL_TESTLINE', '725'],
|
||||
['RPL_NOTESTLINE', '726'],
|
||||
['RPL_XINFO', '771'],
|
||||
['RPL_XINFOSTART', '773'],
|
||||
['RPL_XINFOEND', '774'],
|
||||
['RPL_LOGGEDIN', '900'],
|
||||
['RPL_LOGGEDOUT', '901'],
|
||||
['ERR_NICKLOCKED', '902'],
|
||||
['RPL_SASLSUCCESS', '903'],
|
||||
['ERR_SASLFAIL', '904'],
|
||||
['ERR_SASLTOOLONG', '905'],
|
||||
['ERR_SASLABORTED', '906'],
|
||||
['ERR_SASLALREADY', '907'],
|
||||
['RPL_SASLMECHS', '908'],
|
||||
['ERR_CANNOTDOCOMMAND', '972'],
|
||||
['ERR_CANNOTCHANGEUMODE', '973'],
|
||||
['ERR_CANNOTCHANGECHANMODE', '974'],
|
||||
['ERR_CANNOTCHANGESERVERMODE', '975'],
|
||||
['ERR_CANNOTSENDTONICK', '976'],
|
||||
['ERR_UNKNOWNSERVERMODE', '977'],
|
||||
['ERR_SERVERMODELOCK', '979'],
|
||||
['ERR_BADCHARENCODING', '980'],
|
||||
['ERR_TOOMANYLANGUAGES', '981'],
|
||||
['ERR_NOLANGUAGE', '982'],
|
||||
['ERR_TEXTTOOSHORT', '983'],
|
||||
['ERR_NUMERIC_ERR', '999']
|
||||
]);
|
821
lib/irc/connection.js
Normal file
821
lib/irc/connection.js
Normal file
|
@ -0,0 +1,821 @@
|
|||
'use strict';
|
||||
|
||||
const dns = require('dns');
|
||||
const crypto = require('crypto');
|
||||
const EventEmitter = require('events');
|
||||
const os = require('os');
|
||||
const codes = require('./codes');
|
||||
|
||||
const PING_TIMEOUT = 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.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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
send(payload) {
|
||||
if (!this._socket || !this._socket.writable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload && typeof payload === 'object') {
|
||||
payload.source = payload.source || this.hostname;
|
||||
|
||||
let message = [':' + payload.source];
|
||||
|
||||
if (payload.verb) {
|
||||
let cmd = (payload.verb || '')
|
||||
.toString()
|
||||
.toUpperCase()
|
||||
.trim();
|
||||
if (codes.has(cmd)) {
|
||||
cmd = codes.get(cmd);
|
||||
}
|
||||
message.push(cmd);
|
||||
}
|
||||
|
||||
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) {
|
||||
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 */) {
|
||||
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 (err.code === 'ECONNRESET' || err.code === 'EPIPE') {
|
||||
return this.close(); // mark connection as 'closing'
|
||||
}
|
||||
|
||||
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 = {
|
||||
state: 'AUTHORIZATION',
|
||||
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*/);
|
||||
if (!match) {
|
||||
// TODO: send error message
|
||||
// Can it even happen?
|
||||
return this.processQueue();
|
||||
}
|
||||
|
||||
let verb = (match[1] || '').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) {
|
||||
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](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();
|
||||
}
|
||||
}
|
||||
|
||||
getFormattedName(skipNick) {
|
||||
return (!skipNick && this.session.nick ? this.session.nick + '!' : '') + (this.session.user || 'unknown') + '@' + this.session.clientHostname;
|
||||
}
|
||||
|
||||
checkSessionStart() {
|
||||
if (this.starting || this.started) {
|
||||
return;
|
||||
}
|
||||
if (!this.session.user || !this.session.nick || (this.capStarted && !this.capEnded)) {
|
||||
return;
|
||||
}
|
||||
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' });
|
||||
|
||||
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>'
|
||||
});
|
||||
}
|
||||
|
||||
this.starting = false;
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
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.user = result.username;
|
||||
|
||||
next(null, {
|
||||
id: result.user,
|
||||
username: result.username
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
checkAuth() {
|
||||
if (!this.session.auth) {
|
||||
this.send({ verb: 'ERR_NOTREGISTERED', params: 'PRIVMSG', 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.server.quit(this, '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.server.quit(this, 'Client exited');
|
||||
this.close();
|
||||
}
|
||||
|
||||
command_PING(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', params: 'PING', 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(params, next) {
|
||||
return next();
|
||||
}
|
||||
|
||||
command_NICK(params, next) {
|
||||
if (params.length > 1) {
|
||||
this.send({ verb: 'ERR_ERRONEUSNICKNAME', params, message: 'Erroneous Nickname' });
|
||||
} else if (!params.length) {
|
||||
this.send({ verb: 'ERR_NEEDMOREPARAMS', params, message: 'Not enough parameters' });
|
||||
} else if (this.server.disabledNicks.includes(params[0].trim().toLowerCase())) {
|
||||
this.send({ verb: 'ERR_ERRONEUSNICKNAME', params, message: 'Erroneous Nickname' });
|
||||
} else {
|
||||
this.session.nick = params[0];
|
||||
}
|
||||
this.checkSessionStart();
|
||||
this.server.nick(this);
|
||||
return next();
|
||||
}
|
||||
|
||||
command_PASS(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(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) {
|
||||
this.server.quit(this, 'User registration failed. ' + err.message);
|
||||
return this.close();
|
||||
}
|
||||
if (auth) {
|
||||
this.session.auth = auth;
|
||||
this.checkSessionStart();
|
||||
}
|
||||
return next();
|
||||
});
|
||||
} else {
|
||||
this.checkSessionStart();
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
command_JOIN(params, next) {
|
||||
if (!this.session.user || !this.session.nick) {
|
||||
this.send({ verb: 'ERR_NOTREGISTERED', params: 'JOIN', message: 'You have not registered' });
|
||||
return next();
|
||||
}
|
||||
if (!params.length) {
|
||||
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'JOIN', message: 'Not enough parameters' });
|
||||
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: 'Invalid channel name' });
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!this.checkAuth()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
this.server.join(channel, this);
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
command_PART(params, next) {
|
||||
if (!this.session.user || !this.session.nick) {
|
||||
this.send({ verb: 'ERR_NOTREGISTERED', params: 'JOIN', message: 'You have not registered' });
|
||||
return next();
|
||||
}
|
||||
if (!params.length) {
|
||||
this.send({ verb: 'ERR_NEEDMOREPARAMS', params: 'JOIN', message: 'Not enough parameters' });
|
||||
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();
|
||||
}
|
||||
|
||||
this.server.leave(channel, this, params.slice(1).join(' '));
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
command_PRIVMSG(params, next) {
|
||||
if (!this.session.user || !this.session.nick) {
|
||||
this.send({ verb: 'ERR_NOTREGISTERED', params: 'PRIVMSG', 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(
|
||||
params
|
||||
.slice(1)
|
||||
.join(' ')
|
||||
.split(/\s+/)
|
||||
.filter(arg => arg),
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.checkAuth()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
this.server.send(target, this, params.slice(1).join(' '));
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
command_CAP(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'];
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
this.checkSessionStart();
|
||||
break;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
command_NICKSERV(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.server.quit(this, 'User registration failed. ' + 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
|
||||
});
|
||||
} 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(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.server.quit(this, 'User registration failed. ' + 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' });
|
||||
} else {
|
||||
this.send({ verb: 'ERR_SASLFAIL', target: this.session.nick || '*', message: 'SASL authentication failed' });
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
this._authenticating = false;
|
||||
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IRCConnection;
|
303
lib/irc/server.js
Normal file
303
lib/irc/server.js
Normal file
|
@ -0,0 +1,303 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const packageData = require('../../package.json');
|
||||
const tlsOptions = require('../../imap-core/lib/tls-options');
|
||||
const shared = require('nodemailer/lib/shared');
|
||||
const IRCConnection = require('./connection');
|
||||
|
||||
const CLOSE_TIMEOUT = 1 * 1000; // how much to wait until pending connections are terminated
|
||||
|
||||
class IRCServer extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.version = 'WildDuck-v' + packageData.version;
|
||||
|
||||
this.startTime = new Date();
|
||||
let dateparts = this.startTime.toUTCString().split(/[\s,]+/);
|
||||
dateparts.splice(1, 0, dateparts[2]);
|
||||
dateparts.splice(3, 1);
|
||||
dateparts.splice(4, 0, 'at');
|
||||
this.startTimeFormatted = dateparts.join(' ');
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
this.name = this.options.name || 'Localnet';
|
||||
|
||||
this.motd = 'Wild Duck IRC'; // is changed later
|
||||
|
||||
this.messageHandler = false; // is set later
|
||||
this.userHandler = false; // is set later
|
||||
|
||||
this.disabledNicks = ['admin', 'root', 'nickserv'];
|
||||
|
||||
this.supported = {
|
||||
CASEMAPPING: 'rfc7613',
|
||||
CHANTYPES: '#&',
|
||||
NETWORK: this.name,
|
||||
FNC: true
|
||||
};
|
||||
|
||||
this.supportedFormatted = Object.keys(this.supported).map(key => key.toUpperCase() + (this.supported[key] === true ? '' : '=' + this.supported[key]));
|
||||
|
||||
this._channels = new Map();
|
||||
this._clients = new Map();
|
||||
this._nicks = new Map();
|
||||
|
||||
/**
|
||||
* Timeout after close has been called until pending connections are forcibly closed
|
||||
*/
|
||||
this._closeTimeout = false;
|
||||
|
||||
/**
|
||||
* A set of all currently open connections
|
||||
*/
|
||||
this.connections = new Set();
|
||||
|
||||
// apply TLS defaults if needed
|
||||
if (this.options.secure) {
|
||||
this.options = tlsOptions(this.options);
|
||||
}
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'irc-server'
|
||||
});
|
||||
|
||||
this.server = (this.options.secure ? tls : net).createServer(this.options, socket => this._onConnect(socket));
|
||||
|
||||
this._setListeners();
|
||||
}
|
||||
|
||||
_setListeners() {
|
||||
this.server.on('listening', () => this._onListening());
|
||||
this.server.on('close', () => this._onClose());
|
||||
this.server.on('error', err => this._onError(err));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when server started listening
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onListening() {
|
||||
let address = this.server.address();
|
||||
this.logger.info(
|
||||
//
|
||||
{
|
||||
tnx: 'listen',
|
||||
host: address.address,
|
||||
port: address.port,
|
||||
secure: !!this.options.secure,
|
||||
protocol: 'IRC'
|
||||
},
|
||||
'%s%s Server listening on %s:%s',
|
||||
this.options.secure ? 'Secure ' : '',
|
||||
'IRC',
|
||||
address.family === 'IPv4' ? address.address : '[' + address.address + ']',
|
||||
address.port
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when server is closed
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onClose() {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'closed'
|
||||
},
|
||||
'IRC Server closed'
|
||||
);
|
||||
this.emit('close');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an error occurs with the server
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onError(err) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
_onConnect(socket) {
|
||||
let connection = new IRCConnection(this, socket);
|
||||
this.connections.add(connection);
|
||||
connection.once('error', err => {
|
||||
this.connections.delete(connection);
|
||||
this._onError(err);
|
||||
});
|
||||
connection.once('close', () => {
|
||||
this.connections.delete(connection);
|
||||
});
|
||||
connection.init();
|
||||
}
|
||||
|
||||
close(callback) {
|
||||
let connections = this.connections.size;
|
||||
let timeout = this.options.closeTimeout || CLOSE_TIMEOUT;
|
||||
|
||||
// stop accepting new connections
|
||||
this.server.close(() => {
|
||||
clearTimeout(this._closeTimeout);
|
||||
if (typeof callback === 'function') {
|
||||
return callback();
|
||||
}
|
||||
});
|
||||
|
||||
// close active connections
|
||||
if (connections) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'close'
|
||||
},
|
||||
'Server closing with %s pending connection%s, waiting %s seconds before terminating',
|
||||
connections,
|
||||
connections !== 1 ? 's' : '',
|
||||
timeout / 1000
|
||||
);
|
||||
}
|
||||
|
||||
this._closeTimeout = setTimeout(() => {
|
||||
connections = this.connections.size;
|
||||
if (connections) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'close'
|
||||
},
|
||||
'Closing %s pending connection%s to close the server',
|
||||
connections,
|
||||
connections !== 1 ? 's' : ''
|
||||
);
|
||||
|
||||
this.connections.forEach(connection => {
|
||||
connection.close();
|
||||
});
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
listen(...args) {
|
||||
this.server.listen(...args);
|
||||
}
|
||||
|
||||
join(name, client) {
|
||||
let nameLC = name.toLowerCase();
|
||||
if (!this._channels.has(nameLC)) {
|
||||
this._channels.set(nameLC, {
|
||||
name,
|
||||
topic: 'unset',
|
||||
clients: new Set()
|
||||
});
|
||||
}
|
||||
let channel = this._channels.get(nameLC);
|
||||
if (!channel.clients.has(client)) {
|
||||
channel.clients.add(client);
|
||||
let clientName = client.getFormattedName();
|
||||
let names = [];
|
||||
channel.clients.forEach(c => {
|
||||
c.send({ source: clientName, verb: 'JOIN', target: false, message: name });
|
||||
names.push(c.session.nick);
|
||||
});
|
||||
client.send({ verb: 'RPL_NAMREPLY', params: ['=', name], message: names.join(' ') });
|
||||
client.send({ verb: 'RPL_ENDOFNAMES', params: name, message: 'End of /NAMES list' });
|
||||
if (!this._clients.get(client)) {
|
||||
this._clients.set(client, { nick: client.session.nick, channels: new Set([channel]) });
|
||||
} else {
|
||||
this._clients.get(client).channels.add(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quit(client, message) {
|
||||
if (!this._clients.has(client)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let clientName = client.getFormattedName();
|
||||
this._clients.get(client).channels.forEach(channel => {
|
||||
if (channel.clients.has(client)) {
|
||||
channel.clients.forEach(c => {
|
||||
c.send({ source: clientName, verb: 'QUIT', target: false, message });
|
||||
});
|
||||
channel.clients.delete(client);
|
||||
}
|
||||
});
|
||||
|
||||
this._clients.delete(client);
|
||||
this._nicks.delete(client.session.nick);
|
||||
}
|
||||
|
||||
leave(name, client, message) {
|
||||
let nameLC = name.toLowerCase();
|
||||
if (!this._channels.has(nameLC)) {
|
||||
client.send({ verb: 'ERR_NOSUCHNICK', params: name, message: 'No such channel' });
|
||||
return;
|
||||
}
|
||||
let channel = this._channels.get(nameLC);
|
||||
if (channel.clients.has(client)) {
|
||||
let clientName = client.getFormattedName();
|
||||
channel.clients.forEach(c => {
|
||||
c.send({ source: clientName, verb: 'PART', target: false, params: channel.name, message });
|
||||
});
|
||||
channel.clients.delete(client);
|
||||
this._clients.get(client).channels.delete(channel);
|
||||
}
|
||||
}
|
||||
|
||||
nick(client) {
|
||||
if (!this._clients.has(client)) {
|
||||
this._clients.set(client, { nick: client.session.nick, channels: new Set() });
|
||||
this._nicks.set(client.session.nick.toLowerCase(), client);
|
||||
return;
|
||||
}
|
||||
let entry = this._clients.get(client);
|
||||
if (entry.nick !== client.session.nick) {
|
||||
let updated = new WeakSet();
|
||||
let clientName = entry.nick + '!' + client.getFormattedName(true);
|
||||
entry.channels.forEach(channel => {
|
||||
channel.clients.forEach(c => {
|
||||
if (updated.has(c)) {
|
||||
return;
|
||||
}
|
||||
updated.add(c);
|
||||
c.send({ source: clientName, verb: 'NICK', target: false, params: client.session.nick });
|
||||
});
|
||||
});
|
||||
this._nicks.delete(entry.nick.toLowerCase());
|
||||
}
|
||||
|
||||
entry.nick = client.session.nick;
|
||||
this._nicks.set(client.session.nick.toLowerCase(), client);
|
||||
}
|
||||
|
||||
send(target, client, message) {
|
||||
let nameLC = target.toLowerCase();
|
||||
let clientName = client.getFormattedName();
|
||||
if (/^[#&]/.test(target)) {
|
||||
if (!this._channels.has(nameLC)) {
|
||||
client.send({ verb: 'ERR_NOSUCHNICK', params: target, message: 'No such nick/channel' });
|
||||
return;
|
||||
}
|
||||
let channel = this._channels.get(nameLC);
|
||||
channel.clients.forEach(c => {
|
||||
if (c !== client) {
|
||||
c.send({ source: clientName, target, verb: 'PRIVMSG', message });
|
||||
}
|
||||
});
|
||||
} else if (this._nicks.has(nameLC)) {
|
||||
let client = this._nicks.get(nameLC);
|
||||
client.send({ source: clientName, verb: 'PRIVMSG', message });
|
||||
} else {
|
||||
client.send({ verb: 'ERR_NOSUCHNICK', params: target, message: 'No such nick/channel' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IRCServer;
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const crypto = require('crypto');
|
||||
const EventEmitter = require('events');
|
||||
const packageData = require('../package.json');
|
||||
const packageData = require('../../package.json');
|
||||
const DataStream = require('nodemailer/lib/smtp-connection/data-stream');
|
||||
|
||||
const SOCKET_TIMEOUT = 60 * 1000;
|
||||
|
@ -756,7 +756,9 @@ class POP3Connection extends EventEmitter {
|
|||
return next();
|
||||
}
|
||||
|
||||
let credentials = Buffer.from(plain, 'base64').toString().split('\x00');
|
||||
let credentials = Buffer.from(plain, 'base64')
|
||||
.toString()
|
||||
.split('\x00');
|
||||
if (credentials.length !== 3) {
|
||||
this.send('-ERR malformed command');
|
||||
return next();
|
|
@ -3,9 +3,9 @@
|
|||
const EventEmitter = require('events');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const tlsOptions = require('../imap-core/lib/tls-options');
|
||||
const tlsOptions = require('../../imap-core/lib/tls-options');
|
||||
const shared = require('nodemailer/lib/shared');
|
||||
const POP3Connection = require('./pop3-connection');
|
||||
const POP3Connection = require('./connection');
|
||||
|
||||
const CLOSE_TIMEOUT = 1 * 1000; // how much to wait until pending connections are terminated
|
||||
|
|
@ -347,7 +347,7 @@ class UserHandler {
|
|||
.update(password.substr(0, 4))
|
||||
.digest('hex');
|
||||
|
||||
let allowedScopes = ['imap', 'pop3', 'smtp'];
|
||||
let allowedScopes = ['imap', 'pop3', 'smtp', 'irc'];
|
||||
let hasAllScopes = false;
|
||||
let scopeSet = new Set();
|
||||
let scopes = [].concat(data.scopes || []);
|
||||
|
|
2
pop3.js
2
pop3.js
|
@ -2,7 +2,7 @@
|
|||
|
||||
const config = require('wild-config');
|
||||
const log = require('npmlog');
|
||||
const POP3Server = require('./lib/pop3-server');
|
||||
const POP3Server = require('./lib/pop3/server');
|
||||
const UserHandler = require('./lib/user-handler');
|
||||
const MessageHandler = require('./lib/message-handler');
|
||||
const ObjectID = require('mongodb').ObjectID;
|
||||
|
|
50
worker.js
50
worker.js
|
@ -4,6 +4,7 @@ const config = require('wild-config');
|
|||
const log = require('npmlog');
|
||||
const imap = require('./imap');
|
||||
const pop3 = require('./pop3');
|
||||
const irc = require('./irc');
|
||||
const lmtp = require('./lmtp');
|
||||
const api = require('./api');
|
||||
const db = require('./lib/db');
|
||||
|
@ -49,29 +50,38 @@ db.connect(err => {
|
|||
return setTimeout(() => process.exit(1), 3000);
|
||||
}
|
||||
|
||||
log.info('App', 'All servers started, ready to process some mail');
|
||||
// Start IRC server
|
||||
irc(err => {
|
||||
if (err) {
|
||||
log.error('App', 'Failed to start IRC server');
|
||||
errors.notify(err);
|
||||
return setTimeout(() => process.exit(1), 3000);
|
||||
}
|
||||
|
||||
// downgrade user and group if needed
|
||||
if (config.group) {
|
||||
try {
|
||||
process.setgid(config.group);
|
||||
log.info('App', 'Changed group to "%s" (%s)', config.group, process.getgid());
|
||||
} catch (E) {
|
||||
log.error('App', 'Failed to change group to "%s" (%s)', config.group, E.message);
|
||||
errors.notify(E);
|
||||
return setTimeout(() => process.exit(1), 3000);
|
||||
log.info('App', 'All servers started, ready to process some mail');
|
||||
|
||||
// downgrade user and group if needed
|
||||
if (config.group) {
|
||||
try {
|
||||
process.setgid(config.group);
|
||||
log.info('App', 'Changed group to "%s" (%s)', config.group, process.getgid());
|
||||
} catch (E) {
|
||||
log.error('App', 'Failed to change group to "%s" (%s)', config.group, E.message);
|
||||
errors.notify(E);
|
||||
return setTimeout(() => process.exit(1), 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.user) {
|
||||
try {
|
||||
process.setuid(config.user);
|
||||
log.info('App', 'Changed user to "%s" (%s)', config.user, process.getuid());
|
||||
} catch (E) {
|
||||
log.error('App', 'Failed to change user to "%s" (%s)', config.user, E.message);
|
||||
errors.notify(E);
|
||||
return setTimeout(() => process.exit(1), 3000);
|
||||
if (config.user) {
|
||||
try {
|
||||
process.setuid(config.user);
|
||||
log.info('App', 'Changed user to "%s" (%s)', config.user, process.getuid());
|
||||
} catch (E) {
|
||||
log.error('App', 'Failed to change user to "%s" (%s)', config.user, E.message);
|
||||
errors.notify(E);
|
||||
return setTimeout(() => process.exit(1), 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue