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 SSL server, stackless coroutine
13//
14//------------------------------------------------------------------------------
15
16#include "example/common/server_certificate.hpp"
17
18#include <boost/beast/core.hpp>
19#include <boost/beast/http.hpp>
20#include <boost/beast/ssl.hpp>
21#include <boost/beast/version.hpp>
22#include <boost/asio/coroutine.hpp>
23#include <boost/asio/dispatch.hpp>
24#include <boost/asio/strand.hpp>
25#include <boost/config.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
35namespace beast = boost::beast; // from <boost/beast.hpp>
36namespace http = beast::http; // from <boost/beast/http.hpp>
37namespace net = boost::asio; // from <boost/asio.hpp>
38namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
39using 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.
42beast::string_view
43mime_type(beast::string_view path)
44{
45 using beast::iequals;
46 auto const ext = [&path]
47 {
48 auto const pos = path.rfind(s: ".");
49 if(pos == beast::string_view::npos)
50 return beast::string_view{};
51 return path.substr(pos);
52 }();
53 if(iequals(lhs: ext, rhs: ".htm")) return "text/html";
54 if(iequals(lhs: ext, rhs: ".html")) return "text/html";
55 if(iequals(lhs: ext, rhs: ".php")) return "text/html";
56 if(iequals(lhs: ext, rhs: ".css")) return "text/css";
57 if(iequals(lhs: ext, rhs: ".txt")) return "text/plain";
58 if(iequals(lhs: ext, rhs: ".js")) return "application/javascript";
59 if(iequals(lhs: ext, rhs: ".json")) return "application/json";
60 if(iequals(lhs: ext, rhs: ".xml")) return "application/xml";
61 if(iequals(lhs: ext, rhs: ".swf")) return "application/x-shockwave-flash";
62 if(iequals(lhs: ext, rhs: ".flv")) return "video/x-flv";
63 if(iequals(lhs: ext, rhs: ".png")) return "image/png";
64 if(iequals(lhs: ext, rhs: ".jpe")) return "image/jpeg";
65 if(iequals(lhs: ext, rhs: ".jpeg")) return "image/jpeg";
66 if(iequals(lhs: ext, rhs: ".jpg")) return "image/jpeg";
67 if(iequals(lhs: ext, rhs: ".gif")) return "image/gif";
68 if(iequals(lhs: ext, rhs: ".bmp")) return "image/bmp";
69 if(iequals(lhs: ext, rhs: ".ico")) return "image/vnd.microsoft.icon";
70 if(iequals(lhs: ext, rhs: ".tiff")) return "image/tiff";
71 if(iequals(lhs: ext, rhs: ".tif")) return "image/tiff";
72 if(iequals(lhs: ext, rhs: ".svg")) return "image/svg+xml";
73 if(iequals(lhs: ext, rhs: ".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.
79std::string
80path_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(n: result.size() - 1);
99 result.append(s: path.data(), n: path.size());
100#endif
101 return result;
102}
103
104// Return a response for the given request.
105//
106// The concrete type of the response message (which depends on the
107// request), is type-erased in message_generator.
108template <class Body, class Allocator>
109http::message_generator
110handle_request(
111 beast::string_view doc_root,
112 http::request<Body, http::basic_fields<Allocator>>&& req)
113{
114 // Returns a bad request response
115 auto const bad_request =
116 [&req](beast::string_view why)
117 {
118 http::response<http::string_body> res{http::status::bad_request, req.version()};
119 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
120 res.set(name: http::field::content_type, value: "text/html");
121 res.keep_alive(req.keep_alive());
122 res.body() = std::string(why);
123 res.prepare_payload();
124 return res;
125 };
126
127 // Returns a not found response
128 auto const not_found =
129 [&req](beast::string_view target)
130 {
131 http::response<http::string_body> res{http::status::not_found, req.version()};
132 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
133 res.set(name: http::field::content_type, value: "text/html");
134 res.keep_alive(req.keep_alive());
135 res.body() = "The resource '" + std::string(target) + "' was not found.";
136 res.prepare_payload();
137 return res;
138 };
139
140 // Returns a server error response
141 auto const server_error =
142 [&req](beast::string_view what)
143 {
144 http::response<http::string_body> res{http::status::internal_server_error, req.version()};
145 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
146 res.set(name: http::field::content_type, value: "text/html");
147 res.keep_alive(req.keep_alive());
148 res.body() = "An error occurred: '" + std::string(what) + "'";
149 res.prepare_payload();
150 return res;
151 };
152
153 // Make sure we can handle the method
154 if( req.method() != http::verb::get &&
155 req.method() != http::verb::head)
156 return bad_request("Unknown HTTP-method");
157
158 // Request path must be absolute and not contain "..".
159 if( req.target().empty() ||
160 req.target()[0] != '/' ||
161 req.target().find("..") != beast::string_view::npos)
162 return bad_request("Illegal request-target");
163
164 // Build the path to the requested file
165 std::string path = path_cat(doc_root, req.target());
166 if(req.target().back() == '/')
167 path.append(s: "index.html");
168
169 // Attempt to open the file
170 beast::error_code ec;
171 http::file_body::value_type body;
172 body.open(path: path.c_str(), mode: beast::file_mode::scan, ec);
173
174 // Handle the case where the file doesn't exist
175 if(ec == beast::errc::no_such_file_or_directory)
176 return not_found(req.target());
177
178 // Handle an unknown error
179 if(ec)
180 return server_error(ec.message());
181
182 // Cache the size since we need it after the move
183 auto const size = body.size();
184
185 // Respond to HEAD request
186 if(req.method() == http::verb::head)
187 {
188 http::response<http::empty_body> res{http::status::ok, req.version()};
189 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
190 res.set(name: http::field::content_type, value: mime_type(path));
191 res.content_length(value: size);
192 res.keep_alive(req.keep_alive());
193 return res;
194 }
195
196 // Respond to GET request
197 http::response<http::file_body> res{
198 std::piecewise_construct,
199 std::make_tuple(args: std::move(body)),
200 std::make_tuple(http::status::ok, req.version())};
201 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
202 res.set(name: http::field::content_type, value: mime_type(path));
203 res.content_length(value: size);
204 res.keep_alive(req.keep_alive());
205 return res;
206}
207
208//------------------------------------------------------------------------------
209
210// Report a failure
211void
212fail(beast::error_code ec, char const* what)
213{
214 // ssl::error::stream_truncated, also known as an SSL "short read",
215 // indicates the peer closed the connection without performing the
216 // required closing handshake (for example, Google does this to
217 // improve performance). Generally this can be a security issue,
218 // but if your communication protocol is self-terminated (as
219 // it is with both HTTP and WebSocket) then you may simply
220 // ignore the lack of close_notify.
221 //
222 // https://github.com/boostorg/beast/issues/38
223 //
224 // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
225 //
226 // When a short read would cut off the end of an HTTP message,
227 // Beast returns the error beast::http::error::partial_message.
228 // Therefore, if we see a short read here, it has occurred
229 // after the message has been completed, so it is safe to ignore it.
230
231 if(ec == net::ssl::error::stream_truncated)
232 return;
233
234 std::cerr << what << ": " << ec.message() << "\n";
235}
236
237// Handles an HTTP server connection
238class session
239 : public boost::asio::coroutine
240 , public std::enable_shared_from_this<session>
241{
242 beast::ssl_stream<beast::tcp_stream> stream_;
243 beast::flat_buffer buffer_;
244 std::shared_ptr<std::string const> doc_root_;
245 http::request<http::string_body> req_;
246 bool keep_alive_ = true;
247
248public:
249 // Take ownership of the socket
250 explicit
251 session(
252 tcp::socket&& socket,
253 ssl::context& ctx,
254 std::shared_ptr<std::string const> const& doc_root)
255 : stream_(std::move(socket), ctx)
256 , doc_root_(doc_root)
257 {
258 }
259
260 // Start the asynchronous operation
261 void
262 run()
263 {
264 // We need to be executing within a strand to perform async operations
265 // on the I/O objects in this session.Although not strictly necessary
266 // for single-threaded contexts, this example code is written to be
267 // thread-safe by default.
268 net::dispatch(ex: stream_.get_executor(),
269 token: beast::bind_front_handler(handler: &session::loop,
270 args: shared_from_this(),
271 args: beast::error_code{},
272 args: 0));
273 }
274
275 #include <boost/asio/yield.hpp>
276
277 void
278 loop(beast::error_code ec, std::size_t bytes_transferred)
279 {
280 boost::ignore_unused(bytes_transferred);
281 reenter(*this)
282 {
283 // Set the timeout.
284 beast::get_lowest_layer(t&: stream_).expires_after(expiry_time: std::chrono::seconds(30));
285
286 // Perform the SSL handshake
287 yield stream_.async_handshake(
288 type: ssl::stream_base::server,
289 handler: std::bind(
290 f: &session::loop, args: shared_from_this(),
291 args: std::placeholders::_1, args: 0));
292 if(ec)
293 return fail(ec, what: "handshake");
294
295 for(;;)
296 {
297 // Set the timeout.
298 beast::get_lowest_layer(t&: stream_).expires_after(expiry_time: std::chrono::seconds(30));
299
300 // Make the request empty before reading,
301 // otherwise the operation behavior is undefined.
302 req_ = {};
303
304 // Read a request
305 yield http::async_read(
306 stream&: stream_, buffer&: buffer_, msg&: req_,
307 handler: beast::bind_front_handler(
308 handler: &session::loop, args: shared_from_this()));
309
310 if (ec == http::error::end_of_stream) {
311 // The remote host closed the connection
312 break;
313 }
314 if(ec)
315 return fail(ec, what: "read");
316
317 yield {
318 // Handle request
319 http::message_generator msg =
320 handle_request(doc_root: *doc_root_, req: std::move(req_));
321
322 // Determine if we should close the connection
323 keep_alive_ = msg.keep_alive();
324
325 // Send the response
326 beast::async_write(
327 stream&: stream_, generator: std::move(msg),
328 token: beast::bind_front_handler(
329 handler: &session::loop, args: shared_from_this()));
330 }
331
332 if(ec)
333 return fail(ec, what: "write");
334 if(! keep_alive_)
335 {
336 // This means we should close the connection, usually because
337 // the response indicated the "Connection: close" semantic.
338 break;
339 }
340 }
341
342 // Set the timeout.
343 beast::get_lowest_layer(t&: stream_).expires_after(expiry_time: std::chrono::seconds(30));
344
345 // Perform the SSL shutdown
346 yield stream_.async_shutdown(
347 handler: std::bind(
348 f: &session::loop,
349 args: shared_from_this(),
350 args: std::placeholders::_1,
351 args: 0));
352 if(ec)
353 return fail(ec, what: "shutdown");
354
355 // At this point the connection is closed gracefully
356 }
357 }
358
359 #include <boost/asio/unyield.hpp>
360};
361
362//------------------------------------------------------------------------------
363
364// Accepts incoming connections and launches the sessions
365class listener
366 : public boost::asio::coroutine
367 , public std::enable_shared_from_this<listener>
368{
369 net::io_context& ioc_;
370 ssl::context& ctx_;
371 tcp::acceptor acceptor_;
372 tcp::socket socket_;
373 std::shared_ptr<std::string const> doc_root_;
374
375public:
376 listener(
377 net::io_context& ioc,
378 ssl::context& ctx,
379 tcp::endpoint endpoint,
380 std::shared_ptr<std::string const> const& doc_root)
381 : ioc_(ioc)
382 , ctx_(ctx)
383 , acceptor_(net::make_strand(ctx&: ioc))
384 , socket_(net::make_strand(ctx&: ioc))
385 , doc_root_(doc_root)
386 {
387 beast::error_code ec;
388
389 // Open the acceptor
390 acceptor_.open(protocol: endpoint.protocol(), ec);
391 if(ec)
392 {
393 fail(ec, what: "open");
394 return;
395 }
396
397 // Allow address reuse
398 acceptor_.set_option(option: net::socket_base::reuse_address(true), ec);
399 if(ec)
400 {
401 fail(ec, what: "set_option");
402 return;
403 }
404
405 // Bind to the server address
406 acceptor_.bind(endpoint, ec);
407 if(ec)
408 {
409 fail(ec, what: "bind");
410 return;
411 }
412
413 // Start listening for connections
414 acceptor_.listen(
415 backlog: net::socket_base::max_listen_connections, ec);
416 if(ec)
417 {
418 fail(ec, what: "listen");
419 return;
420 }
421 }
422
423 // Start accepting incoming connections
424 void
425 run()
426 {
427 loop();
428 }
429
430private:
431
432 #include <boost/asio/yield.hpp>
433
434 void
435 loop(beast::error_code ec = {})
436 {
437 reenter(*this)
438 {
439 for(;;)
440 {
441 yield acceptor_.async_accept(
442 peer&: socket_,
443 token: std::bind(
444 f: &listener::loop,
445 args: shared_from_this(),
446 args: std::placeholders::_1));
447 if(ec)
448 {
449 fail(ec, what: "accept");
450 }
451 else
452 {
453 // Create the session and run it
454 std::make_shared<session>(
455 args: std::move(socket_),
456 args&: ctx_,
457 args&: doc_root_)->run();
458 }
459
460 // Make sure each session gets its own strand
461 socket_ = tcp::socket(net::make_strand(ctx&: ioc_));
462 }
463 }
464 }
465
466 #include <boost/asio/unyield.hpp>
467};
468
469//------------------------------------------------------------------------------
470
471int main(int argc, char* argv[])
472{
473 // Check command line arguments.
474 if (argc != 5)
475 {
476 std::cerr <<
477 "Usage: http-server-stackless-ssl <address> <port> <doc_root> <threads>\n" <<
478 "Example:\n" <<
479 " http-server-stackless-ssl 0.0.0.0 8080 . 1\n";
480 return EXIT_FAILURE;
481 }
482 auto const address = net::ip::make_address(str: argv[1]);
483 auto const port = static_cast<unsigned short>(std::atoi(nptr: argv[2]));
484 auto const doc_root = std::make_shared<std::string>(args&: argv[3]);
485 auto const threads = std::max<int>(a: 1, b: std::atoi(nptr: argv[4]));
486
487 // The io_context is required for all I/O
488 net::io_context ioc{threads};
489
490 // The SSL context is required, and holds certificates
491 ssl::context ctx{ssl::context::tlsv12};
492
493 // This holds the self-signed certificate used by the server
494 load_server_certificate(ctx);
495
496 // Create and launch a listening port
497 std::make_shared<listener>(
498 args&: ioc,
499 args&: ctx,
500 args: tcp::endpoint{address, port},
501 args: doc_root)->run();
502
503 // Run the I/O service on the requested number of threads
504 std::vector<std::thread> v;
505 v.reserve(n: threads - 1);
506 for(auto i = threads - 1; i > 0; --i)
507 v.emplace_back(
508 args: [&ioc]
509 {
510 ioc.run();
511 });
512 ioc.run();
513
514 return EXIT_SUCCESS;
515}
516

source code of boost/libs/beast/example/http/server/stackless-ssl/http_server_stackless_ssl.cpp