2016-10-09 19:14:02 +00:00
"use strict" ;
2018-01-11 11:33:36 +00:00
const _ = require ( "lodash" ) ;
2018-06-15 20:31:06 +00:00
const log = require ( "../log" ) ;
2017-11-28 17:25:15 +00:00
const uuidv4 = require ( "uuid/v4" ) ;
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" ) ;
2014-09-13 21:29:45 +00:00
module . exports = Network ;
2017-11-29 19:54:09 +00:00
/ * *
* @ type { Object } List of keys which should not be sent to the client .
* /
const filteredFromClient = {
awayMessage : true ,
chanCache : true ,
highlightRegex : true ,
irc : true ,
password : true ,
2018-03-11 18:17:57 +00:00
ignoreList : 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 : "" ,
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 : "" ,
2014-09-13 21:29:45 +00:00
channels : [ ] ,
2016-04-03 05:12:49 +00:00
ip : null ,
hostname : null ,
2014-09-13 21:29:45 +00:00
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 : [ ] ,
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
) ;
}
2018-03-15 08:37:05 +00:00
Network . prototype . validate = function ( client ) {
2018-10-20 11:10:26 +00:00
// If entered nick is over 100 characters, limit it so we don't try to compile a big regex
if ( this . nick && this . nick . length > 100 ) {
this . nick = this . nick . substring ( 0 , 100 ) ;
}
2019-03-07 08:46:06 +00:00
this . setNick ( String ( this . nick || Helper . getDefaultNick ( ) ) . replace ( /\s/g , "_" ) ) ;
2018-03-15 08:37:05 +00:00
if ( ! this . username ) {
this . username = this . nick . replace ( /[^a-zA-Z0-9]/g , "" ) ;
2019-03-07 08:46:06 +00:00
} else {
// Remove any whitespace from usernames as that is not valid
this . username = this . username . replace ( /\s/g , "_" ) . substring ( 0 , 100 ) ;
2018-03-15 08:37:05 +00:00
}
if ( ! this . realname ) {
this . realname = "The Lounge User" ;
2019-03-07 08:46:06 +00:00
} else {
// Remove newlines from realnames
this . realname = this . realname . replace ( /[\r\n]/g , "_" ) . substring ( 0 , 128 ) ;
2018-03-15 08:37:05 +00:00
}
if ( ! this . port ) {
this . port = this . tls ? 6697 : 6667 ;
}
if ( Helper . config . lockNetwork ) {
// This check is needed to prevent invalid user configurations
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 ) ;
return false ;
}
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 ) {
this . channels [ 0 ] . pushMessage ( client , new Msg ( {
type : Msg . Type . ERROR ,
text : "You must specify a hostname to connect." ,
} ) , true ) ;
return false ;
}
return true ;
} ;
Network . prototype . createIrcFramework = function ( client ) {
this . irc = new IrcFramework . Client ( {
version : false , // We handle it ourselves
host : this . host ,
port : this . port ,
nick : this . nick ,
username : Helper . config . useHexIp ? Helper . ip2hex ( this . ip ) : this . username ,
gecos : this . realname ,
password : this . password ,
tls : this . tls ,
outgoing _addr : Helper . config . bind ,
rejectUnauthorized : this . rejectUnauthorized ,
enable _chghost : true ,
enable _echomessage : true ,
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
webirc : this . createWebIrc ( client ) ,
} ) ;
this . irc . requestCap ( [
2019-03-07 10:14:34 +00:00
"draft/setname" , // https://github.com/ircv3/ircv3-specifications/pull/361
2018-03-15 08:37:05 +00:00
"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" ) ;
}
} ;
Network . prototype . createWebIrc = function ( client ) {
2018-10-13 09:54:46 +00:00
if ( ! Helper . config . webirc || ! Helper . config . webirc . hasOwnProperty ( this . host ) ) {
2018-03-15 08:37:05 +00:00
return null ;
}
if ( ! this . ip ) {
log . warn ( ` Cannot find a valid WEBIRC configuration for ${ this . nick } ! ${ this . username } @ ${ this . host } ` ) ;
return null ;
}
if ( ! this . hostname ) {
this . hostname = this . ip ;
}
2018-10-13 09:54:46 +00:00
if ( typeof Helper . config . webirc [ this . host ] === "function" ) {
2018-03-15 08:37:05 +00:00
return Helper . config . webirc [ this . host ] ( client , this ) ;
}
return {
password : Helper . config . webirc [ this . host ] ,
username : "thelounge" ,
address : this . ip ,
hostname : this . hostname ,
} ;
} ;
2018-03-15 08:37:32 +00:00
Network . prototype . edit = function ( client , args ) {
const oldNick = this . nick ;
2019-03-07 08:17:03 +00:00
const oldRealname = this . realname ;
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 || "" ) ;
// 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
this . irc . raw ( "NICK" , this . nick ) ;
} else {
this . irc . options . nick = this . irc . user . nick = this . nick ;
// 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-03-07 10:14:34 +00:00
if ( connected && this . realname !== oldRealname && this . irc . network . cap . isEnabled ( "draft/setname" ) ) {
2019-03-07 08:17:03 +00:00
this . irc . raw ( "SETNAME" , this . realname ) ;
}
2018-03-15 08:37:32 +00:00
this . irc . options . host = this . host ;
this . irc . options . port = this . port ;
this . irc . options . password = this . password ;
this . irc . options . gecos = this . irc . user . gecos = this . realname ;
this . irc . options . tls = this . tls ;
this . irc . options . rejectUnauthorized = this . rejectUnauthorized ;
if ( ! Helper . config . useHexIp ) {
this . irc . options . username = this . irc . user . username = this . username ;
}
}
client . save ( ) ;
} ;
2017-08-11 12:02:58 +00:00
Network . prototype . destroy = function ( ) {
this . channels . forEach ( ( channel ) => channel . destroy ( ) ) ;
} ;
2016-05-12 11:15:38 +00:00
Network . prototype . setNick = function ( nick ) {
this . nick = nick ;
this . highlightRegex = new RegExp (
// Do not match characters and numbers (unless IRC color)
"(?:^|[^a-z0-9]|\x03[0-9]{1,2})" +
// Escape nickname, as it may contain regex stuff
2016-11-19 08:49:16 +00:00
_ . escapeRegExp ( nick ) +
2016-05-12 11:15:38 +00:00
// Do not match characters and numbers
"(?:[^a-z0-9]|$)" ,
// Case insensitive search
"i"
) ;
} ;
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 }
* /
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
newNetwork [ prop ] = this [ prop ] . map ( ( channel ) => channel . getFilteredClone ( lastActiveChannel , lastMessage ) ) ;
} else if ( ! filteredFromClient [ prop ] ) {
// 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 ;
} ;
Network . prototype . getNetworkStatus = function ( ) {
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
} ;
2018-03-12 12:42:59 +00:00
Network . prototype . addChannel = function ( newChan ) {
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
if ( newChan . name . localeCompare ( compareChan . name , { sensitivity : "base" } ) <= 0
|| ( compareChan . type !== Chan . Type . CHANNEL && compareChan . type !== Chan . Type . QUERY ) ) {
index = i ;
break ;
}
}
}
this . channels . splice ( index , 0 , newChan ) ;
return index ;
} ;
2014-10-11 20:44:56 +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" ,
2016-04-03 05:12:49 +00:00
"commands" ,
"ip" ,
2017-11-15 06:35:15 +00:00
"hostname" ,
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
. 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
} )
. 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
} ;
2016-03-20 14:28:47 +00:00
Network . prototype . getChannel = function ( name ) {
name = name . toLowerCase ( ) ;
2018-02-05 12:35:01 +00:00
return _ . find ( this . channels , function ( that , i ) {
// 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
} ) ;
} ;