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