2020-04-24 09:19:28 +03:00
/ * *
* @ description MeshCentral remote desktop multiplexor
* @ author Ylian Saint - Hilaire
* @ copyright Intel Corporation 2018 - 2020
* @ license Apache - 2.0
* @ version v0 . 0.1
* /
/*jslint node: true */
/*jshint node: true */
/*jshint strict:false */
/*jshint -W097 */
/*jshint esversion: 6 */
"use strict" ;
2020-04-25 04:24:24 +03:00
/ *
-- - KVM Commands -- -
MNG _KVM _NOP = 0 ,
MNG _KVM _KEY = 1 ,
MNG _KVM _MOUSE = 2 ,
MNG _KVM _MOUSE _CURSOR = 88 ,
MNG _KVM _MOUSE _MOVE = 89 ,
MNG _KVM _PICTURE = 3 ,
MNG _KVM _COPY = 4 ,
MNG _KVM _COMPRESSION = 5 ,
MNG _KVM _REFRESH = 6 ,
MNG _KVM _SCREEN = 7 ,
MNG _KVM _PAUSE = 8 ,
MNG _TERMTEXT = 9 ,
MNG _CTRLALTDEL = 10 ,
MNG _KVM _GET _DISPLAYS = 11 ,
MNG _KVM _SET _DISPLAY = 12 ,
MNG _KVM _FRAME _RATE _TIMER = 13 ,
MNG _KVM _INIT _TOUCH = 14 ,
MNG _KVM _TOUCH = 15 ,
MNG _KVM _CONNECTCOUNT = 16 ,
MNG _KVM _MESSAGE = 17 ,
MNG _ECHO = 21 ,
MNG _JUMBO = 27 ,
MNG _GETDIR = 50 ,
MNG _FILEMOVE = 51 ,
MNG _FILEDELETE = 52 ,
MNG _FILECOPY = 53 ,
MNG _FILECREATEDIR = 54 ,
MNG _FILETRANSFER = 55 ,
MNG _FILEUPLOAD = 56 ,
MNG _FILESEARCH = 57 ,
MNG _FILETRANSFER2 = 58 ,
MNG _KVM _DISCONNECT = 59 ,
MNG _GETDIR2 = 60 , // Same as MNG_GETDIR but with date/time.
MNG _FILEUPLOAD2 = 61 , // Used for slot based fast upload.
MNG _FILEDELETEREC = 62 , // Same as MNG_FILEDELETE but recursive
MNG _USERCONSENT = 63 , // Used to notify management console of user consent state
MNG _DEBUG = 64 , // Debug/Logging Message for ILibRemoteLogging
MNG _ERROR = 65 ,
MNG _ENCAPSULATE _AGENT _COMMAND = 70
* /
2020-04-28 22:42:58 +03:00
function CreateDesktopMultiplexor ( parent , domain , nodeid , func ) {
2020-04-24 09:19:28 +03:00
var obj = { } ;
2020-04-28 22:42:58 +03:00
obj . nodeid = nodeid ;
2020-04-28 03:39:21 +03:00
obj . parent = parent ;
2020-04-27 23:58:58 +03:00
obj . agent = null ; // Reference to the connection object that is the agent.
obj . viewers = [ ] ; // Array of references to all viewers.
2020-05-02 08:08:49 +03:00
obj . viewersOverflowCount = 0 ; // Number of viewers currently in overflow state.
2020-04-27 23:58:58 +03:00
obj . width = 0 ; // Current width of the display in pixels.
obj . height = 0 ; // Current height of the display in pixels.
obj . swidth = 0 ; // Current width of the display in tiles.
obj . sheight = 0 ; // Current height of the display in tiles.
obj . screen = null ; // The main screen, (x * y) --> tile index. Indicates this image is covering each tile on the screen.
obj . counter = 1 ; // The main counter, used as index for the obj.images table when now images come in.
obj . imagesCount = 0 ; // Total number of images in the obj.images table.
obj . imagesCounters = { } ; // Main table of indexes --> tile count, the number of tiles still using this image.
obj . images = { } ; // Main table of indexes --> image data object.
obj . lastScreenSizeCmd = null ; // Pointer to the last screen size command from the agent.
obj . lastScreenSizeCounter = 0 ; // Index into the image table of the screen size command, this is generally also the first command.
2020-04-28 22:42:58 +03:00
obj . lastConsoleMessage = null ; // Last agent console message.
2020-04-27 23:58:58 +03:00
obj . firstData = null ; // Index in the image table of the first image in the table, generally this points to the display resolution command.
obj . lastData = null ; // Index in the images table of the last image in the table.
obj . lastDisplayInfoData = null ; // Pointer to the last display information command from the agent (Number of displays).
obj . desktopPaused = true ; // Current desktop pause state, it's true if all viewers are paused.
obj . imageCompression = 50 ; // Current image compression, this is the highest value of all viewers.
obj . imageScaling = 1024 ; // Current image scaling, this is the highest value of all viewers.
obj . imageFrameRate = 50 ; // Current framerate setting, this is the lowest values of all viewers.
obj . protocolOptions = null ; // Set to the protocol options of the first viewer that connected.
obj . viewerConnected = false ; // Set to true if one viewer attempted to connect to the agent.
2020-04-28 03:39:21 +03:00
obj . recordingFile = null ; // Present if we are recording to file.
2020-05-05 23:00:51 +03:00
obj . recordingFileSize = 0 ; // Current size of the recording file.
2020-04-28 03:39:21 +03:00
obj . recordingFileWriting = false ; // Set to true is we are in the process if writing to the recording file.
2020-04-28 22:42:58 +03:00
obj . startTime = null ; // Starting time of the multiplex session.
2020-05-05 23:00:51 +03:00
obj . userIds = [ ] ; // List of userid's that have intertracted with this session.
2020-04-27 23:58:58 +03:00
// Add an agent or viewer
2020-04-25 04:24:24 +03:00
obj . addPeer = function ( peer ) {
2020-05-11 21:41:02 +03:00
if ( obj . viewers == null ) return ;
2020-04-25 04:24:24 +03:00
if ( peer . req . query . browser ) {
2020-04-28 22:42:58 +03:00
//console.log('addPeer-viewer', obj.nodeid);
2020-04-28 02:25:54 +03:00
// Setup the viewer
2020-04-25 04:24:24 +03:00
if ( obj . viewers . indexOf ( peer ) >= 0 ) return true ;
obj . viewers . push ( peer ) ;
peer . desktopPaused = true ;
peer . imageCompression = 30 ;
peer . imageScaling = 1024 ;
peer . imageFrameRate = 100 ;
2020-04-27 23:58:58 +03:00
peer . lastImageNumberSent = null ;
2020-04-25 04:24:24 +03:00
peer . dataPtr = obj . firstData ;
2020-04-27 23:58:58 +03:00
peer . sending = false ;
2020-05-02 08:08:49 +03:00
peer . overflow = false ;
2020-04-27 23:58:58 +03:00
peer . sendQueue = [ ] ;
2020-04-28 02:25:54 +03:00
peer . paused = false ;
2020-05-05 23:00:51 +03:00
// Add the user to the userids list if needed
if ( ( peer . user != null ) && ( obj . userIds . indexOf ( peer . user . _id ) == - 1 ) ) { obj . userIds . push ( peer . user . _id ) ; }
2020-04-28 23:22:49 +03:00
// Setup slow relay is requested. This will show down sending any data to this viewer.
if ( ( peer . req . query . slowrelay != null ) ) {
var sr = null ;
try { sr = parseInt ( peer . req . query . slowrelay ) ; } catch ( ex ) { }
if ( ( typeof sr == 'number' ) && ( sr > 0 ) && ( sr < 1000 ) ) { peer . slowRelay = sr ; }
}
2020-04-28 02:25:54 +03:00
// Indicated we are connected
2020-04-28 03:54:15 +03:00
obj . sendToViewer ( peer , obj . recordingFile ? 'cr' : 'c' ) ;
2020-04-28 22:42:58 +03:00
// If the agent sent display information or console message, send it to the viewer
if ( obj . lastDisplayInfoData != null ) { obj . sendToViewer ( peer , obj . lastDisplayInfoData ) ; }
if ( obj . lastConsoleMessage != null ) { obj . sendToViewer ( peer , obj . lastConsoleMessage ) ; }
// Log joining the multiplex session
if ( obj . startTime != null ) {
var event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : peer . user . _id , username : peer . user . name , msg : "Joined desktop multiplex session" , protocol : 2 } ;
2020-05-05 23:00:51 +03:00
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , peer . user . _id , obj . meshid ] , obj , event ) ;
2020-04-28 22:42:58 +03:00
}
2020-05-07 09:23:27 +03:00
// Send an updated list of all peers to all viewers
obj . sendSessionMetadata ( ) ;
2020-04-25 04:24:24 +03:00
} else {
2020-04-28 22:42:58 +03:00
//console.log('addPeer-agent', obj.nodeid);
2020-04-25 04:24:24 +03:00
if ( obj . agent != null ) return false ;
// Setup the agent
2020-04-28 02:25:54 +03:00
obj . agent = peer ;
2020-04-27 23:58:58 +03:00
peer . sending = false ;
2020-05-02 08:08:49 +03:00
peer . overflow = false ;
2020-04-27 23:58:58 +03:00
peer . sendQueue = [ ] ;
2020-04-28 02:25:54 +03:00
peer . paused = false ;
2020-04-27 23:58:58 +03:00
2020-04-28 02:25:54 +03:00
// Indicated we are connected and send connection options and protocol if needed
2020-04-28 03:54:15 +03:00
obj . sendToAgent ( obj . recordingFile ? 'cr' : 'c' ) ;
2020-04-27 23:58:58 +03:00
if ( obj . viewerConnected == true ) {
if ( obj . protocolOptions != null ) { obj . sendToAgent ( JSON . stringify ( obj . protocolOptions ) ) ; } // Send connection options
obj . sendToAgent ( '2' ) ; // Send remote desktop connect
}
2020-04-25 04:24:24 +03:00
}
2020-04-28 22:42:58 +03:00
// Log multiplex session start
if ( ( obj . agent != null ) && ( obj . viewers . length > 0 ) && ( obj . startTime == null ) ) {
var event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : obj . viewers [ 0 ] . user . _id , username : obj . viewers [ 0 ] . user . name , msg : "Started desktop multiplex session" , protocol : 2 } ;
2020-05-05 23:00:51 +03:00
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , obj . viewers [ 0 ] . user . _id , obj . meshid ] , obj , event ) ;
2020-04-28 22:42:58 +03:00
obj . startTime = Date . now ( ) ;
}
2020-04-25 04:24:24 +03:00
return true ;
}
2020-04-27 23:58:58 +03:00
// Remove an agent or viewer
2020-04-28 02:25:54 +03:00
// Return true if this multiplexor is no longer needed.
2020-04-25 04:24:24 +03:00
obj . removePeer = function ( peer ) {
2020-05-11 21:41:02 +03:00
if ( obj . viewers == null ) return ;
2020-04-28 02:25:54 +03:00
if ( peer == obj . agent ) {
2020-04-28 22:42:58 +03:00
//console.log('removePeer-agent', obj.nodeid);
2020-04-25 04:24:24 +03:00
// Clean up the agent
2020-04-28 02:25:54 +03:00
obj . agent = null ;
2020-04-25 04:24:24 +03:00
// Agent has disconnected, disconnect everyone.
2020-05-07 09:23:27 +03:00
if ( obj . viewers != null ) { for ( var i in obj . viewers ) { obj . viewers [ i ] . close ( ) ; } }
2020-04-28 02:25:54 +03:00
dispose ( ) ;
return true ;
2020-04-25 04:24:24 +03:00
} else {
2020-04-28 22:42:58 +03:00
//console.log('removePeer-viewer', obj.nodeid);
2020-04-25 04:24:24 +03:00
// Remove a viewer
2020-05-07 09:23:27 +03:00
if ( obj . viewers != null ) {
var i = obj . viewers . indexOf ( peer ) ;
if ( i == - 1 ) return false ;
obj . viewers . splice ( i , 1 ) ;
}
2020-04-25 04:24:24 +03:00
2020-05-02 08:08:49 +03:00
// Resume flow control if this was the peer that was limiting traffic (because it was the fastest one).
if ( peer . overflow == true ) {
obj . viewersOverflowCount -- ;
peer . overflow = false ;
if ( ( obj . viewersOverflowCount < obj . viewers . length ) && ( obj . recordingFileWriting == false ) && obj . agent && ( obj . agent . paused == true ) ) { obj . agent . paused = false ; obj . agent . ws . _socket . resume ( ) ; }
}
2020-04-28 02:25:54 +03:00
// Aggressive clean up of the viewer
delete peer . desktopPaused ;
delete peer . imageCompression ;
delete peer . imageScaling ;
delete peer . imageFrameRate ;
delete peer . lastImageNumberSent ;
delete peer . dataPtr ;
delete peer . sending ;
2020-05-02 08:08:49 +03:00
delete peer . overflow ;
2020-04-28 02:25:54 +03:00
delete peer . sendQueue ;
2020-04-28 22:42:58 +03:00
// Log leaving the multiplex session
if ( obj . startTime != null ) {
var event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : peer . user . _id , username : peer . user . name , msg : "Left the desktop multiplex session" , protocol : 2 } ;
2020-05-05 23:00:51 +03:00
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , peer . user . _id , obj . meshid ] , obj , event ) ;
2020-04-28 22:42:58 +03:00
}
2020-04-28 02:25:54 +03:00
// If this is the last viewer, disconnect the agent
2020-05-11 11:17:18 +03:00
if ( ( obj . viewers != null ) && ( obj . viewers . length == 0 ) && ( obj . agent != null ) ) { obj . agent . close ( ) ; dispose ( ) ; return true ; }
2020-05-07 09:23:27 +03:00
// Send an updated list of all peers to all viewers
obj . sendSessionMetadata ( ) ;
2020-04-25 04:24:24 +03:00
}
2020-04-28 02:25:54 +03:00
return false ;
}
// Clean up ourselves
function dispose ( ) {
2020-05-01 04:26:53 +03:00
if ( obj . viewers == null ) return ;
2020-04-28 22:42:58 +03:00
//console.log('dispose', obj.nodeid);
2020-04-28 02:25:54 +03:00
delete obj . viewers ;
delete obj . imagesCounters ;
delete obj . images ;
2020-04-28 03:39:21 +03:00
// Close the recording file if needed
if ( obj . recordingFile != null ) {
2020-05-05 23:00:51 +03:00
// Compute session length
if ( obj . startTime != null ) { obj . sessionStart = obj . startTime ; obj . sessionLength = Math . round ( ( Date . now ( ) - obj . startTime ) / 1000 ) ; }
// Write the last record of the recording file
2020-04-28 03:39:21 +03:00
var rf = obj . recordingFile ;
delete obj . recordingFile ;
recordingEntry ( rf . fd , 3 , 0 , 'MeshCentralMCREC' , function ( fd , filename ) {
parent . parent . fs . close ( fd ) ;
2020-05-05 23:00:51 +03:00
2020-04-28 03:39:21 +03:00
// Now that the recording file is closed, check if we need to index this file.
if ( domain . sessionrecording . index !== false ) { parent . parent . certificateOperations . acceleratorPerformOperation ( 'indexMcRec' , filename ) ; }
2020-05-05 23:00:51 +03:00
// Add a event entry about this recording
var basefile = parent . parent . path . basename ( filename ) ;
var event = { etype : 'relay' , action : 'recording' , domain : domain . id , nodeid : obj . nodeid , msg : "Finished recording session" + ( obj . sessionLength ? ( ', ' + obj . sessionLength + ' second(s)' ) : '' ) , filename : basefile , size : obj . recordingFileSize , protocol : 2 , icon : obj . icon , name : obj . name , meshid : obj . meshid , userids : obj . userIds , multiplex : true } ;
var mesh = parent . meshes [ obj . meshid ] ;
if ( mesh != null ) { event . meshname = mesh . name ; }
if ( obj . sessionStart ) { event . startTime = obj . sessionStart ; event . lengthTime = obj . sessionLength ; }
parent . parent . DispatchEvent ( [ '*' , 'recording' , obj . nodeid , obj . meshid ] , obj , event ) ;
2020-04-30 12:02:23 +03:00
cleanUpRecordings ( ) ;
2020-04-28 03:39:21 +03:00
} , rf . filename ) ;
}
2020-04-28 22:42:58 +03:00
// Log end of multiplex session
if ( obj . startTime != null ) {
var event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , msg : "Closed desktop multiplex session" + ', ' + Math . floor ( ( Date . now ( ) - obj . startTime ) / 1000 ) + ' second(s)' , protocol : 2 } ;
2020-05-05 23:00:51 +03:00
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , obj . meshid ] , obj , event ) ;
2020-04-28 22:42:58 +03:00
obj . startTime = null ;
}
2020-05-01 04:26:53 +03:00
2020-05-12 04:44:46 +03:00
// Send an updated list of all peers to all viewers
obj . sendSessionMetadata ( ) ;
2020-05-01 04:26:53 +03:00
parent . parent . debug ( 'relay' , 'DesktopRelay: Disposing desktop multiplexor' ) ;
2020-04-25 04:24:24 +03:00
}
2020-04-27 23:58:58 +03:00
// Send data to the agent or queue it up for sending
obj . sendToAgent = function ( data ) {
2020-05-11 21:41:02 +03:00
if ( ( obj . viewers == null ) || ( obj . agent == null ) ) return ;
2020-04-27 23:58:58 +03:00
//console.log('SendToAgent', data.length);
if ( obj . agent . sending ) {
obj . agent . sendQueue . push ( data ) ;
2020-04-28 02:25:54 +03:00
2020-05-02 08:08:49 +03:00
// Flow control, pause all viewers is the queue is backing up
if ( obj . agent . sendQueue > 10 ) {
obj . agent . overflow = true ;
for ( var i in obj . viewers ) {
var v = obj . viewers [ i ] ;
if ( v . paused == false ) { v . paused = true ; v . ws . _socket . pause ( ) ; }
}
2020-04-28 02:25:54 +03:00
}
2020-05-02 08:08:49 +03:00
} else {
obj . agent . ws . send ( data , sendAgentNext ) ;
2020-04-27 23:58:58 +03:00
}
}
// Send more data to the agent
function sendAgentNext ( ) {
2020-05-11 21:41:02 +03:00
if ( ( obj . viewers == null ) || ( obj . agent == null ) ) return ;
2020-04-27 23:58:58 +03:00
if ( obj . agent . sendQueue . length > 0 ) {
// Send from the pending send queue
obj . agent . ws . send ( obj . agent . sendQueue . shift ( ) , sendAgentNext ) ;
} else {
// Nothing to send
obj . agent . sending = false ;
2020-04-28 02:25:54 +03:00
// Flow control, resume all viewers
2020-05-02 08:08:49 +03:00
if ( obj . agent . overflow == true ) {
obj . agent . overflow = false ;
for ( var i in obj . viewers ) {
var v = obj . viewers [ i ] ;
if ( v . paused == true ) { v . paused = false ; v . ws . _socket . resume ( ) ; }
}
2020-04-28 02:25:54 +03:00
}
2020-04-27 23:58:58 +03:00
}
}
2020-05-12 02:30:18 +03:00
// Send the list of all users currently vieweing this session to all viewers and servers
2020-05-07 09:23:27 +03:00
obj . sendSessionMetadata = function ( ) {
var allUsers = { } ;
2020-05-12 04:44:46 +03:00
if ( obj . viewers != null ) {
for ( var i in obj . viewers ) { var v = obj . viewers [ i ] ; if ( ( v . user != null ) && ( v . user . _id != null ) ) { if ( allUsers [ v . user . _id ] == null ) { allUsers [ v . user . _id ] = 1 ; } else { allUsers [ v . user . _id ] ++ ; } } }
obj . sendToAllViewers ( JSON . stringify ( { type : 'metadata' , 'ctrlChannel' : '102938' , users : allUsers , startTime : obj . startTime } ) ) ;
}
2020-05-12 02:30:18 +03:00
2020-05-12 04:44:46 +03:00
// Update the sessions attached the to agent
if ( obj . nodeid != null ) {
const xagent = parent . wsagents [ obj . nodeid ] ;
if ( xagent != null ) {
if ( xagent . sessions == null ) { xagent . sessions = { } ; }
xagent . sessions . multidesk = allUsers ;
xagent . updateSessions ( ) ;
}
}
2020-05-07 09:23:27 +03:00
}
2020-04-27 23:58:58 +03:00
// Send this command to all viewers
obj . sendToAllViewers = function ( data ) {
2020-05-11 21:41:02 +03:00
if ( obj . viewers == null ) return ;
2020-04-27 23:58:58 +03:00
for ( var i in obj . viewers ) { obj . sendToViewer ( obj . viewers [ i ] , data ) ; }
}
// Send data to the viewer or queue it up for sending
obj . sendToViewer = function ( viewer , data ) {
2020-05-11 21:41:02 +03:00
if ( ( viewer == null ) || ( obj . viewers == null ) ) return ;
2020-04-27 23:58:58 +03:00
//console.log('SendToViewer', data.length);
if ( viewer . sending ) {
viewer . sendQueue . push ( data ) ;
} else {
viewer . sending = true ;
2020-04-28 23:22:49 +03:00
if ( viewer . slowRelay ) {
setTimeout ( function ( ) { try { viewer . ws . send ( data , function ( ) { sendViewerNext ( viewer ) ; } ) ; } catch ( ex ) { } } , viewer . slowRelay ) ;
} else {
try { viewer . ws . send ( data , function ( ) { sendViewerNext ( viewer ) ; } ) ; } catch ( ex ) { }
}
2020-04-28 02:25:54 +03:00
// Flow control, pause the agent if needed
2020-05-02 08:08:49 +03:00
checkViewerOverflow ( viewer ) ;
}
}
// Check if a viewer is in overflow situation
function checkViewerOverflow ( viewer ) {
2020-05-11 21:41:02 +03:00
if ( ( viewer . overflow == true ) || ( obj . viewers == null ) ) return ;
2020-05-02 08:08:49 +03:00
if ( ( viewer . sendQueue . length > 5 ) || ( ( viewer . dataPtr != null ) && ( viewer . dataPtr != obj . lastData ) ) ) {
viewer . overflow = true ;
obj . viewersOverflowCount ++ ;
if ( ( obj . viewersOverflowCount >= obj . viewers . length ) && obj . agent && ( obj . agent . paused == false ) ) { obj . agent . paused = true ; obj . agent . ws . _socket . pause ( ) ; }
}
}
// Check if a viewer is in underflow situation
function checkViewerUnderflow ( viewer ) {
2020-05-11 21:41:02 +03:00
if ( ( viewer . overflow == false ) || ( obj . viewers == null ) ) return ;
2020-05-02 08:08:49 +03:00
if ( ( viewer . sendQueue . length <= 5 ) && ( ( viewer . dataPtr == null ) || ( viewer . dataPtr == obj . lastData ) ) ) {
viewer . overflow = false ;
obj . viewersOverflowCount -- ;
if ( ( obj . viewersOverflowCount < obj . viewers . length ) && ( obj . recordingFileWriting == false ) && obj . agent && ( obj . agent . paused == true ) ) { obj . agent . paused = false ; obj . agent . ws . _socket . resume ( ) ; }
2020-04-27 23:58:58 +03:00
}
}
// Send more data to the viewer
function sendViewerNext ( viewer ) {
2020-05-11 21:41:02 +03:00
if ( ( viewer . sendQueue == null ) || ( obj . viewers == null ) ) return ;
2020-04-27 23:58:58 +03:00
if ( viewer . sendQueue . length > 0 ) {
// Send from the pending send queue
2020-05-02 08:08:49 +03:00
if ( viewer . sending == false ) { viewer . sending = true ; }
2020-04-28 23:22:49 +03:00
if ( viewer . slowRelay ) {
setTimeout ( function ( ) { try { viewer . ws . send ( viewer . sendQueue . shift ( ) , function ( ) { sendViewerNext ( viewer ) ; } ) ; } catch ( ex ) { } } , viewer . slowRelay ) ;
} else {
try { viewer . ws . send ( viewer . sendQueue . shift ( ) , function ( ) { sendViewerNext ( viewer ) ; } ) ; } catch ( ex ) { }
}
2020-05-02 08:08:49 +03:00
checkViewerOverflow ( viewer ) ;
2020-04-27 23:58:58 +03:00
} else {
if ( viewer . dataPtr != null ) {
// Send the next image
2020-04-28 03:54:15 +03:00
//if ((viewer.lastImageNumberSent != null) && ((viewer.lastImageNumberSent + 1) != (viewer.dataPtr))) { console.log('SVIEW-S1', viewer.lastImageNumberSent, viewer.dataPtr); } // DEBUG
2020-04-27 23:58:58 +03:00
var image = obj . images [ viewer . dataPtr ] ;
viewer . lastImageNumberSent = viewer . dataPtr ;
2020-04-28 03:54:15 +03:00
//if ((image.next != null) && ((viewer.dataPtr + 1) != image.next)) { console.log('SVIEW-S2', viewer.dataPtr, image.next); } // DEBUG
2020-04-27 23:58:58 +03:00
viewer . dataPtr = image . next ;
2020-04-28 23:22:49 +03:00
if ( viewer . slowRelay ) {
setTimeout ( function ( ) { try { viewer . ws . send ( image . data , function ( ) { sendViewerNext ( viewer ) ; } ) ; } catch ( ex ) { } } , viewer . slowRelay ) ;
} else {
try { viewer . ws . send ( image . data , function ( ) { sendViewerNext ( viewer ) ; } ) ; } catch ( ex ) { }
}
2020-04-28 02:25:54 +03:00
// Flow control, pause the agent if needed
2020-05-02 08:08:49 +03:00
if ( viewer . sending == false ) { viewer . sending = true ; }
checkViewerOverflow ( viewer ) ;
2020-04-27 23:58:58 +03:00
} else {
// Nothing to send
viewer . sending = false ;
2020-04-28 02:25:54 +03:00
// Flow control, resume agent if needed
2020-05-02 08:08:49 +03:00
checkViewerUnderflow ( viewer ) ;
2020-04-27 23:58:58 +03:00
}
}
}
2020-04-25 04:24:24 +03:00
// Process data coming from the agent or any viewers
obj . processData = function ( peer , data ) {
2020-05-11 21:41:02 +03:00
if ( obj . viewers == null ) return ;
2020-04-28 03:39:21 +03:00
if ( peer == obj . agent ) {
obj . recordingFileWriting = true ;
recordData ( true , data , function ( ) {
2020-05-12 02:30:18 +03:00
if ( obj . viewers == null ) return ;
2020-04-28 03:39:21 +03:00
obj . recordingFileWriting = false ;
2020-05-02 08:08:49 +03:00
if ( ( obj . viewersOverflowCount < obj . viewers . length ) && obj . agent && ( obj . agent . paused == true ) ) { obj . agent . paused = false ; obj . agent . ws . _socket . resume ( ) ; }
2020-04-28 03:39:21 +03:00
obj . processAgentData ( data ) ;
} ) ;
} else {
obj . processViewerData ( peer , data ) ;
}
2020-04-25 04:24:24 +03:00
}
// Process incoming viewer data
obj . processViewerData = function ( viewer , data ) {
2020-04-27 23:58:58 +03:00
if ( typeof data == 'string' ) {
if ( data == '2' ) {
if ( obj . viewerConnected == false ) {
if ( obj . agent != null ) {
if ( obj . protocolOptions != null ) { obj . sendToAgent ( JSON . stringify ( obj . protocolOptions ) ) ; } // Send connection options
obj . sendToAgent ( '2' ) ; // Send remote desktop connect
}
obj . viewerConnected = true ;
}
return ;
}
var json = null ;
try { json = JSON . parse ( data ) ; } catch ( ex ) { }
if ( json == null ) return ;
if ( ( json . type == 'options' ) && ( obj . protocolOptions == null ) ) { obj . protocolOptions = json ; }
return ;
}
//console.log('ViewerData', data.length, typeof data, data);
2020-04-25 04:24:24 +03:00
if ( ( typeof data != 'object' ) || ( data . length < 4 ) ) return ; // Ignore all control traffic for now (WebRTC)
var command = data . readUInt16BE ( 0 ) ;
var cmdsize = data . readUInt16BE ( 2 ) ;
2020-04-27 23:58:58 +03:00
//console.log('ViewerData', data.length, command, cmdsize);
2020-04-25 04:24:24 +03:00
switch ( command ) {
case 1 : // Key Events, forward to agent
//console.log('Viewer-Keys');
2020-04-27 23:58:58 +03:00
obj . sendToAgent ( data ) ;
2020-04-25 04:24:24 +03:00
break ;
case 2 : // Mouse events, forward to agent
//console.log('Viewer-Mouse');
2020-04-27 23:58:58 +03:00
obj . sendToAgent ( data ) ;
2020-04-25 04:24:24 +03:00
break ;
case 5 : // Compression
if ( data . length < 10 ) return ;
//viewer.imageType = data[4]; // Always 1=JPEG
viewer . imageCompression = data [ 5 ] ;
viewer . imageScaling = data . readUInt16BE ( 6 ) ;
viewer . imageFrameRate = data . readUInt16BE ( 8 ) ;
//console.log('Viewer-Compression', viewer.imageCompression, viewer.imageScaling, viewer.imageFrameRate);
// See if this changes anything
var viewersimageCompression = null ;
var viewersimageScaling = null ;
var viewersimageFrameRate = null ;
for ( var i in obj . viewers ) {
if ( ( viewersimageCompression == null ) || ( obj . viewers [ i ] . imageCompression > viewersimageCompression ) ) { viewersimageCompression = obj . viewers [ i ] . imageCompression ; } ;
if ( ( viewersimageScaling == null ) || ( obj . viewers [ i ] . imageScaling > viewersimageScaling ) ) { viewersimageScaling = obj . viewers [ i ] . imageScaling ; } ;
if ( ( viewersimageFrameRate == null ) || ( obj . viewers [ i ] . imageFrameRate < viewersimageFrameRate ) ) { viewersimageFrameRate = obj . viewers [ i ] . imageFrameRate ; } ;
}
if ( ( obj . imageCompression != viewersimageCompression ) || ( obj . imageScaling != viewersimageScaling ) || ( obj . imageFrameRate != viewersimageFrameRate ) ) {
// Update and send to agent new compression settings
obj . imageCompression = viewersimageCompression ;
obj . imageScaling = viewersimageScaling ;
obj . imageFrameRate = viewersimageFrameRate
2020-04-27 23:58:58 +03:00
//console.log('Send-Agent-Compression', obj.imageCompression, obj.imageScaling, obj.imageFrameRate);
var cmd = Buffer . alloc ( 10 ) ;
cmd . writeUInt16BE ( 5 , 0 ) ; // Command 5, compression
cmd . writeUInt16BE ( 10 , 2 ) ; // Command size, 10 bytes long
cmd [ 4 ] = 1 ; // Image type, 1 = JPEN
cmd [ 5 ] = obj . imageCompression ; // Image compression level
cmd . writeUInt16BE ( obj . imageScaling , 6 ) ; // Scaling level
cmd . writeUInt16BE ( obj . imageFrameRate , 8 ) ; // Frame rate timer
obj . sendToAgent ( cmd ) ;
2020-04-25 04:24:24 +03:00
}
break ;
case 6 : // Refresh, handle this on the server
2020-04-27 23:58:58 +03:00
//console.log('Viewer-Refresh');
2020-04-25 04:24:24 +03:00
viewer . dataPtr = obj . firstData ; // Start over
2020-04-27 23:58:58 +03:00
if ( viewer . sending == false ) { sendViewerNext ( viewer ) ; }
2020-04-25 04:24:24 +03:00
break ;
case 8 : // Pause and unpause
if ( data . length != 5 ) break ;
var pause = data [ 4 ] ; // 0 = Unpause, 1 = Pause
if ( viewer . desktopPaused == ( pause == 1 ) ) break ;
viewer . desktopPaused = ( pause == 1 ) ;
//console.log('Viewer-' + ((pause == 1)?'Pause':'UnPause'));
var viewersPaused = true ;
for ( var i in obj . viewers ) { if ( obj . viewers [ i ] . desktopPaused == false ) { viewersPaused = false ; } ; }
if ( viewersPaused != obj . desktopPaused ) {
obj . desktopPaused = viewersPaused ;
2020-04-27 23:58:58 +03:00
//console.log('Send-Agent-' + ((viewersPaused == true) ? 'Pause' : 'UnPause'));
data [ 4 ] = ( viewersPaused == true ) ? 1 : 0 ;
obj . sendToAgent ( data ) ;
2020-04-25 04:24:24 +03:00
}
break ;
case 10 : // CTRL-ALT-DEL, forward to agent
2020-04-27 23:58:58 +03:00
obj . sendToAgent ( data ) ;
2020-04-25 04:24:24 +03:00
break ;
2020-04-28 22:42:58 +03:00
case 12 : // SET DISPLAY, forward to agent
obj . sendToAgent ( data ) ;
break ;
2020-04-25 04:24:24 +03:00
case 14 : // Touch setup
break ;
default :
console . log ( 'Un-handled viewer command: ' + command ) ;
break ;
}
}
2020-04-27 23:58:58 +03:00
2020-04-25 04:24:24 +03:00
// Process incoming agent data
2020-04-24 09:19:28 +03:00
obj . processAgentData = function ( data ) {
2020-04-28 22:42:58 +03:00
if ( ( typeof data != 'object' ) || ( data . length < 4 ) ) {
if ( typeof data == 'string' ) {
var json = null ;
try { json = JSON . parse ( data ) ; } catch ( ex ) { }
if ( json == null ) return ;
if ( json . type == 'console' ) {
// This is a console message, store it and forward this to all viewers
if ( json . msg != null ) { obj . lastConsoleMessage = data ; } else { obj . lastConsoleMessage = null ; }
obj . sendToAllViewers ( data ) ;
}
// All other control messages (notably WebRTC), are ignored for now.
}
return ; // Ignore all other traffic
}
2020-04-27 23:58:58 +03:00
const jumboData = data ;
2020-04-24 09:19:28 +03:00
var command = data . readUInt16BE ( 0 ) ;
var cmdsize = data . readUInt16BE ( 2 ) ;
2020-04-27 23:58:58 +03:00
//console.log('AgentData', data.length, command, cmdsize);
2020-04-24 09:19:28 +03:00
if ( ( command == 27 ) && ( cmdsize == 8 ) ) {
// Jumbo packet
if ( data . length >= 12 ) {
command = data . readUInt16BE ( 8 ) ;
cmdsize = data . readUInt32BE ( 4 ) ;
if ( data . length == ( cmdsize + 8 ) ) {
2020-04-25 00:58:21 +03:00
data = data . slice ( 8 , data . length ) ;
2020-04-24 09:19:28 +03:00
} else {
console . log ( 'TODO-PARTIAL-JUMBO' , command , cmdsize , data . length ) ;
return ; // TODO
}
}
}
switch ( command ) {
case 3 : // Tile, check dimentions and store
2020-04-28 23:00:26 +03:00
if ( data . length < 10 ) break ;
2020-04-25 00:58:21 +03:00
var x = data . readUInt16BE ( 4 ) , y = data . readUInt16BE ( 6 ) ;
2020-04-24 09:19:28 +03:00
var dimensions = require ( 'image-size' ) ( data . slice ( 8 ) ) ;
2020-04-25 00:58:21 +03:00
var sx = ( x / 16 ) , sy = ( y / 16 ) , sw = ( dimensions . width / 16 ) , sh = ( dimensions . height / 16 ) ;
2020-04-24 09:19:28 +03:00
obj . counter ++ ;
2020-04-25 00:58:21 +03:00
//console.log("Tile", x, y, dimensions.width, dimensions.height);
// Keep a reference to this image & how many tiles it covers
2020-04-27 23:58:58 +03:00
obj . images [ obj . counter ] = { next : null , prev : obj . lastData , data : jumboData } ;
2020-04-25 00:58:21 +03:00
obj . images [ obj . lastData ] . next = obj . counter ;
obj . lastData = obj . counter ;
obj . imagesCounters [ obj . counter ] = ( sw * sh ) ;
obj . imagesCount ++ ;
if ( obj . imagesCount == 2000000000 ) { obj . imagesCount = 1 ; } // Loop the counter if needed
2020-04-27 23:58:58 +03:00
2020-04-28 23:00:26 +03:00
//console.log('Adding Image ' + obj.counter, x, y, dimensions.width, dimensions.height);
2020-04-27 23:58:58 +03:00
2020-04-24 09:19:28 +03:00
// Update the screen with the correct pointers.
for ( var i = 0 ; i < sw ; i ++ ) {
for ( var j = 0 ; j < sh ; j ++ ) {
2020-04-27 23:58:58 +03:00
var k = ( ( obj . swidth * ( j + sy ) ) + ( i + sx ) ) ;
const oi = obj . screen [ k ] ;
obj . screen [ k ] = obj . counter ;
2020-04-25 00:58:21 +03:00
if ( ( oi != null ) && ( -- obj . imagesCounters [ oi ] == 0 ) ) {
// Remove data from the link list
obj . imagesCount -- ;
var d = obj . images [ oi ] ;
2020-04-27 23:58:58 +03:00
//console.log('Removing Image', oi, obj.images[oi].prev, obj.images[oi].next);
2020-04-25 00:58:21 +03:00
obj . images [ d . prev ] . next = d . next ;
obj . images [ d . next ] . prev = d . prev ;
delete obj . images [ oi ] ;
delete obj . imagesCounters [ oi ] ;
// If any viewers are currently on image "oi" must be moved to "d.next"
2020-04-28 23:22:49 +03:00
for ( var l in obj . viewers ) { const v = obj . viewers [ l ] ; if ( v . dataPtr == oi ) { v . dataPtr = d . next ; } }
2020-04-25 00:58:21 +03:00
}
2020-04-24 09:19:28 +03:00
}
}
2020-04-27 23:58:58 +03:00
// Any viewer on dataPtr null, change to this image
for ( var i in obj . viewers ) {
const v = obj . viewers [ i ] ;
if ( v . dataPtr == null ) { v . dataPtr = obj . counter ; if ( v . sending == false ) { sendViewerNext ( v ) ; } }
}
2020-04-25 00:58:21 +03:00
// Debug, display the link list
//var xx = '', xptr = obj.firstData;
//while (xptr != null) { xx += '>' + xptr; xptr = obj.images[xptr].next; }
//console.log('list', xx);
//console.log('images', obj.imagesCount);
2020-04-25 04:24:24 +03:00
break ;
case 4 : // Tile Copy, do nothing.
2020-04-24 09:19:28 +03:00
break ;
2020-04-25 00:58:21 +03:00
case 7 : // Screen Size, clear the screen state and compute the tile count
2020-04-28 23:00:26 +03:00
if ( data . length < 8 ) break ;
if ( ( obj . width === data . readUInt16BE ( 4 ) ) && ( obj . height === data . readUInt16BE ( 6 ) ) ) break ; // Same screen size as before, skip this.
2020-04-24 09:19:28 +03:00
obj . counter ++ ;
obj . lastScreenSizeCmd = data ;
obj . lastScreenSizeCounter = obj . counter ;
obj . width = data . readUInt16BE ( 4 ) ;
obj . height = data . readUInt16BE ( 6 ) ;
obj . swidth = obj . width / 16 ;
obj . sheight = obj . height / 16 ;
if ( Math . floor ( obj . swidth ) != obj . swidth ) { obj . swidth = Math . floor ( obj . swidth ) + 1 ; }
if ( Math . floor ( obj . sheight ) != obj . sheight ) { obj . sheight = Math . floor ( obj . sheight ) + 1 ; }
// Reset the display
obj . screen = new Array ( obj . swidth * obj . sheight ) ;
obj . imagesCount = 0 ;
obj . imagesCounters = { } ;
obj . images = { } ;
2020-04-27 23:58:58 +03:00
obj . images [ obj . counter ] = { next : null , prev : null , data : data } ;
2020-04-25 00:58:21 +03:00
obj . firstData = obj . counter ;
obj . lastData = obj . counter ;
// Add viewers must be set to start at "obj.counter"
2020-04-25 04:24:24 +03:00
for ( var i in obj . viewers ) {
2020-04-27 23:58:58 +03:00
const v = obj . viewers [ i ] ;
v . dataPtr = obj . counter ;
if ( v . sending == false ) { sendViewerNext ( v ) ; }
2020-04-25 04:24:24 +03:00
}
2020-04-24 09:19:28 +03:00
2020-04-25 00:58:21 +03:00
//console.log("ScreenSize", obj.width, obj.height, obj.swidth, obj.sheight, obj.swidth * obj.sheight);
2020-04-25 04:24:24 +03:00
break ;
case 11 : // GetDisplays
// Store and send this to all viewers right away
obj . lastDisplayInfoData = data ;
2020-04-27 23:58:58 +03:00
obj . sendToAllViewers ( data ) ;
2020-04-25 04:24:24 +03:00
break ;
2020-04-28 23:00:26 +03:00
case 12 : // SetDisplay
obj . sendToAllViewers ( data ) ;
break ;
2020-04-25 04:24:24 +03:00
case 14 : // KVM_INIT_TOUCH
break ;
case 15 : // KVM_TOUCH
break ;
case 16 : // MNG_KVM_CONNECTCOUNT
break ;
case 17 : // MNG_KVM_MESSAGE
// Send this to all viewers right away
2020-04-27 23:58:58 +03:00
obj . sendToAllViewers ( data ) ;
2020-04-25 04:24:24 +03:00
break ;
case 65 : // Alert
// Send this to all viewers right away
2020-04-27 23:58:58 +03:00
obj . sendToAllViewers ( data ) ;
2020-04-25 04:24:24 +03:00
break ;
case 88 : // MNG_KVM_MOUSE_CURSOR
// Send this to all viewers right away
2020-04-27 23:58:58 +03:00
obj . sendToAllViewers ( data ) ;
2020-04-25 00:58:21 +03:00
break ;
default :
2020-04-25 04:24:24 +03:00
console . log ( 'Un-handled agent command: ' + command ) ;
2020-04-24 09:19:28 +03:00
break ;
}
}
2020-04-28 03:39:21 +03:00
function recordingSetup ( domain , func ) {
// Setup session recording
if ( ( domain . sessionrecording == true || ( ( typeof domain . sessionrecording == 'object' ) && ( ( domain . sessionrecording . protocols == null ) || ( domain . sessionrecording . protocols . indexOf ( 2 ) >= 0 ) ) ) ) ) {
var now = new Date ( Date . now ( ) ) ;
2020-04-28 22:42:58 +03:00
var recFilename = 'desktopSession' + ( ( domain . id == '' ) ? '' : '-' ) + domain . id + '-' + now . getUTCFullYear ( ) + '-' + parent . common . zeroPad ( now . getUTCMonth ( ) , 2 ) + '-' + parent . common . zeroPad ( now . getUTCDate ( ) , 2 ) + '-' + parent . common . zeroPad ( now . getUTCHours ( ) , 2 ) + '-' + parent . common . zeroPad ( now . getUTCMinutes ( ) , 2 ) + '-' + parent . common . zeroPad ( now . getUTCSeconds ( ) , 2 ) + '-' + obj . nodeid . split ( '/' ) [ 2 ] + '.mcrec'
2020-04-28 03:39:21 +03:00
var recFullFilename = null ;
if ( domain . sessionrecording . filepath ) {
try { parent . parent . fs . mkdirSync ( domain . sessionrecording . filepath ) ; } catch ( e ) { }
recFullFilename = parent . parent . path . join ( domain . sessionrecording . filepath , recFilename ) ;
} else {
try { parent . parent . fs . mkdirSync ( parent . parent . recordpath ) ; } catch ( e ) { }
recFullFilename = parent . parent . path . join ( parent . parent . recordpath , recFilename ) ;
}
parent . parent . fs . open ( recFullFilename , 'w' , function ( err , fd ) {
2020-05-02 10:22:54 +03:00
if ( err != null ) {
parent . parent . debug ( 'relay' , 'Relay: Unable to record to file: ' + recFullFilename ) ;
func ( false ) ;
return ;
}
2020-04-28 03:39:21 +03:00
// Write the recording file header
2020-05-02 10:22:54 +03:00
parent . parent . debug ( 'relay' , 'Relay: Started recoding to file: ' + recFullFilename ) ;
2020-05-05 23:00:51 +03:00
var metadata = { magic : 'MeshCentralRelaySession' , ver : 1 , nodeid : obj . nodeid , meshid : obj . meshid , time : new Date ( ) . toLocaleString ( ) , protocol : 2 , devicename : obj . name , devicegroup : obj . meshname } ;
2020-04-28 03:39:21 +03:00
var firstBlock = JSON . stringify ( metadata ) ;
recordingEntry ( fd , 1 , 0 , firstBlock , function ( ) {
obj . recordingFile = { fd : fd , filename : recFullFilename } ;
obj . recordingFileWriting = false ;
func ( true ) ;
} ) ;
} ) ;
} else {
func ( false ) ;
}
}
// Record data to the recording file
function recordData ( isAgent , data , func ) {
try {
if ( obj . recordingFile != null ) {
// Write data to recording file
recordingEntry ( obj . recordingFile . fd , 2 , ( isAgent ? 0 : 2 ) , data , function ( ) { func ( data ) ; } ) ;
} else {
func ( data ) ;
}
} catch ( ex ) { console . log ( ex ) ; }
}
// Record a new entry in a recording log
function recordingEntry ( fd , type , flags , data , func , tag ) {
try {
if ( typeof data == 'string' ) {
// String write
var blockData = Buffer . from ( data ) , header = Buffer . alloc ( 16 ) ; // Header: Type (2) + Flags (2) + Size(4) + Time(8)
header . writeInt16BE ( type , 0 ) ; // Type (1 = Header, 2 = Network Data)
header . writeInt16BE ( flags , 2 ) ; // Flags (1 = Binary, 2 = User)
header . writeInt32BE ( blockData . length , 4 ) ; // Size
header . writeIntBE ( new Date ( ) , 10 , 6 ) ; // Time
var block = Buffer . concat ( [ header , blockData ] ) ;
parent . parent . fs . write ( fd , block , 0 , block . length , function ( ) { func ( fd , tag ) ; } ) ;
2020-05-05 23:00:51 +03:00
obj . recordingFileSize += block . length ;
2020-04-28 03:39:21 +03:00
} else {
// Binary write
var header = Buffer . alloc ( 16 ) ; // Header: Type (2) + Flags (2) + Size(4) + Time(8)
header . writeInt16BE ( type , 0 ) ; // Type (1 = Header, 2 = Network Data)
header . writeInt16BE ( flags | 1 , 2 ) ; // Flags (1 = Binary, 2 = User)
header . writeInt32BE ( data . length , 4 ) ; // Size
header . writeIntBE ( new Date ( ) , 10 , 6 ) ; // Time
var block = Buffer . concat ( [ header , data ] ) ;
parent . parent . fs . write ( fd , block , 0 , block . length , function ( ) { func ( fd , tag ) ; } ) ;
2020-05-05 23:00:51 +03:00
obj . recordingFileSize += block . length ;
2020-04-28 03:39:21 +03:00
}
} catch ( ex ) { console . log ( ex ) ; func ( fd , tag ) ; }
}
2020-04-30 12:02:23 +03:00
// If there is a recording quota, remove any old recordings if needed
function cleanUpRecordings ( ) {
2020-05-06 00:19:27 +03:00
if ( ( parent . cleanUpRecordingsActive !== true ) && domain . sessionrecording && ( ( typeof domain . sessionrecording . maxrecordings == 'number' ) || ( typeof domain . sessionrecording . maxrecordingsizemegabytes == 'number' ) ) ) {
parent . cleanUpRecordingsActive = true ;
setTimeout ( function ( ) {
var recPath = null , fs = require ( 'fs' ) ;
if ( domain . sessionrecording . filepath ) { recPath = domain . sessionrecording . filepath ; } else { recPath = parent . parent . recordpath ; }
fs . readdir ( recPath , function ( err , files ) {
if ( ( err != null ) || ( files == null ) ) { delete parent . cleanUpRecordingsActive ; return ; }
var recfiles = [ ] ;
for ( var i in files ) {
if ( files [ i ] . endsWith ( '.mcrec' ) ) {
var j = files [ i ] . indexOf ( '-' ) ;
if ( j > 0 ) { recfiles . push ( { n : files [ i ] , r : files [ i ] . substring ( j + 1 ) , s : fs . statSync ( parent . parent . path . join ( recPath , files [ i ] ) ) . size } ) ; }
}
2020-04-30 12:02:23 +03:00
}
2020-05-06 00:19:27 +03:00
recfiles . sort ( function ( a , b ) { if ( a . r < b . r ) return 1 ; if ( a . r > b . r ) return - 1 ; return 0 ; } ) ;
var totalFiles = 0 , totalSize = 0 ;
for ( var i in recfiles ) {
var overQuota = false ;
if ( ( typeof domain . sessionrecording . maxrecordings == 'number' ) && ( totalFiles >= domain . sessionrecording . maxrecordings ) ) { overQuota = true ; }
else if ( ( typeof domain . sessionrecording . maxrecordingsizemegabytes == 'number' ) && ( totalSize >= ( domain . sessionrecording . maxrecordingsizemegabytes * 1048576 ) ) ) { overQuota = true ; }
if ( overQuota ) { fs . unlinkSync ( parent . parent . path . join ( recPath , recfiles [ i ] . n ) ) ; }
totalFiles ++ ;
totalSize += recfiles [ i ] . s ;
}
delete parent . cleanUpRecordingsActive ;
} ) ;
2020-04-30 12:02:23 +03:00
} ) ;
}
}
2020-05-05 23:00:51 +03:00
// Get node information
parent . db . Get ( nodeid , function ( err , nodes ) {
if ( ( err != null ) || ( nodes . length != 1 ) ) { func ( null ) ; }
obj . meshid = nodes [ 0 ] . meshid ;
obj . icon = nodes [ 0 ] . icon ;
obj . name = nodes [ 0 ] . name ;
recordingSetup ( domain , function ( ) { func ( obj ) ; } ) ;
} ) ;
2020-04-24 09:19:28 +03:00
return obj ;
}
module . exports . CreateMeshRelay = function ( parent , ws , req , domain , user , cookie ) {
2020-04-28 22:42:58 +03:00
if ( ( req . query . nodeid == null ) || ( req . query . p != '2' ) || ( req . query . id == null ) || ( domain == null ) ) { try { ws . close ( ) ; } catch ( e ) { } return ; } // Not is not a valid remote desktop connection.
2020-04-24 09:19:28 +03:00
var obj = { } ;
obj . ws = ws ;
obj . ws . me = obj ;
obj . id = req . query . id ;
2020-04-28 22:42:58 +03:00
obj . nodeid = req . query . nodeid ;
2020-04-24 09:19:28 +03:00
obj . user = user ;
obj . ruserid = null ;
obj . req = req ; // Used in multi-server.js
// Check relay authentication
if ( ( user == null ) && ( obj . req . query != null ) && ( obj . req . query . rauth != null ) ) {
const rcookie = parent . parent . decodeCookie ( obj . req . query . rauth , parent . parent . loginCookieEncryptionKey , 240 ) ; // Cookie with 4 hour timeout
if ( rcookie . ruserid != null ) { obj . ruserid = rcookie . ruserid ; }
2020-04-28 22:42:58 +03:00
if ( rcookie . nodeid != null ) { obj . nodeid = rcookie . nodeid ; }
2020-04-24 09:19:28 +03:00
}
// If there is no authentication, drop this connection
2020-05-22 07:25:11 +03:00
if ( ( obj . id != null ) && ( obj . user == null ) && ( obj . ruserid == null ) ) { try { ws . close ( ) ; parent . parent . debug ( 'relay' , 'DesktopRelay: Connection with no authentication (' + obj . req . clientIp + ')' ) ; } catch ( e ) { console . log ( e ) ; } return ; }
2020-04-24 09:19:28 +03:00
// Relay session count (we may remove this in the future)
obj . relaySessionCounted = true ;
parent . relaySessionCount ++ ;
// 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 ;
const MESHRIGHT _REMOTEVIEW = 256 ;
// 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 ;
// Clean a IPv6 address that encodes a IPv4 address
function cleanRemoteAddr ( addr ) { if ( addr . startsWith ( '::ffff:' ) ) { return addr . substring ( 7 ) ; } else { return addr ; } }
// Disconnect this agent
obj . close = function ( arg ) {
2020-04-28 02:25:54 +03:00
if ( obj . ws == null ) return ; // Already closed.
// Close the connection
2020-05-22 07:25:11 +03:00
if ( ( arg == 1 ) || ( arg == null ) ) { try { ws . close ( ) ; parent . parent . debug ( 'relay' , 'DesktopRelay: Soft disconnect (' + obj . req . clientIp + ')' ) ; } catch ( e ) { console . log ( e ) ; } } // Soft close, close the websocket
if ( arg == 2 ) { try { ws . _socket . _parent . end ( ) ; parent . parent . debug ( 'relay' , 'DesktopRelay: Hard disconnect (' + obj . req . clientIp + ')' ) ; } catch ( e ) { console . log ( e ) ; } } // Hard close, close the TCP socket
2020-04-28 02:25:54 +03:00
if ( obj . relaySessionCounted ) { parent . relaySessionCount -- ; delete obj . relaySessionCounted ; }
2020-05-05 23:00:51 +03:00
if ( obj . deskMultiplexor != null ) { if ( obj . deskMultiplexor . removePeer ( obj ) == true ) { delete parent . desktoprelays [ obj . nodeid ] ; } }
2020-04-24 09:19:28 +03:00
// Aggressive cleanup
delete obj . id ;
delete obj . ws ;
2020-04-28 02:25:54 +03:00
delete obj . req ;
delete obj . user ;
2020-04-28 22:42:58 +03:00
delete obj . nodeid ;
2020-04-28 02:25:54 +03:00
delete obj . ruserid ;
2020-05-05 23:00:51 +03:00
delete obj . deskMultiplexor ;
2020-04-28 02:25:54 +03:00
// Clear timers if present
if ( obj . pingtimer != null ) { clearInterval ( obj . pingtimer ) ; delete obj . pingtimer ; }
if ( obj . pongtimer != null ) { clearInterval ( obj . pongtimer ) ; delete obj . pongtimer ; }
2020-04-24 09:19:28 +03:00
} ;
obj . sendAgentMessage = function ( command , userid , domainid ) {
var rights , mesh ;
if ( command . nodeid == null ) return false ;
var user = 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 = parent . wsagents [ command . nodeid ] ;
if ( agent != null ) {
// Check if we have permission to send a message to that node
rights = parent . GetNodeRights ( user , agent . dbMeshKey , agent . dbNodeKey ) ;
mesh = parent . meshes [ agent . dbMeshKey ] ;
if ( ( rights != null ) && ( mesh != null ) || ( ( rights & 16 ) != 0 ) ) { // TODO: 16 is console permission, may need more gradular permission checking
if ( ws . sessionId ) { command . sessionid = ws . sessionId ; } // Set the session id, required for responses.
command . rights = rights . rights ; // Add user rights flags to the message
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
if ( typeof domain . desktopprivacybartext == 'string' ) { command . privacybartext = domain . desktopprivacybartext ; } // Privacy bar text
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 = 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
rights = parent . GetNodeRights ( user , routing . meshid , command . nodeid ) ;
mesh = parent . meshes [ routing . meshid ] ;
if ( rights != null || ( ( rights & 16 ) != 0 ) ) { // TODO: 16 is console permission, may need more gradular permission checking
if ( ws . sessionId ) { command . fromSessionid = ws . sessionId ; } // Set the session id, required for responses.
command . rights = rights . rights ; // Add user rights flags to the message
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
if ( typeof domain . desktopprivacybartext == 'string' ) { command . privacybartext = domain . desktopprivacybartext ; } // Privacy bar text
parent . parent . multiServer . DispatchMessageSingleServer ( command , routing . serverid ) ;
return true ;
}
}
}
}
return false ;
} ;
// Send a PING/PONG message
function sendPing ( ) {
try { obj . ws . send ( '{"ctrlChannel":"102938","type":"ping"}' ) ; } catch ( ex ) { }
try { if ( obj . peer != null ) { obj . peer . ws . send ( '{"ctrlChannel":"102938","type":"ping"}' ) ; } } catch ( ex ) { }
}
function sendPong ( ) {
try { obj . ws . send ( '{"ctrlChannel":"102938","type":"pong"}' ) ; } catch ( ex ) { }
try { if ( obj . peer != null ) { obj . peer . ws . send ( '{"ctrlChannel":"102938","type":"pong"}' ) ; } } catch ( ex ) { }
}
2020-05-01 04:26:53 +03:00
function performRelay ( retryCount ) {
if ( ( obj . id == null ) || ( retryCount > 20 ) ) { try { obj . close ( ) ; } catch ( e ) { } return null ; } // Attempt to connect without id, drop this.
if ( retryCount == 0 ) { ws . _socket . setKeepAlive ( true , 240000 ) ; } // Set TCP keep alive
2020-04-24 09:19:28 +03:00
2020-04-29 00:19:42 +03:00
/ *
2020-04-28 02:25:54 +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 ( ( user == null ) && ( ! parent . args . notls ) ) {
// Check the identifier, if running without TLS, skip this.
var ids = obj . id . split ( ':' ) ;
if ( ids . length != 3 ) { ws . close ( ) ; delete obj . id ; return null ; } // Invalid ID, drop this.
if ( parent . crypto . createHmac ( 'SHA384' , parent . relayRandom ) . update ( ids [ 0 ] + ':' + ids [ 1 ] ) . digest ( 'hex' ) != ids [ 2 ] ) { ws . close ( ) ; delete obj . id ; return null ; } // Invalid HMAC, drop this.
if ( ( Date . now ( ) - parseInt ( ids [ 1 ] ) ) > 120000 ) { ws . close ( ) ; delete obj . id ; return null ; } // Expired time, drop this.
obj . id = ids [ 0 ] ;
}
2020-04-29 00:19:42 +03:00
* /
2020-04-28 02:25:54 +03:00
2020-05-01 04:26:53 +03:00
if ( retryCount == 0 ) {
// Setup the agent PING/PONG timers
if ( ( typeof parent . parent . args . agentping == 'number' ) && ( obj . pingtimer == null ) ) { obj . pingtimer = setInterval ( sendPing , parent . parent . args . agentping * 1000 ) ; }
else if ( ( typeof parent . parent . args . agentpong == 'number' ) && ( obj . pongtimer == null ) ) { obj . pongtimer = setInterval ( sendPong , parent . parent . args . agentpong * 1000 ) ; }
2020-05-22 07:25:11 +03:00
parent . parent . debug ( 'relay' , 'DesktopRelay: Connection (' + obj . req . clientIp + ')' ) ;
2020-05-01 04:26:53 +03:00
}
2020-04-28 02:25:54 +03:00
// Create if needed and add this peer to the desktop multiplexor
2020-05-05 23:00:51 +03:00
obj . deskMultiplexor = parent . desktoprelays [ obj . nodeid ] ;
if ( obj . deskMultiplexor == null ) {
2020-05-01 04:26:53 +03:00
parent . desktoprelays [ obj . nodeid ] = 1 ; // Indicate that the creating of the desktop multiplexor is pending.
parent . parent . debug ( 'relay' , 'DesktopRelay: Creating new desktop multiplexor' ) ;
2020-05-05 23:00:51 +03:00
CreateDesktopMultiplexor ( parent , domain , obj . nodeid , function ( deskMultiplexor ) {
if ( deskMultiplexor != null ) {
// Desktop multiplexor was created, use it.
obj . deskMultiplexor = deskMultiplexor ;
parent . desktoprelays [ obj . nodeid ] = obj . deskMultiplexor ;
obj . deskMultiplexor . addPeer ( obj ) ;
ws . _socket . resume ( ) ; // Release the traffic
} else {
// An error has occured, close this connection
delete parent . desktoprelays [ obj . nodeid ] ;
ws . close ( ) ;
}
2020-04-28 03:39:21 +03:00
} ) ;
} else {
2020-05-05 23:00:51 +03:00
if ( obj . deskMultiplexor == 1 ) {
2020-05-01 04:26:53 +03:00
// The multiplexor is being created, hold a little and try again. This is to prevent a possible race condition.
setTimeout ( function ( ) { performRelay ( ++ retryCount ) ; } , 50 ) ;
} else {
// Hook up this peer to the multiplexor and release the traffic
2020-05-05 23:00:51 +03:00
obj . deskMultiplexor . addPeer ( obj ) ;
2020-05-01 04:26:53 +03:00
ws . _socket . resume ( ) ;
}
2020-04-24 09:19:28 +03:00
}
}
// When data is received from the mesh relay web socket
ws . on ( 'message' , function ( data ) {
2020-04-28 02:25:54 +03:00
// If this data was received by the agent, decode it.
2020-05-05 23:00:51 +03:00
if ( this . me . deskMultiplexor != null ) { this . me . deskMultiplexor . processData ( this . me , data ) ; }
2020-04-24 09:19:28 +03:00
} ) ;
// If error, close both sides of the relay.
ws . on ( 'error' , function ( err ) {
2020-04-28 02:25:54 +03:00
//console.log('ws-error', err);
2020-04-24 09:19:28 +03:00
parent . relaySessionErrorCount ++ ;
2020-05-22 07:25:11 +03:00
console . log ( 'Relay error from ' + obj . req . clientIp + ', ' + err . toString ( ) . split ( '\r' ) [ 0 ] + '.' ) ;
2020-04-28 02:25:54 +03:00
obj . close ( ) ;
2020-04-24 09:19:28 +03:00
} ) ;
// If the relay web socket is closed, close both sides.
ws . on ( 'close' , function ( req ) {
2020-04-28 02:25:54 +03:00
//console.log('ws-close', req);
obj . close ( ) ;
2020-04-24 09:19:28 +03:00
} ) ;
// Mark this relay session as authenticated if this is the user end.
obj . authenticated = ( 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 ( ( cookie != null ) && ( cookie . nodeid != null ) && ( cookie . tcpport != null ) && ( cookie . domainid != null ) ) {
// We have routing instructions in the cookie, but first, check user access for this node.
parent . db . Get ( cookie . nodeid , function ( err , docs ) {
if ( docs . length == 0 ) { console . log ( 'ERR: Node not found' ) ; try { obj . close ( ) ; } catch ( e ) { } return ; } // Disconnect websocket
const node = docs [ 0 ] ;
// Check if this user has permission to manage this computer
if ( ( parent . GetNodeRights ( user , node . meshid , node . _id ) & MESHRIGHT _REMOTECONTROL ) == 0 ) { console . log ( 'ERR: Access denied (1)' ) ; try { obj . close ( ) ; } catch ( e ) { } return ; }
// Send connection request to agent
2020-04-28 22:42:58 +03:00
const rcookie = parent . parent . encodeCookie ( { ruserid : user . _id , nodeid : node . _id } , parent . parent . loginCookieEncryptionKey ) ;
2020-04-24 09:19:28 +03:00
if ( obj . id == undefined ) { obj . id = ( '' + Math . random ( ) ) . substring ( 2 ) ; } // If there is no connection id, generate one.
const command = { nodeid : cookie . nodeid , action : 'msg' , type : 'tunnel' , value : '*/meshrelay.ashx?id=' + obj . id + '&rauth=' + rcookie , tcpport : cookie . tcpport , tcpaddr : cookie . tcpaddr } ;
parent . parent . debug ( 'relay' , 'Relay: Sending agent tunnel command: ' + JSON . stringify ( command ) ) ;
2020-05-22 07:25:11 +03:00
if ( obj . sendAgentMessage ( command , user . _id , cookie . domainid ) == false ) { delete obj . id ; parent . parent . debug ( 'relay' , 'Relay: Unable to contact this agent (' + obj . req . clientIp + ')' ) ; }
2020-05-01 04:26:53 +03:00
performRelay ( 0 ) ;
2020-04-24 09:19:28 +03:00
} ) ;
return obj ;
} else if ( ( obj . req . query . nodeid != null ) && ( ( obj . req . query . tcpport != null ) || ( obj . req . query . udpport != null ) ) ) {
// We have routing instructions in the URL arguments, but first, check user access for this node.
parent . db . Get ( obj . req . query . nodeid , function ( err , docs ) {
if ( docs . length == 0 ) { console . log ( 'ERR: Node not found' ) ; try { obj . close ( ) ; } catch ( e ) { } return ; } // Disconnect websocket
const node = docs [ 0 ] ;
// Check if this user has permission to manage this computer
if ( ( parent . GetNodeRights ( user , node . meshid , node . _id ) & 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.
2020-04-28 22:42:58 +03:00
const rcookie = parent . parent . encodeCookie ( { ruserid : user . _id , nodeid : node . _id } , parent . parent . loginCookieEncryptionKey ) ;
2020-04-24 09:19:28 +03:00
if ( obj . req . query . tcpport != null ) {
const command = { nodeid : obj . req . query . nodeid , action : 'msg' , type : 'tunnel' , value : '*/meshrelay.ashx?id=' + obj . id + '&rauth=' + rcookie , tcpport : obj . req . query . tcpport , tcpaddr : ( ( obj . req . query . tcpaddr == null ) ? '127.0.0.1' : obj . req . query . tcpaddr ) } ;
parent . parent . debug ( 'relay' , 'Relay: Sending agent TCP tunnel command: ' + JSON . stringify ( command ) ) ;
2020-05-22 07:25:11 +03:00
if ( obj . sendAgentMessage ( command , user . _id , domain . id ) == false ) { delete obj . id ; parent . parent . debug ( 'relay' , 'Relay: Unable to contact this agent (' + obj . req . clientIp + ')' ) ; }
2020-04-24 09:19:28 +03:00
} else if ( obj . req . query . udpport != null ) {
const command = { nodeid : obj . req . query . nodeid , action : 'msg' , type : 'tunnel' , value : '*/meshrelay.ashx?id=' + obj . id + '&rauth=' + rcookie , udpport : obj . req . query . udpport , udpaddr : ( ( obj . req . query . udpaddr == null ) ? '127.0.0.1' : obj . req . query . udpaddr ) } ;
parent . parent . debug ( 'relay' , 'Relay: Sending agent UDP tunnel command: ' + JSON . stringify ( command ) ) ;
2020-05-22 07:25:11 +03:00
if ( obj . sendAgentMessage ( command , user . _id , domain . id ) == false ) { delete obj . id ; parent . parent . debug ( 'relay' , 'Relay: Unable to contact this agent (' + obj . req . clientIp + ')' ) ; }
2020-04-24 09:19:28 +03:00
}
2020-05-01 04:26:53 +03:00
performRelay ( 0 ) ;
2020-04-24 09:19:28 +03:00
} ) ;
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.
2020-05-01 04:26:53 +03:00
performRelay ( 0 ) ;
2020-04-24 09:19:28 +03:00
return obj ;
} ;
/ *
Relay session recording required that "SessionRecording" : true be set in the domain section of the config . json .
Once done , a folder "meshcentral-recordings" will be created next to "meshcentral-data" that will contain all
of the recording files with the . mcrec extension .
The recording files are binary and contain a set of :
< HEADER > < DATABLOCK > < HEADER > < DATABLOCK > < HEADER > < DATABLOCK > < HEADER > < DATABLOCK > ...
The header is always 16 bytes long and is encoded like this :
TYPE 2 bytes , 1 = Header , 2 = Network Data , 3 = EndBlock
FLAGS 2 bytes , 0x0001 = Binary , 0x0002 = User
SIZE 4 bytes , Size of the data following this header .
TIME 8 bytes , Time this record was written , number of milliseconds since 1 January , 1970 UTC .
All values are BigEndian encoded . The first data block is of TYPE 1 and contains a JSON string with information
about this recording . It looks something like this :
{
magic : 'MeshCentralRelaySession' ,
ver : 1 ,
userid : "user\domain\userid" ,
username : "username" ,
sessionid : "RandomValue" ,
ipaddr1 : 1.2 . 3.4 ,
ipaddr2 : 1.2 . 3.5 ,
time : new Date ( ) . toLocaleString ( )
}
The rest of the data blocks are all network traffic that was relayed thru the server . They are of TYPE 2 and have
a given size and timestamp . When looking at network traffic the flags are important :
- If traffic has the first ( 0x0001 ) flag set , the data is binary otherwise it ' s a string .
- If the traffic has the second ( 0x0002 ) flag set , traffic is coming from the user 's browser, if not, it' s coming from the MeshAgent .
* /