Skip to content

Instantly share code, notes, and snippets.

@Netzvamp
Last active September 28, 2023 11:47
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Netzvamp/8623def14501de15c9e4 to your computer and use it in GitHub Desktop.
Save Netzvamp/8623def14501de15c9e4 to your computer and use it in GitHub Desktop.
Purebasic Websocketclient
; Websocketclient by Netzvamp
; Version: 2016/01/08
DeclareModule WebsocketClient
Declare OpenWebsocketConnection(URL.s)
Declare SendTextFrame(connection, message.s)
Declare ReceiveFrame(connection, *MsgBuffer)
Declare SetSSLProxy(ProxyServer.s = "", ProxyPort.l = 8182)
Enumeration
#frame_text
#frame_binary
#frame_closing
#frame_ping
#frame_unknown
EndEnumeration
EndDeclareModule
Module WebsocketClient
;TODO: Add function to send binary frame
;TODO: We don't support fragmetation right now
;TODO: We should send an closing frame, but server will also just close
;TODO: Support to send receive bigger frames
Declare Handshake(Connection, Servername.s, Path.s)
Declare ApplyMasking(Array Mask.a(1), *Buffer)
Global Proxy_Server.s, Proxy_Port.l
Macro dbg(txt)
CompilerIf #PB_Compiler_Debugger
Debug "WebsocketClient: " + FormatDate("%yyyy-%mm-%dd %hh:%ii:%ss",Date()) + " > " + txt
CompilerEndIf
EndMacro
Procedure SetSSLProxy(ProxyServer.s = "", ProxyPort.l = 8182)
Proxy_Server.s = ProxyServer.s
Proxy_Port.l = ProxyPort.l
EndProcedure
Procedure OpenWebsocketConnection(URL.s)
Protokol.s = GetURLPart(URL.s, #PB_URL_Protocol)
Servername.s = GetURLPart(URL.s, #PB_URL_Site)
Port.l = Val(GetURLPart(URL.s, #PB_URL_Port))
If Port.l = 0 : Port.l = 80 : EndIf
Path.s = GetURLPart(URL.s, #PB_URL_Path)
If Path.s = "" : Path.s = "/" : EndIf
InitNetwork()
If Protokol.s = "wss" ; If we connect with encryption (https)
If Proxy_Port
Connection = OpenNetworkConnection(Proxy_Server.s, Proxy_Port.l, #PB_Network_TCP, 1000)
Else
dbg("We need an SSL-Proxy like stunnel for encryption. Configure the proxy with SetSSLProxy().")
EndIf
ElseIf Protokol.s = "ws"
Connection = OpenNetworkConnection(Servername.s, Port.l, #PB_Network_TCP, 1000)
EndIf
If Connection
If Handshake(Connection, Servername.s, Path.s)
dbg("Connection and Handshake ok")
ProcedureReturn Connection
Else
dbg("Handshake-Error")
ProcedureReturn #False
EndIf
Else
dbg("Couldn't connect")
ProcedureReturn #False
EndIf
EndProcedure
Procedure Handshake(Connection, Servername.s, Path.s)
Request.s = "GET /" + Path.s + " HTTP/1.1"+ #CRLF$ +
"Host: " + Servername.s + #CRLF$ +
"Upgrade: websocket" + #CRLF$ +
"Connection: Upgrade" + #CRLF$ +
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" + #CRLF$ +
"Sec-WebSocket-Version: 13" + #CRLF$ +
"User-Agent: CustomWebsocketClient"+ #CRLF$ + #CRLF$
SendNetworkString(Connection, Request.s, #PB_UTF8)
*Buffer = AllocateMemory(65536)
; We wait for answer
Repeat
Size = ReceiveNetworkData(connection, *Buffer, 65536)
Answer.s = Answer.s + PeekS(*Buffer, Size, #PB_UTF8)
If FindString(Answer, #CRLF$ + #CRLF$)
Break
EndIf
Until Size <> 65536
Answer.s = UCase(Answer.s)
; Check answer
If FindString(Answer.s, "HTTP/1.1 101") And FindString(Answer.s, "CONNECTION: UPGRADE") And FindString(Answer.s, "UPGRADE: WEBSOCKET")
ProcedureReturn #True
Else
ProcedureReturn #False
EndIf
EndProcedure
Procedure ApplyMasking(Array Mask.a(1), *Buffer)
For i = 0 To MemorySize(*Buffer) - 1
PokeA(*Buffer + i, PeekA(*Buffer + i) ! Mask(i % 4))
Next
EndProcedure
Procedure SendTextFrame(connection, message.s)
; Put String in Buffer
MsgLength.l = StringByteLength(message.s, #PB_UTF8)
*MsgBuffer = AllocateMemory(MsgLength)
PokeS(*MsgBuffer, message.s, MsgLength, #PB_UTF8|#PB_String_NoZero)
dbg("Messagelength to send: " + Str(MsgLength))
; The Framebuffer, we fill with senddata
If MsgLength <= 125
Fieldlength = 6
ElseIf MsgLength >= 126 And MsgLength <= 65535
Fieldlength = 8
Else
Fieldlength = 14
EndIf
dbg("Fieldlength to send: " + Str(Fieldlength))
*FrameBuffer = AllocateMemory(Fieldlength + MsgLength)
; We generate 4 random masking bytes
Dim Mask.a(3)
Mask(0) = Random(255,0)
Mask(1) = Random(255,0)
Mask(2) = Random(255,0)
Mask(3) = Random(255,0)
pos = 0 ; The byteposotion in the framebuffer
; First Byte: FIN(1=finished with this Frame),RSV(0),RSV(0),RSV(0),OPCODE(4 byte)=0001(text)
PokeB(*FrameBuffer, %10000001) : pos + 1 ; = 129
; Second Byte: Masking(1),length(to 125bytes, else we have to extend)
If MsgLength <= 125 ; Length fits in first byte
PokeA(*Framebuffer + pos, MsgLength + 128) : pos + 1 ; + 128 for Masking
ElseIf MsgLength >= 126 And MsgLength <= 65535 ; We have to extend length to third byte
PokeA(*Framebuffer + pos, 126 + 128) : pos + 1 ; 126 for 2 extra length bytes and + 128 for Masking
PokeA(*FrameBuffer + pos, (MsgLength >> 8)) : pos + 1 ; First Byte
PokeA(*FrameBuffer + pos, MsgLength) : pos + 1 ; Second Byte
Else ; It's bigger than 65535, we also use 8 extra bytes
PokeA(*Framebuffer + pos, 127 + 128) : pos + 1 ; 127 for 8 extra length bytes and + 128 for Masking
PokeA(*Framebuffer + pos, 0) : pos + 1 ; 8 Bytes for payload lenght. We don't support giant packages for now, so first bytes are zero :P
PokeA(*Framebuffer + pos, 0) : pos + 1
PokeA(*Framebuffer + pos, 0) : pos + 1
PokeA(*Framebuffer + pos, 0) : pos + 1
PokeA(*Framebuffer + pos, MsgLength >> 24) : pos + 1
PokeA(*Framebuffer + pos, MsgLength >> 16) : pos + 1
PokeA(*Framebuffer + pos, MsgLength >> 8) : pos + 1
PokeA(*Framebuffer + pos, MsgLength) : pos + 1 ; = 10 Byte
EndIf
; Write Masking Bytes
PokeA(*FrameBuffer + pos, Mask(0)) : pos + 1
PokeA(*FrameBuffer + pos, Mask(1)) : pos + 1
PokeA(*FrameBuffer + pos, Mask(2)) : pos + 1
PokeA(*FrameBuffer + pos, Mask(3)) : pos + 1
ApplyMasking(Mask(), *MsgBuffer)
CopyMemory(*MsgBuffer, *FrameBuffer + pos, MsgLength)
;For x = 0 To 100 Step 5
;Debug Str(PeekA(*FrameBuffer + x)) + " | " + Str(PeekA(*FrameBuffer + x + 1)) + " | " + Str(PeekA(*FrameBuffer + x + 2)) + " | " + Str(PeekA(*FrameBuffer + x + 3)) + " | " + Str(PeekA(*FrameBuffer + x + 4))
;Next
If SendNetworkData(connection, *FrameBuffer, Fieldlength + MsgLength) = Fieldlength + MsgLength
dbg("Textframe send, Bytes: " + Str(Fieldlength + MsgLength))
ProcedureReturn #True
Else
ProcedureReturn #False
EndIf
EndProcedure
Procedure ReceiveFrame(connection, *MsgBuffer)
*FrameBuffer = AllocateMemory(65536)
Repeat
*FrameBuffer = ReAllocateMemory(*FrameBuffer, 65536)
Size = ReceiveNetworkData(connection, *FrameBuffer, 65536)
;Answer.s = Answer.s + PeekS(*FrameBuffer, Size, #PB_UTF8)
Until Size <> 65536
dbg("Received Frame, Bytes: " + Str(Size))
*FrameBuffer = ReAllocateMemory(*FrameBuffer, Size)
; ; debug: output any single byte
; If #PB_Compiler_Debugger
; For x = 0 To Size - 1 Step 1
; dbg_bytes.s + Str(PeekA(*FrameBuffer + x)) + " | "
; Next
; dbg(dbg_bytes)
; EndIf
; Getting informations about package
If PeekA(*FrameBuffer) & %10000000 > #False
;dbg("Frame not fragmented")
fragmentation.b = #False
Else
dbg("Frame fragmented! This not supported for now!")
fragmentation.b = #True
EndIf
; Check for Opcodes
If PeekA(*FrameBuffer) = %10000001 ; Textframe
dbg("Text frame")
frame_typ.w = #frame_text
ElseIf PeekA(*FrameBuffer) = %10000010 ; Binary Frame
dbg("Binary frame")
frame_typ.w = #frame_binary
ElseIf PeekA(*FrameBuffer) = %10001000 ; Closing Frame
dbg("Closing frame")
frame_typ.w = #frame_closing
ElseIf PeekA(*FrameBuffer) = %10001001 ; Ping
; We just answer pings
*pongbuffer = AllocateMemory(2)
PokeA(*pongbuffer, 138)
PokeA(*pongbuffer+1, 0)
SendNetworkData(connection, *pongbuffer, 2)
dbg("Received Ping, answered with Pong")
frame_typ.w = #frame_ping
ProcedureReturn
Else
dbg("Opcode unknown")
frame_typ.w = #frame_unknown
ProcedureReturn #False
EndIf
; Check masking
If PeekA(*FrameBuffer + 1) & %10000000 = 128 : masking.b = #True : Else : masking.b = #False : EndIf
dbg("Masking: " + Str(masking))
pos.l = 1
; check size
If PeekA(*FrameBuffer + 1) & %01111111 <= 125 ; size is in this byte
frame_size.l = PeekA(*FrameBuffer + pos) & %01111111 : pos + 1
ElseIf PeekA(*FrameBuffer + 1) & %01111111 >= 126 ; Size is in 2 extra bytes
frame_size.l = PeekA(*FrameBuffer + 2) << 8 + PeekA(*FrameBuffer + 3) : pos + 2
EndIf
dbg("FrameSize: " + Str(frame_size.l))
If masking = #True
Dim Mask.a(3)
Mask(0) = PeekA(*FrameBuffer + pos) : pos + 1
Mask(1) = PeekA(*FrameBuffer + pos) : pos + 1
Mask(2) = PeekA(*FrameBuffer + pos) : pos + 1
Mask(3) = PeekA(*FrameBuffer + pos) : pos + 1
ReAllocateMemory(*MsgBuffer,frame_size)
CopyMemory(*FrameBuffer + pos, *MsgBuffer, frame_size)
ApplyMasking(Mask(), *MsgBuffer)
Else
ReAllocateMemory(*MsgBuffer,frame_size)
CopyMemory(*FrameBuffer + pos, *MsgBuffer, frame_size)
EndIf
ProcedureReturn frame_typ
EndProcedure
EndModule
CompilerIf #PB_Compiler_IsMainFile
; Minimal example to send and receive textmessages
; The preconfigured testserver "echo.websocket.org" will just echo back everything you've send.
XIncludeFile "module_websocketclient_gui.pbf"
Global connection
Procedure gui_button_connect(EventType)
If EventType = #PB_EventType_LeftClick
Debug "Connect clicked"
;connection = WebsocketClient::OpenWebsocketConnection(GetGadgetText(#String_url))
connection = WebsocketClient::OpenWebsocketConnection(GetGadgetText(#String_url))
AddGadgetItem(#ListView_Output, -1, "# Connected to " + GetGadgetText(#String_url))
EndIf
EndProcedure
Procedure gui_button_send(EventType)
If EventType = #PB_EventType_LeftClick And Len(GetGadgetText(#String_send)) > 0 And connection
Debug "Send clicked"
If WebsocketClient::SendTextFrame(connection, GetGadgetText(#String_send)) = #False
Debug "Couldn't send. Are we disconnected?"
Else
AddGadgetItem(#ListView_Output, -1, "> " + GetGadgetText(#String_send))
EndIf
EndIf
EndProcedure
OpenWindow_Websocketclient()
; Proxy Setting:
; If you need an encyrpted connection (https/wss), you currently have to use an
; proxy software like stunnel (https://www.stunnel.org) to redirect unencrypted data into an encrypted connection
; Example stunnel.conf section:
; [websocket]
; client = yes
; accept = 127.0.0.1:8182
; connect = echo.websocket.org:443
WebsocketClient::SetSSLProxy("127.0.0.1",8182)
Repeat
If Window_Websocketclient_Events( WaitWindowEvent(1) ) = #False : End : EndIf
If connection
NetworkEvent = NetworkClientEvent(connection)
Select NetworkEvent
Case #PB_NetworkEvent_Data
Debug "We've got Data"
*FrameBuffer = AllocateMemory(1)
Frametyp = WebsocketClient::ReceiveFrame(connection,*FrameBuffer)
If Frametyp = WebsocketClient::#frame_text
AddGadgetItem(#ListView_Output, -1, "< " + PeekS(*FrameBuffer,MemoryStringLength(*FrameBuffer,#PB_UTF8)-1,#PB_UTF8) )
ElseIf Frametyp = WebsocketClient::#frame_binary
AddGadgetItem(#ListView_Output, -1, "< Received Binaryframe" )
EndIf
Case #PB_NetworkEvent_Disconnect
If disconnected = #False
Debug "Disconnected"
EndIf
disconnected = #True
NetworkEvent = #PB_NetworkEvent_None
Case #PB_NetworkEvent_None
EndSelect
EndIf
ForEver
CompilerEndIf
;
; This code is automatically generated by the FormDesigner.
; Manual modification is possible to adjust existing commands, but anything else will be dropped when the code is compiled.
; Event procedures needs to be put in another source file.
;
Enumeration FormWindow
#Window_Websocketclient
EndEnumeration
Enumeration FormGadget
#String_send
#String_url
#Button_connect
#Frame3D_io
#Button_send
#ListView_Output
EndEnumeration
Declare gui_button_connect(EventType)
Declare gui_button_send(EventType)
Procedure OpenWindow_Websocketclient(x = 0, y = 0, width = 600, height = 400)
OpenWindow(#Window_Websocketclient, x, y, width, height, "Websocketclient :: Test-GUI", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_MaximizeGadget | #PB_Window_ScreenCentered)
StringGadget(#String_send, 20, 60, 460, 25, "")
StringGadget(#String_url, 10, 10, 470, 25, "ws://echo.websocket.org")
ButtonGadget(#Button_connect, 490, 10, 100, 25, "Connect")
FrameGadget(#Frame3D_io, 10, 40, 580, 350, "Input/Output")
ButtonGadget(#Button_send, 490, 60, 90, 25, "Send")
ListViewGadget(#ListView_Output, 20, 100, 560, 280)
EndProcedure
Procedure Window_Websocketclient_Events(event)
Select event
Case #PB_Event_CloseWindow
ProcedureReturn #False
Case #PB_Event_Menu
Select EventMenu()
EndSelect
Case #PB_Event_Gadget
Select EventGadget()
Case #Button_connect
gui_button_connect(EventType())
Case #Button_send
gui_button_send(EventType())
EndSelect
EndSelect
ProcedureReturn #True
EndProcedure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment