• 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: HTTP server, asynchronous
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/dispatch.hpp>
20 #include <boost/asio/strand.hpp>
21 #include <boost/config.hpp>
22 #include <algorithm>
23 #include <cstdlib>
24 #include <functional>
25 #include <iostream>
26 #include <memory>
27 #include <string>
28 #include <thread>
29 #include <vector>
30 
31 namespace beast = boost::beast;         // from <boost/beast.hpp>
32 namespace http = beast::http;           // from <boost/beast/http.hpp>
33 namespace net = boost::asio;            // from <boost/asio.hpp>
34 using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp>
35 
36 // Return a reasonable mime type based on the extension of a file.
37 beast::string_view
mime_type(beast::string_view path)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 
72 // Append an HTTP rel-path to a local filesystem path.
73 // The returned path is normalized for the platform.
74 std::string
path_cat(beast::string_view base,beast::string_view path)75 path_cat(
76     beast::string_view base,
77     beast::string_view path)
78 {
79     if(base.empty())
80         return std::string(path);
81     std::string result(base);
82 #ifdef BOOST_MSVC
83     char constexpr path_separator = '\\';
84     if(result.back() == path_separator)
85         result.resize(result.size() - 1);
86     result.append(path.data(), path.size());
87     for(auto& c : result)
88         if(c == '/')
89             c = path_separator;
90 #else
91     char constexpr path_separator = '/';
92     if(result.back() == path_separator)
93         result.resize(result.size() - 1);
94     result.append(path.data(), path.size());
95 #endif
96     return result;
97 }
98 
99 // This function produces an HTTP response for the given
100 // request. The type of the response object depends on the
101 // contents of the request, so the interface requires the
102 // caller to pass a generic lambda for receiving the response.
103 template<
104     class Body, class Allocator,
105     class Send>
106 void
handle_request(beast::string_view doc_root,http::request<Body,http::basic_fields<Allocator>> && req,Send && send)107 handle_request(
108     beast::string_view doc_root,
109     http::request<Body, http::basic_fields<Allocator>>&& req,
110     Send&& send)
111 {
112     // Returns a bad request response
113     auto const bad_request =
114     [&req](beast::string_view why)
115     {
116         http::response<http::string_body> res{http::status::bad_request, req.version()};
117         res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
118         res.set(http::field::content_type, "text/html");
119         res.keep_alive(req.keep_alive());
120         res.body() = std::string(why);
121         res.prepare_payload();
122         return res;
123     };
124 
125     // Returns a not found response
126     auto const not_found =
127     [&req](beast::string_view target)
128     {
129         http::response<http::string_body> res{http::status::not_found, req.version()};
130         res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
131         res.set(http::field::content_type, "text/html");
132         res.keep_alive(req.keep_alive());
133         res.body() = "The resource '" + std::string(target) + "' was not found.";
134         res.prepare_payload();
135         return res;
136     };
137 
138     // Returns a server error response
139     auto const server_error =
140     [&req](beast::string_view what)
141     {
142         http::response<http::string_body> res{http::status::internal_server_error, req.version()};
143         res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
144         res.set(http::field::content_type, "text/html");
145         res.keep_alive(req.keep_alive());
146         res.body() = "An error occurred: '" + std::string(what) + "'";
147         res.prepare_payload();
148         return res;
149     };
150 
151     // Make sure we can handle the method
152     if( req.method() != http::verb::get &&
153         req.method() != http::verb::head)
154         return send(bad_request("Unknown HTTP-method"));
155 
156     // Request path must be absolute and not contain "..".
157     if( req.target().empty() ||
158         req.target()[0] != '/' ||
159         req.target().find("..") != beast::string_view::npos)
160         return send(bad_request("Illegal request-target"));
161 
162     // Build the path to the requested file
163     std::string path = path_cat(doc_root, req.target());
164     if(req.target().back() == '/')
165         path.append("index.html");
166 
167     // Attempt to open the file
168     beast::error_code ec;
169     http::file_body::value_type body;
170     body.open(path.c_str(), beast::file_mode::scan, ec);
171 
172     // Handle the case where the file doesn't exist
173     if(ec == beast::errc::no_such_file_or_directory)
174         return send(not_found(req.target()));
175 
176     // Handle an unknown error
177     if(ec)
178         return send(server_error(ec.message()));
179 
180     // Cache the size since we need it after the move
181     auto const size = body.size();
182 
183     // Respond to HEAD request
184     if(req.method() == http::verb::head)
185     {
186         http::response<http::empty_body> res{http::status::ok, req.version()};
187         res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
188         res.set(http::field::content_type, mime_type(path));
189         res.content_length(size);
190         res.keep_alive(req.keep_alive());
191         return send(std::move(res));
192     }
193 
194     // Respond to GET request
195     http::response<http::file_body> res{
196         std::piecewise_construct,
197         std::make_tuple(std::move(body)),
198         std::make_tuple(http::status::ok, req.version())};
199     res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
200     res.set(http::field::content_type, mime_type(path));
201     res.content_length(size);
202     res.keep_alive(req.keep_alive());
203     return send(std::move(res));
204 }
205 
206 //------------------------------------------------------------------------------
207 
208 // Report a failure
209 void
fail(beast::error_code ec,char const * what)210 fail(beast::error_code ec, char const* what)
211 {
212     std::cerr << what << ": " << ec.message() << "\n";
213 }
214 
215 // Handles an HTTP server connection
216 class session : public std::enable_shared_from_this<session>
217 {
218     // This is the C++11 equivalent of a generic lambda.
219     // The function object is used to send an HTTP message.
220     struct send_lambda
221     {
222         session& self_;
223 
224         explicit
send_lambdasession::send_lambda225         send_lambda(session& self)
226             : self_(self)
227         {
228         }
229 
230         template<bool isRequest, class Body, class Fields>
231         void
operator ()session::send_lambda232         operator()(http::message<isRequest, Body, Fields>&& msg) const
233         {
234             // The lifetime of the message has to extend
235             // for the duration of the async operation so
236             // we use a shared_ptr to manage it.
237             auto sp = std::make_shared<
238                 http::message<isRequest, Body, Fields>>(std::move(msg));
239 
240             // Store a type-erased version of the shared
241             // pointer in the class to keep it alive.
242             self_.res_ = sp;
243 
244             // Write the response
245             http::async_write(
246                 self_.stream_,
247                 *sp,
248                 beast::bind_front_handler(
249                     &session::on_write,
250                     self_.shared_from_this(),
251                     sp->need_eof()));
252         }
253     };
254 
255     beast::tcp_stream stream_;
256     beast::flat_buffer buffer_;
257     std::shared_ptr<std::string const> doc_root_;
258     http::request<http::string_body> req_;
259     std::shared_ptr<void> res_;
260     send_lambda lambda_;
261 
262 public:
263     // Take ownership of the stream
session(tcp::socket && socket,std::shared_ptr<std::string const> const & doc_root)264     session(
265         tcp::socket&& socket,
266         std::shared_ptr<std::string const> const& doc_root)
267         : stream_(std::move(socket))
268         , doc_root_(doc_root)
269         , lambda_(*this)
270     {
271     }
272 
273     // Start the asynchronous operation
274     void
run()275     run()
276     {
277         // We need to be executing within a strand to perform async operations
278         // on the I/O objects in this session. Although not strictly necessary
279         // for single-threaded contexts, this example code is written to be
280         // thread-safe by default.
281         net::dispatch(stream_.get_executor(),
282                       beast::bind_front_handler(
283                           &session::do_read,
284                           shared_from_this()));
285     }
286 
287     void
do_read()288     do_read()
289     {
290         // Make the request empty before reading,
291         // otherwise the operation behavior is undefined.
292         req_ = {};
293 
294         // Set the timeout.
295         stream_.expires_after(std::chrono::seconds(30));
296 
297         // Read a request
298         http::async_read(stream_, buffer_, req_,
299             beast::bind_front_handler(
300                 &session::on_read,
301                 shared_from_this()));
302     }
303 
304     void
on_read(beast::error_code ec,std::size_t bytes_transferred)305     on_read(
306         beast::error_code ec,
307         std::size_t bytes_transferred)
308     {
309         boost::ignore_unused(bytes_transferred);
310 
311         // This means they closed the connection
312         if(ec == http::error::end_of_stream)
313             return do_close();
314 
315         if(ec)
316             return fail(ec, "read");
317 
318         // Send the response
319         handle_request(*doc_root_, std::move(req_), lambda_);
320     }
321 
322     void
on_write(bool close,beast::error_code ec,std::size_t bytes_transferred)323     on_write(
324         bool close,
325         beast::error_code ec,
326         std::size_t bytes_transferred)
327     {
328         boost::ignore_unused(bytes_transferred);
329 
330         if(ec)
331             return fail(ec, "write");
332 
333         if(close)
334         {
335             // This means we should close the connection, usually because
336             // the response indicated the "Connection: close" semantic.
337             return do_close();
338         }
339 
340         // We're done with the response so delete it
341         res_ = nullptr;
342 
343         // Read another request
344         do_read();
345     }
346 
347     void
do_close()348     do_close()
349     {
350         // Send a TCP shutdown
351         beast::error_code ec;
352         stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
353 
354         // At this point the connection is closed gracefully
355     }
356 };
357 
358 //------------------------------------------------------------------------------
359 
360 // Accepts incoming connections and launches the sessions
361 class listener : public std::enable_shared_from_this<listener>
362 {
363     net::io_context& ioc_;
364     tcp::acceptor acceptor_;
365     std::shared_ptr<std::string const> doc_root_;
366 
367 public:
listener(net::io_context & ioc,tcp::endpoint endpoint,std::shared_ptr<std::string const> const & doc_root)368     listener(
369         net::io_context& ioc,
370         tcp::endpoint endpoint,
371         std::shared_ptr<std::string const> const& doc_root)
372         : ioc_(ioc)
373         , acceptor_(net::make_strand(ioc))
374         , doc_root_(doc_root)
375     {
376         beast::error_code ec;
377 
378         // Open the acceptor
379         acceptor_.open(endpoint.protocol(), ec);
380         if(ec)
381         {
382             fail(ec, "open");
383             return;
384         }
385 
386         // Allow address reuse
387         acceptor_.set_option(net::socket_base::reuse_address(true), ec);
388         if(ec)
389         {
390             fail(ec, "set_option");
391             return;
392         }
393 
394         // Bind to the server address
395         acceptor_.bind(endpoint, ec);
396         if(ec)
397         {
398             fail(ec, "bind");
399             return;
400         }
401 
402         // Start listening for connections
403         acceptor_.listen(
404             net::socket_base::max_listen_connections, ec);
405         if(ec)
406         {
407             fail(ec, "listen");
408             return;
409         }
410     }
411 
412     // Start accepting incoming connections
413     void
run()414     run()
415     {
416         do_accept();
417     }
418 
419 private:
420     void
do_accept()421     do_accept()
422     {
423         // The new connection gets its own strand
424         acceptor_.async_accept(
425             net::make_strand(ioc_),
426             beast::bind_front_handler(
427                 &listener::on_accept,
428                 shared_from_this()));
429     }
430 
431     void
on_accept(beast::error_code ec,tcp::socket socket)432     on_accept(beast::error_code ec, tcp::socket socket)
433     {
434         if(ec)
435         {
436             fail(ec, "accept");
437         }
438         else
439         {
440             // Create the session and run it
441             std::make_shared<session>(
442                 std::move(socket),
443                 doc_root_)->run();
444         }
445 
446         // Accept another connection
447         do_accept();
448     }
449 };
450 
451 //------------------------------------------------------------------------------
452 
main(int argc,char * argv[])453 int main(int argc, char* argv[])
454 {
455     // Check command line arguments.
456     if (argc != 5)
457     {
458         std::cerr <<
459             "Usage: http-server-async <address> <port> <doc_root> <threads>\n" <<
460             "Example:\n" <<
461             "    http-server-async 0.0.0.0 8080 . 1\n";
462         return EXIT_FAILURE;
463     }
464     auto const address = net::ip::make_address(argv[1]);
465     auto const port = static_cast<unsigned short>(std::atoi(argv[2]));
466     auto const doc_root = std::make_shared<std::string>(argv[3]);
467     auto const threads = std::max<int>(1, std::atoi(argv[4]));
468 
469     // The io_context is required for all I/O
470     net::io_context ioc{threads};
471 
472     // Create and launch a listening port
473     std::make_shared<listener>(
474         ioc,
475         tcp::endpoint{address, port},
476         doc_root)->run();
477 
478     // Run the I/O service on the requested number of threads
479     std::vector<std::thread> v;
480     v.reserve(threads - 1);
481     for(auto i = threads - 1; i > 0; --i)
482         v.emplace_back(
483         [&ioc]
484         {
485             ioc.run();
486         });
487     ioc.run();
488 
489     return EXIT_SUCCESS;
490 }
491