Skip to content

Commit 112fc2b

Browse files
authored
Merge pull request #4 from lpcvoid/add-http-functionality
Add http functionality
2 parents e996139 + e9a37e5 commit 112fc2b

File tree

13 files changed

+282
-8
lines changed

13 files changed

+282
-8
lines changed

.github/workflows/test_matrix.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
os: windows-latest,
1919
build_type: "Release", cc: "cl", cxx: "cl",
2020
#this path will change at some point when VC2022 is released I guess
21-
environment_script: "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Auxiliary/Build/vcvars64.bat"
21+
environment_script: "C:/Program Files (x86)/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat"
2222
}
2323
- {
2424
name: "Ubuntu gcc",
@@ -31,9 +31,9 @@ jobs:
3131
build_type: "Release", cc: "clang", cxx: "clang++"
3232
}
3333
- {
34-
name: "MacOS clang",
34+
name: "MacOS gcc",
3535
os: macos-latest,
36-
build_type: "Release", cc: "clang", cxx: "clang++"
36+
build_type: "Release", cc: "gcc", cxx: "g++"
3737
}
3838
steps:
3939
- uses: actions/checkout@v2

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "doctest"]
22
path = doctest
33
url = https://github.com/onqtam/doctest.git
4+
[submodule "extern/cpp-uri-parser"]
5+
path = extern/cpp-uri-parser
6+
url = https://github.com/lpcvoid/cpp-uri-parser

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)

examples/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
set(EXAMPLE_SOURCES echo_server.cpp daytime_client.cpp threadpool.cpp time_client.cpp)
22

3+
if (WITH_HTTP)
4+
set(EXAMPLE_SOURCES ${EXAMPLE_SOURCES} http_client.cpp)
5+
endif()
6+
37
foreach (examplesource ${EXAMPLE_SOURCES})
48
string(REPLACE ".cpp" "" examplename ${examplesource})
59
add_executable(${examplename} ${examplesource})

examples/http_client.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#include "../src/netlib.hpp"
2+
#include "../src/http/client.hpp"
3+
#include "../src/http/http.hpp"
4+
#include <csignal>
5+
#include <iomanip>
6+
#include <iostream>
7+
8+
9+
void exit_handler(int s){
10+
std::cout << "Goodbye!" << std::endl;
11+
exit(EXIT_SUCCESS);
12+
}
13+
14+
int main(int argc, char** argv)
15+
{
16+
netlib::http::http_client client;
17+
18+
auto res = client.get("http://example.com");
19+
20+
if (res.second) {
21+
std::cerr << "Error: " << res.second.message() << std::endl;
22+
exit(1);
23+
}
24+
25+
std::cout << "Got HTTP response: " << res.first->response_code <<
26+
", version " << res.first->version.first << "." << res.first->version.second << std::endl;
27+
std::cout << "Header entries:" << std::endl;
28+
std::for_each(res.first->headers.begin(), res.first->headers.end(), [](auto header_entry) {
29+
std::cout << header_entry.first << " = " << header_entry.second << std::endl;
30+
});
31+
std::cout << "Body:" << std::endl;
32+
std::cout << res.first.value().body << std::endl;
33+
34+
signal(SIGINT, exit_handler);
35+
}

extern/cpp-uri-parser

Submodule cpp-uri-parser added at ed011f5

src/client.hpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313
namespace netlib {
1414

1515
using namespace std::chrono_literals;
16+
static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT = 1000ms;
1617

1718
class client {
18-
private:
19-
static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT = 1000ms;
2019
protected:
2120
std::optional<netlib::socket> _socket;
2221
addrinfo *_endpoint_addr = nullptr;

src/http/client.hpp

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

src/socket.hpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ using ssize_t = signed long long int;
2525
#define MSG_NOSIGNAL 0
2626
//poll implementation
2727
#define poll_syscall ::WSAPoll
28+
//we ignore unused parameter warning on windows, missing impls
29+
#pragma warning(disable: 4100)
30+
//use unsafe functions under windows
31+
#define _CRT_SECURE_NO_WARNINGS
32+
#pragma warning(disable:4996)
33+
#pragma warning(disable:4267)
34+
#pragma warning(disable:4244)
35+
2836
#else
2937
// headers
3038
#include <arpa/inet.h>
@@ -42,7 +50,9 @@ using socket_t = int32_t;
4250
// this is actually a nice one to have
4351
#define INVALID_SOCKET (-1)
4452
#ifdef __APPLE__
45-
#define MSG_NOSIGNAL SO_NOSIGPIPE
53+
#ifndef MSG_NOSIGNAL
54+
#define MSG_NOSIGNAL SO_NOSIGPIPE
55+
#endif
4656
#endif
4757
#endif
4858

0 commit comments

Comments
 (0)