mirror of
				https://github.com/Ylianst/MeshCentral.git
				synced 2025-03-09 15:40:18 +00:00 
			
		
		
		
	Improved RDP error handling, #4022
This commit is contained in:
		
							parent
							
								
									3e97d80470
								
							
						
					
					
						commit
						783ff4be0c
					
				
					 6 changed files with 311 additions and 281 deletions
				
			
		| 
						 | 
					@ -207,7 +207,8 @@ module.exports.CreateMstscRelay = function (parent, db, ws, req, args, domain) {
 | 
				
			||||||
            }).on('close', function () {
 | 
					            }).on('close', function () {
 | 
				
			||||||
                send(['rdp-close']);
 | 
					                send(['rdp-close']);
 | 
				
			||||||
            }).on('error', function (err) {
 | 
					            }).on('error', function (err) {
 | 
				
			||||||
                send(['rdp-error', err]);
 | 
					                if (typeof err == 'string') { send(['rdp-error', err]); }
 | 
				
			||||||
 | 
					                if ((typeof err == 'object') && (err.err) && (err.code)) { send(['rdp-error', err.err, err.code]); }
 | 
				
			||||||
            }).connect('localhost', obj.tcpServerPort);
 | 
					            }).connect('localhost', obj.tcpServerPort);
 | 
				
			||||||
        } catch (ex) {
 | 
					        } catch (ex) {
 | 
				
			||||||
            console.log('startRdpException', ex);
 | 
					            console.log('startRdpException', ex);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ var CreateRDPDesktop = function (canvasid) {
 | 
				
			||||||
    obj.ScreenWidth = obj.width = 1280;
 | 
					    obj.ScreenWidth = obj.width = 1280;
 | 
				
			||||||
    obj.ScreenHeight = obj.height = 1024;
 | 
					    obj.ScreenHeight = obj.height = 1024;
 | 
				
			||||||
    obj.m.onClipboardChanged = null;
 | 
					    obj.m.onClipboardChanged = null;
 | 
				
			||||||
 | 
					    obj.onConsoleMessageChange = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function mouseButtonMap(button) {
 | 
					    function mouseButtonMap(button) {
 | 
				
			||||||
        // Swap mouse buttons if needed
 | 
					        // Swap mouse buttons if needed
 | 
				
			||||||
| 
						 | 
					@ -79,8 +80,28 @@ var CreateRDPDesktop = function (canvasid) {
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    case 'rdp-error': {
 | 
					                    case 'rdp-error': {
 | 
				
			||||||
                        var err = msg[1];
 | 
					                        obj.consoleMessageTimeout = 5; // Seconds
 | 
				
			||||||
                        console.log('[mstsc.js] error : ' + err.code + '(' + err.message + ')');
 | 
					                        obj.consoleMessage = msg[1];
 | 
				
			||||||
 | 
					                        delete obj.consoleMessageArgs;
 | 
				
			||||||
 | 
					                        if (msg.length > 2) { obj.consoleMessageArgs = [ msg[2] ]; }
 | 
				
			||||||
 | 
					                        switch (msg[1]) {
 | 
				
			||||||
 | 
					                            case 'NODE_RDP_PROTOCOL_X224_NEG_FAILURE':
 | 
				
			||||||
 | 
					                                if (msg[2] == 1) { obj.consoleMessageId = 9; } // "SSL required by server";
 | 
				
			||||||
 | 
					                                else if (msg[2] == 2) { obj.consoleMessageId = 10; } // "SSL not allowed by server";
 | 
				
			||||||
 | 
					                                else if (msg[2] == 3) { obj.consoleMessageId = 11; } // "SSL certificate not on server";
 | 
				
			||||||
 | 
					                                else if (msg[2] == 4) { obj.consoleMessageId = 12; } // "Inconsistent flags";
 | 
				
			||||||
 | 
					                                else if (msg[2] == 5) { obj.consoleMessageId = 13; } // "Hybrid required by server";
 | 
				
			||||||
 | 
					                                else if (msg[2] == 6) { obj.consoleMessageId = 14; } // "SSL with user auth required by server";
 | 
				
			||||||
 | 
					                                else obj.consoleMessageId = 7; // "Protocol negotiation failed";
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                            case 'NODE_RDP_PROTOCOL_X224_NLA_NOT_SUPPORTED':
 | 
				
			||||||
 | 
					                                obj.consoleMessageId = 8; // "NLA not supported";
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                            default:
 | 
				
			||||||
 | 
					                                obj.consoleMessageId = null;
 | 
				
			||||||
 | 
					                                break;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        if (obj.onConsoleMessageChange) { obj.onConsoleMessageChange(); }
 | 
				
			||||||
                        obj.Stop();
 | 
					                        obj.Stop();
 | 
				
			||||||
                        break;
 | 
					                        break;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,14 +12,14 @@ const data = require('./data');
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class Cliprdr extends EventEmitter {
 | 
					class Cliprdr extends EventEmitter {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(transport) {
 | 
					    constructor(transport) {
 | 
				
			||||||
    super();
 | 
					        super();
 | 
				
			||||||
    this.transport = transport;
 | 
					        this.transport = transport;
 | 
				
			||||||
    // must be init via connect event
 | 
					        // must be init via connect event
 | 
				
			||||||
    this.userId = 0;
 | 
					        this.userId = 0;
 | 
				
			||||||
    this.serverCapabilities = [];
 | 
					        this.serverCapabilities = [];
 | 
				
			||||||
    this.clientCapabilities = [];
 | 
					        this.clientCapabilities = [];
 | 
				
			||||||
  }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,298 +30,298 @@ class Cliprdr extends EventEmitter {
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class Client extends Cliprdr {
 | 
					class Client extends Cliprdr {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(transport, fastPathTransport) {
 | 
					    constructor(transport, fastPathTransport) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    super(transport, fastPathTransport);
 | 
					        super(transport, fastPathTransport);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.transport.once('connect', (gccCore, userId, channelId) => {
 | 
					        this.transport.once('connect', (gccCore, userId, channelId) => {
 | 
				
			||||||
      this.connect(gccCore, userId, channelId);
 | 
					            this.connect(gccCore, userId, channelId);
 | 
				
			||||||
    }).on('close', () => {
 | 
					        }).on('close', function () {
 | 
				
			||||||
      this.emit('close');
 | 
					            //this.emit('close');
 | 
				
			||||||
    }).on('error', (err) => {
 | 
					        }).on('error', function (err) {
 | 
				
			||||||
      this.emit('error', err);
 | 
					            //this.emit('error', err);
 | 
				
			||||||
    });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.content = '';
 | 
					        this.content = '';
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * connect function
 | 
					 | 
				
			||||||
   * @param gccCore {type.Component(clientCoreData)}
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  connect(gccCore, userId, channelId) {
 | 
					 | 
				
			||||||
    this.gccCore = gccCore;
 | 
					 | 
				
			||||||
    this.userId = userId;
 | 
					 | 
				
			||||||
    this.channelId = channelId;
 | 
					 | 
				
			||||||
    this.transport.once('cliprdr', (s) => {
 | 
					 | 
				
			||||||
      this.recv(s);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  send(message) {
 | 
					 | 
				
			||||||
    this.transport.send('cliprdr', new type.Component([
 | 
					 | 
				
			||||||
      // Channel PDU Header
 | 
					 | 
				
			||||||
      new type.UInt32Le(message.size()),
 | 
					 | 
				
			||||||
      // CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST | CHANNEL_FLAG_SHOW_PROTOCOL
 | 
					 | 
				
			||||||
      new type.UInt32Le(0x13),
 | 
					 | 
				
			||||||
      message
 | 
					 | 
				
			||||||
    ]));
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  recv(s) {
 | 
					 | 
				
			||||||
    s.offset = 18;
 | 
					 | 
				
			||||||
    const pdu = data.clipPDU().read(s), type = data.ClipPDUMsgType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    switch (pdu.obj.header.obj.msgType.value) {
 | 
					 | 
				
			||||||
      case type.CB_MONITOR_READY:
 | 
					 | 
				
			||||||
        this.recvMonitorReadyPDU(s);
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case type.CB_FORMAT_LIST:
 | 
					 | 
				
			||||||
        this.recvFormatListPDU(s);
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case type.CB_FORMAT_LIST_RESPONSE:
 | 
					 | 
				
			||||||
        this.recvFormatListResponsePDU(s);
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case type.CB_FORMAT_DATA_REQUEST:
 | 
					 | 
				
			||||||
        this.recvFormatDataRequestPDU(s);
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case type.CB_FORMAT_DATA_RESPONSE:
 | 
					 | 
				
			||||||
        this.recvFormatDataResponsePDU(s);
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case type.CB_TEMP_DIRECTORY:
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case type.CB_CLIP_CAPS:
 | 
					 | 
				
			||||||
        this.recvClipboardCapsPDU(s);
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
      case type.CB_FILECONTENTS_REQUEST:
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.transport.once('cliprdr', (s) => {
 | 
					    /**
 | 
				
			||||||
      this.recv(s);
 | 
					     * connect function
 | 
				
			||||||
    });
 | 
					     * @param gccCore {type.Component(clientCoreData)}
 | 
				
			||||||
  }
 | 
					     */
 | 
				
			||||||
 | 
					    connect(gccCore, userId, channelId) {
 | 
				
			||||||
  /**
 | 
					        this.gccCore = gccCore;
 | 
				
			||||||
   * Receive capabilities from server
 | 
					        this.userId = userId;
 | 
				
			||||||
   * @param s {type.Stream}
 | 
					        this.channelId = channelId;
 | 
				
			||||||
   */
 | 
					        this.transport.once('cliprdr', (s) => {
 | 
				
			||||||
  recvClipboardCapsPDU(s) {
 | 
					            this.recv(s);
 | 
				
			||||||
    // Start at 18
 | 
					        });
 | 
				
			||||||
    s.offset = 18;
 | 
					    }
 | 
				
			||||||
    // const pdu = data.clipPDU().read(s);
 | 
					 | 
				
			||||||
    // console.log('recvClipboardCapsPDU', s);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    send(message) {
 | 
				
			||||||
   * Receive monitor ready from server
 | 
					        this.transport.send('cliprdr', new type.Component([
 | 
				
			||||||
   * @param s {type.Stream}
 | 
					            // Channel PDU Header
 | 
				
			||||||
   */
 | 
					            new type.UInt32Le(message.size()),
 | 
				
			||||||
  recvMonitorReadyPDU(s) {
 | 
					            // CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST | CHANNEL_FLAG_SHOW_PROTOCOL
 | 
				
			||||||
    s.offset = 18;
 | 
					            new type.UInt32Le(0x13),
 | 
				
			||||||
    // const pdu = data.clipPDU().read(s);
 | 
					            message
 | 
				
			||||||
    // console.log('recvMonitorReadyPDU', s);
 | 
					        ]));
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.sendClipboardCapsPDU();
 | 
					    recv(s) {
 | 
				
			||||||
    // this.sendClientTemporaryDirectoryPDU();
 | 
					        s.offset = 18;
 | 
				
			||||||
    this.sendFormatListPDU();
 | 
					        const pdu = data.clipPDU().read(s), type = data.ClipPDUMsgType;
 | 
				
			||||||
  }
 | 
					
 | 
				
			||||||
 | 
					        switch (pdu.obj.header.obj.msgType.value) {
 | 
				
			||||||
 | 
					            case type.CB_MONITOR_READY:
 | 
				
			||||||
 | 
					                this.recvMonitorReadyPDU(s);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case type.CB_FORMAT_LIST:
 | 
				
			||||||
 | 
					                this.recvFormatListPDU(s);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case type.CB_FORMAT_LIST_RESPONSE:
 | 
				
			||||||
 | 
					                this.recvFormatListResponsePDU(s);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case type.CB_FORMAT_DATA_REQUEST:
 | 
				
			||||||
 | 
					                this.recvFormatDataRequestPDU(s);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case type.CB_FORMAT_DATA_RESPONSE:
 | 
				
			||||||
 | 
					                this.recvFormatDataResponsePDU(s);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case type.CB_TEMP_DIRECTORY:
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case type.CB_CLIP_CAPS:
 | 
				
			||||||
 | 
					                this.recvClipboardCapsPDU(s);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case type.CB_FILECONTENTS_REQUEST:
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.transport.once('cliprdr', (s) => {
 | 
				
			||||||
 | 
					            this.recv(s);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Receive capabilities from server
 | 
				
			||||||
 | 
					     * @param s {type.Stream}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    recvClipboardCapsPDU(s) {
 | 
				
			||||||
 | 
					        // Start at 18
 | 
				
			||||||
 | 
					        s.offset = 18;
 | 
				
			||||||
 | 
					        // const pdu = data.clipPDU().read(s);
 | 
				
			||||||
 | 
					        // console.log('recvClipboardCapsPDU', s);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Send clipboard capabilities PDU
 | 
					     * Receive monitor ready from server
 | 
				
			||||||
   */
 | 
					     * @param s {type.Stream}
 | 
				
			||||||
  sendClipboardCapsPDU() {
 | 
					     */
 | 
				
			||||||
    this.send(new type.Component({
 | 
					    recvMonitorReadyPDU(s) {
 | 
				
			||||||
      msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_CLIP_CAPS),
 | 
					        s.offset = 18;
 | 
				
			||||||
      msgFlags: new type.UInt16Le(0x00),
 | 
					        // const pdu = data.clipPDU().read(s);
 | 
				
			||||||
      dataLen: new type.UInt32Le(0x10),
 | 
					        // console.log('recvMonitorReadyPDU', s);
 | 
				
			||||||
      cCapabilitiesSets: new type.UInt16Le(0x01),
 | 
					
 | 
				
			||||||
      pad1: new type.UInt16Le(0x00),
 | 
					        this.sendClipboardCapsPDU();
 | 
				
			||||||
      capabilitySetType: new type.UInt16Le(0x01),
 | 
					        // this.sendClientTemporaryDirectoryPDU();
 | 
				
			||||||
      lengthCapability: new type.UInt16Le(0x0c),
 | 
					        this.sendFormatListPDU();
 | 
				
			||||||
      version: new type.UInt32Le(0x02),
 | 
					    }
 | 
				
			||||||
      capabilityFlags: new type.UInt32Le(0x02)
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Send client temporary directory PDU
 | 
					     * Send clipboard capabilities PDU
 | 
				
			||||||
   */
 | 
					     */
 | 
				
			||||||
  sendClientTemporaryDirectoryPDU(path = '') {
 | 
					    sendClipboardCapsPDU() {
 | 
				
			||||||
    // TODO
 | 
					        this.send(new type.Component({
 | 
				
			||||||
    this.send(new type.Component({
 | 
					            msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_CLIP_CAPS),
 | 
				
			||||||
      msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_TEMP_DIRECTORY),
 | 
					            msgFlags: new type.UInt16Le(0x00),
 | 
				
			||||||
      msgFlags: new type.UInt16Le(0x00),
 | 
					            dataLen: new type.UInt32Le(0x10),
 | 
				
			||||||
      dataLen: new type.UInt32Le(0x0208),
 | 
					            cCapabilitiesSets: new type.UInt16Le(0x01),
 | 
				
			||||||
      wszTempDir: new type.BinaryString(Buffer.from('D:\\Vectors' + Array(251).join('\x00'), 'ucs2'), { readLength : new type.CallableValue(520)})
 | 
					            pad1: new type.UInt16Le(0x00),
 | 
				
			||||||
    }));
 | 
					            capabilitySetType: new type.UInt16Le(0x01),
 | 
				
			||||||
  }
 | 
					            lengthCapability: new type.UInt16Le(0x0c),
 | 
				
			||||||
 | 
					            version: new type.UInt32Le(0x02),
 | 
				
			||||||
 | 
					            capabilityFlags: new type.UInt32Le(0x02)
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Send format list PDU
 | 
					     * Send client temporary directory PDU
 | 
				
			||||||
   */
 | 
					     */
 | 
				
			||||||
  sendFormatListPDU() {
 | 
					    sendClientTemporaryDirectoryPDU(path = '') {
 | 
				
			||||||
    this.send(new type.Component({
 | 
					        // TODO
 | 
				
			||||||
      msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_LIST),
 | 
					        this.send(new type.Component({
 | 
				
			||||||
      msgFlags: new type.UInt16Le(0x00),
 | 
					            msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_TEMP_DIRECTORY),
 | 
				
			||||||
 | 
					            msgFlags: new type.UInt16Le(0x00),
 | 
				
			||||||
      dataLen: new type.UInt32Le(0x24),
 | 
					            dataLen: new type.UInt32Le(0x0208),
 | 
				
			||||||
 | 
					            wszTempDir: new type.BinaryString(Buffer.from('D:\\Vectors' + Array(251).join('\x00'), 'ucs2'), { readLength: new type.CallableValue(520) })
 | 
				
			||||||
      formatId6: new type.UInt32Le(0xc004),
 | 
					        }));
 | 
				
			||||||
      formatName6: new type.BinaryString(Buffer.from('Native\x00' , 'ucs2'), { readLength : new type.CallableValue(14)}),
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      formatId8: new type.UInt32Le(0x0d),
 | 
					 | 
				
			||||||
      formatName8: new type.UInt16Le(0x00),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      formatId9: new type.UInt32Le(0x10),
 | 
					 | 
				
			||||||
      formatName9: new type.UInt16Le(0x00),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      formatId0: new type.UInt32Le(0x01),
 | 
					 | 
				
			||||||
      formatName0: new type.UInt16Le(0x00),
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      // dataLen: new type.UInt32Le(0xe0),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // formatId1: new type.UInt32Le(0xc08a),
 | 
					 | 
				
			||||||
      // formatName1: new type.BinaryString(Buffer.from('Rich Text Format\x00' , 'ucs2'), { readLength : new type.CallableValue(34)}),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // formatId2: new type.UInt32Le(0xc145),
 | 
					 | 
				
			||||||
      // formatName2: new type.BinaryString(Buffer.from('Rich Text Format Without Objects\x00' , 'ucs2'), { readLength : new type.CallableValue(66)}),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // formatId3: new type.UInt32Le(0xc143),
 | 
					 | 
				
			||||||
      // formatName3: new type.BinaryString(Buffer.from('RTF As Text\x00' , 'ucs2'), { readLength : new type.CallableValue(24)}),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // formatId4: new type.UInt32Le(0x01),
 | 
					 | 
				
			||||||
      // formatName4: new type.BinaryString(0x00),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      formatId5: new type.UInt32Le(0x07),
 | 
					 | 
				
			||||||
      formatName5: new type.UInt16Le(0x00),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // formatId6: new type.UInt32Le(0xc004),
 | 
					 | 
				
			||||||
      // formatName6: new type.BinaryString(Buffer.from('Native\x00' , 'ucs2'), { readLength : new type.CallableValue(14)}),
 | 
					 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      // formatId7: new type.UInt32Le(0xc00e),
 | 
					 | 
				
			||||||
      // formatName7: new type.BinaryString(Buffer.from('Object Descriptor\x00' , 'ucs2'), { readLength : new type.CallableValue(36)}),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // formatId8: new type.UInt32Le(0x03),
 | 
					 | 
				
			||||||
      // formatName8: new type.UInt16Le(0x00),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // formatId9: new type.UInt32Le(0x10),
 | 
					 | 
				
			||||||
      // formatName9: new type.UInt16Le(0x00),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // formatId0: new type.UInt32Le(0x07),
 | 
					 | 
				
			||||||
      // formatName0: new type.UInt16Le(0x00),
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Recvie format list PDU from server
 | 
					 | 
				
			||||||
   * @param {type.Stream} s 
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  recvFormatListPDU(s) {
 | 
					 | 
				
			||||||
    s.offset = 18;
 | 
					 | 
				
			||||||
    // const pdu = data.clipPDU().read(s);
 | 
					 | 
				
			||||||
    // console.log('recvFormatListPDU', s);
 | 
					 | 
				
			||||||
    this.sendFormatListResponsePDU();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Send format list reesponse
 | 
					     * Send format list PDU
 | 
				
			||||||
   */
 | 
					     */
 | 
				
			||||||
   sendFormatListResponsePDU() {
 | 
					    sendFormatListPDU() {
 | 
				
			||||||
     this.send(new type.Component({
 | 
					        this.send(new type.Component({
 | 
				
			||||||
        msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_LIST_RESPONSE),
 | 
					            msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_LIST),
 | 
				
			||||||
        msgFlags: new type.UInt16Le(0x01),
 | 
					            msgFlags: new type.UInt16Le(0x00),
 | 
				
			||||||
        dataLen: new type.UInt32Le(0x00),
 | 
					 | 
				
			||||||
     }));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
     this.sendFormatDataRequestPDU();
 | 
					            dataLen: new type.UInt32Le(0x24),
 | 
				
			||||||
   }
 | 
					
 | 
				
			||||||
 | 
					            formatId6: new type.UInt32Le(0xc004),
 | 
				
			||||||
 | 
					            formatName6: new type.BinaryString(Buffer.from('Native\x00', 'ucs2'), { readLength: new type.CallableValue(14) }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            formatId8: new type.UInt32Le(0x0d),
 | 
				
			||||||
 | 
					            formatName8: new type.UInt16Le(0x00),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            formatId9: new type.UInt32Le(0x10),
 | 
				
			||||||
 | 
					            formatName9: new type.UInt16Le(0x00),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            formatId0: new type.UInt32Le(0x01),
 | 
				
			||||||
 | 
					            formatName0: new type.UInt16Le(0x00),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // dataLen: new type.UInt32Le(0xe0),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId1: new type.UInt32Le(0xc08a),
 | 
				
			||||||
 | 
					            // formatName1: new type.BinaryString(Buffer.from('Rich Text Format\x00' , 'ucs2'), { readLength : new type.CallableValue(34)}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId2: new type.UInt32Le(0xc145),
 | 
				
			||||||
 | 
					            // formatName2: new type.BinaryString(Buffer.from('Rich Text Format Without Objects\x00' , 'ucs2'), { readLength : new type.CallableValue(66)}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId3: new type.UInt32Le(0xc143),
 | 
				
			||||||
 | 
					            // formatName3: new type.BinaryString(Buffer.from('RTF As Text\x00' , 'ucs2'), { readLength : new type.CallableValue(24)}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId4: new type.UInt32Le(0x01),
 | 
				
			||||||
 | 
					            // formatName4: new type.BinaryString(0x00),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            formatId5: new type.UInt32Le(0x07),
 | 
				
			||||||
 | 
					            formatName5: new type.UInt16Le(0x00),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId6: new type.UInt32Le(0xc004),
 | 
				
			||||||
 | 
					            // formatName6: new type.BinaryString(Buffer.from('Native\x00' , 'ucs2'), { readLength : new type.CallableValue(14)}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId7: new type.UInt32Le(0xc00e),
 | 
				
			||||||
 | 
					            // formatName7: new type.BinaryString(Buffer.from('Object Descriptor\x00' , 'ucs2'), { readLength : new type.CallableValue(36)}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId8: new type.UInt32Le(0x03),
 | 
				
			||||||
 | 
					            // formatName8: new type.UInt16Le(0x00),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId9: new type.UInt32Le(0x10),
 | 
				
			||||||
 | 
					            // formatName9: new type.UInt16Le(0x00),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // formatId0: new type.UInt32Le(0x07),
 | 
				
			||||||
 | 
					            // formatName0: new type.UInt16Le(0x00),
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Recvie format list PDU from server
 | 
				
			||||||
 | 
					     * @param {type.Stream} s 
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    recvFormatListPDU(s) {
 | 
				
			||||||
 | 
					        s.offset = 18;
 | 
				
			||||||
 | 
					        // const pdu = data.clipPDU().read(s);
 | 
				
			||||||
 | 
					        // console.log('recvFormatListPDU', s);
 | 
				
			||||||
 | 
					        this.sendFormatListResponsePDU();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Receive format list response from server
 | 
					     * Send format list reesponse
 | 
				
			||||||
   * @param s {type.Stream}
 | 
					     */
 | 
				
			||||||
   */
 | 
					    sendFormatListResponsePDU() {
 | 
				
			||||||
  recvFormatListResponsePDU(s) {
 | 
					        this.send(new type.Component({
 | 
				
			||||||
    s.offset = 18;
 | 
					            msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_LIST_RESPONSE),
 | 
				
			||||||
    // const pdu = data.clipPDU().read(s);
 | 
					            msgFlags: new type.UInt16Le(0x01),
 | 
				
			||||||
    // console.log('recvFormatListResponsePDU', s);
 | 
					            dataLen: new type.UInt32Le(0x00),
 | 
				
			||||||
    // this.sendFormatDataRequestPDU();
 | 
					        }));
 | 
				
			||||||
  }
 | 
					
 | 
				
			||||||
 | 
					        this.sendFormatDataRequestPDU();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Send format data request PDU
 | 
					     * Receive format list response from server
 | 
				
			||||||
   */
 | 
					     * @param s {type.Stream}
 | 
				
			||||||
  sendFormatDataRequestPDU(formartId = 0x0d) {
 | 
					     */
 | 
				
			||||||
    this.send(new type.Component({
 | 
					    recvFormatListResponsePDU(s) {
 | 
				
			||||||
      msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_DATA_REQUEST),
 | 
					        s.offset = 18;
 | 
				
			||||||
      msgFlags: new type.UInt16Le(0x00),
 | 
					        // const pdu = data.clipPDU().read(s);
 | 
				
			||||||
      dataLen: new type.UInt32Le(0x04),
 | 
					        // console.log('recvFormatListResponsePDU', s);
 | 
				
			||||||
      requestedFormatId: new type.UInt32Le(formartId),
 | 
					        // this.sendFormatDataRequestPDU();
 | 
				
			||||||
    }));
 | 
					    }
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Receive format data request PDU from server
 | 
					     * Send format data request PDU
 | 
				
			||||||
   * @param s {type.Stream}
 | 
					     */
 | 
				
			||||||
   */
 | 
					    sendFormatDataRequestPDU(formartId = 0x0d) {
 | 
				
			||||||
  recvFormatDataRequestPDU(s) {
 | 
					        this.send(new type.Component({
 | 
				
			||||||
    s.offset = 18;
 | 
					            msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_DATA_REQUEST),
 | 
				
			||||||
    // const pdu = data.clipPDU().read(s);
 | 
					            msgFlags: new type.UInt16Le(0x00),
 | 
				
			||||||
    // console.log('recvFormatDataRequestPDU', s);
 | 
					            dataLen: new type.UInt32Le(0x04),
 | 
				
			||||||
    this.sendFormatDataResponsePDU();
 | 
					            requestedFormatId: new type.UInt32Le(formartId),
 | 
				
			||||||
  }
 | 
					        }));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Send format data reesponse PDU
 | 
					     * Receive format data request PDU from server
 | 
				
			||||||
   */
 | 
					     * @param s {type.Stream}
 | 
				
			||||||
  sendFormatDataResponsePDU() {
 | 
					     */
 | 
				
			||||||
    
 | 
					    recvFormatDataRequestPDU(s) {
 | 
				
			||||||
    const bufs = Buffer.from(this.content + '\x00' , 'ucs2');
 | 
					        s.offset = 18;
 | 
				
			||||||
 | 
					        // const pdu = data.clipPDU().read(s);
 | 
				
			||||||
    this.send(new type.Component({
 | 
					        // console.log('recvFormatDataRequestPDU', s);
 | 
				
			||||||
      msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_DATA_RESPONSE),
 | 
					        this.sendFormatDataResponsePDU();
 | 
				
			||||||
      msgFlags: new type.UInt16Le(0x01),
 | 
					    }
 | 
				
			||||||
      dataLen: new type.UInt32Le(bufs.length),
 | 
					 | 
				
			||||||
      requestedFormatData: new type.BinaryString(bufs, { readLength : new type.CallableValue(bufs.length)})
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					    /**
 | 
				
			||||||
   * Receive format data response PDU from server
 | 
					     * Send format data reesponse PDU
 | 
				
			||||||
   * @param s {type.Stream}
 | 
					     */
 | 
				
			||||||
   */
 | 
					    sendFormatDataResponsePDU() {
 | 
				
			||||||
  recvFormatDataResponsePDU(s) {
 | 
					
 | 
				
			||||||
    s.offset = 18;
 | 
					        const bufs = Buffer.from(this.content + '\x00', 'ucs2');
 | 
				
			||||||
    // const pdu = data.clipPDU().read(s);
 | 
					
 | 
				
			||||||
    const str = s.buffer.toString('ucs2', 26, s.buffer.length-2);
 | 
					        this.send(new type.Component({
 | 
				
			||||||
    // console.log('recvFormatDataResponsePDU', str);
 | 
					            msgType: new type.UInt16Le(data.ClipPDUMsgType.CB_FORMAT_DATA_RESPONSE),
 | 
				
			||||||
    this.content = str;
 | 
					            msgFlags: new type.UInt16Le(0x01),
 | 
				
			||||||
    this.emit('clipboard', str)
 | 
					            dataLen: new type.UInt32Le(bufs.length),
 | 
				
			||||||
  }
 | 
					            requestedFormatData: new type.BinaryString(bufs, { readLength: new type.CallableValue(bufs.length) })
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// =====================================================================================
 | 
					    /**
 | 
				
			||||||
  setClipboardData(content) {
 | 
					     * Receive format data response PDU from server
 | 
				
			||||||
    this.content = content;
 | 
					     * @param s {type.Stream}
 | 
				
			||||||
    this.sendFormatListPDU();
 | 
					     */
 | 
				
			||||||
  }
 | 
					    recvFormatDataResponsePDU(s) {
 | 
				
			||||||
 | 
					        s.offset = 18;
 | 
				
			||||||
 | 
					        // const pdu = data.clipPDU().read(s);
 | 
				
			||||||
 | 
					        const str = s.buffer.toString('ucs2', 26, s.buffer.length - 2);
 | 
				
			||||||
 | 
					        // console.log('recvFormatDataResponsePDU', str);
 | 
				
			||||||
 | 
					        this.content = str;
 | 
				
			||||||
 | 
					        this.emit('clipboard', str)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // =====================================================================================
 | 
				
			||||||
 | 
					    setClipboardData(content) {
 | 
				
			||||||
 | 
					        this.content = content;
 | 
				
			||||||
 | 
					        this.sendFormatListPDU();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
	
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
  Client
 | 
					    Client
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -184,12 +184,7 @@ function RdpClient(config) {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }).on('error', function (err) {
 | 
					    }).on('error', function (err) {
 | 
				
			||||||
        log.warn(err.code + '(' + err.message + ')\n' + err.stack);
 | 
					        log.warn(err.code + '(' + err.message + ')\n' + err.stack);
 | 
				
			||||||
        if (err instanceof error.FatalError) {
 | 
					        if (err instanceof error.FatalError) { throw err; } else { self.emit('error', err); }
 | 
				
			||||||
            throw err;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else {
 | 
					 | 
				
			||||||
            self.emit('error', err);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -218,8 +218,9 @@ Client.prototype.recvConnectionConfirm = function(s) {
 | 
				
			||||||
	var message = serverConnectionConfirm().read(s);
 | 
						var message = serverConnectionConfirm().read(s);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (message.obj.protocolNeg.obj.type.value == NegotiationType.TYPE_RDP_NEG_FAILURE) {
 | 
						if (message.obj.protocolNeg.obj.type.value == NegotiationType.TYPE_RDP_NEG_FAILURE) {
 | 
				
			||||||
		throw new error.ProtocolError('NODE_RDP_PROTOCOL_X224_NEG_FAILURE',
 | 
					        this.emit('error', { err: 'NODE_RDP_PROTOCOL_X224_NEG_FAILURE', code: message.obj.protocolNeg.obj.result.value });
 | 
				
			||||||
			'Failure code:' + message.obj.protocolNeg.obj.result.value + " (see https://msdn.microsoft.com/en-us/library/cc240507.aspx)");
 | 
					        return;
 | 
				
			||||||
 | 
							//throw new error.ProtocolError('NODE_RDP_PROTOCOL_X224_NEG_FAILURE', 'Failure code:' + message.obj.protocolNeg.obj.result.value + " (see https://msdn.microsoft.com/en-us/library/cc240507.aspx)");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (message.obj.protocolNeg.obj.type.value == NegotiationType.TYPE_RDP_NEG_RSP) {
 | 
						if (message.obj.protocolNeg.obj.type.value == NegotiationType.TYPE_RDP_NEG_RSP) {
 | 
				
			||||||
| 
						 | 
					@ -227,7 +228,9 @@ Client.prototype.recvConnectionConfirm = function(s) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ([Protocols.PROTOCOL_HYBRID_EX].indexOf(this.selectedProtocol) !== -1) {
 | 
					    if ([Protocols.PROTOCOL_HYBRID_EX].indexOf(this.selectedProtocol) !== -1) {
 | 
				
			||||||
        throw new error.ProtocolError('NODE_RDP_PROTOCOL_X224_NLA_NOT_SUPPORTED');
 | 
					        this.emit('error', 'NODE_RDP_PROTOCOL_X224_NLA_NOT_SUPPORTED');
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					        //throw new error.ProtocolError('NODE_RDP_PROTOCOL_X224_NLA_NOT_SUPPORTED');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (this.selectedProtocol == Protocols.PROTOCOL_RDP) {
 | 
						if (this.selectedProtocol == Protocols.PROTOCOL_RDP) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8414,7 +8414,7 @@
 | 
				
			||||||
        function autoConnectDesktop(e) { if (autoConnectDesktopTimer == null) { autoConnectDesktopTimer = setInterval(function() { connectDesktop(null, 1) }, 1000); } else { clearInterval(autoConnectDesktopTimer); autoConnectDesktopTimer = null; } }
 | 
					        function autoConnectDesktop(e) { if (autoConnectDesktopTimer == null) { autoConnectDesktopTimer = setInterval(function() { connectDesktop(null, 1) }, 1000); } else { clearInterval(autoConnectDesktopTimer); autoConnectDesktopTimer = null; } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Used to translate incoming agent console messages
 | 
					        // Used to translate incoming agent console messages
 | 
				
			||||||
        var agentConsoleMessages = [ '', "Waiting for user to grant access...", "Denied", "Failed to start remote terminal session, {0} ({1})", "Timeout", "Received invalid network data", "Unable to capture display" ];
 | 
					        var agentConsoleMessages = [ '', "Waiting for user to grant access...", "Denied", "Failed to start remote terminal session, {0} ({1})", "Timeout", "Received invalid network data", "Unable to capture display", "Protocol negotiation failed ({0})", "NLA not supported", "SSL required by server", "SSL not allowed by server", "SSL certificate not on server", "Inconsistent flags", "Hybrid required by server", "SSL with user auth required by server" ];
 | 
				
			||||||
        function formatAgentConsoleMessage(msg, msgid, msgargs) {
 | 
					        function formatAgentConsoleMessage(msg, msgid, msgargs) {
 | 
				
			||||||
            var r;
 | 
					            var r;
 | 
				
			||||||
            if (msgargs == null) { msgargs = []; }
 | 
					            if (msgargs == null) { msgargs = []; }
 | 
				
			||||||
| 
						 | 
					@ -8569,6 +8569,16 @@
 | 
				
			||||||
                    if (desktopsettings.rdpsmb) { desktop.m.SwapMouse = desktopsettings.rdpsmb; }
 | 
					                    if (desktopsettings.rdpsmb) { desktop.m.SwapMouse = desktopsettings.rdpsmb; }
 | 
				
			||||||
                    desktop.Start(desktopNode._id, currentNode.rdpport ? currentNode.rdpport : 3389, tsid);
 | 
					                    desktop.Start(desktopNode._id, currentNode.rdpport ? currentNode.rdpport : 3389, tsid);
 | 
				
			||||||
                    desktop.contype = 4;
 | 
					                    desktop.contype = 4;
 | 
				
			||||||
 | 
					                    desktop.onConsoleMessageChange = function () {
 | 
				
			||||||
 | 
					                        if (desktop.consoleMessage) {
 | 
				
			||||||
 | 
					                            Q('p11DeskConsoleMsg').innerHTML += formatAgentConsoleMessage(desktop.consoleMessage, desktop.consoleMessageId, desktop.consoleMessageArgs);
 | 
				
			||||||
 | 
					                            QV('p11DeskConsoleMsg', true);
 | 
				
			||||||
 | 
					                            if (p11DeskConsoleMsgTimer != null) { clearTimeout(p11DeskConsoleMsgTimer); }
 | 
				
			||||||
 | 
					                            if (desktop.consoleMessageTimeout) { p11DeskConsoleMsgTimer = setTimeout(p11clearConsoleMsg, desktop.consoleMessageTimeout * 1000); }
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            p11clearConsoleMsg();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                // Disconnect and clean up the remote desktop
 | 
					                // Disconnect and clean up the remote desktop
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue