diff --git a/README.md b/README.md index 2b8fa5d..3b53e79 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ while(true) } //Close connection - client.close(); + client.close_socket(); } ``` After binding to the port, we infinitely try and receive a new request, construct a response with the body of 'Hello, World!' and send it back to the client before closing the socket. fr::HttpRequest, and fr::HttpResponse both inherit fr::Sendable, which allows them to be sent and received through sockets just like fr::Packets. diff --git a/examples/simple_server_and_client/CMakeLists.txt b/examples/simple_server_and_client/CMakeLists.txt index db5b49d..818fc16 100644 --- a/examples/simple_server_and_client/CMakeLists.txt +++ b/examples/simple_server_and_client/CMakeLists.txt @@ -1,8 +1,8 @@ -add_executable(simple_client tcpsocket_client.cpp) -target_link_libraries(simple_client frnetlib) +add_executable(simple_http_client SimpleHttpClient.cpp) +target_link_libraries(simple_http_client frnetlib) -add_executable(simple_server tcpsocket_server.cpp) -target_link_libraries(simple_server frnetlib) +add_executable(simple_http_server SimpleHttpServer.cpp) +target_link_libraries(simple_http_server frnetlib) -install(TARGETS simple_client simple_server +install(TARGETS simple_http_client simple_http_server DESTINATION "bin") diff --git a/examples/simple_server_and_client/SimpleHttpClient.cpp b/examples/simple_server_and_client/SimpleHttpClient.cpp new file mode 100644 index 0000000..9010ed4 --- /dev/null +++ b/examples/simple_server_and_client/SimpleHttpClient.cpp @@ -0,0 +1,54 @@ +// +// Created by fred.nicolson on 01/02/18. +// + +#include +#include +#include +#include +#include + +int main() +{ + //Get an address to query from stdin + std::string url; + std::cout << "Enter URL: "; + std::cin >> url; + + //Parse it into something easy to use + fr::URL parsed_url(url); + if(parsed_url.get_port().empty()) + { + std::cerr << "No schema or port specified. Unable to connect." << std::endl; + return EXIT_FAILURE; + } + + //Try to connect to the parsed address + fr::TcpSocket socket; + if(socket.connect(parsed_url.get_host(), parsed_url.get_port(), {}) != fr::Socket::Success) + { + std::cerr << "Failed to connect to the specified URL" << std::endl; + return EXIT_FAILURE; + } + + //Construct a request, requesting the user provided URI + fr::HttpRequest request; + request.set_uri(parsed_url.get_uri()); + if(socket.send(request) != fr::Socket::Success) + { + std::cerr << "Failed to send HTTP request" << std::endl; + return EXIT_FAILURE; + } + + //Now wait for a response + fr::HttpResponse response; + if(socket.receive(response) != fr::Socket::Success) + { + std::cerr << "Failed to receive HTTP response" << std::endl; + return EXIT_FAILURE; + } + + //Print out the response body + std::cout << response.get_body() << std::endl; + +} \ No newline at end of file diff --git a/examples/simple_server_and_client/SimpleHttpServer.cpp b/examples/simple_server_and_client/SimpleHttpServer.cpp new file mode 100644 index 0000000..ec4f755 --- /dev/null +++ b/examples/simple_server_and_client/SimpleHttpServer.cpp @@ -0,0 +1,52 @@ +// +// Created by fred.nicolson on 01/02/18. +// + +#include +#include +#include +#include + +int main() +{ + fr::Socket::Status err; + fr::TcpSocket client; + fr::TcpListener listener; + + //Bind to a port + if((err = listener.listen("8081")) != fr::Socket::Success) + { + std::cerr << "Failed to bind to port: " << err << std::endl; + return EXIT_FAILURE; + } + + while(true) + { + //Accept a new connection + if((err = listener.accept(client)) != fr::Socket::Success) + { + std::cerr << "Failed to accept new connection: " << err << std::endl; + continue; + } + + //Receive client HTTP request + fr::HttpRequest request; + if((err = client.receive(request)) != fr::Socket::Success) + { + std::cerr << "Failed to receive request from client: " << err << std::endl; + } + + //Construct a response + fr::HttpResponse response; + response.set_body("

Hello, World!

"); + + //Send it + if((err = client.send(response)) != fr::Socket::Success) + { + std::cerr << "Failed to send response to client: " << err << std::endl; + } + + //Close connection + client.close_socket(); + } +} \ No newline at end of file diff --git a/examples/simple_server_and_client/tcpsocket_client.cpp b/examples/simple_server_and_client/tcpsocket_client.cpp deleted file mode 100644 index 8a88593..0000000 --- a/examples/simple_server_and_client/tcpsocket_client.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include -#include -#include -#include - -#define SERVER_IP "127.0.0.1" -#define SERVER_PORT "8081" - - -int send_a_packet(fr::TcpSocket &socket) -{ - std::cout << "Going to send something..." << std::endl; - - //Send the request - fr::Packet packet; - packet << "Hello there, I am " << (float)1.2 << " years old"; - if(socket.send(packet) != fr::Socket::Success) - { - //Failed to send packet - std::cout << "Seems got something wrong when sending" << std::endl; - return -1; - } - - //Receive a response - std::cout << "Waiting for a response..." << std::endl; - if(socket.receive(packet) != fr::Socket::Success) - { - std::cout << "Failed to receive server response!" << std::endl; - return -2; - } - - //Extract the response, you can surround this in a try/catch block to catch errors - std::string str1, str2; - float age; - packet >> str1 >> age >> str2; - std::cout << "Server sent: " << str1 << ", " << age << ", " << str2 << "\n\n\n" << std::endl; - return 0; -} - - -int main() -{ - - //Try and connect to the server, create a new TCP object for the job and then connect - fr::TcpSocket socket; //Use an fr::SSLSocket if SSL - if(socket.connect(SERVER_IP, SERVER_PORT, std::chrono::seconds(20)) != fr::Socket::Success) - { - //Failed to connect - std::cout << "Failed to connect to: " << SERVER_IP << ":" << SERVER_PORT << std::endl; - return -1; - } - - //For storing user input and send_a_packet response - std::string op_str; - int rtn = 0; - - //Keep going until either we, or the server closes the connection - while(true) - { - //Ask the user what to do - std::cout << "Choose what you want to do, `c` for `continue`, `q` for `quit`:" << std::endl; - std::cin >> op_str; - if(op_str.length() > 1) - { - std::cout << "Seems that you inputted more than one character, please retry." << std::endl; - continue; - } - - switch(op_str[0]) - { - case 'c': - std::cout << "continue" << std::endl; - rtn = send_a_packet(socket); - break; - case 'q': - break; - default: - std::cout << "Invalid input!" << std::endl; - } - - //Exit/error check - if(op_str[0] == 'q') - break; - if(rtn != 0) - break; - } - - std::cout << "All done, bye!" << std::endl; -} - diff --git a/examples/simple_server_and_client/tcpsocket_server.cpp b/examples/simple_server_and_client/tcpsocket_server.cpp deleted file mode 100644 index d3889ee..0000000 --- a/examples/simple_server_and_client/tcpsocket_server.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include -#include -#include -#include -#define PORT "8081" - - -int main() -{ - //Create a new TCP socket to maintain connections, and a new Tcp Listener to accept connections - fr::TcpSocket client; //fr::SSLSocket for SSL - fr::TcpListener listener; //fr::SSLListener for HTTPS - - //Bind to a port - if(listener.listen(PORT) != fr::Socket::Success) - { - std::cout << "Failed to bind to port: " << PORT << std::endl; - return -1; - } - - std::cout << "Listener is listening on port: " << PORT << "..." << std::endl; - - //Start accepting connections - while(true) - { - std::cout << "Waiting for a new connection ..." << std::endl; - - //Accept a new connection - if(listener.accept(client) != fr::Socket::Success) - { - std::cout << "Failed to accept client, shutdown" << std::endl; - break; - } - - while(true) //Infinite loop to keep the connection active - { - try - { - //Receive an fr::Packet from the client - fr::Packet packet; - if(client.receive(packet) != fr::Socket::Success) - { - std::cout << "Failed to receive request" << std::endl; - client.close_socket(); - break; - } - - //Extract the data from the packet - std::string str1, str2; - float age; - packet >> str1 >> age >> str2; - - //Print out what we got - std::cout << "Client sent:" << str1 << ", " << age << ", " << str2 << std::endl; - - //Send back the same packet - if(client.send(packet) != fr::Socket::Success) - { - throw std::string("Failed to send packet to client"); - } - } - catch(const std::exception &e) - { - //Print out what happened - std::cout << "Error: " << e.what() << std::endl; - - //Close connection - client.close_socket(); - break; - } - } - - - } - - return 0; -} - diff --git a/include/frnetlib/Http.h b/include/frnetlib/Http.h index 8c8233b..51d9968 100644 --- a/include/frnetlib/Http.h +++ b/include/frnetlib/Http.h @@ -23,7 +23,7 @@ namespace fr Put = 3, Delete = 4, Patch = 5, - RequestTypeCount = 3, + RequestTypeCount = 6, //Keep me at the end and updated }; enum RequestStatus { diff --git a/include/frnetlib/Socket.h b/include/frnetlib/Socket.h index f5e12f4..7cc2e1c 100644 --- a/include/frnetlib/Socket.h +++ b/include/frnetlib/Socket.h @@ -47,20 +47,10 @@ namespace fr virtual ~Socket() noexcept = default; Socket(Socket &&o) noexcept { - outbound_mutex.lock(); - inbound_mutex.lock(); - o.inbound_mutex.lock(); - o.outbound_mutex.lock(); - remote_address = std::move(o.remote_address); is_blocking = o.is_blocking; ai_family = o.ai_family; max_receive_size = o.max_receive_size; - - outbound_mutex.unlock(); - inbound_mutex.unlock(); - o.inbound_mutex.unlock(); - o.outbound_mutex.unlock(); } /*! @@ -195,7 +185,7 @@ namespace fr /*! * Sets the maximum receivable size that may be received by the socket. This does - * not apply to receive_raw(), but only things like fr::Packet, or HTTP responses. + * not apply to receive_raw(), but only things like fr::Packet. * * If a client attempts to send a packet larger than sz bytes, then * the client will be disconnected and an fr::Socket::MaxPacketSizeExceeded @@ -214,6 +204,7 @@ namespace fr * Gets the max packet size. See set_max_packet_size * for more information. * + * * @return The max packet size */ inline uint32_t get_max_receive_size() @@ -240,8 +231,6 @@ namespace fr std::string remote_address; bool is_blocking; - std::mutex outbound_mutex; - std::mutex inbound_mutex; int ai_family; uint32_t max_receive_size; }; diff --git a/include/frnetlib/URL.h b/include/frnetlib/URL.h index 71ab64c..a1d69c5 100644 --- a/include/frnetlib/URL.h +++ b/include/frnetlib/URL.h @@ -32,7 +32,10 @@ namespace fr explicit URL(const std::string &url); /*! - * Parses a given URL, extracting its various components + * Parses a given URL, extracting its various components. This will always + * try to make something out of the provided URL. It will also try its best + * to guess any potentially missing information. For example, if the URL is + * https://example.com, then the 'port' will be guessed as 443. * * @param url The URL to parse */ @@ -86,6 +89,25 @@ namespace fr return fragment; } + /*! + * Returns the combination of other URL elements into a single URI string. + * + * The URL is everything after the IP/Address & Port. + * + * @return The URL's URI + */ + inline std::string get_uri() const + { + std::string result; + if(!get_path().empty()) + result += get_path(); + if(!get_query().empty()) + result += "?" + get_query(); + if(!get_fragment().empty()) + result += "#" + get_fragment(); + return result; + } + /*! * Converts a string to a scheme enum. * diff --git a/src/Http.cpp b/src/Http.cpp index 4167f12..fef1b90 100644 --- a/src/Http.cpp +++ b/src/Http.cpp @@ -9,14 +9,19 @@ namespace fr { - const static std::string request_type_strings[Http::RequestType::RequestTypeCount] = {"UNKNOWN", "GET", "POST"}; + const static std::string request_type_strings[Http::RequestType::RequestTypeCount] = {"UNKNOWN", + "GET", + "POST", + "PUT", + "DELETE", + "PATCH"}; Http::Http() : request_type(Unknown), uri("/"), status(Ok) { - + static_assert(Http::RequestType::RequestTypeCount == 6, "Please update request_type_strings"); } Http::RequestType Http::get_type() const diff --git a/src/HttpResponse.cpp b/src/HttpResponse.cpp index fced97e..3b01cfb 100644 --- a/src/HttpResponse.cpp +++ b/src/HttpResponse.cpp @@ -44,11 +44,9 @@ namespace fr //Ensure that body doesn't exceed maximum length if(body.size() > MAX_HTTP_BODY_SIZE) - { return fr::Socket::HttpBodyTooBig; - } - //Cut off any data if it exceeds content length + //Cut off any data if it exceeds content length, todo: potentially an issue, could cut the next request off if(body.size() > content_length) body.resize(content_length); else if(body.size() < content_length) diff --git a/src/Socket.cpp b/src/Socket.cpp index e4dbf22..026362f 100644 --- a/src/Socket.cpp +++ b/src/Socket.cpp @@ -41,7 +41,6 @@ namespace fr if(!connected()) return Socket::Disconnected; - std::lock_guard guard(inbound_mutex); return obj.receive(this); } diff --git a/src/TcpSocket.cpp b/src/TcpSocket.cpp index 7890cab..5fe36cc 100644 --- a/src/TcpSocket.cpp +++ b/src/TcpSocket.cpp @@ -23,7 +23,6 @@ namespace fr Socket::Status TcpSocket::send_raw(const char *data, size_t size) { - std::lock_guard guard(outbound_mutex); size_t sent = 0; while(sent < size) { @@ -128,7 +127,7 @@ namespace fr //Wait for the socket to do something/expire timeval tv = {}; - tv.tv_sec = timeout.count() == -1 ? DEFAULT_SOCKET_TIMEOUT : timeout.count(); + tv.tv_sec = timeout.count() == 0 ? DEFAULT_SOCKET_TIMEOUT : timeout.count(); tv.tv_usec = 0; fd_set set = {}; FD_ZERO(&set); diff --git a/src/URL.cpp b/src/URL.cpp index de92767..7bdae48 100644 --- a/src/URL.cpp +++ b/src/URL.cpp @@ -38,12 +38,12 @@ namespace fr { auto scheme_pos = scheme_string_map.find(to_lower(url.substr(0, pos))); scheme = (scheme_pos == scheme_string_map.end()) ? URL::Unknown : scheme_pos->second; - parse_offset = pos + 3; + parse_offset = pos + 3; // skip the :// } //Check to see if there's a port pos = url.find(':', parse_offset); - if(pos != std::string::npos) + if(pos != std::string::npos) //A port is provided { //Store host host = url.substr(parse_offset, pos - parse_offset); @@ -55,7 +55,7 @@ namespace fr port = url.substr(pos + 1, port_end - pos - 1); parse_offset = port_end + 1; } - else + else //There's no port { //Store host pos = url.find('/', parse_offset); @@ -96,7 +96,7 @@ namespace fr pos = url.find('?', parse_offset); if(pos != std::string::npos) { - path = url.substr(parse_offset, pos - parse_offset); + path.append("/").append(url.substr(parse_offset, pos - parse_offset)); parse_offset = pos + 1; } else @@ -104,7 +104,7 @@ namespace fr pos = url.find('#', parse_offset); pos = (pos != std::string::npos) ? pos : url.find('?', parse_offset); pos = (pos != std::string::npos) ? pos : url.size(); - path = url.substr(parse_offset, pos - parse_offset); + path.append("/").append(url.substr(parse_offset, pos - parse_offset)); parse_offset = pos + 1; } diff --git a/tests/NetworkEncodingTest.cpp b/tests/NetworkEncodingTest.cpp new file mode 100644 index 0000000..12f95bf --- /dev/null +++ b/tests/NetworkEncodingTest.cpp @@ -0,0 +1,88 @@ +// +// Created by fred.nicolson on 01/02/18. +// + +#include +#include "frnetlib/NetworkEncoding.h" + +constexpr bool is_little_endian() +{ + unsigned short x=0x0001; + auto p = reinterpret_cast(&x); + return *p != 0; +} + +TEST(NetworkEncodingTest, test_htonf) +{ + float input = std::numeric_limits::max(); + float result = htonf(input); + + if(is_little_endian()) + { + float manual; + std::reverse_copy((char*)&input, (char*)&input + sizeof(input), (uint8_t*)&manual); + ASSERT_EQ(memcmp(&result, &manual, sizeof(manual)), 0); + } + else + { + ASSERT_EQ(result, input); + } +} + +TEST(NetworkEncodingTest, test_ntohf) +{ + float input = std::numeric_limits::max(); + float encoded = htonf(input); + float decoded = ntohf(encoded); + ASSERT_EQ(input, decoded); +} + +TEST(NetworkEncodingTest, test_htond) +{ + double input = std::numeric_limits::max(); + double result = htond(input); + + if(is_little_endian()) + { + double manual; + std::reverse_copy((char*)&input, (char*)&input + sizeof(input), (uint8_t*)&manual); + ASSERT_EQ(memcmp(&result, &manual, sizeof(manual)), 0); + } + else + { + ASSERT_EQ(result, input); + } +} + +TEST(NetworkEncodingTest, test_ntohd) +{ + double input = std::numeric_limits::max(); + double encoded = htond(input); + double decoded = ntohd(encoded); + ASSERT_EQ(input, decoded); +} + +TEST(NetworkEncodingTest, test_htonll) +{ + uint64_t input = std::numeric_limits::max(); + uint64_t result = htonll(input); + + if(is_little_endian()) + { + uint64_t manual; + std::reverse_copy((char*)&input, (char*)&input + sizeof(input), (uint8_t*)&manual); + ASSERT_EQ(manual, result); + } + else + { + ASSERT_EQ(result, input); + } +} + +TEST(NetworkEncodingTest, test_ntohll) +{ + uint64_t input = std::numeric_limits::max(); + uint64_t encoded = htonll(input); + uint64_t decoded = ntohll(encoded); + ASSERT_EQ(input, decoded); +} \ No newline at end of file diff --git a/tests/URLTest.cpp b/tests/URLTest.cpp index 1f45890..1563479 100644 --- a/tests/URLTest.cpp +++ b/tests/URLTest.cpp @@ -10,7 +10,7 @@ TEST(URLTest, full_parse) fr::URL url("http://example.com:80/path/path?query=10&bob=20#frag"); ASSERT_EQ(url.get_host(), "example.com"); ASSERT_EQ(url.get_scheme(), fr::URL::HTTP); - ASSERT_EQ(url.get_path(), "path/path"); + ASSERT_EQ(url.get_path(), "/path/path"); ASSERT_EQ(url.get_query(), "query=10&bob=20"); ASSERT_EQ(url.get_fragment(), "frag"); } @@ -38,6 +38,30 @@ TEST(URLTest, fragment_test) TEST(URLTest, path_test) { fr::URL url("example.com/path/hey#frag"); - ASSERT_EQ(url.get_path(), "path/hey"); + ASSERT_EQ(url.get_path(), "/path/hey"); ASSERT_EQ(url.get_fragment(), "frag"); +} + +TEST(URLTest, uri_test) +{ + fr::URL url("http://example.com:80/path/path?query=10#frag"); + ASSERT_EQ(url.get_uri(), "/path/path?query=10#frag"); +} + +TEST(URLTest, uri_test2) +{ + fr::URL url("http://example.com:80/path/path?query=10"); + ASSERT_EQ(url.get_uri(), "/path/path?query=10"); +} + +TEST(URLTest, uri_test3) +{ + fr::URL url("http://example.com:80/path/path#frag"); + ASSERT_EQ(url.get_uri(), "/path/path#frag"); +} + +TEST(URLTest, uri_test4) +{ + fr::URL url("http://example.com:80/?bob=10#frag"); + ASSERT_EQ(url.get_uri(), "/?bob=10#frag"); } \ No newline at end of file