Home > Back-end >  Cannot serve png files and other binary files in hobby HTTP server
Cannot serve png files and other binary files in hobby HTTP server

Time:01-22

I am writing a HTTP server in C , and serving static files is mostly OK, however when reading .PNG files or other binary's, every method I have tried fails. My main problem is when I open up Dev tools, reading a example image would give a transferred size of 29.56kb, and a size of 29.50 kb for my current method. The sizes given also do not match up with what du-sh give, which is 32kb.

My first method was to push the contents of a file onto a string, and call a function to serve that. However, this would also server ~6kb if memory serves correctly.

My current method is to read the file using std::ifstream in binary mode. I am getting the size of the file using C 17's filesystem header and using std::filesystem::file_size. I read the contents into a buffer and then call a function to send the buffer contents 1 byte at a time

void WebServer::sendContents(std::string contents) {
    if (send(this->newFd, contents.c_str(), strlen(contents.c_str()), 0) == -1) {
        throw std::runtime_error("Server accept: "   std::string(strerror(errno)));
    }
}

void WebServer::sendFile(std::string path) {
    path = "./"   path;

    std::string fileCont; //File contents
    std::string mimeType; //Mime type of the file
    std::string contLength;
    std::string::size_type idx = path.rfind('.');

    if (idx != std::string::npos) mimeType = this->getMimeType(path.substr(idx   1));
    else mimeType = "text/html";

    std::filesystem::path reqPath = std::filesystem::path("./"   path).make_preferred();
    std::filesystem::path parentPath = std::filesystem::path("./");
    std::filesystem::path actualPath = std::filesystem::canonical(parentPath / reqPath);

    if (!this->isSubDir(actualPath, parentPath)) { this->sendRoute("404"); return; }


    std::ifstream ifs;
    ifs.open(actualPath, std::ios::binary);

    if (ifs.is_open()) {
        //Get the size of the static file being server
        std::filesystem::path staticPath{path};
        std::size_t length = std::filesystem::file_size(staticPath);


        char* buffer = new char[length]; 
        *buffer = { 0 }; //Initalize the buffer that will send the static file
        ifs.read(buffer, sizeof(char) * length); //Read the buffer

        std::string resp = "HTTP/1.0 200 OK\r\n"
            "Server: webserver-c\r\n"
            "Content-Length"   std::to_string(length)   "\r\n"
            "Content-type: "   mimeType   "\r\n\r\n";

        if (!ifs) std::cout << "Error! Only " << std::string(ifs.gcount()) << " could be read!" << std::endl; 

        this->sendContents(resp); //Send the headers
        for (size_t i=0; i < length; i  ) {
            std::string byte = std::string(1, buffer[i]);
            this->sendContents(byte);
        }


        delete buffer; //We do not need megs of memory stack up, that shit will grow quick
        buffer = nullptr;
    } else {
        this->sendContents("HTTP/1.1 500 Error\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"); return;
    }


    ifs.close();
}

It should be noted that this->newFd is a socket descriptor

It should also be noted that I have tried to take a look at this question here, however the same problem still occurs for me

CodePudding user response:

if (send(this->newFd, contents.c_str(), strlen(contents.c_str()), 0) == -1) {

There are two bugs for the price of one, here.

This is used to send the contents of the binary file. One byte at a time. sendContents gets used, apparently, to send one byte at a time, here. This is horribly inefficient, but it's not the bug. The first bug is as follows.

Your binary file has plenty of bytes that are 00.

In that case, contents will proudly contain this 00 byte, here. c_str() returns a pointer to it. strlen() then reaches the conclusion that it is receiving an empty string, for input, and make a grandiose announcement that the string contains 0 characters.

In the end, send's third parameter will be 0.

No bytes will get sent, at all, instead of the famous 00 byte.

The second bug will come into play once the inefficient algorithm gets fixed, and sendContents gets used to send more than one byte at a time.

send() holds a secret: this system call may return other values, other than -1 to indicate the failure. Such as the actual number of bytes that were sent. So, if send() was called to send, say, 100 bytes, it may decide so send only 30 bytes, return 30, and leaving you holding the bag with the remaining 70 unsent bytes.

This is actually, already, an existing bug in the shown code. sendContents() also gets used to send the entire resp string. Which is, eh, in the neighborhood of a 100 bytes. Give or take a dozen.

You are relying on this house of cards: of send() always doing its job complete job, in this particular case, not slacking off, and actually sending the entire HTTP/1.0 response string.

But, send() is a famous slacker, and you have no guarantees, whatsoever, that this will actually happen. And I have it on good authority that an upcoming Friday the 13th your send() will decide to slack off, all of a sudden.

So, to fix the shown code:

  1. Implement the appropriate logic to handle the return value from send().

  2. Do not use c_str(), followed by strlen(), because: A) it's broken, for strings containing binary data, B) this elaborate routine simply reinvents a wheel called size(). You will be happy to know that size() does exactly what it's name claims to be.

One other bug:

        char* buffer = new char[length]; 

It is possible for an exception to get thrown from the subsequent code. This memory get leaked, because delete does not get called.

C gurus know a weird trick: they rarely use new, but instead use containers, like std::vector, and they don't have to worry about leaking memory, because of that.

  • Related