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, 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/detached.hpp>
23#include <boost/asio/spawn.hpp>
24#include <boost/config.hpp>
25#include <algorithm>
26#include <cstdlib>
27#include <iostream>
28#include <memory>
29#include <string>
30#include <thread>
31#include <vector>
32
33namespace beast = boost::beast; // from <boost/beast.hpp>
34namespace http = beast::http; // from <boost/beast/http.hpp>
35namespace net = boost::asio; // from <boost/asio.hpp>
36namespace ssl = boost::asio::ssl; // from <boost/asio/ssl.hpp>
37using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
38
39// Return a reasonable mime type based on the extension of a file.
40beast::string_view
41mime_type(beast::string_view path)
42{
43 using beast::iequals;
44 auto const ext = [&path]
45 {
46 auto const pos = path.rfind(s: ".");
47 if(pos == beast::string_view::npos)
48 return beast::string_view{};
49 return path.substr(pos);
50 }();
51 if(iequals(lhs: ext, rhs: ".htm")) return "text/html";
52 if(iequals(lhs: ext, rhs: ".html")) return "text/html";
53 if(iequals(lhs: ext, rhs: ".php")) return "text/html";
54 if(iequals(lhs: ext, rhs: ".css")) return "text/css";
55 if(iequals(lhs: ext, rhs: ".txt")) return "text/plain";
56 if(iequals(lhs: ext, rhs: ".js")) return "application/javascript";
57 if(iequals(lhs: ext, rhs: ".json")) return "application/json";
58 if(iequals(lhs: ext, rhs: ".xml")) return "application/xml";
59 if(iequals(lhs: ext, rhs: ".swf")) return "application/x-shockwave-flash";
60 if(iequals(lhs: ext, rhs: ".flv")) return "video/x-flv";
61 if(iequals(lhs: ext, rhs: ".png")) return "image/png";
62 if(iequals(lhs: ext, rhs: ".jpe")) return "image/jpeg";
63 if(iequals(lhs: ext, rhs: ".jpeg")) return "image/jpeg";
64 if(iequals(lhs: ext, rhs: ".jpg")) return "image/jpeg";
65 if(iequals(lhs: ext, rhs: ".gif")) return "image/gif";
66 if(iequals(lhs: ext, rhs: ".bmp")) return "image/bmp";
67 if(iequals(lhs: ext, rhs: ".ico")) return "image/vnd.microsoft.icon";
68 if(iequals(lhs: ext, rhs: ".tiff")) return "image/tiff";
69 if(iequals(lhs: ext, rhs: ".tif")) return "image/tiff";
70 if(iequals(lhs: ext, rhs: ".svg")) return "image/svg+xml";
71 if(iequals(lhs: ext, rhs: ".svgz")) return "image/svg+xml";
72 return "application/text";
73}
74
75// Append an HTTP rel-path to a local filesystem path.
76// The returned path is normalized for the platform.
77std::string
78path_cat(
79 beast::string_view base,
80 beast::string_view path)
81{
82 if(base.empty())
83 return std::string(path);
84 std::string result(base);
85#ifdef BOOST_MSVC
86 char constexpr path_separator = '\\';
87 if(result.back() == path_separator)
88 result.resize(result.size() - 1);
89 result.append(path.data(), path.size());
90 for(auto& c : result)
91 if(c == '/')
92 c = path_separator;
93#else
94 char constexpr path_separator = '/';
95 if(result.back() == path_separator)
96 result.resize(n: result.size() - 1);
97 result.append(s: path.data(), n: path.size());
98#endif
99 return result;
100}
101
102// Return a response for the given request.
103//
104// The concrete type of the response message (which depends on the
105// request), is type-erased in message_generator.
106template <class Body, class Allocator>
107http::message_generator
108handle_request(
109 beast::string_view doc_root,
110 http::request<Body, http::basic_fields<Allocator>>&& req)
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(name: http::field::server, BOOST_BEAST_VERSION_STRING);
118 res.set(name: http::field::content_type, value: "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(name: http::field::server, BOOST_BEAST_VERSION_STRING);
131 res.set(name: http::field::content_type, value: "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(name: http::field::server, BOOST_BEAST_VERSION_STRING);
144 res.set(name: http::field::content_type, value: "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 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 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(s: "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: path.c_str(), mode: 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 not_found(req.target());
175
176 // Handle an unknown error
177 if(ec)
178 return 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(name: http::field::server, BOOST_BEAST_VERSION_STRING);
188 res.set(name: http::field::content_type, value: mime_type(path));
189 res.content_length(value: size);
190 res.keep_alive(req.keep_alive());
191 return res;
192 }
193
194 // Respond to GET request
195 http::response<http::file_body> res{
196 std::piecewise_construct,
197 std::make_tuple(args: std::move(body)),
198 std::make_tuple(http::status::ok, req.version())};
199 res.set(name: http::field::server, BOOST_BEAST_VERSION_STRING);
200 res.set(name: http::field::content_type, value: mime_type(path));
201 res.content_length(value: size);
202 res.keep_alive(req.keep_alive());
203 return res;
204}
205
206//------------------------------------------------------------------------------
207
208// Report a failure
209void
210fail(beast::error_code ec, char const* what)
211{
212 // ssl::error::stream_truncated, also known as an SSL "short read",
213 // indicates the peer closed the connection without performing the
214 // required closing handshake (for example, Google does this to
215 // improve performance). Generally this can be a security issue,
216 // but if your communication protocol is self-terminated (as
217 // it is with both HTTP and WebSocket) then you may simply
218 // ignore the lack of close_notify.
219 //
220 // https://github.com/boostorg/beast/issues/38
221 //
222 // https://security.stackexchange.com/questions/91435/how-to-handle-a-malicious-ssl-tls-shutdown
223 //
224 // When a short read would cut off the end of an HTTP message,
225 // Beast returns the error beast::http::error::partial_message.
226 // Therefore, if we see a short read here, it has occurred
227 // after the message has been completed, so it is safe to ignore it.
228
229 if(ec == net::ssl::error::stream_truncated)
230 return;
231
232 std::cerr << what << ": " << ec.message() << "\n";
233}
234
235// Handles an HTTP server connection
236void
237do_session(
238 beast::ssl_stream<beast::tcp_stream>& stream,
239 std::shared_ptr<std::string const> const& doc_root,
240 net::yield_context yield)
241{
242 bool keep_alive = true;
243 beast::error_code ec;
244
245 // Set the timeout.
246 beast::get_lowest_layer(t&: stream).expires_after(expiry_time: std::chrono::seconds(30));
247
248 // Perform the SSL handshake
249 stream.async_handshake(type: ssl::stream_base::server, handler: yield[ec]);
250 if(ec)
251 return fail(ec, what: "handshake");
252
253 // This buffer is required to persist across reads
254 beast::flat_buffer buffer;
255
256 for(;;)
257 {
258 // Set the timeout.
259 beast::get_lowest_layer(t&: stream).expires_after(expiry_time: std::chrono::seconds(30));
260
261 // Read a request
262 http::request<http::string_body> req;
263 http::async_read(stream, buffer, msg&: req, handler: yield[ec]);
264 if(ec == http::error::end_of_stream)
265 break;
266 if(ec)
267 return fail(ec, what: "read");
268
269 // Handle the request
270 http::message_generator res =
271 handle_request(doc_root: *doc_root, req: std::move(req));
272
273 // Determine if we should close the connection
274 keep_alive = res.keep_alive();
275
276 // Send the response
277 beast::async_write(stream, generator: std::move(res), token: yield[ec]);
278
279 if(ec)
280 return fail(ec, what: "write");
281 if(! keep_alive)
282 {
283 // This means we should close the connection, usually because
284 // the response indicated the "Connection: close" semantic.
285 break;
286 }
287 }
288
289 // Set the timeout.
290 beast::get_lowest_layer(t&: stream).expires_after(expiry_time: std::chrono::seconds(30));
291
292 // Perform the SSL shutdown
293 stream.async_shutdown(handler: yield[ec]);
294 if(ec)
295 return fail(ec, what: "shutdown");
296
297 // At this point the connection is closed gracefully
298}
299
300//------------------------------------------------------------------------------
301
302// Accepts incoming connections and launches the sessions
303void
304do_listen(
305 net::io_context& ioc,
306 ssl::context& ctx,
307 tcp::endpoint endpoint,
308 std::shared_ptr<std::string const> const& doc_root,
309 net::yield_context yield)
310{
311 beast::error_code ec;
312
313 // Open the acceptor
314 tcp::acceptor acceptor(ioc);
315 acceptor.open(protocol: endpoint.protocol(), ec);
316 if(ec)
317 return fail(ec, what: "open");
318
319 // Allow address reuse
320 acceptor.set_option(option: net::socket_base::reuse_address(true), ec);
321 if(ec)
322 return fail(ec, what: "set_option");
323
324 // Bind to the server address
325 acceptor.bind(endpoint, ec);
326 if(ec)
327 return fail(ec, what: "bind");
328
329 // Start listening for connections
330 acceptor.listen(backlog: net::socket_base::max_listen_connections, ec);
331 if(ec)
332 return fail(ec, what: "listen");
333
334 for(;;)
335 {
336 tcp::socket socket(ioc);
337 acceptor.async_accept(peer&: socket, token: yield[ec]);
338 if(ec)
339 fail(ec, what: "accept");
340 else
341 boost::asio::spawn(
342 ex: acceptor.get_executor(),
343 function: std::bind(
344 f: &do_session,
345 args: beast::ssl_stream<beast::tcp_stream>(
346 std::move(socket), ctx),
347 args: doc_root,
348 args: std::placeholders::_1),
349 // we ignore the result of the session,
350 // most errors are handled with error_code
351 token: boost::asio::detached);
352 }
353}
354
355int main(int argc, char* argv[])
356{
357 // Check command line arguments.
358 if (argc != 5)
359 {
360 std::cerr <<
361 "Usage: http-server-coro-ssl <address> <port> <doc_root> <threads>\n" <<
362 "Example:\n" <<
363 " http-server-coro-ssl 0.0.0.0 8080 . 1\n";
364 return EXIT_FAILURE;
365 }
366 auto const address = net::ip::make_address(str: argv[1]);
367 auto const port = static_cast<unsigned short>(std::atoi(nptr: argv[2]));
368 auto const doc_root = std::make_shared<std::string>(args&: argv[3]);
369 auto const threads = std::max<int>(a: 1, b: std::atoi(nptr: argv[4]));
370
371 // The io_context is required for all I/O
372 net::io_context ioc{threads};
373
374 // The SSL context is required, and holds certificates
375 ssl::context ctx{ssl::context::tlsv12};
376
377 // This holds the self-signed certificate used by the server
378 load_server_certificate(ctx);
379
380 // Spawn a listening port
381 boost::asio::spawn(ctx&: ioc,
382 function: std::bind(
383 f: &do_listen,
384 args: std::ref(t&: ioc),
385 args: std::ref(t&: ctx),
386 args: tcp::endpoint{address, port},
387 args: doc_root,
388 args: std::placeholders::_1),
389 // on completion, spawn will call this function
390 token: [](std::exception_ptr ex)
391 {
392 // if an exception occurred in the coroutine,
393 // it's something critical, e.g. out of memory
394 // we capture normal errors in the ec
395 // so we just rethrow the exception here,
396 // which will cause `ioc.run()` to throw
397 if (ex)
398 std::rethrow_exception(ex);
399 });
400
401 // Run the I/O service on the requested number of threads
402 std::vector<std::thread> v;
403 v.reserve(n: threads - 1);
404 for(auto i = threads - 1; i > 0; --i)
405 v.emplace_back(
406 args: [&ioc]
407 {
408 ioc.run();
409 });
410 ioc.run();
411
412 return EXIT_SUCCESS;
413}
414

source code of boost/libs/beast/example/http/server/coro-ssl/http_server_coro_ssl.cpp