csocketsirc

IRC client does not print/receive full response


#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

#include <netdb.h>
#include <unistd.h>

static char *host = "irc.libera.chat";
static char *port = "6667";
static char *chan = "#libera";
static char *nick = "nick";
static char *pass = NULL;

static int sock  = 0;

void
message(char *fmt, ...) {
    va_list ap;
    /* determine size */
    va_start(ap, fmt);
    int n = vsnprintf(NULL, 0, fmt, ap);
    va_end(ap);
    if (n < 0) {
        fputs("vsnprintf() failed", stderr);
        exit(EXIT_FAILURE);
    }
    size_t size = n + 1;
    /* construct */
    char *msg = malloc(size);
    if (msg == NULL) {
        perror("malloc() failed");
        exit(EXIT_FAILURE);
    }
    va_start(ap, fmt);
    n = vsnprintf(msg, size, fmt, ap);
    va_end(ap);
    if (n < 0) {
        fputs("vsnprintf() failed\n", stderr);
        free(msg);
        exit(EXIT_FAILURE);
    }
    /* send */
    ssize_t nsent = send(sock, msg, size, 0);
    free(msg);
    if (nsent == -1) {
        perror("send() failed");
        exit(EXIT_FAILURE);
    } else if ((size_t)nsent != size) {
        fprintf(stderr,
                "send() failed: expected to send %lu bytes, sent %ld instead\n",
                size, nsent);
        exit(EXIT_FAILURE);
    }
}

int
main(void) {
    /* initialize connection */
    struct addrinfo hints = {
        .ai_flags     = 0,
        .ai_family    = AF_UNSPEC,
        .ai_socktype  = SOCK_STREAM,
        .ai_protocol  = 0,
        .ai_addrlen   = 0,
        .ai_addr      = NULL,
        .ai_canonname = NULL,
        .ai_next      = NULL
    };
    struct addrinfo *res;
    int ret = getaddrinfo(host, port, &hints, &res);
    if (ret != 0) {
        fprintf(stderr, "getaddrinfo() failed: %s\n", gai_strerror(ret));
        return EXIT_FAILURE;
    }
    struct addrinfo *rp;
    for (rp = res; rp != NULL; rp = rp->ai_next) {
        sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (sock == -1) {
            perror("socket() failed");
            continue;
        }
        if (connect(sock, rp->ai_addr, rp->ai_addrlen) == -1) {
            perror("connect() failed");
            close(sock);
            continue;
        }
        break;
    }
    freeaddrinfo(res);
    if (rp == NULL) {
        fprintf(stderr, "could not connect to %s:%s\n", host, port);
        return EXIT_FAILURE;
    }
    /* log in */
    if (pass)
        message("PASS %s\n", pass);
    message("NICK %s\n", nick);
    message("USER %s - - :%s\n", nick, nick);
    /* join channel */
    if (chan != NULL)
        message("JOIN %s\n", chan);
    /* print response */
    char buffer[4096];
    ssize_t nbyte;
loop:
    nbyte = recv(sock, buffer, 4095, 0);
    if (nbyte < 0) {
        fputs("recv() failed", stderr);
        return 1;
    } else if (nbyte == 0) {
        fputs("recv() failed: connection closed prematurely", stderr);
        return 1;
    }
    buffer[nbyte] = '\0';
    printf("%s", buffer);
    goto loop;
    /* unreachable */
}

outputs

:calcium.libera.chat NOTICE * :*** Checking Ident
:calcium.libera.chat NOTICE * :*** Looking up your hostname...
:calcium.libera.chat NOTICE * :*** Couldn't look up your hostname
:calcium.libera.chat NOTICE * :*** No Ident response
ERROR :Closing Link: 127.0.0.1 (Connection timed out)
recv() failed: connection closed prematurely

Other irc clients further output

:calcium.libera.chat 001 nick :Welcome to the Libera.Chat Internet Relay Chat Network nick
...

Could the issue be in error handling?

For example, according to send(2)

On success, these calls return the number of bytes sent. On error, -1 is returned, and errno is set to indicate the error.

so

} else if ((size_t)nsent != size) {
    fprintf(stderr,
            "send() failed: expected to send %lu bytes, sent %ld instead\n",
            size, nsent);
    exit(EXIT_FAILURE);
}

seems redundant, as well as its recv counterpart. Am I handling vsnprintf and malloc correctly?


Solution

  • You are handling vsnprintf() and malloc() fine. It is send() that you are not handling correctly. There are two problems with your usage:

    1. you are including the formatted string's null-terminator in the transmission. Don't do that, that is not part of the IRC protocol.

    2. you are not accounting for partial transmissions, as send() can return fewer bytes than requested, thus requiring send() to be called again to send any unsent bytes. So you need to call send() in a loop. A return value that is greater than 0 but less than the number of requested bytes is not an error condition. The only error condition is a return value that is less than 0.

    Try this instead:

    void
    message(char *fmt, ...) {
        va_list ap;
        /* determine size */
        va_start(ap, fmt);
        int n = vsnprintf(NULL, 0, fmt, ap);
        va_end(ap);
        if (n < 0) {
            fputs("vsnprintf() failed", stderr);
            exit(EXIT_FAILURE);
        }
        size_t size = n + 1;
        /* construct */
        char *msg = malloc(size);
        if (msg == NULL) {
            perror("malloc() failed");
            exit(EXIT_FAILURE);
        }
        va_start(ap, fmt);
        n = vsnprintf(msg, size, fmt, ap);
        va_end(ap);
        if (n < 0) {
            fputs("vsnprintf() failed\n", stderr);
            free(msg);
            exit(EXIT_FAILURE);
        }
        /* send */
        char *curr = msg;
        --size; // don't sent null terminator!
        while (size > 0) {
            ssize_t nsent = send(sock, curr, size, 0);
            if (nsent < 0) {
                perror("send() failed");
                free(msg);
                exit(EXIT_FAILURE);
            }
            curr += nsent;
            size -= nsent;
        }
        free(msg);
    }
    

    That said, you really shouldn't be using a goto loop in main(), either. Use a while or do..while loop instead, eg:

    int
    main(void) {
        ...
        /* print response */
        char buffer[4096];
        int exitCode = 0;
    
        do {
            ssize_t nbyte = recv(sock, buffer, sizeof buffer, 0);
            if (nbyte < 0) { 
                perror("recv() failed");
                exitCode = 1;
            } else if (nbyte == 0) {
                fputs("connection closed by peer", stderr);
                exitCode = 1;
            } else {
                printf("%.*s", nbyte, buffer);
            }
        }
        while (exitCode == 0);
    
        close(sock);
        return exitCode;
    }