WebSocket protocol with AHK

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
Bruttosozialprodukt
Posts: 463
Joined: 24 Jan 2014, 22:28

WebSocket protocol with AHK

14 Jun 2015, 04:13

I'd like to communicate with a server via the WebSocket protocol, but I don't really know how.
The protocol always starts with a more or less normal http request/response. Here is an example that I captured from http://www.websocket.org/echo.html

Request:

Code: Select all

GET http://echo.websocket.org/?encoding=text HTTP/1.1
Host: echo.websocket.org
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://www.websocket.org
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: DTcRJBfSp8DBi5ZDmMWumA==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

Response:

Code: Select all

HTTP/1.1 101 Web Socket Protocol Handshake
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Headers: x-websocket-extensions
Access-Control-Allow-Headers: x-websocket-version
Access-Control-Allow-Headers: x-websocket-protocol
Access-Control-Allow-Origin: http://www.websocket.org
Connection: Upgrade
Date: Sun, 14 Jun 2015 08:31:04 GMT
Sec-WebSocket-Accept: BRoSwRJvtoTnuLLw1JCY/jE7SY0=
Server: Kaazing Gateway
Upgrade: websocket
EndTime: 10:31:04.152
ReceivedBytes: 32
SentBytes: 40

But everything after that is being sent on the same connection and doesn't meet the http standards in general.
So WinHttpRequest5.1, XmlHttpRequest etc are probably not able to do the job and it probably needs to be done with a tcp capable socket library. Maybe Bentschis UPD/TCP Class or AHKsock.
guest3456
Posts: 3454
Joined: 09 Oct 2013, 10:31

Re: WebSocket protocol with AHK

14 Jun 2015, 11:56

if browsers already support it, then would just using an IE activex instance inside ahk work?

Bruttosozialprodukt
Posts: 463
Joined: 24 Jan 2014, 22:28

Re: WebSocket protocol with AHK

14 Jun 2015, 12:03

Yes sure, but the first IE that implemented it was IE 10 afaik. So there is no way you would get this to work on Windows XP which is stuck with IE 8.
lexikos
Posts: 9559
Joined: 30 Sep 2013, 04:07
Contact:

Re: WebSocket protocol with AHK

14 Jun 2015, 16:41

If you want to support XP, you'll have to learn about the protocol and use it directly via a TCP/IP client. About WebSocket and WebSockets (MSDN) explain a bit, but are incomplete. You'll probably have to resort to reading the official spec.

Otherwise, The WebSocket API describes the API in IE10 and other browsers.

Normally you would create a WebSocket object in JavaScript using new WebSocket(host), but calling it like a function via COM appears to work just as well. Unfortunately, there appears to be a bug: if I set socket.onmessage := Func(...), the function is not given any parameters. The only workaround I found was to use a JavaScript function like function(p) { fn(p); }, where fn is the AutoHotkey function.

Here's how you could use IE10:

Code: Select all

Gui WebSocket:Add, ActiveX, vWB
    , about:<meta http-equiv='X-UA-Compatible' content='IE=edge'>
while WB.ReadyState != 4
    Sleep 100
if WB.Document.documentMode < 10 {
    MsgBox IE10+ required.
    ExitApp
}
js=
(
({
    open: function(host, handlers) {
        var socket = new WebSocket(host);
        (["open", "close", "error", "message"]).forEach(function(e) {
            var h = handlers[e];
            if (h)
                socket["on"+e] = function(p) { h(handlers, socket, p); };
        })
    }
})
)
global WebSocket := WB.Document.parentWindow.eval(js)

host := "ws://echo.websocket.org/?encoding=text"
WebSocket.open(host, wsTester)

class wsTester {
    open(socket) {
        socket.send("hello")
        MsgBox Socket open; sent hello.
    }
    message(socket, messageEvent) {
        MsgBox % "Message: " messageEvent.data
        socket.close(1000)
    }
    error(socket) {
        MsgBox Socket error.
        ExitApp
    }
    close(socket, closeEvent) {
        MsgBox % "Socket closed.`n"
            . "`ncode: " closeEvent.code
            . "`nwasClean: " (closeEvent.wasClean ? "true" : "false")
        ExitApp
    }
}

#Persistent
For Windows 8 and later, there's also the WebSocket Protocol Component API.
Bruttosozialprodukt
Posts: 463
Joined: 24 Jan 2014, 22:28

Re: WebSocket protocol with AHK

15 Jun 2015, 13:54

Interesting, thanks for sharing, Lexikos.

The handshake wasn't a big problem. But I can't figure out how to send frames correctly.
Here is my code so far:

Code: Select all

#Include AHKsock.ahk ;https://raw.githubusercontent.com/jleb/AHKsock/master/AHKsock.ahk

mySocket := 0 ;global tcp socket handle
;our websocket handshake request:
rawHttpRequestUnicode = 
( Join`r`n
GET /?encoding=text HTTP/1.1
Host: echo.websocket.org
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Sec-WebSocket-Version: 13
Origin: http://www.websocket.org
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: DTcRJBfSp8DBi5ZDmMWumA==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket


)
rawHttpRequestSize := StrLen(rawHttpRequestUnicode) ;save the string length of rawHttpRequestUnicode
VarSetCapacity(rawHttpRequest, rawHttpRequestSize) ;create variable that will hold our string as ASCII
StrPut(rawHttpRequestUnicode, &rawHttpRequest, rawHttpRequestSize, "CP0") ;convert our unicode string to ASCII and store it in the variable rawHttpRequest

AHKsock_ErrorHandler("ErrorCallback") ;for error handling
If (i := AHKsock_Connect("echo.websocket.org", 80, "TcpCallback")) ;connect to echo.websocket.org on port 80 (http) and set the function TcpCallback as our callback function
    MsgBox, % "AHKsock_Connect failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting

Loop ;wait for our tcp connection to be established
    If (mySocket)
        Break
If (i := AHKsock_ForceSend(mySocket, &rawHttpRequest, rawHttpRequestSize)) ;send our formated websocket handshake request
    MsgBox, % "AHKsock_ForceSend failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting

MsgBox, Let's send a frame now!

text := "Test message! Hello!" ;UTF-8 test data that we try to send as a frame
textLen := StrLen(text)
frameSize := textLen*2+2 ;save the string length plus 2 bytes (a frame starts with 0x00 and ends with 0xFF)
VarSetCapacity(textFrame,frameSize)
NumPut(0x00, &textFrame, 1) ;marks frame start
StrPut(text, &textFrame+1, textLen) ;store test string it in the textFrame variable at position 2 (1 is reserved)
NumPut(0xFF, &textFrame, frameSize-1) ;marks frame end 
If (i := AHKsock_ForceSend(mySocket, &textFrame, frameSize)) ;send our frame
    MsgBox, % "AHKsock_ForceSend failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting

TcpCallback(sEvent, iSocket = 0, sName = 0, sAddr = 0, sPort = 0, ByRef bData = 0, bDataLength = 0) { ;callback function that gets called when something happens on our tcp connection
    Global mySocket
    ;MsgBox, % "sEvent: " sEvent "`niSocket: " iSocket "`nsName: " sName "`nsAddr: " sAddr "`nsPort: " sPort "`nbData: " bData "`nbDataLength: " bDataLength
    If (sEvent = "CONNECTED") { ;a tcp connection was successfully established
        mySocket := iSocket ;save the connection handle in the global variable mySocket 
    }
}

ErrorCallback(iError, iSocket) {  ;for error handling
    MsgBox, % "iError: " iError "`niSocket: " iSocket  ;for error handling
} ;for error handling
The frame text is definitely being sent, but for some reason Wireshark doesn't recognize it as WebSocket traffic and the next thing that happens is a "WebSocket Connection Close":
Image
lexikos
Posts: 9559
Joined: 30 Sep 2013, 04:07
Contact:

Re: WebSocket protocol with AHK

15 Jun 2015, 17:16

I'm assuming you took this at face value:
In the case of text frames, each frame starts with a 0x00 byte, ends with a 0xFF byte, and contains UTF-8 data in between.
I can't find anything like that in the actual spec.

If the server receives an invalid or unmasked frame, it closes the connection.

Here's my attempt (according to the frame format defined in the spec and at MDN), which also isn't working:

Code: Select all

#Include AHKsock.ahk ;https://raw.githubusercontent.com/jleb/AHKsock/master/AHKsock.ahk
 
mySocket := 0 ;global tcp socket handle
;our websocket handshake request:
rawHttpRequestUnicode = 
( Join`r`n
GET /?encoding=text HTTP/1.1
Host: echo.websocket.org
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Sec-WebSocket-Version: 13
Origin: http://www.websocket.org
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: DTcRJBfSp8DBi5ZDmMWumA==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
 
 
)
rawHttpRequestSize := StrLen(rawHttpRequestUnicode) ;save the string length of rawHttpRequestUnicode
VarSetCapacity(rawHttpRequest, rawHttpRequestSize) ;create variable that will hold our string as ASCII
StrPut(rawHttpRequestUnicode, &rawHttpRequest, rawHttpRequestSize, "CP0") ;convert our unicode string to ASCII and store it in the variable rawHttpRequest
 
AHKsock_ErrorHandler("ErrorCallback") ;for error handling
If (i := AHKsock_Connect("echo.websocket.org", 80, "TcpCallback")) ;connect to echo.websocket.org on port 80 (http) and set the function TcpCallback as our callback function
    MsgBox, % "AHKsock_Connect failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting
 
Loop ;wait for our tcp connection to be established
    If (mySocket)
        Break
If (i := AHKsock_ForceSend(mySocket, &rawHttpRequest, rawHttpRequestSize)) ;send our formated websocket handshake request
    MsgBox, % "AHKsock_ForceSend failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting
 
MsgBox, Let's send a frame now!
 
text := "Test message! Hello!" ;UTF-8 test data that we try to send as a frame
textLen := StrPut(text, "UTF-8") - 1
frameSize := 4+2+textLen
VarSetCapacity(textFrame,frameSize)
if textLen >= 126
    throw "Payload size needs to be encoded differently"
NumPut(1|(1<<4)|(1<<8)|(textLen<<9), &textFrame, "ushort")
Random mask1, 0, 0xFFFF  ; Had problems with 0,0xFFFFFFFF (acting like -1..0).
Random mask2, 0, 0xFFFF
mask := mask1 | (mask2 << 16)
NumPut(mask, &textFrame+2, "uint")
StrPut(text, &textFrame+6, textLen, "UTF-8")
Loop % textLen  ; Apply mask to payload.
    NumPut(NumGet(textFrame, 5+A_Index, "uchar") ^ NumGet(textFrame, 2+Mod(A_Index-1,4), "uchar")
        , textFrame, 5+A_Index, "uchar")
If (i := AHKsock_ForceSend(mySocket, &textFrame, frameSize)) ;send our frame
    MsgBox, % "AHKsock_ForceSend failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting

TcpCallback(sEvent, iSocket = 0, sName = 0, sAddr = 0, sPort = 0, ByRef bData = 0, bDataLength = 0) { ;callback function that gets called when something happens on our tcp connection
    Global mySocket
    MsgBox, % "sEvent: " sEvent "`niSocket: " iSocket "`nsName: " sName "`nsAddr: " sAddr "`nsPort: " sPort "`nbData: " bData "`nbDataLength: " bDataLength
        . "`nsData: " StrGet(&bData, bDataLength, "UTF-8")
    If (sEvent = "CONNECTED") { ;a tcp connection was successfully established
        mySocket := iSocket ;save the connection handle in the global variable mySocket 
    }
}
 
ErrorCallback(iError, iSocket) {  ;for error handling
    MsgBox, % "iError: " iError "`niSocket: " iSocket  ;for error handling
} ;for error handling
Note that your "UTF-8 test data" wasn't UTF-8. Text payloads must be UTF-8.
Bruttosozialprodukt
Posts: 463
Joined: 24 Jan 2014, 22:28

Re: WebSocket protocol with AHK

17 Jun 2015, 03:15

It looks like the "0x00 header, 0XFF tail" method is obsolete and is only used by Apple products and Opera.
Maybe changing the User-Agent would already do the job...

I'll take a look at the MDN site later..
lexikos
Posts: 9559
Joined: 30 Sep 2013, 04:07
Contact:

Re: WebSocket protocol with AHK

17 Jun 2015, 05:57

I made some progress.

Code: Select all

#Include AHKsock.ahk ;https://raw.githubusercontent.com/jleb/AHKsock/master/AHKsock.ahk
 
mySocket := 0 ;global tcp socket handle
;our websocket handshake request:
rawHttpRequestUnicode = 
( Join`r`n
GET /?encoding=text HTTP/1.1
Origin: http://websocket.org
Host: echo.websocket.org
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: uRovscZjNol/umbTt5uKmw==
Sec-WebSocket-Version: 13


)
rawHttpRequestSize := StrPut(rawHttpRequestUnicode, "UTF-8") - 1
VarSetCapacity(rawHttpRequest, rawHttpRequestSize)
StrPut(rawHttpRequestUnicode, &rawHttpRequest, rawHttpRequestSize, "UTF-8")
 
AHKsock_ErrorHandler("ErrorCallback") ;for error handling
If (i := AHKsock_Connect("echo.websocket.org", 80, "TcpCallback")) ;connect to echo.websocket.org on port 80 (http) and set the function TcpCallback as our callback function
    MsgBox, % "AHKsock_Connect failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting
 
Loop ;wait for our tcp connection to be established
    If (mySocket)
        Break
If (i := AHKsock_ForceSend(mySocket, &rawHttpRequest, rawHttpRequestSize)) ;send our formated websocket handshake request
    MsgBox, % "AHKsock_ForceSend failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting
 
MsgBox, Let's send a frame now!
 
text := "Test message! Hello!" ;UTF-8 test data that we try to send as a frame
textLen := StrPut(text, "UTF-8") - 1
frameSize := 6+textLen
VarSetCapacity(textFrame,frameSize,0)
if textLen >= 126
    throw "Payload size needs to be encoded differently"
fin    := 0x0080     ; 1....... ........
opcode := 0x0001     ; ....0001 ........
masked := 0x8000     ; ........ 1.......
length := textLen<<8 ; ........ .xxxxxxx
NumPut(fin|opcode|masked|length, &textFrame, "ushort")
Random mask1, 0, 0xFFFF  ; Had problems with 0,0xFFFFFFFF (acting like -1..0).
Random mask2, 0, 0xFFFF
mask := mask1 | (mask2 << 16)
NumPut(mask, &textFrame+2, "uint")
StrPut(text, &textFrame+6, textLen, "UTF-8")
Loop % textLen  ; Apply mask to payload.
    NumPut(NumGet(textFrame, 5+A_Index, "uchar") ^ NumGet(textFrame, 2+Mod(A_Index-1,4), "uchar")
        , textFrame, 5+A_Index, "uchar")
If (i := AHKsock_ForceSend(mySocket, &textFrame, frameSize)) ;send our frame
    MsgBox, % "AHKsock_ForceSend failed with return value = " i " and error code = " ErrorLevel " at line " A_LineNumber ;error reporting

Sleep 1000  ; Send a "close" frame:
NumPut(0xe8030288, textFrame, 0, "uint"), frameSize := 4
AHKsock_ForceSend(mySocket, &textFrame, frameSize)
 
TcpCallback(sEvent, iSocket = 0, sName = 0, sAddr = 0, sPort = 0, ByRef bData = 0, bDataLength = 0) { ;callback function that gets called when something happens on our tcp connection
    Global mySocket
    if (bDataLength = 4 && NumGet(bData, "uint") = 0xe8030288) {
        MsgBox Connection closing gracefully.
        AHKsock_Close(iSocket)
        ExitApp
    }
    MsgBox, % "sEvent: " sEvent "`niSocket: " iSocket "`nsName: " sName "`nsAddr: " sAddr "`nsPort: " sPort "`nbData: " bData "`nbDataLength: " bDataLength
        . "`nsData: " StrGet(&bData, bDataLength, "UTF-8")
    If (sEvent = "CONNECTED") { ;a tcp connection was successfully established
        mySocket := iSocket ;save the connection handle in the global variable mySocket 
    }
    if (sEvent = "DISCONNECTED")
        ExitApp
}
 
ErrorCallback(iError, iSocket) {  ;for error handling
    MsgBox, % "iError: " iError "`niSocket: " iSocket  ;for error handling
} ;for error handling
The frame is sent and echoed back, but the received frame isn't decoded yet.

My (main?) mistake was with bit order: bit 0 in the spec is actually the most significant bit of the first byte (0x80 or 0b10000000).

Also note that numbers larger than a byte within the WebSocket protocol are big-endian, but x86 is little-endian. In my script, the integer 0xe8030288 produces the bytes 88 02 03 E8, which represent a close frame (8: fin, 8: opcode, 02: payload length, 03 E8: payload). The number encoded in the payload is 0x03e8 (1000), not 0xe803.

Your script (and my previous copy) uses the sequence "StrLen, VarSetCapacity, StrPut". This is wrong. StrLen tells you the length in the current encoding, which can be different to the number of characters which StrPut will write. I've corrected this in the script above.
Bruttosozialprodukt
Posts: 463
Joined: 24 Jan 2014, 22:28

Re: WebSocket protocol with AHK

17 Jun 2015, 14:29

Wow, that's awesome!
What do you mean by "the received frame isn't decoded yet"? I was able to read the echoed text form the msgbox.

I was just trying to put that code in a nice websocket class.... until I realized that a global callback function would be necessary. I'll try to switch to Bentschi's TCP UDP class...
geek
Posts: 1052
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: WebSocket protocol with AHK

17 Jun 2015, 14:33

Why would a global callback function be needed?
Bruttosozialprodukt
Posts: 463
Joined: 24 Jan 2014, 22:28

Re: WebSocket protocol with AHK

17 Jun 2015, 15:45

because you have to pass the name of a function as a string. so I don't think you could pass an objects method to it... unless this would be valid code:

Code: Select all

class example {
    __new() {
        funcName := "this.myMethod"
        Socket(funcName) 
    }
    myMethod()  {
        msgbox
    } 
}
Socket(callback) {
    %callback%() 
} 
lexikos
Posts: 9559
Joined: 30 Sep 2013, 04:07
Contact:

Re: WebSocket protocol with AHK

17 Jun 2015, 16:25

Bruttosozialprodukt wrote:What do you mean by "the received frame isn't decoded yet"? I was able to read the echoed text form the msgbox.
Didn't you notice the two characters preceding the string? There's a header which is between 2 and 14 bytes. You need to decode it to determine the frame length, whether the frame is complete, what kind of frame it is, etc. You aren't guaranteed to receive (to your TcpCallback) exactly one message at a time, since TCP/IP doesn't preserve message boundaries.
Bruttosozialprodukt wrote:because you have to pass the name of a function as a string.
No, you just need IsFunc(sFunction) to be true. You could pass a function reference, but if you use a method the first parameter will be stored in this. Currently IsFunc returns false for BoundFunc objects, so passing Your.Method.Bind(Your) won't work either. (IsFunc should probably be changed.)

When you're designing a library which accepts a callback, it's probably better to not use IsFunc, since it prevents you from using user-defined function objects (which couldn't be accurately detected by IsFunc even if it was changed).
User avatar
Guhl
Posts: 25
Joined: 29 May 2018, 01:12

Re: WebSocket protocol with AHK

06 Jun 2018, 09:24

I know its old but just found https://github.com/G33kDude/WebSocket.ahk and it works very well

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: Google [Bot], JoeWinograd, yabab33299 and 148 guests