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)
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), "GET ");
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: {}"
47 "\r\n"
48 "Accept: */*\r\n"
49 "Connection: close\r\n\r\n",
50 server);
51 }
52
53 void resolve(const std::string& server, const std::string& port)
54 {
55 m_resolver.async_resolve(
56 server, port,
57 [self = this->shared_from_this()](
58 const boost::system::error_code& err,
59 const tcp::resolver::results_type& endpoints) {
60 self->handle_resolve(err, endpoints);
61 });
62 }
63
64 void close() { m_socket.close(); }
65
66private:
67 void handle_resolve(
68 const boost::system::error_code& err, const tcp::resolver::results_type& endpoints)
69 {
70 if(!err)
71 {
72 boost::asio::async_connect(
73 m_socket, endpoints,
74 [self = shared_from_this()](const boost::system::error_code& err, auto&&...) {
75 self->handle_connect(err);
76 });
77 }
78 else
79 {
80 ossia::logger().error("HTTP Error: {}", err.message());
81 m_err(*this);
82 }
83 }
84
85 void handle_connect(const boost::system::error_code& err)
86 {
87 if(!err)
88 {
89 boost::asio::const_buffer request(m_request.data(), m_request.size());
90 boost::asio::async_write(
91 m_socket, request,
92 [self = shared_from_this()](
93 const boost::system::error_code& err, std::size_t size) {
94 self->handle_write_request(err, size);
95 });
96 }
97 else
98 {
99 ossia::logger().error("HTTP Error: {}", err.message());
100 m_err(*this);
101 }
102 }
103
104 void handle_write_request(const boost::system::error_code& err, std::size_t size)
105 {
106 if(!err)
107 {
108 boost::asio::async_read_until(
109 m_socket, m_response, "\r\n",
110 [self = shared_from_this()](
111 const boost::system::error_code& err, std::size_t size) {
112 self->handle_read_status_line(err, size);
113 });
114 }
115 else
116 {
117 ossia::logger().error("HTTP Error: {}", err.message());
118 m_err(*this);
119 }
120 }
121
122 void handle_read_status_line(const boost::system::error_code& err, std::size_t size)
123 {
124 if(!err)
125 {
126 // Check that response is OK.
127 std::istream response_stream(&m_response);
128 std::string http_version;
129 response_stream >> http_version;
130 unsigned int status_code;
131 response_stream >> status_code;
132 std::string status_message;
133 std::getline(response_stream, status_message);
134 if(!response_stream || http_version.substr(0, 5) != "HTTP/")
135 {
136 ossia::logger().error("HTTP Error: Invalid response");
137 return;
138 }
139 if(status_code != 200)
140 {
141 ossia::logger().error("HTTP Error: status code {}", status_code);
142 return;
143 }
144
145 // Read the response headers, which are terminated by a blank line.
146 boost::asio::async_read_until(
147 m_socket, m_response, "\r\n\r\n",
148 [self = shared_from_this()](
149 const boost::system::error_code& err, std::size_t size) {
150 self->handle_read_headers(err, size);
151 });
152 }
153 else
154 {
155 ossia::logger().error("HTTP Error: {}", err.message());
156 m_err(*this);
157 }
158 }
159
160 void handle_read_headers(const boost::system::error_code& err, std::size_t size)
161 {
162 if(!err)
163 {
164 // Process the response headers.
165 std::istream response_stream(&m_response);
166 std::string header;
167 while(std::getline(response_stream, header) && header != "\r")
168 {
169 if(header.starts_with("Content-Length: "))
170 {
171 std::string_view sz(header.begin() + strlen("Content-Length: "), header.end());
172 if(auto num = ossia::parse_relax<int>(sz))
173 {
174 m_contentLength = *num;
175 }
176 else
177 {
178 ossia::logger().error("Invalid HTTP Content-length: {}", sz);
179 return;
180 }
181 }
182 }
183
184 if(m_contentLength > 0)
185 {
186 if(m_contentLength == m_response.size())
187 {
188 finish_read(boost::asio::error::eof, size);
189 }
190 else
191 {
192 boost::asio::async_read(
193 m_socket, m_response, boost::asio::transfer_exactly(m_contentLength - m_response.size()),
194 [self = shared_from_this()](
195 const boost::system::error_code& err, std::size_t size) {
196 self->handle_read_content(err, size);
197 });
198 }
199 }
200 else
201 {
202 // Start reading remaining data until EOF.
203 boost::asio::async_read(
204 m_socket, m_response, boost::asio::transfer_all(),
205 [self = shared_from_this()](
206 const boost::system::error_code& err, std::size_t size) {
207 self->handle_read_content(err, size);
208 });
209 }
210 }
211 else
212 {
213 ossia::logger().error("HTTP Error: {}", err.message());
214 m_err(*this);
215 }
216 }
217
218 void handle_read_content(const boost::system::error_code& err, std::size_t size)
219 {
220 if(!err || err == boost::asio::error::eof)
221 finish_read(err, size);
222 else
223 {
224 ossia::logger().error("HTTP Error: {}", err.message());
225 m_err(*this);
226 }
227 }
228
229 void finish_read(const boost::system::error_code& err, std::size_t size)
230 {
231 const auto& dat = m_response.data();
232 auto begin = boost::asio::buffers_begin(dat);
233 auto end = boost::asio::buffers_end(dat);
234 auto sz = end - begin;
235 std::string str;
236 str.reserve(sz + 16); // for RapidJSON simd parsing which reads past bounds
237 str.assign(begin, end);
238 m_fun(*this, str);
239 close();
240 }
241
242 tcp::resolver m_resolver;
243 tcp::socket m_socket;
244 boost::asio::streambuf m_response;
245 int m_contentLength{-1};
246 Fun m_fun;
247 Err m_err;
248};
249}
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:118