From 252a4788b5938e265a6aaf9d61dcb84cba574967 Mon Sep 17 00:00:00 2001 From: Fred Nicolson Date: Mon, 19 Nov 2018 15:23:10 +0000 Subject: [PATCH] Added ability to set/get HTTP response/request version + Additional tests --- include/frnetlib/Http.h | 29 ++++++++++++++++++---- src/Http.cpp | 15 ++++++++++- src/HttpRequest.cpp | 49 ++++++++++++++++++++---------------- src/HttpResponse.cpp | 8 +++++- src/SocketSelector.cpp | 5 ++-- tests/HttpRequestTest.cpp | 32 +++++++++++++++++++++++- tests/HttpResponseTest.cpp | 51 +++++++++++++++++++++++++++++++++++++- 7 files changed, 156 insertions(+), 33 deletions(-) diff --git a/include/frnetlib/Http.h b/include/frnetlib/Http.h index 0a518b7..cb864a1 100644 --- a/include/frnetlib/Http.h +++ b/include/frnetlib/Http.h @@ -15,6 +15,12 @@ namespace fr class Http : public Sendable { public: + enum RequestVersion + { + V1 = 1, // HTTP/1.0 + V1_1 = 2, // HTTP/1.1 + VersionCount = 3 + }; enum RequestType { Get = 0, @@ -224,6 +230,20 @@ namespace fr */ const std::string &get_body() const; + /*! + * Sets the HTTP version + * + * @param version The HTTP version to use + */ + void set_version(RequestVersion version); + + /*! + * Gets the HTTP version + * + * @return The current HTTP version + */ + RequestVersion get_version() const; + /*! * URL Encodes a given string * @@ -263,7 +283,7 @@ namespace fr * @param str The string to convert * @return The converted RequestType. Unknown on failure. Or Partial if str is part of a request type. */ - static RequestType string_to_request_type(const std::string &str) ; + static RequestType string_to_request_type(const std::string &str); protected: /*! @@ -298,7 +318,7 @@ namespace fr * @param socket The socket to send through * @return Status indicating if the send succeeded or not. */ - virtual Socket::Status send(Socket *socket) const override; + Socket::Status send(Socket *socket) const override; /*! * Overrideable receive, to allow @@ -308,7 +328,7 @@ namespace fr * @param socket The socket to send through * @return Status indicating if the send succeeded or not. */ - virtual Socket::Status receive(Socket *socket) override; + Socket::Status receive(Socket *socket) override; //Other request info std::unordered_map header_data; @@ -318,8 +338,7 @@ namespace fr RequestType request_type; std::string uri; RequestStatus status; - - private: + RequestVersion version; }; } diff --git a/src/Http.cpp b/src/Http.cpp index 6790ccd..bb30059 100644 --- a/src/Http.cpp +++ b/src/Http.cpp @@ -6,6 +6,8 @@ #include #include #include +#include + #include "frnetlib/Http.h" namespace fr @@ -13,7 +15,8 @@ namespace fr Http::Http() : request_type(Unknown), uri("/"), - status(Ok) + status(Ok), + version(V1_1) { } @@ -1016,4 +1019,14 @@ namespace fr return state; } + + void Http::set_version(Http::RequestVersion v) + { + version = v; + } + + Http::RequestVersion Http::get_version() const + { + return version; + } } \ No newline at end of file diff --git a/src/HttpRequest.cpp b/src/HttpRequest.cpp index 0e0a56a..94e307d 100644 --- a/src/HttpRequest.cpp +++ b/src/HttpRequest.cpp @@ -119,7 +119,9 @@ namespace fr request += "&"; } } - request += " HTTP/1.1\r\n"; + + static_assert(RequestVersion::VersionCount == 3, "Update me"); + request += (version == RequestVersion::V1) ? " HTTP/1.0\r\n" : " HTTP/1.1\r\n"; //Add the headers to the request for(const auto &header : header_data) @@ -197,27 +199,32 @@ namespace fr void HttpRequest::parse_header_uri(const std::string &str) { auto uri_begin = str.find('/'); - auto uri_end = str.find("HTTP") - 1; - if(uri_begin != std::string::npos) + auto uri_end = str.find("HTTP"); + if(uri_begin == std::string::npos || uri_end == std::string::npos) { - //Parse GET variables - auto get_begin = str.find('?'); - if(get_begin != std::string::npos) - { - auto get_vars = parse_argument_list(str.substr(get_begin, uri_end - get_begin)); - for(auto &c : get_vars) - { - std::transform(c.first.begin(), c.first.end(), c.first.begin(), ::tolower); - get_data.emplace(std::move(c.first), std::move(c.second)); - } - set_uri(str.substr(uri_begin, get_begin - uri_begin)); - } - else - { - set_uri(str.substr(uri_begin, uri_end - uri_begin)); - } - return; + throw std::invalid_argument("No URI found in: " + str); } - throw std::invalid_argument("No URI found in: " + str); + --uri_end; + + //Parse GET variables + auto get_begin = str.find('?'); + if(get_begin != std::string::npos) + { + auto get_vars = parse_argument_list(str.substr(get_begin, uri_end - get_begin)); + for(auto &c : get_vars) + { + std::transform(c.first.begin(), c.first.end(), c.first.begin(), ::tolower); + get_data.emplace(std::move(c.first), std::move(c.second)); + } + set_uri(str.substr(uri_begin, get_begin - uri_begin)); + } + else + { + set_uri(str.substr(uri_begin, uri_end - uri_begin)); + } + + //Parse HTTP version. HTTP/1.0 or HTTP/1.1 + static_assert(RequestVersion::VersionCount == 3, "Update me"); + version = str.compare(uri_end + 1, 8, "HTTP/1.0") == 0 ? RequestVersion::V1 : RequestVersion::V1_1; } } \ No newline at end of file diff --git a/src/HttpResponse.cpp b/src/HttpResponse.cpp index 35e5f04..8651874 100644 --- a/src/HttpResponse.cpp +++ b/src/HttpResponse.cpp @@ -62,7 +62,9 @@ namespace fr std::string HttpResponse::construct(const std::string &host) const { //Add HTTP header - std::string response = "HTTP/1.1 " + std::to_string(status) + " \r\n"; + + static_assert(RequestVersion::VersionCount == 3, "Update me"); + std::string response = ((version == RequestVersion::V1) ? "HTTP/1.0 " : "HTTP/1.1 ") + std::to_string(status) + " \r\n"; //Add the headers to the response for(const auto &header : header_data) @@ -103,6 +105,10 @@ namespace fr return false; auto end_pos = header_lines[0].find(' ', status_begin + 1); status = (RequestStatus)std::stoi(header_lines[0].substr(status_begin, end_pos - status_begin)); + + //Get HTTP version + static_assert(RequestVersion::VersionCount == 3, "Update me"); + version = header_lines[0].compare(0, status_begin, "HTTP/1.0") == 0 ? RequestVersion::V1 : RequestVersion::V1_1; line++; //Read in headers diff --git a/src/SocketSelector.cpp b/src/SocketSelector.cpp index 6cab833..7a4f4fc 100644 --- a/src/SocketSelector.cpp +++ b/src/SocketSelector.cpp @@ -12,7 +12,7 @@ namespace fr #ifndef _WIN32 SocketSelector::SocketSelector() - : epoll_fd(-1) + : epoll_fd(-1) { epoll_fd = epoll_create1(O_CLOEXEC); if(epoll_fd < 0) @@ -33,8 +33,7 @@ namespace fr throw std::logic_error("Can't add disconnected socket"); } - auto add_iter = added_sockets.emplace(socket->get_socket_descriptor(), - Opaque(socket->get_socket_descriptor(), socket, opaque)); + auto add_iter = added_sockets.emplace(socket->get_socket_descriptor(), Opaque(socket->get_socket_descriptor(), socket, opaque)); if(!add_iter.second) { throw std::logic_error("Can't add duplicate socket: " + std::to_string(socket->get_socket_descriptor())); diff --git a/tests/HttpRequestTest.cpp b/tests/HttpRequestTest.cpp index 45f7b0b..38e834c 100644 --- a/tests/HttpRequestTest.cpp +++ b/tests/HttpRequestTest.cpp @@ -31,6 +31,7 @@ TEST(HttpRequestTest, get_request_parse) ASSERT_EQ(request.header_exists("non-existant"), false); //Check that headers are intact + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); ASSERT_EQ(request.header("Host"), "frednicolson.co.uk"); ASSERT_EQ(request.header("Content-Type"), "application/x-www-form-urlencoded"); ASSERT_EQ(request.header("My-Other-Header"), "header2"); @@ -62,6 +63,7 @@ TEST(HttpRequestTest, post_request_parse) ASSERT_EQ(request.get_type(), fr::Http::Post); //Test that URI is intact + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); ASSERT_EQ(request.get_uri(), "/index.html"); //Parse code is the same for GET, so skip header checks. Test if POST data exists. @@ -77,7 +79,9 @@ TEST(HttpRequestTest, post_request_parse) TEST(HttpRequestTest, request_type_parse) { const std::string get_request = "GET / HTTP/1.1\r\n\r\n"; + const std::string get_request_v2 = "GET / HTTP/1.0\r\n\r\n"; const std::string post_request = "POST / HTTP/1.1\r\n\r\n"; + const std::string post_request_v2 = "POST / HTTP/1.0\r\n\r\n"; const std::string put_request = "PUT / HTTP/1.1\r\n\r\n"; const std::string delete_request = "DELETE / HTTP/1.1\r\n\r\n"; const std::string patch_request = "PATCH / HTTP/1.1\r\n\r\n"; @@ -87,30 +91,47 @@ TEST(HttpRequestTest, request_type_parse) fr::HttpRequest request; ASSERT_EQ(request.parse(get_request.c_str(), get_request.size()), fr::Socket::Success); ASSERT_EQ(request.get_type(), fr::Http::Get); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); request = {}; ASSERT_EQ(request.parse(post_request.c_str(), post_request.size()), fr::Socket::Success); ASSERT_EQ(request.get_type(), fr::Http::Post); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); + request = {}; + + ASSERT_EQ(request.parse(get_request_v2.c_str(), get_request_v2.size()), fr::Socket::Success); + ASSERT_EQ(request.get_type(), fr::Http::Get); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1); + request = {}; + + ASSERT_EQ(request.parse(post_request_v2.c_str(), post_request_v2.size()), fr::Socket::Success); + ASSERT_EQ(request.get_type(), fr::Http::Post); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1); request = {}; ASSERT_EQ(request.parse(put_request.c_str(), put_request.size()), fr::Socket::Success); ASSERT_EQ(request.get_type(), fr::Http::Put); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); request = {}; ASSERT_EQ(request.parse(delete_request.c_str(), delete_request.size()), fr::Socket::Success); ASSERT_EQ(request.get_type(), fr::Http::Delete); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); request = {}; ASSERT_EQ(request.parse(patch_request.c_str(), patch_request.size()), fr::Socket::Success); ASSERT_EQ(request.get_type(), fr::Http::Patch); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); request = {}; ASSERT_EQ(request.parse(invalid_request.c_str(), invalid_request.size()), fr::Socket::ParseError); ASSERT_EQ(request.get_type(), fr::Http::Unknown); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); request = {}; ASSERT_EQ(request.parse(invalid_request2.c_str(), invalid_request2.size()), fr::Socket::ParseError); ASSERT_EQ(request.get_type(), fr::Http::Unknown); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); request = {}; } @@ -119,6 +140,7 @@ TEST(HttpRequestTest, get_request_construction) //Create a request fr::HttpRequest request; ASSERT_EQ(request.get_uri(), "/"); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); request.header("MyHeader") = "header1"; request.header("MyOther-Header") = "header2"; @@ -126,7 +148,7 @@ TEST(HttpRequestTest, get_request_construction) request.get("my_other_get") = "var2"; request.set_uri("heyo/bobby"); request.set_type(fr::Http::Get); - const std::string constructed_request = request.construct("frednicolson.co.uk"); + std::string constructed_request = request.construct("frednicolson.co.uk"); //Parse it and check that everything's correct request = {}; @@ -137,6 +159,14 @@ TEST(HttpRequestTest, get_request_construction) ASSERT_EQ(request.get("my_other_get"), "var2"); ASSERT_EQ(request.get_uri(), "/heyo/bobby"); ASSERT_EQ(request.get_type(), fr::Http::Get); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1_1); + + //Quick v1 test + request.set_version(fr::Http::RequestVersion::V1); + constructed_request = request.construct("frednicolson.co.uk"); + request = {}; + request.parse(constructed_request.c_str(), constructed_request.size()); + ASSERT_EQ(request.get_version(), fr::Http::RequestVersion::V1); } TEST(HttpRequestTest, post_request_construction) diff --git a/tests/HttpResponseTest.cpp b/tests/HttpResponseTest.cpp index 9e67213..136261e 100644 --- a/tests/HttpResponseTest.cpp +++ b/tests/HttpResponseTest.cpp @@ -5,7 +5,26 @@ #include #include -TEST(HttpResponseTest, response_parse) +TEST(HttpResponseTest, response_parse_v1) +{ + const std::string raw_response = + "HTTP/1.0 301 Moved Permanently\n" + "Server: nginx/1.10.2\n" + "Date: Mon, 25 Sep 2017 13:51:56 GMT\n" + "Content-Type: text/html\n" + "Content-Length: 0\n" + "Connection: keep-alive\n" + "Location: https://frednicolson.co.uk/\n\n"; + + //Parse response + fr::HttpResponse test; + ASSERT_EQ(test.parse(raw_response.c_str(), raw_response.size()), fr::Socket::Success); + + //Verify it + ASSERT_EQ(test.get_version(), fr::Http::RequestVersion::V1); +} + +TEST(HttpResponseTest, response_parse_v2) { const std::string raw_response = "HTTP/1.1 301 Moved Permanently\n" @@ -38,6 +57,7 @@ TEST(HttpResponseTest, response_parse) ASSERT_EQ(test.parse(raw_response.c_str(), raw_response.size()), fr::Socket::Success); //Verify it + ASSERT_EQ(test.get_version(), fr::Http::RequestVersion::V1_1); ASSERT_EQ(test.get_status(), fr::Http::MovedPermanently); ASSERT_EQ(test.header("Content-length"), "177"); ASSERT_EQ(test.get_body(), response_body); @@ -116,4 +136,33 @@ TEST(HttpResponseTest, body_length_test) buff += std::string(MAX_HTTP_BODY_SIZE + 1, '\0'); fr::HttpResponse response; ASSERT_EQ(response.parse(buff.c_str(), buff.size()), fr::Socket::HttpBodyTooBig); +} + +TEST(HttpResponseTest, HttpResponseConstruction) +{ + { + fr::HttpResponse response; + response.set_status(fr::Http::ImATeapot); + response.header("bob") = "trob"; + response.set_body("lob"); + auto constructed = response.construct("frednicolson.co.uk"); + response = {}; + ASSERT_EQ(response.parse(constructed.c_str(), constructed.size()), fr::Socket::Status::Success); + + ASSERT_EQ(response.get_version(), fr::Http::RequestVersion::V1_1); + ASSERT_EQ(response.get_status(), fr::Http::RequestStatus::ImATeapot); + ASSERT_EQ(response.get_body(), "lob"); + ASSERT_EQ(response.header("bob"), "trob"); + } + + { + fr::HttpResponse response; + response.set_version(fr::Http::RequestVersion::V1); + auto constructed = response.construct("frednicolson.co.uk"); + response = {}; + response.parse(constructed.c_str(), constructed.size()); + + ASSERT_EQ(response.get_version(), fr::Http::RequestVersion::V1); + } + } \ No newline at end of file