diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..50f9423 --- /dev/null +++ b/.clang-format @@ -0,0 +1,9 @@ +IndentWidth: 2 +AccessModifierOffset: -2 +UseTab: Never +ColumnLimit: 0 +MaxEmptyLinesToKeep: 2 +SpaceBeforeParens: Never +BreakBeforeBraces: Custom +BraceWrapping: {BeforeElse: true, BeforeCatch: true} +NamespaceIndentation: All diff --git a/client_http.hpp b/client_http.hpp index 95f0381..96a17f4 100644 --- a/client_http.hpp +++ b/client_http.hpp @@ -1,580 +1,586 @@ #ifndef CLIENT_HTTP_HPP -#define CLIENT_HTTP_HPP +#define CLIENT_HTTP_HPP #include "utility.hpp" -#include -#include #include +#include +#include #ifdef USE_STANDALONE_ASIO #include namespace SimpleWeb { - using error_code = std::error_code; - using errc = std::errc; - using system_error = std::system_error; - namespace make_error_code = std; - using string_view = const std::string&; // TODO c++17: use std::string_view -} + using error_code = std::error_code; + using errc = std::errc; + using system_error = std::system_error; + namespace make_error_code = std; + using string_view = const std::string &; // TODO c++17: use std::string_view +} // namespace SimpleWeb #else #include #include namespace SimpleWeb { - namespace asio = boost::asio; - using error_code = boost::system::error_code; - namespace errc = boost::system::errc; - using system_error = boost::system::system_error; - namespace make_error_code = boost::system::errc; - using string_view = boost::string_ref; -} + namespace asio = boost::asio; + using error_code = boost::system::error_code; + namespace errc = boost::system::errc; + using system_error = boost::system::system_error; + namespace make_error_code = boost::system::errc; + using string_view = boost::string_ref; +} // namespace SimpleWeb #endif namespace SimpleWeb { - template - class Client; - - template - class ClientBase { - public: - class Content : public std::istream { - friend class ClientBase; - public: - size_t size() { - return streambuf.size(); - } - /// Convenience function to return std::string. Note that the stream buffer is emptied when this functions is used. - std::string string() { - std::stringstream ss; - ss << rdbuf(); - return ss.str(); - } - private: - asio::streambuf &streambuf; - Content(asio::streambuf &streambuf): std::istream(&streambuf), streambuf(streambuf) {} - }; - - class Response { - friend class ClientBase; - friend class Client; - public: - std::string http_version, status_code; + template + class Client; - Content content; + template + class ClientBase { + public: + class Content : public std::istream { + friend class ClientBase; - CaseInsensitiveMultimap header; - - private: - asio::streambuf content_buffer; - - Response(): content(content_buffer) {} - }; - - class Config { - friend class ClientBase; - private: - Config() {} - public: - /// Set timeout on requests in seconds. Default value: 0 (no timeout). - size_t timeout=0; - /// Set connect timeout in seconds. Default value: 0 (Config::timeout is then used instead). - size_t timeout_connect=0; - /// Set proxy server (server:port) - std::string proxy_server; - }; - - protected: - class Connection { - public: - Connection(const std::string &host, unsigned short port, const Config &config, std::unique_ptr &&socket) : - host(host), port(port), config(config), socket(std::move(socket)) { - if(config.proxy_server.empty()) - query=std::unique_ptr(new asio::ip::tcp::resolver::query(host, std::to_string(port))); - else { - auto proxy_host_port=parse_host_port(config.proxy_server, 8080); - query=std::unique_ptr(new asio::ip::tcp::resolver::query(proxy_host_port.first, std::to_string(proxy_host_port.second))); - } - } - - std::string host; - unsigned short port; - Config config; - - std::unique_ptr socket; - bool in_use=false; - bool reconnecting=false; - - std::unique_ptr query; - }; - - class Session { - public: - Session(const std::shared_ptr &io_service, const std::shared_ptr &connection, std::unique_ptr &&request_buffer) : - io_service(io_service), connection(connection), request_buffer(std::move(request_buffer)), response(new Response()) {} - std::shared_ptr io_service; - std::shared_ptr connection; - std::unique_ptr request_buffer; - std::shared_ptr response; - std::function callback; - }; - public: - /// Set before calling request - Config config; - - /// If you have your own asio::io_service, store its pointer here before calling request(). - /// When using asynchronous requests, running the io_service is up to the programmer. - std::shared_ptr io_service; - - virtual ~ClientBase() {} - - /// Convenience function to perform synchronous request. The io_service is run within this function. - /// If reusing the io_service for other tasks, please use the asynchronous request functions instead. - std::shared_ptr request(const std::string& method, const std::string& path=std::string("/"), string_view content="", const CaseInsensitiveMultimap& header=CaseInsensitiveMultimap()) { - std::shared_ptr response; - request(method, path, content, header, [&response](std::shared_ptr response_, const error_code &ec) { - response=response_; - if(ec) - throw system_error(ec); - }); - - io_service->reset(); - io_service->run(); - - return response; - } - - /// Convenience function to perform synchronous request. The io_service is run within this function. - /// If reusing the io_service for other tasks, please use the asynchronous request functions instead. - std::shared_ptr request(const std::string& method, const std::string& path, std::istream& content, const CaseInsensitiveMultimap& header=CaseInsensitiveMultimap()) { - std::shared_ptr response; - request(method, path, content, header, [&response](std::shared_ptr response_, const error_code &ec) { - response=response_; - if(ec) - throw system_error(ec); - }); - - io_service->reset(); - io_service->run(); - - return response; - } - - /// Asynchronous request where setting and/or running Client's io_service is required. - void request(const std::string &method, const std::string &path, string_view content, const CaseInsensitiveMultimap& header, - std::function, const error_code&)> &&request_callback_) { - auto session=std::make_shared(io_service, get_connection(), create_request_header(method, path, header)); - auto connection=session->connection; - auto response=session->response; - auto request_callback=std::make_shared, const error_code&)>>(std::move(request_callback_)); - auto connections=this->connections; - auto connections_mutex=this->connections_mutex; - session->callback=[connection, response, request_callback, connections, connections_mutex](const error_code &ec) { - { - std::lock_guard lock(*connections_mutex); - connection->in_use=false; - - // Remove unused connections, but keep one open for HTTP persistent connection: - size_t unused_connections=0; - for(auto it=connections->begin();it!=connections->end();) { - if((*it)->in_use) - ++it; - else { - ++unused_connections; - if(unused_connections>1) - it=connections->erase(it); - else - ++it; - } - } - } - - if(*request_callback) - (*request_callback)(response, ec); - }; - - std::ostream write_stream(session->request_buffer.get()); - if(content.size()>0) - write_stream << "Content-Length: " << content.size() << "\r\n"; - write_stream << "\r\n" << content; - - Client::connect(session); - } - - /// Asynchronous request where setting and/or running Client's io_service is required. - void request(const std::string &method, const std::string &path, string_view content, - std::function, const error_code&)> &&request_callback) { - request(method, path, content, CaseInsensitiveMultimap(), std::move(request_callback)); - } - - /// Asynchronous request where setting and/or running Client's io_service is required. - void request(const std::string &method, const std::string &path, - std::function, const error_code&)> &&request_callback) { - request(method, path, std::string(), CaseInsensitiveMultimap(), std::move(request_callback)); - } - - /// Asynchronous request where setting and/or running Client's io_service is required. - void request(const std::string &method, std::function, const error_code&)> &&request_callback) { - request(method, std::string("/"), std::string(), CaseInsensitiveMultimap(), std::move(request_callback)); - } - - /// Asynchronous request where setting and/or running Client's io_service is required. - void request(const std::string &method, const std::string &path, std::istream& content, const CaseInsensitiveMultimap& header, - std::function, const error_code&)> &&request_callback_) { - auto session=std::make_shared(io_service, get_connection(), create_request_header(method, path, header)); - auto connection=session->connection; - auto response=session->response; - auto request_callback=std::make_shared, const error_code&)>>(std::move(request_callback_)); - auto connections=this->connections; - auto connections_mutex=this->connections_mutex; - session->callback=[connection, response, request_callback, connections, connections_mutex](const error_code &ec) { - { - std::lock_guard lock(*connections_mutex); - connection->in_use=false; - - // Remove unused connections, but keep one open for HTTP persistent connection: - size_t unused_connections=0; - for(auto it=connections->begin();it!=connections->end();) { - if((*it)->in_use) - ++it; - else { - ++unused_connections; - if(unused_connections>1) - it=connections->erase(it); - else - ++it; - } - } - } - - if(*request_callback) - (*request_callback)(response, ec); - }; - - content.seekg(0, std::ios::end); - auto content_length=content.tellg(); - content.seekg(0, std::ios::beg); - std::ostream write_stream(session->request_buffer.get()); - if(content_length>0) - write_stream << "Content-Length: " << content_length << "\r\n"; - write_stream << "\r\n"; - if(content_length>0) - write_stream << content.rdbuf(); - - Client::connect(session); - } - - /// Asynchronous request where setting and/or running Client's io_service is required. - void request(const std::string &method, const std::string &path, std::istream& content, - std::function, const error_code&)> &&request_callback) { - request(method, path, content, CaseInsensitiveMultimap(), std::move(request_callback)); - } - - protected: - std::string host; - unsigned short port; - - std::shared_ptr>> connections; - std::shared_ptr connections_mutex; - - ClientBase(const std::string& host_port, unsigned short default_port) : io_service(new asio::io_service()), connections(new std::vector>()), connections_mutex(new std::mutex()) { - auto parsed_host_port=parse_host_port(host_port, default_port); - host=parsed_host_port.first; - port=parsed_host_port.second; - } - - std::shared_ptr get_connection() { - std::shared_ptr connection; - std::lock_guard lock(*connections_mutex); - for(auto it=connections->begin();it!=connections->end();++it) { - if(!(*it)->in_use && !connection) { - connection=*it; - break; - } - } - if(!connection) { - connection=create_connection(); - connections->emplace_back(connection); - } - connection->reconnecting=false; - connection->in_use=true; - return connection; - } - - virtual std::shared_ptr create_connection()=0; - - std::unique_ptr create_request_header(const std::string& method, const std::string& path, const CaseInsensitiveMultimap& header) const { - auto corrected_path=path; - if(corrected_path=="") - corrected_path="/"; - if(!config.proxy_server.empty() && std::is_same::value) - corrected_path="http://"+host+':'+std::to_string(port)+corrected_path; - - std::unique_ptr request_buffer(new asio::streambuf()); - std::ostream write_stream(request_buffer.get()); - write_stream << method << " " << corrected_path << " HTTP/1.1\r\n"; - write_stream << "Host: " << host << "\r\n"; - for(auto& h: header) - write_stream << h.first << ": " << h.second << "\r\n"; - return request_buffer; - } - - static std::pair parse_host_port(const std::string &host_port, unsigned short default_port) { - std::pair parsed_host_port; - size_t host_end=host_port.find(':'); - if(host_end==std::string::npos) { - parsed_host_port.first=host_port; - parsed_host_port.second=default_port; - } - else { - parsed_host_port.first=host_port.substr(0, host_end); - parsed_host_port.second=static_cast(stoul(host_port.substr(host_end+1))); - } - return parsed_host_port; - } - - static std::shared_ptr get_timeout_timer(const std::shared_ptr &session, size_t timeout=0) { - if(timeout==0) - timeout=session->connection->config.timeout; - if(timeout==0) - return nullptr; - - auto timer=std::make_shared(*session->io_service); - timer->expires_from_now(boost::posix_time::seconds(timeout)); - timer->async_wait([session](const error_code& ec) { - if(!ec) - close(session); - }); - return timer; - } - - static void parse_response_header(const std::shared_ptr &response) { - std::string line; - getline(response->content, line); - size_t version_end=line.find(' '); - if(version_end!=std::string::npos) { - if(5http_version=line.substr(5, version_end-5); - if((version_end+1)status_code=line.substr(version_end+1, line.size()-(version_end+1)-1); - - getline(response->content, line); - size_t param_end; - while((param_end=line.find(':'))!=std::string::npos) { - size_t value_start=param_end+1; - if((value_start)header.insert(std::make_pair(line.substr(0, param_end), line.substr(value_start, line.size()-value_start-1))); - } - - getline(response->content, line); - } - } - } - - static void write(const std::shared_ptr &session) { - auto timer=get_timeout_timer(session); - asio::async_write(*session->connection->socket, session->request_buffer->data(), [session, timer](const error_code &ec, size_t /*bytes_transferred*/) { - if(timer) - timer->cancel(); - if(!ec) - read(session); - else { - close(session); - session->callback(ec); - } - }); - } - - static void read(const std::shared_ptr &session) { - auto timer=get_timeout_timer(session); - asio::async_read_until(*session->connection->socket, session->response->content_buffer, "\r\n\r\n", - [session, timer](const error_code& ec, size_t bytes_transferred) { - if(timer) - timer->cancel(); - if(!ec) { - session->connection->reconnecting=false; - - size_t num_additional_bytes=session->response->content_buffer.size()-bytes_transferred; - - parse_response_header(session->response); - - auto header_it=session->response->header.find("Content-Length"); - if(header_it!=session->response->header.end()) { - auto content_length=stoull(header_it->second); - if(content_length>num_additional_bytes) { - auto timer=get_timeout_timer(session); - asio::async_read(*session->connection->socket, session->response->content_buffer, asio::transfer_exactly(content_length-num_additional_bytes), - [session, timer](const error_code& ec, size_t /*bytes_transferred*/) { - if(timer) - timer->cancel(); - if(!ec) - session->callback(ec); - else { - close(session); - session->callback(ec); - } - }); - } - else - session->callback(ec); - } - else if((header_it=session->response->header.find("Transfer-Encoding"))!=session->response->header.end() && header_it->second=="chunked") { - auto tmp_streambuf=std::make_shared(); - read_chunked(session, tmp_streambuf); - } - else if(session->response->http_version<"1.1" || ((header_it=session->response->header.find("Session"))!=session->response->header.end() && header_it->second=="close")) { - auto timer=get_timeout_timer(session); - asio::async_read(*session->connection->socket, session->response->content_buffer, [session, timer](const error_code& ec, size_t /*bytes_transferred*/) { - if(timer) - timer->cancel(); - if(!ec) - session->callback(ec); - else { - close(session); - if(ec==asio::error::eof) { - error_code ec; - session->callback(ec); - } - else - session->callback(ec); - } - }); - } - else - session->callback(ec); - } - else { - if(!session->connection->reconnecting) { - session->connection->reconnecting=true; - close(session); - Client::connect(session); - } - else { - close(session); - session->callback(ec); - } - } - }); - } - - static void read_chunked(const std::shared_ptr &session, const std::shared_ptr &tmp_streambuf) { - auto timer=get_timeout_timer(session); - asio::async_read_until(*session->connection->socket, session->response->content_buffer, "\r\n", [session, tmp_streambuf, timer](const error_code& ec, size_t bytes_transferred) { - if(timer) - timer->cancel(); - if(!ec) { - std::string line; - getline(session->response->content, line); - bytes_transferred-=line.size()+1; - line.pop_back(); - std::streamsize length=stol(line, 0, 16); - - auto num_additional_bytes=static_cast(session->response->content_buffer.size()-bytes_transferred); - - auto post_process=[session, tmp_streambuf, length] { - std::ostream tmp_stream(tmp_streambuf.get()); - if(length>0) { - std::vector buffer(static_cast(length)); - session->response->content.read(&buffer[0], length); - tmp_stream.write(&buffer[0], length); - } - - //Remove "\r\n" - session->response->content.get(); - session->response->content.get(); - - if(length>0) - read_chunked(session, tmp_streambuf); - else { - std::ostream response_stream(&session->response->content_buffer); - response_stream << tmp_stream.rdbuf(); - error_code ec; - session->callback(ec); - } - }; - - if((2+length)>num_additional_bytes) { - auto timer=get_timeout_timer(session); - asio::async_read(*session->connection->socket, session->response->content_buffer, asio::transfer_exactly(2+length-num_additional_bytes), - [session, post_process, timer](const error_code& ec, size_t /*bytes_transferred*/) { - if(timer) - timer->cancel(); - if(!ec) - post_process(); - else { - close(session); - session->callback(ec); - } - }); - } - else - post_process(); - } - else { - close(session); - session->callback(ec); - } - }); - } - - static void close(const std::shared_ptr &session) { - error_code ec; - session->connection->socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec); - session->connection->socket->lowest_layer().close(ec); - } + size_t size() { + return streambuf.size(); + } + /// Convenience function to return std::string. Note that the stream buffer is emptied when this functions is used. + std::string string() { + std::stringstream ss; + ss << rdbuf(); + return ss.str(); + } + + private: + asio::streambuf &streambuf; + Content(asio::streambuf &streambuf) : std::istream(&streambuf), streambuf(streambuf) {} }; - - template - class Client : public ClientBase {}; - - typedef asio::ip::tcp::socket HTTP; - - template<> - class Client : public ClientBase { + + class Response { + friend class ClientBase; + friend class Client; + public: - friend ClientBase; - - Client(const std::string& server_port_path) : ClientBase::ClientBase(server_port_path, 80) {} - - protected: - std::shared_ptr create_connection() override { - return std::make_shared(host, port, config, std::unique_ptr(new HTTP(*io_service))); + std::string http_version, status_code; + + Content content; + + CaseInsensitiveMultimap header; + + private: + asio::streambuf content_buffer; + + Response() : content(content_buffer) {} + }; + + class Config { + friend class ClientBase; + + private: + Config() {} + + public: + /// Set timeout on requests in seconds. Default value: 0 (no timeout). + size_t timeout = 0; + /// Set connect timeout in seconds. Default value: 0 (Config::timeout is then used instead). + size_t timeout_connect = 0; + /// Set proxy server (server:port) + std::string proxy_server; + }; + + protected: + class Connection { + public: + Connection(const std::string &host, unsigned short port, const Config &config, std::unique_ptr &&socket) + : host(host), port(port), config(config), socket(std::move(socket)) { + if(config.proxy_server.empty()) + query = std::unique_ptr(new asio::ip::tcp::resolver::query(host, std::to_string(port))); + else { + auto proxy_host_port = parse_host_port(config.proxy_server, 8080); + query = std::unique_ptr(new asio::ip::tcp::resolver::query(proxy_host_port.first, std::to_string(proxy_host_port.second))); } - - static void connect(const std::shared_ptr &session) { - if(!session->connection->socket->lowest_layer().is_open()) { - auto resolver=std::make_shared(*session->io_service); - auto timer=get_timeout_timer(session, session->connection->config.timeout_connect); - resolver->async_resolve(*session->connection->query, [session, timer, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator it){ - if(timer) - timer->cancel(); - if(!ec) { - auto timer=get_timeout_timer(session, session->connection->config.timeout_connect); - asio::async_connect(*session->connection->socket, it, [session, timer, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator /*it*/){ - if(timer) - timer->cancel(); - if(!ec) { - asio::ip::tcp::no_delay option(true); - session->connection->socket->set_option(option); - write(session); - } - else { - close(session); - session->callback(ec); - } - }); - } - else { - close(session); - session->callback(ec); - } - }); + } + + std::string host; + unsigned short port; + Config config; + + std::unique_ptr socket; + bool in_use = false; + bool reconnecting = false; + + std::unique_ptr query; + }; + + class Session { + public: + Session(const std::shared_ptr &io_service, const std::shared_ptr &connection, std::unique_ptr &&request_buffer) + : io_service(io_service), connection(connection), request_buffer(std::move(request_buffer)), response(new Response()) {} + std::shared_ptr io_service; + std::shared_ptr connection; + std::unique_ptr request_buffer; + std::shared_ptr response; + std::function callback; + }; + + public: + /// Set before calling request + Config config; + + /// If you have your own asio::io_service, store its pointer here before calling request(). + /// When using asynchronous requests, running the io_service is up to the programmer. + std::shared_ptr io_service; + + virtual ~ClientBase() {} + + /// Convenience function to perform synchronous request. The io_service is run within this function. + /// If reusing the io_service for other tasks, please use the asynchronous request functions instead. + std::shared_ptr request(const std::string &method, const std::string &path = std::string("/"), + string_view content = "", const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + std::shared_ptr response; + request(method, path, content, header, [&response](std::shared_ptr response_, const error_code &ec) { + response = response_; + if(ec) + throw system_error(ec); + }); + + io_service->reset(); + io_service->run(); + + return response; + } + + /// Convenience function to perform synchronous request. The io_service is run within this function. + /// If reusing the io_service for other tasks, please use the asynchronous request functions instead. + std::shared_ptr request(const std::string &method, const std::string &path, std::istream &content, + const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + std::shared_ptr response; + request(method, path, content, header, [&response](std::shared_ptr response_, const error_code &ec) { + response = response_; + if(ec) + throw system_error(ec); + }); + + io_service->reset(); + io_service->run(); + + return response; + } + + /// Asynchronous request where setting and/or running Client's io_service is required. + void request(const std::string &method, const std::string &path, string_view content, const CaseInsensitiveMultimap &header, + std::function, const error_code &)> &&request_callback_) { + auto session = std::make_shared(io_service, get_connection(), create_request_header(method, path, header)); + auto connection = session->connection; + auto response = session->response; + auto request_callback = std::make_shared, const error_code &)>>(std::move(request_callback_)); + auto connections = this->connections; + auto connections_mutex = this->connections_mutex; + session->callback = [connection, response, request_callback, connections, connections_mutex](const error_code &ec) { + { + std::lock_guard lock(*connections_mutex); + connection->in_use = false; + + // Remove unused connections, but keep one open for HTTP persistent connection: + size_t unused_connections = 0; + for(auto it = connections->begin(); it != connections->end();) { + if((*it)->in_use) + ++it; + else { + ++unused_connections; + if(unused_connections > 1) + it = connections->erase(it); + else + ++it; + } + } + } + + if(*request_callback) + (*request_callback)(response, ec); + }; + + std::ostream write_stream(session->request_buffer.get()); + if(content.size() > 0) + write_stream << "Content-Length: " << content.size() << "\r\n"; + write_stream << "\r\n" + << content; + + Client::connect(session); + } + + /// Asynchronous request where setting and/or running Client's io_service is required. + void request(const std::string &method, const std::string &path, string_view content, + std::function, const error_code &)> &&request_callback) { + request(method, path, content, CaseInsensitiveMultimap(), std::move(request_callback)); + } + + /// Asynchronous request where setting and/or running Client's io_service is required. + void request(const std::string &method, const std::string &path, + std::function, const error_code &)> &&request_callback) { + request(method, path, std::string(), CaseInsensitiveMultimap(), std::move(request_callback)); + } + + /// Asynchronous request where setting and/or running Client's io_service is required. + void request(const std::string &method, std::function, const error_code &)> &&request_callback) { + request(method, std::string("/"), std::string(), CaseInsensitiveMultimap(), std::move(request_callback)); + } + + /// Asynchronous request where setting and/or running Client's io_service is required. + void request(const std::string &method, const std::string &path, std::istream &content, const CaseInsensitiveMultimap &header, + std::function, const error_code &)> &&request_callback_) { + auto session = std::make_shared(io_service, get_connection(), create_request_header(method, path, header)); + auto connection = session->connection; + auto response = session->response; + auto request_callback = std::make_shared, const error_code &)>>(std::move(request_callback_)); + auto connections = this->connections; + auto connections_mutex = this->connections_mutex; + session->callback = [connection, response, request_callback, connections, connections_mutex](const error_code &ec) { + { + std::lock_guard lock(*connections_mutex); + connection->in_use = false; + + // Remove unused connections, but keep one open for HTTP persistent connection: + size_t unused_connections = 0; + for(auto it = connections->begin(); it != connections->end();) { + if((*it)->in_use) + ++it; + else { + ++unused_connections; + if(unused_connections > 1) + it = connections->erase(it); + else + ++it; + } + } + } + + if(*request_callback) + (*request_callback)(response, ec); + }; + + content.seekg(0, std::ios::end); + auto content_length = content.tellg(); + content.seekg(0, std::ios::beg); + std::ostream write_stream(session->request_buffer.get()); + if(content_length > 0) + write_stream << "Content-Length: " << content_length << "\r\n"; + write_stream << "\r\n"; + if(content_length > 0) + write_stream << content.rdbuf(); + + Client::connect(session); + } + + /// Asynchronous request where setting and/or running Client's io_service is required. + void request(const std::string &method, const std::string &path, std::istream &content, + std::function, const error_code &)> &&request_callback) { + request(method, path, content, CaseInsensitiveMultimap(), std::move(request_callback)); + } + + protected: + std::string host; + unsigned short port; + + std::shared_ptr>> connections; + std::shared_ptr connections_mutex; + + ClientBase(const std::string &host_port, unsigned short default_port) + : io_service(new asio::io_service()), connections(new std::vector>()), connections_mutex(new std::mutex()) { + auto parsed_host_port = parse_host_port(host_port, default_port); + host = parsed_host_port.first; + port = parsed_host_port.second; + } + + std::shared_ptr get_connection() { + std::shared_ptr connection; + std::lock_guard lock(*connections_mutex); + for(auto it = connections->begin(); it != connections->end(); ++it) { + if(!(*it)->in_use && !connection) { + connection = *it; + break; + } + } + if(!connection) { + connection = create_connection(); + connections->emplace_back(connection); + } + connection->reconnecting = false; + connection->in_use = true; + return connection; + } + + virtual std::shared_ptr create_connection() = 0; + + std::unique_ptr create_request_header(const std::string &method, const std::string &path, const CaseInsensitiveMultimap &header) const { + auto corrected_path = path; + if(corrected_path == "") + corrected_path = "/"; + if(!config.proxy_server.empty() && std::is_same::value) + corrected_path = "http://" + host + ':' + std::to_string(port) + corrected_path; + + std::unique_ptr request_buffer(new asio::streambuf()); + std::ostream write_stream(request_buffer.get()); + write_stream << method << " " << corrected_path << " HTTP/1.1\r\n"; + write_stream << "Host: " << host << "\r\n"; + for(auto &h : header) + write_stream << h.first << ": " << h.second << "\r\n"; + return request_buffer; + } + + static std::pair parse_host_port(const std::string &host_port, unsigned short default_port) { + std::pair parsed_host_port; + size_t host_end = host_port.find(':'); + if(host_end == std::string::npos) { + parsed_host_port.first = host_port; + parsed_host_port.second = default_port; + } + else { + parsed_host_port.first = host_port.substr(0, host_end); + parsed_host_port.second = static_cast(stoul(host_port.substr(host_end + 1))); + } + return parsed_host_port; + } + + static std::shared_ptr get_timeout_timer(const std::shared_ptr &session, size_t timeout = 0) { + if(timeout == 0) + timeout = session->connection->config.timeout; + if(timeout == 0) + return nullptr; + + auto timer = std::make_shared(*session->io_service); + timer->expires_from_now(boost::posix_time::seconds(timeout)); + timer->async_wait([session](const error_code &ec) { + if(!ec) + close(session); + }); + return timer; + } + + static void parse_response_header(const std::shared_ptr &response) { + std::string line; + getline(response->content, line); + size_t version_end = line.find(' '); + if(version_end != std::string::npos) { + if(5 < line.size()) + response->http_version = line.substr(5, version_end - 5); + if((version_end + 1) < line.size()) + response->status_code = line.substr(version_end + 1, line.size() - (version_end + 1) - 1); + + getline(response->content, line); + size_t param_end; + while((param_end = line.find(':')) != std::string::npos) { + size_t value_start = param_end + 1; + if((value_start) < line.size()) { + if(line[value_start] == ' ') + value_start++; + if(value_start < line.size()) + response->header.insert(std::make_pair(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - 1))); + } + + getline(response->content, line); + } + } + } + + static void write(const std::shared_ptr &session) { + auto timer = get_timeout_timer(session); + asio::async_write(*session->connection->socket, session->request_buffer->data(), [session, timer](const error_code &ec, size_t /*bytes_transferred*/) { + if(timer) + timer->cancel(); + if(!ec) + read(session); + else { + close(session); + session->callback(ec); + } + }); + } + + static void read(const std::shared_ptr &session) { + auto timer = get_timeout_timer(session); + asio::async_read_until(*session->connection->socket, session->response->content_buffer, "\r\n\r\n", [session, timer](const error_code &ec, size_t bytes_transferred) { + if(timer) + timer->cancel(); + if(!ec) { + session->connection->reconnecting = false; + + size_t num_additional_bytes = session->response->content_buffer.size() - bytes_transferred; + + parse_response_header(session->response); + + auto header_it = session->response->header.find("Content-Length"); + if(header_it != session->response->header.end()) { + auto content_length = stoull(header_it->second); + if(content_length > num_additional_bytes) { + auto timer = get_timeout_timer(session); + asio::async_read(*session->connection->socket, session->response->content_buffer, asio::transfer_exactly(content_length - num_additional_bytes), [session, timer](const error_code &ec, size_t /*bytes_transferred*/) { + if(timer) + timer->cancel(); + if(!ec) + session->callback(ec); + else { + close(session); + session->callback(ec); + } + }); } else - write(session); + session->callback(ec); + } + else if((header_it = session->response->header.find("Transfer-Encoding")) != session->response->header.end() && header_it->second == "chunked") { + auto tmp_streambuf = std::make_shared(); + read_chunked(session, tmp_streambuf); + } + else if(session->response->http_version < "1.1" || ((header_it = session->response->header.find("Session")) != session->response->header.end() && header_it->second == "close")) { + auto timer = get_timeout_timer(session); + asio::async_read(*session->connection->socket, session->response->content_buffer, [session, timer](const error_code &ec, size_t /*bytes_transferred*/) { + if(timer) + timer->cancel(); + if(!ec) + session->callback(ec); + else { + close(session); + if(ec == asio::error::eof) { + error_code ec; + session->callback(ec); + } + else + session->callback(ec); + } + }); + } + else + session->callback(ec); } - }; -} + else { + if(!session->connection->reconnecting) { + session->connection->reconnecting = true; + close(session); + Client::connect(session); + } + else { + close(session); + session->callback(ec); + } + } + }); + } -#endif /* CLIENT_HTTP_HPP */ + static void read_chunked(const std::shared_ptr &session, const std::shared_ptr &tmp_streambuf) { + auto timer = get_timeout_timer(session); + asio::async_read_until(*session->connection->socket, session->response->content_buffer, "\r\n", [session, tmp_streambuf, timer](const error_code &ec, size_t bytes_transferred) { + if(timer) + timer->cancel(); + if(!ec) { + std::string line; + getline(session->response->content, line); + bytes_transferred -= line.size() + 1; + line.pop_back(); + std::streamsize length = stol(line, 0, 16); + + auto num_additional_bytes = static_cast(session->response->content_buffer.size() - bytes_transferred); + + auto post_process = [session, tmp_streambuf, length] { + std::ostream tmp_stream(tmp_streambuf.get()); + if(length > 0) { + std::vector buffer(static_cast(length)); + session->response->content.read(&buffer[0], length); + tmp_stream.write(&buffer[0], length); + } + + //Remove "\r\n" + session->response->content.get(); + session->response->content.get(); + + if(length > 0) + read_chunked(session, tmp_streambuf); + else { + std::ostream response_stream(&session->response->content_buffer); + response_stream << tmp_stream.rdbuf(); + error_code ec; + session->callback(ec); + } + }; + + if((2 + length) > num_additional_bytes) { + auto timer = get_timeout_timer(session); + asio::async_read(*session->connection->socket, session->response->content_buffer, asio::transfer_exactly(2 + length - num_additional_bytes), [session, post_process, timer](const error_code &ec, size_t /*bytes_transferred*/) { + if(timer) + timer->cancel(); + if(!ec) + post_process(); + else { + close(session); + session->callback(ec); + } + }); + } + else + post_process(); + } + else { + close(session); + session->callback(ec); + } + }); + } + + static void close(const std::shared_ptr &session) { + error_code ec; + session->connection->socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec); + session->connection->socket->lowest_layer().close(ec); + } + }; + + template + class Client : public ClientBase {}; + + typedef asio::ip::tcp::socket HTTP; + + template <> + class Client : public ClientBase { + public: + friend ClientBase; + + Client(const std::string &server_port_path) : ClientBase::ClientBase(server_port_path, 80) {} + + protected: + std::shared_ptr create_connection() override { + return std::make_shared(host, port, config, std::unique_ptr(new HTTP(*io_service))); + } + + static void connect(const std::shared_ptr &session) { + if(!session->connection->socket->lowest_layer().is_open()) { + auto resolver = std::make_shared(*session->io_service); + auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); + resolver->async_resolve(*session->connection->query, [session, timer, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator it) { + if(timer) + timer->cancel(); + if(!ec) { + auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); + asio::async_connect(*session->connection->socket, it, [session, timer, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator /*it*/) { + if(timer) + timer->cancel(); + if(!ec) { + asio::ip::tcp::no_delay option(true); + session->connection->socket->set_option(option); + write(session); + } + else { + close(session); + session->callback(ec); + } + }); + } + else { + close(session); + session->callback(ec); + } + }); + } + else + write(session); + } + }; +} // namespace SimpleWeb + +#endif /* CLIENT_HTTP_HPP */ diff --git a/client_https.hpp b/client_https.hpp index 36ec316..6d55e86 100644 --- a/client_https.hpp +++ b/client_https.hpp @@ -1,5 +1,5 @@ #ifndef CLIENT_HTTPS_HPP -#define CLIENT_HTTPS_HPP +#define CLIENT_HTTPS_HPP #include "client_http.hpp" @@ -10,125 +10,125 @@ #endif namespace SimpleWeb { - typedef asio::ssl::stream HTTPS; - - template<> - class Client : public ClientBase { - public: - friend ClientBase; - - Client(const std::string& server_port_path, bool verify_certificate=true, - const std::string& cert_file=std::string(), const std::string& private_key_file=std::string(), - const std::string& verify_file=std::string()) : - ClientBase::ClientBase(server_port_path, 443), context(asio::ssl::context::tlsv12) { - if(cert_file.size()>0 && private_key_file.size()>0) { - context.use_certificate_chain_file(cert_file); - context.use_private_key_file(private_key_file, asio::ssl::context::pem); - } - - if(verify_certificate) - context.set_verify_callback(asio::ssl::rfc2818_verification(host)); - - if(verify_file.size()>0) - context.load_verify_file(verify_file); - else - context.set_default_verify_paths(); - - if(verify_file.size()>0 || verify_certificate) - context.set_verify_mode(asio::ssl::verify_peer); - else - context.set_verify_mode(asio::ssl::verify_none); - } + typedef asio::ssl::stream HTTPS; - protected: - asio::ssl::context context; - - std::shared_ptr create_connection() override { - return std::make_shared(host, port, config, std::unique_ptr(new HTTPS(*io_service, context))); - } - - static void connect(const std::shared_ptr &session) { - if(!session->connection->socket->lowest_layer().is_open()) { - auto resolver=std::make_shared(*session->io_service); - resolver->async_resolve(*session->connection->query, [session, resolver] (const error_code &ec, asio::ip::tcp::resolver::iterator it){ + template <> + class Client : public ClientBase { + public: + friend ClientBase; + + Client(const std::string &server_port_path, bool verify_certificate = true, const std::string &cert_file = std::string(), + const std::string &private_key_file = std::string(), const std::string &verify_file = std::string()) + : ClientBase::ClientBase(server_port_path, 443), context(asio::ssl::context::tlsv12) { + if(cert_file.size() > 0 && private_key_file.size() > 0) { + context.use_certificate_chain_file(cert_file); + context.use_private_key_file(private_key_file, asio::ssl::context::pem); + } + + if(verify_certificate) + context.set_verify_callback(asio::ssl::rfc2818_verification(host)); + + if(verify_file.size() > 0) + context.load_verify_file(verify_file); + else + context.set_default_verify_paths(); + + if(verify_file.size() > 0 || verify_certificate) + context.set_verify_mode(asio::ssl::verify_peer); + else + context.set_verify_mode(asio::ssl::verify_none); + } + + protected: + asio::ssl::context context; + + std::shared_ptr create_connection() override { + return std::make_shared(host, port, config, std::unique_ptr(new HTTPS(*io_service, context))); + } + + static void connect(const std::shared_ptr &session) { + if(!session->connection->socket->lowest_layer().is_open()) { + auto resolver = std::make_shared(*session->io_service); + resolver->async_resolve(*session->connection->query, [session, resolver](const error_code &ec, asio::ip::tcp::resolver::iterator it) { + if(!ec) { + auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); + asio::async_connect(session->connection->socket->lowest_layer(), it, [session, resolver, timer](const error_code &ec, asio::ip::tcp::resolver::iterator /*it*/) { + if(timer) + timer->cancel(); + if(!ec) { + asio::ip::tcp::no_delay option(true); + session->connection->socket->lowest_layer().set_option(option); + + if(!session->connection->config.proxy_server.empty()) { + auto write_buffer = std::make_shared(); + std::ostream write_stream(write_buffer.get()); + auto host_port = session->connection->host + ':' + std::to_string(session->connection->port); + write_stream << "CONNECT " + host_port + " HTTP/1.1\r\n" + << "Host: " << host_port << "\r\n\r\n"; + auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); + asio::async_write(session->connection->socket->next_layer(), *write_buffer, [session, write_buffer, timer](const error_code &ec, size_t /*bytes_transferred*/) { + if(timer) + timer->cancel(); if(!ec) { - auto timer=get_timeout_timer(session, session->connection->config.timeout_connect); - asio::async_connect(session->connection->socket->lowest_layer(), it, [session, resolver, timer] (const error_code &ec, asio::ip::tcp::resolver::iterator /*it*/){ - if(timer) - timer->cancel(); - if(!ec) { - asio::ip::tcp::no_delay option(true); - session->connection->socket->lowest_layer().set_option(option); - - if(!session->connection->config.proxy_server.empty()) { - auto write_buffer=std::make_shared(); - std::ostream write_stream(write_buffer.get()); - auto host_port=session->connection->host+':'+std::to_string(session->connection->port); - write_stream << "CONNECT "+host_port+" HTTP/1.1\r\n" << "Host: " << host_port << "\r\n\r\n"; - auto timer=get_timeout_timer(session, session->connection->config.timeout_connect); - asio::async_write(session->connection->socket->next_layer(), *write_buffer, [session, write_buffer, timer](const error_code &ec, size_t /*bytes_transferred*/) { - if(timer) - timer->cancel(); - if(!ec) { - std::shared_ptr response(new Response()); - auto timer=get_timeout_timer(session, session->connection->config.timeout_connect); - asio::async_read_until(session->connection->socket->next_layer(), response->content_buffer, "\r\n\r\n", [session, response, timer](const error_code& ec, size_t /*bytes_transferred*/) { - if(timer) - timer->cancel(); - if(!ec) { - parse_response_header(response); - if (response->status_code.empty() || response->status_code.compare(0, 3, "200") != 0) { - close(session); - session->callback(make_error_code::make_error_code(errc::permission_denied)); - } - else - handshake(session); - } - else { - close(session); - session->callback(ec); - } - }); - } - else { - close(session); - session->callback(ec); - } - }); - } - else - handshake(session); - } - else { - close(session); - session->callback(ec); - } - }); + std::shared_ptr response(new Response()); + auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); + asio::async_read_until(session->connection->socket->next_layer(), response->content_buffer, "\r\n\r\n", [session, response, timer](const error_code &ec, size_t /*bytes_transferred*/) { + if(timer) + timer->cancel(); + if(!ec) { + parse_response_header(response); + if(response->status_code.empty() || response->status_code.compare(0, 3, "200") != 0) { + close(session); + session->callback(make_error_code::make_error_code(errc::permission_denied)); + } + else + handshake(session); + } + else { + close(session); + session->callback(ec); + } + }); } else { - close(session); - session->callback(ec); + close(session); + session->callback(ec); } - }); - } - else - write(session); - } - - static void handshake(const std::shared_ptr &session) { - auto timer=get_timeout_timer(session, session->connection->config.timeout_connect); - session->connection->socket->async_handshake(asio::ssl::stream_base::client, [session, timer](const error_code& ec) { - if(timer) - timer->cancel(); - if(!ec) - write(session); - else { - close(session); - session->callback(ec); + }); } + else + handshake(session); + } + else { + close(session); + session->callback(ec); + } }); - } - }; -} + } + else { + close(session); + session->callback(ec); + } + }); + } + else + write(session); + } -#endif /* CLIENT_HTTPS_HPP */ + static void handshake(const std::shared_ptr &session) { + auto timer = get_timeout_timer(session, session->connection->config.timeout_connect); + session->connection->socket->async_handshake(asio::ssl::stream_base::client, [session, timer](const error_code &ec) { + if(timer) + timer->cancel(); + if(!ec) + write(session); + else { + close(session); + session->callback(ec); + } + }); + } + }; +} // namespace SimpleWeb + +#endif /* CLIENT_HTTPS_HPP */ diff --git a/crypto.hpp b/crypto.hpp index 8421c42..ef92219 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -1,219 +1,220 @@ #ifndef SIMPLE_WEB_CRYPTO_HPP -#define SIMPLE_WEB_CRYPTO_HPP +#define SIMPLE_WEB_CRYPTO_HPP -#include #include -#include #include #include +#include +#include #include //Moving these to a seperate namespace for minimal global namespace cluttering does not work with clang++ -#include #include -#include +#include #include +#include namespace SimpleWeb { - //TODO 2017: remove workaround for MSVS 2012 - #if _MSC_VER == 1700 //MSVS 2012 has no definition for round() - inline double round(double x) { //custom definition of round() for positive numbers - return floor(x + 0.5); - } - #endif +//TODO 2017: remove workaround for MSVS 2012 +#if _MSC_VER == 1700 //MSVS 2012 has no definition for round() + inline double round(double x) { //custom definition of round() for positive numbers + return floor(x + 0.5); + } +#endif - class Crypto { - const static size_t buffer_size=131072; + class Crypto { + const static size_t buffer_size = 131072; + + public: + class Base64 { public: - class Base64 { - public: - static std::string encode(const std::string &ascii) { - std::string base64; - - BIO *bio, *b64; - BUF_MEM *bptr=BUF_MEM_new(); + static std::string encode(const std::string &ascii) { + std::string base64; - b64 = BIO_new(BIO_f_base64()); - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - bio = BIO_new(BIO_s_mem()); - BIO_push(b64, bio); - BIO_set_mem_buf(b64, bptr, BIO_CLOSE); - - //Write directly to base64-buffer to avoid copy - int base64_length=static_cast(round(4*ceil((double)ascii.size()/3.0))); - base64.resize(base64_length); - bptr->length=0; - bptr->max=base64_length+1; - bptr->data=(char*)&base64[0]; + BIO *bio, *b64; + BUF_MEM *bptr = BUF_MEM_new(); - BIO_write(b64, &ascii[0], static_cast(ascii.size())); - BIO_flush(b64); + b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + bio = BIO_new(BIO_s_mem()); + BIO_push(b64, bio); + BIO_set_mem_buf(b64, bptr, BIO_CLOSE); - //To keep &base64[0] through BIO_free_all(b64) - bptr->length=0; - bptr->max=0; - bptr->data=nullptr; + //Write directly to base64-buffer to avoid copy + int base64_length = static_cast(round(4 * ceil((double)ascii.size() / 3.0))); + base64.resize(base64_length); + bptr->length = 0; + bptr->max = base64_length + 1; + bptr->data = (char *)&base64[0]; - BIO_free_all(b64); - - return base64; - } - - static std::string decode(const std::string &base64) { - std::string ascii; - - //Resize ascii, however, the size is a up to two bytes too large. - ascii.resize((6*base64.size())/8); - BIO *b64, *bio; + BIO_write(b64, &ascii[0], static_cast(ascii.size())); + BIO_flush(b64); - b64 = BIO_new(BIO_f_base64()); - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - bio = BIO_new_mem_buf((char*)&base64[0], static_cast(base64.size())); - bio = BIO_push(b64, bio); + //To keep &base64[0] through BIO_free_all(b64) + bptr->length = 0; + bptr->max = 0; + bptr->data = nullptr; - int decoded_length = BIO_read(bio, &ascii[0], static_cast(ascii.size())); - ascii.resize(decoded_length); + BIO_free_all(b64); - BIO_free_all(b64); - - return ascii; - } - }; - - /// Return hex string from bytes in input string. - static std::string to_hex_string(const std::string &input) { - std::stringstream hex_stream; - hex_stream << std::hex << std::internal << std::setfill('0'); - for (auto &byte : input) - hex_stream << std::setw(2) << static_cast(static_cast(byte)); - return hex_stream.str(); - } - - static std::string md5(const std::string &input, size_t iterations=1) { - std::string hash; - - hash.resize(128 / 8); - MD5(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); - - for (size_t c = 1; c < iterations; ++c) - MD5(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); - - return hash; - } - - static std::string md5(std::istream &stream, size_t iterations=1) { - MD5_CTX context; - MD5_Init(&context); - std::streamsize read_length; - std::vector buffer(buffer_size); - while((read_length=stream.read(&buffer[0], buffer_size).gcount())>0) - MD5_Update(&context, buffer.data(), read_length); - std::string hash; - hash.resize(128 / 8); - MD5_Final(reinterpret_cast(&hash[0]), &context); - - for (size_t c = 1; c < iterations; ++c) - MD5(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); - - return hash; - } + return base64; + } - static std::string sha1(const std::string &input, size_t iterations=1) { - std::string hash; - - hash.resize(160 / 8); - SHA1(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); - - for (size_t c = 1; c < iterations; ++c) - SHA1(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); - - return hash; - } - - static std::string sha1(std::istream &stream, size_t iterations=1) { - SHA_CTX context; - SHA1_Init(&context); - std::streamsize read_length; - std::vector buffer(buffer_size); - while((read_length=stream.read(&buffer[0], buffer_size).gcount())>0) - SHA1_Update(&context, buffer.data(), read_length); - std::string hash; - hash.resize(160 / 8); - SHA1_Final(reinterpret_cast(&hash[0]), &context); - - for (size_t c = 1; c < iterations; ++c) - SHA1(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); - - return hash; - } + static std::string decode(const std::string &base64) { + std::string ascii; - static std::string sha256(const std::string &input, size_t iterations=1) { - std::string hash; - - hash.resize(256 / 8); - SHA256(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); - - for (size_t c = 1; c < iterations; ++c) - SHA256(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); - - return hash; - } - - static std::string sha256(std::istream &stream, size_t iterations=1) { - SHA256_CTX context; - SHA256_Init(&context); - std::streamsize read_length; - std::vector buffer(buffer_size); - while((read_length=stream.read(&buffer[0], buffer_size).gcount())>0) - SHA256_Update(&context, buffer.data(), read_length); - std::string hash; - hash.resize(256 / 8); - SHA256_Final(reinterpret_cast(&hash[0]), &context); - - for (size_t c = 1; c < iterations; ++c) - SHA256(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); - - return hash; - } + //Resize ascii, however, the size is a up to two bytes too large. + ascii.resize((6 * base64.size()) / 8); + BIO *b64, *bio; - static std::string sha512(const std::string &input, size_t iterations=1) { - std::string hash; - - hash.resize(512 / 8); - SHA512(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); - - for (size_t c = 1; c < iterations; ++c) - SHA512(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); - - return hash; - } - - static std::string sha512(std::istream &stream, size_t iterations=1) { - SHA512_CTX context; - SHA512_Init(&context); - std::streamsize read_length; - std::vector buffer(buffer_size); - while((read_length=stream.read(&buffer[0], buffer_size).gcount())>0) - SHA512_Update(&context, buffer.data(), read_length); - std::string hash; - hash.resize(512 / 8); - SHA512_Final(reinterpret_cast(&hash[0]), &context); - - for (size_t c = 1; c < iterations; ++c) - SHA512(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); - - return hash; - } - - /// key_size is number of bytes of the returned key. - static std::string pbkdf2(const std::string &password, const std::string &salt, int iterations, int key_size) { - std::string key; - key.resize(key_size); - PKCS5_PBKDF2_HMAC_SHA1(password.c_str(), password.size(), - reinterpret_cast(salt.c_str()), salt.size(), iterations, - key_size, reinterpret_cast(&key[0])); - return key; - } + b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + bio = BIO_new_mem_buf((char *)&base64[0], static_cast(base64.size())); + bio = BIO_push(b64, bio); + + int decoded_length = BIO_read(bio, &ascii[0], static_cast(ascii.size())); + ascii.resize(decoded_length); + + BIO_free_all(b64); + + return ascii; + } }; + + /// Return hex string from bytes in input string. + static std::string to_hex_string(const std::string &input) { + std::stringstream hex_stream; + hex_stream << std::hex << std::internal << std::setfill('0'); + for(auto &byte : input) + hex_stream << std::setw(2) << static_cast(static_cast(byte)); + return hex_stream.str(); + } + + static std::string md5(const std::string &input, size_t iterations = 1) { + std::string hash; + + hash.resize(128 / 8); + MD5(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); + + for(size_t c = 1; c < iterations; ++c) + MD5(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); + + return hash; + } + + static std::string md5(std::istream &stream, size_t iterations = 1) { + MD5_CTX context; + MD5_Init(&context); + std::streamsize read_length; + std::vector buffer(buffer_size); + while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) + MD5_Update(&context, buffer.data(), read_length); + std::string hash; + hash.resize(128 / 8); + MD5_Final(reinterpret_cast(&hash[0]), &context); + + for(size_t c = 1; c < iterations; ++c) + MD5(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); + + return hash; + } + + static std::string sha1(const std::string &input, size_t iterations = 1) { + std::string hash; + + hash.resize(160 / 8); + SHA1(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); + + for(size_t c = 1; c < iterations; ++c) + SHA1(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); + + return hash; + } + + static std::string sha1(std::istream &stream, size_t iterations = 1) { + SHA_CTX context; + SHA1_Init(&context); + std::streamsize read_length; + std::vector buffer(buffer_size); + while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) + SHA1_Update(&context, buffer.data(), read_length); + std::string hash; + hash.resize(160 / 8); + SHA1_Final(reinterpret_cast(&hash[0]), &context); + + for(size_t c = 1; c < iterations; ++c) + SHA1(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); + + return hash; + } + + static std::string sha256(const std::string &input, size_t iterations = 1) { + std::string hash; + + hash.resize(256 / 8); + SHA256(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); + + for(size_t c = 1; c < iterations; ++c) + SHA256(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); + + return hash; + } + + static std::string sha256(std::istream &stream, size_t iterations = 1) { + SHA256_CTX context; + SHA256_Init(&context); + std::streamsize read_length; + std::vector buffer(buffer_size); + while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) + SHA256_Update(&context, buffer.data(), read_length); + std::string hash; + hash.resize(256 / 8); + SHA256_Final(reinterpret_cast(&hash[0]), &context); + + for(size_t c = 1; c < iterations; ++c) + SHA256(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); + + return hash; + } + + static std::string sha512(const std::string &input, size_t iterations = 1) { + std::string hash; + + hash.resize(512 / 8); + SHA512(reinterpret_cast(&input[0]), input.size(), reinterpret_cast(&hash[0])); + + for(size_t c = 1; c < iterations; ++c) + SHA512(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); + + return hash; + } + + static std::string sha512(std::istream &stream, size_t iterations = 1) { + SHA512_CTX context; + SHA512_Init(&context); + std::streamsize read_length; + std::vector buffer(buffer_size); + while((read_length = stream.read(&buffer[0], buffer_size).gcount()) > 0) + SHA512_Update(&context, buffer.data(), read_length); + std::string hash; + hash.resize(512 / 8); + SHA512_Final(reinterpret_cast(&hash[0]), &context); + + for(size_t c = 1; c < iterations; ++c) + SHA512(reinterpret_cast(&hash[0]), hash.size(), reinterpret_cast(&hash[0])); + + return hash; + } + + /// key_size is number of bytes of the returned key. + static std::string pbkdf2(const std::string &password, const std::string &salt, int iterations, int key_size) { + std::string key; + key.resize(key_size); + PKCS5_PBKDF2_HMAC_SHA1(password.c_str(), password.size(), + reinterpret_cast(salt.c_str()), salt.size(), iterations, + key_size, reinterpret_cast(&key[0])); + return key; + } + }; } -#endif /* SIMPLE_WEB_CRYPTO_HPP */ +#endif /* SIMPLE_WEB_CRYPTO_HPP */ diff --git a/http_examples.cpp b/http_examples.cpp index aa91c4e..1b284f0 100644 --- a/http_examples.cpp +++ b/http_examples.cpp @@ -1,16 +1,16 @@ -#include "server_http.hpp" #include "client_http.hpp" +#include "server_http.hpp" //Added for the json-example #define BOOST_SPIRIT_THREADSAFE -#include #include +#include //Added for the default_resource example -#include -#include -#include #include +#include +#include +#include #ifdef HAVE_OPENSSL #include "crypto.hpp" #endif @@ -23,224 +23,224 @@ typedef SimpleWeb::Server HttpServer; typedef SimpleWeb::Client HttpClient; //Added for the default_resource example -void default_resource_send(const HttpServer &server, const shared_ptr &response, - const shared_ptr &ifs); +void default_resource_send(const HttpServer &server, const shared_ptr &response, const shared_ptr &ifs); int main() { - //HTTP-server at port 8080 using 1 thread - //Unless you do more heavy non-threaded processing in the resources, - //1 thread is usually faster than several threads - HttpServer server; - server.config.port=8080; - - //Add resources using path-regex and method-string, and an anonymous function - //POST-example for the path /string, responds the posted string - server.resource["^/string$"]["POST"]=[](shared_ptr response, shared_ptr request) { - //Retrieve string: - auto content=request->content.string(); - //request->content.string() is a convenience function for: - //stringstream ss; - //ss << request->content.rdbuf(); - //auto content=ss.str(); - - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; - - - // Alternatively, use one of the convenience functions, for instance: - // response->write(content); - }; - - //POST-example for the path /json, responds firstName+" "+lastName from the posted json - //Responds with an appropriate error message if the posted json is not valid, or if firstName or lastName is missing - //Example posted json: - //{ - // "firstName": "John", - // "lastName": "Smith", - // "age": 25 - //} - server.resource["^/json$"]["POST"]=[](shared_ptr response, shared_ptr request) { - try { - ptree pt; - read_json(request->content, pt); + //HTTP-server at port 8080 using 1 thread + //Unless you do more heavy non-threaded processing in the resources, + //1 thread is usually faster than several threads + HttpServer server; + server.config.port = 8080; - auto name=pt.get("firstName")+" "+pt.get("lastName"); + //Add resources using path-regex and method-string, and an anonymous function + //POST-example for the path /string, responds the posted string + server.resource["^/string$"]["POST"] = [](shared_ptr response, shared_ptr request) { + //Retrieve string: + auto content = request->content.string(); + //request->content.string() is a convenience function for: + //stringstream ss; + //ss << request->content.rdbuf(); + //auto content=ss.str(); - *response << "HTTP/1.1 200 OK\r\n" - << "Content-Type: application/json\r\n" - << "Content-Length: " << name.length() << "\r\n\r\n" - << name; - } - catch(const exception &e) { - *response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" << e.what(); - } - - - // Alternatively, using a convenience function: - // try { - // ptree pt; - // read_json(request->content, pt); - - // auto name=pt.get("firstName")+" "+pt.get("lastName"); - // response->write(name, {{"Content-Type", "application/json"}}); - // } - // catch(const exception &e) { - // response->write(SimpleWeb::StatusCode::client_error_bad_request, e.what()); - // } - }; + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" + << content; - //GET-example for the path /info - //Responds with request-information - server.resource["^/info$"]["GET"]=[](shared_ptr response, shared_ptr request) { - stringstream stream; - stream << "

Request from " << request->remote_endpoint_address << " (" << request->remote_endpoint_port << ")

"; - stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; - for(auto &header: request->header) - stream << header.first << ": " << header.second << "
"; - - //find length of content_stream (length received using content_stream.tellp()) - stream.seekp(0, ios::end); - - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << stream.tellp() << "\r\n\r\n" << stream.rdbuf(); - - - // Alternatively, using a convenience function: - // stringstream stream; - // stream << "

Request from " << request->remote_endpoint_address << " (" << request->remote_endpoint_port << ")

"; - // stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; - // for(auto &header: request->header) - // stream << header.first << ": " << header.second << "
"; - // response->write(stream); - }; - - //GET-example for the path /match/[number], responds with the matched string in path (number) - //For instance a request GET /match/123 will receive: 123 - server.resource["^/match/([0-9]+)$"]["GET"]=[](shared_ptr response, shared_ptr request) { - string number=request->path_match[1]; - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" << number; - - - // Alternatively, using a convenience function: - // response->write(request->path_match[1]); - }; - - //Get example simulating heavy work in a separate thread - server.resource["^/work$"]["GET"]=[](shared_ptr response, shared_ptr /*request*/) { - thread work_thread([response] { - this_thread::sleep_for(chrono::seconds(5)); - response->write("Work done"); - }); - work_thread.detach(); - }; - - //Default GET-example. If no other matches, this anonymous function will be called. - //Will respond with content in the web/-directory, and its subdirectories. - //Default file: index.html - //Can for instance be used to retrieve an HTML 5 client that uses REST-resources on this server - server.default_resource["GET"]=[&server](shared_ptr response, shared_ptr request) { - try { - auto web_root_path=boost::filesystem::canonical("web"); - auto path=boost::filesystem::canonical(web_root_path/request->path); - //Check if path is within web_root_path - if(distance(web_root_path.begin(), web_root_path.end())>distance(path.begin(), path.end()) || - !equal(web_root_path.begin(), web_root_path.end(), path.begin())) - throw invalid_argument("path must be within root path"); - if(boost::filesystem::is_directory(path)) - path/="index.html"; - SimpleWeb::CaseInsensitiveMultimap header; - - // Uncomment the following line to enable Cache-Control - // header.emplace("Cache-Control", "max-age=86400"); + // Alternatively, use one of the convenience functions, for instance: + // response->write(content); + }; + + //POST-example for the path /json, responds firstName+" "+lastName from the posted json + //Responds with an appropriate error message if the posted json is not valid, or if firstName or lastName is missing + //Example posted json: + //{ + // "firstName": "John", + // "lastName": "Smith", + // "age": 25 + //} + server.resource["^/json$"]["POST"] = [](shared_ptr response, shared_ptr request) { + try { + ptree pt; + read_json(request->content, pt); + + auto name = pt.get("firstName") + " " + pt.get("lastName"); + + *response << "HTTP/1.1 200 OK\r\n" + << "Content-Type: application/json\r\n" + << "Content-Length: " << name.length() << "\r\n\r\n" + << name; + } + catch(const exception &e) { + *response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" + << e.what(); + } + + + // Alternatively, using a convenience function: + // try { + // ptree pt; + // read_json(request->content, pt); + + // auto name=pt.get("firstName")+" "+pt.get("lastName"); + // response->write(name, {{"Content-Type", "application/json"}}); + // } + // catch(const exception &e) { + // response->write(SimpleWeb::StatusCode::client_error_bad_request, e.what()); + // } + }; + + //GET-example for the path /info + //Responds with request-information + server.resource["^/info$"]["GET"] = [](shared_ptr response, shared_ptr request) { + stringstream stream; + stream << "

Request from " << request->remote_endpoint_address << " (" << request->remote_endpoint_port << ")

"; + stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; + for(auto &header : request->header) + stream << header.first << ": " << header.second << "
"; + + //find length of content_stream (length received using content_stream.tellp()) + stream.seekp(0, ios::end); + + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << stream.tellp() << "\r\n\r\n" + << stream.rdbuf(); + + + // Alternatively, using a convenience function: + // stringstream stream; + // stream << "

Request from " << request->remote_endpoint_address << " (" << request->remote_endpoint_port << ")

"; + // stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; + // for(auto &header: request->header) + // stream << header.first << ": " << header.second << "
"; + // response->write(stream); + }; + + //GET-example for the path /match/[number], responds with the matched string in path (number) + //For instance a request GET /match/123 will receive: 123 + server.resource["^/match/([0-9]+)$"]["GET"] = [](shared_ptr response, shared_ptr request) { + string number = request->path_match[1]; + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" + << number; + + + // Alternatively, using a convenience function: + // response->write(request->path_match[1]); + }; + + //Get example simulating heavy work in a separate thread + server.resource["^/work$"]["GET"] = [](shared_ptr response, shared_ptr /*request*/) { + thread work_thread([response] { + this_thread::sleep_for(chrono::seconds(5)); + response->write("Work done"); + }); + work_thread.detach(); + }; + + //Default GET-example. If no other matches, this anonymous function will be called. + //Will respond with content in the web/-directory, and its subdirectories. + //Default file: index.html + //Can for instance be used to retrieve an HTML 5 client that uses REST-resources on this server + server.default_resource["GET"] = [&server](shared_ptr response, shared_ptr request) { + try { + auto web_root_path = boost::filesystem::canonical("web"); + auto path = boost::filesystem::canonical(web_root_path / request->path); + //Check if path is within web_root_path + if(distance(web_root_path.begin(), web_root_path.end()) > distance(path.begin(), path.end()) || + !equal(web_root_path.begin(), web_root_path.end(), path.begin())) + throw invalid_argument("path must be within root path"); + if(boost::filesystem::is_directory(path)) + path /= "index.html"; + + SimpleWeb::CaseInsensitiveMultimap header; + +// Uncomment the following line to enable Cache-Control +// header.emplace("Cache-Control", "max-age=86400"); #ifdef HAVE_OPENSSL - // Uncomment the following lines to enable ETag - // { - // ifstream ifs(path.string(), ifstream::in | ios::binary); - // if(ifs) { - // auto hash=SimpleWeb::Crypto::to_hex_string(SimpleWeb::Crypto::md5(ifs)); - // header.emplace("ETag", "\""+hash+"\""); - // auto it=request->header.find("If-None-Match"); - // if(it!=request->header.end()) { - // if(!it->second.empty() && it->second.compare(1, hash.size(), hash)==0) { - // response->write(SimpleWeb::StatusCode::redirection_not_modified, header); - // return; - // } - // } - // } - // else - // throw invalid_argument("could not read file"); - // } +// Uncomment the following lines to enable ETag +// { +// ifstream ifs(path.string(), ifstream::in | ios::binary); +// if(ifs) { +// auto hash = SimpleWeb::Crypto::to_hex_string(SimpleWeb::Crypto::md5(ifs)); +// header.emplace("ETag", "\"" + hash + "\""); +// auto it = request->header.find("If-None-Match"); +// if(it != request->header.end()) { +// if(!it->second.empty() && it->second.compare(1, hash.size(), hash) == 0) { +// response->write(SimpleWeb::StatusCode::redirection_not_modified, header); +// return; +// } +// } +// } +// else +// throw invalid_argument("could not read file"); +// } #endif - auto ifs=make_shared(); - ifs->open(path.string(), ifstream::in | ios::binary | ios::ate); - - if(*ifs) { - auto length=ifs->tellg(); - ifs->seekg(0, ios::beg); - - header.emplace("Content-Length", to_string(length)); - response->write(header); - default_resource_send(server, response, ifs); - } - else - throw invalid_argument("could not read file"); - } - catch(const exception &e) { - response->write(SimpleWeb::StatusCode::client_error_bad_request, "Could not open path "+request->path+": "+e.what()); - } - }; - - server.on_error=[](std::shared_ptr /*request*/, const SimpleWeb::error_code &/*ec*/) { - // handle errors here - }; - - thread server_thread([&server](){ - //Start server - server.start(); - }); - - //Wait for server to start so that the client can connect - this_thread::sleep_for(chrono::seconds(1)); - - //Client examples - HttpClient client("localhost:8080"); - - // synchronous request examples - auto r1=client.request("GET", "/match/123"); - cout << r1->content.rdbuf() << endl; // Alternatively, use the convenience function r1->content.string() + auto ifs = make_shared(); + ifs->open(path.string(), ifstream::in | ios::binary | ios::ate); - string json_string="{\"firstName\": \"John\",\"lastName\": \"Smith\",\"age\": 25}"; - auto r2=client.request("POST", "/string", json_string); - cout << r2->content.rdbuf() << endl; - - // asynchronous request example - client.request("POST", "/json", json_string, [](std::shared_ptr response, const SimpleWeb::error_code &ec) { - if(!ec) - cout << response->content.rdbuf() << endl; - }); - client.io_service->reset(); // needed because the io_service has been run already in the synchronous examples - client.io_service->run(); - - server_thread.join(); - - return 0; -} + if(*ifs) { + auto length = ifs->tellg(); + ifs->seekg(0, ios::beg); -void default_resource_send(const HttpServer &server, const shared_ptr &response, - const shared_ptr &ifs) { - //read and send 128 KB at a time - static vector buffer(131072); // Safe when server is running on one thread - streamsize read_length; - if((read_length=ifs->read(&buffer[0], buffer.size()).gcount())>0) { - response->write(&buffer[0], read_length); - if(read_length==static_cast(buffer.size())) { - server.send(response, [&server, response, ifs](const SimpleWeb::error_code &ec) { - if(!ec) - default_resource_send(server, response, ifs); - else - cerr << "Connection interrupted" << endl; - }); - } + header.emplace("Content-Length", to_string(length)); + response->write(header); + default_resource_send(server, response, ifs); + } + else + throw invalid_argument("could not read file"); } + catch(const exception &e) { + response->write(SimpleWeb::StatusCode::client_error_bad_request, "Could not open path " + request->path + ": " + e.what()); + } + }; + + server.on_error = [](std::shared_ptr /*request*/, const SimpleWeb::error_code & /*ec*/) { + // handle errors here + }; + + thread server_thread([&server]() { + //Start server + server.start(); + }); + + //Wait for server to start so that the client can connect + this_thread::sleep_for(chrono::seconds(1)); + + //Client examples + HttpClient client("localhost:8080"); + + // synchronous request examples + auto r1 = client.request("GET", "/match/123"); + cout << r1->content.rdbuf() << endl; // Alternatively, use the convenience function r1->content.string() + + string json_string = "{\"firstName\": \"John\",\"lastName\": \"Smith\",\"age\": 25}"; + auto r2 = client.request("POST", "/string", json_string); + cout << r2->content.rdbuf() << endl; + + // asynchronous request example + client.request("POST", "/json", json_string, [](std::shared_ptr response, const SimpleWeb::error_code &ec) { + if(!ec) + cout << response->content.rdbuf() << endl; + }); + client.io_service->reset(); // needed because the io_service has been run already in the synchronous examples + client.io_service->run(); + + server_thread.join(); +} + +void default_resource_send(const HttpServer &server, const shared_ptr &response, const shared_ptr &ifs) { + //read and send 128 KB at a time + static vector buffer(131072); // Safe when server is running on one thread + streamsize read_length; + if((read_length = ifs->read(&buffer[0], buffer.size()).gcount()) > 0) { + response->write(&buffer[0], read_length); + if(read_length == static_cast(buffer.size())) { + server.send(response, [&server, response, ifs](const SimpleWeb::error_code &ec) { + if(!ec) + default_resource_send(server, response, ifs); + else + cerr << "Connection interrupted" << endl; + }); + } + } } diff --git a/https_examples.cpp b/https_examples.cpp index 6ae6a9a..5ef2f83 100644 --- a/https_examples.cpp +++ b/https_examples.cpp @@ -1,17 +1,17 @@ -#include "server_https.hpp" #include "client_https.hpp" +#include "server_https.hpp" //Added for the json-example #define BOOST_SPIRIT_THREADSAFE -#include #include +#include //Added for the default_resource example -#include -#include -#include -#include #include "crypto.hpp" +#include +#include +#include +#include using namespace std; //Added for the json-example: @@ -21,225 +21,225 @@ typedef SimpleWeb::Server HttpsServer; typedef SimpleWeb::Client HttpsClient; //Added for the default_resource example -void default_resource_send(const HttpsServer &server, const shared_ptr &response, - const shared_ptr &ifs); +void default_resource_send(const HttpsServer &server, const shared_ptr &response, const shared_ptr &ifs); int main() { - //HTTPS-server at port 8080 using 1 thread - //Unless you do more heavy non-threaded processing in the resources, - //1 thread is usually faster than several threads - HttpsServer server("server.crt", "server.key"); - server.config.port=8080; - - //Add resources using path-regex and method-string, and an anonymous function - //POST-example for the path /string, responds the posted string - server.resource["^/string$"]["POST"]=[](shared_ptr response, shared_ptr request) { - //Retrieve string: - auto content=request->content.string(); - //request->content.string() is a convenience function for: - //stringstream ss; - //ss << request->content.rdbuf(); - //auto content=ss.str(); - - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; - - - // Alternatively, use one of the convenience functions, for instance: - // response->write(content); - }; - - //POST-example for the path /json, responds firstName+" "+lastName from the posted json - //Responds with an appropriate error message if the posted json is not valid, or if firstName or lastName is missing - //Example posted json: - //{ - // "firstName": "John", - // "lastName": "Smith", - // "age": 25 - //} - server.resource["^/json$"]["POST"]=[](shared_ptr response, shared_ptr request) { - try { - ptree pt; - read_json(request->content, pt); + //HTTPS-server at port 8080 using 1 thread + //Unless you do more heavy non-threaded processing in the resources, + //1 thread is usually faster than several threads + HttpsServer server("server.crt", "server.key"); + server.config.port = 8080; - auto name=pt.get("firstName")+" "+pt.get("lastName"); + //Add resources using path-regex and method-string, and an anonymous function + //POST-example for the path /string, responds the posted string + server.resource["^/string$"]["POST"] = [](shared_ptr response, shared_ptr request) { + //Retrieve string: + auto content = request->content.string(); + //request->content.string() is a convenience function for: + //stringstream ss; + //ss << request->content.rdbuf(); + //auto content=ss.str(); - *response << "HTTP/1.1 200 OK\r\n" - << "Content-Type: application/json\r\n" - << "Content-Length: " << name.length() << "\r\n\r\n" - << name; - } - catch(const exception &e) { - *response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" << e.what(); - } - - - // Alternatively, using a convenience function: - // try { - // ptree pt; - // read_json(request->content, pt); - - // auto name=pt.get("firstName")+" "+pt.get("lastName"); - // response->write(name, {{"Content-Type", "application/json"}}); - // } - // catch(const exception &e) { - // response->write(SimpleWeb::StatusCode::client_error_bad_request, e.what()); - // } - }; - - //GET-example for the path /info - //Responds with request-information - server.resource["^/info$"]["GET"]=[](shared_ptr response, shared_ptr request) { - stringstream stream; - stream << "

Request from " << request->remote_endpoint_address << " (" << request->remote_endpoint_port << ")

"; - stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; - for(auto &header: request->header) - stream << header.first << ": " << header.second << "
"; - - //find length of content_stream (length received using content_stream.tellp()) - stream.seekp(0, ios::end); - - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << stream.tellp() << "\r\n\r\n" << stream.rdbuf(); - - - // Alternatively, using a convenience function: - // stringstream stream; - // stream << "

Request from " << request->remote_endpoint_address << " (" << request->remote_endpoint_port << ")

"; - // stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; - // for(auto &header: request->header) - // stream << header.first << ": " << header.second << "
"; - // response->write(stream); - }; - - //GET-example for the path /match/[number], responds with the matched string in path (number) - //For instance a request GET /match/123 will receive: 123 - server.resource["^/match/([0-9]+)$"]["GET"]=[](shared_ptr response, shared_ptr request) { - string number=request->path_match[1]; - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" << number; - - - // Alternatively, using a convenience function: - // response->write(request->path_match[1]); - }; - - //Get example simulating heavy work in a separate thread - server.resource["^/work$"]["GET"]=[](shared_ptr response, shared_ptr /*request*/) { - thread work_thread([response] { - this_thread::sleep_for(chrono::seconds(5)); - response->write("Work done"); - }); - work_thread.detach(); - }; - - //Default GET-example. If no other matches, this anonymous function will be called. - //Will respond with content in the web/-directory, and its subdirectories. - //Default file: index.html - //Can for instance be used to retrieve an HTML 5 client that uses REST-resources on this server - server.default_resource["GET"]=[&server](shared_ptr response, shared_ptr request) { - try { - auto web_root_path=boost::filesystem::canonical("web"); - auto path=boost::filesystem::canonical(web_root_path/request->path); - //Check if path is within web_root_path - if(distance(web_root_path.begin(), web_root_path.end())>distance(path.begin(), path.end()) || - !equal(web_root_path.begin(), web_root_path.end(), path.begin())) - throw invalid_argument("path must be within root path"); - if(boost::filesystem::is_directory(path)) - path/="index.html"; + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" + << content; - SimpleWeb::CaseInsensitiveMultimap header; - - // Uncomment the following line to enable Cache-Control - // header.emplace("Cache-Control", "max-age=86400"); + + // Alternatively, use one of the convenience functions, for instance: + // response->write(content); + }; + + //POST-example for the path /json, responds firstName+" "+lastName from the posted json + //Responds with an appropriate error message if the posted json is not valid, or if firstName or lastName is missing + //Example posted json: + //{ + // "firstName": "John", + // "lastName": "Smith", + // "age": 25 + //} + server.resource["^/json$"]["POST"] = [](shared_ptr response, shared_ptr request) { + try { + ptree pt; + read_json(request->content, pt); + + auto name = pt.get("firstName") + " " + pt.get("lastName"); + + *response << "HTTP/1.1 200 OK\r\n" + << "Content-Type: application/json\r\n" + << "Content-Length: " << name.length() << "\r\n\r\n" + << name; + } + catch(const exception &e) { + *response << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" + << e.what(); + } + + + // Alternatively, using a convenience function: + // try { + // ptree pt; + // read_json(request->content, pt); + + // auto name=pt.get("firstName")+" "+pt.get("lastName"); + // response->write(name, {{"Content-Type", "application/json"}}); + // } + // catch(const exception &e) { + // response->write(SimpleWeb::StatusCode::client_error_bad_request, e.what()); + // } + }; + + //GET-example for the path /info + //Responds with request-information + server.resource["^/info$"]["GET"] = [](shared_ptr response, shared_ptr request) { + stringstream stream; + stream << "

Request from " << request->remote_endpoint_address << " (" << request->remote_endpoint_port << ")

"; + stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; + for(auto &header : request->header) + stream << header.first << ": " << header.second << "
"; + + //find length of content_stream (length received using content_stream.tellp()) + stream.seekp(0, ios::end); + + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << stream.tellp() << "\r\n\r\n" + << stream.rdbuf(); + + + // Alternatively, using a convenience function: + // stringstream stream; + // stream << "

Request from " << request->remote_endpoint_address << " (" << request->remote_endpoint_port << ")

"; + // stream << request->method << " " << request->path << " HTTP/" << request->http_version << "
"; + // for(auto &header: request->header) + // stream << header.first << ": " << header.second << "
"; + // response->write(stream); + }; + + //GET-example for the path /match/[number], responds with the matched string in path (number) + //For instance a request GET /match/123 will receive: 123 + server.resource["^/match/([0-9]+)$"]["GET"] = [](shared_ptr response, shared_ptr request) { + string number = request->path_match[1]; + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" + << number; + + + // Alternatively, using a convenience function: + // response->write(request->path_match[1]); + }; + + //Get example simulating heavy work in a separate thread + server.resource["^/work$"]["GET"] = [](shared_ptr response, shared_ptr /*request*/) { + thread work_thread([response] { + this_thread::sleep_for(chrono::seconds(5)); + response->write("Work done"); + }); + work_thread.detach(); + }; + + //Default GET-example. If no other matches, this anonymous function will be called. + //Will respond with content in the web/-directory, and its subdirectories. + //Default file: index.html + //Can for instance be used to retrieve an HTML 5 client that uses REST-resources on this server + server.default_resource["GET"] = [&server](shared_ptr response, shared_ptr request) { + try { + auto web_root_path = boost::filesystem::canonical("web"); + auto path = boost::filesystem::canonical(web_root_path / request->path); + //Check if path is within web_root_path + if(distance(web_root_path.begin(), web_root_path.end()) > distance(path.begin(), path.end()) || + !equal(web_root_path.begin(), web_root_path.end(), path.begin())) + throw invalid_argument("path must be within root path"); + if(boost::filesystem::is_directory(path)) + path /= "index.html"; + + SimpleWeb::CaseInsensitiveMultimap header; + +// Uncomment the following line to enable Cache-Control +// header.emplace("Cache-Control", "max-age=86400"); #ifdef HAVE_OPENSSL - // Uncomment the following lines to enable ETag - // { - // ifstream ifs(path.string(), ifstream::in | ios::binary); - // if(ifs) { - // auto hash=SimpleWeb::Crypto::to_hex_string(SimpleWeb::Crypto::md5(ifs)); - // header.emplace("ETag", "\""+hash+"\""); - // auto it=request->header.find("If-None-Match"); - // if(it!=request->header.end()) { - // if(!it->second.empty() && it->second.compare(1, hash.size(), hash)==0) { - // response->write(SimpleWeb::StatusCode::redirection_not_modified, header); - // return; - // } - // } - // } - // else - // throw invalid_argument("could not read file"); - // } +// Uncomment the following lines to enable ETag +// { +// ifstream ifs(path.string(), ifstream::in | ios::binary); +// if(ifs) { +// auto hash = SimpleWeb::Crypto::to_hex_string(SimpleWeb::Crypto::md5(ifs)); +// header.emplace("ETag", "\"" + hash + "\""); +// auto it = request->header.find("If-None-Match"); +// if(it != request->header.end()) { +// if(!it->second.empty() && it->second.compare(1, hash.size(), hash) == 0) { +// response->write(SimpleWeb::StatusCode::redirection_not_modified, header); +// return; +// } +// } +// } +// else +// throw invalid_argument("could not read file"); +// } #endif - auto ifs=make_shared(); - ifs->open(path.string(), ifstream::in | ios::binary | ios::ate); - - if(*ifs) { - auto length=ifs->tellg(); - ifs->seekg(0, ios::beg); - - header.emplace("Content-Length", to_string(length)); - response->write(header); - default_resource_send(server, response, ifs); - } - else - throw invalid_argument("could not read file"); - } - catch(const exception &e) { - response->write(SimpleWeb::StatusCode::client_error_bad_request, "Could not open path "+request->path+": "+e.what()); - } - }; - - server.on_error=[](std::shared_ptr /*request*/, const SimpleWeb::error_code &/*ec*/) { - // handle errors here - }; - - thread server_thread([&server](){ - //Start server - server.start(); - }); - - //Wait for server to start so that the client can connect - this_thread::sleep_for(chrono::seconds(1)); - - //Client examples - //Second Client() parameter set to false: no certificate verification - HttpsClient client("localhost:8080", false); - - // synchronous request examples - auto r1=client.request("GET", "/match/123"); - cout << r1->content.rdbuf() << endl; // Alternatively, use the convenience function r1->content.string() + auto ifs = make_shared(); + ifs->open(path.string(), ifstream::in | ios::binary | ios::ate); - string json_string="{\"firstName\": \"John\",\"lastName\": \"Smith\",\"age\": 25}"; - auto r2=client.request("POST", "/string", json_string); - cout << r2->content.rdbuf() << endl; - - // asynchronous request example - client.request("POST", "/json", json_string, [](std::shared_ptr response, const SimpleWeb::error_code &ec) { - if(!ec) - cout << response->content.rdbuf() << endl; - }); - client.io_service->reset(); // needed because the io_service has been run already in the synchronous examples - client.io_service->run(); - - server_thread.join(); - - return 0; -} + if(*ifs) { + auto length = ifs->tellg(); + ifs->seekg(0, ios::beg); -void default_resource_send(const HttpsServer &server, const shared_ptr &response, - const shared_ptr &ifs) { - //read and send 128 KB at a time - static vector buffer(131072); // Safe when server is running on one thread - streamsize read_length; - if((read_length=ifs->read(&buffer[0], buffer.size()).gcount())>0) { - response->write(&buffer[0], read_length); - if(read_length==static_cast(buffer.size())) { - server.send(response, [&server, response, ifs](const SimpleWeb::error_code &ec) { - if(!ec) - default_resource_send(server, response, ifs); - else - cerr << "Connection interrupted" << endl; - }); - } + header.emplace("Content-Length", to_string(length)); + response->write(header); + default_resource_send(server, response, ifs); + } + else + throw invalid_argument("could not read file"); } + catch(const exception &e) { + response->write(SimpleWeb::StatusCode::client_error_bad_request, "Could not open path " + request->path + ": " + e.what()); + } + }; + + server.on_error = [](std::shared_ptr /*request*/, const SimpleWeb::error_code & /*ec*/) { + // handle errors here + }; + + thread server_thread([&server]() { + //Start server + server.start(); + }); + + //Wait for server to start so that the client can connect + this_thread::sleep_for(chrono::seconds(1)); + + //Client examples + //Second Client() parameter set to false: no certificate verification + HttpsClient client("localhost:8080", false); + + // synchronous request examples + auto r1 = client.request("GET", "/match/123"); + cout << r1->content.rdbuf() << endl; // Alternatively, use the convenience function r1->content.string() + + string json_string = "{\"firstName\": \"John\",\"lastName\": \"Smith\",\"age\": 25}"; + auto r2 = client.request("POST", "/string", json_string); + cout << r2->content.rdbuf() << endl; + + // asynchronous request example + client.request("POST", "/json", json_string, [](std::shared_ptr response, const SimpleWeb::error_code &ec) { + if(!ec) + cout << response->content.rdbuf() << endl; + }); + client.io_service->reset(); // needed because the io_service has been run already in the synchronous examples + client.io_service->run(); + + server_thread.join(); +} + +void default_resource_send(const HttpsServer &server, const shared_ptr &response, const shared_ptr &ifs) { + //read and send 128 KB at a time + static vector buffer(131072); // Safe when server is running on one thread + streamsize read_length; + if((read_length = ifs->read(&buffer[0], buffer.size()).gcount()) > 0) { + response->write(&buffer[0], read_length); + if(read_length == static_cast(buffer.size())) { + server.send(response, [&server, response, ifs](const SimpleWeb::error_code &ec) { + if(!ec) + default_resource_send(server, response, ifs); + else + cerr << "Connection interrupted" << endl; + }); + } + } } diff --git a/server_http.hpp b/server_http.hpp index 57d72cc..74fe2f8 100644 --- a/server_http.hpp +++ b/server_http.hpp @@ -1,523 +1,526 @@ #ifndef SERVER_HTTP_HPP -#define SERVER_HTTP_HPP +#define SERVER_HTTP_HPP #include "utility.hpp" -#include -#include #include #include +#include #include +#include #ifdef USE_STANDALONE_ASIO #include namespace SimpleWeb { - using error_code = std::error_code; - using errc = std::errc; - namespace make_error_code = std; -} + using error_code = std::error_code; + using errc = std::errc; + namespace make_error_code = std; +} // namespace SimpleWeb #else #include namespace SimpleWeb { - namespace asio = boost::asio; - using error_code = boost::system::error_code; - namespace errc = boost::system::errc; - namespace make_error_code = boost::system::errc; -} + namespace asio = boost::asio; + using error_code = boost::system::error_code; + namespace errc = boost::system::errc; + namespace make_error_code = boost::system::errc; +} // namespace SimpleWeb #endif // Late 2017 TODO: remove the following checks and always use std::regex #ifdef USE_BOOST_REGEX #include namespace SimpleWeb { - namespace regex = boost; + namespace regex = boost; } #else #include namespace SimpleWeb { - namespace regex = std; + namespace regex = std; } #endif namespace SimpleWeb { - template - class Server; - - template - class ServerBase { + template + class Server; + + template + class ServerBase { + public: + virtual ~ServerBase() {} + + class Response : public std::ostream { + friend class ServerBase; + + asio::streambuf streambuf; + + std::shared_ptr socket; + + Response(const std::shared_ptr &socket) : std::ostream(&streambuf), socket(socket) {} + + template + void write_header(const CaseInsensitiveMultimap &header, size_type size) { + bool content_length_written = false; + bool chunked_transfer_encoding = false; + for(auto &field : header) { + if(!content_length_written && case_insensitive_equal(field.first, "content-length")) + content_length_written = true; + else if(!chunked_transfer_encoding && case_insensitive_equal(field.first, "transfer-encoding") && case_insensitive_equal(field.second, "chunked")) + chunked_transfer_encoding = true; + + *this << field.first << ": " << field.second << "\r\n"; + } + if(!content_length_written && !chunked_transfer_encoding && !close_connection_after_response) + *this << "Content-Length: " << size << "\r\n\r\n"; + else + *this << "\r\n"; + } + public: - virtual ~ServerBase() {} + size_t size() { + return streambuf.size(); + } - class Response : public std::ostream { - friend class ServerBase; + /// Write directly to stream buffer using std::ostream::write + void write(const char_type *ptr, std::streamsize n) { + std::ostream::write(ptr, n); + } - asio::streambuf streambuf; + /// Convenience function for writing status line, potential header fields, and empty content + void write(StatusCode status_code = StatusCode::success_ok, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; + write_header(header, 0); + } - std::shared_ptr socket; + /// Convenience function for writing status line, header fields, and content + void write(StatusCode status_code, const std::string &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; + write_header(header, content.size()); + if(!content.empty()) + *this << content; + } - Response(const std::shared_ptr &socket): std::ostream(&streambuf), socket(socket) {} + /// Convenience function for writing status line, header fields, and content + void write(StatusCode status_code, std::istream &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; + content.seekg(0, std::ios::end); + auto size = content.tellg(); + content.seekg(0, std::ios::beg); + write_header(header, size); + if(size) + *this << content.rdbuf(); + } - template - void write_header(const CaseInsensitiveMultimap &header, size_type size) { - bool content_length_written=false; - bool chunked_transfer_encoding=false; - for(auto &field: header) { - if(!content_length_written && case_insensitive_equal(field.first, "content-length")) - content_length_written=true; - else if(!chunked_transfer_encoding && case_insensitive_equal(field.first, "transfer-encoding") && case_insensitive_equal(field.second, "chunked")) - chunked_transfer_encoding=true; - - *this << field.first << ": " << field.second << "\r\n"; - } - if(!content_length_written && !chunked_transfer_encoding && !close_connection_after_response) - *this << "Content-Length: " << size << "\r\n\r\n"; - else - *this << "\r\n"; - } - public: - size_t size() { - return streambuf.size(); - } - - /// Write directly to stream buffer using std::ostream::write - void write(const char_type *ptr, std::streamsize n) { - std::ostream::write(ptr, n); - } - - /// Convenience function for writing status line, potential header fields, and empty content - void write(StatusCode status_code=StatusCode::success_ok, const CaseInsensitiveMultimap &header=CaseInsensitiveMultimap()) { - *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; - write_header(header, 0); - } - - /// Convenience function for writing status line, header fields, and content - void write(StatusCode status_code, const std::string &content, const CaseInsensitiveMultimap &header=CaseInsensitiveMultimap()) { - *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; - write_header(header, content.size()); - if(!content.empty()) - *this << content; - } - - /// Convenience function for writing status line, header fields, and content - void write(StatusCode status_code, std::istream &content, const CaseInsensitiveMultimap &header=CaseInsensitiveMultimap()) { - *this << "HTTP/1.1 " << SimpleWeb::status_code(status_code) << "\r\n"; - content.seekg(0, std::ios::end); - auto size=content.tellg(); - content.seekg(0, std::ios::beg); - write_header(header, size); - if(size) - *this << content.rdbuf(); - } - - /// Convenience function for writing success status line, header fields, and content - void write(const std::string &content, const CaseInsensitiveMultimap &header=CaseInsensitiveMultimap()) { - write(StatusCode::success_ok, content, header); - } - - /// Convenience function for writing success status line, header fields, and content - void write(std::istream &content, const CaseInsensitiveMultimap &header=CaseInsensitiveMultimap()) { - write(StatusCode::success_ok, content, header); - } - - /// Convenience function for writing success status line, and header fields - void write(const CaseInsensitiveMultimap &header) { - write(StatusCode::success_ok, std::string(), header); - } + /// Convenience function for writing success status line, header fields, and content + void write(const std::string &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + write(StatusCode::success_ok, content, header); + } - /// If true, force server to close the connection after the response have been sent. - /// - /// This is useful when implementing a HTTP/1.0-server sending content - /// without specifying the content length. - bool close_connection_after_response = false; - }; - - class Content : public std::istream { - friend class ServerBase; - public: - size_t size() { - return streambuf.size(); - } - /// Convenience function to return std::string. Note that the stream buffer is emptied when this functions is used. - std::string string() { - std::stringstream ss; - ss << rdbuf(); - return ss.str(); - } - private: - asio::streambuf &streambuf; - Content(asio::streambuf &streambuf): std::istream(&streambuf), streambuf(streambuf) {} - }; - - class Request { - friend class ServerBase; - friend class Server; - public: - std::string method, path, http_version; + /// Convenience function for writing success status line, header fields, and content + void write(std::istream &content, const CaseInsensitiveMultimap &header = CaseInsensitiveMultimap()) { + write(StatusCode::success_ok, content, header); + } - Content content; + /// Convenience function for writing success status line, and header fields + void write(const CaseInsensitiveMultimap &header) { + write(StatusCode::success_ok, std::string(), header); + } - CaseInsensitiveMultimap header; + /// If true, force server to close the connection after the response have been sent. + /// + /// This is useful when implementing a HTTP/1.0-server sending content + /// without specifying the content length. + bool close_connection_after_response = false; + }; - regex::smatch path_match; - - std::string remote_endpoint_address; - unsigned short remote_endpoint_port; - - /// Returns query keys with percent-decoded values. - CaseInsensitiveMultimap parse_query_string() { - auto pos = path.find('?'); - if (pos != std::string::npos && pos + 1 < path.size()) - return SimpleWeb::QueryString::parse(path.substr(pos + 1)); - else - return CaseInsensitiveMultimap(); - } + class Content : public std::istream { + friend class ServerBase; - private: - Request(const socket_type &socket): content(streambuf) { - try { - remote_endpoint_address=socket.lowest_layer().remote_endpoint().address().to_string(); - remote_endpoint_port=socket.lowest_layer().remote_endpoint().port(); - } - catch(...) {} - } - - asio::streambuf streambuf; - }; - - class Config { - friend class ServerBase; + public: + size_t size() { + return streambuf.size(); + } + /// Convenience function to return std::string. Note that the stream buffer is emptied when this functions is used. + std::string string() { + std::stringstream ss; + ss << rdbuf(); + return ss.str(); + } - Config(unsigned short port): port(port) {} - public: - /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS. - unsigned short port; - /// Number of threads that the server will use when start() is called. Defaults to 1 thread. - size_t thread_pool_size=1; - /// Timeout on request handling. Defaults to 5 seconds. - size_t timeout_request=5; - /// Timeout on content handling. Defaults to 300 seconds. - size_t timeout_content=300; - /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. - /// If empty, the address will be any address. - std::string address; - /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true. - bool reuse_address=true; - }; - ///Set before calling start(). - Config config; - private: - class regex_orderable : public regex::regex { - std::string str; - public: - regex_orderable(const char *regex_cstr) : regex::regex(regex_cstr), str(regex_cstr) {} - regex_orderable(const std::string ®ex_str) : regex::regex(regex_str), str(regex_str) {} - bool operator<(const regex_orderable &rhs) const { - return str; + friend class Server; + public: - /// Warning: do not add or remove resources after start() is called - std::map::Response>, std::shared_ptr::Request>)> > > resource; - - std::map::Response>, std::shared_ptr::Request>)> > default_resource; - - std::function::Request>, const error_code&)> on_error; - - std::function socket, std::shared_ptr::Request>)> on_upgrade; - - virtual void start() { - if(!io_service) - io_service=std::make_shared(); + std::string method, path, http_version; - if(io_service->stopped()) - io_service->reset(); + Content content; - asio::ip::tcp::endpoint endpoint; - if(config.address.size()>0) - endpoint=asio::ip::tcp::endpoint(asio::ip::address::from_string(config.address), config.port); - else - endpoint=asio::ip::tcp::endpoint(asio::ip::tcp::v4(), config.port); - - if(!acceptor) - acceptor=std::unique_ptr(new asio::ip::tcp::acceptor(*io_service)); - acceptor->open(endpoint.protocol()); - acceptor->set_option(asio::socket_base::reuse_address(config.reuse_address)); - acceptor->bind(endpoint); - acceptor->listen(); - - accept(); - - //If thread_pool_size>1, start m_io_service.run() in (thread_pool_size-1) threads for thread-pooling - threads.clear(); - for(size_t c=1;crun(); - }); - } + CaseInsensitiveMultimap header; - //Main thread - if(config.thread_pool_size>0) - io_service->run(); + regex::smatch path_match; - //Wait for the rest of the threads, if any, to finish as well - for(auto& t: threads) { - t.join(); - } + std::string remote_endpoint_address; + unsigned short remote_endpoint_port; + + /// Returns query keys with percent-decoded values. + CaseInsensitiveMultimap parse_query_string() { + auto pos = path.find('?'); + if(pos != std::string::npos && pos + 1 < path.size()) + return SimpleWeb::QueryString::parse(path.substr(pos + 1)); + else + return CaseInsensitiveMultimap(); + } + + private: + Request(const socket_type &socket) : content(streambuf) { + try { + remote_endpoint_address = socket.lowest_layer().remote_endpoint().address().to_string(); + remote_endpoint_port = socket.lowest_layer().remote_endpoint().port(); } - - void stop() { - acceptor->close(); - if(config.thread_pool_size>0) - io_service->stop(); + catch(...) { } - - ///Use this function if you need to recursively send parts of a longer message - void send(const std::shared_ptr &response, const std::function& callback=nullptr) const { - asio::async_write(*response->socket, response->streambuf, [this, response, callback](const error_code& ec, size_t /*bytes_transferred*/) { - if(callback) - callback(ec); - }); + } + + asio::streambuf streambuf; + }; + + class Config { + friend class ServerBase; + + Config(unsigned short port) : port(port) {} + + public: + /// Port number to use. Defaults to 80 for HTTP and 443 for HTTPS. + unsigned short port; + /// Number of threads that the server will use when start() is called. Defaults to 1 thread. + size_t thread_pool_size = 1; + /// Timeout on request handling. Defaults to 5 seconds. + size_t timeout_request = 5; + /// Timeout on content handling. Defaults to 300 seconds. + size_t timeout_content = 300; + /// IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. + /// If empty, the address will be any address. + std::string address; + /// Set to false to avoid binding the socket to an address that is already in use. Defaults to true. + bool reuse_address = true; + }; + ///Set before calling start(). + Config config; + + private: + class regex_orderable : public regex::regex { + std::string str; + + public: + regex_orderable(const char *regex_cstr) : regex::regex(regex_cstr), str(regex_cstr) {} + regex_orderable(const std::string ®ex_str) : regex::regex(regex_str), str(regex_str) {} + bool operator<(const regex_orderable &rhs) const { + return str < rhs.str; + } + }; + + public: + /// Warning: do not add or remove resources after start() is called + std::map::Response>, std::shared_ptr::Request>)>>> resource; + + std::map::Response>, std::shared_ptr::Request>)>> default_resource; + + std::function::Request>, const error_code &)> on_error; + + std::function socket, std::shared_ptr::Request>)> on_upgrade; + + virtual void start() { + if(!io_service) + io_service = std::make_shared(); + + if(io_service->stopped()) + io_service->reset(); + + asio::ip::tcp::endpoint endpoint; + if(config.address.size() > 0) + endpoint = asio::ip::tcp::endpoint(asio::ip::address::from_string(config.address), config.port); + else + endpoint = asio::ip::tcp::endpoint(asio::ip::tcp::v4(), config.port); + + if(!acceptor) + acceptor = std::unique_ptr(new asio::ip::tcp::acceptor(*io_service)); + acceptor->open(endpoint.protocol()); + acceptor->set_option(asio::socket_base::reuse_address(config.reuse_address)); + acceptor->bind(endpoint); + acceptor->listen(); + + accept(); + + //If thread_pool_size>1, start m_io_service.run() in (thread_pool_size-1) threads for thread-pooling + threads.clear(); + for(size_t c = 1; c < config.thread_pool_size; c++) { + threads.emplace_back([this]() { + io_service->run(); + }); + } + + //Main thread + if(config.thread_pool_size > 0) + io_service->run(); + + //Wait for the rest of the threads, if any, to finish as well + for(auto &t : threads) { + t.join(); + } + } + + void stop() { + acceptor->close(); + if(config.thread_pool_size > 0) + io_service->stop(); + } + + ///Use this function if you need to recursively send parts of a longer message + void send(const std::shared_ptr &response, const std::function &callback = nullptr) const { + asio::async_write(*response->socket, response->streambuf, [this, response, callback](const error_code &ec, size_t /*bytes_transferred*/) { + if(callback) + callback(ec); + }); + } + + /// If you have your own asio::io_service, store its pointer here before running start(). + /// You might also want to set config.thread_pool_size to 0. + std::shared_ptr io_service; + + protected: + std::unique_ptr acceptor; + std::vector threads; + + ServerBase(unsigned short port) : config(port) {} + + virtual void accept() = 0; + + std::shared_ptr get_timeout_timer(const std::shared_ptr &socket, long seconds) { + if(seconds == 0) + return nullptr; + + auto timer = std::make_shared(*io_service); + timer->expires_from_now(boost::posix_time::seconds(seconds)); + timer->async_wait([socket](const error_code &ec) { + if(!ec) { + error_code ec; + socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec); + socket->lowest_layer().close(); } + }); + return timer; + } - /// If you have your own asio::io_service, store its pointer here before running start(). - /// You might also want to set config.thread_pool_size to 0. - std::shared_ptr io_service; - protected: - std::unique_ptr acceptor; - std::vector threads; - - ServerBase(unsigned short port) : config(port) {} - - virtual void accept()=0; - - std::shared_ptr get_timeout_timer(const std::shared_ptr &socket, long seconds) { - if(seconds==0) - return nullptr; - - auto timer=std::make_shared(*io_service); - timer->expires_from_now(boost::posix_time::seconds(seconds)); - timer->async_wait([socket](const error_code& ec){ - if(!ec) { - error_code ec; - socket->lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ec); - socket->lowest_layer().close(); - } - }); - return timer; - } - - void read_request_and_content(const std::shared_ptr &socket) { - //Create new streambuf (Request::streambuf) for async_read_until() - //shared_ptr is used to pass temporary objects to the asynchronous functions - std::shared_ptr request(new Request(*socket)); + void read_request_and_content(const std::shared_ptr &socket) { + //Create new streambuf (Request::streambuf) for async_read_until() + //shared_ptr is used to pass temporary objects to the asynchronous functions + std::shared_ptr request(new Request(*socket)); - //Set timeout on the following asio::async-read or write function - auto timer=this->get_timeout_timer(socket, config.timeout_request); - - asio::async_read_until(*socket, request->streambuf, "\r\n\r\n", - [this, socket, request, timer](const error_code& ec, size_t bytes_transferred) { - if(timer) - timer->cancel(); - if(!ec) { - //request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs: - //"After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter" - //The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the - //streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). - size_t num_additional_bytes=request->streambuf.size()-bytes_transferred; - - if(!this->parse_request(request)) - return; - - //If content, read that as well - auto it=request->header.find("Content-Length"); - if(it!=request->header.end()) { - unsigned long long content_length; - try { - content_length=stoull(it->second); - } - catch(const std::exception &e) { - if(on_error) - on_error(request, make_error_code::make_error_code(errc::protocol_error)); - return; - } - if(content_length>num_additional_bytes) { - //Set timeout on the following asio::async-read or write function - auto timer=this->get_timeout_timer(socket, config.timeout_content); - asio::async_read(*socket, request->streambuf, - asio::transfer_exactly(content_length-num_additional_bytes), - [this, socket, request, timer] - (const error_code& ec, size_t /*bytes_transferred*/) { - if(timer) - timer->cancel(); - if(!ec) - this->find_resource(socket, request); - else if(on_error) - on_error(request, ec); - }); - } - else - this->find_resource(socket, request); - } - else - this->find_resource(socket, request); - } - else if(on_error) - on_error(request, ec); - }); - } + //Set timeout on the following asio::async-read or write function + auto timer = this->get_timeout_timer(socket, config.timeout_request); - bool parse_request(const std::shared_ptr &request) const { - std::string line; - getline(request->content, line); - size_t method_end; - if((method_end=line.find(' '))!=std::string::npos) { - size_t path_end; - if((path_end=line.find(' ', method_end+1))!=std::string::npos) { - request->method=line.substr(0, method_end); - request->path=line.substr(method_end+1, path_end-method_end-1); + asio::async_read_until(*socket, request->streambuf, "\r\n\r\n", [this, socket, request, timer](const error_code &ec, size_t bytes_transferred) { + if(timer) + timer->cancel(); + if(!ec) { + //request->streambuf.size() is not necessarily the same as bytes_transferred, from Boost-docs: + //"After a successful async_read_until operation, the streambuf may contain additional data beyond the delimiter" + //The chosen solution is to extract lines from the stream directly when parsing the header. What is left of the + //streambuf (maybe some bytes of the content) is appended to in the async_read-function below (for retrieving content). + size_t num_additional_bytes = request->streambuf.size() - bytes_transferred; - size_t protocol_end; - if((protocol_end=line.find('/', path_end+1))!=std::string::npos) { - if(line.compare(path_end+1, protocol_end-path_end-1, "HTTP")!=0) - return false; - request->http_version=line.substr(protocol_end+1, line.size()-protocol_end-2); - } - else - return false; - - getline(request->content, line); - size_t param_end; - while((param_end=line.find(':'))!=std::string::npos) { - size_t value_start=param_end+1; - if((value_start)header.emplace(line.substr(0, param_end), line.substr(value_start, line.size()-value_start-1)); - } - - getline(request->content, line); - } - } - else - return false; - } - else - return false; - return true; - } - - void find_resource(const std::shared_ptr &socket, const std::shared_ptr &request) { - //Upgrade connection - if(on_upgrade) { - auto it=request->header.find("Upgrade"); - if(it!=request->header.end()) { - on_upgrade(socket, request); - return; - } - } - //Find path- and method-match, and call write_response - for(auto ®ex_method: resource) { - auto it=regex_method.second.find(request->method); - if(it!=regex_method.second.end()) { - regex::smatch sm_res; - if(regex::regex_match(request->path, sm_res, regex_method.first)) { - request->path_match=std::move(sm_res); - write_response(socket, request, it->second); - return; - } - } - } - auto it=default_resource.find(request->method); - if(it!=default_resource.end()) { - write_response(socket, request, it->second); - } - } - - void write_response(const std::shared_ptr &socket, const std::shared_ptr &request, - std::function::Response>, - std::shared_ptr::Request>)>& resource_function) { - //Set timeout on the following asio::async-read or write function - auto timer=this->get_timeout_timer(socket, config.timeout_content); - - auto response=std::shared_ptr(new Response(socket), [this, request, timer](Response *response_ptr) { - auto response=std::shared_ptr(response_ptr); - this->send(response, [this, response, request, timer](const error_code& ec) { - if(timer) - timer->cancel(); - if(!ec) { - if (response->close_connection_after_response) - return; - - auto range=request->header.equal_range("Connection"); - for(auto it=range.first;it!=range.second;it++) { - if(case_insensitive_equal(it->second, "close")) { - return; - } else if (case_insensitive_equal(it->second, "keep-alive")) { - this->read_request_and_content(response->socket); - return; - } - } - if(request->http_version >= "1.1") - this->read_request_and_content(response->socket); - } - else if(on_error) - on_error(request, ec); - }); - }); + if(!this->parse_request(request)) + return; + //If content, read that as well + auto it = request->header.find("Content-Length"); + if(it != request->header.end()) { + unsigned long long content_length; try { - resource_function(response, request); + content_length = stoull(it->second); } catch(const std::exception &e) { - if(on_error) - on_error(request, make_error_code::make_error_code(errc::operation_canceled)); - return; + if(on_error) + on_error(request, make_error_code::make_error_code(errc::protocol_error)); + return; } - } - }; - - template - class Server : public ServerBase {}; - - typedef asio::ip::tcp::socket HTTP; - - template<> - class Server : public ServerBase { - public: - DEPRECATED Server(unsigned short port, size_t thread_pool_size=1, long timeout_request=5, long timeout_content=300) : - Server() { - config.port=port; - config.thread_pool_size=thread_pool_size; - config.timeout_request=timeout_request; - config.timeout_content=timeout_content; - } - - Server() : ServerBase::ServerBase(80) {} - - protected: - void accept() { - //Create new socket for this connection - //Shared_ptr is used to pass temporary objects to the asynchronous functions - auto socket=std::make_shared(*io_service); - - acceptor->async_accept(*socket, [this, socket](const error_code& ec){ - //Immediately start accepting a new connection (if io_service hasn't been stopped) - if (ec != asio::error::operation_aborted) - accept(); - - if(!ec) { - asio::ip::tcp::no_delay option(true); - socket->set_option(option); - - this->read_request_and_content(socket); - } + if(content_length > num_additional_bytes) { + //Set timeout on the following asio::async-read or write function + auto timer = this->get_timeout_timer(socket, config.timeout_content); + asio::async_read(*socket, request->streambuf, asio::transfer_exactly(content_length - num_additional_bytes), [this, socket, request, timer](const error_code &ec, size_t /*bytes_transferred*/) { + if(timer) + timer->cancel(); + if(!ec) + this->find_resource(socket, request); else if(on_error) - on_error(std::shared_ptr(new Request(*socket)), ec); - }); + on_error(request, ec); + }); + } + else + this->find_resource(socket, request); + } + else + this->find_resource(socket, request); } - }; -} -#endif /* SERVER_HTTP_HPP */ + else if(on_error) + on_error(request, ec); + }); + } + + bool parse_request(const std::shared_ptr &request) const { + std::string line; + getline(request->content, line); + size_t method_end; + if((method_end = line.find(' ')) != std::string::npos) { + size_t path_end; + if((path_end = line.find(' ', method_end + 1)) != std::string::npos) { + request->method = line.substr(0, method_end); + request->path = line.substr(method_end + 1, path_end - method_end - 1); + + size_t protocol_end; + if((protocol_end = line.find('/', path_end + 1)) != std::string::npos) { + if(line.compare(path_end + 1, protocol_end - path_end - 1, "HTTP") != 0) + return false; + request->http_version = line.substr(protocol_end + 1, line.size() - protocol_end - 2); + } + else + return false; + + getline(request->content, line); + size_t param_end; + while((param_end = line.find(':')) != std::string::npos) { + size_t value_start = param_end + 1; + if((value_start) < line.size()) { + if(line[value_start] == ' ') + value_start++; + if(value_start < line.size()) + request->header.emplace(line.substr(0, param_end), line.substr(value_start, line.size() - value_start - 1)); + } + + getline(request->content, line); + } + } + else + return false; + } + else + return false; + return true; + } + + void find_resource(const std::shared_ptr &socket, const std::shared_ptr &request) { + //Upgrade connection + if(on_upgrade) { + auto it = request->header.find("Upgrade"); + if(it != request->header.end()) { + on_upgrade(socket, request); + return; + } + } + //Find path- and method-match, and call write_response + for(auto ®ex_method : resource) { + auto it = regex_method.second.find(request->method); + if(it != regex_method.second.end()) { + regex::smatch sm_res; + if(regex::regex_match(request->path, sm_res, regex_method.first)) { + request->path_match = std::move(sm_res); + write_response(socket, request, it->second); + return; + } + } + } + auto it = default_resource.find(request->method); + if(it != default_resource.end()) { + write_response(socket, request, it->second); + } + } + + void write_response(const std::shared_ptr &socket, const std::shared_ptr &request, + std::function::Response>, std::shared_ptr::Request>)> &resource_function) { + //Set timeout on the following asio::async-read or write function + auto timer = this->get_timeout_timer(socket, config.timeout_content); + + auto response = std::shared_ptr(new Response(socket), [this, request, timer](Response *response_ptr) { + auto response = std::shared_ptr(response_ptr); + this->send(response, [this, response, request, timer](const error_code &ec) { + if(timer) + timer->cancel(); + if(!ec) { + if(response->close_connection_after_response) + return; + + auto range = request->header.equal_range("Connection"); + for(auto it = range.first; it != range.second; it++) { + if(case_insensitive_equal(it->second, "close")) { + return; + } + else if(case_insensitive_equal(it->second, "keep-alive")) { + this->read_request_and_content(response->socket); + return; + } + } + if(request->http_version >= "1.1") + this->read_request_and_content(response->socket); + } + else if(on_error) + on_error(request, ec); + }); + }); + + try { + resource_function(response, request); + } + catch(const std::exception &e) { + if(on_error) + on_error(request, make_error_code::make_error_code(errc::operation_canceled)); + return; + } + } + }; + + template + class Server : public ServerBase {}; + + typedef asio::ip::tcp::socket HTTP; + + template <> + class Server : public ServerBase { + public: + DEPRECATED Server(unsigned short port, size_t thread_pool_size = 1, long timeout_request = 5, long timeout_content = 300) : Server() { + config.port = port; + config.thread_pool_size = thread_pool_size; + config.timeout_request = timeout_request; + config.timeout_content = timeout_content; + } + + Server() : ServerBase::ServerBase(80) {} + + protected: + void accept() { + //Create new socket for this connection + //Shared_ptr is used to pass temporary objects to the asynchronous functions + auto socket = std::make_shared(*io_service); + + acceptor->async_accept(*socket, [this, socket](const error_code &ec) { + //Immediately start accepting a new connection (if io_service hasn't been stopped) + if(ec != asio::error::operation_aborted) + accept(); + + if(!ec) { + asio::ip::tcp::no_delay option(true); + socket->set_option(option); + + this->read_request_and_content(socket); + } + else if(on_error) + on_error(std::shared_ptr(new Request(*socket)), ec); + }); + } + }; +} // namespace SimpleWeb + +#endif /* SERVER_HTTP_HPP */ diff --git a/server_https.hpp b/server_https.hpp index 28ac039..f9f1abb 100644 --- a/server_https.hpp +++ b/server_https.hpp @@ -1,5 +1,5 @@ #ifndef SERVER_HTTPS_HPP -#define SERVER_HTTPS_HPP +#define SERVER_HTTPS_HPP #include "server_http.hpp" @@ -9,88 +9,84 @@ #include #endif -#include #include +#include namespace SimpleWeb { - typedef asio::ssl::stream HTTPS; - - template<> - class Server : public ServerBase { - std::string session_id_context; - bool set_session_id_context=false; - public: - DEPRECATED Server(unsigned short port, size_t thread_pool_size, const std::string& cert_file, const std::string& private_key_file, - long timeout_request=5, long timeout_content=300, - const std::string& verify_file=std::string()) : - Server(cert_file, private_key_file, verify_file) { - config.port=port; - config.thread_pool_size=thread_pool_size; - config.timeout_request=timeout_request; - config.timeout_content=timeout_content; - } - - Server(const std::string& cert_file, const std::string& private_key_file, const std::string& verify_file=std::string()): - ServerBase::ServerBase(443), context(asio::ssl::context::tlsv12) { - context.use_certificate_chain_file(cert_file); - context.use_private_key_file(private_key_file, asio::ssl::context::pem); - - if(verify_file.size()>0) { - context.load_verify_file(verify_file); - context.set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert | - asio::ssl::verify_client_once); - set_session_id_context=true; - } - } - - void start() { - if(set_session_id_context) { - // Creating session_id_context from address:port but reversed due to small SSL_MAX_SSL_SESSION_ID_LENGTH - session_id_context=std::to_string(config.port)+':'; - session_id_context.append(config.address.rbegin(), config.address.rend()); - SSL_CTX_set_session_id_context(context.native_handle(), reinterpret_cast(session_id_context.data()), - std::min(session_id_context.size(), SSL_MAX_SSL_SESSION_ID_LENGTH)); - } - ServerBase::start(); + typedef asio::ssl::stream HTTPS; + + template <> + class Server : public ServerBase { + std::string session_id_context; + bool set_session_id_context = false; + + public: + DEPRECATED Server(unsigned short port, size_t thread_pool_size, const std::string &cert_file, const std::string &private_key_file, + long timeout_request = 5, long timeout_content = 300, const std::string &verify_file = std::string()) + : Server(cert_file, private_key_file, verify_file) { + config.port = port; + config.thread_pool_size = thread_pool_size; + config.timeout_request = timeout_request; + config.timeout_content = timeout_content; + } + + Server(const std::string &cert_file, const std::string &private_key_file, const std::string &verify_file = std::string()) + : ServerBase::ServerBase(443), context(asio::ssl::context::tlsv12) { + context.use_certificate_chain_file(cert_file); + context.use_private_key_file(private_key_file, asio::ssl::context::pem); + + if(verify_file.size() > 0) { + context.load_verify_file(verify_file); + context.set_verify_mode(asio::ssl::verify_peer | asio::ssl::verify_fail_if_no_peer_cert | asio::ssl::verify_client_once); + set_session_id_context = true; + } + } + + void start() { + if(set_session_id_context) { + // Creating session_id_context from address:port but reversed due to small SSL_MAX_SSL_SESSION_ID_LENGTH + session_id_context = std::to_string(config.port) + ':'; + session_id_context.append(config.address.rbegin(), config.address.rend()); + SSL_CTX_set_session_id_context(context.native_handle(), reinterpret_cast(session_id_context.data()), + std::min(session_id_context.size(), SSL_MAX_SSL_SESSION_ID_LENGTH)); + } + ServerBase::start(); + } + + protected: + asio::ssl::context context; + + void accept() { + //Create new socket for this connection + //Shared_ptr is used to pass temporary objects to the asynchronous functions + auto socket = std::make_shared(*io_service, context); + + acceptor->async_accept((*socket).lowest_layer(), [this, socket](const error_code &ec) { + //Immediately start accepting a new connection (if io_service hasn't been stopped) + if(ec != asio::error::operation_aborted) + accept(); + + + if(!ec) { + asio::ip::tcp::no_delay option(true); + socket->lowest_layer().set_option(option); + + //Set timeout on the following asio::ssl::stream::async_handshake + auto timer = get_timeout_timer(socket, config.timeout_request); + socket->async_handshake(asio::ssl::stream_base::server, [this, socket, timer](const error_code &ec) { + if(timer) + timer->cancel(); + if(!ec) + read_request_and_content(socket); + else if(on_error) + on_error(std::shared_ptr(new Request(*socket)), ec); + }); } + else if(on_error) + on_error(std::shared_ptr(new Request(*socket)), ec); + }); + } + }; +} // namespace SimpleWeb - protected: - asio::ssl::context context; - - void accept() { - //Create new socket for this connection - //Shared_ptr is used to pass temporary objects to the asynchronous functions - auto socket=std::make_shared(*io_service, context); - - acceptor->async_accept((*socket).lowest_layer(), [this, socket](const error_code& ec) { - //Immediately start accepting a new connection (if io_service hasn't been stopped) - if (ec != asio::error::operation_aborted) - accept(); - - - if(!ec) { - asio::ip::tcp::no_delay option(true); - socket->lowest_layer().set_option(option); - - //Set timeout on the following asio::ssl::stream::async_handshake - auto timer=get_timeout_timer(socket, config.timeout_request); - socket->async_handshake(asio::ssl::stream_base::server, [this, socket, timer] - (const error_code& ec) { - if(timer) - timer->cancel(); - if(!ec) - read_request_and_content(socket); - else if(on_error) - on_error(std::shared_ptr(new Request(*socket)), ec); - }); - } - else if(on_error) - on_error(std::shared_ptr(new Request(*socket)), ec); - }); - } - }; -} - - -#endif /* SERVER_HTTPS_HPP */ - +#endif /* SERVER_HTTPS_HPP */ diff --git a/status_code.hpp b/status_code.hpp index 0936c7d..3c3f833 100644 --- a/status_code.hpp +++ b/status_code.hpp @@ -5,153 +5,153 @@ #include namespace SimpleWeb { -enum class StatusCode { - unknown = 0, - information_continue = 100, - information_switching_protocols, - information_processing, - success_ok = 200, - success_created, - success_accepted, - success_non_authoritative_information, - success_no_content, - success_reset_content, - success_partial_content, - success_multi_status, - success_already_reported, - success_im_used = 226, - redirection_multiple_choices = 300, - redirection_moved_permanently, - redirection_found, - redirection_see_other, - redirection_not_modified, - redirection_use_proxy, - redirection_switch_proxy, - redirection_temporary_redirect, - redirection_permanent_redirect, - client_error_bad_request = 400, - client_error_unauthorized, - client_error_payment_required, - client_error_forbidden, - client_error_not_found, - client_error_method_not_allowed, - client_error_not_acceptable, - client_error_proxy_authentication_required, - client_error_request_timeout, - client_error_conflict, - client_error_gone, - client_error_length_required, - client_error_precondition_failed, - client_error_payload_too_large, - client_error_uri_too_long, - client_error_unsupported_media_type, - client_error_range_not_satisfiable, - client_error_expectation_failed, - client_error_im_a_teapot, - client_error_misdirection_required = 421, - client_error_unprocessable_entity, - client_error_locked, - client_error_failed_dependency, - client_error_upgrade_required = 426, - client_error_precondition_required = 428, - client_error_too_many_requests, - client_error_request_header_fields_too_large = 431, - client_error_unavailable_for_legal_reasons = 451, - server_error_internal_server_error = 500, - server_error_not_implemented, - server_error_bad_gateway, - server_error_service_unavailable, - server_error_gateway_timeout, - server_error_http_version_not_supported, - server_error_variant_also_negotiates, - server_error_insufficient_storage, - server_error_loop_detected, - server_error_not_extended = 510, - server_error_network_authentication_required -}; + enum class StatusCode { + unknown = 0, + information_continue = 100, + information_switching_protocols, + information_processing, + success_ok = 200, + success_created, + success_accepted, + success_non_authoritative_information, + success_no_content, + success_reset_content, + success_partial_content, + success_multi_status, + success_already_reported, + success_im_used = 226, + redirection_multiple_choices = 300, + redirection_moved_permanently, + redirection_found, + redirection_see_other, + redirection_not_modified, + redirection_use_proxy, + redirection_switch_proxy, + redirection_temporary_redirect, + redirection_permanent_redirect, + client_error_bad_request = 400, + client_error_unauthorized, + client_error_payment_required, + client_error_forbidden, + client_error_not_found, + client_error_method_not_allowed, + client_error_not_acceptable, + client_error_proxy_authentication_required, + client_error_request_timeout, + client_error_conflict, + client_error_gone, + client_error_length_required, + client_error_precondition_failed, + client_error_payload_too_large, + client_error_uri_too_long, + client_error_unsupported_media_type, + client_error_range_not_satisfiable, + client_error_expectation_failed, + client_error_im_a_teapot, + client_error_misdirection_required = 421, + client_error_unprocessable_entity, + client_error_locked, + client_error_failed_dependency, + client_error_upgrade_required = 426, + client_error_precondition_required = 428, + client_error_too_many_requests, + client_error_request_header_fields_too_large = 431, + client_error_unavailable_for_legal_reasons = 451, + server_error_internal_server_error = 500, + server_error_not_implemented, + server_error_bad_gateway, + server_error_service_unavailable, + server_error_gateway_timeout, + server_error_http_version_not_supported, + server_error_variant_also_negotiates, + server_error_insufficient_storage, + server_error_loop_detected, + server_error_not_extended = 510, + server_error_network_authentication_required + }; -inline const static std::vector> &status_codes() { - static std::vector> status_codes = { - {StatusCode::unknown, ""}, - {StatusCode::information_continue, "100 Continue"}, - {StatusCode::information_switching_protocols, "101 Switching Protocols"}, - {StatusCode::information_processing, "102 Processing"}, - {StatusCode::success_ok, "200 OK"}, - {StatusCode::success_created, "201 Created"}, - {StatusCode::success_accepted, "202 Accepted"}, - {StatusCode::success_non_authoritative_information, "203 Non-Authoritative Information"}, - {StatusCode::success_no_content, "204 No Content"}, - {StatusCode::success_reset_content, "205 Reset Content"}, - {StatusCode::success_partial_content, "206 Partial Content"}, - {StatusCode::success_multi_status, "207 Multi-Status"}, - {StatusCode::success_already_reported, "208 Already Reported"}, - {StatusCode::success_im_used, "226 IM Used"}, - {StatusCode::redirection_multiple_choices, "300 Multiple Choices"}, - {StatusCode::redirection_moved_permanently, "301 Moved Permanently"}, - {StatusCode::redirection_found, "302 Found"}, - {StatusCode::redirection_see_other, "303 See Other"}, - {StatusCode::redirection_not_modified, "304 Not Modified"}, - {StatusCode::redirection_use_proxy, "305 Use Proxy"}, - {StatusCode::redirection_switch_proxy, "306 Switch Proxy"}, - {StatusCode::redirection_temporary_redirect, "307 Temporary Redirect"}, - {StatusCode::redirection_permanent_redirect, "308 Permanent Redirect"}, - {StatusCode::client_error_bad_request, "400 Bad Request"}, - {StatusCode::client_error_unauthorized, "401 Unauthorized"}, - {StatusCode::client_error_payment_required, "402 Payment Required"}, - {StatusCode::client_error_forbidden, "403 Forbidden"}, - {StatusCode::client_error_not_found, "404 Not Found"}, - {StatusCode::client_error_method_not_allowed, "405 Method Not Allowed"}, - {StatusCode::client_error_not_acceptable, "406 Not Acceptable"}, - {StatusCode::client_error_proxy_authentication_required, "407 Proxy Authentication Required"}, - {StatusCode::client_error_request_timeout, "408 Request Timeout"}, - {StatusCode::client_error_conflict, "409 Conflict"}, - {StatusCode::client_error_gone, "410 Gone"}, - {StatusCode::client_error_length_required, "411 Length Required"}, - {StatusCode::client_error_precondition_failed, "412 Precondition Failed"}, - {StatusCode::client_error_payload_too_large, "413 Payload Too Large"}, - {StatusCode::client_error_uri_too_long, "414 URI Too Long"}, - {StatusCode::client_error_unsupported_media_type, "415 Unsupported Media Type"}, - {StatusCode::client_error_range_not_satisfiable, "416 Range Not Satisfiable"}, - {StatusCode::client_error_expectation_failed, "417 Expectation Failed"}, - {StatusCode::client_error_im_a_teapot, "418 I'm a teapot"}, - {StatusCode::client_error_misdirection_required, "421 Misdirected Request"}, - {StatusCode::client_error_unprocessable_entity, "422 Unprocessable Entity"}, - {StatusCode::client_error_locked, "423 Locked"}, - {StatusCode::client_error_failed_dependency, "424 Failed Dependency"}, - {StatusCode::client_error_upgrade_required, "426 Upgrade Required"}, - {StatusCode::client_error_precondition_required, "428 Precondition Required"}, - {StatusCode::client_error_too_many_requests, "429 Too Many Requests"}, - {StatusCode::client_error_request_header_fields_too_large, "431 Request Header Fields Too Large"}, - {StatusCode::client_error_unavailable_for_legal_reasons, "451 Unavailable For Legal Reasons"}, - {StatusCode::server_error_internal_server_error, "500 Internal Server Error"}, - {StatusCode::server_error_not_implemented, "501 Not Implemented"}, - {StatusCode::server_error_bad_gateway, "502 Bad Gateway"}, - {StatusCode::server_error_service_unavailable, "503 Service Unavailable"}, - {StatusCode::server_error_gateway_timeout, "504 Gateway Timeout"}, - {StatusCode::server_error_http_version_not_supported, "505 HTTP Version Not Supported"}, - {StatusCode::server_error_variant_also_negotiates, "506 Variant Also Negotiates"}, - {StatusCode::server_error_insufficient_storage, "507 Insufficient Storage"}, - {StatusCode::server_error_loop_detected, "508 Loop Detected"}, - {StatusCode::server_error_not_extended, "510 Not Extended"}, - {StatusCode::server_error_network_authentication_required, "511 Network Authentication Required"}}; - return status_codes; -} - -inline StatusCode status_code(const std::string &status_code_str) { - for(auto &status_code : status_codes()) { - if(status_code.second == status_code_str) - return status_code.first; + inline const static std::vector> &status_codes() { + static std::vector> status_codes = { + {StatusCode::unknown, ""}, + {StatusCode::information_continue, "100 Continue"}, + {StatusCode::information_switching_protocols, "101 Switching Protocols"}, + {StatusCode::information_processing, "102 Processing"}, + {StatusCode::success_ok, "200 OK"}, + {StatusCode::success_created, "201 Created"}, + {StatusCode::success_accepted, "202 Accepted"}, + {StatusCode::success_non_authoritative_information, "203 Non-Authoritative Information"}, + {StatusCode::success_no_content, "204 No Content"}, + {StatusCode::success_reset_content, "205 Reset Content"}, + {StatusCode::success_partial_content, "206 Partial Content"}, + {StatusCode::success_multi_status, "207 Multi-Status"}, + {StatusCode::success_already_reported, "208 Already Reported"}, + {StatusCode::success_im_used, "226 IM Used"}, + {StatusCode::redirection_multiple_choices, "300 Multiple Choices"}, + {StatusCode::redirection_moved_permanently, "301 Moved Permanently"}, + {StatusCode::redirection_found, "302 Found"}, + {StatusCode::redirection_see_other, "303 See Other"}, + {StatusCode::redirection_not_modified, "304 Not Modified"}, + {StatusCode::redirection_use_proxy, "305 Use Proxy"}, + {StatusCode::redirection_switch_proxy, "306 Switch Proxy"}, + {StatusCode::redirection_temporary_redirect, "307 Temporary Redirect"}, + {StatusCode::redirection_permanent_redirect, "308 Permanent Redirect"}, + {StatusCode::client_error_bad_request, "400 Bad Request"}, + {StatusCode::client_error_unauthorized, "401 Unauthorized"}, + {StatusCode::client_error_payment_required, "402 Payment Required"}, + {StatusCode::client_error_forbidden, "403 Forbidden"}, + {StatusCode::client_error_not_found, "404 Not Found"}, + {StatusCode::client_error_method_not_allowed, "405 Method Not Allowed"}, + {StatusCode::client_error_not_acceptable, "406 Not Acceptable"}, + {StatusCode::client_error_proxy_authentication_required, "407 Proxy Authentication Required"}, + {StatusCode::client_error_request_timeout, "408 Request Timeout"}, + {StatusCode::client_error_conflict, "409 Conflict"}, + {StatusCode::client_error_gone, "410 Gone"}, + {StatusCode::client_error_length_required, "411 Length Required"}, + {StatusCode::client_error_precondition_failed, "412 Precondition Failed"}, + {StatusCode::client_error_payload_too_large, "413 Payload Too Large"}, + {StatusCode::client_error_uri_too_long, "414 URI Too Long"}, + {StatusCode::client_error_unsupported_media_type, "415 Unsupported Media Type"}, + {StatusCode::client_error_range_not_satisfiable, "416 Range Not Satisfiable"}, + {StatusCode::client_error_expectation_failed, "417 Expectation Failed"}, + {StatusCode::client_error_im_a_teapot, "418 I'm a teapot"}, + {StatusCode::client_error_misdirection_required, "421 Misdirected Request"}, + {StatusCode::client_error_unprocessable_entity, "422 Unprocessable Entity"}, + {StatusCode::client_error_locked, "423 Locked"}, + {StatusCode::client_error_failed_dependency, "424 Failed Dependency"}, + {StatusCode::client_error_upgrade_required, "426 Upgrade Required"}, + {StatusCode::client_error_precondition_required, "428 Precondition Required"}, + {StatusCode::client_error_too_many_requests, "429 Too Many Requests"}, + {StatusCode::client_error_request_header_fields_too_large, "431 Request Header Fields Too Large"}, + {StatusCode::client_error_unavailable_for_legal_reasons, "451 Unavailable For Legal Reasons"}, + {StatusCode::server_error_internal_server_error, "500 Internal Server Error"}, + {StatusCode::server_error_not_implemented, "501 Not Implemented"}, + {StatusCode::server_error_bad_gateway, "502 Bad Gateway"}, + {StatusCode::server_error_service_unavailable, "503 Service Unavailable"}, + {StatusCode::server_error_gateway_timeout, "504 Gateway Timeout"}, + {StatusCode::server_error_http_version_not_supported, "505 HTTP Version Not Supported"}, + {StatusCode::server_error_variant_also_negotiates, "506 Variant Also Negotiates"}, + {StatusCode::server_error_insufficient_storage, "507 Insufficient Storage"}, + {StatusCode::server_error_loop_detected, "508 Loop Detected"}, + {StatusCode::server_error_not_extended, "510 Not Extended"}, + {StatusCode::server_error_network_authentication_required, "511 Network Authentication Required"}}; + return status_codes; } - return StatusCode::unknown; -} -inline const std::string &status_code(StatusCode status_code_enum) { - for(auto &status_code : status_codes()) { - if(status_code.first == status_code_enum) - return status_code.second; + inline StatusCode status_code(const std::string &status_code_str) { + for(auto &status_code : status_codes()) { + if(status_code.second == status_code_str) + return status_code.first; + } + return StatusCode::unknown; + } + + inline const std::string &status_code(StatusCode status_code_enum) { + for(auto &status_code : status_codes()) { + if(status_code.first == status_code_enum) + return status_code.second; + } + return status_codes()[0].second; } - return status_codes()[0].second; -} } // namespace SimpleWeb #endif // SIMPLE_WEB_SERVER_STATUS_CODE_HPP diff --git a/tests/crypto_test.cpp b/tests/crypto_test.cpp index 26b8ce5..412c023 100644 --- a/tests/crypto_test.cpp +++ b/tests/crypto_test.cpp @@ -1,77 +1,72 @@ -#include #include +#include #include "crypto.hpp" using namespace std; using namespace SimpleWeb; -const vector > base64_string_tests = { +const vector> base64_string_tests = { {"", ""}, - {"f" , "Zg=="}, + {"f", "Zg=="}, {"fo", "Zm8="}, {"foo", "Zm9v"}, {"foob", "Zm9vYg=="}, {"fooba", "Zm9vYmE="}, - {"foobar", "Zm9vYmFy"} -}; + {"foobar", "Zm9vYmFy"}}; -const vector > md5_string_tests = { +const vector> md5_string_tests = { {"", "d41d8cd98f00b204e9800998ecf8427e"}, - {"The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6"} -}; + {"The quick brown fox jumps over the lazy dog", "9e107d9d372bb6826bd81d3542a419d6"}}; -const vector > sha1_string_tests = { +const vector> sha1_string_tests = { {"", "da39a3ee5e6b4b0d3255bfef95601890afd80709"}, - {"The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"} -}; + {"The quick brown fox jumps over the lazy dog", "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"}}; -const vector > sha256_string_tests = { +const vector> sha256_string_tests = { {"", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, - {"The quick brown fox jumps over the lazy dog", "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"} -}; + {"The quick brown fox jumps over the lazy dog", "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"}}; -const vector > sha512_string_tests = { +const vector> sha512_string_tests = { {"", "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"}, - {"The quick brown fox jumps over the lazy dog", "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6"} -}; + {"The quick brown fox jumps over the lazy dog", "07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6"}}; int main() { - for(auto& string_test: base64_string_tests) { - assert(Crypto::Base64::encode(string_test.first)==string_test.second); - assert(Crypto::Base64::decode(string_test.second)==string_test.first); - } - - for(auto& string_test: md5_string_tests) { - assert(Crypto::to_hex_string(Crypto::md5(string_test.first)) == string_test.second); - stringstream ss(string_test.first); - assert(Crypto::to_hex_string(Crypto::md5(ss)) == string_test.second); - } - - for(auto& string_test: sha1_string_tests) { - assert(Crypto::to_hex_string(Crypto::sha1(string_test.first)) == string_test.second); - stringstream ss(string_test.first); - assert(Crypto::to_hex_string(Crypto::sha1(ss)) == string_test.second); - } - - for(auto& string_test: sha256_string_tests) { - assert(Crypto::to_hex_string(Crypto::sha256(string_test.first)) == string_test.second); - stringstream ss(string_test.first); - assert(Crypto::to_hex_string(Crypto::sha256(ss)) == string_test.second); - } - - for(auto& string_test: sha512_string_tests) { - assert(Crypto::to_hex_string(Crypto::sha512(string_test.first)) == string_test.second); - stringstream ss(string_test.first); - assert(Crypto::to_hex_string(Crypto::sha512(ss)) == string_test.second); - } - - //Testing iterations - assert(Crypto::to_hex_string(Crypto::sha1("Test", 1)) == "640ab2bae07bedc4c163f679a746f7ab7fb5d1fa"); - assert(Crypto::to_hex_string(Crypto::sha1("Test", 2)) == "af31c6cbdecd88726d0a9b3798c71ef41f1624d5"); - stringstream ss("Test"); - assert(Crypto::to_hex_string(Crypto::sha1(ss, 2)) == "af31c6cbdecd88726d0a9b3798c71ef41f1624d5"); - - assert(Crypto::to_hex_string(Crypto::pbkdf2("Password", "Salt", 4096, 128 / 8)) == "f66df50f8aaa11e4d9721e1312ff2e66"); - assert(Crypto::to_hex_string(Crypto::pbkdf2("Password", "Salt", 8192, 512 / 8)) == "a941ccbc34d1ee8ebbd1d34824a419c3dc4eac9cbc7c36ae6c7ca8725e2b618a6ad22241e787af937b0960cf85aa8ea3a258f243e05d3cc9b08af5dd93be046c"); + for(auto &string_test : base64_string_tests) { + assert(Crypto::Base64::encode(string_test.first) == string_test.second); + assert(Crypto::Base64::decode(string_test.second) == string_test.first); + } + + for(auto &string_test : md5_string_tests) { + assert(Crypto::to_hex_string(Crypto::md5(string_test.first)) == string_test.second); + stringstream ss(string_test.first); + assert(Crypto::to_hex_string(Crypto::md5(ss)) == string_test.second); + } + + for(auto &string_test : sha1_string_tests) { + assert(Crypto::to_hex_string(Crypto::sha1(string_test.first)) == string_test.second); + stringstream ss(string_test.first); + assert(Crypto::to_hex_string(Crypto::sha1(ss)) == string_test.second); + } + + for(auto &string_test : sha256_string_tests) { + assert(Crypto::to_hex_string(Crypto::sha256(string_test.first)) == string_test.second); + stringstream ss(string_test.first); + assert(Crypto::to_hex_string(Crypto::sha256(ss)) == string_test.second); + } + + for(auto &string_test : sha512_string_tests) { + assert(Crypto::to_hex_string(Crypto::sha512(string_test.first)) == string_test.second); + stringstream ss(string_test.first); + assert(Crypto::to_hex_string(Crypto::sha512(ss)) == string_test.second); + } + + //Testing iterations + assert(Crypto::to_hex_string(Crypto::sha1("Test", 1)) == "640ab2bae07bedc4c163f679a746f7ab7fb5d1fa"); + assert(Crypto::to_hex_string(Crypto::sha1("Test", 2)) == "af31c6cbdecd88726d0a9b3798c71ef41f1624d5"); + stringstream ss("Test"); + assert(Crypto::to_hex_string(Crypto::sha1(ss, 2)) == "af31c6cbdecd88726d0a9b3798c71ef41f1624d5"); + + assert(Crypto::to_hex_string(Crypto::pbkdf2("Password", "Salt", 4096, 128 / 8)) == "f66df50f8aaa11e4d9721e1312ff2e66"); + assert(Crypto::to_hex_string(Crypto::pbkdf2("Password", "Salt", 8192, 512 / 8)) == "a941ccbc34d1ee8ebbd1d34824a419c3dc4eac9cbc7c36ae6c7ca8725e2b618a6ad22241e787af937b0960cf85aa8ea3a258f243e05d3cc9b08af5dd93be046c"); } diff --git a/tests/io_test.cpp b/tests/io_test.cpp index 39cbec3..d0d75ef 100644 --- a/tests/io_test.cpp +++ b/tests/io_test.cpp @@ -1,5 +1,5 @@ -#include "server_http.hpp" #include "client_http.hpp" +#include "server_http.hpp" #include @@ -9,235 +9,239 @@ typedef SimpleWeb::Server HttpServer; typedef SimpleWeb::Client HttpClient; int main() { - HttpServer server; - server.config.port=8080; - - server.resource["^/string$"]["POST"]=[](shared_ptr response, shared_ptr request) { - auto content=request->content.string(); - - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; - }; - - server.resource["^/string2$"]["POST"]=[](shared_ptr response, shared_ptr request) { - response->write(request->content.string()); - }; - - server.resource["^/string3$"]["POST"]=[](shared_ptr response, shared_ptr request) { - std::stringstream stream; - stream << request->content.rdbuf(); - response->write(stream); - }; - - server.resource["^/string4$"]["POST"]=[](shared_ptr response, shared_ptr /*request*/) { - response->write(SimpleWeb::StatusCode::client_error_forbidden, {{"Test1", "test2"}, {"tesT3", "test4"}}); - }; - - server.resource["^/info$"]["GET"]=[](shared_ptr response, shared_ptr request) { - stringstream content_stream; - content_stream << request->method << " " << request->path << " " << request->http_version << " "; - content_stream << request->header.find("test parameter")->second; + HttpServer server; + server.config.port = 8080; - content_stream.seekp(0, ios::end); - - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content_stream.tellp() << "\r\n\r\n" << content_stream.rdbuf(); - }; - - server.resource["^/match/([0-9]+)$"]["GET"]=[&server](shared_ptr response, shared_ptr request) { - string number=request->path_match[1]; - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" << number; - }; - - server.resource["^/header$"]["GET"]=[](shared_ptr response, shared_ptr request) { - auto content=request->header.find("test1")->second+request->header.find("test2")->second; - - *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" << content; - }; - - thread server_thread([&server](){ - //Start server - server.start(); + server.resource["^/string$"]["POST"] = [](shared_ptr response, shared_ptr request) { + auto content = request->content.string(); + + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" + << content; + }; + + server.resource["^/string2$"]["POST"] = [](shared_ptr response, shared_ptr request) { + response->write(request->content.string()); + }; + + server.resource["^/string3$"]["POST"] = [](shared_ptr response, shared_ptr request) { + std::stringstream stream; + stream << request->content.rdbuf(); + response->write(stream); + }; + + server.resource["^/string4$"]["POST"] = [](shared_ptr response, shared_ptr /*request*/) { + response->write(SimpleWeb::StatusCode::client_error_forbidden, {{"Test1", "test2"}, {"tesT3", "test4"}}); + }; + + server.resource["^/info$"]["GET"] = [](shared_ptr response, shared_ptr request) { + stringstream content_stream; + content_stream << request->method << " " << request->path << " " << request->http_version << " "; + content_stream << request->header.find("test parameter")->second; + + content_stream.seekp(0, ios::end); + + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content_stream.tellp() << "\r\n\r\n" + << content_stream.rdbuf(); + }; + + server.resource["^/match/([0-9]+)$"]["GET"] = [&server](shared_ptr response, shared_ptr request) { + string number = request->path_match[1]; + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << number.length() << "\r\n\r\n" + << number; + }; + + server.resource["^/header$"]["GET"] = [](shared_ptr response, shared_ptr request) { + auto content = request->header.find("test1")->second + request->header.find("test2")->second; + + *response << "HTTP/1.1 200 OK\r\nContent-Length: " << content.length() << "\r\n\r\n" + << content; + }; + + thread server_thread([&server]() { + //Start server + server.start(); + }); + + this_thread::sleep_for(chrono::seconds(1)); + { + HttpClient client("localhost:8080"); + + { + stringstream output; + auto r = client.request("POST", "/string", "A string"); + assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); + output << r->content.rdbuf(); + assert(output.str() == "A string"); + } + + { + stringstream output; + auto r = client.request("POST", "/string", "A string"); + assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); + assert(r->content.string() == "A string"); + } + + { + stringstream output; + auto r = client.request("POST", "/string2", "A string"); + assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); + output << r->content.rdbuf(); + assert(output.str() == "A string"); + } + + { + stringstream output; + auto r = client.request("POST", "/string3", "A string"); + assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); + output << r->content.rdbuf(); + assert(output.str() == "A string"); + } + + { + stringstream output; + auto r = client.request("POST", "/string4", "A string"); + assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::client_error_forbidden); + assert(r->header.size() == 3); + assert(r->header.find("test1")->second == "test2"); + assert(r->header.find("tEst3")->second == "test4"); + assert(r->header.find("content-length")->second == "0"); + output << r->content.rdbuf(); + assert(output.str() == ""); + } + + { + stringstream output; + stringstream content("A string"); + auto r = client.request("POST", "/string", content); + output << r->content.rdbuf(); + assert(output.str() == "A string"); + } + + { + stringstream output; + auto r = client.request("GET", "/info", "", {{"Test Parameter", "test value"}}); + output << r->content.rdbuf(); + assert(output.str() == "GET /info 1.1 test value"); + } + + { + stringstream output; + auto r = client.request("GET", "/match/123"); + output << r->content.rdbuf(); + assert(output.str() == "123"); + } + } + { + HttpClient client("localhost:8080"); + + HttpClient::Connection *connection; + { + // test performing the stream version of the request methods first + stringstream output; + stringstream content("A string"); + auto r = client.request("POST", "/string", content); + output << r->content.rdbuf(); + assert(output.str() == "A string"); + assert(client.connections->size() == 1); + connection = client.connections->front().get(); + } + + { + stringstream output; + auto r = client.request("POST", "/string", "A string"); + output << r->content.rdbuf(); + assert(output.str() == "A string"); + assert(client.connections->size() == 1); + assert(connection == client.connections->front().get()); + } + + { + stringstream output; + auto r = client.request("GET", "/header", "", {{"test1", "test"}, {"test2", "ing"}}); + output << r->content.rdbuf(); + assert(output.str() == "testing"); + assert(client.connections->size() == 1); + assert(connection == client.connections->front().get()); + } + } + + { + HttpClient client("localhost:8080"); + bool call = false; + client.request("GET", "/match/123", [&call](shared_ptr response, const SimpleWeb::error_code &ec) { + assert(!ec); + stringstream output; + output << response->content.rdbuf(); + assert(output.str() == "123"); + call = true; }); - - this_thread::sleep_for(chrono::seconds(1)); + client.io_service->run(); + assert(call); + { - HttpClient client("localhost:8080"); - - { - stringstream output; - auto r=client.request("POST", "/string", "A string"); - assert(SimpleWeb::status_code(r->status_code)==SimpleWeb::StatusCode::success_ok); - output << r->content.rdbuf(); - assert(output.str()=="A string"); - } - - { - stringstream output; - auto r=client.request("POST", "/string", "A string"); - assert(SimpleWeb::status_code(r->status_code)==SimpleWeb::StatusCode::success_ok); - assert(r->content.string()=="A string"); - } - - { - stringstream output; - auto r=client.request("POST", "/string2", "A string"); - assert(SimpleWeb::status_code(r->status_code)==SimpleWeb::StatusCode::success_ok); - output << r->content.rdbuf(); - assert(output.str()=="A string"); - } - - { - stringstream output; - auto r=client.request("POST", "/string3", "A string"); - assert(SimpleWeb::status_code(r->status_code)==SimpleWeb::StatusCode::success_ok); - output << r->content.rdbuf(); - assert(output.str()=="A string"); - } - - { - stringstream output; - auto r=client.request("POST", "/string4", "A string"); - assert(SimpleWeb::status_code(r->status_code)==SimpleWeb::StatusCode::client_error_forbidden); - assert(r->header.size()==3); - assert(r->header.find("test1")->second=="test2"); - assert(r->header.find("tEst3")->second=="test4"); - assert(r->header.find("content-length")->second=="0"); - output << r->content.rdbuf(); - assert(output.str()==""); - } - - { - stringstream output; - stringstream content("A string"); - auto r=client.request("POST", "/string", content); - output << r->content.rdbuf(); - assert(output.str()=="A string"); - } - - { - stringstream output; - auto r=client.request("GET", "/info", "", {{"Test Parameter", "test value"}}); - output << r->content.rdbuf(); - assert(output.str()=="GET /info 1.1 test value"); - } - - { - stringstream output; - auto r=client.request("GET", "/match/123"); - output << r->content.rdbuf(); - assert(output.str()=="123"); - } - } - { - HttpClient client("localhost:8080"); - - HttpClient::Connection *connection; - { - // test performing the stream version of the request methods first - stringstream output; - stringstream content("A string"); - auto r=client.request("POST", "/string", content); - output << r->content.rdbuf(); - assert(output.str()=="A string"); - assert(client.connections->size()==1); - connection=client.connections->front().get(); - } - - { - stringstream output; - auto r=client.request("POST", "/string", "A string"); - output << r->content.rdbuf(); - assert(output.str()=="A string"); - assert(client.connections->size()==1); - assert(connection==client.connections->front().get()); - } - - { - stringstream output; - auto r=client.request("GET", "/header", "", {{"test1", "test"}, {"test2", "ing"}}); - output << r->content.rdbuf(); - assert(output.str()=="testing"); - assert(client.connections->size()==1); - assert(connection==client.connections->front().get()); - } - } - - { - HttpClient client("localhost:8080"); - bool call=false; - client.request("GET", "/match/123", [&call](shared_ptr response, const SimpleWeb::error_code &ec) { + vector calls(100); + vector threads; + for(size_t c = 0; c < 100; ++c) { + calls[c] = 0; + threads.emplace_back([c, &client, &calls] { + client.request("GET", "/match/123", [c, &calls](shared_ptr response, const SimpleWeb::error_code &ec) { assert(!ec); stringstream output; output << response->content.rdbuf(); - assert(output.str()=="123"); - call=true; + assert(output.str() == "123"); + calls[c] = 1; + }); }); - client.io_service->run(); + } + for(auto &thread : threads) + thread.join(); + assert(client.connections->size() == 100); + client.io_service->reset(); + client.io_service->run(); + assert(client.connections->size() == 1); + for(auto call : calls) assert(call); - - { - vector calls(100); - vector threads; - for(size_t c=0;c<100;++c) { - calls[c]=0; - threads.emplace_back([c, &client, &calls] { - client.request("GET", "/match/123", [c, &calls](shared_ptr response, const SimpleWeb::error_code &ec) { - assert(!ec); - stringstream output; - output << response->content.rdbuf(); - assert(output.str()=="123"); - calls[c]=1; - }); - }); - } - for(auto &thread: threads) - thread.join(); - assert(client.connections->size()==100); - client.io_service->reset(); - client.io_service->run(); - assert(client.connections->size()==1); - for(auto call: calls) - assert(call); - } } - + } + + { + HttpClient client("localhost:8080"); + assert(client.connections->size() == 0); + for(size_t c = 0; c < 5000; ++c) { + auto r1 = client.request("POST", "/string", "A string"); + assert(SimpleWeb::status_code(r1->status_code) == SimpleWeb::StatusCode::success_ok); + assert(r1->content.string() == "A string"); + assert(client.connections->size() == 1); + + stringstream content("A string"); + auto r2 = client.request("POST", "/string", content); + assert(SimpleWeb::status_code(r2->status_code) == SimpleWeb::StatusCode::success_ok); + assert(r2->content.string() == "A string"); + assert(client.connections->size() == 1); + } + } + + for(size_t c = 0; c < 500; ++c) { { - HttpClient client("localhost:8080"); - assert(client.connections->size()==0); - for(size_t c=0;c<5000;++c) { - auto r1=client.request("POST", "/string", "A string"); - assert(SimpleWeb::status_code(r1->status_code)==SimpleWeb::StatusCode::success_ok); - assert(r1->content.string()=="A string"); - assert(client.connections->size()==1); - - stringstream content("A string"); - auto r2 = client.request("POST", "/string", content); - assert(SimpleWeb::status_code(r2->status_code) == SimpleWeb::StatusCode::success_ok); - assert(r2->content.string() == "A string"); - assert(client.connections->size() == 1); - } + HttpClient client("localhost:8080"); + auto r = client.request("POST", "/string", "A string"); + assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); + assert(r->content.string() == "A string"); + assert(client.connections->size() == 1); } - - for(size_t c=0;c<500;++c) { - { - HttpClient client("localhost:8080"); - auto r=client.request("POST", "/string", "A string"); - assert(SimpleWeb::status_code(r->status_code)==SimpleWeb::StatusCode::success_ok); - assert(r->content.string()=="A string"); - assert(client.connections->size()==1); - } - - { - HttpClient client("localhost:8080"); - stringstream content("A string"); - auto r = client.request("POST", "/string", content); - assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); - assert(r->content.string() == "A string"); - assert(client.connections->size() == 1); - } + + { + HttpClient client("localhost:8080"); + stringstream content("A string"); + auto r = client.request("POST", "/string", content); + assert(SimpleWeb::status_code(r->status_code) == SimpleWeb::StatusCode::success_ok); + assert(r->content.string() == "A string"); + assert(client.connections->size() == 1); } - - server.stop(); - server_thread.join(); - - return 0; + } + + server.stop(); + server_thread.join(); + + return 0; } diff --git a/tests/parse_test.cpp b/tests/parse_test.cpp index a5bbda7..cfb38bc 100644 --- a/tests/parse_test.cpp +++ b/tests/parse_test.cpp @@ -1,198 +1,198 @@ -#include "server_http.hpp" #include "client_http.hpp" -#include +#include "server_http.hpp" #include +#include using namespace std; using namespace SimpleWeb; class ServerTest : public ServerBase { public: - ServerTest() : ServerBase::ServerBase(8080) {} - - void accept() {} - - void parse_request_test() { - HTTP socket(*io_service); - std::shared_ptr request(new Request(socket)); - - std::ostream stream(&request->content.streambuf); - stream << "GET /test/ HTTP/1.1\r\n"; - stream << "TestHeader: test\r\n"; - stream << "TestHeader2:test2\r\n"; - stream << "TestHeader3:test3a\r\n"; - stream << "TestHeader3:test3b\r\n"; - stream << "\r\n"; - - assert(parse_request(request)); - - assert(request->method=="GET"); - assert(request->path=="/test/"); - assert(request->http_version=="1.1"); - - assert(request->header.size()==4); - auto header_it=request->header.find("TestHeader"); - assert(header_it!=request->header.end() && header_it->second=="test"); - header_it=request->header.find("TestHeader2"); - assert(header_it!=request->header.end() && header_it->second=="test2"); - - header_it=request->header.find("testheader"); - assert(header_it!=request->header.end() && header_it->second=="test"); - header_it=request->header.find("testheader2"); - assert(header_it!=request->header.end() && header_it->second=="test2"); - - auto range=request->header.equal_range("testheader3"); - auto first=range.first; - auto second=first; - ++second; - assert(range.first!=request->header.end() && range.second!=request->header.end() && - ((first->second=="test3a" && second->second=="test3b") || - (first->second=="test3b" && second->second=="test3a"))); - } + ServerTest() : ServerBase::ServerBase(8080) {} + + void accept() {} + + void parse_request_test() { + HTTP socket(*io_service); + std::shared_ptr request(new Request(socket)); + + std::ostream stream(&request->content.streambuf); + stream << "GET /test/ HTTP/1.1\r\n"; + stream << "TestHeader: test\r\n"; + stream << "TestHeader2:test2\r\n"; + stream << "TestHeader3:test3a\r\n"; + stream << "TestHeader3:test3b\r\n"; + stream << "\r\n"; + + assert(parse_request(request)); + + assert(request->method == "GET"); + assert(request->path == "/test/"); + assert(request->http_version == "1.1"); + + assert(request->header.size() == 4); + auto header_it = request->header.find("TestHeader"); + assert(header_it != request->header.end() && header_it->second == "test"); + header_it = request->header.find("TestHeader2"); + assert(header_it != request->header.end() && header_it->second == "test2"); + + header_it = request->header.find("testheader"); + assert(header_it != request->header.end() && header_it->second == "test"); + header_it = request->header.find("testheader2"); + assert(header_it != request->header.end() && header_it->second == "test2"); + + auto range = request->header.equal_range("testheader3"); + auto first = range.first; + auto second = first; + ++second; + assert(range.first != request->header.end() && range.second != request->header.end() && + ((first->second == "test3a" && second->second == "test3b") || + (first->second == "test3b" && second->second == "test3a"))); + } }; class ClientTest : public ClientBase { public: - ClientTest(const std::string& server_port_path) : ClientBase::ClientBase(server_port_path, 80) {} - - std::shared_ptr create_connection() override { - return nullptr; - } - - void constructor_parse_test1() { - assert(host=="test.org"); - assert(port==8080); - } - - void constructor_parse_test2() { - assert(host=="test.org"); - assert(port==80); - } - - void parse_response_header_test() { - std::shared_ptr response(new Response()); - - ostream stream(&response->content_buffer); - stream << "HTTP/1.1 200 OK\r\n"; - stream << "TestHeader: test\r\n"; - stream << "TestHeader2:test2\r\n"; - stream << "TestHeader3:test3a\r\n"; - stream << "TestHeader3:test3b\r\n"; - stream << "\r\n"; - - parse_response_header(response); - - assert(response->http_version=="1.1"); - assert(response->status_code=="200 OK"); - - assert(response->header.size()==4); - auto header_it=response->header.find("TestHeader"); - assert(header_it!=response->header.end() && header_it->second=="test"); - header_it=response->header.find("TestHeader2"); - assert(header_it!=response->header.end() && header_it->second=="test2"); - - header_it=response->header.find("testheader"); - assert(header_it!=response->header.end() && header_it->second=="test"); - header_it=response->header.find("testheader2"); - assert(header_it!=response->header.end() && header_it->second=="test2"); - - auto range=response->header.equal_range("testheader3"); - auto first=range.first; - auto second=first; - ++second; - assert(range.first!=response->header.end() && range.second!=response->header.end() && - ((first->second=="test3a" && second->second=="test3b") || - (first->second=="test3b" && second->second=="test3a"))); - } + ClientTest(const std::string &server_port_path) : ClientBase::ClientBase(server_port_path, 80) {} + + std::shared_ptr create_connection() override { + return nullptr; + } + + void constructor_parse_test1() { + assert(host == "test.org"); + assert(port == 8080); + } + + void constructor_parse_test2() { + assert(host == "test.org"); + assert(port == 80); + } + + void parse_response_header_test() { + std::shared_ptr response(new Response()); + + ostream stream(&response->content_buffer); + stream << "HTTP/1.1 200 OK\r\n"; + stream << "TestHeader: test\r\n"; + stream << "TestHeader2:test2\r\n"; + stream << "TestHeader3:test3a\r\n"; + stream << "TestHeader3:test3b\r\n"; + stream << "\r\n"; + + parse_response_header(response); + + assert(response->http_version == "1.1"); + assert(response->status_code == "200 OK"); + + assert(response->header.size() == 4); + auto header_it = response->header.find("TestHeader"); + assert(header_it != response->header.end() && header_it->second == "test"); + header_it = response->header.find("TestHeader2"); + assert(header_it != response->header.end() && header_it->second == "test2"); + + header_it = response->header.find("testheader"); + assert(header_it != response->header.end() && header_it->second == "test"); + header_it = response->header.find("testheader2"); + assert(header_it != response->header.end() && header_it->second == "test2"); + + auto range = response->header.equal_range("testheader3"); + auto first = range.first; + auto second = first; + ++second; + assert(range.first != response->header.end() && range.second != response->header.end() && + ((first->second == "test3a" && second->second == "test3b") || + (first->second == "test3b" && second->second == "test3a"))); + } }; int main() { - assert(case_insensitive_equal("Test", "tesT")); - assert(case_insensitive_equal("tesT", "test")); - assert(!case_insensitive_equal("test", "tseT")); - CaseInsensitiveEqual equal; - assert(equal("Test", "tesT")); - assert(equal("tesT", "test")); - assert(!equal("test", "tset")); - CaseInsensitiveHash hash; - assert(hash("Test")==hash("tesT")); - assert(hash("tesT")==hash("test")); - assert(hash("test")!=hash("tset")); - - auto percent_decoded="testing æøå !#$&'()*+,/:;=?@[]"; - auto percent_encoded="testing+æøå+%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"; - assert(Percent::encode(percent_decoded)==percent_encoded); - assert(Percent::decode(percent_encoded)==percent_decoded); - assert(Percent::decode(Percent::encode(percent_decoded))==percent_decoded); - - SimpleWeb::CaseInsensitiveMultimap fields={{"test1", "æøå"}, {"test2", "!#$&'()*+,/:;=?@[]"}}; - auto query_string1="test1=æøå&test2=%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"; - auto query_string2="test2=%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D&test1=æøå"; - auto query_string_result=QueryString::create(fields); - assert(query_string_result==query_string1 || query_string_result==query_string2); - auto fields_result1=QueryString::parse(query_string1); - auto fields_result2=QueryString::parse(query_string2); - assert(fields_result1==fields_result2 && fields_result1==fields); - - ServerTest serverTest; - serverTest.io_service=std::make_shared(); - - serverTest.parse_request_test(); - - ClientTest clientTest("test.org:8080"); - clientTest.constructor_parse_test1(); - - ClientTest clientTest2("test.org"); - clientTest2.constructor_parse_test2(); - - clientTest2.parse_response_header_test(); + assert(case_insensitive_equal("Test", "tesT")); + assert(case_insensitive_equal("tesT", "test")); + assert(!case_insensitive_equal("test", "tseT")); + CaseInsensitiveEqual equal; + assert(equal("Test", "tesT")); + assert(equal("tesT", "test")); + assert(!equal("test", "tset")); + CaseInsensitiveHash hash; + assert(hash("Test") == hash("tesT")); + assert(hash("tesT") == hash("test")); + assert(hash("test") != hash("tset")); + + auto percent_decoded = "testing æøå !#$&'()*+,/:;=?@[]"; + auto percent_encoded = "testing+æøå+%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"; + assert(Percent::encode(percent_decoded) == percent_encoded); + assert(Percent::decode(percent_encoded) == percent_decoded); + assert(Percent::decode(Percent::encode(percent_decoded)) == percent_decoded); + + SimpleWeb::CaseInsensitiveMultimap fields = {{"test1", "æøå"}, {"test2", "!#$&'()*+,/:;=?@[]"}}; + auto query_string1 = "test1=æøå&test2=%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D"; + auto query_string2 = "test2=%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D&test1=æøå"; + auto query_string_result = QueryString::create(fields); + assert(query_string_result == query_string1 || query_string_result == query_string2); + auto fields_result1 = QueryString::parse(query_string1); + auto fields_result2 = QueryString::parse(query_string2); + assert(fields_result1 == fields_result2 && fields_result1 == fields); + + ServerTest serverTest; + serverTest.io_service = std::make_shared(); + + serverTest.parse_request_test(); + + ClientTest clientTest("test.org:8080"); + clientTest.constructor_parse_test1(); + + ClientTest clientTest2("test.org"); + clientTest2.constructor_parse_test2(); + + clientTest2.parse_response_header_test(); - asio::io_service io_service; - asio::ip::tcp::socket socket(io_service); - SimpleWeb::Server::Request request(socket); + asio::io_service io_service; + asio::ip::tcp::socket socket(io_service); + SimpleWeb::Server::Request request(socket); + { + request.path = "/?"; + auto queries = request.parse_query_string(); + assert(queries.empty()); + } + { + request.path = "/"; + auto queries = request.parse_query_string(); + assert(queries.empty()); + } + { + request.path = "/?="; + auto queries = request.parse_query_string(); + assert(queries.empty()); + } + { + request.path = "/?=test"; + auto queries = request.parse_query_string(); + assert(queries.empty()); + } + { + request.path = "/?a=1%202%20%203&b=3+4&c&d=æ%25ø%26å%3F"; + auto queries = request.parse_query_string(); { - request.path = "/?"; - auto queries = request.parse_query_string(); - assert(queries.empty()); + auto range = queries.equal_range("a"); + assert(range.first != range.second); + assert(range.first->second == "1 2 3"); } { - request.path = "/"; - auto queries = request.parse_query_string(); - assert(queries.empty()); + auto range = queries.equal_range("b"); + assert(range.first != range.second); + assert(range.first->second == "3 4"); } { - request.path = "/?="; - auto queries = request.parse_query_string(); - assert(queries.empty()); + auto range = queries.equal_range("c"); + assert(range.first != range.second); + assert(range.first->second == ""); } { - request.path = "/?=test"; - auto queries = request.parse_query_string(); - assert(queries.empty()); - } - { - request.path = "/?a=1%202%20%203&b=3+4&c&d=æ%25ø%26å%3F"; - auto queries = request.parse_query_string(); - { - auto range = queries.equal_range("a"); - assert(range.first != range.second); - assert(range.first->second == "1 2 3"); - } - { - auto range = queries.equal_range("b"); - assert(range.first != range.second); - assert(range.first->second == "3 4"); - } - { - auto range = queries.equal_range("c"); - assert(range.first != range.second); - assert(range.first->second == ""); - } - { - auto range = queries.equal_range("d"); - assert(range.first != range.second); - assert(range.first->second == "æ%ø&å?"); - } + auto range = queries.equal_range("d"); + assert(range.first != range.second); + assert(range.first->second == "æ%ø&å?"); } + } } diff --git a/utility.hpp b/utility.hpp index d429792..bd5c63b 100644 --- a/utility.hpp +++ b/utility.hpp @@ -20,136 +20,136 @@ namespace SimpleWeb { #ifndef CASE_INSENSITIVE_EQUAL_AND_HASH #define CASE_INSENSITIVE_EQUAL_AND_HASH -inline bool case_insensitive_equal(const std::string &str1, const std::string &str2) { - return str1.size() == str2.size() && - std::equal(str1.begin(), str1.end(), str2.begin(), [](char a, char b) { - return tolower(a) == tolower(b); - }); -} -class CaseInsensitiveEqual { -public: - bool operator()(const std::string &str1, const std::string &str2) const { - return case_insensitive_equal(str1, str2); + inline bool case_insensitive_equal(const std::string &str1, const std::string &str2) { + return str1.size() == str2.size() && + std::equal(str1.begin(), str1.end(), str2.begin(), [](char a, char b) { + return tolower(a) == tolower(b); + }); } -}; -// Based on https://stackoverflow.com/questions/2590677/how-do-i-combine-hash-values-in-c0x/2595226#2595226 -class CaseInsensitiveHash { -public: - size_t operator()(const std::string &str) const { - size_t h = 0; - std::hash hash; - for(auto c : str) - h ^= hash(tolower(c)) + 0x9e3779b9 + (h << 6) + (h >> 2); - return h; - } -}; + class CaseInsensitiveEqual { + public: + bool operator()(const std::string &str1, const std::string &str2) const { + return case_insensitive_equal(str1, str2); + } + }; + // Based on https://stackoverflow.com/questions/2590677/how-do-i-combine-hash-values-in-c0x/2595226#2595226 + class CaseInsensitiveHash { + public: + size_t operator()(const std::string &str) const { + size_t h = 0; + std::hash hash; + for(auto c : str) + h ^= hash(tolower(c)) + 0x9e3779b9 + (h << 6) + (h >> 2); + return h; + } + }; #endif -typedef std::unordered_multimap CaseInsensitiveMultimap; + typedef std::unordered_multimap CaseInsensitiveMultimap; -/// Percent encoding and decoding -class Percent { -public: - /// Returns percent-encoded string - static std::string encode(const std::string &value) { - static auto hex_chars = "0123456789ABCDEF"; + /// Percent encoding and decoding + class Percent { + public: + /// Returns percent-encoded string + static std::string encode(const std::string &value) { + static auto hex_chars = "0123456789ABCDEF"; - std::string result; - result.reserve(value.size()); // minimum size of result + std::string result; + result.reserve(value.size()); // minimum size of result - for(auto &chr : value) { - if(chr == ' ') - result += '+'; - else if(chr == '!' || chr == '#' || chr == '$' || (chr >= '&' && chr <= ',') || (chr >= '/' && chr <= ';') || chr == '=' || chr == '?' || chr == '@' || chr == '[' || chr == ']') - result += std::string("%") + hex_chars[chr >> 4] + hex_chars[chr & 15]; - else - result += chr; - } - - return result; - } - - /// Returns percent-decoded string - static std::string decode(const std::string &value) { - std::string result; - result.reserve(value.size() / 3 + (value.size() % 3)); // minimum size of result - - for(size_t i = 0; i < value.size(); ++i) { - auto &chr = value[i]; - if(chr == '%' && i + 2 < value.size()) { - auto hex = value.substr(i + 1, 2); - auto decoded_chr = static_cast(std::strtol(hex.c_str(), nullptr, 16)); - result += decoded_chr; - i += 2; + for(auto &chr : value) { + if(chr == ' ') + result += '+'; + else if(chr == '!' || chr == '#' || chr == '$' || (chr >= '&' && chr <= ',') || (chr >= '/' && chr <= ';') || chr == '=' || chr == '?' || chr == '@' || chr == '[' || chr == ']') + result += std::string("%") + hex_chars[chr >> 4] + hex_chars[chr & 15]; + else + result += chr; } - else if(chr == '+') - result += ' '; - else - result += chr; - } - return result; - } -}; - -/// Query string creation and parsing -class QueryString { -public: - /// Returns query string created from given field names and values - static std::string create(const CaseInsensitiveMultimap &fields) { - std::string result; - - bool first = true; - for(auto &field : fields) { - result += (!first ? "&" : "") + field.first + '=' + Percent::encode(field.second); - first = false; - } - - return result; - } - - /// Returns query keys with percent-decoded values. - static CaseInsensitiveMultimap parse(const std::string &query_string) { - CaseInsensitiveMultimap result; - - if(query_string.empty()) return result; + } - size_t name_pos = 0; - size_t name_end_pos = -1; - size_t value_pos = -1; - for(size_t c = 0; c < query_string.size(); ++c) { - if(query_string[c] == '&') { - auto name = query_string.substr(name_pos, (name_end_pos == std::string::npos ? c : name_end_pos) - name_pos); + /// Returns percent-decoded string + static std::string decode(const std::string &value) { + std::string result; + result.reserve(value.size() / 3 + (value.size() % 3)); // minimum size of result + + for(size_t i = 0; i < value.size(); ++i) { + auto &chr = value[i]; + if(chr == '%' && i + 2 < value.size()) { + auto hex = value.substr(i + 1, 2); + auto decoded_chr = static_cast(std::strtol(hex.c_str(), nullptr, 16)); + result += decoded_chr; + i += 2; + } + else if(chr == '+') + result += ' '; + else + result += chr; + } + + return result; + } + }; + + /// Query string creation and parsing + class QueryString { + public: + /// Returns query string created from given field names and values + static std::string create(const CaseInsensitiveMultimap &fields) { + std::string result; + + bool first = true; + for(auto &field : fields) { + result += (!first ? "&" : "") + field.first + '=' + Percent::encode(field.second); + first = false; + } + + return result; + } + + /// Returns query keys with percent-decoded values. + static CaseInsensitiveMultimap parse(const std::string &query_string) { + CaseInsensitiveMultimap result; + + if(query_string.empty()) + return result; + + size_t name_pos = 0; + size_t name_end_pos = -1; + size_t value_pos = -1; + for(size_t c = 0; c < query_string.size(); ++c) { + if(query_string[c] == '&') { + auto name = query_string.substr(name_pos, (name_end_pos == std::string::npos ? c : name_end_pos) - name_pos); + if(!name.empty()) { + auto value = value_pos == std::string::npos ? std::string() : query_string.substr(value_pos, c - value_pos); + result.emplace(std::move(name), Percent::decode(value)); + } + name_pos = c + 1; + name_end_pos = -1; + value_pos = -1; + } + else if(query_string[c] == '=') { + name_end_pos = c; + value_pos = c + 1; + } + } + if(name_pos < query_string.size()) { + auto name = query_string.substr(name_pos, name_end_pos - name_pos); if(!name.empty()) { - auto value = value_pos == std::string::npos ? std::string() : query_string.substr(value_pos, c - value_pos); + auto value = value_pos >= query_string.size() ? std::string() : query_string.substr(value_pos); result.emplace(std::move(name), Percent::decode(value)); } - name_pos = c + 1; - name_end_pos = -1; - value_pos = -1; } - else if(query_string[c] == '=') { - name_end_pos = c; - value_pos = c + 1; - } - } - if(name_pos < query_string.size()) { - auto name = query_string.substr(name_pos, name_end_pos - name_pos); - if(!name.empty()) { - auto value = value_pos >= query_string.size() ? std::string() : query_string.substr(value_pos); - result.emplace(std::move(name), Percent::decode(value)); - } - } - return result; + return result; + } + }; + + /// Returns query keys with percent-decoded values. + DEPRECATED inline CaseInsensitiveMultimap parse_query_string(const std::string &query_string) { + return QueryString::parse(query_string); } -}; - -/// Returns query keys with percent-decoded values. -DEPRECATED inline CaseInsensitiveMultimap parse_query_string(const std::string &query_string) { - return QueryString::parse(query_string); -} } // namespace SimpleWeb #endif // SIMPLE_WEB_SERVER_UTILITY_HPP