|
| 1 | +// Copyright 2018 Martin Trenkmann |
| 2 | +// Based on cpp-netlib/libs/network/example/http/fileserver.cpp |
| 3 | +// Distributed under the Boost Software License, Version 1.0. |
| 4 | +// (See accompanying file LICENSE_1_0.txt or copy at |
| 5 | +// http://www.boost.org/LICENSE_1_0.txt) |
| 6 | + |
| 7 | +// This example implements a minimal echo server that receives plain text via |
| 8 | +// POST requests and sends the same text back to the client (with an additional |
| 9 | +// newline appended for nicer output). Since the server uses asynchronous I/O, |
| 10 | +// the body data is not available through request.body, but must be read using |
| 11 | +// connection->read(callback). |
| 12 | +// |
| 13 | +// This code is minimal, it does not... |
| 14 | +// - handle any kinds of errors, |
| 15 | +// - look at the URI path (request.destination), |
| 16 | +// - handle request methods other than POST (request.method), |
| 17 | +// - check for too large bodies (which make the server vulnerable to attacks), |
| 18 | +// - model state, e.g. database connections (this type of context should be |
| 19 | +// injected into the post_request_handler constructor). |
| 20 | +// |
| 21 | +// About handling large body data: |
| 22 | +// According to https://tools.ietf.org/html/rfc2616#section-8.2.3, HTTP clients |
| 23 | +// may send a "Expect: 100-continue" header before sending large body data in |
| 24 | +// order to negotiate whether the server is willing to accept the data. If the |
| 25 | +// server agrees, it should send an interim response with a "100 Continue" |
| 26 | +// status code. The client then should start sending the data and upon |
| 27 | +// completion, the server should send a final response including a status code |
| 28 | +// and possibly headers and body data. |
| 29 | +// |
| 30 | +// About cpp-netlib 0.13 and the "Expect: 100-continue" header: |
| 31 | +// When a client sends a POST request with a "Expect: 100-continue" header, the |
| 32 | +// callback passed into connection->read() gets triggered with an empty chunk of |
| 33 | +// data. As described before, the server should then send an "100 Continue" |
| 34 | +// interim response, but this seems to be not supported by the library at the |
| 35 | +// moment, because trying to set and send a status code on the connection object |
| 36 | +// more than once throws an exception. However, since clients are allowed to |
| 37 | +// send data even though they never received a "100 Continue", the server should |
| 38 | +// continue reading data from the connection, by calling connection->read() |
| 39 | +// again, until the expected number of bytes, based on the "Content-Length" |
| 40 | +// header, has been received. |
| 41 | +// |
| 42 | +// About curl and the "Expect: 100-continue" header: |
| 43 | +// According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect |
| 44 | +// no common browsers make use of the "Expect" header, but some other clients |
| 45 | +// such as curl do so by default. In fact, curl sends a "Expect: 100-continue" |
| 46 | +// header with POST requests that contain at least 1KiB of data (see example |
| 47 | +// session below). Fortunately, curl also starts sending the data if it does |
| 48 | +// not receive a "100 Continue" status code after a timeout of 1 second, which |
| 49 | +// makes it "compatible" with servers that cannot send interim responses such as |
| 50 | +// cpp-netlib 0.13. |
| 51 | +// |
| 52 | +// The timeout in curl is configurable via "--expect100-timeout <seconds>" |
| 53 | +// command line argument. The minimum value seems to be 1 second. |
| 54 | +// |
| 55 | +// Example session: |
| 56 | +// Two POST requests are sent to the echo server using curl. The first one with |
| 57 | +// data less than 1KiB, the second one with data more than 1KiB which shows the |
| 58 | +// use of the "Expect: 100-continue" header. |
| 59 | +// |
| 60 | +// The "lorem" command generates lorem ipsum text. It can be installed via |
| 61 | +// "sudo apt-get install libtext-lorem-perl" on Debian-based systems. |
| 62 | +// |
| 63 | +// 1. Sending less than 1KiB |
| 64 | +// ------------------------- |
| 65 | +// |
| 66 | +// $ curl -v -H 'Content-Type: text/plain' -d "$(lorem -s 3 | fold)" http://localhost:8000 |
| 67 | +// * Rebuilt URL to: http://localhost:8000/ |
| 68 | +// * Trying 127.0.0.1... |
| 69 | +// * Connected to localhost (127.0.0.1) port 8000 (#0) |
| 70 | +// > POST / HTTP/1.1 |
| 71 | +// > Host: localhost:8000 |
| 72 | +// > User-Agent: curl/7.47.0 |
| 73 | +// > Accept: */* |
| 74 | +// > Content-Type: text/plain |
| 75 | +// > Content-Length: 155 |
| 76 | +// > |
| 77 | +// * upload completely sent off: 155 out of 155 bytes |
| 78 | +// < HTTP/1.1 200 OK |
| 79 | +// < Content-Type: text/plain |
| 80 | +// < Content-Length: 156 |
| 81 | +// < |
| 82 | +// Iusto autem omnis necessitatibus quia omnis nemo sequi. Occaecati est explicabo |
| 83 | +// qui placeat ipsa debitis fugit sit. Quasi sequi in est eius qui molestiae. |
| 84 | +// * Connection #0 to host localhost left intact |
| 85 | +// |
| 86 | +// SERVER OUTPUT |
| 87 | +// |
| 88 | +// Host: localhost:8000 |
| 89 | +// User-Agent: curl/7.47.0 |
| 90 | +// Accept: */* |
| 91 | +// Content-Type: text/plain |
| 92 | +// Content-Length: 155 |
| 93 | +// Chunk size: 155 |
| 94 | +// |
| 95 | +// 2. Sending more than 1KiB |
| 96 | +// ------------------------- |
| 97 | +// |
| 98 | +// $ curl -v -H 'Content-Type: text/plain' -d "$(lorem -s 30 | fold)" http://localhost:8000 |
| 99 | +// * Rebuilt URL to: http://localhost:8000/ |
| 100 | +// * Trying 127.0.0.1... |
| 101 | +// * Connected to localhost (127.0.0.1) port 8000 (#0) |
| 102 | +// > POST / HTTP/1.1 |
| 103 | +// > Host: localhost:8000 |
| 104 | +// > User-Agent: curl/7.47.0 |
| 105 | +// > Accept: */* |
| 106 | +// > Content-Type: text/plain |
| 107 | +// > Content-Length: 1324 |
| 108 | +// > Expect: 100-continue |
| 109 | +// > |
| 110 | +// * Done waiting for 100-continue |
| 111 | +// * We are completely uploaded and fine |
| 112 | +// < HTTP/1.1 200 OK |
| 113 | +// < Content-Type: text/plain |
| 114 | +// < Content-Length: 1325 |
| 115 | +// < |
| 116 | +// Fugit quo aliquid in et consectetur sed id. Aliquam dolor optio labore sit autem |
| 117 | +// . Culpa at omnis et consectetur minima nostrum sed. Veniam similique dolorum des |
| 118 | +// erunt aut et et aut quo. Laudantium nesciunt est repellat. Dolores adipisci alia |
| 119 | +// s dicta dicta. Impedit porro pariatur quisquam sit ex ducimus a. Consequatur ius |
| 120 | +// to possimus in sint nesciunt molestiae fugiat et. Est praesentium quos quam libe |
| 121 | +// ro vel nostrum placeat consequuntur. Tempora nihil aut aliquam. Atque ab sunt ut |
| 122 | +// sed quo ut. Quia qui omnis non. In laboriosam possimus laboriosam consequatur v |
| 123 | +// el dolores. Reprehenderit totam quis dolore debitis ullam. Aut iure omnis invent |
| 124 | +// ore quaerat aut veniam vel magni. Temporibus voluptatibus accusamus qui facilis |
| 125 | +// at aut. Voluptatem provident incidunt officia. Quos quo autem quae illo. Modi am |
| 126 | +// et quis eveniet. Nemo tenetur quia unde. Velit molestiae laborum eum. Repellat m |
| 127 | +// olestias eos eos accusantium dolorem molestias pariatur ex. Nihil dolorum possim |
| 128 | +// us ut ut beatae. Quia nam sit aut voluptatum maiores quibusdam id aliquid. Nulla |
| 129 | +// numquam rem quo doloremque ut ut. Aspernatur accusamus illo illo dolores repudi |
| 130 | +// andae dicta reiciendis. Quis laborum magni et incidunt nihil. Ea quia consequunt |
| 131 | +// ur quos minima aut veniam ratione sed. Ea deleniti accusamus est quo nisi. Quia |
| 132 | +// quibusdam et aut reiciendis. |
| 133 | +// * Connection #0 to host localhost left intact |
| 134 | +// |
| 135 | +// SERVER OUTPUT |
| 136 | +// |
| 137 | +// Host: localhost:8000 |
| 138 | +// User-Agent: curl/7.47.0 |
| 139 | +// Accept: */* |
| 140 | +// Content-Type: text/plain |
| 141 | +// Content-Length: 1324 |
| 142 | +// Expect: 100-continue |
| 143 | +// Chunk size: 0 |
| 144 | +// Chunk size: 1024 |
| 145 | +// Chunk size: 300 |
| 146 | + |
| 147 | +#include <boost/asio/signal_set.hpp> |
| 148 | +#include <boost/network/protocol/http/server.hpp> |
| 149 | +#include <cassert> |
| 150 | +#include <memory> |
| 151 | +#include <string> |
| 152 | +#include <vector> |
| 153 | + |
| 154 | +namespace http = boost::network::http; |
| 155 | + |
| 156 | +struct request_handler; |
| 157 | +using echo_server = http::server<request_handler>; |
| 158 | + |
| 159 | +struct post_request_handler |
| 160 | + : public std::enable_shared_from_this<post_request_handler> { |
| 161 | + explicit post_request_handler(const echo_server::request& request) |
| 162 | + : content_length_(0) { |
| 163 | + for (const auto& header : request.headers) { |
| 164 | + std::cout << header.name << ": " << header.value << '\n'; |
| 165 | + if (boost::iequals(header.name, "content-length")) { |
| 166 | + content_length_ = std::stoul(header.value); |
| 167 | + } |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + void operator()(echo_server::connection::input_range chunk, |
| 172 | + boost::system::error_code ec, size_t chunk_size, |
| 173 | + echo_server::connection_ptr connection) { |
| 174 | + assert(chunk.size() == chunk_size); |
| 175 | + std::cout << "Chunk size: " << chunk_size << '\n'; |
| 176 | + |
| 177 | + if (ec) { |
| 178 | + std::cerr << "Error code: " << ec << '\n'; |
| 179 | + return; |
| 180 | + } |
| 181 | + |
| 182 | + body_.append(chunk.begin(), chunk.end()); |
| 183 | + if (body_.size() < content_length_) { |
| 184 | + auto self = this->shared_from_this(); |
| 185 | + connection->read([self](echo_server::connection::input_range chunk, |
| 186 | + boost::system::error_code ec, size_t chunk_size, |
| 187 | + echo_server::connection_ptr connection) { |
| 188 | + (*self)(chunk, ec, chunk_size, connection); |
| 189 | + }); |
| 190 | + return; |
| 191 | + } |
| 192 | + |
| 193 | + body_.push_back('\n'); |
| 194 | + std::vector<echo_server::response_header> headers; |
| 195 | + headers.push_back({"Content-Type", "text/plain"}); |
| 196 | + headers.push_back({"Content-Length", std::to_string(body_.size())}); |
| 197 | + connection->set_status(echo_server::connection::ok); |
| 198 | + connection->set_headers(headers); |
| 199 | + connection->write(body_); |
| 200 | + } |
| 201 | + |
| 202 | + private: |
| 203 | + size_t content_length_; |
| 204 | + std::string body_; |
| 205 | +}; |
| 206 | + |
| 207 | +struct request_handler { |
| 208 | + void operator()(const echo_server::request& request, |
| 209 | + echo_server::connection_ptr connection) { |
| 210 | + if (request.method == "POST") { |
| 211 | + auto h = std::make_shared<post_request_handler>(request); |
| 212 | + connection->read([h](echo_server::connection::input_range chunk, |
| 213 | + boost::system::error_code ec, size_t chunk_size, |
| 214 | + echo_server::connection_ptr connection) { |
| 215 | + (*h)(chunk, ec, chunk_size, connection); |
| 216 | + }); |
| 217 | + } |
| 218 | + } |
| 219 | +}; |
| 220 | + |
| 221 | +int main() { |
| 222 | + try { |
| 223 | + request_handler handler; |
| 224 | + auto io_service = std::make_shared<boost::asio::io_service>(); |
| 225 | + echo_server server( |
| 226 | + echo_server::options(handler).io_service(io_service).port("8000")); |
| 227 | + server.run(); |
| 228 | + |
| 229 | + // Clean shutdown when pressing Ctrl+C. |
| 230 | + boost::asio::signal_set signals(*io_service, SIGINT, SIGTERM); |
| 231 | + signals.async_wait([&server](const boost::system::error_code& ec, |
| 232 | + int /* signal_number */) { |
| 233 | + if (!ec) { |
| 234 | + server.stop(); |
| 235 | + } |
| 236 | + }); |
| 237 | + return EXIT_SUCCESS; |
| 238 | + } catch (const std::exception& error) { |
| 239 | + std::cerr << error.what() << std::endl; |
| 240 | + return EXIT_FAILURE; |
| 241 | + } |
| 242 | +} |
0 commit comments