Skip to content

Commit c6c7ab5

Browse files
committed
Add rudimentary http support (client only so far)
1 parent e9bd766 commit c6c7ab5

File tree

3 files changed

+184
-13
lines changed

3 files changed

+184
-13
lines changed

CMakeLists.txt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,29 @@ set(NETLIB_SRC
1919
src/service_resolver.hpp
2020
src/endpoint_accessor.hpp
2121
src/thread_pool.hpp
22-
src/socket_operations.hpp)
22+
src/socket_operations.hpp
23+
)
24+
25+
set(NETLIB_HTTP
26+
src/http/client.hpp
27+
src/http/http.hpp
28+
)
2329

2430
option(BUILD_TESTS "Build tests" ON)
2531
option(BUILD_EXAMPLES "Build example programs" ON)
32+
option(WITH_HTTP "Build with http support" ON)
2633

2734
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/doctest/CMakeLists.txt")
2835
set(BUILD_TESTS OFF)
36+
message(WARNING "Cannot build tests without doctest! Deactivating tests.")
37+
endif()
38+
39+
if (WITH_HTTP)
40+
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/extern/cpp-uri-parser/URI.hpp")
41+
set(WITH_HTTP OFF)
42+
message(WARNING "If you want HTTP support, you need to check out submodules (cpp-uri-parser)")
43+
endif()
44+
message(NOTICE "Building with HTTP support.")
2945
endif()
3046

3147
if(BUILD_TESTS)

src/http/client.hpp

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,83 @@
1-
//
2-
// Created by lpcvoid on 25.12.22.
3-
//
1+
#include "../extern/cpp-uri-parser/URI.hpp"
2+
#include "http.hpp"
3+
#pragma once
44

5-
#ifndef NETLIB_CLIENT_HPP
6-
#define NETLIB_CLIENT_HPP
5+
namespace netlib::http {
76

8-
#endif // NETLIB_CLIENT_HPP
7+
using namespace std::chrono_literals;
8+
9+
class http_client {
10+
private:
11+
netlib::thread_pool _thread_pool;
12+
netlib::client _client;
13+
public:
14+
15+
inline http_client() {}
16+
17+
inline std::pair<std::optional<netlib::http::http_response>, std::error_condition> get(const std::string& url) {
18+
auto uri = URI(url);
19+
if (uri.get_result() != URI::URIParsingResult::success) {
20+
return std::make_pair(std::nullopt, std::errc::bad_address);
21+
}
22+
if (!_client.is_connected()) {
23+
uint16_t port = 80;
24+
if (uri.get_protocol().has_value()) {
25+
std::string_view protocol_str = uri.get_protocol().value();
26+
if (protocol_str != "http"){
27+
// no support for TLS so far...
28+
// also, only http and https shall ever be expected
29+
return std::make_pair(std::nullopt, std::errc::protocol_not_supported);
30+
}
31+
}
32+
auto res = _client.connect(std::string(uri.get_host().value()), port, AddressFamily::IPv4, AddressProtocol::TCP);
33+
if (res) {
34+
return std::make_pair(std::nullopt, res);
35+
}
36+
}
37+
38+
std::string query = (uri.get_query().has_value() ? std::string(uri.get_query().value()) : "/");
39+
std::string http_get = "GET " + query + " HTTP/1.1\r\nHost:" + std::string(uri.get_host().value()) + "\r\n\r\n";
40+
const std::vector<uint8_t> get_data(http_get.begin(), http_get.end());
41+
auto send_res = _client.send(get_data);
42+
if (send_res.first != get_data.size()) {
43+
return std::make_pair(std::nullopt, send_res.second);
44+
}
45+
std::vector<uint8_t> data_buffer;
46+
std::chrono::milliseconds time_spent = std::chrono::milliseconds(0);
47+
const std::chrono::milliseconds TICK_TIME = std::chrono::milliseconds(50);
48+
while (true) {
49+
auto recv_res = _client.recv(0, TICK_TIME);
50+
time_spent += TICK_TIME;
51+
if (!recv_res.first.empty()) {
52+
data_buffer.insert(data_buffer.begin(), recv_res.first.begin(), recv_res.first.end());
53+
}
54+
if ((recv_res.second == std::errc::timed_out) && (!data_buffer.empty())) {
55+
netlib::http::http_response response;
56+
std::string raw_response (data_buffer.begin(), data_buffer.end());
57+
std::error_condition parse_resp = response.from_raw_response(raw_response);
58+
if (!parse_resp) {
59+
return {response, {}};
60+
} else {
61+
return {std::nullopt, parse_resp};
62+
}
63+
}
64+
if (time_spent > DEFAULT_TIMEOUT) {
65+
return {std::nullopt, std::errc::timed_out};
66+
}
67+
}
68+
}
69+
70+
inline std::future<std::pair<std::optional<netlib::http::http_response>, std::error_condition>> get_async(const std::string& url) {
71+
return _thread_pool.add_task(
72+
[&](std::string url) {
73+
return this->get(url);
74+
},
75+
url);
76+
}
77+
78+
79+
80+
81+
};
82+
83+
}

src/http/http.hpp

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,88 @@
1-
//
2-
// Created by lpcvoid on 25.12.22.
3-
//
1+
#pragma once
42

5-
#ifndef NETLIB_HTTP_HPP
6-
#define NETLIB_HTTP_HPP
3+
#include <string>
4+
#include <system_error>
5+
#include <utility>
6+
#include <vector>
7+
namespace netlib::http {
78

8-
#endif // NETLIB_HTTP_HPP
9+
using http_header_entry = std::pair<std::string, std::string>;
10+
using http_headers = std::vector<http_header_entry>;
11+
12+
struct http_response {
13+
http_headers headers;
14+
uint32_t response_code;
15+
std::pair<uint32_t, uint32_t> version;
16+
std::string body;
17+
inline std::error_condition from_raw_response(const std::string& raw_response) {
18+
// a very rudimentary http response parser
19+
if (raw_response.empty()) {
20+
return std::errc::no_message;
21+
}
22+
23+
/* strategy:
24+
* split into multiple (at least two) parts, delimited by \r\n\r\n"
25+
* within the first part:
26+
* first, parse the status line
27+
* second, parse the header fields, until we arrive at an empty line (only CR LF)
28+
* last, an optional body
29+
* then, for the rest of the parts, concat into body
30+
*/
31+
32+
auto split = [](const std::string& str, const std::string& delimiter) -> std::vector<std::string> {
33+
std::vector<std::string> split_tokens;
34+
std::size_t start;
35+
std::size_t end = 0;
36+
while ((start = str.find_first_not_of(delimiter, end)) != std::string::npos)
37+
{
38+
end = str.find(delimiter, start);
39+
split_tokens.push_back(str.substr(start, end - start));
40+
}
41+
return split_tokens;
42+
};
43+
44+
std::vector<std::string> header_body_split = split(raw_response, "\r\n\r\n");
45+
//split header part of response into response_header_lines
46+
std::vector<std::string> response_header_lines = split(header_body_split.front(), "\r\n");
47+
//first line should start with "HTTP"
48+
if (!response_header_lines.front().starts_with("HTTP")) {
49+
return std::errc::result_out_of_range;
50+
}
51+
//attempt to parse status line
52+
//split into parts by space
53+
auto status_parts = split(response_header_lines.front(), " ");
54+
if (status_parts.size() < 3) {
55+
return std::errc::bad_message;
56+
}
57+
//parse "HTTP/x.x"
58+
auto version_parts = split(status_parts.front(), "/");
59+
if (version_parts.size() != 2) {
60+
return std::errc::bad_message;
61+
}
62+
//parse "x.x"
63+
auto version_components = split(version_parts.back(), ".");
64+
version.first = std::stoi(version_components.front());
65+
version.second = std::stoi(version_components.back());
66+
//parse response code
67+
response_code = std::stoi(status_parts[1]);
68+
//there can be an optional code description in the first line, but we ignore that here
69+
//parse the response header lines until the end
70+
//start at second line, first is status
71+
std::for_each(response_header_lines.begin(), response_header_lines.end(), [&](const std::string& header_component){
72+
auto component_parts = split(header_component, ":");
73+
if (component_parts.size() == 2) {
74+
headers.emplace_back(component_parts.front(), component_parts.back());
75+
}
76+
});
77+
78+
//now, take the body part(s) and concat them
79+
std::for_each(header_body_split.begin() + 1, header_body_split.end(), [&](const std::string& body_line){
80+
body += body_line;
81+
});
82+
83+
return {};
84+
85+
};
86+
};
87+
88+
}

0 commit comments

Comments
 (0)