2017-08-28 19:27:45 +03:00
/ * *
2018-01-04 23:15:21 +03:00
* @ description MeshCentral connection relay module
2017-08-28 19:27:45 +03:00
* @ author Ylian Saint - Hilaire
2019-01-04 03:22:15 +03:00
* @ copyright Intel Corporation 2018 - 2019
2018-01-04 23:15:21 +03:00
* @ license Apache - 2.0
2017-08-28 19:27:45 +03:00
* @ version v0 . 0.1
* /
2018-08-30 22:05:23 +03:00
/*jslint node: true */
/*jshint node: true */
/*jshint strict:false */
/*jshint -W097 */
/*jshint esversion: 6 */
"use strict" ;
2018-08-27 22:24:15 +03:00
2018-10-16 03:21:37 +03:00
module . exports . CreateMeshRelay = function ( parent , ws , req , domain , user , cookie ) {
2017-08-28 19:27:45 +03:00
var obj = { } ;
obj . ws = ws ;
2017-09-18 03:22:18 +03:00
obj . req = req ;
2017-08-28 19:27:45 +03:00
obj . peer = null ;
2018-10-16 03:21:37 +03:00
obj . user = user ;
obj . cookie = cookie ;
2017-09-01 21:23:22 +03:00
obj . parent = parent ;
2017-10-24 00:09:58 +03:00
obj . id = req . query . id ;
2017-09-01 21:23:22 +03:00
obj . remoteaddr = obj . ws . _socket . remoteAddress ;
2017-11-01 02:19:58 +03:00
obj . domain = domain ;
2017-09-01 21:23:22 +03:00
if ( obj . remoteaddr . startsWith ( '::ffff:' ) ) { obj . remoteaddr = obj . remoteaddr . substring ( 7 ) ; }
2019-02-19 04:14:00 +03:00
obj . parent . relaySessionCount ++ ;
2017-08-28 19:27:45 +03:00
2018-10-16 20:52:05 +03:00
// Mesh Rights
const MESHRIGHT _EDITMESH = 1 ;
const MESHRIGHT _MANAGEUSERS = 2 ;
const MESHRIGHT _MANAGECOMPUTERS = 4 ;
const MESHRIGHT _REMOTECONTROL = 8 ;
const MESHRIGHT _AGENTCONSOLE = 16 ;
const MESHRIGHT _SERVERFILES = 32 ;
const MESHRIGHT _WAKEDEVICE = 64 ;
const MESHRIGHT _SETNOTES = 128 ;
2018-11-28 04:13:01 +03:00
const MESHRIGHT _REMOTEVIEW = 256 ;
2018-10-16 20:52:05 +03:00
// Site rights
const SITERIGHT _SERVERBACKUP = 1 ;
const SITERIGHT _MANAGEUSERS = 2 ;
const SITERIGHT _SERVERRESTORE = 4 ;
const SITERIGHT _FILEACCESS = 8 ;
const SITERIGHT _SERVERUPDATE = 16 ;
const SITERIGHT _LOCKED = 32 ;
2017-10-24 00:09:58 +03:00
// Disconnect this agent
obj . close = function ( arg ) {
if ( ( arg == 1 ) || ( arg == null ) ) { try { obj . ws . close ( ) ; obj . parent . parent . debug ( 1 , 'Relay: Soft disconnect (' + obj . remoteaddr + ')' ) ; } catch ( e ) { console . log ( e ) ; } } // Soft close, close the websocket
if ( arg == 2 ) { try { obj . ws . _socket . _parent . end ( ) ; obj . parent . parent . debug ( 1 , 'Relay: Hard disconnect (' + obj . remoteaddr + ')' ) ; } catch ( e ) { console . log ( e ) ; } } // Hard close, close the TCP socket
2018-08-30 22:05:23 +03:00
} ;
2017-10-24 00:09:58 +03:00
obj . sendAgentMessage = function ( command , userid , domainid ) {
2019-04-13 00:19:03 +03:00
var rights , mesh ;
2017-10-24 00:09:58 +03:00
if ( command . nodeid == null ) return false ;
var user = obj . parent . users [ userid ] ;
if ( user == null ) return false ;
var splitnodeid = command . nodeid . split ( '/' ) ;
// Check that we are in the same domain and the user has rights over this node.
if ( ( splitnodeid [ 0 ] == 'node' ) && ( splitnodeid [ 1 ] == domainid ) ) {
// Get the user object
// See if the node is connected
var agent = obj . parent . wsagents [ command . nodeid ] ;
if ( agent != null ) {
// Check if we have permission to send a message to that node
2018-08-30 22:05:23 +03:00
rights = user . links [ agent . dbMeshKey ] ;
2019-04-13 00:19:03 +03:00
mesh = parent . meshes [ agent . dbMeshKey ] ;
if ( ( rights != null ) && ( mesh != null ) || ( ( rights & 16 ) != 0 ) ) { // TODO: 16 is console permission, may need more gradular permission checking
2017-10-24 00:09:58 +03:00
command . sessionid = ws . sessionId ; // Set the session id, required for responses.
command . rights = rights . rights ; // Add user rights flags to the message
2019-04-13 00:19:03 +03:00
command . consent = mesh . consent ; // Add user consent
if ( typeof domain . userconsentflags == 'number' ) { command . consent |= domain . userconsentflags ; } // Add server required consent flags
command . username = user . name ; // Add user name
2017-10-24 00:09:58 +03:00
delete command . nodeid ; // Remove the nodeid since it's implyed.
agent . send ( JSON . stringify ( command ) ) ;
return true ;
}
} else {
// Check if a peer server is connected to this agent
var routing = obj . parent . parent . GetRoutingServerId ( command . nodeid , 1 ) ; // 1 = MeshAgent routing type
if ( routing != null ) {
// Check if we have permission to send a message to that node
2018-08-30 22:05:23 +03:00
rights = user . links [ routing . meshid ] ;
2019-04-13 00:19:03 +03:00
mesh = parent . meshes [ routing . meshid ] ;
2017-10-24 00:09:58 +03:00
if ( rights != null || ( ( rights & 16 ) != 0 ) ) { // TODO: 16 is console permission, may need more gradular permission checking
command . fromSessionid = ws . sessionId ; // Set the session id, required for responses.
command . rights = rights . rights ; // Add user rights flags to the message
2019-04-13 00:19:03 +03:00
command . consent = mesh . consent ; // Add user consent
if ( typeof domain . userconsentflags == 'number' ) { command . consent |= domain . userconsentflags ; } // Add server required consent flags
command . username = user . name ; // Add user name
2017-10-24 00:09:58 +03:00
obj . parent . parent . multiServer . DispatchMessageSingleServer ( command , routing . serverid ) ;
return true ;
}
}
}
}
return false ;
2018-08-30 22:05:23 +03:00
} ;
2018-10-16 03:21:37 +03:00
2017-11-01 02:19:58 +03:00
function performRelay ( ) {
if ( obj . id == null ) { try { obj . close ( ) ; } catch ( e ) { } return null ; } // Attempt to connect without id, drop this.
ws . _socket . setKeepAlive ( true , 240000 ) ; // Set TCP keep alive
2017-10-24 00:09:58 +03:00
2018-12-02 10:41:57 +03:00
// If this is a MeshMessenger session, the ID is the two userid's and authentication must match one of them.
if ( obj . id . startsWith ( 'meshmessenger/' ) ) {
2019-01-24 23:08:48 +03:00
if ( ( obj . id . startsWith ( 'meshmessenger/user/' ) == true ) && ( obj . user == null ) ) { try { obj . close ( ) ; } catch ( e ) { } return null ; } // If user-to-user, both sides need to be authenticated.
2018-12-02 10:41:57 +03:00
var x = obj . id . split ( '/' ) , user1 = x [ 1 ] + '/' + x [ 2 ] + '/' + x [ 3 ] , user2 = x [ 4 ] + '/' + x [ 5 ] + '/' + x [ 6 ] ;
2018-12-08 03:36:27 +03:00
if ( ( x [ 1 ] != 'user' ) && ( x [ 4 ] != 'user' ) ) { try { obj . close ( ) ; } catch ( e ) { } return null ; } // MeshMessenger session must have at least one authenticated user
if ( ( x [ 1 ] == 'user' ) && ( x [ 4 ] == 'user' ) ) {
// If this is a user-to-user session, you must be authenticated to join.
if ( ( obj . user . _id != user1 ) && ( obj . user . _id != user2 ) ) { try { obj . close ( ) ; } catch ( e ) { } return null ; }
} else {
// If only one side of the session is a user
// !!!!! TODO: Need to make sure that one of the two sides is the correct user. !!!!!
}
2018-12-02 10:41:57 +03:00
}
2017-11-01 02:19:58 +03:00
// Validate that the id is valid, we only need to do this on non-authenticated sessions.
// TODO: Figure out when this needs to be done.
/ *
if ( ! parent . args . notls ) {
// Check the identifier, if running without TLS, skip this.
var ids = obj . id . split ( ':' ) ;
if ( ids . length != 3 ) { obj . ws . close ( ) ; obj . id = null ; return null ; } // Invalid ID, drop this.
if ( parent . crypto . createHmac ( 'SHA384' , parent . relayRandom ) . update ( ids [ 0 ] + ':' + ids [ 1 ] ) . digest ( 'hex' ) != ids [ 2 ] ) { obj . ws . close ( ) ; obj . id = null ; return null ; } // Invalid HMAC, drop this.
if ( ( Date . now ( ) - parseInt ( ids [ 1 ] ) ) > 120000 ) { obj . ws . close ( ) ; obj . id = null ; return null ; } // Expired time, drop this.
obj . id = ids [ 0 ] ;
}
* /
// Check the peer connection status
{
var relayinfo = parent . wsrelays [ obj . id ] ;
if ( relayinfo ) {
if ( relayinfo . state == 1 ) {
// Check that at least one connection is authenticated
if ( ( obj . authenticated != true ) && ( relayinfo . peer1 . authenticated != true ) ) {
obj . id = null ;
obj . ws . close ( ) ;
obj . parent . parent . debug ( 1 , 'Relay without-auth: ' + obj . id + ' (' + obj . remoteaddr + ')' ) ;
return null ;
}
// Connect to peer
obj . peer = relayinfo . peer1 ;
obj . peer . peer = obj ;
relayinfo . peer2 = obj ;
relayinfo . state = 2 ;
obj . ws . send ( 'c' ) ; // Send connect to both peers
relayinfo . peer1 . ws . send ( 'c' ) ;
2018-12-01 08:23:10 +03:00
relayinfo . peer1 . ws . _socket . resume ( ) ; // Release the traffic
relayinfo . peer2 . ws . _socket . resume ( ) ; // Release the traffic
2017-08-28 19:27:45 +03:00
2017-11-01 02:19:58 +03:00
relayinfo . peer1 . ws . peer = relayinfo . peer2 . ws ;
relayinfo . peer2 . ws . peer = relayinfo . peer1 . ws ;
obj . parent . parent . debug ( 1 , 'Relay connected: ' + obj . id + ' (' + obj . remoteaddr + ' --> ' + obj . peer . remoteaddr + ')' ) ;
} else {
// Connected already, drop (TODO: maybe we should re-connect?)
2017-09-06 20:45:09 +03:00
obj . id = null ;
obj . ws . close ( ) ;
2017-11-01 02:19:58 +03:00
obj . parent . parent . debug ( 1 , 'Relay duplicate: ' + obj . id + ' (' + obj . remoteaddr + ')' ) ;
2017-09-06 20:45:09 +03:00
return null ;
}
2017-08-28 19:27:45 +03:00
} else {
2017-11-01 02:19:58 +03:00
// Wait for other relay connection
2018-12-01 08:23:10 +03:00
ws . _socket . pause ( ) ; // Hold traffic until the other connection
2017-11-01 02:19:58 +03:00
parent . wsrelays [ obj . id ] = { peer1 : obj , state : 1 } ;
2018-08-30 22:05:23 +03:00
obj . parent . parent . debug ( 1 , 'Relay holding: ' + obj . id + ' (' + obj . remoteaddr + ') ' + ( obj . authenticated ? 'Authenticated' : '' ) ) ;
2017-09-18 03:22:18 +03:00
2017-11-01 02:19:58 +03:00
// Check if a peer server has this connection
if ( parent . parent . multiServer != null ) {
var rsession = obj . parent . wsPeerRelays [ obj . id ] ;
if ( ( rsession != null ) && ( rsession . serverId > obj . parent . parent . serverId ) ) {
// We must initiate the connection to the peer
parent . parent . multiServer . createPeerRelay ( ws , req , rsession . serverId , req . session . userid ) ;
delete parent . wsrelays [ obj . id ] ;
} else {
// Send message to other peers that we have this connection
parent . parent . multiServer . DispatchMessage ( JSON . stringify ( { action : 'relay' , id : obj . id } ) ) ;
}
2017-09-18 03:22:18 +03:00
}
}
2017-08-28 19:27:45 +03:00
}
}
2018-01-17 04:30:34 +03:00
2018-12-12 04:52:37 +03:00
ws . flushSink = function ( ) { try { ws . _socket . resume ( ) ; } catch ( ex ) { console . log ( ex ) ; } } ;
2017-08-28 19:27:45 +03:00
// When data is received from the mesh relay web socket
2017-09-01 21:23:22 +03:00
ws . on ( 'message' , function ( data ) {
2017-12-19 19:50:19 +03:00
//console.log(typeof data, data.length);
2018-01-17 04:30:34 +03:00
if ( this . peer != null ) {
2018-07-06 20:13:19 +03:00
//if (typeof data == 'string') { console.log('Relay: ' + data); } else { console.log('Relay:' + data.length + ' byte(s)'); }
2018-12-12 04:52:37 +03:00
try {
this . _socket . pause ( ) ;
this . peer . send ( data , ws . flushSink ) ;
} catch ( ex ) { console . log ( ex ) ; }
2018-01-17 04:30:34 +03:00
}
2017-09-01 21:23:22 +03:00
} ) ;
2017-08-28 19:27:45 +03:00
2019-01-03 05:34:50 +03:00
// If error, close both sides of the relay.
2018-12-12 04:52:37 +03:00
ws . on ( 'error' , function ( err ) {
2019-02-19 04:14:00 +03:00
obj . parent . relaySessionErrorCount ++ ;
2019-01-03 05:03:34 +03:00
console . log ( 'Relay error from ' + obj . remoteaddr + ', ' + err . toString ( ) . split ( '\r' ) [ 0 ] + '.' ) ;
closeBothSides ( ) ;
2018-12-12 04:52:37 +03:00
} ) ;
2017-08-28 19:27:45 +03:00
2019-01-03 05:34:50 +03:00
// If the relay web socket is closed, close both sides.
2017-08-28 19:27:45 +03:00
ws . on ( 'close' , function ( req ) {
2019-02-19 04:14:00 +03:00
obj . parent . relaySessionCount -- ;
2019-01-03 05:03:34 +03:00
closeBothSides ( ) ;
} ) ;
// Close both our side and the peer side.
function closeBothSides ( ) {
2017-08-28 19:27:45 +03:00
if ( obj . id != null ) {
var relayinfo = parent . wsrelays [ obj . id ] ;
2017-09-18 03:22:18 +03:00
if ( relayinfo != null ) {
if ( relayinfo . state == 2 ) {
// Disconnect the peer
var peer = ( relayinfo . peer1 == obj ) ? relayinfo . peer2 : relayinfo . peer1 ;
obj . parent . parent . debug ( 1 , 'Relay disconnect: ' + obj . id + ' (' + obj . remoteaddr + ' --> ' + peer . remoteaddr + ')' ) ;
peer . id = null ;
try { peer . ws . close ( ) ; } catch ( e ) { } // Soft disconnect
try { peer . ws . _socket . _parent . end ( ) ; } catch ( e ) { } // Hard disconnect
} else {
obj . parent . parent . debug ( 1 , 'Relay disconnect: ' + obj . id + ' (' + obj . remoteaddr + ')' ) ;
}
delete parent . wsrelays [ obj . id ] ;
2017-08-28 19:27:45 +03:00
}
obj . peer = null ;
obj . id = null ;
}
2019-01-03 05:03:34 +03:00
}
2018-08-30 22:05:23 +03:00
2018-10-16 20:52:05 +03:00
// Mark this relay session as authenticated if this is the user end.
obj . authenticated = ( obj . user != null ) ;
if ( obj . authenticated ) {
// Kick off the routing, if we have agent routing instructions, process them here.
// Routing instructions can only be given by a authenticated user
if ( ( obj . cookie != null ) && ( obj . cookie . nodeid != null ) && ( obj . cookie . tcpport != null ) && ( obj . cookie . domainid != null ) ) {
// We have routing instructions in the cookie, but first, check user access for this node.
obj . parent . db . Get ( obj . cookie . nodeid , function ( err , docs ) {
if ( docs . length == 0 ) { console . log ( 'ERR: Node not found' ) ; try { obj . close ( ) ; } catch ( e ) { } return ; } // Disconnect websocket
var node = docs [ 0 ] ;
// Check if this user has permission to manage this computer
var meshlinks = obj . user . links [ node . meshid ] ;
if ( ( ! meshlinks ) || ( ! meshlinks . rights ) || ( ( meshlinks . rights & MESHRIGHT _REMOTECONTROL ) == 0 ) ) { console . log ( 'ERR: Access denied (2)' ) ; try { obj . close ( ) ; } catch ( e ) { } return ; }
// Send connection request to agent
if ( obj . id == undefined ) { obj . id = ( '' + Math . random ( ) ) . substring ( 2 ) ; } // If there is no connection id, generate one.
var command = { nodeid : obj . cookie . nodeid , action : 'msg' , type : 'tunnel' , value : '*/meshrelay.ashx?id=' + obj . id , tcpport : obj . cookie . tcpport , tcpaddr : obj . cookie . tcpaddr } ;
obj . parent . parent . debug ( 1 , 'Relay: Sending agent tunnel command: ' + JSON . stringify ( command ) ) ;
if ( obj . sendAgentMessage ( command , obj . user . _id , obj . cookie . domainid ) == false ) { obj . id = null ; obj . parent . parent . debug ( 1 , 'Relay: Unable to contact this agent (' + obj . remoteaddr + ')' ) ; }
performRelay ( ) ;
} ) ;
return obj ;
} else if ( ( req . query . nodeid != null ) && ( req . query . tcpport != null ) ) {
// We have routing instructions in the URL arguments, but first, check user access for this node.
obj . parent . db . Get ( req . query . nodeid , function ( err , docs ) {
if ( docs . length == 0 ) { console . log ( 'ERR: Node not found' ) ; try { obj . close ( ) ; } catch ( e ) { } return ; } // Disconnect websocket
var node = docs [ 0 ] ;
// Check if this user has permission to manage this computer
var meshlinks = obj . user . links [ node . meshid ] ;
if ( ( ! meshlinks ) || ( ! meshlinks . rights ) || ( ( meshlinks . rights & MESHRIGHT _REMOTECONTROL ) == 0 ) ) { console . log ( 'ERR: Access denied (2)' ) ; try { obj . close ( ) ; } catch ( e ) { } return ; }
// Send connection request to agent
if ( obj . id == null ) { obj . id = ( '' + Math . random ( ) ) . substring ( 2 ) ; } // If there is no connection id, generate one.
var command = { nodeid : req . query . nodeid , action : 'msg' , type : 'tunnel' , value : '*/meshrelay.ashx?id=' + obj . id , tcpport : req . query . tcpport , tcpaddr : ( ( req . query . tcpaddr == null ) ? '127.0.0.1' : req . query . tcpaddr ) } ;
obj . parent . parent . debug ( 1 , 'Relay: Sending agent tunnel command: ' + JSON . stringify ( command ) ) ;
if ( obj . sendAgentMessage ( command , obj . user . _id , obj . domain . id ) == false ) { obj . id = null ; obj . parent . parent . debug ( 1 , 'Relay: Unable to contact this agent (' + obj . remoteaddr + ')' ) ; }
performRelay ( ) ;
} ) ;
return obj ;
}
}
// If this is not an authenticated session, or the session does not have routing instructions, just go ahead an connect to existing session.
performRelay ( ) ;
2017-08-28 19:27:45 +03:00
return obj ;
2018-08-30 22:05:23 +03:00
} ;