Added more tests. Tweaks.
Receiving an http request/response will now return errors like HttpHeaderTooBig, instead of the type being set to that. Added fr::Socket::status_to_string for converting status values into English strings.
This commit is contained in:
parent
abb3655739
commit
c8e03f2df8
@ -27,9 +27,6 @@ namespace fr
|
||||
};
|
||||
enum RequestStatus
|
||||
{
|
||||
ParseError = 0,
|
||||
HttpHeaderTooBig = 1,
|
||||
HttpBodyTooBig = 2,
|
||||
Continue = 100,
|
||||
SwitchingProtocols = 101,
|
||||
Ok = 200,
|
||||
@ -100,9 +97,9 @@ namespace fr
|
||||
*
|
||||
* @param data The request/response to parse
|
||||
* @param datasz The length of data in bytes
|
||||
* @return True if more data is needed, false if finished.
|
||||
* @return NotEnoughData if parse needs to be called again. Success on success, other on error.
|
||||
*/
|
||||
virtual bool parse(const char *data, size_t datasz)=0;
|
||||
virtual fr::Socket::Status parse(const char *data, size_t datasz)=0;
|
||||
|
||||
/*!
|
||||
* Constructs a HTTP request/response to send.
|
||||
|
||||
@ -30,7 +30,7 @@ namespace fr
|
||||
* @param datasz The length of data in bytes
|
||||
* @return True if more data is needed, false if finished.
|
||||
*/
|
||||
bool parse(const char *data, size_t datasz) override;
|
||||
fr::Socket::Status parse(const char *data, size_t datasz) override;
|
||||
|
||||
/*!
|
||||
* Constructs a Http Request, ready to send.
|
||||
@ -44,6 +44,7 @@ namespace fr
|
||||
* Parses the request header.
|
||||
*
|
||||
* @param header_end_pos The position in 'body' of the end of the header
|
||||
* @return True on success, false on failure
|
||||
*/
|
||||
bool parse_header(int64_t header_end_pos);
|
||||
|
||||
|
||||
@ -24,13 +24,14 @@ namespace fr
|
||||
virtual ~HttpResponse() = default;
|
||||
|
||||
/*!
|
||||
* Parse a HTTP response.
|
||||
* Parse a raw request or response from a string
|
||||
* into the object.
|
||||
*
|
||||
* @param data The HTTP response to parse
|
||||
* @param data The request/response to parse
|
||||
* @param datasz The length of data in bytes
|
||||
* @return True if more data is needed, false if finished.
|
||||
* @return NotEnoughData if parse needs to be called again. Success on success, other on error.
|
||||
*/
|
||||
bool parse(const char *data, size_t datasz) override;
|
||||
fr::Socket::Status parse(const char *data, size_t datasz) override;
|
||||
|
||||
/*!
|
||||
* Constructs a HttpResponse, ready to send.
|
||||
|
||||
@ -84,6 +84,8 @@ namespace fr
|
||||
*/
|
||||
int32_t get_socket_descriptor() const override
|
||||
{
|
||||
if(!ssl_socket_descriptor)
|
||||
return -1;
|
||||
return ssl_socket_descriptor->fd;
|
||||
}
|
||||
|
||||
@ -104,7 +106,7 @@ namespace fr
|
||||
*/
|
||||
inline bool connected() const final
|
||||
{
|
||||
return ssl_socket_descriptor->fd > -1;
|
||||
return ssl_socket_descriptor && ssl_socket_descriptor->fd > -1;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
||||
@ -19,7 +19,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) = 0;
|
||||
virtual Socket::Status send(Socket *socket) = 0; //TODO: RETURN PROPER VALUE FROM HTTP PARSE
|
||||
|
||||
/*!
|
||||
* Overrideable receive, to allow
|
||||
|
||||
@ -29,7 +29,11 @@ namespace fr
|
||||
HandshakeFailed = 8,
|
||||
VerificationFailed = 9,
|
||||
MaxPacketSizeExceeded = 10,
|
||||
NotEnoughData = 11
|
||||
NotEnoughData = 11,
|
||||
ParseError = 12,
|
||||
HttpHeaderTooBig = 13,
|
||||
HttpBodyTooBig = 14,
|
||||
//Remember to update status_to_string if more are added
|
||||
};
|
||||
|
||||
enum IP
|
||||
@ -215,6 +219,16 @@ namespace fr
|
||||
{
|
||||
return max_receive_size;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Converts an fr::Socket::Status value to a printable string
|
||||
*
|
||||
* Throws an std::logic_error if status is out of range.
|
||||
*
|
||||
* @param status Status value to convert
|
||||
* @return A string form version
|
||||
*/
|
||||
static const std::string &status_to_string(fr::Socket::Status status);
|
||||
protected:
|
||||
|
||||
/*!
|
||||
|
||||
@ -953,6 +953,7 @@ namespace fr
|
||||
{
|
||||
char recv_buffer[RECV_CHUNK_SIZE];
|
||||
size_t received = 0;
|
||||
fr::Socket::Status state;
|
||||
do
|
||||
{
|
||||
//Receive the request
|
||||
@ -961,8 +962,9 @@ namespace fr
|
||||
return status;
|
||||
|
||||
//Parse it
|
||||
} while(parse(recv_buffer, received));
|
||||
state = parse(recv_buffer, received);
|
||||
} while(state == fr::Socket::NotEnoughData);
|
||||
|
||||
return Socket::Success;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@ -15,20 +15,13 @@ namespace fr
|
||||
|
||||
}
|
||||
|
||||
bool HttpRequest::parse(const char *request, size_t requestsz)
|
||||
fr::Socket::Status HttpRequest::parse(const char *request, size_t requestsz)
|
||||
{
|
||||
body += std::string(request, requestsz);
|
||||
|
||||
//Ensure that the whole header has been parsed first
|
||||
if(!header_ended)
|
||||
{
|
||||
//Ensure that the header doesn't exceed max length
|
||||
if(body.size() > MAX_HTTP_HEADER_SIZE)
|
||||
{
|
||||
status = HttpHeaderTooBig;
|
||||
return false; //End parse
|
||||
}
|
||||
|
||||
//Check to see if this request data contains the end of the header
|
||||
uint16_t header_end_size = 4;
|
||||
auto header_end = body.find("\r\n\r\n");
|
||||
@ -39,13 +32,19 @@ namespace fr
|
||||
}
|
||||
header_ended = header_end != std::string::npos;
|
||||
|
||||
//If the header end has not been found, return true, indicating that we need more data.
|
||||
//Ensure that the header doesn't exceed max length
|
||||
if(!header_ended && body.size() > MAX_HTTP_HEADER_SIZE || header_ended && header_end > MAX_HTTP_HEADER_SIZE)
|
||||
{
|
||||
return fr::Socket::HttpHeaderTooBig;
|
||||
}
|
||||
|
||||
//If the header end has not been found, ask for more data.
|
||||
if(!header_ended)
|
||||
return true;
|
||||
return fr::Socket::NotEnoughData;
|
||||
|
||||
//Else parse it
|
||||
if(!parse_header(header_end))
|
||||
return false;
|
||||
return fr::Socket::ParseError;
|
||||
|
||||
//Leave things after the header intact
|
||||
body.erase(0, header_end + header_end_size);
|
||||
@ -54,8 +53,7 @@ namespace fr
|
||||
//Ensure that body doesn't exceed maximum length
|
||||
if(body.size() > MAX_HTTP_BODY_SIZE)
|
||||
{
|
||||
status = HttpBodyTooBig;
|
||||
return false; //End parse
|
||||
return fr::Socket::HttpBodyTooBig;
|
||||
}
|
||||
|
||||
//If we've got the whole request, parse the POST if it exists
|
||||
@ -63,10 +61,10 @@ namespace fr
|
||||
{
|
||||
if(request_type == RequestType::Post)
|
||||
parse_post_body();
|
||||
return false;
|
||||
return fr::Socket::Success;
|
||||
}
|
||||
|
||||
return true;
|
||||
return fr::Socket::NotEnoughData;
|
||||
}
|
||||
|
||||
bool HttpRequest::parse_header(int64_t header_end_pos)
|
||||
@ -77,7 +75,7 @@ namespace fr
|
||||
size_t line = 0;
|
||||
std::vector<std::string> header_lines = split_string(body.substr(0, (unsigned long)header_end_pos));
|
||||
if(header_lines.empty())
|
||||
return false;
|
||||
return true;
|
||||
|
||||
//Parse request type & uri
|
||||
parse_header_type(header_lines[line]);
|
||||
|
||||
@ -7,20 +7,13 @@
|
||||
|
||||
namespace fr
|
||||
{
|
||||
bool HttpResponse::parse(const char *response_data, size_t datasz)
|
||||
fr::Socket::Status HttpResponse::parse(const char *response_data, size_t datasz)
|
||||
{
|
||||
body += std::string(response_data, datasz);
|
||||
|
||||
//Ensure that the whole header has been parsed first
|
||||
if(!header_ended)
|
||||
{
|
||||
//Ensure that the header doesn't exceed max length
|
||||
if(body.size() > MAX_HTTP_HEADER_SIZE)
|
||||
{
|
||||
status = HttpHeaderTooBig;
|
||||
return false; //End parse
|
||||
}
|
||||
|
||||
//Check to see if this request data contains the end of the header
|
||||
uint16_t header_end_size = 4;
|
||||
auto header_end = body.find("\r\n\r\n");
|
||||
@ -31,13 +24,19 @@ namespace fr
|
||||
}
|
||||
header_ended = header_end != std::string::npos;
|
||||
|
||||
//If the header end has not been found, return true, indicating that we need more data.
|
||||
//Ensure that the header doesn't exceed max length
|
||||
if(!header_ended && body.size() > MAX_HTTP_HEADER_SIZE || header_ended && header_end > MAX_HTTP_HEADER_SIZE)
|
||||
{
|
||||
return fr::Socket::HttpHeaderTooBig;
|
||||
}
|
||||
|
||||
//If the header end has not been found, ask for more data.
|
||||
if(!header_ended)
|
||||
return true;
|
||||
return fr::Socket::NotEnoughData;
|
||||
|
||||
//Else parse it
|
||||
parse_header(header_end);
|
||||
body.clear();
|
||||
if(!parse_header(header_end))
|
||||
return fr::Socket::ParseError;
|
||||
|
||||
//Leave things after the header intact
|
||||
body.erase(0, header_end + header_end_size);
|
||||
@ -46,14 +45,15 @@ namespace fr
|
||||
//Ensure that body doesn't exceed maximum length
|
||||
if(body.size() > MAX_HTTP_BODY_SIZE)
|
||||
{
|
||||
status = HttpBodyTooBig;
|
||||
return false; //End parse
|
||||
return fr::Socket::HttpBodyTooBig;
|
||||
}
|
||||
|
||||
//Cut off any data if it exceeds content length
|
||||
if(body.size() > content_length)
|
||||
body.resize(content_length);
|
||||
return body.size() < content_length;
|
||||
else if(body.size() < content_length)
|
||||
return fr::Socket::NotEnoughData;
|
||||
return fr::Socket::Success;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <mutex>
|
||||
#include <csignal>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include "frnetlib/NetworkEncoding.h"
|
||||
#include "frnetlib/Socket.h"
|
||||
#include "frnetlib/Sendable.h"
|
||||
@ -102,4 +103,28 @@ namespace fr
|
||||
{
|
||||
max_receive_size = sz;
|
||||
}
|
||||
|
||||
const std::string &Socket::status_to_string(fr::Socket::Status status)
|
||||
{
|
||||
static std::vector<std::string> map = {
|
||||
"Unknown",
|
||||
"Success",
|
||||
"Listen Failed",
|
||||
"Bind Failed",
|
||||
"Disconnected",
|
||||
"Error",
|
||||
"Would Block",
|
||||
"Connection Failed",
|
||||
"Handshake Failed",
|
||||
"Verification Failed",
|
||||
"Max packet size exceeded",
|
||||
"Not enough data",
|
||||
"Parse error",
|
||||
"HTTP header too big",
|
||||
"HTTP body too big"};
|
||||
|
||||
if(status < 0 || status > map.size())
|
||||
throw std::logic_error("Socket::status_to_string(): Invalid status value " + std::to_string(status));
|
||||
return map[status];
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ TEST(HttpRequestTest, get_request_parse)
|
||||
|
||||
//Parse it
|
||||
fr::HttpRequest request;
|
||||
ASSERT_EQ(request.parse(raw_request.c_str(), raw_request.size()), false);
|
||||
ASSERT_EQ(request.parse(raw_request.c_str(), raw_request.size()), fr::Socket::Success);
|
||||
|
||||
//Check that the request type is intact
|
||||
ASSERT_EQ(request.get_type(), fr::Http::Get);
|
||||
@ -56,7 +56,7 @@ TEST(HttpRequestTest, post_request_parse)
|
||||
|
||||
//Parse it
|
||||
fr::HttpRequest request;
|
||||
ASSERT_EQ(request.parse(raw_request.c_str(), raw_request.size()), false);
|
||||
ASSERT_EQ(request.parse(raw_request.c_str(), raw_request.size()), fr::Socket::Success);
|
||||
|
||||
//Check that the request type is intact
|
||||
ASSERT_EQ(request.get_type(), fr::Http::Post);
|
||||
@ -157,4 +157,32 @@ TEST(HttpRequestTest, post_request_construction)
|
||||
ASSERT_EQ(request.post("my_post"), "post_data");
|
||||
ASSERT_EQ(request.post("some_post"), "more_post");
|
||||
ASSERT_EQ(request.get_type(), fr::Http::Post);
|
||||
}
|
||||
|
||||
TEST(HttpRequestTest, partial_parse)
|
||||
{
|
||||
//The test request to parse
|
||||
const std::string raw_request1 =
|
||||
"GET /index.html?var=bob&other=trob HTTP/1.1\n"
|
||||
"Host: frednicolson.co.uk\r\n"
|
||||
"Content-Type: application/x-www-form-urlencoded\r\n"
|
||||
"My-Header: ";
|
||||
|
||||
const std::string raw_request2 =
|
||||
" header1\n"
|
||||
"My-Other-Header:header2\r\n"
|
||||
"Cache-Control: no-cache\r\n\r\n";
|
||||
|
||||
|
||||
//Parse part 1
|
||||
fr::HttpRequest request;
|
||||
ASSERT_EQ(request.parse(raw_request1.c_str(), raw_request1.size()), fr::Socket::NotEnoughData);
|
||||
|
||||
//Parse part 2
|
||||
ASSERT_EQ(request.parse(raw_request2.c_str(), raw_request2.size()), fr::Socket::Success);
|
||||
|
||||
//Verify it
|
||||
ASSERT_EQ(request.get_type(), fr::Http::Get);
|
||||
ASSERT_EQ(request.header("content-type"), "application/x-www-form-urlencoded");
|
||||
ASSERT_EQ(request.header("Cache-Control"), "no-cache");
|
||||
}
|
||||
118
tests/HttpResponseTest.cpp
Normal file
118
tests/HttpResponseTest.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
//
|
||||
// Created by fred.nicolson on 25/09/17.
|
||||
//
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <frnetlib/HttpResponse.h>
|
||||
|
||||
TEST(HttpResponseTest, response_parse)
|
||||
{
|
||||
const std::string raw_response =
|
||||
"HTTP/1.1 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: 177\n"
|
||||
"Connection: keep-alive\n"
|
||||
"Location: https://frednicolson.co.uk/\n"
|
||||
"\n"
|
||||
"<html>\n"
|
||||
"<head><title>301 Moved Permanently</title></head>\n"
|
||||
"<body bgcolor=\"white\">\n"
|
||||
"<center><h1>301 Moved Permanently</h1></center>\n"
|
||||
"<hr><center>nginx/1.10.2</center>\n"
|
||||
"</body>\n"
|
||||
"</html>";
|
||||
|
||||
const std::string response_body =
|
||||
"<html>\n"
|
||||
"<head><title>301 Moved Permanently</title></head>\n"
|
||||
"<body bgcolor=\"white\">\n"
|
||||
"<center><h1>301 Moved Permanently</h1></center>\n"
|
||||
"<hr><center>nginx/1.10.2</center>\n"
|
||||
"</body>\n"
|
||||
"</html>";
|
||||
|
||||
//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_status(), fr::Http::MovedPermanently);
|
||||
ASSERT_EQ(test.header("Content-length"), "177");
|
||||
ASSERT_EQ(test.get_body(), response_body);
|
||||
}
|
||||
|
||||
TEST(HttpResponseTest, response_partial_parse)
|
||||
{
|
||||
const std::string raw_response1 =
|
||||
"HTTP/1.1 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: 177\n"
|
||||
"Connection: keep-alive\n";
|
||||
|
||||
std::string raw_response2 =
|
||||
"Location: https://frednicolson.co.uk/\n"
|
||||
"\n"
|
||||
"<html>\n"
|
||||
"<head><title>301 Moved Permanently</title></head>\n"
|
||||
"<body bgcolor=\"white\">\n";
|
||||
|
||||
std::string raw_response3 =
|
||||
"<center><h1>301 Moved Permanently</h1></center>\n"
|
||||
"<hr><center>nginx/1.10.2</center>\n"
|
||||
"</body>\n"
|
||||
"</html>";
|
||||
|
||||
const std::string response_body =
|
||||
"<html>\n"
|
||||
"<head><title>301 Moved Permanently</title></head>\n"
|
||||
"<body bgcolor=\"white\">\n"
|
||||
"<center><h1>301 Moved Permanently</h1></center>\n"
|
||||
"<hr><center>nginx/1.10.2</center>\n"
|
||||
"</body>\n"
|
||||
"</html>";
|
||||
|
||||
//Parse response
|
||||
fr::HttpResponse test;
|
||||
ASSERT_EQ(test.parse(raw_response1.c_str(), raw_response1.size()), fr::Socket::NotEnoughData);
|
||||
ASSERT_EQ(test.parse(raw_response2.c_str(), raw_response2.size()), fr::Socket::NotEnoughData);
|
||||
ASSERT_EQ(test.parse(raw_response3.c_str(), raw_response3.size()), fr::Socket::Success);
|
||||
|
||||
//Verify it
|
||||
ASSERT_EQ(test.get_status(), fr::Http::MovedPermanently);
|
||||
ASSERT_EQ(test.header("Content-length"), "177");
|
||||
ASSERT_EQ(test.get_body(), response_body);
|
||||
}
|
||||
|
||||
TEST(HttpResponseTest, header_length_test)
|
||||
{
|
||||
//Try data with no header end first
|
||||
std::string buff(MAX_HTTP_HEADER_SIZE + 1, '\0');
|
||||
fr::HttpResponse response;
|
||||
ASSERT_EQ(response.parse(buff.c_str(), buff.size()), fr::Socket::HttpHeaderTooBig);
|
||||
response = {};
|
||||
|
||||
//Now try short header but long data, this should work
|
||||
buff = "HTTP/1.1 301 Moved Permanently\n"
|
||||
"Content-Type: text/html\n"
|
||||
"Content-Length: " + std::to_string(MAX_HTTP_BODY_SIZE - 1) + "\n"
|
||||
"Connection: keep-alive\n"
|
||||
"\n" + std::string(MAX_HTTP_BODY_SIZE - 1, '\0');
|
||||
ASSERT_EQ(response.parse(buff.c_str(), buff.size()), fr::Socket::Success);
|
||||
}
|
||||
|
||||
TEST(HttpResponseTest, body_length_test)
|
||||
{
|
||||
std::string buff =
|
||||
"HTTP/1.1 301 Moved Permanently\n"
|
||||
"Content-Type: text/html\n"
|
||||
"Content-Length: " + std::to_string(MAX_HTTP_BODY_SIZE + 1) + "\n"
|
||||
"Connection: keep-alive\n"
|
||||
"\n";
|
||||
buff += std::string(MAX_HTTP_BODY_SIZE + 1, '\0');
|
||||
fr::HttpResponse response;
|
||||
ASSERT_EQ(response.parse(buff.c_str(), buff.size()), fr::Socket::HttpBodyTooBig);
|
||||
}
|
||||
32
tests/SocketTest.cpp
Normal file
32
tests/SocketTest.cpp
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// Created by fred.nicolson on 25/09/17.
|
||||
//
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <frnetlib/Socket.h>
|
||||
|
||||
TEST(SocketTest, status_to_string_valid)
|
||||
{
|
||||
ASSERT_EQ(fr::Socket::status_to_string(fr::Socket::Status::Unknown), "Unknown");
|
||||
ASSERT_EQ(fr::Socket::status_to_string(fr::Socket::Status::HttpBodyTooBig), "HTTP body too big");
|
||||
}
|
||||
|
||||
TEST(SocketTest, status_to_string_invalid)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto str = fr::Socket::status_to_string(static_cast<fr::Socket::Status>(-1));
|
||||
}
|
||||
catch(const std::logic_error &)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto str = fr::Socket::status_to_string(static_cast<fr::Socket::Status>(99999));
|
||||
}
|
||||
catch(const std::logic_error &)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(false);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user