Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test_matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
os: windows-latest,
build_type: "Release", cc: "cl", cxx: "cl",
#this path will change at some point when VC2022 is released I guess
environment_script: "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Auxiliary/Build/vcvars64.bat"
environment_script: "C:/Program Files (x86)/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat"
}
- {
name: "Ubuntu gcc",
Expand All @@ -31,9 +31,9 @@ jobs:
build_type: "Release", cc: "clang", cxx: "clang++"
}
- {
name: "MacOS clang",
name: "MacOS gcc",
os: macos-latest,
build_type: "Release", cc: "clang", cxx: "clang++"
build_type: "Release", cc: "gcc", cxx: "g++"
}
steps:
- uses: actions/checkout@v2
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "doctest"]
path = doctest
url = https://github.com/onqtam/doctest.git
[submodule "extern/cpp-uri-parser"]
path = extern/cpp-uri-parser
url = https://github.com/lpcvoid/cpp-uri-parser
18 changes: 17 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,29 @@ set(NETLIB_SRC
src/service_resolver.hpp
src/endpoint_accessor.hpp
src/thread_pool.hpp
src/socket_operations.hpp)
src/socket_operations.hpp
)

set(NETLIB_HTTP
src/http/client.hpp
src/http/http.hpp
)

option(BUILD_TESTS "Build tests" ON)
option(BUILD_EXAMPLES "Build example programs" ON)
option(WITH_HTTP "Build with http support" ON)

if(NOT EXISTS "${CMAKE_SOURCE_DIR}/doctest/CMakeLists.txt")
set(BUILD_TESTS OFF)
message(WARNING "Cannot build tests without doctest! Deactivating tests.")
endif()

if (WITH_HTTP)
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/extern/cpp-uri-parser/URI.hpp")
set(WITH_HTTP OFF)
message(WARNING "If you want HTTP support, you need to check out submodules (cpp-uri-parser)")
endif()
message(NOTICE "Building with HTTP support.")
endif()

if(BUILD_TESTS)
Expand Down
4 changes: 4 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
set(EXAMPLE_SOURCES echo_server.cpp daytime_client.cpp threadpool.cpp time_client.cpp)

if (WITH_HTTP)
set(EXAMPLE_SOURCES ${EXAMPLE_SOURCES} http_client.cpp)
endif()

foreach (examplesource ${EXAMPLE_SOURCES})
string(REPLACE ".cpp" "" examplename ${examplesource})
add_executable(${examplename} ${examplesource})
Expand Down
35 changes: 35 additions & 0 deletions examples/http_client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include "../src/netlib.hpp"
#include "../src/http/client.hpp"
#include "../src/http/http.hpp"
#include <csignal>
#include <iomanip>
#include <iostream>


void exit_handler(int s){
std::cout << "Goodbye!" << std::endl;
exit(EXIT_SUCCESS);
}

int main(int argc, char** argv)
{
netlib::http::http_client client;

auto res = client.get("http://example.com");

if (res.second) {
std::cerr << "Error: " << res.second.message() << std::endl;
exit(1);
}

std::cout << "Got HTTP response: " << res.first->response_code <<
", version " << res.first->version.first << "." << res.first->version.second << std::endl;
std::cout << "Header entries:" << std::endl;
std::for_each(res.first->headers.begin(), res.first->headers.end(), [](auto header_entry) {
std::cout << header_entry.first << " = " << header_entry.second << std::endl;
});
std::cout << "Body:" << std::endl;
std::cout << res.first.value().body << std::endl;

signal(SIGINT, exit_handler);
}
1 change: 1 addition & 0 deletions extern/cpp-uri-parser
Submodule cpp-uri-parser added at ed011f
3 changes: 1 addition & 2 deletions src/client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@
namespace netlib {

using namespace std::chrono_literals;
static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT = 1000ms;

class client {
private:
static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT = 1000ms;
protected:
std::optional<netlib::socket> _socket;
addrinfo *_endpoint_addr = nullptr;
Expand Down
83 changes: 83 additions & 0 deletions src/http/client.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#include "../extern/cpp-uri-parser/URI.hpp"
#include "http.hpp"
#pragma once

namespace netlib::http {

using namespace std::chrono_literals;

class http_client {
private:
netlib::thread_pool _thread_pool;
netlib::client _client;
public:

inline http_client() {}

inline std::pair<std::optional<netlib::http::http_response>, std::error_condition> get(const std::string& url) {
auto uri = URI(url);
if (uri.get_result() != URI::URIParsingResult::success) {
return std::make_pair(std::nullopt, std::errc::bad_address);
}
if (!_client.is_connected()) {
uint16_t port = 80;
if (uri.get_protocol().has_value()) {
std::string_view protocol_str = uri.get_protocol().value();
if (protocol_str != "http"){
// no support for TLS so far...
// also, only http and https shall ever be expected
return std::make_pair(std::nullopt, std::errc::protocol_not_supported);
}
}
auto res = _client.connect(std::string(uri.get_host().value()), port, AddressFamily::IPv4, AddressProtocol::TCP);
if (res) {
return std::make_pair(std::nullopt, res);
}
}

std::string query = (uri.get_query().has_value() ? std::string(uri.get_query().value()) : "/");
std::string http_get = "GET " + query + " HTTP/1.1\r\nHost:" + std::string(uri.get_host().value()) + "\r\n\r\n";
const std::vector<uint8_t> get_data(http_get.begin(), http_get.end());
auto send_res = _client.send(get_data);
if (send_res.first != get_data.size()) {
return std::make_pair(std::nullopt, send_res.second);
}
std::vector<uint8_t> data_buffer;
std::chrono::milliseconds time_spent = std::chrono::milliseconds(0);
const std::chrono::milliseconds TICK_TIME = std::chrono::milliseconds(50);
while (true) {
auto recv_res = _client.recv(0, TICK_TIME);
time_spent += TICK_TIME;
if (!recv_res.first.empty()) {
data_buffer.insert(data_buffer.begin(), recv_res.first.begin(), recv_res.first.end());
}
if ((recv_res.second == std::errc::timed_out) && (!data_buffer.empty())) {
netlib::http::http_response response;
std::string raw_response (data_buffer.begin(), data_buffer.end());
std::error_condition parse_resp = response.from_raw_response(raw_response);
if (!parse_resp) {
return {response, {}};
} else {
return {std::nullopt, parse_resp};
}
}
if (time_spent > DEFAULT_TIMEOUT) {
return {std::nullopt, std::errc::timed_out};
}
}
}

inline std::future<std::pair<std::optional<netlib::http::http_response>, std::error_condition>> get_async(const std::string& url) {
return _thread_pool.add_task(
[&](std::string url) {
return this->get(url);
},
url);
}




};

}
90 changes: 90 additions & 0 deletions src/http/http.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#pragma once

#include <string>
#include <system_error>
#include <utility>
#include <vector>
#include <algorithm>

namespace netlib::http {

using http_header_entry = std::pair<std::string, std::string>;
using http_headers = std::vector<http_header_entry>;

struct http_response {
http_headers headers;
uint32_t response_code;
std::pair<uint32_t, uint32_t> version;
std::string body;
inline std::error_condition from_raw_response(const std::string& raw_response) {
// a very rudimentary http response parser
if (raw_response.empty()) {
return std::errc::no_message;
}

/* strategy:
* split into multiple (at least two) parts, delimited by \r\n\r\n"
* within the first part:
* first, parse the status line
* second, parse the header fields, until we arrive at an empty line (only CR LF)
* last, an optional body
* then, for the rest of the parts, concat into body
*/

auto split = [](const std::string& str, const std::string& delimiter) -> std::vector<std::string> {
std::vector<std::string> split_tokens;
std::size_t start;
std::size_t end = 0;
while ((start = str.find_first_not_of(delimiter, end)) != std::string::npos)
{
end = str.find(delimiter, start);
split_tokens.push_back(str.substr(start, end - start));
}
return split_tokens;
};

std::vector<std::string> header_body_split = split(raw_response, "\r\n\r\n");
//split header part of response into response_header_lines
std::vector<std::string> response_header_lines = split(header_body_split.front(), "\r\n");
//first line should start with "HTTP"
if (!response_header_lines.front().starts_with("HTTP")) {
return std::errc::result_out_of_range;
}
//attempt to parse status line
//split into parts by space
auto status_parts = split(response_header_lines.front(), " ");
if (status_parts.size() < 3) {
return std::errc::bad_message;
}
//parse "HTTP/x.x"
auto version_parts = split(status_parts.front(), "/");
if (version_parts.size() != 2) {
return std::errc::bad_message;
}
//parse "x.x"
auto version_components = split(version_parts.back(), ".");
version.first = std::stoi(version_components.front());
version.second = std::stoi(version_components.back());
//parse response code
response_code = std::stoi(status_parts[1]);
//there can be an optional code description in the first line, but we ignore that here
//parse the response header lines until the end
//start at second line, first is status
std::for_each(response_header_lines.begin(), response_header_lines.end(), [&](const std::string& header_component){
auto component_parts = split(header_component, ":");
if (component_parts.size() == 2) {
headers.emplace_back(component_parts.front(), component_parts.back());
}
});

//now, take the body part(s) and concat them
std::for_each(header_body_split.begin() + 1, header_body_split.end(), [&](const std::string& body_line){
body += body_line;
});

return {};

};
};

}
12 changes: 11 additions & 1 deletion src/socket.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ using ssize_t = signed long long int;
#define MSG_NOSIGNAL 0
//poll implementation
#define poll_syscall ::WSAPoll
//we ignore unused parameter warning on windows, missing impls
#pragma warning(disable: 4100)
//use unsafe functions under windows
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)
#pragma warning(disable:4267)
#pragma warning(disable:4244)

#else
// headers
#include <arpa/inet.h>
Expand All @@ -42,7 +50,9 @@ using socket_t = int32_t;
// this is actually a nice one to have
#define INVALID_SOCKET (-1)
#ifdef __APPLE__
#define MSG_NOSIGNAL SO_NOSIGPIPE
#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL SO_NOSIGPIPE
#endif
#endif
#endif

Expand Down
2 changes: 1 addition & 1 deletion src/socket_operations.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class operations {

std::size_t remaining_bytes = data.size() - total_sent_size;
std::size_t actual_chunk_size = (remaining_bytes > chunk_size) ? chunk_size : remaining_bytes;
ssize_t send_res = ::send(sock.get_raw().value(), data_pointer, actual_chunk_size, MSG_NOSIGNAL);
ssize_t send_res = ::send(sock.get_raw().value(), data_pointer, static_cast<int32_t>(actual_chunk_size), MSG_NOSIGNAL);
if (send_res > 0) {
total_sent_size += static_cast<std::size_t>(send_res);
data_pointer += send_res;
Expand Down
4 changes: 4 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ set(TEST_SOURCES
test_large_data_transfer.cpp
test_raii.cpp)

if (WITH_HTTP)
set(TEST_SOURCES ${TEST_SOURCES} test_http_parser.cpp)
endif()

enable_testing()
add_executable(test_libnetcpp main.cpp ${TEST_SOURCES})
target_link_libraries(test_libnetcpp ${CMAKE_THREAD_LIBS_INIT})
Expand Down
29 changes: 29 additions & 0 deletions tests/test_http_parser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include "../doctest/doctest/doctest.h"
#include "../src/http/http.hpp"
#include "../src/netlib.hpp"

using namespace std::chrono_literals;
extern uint16_t test_port;

TEST_CASE("Test HTTP response parser")
{
netlib::http::http_response resp;

std::string raw_response = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=UTF-8\r\n"
"Content-Length: 1256\r\n"
"\r\n\r\n"
"TEST";

CHECK_FALSE(resp.from_raw_response(raw_response));
CHECK_EQ(resp.version.first, 1);
CHECK_EQ(resp.version.second, 1);
CHECK_EQ(resp.response_code, 200);
CHECK_EQ(resp.headers.size(), 2);
CHECK_EQ(resp.headers[0].first, "Content-Type");
CHECK_EQ(resp.headers[1].first, "Content-Length");
CHECK_EQ(resp.headers[0].second, " text/html; charset=UTF-8");
CHECK_EQ(resp.headers[1].second, " 1256");
CHECK_EQ(resp.body, "TEST");

}