Added experimental IRC server component

This commit is contained in:
Andris Reinman 2017-09-17 13:31:02 +03:00
parent 019ae9cef9
commit 9b54586e2a
14 changed files with 1830 additions and 37 deletions

View file

@ -66,6 +66,9 @@ bugsnagCode=""
[api]
# @include "api.toml"
[irc]
# @include "irc.toml"
[sender]
# @include "sender.toml"

25
config/irc.toml Normal file
View 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
View 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;
});
}
}

View file

@ -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'

View file

@ -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

View file

@ -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
View 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
View 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
View 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;

View file

@ -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();

View file

@ -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

View file

@ -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 || []);

View file

@ -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;

View file

@ -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);
}
}
}
});
});
});
});