1 #ifndef SERVER_HTTP_HPP 2 #define SERVER_HTTP_HPP 3 4 #include <boost/asio.hpp> 5 #include <boost/algorithm/string/predicate.hpp> 6 #include <boost/functional/hash.hpp> 7 8 #include <map> 9 #include <unordered_map> 10 #include <thread> 11 #include <functional> 12 #include <iostream> 13 #include <sstream> 14 15 #ifndef CASE_INSENSITIVE_EQUALS_AND_HASH 16 #define CASE_INSENSITIVE_EQUALS_AND_HASH 17 //Based on http://www.boost.org/doc/libs/1_60_0/doc/html/unordered/hash_equality.html 18 class case_insensitive_equals { 19 public: operator ()(const std::string & key1,const std::string & key2) const20 bool operator()(const std::string &key1, const std::string &key2) const { 21 return boost::algorithm::iequals(key1, key2); 22 } 23 }; 24 class case_insensitive_hash { 25 public: operator ()(const std::string & key) const26 size_t operator()(const std::string &key) const { 27 std::size_t seed=0; 28 for(auto &c: key) 29 boost::hash_combine(seed, std::tolower(c)); 30 return seed; 31 } 32 }; 33 #endif 34 35 // Late 2017 TODO: remove the following checks and always use std::regex 36 #ifdef USE_BOOST_REGEX 37 #include <boost/regex.hpp> 38 #define REGEX_NS boost 39 #else 40 #include <regex> 41 #define REGEX_NS std 42 #endif 43 44 // TODO when switching to c++14, use [[deprecated]] instead 45 #ifndef DEPRECATED 46 #ifdef __GNUC__ 47 #define DEPRECATED __attribute__((deprecated)) 48 #elif defined(_MSC_VER) 49 #define DEPRECATED __declspec(deprecated) 50 #else 51 #define DEPRECATED 52 #endif 53 #endif 54 55 namespace SimpleWeb { 56 template <class socket_type> 57 class Server; 58 59 template <class socket_type> 60 class ServerBase { 61 public: ~ServerBase()62 virtual ~ServerBase() {} 63 64 class Response : public std::ostream { 65 friend class ServerBase<socket_type>; 66 67 boost::asio::streambuf streambuf; 68 69 std::shared_ptr<socket_type> socket; 70 Response(const std::shared_ptr<socket_type> & socket)71 Response(const std::shared_ptr<socket_type> &socket): std::ostream(&streambuf), socket(socket) {} 72 73 public: size()74 size_t size() { 75 return streambuf.size(); 76 } 77 78 /// If true, force server to close the connection after the response have been sent. 79 /// 80 /// This is useful when implementing a HTTP/1.0-server sending content 81 /// without specifying the content length. 82 bool close_connection_after_response = false; 83 }; 84 85 class Content : public std::istream { 86 friend class ServerBase<socket_type>; 87 public: size()88 size_t size() { 89 return streambuf.size(); 90 } string()91 std::string string() { 92 std::stringstream ss; 93 ss << rdbuf(); 94 return ss.str(); 95 } 96 private: 97 boost::asio::streambuf &streambuf; Content(boost::asio::streambuf & streambuf)98 Content(boost::asio::streambuf &streambuf): std::istream(&streambuf), streambuf(streambuf) {} 99 }; 100 101 class Request { 102 friend class ServerBase<socket_type>; 103 friend class Server<socket_type>; 104 public: 105 std::string method, path, http_version; 106 107 Content content; 108 109 std::unordered_multimap<std::string, std::string, case_insensitive_hash, case_insensitive_equals> header; 110 111 REGEX_NS::smatch path_match; 112 113 std::string remote_endpoint_address; 114 unsigned short remote_endpoint_port; 115 116 private: Request(const socket_type & socket)117 Request(const socket_type &socket): content(streambuf) { 118 try { 119 remote_endpoint_address=socket.lowest_layer().remote_endpoint().address().to_string(); 120 remote_endpoint_port=socket.lowest_layer().remote_endpoint().port(); 121 } 122 catch(...) {} 123 } 124 125 boost::asio::streambuf streambuf; 126 }; 127 128 class Config { 129 friend class ServerBase<socket_type>; 130 Config(unsigned short port)131 Config(unsigned short port): port(port) {} 132 public: 133 /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS. 134 unsigned short port; 135 /// Number of threads that the server will use when start() is called. Defaults to 1 thread. 136 size_t thread_pool_size=1; 137 /// Timeout on request handling. Defaults to 5 seconds. 138 size_t timeout_request=5; 139 /// Timeout on content handling. Defaults to 300 seconds. 140 size_t timeout_content=300; 141 /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. 142 /// If empty, the address will be any address. 143 std::string address; 144 /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true. 145 bool reuse_address=true; 146 }; 147 ///Set before calling start(). 148 Config config; 149 150 private: 151 class regex_orderable : public REGEX_NS::regex { 152 std::string str; 153 public: regex_orderable(const char * regex_cstr)154 regex_orderable(const char *regex_cstr) : REGEX_NS::regex(regex_cstr), str(regex_cstr) {} regex_orderable(const std::string & regex_str)155 regex_orderable(const std::string ®ex_str) : REGEX_NS::regex(regex_str), str(regex_str) {} operator <(const regex_orderable & rhs) const156 bool operator<(const regex_orderable &rhs) const { 157 return str<rhs.str; 158 } 159 }; 160 public: 161 /// Warning: do not add or remove resources after start() is called 162 std::map<regex_orderable, std::map<std::string, 163 std::function<void(std::shared_ptr<typename ServerBase<socket_type>::Response>, std::shared_ptr<typename ServerBase<socket_type>::Request>)> > > resource; 164 165 std::map<std::string, 166 std::function<void(std::shared_ptr<typename ServerBase<socket_type>::Response>, std::shared_ptr<typename ServerBase<socket_type>::Request>)> > default_resource; 167 168 std::function<void(std::shared_ptr<typename ServerBase<socket_type>::Request>, const boost::system::error_code&)> on_error; 169 170 std::function<void(std::shared_ptr<socket_type> socket, std::shared_ptr<typename ServerBase<socket_type>::Request>)> on_upgrade; 171 start()172 virtual void start() { 173 if(!io_service) 174 io_service=std::make_shared<boost::asio::io_service>(); 175 176 if(io_service->stopped()) 177 io_service->reset(); 178 179 boost::asio::ip::tcp::endpoint endpoint; 180 if(config.address.size()>0) 181 endpoint=boost::asio::ip::tcp::endpoint(boost::asio::ip::address::from_string(config.address), config.port); 182 else 183 endpoint=boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), config.port); 184 185 if(!acceptor) 186 acceptor=std::unique_ptr<boost::asio::ip::tcp::acceptor>(new boost::asio::ip::tcp::acceptor(*io_service)); 187 acceptor->open(endpoint.protocol()); 188 acceptor->set_option(boost::asio::socket_base::reuse_address(config.reuse_address)); 189 acceptor->bind(endpoint); 190 acceptor->listen(); 191 192 accept(); 193 194 //If thread_pool_size>1, start m_io_service.run() in (thread_pool_size-1) threads for thread-pooling 195 threads.clear(); 196 for(size_t c=1;c<config.thread_pool_size;c++) { 197 threads.emplace_back([this]() { 198 io_service->run(); 199 }); 200 } 201 202 //Main thread 203 if(config.thread_pool_size>0) 204 io_service->run(); 205 206 //Wait for the rest of the threads, if any, to finish as well 207 for(auto& t: threads) { 208 t.join(); 209 } 210 } 211 stop()212 void stop() { 213 acceptor->close(); 214 if(config.thread_pool_size>0) 215 io_service->stop(); 216 } 217 218 ///Use this function if you need to recursively send parts of a longer message send(const std::shared_ptr<Response> & response,const std::function<void (const boost::system::error_code &)> & callback=nullptr) const219 void send(const std::shared_ptr<Response> &response, const std::function<void(const boost::system::error_code&)>& callback=nullptr) const { 220 boost::asio::async_write(*response->socket, response->streambuf, [this, response, callback](const boost::system::error_code& ec, size_t /*bytes_transferred*/) { 221 if(callback) 222 callback(ec); 223 }); 224 } 225 226 /// If you have your own boost::asio::io_service, store its pointer here before running start(). 227 /// You might also want to set config.thread_pool_size to 0. 228 std::shared_ptr<boost::asio::io_service> io_service; 229 protected: 230 std::unique_ptr<boost::asio::ip::tcp::acceptor> acceptor; 231 std::vector<std::thread> threads; 232 ServerBase(unsigned short port)233 ServerBase(unsigned short port) : config(port) {} 234 235 virtual void accept()=0; 236 get_timeout_timer(const std::shared_ptr<socket_type> & socket,long seconds)237 std::shared_ptr<boost::asio::deadline_timer> get_timeout_timer(const std::shared_ptr<socket_type> &socket, long seconds) { 238 if(seconds==0) 239 return nullptr; 240 241 auto timer=std::make_shared<boost::asio::deadline_timer>(*io_service); 242 timer->expires_from_now(boost::posix_time::seconds(seconds)); 243 timer->async_wait([socket](const boost::system::error_code& ec){ 244 if(!ec) { 245 boost::system::error_code ec; 246 socket->lowest_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 247 socket->lowest_layer().close(); 248 } 249 }); 250 return timer; 251 } 252 read_request_and_content(const std::shared_ptr<socket_type> & socket)253 void read_request_and_content(const std::shared_ptr<socket_type> &socket) { 254 //Create new streambuf (Request::streambuf) for async_read_until() 255 //shared_ptr is used to pass temporary objects to the asynchronous functions 256 std::shared_ptr<Request> request(new Request(*socket)); 257 258 //Set timeout on the following boost::asio::async-read or write function 259 auto timer=this->get_timeout_timer(socket, config.timeout_request); 260 261 boost::asio::async_read_until(*socket, request->streambuf, "\r\n\r\n", 262 [this, socket, request, timer](const boost::system::error_code& ec, size_t bytes_transferred) { 263 if(timer) 264 timer->cancel(); 265 if(!ec) { 266 //request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs: 267 //"After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter" 268 //The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the 269 //streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). 270 size_t num_additional_bytes=request->streambuf.size()-bytes_transferred; 271 272 if(!this->parse_request(request)) 273 return; 274 275 //If content, read that as well 276 auto it=request->header.find("Content-Length"); 277 if(it!=request->header.end()) { 278 unsigned long long content_length; 279 try { 280 content_length=stoull(it->second); 281 } 282 catch(const std::exception &e) { 283 if(on_error) 284 on_error(request, boost::system::error_code(boost::system::errc::protocol_error, boost::system::generic_category())); 285 return; 286 } 287 if(content_length>num_additional_bytes) { 288 //Set timeout on the following boost::asio::async-read or write function 289 auto timer=this->get_timeout_timer(socket, config.timeout_content); 290 boost::asio::async_read(*socket, request->streambuf, 291 boost::asio::transfer_exactly(content_length-num_additional_bytes), 292 [this, socket, request, timer] 293 (const boost::system::error_code& ec, size_t /*bytes_transferred*/) { 294 if(timer) 295 timer->cancel(); 296 if(!ec) 297 this->find_resource(socket, request); 298 else if(on_error) 299 on_error(request, ec); 300 }); 301 } 302 else 303 this->find_resource(socket, request); 304 } 305 else 306 this->find_resource(socket, request); 307 } 308 else if(on_error) 309 on_error(request, ec); 310 }); 311 } 312 parse_request(const std::shared_ptr<Request> & request) const313 bool parse_request(const std::shared_ptr<Request> &request) const { 314 std::string line; 315 getline(request->content, line); 316 size_t method_end; 317 if((method_end=line.find(' '))!=std::string::npos) { 318 size_t path_end; 319 if((path_end=line.find(' ', method_end+1))!=std::string::npos) { 320 request->method=line.substr(0, method_end); 321 request->path=line.substr(method_end+1, path_end-method_end-1); 322 323 size_t protocol_end; 324 if((protocol_end=line.find('/', path_end+1))!=std::string::npos) { 325 if(line.compare(path_end+1, protocol_end-path_end-1, "HTTP")!=0) 326 return false; 327 request->http_version=line.substr(protocol_end+1, line.size()-protocol_end-2); 328 } 329 else 330 return false; 331 332 getline(request->content, line); 333 size_t param_end; 334 while((param_end=line.find(':'))!=std::string::npos) { 335 size_t value_start=param_end+1; 336 if((value_start)<line.size()) { 337 if(line[value_start]==' ') 338 value_start++; 339 if(value_start<line.size()) 340 request->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size()-value_start-1)); 341 } 342 343 getline(request->content, line); 344 } 345 } 346 else 347 return false; 348 } 349 else 350 return false; 351 return true; 352 } 353 find_resource(const std::shared_ptr<socket_type> & socket,const std::shared_ptr<Request> & request)354 void find_resource(const std::shared_ptr<socket_type> &socket, const std::shared_ptr<Request> &request) { 355 //Upgrade connection 356 if(on_upgrade) { 357 auto it=request->header.find("Upgrade"); 358 if(it!=request->header.end()) { 359 on_upgrade(socket, request); 360 return; 361 } 362 } 363 //Find path- and method-match, and call write_response 364 for(auto ®ex_method: resource) { 365 auto it=regex_method.second.find(request->method); 366 if(it!=regex_method.second.end()) { 367 REGEX_NS::smatch sm_res; 368 if(REGEX_NS::regex_match(request->path, sm_res, regex_method.first)) { 369 request->path_match=std::move(sm_res); 370 write_response(socket, request, it->second); 371 return; 372 } 373 } 374 } 375 auto it=default_resource.find(request->method); 376 if(it!=default_resource.end()) { 377 write_response(socket, request, it->second); 378 } 379 } 380 write_response(const std::shared_ptr<socket_type> & socket,const std::shared_ptr<Request> & request,std::function<void (std::shared_ptr<typename ServerBase<socket_type>::Response>,std::shared_ptr<typename ServerBase<socket_type>::Request>)> & resource_function)381 void write_response(const std::shared_ptr<socket_type> &socket, const std::shared_ptr<Request> &request, 382 std::function<void(std::shared_ptr<typename ServerBase<socket_type>::Response>, 383 std::shared_ptr<typename ServerBase<socket_type>::Request>)>& resource_function) { 384 //Set timeout on the following boost::asio::async-read or write function 385 auto timer=this->get_timeout_timer(socket, config.timeout_content); 386 387 auto response=std::shared_ptr<Response>(new Response(socket), [this, request, timer](Response *response_ptr) { 388 auto response=std::shared_ptr<Response>(response_ptr); 389 this->send(response, [this, response, request, timer](const boost::system::error_code& ec) { 390 if(timer) 391 timer->cancel(); 392 if(!ec) { 393 if (response->close_connection_after_response) 394 return; 395 396 auto range=request->header.equal_range("Connection"); 397 for(auto it=range.first;it!=range.second;it++) { 398 if(boost::iequals(it->second, "close")) { 399 return; 400 } else if (boost::iequals(it->second, "keep-alive")) { 401 this->read_request_and_content(response->socket); 402 return; 403 } 404 } 405 if(request->http_version >= "1.1") 406 this->read_request_and_content(response->socket); 407 } 408 else if(on_error) 409 on_error(request, ec); 410 }); 411 }); 412 413 try { 414 resource_function(response, request); 415 } 416 catch(const std::exception &e) { 417 if(on_error) 418 on_error(request, boost::system::error_code(boost::system::errc::operation_canceled, boost::system::generic_category())); 419 return; 420 } 421 } 422 }; 423 424 template<class socket_type> 425 class Server : public ServerBase<socket_type> {}; 426 427 typedef boost::asio::ip::tcp::socket HTTP; 428 429 template<> 430 class Server<HTTP> : public ServerBase<HTTP> { 431 public: Server(unsigned short port,size_t thread_pool_size=1,long timeout_request=5,long timeout_content=300)432 DEPRECATED Server(unsigned short port, size_t thread_pool_size=1, long timeout_request=5, long timeout_content=300) : 433 Server() { 434 config.port=port; 435 config.thread_pool_size=thread_pool_size; 436 config.timeout_request=timeout_request; 437 config.timeout_content=timeout_content; 438 } 439 Server()440 Server() : ServerBase<HTTP>::ServerBase(80) {} 441 442 protected: accept()443 void accept() { 444 //Create new socket for this connection 445 //Shared_ptr is used to pass temporary objects to the asynchronous functions 446 auto socket=std::make_shared<HTTP>(*io_service); 447 448 acceptor->async_accept(*socket, [this, socket](const boost::system::error_code& ec){ 449 //Immediately start accepting a new connection (if io_service hasn't been stopped) 450 if (ec != boost::asio::error::operation_aborted) 451 accept(); 452 453 if(!ec) { 454 boost::asio::ip::tcp::no_delay option(true); 455 socket->set_option(option); 456 457 this->read_request_and_content(socket); 458 } 459 else if(on_error) 460 on_error(std::shared_ptr<Request>(new Request(*socket)), ec); 461 }); 462 } 463 }; 464 } 465 #endif /* SERVER_HTTP_HPP */ 466