Search code examples
chttppostmultipartform-databinaryfiles

How to send image or binary data through HTTP POST request in C


I'm trying to POST a binary file to a web server with a client program written in C (Windows). I'm pretty new to socket programming, so tried POST requests using multipart/form-data with plain text messages, and text-based files (.txt, .html, .xml). Those seem to work fine. But when trying to send a PNG file, I'm running into some problems.

The following is how I read the binary file

    FILE *file;
    char *fileName = "download.png";
    long int fileLength;
    
    //Open file, get its size
    file = fopen(fileName, "rb");
    fseek(file, 0, SEEK_END);
    fileLength = ftell(file);
    rewind(file);

    //Allocate buffer and read the file
    void *fileData = malloc(fileLength);
    memset(fileData, 0, fileLength);
    int n = fread(fileData, 1, fileLength, file);
    fclose(file);

I confirmed that all the bytes are getting read properly.

This is how I form my message header and body

    //Prepare message body and header
    message_body = malloc((int)1000);
    sprintf(message_body, "--myboundary\r\n"
                          "Content-Type: application/octet-stream\r\n"
                          "Content-Disposition: form-data; name=\"myFile\"; filename=\"%s\"\r\n\r\n"
                          "%s\r\n--myboundary--", fileName, fileData);

    printf("\nSize of message_body is %d and message_body is \n%s\n", strlen(message_body), message_body);

    message_header = malloc((int)1024);
    sprintf(message_header, "POST %s HTTP/1.1\r\n"
                            "Host: %s\r\n"
                            "Content-Type: multipart/form-data; boundary=myboundary\r\n"
                            "Content-Length: %d\r\n\r\n", path, host, strlen(message_body));

    printf("Size of message_header is %d and message_header is \n%s\n", strlen(message_header), message_header);

The connection and sending part also works fine as the request is received properly. But, the received png file is ill-formatted. The terminal prints out the following for fileData if I use %s in printf

ëPNG

I searched around and came to know that binary data doesn't behave like strings and thus printf/ sprintf/ strcat etc. cannot be used on them. As binary files have embedded null characters, %s won't print properly. It looks like that is the reason fileData only printed the PNG header.

Currently, I send two send() requests to server. One with the header and the other with body and footer combined. That was working for text-based files. To avoid using sprintf for binary data, I tried sending one request for header, one for binary data (body) & one for footer. That doesn't seem to work either.

Also, found that memcpy could be used to append binary data to normal string. That didn't work either. Here is how I tried that (Not sure whether my implementation is correct or not).

    sprintf(message_body, "--myboundary\r\n"
                          "Content-Disposition: form-data; name=\"text1\"\r\n\r\n"
                          "text default\r\n"
                          "--myboundary\r\n"
                          "Content-Type: application/octet-stream\r\n"
                          "Content-Disposition: form-data; name=\"myFile\"; filename=\"%s\"\r\n\r\n", fileName);

    char *message_footer = "\r\n--myboundary--";

    char *message = (char *)malloc(strlen(message_body) + strlen(message_footer) + fileLength);
    
    strcat(message, message_body);
    memcpy(message, fileData, fileLength);
    memcpy(message, message_footer, strlen(message_footer));

I'm stuck at how I could send my payload which requires appending of string (headers), binary data (payload), string (footer).

Any advice/ pointers/ reference links for sending the whole file would be appreciated. Thank You!


Solution

  • How to print binary data

    In your question, you stated you were having trouble printing binary data with printf, due to the binary data containing bytes with the value 0. Another problem (that you did not mention) is that binary data may contain non-printable characters.

    Binary data is commonly represented in one of the following ways:

    1. in hexadecimal representation
    2. in textual representation, replacing non-printable characters with placeholder characters
    3. both of the above

    I suggest that you create your own simple function for printing binary data, which implements option #3. You can use the function isprint to determine whether a character is printable, and if it isn't, you can place some placeholer character (such as 'X') instead.

    Here is a small program which does that:

    #include <stdio.h>
    #include <ctype.h>
    #include <string.h>
    
    void print_binary( char *data, size_t length )
    {
        for ( size_t i = 0; i < length; i += 16 )
        {
            int bytes_in_line = length - i >= 16 ? 16 : length - i;
    
            //print line in hexadecimal representation
            for ( int j = 0; j < 16; j++ )
            {
                if ( j < bytes_in_line )
                    printf( "%02X ", data[i+j] );
                else
                    printf( "   " );
            }
    
            //add spacing between hexadecimal and textual representation
            printf( "  " );
    
            //print line in textual representation
            for ( int j = 0; j < 16; j++ )
            {
                if ( j < bytes_in_line )
                {
                    if ( isprint( (unsigned char)data[i+j] ) )
                        putchar( data[i+j] );
                    else
                        putchar( 'X' );
                }
                else
                {
                    putchar( ' ' );
                }
            }
    
            putchar( '\n' );
        }
    }
    
    int main( void )
    {
        char *text = "This is a string with the unprintable backspace character \b.";
        print_binary( text, strlen( text ) );
    
        return 0;
    }
    

    The output of this program is the following:

    54 68 69 73 20 69 73 20 61 20 73 74 72 69 6E 67   This is a string
    20 77 69 74 68 20 74 68 65 20 75 6E 70 72 69 6E    with the unprin
    74 61 62 6C 65 20 62 61 63 6B 73 70 61 63 65 20   table backspace 
    63 68 61 72 61 63 74 65 72 20 08 2E               character X.    
    

    As you can see, the function print_binary printed the data in both hexadecimal representation and textual representation, 16 bytes per line, and it correctly replaced the non-printable backspace character with the placeholer 'X' character when printing the textual representation.

    Wrong printf conversion format specifier

    The line

    printf("\nSize of message_body is %d and message_body is \n%s\n", strlen(message_body), message_body);
    

    is wrong. The return type of strlen is size_t, not int. The correct printf conversion format specifier for size_t is %zu, not %d. Using the wrong format specifier causes undefined behavior, which means that it may work on some platforms, but not on others.

    Concatenating string with binary data

    The following lines are wrong:

        char *message = (char *)malloc(strlen(message_body) + strlen(message_footer) + fileLength);
        
        strcat(message, message_body);
        memcpy(message, fileData, fileLength);
        memcpy(message, message_footer, strlen(message_footer));
    

    The function strcat requires both function arguments to point to null-terminated strings. However, the first function argument is not guaranteed to be null-terminated. I suggest that you use strcpy instead of strcat.

    Also, in your question, you correctly stated that the file binary data should be appended to the string. However, that is not what the line

    memcpy(message, fileData, fileLength);
    

    is doing. It is instead overwriting the string.

    In order to append binary data to a string, you should only overwrite the terminating null character of the string, for example like this:

    memcpy( message + strlen(message), fileData, fileLength );