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: Advanced server
13 //
14 //------------------------------------------------------------------------------
15
16 #include <boost/beast/core.hpp>
17 #include <boost/beast/http.hpp>
18 #include <boost/beast/websocket.hpp>
19 #include <boost/beast/version.hpp>
20 #include <boost/asio/bind_executor.hpp>
21 #include <boost/asio/dispatch.hpp>
22 #include <boost/asio/signal_set.hpp>
23 #include <boost/asio/strand.hpp>
24 #include <boost/make_unique.hpp>
25 #include <boost/optional.hpp>
26 #include <algorithm>
27 #include <cstdlib>
28 #include <functional>
29 #include <iostream>
30 #include <memory>
31 #include <string>
32 #include <thread>
33 #include <vector>
34
35 namespace beast = boost::beast; // from <boost/beast.hpp>
36 namespace http = beast::http; // from <boost/beast/http.hpp>
37 namespace websocket = beast::websocket; // from <boost/beast/websocket.hpp>
38 namespace net = boost::asio; // from <boost/asio.hpp>
39 using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
40
41 // Return a reasonable mime type based on the extension of a file.
42 beast::string_view
mime_type(beast::string_view path)43 mime_type(beast::string_view path)
44 {
45 using beast::iequals;
46 auto const ext = [&path]
47 {
48 auto const pos = path.rfind(".");
49 if(pos == beast::string_view::npos)
50 return beast::string_view{};
51 return path.substr(pos);
52 }();
53 if(iequals(ext, ".htm")) return "text/html";
54 if(iequals(ext, ".html")) return "text/html";
55 if(iequals(ext, ".php")) return "text/html";
56 if(iequals(ext, ".css")) return "text/css";
57 if(iequals(ext, ".txt")) return "text/plain";
58 if(iequals(ext, ".js")) return "application/javascript";
59 if(iequals(ext, ".json")) return "application/json";
60 if(iequals(ext, ".xml")) return "application/xml";
61 if(iequals(ext, ".swf")) return "application/x-shockwave-flash";
62 if(iequals(ext, ".flv")) return "video/x-flv";
63 if(iequals(ext, ".png")) return "image/png";
64 if(iequals(ext, ".jpe")) return "image/jpeg";
65 if(iequals(ext, ".jpeg")) return "image/jpeg";
66 if(iequals(ext, ".jpg")) return "image/jpeg";
67 if(iequals(ext, ".gif")) return "image/gif";
68 if(iequals(ext, ".bmp")) return "image/bmp";
69 if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon";
70 if(iequals(ext, ".tiff")) return "image/tiff";
71 if(iequals(ext, ".tif")) return "image/tiff";
72 if(iequals(ext, ".svg")) return "image/svg+xml";
73 if(iequals(ext, ".svgz")) return "image/svg+xml";
74 return "application/text";
75 }
76
77 // Append an HTTP rel-path to a local filesystem path.
78 // The returned path is normalized for the platform.
79 std::string
path_cat(beast::string_view base,beast::string_view path)80 path_cat(
81 beast::string_view base,
82 beast::string_view path)
83 {
84 if(base.empty())
85 return std::string(path);
86 std::string result(base);
87 #ifdef BOOST_MSVC
88 char constexpr path_separator = '\\';
89 if(result.back() == path_separator)
90 result.resize(result.size() - 1);
91 result.append(path.data(), path.size());
92 for(auto& c : result)
93 if(c == '/')
94 c = path_separator;
95 #else
96 char constexpr path_separator = '/';
97 if(result.back() == path_separator)
98 result.resize(result.size() - 1);
99 result.append(path.data(), path.size());
100 #endif
101 return result;
102 }
103
104 // This function produces an HTTP response for the given
105 // request. The type of the response object depends on the
106 // contents of the request, so the interface requires the
107 // caller to pass a generic lambda for receiving the response.
108 template<
109 class Body, class Allocator,
110 class Send>
111 void
handle_request(beast::string_view doc_root,http::request<Body,http::basic_fields<Allocator>> && req,Send && send)112 handle_request(
113 beast::string_view doc_root,
114 http::request<Body, http::basic_fields<Allocator>>&& req,
115 Send&& send)
116 {
117 // Returns a bad request response
118 auto const bad_request =
119 [&req](beast::string_view why)
120 {
121 http::response<http::string_body> res{http::status::bad_request, req.version()};
122 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
123 res.set(http::field::content_type, "text/html");
124 res.keep_alive(req.keep_alive());
125 res.body() = std::string(why);
126 res.prepare_payload();
127 return res;
128 };
129
130 // Returns a not found response
131 auto const not_found =
132 [&req](beast::string_view target)
133 {
134 http::response<http::string_body> res{http::status::not_found, req.version()};
135 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
136 res.set(http::field::content_type, "text/html");
137 res.keep_alive(req.keep_alive());
138 res.body() = "The resource '" + std::string(target) + "' was not found.";
139 res.prepare_payload();
140 return res;
141 };
142
143 // Returns a server error response
144 auto const server_error =
145 [&req](beast::string_view what)
146 {
147 http::response<http::string_body> res{http::status::internal_server_error, req.version()};
148 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
149 res.set(http::field::content_type, "text/html");
150 res.keep_alive(req.keep_alive());
151 res.body() = "An error occurred: '" + std::string(what) + "'";
152 res.prepare_payload();
153 return res;
154 };
155
156 // Make sure we can handle the method
157 if( req.method() != http::verb::get &&
158 req.method() != http::verb::head)
159 return send(bad_request("Unknown HTTP-method"));
160
161 // Request path must be absolute and not contain "..".
162 if( req.target().empty() ||
163 req.target()[0] != '/' ||
164 req.target().find("..") != beast::string_view::npos)
165 return send(bad_request("Illegal request-target"));
166
167 // Build the path to the requested file
168 std::string path = path_cat(doc_root, req.target());
169 if(req.target().back() == '/')
170 path.append("index.html");
171
172 // Attempt to open the file
173 beast::error_code ec;
174 http::file_body::value_type body;
175 body.open(path.c_str(), beast::file_mode::scan, ec);
176
177 // Handle the case where the file doesn't exist
178 if(ec == beast::errc::no_such_file_or_directory)
179 return send(not_found(req.target()));
180
181 // Handle an unknown error
182 if(ec)
183 return send(server_error(ec.message()));
184
185 // Cache the size since we need it after the move
186 auto const size = body.size();
187
188 // Respond to HEAD request
189 if(req.method() == http::verb::head)
190 {
191 http::response<http::empty_body> res{http::status::ok, req.version()};
192 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
193 res.set(http::field::content_type, mime_type(path));
194 res.content_length(size);
195 res.keep_alive(req.keep_alive());
196 return send(std::move(res));
197 }
198
199 // Respond to GET request
200 http::response<http::file_body> res{
201 std::piecewise_construct,
202 std::make_tuple(std::move(body)),
203 std::make_tuple(http::status::ok, req.version())};
204 res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
205 res.set(http::field::content_type, mime_type(path));
206 res.content_length(size);
207 res.keep_alive(req.keep_alive());
208 return send(std::move(res));
209 }
210
211 //------------------------------------------------------------------------------
212
213 // Report a failure
214 void
fail(beast::error_code ec,char const * what)215 fail(beast::error_code ec, char const* what)
216 {
217 std::cerr << what << ": " << ec.message() << "\n";
218 }
219
220 // Echoes back all received WebSocket messages
221 class websocket_session : public std::enable_shared_from_this<websocket_session>
222 {
223 websocket::stream<beast::tcp_stream> ws_;
224 beast::flat_buffer buffer_;
225
226 public:
227 // Take ownership of the socket
228 explicit
websocket_session(tcp::socket && socket)229 websocket_session(tcp::socket&& socket)
230 : ws_(std::move(socket))
231 {
232 }
233
234 // Start the asynchronous accept operation
235 template<class Body, class Allocator>
236 void
do_accept(http::request<Body,http::basic_fields<Allocator>> req)237 do_accept(http::request<Body, http::basic_fields<Allocator>> req)
238 {
239 // Set suggested timeout settings for the websocket
240 ws_.set_option(
241 websocket::stream_base::timeout::suggested(
242 beast::role_type::server));
243
244 // Set a decorator to change the Server of the handshake
245 ws_.set_option(websocket::stream_base::decorator(
246 [](websocket::response_type& res)
247 {
248 res.set(http::field::server,
249 std::string(BOOST_BEAST_VERSION_STRING) +
250 " advanced-server");
251 }));
252
253 // Accept the websocket handshake
254 ws_.async_accept(
255 req,
256 beast::bind_front_handler(
257 &websocket_session::on_accept,
258 shared_from_this()));
259 }
260
261 private:
262 void
on_accept(beast::error_code ec)263 on_accept(beast::error_code ec)
264 {
265 if(ec)
266 return fail(ec, "accept");
267
268 // Read a message
269 do_read();
270 }
271
272 void
do_read()273 do_read()
274 {
275 // Read a message into our buffer
276 ws_.async_read(
277 buffer_,
278 beast::bind_front_handler(
279 &websocket_session::on_read,
280 shared_from_this()));
281 }
282
283 void
on_read(beast::error_code ec,std::size_t bytes_transferred)284 on_read(
285 beast::error_code ec,
286 std::size_t bytes_transferred)
287 {
288 boost::ignore_unused(bytes_transferred);
289
290 // This indicates that the websocket_session was closed
291 if(ec == websocket::error::closed)
292 return;
293
294 if(ec)
295 fail(ec, "read");
296
297 // Echo the message
298 ws_.text(ws_.got_text());
299 ws_.async_write(
300 buffer_.data(),
301 beast::bind_front_handler(
302 &websocket_session::on_write,
303 shared_from_this()));
304 }
305
306 void
on_write(beast::error_code ec,std::size_t bytes_transferred)307 on_write(
308 beast::error_code ec,
309 std::size_t bytes_transferred)
310 {
311 boost::ignore_unused(bytes_transferred);
312
313 if(ec)
314 return fail(ec, "write");
315
316 // Clear the buffer
317 buffer_.consume(buffer_.size());
318
319 // Do another read
320 do_read();
321 }
322 };
323
324 //------------------------------------------------------------------------------
325
326 // Handles an HTTP server connection
327 class http_session : public std::enable_shared_from_this<http_session>
328 {
329 // This queue is used for HTTP pipelining.
330 class queue
331 {
332 enum
333 {
334 // Maximum number of responses we will queue
335 limit = 8
336 };
337
338 // The type-erased, saved work item
339 struct work
340 {
341 virtual ~work() = default;
342 virtual void operator()() = 0;
343 };
344
345 http_session& self_;
346 std::vector<std::unique_ptr<work>> items_;
347
348 public:
349 explicit
queue(http_session & self)350 queue(http_session& self)
351 : self_(self)
352 {
353 static_assert(limit > 0, "queue limit must be positive");
354 items_.reserve(limit);
355 }
356
357 // Returns `true` if we have reached the queue limit
358 bool
is_full() const359 is_full() const
360 {
361 return items_.size() >= limit;
362 }
363
364 // Called when a message finishes sending
365 // Returns `true` if the caller should initiate a read
366 bool
on_write()367 on_write()
368 {
369 BOOST_ASSERT(! items_.empty());
370 auto const was_full = is_full();
371 items_.erase(items_.begin());
372 if(! items_.empty())
373 (*items_.front())();
374 return was_full;
375 }
376
377 // Called by the HTTP handler to send a response.
378 template<bool isRequest, class Body, class Fields>
379 void
operator ()(http::message<isRequest,Body,Fields> && msg)380 operator()(http::message<isRequest, Body, Fields>&& msg)
381 {
382 // This holds a work item
383 struct work_impl : work
384 {
385 http_session& self_;
386 http::message<isRequest, Body, Fields> msg_;
387
388 work_impl(
389 http_session& self,
390 http::message<isRequest, Body, Fields>&& msg)
391 : self_(self)
392 , msg_(std::move(msg))
393 {
394 }
395
396 void
397 operator()()
398 {
399 http::async_write(
400 self_.stream_,
401 msg_,
402 beast::bind_front_handler(
403 &http_session::on_write,
404 self_.shared_from_this(),
405 msg_.need_eof()));
406 }
407 };
408
409 // Allocate and store the work
410 items_.push_back(
411 boost::make_unique<work_impl>(self_, std::move(msg)));
412
413 // If there was no previous work, start this one
414 if(items_.size() == 1)
415 (*items_.front())();
416 }
417 };
418
419 beast::tcp_stream stream_;
420 beast::flat_buffer buffer_;
421 std::shared_ptr<std::string const> doc_root_;
422 queue queue_;
423
424 // The parser is stored in an optional container so we can
425 // construct it from scratch it at the beginning of each new message.
426 boost::optional<http::request_parser<http::string_body>> parser_;
427
428 public:
429 // Take ownership of the socket
http_session(tcp::socket && socket,std::shared_ptr<std::string const> const & doc_root)430 http_session(
431 tcp::socket&& socket,
432 std::shared_ptr<std::string const> const& doc_root)
433 : stream_(std::move(socket))
434 , doc_root_(doc_root)
435 , queue_(*this)
436 {
437 }
438
439 // Start the session
440 void
run()441 run()
442 {
443 // We need to be executing within a strand to perform async operations
444 // on the I/O objects in this session. Although not strictly necessary
445 // for single-threaded contexts, this example code is written to be
446 // thread-safe by default.
447 net::dispatch(
448 stream_.get_executor(),
449 beast::bind_front_handler(
450 &http_session::do_read,
451 this->shared_from_this()));
452 }
453
454
455 private:
456 void
do_read()457 do_read()
458 {
459 // Construct a new parser for each message
460 parser_.emplace();
461
462 // Apply a reasonable limit to the allowed size
463 // of the body in bytes to prevent abuse.
464 parser_->body_limit(10000);
465
466 // Set the timeout.
467 stream_.expires_after(std::chrono::seconds(30));
468
469 // Read a request using the parser-oriented interface
470 http::async_read(
471 stream_,
472 buffer_,
473 *parser_,
474 beast::bind_front_handler(
475 &http_session::on_read,
476 shared_from_this()));
477 }
478
479 void
on_read(beast::error_code ec,std::size_t bytes_transferred)480 on_read(beast::error_code ec, std::size_t bytes_transferred)
481 {
482 boost::ignore_unused(bytes_transferred);
483
484 // This means they closed the connection
485 if(ec == http::error::end_of_stream)
486 return do_close();
487
488 if(ec)
489 return fail(ec, "read");
490
491 // See if it is a WebSocket Upgrade
492 if(websocket::is_upgrade(parser_->get()))
493 {
494 // Create a websocket session, transferring ownership
495 // of both the socket and the HTTP request.
496 std::make_shared<websocket_session>(
497 stream_.release_socket())->do_accept(parser_->release());
498 return;
499 }
500
501 // Send the response
502 handle_request(*doc_root_, parser_->release(), queue_);
503
504 // If we aren't at the queue limit, try to pipeline another request
505 if(! queue_.is_full())
506 do_read();
507 }
508
509 void
on_write(bool close,beast::error_code ec,std::size_t bytes_transferred)510 on_write(bool close, beast::error_code ec, std::size_t bytes_transferred)
511 {
512 boost::ignore_unused(bytes_transferred);
513
514 if(ec)
515 return fail(ec, "write");
516
517 if(close)
518 {
519 // This means we should close the connection, usually because
520 // the response indicated the "Connection: close" semantic.
521 return do_close();
522 }
523
524 // Inform the queue that a write completed
525 if(queue_.on_write())
526 {
527 // Read another request
528 do_read();
529 }
530 }
531
532 void
do_close()533 do_close()
534 {
535 // Send a TCP shutdown
536 beast::error_code ec;
537 stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
538
539 // At this point the connection is closed gracefully
540 }
541 };
542
543 //------------------------------------------------------------------------------
544
545 // Accepts incoming connections and launches the sessions
546 class listener : public std::enable_shared_from_this<listener>
547 {
548 net::io_context& ioc_;
549 tcp::acceptor acceptor_;
550 std::shared_ptr<std::string const> doc_root_;
551
552 public:
listener(net::io_context & ioc,tcp::endpoint endpoint,std::shared_ptr<std::string const> const & doc_root)553 listener(
554 net::io_context& ioc,
555 tcp::endpoint endpoint,
556 std::shared_ptr<std::string const> const& doc_root)
557 : ioc_(ioc)
558 , acceptor_(net::make_strand(ioc))
559 , doc_root_(doc_root)
560 {
561 beast::error_code ec;
562
563 // Open the acceptor
564 acceptor_.open(endpoint.protocol(), ec);
565 if(ec)
566 {
567 fail(ec, "open");
568 return;
569 }
570
571 // Allow address reuse
572 acceptor_.set_option(net::socket_base::reuse_address(true), ec);
573 if(ec)
574 {
575 fail(ec, "set_option");
576 return;
577 }
578
579 // Bind to the server address
580 acceptor_.bind(endpoint, ec);
581 if(ec)
582 {
583 fail(ec, "bind");
584 return;
585 }
586
587 // Start listening for connections
588 acceptor_.listen(
589 net::socket_base::max_listen_connections, ec);
590 if(ec)
591 {
592 fail(ec, "listen");
593 return;
594 }
595 }
596
597 // Start accepting incoming connections
598 void
run()599 run()
600 {
601 // We need to be executing within a strand to perform async operations
602 // on the I/O objects in this session. Although not strictly necessary
603 // for single-threaded contexts, this example code is written to be
604 // thread-safe by default.
605 net::dispatch(
606 acceptor_.get_executor(),
607 beast::bind_front_handler(
608 &listener::do_accept,
609 this->shared_from_this()));
610 }
611
612 private:
613 void
do_accept()614 do_accept()
615 {
616 // The new connection gets its own strand
617 acceptor_.async_accept(
618 net::make_strand(ioc_),
619 beast::bind_front_handler(
620 &listener::on_accept,
621 shared_from_this()));
622 }
623
624 void
on_accept(beast::error_code ec,tcp::socket socket)625 on_accept(beast::error_code ec, tcp::socket socket)
626 {
627 if(ec)
628 {
629 fail(ec, "accept");
630 }
631 else
632 {
633 // Create the http session and run it
634 std::make_shared<http_session>(
635 std::move(socket),
636 doc_root_)->run();
637 }
638
639 // Accept another connection
640 do_accept();
641 }
642 };
643
644 //------------------------------------------------------------------------------
645
main(int argc,char * argv[])646 int main(int argc, char* argv[])
647 {
648 // Check command line arguments.
649 if (argc != 5)
650 {
651 std::cerr <<
652 "Usage: advanced-server <address> <port> <doc_root> <threads>\n" <<
653 "Example:\n" <<
654 " advanced-server 0.0.0.0 8080 . 1\n";
655 return EXIT_FAILURE;
656 }
657 auto const address = net::ip::make_address(argv[1]);
658 auto const port = static_cast<unsigned short>(std::atoi(argv[2]));
659 auto const doc_root = std::make_shared<std::string>(argv[3]);
660 auto const threads = std::max<int>(1, std::atoi(argv[4]));
661
662 // The io_context is required for all I/O
663 net::io_context ioc{threads};
664
665 // Create and launch a listening port
666 std::make_shared<listener>(
667 ioc,
668 tcp::endpoint{address, port},
669 doc_root)->run();
670
671 // Capture SIGINT and SIGTERM to perform a clean shutdown
672 net::signal_set signals(ioc, SIGINT, SIGTERM);
673 signals.async_wait(
674 [&](beast::error_code const&, int)
675 {
676 // Stop the `io_context`. This will cause `run()`
677 // to return immediately, eventually destroying the
678 // `io_context` and all of the sockets in it.
679 ioc.stop();
680 });
681
682 // Run the I/O service on the requested number of threads
683 std::vector<std::thread> v;
684 v.reserve(threads - 1);
685 for(auto i = threads - 1; i > 0; --i)
686 v.emplace_back(
687 [&ioc]
688 {
689 ioc.run();
690 });
691 ioc.run();
692
693 // (If we get here, it means we got a SIGINT or SIGTERM)
694
695 // Block until all the threads exit
696 for(auto& t : v)
697 t.join();
698
699 return EXIT_SUCCESS;
700 }
701