Author Topic: A working websocket for the pro!  (Read 5723 times)

digi_guy

  • Jr. Member
  • **
  • Posts: 87
A working websocket for the pro!
« on: February 17, 2015, 11:37:42 am »
The arduino script is:

Code: [Select]
boolean CONNECTED = false;
char sending = 65;
String msg = "AAAAAAAAAAAAAAAAAAAAAA";
#define SHA1_K0 0x5a827999
#define SHA1_K20 0x6ed9eba1
#define SHA1_K40 0x8f1bbcdc
#define SHA1_K60 0xca62c1d6
#define w6 0x32353845
#define w7 0x41464135
#define w8 0x2d453931
#define w9 0x342d3437
#define w10 0x44412d39
#define w11 0x3543412d
#define w12 0x43354142
#define w13 0x30444338
#define w14 0x35423131
#define w15 0x80000000



void setup() {
  Serial.begin(9600); //open connection to wifi module

}


void loop() {

  if (CONNECTED) {
      char toSend[] = {0x81, 1, sending++};
      Serial.print(toSend);
      delay(1000);
  }
 
  if(serverRequest()){
    String path = getRequestPath();
   
    if(path == F("/WS")){
      Serial.find("Key: ");
      msg = Serial.readStringUntil('==');
      Serial.flush();
      Serial.print(F("HTTP/1.1 101 Switching"));
      Serial.print(F(" Protocols\r\nUpgrade: "));
      Serial.print(F("Websocket\r\nConnection: "));
      Serial.print(F("Upgrade\r\nSec-WebSocket-Accept: "));
      generateResponse();     
      Serial.print(F("=\r\n\r\n"));
      CONNECTED = true;
    }
    else{
    Serial.flush();
    }
  }
   
}

bool serverRequest(){
  if(Serial.available()>4){
    return Serial.find("GET ");
  }
  return false;
}

String getRequestPath(){
    String path = Serial.readStringUntil(' ');
    return path;
}
void sendResponse(String response){
    sendResponseStart();
    sendResponseChunk(response);
    sendResponseEnd();
}

void sendResponseStart(){
    //sends a chunked response
    Serial.println(F("HTTP/1.1 200 OK"));
    Serial.println(F("Content-Type: text/html"));
    Serial.println(F("Connection: close"));
    Serial.println(F("Transfer-Encoding: chunked"));
    Serial.println();
}

void sendResponseChunk(String response){
    Serial.println(response.length()+2,HEX);
    Serial.println(response);
    Serial.println();
}
void sendResponseEnd(){
    Serial.println(F("0"));
    Serial.println();
}

void generateResponse() {
  // this main function has the initial hash variables, fills the w array, then makes two passes through the sha-1 function
    uint32_t H[] = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0}; // Initial H values
    uint32_t H1[5] = {0,0,0,0,0}; 
    uint32_t w[16];

    getMessage(w);
    getHash(w, H, H1);
     
    for (int w_cntr = 0; w_cntr<16; w_cntr++) { w[w_cntr] = 0; }
     w[15]=480 & 0xffff;
     getHash(w, H1, H1);

    convertBase64(H1);
}



void getMessage(uint32_t* w_out){ // takes the websocket key (msg) and fills up the w array
  uint8_t temp;
  int k=0;
  for (int j=0; j<6; j++){
      w_out[j]=0x00;
      for (int i=0; i<4; i++) {
        temp = msg[k++] ;
        if (k>22) temp=0x3d; //this adds thetwo '=' to the key
        w_out[j] = (w_out[j] <<8) | (temp);
      }
  } 
  w_out[6] = w6;  // the last 10 w's are constant
  w_out[7] = w7;
  w_out[8] = w8;
  w_out[9] = w9;
  w_out[10] = w10;
  w_out[11] = w11;
  w_out[12] = w12;
  w_out[13] = w13;
  w_out[14] = w14;
  w_out[15] = w15;
}


uint32_t ROTL(uint32_t number, uint8_t bits) {
  return ((number << bits) | (number >> (32-bits)));
}

char b64(int in) {
  //I used this instead of the huge 64 byte string
  // char b64[64]="ABCDEFGHIJKLMNOP...
  if (in > 62) {return '/';}
  else if (in > 61) {return '+';}
  else if (in > 51) {return (char)(in + -4 );}
  else if (in > 25) {return (char)(in + 71 );}
  else {return (char)(in + 65);} 
}
void convertBase64(uint32_t input_msg[]) {
  //This takes the final hash digest and prints out the base64 conversion
  int j = 0;
  for (int i=26; i>0; i-=6) {
  Serial.print(b64(input_msg[j]>>i&63));
  }
  Serial.print(b64( (input_msg[j]<<4 | input_msg[j+1]>>28)&63));
j=1;
  for (int i=22; i>0; i-=6) {
  Serial.print(b64(input_msg[j]>>i&63));
  }
  Serial.print(b64( (input_msg[j]<<2 | input_msg[j+1]>>30)&63));
j=2;
  for (int i=24; i>=0; i-=6) {
  Serial.print(b64(input_msg[j]>>i&63));
  }
  j=3;

  for (int i=26; i>0; i-=6) {
  Serial.print(b64(input_msg[j]>>i&63));
  }
  Serial.print(b64( (input_msg[j]<<4 | input_msg[j+1]>>28)&63));
j=4;
  for (int i=22; i>=0; i-=6) {
  Serial.print(b64(input_msg[j]>>i&63));
  }
Serial.print(b64( (input_msg[j]<<2)&63));
// technically it still needs to add '=' to the end, but that is done after the getresponse() call
}


void getHash(uint32_t* W, uint32_t* hash, uint32_t* hash1) {
  // this is the main sha-1 conversion takes in the 16 word array 'w' along with the initial hash and returns the new hash
  // it is called twice, first with the initial hash constants and the full w, then with the generated hash values
  // and the mostly empty w
  uint32_t a = hash[0];
  uint32_t b = hash[1];
  uint32_t c = hash[2];
  uint32_t d = hash[3];
  uint32_t e = hash[4];
  uint32_t temp = 0;
  uint32_t k = 0;
  uint32_t t;   


  for (uint32_t i=0; i<80; i++) {  //I don't htink I needs to be 32, probably just int or 8
    if (i>=16) {
      t = W[(i+13)&15] ^ W[(i+8)&15] ^ W[(i+2)&15] ^ W[i&15];
      W[i&15] = ROTL(t,1);
    }
    if (i<20) {
      t = (d ^ (b & (c ^ d))) + SHA1_K0;
    } else if (i<40) {
      t = (b ^ c ^ d) + SHA1_K20;
    } else if (i<60) {
      t = ((b & c) | (d & (b | c))) + SHA1_K40;
    } else {
      t = (b ^ c ^ d) + SHA1_K60;
    }
    t+=ROTL(a,5) + e + W[i&15];
    e=d;
    d=c;
    c=ROTL(b,30);
   
    b=a;
    a=t;
  }
  hash1[0] = hash[0] + a;
  hash1[1] = hash[1] + b;
  hash1[2] = hash[2] + c;
  hash1[3] = hash[3] + d;
  hash1[4] = hash[4] + e;
}

void webPage() {
//This would generate the webpage, but I can't get it to send 
  sendResponseStart();
  sendResponseChunk(F("<!DOCTYPE HTML><html><head>"));
  sendResponseChunk(F("<script type=\"text/javascript\">"));
  sendResponseChunk(F("function WebSocketTest(){"));
  sendResponseChunk(F("var ws = new WebSocket(\"ws://10.0.0.9:8080/WS\");")); // change the address to match your pro
  sendResponseChunk(F("ws.onopen = function(){"));
  sendResponseChunk(F("alert(\"open\");};"));
  sendResponseChunk(F("ws.onmessage = function (evt) { "));
  sendResponseChunk(F("alert(evt.data);};"));
  sendResponseChunk(F("ws.onclose = function(){ "));
  sendResponseChunk(F("alert(\"closed\");};}"));
  sendResponseChunk(F("</script></head><body>"));
  sendResponseChunk(F("<a href=\"javascript:WebSocketTest()\">Run</a>"));
  sendResponseChunk(F("</body></html>"));
  sendResponseEnd();
}

And the webpage required is:
Code: [Select]
<!DOCTYPE html>

<meta charset="utf-8" />

<title>WebSocket Test</title>

<script language="javascript" type="text/javascript">


  function init()
  {
document.myform.url.value = "ws://localhost:8080/WS"
document.myform.inputtext.value = "Hello World!"
document.myform.disconnectButton.disabled = true;
  }

  function doConnect()
  {
    websocket = new WebSocket(document.myform.url.value);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }

  function onOpen(evt)
  {
    writeToScreen("connected\n");
document.myform.connectButton.disabled = true;
document.myform.disconnectButton.disabled = false;
  }

  function onClose(evt)
  {
    writeToScreen("disconnected\n");
document.myform.connectButton.disabled = false;
document.myform.disconnectButton.disabled = true;
  }

  function onMessage(evt)
  {
    writeToScreen("response: " + evt.data + '\n');
  }

  function onError(evt)
  {
    writeToScreen('error: ' + evt.data + '\n');

websocket.close();

document.myform.connectButton.disabled = false;
document.myform.disconnectButton.disabled = true;

  }

  function doSend(message)
  {
    writeToScreen("sent: " + message + '\n');
    websocket.send(message);
  }

  function writeToScreen(message)
  {
    document.myform.outputtext.value += message
document.myform.outputtext.scrollTop = document.myform.outputtext.scrollHeight;

  }

  window.addEventListener("load", init, false);


   function sendText() {
doSend( document.myform.inputtext.value );
   }

  function clearText() {
document.myform.outputtext.value = "";
   }

   function doDisconnect() {
websocket.close();
   }


</script>

<div id="output"></div>

<form name="myform">
<p>
<textarea name="outputtext" rows="20" cols="50"></textarea>
</p>
<p>
<textarea name="inputtext" cols="50"></textarea>
</p>
<p>
<textarea name="url" cols="50"></textarea>
</p>
<p>
<input type="button" name=sendButton value="Send" onClick="sendText();">
<input type="button" name=clearButton value="Clear" onClick="clearText();">
<input type="button" name=disconnectButton value="Disconnect" onClick="doDisconnect();">
<input type="button" name=connectButton value="Connect" onClick="doConnect();">
</p>


</form>
</html>

Once you get this working, the pro will send "A" to the browser, and increment that every second until it crashes.
 
Description to follow.
« Last Edit: February 17, 2015, 12:19:43 pm by digi_guy »

digi_guy

  • Jr. Member
  • **
  • Posts: 87
Re: A working websocket for the pro!
« Reply #1 on: February 17, 2015, 11:55:32 am »
I should probably put "working" in italics since I had to cheat a lot. To make this "work" load the first chunk of code into your pro, making sure to match the baud rate (9600) with your wifi module. Then make a txt file on your host computer called socket.html and load the second chunk of code into it. You will need to know the IP and port for your pro. You will also need to make sure that the address ends with /WS. I wasn't able to get the pro to host the webpage, even an extremely trimmed down version, so to compensate you'll have to have the webpage file on your computer.

A bit about websockets:
1. The process starts with a javascript function that calls "var ws = new WebSocket("ws://10.0.0.9:8080/WS");" which will generate the following message:
Code: [Select]
GET /wS  HTTP/1.1\r\nHost: localhost:8080\r\nConnection: Upgrade\r\nPrag
ma: no-cache\r\nCache-Control: no-cache\r\nUpgrade: Websocket\r\nOrigin: null\r\
nSec-WebSocket-Version: 13\r\nUser-Agent: Mozilla/5.0 (WindoWs NT 6.3; WOW64) Ap
pleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36\r\nAccept
-Encoding: gzip, deflate, sdch\r\nAccept-Language: en-US,en;q=0.8\r\nSec-WebSock
et-Key: AYhW8JzaPArQF9SR4gMv8A==\r\nSec-WebSocket-Extensions: permessage-deflate
; client_max_WindoW_bits\r\n\r\n

Buried within that is a key "AYhW8JzaPArQF9SR4gMv8A==" which as far as I can tell always ends with "==" and is always 22 characters long.

2. The server then replies with the following message:
Code: [Select]
HTTP/1.1 101 SWitching Protocols\r\n
Upgrade: Websocket\r\n
Connection: Upgrade\r\n
Sec-WebSocket-Accept: ICX+Ymv66kxgM0FcWaLWlGLwTAI=
\r\n\r\n
Which actually looks more like this:
"HTTP/1.1 101 SWitching Protocols\r\nUpgrade: Websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+ICX+Ymv66kxgM0FcWaLWlGLwTAI=+"\r\n\r\n"

That mess in the middle is the base64 conversion of the sha-1 digest of the key we saw earlier. This message has to be EXACTLY the right sequence, and right number of characters, and obviously the sha-1 key you return has to match what the browser expects.

3. For the server to send a message it has to look like:
{0x81, length, message}
if any other line or character is sent, just slightly out of place, the whole thing crashes.

4. For the server to receive a message is an amazing mess. The string looks like "\x81\x8fSRUN\x1e7&=250n'=u=6<1"
which starts with 0x81, then has some info about the length of the message, then 4 bytes are masks that are used to decode the message. I haven't had a chance to implement it on the pro, in python the decoding process looks like this:
Code: [Select]
def DecodedCharArrayFromByteStreamIn(stringStreamIn):
    #turn string values into opererable numeric byte values
    byteArray = [ord(character) for character in stringStreamIn]
    datalength = byteArray[1] & 127
    indexFirstMask = 2
    if datalength == 126:
        indexFirstMask = 4
    elif datalength == 127:
        indexFirstMask = 10
    masks = [m for m in byteArray[indexFirstMask : indexFirstMask+4]]
    indexFirstDataByte = indexFirstMask + 4
    decodedChars = []
    i = indexFirstDataByte
    j = 0
    while i < len(byteArray):
        decodedChars.append( chr(byteArray[i] ^ masks[j % 4]) )
        i += 1
        j += 1   
    return ''.join(decodedChars)

Next I'll give a rundown of my code.

digi_guy

  • Jr. Member
  • **
  • Posts: 87
Re: A working websocket for the pro!
« Reply #2 on: February 17, 2015, 12:18:38 pm »
So to make this work on the pro, I rewrote the sha-1 function and the base64 conversion in order to strip away everything I could so that it would fit within the available memory.

The browser sends a key, and the server has to take that key, add 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, find the sha-1 digest, then convert to base64.

The sha-1 process involves creating an array w, which gets filled with the key, 4 characters at time, then filled with zeros to make 16 x 32 bit words (64 characters). Since the message is only 60 characters long, 0x80 to add the single bit after the message, and the rest are filled with zeros to make 512 bits. The problem with sha-1 is that the last 64 bits are reserved for the message length, so we have to make two arrays, the second all zeros with the message length at the end.

The arrays are then scrambled to generate a hash digest consisting of 5 x 32 bit words.

From there those words are converted to base64 which involves taking 6 bits at a time, and encoding them based on
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
if the message isn't multiples of 6bits, either two or four zeros are added and then either "=" or "==" is used to indicate how many zeros were added.

Some fun things to note: base64 uses multiple of 6 bits, but the sha-1 generates 5 x 32 bit words, which is a huge mess to decode since you'll get 5 letters from the first hash, with 2 bits left over. So you combine that with the first 4 bits of the next hash, then read the remaining 4 letters, with 4 bits left over and so on and so on.

If you have a computer with enough ram a lot of this is pretty easy, but we have none.  I'd love it if someone cleaned this up a bit and made it run a little smoother. I am obviously not a c programmer.


digi_guy

  • Jr. Member
  • **
  • Posts: 87
Re: A working websocket for the pro!
« Reply #3 on: February 20, 2015, 09:49:50 am »
I managed to get messages from the browser decoded so your pro will act as an echo, repeating what ever is sent from the browser:

Code: [Select]
void decodeMessage() {
      delay(500);
      if (Serial.available()>6){
 
        if (Serial.peek() == 129) {
          Serial.read();
          int L = Serial.read() & 127;
          uint8_t msg_out[L+2];
          msg_out[0]=0x81;
          msg_out[1]= L;
         
          uint8_t mask[4];
          for (int i=0; i<4; i++) {
            mask[i] = Serial.read();
            delay(5);
          }
          for (int i=0; i<L; i++) {
            uint8_t character = mask[i] ^ Serial.read();
            msg_out[2+i] = character;
            delay(5);
          }

          for (int i=0; i<L+2; i++) {
            Serial.print((char)msg_out[i]);
          }
         
        } else {
          CONNECTED = false;
          Serial.println(F("connection closed"));
        }
        while(Serial.read() != -1) {delay(5);} //clear read buffer
      }
}

To make it work just replace the first part of the main loop:
Code: [Select]
  if (CONNECTED) {
    decodeMessage();

Messages sent from the server are relatively simple as I posted earlier: start with 0x81, then the length of your message (8 bits), followed by your message. There is a limit to the size that's a bit beyond my abilities here.

The message from the browser is a mess because the data is masked. It's going to start with 0x81 followed by a 1 bit to indicate that the data is masked (this has to be zero from the server), followed by the length of the data. So the first byte through is 0x81, the second byte needs (&0x7f) to get the 7 bits of the length. If the length 126 or 127 that indicates that the message is way longer and has a bunch of other conditions, we don't go into that here.

The next four bytes are the mask, and the remaining bytes are the messages. To decode the messages you XOR with the mask, rotating through the 4 mask options. message = msg ^ mask[i%4]

If anything goes wrong the browser will send a disconnect message that starts with 0x88. For simplicity I've set up the loop to disconnect on any message other than 0x81.

I'm going to make one more change so pro can send a pin value.