• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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