dlib C++ Library - server_http.cpp

    // Copyright (C) 2003  Davis E. King (davis@dlib.net)
    // License: Boost Software License   See LICENSE.txt for the full license.
    #ifndef DLIB_SERVER_HTTP_CPp_
    #define DLIB_SERVER_HTTP_CPp_

    #include "server_http.h"

    namespace dlib
    {

    // ----------------------------------------------------------------------------------------

        namespace http_impl
        {
            inline unsigned char to_hex( unsigned char x )
            {
                return x + (x > 9 ? ('A'-10) : '0');
            }

            const std::string urlencode( const std::string& s )
            {
                std::ostringstream os;

                for ( std::string::const_iterator ci = s.begin(); ci != s.end(); ++ci )
                {
                    if ( (*ci >= 'a' && *ci <= 'z') ||
                         (*ci >= 'A' && *ci <= 'Z') ||
                         (*ci >= '0' && *ci <= '9') )
                    { // allowed
                        os << *ci;
                    }
                    else if ( *ci == ' ')
                    {
                        os << '+';
                    }
                    else
                    {
                        os << '%' << to_hex(*ci >> 4) << to_hex(*ci % 16);
                    }
                }

                return os.str();
            }

            inline unsigned char from_hex (
                unsigned char ch
            )
            {
                if (ch <= '9' && ch >= '0')
                    ch -= '0';
                else if (ch <= 'f' && ch >= 'a')
                    ch -= 'a' - 10;
                else if (ch <= 'F' && ch >= 'A')
                    ch -= 'A' - 10;
                else
                    ch = 0;
                return ch;
            }

            const std::string urldecode (
                const std::string& str
            )
            {
                using namespace std;
                string result;
                string::size_type i;
                for (i = 0; i < str.size(); ++i)
                {
                    if (str[i] == '+')
                    {
                        result += ' ';
                    }
                    else if (str[i] == '%' && str.size() > i+2)
                    {
                        const unsigned char ch1 = from_hex(str[i+1]);
                        const unsigned char ch2 = from_hex(str[i+2]);
                        const unsigned char ch = (ch1 << 4) | ch2;
                        result += ch;
                        i += 2;
                    }
                    else
                    {
                        result += str[i];
                    }
                }
                return result;
            }

            void parse_url(
                std::string word,
                key_value_map& queries
            )
            /*!
                Parses the query string of a URL.  word should be the stuff that comes
                after the ? in the query URL.
            !*/
            {
                std::string::size_type pos;

                for (pos = 0; pos < word.size(); ++pos)
                {
                    if (word[pos] == '&')
                        word[pos] = ' ';
                }

                std::istringstream sin(word);
                sin >> word;
                while (sin)
                {
                    pos = word.find_first_of("=");
                    if (pos != std::string::npos)
                    {
                        std::string key = urldecode(word.substr(0,pos));
                        std::string value = urldecode(word.substr(pos+1));

                        queries[key] = value;
                    }
                    sin >> word;
                }
            }

            void read_with_limit(
                std::istream& in,
                std::string& buffer,
                int delim = '\n'
            )
            {
                using namespace std;
                const size_t max = 64*1024;
                buffer.clear();
                buffer.reserve(300);

                while (in.peek() != delim && in.peek() != '\n' && in.peek() != EOF && buffer.size() < max)
                {
                    buffer += (char)in.get();
                }

                // if we quit the loop because the data is longer than expected or we hit EOF
                if (in.peek() == EOF)
                    throw http_parse_error("HTTP field from client terminated incorrectly", 414);
                if (buffer.size() == max)
                    throw http_parse_error("HTTP field from client is too long", 414);

                in.get();
                // eat any remaining whitespace
                if (delim == ' ')
                {
                    while (in.peek() == ' ')
                        in.get();
                }
            }
        }

    // ----------------------------------------------------------------------------------------

        unsigned long parse_http_request (
            std::istream& in,
            incoming_things& incoming,
            unsigned long max_content_length
        )
        {
            using namespace std;
            using namespace http_impl;
            read_with_limit(in, incoming.request_type, ' ');

            // get the path
            read_with_limit(in, incoming.path, ' ');

            // Get the HTTP/1.1 - Ignore for now...
            read_with_limit(in, incoming.protocol);

            key_value_map_ci& incoming_headers = incoming.headers;
            key_value_map& cookies          = incoming.cookies;
            std::string& path               = incoming.path;
            std::string& content_type       = incoming.content_type;
            unsigned long content_length = 0;

            string line;
            read_with_limit(in, line);
            string first_part_of_header;
            string::size_type position_of_double_point;
            // now loop over all the incoming_headers
            while (line != "\r")
            {
                position_of_double_point = line.find_first_of(':');
                if ( position_of_double_point != string::npos )
                {
                    first_part_of_header = dlib::trim(line.substr(0, position_of_double_point));

                    if ( !incoming_headers[first_part_of_header].empty() )
                        incoming_headers[ first_part_of_header ] += " ";
                    incoming_headers[first_part_of_header] += dlib::trim(line.substr(position_of_double_point+1));

                    // look for Content-Type:
                    if (line.size() > 14 && strings_equal_ignore_case(line, "Content-Type:", 13))
                    {
                        content_type = line.substr(14);
                        if (content_type[content_type.size()-1] == '\r')
                            content_type.erase(content_type.size()-1);
                    }
                    // look for Content-Length:
                    else if (line.size() > 16 && strings_equal_ignore_case(line, "Content-Length:", 15))
                    {
                        istringstream sin(line.substr(16));
                        sin >> content_length;
                        if (!sin)
                        {
                            throw http_parse_error("Invalid Content-Length of '" + line.substr(16) + "'", 411);
                        }

                        if (content_length > max_content_length)
                        {
                            std::ostringstream sout;
                            sout << "Content-Length of post back is too large.  It must be less than " << max_content_length;
                            throw http_parse_error(sout.str(), 413);
                        }
                    }
                    // look for any cookies
                    else if (line.size() > 6 && strings_equal_ignore_case(line, "Cookie:", 7))
                    {
                        string::size_type pos = 6;
                        string key, value;
                        bool seen_key_start = false;
                        bool seen_equal_sign = false;
                        while (pos + 1 < line.size())
                        {
                            ++pos;
                            // ignore whitespace between cookies
                            if (!seen_key_start && line[pos] == ' ')
                                continue;

                            seen_key_start = true;
                            if (!seen_equal_sign)
                            {
                                if (line[pos] == '=')
                                {
                                    seen_equal_sign = true;
                                }
                                else
                                {
                                    key += line[pos];
                                }
                            }
                            else
                            {
                                if (line[pos] == ';')
                                {
                                    cookies[urldecode(key)] = urldecode(value);
                                    seen_equal_sign = false;
                                    seen_key_start = false;
                                    key.clear();
                                    value.clear();
                                }
                                else
                                {
                                    value += line[pos];
                                }
                            }
                        }
                        if (key.size() > 0)
                        {
                            cookies[urldecode(key)] = urldecode(value);
                            key.clear();
                            value.clear();
                        }
                    }
                } // no ':' in it!
                read_with_limit(in, line);
            } // while (line != "\r")

            // If there is data being posted back to us as a query string then
            // pick out the queries using parse_url.
            if ((strings_equal_ignore_case(incoming.request_type, "POST") ||
                 strings_equal_ignore_case(incoming.request_type, "PUT")) &&
                strings_equal_ignore_case(left_substr(content_type,";"), "application/x-www-form-urlencoded"))
            {
                if (content_length > 0)
                {
                    incoming.body.resize(content_length);
                    in.read(&incoming.body[0],content_length);
                }
                parse_url(incoming.body, incoming.queries);
            }

            string::size_type pos = path.find_first_of("?");
            if (pos != string::npos)
            {
                parse_url(path.substr(pos+1), incoming.queries);
            }

            if (!in)
                throw http_parse_error("Error parsing HTTP request", 500);

            return content_length;
        }

    // ----------------------------------------------------------------------------------------

        void read_body (
            std::istream& in,
            incoming_things& incoming
        )
        {
            // if the body hasn't already been loaded and there is data to load
            if (incoming.body.size() == 0 &&
                incoming.headers.count("Content-Length") != 0)
            {
                const unsigned long content_length = string_cast<unsigned long>(incoming.headers["Content-Length"]);

                incoming.body.resize(content_length);
                if (content_length > 0)
                {
                    in.read(&incoming.body[0],content_length);
                }
            }
        }

    // ----------------------------------------------------------------------------------------

        void write_http_response (
            std::ostream& out,
            outgoing_things outgoing,
            const std::string& result
        )
        {
            using namespace http_impl;
            key_value_map& new_cookies      = outgoing.cookies;
            key_value_map_ci& response_headers = outgoing.headers;

            // only send this header if the user hasn't told us to send another kind
            bool has_content_type = false, has_location = false;
            for(key_value_map_ci::const_iterator ci = response_headers.begin(); ci != response_headers.end(); ++ci )
            {
                if ( !has_content_type && strings_equal_ignore_case(ci->first , "content-type") )
                {
                    has_content_type = true;
                }
                else if ( !has_location && strings_equal_ignore_case(ci->first , "location") )
                {
                    has_location = true;
                }
            }

            if ( has_location )
            {
                outgoing.http_return = 302;
            }

            if ( !has_content_type )
            {
                response_headers["Content-Type"] = "text/html";
            }

            response_headers["Content-Length"] = cast_to_string(result.size());

            out << "HTTP/1.0 " << outgoing.http_return << " " << outgoing.http_return_status << "\r\n";

            // Set any new headers
            for(key_value_map_ci::const_iterator ci = response_headers.begin(); ci != response_headers.end(); ++ci )
            {
                out << ci->first << ": " << ci->second << "\r\n";
            }

            // set any cookies
            for(key_value_map::const_iterator ci = new_cookies.begin(); ci != new_cookies.end(); ++ci )
            {
                out << "Set-Cookie: " << urlencode(ci->first) << '=' << urlencode(ci->second) << "\r\n";
            }
            out << "\r\n" << result;
        }

    // ----------------------------------------------------------------------------------------

        void write_http_response (
            std::ostream& out,
            const http_parse_error& e
        )
        {
            outgoing_things outgoing;
            outgoing.http_return = e.http_error_code;
            outgoing.http_return_status = e.what();
            write_http_response(out, outgoing, std::string("Error processing request: ") + e.what());
        }

    // ----------------------------------------------------------------------------------------

        void write_http_response (
            std::ostream& out,
            const std::exception& e
        )
        {
            outgoing_things outgoing;
            outgoing.http_return = 500;
            outgoing.http_return_status = e.what();
            write_http_response(out, outgoing, std::string("Error processing request: ") + e.what());
        }

    // ----------------------------------------------------------------------------------------

        const logger server_http::dlog("dlib.server_http");

    // ----------------------------------------------------------------------------------------

    }

    #endif // DLIB_SERVER_HTTP_CPp_