OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
http_client.hpp
1#pragma once
2#include <ossia/detail/config.hpp>
3
4#include <ossia/detail/fmt.hpp>
6#include <ossia/detail/parse_relax.hpp>
7
8#include <boost/asio.hpp>
9#include <boost/asio/placeholders.hpp>
10
11#include <utility>
12
13namespace ossia::net
14{
15using tcp = boost::asio::ip::tcp;
16
17template <typename Fun, typename Err>
18class http_get_request : public std::enable_shared_from_this<http_get_request<Fun, Err>>
19{
20 fmt::memory_buffer m_request;
21
22public:
23 using std::enable_shared_from_this<http_get_request<Fun, Err>>::shared_from_this;
24 http_get_request(
25 Fun f, Err err, boost::asio::io_context& ctx, const std::string& server,
26 const std::string& path, std::string_view verb = "GET")
27 : m_resolver(ctx)
28 , m_socket(ctx)
29 , m_fun{std::move(f)}
30 , m_err{std::move(err)}
31 {
32 m_request.reserve(100 + server.size() + path.size());
33 m_response.prepare(Fun::reserve_expect);
34 fmt::format_to(fmt::appender(m_request), "{} ", verb);
35 // Technically other characters should be encoded... but
36 // they aren't legal in OSC address patterns.
37 for(auto c : path)
38 if(c != ' ')
39 fmt::format_to(fmt::appender(m_request), "{}", c);
40 else
41 fmt::format_to(fmt::appender(m_request), "%20");
42
43 fmt::format_to(
44 fmt::appender(m_request),
45 " HTTP/1.1\r\n"
46 "Host: {}\r\n"
47 "Accept: */*\r\n"
48 "Connection: close\r\n\r\n",
49 server);
50 }
51
52 void resolve(const std::string& server, const std::string& port)
53 {
54 m_resolver.async_resolve(
55 server, port,
56 [self = this->shared_from_this()](
57 const boost::system::error_code& err,
58 const tcp::resolver::results_type& endpoints) {
59 self->handle_resolve(err, endpoints);
60 });
61 }
62
63 void close() { m_socket.close(); }
64
65private:
66 void handle_resolve(
67 const boost::system::error_code& err, const tcp::resolver::results_type& endpoints)
68 {
69 if(!err)
70 {
71 boost::asio::async_connect(
72 m_socket, endpoints,
73 [self = shared_from_this()](const boost::system::error_code& err, auto&&...) {
74 self->handle_connect(err);
75 });
76 }
77 else
78 {
79 ossia::logger().error("HTTP Error: {}", err.message());
80 m_err(*this);
81 }
82 }
83
84 void handle_connect(const boost::system::error_code& err)
85 {
86 if(!err)
87 {
88 boost::asio::const_buffer request(m_request.data(), m_request.size());
89 boost::asio::async_write(
90 m_socket, request,
91 [self = shared_from_this()](
92 const boost::system::error_code& err, std::size_t size) {
93 self->handle_write_request(err, size);
94 });
95 }
96 else
97 {
98 ossia::logger().error("HTTP Error: {}", err.message());
99 m_err(*this);
100 }
101 }
102
103 void handle_write_request(const boost::system::error_code& err, std::size_t size)
104 {
105 if(!err)
106 {
107 boost::asio::async_read_until(
108 m_socket, m_response, "\r\n",
109 [self = shared_from_this()](
110 const boost::system::error_code& err, std::size_t size) {
111 self->handle_read_status_line(err, size);
112 });
113 }
114 else
115 {
116 ossia::logger().error("HTTP Error: {}", err.message());
117 m_err(*this);
118 }
119 }
120
121 void handle_read_status_line(const boost::system::error_code& err, std::size_t size)
122 {
123 if(!err)
124 {
125 // Check that response is OK.
126 std::istream response_stream(&m_response);
127 std::string http_version;
128 response_stream >> http_version;
129 unsigned int status_code;
130 response_stream >> status_code;
131 std::string status_message;
132 std::getline(response_stream, status_message);
133 if(!response_stream || http_version.substr(0, 5) != "HTTP/")
134 {
135 ossia::logger().error("HTTP Error: Invalid response");
136 return;
137 }
138 if(status_code != 200)
139 {
140 ossia::logger().error("HTTP Error: status code {}", status_code);
141 return;
142 }
143
144 // Read the response headers, which are terminated by a blank line.
145 boost::asio::async_read_until(
146 m_socket, m_response, "\r\n\r\n",
147 [self = shared_from_this()](
148 const boost::system::error_code& err, std::size_t size) {
149 self->handle_read_headers(err, size);
150 });
151 }
152 else
153 {
154 ossia::logger().error("HTTP Error: {}", err.message());
155 m_err(*this);
156 }
157 }
158
159 void handle_read_headers(const boost::system::error_code& err, std::size_t size)
160 {
161 if(!err)
162 {
163 // Process the response headers.
164 std::istream response_stream(&m_response);
165 std::string header;
166 while(std::getline(response_stream, header) && header != "\r")
167 {
168 if(header.starts_with("Content-Length: "))
169 {
170 std::string_view sz(header.begin() + strlen("Content-Length: "), header.end());
171 if(auto num = ossia::parse_relax<int>(sz))
172 {
173 m_contentLength = *num;
174 }
175 else
176 {
177 ossia::logger().error("Invalid HTTP Content-length: {}", sz);
178 return;
179 }
180 }
181 }
182
183 if(m_contentLength > 0)
184 {
185 if(m_contentLength == m_response.size())
186 {
187 finish_read(boost::asio::error::eof, size);
188 }
189 else
190 {
191 boost::asio::async_read(
192 m_socket, m_response, boost::asio::transfer_exactly(m_contentLength - m_response.size()),
193 [self = shared_from_this()](
194 const boost::system::error_code& err, std::size_t size) {
195 self->handle_read_content(err, size);
196 });
197 }
198 }
199 else
200 {
201 // Start reading remaining data until EOF.
202 boost::asio::async_read(
203 m_socket, m_response, boost::asio::transfer_all(),
204 [self = shared_from_this()](
205 const boost::system::error_code& err, std::size_t size) {
206 self->handle_read_content(err, size);
207 });
208 }
209 }
210 else
211 {
212 ossia::logger().error("HTTP Error: {}", err.message());
213 m_err(*this);
214 }
215 }
216
217 void handle_read_content(const boost::system::error_code& err, std::size_t size)
218 {
219 if(!err || err == boost::asio::error::eof)
220 finish_read(err, size);
221 else
222 {
223 ossia::logger().error("HTTP Error: {}", err.message());
224 m_err(*this);
225 }
226 }
227
228 void finish_read(const boost::system::error_code& err, std::size_t size)
229 {
230 const auto& dat = m_response.data();
231 auto begin = boost::asio::buffers_begin(dat);
232 auto end = boost::asio::buffers_end(dat);
233 auto sz = end - begin;
234 std::string str;
235 str.reserve(sz + 16); // for RapidJSON simd parsing which reads past bounds
236 str.assign(begin, end);
237 m_fun(*this, str);
238 close();
239 }
240
241 tcp::resolver m_resolver;
242 tcp::socket m_socket;
243 boost::asio::streambuf m_response;
244 int m_contentLength{-1};
245 Fun m_fun;
246 Err m_err;
247};
248}
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:118