2016-10-09 19:14:02 +00:00
"use strict" ;
2018-01-11 11:33:36 +00:00
const _ = require ( "lodash" ) ;
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-09-20 07:44:36 +00:00
let id = 1 ;
2014-09-13 21:29:45 +00:00
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 ,
} ;
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-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
id : id ++ ,
irc : null ,
2016-03-08 18:50:48 +00:00
serverOptions : {
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 : [ ] ,
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 ) {
this . setNick ( String ( this . nick || "thelounge" ) . replace ( " " , "_" ) ) ;
if ( ! this . username ) {
this . username = this . nick . replace ( /[^a-zA-Z0-9]/g , "" ) ;
}
if ( ! this . realname ) {
this . realname = "The Lounge User" ;
}
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 ( [
"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
if ( client . config . log && Helper . config . messageStorage . includes ( "sqlite" ) ) {
this . irc . requestCap ( "znc.in/playback" ) ;
}
} ;
Network . prototype . createWebIrc = function ( client ) {
if ( ! Helper . config . webirc || ! ( this . host in Helper . config . webirc ) ) {
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 ;
}
if ( Helper . config . webirc [ this . host ] instanceof Function ) {
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 ;
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 ) {
if ( this . nick !== oldNick ) {
if ( this . irc . connection && this . irc . connection . connected ) {
// 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" , {
network : this . id ,
nick : this . nick ,
} ) ;
}
}
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-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" ,
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
} ) ;
} ;