5

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?

Bertrand Martel
  • 42,756
  • 16
  • 135
  • 159
Jass
  • 341
  • 3
  • 12

1 Answers1

11

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 git@github.com: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);
  });
}
Bertrand Martel
  • 42,756
  • 16
  • 135
  • 159
  • Hi @Bertrand Martel, thank you very much for answer. The method of implementation and your explanation looks very good and informative. I am trying to implement this in my code. – Jass Oct 08 '20 at 08:11
  • @Jass you can also see [this aws admin project](https://github.com/bertrandmartel/aws-admin) which is the project for which I've developed this library. It's an AWS dashboard using the api designed to be used locally and in the SSM section you can start a session in the browser using "ssm-session" library – Bertrand Martel Oct 08 '20 at 09:25
  • Yes,@Bertrand Martel I'll look at it. But in above code, import is not working for me. Is there any other alternative for import in browser ? – Jass Oct 08 '20 at 10:28
  • @Jass you can use `import {ssm} from "../../src/index.js"` check the source code of the example [here](https://github.com/bertrandmartel/aws-ssm-session/blob/master/test/web/index.html) specifying `type="module"` in script. You will need to get the source code or use a transpiler – Bertrand Martel Oct 08 '20 at 10:53
  • Okay @Bertrand Martel, I could import ssm from index.js. But in ssm. js it's giving me error that you can not import outside a module. And in ssm.js, I can not use script tag to define module – Jass Oct 08 '20 at 11:34
  • can you guide on how to import in js without specifying module? @Bertrand Marte – Jass Oct 08 '20 at 13:26
  • I didn't plan to add a `window.ssm` to this library since this is done automatically by transpiler on modern framework such as react or vue.js. This is something that should be managed at the library level and will need to refactor things in order not to break the import/require compatibilty. The easiest way for the moment if you don't use transpilers nor frameworks nor module script is to copy/paste the code directly [this](https://github.com/bertrandmartel/aws-ssm-session/blob/master/src/ssm.js) and [this](https://github.com/bertrandmartel/aws-ssm-session/blob/master/src/sha256.js) – Bertrand Martel Oct 08 '20 at 13:47
  • Okay @Bertrand Martel, thank you for your support. It did work at my end. – Jass Oct 12 '20 at 11:49
  • Hi @Bertrand Martel , AWS SSM is sending inappropriate number of $ as response. Do you know any reason? – Jass Apr 27 '21 at 08:23
  • hi @BertrandMartel, I was able to run the WebSocket through the browser but it's not working when I try to run in the backend `examples/node/app.js` I'm trying to access ECS container. I'm able to get the url and token, but websocket connection gets closed. Any ideia? Would work for ecs container this script? – gmilleo Jul 18 '23 at 01:02