2016-07-18 21:35:02 -04:00
"use strict" ;
2014-08-14 12:35:37 -04:00
var _ = require ( "lodash" ) ;
2016-06-11 21:44:28 -04:00
var pkg = require ( "../package.json" ) ;
2014-08-14 12:35:37 -04:00
var Client = require ( "./client" ) ;
var ClientManager = require ( "./clientManager" ) ;
2014-09-26 18:12:53 -04:00
var express = require ( "express" ) ;
2017-04-20 06:17:25 -04:00
var expressHandlebars = require ( "express-handlebars" ) ;
2014-08-14 12:35:37 -04:00
var fs = require ( "fs" ) ;
2017-04-20 06:17:25 -04:00
var path = require ( "path" ) ;
2014-08-14 12:35:37 -04:00
var io = require ( "socket.io" ) ;
2016-04-03 01:12:49 -04:00
var dns = require ( "dns" ) ;
2014-09-13 08:23:17 -04:00
var Helper = require ( "./helper" ) ;
2016-11-18 12:25:23 -05:00
var colors = require ( "colors/safe" ) ;
2017-06-11 15:33:04 -04:00
const net = require ( "net" ) ;
2016-12-17 04:51:33 -05:00
const Identification = require ( "./identification" ) ;
2017-06-22 17:09:55 -04:00
const themes = require ( "./plugins/themes" ) ;
2014-08-14 12:35:37 -04:00
2017-09-01 05:44:53 -04:00
// The order defined the priority: the first available plugin is used
// ALways keep local auth in the end, which should always be enabled.
const authPlugins = [
require ( "./plugins/auth/ldap" ) ,
require ( "./plugins/auth/local" ) ,
] ;
2016-04-26 16:41:08 -04:00
var manager = null ;
2014-08-14 12:35:37 -04:00
2016-06-08 05:26:24 -04:00
module . exports = function ( ) {
2016-12-17 04:51:33 -05:00
log . info ( ` The Lounge ${ colors . green ( Helper . getVersion ( ) ) } \
( node $ { colors . green ( process . versions . node ) } on $ { colors . green ( process . platform ) } $ { process . arch } ) ` );
log . info ( ` Configuration file: ${ colors . green ( Helper . CONFIG _PATH ) } ` ) ;
2014-08-14 12:35:37 -04:00
2017-01-07 19:05:42 -05:00
if ( ! fs . existsSync ( "client/js/bundle.js" ) ) {
log . error ( ` The client application was not built. Run ${ colors . bold ( "NODE_ENV=production npm run build" ) } to resolve this. ` ) ;
process . exit ( ) ;
}
2014-09-26 18:12:53 -04:00
var app = express ( )
2016-05-01 13:27:10 -04:00
. use ( allRequests )
2014-08-14 12:35:37 -04:00
. use ( index )
2017-04-20 06:17:25 -04:00
. use ( express . static ( "client" ) )
2017-07-06 11:33:09 -04:00
. use ( "/storage/" , express . static ( Helper . getStoragePath ( ) , {
redirect : false ,
maxAge : 86400 * 1000 ,
} ) )
2017-06-01 15:43:23 -04:00
. engine ( "html" , expressHandlebars ( {
extname : ".html" ,
helpers : {
2017-04-08 08:34:31 -04:00
tojson : ( c ) => JSON . stringify ( c )
2017-06-01 15:43:23 -04:00
}
} ) )
2017-04-20 06:17:25 -04:00
. set ( "view engine" , "html" )
. set ( "views" , path . join ( _ _dirname , ".." , "client" ) ) ;
2014-11-01 16:06:01 -04:00
2017-06-22 17:09:55 -04:00
app . get ( "/themes/:theme.css" , ( req , res ) => {
const themeName = req . params . theme ;
const theme = themes . getFilename ( themeName ) ;
if ( theme === undefined ) {
return res . status ( 404 ) . send ( "Not found" ) ;
}
return res . sendFile ( theme ) ;
} ) ;
2016-06-08 05:26:24 -04:00
var config = Helper . config ;
2014-09-26 19:26:21 -04:00
var server = null ;
2016-07-29 21:20:38 -04:00
if ( config . public && ( config . ldap || { } ) . enable ) {
2016-08-10 02:14:09 -04:00
log . warn ( "Server is public and set to use LDAP. Set to private mode if trying to use LDAP authentication." ) ;
2016-07-29 21:20:38 -04:00
}
2016-06-08 05:26:24 -04:00
if ( ! config . https . enable ) {
2014-09-26 19:26:21 -04:00
server = require ( "http" ) ;
2016-12-17 04:51:33 -05:00
server = server . createServer ( app ) ;
2014-09-26 19:26:21 -04:00
} else {
2016-10-04 18:35:04 -04:00
const keyPath = Helper . expandHome ( config . https . key ) ;
const certPath = Helper . expandHome ( config . https . certificate ) ;
2017-04-10 14:49:58 -04:00
const caPath = Helper . expandHome ( config . https . ca ) ;
2017-04-13 17:05:28 -04:00
if ( ! keyPath . length || ! fs . existsSync ( keyPath ) ) {
2016-10-04 18:35:04 -04:00
log . error ( "Path to SSL key is invalid. Stopping server..." ) ;
process . exit ( ) ;
}
2017-04-13 17:05:28 -04:00
if ( ! certPath . length || ! fs . existsSync ( certPath ) ) {
2016-10-04 18:35:04 -04:00
log . error ( "Path to SSL certificate is invalid. Stopping server..." ) ;
process . exit ( ) ;
}
2017-04-13 17:05:28 -04:00
2017-04-10 14:49:58 -04:00
if ( caPath . length && ! fs . existsSync ( caPath ) ) {
log . error ( "Path to SSL ca bundle is invalid. Stopping server..." ) ;
process . exit ( ) ;
}
2017-04-13 17:05:28 -04:00
server = require ( "spdy" ) ;
2014-09-26 19:26:21 -04:00
server = server . createServer ( {
2016-10-04 18:35:04 -04:00
key : fs . readFileSync ( keyPath ) ,
2017-04-10 14:49:58 -04:00
cert : fs . readFileSync ( certPath ) ,
ca : caPath ? fs . readFileSync ( caPath ) : undefined
2016-12-17 04:51:33 -05:00
} , app ) ;
2014-09-26 19:26:21 -04:00
}
2017-08-31 14:56:20 -04:00
let listenParams ;
if ( typeof config . host === "string" && config . host . startsWith ( "unix:" ) ) {
listenParams = config . host . replace ( /^unix:/ , "" ) ;
} else {
listenParams = {
port : config . port ,
host : config . host ,
} ;
}
2017-09-03 08:13:56 -04:00
server . on ( "error" , ( err ) => log . error ( ` ${ err } ` ) ) ;
2017-08-31 14:56:20 -04:00
server . listen ( listenParams , ( ) => {
if ( typeof listenParams === "string" ) {
log . info ( "Available on socket " + colors . green ( listenParams ) ) ;
} else {
const protocol = config . https . enable ? "https" : "http" ;
const address = server . address ( ) ;
log . info (
"Available at " +
colors . green ( ` ${ protocol } :// ${ address . address } : ${ address . port } / ` ) +
` in ${ colors . bold ( config . public ? "public" : "private" ) } mode `
) ;
}
2017-08-23 01:19:50 -04:00
const sockets = io ( server , {
serveClient : false ,
transports : config . transports
} ) ;
sockets . on ( "connect" , ( socket ) => {
if ( config . public ) {
performAuthentication . call ( socket , { } ) ;
} else {
socket . emit ( "auth" , { success : true } ) ;
socket . on ( "auth" , performAuthentication ) ;
}
} ) ;
2014-08-14 12:35:37 -04:00
2017-08-23 01:19:50 -04:00
manager = new ClientManager ( ) ;
2016-12-07 00:50:11 -05:00
2017-08-23 01:19:50 -04:00
new Identification ( ( identHandler ) => {
manager . init ( identHandler , sockets ) ;
} ) ;
2017-08-30 13:26:45 -04:00
// Handle ctrl+c and kill gracefully
let suicideTimeout = null ;
const exitGracefully = function ( ) {
if ( suicideTimeout !== null ) {
return ;
}
// Forcefully exit after 3 seconds
suicideTimeout = setTimeout ( ( ) => process . exit ( 1 ) , 3000 ) ;
log . info ( "Exiting..." ) ;
// Close all client and IRC connections
manager . clients . forEach ( ( client ) => client . quit ( ) ) ;
// Close http server
server . close ( ( ) => {
clearTimeout ( suicideTimeout ) ;
process . exit ( 0 ) ;
} ) ;
} ;
process . on ( "SIGINT" , exitGracefully ) ;
process . on ( "SIGTERM" , exitGracefully ) ;
2016-12-17 04:51:33 -05:00
} ) ;
2014-08-14 12:35:37 -04:00
} ;
2017-06-11 15:33:04 -04:00
function getClientIp ( request ) {
let ip = request . connection . remoteAddress ;
2016-07-30 20:54:09 -04:00
2017-06-11 15:33:04 -04:00
if ( Helper . config . reverseProxy ) {
const forwarded = ( request . headers [ "x-forwarded-for" ] || "" ) . split ( /\s*,\s*/ ) . filter ( Boolean ) ;
if ( forwarded . length && net . isIP ( forwarded [ 0 ] ) ) {
ip = forwarded [ 0 ] ;
}
2016-04-03 01:12:49 -04:00
}
2016-07-30 20:54:09 -04:00
return ip . replace ( /^::ffff:/ , "" ) ;
2016-04-03 01:12:49 -04:00
}
2016-05-01 13:27:10 -04:00
function allRequests ( req , res , next ) {
res . setHeader ( "X-Content-Type-Options" , "nosniff" ) ;
return next ( ) ;
}
2014-08-14 12:35:37 -04:00
function index ( req , res , next ) {
2016-05-01 05:41:17 -04:00
if ( req . url . split ( "?" ) [ 0 ] !== "/" ) {
return next ( ) ;
}
2017-04-20 06:17:25 -04:00
var data = _ . merge (
pkg ,
Helper . config
) ;
2017-06-22 17:09:55 -04:00
if ( ! data . theme . includes ( ".css" ) ) { // Backwards compatibility for old way of specifying themes in settings
data . theme = ` themes/ ${ data . theme } .css ` ;
} else {
log . warn ( ` Referring to CSS files in the ${ colors . green ( "theme" ) } setting of ${ colors . green ( Helper . CONFIG _PATH ) } is ${ colors . bold ( "deprecated" ) } and will be removed in a future version. ` ) ;
}
2017-04-20 06:17:25 -04:00
data . gitCommit = Helper . getGitCommit ( ) ;
2017-06-22 17:09:55 -04:00
data . themes = themes . getAll ( ) ;
2017-07-06 11:33:09 -04:00
const policies = [
"default-src *" ,
"connect-src 'self' ws: wss:" ,
"style-src * 'unsafe-inline'" ,
"script-src 'self'" ,
"child-src 'self'" ,
"object-src 'none'" ,
"form-action 'none'" ,
] ;
// If prefetch is enabled, but storage is not, we have to allow mixed content
if ( Helper . config . prefetchStorage || ! Helper . config . prefetch ) {
policies . push ( "img-src 'self'" ) ;
policies . unshift ( "block-all-mixed-content" ) ;
}
res . setHeader ( "Content-Security-Policy" , policies . join ( "; " ) ) ;
2017-04-20 06:17:25 -04:00
res . setHeader ( "Referrer-Policy" , "no-referrer" ) ;
res . render ( "index" , data ) ;
2014-08-14 12:35:37 -04:00
}
2017-08-26 12:37:56 -04:00
function initializeClient ( socket , client , token ) {
2017-08-13 14:37:12 -04:00
socket . emit ( "authorized" ) ;
2016-09-25 02:52:16 -04:00
2017-08-13 14:37:12 -04:00
socket . on ( "disconnect" , function ( ) {
client . clientDetach ( socket . id ) ;
} ) ;
2017-07-10 15:47:03 -04:00
client . clientAttach ( socket . id , token ) ;
2016-11-19 15:34:05 -05:00
2017-08-13 14:37:12 -04:00
socket . on (
"input" ,
function ( data ) {
client . input ( data ) ;
}
) ;
2016-09-25 02:41:10 -04:00
2017-08-13 14:37:12 -04:00
socket . on (
"more" ,
function ( data ) {
client . more ( data ) ;
}
) ;
socket . on (
"conn" ,
function ( data ) {
// prevent people from overriding webirc settings
data . ip = null ;
data . hostname = null ;
client . connect ( data ) ;
}
) ;
if ( ! Helper . config . public && ! Helper . config . ldap . enable ) {
2014-08-14 12:35:37 -04:00
socket . on (
2017-08-13 14:37:12 -04:00
"change-password" ,
2014-08-14 12:35:37 -04:00
function ( data ) {
2017-08-13 14:37:12 -04:00
var old = data . old _password ;
var p1 = data . new _password ;
var p2 = data . verify _password ;
if ( typeof p1 === "undefined" || p1 === "" ) {
socket . emit ( "change-password" , {
error : "Please enter a new password"
} ) ;
return ;
}
if ( p1 !== p2 ) {
socket . emit ( "change-password" , {
error : "Both new password fields must match"
} ) ;
return ;
}
2016-05-31 17:28:31 -04:00
2017-08-13 14:37:12 -04:00
Helper . password
. compare ( old || "" , client . config . password )
. then ( ( matching ) => {
if ( ! matching ) {
socket . emit ( "change-password" , {
error : "The current password field does not match your account password"
} ) ;
return ;
}
const hash = Helper . password . hash ( p1 ) ;
2017-03-23 03:47:51 -04:00
2017-08-13 14:37:12 -04:00
client . setPassword ( hash , ( success ) => {
const obj = { } ;
2017-03-23 03:47:51 -04:00
2017-08-13 14:37:12 -04:00
if ( success ) {
obj . success = "Successfully updated your password" ;
} else {
obj . error = "Failed to update your password" ;
}
2017-03-23 03:47:51 -04:00
2017-08-13 14:37:12 -04:00
socket . emit ( "change-password" , obj ) ;
2017-03-23 03:47:51 -04:00
} ) ;
2017-08-13 14:37:12 -04:00
} ) . catch ( ( error ) => {
log . error ( ` Error while checking users password. Error: ${ error } ` ) ;
} ) ;
2016-02-16 21:29:44 -05:00
}
) ;
2017-08-13 14:37:12 -04:00
}
2017-07-24 02:01:25 -04:00
2017-08-13 14:37:12 -04:00
socket . on (
"open" ,
function ( data ) {
client . open ( socket . id , data ) ;
}
) ;
2017-07-24 02:01:25 -04:00
2017-08-13 14:37:12 -04:00
socket . on (
"sort" ,
function ( data ) {
client . sort ( data ) ;
}
) ;
2017-07-24 02:01:25 -04:00
2017-08-13 14:37:12 -04:00
socket . on (
"names" ,
function ( data ) {
client . names ( data ) ;
}
) ;
socket . on ( "msg:preview:toggle" , function ( data ) {
const networkAndChan = client . find ( data . target ) ;
if ( ! networkAndChan ) {
return ;
}
const message = networkAndChan . chan . findMessage ( data . msgId ) ;
if ( ! message ) {
return ;
}
const preview = message . findPreview ( data . link ) ;
if ( preview ) {
preview . shown = data . shown ;
}
} ) ;
2017-07-24 02:01:25 -04:00
2017-07-10 15:47:03 -04:00
socket . on ( "push:register" , ( subscription ) => {
if ( ! client . isRegistered ( ) || ! client . config . sessions [ token ] ) {
return ;
}
const registration = client . registerPushSubscription ( client . config . sessions [ token ] , subscription ) ;
if ( registration ) {
client . manager . webPush . pushSingle ( client , registration , {
type : "notification" ,
timestamp : Date . now ( ) ,
title : "The Lounge" ,
body : "🚀 Push notifications have been enabled"
} ) ;
}
} ) ;
socket . on ( "push:unregister" , ( ) => {
if ( ! client . isRegistered ( ) ) {
return ;
}
client . unregisterPushSubscription ( token ) ;
} ) ;
2017-08-13 14:37:12 -04:00
socket . on ( "sign-out" , ( ) => {
delete client . config . sessions [ token ] ;
2017-07-24 02:01:25 -04:00
2017-08-13 14:37:12 -04:00
client . manager . updateUser ( client . name , {
sessions : client . config . sessions
} , ( err ) => {
if ( err ) {
log . error ( "Failed to update sessions for" , client . name , err ) ;
2017-07-24 02:01:25 -04:00
}
} ) ;
2017-08-13 14:37:12 -04:00
socket . emit ( "sign-out" ) ;
} ) ;
socket . join ( client . id ) ;
const sendInitEvent = ( tokenToSend ) => {
socket . emit ( "init" , {
2017-07-10 15:47:03 -04:00
applicationServerKey : manager . webPush . vapidKeys . publicKey ,
pushSubscription : client . config . sessions [ token ] ,
2017-08-13 14:37:12 -04:00
active : client . lastActiveChannel ,
networks : client . networks ,
token : tokenToSend
} ) ;
} ;
2017-08-26 12:37:56 -04:00
if ( ! Helper . config . public && token === null ) {
2017-08-13 14:37:12 -04:00
client . generateToken ( ( newToken ) => {
token = newToken ;
client . updateSession ( token , getClientIp ( socket . request ) , socket . request ) ;
2017-06-21 03:58:29 -04:00
client . manager . updateUser ( client . name , {
sessions : client . config . sessions
} , ( err ) => {
if ( err ) {
log . error ( "Failed to update sessions for" , client . name , err ) ;
}
} ) ;
2017-08-13 14:37:12 -04:00
sendInitEvent ( token ) ;
2014-08-14 12:35:37 -04:00
} ) ;
2017-08-13 14:37:12 -04:00
} else {
sendInitEvent ( null ) ;
2014-08-14 12:35:37 -04:00
}
}
2017-08-13 14:37:12 -04:00
function performAuthentication ( data ) {
2017-06-21 03:58:29 -04:00
const socket = this ;
let client ;
2017-08-26 12:37:56 -04:00
const finalInit = ( ) => initializeClient ( socket , client , data . token || null ) ;
2017-08-13 14:37:12 -04:00
2017-06-21 03:58:29 -04:00
const initClient = ( ) => {
2017-08-13 14:37:12 -04:00
client . ip = getClientIp ( socket . request ) ;
// If webirc is enabled perform reverse dns lookup
if ( Helper . config . webirc === null ) {
return finalInit ( ) ;
2017-06-21 03:58:29 -04:00
}
2017-08-13 14:37:12 -04:00
reverseDnsLookup ( client . ip , ( hostname ) => {
client . hostname = hostname ;
finalInit ( ) ;
} ) ;
2017-06-21 03:58:29 -04:00
} ;
2016-06-08 05:26:24 -04:00
if ( Helper . config . public ) {
2016-07-29 21:20:38 -04:00
client = new Client ( manager ) ;
2014-08-14 12:35:37 -04:00
manager . clients . push ( client ) ;
2017-06-21 03:58:29 -04:00
2014-08-14 12:35:37 -04:00
socket . on ( "disconnect" , function ( ) {
manager . clients = _ . without ( manager . clients , client ) ;
client . quit ( ) ;
} ) ;
2017-06-21 03:58:29 -04:00
initClient ( ) ;
return ;
}
const authCallback = ( success ) => {
// Authorization failed
if ( ! success ) {
socket . emit ( "auth" , { success : false } ) ;
return ;
2016-04-03 01:12:49 -04:00
}
2016-07-29 21:20:38 -04:00
2017-06-21 03:58:29 -04:00
// If authorization succeeded but there is no loaded user,
// load it and find the user again (this happens with LDAP)
if ( ! client ) {
manager . loadUser ( data . user ) ;
client = manager . findClient ( data . user ) ;
2016-07-29 21:20:38 -04:00
}
2017-06-21 03:58:29 -04:00
initClient ( ) ;
} ;
2016-07-29 21:20:38 -04:00
2017-06-21 03:58:29 -04:00
client = manager . findClient ( data . user ) ;
// We have found an existing user and client has provided a token
if ( client && data . token && typeof client . config . sessions [ data . token ] !== "undefined" ) {
client . updateSession ( data . token , getClientIp ( socket . request ) , socket . request ) ;
authCallback ( true ) ;
return ;
}
// Perform password checking
2017-09-01 05:44:53 -04:00
let auth = ( ) => {
log . error ( "None of the auth plugins is enabled" ) ;
} ;
for ( let i = 0 ; i < authPlugins . length ; ++ i ) {
if ( authPlugins [ i ] . isEnabled ( ) ) {
auth = authPlugins [ i ] . auth ;
break ;
}
2014-08-14 12:35:37 -04:00
}
2017-08-29 12:05:06 -04:00
auth ( manager , client , data . user , data . password , authCallback ) ;
2014-08-14 12:35:37 -04:00
}
2017-08-13 14:37:12 -04:00
function reverseDnsLookup ( ip , callback ) {
dns . reverse ( ip , ( err , hostnames ) => {
if ( ! err && hostnames . length ) {
return callback ( hostnames [ 0 ] ) ;
}
callback ( ip ) ;
} ) ;
}