2016-07-19 01:35:02 +00:00
"use strict" ;
2014-08-14 16:35:37 +00:00
var _ = require ( "lodash" ) ;
2016-06-12 01:44:28 +00:00
var pkg = require ( "../package.json" ) ;
2014-08-14 16:35:37 +00:00
var Client = require ( "./client" ) ;
var ClientManager = require ( "./clientManager" ) ;
2014-09-26 22:12:53 +00:00
var express = require ( "express" ) ;
2017-04-20 10:17:25 +00:00
var expressHandlebars = require ( "express-handlebars" ) ;
2014-08-14 16:35:37 +00:00
var fs = require ( "fs" ) ;
2017-04-20 10:17:25 +00:00
var path = require ( "path" ) ;
2014-08-14 16:35:37 +00:00
var io = require ( "socket.io" ) ;
2016-04-03 05:12:49 +00:00
var dns = require ( "dns" ) ;
2014-09-13 12:23:17 +00:00
var Helper = require ( "./helper" ) ;
2016-11-18 17:25:23 +00:00
var colors = require ( "colors/safe" ) ;
2017-06-11 19:33:04 +00:00
const net = require ( "net" ) ;
2016-12-17 09:51:33 +00:00
const Identification = require ( "./identification" ) ;
2017-06-22 21:09:55 +00:00
const themes = require ( "./plugins/themes" ) ;
2014-08-14 16:35:37 +00:00
2017-09-01 09:44:53 +00: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" ) ,
] ;
2017-08-28 09:18:31 +00:00
// A random number that will force clients to reload the page if it differs
const serverHash = Math . floor ( Date . now ( ) * Math . random ( ) ) ;
2016-04-26 20:41:08 +00:00
var manager = null ;
2014-08-14 16:35:37 +00:00
2016-06-08 09:26:24 +00:00
module . exports = function ( ) {
2016-12-17 09:51:33 +00: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 16:35:37 +00:00
2017-01-08 00:05:42 +00: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 22:12:53 +00:00
var app = express ( )
2016-05-01 17:27:10 +00:00
. use ( allRequests )
2014-08-14 16:35:37 +00:00
. use ( index )
2017-04-20 10:17:25 +00:00
. use ( express . static ( "client" ) )
2017-07-06 15:33:09 +00:00
. use ( "/storage/" , express . static ( Helper . getStoragePath ( ) , {
redirect : false ,
maxAge : 86400 * 1000 ,
} ) )
2017-06-01 19:43:23 +00:00
. engine ( "html" , expressHandlebars ( {
extname : ".html" ,
helpers : {
2017-04-08 12:34:31 +00:00
tojson : ( c ) => JSON . stringify ( c )
2017-06-01 19:43:23 +00:00
}
} ) )
2017-04-20 10:17:25 +00:00
. set ( "view engine" , "html" )
. set ( "views" , path . join ( _ _dirname , ".." , "client" ) ) ;
2014-11-01 20:06:01 +00:00
2017-06-22 21:09:55 +00: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 09:26:24 +00:00
var config = Helper . config ;
2014-09-26 23:26:21 +00:00
var server = null ;
2016-07-30 01:20:38 +00:00
if ( config . public && ( config . ldap || { } ) . enable ) {
2016-08-10 06:14:09 +00:00
log . warn ( "Server is public and set to use LDAP. Set to private mode if trying to use LDAP authentication." ) ;
2016-07-30 01:20:38 +00:00
}
2016-06-08 09:26:24 +00:00
if ( ! config . https . enable ) {
2014-09-26 23:26:21 +00:00
server = require ( "http" ) ;
2016-12-17 09:51:33 +00:00
server = server . createServer ( app ) ;
2014-09-26 23:26:21 +00:00
} else {
2016-10-04 22:35:04 +00:00
const keyPath = Helper . expandHome ( config . https . key ) ;
const certPath = Helper . expandHome ( config . https . certificate ) ;
2017-04-10 18:49:58 +00:00
const caPath = Helper . expandHome ( config . https . ca ) ;
2017-04-13 21:05:28 +00:00
if ( ! keyPath . length || ! fs . existsSync ( keyPath ) ) {
2016-10-04 22:35:04 +00:00
log . error ( "Path to SSL key is invalid. Stopping server..." ) ;
process . exit ( ) ;
}
2017-04-13 21:05:28 +00:00
if ( ! certPath . length || ! fs . existsSync ( certPath ) ) {
2016-10-04 22:35:04 +00:00
log . error ( "Path to SSL certificate is invalid. Stopping server..." ) ;
process . exit ( ) ;
}
2017-04-13 21:05:28 +00:00
2017-04-10 18:49:58 +00:00
if ( caPath . length && ! fs . existsSync ( caPath ) ) {
log . error ( "Path to SSL ca bundle is invalid. Stopping server..." ) ;
process . exit ( ) ;
}
2017-04-13 21:05:28 +00:00
server = require ( "spdy" ) ;
2014-09-26 23:26:21 +00:00
server = server . createServer ( {
2016-10-04 22:35:04 +00:00
key : fs . readFileSync ( keyPath ) ,
2017-04-10 18:49:58 +00:00
cert : fs . readFileSync ( certPath ) ,
ca : caPath ? fs . readFileSync ( caPath ) : undefined
2016-12-17 09:51:33 +00:00
} , app ) ;
2014-09-26 23:26:21 +00:00
}
2017-08-31 18:56:20 +00: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 12:13:56 +00:00
server . on ( "error" , ( err ) => log . error ( ` ${ err } ` ) ) ;
2017-08-31 18:56:20 +00: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 05:19:50 +00:00
const sockets = io ( server , {
serveClient : false ,
transports : config . transports
} ) ;
sockets . on ( "connect" , ( socket ) => {
if ( config . public ) {
performAuthentication . call ( socket , { } ) ;
} else {
2017-08-28 09:18:31 +00:00
socket . emit ( "auth" , {
serverHash : serverHash ,
success : true ,
} ) ;
2017-08-23 05:19:50 +00:00
socket . on ( "auth" , performAuthentication ) ;
}
} ) ;
2014-08-14 16:35:37 +00:00
2017-08-23 05:19:50 +00:00
manager = new ClientManager ( ) ;
2016-12-07 05:50:11 +00:00
2017-08-23 05:19:50 +00:00
new Identification ( ( identHandler ) => {
manager . init ( identHandler , sockets ) ;
} ) ;
2017-08-30 17:26:45 +00: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 09:51:33 +00:00
} ) ;
2014-08-14 16:35:37 +00:00
} ;
2017-06-11 19:33:04 +00:00
function getClientIp ( request ) {
let ip = request . connection . remoteAddress ;
2016-07-31 00:54:09 +00:00
2017-06-11 19:33:04 +00: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 05:12:49 +00:00
}
2016-07-31 00:54:09 +00:00
return ip . replace ( /^::ffff:/ , "" ) ;
2016-04-03 05:12:49 +00:00
}
2016-05-01 17:27:10 +00:00
function allRequests ( req , res , next ) {
res . setHeader ( "X-Content-Type-Options" , "nosniff" ) ;
return next ( ) ;
}
2014-08-14 16:35:37 +00:00
function index ( req , res , next ) {
2016-05-01 09:41:17 +00:00
if ( req . url . split ( "?" ) [ 0 ] !== "/" ) {
return next ( ) ;
}
2017-04-20 10:17:25 +00:00
var data = _ . merge (
pkg ,
Helper . config
) ;
2017-06-22 21:09:55 +00: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 10:17:25 +00:00
data . gitCommit = Helper . getGitCommit ( ) ;
2017-06-22 21:09:55 +00:00
data . themes = themes . getAll ( ) ;
2017-07-06 15:33:09 +00: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 10:17:25 +00:00
res . setHeader ( "Referrer-Policy" , "no-referrer" ) ;
res . render ( "index" , data ) ;
2014-08-14 16:35:37 +00:00
}
2017-08-26 16:37:56 +00:00
function initializeClient ( socket , client , token ) {
2017-08-13 18:37:12 +00:00
socket . emit ( "authorized" ) ;
2016-09-25 06:52:16 +00:00
2017-08-13 18:37:12 +00:00
socket . on ( "disconnect" , function ( ) {
client . clientDetach ( socket . id ) ;
} ) ;
2017-07-10 19:47:03 +00:00
client . clientAttach ( socket . id , token ) ;
2016-11-19 20:34:05 +00:00
2017-08-13 18:37:12 +00:00
socket . on (
"input" ,
function ( data ) {
client . input ( data ) ;
}
) ;
2016-09-25 06:41:10 +00:00
2017-08-13 18:37:12 +00: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 16:35:37 +00:00
socket . on (
2017-08-13 18:37:12 +00:00
"change-password" ,
2014-08-14 16:35:37 +00:00
function ( data ) {
2017-08-13 18:37:12 +00: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 21:28:31 +00:00
2017-08-13 18:37:12 +00: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 07:47:51 +00:00
2017-08-13 18:37:12 +00:00
client . setPassword ( hash , ( success ) => {
const obj = { } ;
2017-03-23 07:47:51 +00:00
2017-08-13 18:37:12 +00:00
if ( success ) {
obj . success = "Successfully updated your password" ;
} else {
obj . error = "Failed to update your password" ;
}
2017-03-23 07:47:51 +00:00
2017-08-13 18:37:12 +00:00
socket . emit ( "change-password" , obj ) ;
2017-03-23 07:47:51 +00:00
} ) ;
2017-08-13 18:37:12 +00:00
} ) . catch ( ( error ) => {
log . error ( ` Error while checking users password. Error: ${ error } ` ) ;
} ) ;
2016-02-17 02:29:44 +00:00
}
) ;
2017-08-13 18:37:12 +00:00
}
2017-07-24 06:01:25 +00:00
2017-08-13 18:37:12 +00:00
socket . on (
"open" ,
function ( data ) {
client . open ( socket . id , data ) ;
}
) ;
2017-07-24 06:01:25 +00:00
2017-08-13 18:37:12 +00:00
socket . on (
"sort" ,
function ( data ) {
client . sort ( data ) ;
}
) ;
2017-07-24 06:01:25 +00:00
2017-08-13 18:37:12 +00: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 06:01:25 +00:00
2017-07-10 19:47:03 +00: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 18:37:12 +00:00
socket . on ( "sign-out" , ( ) => {
delete client . config . sessions [ token ] ;
2017-07-24 06:01:25 +00:00
2017-08-13 18:37:12 +00: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 06:01:25 +00:00
}
} ) ;
2017-08-13 18:37:12 +00:00
socket . emit ( "sign-out" ) ;
} ) ;
socket . join ( client . id ) ;
const sendInitEvent = ( tokenToSend ) => {
socket . emit ( "init" , {
2017-07-10 19:47:03 +00:00
applicationServerKey : manager . webPush . vapidKeys . publicKey ,
pushSubscription : client . config . sessions [ token ] ,
2017-08-13 18:37:12 +00:00
active : client . lastActiveChannel ,
networks : client . networks ,
token : tokenToSend
} ) ;
} ;
2017-08-26 16:37:56 +00:00
if ( ! Helper . config . public && token === null ) {
2017-08-13 18:37:12 +00:00
client . generateToken ( ( newToken ) => {
token = newToken ;
client . updateSession ( token , getClientIp ( socket . request ) , socket . request ) ;
2017-06-21 07:58:29 +00: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 18:37:12 +00:00
sendInitEvent ( token ) ;
2014-08-14 16:35:37 +00:00
} ) ;
2017-08-13 18:37:12 +00:00
} else {
sendInitEvent ( null ) ;
2014-08-14 16:35:37 +00:00
}
}
2017-08-13 18:37:12 +00:00
function performAuthentication ( data ) {
2017-06-21 07:58:29 +00:00
const socket = this ;
let client ;
2017-08-26 16:37:56 +00:00
const finalInit = ( ) => initializeClient ( socket , client , data . token || null ) ;
2017-08-13 18:37:12 +00:00
2017-06-21 07:58:29 +00:00
const initClient = ( ) => {
2017-08-13 18:37:12 +00:00
client . ip = getClientIp ( socket . request ) ;
// If webirc is enabled perform reverse dns lookup
if ( Helper . config . webirc === null ) {
return finalInit ( ) ;
2017-06-21 07:58:29 +00:00
}
2017-08-13 18:37:12 +00:00
reverseDnsLookup ( client . ip , ( hostname ) => {
client . hostname = hostname ;
finalInit ( ) ;
} ) ;
2017-06-21 07:58:29 +00:00
} ;
2016-06-08 09:26:24 +00:00
if ( Helper . config . public ) {
2016-07-30 01:20:38 +00:00
client = new Client ( manager ) ;
2014-08-14 16:35:37 +00:00
manager . clients . push ( client ) ;
2017-06-21 07:58:29 +00:00
2014-08-14 16:35:37 +00:00
socket . on ( "disconnect" , function ( ) {
manager . clients = _ . without ( manager . clients , client ) ;
client . quit ( ) ;
} ) ;
2017-06-21 07:58:29 +00:00
initClient ( ) ;
return ;
}
const authCallback = ( success ) => {
// Authorization failed
if ( ! success ) {
socket . emit ( "auth" , { success : false } ) ;
return ;
2016-04-03 05:12:49 +00:00
}
2016-07-30 01:20:38 +00:00
2017-06-21 07:58:29 +00: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-30 01:20:38 +00:00
}
2017-06-21 07:58:29 +00:00
initClient ( ) ;
} ;
2016-07-30 01:20:38 +00:00
2017-06-21 07:58:29 +00: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 09:44:53 +00: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 16:35:37 +00:00
}
2017-08-29 16:05:06 +00:00
auth ( manager , client , data . user , data . password , authCallback ) ;
2014-08-14 16:35:37 +00:00
}
2017-08-13 18:37:12 +00:00
function reverseDnsLookup ( ip , callback ) {
dns . reverse ( ip , ( err , hostnames ) => {
if ( ! err && hostnames . length ) {
return callback ( hostnames [ 0 ] ) ;
}
callback ( ip ) ;
} ) ;
}