how to send commands to AWS Session manager websocket url using xterm.js?

3.5k views Asked by At

I have a websocket url created by AWS. URL is created by aws ssm start session using .net sdk. Start session method gives me streamUrl, token and session ID. URL is in following format:

wss://ssmmessages.ap-south-1.amazonaws.com/v1/data-channel/sessionidhere?role=publish_subscribe

There is actual session id at placeof "sessionidhere" that I can not share.

I want to open terminal on web using xterm.js. I've read that xterm.js can connect to websocket URL, send messages and receive outputs.

My javascript code is here :

<!doctype html>
<html>
<head>
    <link href="~/xterm.css" rel="stylesheet" />
    <script src="~/Scripts/jquery-3.4.1.js"></script>
    <script src="~/Scripts/bootstrap.js"></script>
    <script src="~/xterm.js"></script>
</head>
<body>
    <div id="terminal"></div>
    <script type="text/javascript">
        var term = new Terminal({
            cursorBlink: "block"
        });
        var curr_line = "";
        var entries = [];
        term.open(document.getElementById('terminal'));    
        const ws = new WebSocket("wss://ssmmessages.ap-south-1.amazonaws.com/v1/data-channel/sessionid?role=publish_subscribe?token=tokenvalue");
        var curr_line = "";
        var entries = [];
      
        term.write("web shell $ ");

        term.prompt = () => {
            if (curr_line) {
                let data = {
                    method: "command", command: curr_line
                }
                ws.send(JSON.stringify(data));
            }
        };
        term.prompt();
        ws.onopen = function (e) {
            alert("[open] Connection established");
            alert("Sending to server");         
            var enc = new TextEncoder("utf-8"); // always utf-8
            // console.log(enc.encode("This is a string converted to a Uint8Array"));
            var data = "ls";
            console.log(enc.encode(data));
            alert(enc.encode(data));
            ws.send(enc.encode(data));
            alert(JSON.stringify(e));
        };
        ws.onclose = function (event) {
            if (event.wasClean) {
                alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
            } else {
                // e.g. server process killed or network down
                // event.code is usually 1006 in this case
                alert('[close] Connection died');
            }
        };

        ws.onerror = function (error) {
            alert(`[error] ${error.message}`);
        };

        // Receive data from socket
        ws.onmessage = msg => {
            alert(data);
            term.write("\r\n" + JSON.parse(msg.data).data);
            curr_line = "";
        };

        term.on("key", function (key, ev) {
            //Enter
            if (ev.keyCode === 13) {
                if (curr_line) {
                    entries.push(curr_line);
                    term.write("\r\n");
                    term.prompt();
                }
            } else if (ev.keyCode === 8) {
                // Backspace
                if (curr_line) {
                    curr_line = curr_line.slice(0, curr_line.length - 1);
                    term.write("\b \b");
                }
            } else {
                curr_line += key;
                term.write(key);
            }
        });

        // paste value
        term.on("paste", function (data) {
            curr_line += data;
            term.write(data);
        });
    </script>
</body>
</html>

Now, the session is being opened, I am getting alert of connection established. It's being successful connection, but whenever I try to send commands, the connection is being closed by saying 'request to open data channel does not contain a token'. I've tried to send command in 3 ways.

First is :

ws.send("ls")

second:

let data = {
    method: "command", command: curr_line
}
ws.send(JSON.stringify(data));

But facing same error i.e. request to open data channel does not contain token, connection died

third:

var enc = new TextEncoder("utf-8"); 
var data = "ls";           
ws.send(enc.encode(data));

For third, I'm not getting any error, but not getting output too... Can someone please help?

1

There are 1 answers

9
Bertrand Martel On BEST ANSWER

The protocol used by AWS Session manager consists of the following :

  • open a websocket connection on the stream URL
  • send an authentication request composed of the following JSON stringified :
{
  "MessageSchemaVersion": "1.0",
  "RequestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "TokenValue": "<YOUR-TOKEN-VALUE>"
}

From this moment the protocol is not JSON anymore. It is implemented in the offical Amazon SSM agent which is required if you want start a SSM session from the AWS CLI. The payload must be sent & receive according this binary format

I had exactly the same requirement as you few months ago so I've made an AWS Session manager client library : https://github.com/bertrandmartel/aws-ssm-session for nodejs and browser. If you want more information about how the protocol works, checkout this

The sample code available for browser use xterm.js

First clone the project and generate websocket URL and token using aws-api with some utility script :

git clone [email protected]:bertrandmartel/aws-ssm-session.git
cd aws-ssm-session
npm i
npm run build
node scripts/generate-session.js

which gives you :

{
  SessionId: 'xxxxxx-xxxxxxxxxxxxxx',
  TokenValue: 'YOUR_TOKEN',
  StreamUrl: 'wss://ssmmessages.eu-west-3.amazonaws.com/v1/data-channel/user-xxxxxxxxxxxxxx?role=publish_subscribe'
}

Then serve the sample app :

npm install http-server -g
http-server -a localhost -p 3000

go to http://localhost:3000/test/web, enter the websocket URI and token :

enter image description here

The sample code for browser :

import { ssm } from "ssm-session";

var socket;
var terminal;

const termOptions = {
  rows: 34,
  cols: 197
};

function startSession(){
  var tokenValue = document.getElementById("tokenValue").value;
  var websocketStreamURL = document.getElementById("websocketStreamURL").value;
  
  socket = new WebSocket(websocketStreamURL);
  socket.binaryType = "arraybuffer";
  initTerminal()

  socket.addEventListener('open', function (event) {
    ssm.init(socket, {
      token: tokenValue,
      termOptions: termOptions
    });
  });
  socket.addEventListener('close', function (event) {
    console.log("Websocket closed")
  });
  socket.addEventListener('message', function (event) {
    var agentMessage = ssm.decode(event.data);
    //console.log(agentMessage);
    ssm.sendACK(socket, agentMessage);
    if (agentMessage.payloadType === 1){
      terminal.write(agentMessage.payload)
    } else if (agentMessage.payloadType === 17){
      ssm.sendInitMessage(socket, termOptions);
    }
  });
}

function stopSession(){
  if (socket){
    socket.close();
  }
  terminal.dispose()
}

function initTerminal() {
  terminal = new window.Terminal(termOptions);
  terminal.open(document.getElementById('terminal'));
  terminal.onKey(e => {
    ssm.sendText(socket, e.key);
  });
  terminal.on('paste', function(data) {
    ssm.sendText(socket, data);
  });
}