2016-10-09 19:14:02 +00:00
"use strict" ;
2018-01-11 11:33:36 +00:00
const _ = require ( "lodash" ) ;
2020-02-24 13:35:15 +00:00
const { v4 : uuidv4 } = require ( "uuid" ) ;
2018-03-15 08:37:05 +00:00
const IrcFramework = require ( "irc-framework" ) ;
2018-01-11 11:33:36 +00:00
const Chan = require ( "./chan" ) ;
2018-03-15 08:37:05 +00:00
const Msg = require ( "./msg" ) ;
const Helper = require ( "../helper" ) ;
2020-02-19 11:20:22 +00:00
const STSPolicies = require ( "../plugins/sts" ) ;
2020-03-30 20:15:32 +00:00
const ClientCertificate = require ( "../plugins/clientCertificate" ) ;
2014-09-13 21:29:45 +00:00
module . exports = Network ;
2017-11-29 19:54:09 +00:00
/ * *
2019-11-02 18:45:00 +00:00
* @ type { Object } List of keys which should be sent to the client by default .
2017-11-29 19:54:09 +00:00
* /
2019-11-02 18:45:00 +00:00
const fieldsForClient = {
uuid : true ,
name : true ,
nick : true ,
serverOptions : true ,
2017-11-29 19:54:09 +00:00
} ;
2014-09-13 21:29:45 +00:00
function Network ( attr ) {
2016-10-02 07:37:37 +00:00
_ . defaults ( this , attr , {
2014-10-11 20:44:56 +00:00
name : "" ,
2019-11-02 18:45:00 +00:00
nick : "" ,
2014-10-11 20:44:56 +00:00
host : "" ,
port : 6667 ,
tls : false ,
2018-08-25 09:11:59 +00:00
userDisconnected : false ,
2018-02-17 08:22:28 +00:00
rejectUnauthorized : false ,
2014-10-11 20:44:56 +00:00
password : "" ,
2016-12-18 09:24:50 +00:00
awayMessage : "" ,
2014-11-09 16:01:22 +00:00
commands : [ ] ,
2014-10-11 20:44:56 +00:00
username : "" ,
realname : "" ,
2020-03-31 08:02:18 +00:00
sasl : "" ,
saslAccount : "" ,
saslPassword : "" ,
2014-09-13 21:29:45 +00:00
channels : [ ] ,
irc : null ,
2016-03-08 18:50:48 +00:00
serverOptions : {
2018-10-13 10:54:32 +00:00
CHANTYPES : [ "#" , "&" ] ,
PREFIX : [ "!" , "@" , "%" , "+" ] ,
2017-04-16 09:31:32 +00:00
NETWORK : "" ,
2016-03-08 18:50:48 +00:00
} ,
2016-05-29 02:07:34 +00:00
chanCache : [ ] ,
2018-03-11 18:17:57 +00:00
ignoreList : [ ] ,
2019-09-15 19:35:18 +00:00
keepNick : null ,
2016-10-02 07:37:37 +00:00
} ) ;
2017-11-28 17:25:15 +00:00
if ( ! this . uuid ) {
this . uuid = uuidv4 ( ) ;
}
2017-07-06 12:02:32 +00:00
if ( ! this . name ) {
this . name = this . host ;
}
2014-09-13 21:29:45 +00:00
this . channels . unshift (
2014-10-11 22:47:24 +00:00
new Chan ( {
name : this . name ,
2017-11-15 06:35:15 +00:00
type : Chan . Type . LOBBY ,
2014-10-11 22:47:24 +00:00
} )
2014-09-13 21:29:45 +00:00
) ;
}
2020-03-21 20:55:36 +00:00
Network . prototype . validate = function ( client ) {
2020-01-21 13:42:29 +00:00
// Remove !, :, @ and whitespace characters from nicknames and usernames
const cleanNick = ( str ) => str . replace ( /[\x00\s:!@]/g , "_" ) . substring ( 0 , 100 ) ;
// Remove new lines and limit length
const cleanString = ( str ) => str . replace ( /[\x00\r\n]/g , "" ) . substring ( 0 , 300 ) ;
2018-10-20 11:10:26 +00:00
2020-01-21 13:42:29 +00:00
this . setNick ( cleanNick ( String ( this . nick || Helper . getDefaultNick ( ) ) ) ) ;
2018-03-15 08:37:05 +00:00
if ( ! this . username ) {
2020-01-21 13:42:29 +00:00
// If username is empty, make one from the provided nick
2018-03-15 08:37:05 +00:00
this . username = this . nick . replace ( /[^a-zA-Z0-9]/g , "" ) ;
}
2020-01-23 20:14:30 +00:00
this . username = cleanString ( this . username ) || "thelounge" ;
2020-01-21 13:42:29 +00:00
this . realname = cleanString ( this . realname ) || "The Lounge User" ;
this . password = cleanString ( this . password ) ;
2020-02-19 11:20:22 +00:00
this . host = cleanString ( this . host ) . toLowerCase ( ) ;
2020-01-21 13:42:29 +00:00
this . name = cleanString ( this . name ) ;
2020-03-31 08:02:18 +00:00
this . saslAccount = cleanString ( this . saslAccount ) ;
this . saslPassword = cleanString ( this . saslPassword ) ;
2018-03-15 08:37:05 +00:00
if ( ! this . port ) {
this . port = this . tls ? 6697 : 6667 ;
}
2020-03-31 08:02:18 +00:00
if ( ! [ "" , "plain" , "external" ] . includes ( this . sasl ) ) {
this . sasl = "" ;
}
2020-03-30 20:15:32 +00:00
if ( ! this . tls ) {
ClientCertificate . remove ( this . uuid ) ;
}
2018-03-15 08:37:05 +00:00
if ( Helper . config . lockNetwork ) {
// This check is needed to prevent invalid user configurations
2019-07-17 09:33:59 +00:00
if (
! Helper . config . public &&
this . host &&
this . host . length > 0 &&
this . host !== Helper . config . defaults . host
) {
this . channels [ 0 ] . pushMessage (
client ,
new Msg ( {
type : Msg . Type . ERROR ,
text : "Hostname you specified is not allowed." ,
} ) ,
true
) ;
2018-03-15 08:37:05 +00:00
return false ;
}
2020-03-31 17:03:40 +00:00
this . name = Helper . config . defaults . name ;
2018-03-15 08:37:05 +00:00
this . host = Helper . config . defaults . host ;
this . port = Helper . config . defaults . port ;
this . tls = Helper . config . defaults . tls ;
this . rejectUnauthorized = Helper . config . defaults . rejectUnauthorized ;
}
if ( this . host . length === 0 ) {
2019-07-17 09:33:59 +00:00
this . channels [ 0 ] . pushMessage (
client ,
new Msg ( {
type : Msg . Type . ERROR ,
text : "You must specify a hostname to connect." ,
} ) ,
true
) ;
2018-03-15 08:37:05 +00:00
return false ;
}
2020-02-19 11:20:22 +00:00
const stsPolicy = STSPolicies . get ( this . host ) ;
if ( stsPolicy && ! this . tls ) {
this . channels [ 0 ] . pushMessage (
client ,
new Msg ( {
type : Msg . Type . ERROR ,
text : ` ${ this . host } has an active strict transport security policy, will connect to port ${ stsPolicy . port } over a secure connection. ` ,
} ) ,
true
) ;
this . port = stsPolicy . port ;
this . tls = true ;
this . rejectUnauthorized = true ;
}
2018-03-15 08:37:05 +00:00
return true ;
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . createIrcFramework = function ( client ) {
2018-03-15 08:37:05 +00:00
this . irc = new IrcFramework . Client ( {
version : false , // We handle it ourselves
outgoing _addr : Helper . config . bind ,
enable _chghost : true ,
enable _echomessage : true ,
2020-02-18 12:07:03 +00:00
enable _setname : true ,
2018-03-15 08:37:05 +00:00
auto _reconnect : true ,
auto _reconnect _wait : 10000 + Math . floor ( Math . random ( ) * 1000 ) , // If multiple users are connected to the same network, randomize their reconnections a little
auto _reconnect _max _retries : 360 , // At least one hour (plus timeouts) worth of reconnections
} ) ;
2020-03-30 20:14:40 +00:00
this . setIrcFrameworkOptions ( client ) ;
2018-03-15 08:37:05 +00:00
this . irc . requestCap ( [
"znc.in/self-message" , // Legacy echo-message for ZNC
] ) ;
// Request only new messages from ZNC if we have sqlite logging enabled
// See http://wiki.znc.in/Playback
2018-04-17 08:06:08 +00:00
if ( client . config . log && client . messageStorage . find ( ( s ) => s . canProvideMessages ( ) ) ) {
2018-03-15 08:37:05 +00:00
this . irc . requestCap ( "znc.in/playback" ) ;
}
} ;
2020-03-30 20:14:40 +00:00
Network . prototype . setIrcFrameworkOptions = function ( client ) {
this . irc . options . host = this . host ;
this . irc . options . port = this . port ;
this . irc . options . password = this . password ;
this . irc . options . nick = this . nick ;
this . irc . options . username = Helper . config . useHexIp
? Helper . ip2hex ( client . config . browser . ip )
: this . username ;
this . irc . options . gecos = this . realname ;
this . irc . options . tls = this . tls ;
this . irc . options . rejectUnauthorized = this . rejectUnauthorized ;
this . irc . options . webirc = this . createWebIrc ( client ) ;
2020-03-30 20:15:32 +00:00
this . irc . options . client _certificate = this . tls ? ClientCertificate . get ( this . uuid ) : null ;
2020-03-31 08:02:18 +00:00
if ( ! this . sasl ) {
delete this . irc . options . sasl _mechanism ;
delete this . irc . options . account ;
} else if ( this . sasl === "external" ) {
2020-03-30 20:15:32 +00:00
this . irc . options . sasl _mechanism = "EXTERNAL" ;
2020-03-31 08:02:18 +00:00
this . irc . options . account = { } ;
} else if ( this . sasl === "plain" ) {
2020-03-30 20:15:32 +00:00
delete this . irc . options . sasl _mechanism ;
2020-03-31 08:02:18 +00:00
this . irc . options . account = {
account : this . saslAccount ,
password : this . saslPassword ,
} ;
2020-03-30 20:15:32 +00:00
}
2020-03-30 20:14:40 +00:00
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . createWebIrc = function ( client ) {
2019-07-17 09:33:59 +00:00
if (
! Helper . config . webirc ||
! Object . prototype . hasOwnProperty . call ( Helper . config . webirc , this . host )
) {
2018-03-15 08:37:05 +00:00
return null ;
}
2019-07-16 09:51:22 +00:00
const webircObject = {
password : Helper . config . webirc [ this . host ] ,
username : "thelounge" ,
address : client . config . browser . ip ,
hostname : client . config . browser . hostname ,
} ;
2018-03-15 08:37:05 +00:00
2020-01-06 10:54:23 +00:00
// https://ircv3.net/specs/extensions/webirc#options
if ( client . config . browser . isSecure ) {
webircObject . options = {
secure : true ,
} ;
}
2018-10-13 09:54:46 +00:00
if ( typeof Helper . config . webirc [ this . host ] === "function" ) {
2019-07-16 09:51:22 +00:00
webircObject . password = null ;
return Helper . config . webirc [ this . host ] ( webircObject , this ) ;
2018-03-15 08:37:05 +00:00
}
2019-07-16 09:51:22 +00:00
return webircObject ;
2018-03-15 08:37:05 +00:00
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . edit = function ( client , args ) {
2018-03-15 08:37:32 +00:00
const oldNick = this . nick ;
2019-03-07 08:17:03 +00:00
const oldRealname = this . realname ;
2018-03-15 08:37:32 +00:00
2019-09-15 19:35:18 +00:00
this . keepNick = null ;
2018-03-15 08:37:32 +00:00
this . nick = args . nick ;
this . host = String ( args . host || "" ) ;
this . name = String ( args . name || "" ) || this . host ;
this . port = parseInt ( args . port , 10 ) ;
this . tls = ! ! args . tls ;
this . rejectUnauthorized = ! ! args . rejectUnauthorized ;
this . password = String ( args . password || "" ) ;
this . username = String ( args . username || "" ) ;
this . realname = String ( args . realname || "" ) ;
2020-03-31 08:02:18 +00:00
this . sasl = String ( args . sasl || "" ) ;
this . saslAccount = String ( args . saslAccount || "" ) ;
this . saslPassword = String ( args . saslPassword || "" ) ;
2018-03-15 08:37:32 +00:00
// Split commands into an array
this . commands = String ( args . commands || "" )
. replace ( /\r\n|\r|\n/g , "\n" )
. split ( "\n" )
. filter ( ( command ) => command . length > 0 ) ;
// Sync lobby channel name
this . channels [ 0 ] . name = this . name ;
if ( ! this . validate ( client ) ) {
return ;
}
if ( this . irc ) {
2019-03-07 08:17:03 +00:00
const connected = this . irc . connection && this . irc . connection . connected ;
2018-03-15 08:37:32 +00:00
if ( this . nick !== oldNick ) {
2019-03-07 08:17:03 +00:00
if ( connected ) {
2018-03-15 08:37:32 +00:00
// Send new nick straight away
2019-09-15 19:35:18 +00:00
this . irc . changeNick ( this . nick ) ;
2018-03-15 08:37:32 +00:00
} else {
2020-03-30 20:14:40 +00:00
this . irc . user . nick = this . nick ;
2018-03-15 08:37:32 +00:00
// Update UI nick straight away if IRC is not connected
client . emit ( "nick" , {
2018-04-26 09:06:01 +00:00
network : this . uuid ,
2018-03-15 08:37:32 +00:00
nick : this . nick ,
} ) ;
}
}
2019-07-17 09:33:59 +00:00
if (
connected &&
this . realname !== oldRealname &&
2020-02-18 12:07:03 +00:00
this . irc . network . cap . isEnabled ( "setname" )
2019-07-17 09:33:59 +00:00
) {
2019-03-07 08:17:03 +00:00
this . irc . raw ( "SETNAME" , this . realname ) ;
}
2020-03-30 20:14:40 +00:00
this . setIrcFrameworkOptions ( client ) ;
2018-03-15 08:37:32 +00:00
2020-03-30 20:14:40 +00:00
this . irc . user . username = this . irc . options . username ;
this . irc . user . gecos = this . irc . options . gecos ;
2018-03-15 08:37:32 +00:00
}
client . save ( ) ;
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . destroy = function ( ) {
2017-08-11 12:02:58 +00:00
this . channels . forEach ( ( channel ) => channel . destroy ( ) ) ;
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . setNick = function ( nick ) {
2016-05-12 11:15:38 +00:00
this . nick = nick ;
this . highlightRegex = new RegExp (
// Do not match characters and numbers (unless IRC color)
"(?:^|[^a-z0-9]|\x03[0-9]{1,2})" +
2019-07-17 09:33:59 +00:00
// Escape nickname, as it may contain regex stuff
_ . escapeRegExp ( nick ) +
// Do not match characters and numbers
"(?:[^a-z0-9]|$)" ,
2016-05-12 11:15:38 +00:00
// Case insensitive search
"i"
) ;
2019-09-15 19:35:18 +00:00
if ( this . keepNick === nick ) {
this . keepNick = null ;
}
2020-03-30 20:14:40 +00:00
if ( this . irc ) {
this . irc . options . nick = nick ;
}
2016-05-12 11:15:38 +00:00
} ;
2017-11-29 19:54:09 +00:00
/ * *
* Get a clean clone of this network that will be sent to the client .
* This function performs manual cloning of network object for
* better control of performance and memory usage .
*
* Both of the parameters that are accepted by this function are passed into channels ' getFilteredClone call .
*
* @ see { @ link Chan # getFilteredClone }
* /
2020-03-21 20:55:36 +00:00
Network . prototype . getFilteredClone = function ( lastActiveChannel , lastMessage ) {
2018-02-19 11:12:01 +00:00
const filteredNetwork = Object . keys ( this ) . reduce ( ( newNetwork , prop ) => {
2017-11-29 19:54:09 +00:00
if ( prop === "channels" ) {
// Channels objects perform their own cloning
2019-07-17 09:33:59 +00:00
newNetwork [ prop ] = this [ prop ] . map ( ( channel ) =>
channel . getFilteredClone ( lastActiveChannel , lastMessage )
) ;
2019-11-02 18:45:00 +00:00
} else if ( fieldsForClient [ prop ] ) {
2017-11-29 19:54:09 +00:00
// Some properties that are not useful for the client are skipped
newNetwork [ prop ] = this [ prop ] ;
}
return newNetwork ;
} , { } ) ;
2018-02-19 11:12:01 +00:00
filteredNetwork . status = this . getNetworkStatus ( ) ;
return filteredNetwork ;
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . getNetworkStatus = function ( ) {
2018-02-19 11:12:01 +00:00
const status = {
connected : false ,
secure : false ,
} ;
if ( this . irc && this . irc . connection && this . irc . connection . transport ) {
const transport = this . irc . connection . transport ;
if ( transport . socket ) {
2018-02-20 08:35:45 +00:00
const isLocalhost = transport . socket . remoteAddress === "127.0.0.1" ;
const isAuthorized = transport . socket . encrypted && transport . socket . authorized ;
2018-02-19 11:12:01 +00:00
status . connected = transport . isConnected ( ) ;
2018-02-20 08:35:45 +00:00
status . secure = isAuthorized || isLocalhost ;
2018-02-19 11:12:01 +00:00
}
}
return status ;
2014-10-11 20:44:56 +00:00
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . addChannel = function ( newChan ) {
2018-03-12 12:42:59 +00:00
let index = this . channels . length ; // Default to putting as the last item in the array
// Don't sort special channels in amongst channels/users.
if ( newChan . type === Chan . Type . CHANNEL || newChan . type === Chan . Type . QUERY ) {
// We start at 1 so we don't test against the lobby
for ( let i = 1 ; i < this . channels . length ; i ++ ) {
const compareChan = this . channels [ i ] ;
// Negative if the new chan is alphabetically before the next chan in the list, positive if after
2019-07-17 09:33:59 +00:00
if (
newChan . name . localeCompare ( compareChan . name , { sensitivity : "base" } ) <= 0 ||
( compareChan . type !== Chan . Type . CHANNEL && compareChan . type !== Chan . Type . QUERY )
) {
2018-03-12 12:42:59 +00:00
index = i ;
break ;
}
}
}
this . channels . splice ( index , 0 , newChan ) ;
return index ;
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . quit = function ( quitMessage ) {
2020-02-19 12:09:22 +00:00
if ( ! this . irc ) {
return ;
}
// https://ircv3.net/specs/extensions/sts#rescheduling-expiry-on-disconnect
STSPolicies . refreshExpiration ( this . host ) ;
this . irc . quit ( quitMessage || Helper . config . leaveMessage ) ;
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . exportForEdit = function ( ) {
2020-03-31 08:02:18 +00:00
const fieldsToReturn = [
"uuid" ,
"name" ,
"nick" ,
"password" ,
"username" ,
"realname" ,
"sasl" ,
"saslAccount" ,
"saslPassword" ,
"commands" ,
] ;
2019-11-02 18:45:00 +00:00
2020-03-31 17:03:40 +00:00
if ( ! Helper . config . lockNetwork ) {
2020-03-31 08:02:18 +00:00
fieldsToReturn . push ( "host" ) ;
fieldsToReturn . push ( "port" ) ;
fieldsToReturn . push ( "tls" ) ;
fieldsToReturn . push ( "rejectUnauthorized" ) ;
2019-11-02 18:45:00 +00:00
}
2020-02-19 11:26:43 +00:00
const data = _ . pick ( this , fieldsToReturn ) ;
data . hasSTSPolicy = ! ! STSPolicies . get ( this . host ) ;
return data ;
2019-11-02 18:45:00 +00:00
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . export = function ( ) {
2018-01-11 11:33:36 +00:00
const network = _ . pick ( this , [
2017-11-28 17:25:15 +00:00
"uuid" ,
2016-12-18 09:24:50 +00:00
"awayMessage" ,
2016-05-12 11:15:38 +00:00
"nick" ,
2014-10-11 22:47:24 +00:00
"name" ,
"host" ,
"port" ,
"tls" ,
2018-08-25 09:11:59 +00:00
"userDisconnected" ,
2018-02-17 08:22:28 +00:00
"rejectUnauthorized" ,
2014-10-11 22:47:24 +00:00
"password" ,
"username" ,
2014-11-09 16:01:22 +00:00
"realname" ,
2020-03-31 08:02:18 +00:00
"sasl" ,
"saslAccount" ,
"saslPassword" ,
2016-04-03 05:12:49 +00:00
"commands" ,
2018-03-11 18:17:57 +00:00
"ignoreList" ,
2014-10-11 22:47:24 +00:00
] ) ;
2016-05-12 11:15:38 +00:00
2016-06-19 17:12:42 +00:00
network . channels = this . channels
2020-03-21 20:55:36 +00:00
. filter ( function ( channel ) {
2018-01-30 16:46:34 +00:00
return channel . type === Chan . Type . CHANNEL || channel . type === Chan . Type . QUERY ;
2016-06-19 17:12:42 +00:00
} )
2020-03-21 20:55:36 +00:00
. map ( function ( chan ) {
2018-01-30 16:46:34 +00:00
const keys = [ "name" ] ;
2018-02-20 07:28:04 +00:00
2018-01-30 16:46:34 +00:00
if ( chan . type === Chan . Type . CHANNEL ) {
keys . push ( "key" ) ;
} else if ( chan . type === Chan . Type . QUERY ) {
keys . push ( "type" ) ;
}
2018-02-20 07:28:04 +00:00
2018-01-30 16:46:34 +00:00
return _ . pick ( chan , keys ) ;
2016-06-19 17:12:42 +00:00
} ) ;
2016-05-12 11:15:38 +00:00
2014-10-11 20:44:56 +00:00
return network ;
2014-09-13 21:29:45 +00:00
} ;
2020-03-21 20:55:36 +00:00
Network . prototype . getChannel = function ( name ) {
2016-03-20 14:28:47 +00:00
name = name . toLowerCase ( ) ;
2020-03-21 20:55:36 +00:00
return _ . find ( this . channels , function ( that , i ) {
2018-02-05 12:35:01 +00:00
// Skip network lobby (it's always unshifted into first position)
return i > 0 && that . name . toLowerCase ( ) === name ;
2016-03-20 14:28:47 +00:00
} ) ;
} ;