advanced_server.cpp 19 KB

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