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
31namespace beast = boost::beast; // from <boost/beast.hpp>
32namespace http = beast::http; // from <boost/beast/http.hpp>
33namespace net = boost::asio; // from <boost/asio.hpp>
34using 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.
37beast::string_view
38mime_type(beast::string_view path)
39{
40 using beast::iequals;
41 auto const ext = [&path]
42 {
43 auto const pos = path.rfind(s: ".");
44 if(pos == beast::string_view::npos)
45 return beast::string_view{};
46 return path.substr(pos);
47 }();
48 if(iequals(lhs: ext, rhs: ".htm")) return "text/html";
49 if(iequals(lhs: ext, rhs: ".html")) return "text/html";
50 if(iequals(lhs: ext, rhs: ".php")) return "text/html";
51 if(iequals(lhs: ext, rhs: ".css")) return "text/css";
52 if(iequals(lhs: ext, rhs: ".txt")) return "text/plain";
53 if(iequals(lhs: ext, rhs: ".js")) return "application/javascript";
54 if(iequals(lhs: ext, rhs: ".json")) return "application/json";
55 if(iequals(lhs: ext, rhs: ".xml")) return "application/xml";
56 if(iequals(lhs: ext, rhs: ".swf")) return "application/x-shockwave-flash";
57 if(iequals(lhs: ext, rhs: ".flv")) return "video/x-flv";
58 if(iequals(lhs: ext, rhs: ".png")) return "image/png";
59 if(iequals(lhs: ext, rhs: ".jpe")) return "image/jpeg";
60 if(iequals(lhs: ext, rhs: ".jpeg")) return "image/jpeg";
61 if(iequals(lhs: ext, rhs: ".jpg")) return "image/jpeg";
62 if(iequals(lhs: ext, rhs: ".gif")) return "image/gif";
63 if(iequals(lhs: ext, rhs: ".bmp")) return "image/bmp";
64 if(iequals(lhs: ext, rhs: ".ico")) return "image/vnd.microsoft.icon";
65 if(iequals(lhs: ext, rhs: ".tiff")) return "image/tiff";
66 if(iequals(lhs: ext, rhs: ".tif")) return "image/tiff";
67 if(iequals(lhs: ext, rhs: ".svg")) return "image/svg+xml";
68 if(iequals(lhs: ext, rhs: ".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.
74std::string
75path_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(n: result.size() - 1);
94 result.append(s: path.data(), n: path.size());
95#endif
96 return result;
97}
98
99// Return a response for the given request.
100//
101// The concrete type of the response message (which depends on the
102// request), is type-erased in message_generator.
103template <class Body, class Allocator>
104http::message_generator
105handle_request(
106 beast::string_view doc_root,
107 http::request<Body, http::basic_fields<Allocator>>&& req)
108{
109 // Returns a bad request response
110 auto const bad_request =
111 [&req](beast::string_view why)
112 {
113 http::response<http::string_body> res{http::status::bad_request, req.version()};
114 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
115 res.set(name: http::field::content_type, value: "text/html");
116 res.keep_alive(req.keep_alive());
117 res.body() = std::string(why);
118 res.prepare_payload();
119 return res;
120 };
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(name: http::field::server, BOOST_BEAST_VERSION_STRING);
128 res.set(name: http::field::content_type, value: "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
135 // Returns a server error response
136 auto const server_error =
137 [&req](beast::string_view what)
138 {
139 http::response<http::string_body> res{http::status::internal_server_error, req.version()};
140 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
141 res.set(name: http::field::content_type, value: "text/html");
142 res.keep_alive(req.keep_alive());
143 res.body() = "An error occurred: '" + std::string(what) + "'";
144 res.prepare_payload();
145 return res;
146 };
147
148 // Make sure we can handle the method
149 if( req.method() != http::verb::get &&
150 req.method() != http::verb::head)
151 return bad_request("Unknown HTTP-method");
152
153 // Request path must be absolute and not contain "..".
154 if( req.target().empty() ||
155 req.target()[0] != '/' ||
156 req.target().find("..") != beast::string_view::npos)
157 return bad_request("Illegal request-target");
158
159 // Build the path to the requested file
160 std::string path = path_cat(doc_root, req.target());
161 if(req.target().back() == '/')
162 path.append(s: "index.html");
163
164 // Attempt to open the file
165 beast::error_code ec;
166 http::file_body::value_type body;
167 body.open(path: path.c_str(), mode: beast::file_mode::scan, ec);
168
169 // Handle the case where the file doesn't exist
170 if(ec == beast::errc::no_such_file_or_directory)
171 return not_found(req.target());
172
173 // Handle an unknown error
174 if(ec)
175 return server_error(ec.message());
176
177 // Cache the size since we need it after the move
178 auto const size = body.size();
179
180 // Respond to HEAD request
181 if(req.method() == http::verb::head)
182 {
183 http::response<http::empty_body> res{http::status::ok, req.version()};
184 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
185 res.set(name: http::field::content_type, value: mime_type(path));
186 res.content_length(value: size);
187 res.keep_alive(req.keep_alive());
188 return res;
189 }
190
191 // Respond to GET request
192 http::response<http::file_body> res{
193 std::piecewise_construct,
194 std::make_tuple(args: std::move(body)),
195 std::make_tuple(http::status::ok, req.version())};
196 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
197 res.set(name: http::field::content_type, value: mime_type(path));
198 res.content_length(value: size);
199 res.keep_alive(req.keep_alive());
200 return res;
201}
202
203//------------------------------------------------------------------------------
204
205// Report a failure
206void
207fail(beast::error_code ec, char const* what)
208{
209 std::cerr << what << ": " << ec.message() << "\n";
210}
211
212// Handles an HTTP server connection
213class session : public std::enable_shared_from_this<session>
214{
215 beast::tcp_stream stream_;
216 beast::flat_buffer buffer_;
217 std::shared_ptr<std::string const> doc_root_;
218 http::request<http::string_body> req_;
219
220public:
221 // Take ownership of the stream
222 session(
223 tcp::socket&& socket,
224 std::shared_ptr<std::string const> const& doc_root)
225 : stream_(std::move(socket))
226 , doc_root_(doc_root)
227 {
228 }
229
230 // Start the asynchronous operation
231 void
232 run()
233 {
234 // We need to be executing within a strand to perform async operations
235 // on the I/O objects in this session. Although not strictly necessary
236 // for single-threaded contexts, this example code is written to be
237 // thread-safe by default.
238 net::dispatch(ex: stream_.get_executor(),
239 token: beast::bind_front_handler(
240 handler: &session::do_read,
241 args: shared_from_this()));
242 }
243
244 void
245 do_read()
246 {
247 // Make the request empty before reading,
248 // otherwise the operation behavior is undefined.
249 req_ = {};
250
251 // Set the timeout.
252 stream_.expires_after(expiry_time: std::chrono::seconds(30));
253
254 // Read a request
255 http::async_read(stream&: stream_, buffer&: buffer_, msg&: req_,
256 handler: beast::bind_front_handler(
257 handler: &session::on_read,
258 args: shared_from_this()));
259 }
260
261 void
262 on_read(
263 beast::error_code ec,
264 std::size_t bytes_transferred)
265 {
266 boost::ignore_unused(bytes_transferred);
267
268 // This means they closed the connection
269 if(ec == http::error::end_of_stream)
270 return do_close();
271
272 if(ec)
273 return fail(ec, what: "read");
274
275 // Send the response
276 send_response(
277 msg: handle_request(doc_root: *doc_root_, req: std::move(req_)));
278 }
279
280 void
281 send_response(http::message_generator&& msg)
282 {
283 bool keep_alive = msg.keep_alive();
284
285 // Write the response
286 beast::async_write(
287 stream&: stream_,
288 generator: std::move(msg),
289 token: beast::bind_front_handler(
290 handler: &session::on_write, args: shared_from_this(), args&: keep_alive));
291 }
292
293 void
294 on_write(
295 bool keep_alive,
296 beast::error_code ec,
297 std::size_t bytes_transferred)
298 {
299 boost::ignore_unused(bytes_transferred);
300
301 if(ec)
302 return fail(ec, what: "write");
303
304 if(! keep_alive)
305 {
306 // This means we should close the connection, usually because
307 // the response indicated the "Connection: close" semantic.
308 return do_close();
309 }
310
311 // Read another request
312 do_read();
313 }
314
315 void
316 do_close()
317 {
318 // Send a TCP shutdown
319 beast::error_code ec;
320 stream_.socket().shutdown(what: tcp::socket::shutdown_send, ec);
321
322 // At this point the connection is closed gracefully
323 }
324};
325
326//------------------------------------------------------------------------------
327
328// Accepts incoming connections and launches the sessions
329class listener : public std::enable_shared_from_this<listener>
330{
331 net::io_context& ioc_;
332 tcp::acceptor acceptor_;
333 std::shared_ptr<std::string const> doc_root_;
334
335public:
336 listener(
337 net::io_context& ioc,
338 tcp::endpoint endpoint,
339 std::shared_ptr<std::string const> const& doc_root)
340 : ioc_(ioc)
341 , acceptor_(net::make_strand(ctx&: ioc))
342 , doc_root_(doc_root)
343 {
344 beast::error_code ec;
345
346 // Open the acceptor
347 acceptor_.open(protocol: endpoint.protocol(), ec);
348 if(ec)
349 {
350 fail(ec, what: "open");
351 return;
352 }
353
354 // Allow address reuse
355 acceptor_.set_option(option: net::socket_base::reuse_address(true), ec);
356 if(ec)
357 {
358 fail(ec, what: "set_option");
359 return;
360 }
361
362 // Bind to the server address
363 acceptor_.bind(endpoint, ec);
364 if(ec)
365 {
366 fail(ec, what: "bind");
367 return;
368 }
369
370 // Start listening for connections
371 acceptor_.listen(
372 backlog: net::socket_base::max_listen_connections, ec);
373 if(ec)
374 {
375 fail(ec, what: "listen");
376 return;
377 }
378 }
379
380 // Start accepting incoming connections
381 void
382 run()
383 {
384 do_accept();
385 }
386
387private:
388 void
389 do_accept()
390 {
391 // The new connection gets its own strand
392 acceptor_.async_accept(
393 ex: net::make_strand(ctx&: ioc_),
394 token: beast::bind_front_handler(
395 handler: &listener::on_accept,
396 args: shared_from_this()));
397 }
398
399 void
400 on_accept(beast::error_code ec, tcp::socket socket)
401 {
402 if(ec)
403 {
404 fail(ec, what: "accept");
405 return; // To avoid infinite loop
406 }
407 else
408 {
409 // Create the session and run it
410 std::make_shared<session>(
411 args: std::move(socket),
412 args&: doc_root_)->run();
413 }
414
415 // Accept another connection
416 do_accept();
417 }
418};
419
420//------------------------------------------------------------------------------
421
422int main(int argc, char* argv[])
423{
424 // Check command line arguments.
425 if (argc != 5)
426 {
427 std::cerr <<
428 "Usage: http-server-async <address> <port> <doc_root> <threads>\n" <<
429 "Example:\n" <<
430 " http-server-async 0.0.0.0 8080 . 1\n";
431 return EXIT_FAILURE;
432 }
433 auto const address = net::ip::make_address(str: argv[1]);
434 auto const port = static_cast<unsigned short>(std::atoi(nptr: argv[2]));
435 auto const doc_root = std::make_shared<std::string>(args&: argv[3]);
436 auto const threads = std::max<int>(a: 1, b: std::atoi(nptr: argv[4]));
437
438 // The io_context is required for all I/O
439 net::io_context ioc{threads};
440
441 // Create and launch a listening port
442 std::make_shared<listener>(
443 args&: ioc,
444 args: tcp::endpoint{address, port},
445 args: doc_root)->run();
446
447 // Run the I/O service on the requested number of threads
448 std::vector<std::thread> v;
449 v.reserve(n: threads - 1);
450 for(auto i = threads - 1; i > 0; --i)
451 v.emplace_back(
452 args: [&ioc]
453 {
454 ioc.run();
455 });
456 ioc.run();
457
458 return EXIT_SUCCESS;
459}
460

source code of boost/libs/beast/example/http/server/async/http_server_async.cpp