2021-04-30 08:31:01 +03:00
/ * *
* @ description MeshCentral MSTSC & SSH relay
* @ author Ylian Saint - Hilaire & Bryan Roe
2022-01-24 10:21:24 +03:00
* @ copyright Intel Corporation 2018 - 2022
2021-04-30 08:31:01 +03:00
* @ license Apache - 2.0
* @ version v0 . 0.1
* /
/*jslint node: true */
/*jshint node: true */
/*jshint strict:false */
/*jshint -W097 */
/*jshint esversion: 6 */
"use strict" ;
2021-06-29 05:26:34 +03:00
/ *
Protocol numbers
10 = RDP
11 = SSH - TERM
12 = VNC
2022-06-25 02:53:31 +03:00
13 = SSH - FILES
14 = Web - TCP
2021-06-29 05:26:34 +03:00
* /
2021-09-11 03:28:08 +03:00
// Protocol Numbers
const PROTOCOL _TERMINAL = 1 ;
const PROTOCOL _DESKTOP = 2 ;
const PROTOCOL _FILES = 5 ;
const PROTOCOL _AMTWSMAN = 100 ;
const PROTOCOL _AMTREDIR = 101 ;
const PROTOCOL _MESSENGER = 200 ;
const PROTOCOL _WEBRDP = 201 ;
const PROTOCOL _WEBSSH = 202 ;
const PROTOCOL _WEBSFTP = 203 ;
const PROTOCOL _WEBVNC = 204 ;
2022-05-08 01:47:50 +03:00
// Mesh Rights
const MESHRIGHT _EDITMESH = 0x00000001 ; // 1
const MESHRIGHT _MANAGEUSERS = 0x00000002 ; // 2
const MESHRIGHT _MANAGECOMPUTERS = 0x00000004 ; // 4
const MESHRIGHT _REMOTECONTROL = 0x00000008 ; // 8
const MESHRIGHT _AGENTCONSOLE = 0x00000010 ; // 16
const MESHRIGHT _SERVERFILES = 0x00000020 ; // 32
const MESHRIGHT _WAKEDEVICE = 0x00000040 ; // 64
const MESHRIGHT _SETNOTES = 0x00000080 ; // 128
const MESHRIGHT _REMOTEVIEWONLY = 0x00000100 ; // 256
const MESHRIGHT _NOTERMINAL = 0x00000200 ; // 512
const MESHRIGHT _NOFILES = 0x00000400 ; // 1024
const MESHRIGHT _NOAMT = 0x00000800 ; // 2048
const MESHRIGHT _DESKLIMITEDINPUT = 0x00001000 ; // 4096
const MESHRIGHT _LIMITEVENTS = 0x00002000 ; // 8192
const MESHRIGHT _CHATNOTIFY = 0x00004000 ; // 16384
const MESHRIGHT _UNINSTALL = 0x00008000 ; // 32768
const MESHRIGHT _NODESKTOP = 0x00010000 ; // 65536
const MESHRIGHT _REMOTECOMMAND = 0x00020000 ; // 131072
const MESHRIGHT _RESETOFF = 0x00040000 ; // 262144
const MESHRIGHT _GUESTSHARING = 0x00080000 ; // 524288
const MESHRIGHT _DEVICEDETAILS = 0x00100000 ; // 1048576
const MESHRIGHT _ADMIN = 0xFFFFFFFF ;
2021-06-29 05:26:34 +03:00
2022-06-26 08:10:07 +03:00
// SerialTunnel object is used to embed TLS within another connection.
function SerialTunnel ( options ) {
var obj = new require ( 'stream' ) . Duplex ( options ) ;
obj . forwardwrite = null ;
obj . updateBuffer = function ( chunk ) { this . push ( chunk ) ; } ;
obj . _write = function ( chunk , encoding , callback ) { if ( obj . forwardwrite != null ) { obj . forwardwrite ( chunk ) ; } else { console . err ( "Failed to fwd _write." ) ; } if ( callback ) callback ( ) ; } ; // Pass data written to forward
obj . _read = function ( size ) { } ; // Push nothing, anything to read should be pushed from updateBuffer()
return obj ;
}
2022-06-25 02:53:31 +03:00
2022-06-25 23:29:24 +03:00
// Construct a Web relay object
2022-06-27 02:55:07 +03:00
module . exports . CreateWebRelaySession = function ( parent , db , req , args , domain , userid , nodeid , addr , port , appid ) {
2022-06-25 23:29:24 +03:00
const obj = { } ;
2022-06-26 02:22:51 +03:00
obj . parent = parent ;
2022-06-25 23:29:24 +03:00
obj . lastOperation = Date . now ( ) ;
2022-06-26 02:22:51 +03:00
obj . domain = domain ;
2022-06-25 23:29:24 +03:00
obj . userid = userid ;
2022-06-26 02:22:51 +03:00
obj . nodeid = nodeid ;
obj . addr = addr ;
obj . port = port ;
2022-06-26 08:10:07 +03:00
obj . appid = appid ;
2022-06-25 23:29:24 +03:00
var pendingRequests = [ ] ;
var nextTunnelId = 1 ;
var tunnels = { } ;
2022-06-26 02:22:51 +03:00
// Any HTTP cookie set by the device is going to be shared between all tunnels to that device.
obj . webCookie = null ;
2022-06-25 23:29:24 +03:00
// Events
obj . closed = false ;
obj . onclose = null ;
2022-06-27 02:55:07 +03:00
// Check if any tunnels need to be cleaned up
obj . checkTimeout = function ( ) {
const limit = Date . now ( ) - ( 1 * 60 * 1000 ) ; // This is is 5 minutes before current time
// Close any old non-websocket tunnels
const tunnelToRemove = [ ] ;
for ( var i in tunnels ) { if ( ( tunnels [ i ] . lastOperation < limit ) && ( tunnels [ i ] . isWebSocket !== true ) ) { tunnelToRemove . push ( tunnels [ i ] ) ; } }
for ( var i in tunnelToRemove ) { console . log ( 'session-close-tunnel' ) ; tunnelToRemove [ i ] . close ( ) ; }
// Close this session if no longer used
if ( obj . lastOperation < limit ) {
var count = 0 ;
for ( var i in tunnels ) { count ++ ; }
if ( count == 0 ) { console . log ( 'session-close-self' ) ; close ( ) ; } // Time limit reached and no tunnels, clean up.
}
}
2022-06-25 23:29:24 +03:00
// Handle new HTTP request
obj . handleRequest = function ( req , res ) {
pendingRequests . push ( [ req , res ] ) ;
handleNextRequest ( ) ;
}
// Handle request
function handleNextRequest ( ) {
// Check to see if any of the tunnels are free
var count = 0 ;
for ( var i in tunnels ) {
count += ( tunnels [ i ] . isWebSocket ? 0 : 1 ) ;
if ( ( tunnels [ i ] . relayActive == true ) && ( tunnels [ i ] . res == null ) ) {
// Found a free tunnel, use it
const x = pendingRequests . shift ( ) ;
tunnels [ i ] . processRequest ( x [ 0 ] , x [ 1 ] ) ;
return ;
}
}
if ( count > 0 ) return ;
// Launch a new tunnel
2022-06-26 02:22:51 +03:00
const tunnel = module . exports . CreateWebRelay ( obj , db , args , domain ) ;
2022-06-26 08:10:07 +03:00
tunnel . onclose = function ( tunnelId ) {
delete tunnels [ tunnelId ] ;
// Count how many non-websocket tunnels are active
var count = 0 ;
for ( var i in tunnels ) { count += ( tunnels [ i ] . isWebSocket ? 0 : 1 ) ; }
// If there are none, discard all pending HTTP requests
if ( count == 0 ) { for ( var i in pendingRequests ) { const x = pendingRequests [ i ] ; x [ 1 ] . end ( ) ; pendingRequests = [ ] ; } }
}
2022-06-26 02:22:51 +03:00
tunnel . onconnect = function ( tunnelId ) { if ( pendingRequests . length > 0 ) { const x = pendingRequests . shift ( ) ; tunnels [ tunnelId ] . processRequest ( x [ 0 ] , x [ 1 ] ) ; } }
tunnel . oncompleted = function ( tunnelId ) { if ( pendingRequests . length > 0 ) { const x = pendingRequests . shift ( ) ; tunnels [ tunnelId ] . processRequest ( x [ 0 ] , x [ 1 ] ) ; } }
2022-06-26 08:10:07 +03:00
tunnel . connect ( userid , nodeid , addr , port , appid ) ;
2022-06-25 23:29:24 +03:00
tunnel . tunnelId = nextTunnelId ++ ;
tunnels [ tunnel . tunnelId ] = tunnel ;
}
// Close all tunnels
function close ( ) {
if ( obj . closed == true ) return ;
obj . closed = true ;
for ( var i in tunnels ) { tunnels [ i ] . close ( ) ; }
tunnels = null ;
2022-06-27 02:55:07 +03:00
if ( obj . onclose ) { obj . onclose ( obj . userid + '/' + obj . sessionId ) ; }
2022-06-25 23:29:24 +03:00
delete obj . userid ;
delete obj . lastOperation ;
}
return obj ;
}
// Construct a Web relay object
module . exports . CreateWebRelay = function ( parent , db , args , domain ) {
//const Net = require('net');
const WebSocket = require ( 'ws' )
2022-06-25 02:53:31 +03:00
const obj = { } ;
2022-06-27 02:55:07 +03:00
obj . lastOperation = Date . now ( ) ;
2022-06-25 02:53:31 +03:00
obj . relayActive = false ;
obj . closed = false ;
2022-06-25 23:29:24 +03:00
obj . isWebSocket = false ;
2022-06-26 08:10:07 +03:00
const constants = ( require ( 'crypto' ) . constants ? require ( 'crypto' ) . constants : require ( 'constants' ) ) ; // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
2022-06-25 02:53:31 +03:00
// Events
obj . onclose = null ;
2022-06-25 23:29:24 +03:00
obj . oncompleted = null ;
obj . onconnect = null ;
// Process a HTTP request
obj . processRequest = function ( req , res ) {
if ( obj . relayActive == false ) { console . log ( "ERROR: Attempt to use an unconnected tunnel" ) ; return false ; }
2022-06-27 02:55:07 +03:00
parent . lastOperation = obj . lastOperation = Date . now ( ) ;
2022-06-25 23:29:24 +03:00
// Construct the HTTP request
var request = req . method + ' ' + req . url + ' HTTP/' + req . httpVersion + '\r\n' ;
request += 'host: ' + obj . addr + ':' + obj . port + '\r\n' ;
2022-06-26 02:22:51 +03:00
const blockedHeaders = [ 'origin' , 'host' , 'cookie' ] ; // These are headers we do not forward
for ( var i in req . headers ) { if ( blockedHeaders . indexOf ( i ) == - 1 ) { request += i + ': ' + req . headers [ i ] + '\r\n' ; } }
if ( parent . webCookie != null ) { request += 'cookie: ' + parent . webCookie + '\r\n' } // If we have a sessin cookie, use it.
2022-06-25 23:29:24 +03:00
request += '\r\n' ;
if ( ( req . headers [ 'transfer-encoding' ] != null ) || ( req . headers [ 'content-length' ] != null ) ) {
// Read the HTTP body and send the request to the device
obj . requestBinary = [ Buffer . from ( request ) ] ;
req . on ( 'data' , function ( data ) { obj . requestBinary . push ( data ) ; } ) ;
2022-06-26 08:10:07 +03:00
req . on ( 'end' , function ( ) { send ( Buffer . concat ( obj . requestBinary ) ) ; delete obj . requestBinary ; } ) ;
2022-06-25 23:29:24 +03:00
} else {
// Request has no body, send it now
2022-06-26 08:10:07 +03:00
send ( Buffer . from ( request ) ) ;
2022-06-25 23:29:24 +03:00
}
obj . res = res ;
}
2022-06-25 02:53:31 +03:00
// Disconnect
obj . close = function ( arg ) {
if ( obj . closed == true ) return ;
obj . closed = true ;
2022-06-26 08:10:07 +03:00
if ( obj . tls ) {
try { obj . tls . end ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . tls ;
}
2022-06-26 02:22:51 +03:00
/ *
2022-06-25 02:53:31 +03:00
// Event the session ending
if ( ( obj . startTime ) && ( obj . meshid != null ) ) {
// Collect how many raw bytes where received and sent.
// We sum both the websocket and TCP client in this case.
var inTraffc = obj . ws . _socket . bytesRead , outTraffc = obj . ws . _socket . bytesWritten ;
if ( obj . wsClient != null ) { inTraffc += obj . wsClient . _socket . bytesRead ; outTraffc += obj . wsClient . _socket . bytesWritten ; }
const sessionSeconds = Math . round ( ( Date . now ( ) - obj . startTime ) / 1000 ) ;
const user = parent . users [ obj . cookie . userid ] ;
const username = ( user != null ) ? user . name : null ;
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : obj . cookie . userid , username : username , sessionid : obj . sessionid , msgid : 123 , msgArgs : [ sessionSeconds , obj . sessionid ] , msg : "Left Web-SSH session \"" + obj . sessionid + "\" after " + sessionSeconds + " second(s)." , protocol : PROTOCOL _WEBSSH , bytesin : inTraffc , bytesout : outTraffc } ;
2022-06-25 23:29:24 +03:00
parent . DispatchEvent ( [ '*' , obj . nodeid , obj . cookie . userid , obj . meshid ] , obj , event ) ;
2022-06-25 02:53:31 +03:00
delete obj . startTime ;
delete obj . sessionid ;
}
2022-06-26 02:22:51 +03:00
* /
2022-06-25 02:53:31 +03:00
if ( obj . wsClient ) {
obj . wsClient . removeAllListeners ( 'open' ) ;
obj . wsClient . removeAllListeners ( 'message' ) ;
obj . wsClient . removeAllListeners ( 'close' ) ;
try { obj . wsClient . close ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . wsClient ;
}
2022-06-25 23:29:24 +03:00
// Close any pending request
if ( obj . res ) { obj . res . end ( ) ; delete obj . res ; }
2022-06-25 02:53:31 +03:00
// Event disconnection
2022-06-25 23:29:24 +03:00
if ( obj . onclose ) { obj . onclose ( obj . tunnelId ) ; }
2022-06-25 02:53:31 +03:00
obj . relayActive = false ;
} ;
// Start the looppback server
2022-06-26 08:10:07 +03:00
obj . connect = function ( userid , nodeid , addr , port , appid ) {
2022-06-25 23:29:24 +03:00
if ( obj . relayActive || obj . closed ) return ;
obj . addr = addr ;
obj . port = port ;
2022-06-26 08:10:07 +03:00
obj . appid = appid ;
2022-06-25 23:29:24 +03:00
// Encode a cookie for the mesh relay
const cookieContent = { userid : userid , domainid : domain . id , nodeid : nodeid , tcpport : port } ;
if ( addr != null ) { cookieContent . tcpaddr = addr ; }
2022-06-26 02:22:51 +03:00
const cookie = parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ;
2022-06-25 23:29:24 +03:00
2022-06-25 02:53:31 +03:00
try {
// Setup the correct URL with domain and use TLS only if needed.
const options = { rejectUnauthorized : false } ;
const protocol = ( args . tlsoffload ) ? 'ws' : 'wss' ;
var domainadd = '' ;
if ( ( domain . dns == null ) && ( domain . id != '' ) ) { domainadd = domain . id + '/' }
2022-06-25 23:29:24 +03:00
const url = protocol + '://localhost:' + args . port + '/' + domainadd + ( ( ( obj . mtype == 3 ) && ( obj . relaynodeid == null ) ) ? 'local' : 'mesh' ) + 'relay.ashx?p=14&auth=' + cookie ; // Protocol 14 is Web-TCP
2022-06-26 02:22:51 +03:00
parent . parent . debug ( 'relay' , 'TCP: Connection websocket to ' + url ) ;
2022-06-25 02:53:31 +03:00
obj . wsClient = new WebSocket ( url , options ) ;
2022-06-26 02:22:51 +03:00
obj . wsClient . on ( 'open' , function ( ) { parent . parent . debug ( 'relay' , 'TCP: Relay websocket open' ) ; } ) ;
2022-06-25 02:53:31 +03:00
obj . wsClient . on ( 'message' , function ( data ) { // Make sure to handle flow control.
2022-06-26 08:10:07 +03:00
if ( obj . tls ) {
// WS --> TLS
processRawHttpData ( data ) ;
} else if ( obj . relayActive == false ) {
2022-06-25 02:53:31 +03:00
if ( ( data == 'c' ) || ( data == 'cr' ) ) {
2022-06-26 08:10:07 +03:00
if ( appid == 2 ) {
// TLS needs to be setup
obj . ser = new SerialTunnel ( ) ;
obj . ser . forwardwrite = function ( data ) { if ( data . length > 0 ) { try { obj . wsClient . send ( data ) ; } catch ( ex ) { } } } ; // TLS ---> WS
// TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel
const tlsoptions = { socket : obj . ser , rejectUnauthorized : false } ;
obj . tls = require ( 'tls' ) . connect ( tlsoptions , function ( ) {
parent . parent . debug ( 'relay' , "Web Relay Secure TLS Connection" ) ;
obj . relayActive = true ;
2022-06-27 02:55:07 +03:00
parent . lastOperation = obj . lastOperation = Date . now ( ) ; // Update time of last opertion performed
2022-06-26 08:10:07 +03:00
if ( obj . onconnect ) { obj . onconnect ( obj . tunnelId ) ; } // Event connection
} ) ;
obj . tls . setEncoding ( 'binary' ) ;
obj . tls . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , "Web Relay TLS Connection Error" , err ) ; obj . close ( ) ; } ) ;
// Decrypted tunnel from TLS communcation to be forwarded to the browser
obj . tls . on ( 'data' , function ( data ) { processHttpData ( data ) ; } ) ; // TLS ---> Browser
} else {
// No TLS needed, tunnel is now active
obj . relayActive = true ;
2022-06-27 02:55:07 +03:00
parent . lastOperation = obj . lastOperation = Date . now ( ) ; // Update time of last opertion performed
2022-06-26 08:10:07 +03:00
if ( obj . onconnect ) { obj . onconnect ( obj . tunnelId ) ; } // Event connection
}
2022-06-25 02:53:31 +03:00
}
} else {
2022-06-26 08:10:07 +03:00
processRawHttpData ( data ) ;
2022-06-25 02:53:31 +03:00
}
} ) ;
2022-06-26 02:22:51 +03:00
obj . wsClient . on ( 'close' , function ( ) { parent . parent . debug ( 'relay' , 'TCP: Relay websocket closed' ) ; obj . close ( ) ; } ) ;
obj . wsClient . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'TCP: Relay websocket error: ' + err ) ; obj . close ( ) ; } ) ;
2022-06-25 02:53:31 +03:00
} catch ( ex ) {
console . log ( ex ) ;
}
}
2022-06-26 08:10:07 +03:00
function processRawHttpData ( data ) {
if ( typeof data == 'string' ) {
// Forward any ping/pong commands to the browser
var cmd = null ;
try { cmd = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( cmd != null ) && ( cmd . ctrlChannel == '102938' ) && ( cmd . type == 'ping' ) ) { cmd . type = 'pong' ; obj . wsClient . send ( JSON . stringify ( cmd ) ) ; }
return ;
}
if ( obj . tls ) {
// If TLS is in use, WS --> TLS
if ( data . length > 0 ) { try { obj . ser . updateBuffer ( data ) ; } catch ( ex ) { console . log ( ex ) ; } }
} else {
// Relay WS --> TCP, event data coming in
processHttpData ( data . toString ( 'binary' ) ) ;
}
}
2022-06-25 23:29:24 +03:00
// Process incoming HTTP data
obj . socketAccumulator = '' ;
obj . socketParseState = 0 ;
function processHttpData ( data ) {
obj . socketAccumulator += data ;
while ( true ) {
//console.log('ACC(' + obj.socketAccumulator + '): ' + obj.socketAccumulator);
if ( obj . socketParseState == 0 ) {
var headersize = obj . socketAccumulator . indexOf ( '\r\n\r\n' ) ;
if ( headersize < 0 ) return ;
//obj.Debug("Header: "+obj.socketAccumulator.substring(0, headersize)); // Display received HTTP header
obj . socketHeader = obj . socketAccumulator . substring ( 0 , headersize ) . split ( '\r\n' ) ;
obj . socketAccumulator = obj . socketAccumulator . substring ( headersize + 4 ) ;
obj . socketParseState = 1 ;
obj . socketData = '' ;
obj . socketXHeader = { Directive : obj . socketHeader [ 0 ] . split ( ' ' ) } ;
for ( var i in obj . socketHeader ) {
if ( i != 0 ) {
var x2 = obj . socketHeader [ i ] . indexOf ( ':' ) ;
obj . socketXHeader [ obj . socketHeader [ i ] . substring ( 0 , x2 ) . toLowerCase ( ) ] = obj . socketHeader [ i ] . substring ( x2 + 2 ) ;
}
}
}
if ( obj . socketParseState == 1 ) {
var csize = - 1 ;
if ( ( obj . socketXHeader [ 'connection' ] != undefined ) && ( obj . socketXHeader [ 'connection' ] . toLowerCase ( ) == 'close' ) && ( ( obj . socketXHeader [ "transfer-encoding" ] == undefined ) || ( obj . socketXHeader [ "transfer-encoding" ] . toLowerCase ( ) != 'chunked' ) ) ) {
// The body ends with a close, in this case, we will only process the header
csize = 0 ;
} else if ( obj . socketXHeader [ 'content-length' ] != undefined ) {
// The body length is specified by the content-length
csize = parseInt ( obj . socketXHeader [ 'content-length' ] ) ;
if ( obj . socketAccumulator . length < csize ) return ;
var data = obj . socketAccumulator . substring ( 0 , csize ) ;
obj . socketAccumulator = obj . socketAccumulator . substring ( csize ) ;
obj . socketData = data ;
csize = 0 ;
} else {
// The body is chunked
var clen = obj . socketAccumulator . indexOf ( '\r\n' ) ;
if ( clen < 0 ) return ; // Chunk length not found, exit now and get more data.
// Chunk length if found, lets see if we can get the data.
csize = parseInt ( obj . socketAccumulator . substring ( 0 , clen ) , 16 ) ;
if ( obj . socketAccumulator . length < clen + 2 + csize + 2 ) return ;
// We got a chunk with all of the data, handle the chunck now.
var data = obj . socketAccumulator . substring ( clen + 2 , clen + 2 + csize ) ;
obj . socketAccumulator = obj . socketAccumulator . substring ( clen + 2 + csize + 2 ) ;
try { obj . socketData += data ; } catch ( ex ) { console . log ( ex , typeof data , data . length ) ; }
}
if ( csize == 0 ) {
//obj.Debug("xxOnSocketData DONE: (" + obj.socketData.length + "): " + obj.socketData);
processHttpResponse ( obj . socketXHeader , obj . socketData ) ;
obj . socketParseState = 0 ;
obj . socketHeader = null ;
}
}
}
}
// This is a fully parsed HTTP response from the remote device
function processHttpResponse ( header , data ) {
2022-06-26 02:22:51 +03:00
//console.log('processHttpResponse', header);
2022-06-25 23:29:24 +03:00
obj . res . status ( parseInt ( header . Directive [ 1 ] ) ) ; // Set the status
2022-06-26 02:22:51 +03:00
const blockHeaders = [ 'Directive' ] ; // These are headers we do not forward
for ( var i in header ) {
if ( i == 'set-cookie' ) { parent . webCookie = header [ i ] ; } // Keep the cookie, don't forward it
else if ( blockHeaders . indexOf ( i ) == - 1 ) { obj . res . set ( i , header [ i ] ) ; } // Set the headers if not blocked
}
2022-06-25 23:29:24 +03:00
obj . res . set ( 'Content-Security-Policy' , "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:;" ) ; // Set an "allow all" policy, see if the can restrict this in the future
obj . res . end ( data , 'binary' ) ; // Write the data
delete obj . res ;
2022-06-27 02:55:07 +03:00
parent . lastOperation = obj . lastOperation = Date . now ( ) ; // Update time of last opertion performed
2022-06-25 23:29:24 +03:00
// Event completion
if ( obj . oncompleted ) { obj . oncompleted ( obj . tunnelId ) ; }
}
2022-06-26 08:10:07 +03:00
// Send data thru the relay tunnel. Written to use TLS if needed.
function send ( data ) { try { if ( obj . tls ) { obj . tls . write ( data ) ; } else { obj . wsClient . send ( data ) ; } } catch ( ex ) { } }
2022-06-25 02:53:31 +03:00
2022-06-26 02:22:51 +03:00
parent . parent . debug ( 'relay' , 'TCP: Request for web relay' ) ;
2022-06-25 02:53:31 +03:00
return obj ;
} ;
2021-04-30 08:31:01 +03:00
// Construct a MSTSC Relay object, called upon connection
2022-04-23 23:17:12 +03:00
// This implementation does not have TLS support
2021-04-30 08:31:01 +03:00
// This is a bit of a hack as we are going to run the RDP connection thru a loopback connection.
// If the "node-rdpjs-2" module supported passing a socket, we would do something different.
module . exports . CreateMstscRelay = function ( parent , db , ws , req , args , domain ) {
const Net = require ( 'net' ) ;
const WebSocket = require ( 'ws' ) ;
2022-04-14 21:47:04 +03:00
const obj = { } ;
2021-04-30 08:31:01 +03:00
obj . ws = ws ;
obj . tcpServerPort = 0 ;
obj . relayActive = false ;
var rdpClient = null ;
parent . parent . debug ( 'relay' , 'RDP: Request for RDP relay (' + req . clientIp + ')' ) ;
// Disconnect
obj . close = function ( arg ) {
2021-05-05 10:18:41 +03:00
if ( obj . ws == null ) return ;
2021-09-11 03:28:08 +03:00
// Event the session ending
if ( ( obj . startTime ) && ( obj . meshid != null ) ) {
// Collect how many raw bytes where received and sent.
// We sum both the websocket and TCP client in this case.
var inTraffc = obj . ws . _socket . bytesRead , outTraffc = obj . ws . _socket . bytesWritten ;
if ( obj . wsClient != null ) { inTraffc += obj . wsClient . _socket . bytesRead ; outTraffc += obj . wsClient . _socket . bytesWritten ; }
const sessionSeconds = Math . round ( ( Date . now ( ) - obj . startTime ) / 1000 ) ;
2022-05-03 07:06:32 +03:00
const user = parent . users [ obj . userid ] ;
2022-02-28 20:37:25 +03:00
const username = ( user != null ) ? user . name : null ;
2022-05-03 07:06:32 +03:00
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : obj . userid , username : username , sessionid : obj . sessionid , msgid : 125 , msgArgs : [ sessionSeconds , obj . sessionid ] , msg : "Left Web-RDP session \"" + obj . sessionid + "\" after " + sessionSeconds + " second(s)." , protocol : PROTOCOL _WEBRDP , bytesin : inTraffc , bytesout : outTraffc } ;
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , obj . userid , obj . meshid ] , obj , event ) ;
2021-09-11 03:28:08 +03:00
delete obj . startTime ;
2022-02-22 21:56:45 +03:00
delete obj . sessionid ;
2021-09-11 03:28:08 +03:00
}
2021-05-05 10:18:41 +03:00
if ( obj . wsClient ) { obj . wsClient . close ( ) ; delete obj . wsClient ; }
2021-06-02 22:42:38 +03:00
if ( obj . tcpServer ) { obj . tcpServer . close ( ) ; delete obj . tcpServer ; }
2021-05-05 10:18:41 +03:00
if ( rdpClient ) { rdpClient . close ( ) ; rdpClient = null ; }
2022-05-05 22:38:03 +03:00
if ( ( arg == 1 ) || ( arg == null ) ) { try { ws . close ( ) ; } catch ( ex ) { console . log ( ex ) ; } } // Soft close, close the websocket
if ( arg == 2 ) { try { ws . _socket . _parent . end ( ) ; } catch ( ex ) { console . log ( ex ) ; } } // Hard close, close the TCP socket
2021-05-05 10:18:41 +03:00
obj . ws . removeAllListeners ( ) ;
obj . relayActive = false ;
2021-09-11 03:28:08 +03:00
2021-04-30 08:31:01 +03:00
delete obj . ws ;
2021-09-11 03:28:08 +03:00
delete obj . nodeid ;
delete obj . meshid ;
delete obj . userid ;
2021-04-30 08:31:01 +03:00
} ;
// Start the looppback server
function startTcpServer ( ) {
obj . tcpServer = new Net . Server ( ) ;
2022-04-14 21:47:04 +03:00
obj . tcpServer . listen ( 0 , 'localhost' , function ( ) { obj . tcpServerPort = obj . tcpServer . address ( ) . port ; startRdp ( obj . tcpServerPort ) ; } ) ;
2021-04-30 08:31:01 +03:00
obj . tcpServer . on ( 'connection' , function ( socket ) {
if ( obj . relaySocket != null ) {
socket . close ( ) ;
} else {
obj . relaySocket = socket ;
obj . relaySocket . pause ( ) ;
obj . relaySocket . on ( 'data' , function ( chunk ) { // Make sure to handle flow control.
if ( obj . relayActive == true ) { obj . relaySocket . pause ( ) ; obj . wsClient . send ( chunk , function ( ) { obj . relaySocket . resume ( ) ; } ) ; }
} ) ;
obj . relaySocket . on ( 'end' , function ( ) { obj . close ( ) ; } ) ;
obj . relaySocket . on ( 'error' , function ( err ) { obj . close ( ) ; } ) ;
// Setup the correct URL with domain and use TLS only if needed.
2022-04-14 21:47:04 +03:00
const options = { rejectUnauthorized : false } ;
const protocol = ( args . tlsoffload ) ? 'ws' : 'wss' ;
2021-04-30 08:31:01 +03:00
var domainadd = '' ;
if ( ( domain . dns == null ) && ( domain . id != '' ) ) { domainadd = domain . id + '/' }
2022-05-05 22:38:03 +03:00
const url = protocol + '://localhost:' + args . port + '/' + domainadd + ( ( ( obj . mtype == 3 ) && ( obj . relaynodeid == null ) ) ? 'local' : 'mesh' ) + 'relay.ashx?p=10&auth=' + obj . infos . ip ; // Protocol 10 is Web-RDP
2021-04-30 08:31:01 +03:00
parent . parent . debug ( 'relay' , 'RDP: Connection websocket to ' + url ) ;
obj . wsClient = new WebSocket ( url , options ) ;
obj . wsClient . on ( 'open' , function ( ) { parent . parent . debug ( 'relay' , 'RDP: Relay websocket open' ) ; } ) ;
obj . wsClient . on ( 'message' , function ( data ) { // Make sure to handle flow control.
2022-05-05 22:38:03 +03:00
if ( obj . relayActive == false ) {
if ( ( data == 'c' ) || ( data == 'cr' ) ) {
obj . relayActive = true ;
obj . relaySocket . resume ( ) ;
}
2021-04-30 08:31:01 +03:00
} else {
2022-05-05 23:17:34 +03:00
if ( typeof data == 'string' ) {
// Forward any ping/pong commands to the browser
var cmd = null ;
try { cmd = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( cmd != null ) && ( cmd . ctrlChannel == '102938' ) ) {
if ( cmd . type == 'ping' ) { send ( [ 'ping' ] ) ; }
else if ( cmd . type == 'pong' ) { send ( [ 'pong' ] ) ; }
}
return ;
}
2021-04-30 08:31:01 +03:00
obj . wsClient . _socket . pause ( ) ;
2022-05-05 22:38:03 +03:00
try {
obj . relaySocket . write ( data , function ( ) {
2022-05-15 09:00:57 +03:00
if ( obj . wsClient && obj . wsClient . _socket ) { try { obj . wsClient . _socket . resume ( ) ; } catch ( ex ) { console . log ( ex ) ; } }
2022-05-05 22:38:03 +03:00
} ) ;
} catch ( ex ) { console . log ( ex ) ; obj . close ( ) ; }
2021-04-30 08:31:01 +03:00
}
} ) ;
obj . wsClient . on ( 'close' , function ( ) { parent . parent . debug ( 'relay' , 'RDP: Relay websocket closed' ) ; obj . close ( ) ; } ) ;
obj . wsClient . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'RDP: Relay websocket error: ' + err ) ; obj . close ( ) ; } ) ;
obj . tcpServer . close ( ) ;
obj . tcpServer = null ;
}
} ) ;
}
// Start the RDP client
function startRdp ( port ) {
parent . parent . debug ( 'relay' , 'RDP: Starting RDP client on loopback port ' + port ) ;
try {
2022-05-03 07:06:32 +03:00
const args = {
2022-04-29 21:13:58 +03:00
logLevel : 'NONE' , // 'ERROR',
2021-04-30 08:31:01 +03:00
domain : obj . infos . domain ,
userName : obj . infos . username ,
password : obj . infos . password ,
enablePerf : true ,
autoLogin : true ,
screen : obj . infos . screen ,
2022-05-04 23:11:52 +03:00
locale : obj . infos . locale ,
2022-05-03 07:06:32 +03:00
} ;
2022-05-08 21:46:07 +03:00
if ( obj . infos . options ) {
if ( obj . infos . options . flags != null ) { args . perfFlags = obj . infos . options . flags ; delete obj . infos . options . flags ; }
if ( ( obj . infos . options . workingDir != null ) && ( obj . infos . options . workingDir != '' ) ) { args . workingDir = obj . infos . options . workingDir ; }
if ( ( obj . infos . options . alternateShell != null ) && ( obj . infos . options . alternateShell != '' ) ) { args . alternateShell = obj . infos . options . alternateShell ; }
}
2022-05-03 07:06:32 +03:00
rdpClient = require ( './rdp' ) . createClient ( args ) . on ( 'connect' , function ( ) {
2021-04-30 08:31:01 +03:00
send ( [ 'rdp-connect' ] ) ;
2021-06-30 03:13:18 +03:00
if ( ( typeof obj . infos . options == 'object' ) && ( obj . infos . options . savepass == true ) ) { saveRdpCredentials ( ) ; } // Save the credentials if needed
2022-02-22 21:56:45 +03:00
obj . sessionid = Buffer . from ( parent . crypto . randomBytes ( 9 ) , 'binary' ) . toString ( 'base64' ) ;
2021-09-11 03:28:08 +03:00
obj . startTime = Date . now ( ) ;
2022-02-22 21:56:45 +03:00
// Event session start
try {
2022-05-03 07:06:32 +03:00
const user = parent . users [ obj . userid ] ;
2022-02-22 21:56:45 +03:00
const username = ( user != null ) ? user . name : null ;
2022-05-03 07:06:32 +03:00
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : obj . userid , username : username , sessionid : obj . sessionid , msgid : 150 , msgArgs : [ obj . sessionid ] , msg : "Started Web-RDP session \"" + obj . sessionid + "\"." , protocol : PROTOCOL _WEBRDP } ;
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , obj . userid , obj . meshid ] , obj , event ) ;
2022-02-22 21:56:45 +03:00
} catch ( ex ) { console . log ( ex ) ; }
2021-04-30 08:31:01 +03:00
} ) . on ( 'bitmap' , function ( bitmap ) {
try { ws . send ( bitmap . data ) ; } catch ( ex ) { } // Send the bitmap data as binary
delete bitmap . data ;
send ( [ 'rdp-bitmap' , bitmap ] ) ; // Send the bitmap metadata seperately, without bitmap data.
2022-05-15 09:00:57 +03:00
} ) . on ( 'clipboard' , function ( content ) {
2022-06-24 08:10:07 +03:00
send ( [ 'rdp-clipboard' , content ] ) ; // The clipboard data has changed
} ) . on ( 'pointer' , function ( cursorId , cursorStr ) {
if ( cursorStr == null ) { cursorStr = 'default' ; }
if ( obj . lastCursorStrSent != cursorStr ) {
obj . lastCursorStrSent = cursorStr ;
//console.log('pointer', cursorStr);
send ( [ 'rdp-pointer' , cursorStr ] ) ; // The mouse pointer has changed
}
2021-04-30 08:31:01 +03:00
} ) . on ( 'close' , function ( ) {
2022-06-24 08:10:07 +03:00
send ( [ 'rdp-close' ] ) ; // This RDP session has closed
2021-04-30 08:31:01 +03:00
} ) . on ( 'error' , function ( err ) {
2022-05-21 07:52:03 +03:00
if ( typeof err == 'string' ) { send ( [ 'rdp-error' , err ] ) ; }
if ( ( typeof err == 'object' ) && ( err . err ) && ( err . code ) ) { send ( [ 'rdp-error' , err . err , err . code ] ) ; }
2022-04-14 21:47:04 +03:00
} ) . connect ( 'localhost' , obj . tcpServerPort ) ;
2021-04-30 08:31:01 +03:00
} catch ( ex ) {
console . log ( 'startRdpException' , ex ) ;
obj . close ( ) ;
}
}
2022-05-18 02:09:35 +03:00
// Save RDP credentials into database
2021-06-30 03:13:18 +03:00
function saveRdpCredentials ( ) {
2022-03-16 03:00:43 +03:00
if ( domain . allowsavingdevicecredentials == false ) return ;
2021-06-30 03:13:18 +03:00
parent . parent . db . Get ( obj . nodeid , function ( err , nodes ) {
if ( ( err != null ) || ( nodes == null ) || ( nodes . length != 1 ) ) return ;
const node = nodes [ 0 ] ;
2022-05-18 02:09:35 +03:00
if ( node . rdp == null ) { node . rdp = { } ; }
2021-06-30 03:13:18 +03:00
2022-05-18 02:09:35 +03:00
// Check if credentials are already set
if ( ( typeof node . rdp [ obj . userid ] == 'object' ) && ( node . rdp [ obj . userid ] . d == obj . infos . domain ) && ( node . rdp [ obj . userid ] . u == obj . infos . username ) && ( node . rdp [ obj . userid ] . p == obj . infos . password ) ) return ;
// Clear up any existing credentials or credentials for users that don't exist anymore
for ( var i in node . rdp ) { if ( ! i . startsWith ( 'user/' ) || ( parent . users [ i ] == null ) ) { delete node . rdp [ i ] ; } }
// Clear legacy credentials
delete node . rdp . d ;
delete node . rdp . u ;
delete node . rdp . p ;
2021-06-30 03:13:18 +03:00
// Save the credentials
2022-05-18 02:09:35 +03:00
node . rdp [ obj . userid ] = { d : obj . infos . domain , u : obj . infos . username , p : obj . infos . password } ;
2021-06-30 03:13:18 +03:00
parent . parent . db . Set ( node ) ;
2022-05-18 02:09:35 +03:00
// Event the node change
const event = { etype : 'node' , action : 'changenode' , nodeid : obj . nodeid , domain : domain . id , userid : obj . userid , node : parent . CloneSafeNode ( node ) , msg : "Changed RDP credentials" } ;
if ( parent . parent . db . changeStream ) { event . noact = 1 ; } // If DB change stream is active, don't use this event to change the node. Another event will come.
parent . parent . DispatchEvent ( parent . CreateMeshDispatchTargets ( node . meshid , [ obj . nodeid ] ) , obj , event ) ;
2021-06-30 03:13:18 +03:00
} ) ;
}
2021-04-30 08:31:01 +03:00
// When data is received from the web socket
// RDP default port is 3389
2022-05-06 02:16:58 +03:00
ws . on ( 'message' , function ( data ) {
2021-04-30 08:31:01 +03:00
try {
2022-05-06 02:16:58 +03:00
var msg = null ;
try { msg = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( msg == null ) || ( typeof msg != 'object' ) ) return ;
2021-04-30 08:31:01 +03:00
switch ( msg [ 0 ] ) {
2021-06-30 03:13:18 +03:00
case 'infos' : {
obj . infos = msg [ 1 ] ;
2022-05-03 07:06:32 +03:00
if ( obj . infos . ip . startsWith ( 'node/' ) ) {
// Use the user session
obj . nodeid = obj . infos . ip ;
obj . userid = req . session . userid ;
} else {
// Decode the authentication cookie
obj . cookie = parent . parent . decodeCookie ( obj . infos . ip , parent . parent . loginCookieEncryptionKey ) ;
if ( ( obj . cookie == null ) || ( typeof obj . cookie . nodeid != 'string' ) || ( typeof obj . cookie . userid != 'string' ) ) { obj . close ( ) ; return ; }
obj . nodeid = obj . cookie . nodeid ;
obj . userid = obj . cookie . userid ;
}
2021-06-30 03:13:18 +03:00
2022-05-08 01:47:50 +03:00
// Get node and rights
parent . GetNodeWithRights ( domain , obj . userid , obj . nodeid , function ( node , rights , visible ) {
2022-04-14 21:47:04 +03:00
if ( obj . ws == null ) return ; // obj has been cleaned up, just exit.
2022-05-08 01:47:50 +03:00
if ( ( node == null ) || ( visible == false ) || ( ( rights & MESHRIGHT _REMOTECONTROL ) == 0 ) ) { obj . close ( ) ; return ; }
if ( ( rights != MESHRIGHT _ADMIN ) && ( ( rights & MESHRIGHT _REMOTEVIEWONLY ) != 0 ) ) { obj . viewonly = true ; }
if ( ( rights != MESHRIGHT _ADMIN ) && ( ( rights & MESHRIGHT _DESKLIMITEDINPUT ) != 0 ) ) { obj . limitedinput = true ; }
2022-04-14 04:58:37 +03:00
obj . mtype = node . mtype ; // Store the device group type
obj . meshid = node . meshid ; // Store the MeshID
// Check if we need to relay thru a different agent
2022-04-14 21:47:04 +03:00
const mesh = parent . meshes [ obj . meshid ] ;
2022-04-14 04:58:37 +03:00
if ( mesh && mesh . relayid ) {
obj . relaynodeid = mesh . relayid ;
obj . tcpaddr = node . host ;
2022-05-04 02:24:43 +03:00
// Get the TCP port to use
var tcpport = 3389 ;
if ( ( obj . cookie != null ) && ( obj . cookie . tcpport != null ) ) { tcpport = obj . cookie . tcpport ; } else { if ( node . rdpport ) { tcpport = node . rdpport } }
2022-04-14 04:58:37 +03:00
// Re-encode a cookie with a device relay
2022-05-04 02:24:43 +03:00
const cookieContent = { userid : obj . userid , domainid : domain . id , nodeid : mesh . relayid , tcpaddr : node . host , tcpport : tcpport } ;
2022-05-03 07:06:32 +03:00
obj . infos . ip = parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ;
} else if ( obj . infos . ip . startsWith ( 'node/' ) ) {
// Encode a cookie with a device relay
const cookieContent = { userid : obj . userid , domainid : domain . id , nodeid : obj . nodeid , tcpport : node . rdpport ? node . rdpport : 3389 } ;
2022-04-14 04:58:37 +03:00
obj . infos . ip = parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ;
}
2021-06-30 03:13:18 +03:00
2022-04-14 21:47:04 +03:00
// Check if we have rights to the relayid device, does nothing if a relay is not used
checkRelayRights ( parent , domain , obj . userid , obj . relaynodeid , function ( allowed ) {
if ( obj . ws == null ) return ; // obj has been cleaned up, just exit.
if ( allowed !== true ) { parent . parent . debug ( 'relay' , 'RDP: Attempt to use un-authorized relay' ) ; obj . close ( ) ; return ; }
// Check if we need to load server stored credentials
if ( ( typeof obj . infos . options == 'object' ) && ( obj . infos . options . useServerCreds == true ) ) {
// Check if RDP credentials exist
2022-05-18 02:09:35 +03:00
if ( ( domain . allowsavingdevicecredentials !== false ) && ( typeof node . rdp == 'object' ) && ( typeof node . rdp [ obj . userid ] == 'object' ) && ( typeof node . rdp [ obj . userid ] . d == 'string' ) && ( typeof node . rdp [ obj . userid ] . u == 'string' ) && ( typeof node . rdp [ obj . userid ] . p == 'string' ) ) {
obj . infos . domain = node . rdp [ obj . userid ] . d ;
obj . infos . username = node . rdp [ obj . userid ] . u ;
obj . infos . password = node . rdp [ obj . userid ] . p ;
2022-04-14 21:47:04 +03:00
startTcpServer ( ) ;
} else {
// No server credentials.
obj . infos . domain = '' ;
obj . infos . username = '' ;
obj . infos . password = '' ;
startTcpServer ( ) ;
}
2021-06-30 03:13:18 +03:00
} else {
startTcpServer ( ) ;
}
2022-04-14 21:47:04 +03:00
} ) ;
2021-09-11 03:28:08 +03:00
} ) ;
2021-06-30 03:13:18 +03:00
break ;
}
2022-05-08 01:47:50 +03:00
case 'mouse' : { if ( rdpClient && ( obj . viewonly != true ) ) { rdpClient . sendPointerEvent ( msg [ 1 ] , msg [ 2 ] , msg [ 3 ] , msg [ 4 ] ) ; } break ; }
case 'wheel' : { if ( rdpClient && ( obj . viewonly != true ) ) { rdpClient . sendWheelEvent ( msg [ 1 ] , msg [ 2 ] , msg [ 3 ] , msg [ 4 ] ) ; } break ; }
2022-05-15 09:00:57 +03:00
case 'clipboard' : { rdpClient . setClipboardData ( msg [ 1 ] ) ; break ; }
2022-05-08 01:47:50 +03:00
case 'scancode' : {
if ( obj . limitedinput == true ) { // Limit keyboard input
var ok = false , k = msg [ 1 ] ;
if ( ( k >= 2 ) && ( k <= 11 ) ) { ok = true ; } // Number keys 1 to 0
if ( ( k >= 16 ) && ( k <= 25 ) ) { ok = true ; } // First keyboard row
if ( ( k >= 30 ) && ( k <= 38 ) ) { ok = true ; } // Second keyboard row
if ( ( k >= 44 ) && ( k <= 50 ) ) { ok = true ; } // Third keyboard row
if ( ( k == 14 ) || ( k == 28 ) ) { ok = true ; } // Enter and backspace
if ( ok == false ) return ;
}
if ( rdpClient && ( obj . viewonly != true ) ) { rdpClient . sendKeyEventScancode ( msg [ 1 ] , msg [ 2 ] ) ; } break ;
}
case 'unicode' : { if ( rdpClient && ( obj . viewonly != true ) ) { rdpClient . sendKeyEventUnicode ( msg [ 1 ] , msg [ 2 ] ) ; } break ; }
2022-05-06 03:42:40 +03:00
case 'utype' : {
if ( ! rdpClient ) return ;
obj . utype = msg [ 1 ] ;
if ( obj . utypetimer == null ) {
obj . utypetimer = setInterval ( function ( ) {
if ( ( obj . utype == null ) || ( obj . utype . length == 0 ) ) { clearInterval ( obj . utypetimer ) ; obj . utypetimer = null ; return ; }
var c = obj . utype . charCodeAt ( 0 ) ;
obj . utype = obj . utype . substring ( 1 ) ;
if ( c == 13 ) return ;
if ( c == 10 ) { rdpClient . sendKeyEventScancode ( 28 , true ) ; rdpClient . sendKeyEventScancode ( 28 , false ) ; }
else { rdpClient . sendKeyEventUnicode ( c , true ) ; rdpClient . sendKeyEventUnicode ( c , false ) ; }
} , 5 ) ;
}
break ;
}
2022-05-05 23:17:34 +03:00
case 'ping' : { try { obj . wsClient . send ( '{"ctrlChannel":102938,"type":"ping"}' ) ; } catch ( ex ) { } break ; }
case 'pong' : { try { obj . wsClient . send ( '{"ctrlChannel":102938,"type":"pong"}' ) ; } catch ( ex ) { } break ; }
2021-04-30 08:31:01 +03:00
case 'disconnect' : { obj . close ( ) ; break ; }
}
} catch ( ex ) {
console . log ( 'RdpMessageException' , msg , ex ) ;
obj . close ( ) ;
}
} ) ;
// If error, do nothing
ws . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'RDP: Browser websocket error: ' + err ) ; obj . close ( ) ; } ) ;
// If the web socket is closed
ws . on ( 'close' , function ( req ) { parent . parent . debug ( 'relay' , 'RDP: Browser websocket closed' ) ; obj . close ( ) ; } ) ;
// Send an object with flow control
function send ( obj ) {
try { rdpClient . bufferLayer . socket . pause ( ) ; } catch ( ex ) { }
try { ws . send ( JSON . stringify ( obj ) , function ( ) { try { rdpClient . bufferLayer . socket . resume ( ) ; } catch ( ex ) { } } ) ; } catch ( ex ) { }
}
// We are all set, start receiving data
ws . _socket . resume ( ) ;
return obj ;
} ;
// Construct a SSH Relay object, called upon connection
module . exports . CreateSshRelay = function ( parent , db , ws , req , args , domain ) {
const Net = require ( 'net' ) ;
const WebSocket = require ( 'ws' ) ;
2022-05-18 02:09:35 +03:00
2021-04-30 08:31:01 +03:00
// SerialTunnel object is used to embed SSH within another connection.
function SerialTunnel ( options ) {
2022-04-14 21:47:04 +03:00
const obj = new require ( 'stream' ) . Duplex ( options ) ;
2021-04-30 08:31:01 +03:00
obj . forwardwrite = null ;
obj . updateBuffer = function ( chunk ) { this . push ( chunk ) ; } ;
2021-05-05 10:18:41 +03:00
obj . _write = function ( chunk , encoding , callback ) { if ( obj . forwardwrite != null ) { obj . forwardwrite ( chunk ) ; } if ( callback ) callback ( ) ; } ; // Pass data written to forward
2021-04-30 08:31:01 +03:00
obj . _read = function ( size ) { } ; // Push nothing, anything to read should be pushed from updateBuffer()
2021-05-05 10:18:41 +03:00
obj . destroy = function ( ) { delete obj . forwardwrite ; }
2021-04-30 08:31:01 +03:00
return obj ;
}
const obj = { } ;
obj . ws = ws ;
obj . relayActive = false ;
// Disconnect
obj . close = function ( arg ) {
2021-05-05 10:18:41 +03:00
if ( obj . ws == null ) return ;
2021-09-11 03:28:08 +03:00
// Event the session ending
if ( ( obj . startTime ) && ( obj . meshid != null ) ) {
// Collect how many raw bytes where received and sent.
// We sum both the websocket and TCP client in this case.
var inTraffc = obj . ws . _socket . bytesRead , outTraffc = obj . ws . _socket . bytesWritten ;
if ( obj . wsClient != null ) { inTraffc += obj . wsClient . _socket . bytesRead ; outTraffc += obj . wsClient . _socket . bytesWritten ; }
const sessionSeconds = Math . round ( ( Date . now ( ) - obj . startTime ) / 1000 ) ;
2022-02-22 21:56:45 +03:00
const user = parent . users [ obj . cookie . userid ] ;
const username = ( user != null ) ? user . name : null ;
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : obj . cookie . userid , username : username , sessionid : obj . sessionid , msgid : 123 , msgArgs : [ sessionSeconds , obj . sessionid ] , msg : "Left Web-SSH session \"" + obj . sessionid + "\" after " + sessionSeconds + " second(s)." , protocol : PROTOCOL _WEBSSH , bytesin : inTraffc , bytesout : outTraffc } ;
2021-09-11 03:28:08 +03:00
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , obj . cookie . userid , obj . meshid ] , obj , event ) ;
delete obj . startTime ;
2022-02-22 21:56:45 +03:00
delete obj . sessionid ;
2021-09-11 03:28:08 +03:00
}
2021-05-05 10:18:41 +03:00
if ( obj . sshShell ) {
obj . sshShell . destroy ( ) ;
obj . sshShell . removeAllListeners ( 'data' ) ;
obj . sshShell . removeAllListeners ( 'close' ) ;
try { obj . sshShell . end ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . sshShell ;
2021-04-30 08:31:01 +03:00
}
2021-05-05 10:18:41 +03:00
if ( obj . sshClient ) {
obj . sshClient . destroy ( ) ;
obj . sshClient . removeAllListeners ( 'ready' ) ;
2021-04-30 08:31:01 +03:00
try { obj . sshClient . end ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . sshClient ;
}
2021-05-05 10:18:41 +03:00
if ( obj . wsClient ) {
obj . wsClient . removeAllListeners ( 'open' ) ;
obj . wsClient . removeAllListeners ( 'message' ) ;
obj . wsClient . removeAllListeners ( 'close' ) ;
try { obj . wsClient . close ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . wsClient ;
2021-04-30 08:31:01 +03:00
}
2021-05-12 19:10:54 +03:00
if ( ( arg == 1 ) || ( arg == null ) ) { try { ws . close ( ) ; } catch ( ex ) { console . log ( ex ) ; } } // Soft close, close the websocket
if ( arg == 2 ) { try { ws . _socket . _parent . end ( ) ; } catch ( ex ) { console . log ( ex ) ; } } // Hard close, close the TCP socket
2021-05-05 10:18:41 +03:00
obj . ws . removeAllListeners ( ) ;
2021-04-30 08:31:01 +03:00
obj . relayActive = false ;
delete obj . termSize ;
delete obj . cookie ;
2021-09-11 03:28:08 +03:00
delete obj . nodeid ;
delete obj . meshid ;
2022-05-18 02:09:35 +03:00
delete obj . userid ;
2021-04-30 08:31:01 +03:00
delete obj . ws ;
} ;
2022-05-18 02:09:35 +03:00
// Save SSH credentials into database
2022-05-11 03:44:34 +03:00
function saveSshCredentials ( keep ) {
if ( ( ( keep != 1 ) && ( keep != 2 ) ) || ( domain . allowsavingdevicecredentials == false ) ) return ;
parent . parent . db . Get ( obj . nodeid , function ( err , nodes ) {
2021-09-07 22:42:51 +03:00
if ( ( err != null ) || ( nodes == null ) || ( nodes . length != 1 ) ) return ;
const node = nodes [ 0 ] ;
2022-05-18 02:09:35 +03:00
if ( node . ssh == null ) { node . ssh = { } ; }
2021-09-07 22:42:51 +03:00
// Check if credentials are the same
2022-05-18 02:09:35 +03:00
//if ((typeof node.ssh[obj.userid] == 'object') && (node.ssh[obj.userid].u == obj.username) && (node.ssh[obj.userid].p == obj.password)) return; // TODO
// Clear up any existing credentials or credentials for users that don't exist anymore
for ( var i in node . ssh ) { if ( ! i . startsWith ( 'user/' ) || ( parent . users [ i ] == null ) ) { delete node . ssh [ i ] ; } }
// Clear legacy credentials
delete node . ssh . u ;
delete node . ssh . p ;
delete node . ssh . k ;
delete node . ssh . kp ;
2021-09-07 22:42:51 +03:00
// Save the credentials
if ( obj . password != null ) {
2022-05-18 02:09:35 +03:00
node . ssh [ obj . userid ] = { u : obj . username , p : obj . password } ;
2021-09-07 22:42:51 +03:00
} else if ( obj . privateKey != null ) {
2022-05-18 02:09:35 +03:00
node . ssh [ obj . userid ] = { u : obj . username , k : obj . privateKey } ;
if ( keep == 2 ) { node . ssh [ obj . userid ] . kp = obj . privateKeyPass ; }
2021-09-07 22:42:51 +03:00
} else return ;
parent . parent . db . Set ( node ) ;
2022-05-18 02:09:35 +03:00
// Event the node change
const event = { etype : 'node' , action : 'changenode' , nodeid : obj . nodeid , domain : domain . id , userid : obj . userid , node : parent . CloneSafeNode ( node ) , msg : "Changed SSH credentials" } ;
if ( parent . parent . db . changeStream ) { event . noact = 1 ; } // If DB change stream is active, don't use this event to change the node. Another event will come.
parent . parent . DispatchEvent ( parent . CreateMeshDispatchTargets ( node . meshid , [ obj . nodeid ] ) , obj , event ) ;
2021-09-07 22:42:51 +03:00
} ) ;
}
2021-04-30 08:31:01 +03:00
// Start the looppback server
function startRelayConnection ( ) {
try {
// Setup the correct URL with domain and use TLS only if needed.
2022-04-14 21:47:04 +03:00
const options = { rejectUnauthorized : false } ;
const protocol = ( args . tlsoffload ) ? 'ws' : 'wss' ;
2021-04-30 08:31:01 +03:00
var domainadd = '' ;
if ( ( domain . dns == null ) && ( domain . id != '' ) ) { domainadd = domain . id + '/' }
2022-05-05 23:17:34 +03:00
const url = protocol + '://localhost:' + args . port + '/' + domainadd + ( ( ( obj . mtype == 3 ) && ( obj . relaynodeid == null ) ) ? 'local' : 'mesh' ) + 'relay.ashx?p=11&auth=' + obj . xcookie ; // Protocol 11 is Web-SSH
2021-04-30 08:31:01 +03:00
parent . parent . debug ( 'relay' , 'SSH: Connection websocket to ' + url ) ;
obj . wsClient = new WebSocket ( url , options ) ;
obj . wsClient . on ( 'open' , function ( ) { parent . parent . debug ( 'relay' , 'SSH: Relay websocket open' ) ; } ) ;
obj . wsClient . on ( 'message' , function ( data ) { // Make sure to handle flow control.
2022-05-05 23:17:34 +03:00
if ( obj . relayActive == false ) {
if ( ( data == 'c' ) || ( data == 'cr' ) ) {
obj . relayActive = true ;
// Create a serial tunnel && SSH module
obj . ser = new SerialTunnel ( ) ;
const Client = require ( 'ssh2' ) . Client ;
obj . sshClient = new Client ( ) ;
obj . sshClient . on ( 'ready' , function ( ) { // Authentication was successful.
// If requested, save the credentials
2022-05-11 03:44:34 +03:00
saveSshCredentials ( obj . keep ) ;
2022-05-05 23:17:34 +03:00
obj . sessionid = Buffer . from ( parent . crypto . randomBytes ( 9 ) , 'binary' ) . toString ( 'base64' ) ;
obj . startTime = Date . now ( ) ;
// Event start of session
try {
const user = parent . users [ obj . cookie . userid ] ;
const username = ( user != null ) ? user . name : null ;
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 148 , msgArgs : [ obj . sessionid ] , msg : "Started Web-SSH session \"" + obj . sessionid + "\"." , protocol : PROTOCOL _WEBSSH } ;
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , user . _id , obj . meshid ] , obj , event ) ;
} catch ( ex ) { console . log ( ex ) ; }
obj . sshClient . shell ( function ( err , stream ) { // Start a remote shell
if ( err ) { obj . close ( ) ; return ; }
obj . sshShell = stream ;
obj . sshShell . setWindow ( obj . termSize . rows , obj . termSize . cols , obj . termSize . height , obj . termSize . width ) ;
obj . sshShell . on ( 'close' , function ( ) { obj . close ( ) ; } ) ;
obj . sshShell . on ( 'data' , function ( data ) { obj . ws . send ( '~' + data . toString ( ) ) ; } ) ;
} ) ;
obj . ws . send ( JSON . stringify ( { action : 'connected' } ) ) ;
} ) ;
obj . sshClient . on ( 'error' , function ( err ) {
if ( err . level == 'client-authentication' ) { try { obj . ws . send ( JSON . stringify ( { action : 'autherror' } ) ) ; } catch ( ex ) { } }
if ( err . level == 'client-timeout' ) { try { obj . ws . send ( JSON . stringify ( { action : 'sessiontimeout' } ) ) ; } catch ( ex ) { } }
obj . close ( ) ;
2021-04-30 08:31:01 +03:00
} ) ;
2022-05-05 23:17:34 +03:00
// Setup the serial tunnel, SSH ---> Relay WS
obj . ser . forwardwrite = function ( data ) { if ( ( data . length > 0 ) && ( obj . wsClient != null ) ) { try { obj . wsClient . send ( data ) ; } catch ( ex ) { } } } ;
// Connect the SSH module to the serial tunnel
const connectionOptions = { sock : obj . ser }
if ( typeof obj . username == 'string' ) { connectionOptions . username = obj . username ; }
if ( typeof obj . password == 'string' ) { connectionOptions . password = obj . password ; }
if ( typeof obj . privateKey == 'string' ) { connectionOptions . privateKey = obj . privateKey ; }
if ( typeof obj . privateKeyPass == 'string' ) { connectionOptions . passphrase = obj . privateKeyPass ; }
try {
obj . sshClient . connect ( connectionOptions ) ;
} catch ( ex ) {
// Exception, this is generally because we did not provide proper credentials. Ask again.
obj . relayActive = false ;
delete obj . sshClient ;
delete obj . ser . forwardwrite ;
obj . close ( ) ;
return ;
}
2021-04-30 08:31:01 +03:00
2022-05-05 23:17:34 +03:00
// We are all set, start receiving data
ws . _socket . resume ( ) ;
}
2021-04-30 08:31:01 +03:00
} else {
2022-05-06 02:16:58 +03:00
if ( typeof data == 'string' ) {
// Forward any ping/pong commands to the browser
var cmd = null ;
try { cmd = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( cmd != null ) && ( cmd . ctrlChannel == '102938' ) && ( ( cmd . type == 'ping' ) || ( cmd . type == 'pong' ) ) ) { obj . ws . send ( data ) ; }
return ;
}
2022-05-05 23:17:34 +03:00
2021-04-30 08:31:01 +03:00
// Relay WS --> SSH
if ( ( data . length > 0 ) && ( obj . ser != null ) ) { try { obj . ser . updateBuffer ( data ) ; } catch ( ex ) { console . log ( ex ) ; } }
}
} ) ;
obj . wsClient . on ( 'close' , function ( ) { parent . parent . debug ( 'relay' , 'SSH: Relay websocket closed' ) ; obj . close ( ) ; } ) ;
obj . wsClient . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'SSH: Relay websocket error: ' + err ) ; obj . close ( ) ; } ) ;
} catch ( ex ) {
console . log ( ex ) ;
}
}
// When data is received from the web socket
// SSH default port is 22
2022-05-06 02:16:58 +03:00
ws . on ( 'message' , function ( data ) {
2021-04-30 08:31:01 +03:00
try {
2022-05-06 02:16:58 +03:00
if ( typeof data != 'string' ) return ;
if ( data [ 0 ] == '{' ) {
2021-04-30 08:31:01 +03:00
// Control data
2022-05-06 02:16:58 +03:00
var msg = null ;
try { msg = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( msg == null ) || ( typeof msg != 'object' ) ) return ;
if ( ( msg . ctrlChannel == '102938' ) && ( ( msg . type == 'ping' ) || ( msg . type == 'pong' ) ) ) { try { obj . wsClient . send ( data ) ; } catch ( ex ) { } return ; }
2021-04-30 08:31:01 +03:00
if ( typeof msg . action != 'string' ) return ;
switch ( msg . action ) {
case 'connect' : {
2021-09-07 22:42:51 +03:00
if ( msg . useexisting ) {
// Check if we have SSH credentials for this device
parent . parent . db . Get ( obj . cookie . nodeid , function ( err , nodes ) {
if ( ( err != null ) || ( nodes == null ) || ( nodes . length != 1 ) ) return ;
const node = nodes [ 0 ] ;
2022-05-20 00:41:32 +03:00
if ( ( domain . allowsavingdevicecredentials === false ) || ( node . ssh == null ) || ( typeof node . ssh != 'object' ) || ( node . ssh [ obj . userid ] == null ) || ( typeof node . ssh [ obj . userid ] . u != 'string' ) || ( ( typeof node . ssh [ obj . userid ] . p != 'string' ) && ( typeof node . ssh [ obj . userid ] . k != 'string' ) ) ) {
2021-09-07 22:42:51 +03:00
// Send a request for SSH authentication
try { ws . send ( JSON . stringify ( { action : 'sshauth' } ) ) } catch ( ex ) { }
2022-05-20 00:41:32 +03:00
} else if ( ( domain . allowsavingdevicecredentials !== false ) && ( node . ssh != null ) && ( typeof node . ssh [ obj . userid ] . k == 'string' ) && ( node . ssh [ obj . userid ] . kp == null ) ) {
2022-05-11 03:44:34 +03:00
// Send a request for SSH authentication with option for only the private key password
2022-05-20 00:41:32 +03:00
obj . username = node . ssh [ obj . userid ] . u ;
obj . privateKey = node . ssh [ obj . userid ] . k ;
2022-05-11 03:44:34 +03:00
try { ws . send ( JSON . stringify ( { action : 'sshauth' , askkeypass : true } ) ) } catch ( ex ) { }
2021-09-07 22:42:51 +03:00
} else {
// Use our existing credentials
obj . termSize = msg ;
2022-05-11 03:44:34 +03:00
delete obj . keep ;
2022-05-20 00:41:32 +03:00
obj . username = node . ssh [ obj . userid ] . u ;
if ( typeof node . ssh [ obj . userid ] . p == 'string' ) {
obj . password = node . ssh [ obj . userid ] . p ;
} else if ( typeof node . ssh [ obj . userid ] . k == 'string' ) {
obj . privateKey = node . ssh [ obj . userid ] . k ;
obj . privateKeyPass = node . ssh [ obj . userid ] . kp ;
2021-09-07 22:42:51 +03:00
}
startRelayConnection ( ) ;
}
} ) ;
} else {
// Verify inputs
if ( ( typeof msg . username != 'string' ) || ( ( typeof msg . password != 'string' ) && ( typeof msg . key != 'string' ) ) ) break ;
if ( ( typeof msg . rows != 'number' ) || ( typeof msg . cols != 'number' ) || ( typeof msg . height != 'number' ) || ( typeof msg . width != 'number' ) ) break ;
obj . termSize = msg ;
2022-05-11 03:44:34 +03:00
if ( msg . keep === true ) { msg . keep = 1 ; } // If true, change to 1. For user/pass, 1 to store user/pass in db. For user/key/pass, 1 to store user/key in db, 2 to store everything in db.
obj . keep = msg . keep ; // If set, keep store credentials on the server if the SSH tunnel connected succesfully.
2021-09-07 22:42:51 +03:00
obj . username = msg . username ;
obj . password = msg . password ;
obj . privateKey = msg . key ;
obj . privateKeyPass = msg . keypass ;
startRelayConnection ( ) ;
}
2021-04-30 08:31:01 +03:00
break ;
}
2022-05-11 03:44:34 +03:00
case 'connectKeyPass' : {
// Verify inputs
if ( typeof msg . keypass != 'string' ) break ;
// Check if we have SSH credentials for this device
obj . privateKeyPass = msg . keypass ;
obj . termSize = msg ;
parent . parent . db . Get ( obj . cookie . nodeid , function ( err , nodes ) {
if ( ( err != null ) || ( nodes == null ) || ( nodes . length != 1 ) ) return ;
const node = nodes [ 0 ] ;
if ( node . ssh != null ) {
obj . username = node . ssh . u ;
obj . privateKey = node . ssh . k ;
startRelayConnection ( ) ;
}
} ) ;
break ;
}
2021-04-30 08:31:01 +03:00
case 'resize' : {
2021-05-09 05:03:35 +03:00
// Verify inputs
if ( ( typeof msg . rows != 'number' ) || ( typeof msg . cols != 'number' ) || ( typeof msg . height != 'number' ) || ( typeof msg . width != 'number' ) ) break ;
2021-04-30 08:31:01 +03:00
obj . termSize = msg ;
if ( obj . sshShell != null ) { obj . sshShell . setWindow ( obj . termSize . rows , obj . termSize . cols , obj . termSize . height , obj . termSize . width ) ; }
break ;
}
}
2022-05-06 02:16:58 +03:00
} else if ( data [ 0 ] == '~' ) {
2021-04-30 08:31:01 +03:00
// Terminal data
2022-05-06 02:16:58 +03:00
if ( obj . sshShell != null ) { obj . sshShell . write ( data . substring ( 1 ) ) ; }
2021-04-30 08:31:01 +03:00
}
2021-05-05 10:18:41 +03:00
} catch ( ex ) { obj . close ( ) ; }
2021-04-30 08:31:01 +03:00
} ) ;
// If error, do nothing
ws . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'SSH: Browser websocket error: ' + err ) ; obj . close ( ) ; } ) ;
// If the web socket is closed
ws . on ( 'close' , function ( req ) { parent . parent . debug ( 'relay' , 'SSH: Browser websocket closed' ) ; obj . close ( ) ; } ) ;
2021-09-11 03:28:08 +03:00
2022-04-14 21:47:04 +03:00
parent . parent . debug ( 'relay' , 'SSH: Request for SSH relay (' + req . clientIp + ')' ) ;
// Decode the authentication cookie
obj . cookie = parent . parent . decodeCookie ( req . query . auth , parent . parent . loginCookieEncryptionKey ) ;
2022-05-20 00:41:32 +03:00
if ( ( obj . cookie == null ) || ( obj . cookie . userid == null ) || ( parent . users [ obj . cookie . userid ] == null ) ) { obj . ws . send ( JSON . stringify ( { action : 'sessionerror' } ) ) ; obj . close ( ) ; return ; }
obj . userid = obj . cookie . userid ;
2022-04-14 21:47:04 +03:00
2021-09-11 03:28:08 +03:00
// Get the meshid for this device
parent . parent . db . Get ( obj . cookie . nodeid , function ( err , nodes ) {
2022-04-14 21:47:04 +03:00
if ( obj . cookie == null ) return ; // obj has been cleaned up, just exit.
2021-09-11 03:28:08 +03:00
if ( ( err != null ) || ( nodes == null ) || ( nodes . length != 1 ) ) { parent . parent . debug ( 'relay' , 'SSH: Invalid device' ) ; obj . close ( ) ; }
const node = nodes [ 0 ] ;
obj . nodeid = node . _id ; // Store the NodeID
obj . meshid = node . meshid ; // Store the MeshID
2022-04-14 04:58:37 +03:00
obj . mtype = node . mtype ; // Store the device group type
// Check if we need to relay thru a different agent
2022-04-14 21:47:04 +03:00
const mesh = parent . meshes [ obj . meshid ] ;
2022-04-14 04:58:37 +03:00
if ( mesh && mesh . relayid ) {
obj . relaynodeid = mesh . relayid ;
obj . tcpaddr = node . host ;
2022-04-14 21:47:04 +03:00
// Check if we have rights to the relayid device, does nothing if a relay is not used
checkRelayRights ( parent , domain , obj . cookie . userid , obj . relaynodeid , function ( allowed ) {
if ( obj . cookie == null ) return ; // obj has been cleaned up, just exit.
if ( allowed !== true ) { parent . parent . debug ( 'relay' , 'SSH: Attempt to use un-authorized relay' ) ; obj . close ( ) ; return ; }
// Re-encode a cookie with a device relay
const cookieContent = { userid : obj . cookie . userid , domainid : obj . cookie . domainid , nodeid : mesh . relayid , tcpaddr : node . host , tcpport : obj . cookie . tcpport } ;
obj . xcookie = parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ;
} ) ;
2022-04-14 04:58:37 +03:00
} else {
obj . xcookie = req . query . auth ;
}
2021-09-11 03:28:08 +03:00
} ) ;
2021-04-30 08:31:01 +03:00
return obj ;
2021-05-09 04:09:49 +03:00
} ;
// Construct a SSH Terminal Relay object, called upon connection
module . exports . CreateSshTerminalRelay = function ( parent , db , ws , req , domain , user , cookie , args ) {
const Net = require ( 'net' ) ;
const WebSocket = require ( 'ws' ) ;
// SerialTunnel object is used to embed SSH within another connection.
function SerialTunnel ( options ) {
2022-04-14 21:47:04 +03:00
const obj = new require ( 'stream' ) . Duplex ( options ) ;
2021-05-09 04:09:49 +03:00
obj . forwardwrite = null ;
obj . updateBuffer = function ( chunk ) { this . push ( chunk ) ; } ;
obj . _write = function ( chunk , encoding , callback ) { if ( obj . forwardwrite != null ) { obj . forwardwrite ( chunk ) ; } if ( callback ) callback ( ) ; } ; // Pass data written to forward
obj . _read = function ( size ) { } ; // Push nothing, anything to read should be pushed from updateBuffer()
obj . destroy = function ( ) { delete obj . forwardwrite ; }
return obj ;
}
const obj = { } ;
obj . ws = ws ;
obj . relayActive = false ;
2021-05-25 20:13:18 +03:00
parent . parent . debug ( 'relay' , 'SSH: Request for SSH terminal relay (' + req . clientIp + ')' ) ;
2021-05-09 04:09:49 +03:00
// Disconnect
obj . close = function ( arg ) {
if ( obj . ws == null ) return ;
2021-09-11 03:28:08 +03:00
// Event the session ending
if ( obj . startTime ) {
// Collect how many raw bytes where received and sent.
// We sum both the websocket and TCP client in this case.
var inTraffc = obj . ws . _socket . bytesRead , outTraffc = obj . ws . _socket . bytesWritten ;
if ( obj . wsClient != null ) { inTraffc += obj . wsClient . _socket . bytesRead ; outTraffc += obj . wsClient . _socket . bytesWritten ; }
const sessionSeconds = Math . round ( ( Date . now ( ) - obj . startTime ) / 1000 ) ;
2022-02-22 21:56:45 +03:00
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 123 , msgArgs : [ sessionSeconds , obj . sessionid ] , msg : "Left Web-SSH session \"" + obj . sessionid + "\" after " + sessionSeconds + " second(s)." , protocol : PROTOCOL _WEBSSH , bytesin : inTraffc , bytesout : outTraffc } ;
2021-09-11 03:28:08 +03:00
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , user . _id , obj . meshid ] , obj , event ) ;
delete obj . startTime ;
2022-02-22 21:56:45 +03:00
delete obj . sessionid ;
2021-09-11 03:28:08 +03:00
}
2021-05-09 04:09:49 +03:00
if ( obj . sshShell ) {
obj . sshShell . destroy ( ) ;
obj . sshShell . removeAllListeners ( 'data' ) ;
obj . sshShell . removeAllListeners ( 'close' ) ;
try { obj . sshShell . end ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . sshShell ;
}
if ( obj . sshClient ) {
obj . sshClient . destroy ( ) ;
obj . sshClient . removeAllListeners ( 'ready' ) ;
try { obj . sshClient . end ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . sshClient ;
}
if ( obj . wsClient ) {
obj . wsClient . removeAllListeners ( 'open' ) ;
obj . wsClient . removeAllListeners ( 'message' ) ;
obj . wsClient . removeAllListeners ( 'close' ) ;
try { obj . wsClient . close ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . wsClient ;
}
2021-05-12 19:10:54 +03:00
if ( ( arg == 1 ) || ( arg == null ) ) { try { ws . close ( ) ; } catch ( ex ) { console . log ( ex ) ; } } // Soft close, close the websocket
if ( arg == 2 ) { try { ws . _socket . _parent . end ( ) ; } catch ( ex ) { console . log ( ex ) ; } } // Hard close, close the TCP socket
2021-05-09 04:09:49 +03:00
obj . ws . removeAllListeners ( ) ;
obj . relayActive = false ;
delete obj . termSize ;
2021-09-11 03:28:08 +03:00
delete obj . nodeid ;
delete obj . meshid ;
2021-05-09 04:09:49 +03:00
delete obj . ws ;
} ;
2021-05-21 23:43:32 +03:00
// Save SSH credentials into device
2022-05-11 03:44:34 +03:00
function saveSshCredentials ( keep ) {
if ( ( ( keep != 1 ) && ( keep != 2 ) ) || ( domain . allowsavingdevicecredentials == false ) ) return ;
2021-05-21 23:43:32 +03:00
parent . parent . db . Get ( obj . nodeid , function ( err , nodes ) {
if ( ( err != null ) || ( nodes == null ) || ( nodes . length != 1 ) ) return ;
const node = nodes [ 0 ] ;
2022-05-18 02:09:35 +03:00
if ( node . ssh == null ) { node . ssh = { } ; }
2021-05-21 23:43:32 +03:00
2021-06-30 03:13:18 +03:00
// Check if credentials are the same
2021-08-13 08:52:42 +03:00
//if ((typeof node.ssh == 'object') && (node.ssh.u == obj.username) && (node.ssh.p == obj.password)) return; // TODO
2021-06-30 03:13:18 +03:00
2022-05-18 02:09:35 +03:00
// Clear up any existing credentials or credentials for users that don't exist anymore
for ( var i in node . ssh ) { if ( ! i . startsWith ( 'user/' ) || ( parent . users [ i ] == null ) ) { delete node . ssh [ i ] ; } }
// Clear legacy credentials
delete node . ssh . u ;
delete node . ssh . p ;
delete node . ssh . k ;
delete node . ssh . kp ;
2021-05-21 23:43:32 +03:00
// Save the credentials
2021-08-13 08:52:42 +03:00
if ( obj . password != null ) {
2022-05-18 02:09:35 +03:00
node . ssh [ user . _id ] = { u : obj . username , p : obj . password } ;
2021-08-13 08:52:42 +03:00
} else if ( obj . privateKey != null ) {
2022-05-18 02:09:35 +03:00
node . ssh [ user . _id ] = { u : obj . username , k : obj . privateKey } ;
if ( keep == 2 ) { node . ssh [ user . _id ] . kp = obj . privateKeyPass ; }
2022-05-11 03:44:34 +03:00
} else return ;
2021-05-21 23:43:32 +03:00
parent . parent . db . Set ( node ) ;
2022-05-18 02:09:35 +03:00
// Event the node change
const event = { etype : 'node' , action : 'changenode' , nodeid : obj . nodeid , domain : domain . id , userid : user . _id , username : user . name , node : parent . CloneSafeNode ( node ) , msg : "Changed SSH credentials" } ;
if ( parent . parent . db . changeStream ) { event . noact = 1 ; } // If DB change stream is active, don't use this event to change the node. Another event will come.
parent . parent . DispatchEvent ( parent . CreateMeshDispatchTargets ( node . meshid , [ obj . nodeid ] ) , obj , event ) ;
2021-05-21 23:43:32 +03:00
} ) ;
}
2022-05-18 02:09:35 +03:00
2021-05-09 04:09:49 +03:00
// Start the looppback server
function startRelayConnection ( authCookie ) {
try {
// Setup the correct URL with domain and use TLS only if needed.
2022-04-14 21:47:04 +03:00
const options = { rejectUnauthorized : false } ;
const protocol = ( args . tlsoffload ) ? 'ws' : 'wss' ;
2021-05-09 04:09:49 +03:00
var domainadd = '' ;
if ( ( domain . dns == null ) && ( domain . id != '' ) ) { domainadd = domain . id + '/' }
2022-05-05 23:17:34 +03:00
const url = protocol + '://localhost:' + args . port + '/' + domainadd + ( ( ( obj . mtype == 3 ) && ( obj . relaynodeid == null ) ) ? 'local' : 'mesh' ) + 'relay.ashx?p=11&auth=' + authCookie // Protocol 11 is Web-SSH
2021-05-09 04:09:49 +03:00
parent . parent . debug ( 'relay' , 'SSH: Connection websocket to ' + url ) ;
obj . wsClient = new WebSocket ( url , options ) ;
obj . wsClient . on ( 'open' , function ( ) { parent . parent . debug ( 'relay' , 'SSH: Relay websocket open' ) ; } ) ;
obj . wsClient . on ( 'message' , function ( data ) { // Make sure to handle flow control.
2022-05-05 23:17:34 +03:00
if ( obj . relayActive == false ) {
if ( ( data == 'c' ) || ( data == 'cr' ) ) {
obj . relayActive = true ;
// Create a serial tunnel && SSH module
obj . ser = new SerialTunnel ( ) ;
const Client = require ( 'ssh2' ) . Client ;
obj . sshClient = new Client ( ) ;
obj . sshClient . on ( 'ready' , function ( ) { // Authentication was successful.
// If requested, save the credentials
2022-05-11 03:44:34 +03:00
saveSshCredentials ( obj . keep ) ;
2022-05-05 23:17:34 +03:00
obj . sessionid = Buffer . from ( parent . crypto . randomBytes ( 9 ) , 'binary' ) . toString ( 'base64' ) ;
obj . startTime = Date . now ( ) ;
try {
// Event start of session
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 148 , msgArgs : [ obj . sessionid ] , msg : "Started Web-SSH session \"" + obj . sessionid + "\"." , protocol : PROTOCOL _WEBSSH } ;
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , user . _id , obj . meshid ] , obj , event ) ;
} catch ( ex ) {
console . log ( ex ) ;
}
2021-05-21 23:43:32 +03:00
2022-05-05 23:17:34 +03:00
obj . sshClient . shell ( function ( err , stream ) { // Start a remote shell
if ( err ) { obj . close ( ) ; return ; }
obj . sshShell = stream ;
obj . sshShell . setWindow ( obj . termSize . rows , obj . termSize . cols , obj . termSize . height , obj . termSize . width ) ;
obj . sshShell . on ( 'close' , function ( ) { obj . close ( ) ; } ) ;
obj . sshShell . on ( 'data' , function ( data ) { obj . ws . send ( '~' + data . toString ( ) ) ; } ) ;
} ) ;
2022-02-22 21:56:45 +03:00
2022-05-05 23:17:34 +03:00
obj . connected = true ;
obj . ws . send ( 'c' ) ;
} ) ;
obj . sshClient . on ( 'error' , function ( err ) {
if ( err . level == 'client-authentication' ) { try { obj . ws . send ( JSON . stringify ( { action : 'autherror' } ) ) ; } catch ( ex ) { } }
if ( err . level == 'client-timeout' ) { try { obj . ws . send ( JSON . stringify ( { action : 'sessiontimeout' } ) ) ; } catch ( ex ) { } }
obj . close ( ) ;
2021-05-09 04:09:49 +03:00
} ) ;
2021-05-25 05:22:29 +03:00
2022-05-05 23:17:34 +03:00
// Setup the serial tunnel, SSH ---> Relay WS
obj . ser . forwardwrite = function ( data ) { if ( ( data . length > 0 ) && ( obj . wsClient != null ) ) { try { obj . wsClient . send ( data ) ; } catch ( ex ) { } } } ;
2021-05-09 04:09:49 +03:00
2022-05-05 23:17:34 +03:00
// Connect the SSH module to the serial tunnel
const connectionOptions = { sock : obj . ser }
if ( typeof obj . username == 'string' ) { connectionOptions . username = obj . username ; }
if ( typeof obj . password == 'string' ) { connectionOptions . password = obj . password ; }
if ( typeof obj . privateKey == 'string' ) { connectionOptions . privateKey = obj . privateKey ; }
if ( typeof obj . privateKeyPass == 'string' ) { connectionOptions . passphrase = obj . privateKeyPass ; }
try {
obj . sshClient . connect ( connectionOptions ) ;
} catch ( ex ) {
// Exception, this is generally because we did not provide proper credentials. Ask again.
obj . relayActive = false ;
delete obj . sshClient ;
delete obj . ser . forwardwrite ;
2022-05-11 03:44:34 +03:00
try { ws . send ( JSON . stringify ( { action : 'sshauth' , askkeypass : ( ( obj . username != null ) && ( obj . privateKey != null ) ) } ) ) } catch ( ex ) { }
2022-05-05 23:17:34 +03:00
}
2021-05-09 04:09:49 +03:00
2022-05-05 23:17:34 +03:00
// We are all set, start receiving data
ws . _socket . resume ( ) ;
}
2021-05-09 04:09:49 +03:00
} else {
2022-05-06 02:16:58 +03:00
if ( typeof data == 'string' ) {
// Forward any ping/pong commands to the browser
var cmd = null ;
try { cmd = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( cmd != null ) && ( cmd . ctrlChannel == '102938' ) && ( ( cmd . type == 'ping' ) || ( cmd . type == 'pong' ) ) ) { try { obj . ws . send ( data ) ; } catch ( ex ) { console . log ( ex ) ; } }
return ;
}
2022-05-05 23:17:34 +03:00
2021-05-09 04:09:49 +03:00
// Relay WS --> SSH
if ( ( data . length > 0 ) && ( obj . ser != null ) ) { try { obj . ser . updateBuffer ( data ) ; } catch ( ex ) { console . log ( ex ) ; } }
}
} ) ;
2021-05-25 05:22:29 +03:00
obj . wsClient . on ( 'close' , function ( ) {
if ( obj . connected !== true ) { try { obj . ws . send ( JSON . stringify ( { action : 'connectionerror' } ) ) ; } catch ( ex ) { } }
parent . parent . debug ( 'relay' , 'SSH: Relay websocket closed' ) ; obj . close ( ) ;
} ) ;
2021-05-09 04:09:49 +03:00
obj . wsClient . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'SSH: Relay websocket error: ' + err ) ; obj . close ( ) ; } ) ;
} catch ( ex ) {
console . log ( ex ) ;
}
}
// When data is received from the web socket
// SSH default port is 22
2022-05-06 02:16:58 +03:00
ws . on ( 'message' , function ( data ) {
2021-05-09 04:09:49 +03:00
try {
2022-05-06 02:16:58 +03:00
if ( typeof data != 'string' ) return ;
if ( data [ 0 ] == '{' ) {
2021-05-09 04:09:49 +03:00
// Control data
2022-05-06 02:16:58 +03:00
var msg = null ;
try { msg = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( msg == null ) || ( typeof msg != 'object' ) ) return ;
if ( ( msg . ctrlChannel == '102938' ) && ( ( msg . type == 'ping' ) || ( msg . type == 'pong' ) ) ) { try { obj . wsClient . send ( data ) ; } catch ( ex ) { } return ; }
2021-05-09 04:09:49 +03:00
switch ( msg . action ) {
case 'sshauth' : {
2021-05-09 05:03:35 +03:00
// Verify inputs
2021-08-13 08:52:42 +03:00
if ( ( typeof msg . username != 'string' ) || ( ( typeof msg . password != 'string' ) && ( typeof msg . key != 'string' ) ) ) break ;
2021-05-09 05:03:35 +03:00
if ( ( typeof msg . rows != 'number' ) || ( typeof msg . cols != 'number' ) || ( typeof msg . height != 'number' ) || ( typeof msg . width != 'number' ) ) break ;
2022-05-11 03:44:34 +03:00
if ( msg . keep === true ) { msg . keep = 1 ; } // If true, change to 1. For user/pass, 1 to store user/pass in db. For user/key/pass, 1 to store user/key in db, 2 to store everything in db.
obj . keep = msg . keep ; // If set, keep store credentials on the server if the SSH tunnel connected succesfully.
2021-05-09 04:09:49 +03:00
obj . termSize = msg ;
obj . username = msg . username ;
obj . password = msg . password ;
2021-08-13 08:52:42 +03:00
obj . privateKey = msg . key ;
obj . privateKeyPass = msg . keypass ;
2021-05-09 04:09:49 +03:00
// Create a mesh relay authentication cookie
2022-04-14 21:47:04 +03:00
const cookieContent = { userid : user . _id , domainid : user . domain , nodeid : obj . nodeid , tcpport : obj . tcpport } ;
2022-04-14 04:58:37 +03:00
if ( obj . relaynodeid ) {
cookieContent . nodeid = obj . relaynodeid ;
cookieContent . tcpaddr = obj . tcpaddr ;
} else {
if ( obj . mtype == 3 ) { cookieContent . lc = 1 ; } // This is a local device
}
2021-05-09 04:09:49 +03:00
startRelayConnection ( parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ) ;
break ;
}
2022-05-11 03:44:34 +03:00
case 'sshkeyauth' : {
// Verify inputs
if ( typeof msg . keypass != 'string' ) break ;
if ( ( typeof msg . rows != 'number' ) || ( typeof msg . cols != 'number' ) || ( typeof msg . height != 'number' ) || ( typeof msg . width != 'number' ) ) break ;
delete obj . keep ;
obj . termSize = msg ;
obj . privateKeyPass = msg . keypass ;
// Create a mesh relay authentication cookie
const cookieContent = { userid : user . _id , domainid : user . domain , nodeid : obj . nodeid , tcpport : obj . tcpport } ;
if ( obj . relaynodeid ) {
cookieContent . nodeid = obj . relaynodeid ;
cookieContent . tcpaddr = obj . tcpaddr ;
} else {
if ( obj . mtype == 3 ) { cookieContent . lc = 1 ; } // This is a local device
}
startRelayConnection ( parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ) ;
break ;
}
2021-05-21 23:43:32 +03:00
case 'sshautoauth' : {
// Verify inputs
if ( ( typeof msg . rows != 'number' ) || ( typeof msg . cols != 'number' ) || ( typeof msg . height != 'number' ) || ( typeof msg . width != 'number' ) ) break ;
obj . termSize = msg ;
2021-08-13 08:52:42 +03:00
if ( ( obj . username == null ) || ( ( obj . password == null ) && ( obj . privateKey == null ) ) ) return ;
2021-05-21 23:43:32 +03:00
// Create a mesh relay authentication cookie
2022-04-14 21:47:04 +03:00
const cookieContent = { userid : user . _id , domainid : user . domain , nodeid : obj . nodeid , tcpport : obj . tcpport } ;
2022-04-14 04:58:37 +03:00
if ( obj . relaynodeid ) {
cookieContent . nodeid = obj . relaynodeid ;
cookieContent . tcpaddr = obj . tcpaddr ;
} else {
if ( obj . mtype == 3 ) { cookieContent . lc = 1 ; } // This is a local device
}
2021-05-21 23:43:32 +03:00
startRelayConnection ( parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ) ;
break ;
}
2021-05-09 04:09:49 +03:00
case 'resize' : {
2021-05-09 05:03:35 +03:00
// Verify inputs
if ( ( typeof msg . rows != 'number' ) || ( typeof msg . cols != 'number' ) || ( typeof msg . height != 'number' ) || ( typeof msg . width != 'number' ) ) break ;
2021-05-09 04:09:49 +03:00
obj . termSize = msg ;
if ( obj . sshShell != null ) { obj . sshShell . setWindow ( obj . termSize . rows , obj . termSize . cols , obj . termSize . height , obj . termSize . width ) ; }
break ;
}
}
2022-05-06 02:16:58 +03:00
} else if ( data [ 0 ] == '~' ) {
2021-05-09 04:09:49 +03:00
// Terminal data
2022-05-06 02:16:58 +03:00
if ( obj . sshShell != null ) { obj . sshShell . write ( data . substring ( 1 ) ) ; }
2021-05-09 04:09:49 +03:00
}
} catch ( ex ) { obj . close ( ) ; }
} ) ;
// If error, do nothing
ws . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'SSH: Browser websocket error: ' + err ) ; obj . close ( ) ; } ) ;
// If the web socket is closed
ws . on ( 'close' , function ( req ) { parent . parent . debug ( 'relay' , 'SSH: Browser websocket closed' ) ; obj . close ( ) ; } ) ;
2021-05-25 20:35:37 +03:00
// Check that we have a user and nodeid
if ( ( user == null ) || ( req . query . nodeid == null ) ) { obj . close ( ) ; return ; } // Invalid nodeid
2021-05-09 04:09:49 +03:00
parent . GetNodeWithRights ( domain , user , req . query . nodeid , function ( node , rights , visible ) {
2022-04-14 21:47:04 +03:00
if ( obj . ws == null ) return ; // obj has been cleaned up, just exit.
2021-05-09 04:09:49 +03:00
// Check permissions
if ( ( rights & 8 ) == 0 ) { obj . close ( ) ; return ; } // No MESHRIGHT_REMOTECONTROL rights
if ( ( rights != 0xFFFFFFFF ) && ( rights & 0x00000200 ) ) { obj . close ( ) ; return ; } // MESHRIGHT_NOTERMINAL is set
obj . mtype = node . mtype ; // Store the device group type
obj . nodeid = node . _id ; // Store the NodeID
2021-09-11 03:28:08 +03:00
obj . meshid = node . meshid ; // Store the MeshID
2021-05-09 04:09:49 +03:00
// Check the SSH port
obj . tcpport = 22 ;
if ( typeof node . sshport == 'number' ) { obj . tcpport = node . sshport ; }
2022-04-14 04:58:37 +03:00
// Check if we need to relay thru a different agent
2022-04-14 21:47:04 +03:00
const mesh = parent . meshes [ obj . meshid ] ;
2022-04-14 04:58:37 +03:00
if ( mesh && mesh . relayid ) { obj . relaynodeid = mesh . relayid ; obj . tcpaddr = node . host ; }
2022-04-14 21:47:04 +03:00
// Check if we have rights to the relayid device, does nothing if a relay is not used
checkRelayRights ( parent , domain , user , obj . relaynodeid , function ( allowed ) {
if ( obj . ws == null ) return ; // obj has been cleaned up, just exit.
if ( allowed !== true ) { parent . parent . debug ( 'relay' , 'SSH: Attempt to use un-authorized relay' ) ; obj . close ( ) ; return ; }
2021-05-09 04:09:49 +03:00
2022-04-14 21:47:04 +03:00
// We are all set, start receiving data
ws . _socket . resume ( ) ;
2021-05-21 23:43:32 +03:00
2022-04-14 21:47:04 +03:00
// Check if we have SSH credentials for this device
2022-05-18 02:09:35 +03:00
if ( ( domain . allowsavingdevicecredentials === false ) || ( node . ssh == null ) || ( typeof node . ssh != 'object' ) || ( node . ssh [ user . _id ] == null ) || ( typeof node . ssh [ user . _id ] . u != 'string' ) || ( ( typeof node . ssh [ user . _id ] . p != 'string' ) && ( typeof node . ssh [ user . _id ] . k != 'string' ) ) ) {
2021-05-21 23:43:32 +03:00
// Send a request for SSH authentication
try { ws . send ( JSON . stringify ( { action : 'sshauth' } ) ) } catch ( ex ) { }
2022-05-18 02:09:35 +03:00
} else if ( ( typeof node . ssh [ user . _id ] . k == 'string' ) && ( typeof node . ssh [ user . _id ] . kp != 'string' ) ) {
2022-05-11 03:44:34 +03:00
// Send a request for SSH authentication with option for only the private key password
2022-05-18 02:09:35 +03:00
obj . username = node . ssh [ user . _id ] . u ;
obj . privateKey = node . ssh [ user . _id ] . k ;
2022-05-11 03:44:34 +03:00
try { ws . send ( JSON . stringify ( { action : 'sshauth' , askkeypass : true } ) ) } catch ( ex ) { }
2021-05-21 23:43:32 +03:00
} else {
// Use our existing credentials
2022-05-18 02:09:35 +03:00
obj . username = node . ssh [ user . _id ] . u ;
if ( typeof node . ssh [ user . _id ] . p == 'string' ) {
obj . password = node . ssh [ user . _id ] . p ;
} else if ( typeof node . ssh [ user . _id ] . k == 'string' ) {
obj . privateKey = node . ssh [ user . _id ] . k ;
obj . privateKeyPass = node . ssh [ user . _id ] . kp ;
2021-08-13 08:52:42 +03:00
}
2021-05-21 23:43:32 +03:00
try { ws . send ( JSON . stringify ( { action : 'sshautoauth' } ) ) } catch ( ex ) { }
}
} ) ;
2021-05-09 04:09:49 +03:00
} ) ;
return obj ;
} ;
2021-05-25 01:26:54 +03:00
// Construct a SSH Files Relay object, called upon connection
module . exports . CreateSshFilesRelay = function ( parent , db , ws , req , domain , user , cookie , args ) {
const Net = require ( 'net' ) ;
const WebSocket = require ( 'ws' ) ;
// SerialTunnel object is used to embed SSH within another connection.
function SerialTunnel ( options ) {
2022-04-14 21:47:04 +03:00
const obj = new require ( 'stream' ) . Duplex ( options ) ;
2021-05-25 01:26:54 +03:00
obj . forwardwrite = null ;
obj . updateBuffer = function ( chunk ) { this . push ( chunk ) ; } ;
obj . _write = function ( chunk , encoding , callback ) { if ( obj . forwardwrite != null ) { obj . forwardwrite ( chunk ) ; } if ( callback ) callback ( ) ; } ; // Pass data written to forward
obj . _read = function ( size ) { } ; // Push nothing, anything to read should be pushed from updateBuffer()
obj . destroy = function ( ) { delete obj . forwardwrite ; }
return obj ;
}
const obj = { } ;
obj . ws = ws ;
obj . path = require ( 'path' ) ;
obj . relayActive = false ;
obj . firstMessage = true ;
parent . parent . debug ( 'relay' , 'SSH: Request for SSH files relay (' + req . clientIp + ')' ) ;
// Disconnect
obj . close = function ( arg ) {
if ( obj . ws == null ) return ;
2021-09-11 03:28:08 +03:00
// Event the session ending
if ( obj . startTime ) {
// Collect how many raw bytes where received and sent.
// We sum both the websocket and TCP client in this case.
var inTraffc = obj . ws . _socket . bytesRead , outTraffc = obj . ws . _socket . bytesWritten ;
if ( obj . wsClient != null ) { inTraffc += obj . wsClient . _socket . bytesRead ; outTraffc += obj . wsClient . _socket . bytesWritten ; }
const sessionSeconds = Math . round ( ( Date . now ( ) - obj . startTime ) / 1000 ) ;
2022-02-22 21:56:45 +03:00
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : user . _id , username : user . name , sessionid : obj . sessionid , msgid : 124 , msgArgs : [ sessionSeconds , obj . sessionid ] , msg : "Left Web-SFTP session \"" + obj . sessionid + "\" after " + sessionSeconds + " second(s)." , protocol : PROTOCOL _WEBSFTP , bytesin : inTraffc , bytesout : outTraffc } ;
2021-09-11 03:28:08 +03:00
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , user . _id , obj . meshid ] , obj , event ) ;
delete obj . startTime ;
2022-02-22 21:56:45 +03:00
delete obj . sessionid ;
2021-09-11 03:28:08 +03:00
}
2021-05-25 01:26:54 +03:00
if ( obj . sshClient ) {
obj . sshClient . destroy ( ) ;
obj . sshClient . removeAllListeners ( 'ready' ) ;
try { obj . sshClient . end ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . sshClient ;
}
if ( obj . wsClient ) {
obj . wsClient . removeAllListeners ( 'open' ) ;
obj . wsClient . removeAllListeners ( 'message' ) ;
obj . wsClient . removeAllListeners ( 'close' ) ;
try { obj . wsClient . close ( ) ; } catch ( ex ) { console . log ( ex ) ; }
delete obj . wsClient ;
}
if ( ( arg == 1 ) || ( arg == null ) ) { try { ws . close ( ) ; } catch ( ex ) { console . log ( ex ) ; } } // Soft close, close the websocket
if ( arg == 2 ) { try { ws . _socket . _parent . end ( ) ; } catch ( ex ) { console . log ( ex ) ; } } // Hard close, close the TCP socket
obj . ws . removeAllListeners ( ) ;
obj . relayActive = false ;
delete obj . sftp ;
2021-09-11 03:28:08 +03:00
delete obj . nodeid ;
delete obj . meshid ;
2021-05-25 01:26:54 +03:00
delete obj . ws ;
} ;
// Save SSH credentials into device
2022-05-11 03:44:34 +03:00
function saveSshCredentials ( keep ) {
if ( ( ( keep != 1 ) && ( keep != 2 ) ) || ( domain . allowsavingdevicecredentials == false ) ) return ;
2021-05-25 01:26:54 +03:00
parent . parent . db . Get ( obj . nodeid , function ( err , nodes ) {
if ( ( err != null ) || ( nodes == null ) || ( nodes . length != 1 ) ) return ;
const node = nodes [ 0 ] ;
2022-06-06 18:06:32 +03:00
if ( node . ssh == null ) { node . ssh = { } ; }
2021-05-25 01:26:54 +03:00
2021-08-13 08:52:42 +03:00
// Check if credentials are the same
2022-05-18 02:09:35 +03:00
//if ((typeof node.ssh[obj.userid] == 'object') && (node.ssh[obj.userid].u == obj.username) && (node.ssh[obj.userid].p == obj.password)) return; // TODO
// Clear up any existing credentials or credentials for users that don't exist anymore
for ( var i in node . ssh ) { if ( ! i . startsWith ( 'user/' ) || ( parent . users [ i ] == null ) ) { delete node . ssh [ i ] ; } }
// Clear legacy credentials
delete node . ssh . u ;
delete node . ssh . p ;
delete node . ssh . k ;
delete node . ssh . kp ;
2021-08-13 08:52:42 +03:00
2021-05-25 01:26:54 +03:00
// Save the credentials
2021-08-13 08:52:42 +03:00
if ( obj . password != null ) {
2022-05-18 02:09:35 +03:00
node . ssh [ user . _id ] = { u : obj . username , p : obj . password } ;
2021-08-13 08:52:42 +03:00
} else if ( obj . privateKey != null ) {
2022-05-18 02:09:35 +03:00
node . ssh [ user . _id ] = { u : obj . username , k : obj . privateKey } ;
if ( keep == 2 ) { node . ssh [ user . _id ] . kp = obj . privateKeyPass ; }
2022-05-11 03:44:34 +03:00
} else return ;
2021-05-25 01:26:54 +03:00
parent . parent . db . Set ( node ) ;
2022-05-18 02:09:35 +03:00
// Event the node change
const event = { etype : 'node' , action : 'changenode' , nodeid : obj . nodeid , domain : domain . id , userid : user . _id , username : user . name , node : parent . CloneSafeNode ( node ) , msg : "Changed SSH credentials" } ;
if ( parent . parent . db . changeStream ) { event . noact = 1 ; } // If DB change stream is active, don't use this event to change the node. Another event will come.
parent . parent . DispatchEvent ( parent . CreateMeshDispatchTargets ( node . meshid , [ obj . nodeid ] ) , obj , event ) ;
2021-05-25 01:26:54 +03:00
} ) ;
}
2022-05-18 02:09:35 +03:00
2021-05-25 01:26:54 +03:00
// Start the looppback server
function startRelayConnection ( authCookie ) {
try {
// Setup the correct URL with domain and use TLS only if needed.
2022-04-14 21:47:04 +03:00
const options = { rejectUnauthorized : false } ;
const protocol = ( args . tlsoffload ) ? 'ws' : 'wss' ;
2021-05-25 01:26:54 +03:00
var domainadd = '' ;
if ( ( domain . dns == null ) && ( domain . id != '' ) ) { domainadd = domain . id + '/' }
2022-05-05 23:17:34 +03:00
const url = protocol + '://localhost:' + args . port + '/' + domainadd + ( ( ( obj . mtype == 3 ) && ( obj . relaynodeid == null ) ) ? 'local' : 'mesh' ) + 'relay.ashx?p=13&auth=' + authCookie // Protocol 13 is Web-SSH-Files
2021-05-25 01:26:54 +03:00
parent . parent . debug ( 'relay' , 'SSH: Connection websocket to ' + url ) ;
obj . wsClient = new WebSocket ( url , options ) ;
obj . wsClient . on ( 'open' , function ( ) { parent . parent . debug ( 'relay' , 'SSH: Relay websocket open' ) ; } ) ;
obj . wsClient . on ( 'message' , function ( data ) { // Make sure to handle flow control.
2022-05-05 23:17:34 +03:00
if ( obj . relayActive == false ) {
if ( ( data == 'c' ) || ( data == 'cr' ) ) {
obj . relayActive = true ;
// Create a serial tunnel && SSH module
obj . ser = new SerialTunnel ( ) ;
const Client = require ( 'ssh2' ) . Client ;
obj . sshClient = new Client ( ) ;
obj . sshClient . on ( 'ready' , function ( ) { // Authentication was successful.
// If requested, save the credentials
2022-05-11 03:44:34 +03:00
saveSshCredentials ( obj . keep ) ;
2022-05-05 23:17:34 +03:00
obj . sessionid = Buffer . from ( parent . crypto . randomBytes ( 9 ) , 'binary' ) . toString ( 'base64' ) ;
obj . startTime = Date . now ( ) ;
2022-02-22 21:56:45 +03:00
2022-05-05 23:17:34 +03:00
// Event start of session
try {
const event = { etype : 'relay' , action : 'relaylog' , domain : domain . id , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 149 , msgArgs : [ obj . sessionid ] , msg : "Started Web-SFTP session \"" + obj . sessionid + "\"." , protocol : PROTOCOL _WEBSFTP } ;
parent . parent . DispatchEvent ( [ '*' , obj . nodeid , user . _id , obj . meshid ] , obj , event ) ;
} catch ( ex ) { console . log ( ex ) ; }
obj . sshClient . sftp ( function ( err , sftp ) {
if ( err ) { obj . close ( ) ; return ; }
obj . connected = true ;
obj . sftp = sftp ;
obj . ws . send ( 'c' ) ;
} ) ;
} ) ;
obj . sshClient . on ( 'error' , function ( err ) {
if ( err . level == 'client-authentication' ) { try { obj . ws . send ( JSON . stringify ( { action : 'autherror' } ) ) ; } catch ( ex ) { } }
if ( err . level == 'client-timeout' ) { try { obj . ws . send ( JSON . stringify ( { action : 'sessiontimeout' } ) ) ; } catch ( ex ) { } }
obj . close ( ) ;
2021-05-25 01:26:54 +03:00
} ) ;
2022-05-05 23:17:34 +03:00
// Setup the serial tunnel, SSH ---> Relay WS
obj . ser . forwardwrite = function ( data ) { if ( ( data . length > 0 ) && ( obj . wsClient != null ) ) { try { obj . wsClient . send ( data ) ; } catch ( ex ) { } } } ;
2021-05-25 01:26:54 +03:00
2022-05-05 23:17:34 +03:00
// Connect the SSH module to the serial tunnel
const connectionOptions = { sock : obj . ser }
if ( typeof obj . username == 'string' ) { connectionOptions . username = obj . username ; }
if ( typeof obj . password == 'string' ) { connectionOptions . password = obj . password ; }
if ( typeof obj . privateKey == 'string' ) { connectionOptions . privateKey = obj . privateKey ; }
if ( typeof obj . privateKeyPass == 'string' ) { connectionOptions . passphrase = obj . privateKeyPass ; }
try {
obj . sshClient . connect ( connectionOptions ) ;
} catch ( ex ) {
// Exception, this is generally because we did not provide proper credentials. Ask again.
obj . relayActive = false ;
delete obj . sshClient ;
delete obj . ser . forwardwrite ;
2022-05-11 03:44:34 +03:00
try { ws . send ( JSON . stringify ( { action : 'sshauth' , askkeypass : ( ( obj . username != null ) && ( obj . privateKey != null ) ) } ) ) } catch ( ex ) { }
2022-05-05 23:17:34 +03:00
}
// We are all set, start receiving data
ws . _socket . resume ( ) ;
}
2021-05-25 01:26:54 +03:00
} else {
2022-05-06 02:16:58 +03:00
if ( typeof data == 'string' ) {
// Forward any ping/pong commands to the browser
var cmd = null ;
try { cmd = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( cmd != null ) && ( cmd . ctrlChannel == '102938' ) && ( ( cmd . type == 'ping' ) || ( cmd . type == 'pong' ) ) ) { obj . ws . send ( data ) ; }
return ;
}
2022-05-05 23:17:34 +03:00
2021-05-25 01:26:54 +03:00
// Relay WS --> SSH
if ( ( data . length > 0 ) && ( obj . ser != null ) ) { try { obj . ser . updateBuffer ( data ) ; } catch ( ex ) { console . log ( ex ) ; } }
}
} ) ;
2021-05-25 05:22:29 +03:00
obj . wsClient . on ( 'close' , function ( ) {
if ( obj . connected !== true ) { try { obj . ws . send ( JSON . stringify ( { action : 'connectionerror' } ) ) ; } catch ( ex ) { } }
parent . parent . debug ( 'relay' , 'SSH: Files relay websocket closed' ) ; obj . close ( ) ;
} ) ;
2021-05-25 01:26:54 +03:00
obj . wsClient . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'SSH: Files relay websocket error: ' + err ) ; obj . close ( ) ; } ) ;
} catch ( ex ) {
console . log ( ex ) ;
}
}
// When data is received from the web socket
// SSH default port is 22
2022-05-06 02:16:58 +03:00
ws . on ( 'message' , function ( data ) {
2021-05-25 05:22:29 +03:00
//if ((obj.firstMessage === true) && (msg != 5)) { obj.close(); return; } else { delete obj.firstMessage; }
2021-05-25 01:26:54 +03:00
try {
2022-05-06 02:16:58 +03:00
if ( typeof data != 'string' ) {
if ( data [ 0 ] == 123 ) {
data = data . toString ( ) ;
2021-05-25 01:26:54 +03:00
} else if ( ( obj . sftp != null ) && ( obj . uploadHandle != null ) ) {
2022-05-06 02:16:58 +03:00
const off = ( data [ 0 ] == 0 ) ? 1 : 0 ;
obj . sftp . write ( obj . uploadHandle , data , off , data . length - off , obj . uploadPosition , function ( err ) {
2021-05-25 01:26:54 +03:00
if ( err != null ) {
obj . sftp . close ( obj . uploadHandle , function ( ) { } ) ;
try { obj . ws . send ( Buffer . from ( JSON . stringify ( { action : 'uploaddone' , reqid : obj . uploadReqid } ) ) ) } catch ( ex ) { }
delete obj . uploadHandle ;
delete obj . uploadFullpath ;
delete obj . uploadSize ;
delete obj . uploadReqid ;
delete obj . uploadPosition ;
} else {
try { obj . ws . send ( Buffer . from ( JSON . stringify ( { action : 'uploadack' , reqid : obj . uploadReqid } ) ) ) } catch ( ex ) { }
}
} ) ;
2022-05-06 02:16:58 +03:00
obj . uploadPosition += ( data . length - off ) ;
2021-05-25 01:26:54 +03:00
return ;
}
}
2022-05-06 02:16:58 +03:00
if ( data [ 0 ] == '{' ) {
2021-05-25 01:26:54 +03:00
// Control data
2022-05-06 02:16:58 +03:00
var msg = null ;
try { msg = JSON . parse ( data ) ; } catch ( ex ) { }
if ( ( msg == null ) || ( typeof msg != 'object' ) ) return ;
if ( ( msg . ctrlChannel == '102938' ) && ( ( msg . type == 'ping' ) || ( msg . type == 'pong' ) ) ) { try { obj . wsClient . send ( data ) ; } catch ( ex ) { } return ; }
2021-05-25 01:26:54 +03:00
if ( typeof msg . action != 'string' ) return ;
switch ( msg . action ) {
case 'ls' : {
if ( obj . sftp == null ) return ;
var requestedPath = msg . path ;
if ( requestedPath . startsWith ( '/' ) == false ) { requestedPath = '/' + requestedPath ; }
obj . sftp . readdir ( requestedPath , function ( err , list ) {
if ( err ) { console . log ( err ) ; obj . close ( ) ; }
2022-04-14 21:47:04 +03:00
const r = { path : requestedPath , reqid : msg . reqid , dir : [ ] } ;
2021-05-25 01:26:54 +03:00
for ( var i in list ) {
2022-04-14 21:47:04 +03:00
const file = list [ i ] ;
2021-05-25 01:26:54 +03:00
if ( file . longname [ 0 ] == 'd' ) { r . dir . push ( { t : 2 , n : file . filename , d : new Date ( file . attrs . mtime * 1000 ) . toISOString ( ) } ) ; }
else { r . dir . push ( { t : 3 , n : file . filename , d : new Date ( file . attrs . mtime * 1000 ) . toISOString ( ) , s : file . attrs . size } ) ; }
}
try { obj . ws . send ( Buffer . from ( JSON . stringify ( r ) ) ) } catch ( ex ) { }
} ) ;
break ;
}
case 'mkdir' : {
if ( obj . sftp == null ) return ;
var requestedPath = msg . path ;
if ( requestedPath . startsWith ( '/' ) == false ) { requestedPath = '/' + requestedPath ; }
2021-05-25 05:47:29 +03:00
obj . sftp . mkdir ( requestedPath , function ( err ) { } ) ;
// Event the file delete
2022-04-14 21:47:04 +03:00
const targets = [ '*' , 'server-users' ] ;
2021-05-25 05:47:29 +03:00
if ( user . groups ) { for ( var i in user . groups ) { targets . push ( 'server-users:' + i ) ; } }
parent . parent . DispatchEvent ( targets , obj , { etype : 'node' , action : 'agentlog' , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 44 , msgArgs : [ requestedPath ] , msg : 'Create folder: \"' + requestedPath + '\"' , domain : domain . id } ) ;
2021-05-25 01:26:54 +03:00
break ;
}
case 'rm' : {
if ( obj . sftp == null ) return ;
var requestedPath = msg . path ;
if ( requestedPath . startsWith ( '/' ) == false ) { requestedPath = '/' + requestedPath ; }
for ( var i in msg . delfiles ) {
const ul = obj . path . join ( requestedPath , msg . delfiles [ i ] ) . split ( '\\' ) . join ( '/' ) ;
obj . sftp . unlink ( ul , function ( err ) { } ) ;
if ( msg . rec === true ) { obj . sftp . rmdir ( ul + '/' , function ( err ) { } ) ; }
2021-05-25 05:47:29 +03:00
// Event the file delete
2022-04-14 21:47:04 +03:00
const targets = [ '*' , 'server-users' ] ;
2021-05-25 05:47:29 +03:00
if ( user . groups ) { for ( var i in user . groups ) { targets . push ( 'server-users:' + i ) ; } }
parent . parent . DispatchEvent ( targets , obj , { etype : 'node' , action : 'agentlog' , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 45 , msgArgs : [ ul ] , msg : 'Delete: \"' + ul + '\"' , domain : domain . id } ) ;
2021-05-25 01:26:54 +03:00
}
2021-05-25 05:47:29 +03:00
2021-05-25 01:26:54 +03:00
break ;
}
case 'rename' : {
if ( obj . sftp == null ) return ;
var requestedPath = msg . path ;
if ( requestedPath . startsWith ( '/' ) == false ) { requestedPath = '/' + requestedPath ; }
const oldpath = obj . path . join ( requestedPath , msg . oldname ) . split ( '\\' ) . join ( '/' ) ;
const newpath = obj . path . join ( requestedPath , msg . newname ) . split ( '\\' ) . join ( '/' ) ;
obj . sftp . rename ( oldpath , newpath , function ( err ) { } ) ;
2021-05-25 05:47:29 +03:00
// Event the file rename
2022-04-14 21:47:04 +03:00
const targets = [ '*' , 'server-users' ] ;
2021-05-25 05:47:29 +03:00
if ( user . groups ) { for ( var i in user . groups ) { targets . push ( 'server-users:' + i ) ; } }
parent . parent . DispatchEvent ( targets , obj , { etype : 'node' , action : 'agentlog' , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 48 , msgArgs : [ oldpath , msg . newname ] , msg : 'Rename: \"' + oldpath + '\" to \"' + msg . newname + '\"' , domain : domain . id } ) ;
2021-05-25 01:26:54 +03:00
break ;
}
case 'upload' : {
if ( obj . sftp == null ) return ;
var requestedPath = msg . path ;
if ( requestedPath . startsWith ( '/' ) == false ) { requestedPath = '/' + requestedPath ; }
obj . uploadFullpath = obj . path . join ( requestedPath , msg . name ) . split ( '\\' ) . join ( '/' ) ;
obj . uploadSize = msg . size ;
obj . uploadReqid = msg . reqid ;
obj . uploadPosition = 0 ;
obj . sftp . open ( obj . uploadFullpath , 'w' , 0o666 , function ( err , handle ) {
if ( err != null ) {
try { obj . ws . send ( Buffer . from ( JSON . stringify ( { action : 'uploaderror' , reqid : obj . uploadReqid } ) ) ) } catch ( ex ) { }
} else {
obj . uploadHandle = handle ;
try { obj . ws . send ( Buffer . from ( JSON . stringify ( { action : 'uploadstart' , reqid : obj . uploadReqid } ) ) ) } catch ( ex ) { }
2021-05-25 05:47:29 +03:00
// Event the file upload
2022-04-14 21:47:04 +03:00
const targets = [ '*' , 'server-users' ] ;
2021-05-25 05:47:29 +03:00
if ( user . groups ) { for ( var i in user . groups ) { targets . push ( 'server-users:' + i ) ; } }
parent . parent . DispatchEvent ( targets , obj , { etype : 'node' , action : 'agentlog' , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 105 , msgArgs : [ obj . uploadFullpath , obj . uploadSize ] , msg : 'Upload: ' + obj . uploadFullpath + ', Size: ' + obj . uploadSize , domain : domain . id } ) ;
2021-05-25 01:26:54 +03:00
}
} ) ;
break ;
}
case 'uploaddone' : {
if ( obj . sftp == null ) return ;
if ( obj . uploadHandle != null ) {
obj . sftp . close ( obj . uploadHandle , function ( ) { } ) ;
try { obj . ws . send ( Buffer . from ( JSON . stringify ( { action : 'uploaddone' , reqid : obj . uploadReqid } ) ) ) } catch ( ex ) { }
delete obj . uploadHandle ;
delete obj . uploadFullpath ;
delete obj . uploadSize ;
delete obj . uploadReqid ;
delete obj . uploadPosition ;
}
break ;
}
case 'uploadcancel' : {
if ( obj . sftp == null ) return ;
if ( obj . uploadHandle != null ) {
obj . sftp . close ( obj . uploadHandle , function ( ) { } ) ;
obj . sftp . unlink ( obj . uploadFullpath , function ( err ) { } ) ;
try { obj . ws . send ( Buffer . from ( JSON . stringify ( { action : 'uploadcancel' , reqid : obj . uploadReqid } ) ) ) } catch ( ex ) { }
delete obj . uploadHandle ;
delete obj . uploadFullpath ;
delete obj . uploadSize ;
delete obj . uploadReqid ;
delete obj . uploadPosition ;
}
break ;
}
2021-05-25 04:13:53 +03:00
case 'download' : {
if ( obj . sftp == null ) return ;
switch ( msg . sub ) {
case 'start' : {
var requestedPath = msg . path ;
if ( requestedPath . startsWith ( '/' ) == false ) { requestedPath = '/' + requestedPath ; }
obj . downloadFullpath = requestedPath ;
obj . downloadId = msg . id ;
obj . downloadPosition = 0 ;
obj . downloadBuffer = Buffer . alloc ( 16384 ) ;
obj . sftp . open ( obj . downloadFullpath , 'r' , function ( err , handle ) {
if ( err != null ) {
try { obj . ws . send ( Buffer . from ( JSON . stringify ( { action : 'download' , sub : 'cancel' , id : obj . downloadId } ) ) ) } catch ( ex ) { }
} else {
obj . downloadHandle = handle ;
try { obj . ws . send ( JSON . stringify ( { action : 'download' , sub : 'start' , id : obj . downloadId } ) ) } catch ( ex ) { }
2021-05-25 05:47:29 +03:00
// Event the file download
2022-04-14 21:47:04 +03:00
const targets = [ '*' , 'server-users' ] ;
2021-05-25 05:47:29 +03:00
if ( user . groups ) { for ( var i in user . groups ) { targets . push ( 'server-users:' + i ) ; } }
parent . parent . DispatchEvent ( targets , obj , { etype : 'node' , action : 'agentlog' , nodeid : obj . nodeid , userid : user . _id , username : user . name , msgid : 49 , msgArgs : [ obj . downloadFullpath ] , msg : 'Download: ' + obj . downloadFullpath , domain : domain . id } ) ;
2021-05-25 04:13:53 +03:00
}
} ) ;
break ;
}
case 'startack' : {
if ( ( obj . downloadHandle == null ) || ( obj . downloadId != msg . id ) ) break ;
obj . downloadPendingBlockCount = ( typeof msg . ack == 'number' ) ? msg . ack : 8 ;
uploadNextBlock ( ) ;
break ;
}
case 'ack' : {
if ( ( obj . downloadHandle == null ) || ( obj . downloadId != msg . id ) ) break ;
if ( obj . downloadPendingBlockCount == 0 ) { obj . downloadPendingBlockCount = 1 ; uploadNextBlock ( ) ; }
break ;
}
case 'stop' : {
if ( ( obj . downloadHandle == null ) || ( obj . downloadId != msg . id ) ) break ;
if ( obj . downloadHandle != null ) { obj . sftp . close ( obj . downloadHandle , function ( ) { } ) ; }
delete obj . downloadId ;
delete obj . downloadBuffer ;
delete obj . downloadHandle ;
delete obj . downloadFullpath ;
delete obj . downloadPosition ;
delete obj . downloadPendingBlockCount ;
break ;
}
}
break ;
}
2021-05-25 01:26:54 +03:00
case 'sshauth' : {
if ( obj . sshClient != null ) return ;
// Verify inputs
2021-08-13 08:52:42 +03:00
if ( ( typeof msg . username != 'string' ) || ( ( typeof msg . password != 'string' ) && ( typeof msg . key != 'string' ) ) ) break ;
2021-05-25 01:26:54 +03:00
2022-05-11 03:44:34 +03:00
if ( msg . keep === true ) { msg . keep = 1 ; } // If true, change to 1. For user/pass, 1 to store user/pass in db. For user/key/pass, 1 to store user/key in db, 2 to store everything in db.
obj . keep = msg . keep ; // If set, keep store credentials on the server if the SSH tunnel connected succesfully.
2021-05-25 01:26:54 +03:00
obj . username = msg . username ;
obj . password = msg . password ;
2021-08-13 08:52:42 +03:00
obj . privateKey = msg . key ;
obj . privateKeyPass = msg . keypass ;
2021-05-25 01:26:54 +03:00
2022-05-11 03:44:34 +03:00
// Create a mesh relay authentication cookie
const cookieContent = { userid : user . _id , domainid : user . domain , nodeid : obj . nodeid , tcpport : obj . tcpport } ;
if ( obj . relaynodeid ) {
cookieContent . nodeid = obj . relaynodeid ;
cookieContent . tcpaddr = obj . tcpaddr ;
} else {
if ( obj . mtype == 3 ) { cookieContent . lc = 1 ; } // This is a local device
}
startRelayConnection ( parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ) ;
break ;
}
case 'sshkeyauth' : {
if ( obj . sshClient != null ) return ;
// Verify inputs
if ( typeof msg . keypass != 'string' ) break ;
delete obj . keep ;
obj . privateKeyPass = msg . keypass ;
2021-05-25 01:26:54 +03:00
// Create a mesh relay authentication cookie
2022-04-14 21:47:04 +03:00
const cookieContent = { userid : user . _id , domainid : user . domain , nodeid : obj . nodeid , tcpport : obj . tcpport } ;
2022-04-14 04:58:37 +03:00
if ( obj . relaynodeid ) {
cookieContent . nodeid = obj . relaynodeid ;
cookieContent . tcpaddr = obj . tcpaddr ;
} else {
if ( obj . mtype == 3 ) { cookieContent . lc = 1 ; } // This is a local device
}
2021-05-25 01:26:54 +03:00
startRelayConnection ( parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ) ;
break ;
}
}
}
2021-05-25 05:47:29 +03:00
} catch ( ex ) { obj . close ( ) ; }
2021-05-25 01:26:54 +03:00
} ) ;
2021-05-25 04:13:53 +03:00
function uploadNextBlock ( ) {
if ( obj . downloadBuffer == null ) return ;
obj . sftp . read ( obj . downloadHandle , obj . downloadBuffer , 4 , obj . downloadBuffer . length - 4 , obj . downloadPosition , function ( err , len , buf ) {
obj . downloadPendingBlockCount -- ;
if ( obj . downloadBuffer == null ) return ;
if ( err != null ) {
try { obj . ws . send ( Buffer . from ( JSON . stringify ( { action : 'download' , sub : 'cancel' , id : obj . downloadId } ) ) ) } catch ( ex ) { }
} else {
obj . downloadPosition += len ;
if ( len < ( obj . downloadBuffer . length - 4 ) ) {
obj . downloadBuffer . writeInt32BE ( 0x01000001 , 0 )
if ( len > 0 ) { try { obj . ws . send ( obj . downloadBuffer . slice ( 0 , len + 4 ) ) ; } catch ( ex ) { console . log ( ex ) ; } }
} else {
obj . downloadBuffer . writeInt32BE ( 0x01000000 , 0 ) ;
try { obj . ws . send ( obj . downloadBuffer . slice ( 0 , len + 4 ) ) ; } catch ( ex ) { console . log ( ex ) ; }
if ( obj . downloadPendingBlockCount > 0 ) { uploadNextBlock ( ) ; }
return ;
}
}
if ( obj . downloadHandle != null ) { obj . sftp . close ( obj . downloadHandle , function ( ) { } ) ; }
delete obj . downloadId ;
delete obj . downloadBuffer ;
delete obj . downloadHandle ;
delete obj . downloadFullpath ;
delete obj . downloadPosition ;
delete obj . downloadPendingBlockCount ;
} ) ;
}
2021-05-25 01:26:54 +03:00
// If error, do nothing
ws . on ( 'error' , function ( err ) { parent . parent . debug ( 'relay' , 'SSH: Browser websocket error: ' + err ) ; obj . close ( ) ; } ) ;
// If the web socket is closed
ws . on ( 'close' , function ( req ) { parent . parent . debug ( 'relay' , 'SSH: Browser websocket closed' ) ; obj . close ( ) ; } ) ;
2021-05-25 20:35:37 +03:00
// Check that we have a user and nodeid
if ( ( user == null ) || ( req . query . nodeid == null ) ) { obj . close ( ) ; return ; } // Invalid nodeid
2021-05-25 01:26:54 +03:00
parent . GetNodeWithRights ( domain , user , req . query . nodeid , function ( node , rights , visible ) {
2022-04-14 21:47:04 +03:00
if ( obj . ws == null ) return ; // obj has been cleaned up, just exit.
2021-05-25 01:26:54 +03:00
// Check permissions
if ( ( rights & 8 ) == 0 ) { obj . close ( ) ; return ; } // No MESHRIGHT_REMOTECONTROL rights
if ( ( rights != 0xFFFFFFFF ) && ( rights & 0x00000200 ) ) { obj . close ( ) ; return ; } // MESHRIGHT_NOTERMINAL is set
obj . mtype = node . mtype ; // Store the device group type
obj . nodeid = node . _id ; // Store the NodeID
2021-09-11 03:28:08 +03:00
obj . meshid = node . meshid ; // Store the MeshID
2021-05-25 01:26:54 +03:00
// Check the SSH port
obj . tcpport = 22 ;
if ( typeof node . sshport == 'number' ) { obj . tcpport = node . sshport ; }
2022-04-14 04:58:37 +03:00
// Check if we need to relay thru a different agent
2022-04-14 21:47:04 +03:00
const mesh = parent . meshes [ obj . meshid ] ;
2022-04-14 04:58:37 +03:00
if ( mesh && mesh . relayid ) { obj . relaynodeid = mesh . relayid ; obj . tcpaddr = node . host ; }
2022-04-14 21:47:04 +03:00
// Check if we have rights to the relayid device, does nothing if a relay is not used
checkRelayRights ( parent , domain , user , obj . relaynodeid , function ( allowed ) {
if ( obj . ws == null ) return ; // obj has been cleaned up, just exit.
if ( allowed !== true ) { parent . parent . debug ( 'relay' , 'SSH: Attempt to use un-authorized relay' ) ; obj . close ( ) ; return ; }
2021-05-25 01:26:54 +03:00
2022-04-14 21:47:04 +03:00
// We are all set, start receiving data
ws . _socket . resume ( ) ;
2021-05-25 01:26:54 +03:00
2022-04-14 21:47:04 +03:00
// Check if we have SSH credentials for this device
2022-05-18 02:09:35 +03:00
if ( ( domain . allowsavingdevicecredentials === false ) || ( node . ssh == null ) || ( typeof node . ssh != 'object' ) || ( node . ssh [ user . _id ] == null ) || ( typeof node . ssh [ user . _id ] . u != 'string' ) || ( ( typeof node . ssh [ user . _id ] . p != 'string' ) && ( typeof node . ssh [ user . _id ] . k != 'string' ) ) ) {
2021-05-25 01:26:54 +03:00
// Send a request for SSH authentication
try { ws . send ( JSON . stringify ( { action : 'sshauth' } ) ) } catch ( ex ) { }
2022-05-18 02:09:35 +03:00
} else if ( ( typeof node . ssh [ user . _id ] . k == 'string' ) && ( typeof node . ssh [ user . _id ] . kp != 'string' ) ) {
2022-05-11 03:44:34 +03:00
// Send a request for SSH authentication with option for only the private key password
2022-05-18 02:09:35 +03:00
obj . username = node . ssh [ user . _id ] . u ;
obj . privateKey = node . ssh [ user . _id ] . k ;
2022-05-11 03:44:34 +03:00
try { ws . send ( JSON . stringify ( { action : 'sshauth' , askkeypass : true } ) ) } catch ( ex ) { }
2021-05-25 01:26:54 +03:00
} else {
// Use our existing credentials
2022-05-18 02:09:35 +03:00
obj . username = node . ssh [ user . _id ] . u ;
if ( typeof node . ssh [ user . _id ] . p == 'string' ) {
obj . password = node . ssh [ user . _id ] . p ;
} else if ( typeof node . ssh [ user . _id ] . k == 'string' ) {
obj . privateKey = node . ssh [ user . _id ] . k ;
obj . privateKeyPass = node . ssh [ user . _id ] . kp ;
2021-08-13 08:52:42 +03:00
}
2021-05-25 01:26:54 +03:00
// Create a mesh relay authentication cookie
2022-04-14 21:47:04 +03:00
const cookieContent = { userid : user . _id , domainid : user . domain , nodeid : obj . nodeid , tcpport : obj . tcpport } ;
2022-04-14 04:58:37 +03:00
if ( obj . relaynodeid ) {
cookieContent . nodeid = obj . relaynodeid ;
cookieContent . tcpaddr = obj . tcpaddr ;
} else {
if ( obj . mtype == 3 ) { cookieContent . lc = 1 ; } // This is a local device
}
2021-05-25 01:26:54 +03:00
startRelayConnection ( parent . parent . encodeCookie ( cookieContent , parent . parent . loginCookieEncryptionKey ) ) ;
}
} ) ;
} ) ;
return obj ;
} ;
2022-04-14 21:47:04 +03:00
// Check that the user has full rights on a relay device before allowing it.
function checkRelayRights ( parent , domain , user , relayNodeId , func ) {
if ( relayNodeId == null ) { func ( true ) ; return ; } // No relay, do nothing.
parent . GetNodeWithRights ( domain , user , relayNodeId , function ( node , rights , visible ) {
func ( ( node != null ) && ( rights == 0xFFFFFFFF ) ) ;
} ) ;
}