Theldus / wsServer

wsServer - a tiny WebSocket server library written in C

Home Page:https://theldus.github.io/wsServer

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Use wsServer code as a webscoket client

westsuhanic opened this issue · comments

Hello:

I use wsServer successfully in my project. I need a websocket client written in C.
How would I use your existing code to implement a simple websocket client? Is it possible? Any pointers?

thank you,

west suhanic

Hi @westsuhanic,
(sorry for the long delay)
Well, I would recommend using an appropriate WebSocket client for this, since this project, at the moment, only aims to build a server, not a client.

But to answer your question: the client and server have a lot in common, which could be broken down into:
a) Client handshake
b) Sending masked messages
c) Closing connection

A fairly minimal client (and with an emphasis on that) might look like the one below:

#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <unistd.h>

#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define FRM_TXT 1
#define FRM_BIN 2
#define FRM_FIN 128
#define FRM_MSK 128

char dummy_buff[1024];

const char request[] = 
    "GET / HTTP/1.1\r\n"
    "Host: localhost:8080\r\n"
    "Connection: Upgrade\r\n"
    "Upgrade: websocket\r\n"
    "Sec-WebSocket-Version: 13\r\n"
    "Sec-WebSocket-Key: uaGPoPbZRzHcWDXiNQ5dyg==\r\n\r\n";

static int do_connect(const char *ip, uint16_t port)
{
    int sock;
    in_addr_t ip_addr;
    struct timeval tv;
    struct sockaddr_in s_addr;

    /* Create socket. */
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
        return (-1);

    memset((void*)&s_addr, 0, sizeof(s_addr));
    s_addr.sin_family = AF_INET;

    if ((ip_addr = inet_addr(ip)) == INADDR_NONE)
        return (-1);

    s_addr.sin_addr.s_addr = ip_addr;
    s_addr.sin_port = htons(port);

    /* Connect. */
    if (connect(sock, (struct sockaddr *)&s_addr, sizeof(s_addr)) < 0)
        return (-1);

    /* Do handhshake. */
    if (send(sock, request, strlen(request), MSG_NOSIGNAL) < 0)
        return (-1);
    
    /* Wait for 'switching protocols'. */
    if (recv(sock, dummy_buff, sizeof(dummy_buff), 0) < 0)
        return (-1);
    
    return (sock);
}

static int sendframe(int fd, uint8_t *msg, uint64_t size,
    int type)
{
    uint8_t frame[10] = {0};
    uint8_t masks[4];
    uint64_t length;
    uint8_t hdr_len;
    size_t count;
    uint8_t *p;

    frame[0]  = FRM_FIN | type;
    frame[1] |= FRM_MSK;
    length    = (uint64_t)size;

    /* Split the size between octets. */
    if (length <= 125) {
        frame[1] |= length & 0x7F;
        hdr_len = 2;
    }

    /* Size between 126 and 65535 bytes. */
    else if (length >= 126 && length <= 65535) {
        frame[1] |= 126;
        frame[2]  = (length >> 8) & 255;
        frame[3]  = length & 255;
        hdr_len = 4;
    }

    /* More than 65535 bytes. */
    else {
        frame[1] |= 127;
        frame[2]  = (uint8_t)((length >> 56) & 255);
        frame[3]  = (uint8_t)((length >> 48) & 255);
        frame[4]  = (uint8_t)((length >> 40) & 255);
        frame[5]  = (uint8_t)((length >> 32) & 255);
        frame[6]  = (uint8_t)((length >> 24) & 255);
        frame[7]  = (uint8_t)((length >> 16) & 255);
        frame[8]  = (uint8_t)((length >> 8) & 255);
        frame[9]  = (uint8_t)(length & 255);
        hdr_len = 10;
    }
    
    /* Send header. */
    if (send(fd, frame, hdr_len, MSG_NOSIGNAL) < 0)
        return (-1);
        
    /* Send dummy masks. */
    masks[0] = masks[1] = masks[2] = masks[3] = 0xAA;
    if (send(fd, masks, 4, MSG_NOSIGNAL) < 0)
        return (-2);
    
    /* Mask message and send it. */
    for (p = msg, count = 0; count < size; p++, count++)
        *p ^= masks[count % 4];
    
    if (send(fd, msg, size, MSG_NOSIGNAL) < 0)
        return (-3);

    return (0);
}

static int do_close(int fd) {
    shutdown(fd, SHUT_RDWR);
    close(fd);
}

int main(void) {
    int fd;
    char msg[] = "Hello";

    fd = do_connect("127.0.0.1", 8080); 

    printf("send: %s\n",
        (sendframe(fd, msg, strlen(msg), FRM_TXT) >= 0 ?
            "Success" : "Failed"));
    
    do_close(fd);
    return (0);
}

but please note that:

  • Handshake header is fixed
  • The frame mask is fixed
  • No PING/PONG frame support
  • No close handshake
  • Possibly other things too...

so I don't recommend using this in production at all, however 'it works'.

Tested on wsServer and websocat.

Hi @westsuhanic,

2)download a file;

From the server to the client? the example I sent above only sends messages to the server, it cannot receive; if you want to receive, you need to make major changes to the code.

I got a client going using Boost. However I also need the code to run under cygwin like your server code. Unfortunately the version of Boost available under cygwin does not include the websocket functionality. I would really prefer to do all my websocket work with one code base.

Got it, you want to remove all the bloat and as many dependencies as possible...

To add client support to wsServer (robustly, as the server is intended to be), I would have to make major changes to the code, to make the common parts more decoupled, etc. Unfortunately it's not exactly something I intend to work on at the moment, as I have other issues opened that I should work on in the near future.

Maybe what I could do at the moment is to add a 'toy client' and leave it as 'extra', making it clear its compatibility (guaranteed) only with wsServer.

Thank you again for your wonderful work.

Thank you.

It works against my server once. I have to restart the server to get the client to work again.

By 'get to work again' what do you mean? here it works fine.

A point worth mentioning in the example is that it modifies the original message (when xoring the mask in sendframe), so when invoking sendframe again with the same buffer, strlen doesn't work and the message isn't even sent.

I did it this way because I was avoiding using dynamic memory, the patch below allocates a new buffer and fixes this behavior:

--- old-cli.c	2022-01-14 21:38:54.369262588 -0300
+++ cli.c	2022-01-14 21:41:08.832252561 -0300
@@ -1,4 +1,5 @@
 #include <stdio.h>
+#include <stdlib.h>
 #include <string.h>
 #include <inttypes.h>
 #include <unistd.h>
@@ -66,6 +67,7 @@
     uint64_t length;
     uint8_t hdr_len;
     size_t count;
+    uint8_t *tmp;
     uint8_t *p;
 
     frame[0]  = FRM_FIN | type;
@@ -110,12 +112,21 @@
         return (-2);
     
     /* Mask message and send it. */
-    for (p = msg, count = 0; count < size; p++, count++)
-        *p ^= masks[count % 4];
+    p = calloc(1, size);
+    if (!p)
+    	return (-3);
     
-    if (send(fd, msg, size, MSG_NOSIGNAL) < 0)
-        return (-3);
+    memcpy(p, msg, size);
 
+    for (tmp = p, count = 0; count < size; tmp++, count++)
+        *tmp ^= masks[count % 4];
+    
+    if (send(fd, p, size, MSG_NOSIGNAL) < 0) {
+    	free(p);
+        return (-4);
+    }
+    
+    free(p);
     return (0);
 }
 

In the onclose handler-(the stock one you have provided)-it does not return the client ip address.

Yes, this is a known issue. The ws_getaddress() routine invokes inet_ntop() under the hood. It turns out that when an abrupt close occurs (no close handshake), inet_ntop() cannot get the IP address of the client, since the client has already disconnected.

A cleaner approach would be to save the IP address of the client right at the beginning of the connection and return it when required by the user. I have to do this at some point.

Is that related to the noclose handshake?

As I explained above, yes it is, but I consider this a bug of wsServer: it should be able to return the IP address of the client even on an abrupt close. I will fix this later.

You don't have to worry about doing a close handshake right now. At the moment, if you want the IP of the client, you can save it in onopen() and use it later ;-).

Here is a quick client close connection function. I used the close code from your server [...] It seems to work against my server as I can run the client repeatedly without having to restart the server. Thoughts?

It is only partially correct. The close handshake must be done by both parties:

  • Client sends close frame
  • Server answers with the same or different close code
  • Client closes the connection

Your code closes the connection before the server sends the response frame. This 'works' because the wsServer's onclose() event is no longer invoked by the loss of the connection, but by the receipt of the close frame. Anyway, being pedantic is wrong as the handshake is not completed.

As I said, you don't have to worry about the close handshake, as the failure to get the IP address in onclose() is a bug that should be fixed.

Hi @westsuhanic,
I'm working on a 'toy websocket client' that will be integrated along with this repository as an 'extra' tool (as we had talked about). I should push something to the master branch soon, I'll let you know ;-).

@westsuhanic done.
'ToyWS' has been added to extra/toyws, its API (w/ example) is available here.

Hope it meets your expectations =).

Since all points have been addressed (websocket client, included as 'ToyWS' and buggy IP address on on-close events fixed), I believe I can close this issue now.

Feel free to reopen it if you have additional questions about it, or create a new issue if necessary.