1 //
2 // Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com)
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 // Official repository: https://github.com/boostorg/beast
8 //
9
10 //------------------------------------------------------------------------------
11 //
12 // Example: HTTP server, coroutine
13 //
14 //------------------------------------------------------------------------------
15
16 #include <boost/beast/core.hpp>
17 #include <boost/beast/http.hpp>
18 #include <boost/beast/version.hpp>
19 #include <boost/asio/ip/tcp.hpp>
20 #include <boost/asio/spawn.hpp>
21 #include <boost/config.hpp>
22 #include <algorithm>
23 #include <cstdlib>
24 #include <iostream>
25 #include <memory>
26 #include <string>
27 #include <thread>
28 #include <vector>
29
30 namespace beast = boost::beast; // from <boost/beast.hpp>
31 namespace http = beast::http; // from <boost/beast/http.hpp>
32 namespace net = boost::asio; // from <boost/asio.hpp>
33 using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
34
35 // Return a reasonable mime type based on the extension of a file.
36 beast::string_view
mime_type(beast::string_view path)37 mime_type(beast::string_view path)
38 {
39 using beast::iequals;
40 auto const ext = [&path]
41 {
42 auto const pos = path.rfind(".");
43 if(pos == beast::string_view::npos)
44 return beast::string_view{};
45 return path.substr(pos);
46 }();
47 if(iequals(ext, ".htm")) return "text/html";
48 if(iequals(ext, ".html")) return "text/html";
49 if(iequals(ext, ".php")) return "text/html";
50 if(iequals(ext, ".css")) return "text/css";
51 if(iequals(ext, ".txt")) return "text/plain";
52 if(iequals(ext, ".js")) return "application/javascript";
53 if(iequals(ext, ".json")) return "application/json";
54 if(iequals(ext, ".xml")) return "application/xml";
55 if(iequals(ext, ".swf")) return "application/x-shockwave-flash";
56 if(iequals(ext, ".flv")) return "video/x-flv";
57 if(iequals(ext, ".png")) return "image/png";
58 if(iequals(ext, ".jpe")) return "image/jpeg";
59 if(iequals(ext, ".jpeg")) return "image/jpeg";
60 if(iequals(ext, ".jpg")) return "image/jpeg";
61 if(iequals(ext, ".gif")) return "image/gif";
62 if(iequals(ext, ".bmp")) return "image/bmp";
63 if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon";
64 if(iequals(ext, ".tiff")) return "image/tiff";
65 if(iequals(ext, ".tif")) return "image/tiff";
66 if(iequals(ext, ".svg")) return "image/svg+xml";
67 if(iequals(ext, ".svgz")) return "image/svg+xml";
68 return "application/text";
69 }
70
71 // Append an HTTP rel-path to a local filesystem path.
72 // The returned path is normalized for the platform.
73 std::string
path_cat(beast::string_view base,beast::string_view path)74 path_cat(
75 beast::string_view base,
76 beast::string_view path)
77 {
78 if(base.empty())
79 return std::string(path);
80 std::string result(base);
81 #ifdef BOOST_MSVC
82 char constexpr path_separator = '\\';
83 if(result.back() == path_separator)
84 result.resize(result.size() - 1);
85 result.append(path.data(), path.size());
86 for(auto& c : result)
87 if(c == '/')
88 c = path_separator;
89 #else
90 char constexpr path_separator = '/';
91 if(result.back() == path_separator)
92 result.resize(result.size() - 1);
93 result.append(path.data(), path.size());
94 #endif
95 return result;
96 }
97
98 // This function produces an HTTP response for the given
99 // request. The type of the response object depends on the
100 // contents of the request, so the interface requires the
101 // caller to pass a generic lambda for receiving the response.
102 template<
103 class Body, class Allocator,
104 class Send>
105 void
handle_request(beast::string_view doc_root,http::request<Body,http::basic_fields<Allocator>> && req,Send && send)106 handle_request(
107 beast::string_view doc_root,
108 http::request<Body, http::basic_fields<Allocator>>&& req,
109 Send&& send)
110 {
111 // Returns a bad request response
112 auto const bad_request =
113 [&req](beast::string_view why)
114 {
115 http::response<http::string_body> res{http::status::bad_request, req.version()};
116 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
117 res.set(http::field::content_type, "text/html");
118 res.keep_alive(req.keep_alive());
119 res.body() = std::string(why);
120 res.prepare_payload();
121 return res;
122 };
123
124 // Returns a not found response
125 auto const not_found =
126 [&req](beast::string_view target)
127 {
128 http::response<http::string_body> res{http::status::not_found, req.version()};
129 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
130 res.set(http::field::content_type, "text/html");
131 res.keep_alive(req.keep_alive());
132 res.body() = "The resource '" + std::string(target) + "' was not found.";
133 res.prepare_payload();
134 return res;
135 };
136
137 // Returns a server error response
138 auto const server_error =
139 [&req](beast::string_view what)
140 {
141 http::response<http::string_body> res{http::status::internal_server_error, req.version()};
142 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
143 res.set(http::field::content_type, "text/html");
144 res.keep_alive(req.keep_alive());
145 res.body() = "An error occurred: '" + std::string(what) + "'";
146 res.prepare_payload();
147 return res;
148 };
149
150 // Make sure we can handle the method
151 if( req.method() != http::verb::get &&
152 req.method() != http::verb::head)
153 return send(bad_request("Unknown HTTP-method"));
154
155 // Request path must be absolute and not contain "..".
156 if( req.target().empty() ||
157 req.target()[0] != '/' ||
158 req.target().find("..") != beast::string_view::npos)
159 return send(bad_request("Illegal request-target"));
160
161 // Build the path to the requested file
162 std::string path = path_cat(doc_root, req.target());
163 if(req.target().back() == '/')
164 path.append("index.html");
165
166 // Attempt to open the file
167 beast::error_code ec;
168 http::file_body::value_type body;
169 body.open(path.c_str(), beast::file_mode::scan, ec);
170
171 // Handle the case where the file doesn't exist
172 if(ec == beast::errc::no_such_file_or_directory)
173 return send(not_found(req.target()));
174
175 // Handle an unknown error
176 if(ec)
177 return send(server_error(ec.message()));
178
179 // Cache the size since we need it after the move
180 auto const size = body.size();
181
182 // Respond to HEAD request
183 if(req.method() == http::verb::head)
184 {
185 http::response<http::empty_body> res{http::status::ok, req.version()};
186 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
187 res.set(http::field::content_type, mime_type(path));
188 res.content_length(size);
189 res.keep_alive(req.keep_alive());
190 return send(std::move(res));
191 }
192
193 // Respond to GET request
194 http::response<http::file_body> res{
195 std::piecewise_construct,
196 std::make_tuple(std::move(body)),
197 std::make_tuple(http::status::ok, req.version())};
198 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
199 res.set(http::field::content_type, mime_type(path));
200 res.content_length(size);
201 res.keep_alive(req.keep_alive());
202 return send(std::move(res));
203 }
204
205 //------------------------------------------------------------------------------
206
207 // Report a failure
208 void
fail(beast::error_code ec,char const * what)209 fail(beast::error_code ec, char const* what)
210 {
211 std::cerr << what << ": " << ec.message() << "\n";
212 }
213
214 // This is the C++11 equivalent of a generic lambda.
215 // The function object is used to send an HTTP message.
216 struct send_lambda
217 {
218 beast::tcp_stream& stream_;
219 bool& close_;
220 beast::error_code& ec_;
221 net::yield_context yield_;
222
send_lambdasend_lambda223 send_lambda(
224 beast::tcp_stream& stream,
225 bool& close,
226 beast::error_code& ec,
227 net::yield_context yield)
228 : stream_(stream)
229 , close_(close)
230 , ec_(ec)
231 , yield_(yield)
232 {
233 }
234
235 template<bool isRequest, class Body, class Fields>
236 void
operator ()send_lambda237 operator()(http::message<isRequest, Body, Fields>&& msg) const
238 {
239 // Determine if we should close the connection after
240 close_ = msg.need_eof();
241
242 // We need the serializer here because the serializer requires
243 // a non-const file_body, and the message oriented version of
244 // http::write only works with const messages.
245 http::serializer<isRequest, Body, Fields> sr{msg};
246 http::async_write(stream_, sr, yield_[ec_]);
247 }
248 };
249
250 // Handles an HTTP server connection
251 void
do_session(beast::tcp_stream & stream,std::shared_ptr<std::string const> const & doc_root,net::yield_context yield)252 do_session(
253 beast::tcp_stream& stream,
254 std::shared_ptr<std::string const> const& doc_root,
255 net::yield_context yield)
256 {
257 bool close = false;
258 beast::error_code ec;
259
260 // This buffer is required to persist across reads
261 beast::flat_buffer buffer;
262
263 // This lambda is used to send messages
264 send_lambda lambda{stream, close, ec, yield};
265
266 for(;;)
267 {
268 // Set the timeout.
269 stream.expires_after(std::chrono::seconds(30));
270
271 // Read a request
272 http::request<http::string_body> req;
273 http::async_read(stream, buffer, req, yield[ec]);
274 if(ec == http::error::end_of_stream)
275 break;
276 if(ec)
277 return fail(ec, "read");
278
279 // Send the response
280 handle_request(*doc_root, std::move(req), lambda);
281 if(ec)
282 return fail(ec, "write");
283 if(close)
284 {
285 // This means we should close the connection, usually because
286 // the response indicated the "Connection: close" semantic.
287 break;
288 }
289 }
290
291 // Send a TCP shutdown
292 stream.socket().shutdown(tcp::socket::shutdown_send, ec);
293
294 // At this point the connection is closed gracefully
295 }
296
297 //------------------------------------------------------------------------------
298
299 // Accepts incoming connections and launches the sessions
300 void
do_listen(net::io_context & ioc,tcp::endpoint endpoint,std::shared_ptr<std::string const> const & doc_root,net::yield_context yield)301 do_listen(
302 net::io_context& ioc,
303 tcp::endpoint endpoint,
304 std::shared_ptr<std::string const> const& doc_root,
305 net::yield_context yield)
306 {
307 beast::error_code ec;
308
309 // Open the acceptor
310 tcp::acceptor acceptor(ioc);
311 acceptor.open(endpoint.protocol(), ec);
312 if(ec)
313 return fail(ec, "open");
314
315 // Allow address reuse
316 acceptor.set_option(net::socket_base::reuse_address(true), ec);
317 if(ec)
318 return fail(ec, "set_option");
319
320 // Bind to the server address
321 acceptor.bind(endpoint, ec);
322 if(ec)
323 return fail(ec, "bind");
324
325 // Start listening for connections
326 acceptor.listen(net::socket_base::max_listen_connections, ec);
327 if(ec)
328 return fail(ec, "listen");
329
330 for(;;)
331 {
332 tcp::socket socket(ioc);
333 acceptor.async_accept(socket, yield[ec]);
334 if(ec)
335 fail(ec, "accept");
336 else
337 boost::asio::spawn(
338 acceptor.get_executor(),
339 std::bind(
340 &do_session,
341 beast::tcp_stream(std::move(socket)),
342 doc_root,
343 std::placeholders::_1));
344 }
345 }
346
main(int argc,char * argv[])347 int main(int argc, char* argv[])
348 {
349 // Check command line arguments.
350 if (argc != 5)
351 {
352 std::cerr <<
353 "Usage: http-server-coro <address> <port> <doc_root> <threads>\n" <<
354 "Example:\n" <<
355 " http-server-coro 0.0.0.0 8080 . 1\n";
356 return EXIT_FAILURE;
357 }
358 auto const address = net::ip::make_address(argv[1]);
359 auto const port = static_cast<unsigned short>(std::atoi(argv[2]));
360 auto const doc_root = std::make_shared<std::string>(argv[3]);
361 auto const threads = std::max<int>(1, std::atoi(argv[4]));
362
363 // The io_context is required for all I/O
364 net::io_context ioc{threads};
365
366 // Spawn a listening port
367 boost::asio::spawn(ioc,
368 std::bind(
369 &do_listen,
370 std::ref(ioc),
371 tcp::endpoint{address, port},
372 doc_root,
373 std::placeholders::_1));
374
375 // Run the I/O service on the requested number of threads
376 std::vector<std::thread> v;
377 v.reserve(threads - 1);
378 for(auto i = threads - 1; i > 0; --i)
379 v.emplace_back(
380 [&ioc]
381 {
382 ioc.run();
383 });
384 ioc.run();
385
386 return EXIT_SUCCESS;
387 }
388